Browse Source

Merge branch 'Rapptz:master' into master

pull/10211/head
blord0 2 weeks ago
committed by GitHub
parent
commit
85f9830385
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      discord/abc.py
  2. 11
      discord/audit_logs.py
  3. 1
      discord/enums.py
  4. 57
      discord/flags.py
  5. 16
      discord/gateway.py
  6. 9
      discord/http.py
  7. 46
      discord/interactions.py
  8. 11
      discord/invite.py
  9. 3
      discord/member.py
  10. 2
      discord/types/invite.py
  11. 2
      discord/types/member.py
  12. 40
      discord/voice_state.py
  13. 16
      docs/api.rst

12
discord/abc.py

@ -60,6 +60,7 @@ from .http import handle_message_parameters
from .voice_client import VoiceClient, VoiceProtocol from .voice_client import VoiceClient, VoiceProtocol
from .sticker import GuildSticker, StickerItem from .sticker import GuildSticker, StickerItem
from . import utils from . import utils
from .flags import InviteFlags
__all__ = ( __all__ = (
'Snowflake', 'Snowflake',
@ -1257,6 +1258,7 @@ class GuildChannel:
target_type: Optional[InviteTarget] = None, target_type: Optional[InviteTarget] = None,
target_user: Optional[User] = None, target_user: Optional[User] = None,
target_application_id: Optional[int] = None, target_application_id: Optional[int] = None,
guest: bool = False,
) -> Invite: ) -> Invite:
"""|coro| """|coro|
@ -1295,6 +1297,10 @@ class GuildChannel:
The id of the embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`. The id of the embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`.
.. versionadded:: 2.0 .. versionadded:: 2.0
guest: :class:`bool`
Whether the invite is a guest invite.
.. versionadded:: 2.6
Raises Raises
------- -------
@ -1312,6 +1318,11 @@ class GuildChannel:
if target_type is InviteTarget.unknown: if target_type is InviteTarget.unknown:
raise ValueError('Cannot create invite with an unknown target type') raise ValueError('Cannot create invite with an unknown target type')
flags: Optional[InviteFlags] = None
if guest:
flags = InviteFlags._from_value(0)
flags.guest = True
data = await self._state.http.create_invite( data = await self._state.http.create_invite(
self.id, self.id,
reason=reason, reason=reason,
@ -1322,6 +1333,7 @@ class GuildChannel:
target_type=target_type.value if target_type else None, target_type=target_type.value if target_type else None,
target_user_id=target_user.id if target_user else None, target_user_id=target_user.id if target_user else None,
target_application_id=target_application_id, target_application_id=target_application_id,
flags=flags.value if flags else None,
) )
return Invite.from_incomplete(data=data, state=self._state) return Invite.from_incomplete(data=data, state=self._state)

11
discord/audit_logs.py

@ -145,8 +145,8 @@ def _transform_applied_forum_tags(entry: AuditLogEntry, data: List[Snowflake]) -
return [Object(id=tag_id, type=ForumTag) for tag_id in data] return [Object(id=tag_id, type=ForumTag) for tag_id in data]
def _transform_overloaded_flags(entry: AuditLogEntry, data: int) -> Union[int, flags.ChannelFlags]: def _transform_overloaded_flags(entry: AuditLogEntry, data: int) -> Union[int, flags.ChannelFlags, flags.InviteFlags]:
# The `flags` key is definitely overloaded. Right now it's for channels and threads but # The `flags` key is definitely overloaded. Right now it's for channels, threads and invites but
# I am aware of `member.flags` and `user.flags` existing. However, this does not impact audit logs # I am aware of `member.flags` and `user.flags` existing. However, this does not impact audit logs
# at the moment but better safe than sorry. # at the moment but better safe than sorry.
channel_audit_log_types = ( channel_audit_log_types = (
@ -157,9 +157,16 @@ def _transform_overloaded_flags(entry: AuditLogEntry, data: int) -> Union[int, f
enums.AuditLogAction.thread_update, enums.AuditLogAction.thread_update,
enums.AuditLogAction.thread_delete, enums.AuditLogAction.thread_delete,
) )
invite_audit_log_types = (
enums.AuditLogAction.invite_create,
enums.AuditLogAction.invite_update,
enums.AuditLogAction.invite_delete,
)
if entry.action in channel_audit_log_types: if entry.action in channel_audit_log_types:
return flags.ChannelFlags._from_value(data) return flags.ChannelFlags._from_value(data)
elif entry.action in invite_audit_log_types:
return flags.InviteFlags._from_value(data)
return data return data

1
discord/enums.py

@ -621,6 +621,7 @@ class InteractionResponseType(Enum):
autocomplete_result = 8 autocomplete_result = 8
modal = 9 # for modals modal = 9 # for modals
# premium_required = 10 (deprecated) # premium_required = 10 (deprecated)
launch_activity = 12
class VideoQualityMode(Enum): class VideoQualityMode(Enum):

57
discord/flags.py

@ -64,6 +64,7 @@ __all__ = (
'AppInstallationType', 'AppInstallationType',
'SKUFlags', 'SKUFlags',
'EmbedFlags', 'EmbedFlags',
'InviteFlags',
) )
BF = TypeVar('BF', bound='BaseFlags') BF = TypeVar('BF', bound='BaseFlags')
@ -2397,3 +2398,59 @@ class EmbedFlags(BaseFlags):
longer displayed. longer displayed.
""" """
return 1 << 5 return 1 << 5
class InviteFlags(BaseFlags):
r"""Wraps up the Discord Invite flags
.. versionadded:: 2.6
.. container:: operations
.. describe:: x == y
Checks if two InviteFlags are equal.
.. describe:: x != y
Checks if two InviteFlags are not equal.
.. describe:: x | y, x |= y
Returns a InviteFlags instance with all enabled flags from
both x and y.
.. describe:: x ^ y, x ^= y
Returns a InviteFlags instance with only flags enabled on
only one of x or y, not on both.
.. describe:: ~x
Returns a InviteFlags instance with all flags inverted from x.
.. describe:: hash(x)
Returns the flag's hash.
.. describe:: iter(x)
Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.
.. describe:: bool(b)
Returns whether any flag is set to ``True``.
Attributes
----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""
@flag_value
def guest(self):
""":class:`bool`: Returns ``True`` if this is a guest invite for a voice channel."""
return 1 << 0

16
discord/gateway.py

@ -212,6 +212,9 @@ class KeepAliveHandler(threading.Thread):
class VoiceKeepAliveHandler(KeepAliveHandler): class VoiceKeepAliveHandler(KeepAliveHandler):
if TYPE_CHECKING:
ws: DiscordVoiceWebSocket
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}') name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}')
super().__init__(*args, name=name, **kwargs) super().__init__(*args, name=name, **kwargs)
@ -223,7 +226,10 @@ class VoiceKeepAliveHandler(KeepAliveHandler):
def get_payload(self) -> Dict[str, Any]: def get_payload(self) -> Dict[str, Any]:
return { return {
'op': self.ws.HEARTBEAT, 'op': self.ws.HEARTBEAT,
'd': int(time.time() * 1000), 'd': {
't': int(time.time() * 1000),
'seq_ack': self.ws.seq_ack,
},
} }
def ack(self) -> None: def ack(self) -> None:
@ -830,6 +836,8 @@ class DiscordVoiceWebSocket:
self._keep_alive: Optional[VoiceKeepAliveHandler] = None self._keep_alive: Optional[VoiceKeepAliveHandler] = None
self._close_code: Optional[int] = None self._close_code: Optional[int] = None
self.secret_key: Optional[List[int]] = None self.secret_key: Optional[List[int]] = None
# defaulting to -1
self.seq_ack: int = -1
if hook: if hook:
self._hook = hook # type: ignore self._hook = hook # type: ignore
@ -850,6 +858,7 @@ class DiscordVoiceWebSocket:
'token': state.token, 'token': state.token,
'server_id': str(state.server_id), 'server_id': str(state.server_id),
'session_id': state.session_id, 'session_id': state.session_id,
'seq_ack': self.seq_ack,
}, },
} }
await self.send_as_json(payload) await self.send_as_json(payload)
@ -874,14 +883,16 @@ class DiscordVoiceWebSocket:
*, *,
resume: bool = False, resume: bool = False,
hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None, hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None,
seq_ack: int = -1,
) -> Self: ) -> Self:
"""Creates a voice websocket for the :class:`VoiceClient`.""" """Creates a voice websocket for the :class:`VoiceClient`."""
gateway = f'wss://{state.endpoint}/?v=4' gateway = f'wss://{state.endpoint}/?v=8'
client = state.voice_client client = state.voice_client
http = client._state.http http = client._state.http
socket = await http.ws_connect(gateway, compress=15) socket = await http.ws_connect(gateway, compress=15)
ws = cls(socket, loop=client.loop, hook=hook) ws = cls(socket, loop=client.loop, hook=hook)
ws.gateway = gateway ws.gateway = gateway
ws.seq_ack = seq_ack
ws._connection = state ws._connection = state
ws._max_heartbeat_timeout = 60.0 ws._max_heartbeat_timeout = 60.0
ws.thread_id = threading.get_ident() ws.thread_id = threading.get_ident()
@ -934,6 +945,7 @@ class DiscordVoiceWebSocket:
_log.debug('Voice websocket frame received: %s', msg) _log.debug('Voice websocket frame received: %s', msg)
op = msg['op'] op = msg['op']
data = msg['d'] # According to Discord this key is always given data = msg['d'] # According to Discord this key is always given
self.seq_ack = msg.get('seq', self.seq_ack) # this key could not be given
if op == self.READY: if op == self.READY:
await self.initial_connection(data) await self.initial_connection(data)

9
discord/http.py

@ -673,14 +673,13 @@ class HTTPClient:
_log.debug(fmt, route_key, bucket_hash, discord_hash) _log.debug(fmt, route_key, bucket_hash, discord_hash)
self._bucket_hashes[route_key] = discord_hash self._bucket_hashes[route_key] = discord_hash
recalculated_key = discord_hash + route.major_parameters self._buckets[f'{discord_hash}:{route.major_parameters}'] = ratelimit
self._buckets[recalculated_key] = ratelimit
self._buckets.pop(key, None) self._buckets.pop(key, None)
elif route_key not in self._bucket_hashes: elif route_key not in self._bucket_hashes:
fmt = '%s has found its initial rate limit bucket hash (%s).' fmt = '%s has found its initial rate limit bucket hash (%s).'
_log.debug(fmt, route_key, discord_hash) _log.debug(fmt, route_key, discord_hash)
self._bucket_hashes[route_key] = discord_hash self._bucket_hashes[route_key] = discord_hash
self._buckets[discord_hash + route.major_parameters] = ratelimit self._buckets[f'{discord_hash}:{route.major_parameters}'] = ratelimit
if has_ratelimit_headers: if has_ratelimit_headers:
if response.status != 429: if response.status != 429:
@ -1835,6 +1834,7 @@ class HTTPClient:
target_type: Optional[invite.InviteTargetType] = None, target_type: Optional[invite.InviteTargetType] = None,
target_user_id: Optional[Snowflake] = None, target_user_id: Optional[Snowflake] = None,
target_application_id: Optional[Snowflake] = None, target_application_id: Optional[Snowflake] = None,
flags: Optional[int] = None,
) -> Response[invite.Invite]: ) -> Response[invite.Invite]:
r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id)
payload = { payload = {
@ -1853,6 +1853,9 @@ class HTTPClient:
if target_application_id: if target_application_id:
payload['target_application_id'] = str(target_application_id) payload['target_application_id'] = str(target_application_id)
if flags:
payload['flags'] = flags
return self.request(r, reason=reason, json=payload) return self.request(r, reason=reason, json=payload)
def get_invite( def get_invite(

46
discord/interactions.py

@ -1296,6 +1296,52 @@ class InteractionResponse(Generic[ClientT]):
self._response_type = InteractionResponseType.autocomplete_result self._response_type = InteractionResponseType.autocomplete_result
async def launch_activity(self) -> InteractionCallbackResponse[ClientT]:
"""|coro|
Responds to this interaction by launching the activity associated with the app.
Only available for apps with activities enabled.
.. versionadded:: 2.6
Raises
-------
HTTPException
Launching the activity failed.
InteractionResponded
This interaction has already been responded to before.
Returns
-------
:class:`InteractionCallbackResponse`
The interaction callback data.
"""
if self._response_type:
raise InteractionResponded(self._parent)
parent = self._parent
adapter = async_context.get()
http = parent._state.http
params = interaction_response_params(InteractionResponseType.launch_activity.value)
response = await adapter.create_interaction_response(
parent.id,
parent.token,
session=parent._session,
proxy=http.proxy,
proxy_auth=http.proxy_auth,
params=params,
)
self._response_type = InteractionResponseType.launch_activity
return InteractionCallbackResponse(
data=response,
parent=self._parent,
state=self._parent._state,
type=self._response_type,
)
class _InteractionMessageState: class _InteractionMessageState:
__slots__ = ('_parent', '_interaction') __slots__ = ('_parent', '_interaction')

11
discord/invite.py

@ -32,6 +32,7 @@ from .mixins import Hashable
from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, InviteType, try_enum from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, InviteType, try_enum
from .appinfo import PartialAppInfo from .appinfo import PartialAppInfo
from .scheduled_event import ScheduledEvent from .scheduled_event import ScheduledEvent
from .flags import InviteFlags
__all__ = ( __all__ = (
'PartialInviteChannel', 'PartialInviteChannel',
@ -379,6 +380,7 @@ class Invite(Hashable):
'scheduled_event', 'scheduled_event',
'scheduled_event_id', 'scheduled_event_id',
'type', 'type',
'_flags',
) )
BASE = 'https://discord.gg' BASE = 'https://discord.gg'
@ -432,6 +434,7 @@ class Invite(Hashable):
else None else None
) )
self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None
self._flags: int = data.get('flags', 0)
@classmethod @classmethod
def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self: def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self:
@ -523,6 +526,14 @@ class Invite(Hashable):
url += '?event=' + str(self.scheduled_event_id) url += '?event=' + str(self.scheduled_event_id)
return url return url
@property
def flags(self) -> InviteFlags:
""":class:`InviteFlags`: Returns the flags for this invite.
.. versionadded:: 2.6
"""
return InviteFlags._from_value(self._flags)
def set_scheduled_event(self, scheduled_event: Snowflake, /) -> Self: def set_scheduled_event(self, scheduled_event: Snowflake, /) -> Self:
"""Sets the scheduled event for this invite. """Sets the scheduled event for this invite.

3
discord/member.py

@ -239,7 +239,8 @@ class Member(discord.abc.Messageable, _UserTag):
---------- ----------
joined_at: Optional[:class:`datetime.datetime`] joined_at: Optional[:class:`datetime.datetime`]
An aware datetime object that specifies the date and time in UTC that the member joined the guild. An aware datetime object that specifies the date and time in UTC that the member joined the guild.
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``. If the member left and rejoined the guild, this will be the latest date.
This can be ``None``, such as when the member is a guest.
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]] activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
The activities that the user is currently doing. The activities that the user is currently doing.

2
discord/types/invite.py

@ -65,6 +65,7 @@ class Invite(IncompleteInvite, total=False):
target_application: PartialAppInfo target_application: PartialAppInfo
guild_scheduled_event: GuildScheduledEvent guild_scheduled_event: GuildScheduledEvent
type: InviteType type: InviteType
flags: NotRequired[int]
class InviteWithCounts(Invite, _GuildPreviewUnique): class InviteWithCounts(Invite, _GuildPreviewUnique):
@ -84,6 +85,7 @@ class GatewayInviteCreate(TypedDict):
target_type: NotRequired[InviteTargetType] target_type: NotRequired[InviteTargetType]
target_user: NotRequired[PartialUser] target_user: NotRequired[PartialUser]
target_application: NotRequired[PartialAppInfo] target_application: NotRequired[PartialAppInfo]
flags: NotRequired[int]
class GatewayInviteDelete(TypedDict): class GatewayInviteDelete(TypedDict):

2
discord/types/member.py

@ -34,7 +34,7 @@ class Nickname(TypedDict):
class PartialMember(TypedDict): class PartialMember(TypedDict):
roles: SnowflakeList roles: SnowflakeList
joined_at: str joined_at: Optional[str] # null if guest
deaf: bool deaf: bool
mute: bool mute: bool
flags: int flags: int

40
discord/voice_state.py

@ -321,7 +321,7 @@ class VoiceConnectionState:
) )
return return
self.endpoint, _, _ = endpoint.rpartition(':') self.endpoint = endpoint
if self.endpoint.startswith('wss://'): if self.endpoint.startswith('wss://'):
# Just in case, strip it off since we're going to add it later # Just in case, strip it off since we're going to add it later
self.endpoint = self.endpoint[6:] self.endpoint = self.endpoint[6:]
@ -574,7 +574,10 @@ class VoiceConnectionState:
self._disconnected.clear() self._disconnected.clear()
async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket: async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket:
ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook) seq_ack = -1
if self.ws is not MISSING:
seq_ack = self.ws.seq_ack
ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook, seq_ack=seq_ack)
self.state = ConnectionFlowState.websocket_connected self.state = ConnectionFlowState.websocket_connected
return ws return ws
@ -603,15 +606,17 @@ class VoiceConnectionState:
# The following close codes are undocumented so I will document them here. # The following close codes are undocumented so I will document them here.
# 1000 - normal closure (obviously) # 1000 - normal closure (obviously)
# 4014 - we were externally disconnected (voice channel deleted, we were moved, etc) # 4014 - we were externally disconnected (voice channel deleted, we were moved, etc)
# 4015 - voice server has crashed # 4015 - voice server has crashed, we should resume
if exc.code in (1000, 4015): # 4021 - rate limited, we should not reconnect
# 4022 - call terminated, similar to 4014
if exc.code == 1000:
# Don't call disconnect a second time if the websocket closed from a disconnect call # Don't call disconnect a second time if the websocket closed from a disconnect call
if not self._expecting_disconnect: if not self._expecting_disconnect:
_log.info('Disconnecting from voice normally, close code %d.', exc.code) _log.info('Disconnecting from voice normally, close code %d.', exc.code)
await self.disconnect() await self.disconnect()
break break
if exc.code == 4014: if exc.code in (4014, 4022):
# We were disconnected by discord # We were disconnected by discord
# This condition is a race between the main ws event and the voice ws closing # This condition is a race between the main ws event and the voice ws closing
if self._disconnected.is_set(): if self._disconnected.is_set():
@ -631,6 +636,31 @@ class VoiceConnectionState:
else: else:
continue continue
if exc.code == 4021:
_log.warning('We are being ratelimited while trying to connect to voice. Disconnecting...')
if self.state is not ConnectionFlowState.disconnected:
await self.disconnect()
break
if exc.code == 4015:
_log.info('Disconnected from voice, attempting a resume...')
try:
await self._connect(
reconnect=reconnect,
timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute,
resume=True,
)
except asyncio.TimeoutError:
_log.info('Could not resume the voice connection... Disconnecting...')
if self.state is not ConnectionFlowState.disconnected:
await self.disconnect()
break
else:
_log.info('Successfully resumed voice connection')
continue
_log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason') _log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason')
if not reconnect: if not reconnect:

16
docs/api.rst

@ -2517,6 +2517,7 @@ of :class:`enum.Enum`.
- :attr:`~AuditLogDiff.channel` - :attr:`~AuditLogDiff.channel`
- :attr:`~AuditLogDiff.uses` - :attr:`~AuditLogDiff.uses`
- :attr:`~AuditLogDiff.max_uses` - :attr:`~AuditLogDiff.max_uses`
- :attr:`~AuditLogDiff.flags`
.. attribute:: invite_update .. attribute:: invite_update
@ -2541,6 +2542,7 @@ of :class:`enum.Enum`.
- :attr:`~AuditLogDiff.channel` - :attr:`~AuditLogDiff.channel`
- :attr:`~AuditLogDiff.uses` - :attr:`~AuditLogDiff.uses`
- :attr:`~AuditLogDiff.max_uses` - :attr:`~AuditLogDiff.max_uses`
- :attr:`~AuditLogDiff.flags`
.. attribute:: webhook_create .. attribute:: webhook_create
@ -4552,11 +4554,11 @@ AuditLogDiff
.. attribute:: flags .. attribute:: flags
The channel flags associated with this thread or forum post. The flags associated with this thread, forum post or invite.
See also :attr:`ForumChannel.flags` and :attr:`Thread.flags` See also :attr:`ForumChannel.flags`, :attr:`Thread.flags` and :attr:`Invite.flags`
:type: :class:`ChannelFlags` :type: Union[:class:`ChannelFlags`, :class:`InviteFlags`]
.. attribute:: default_thread_slowmode_delay .. attribute:: default_thread_slowmode_delay
@ -5742,6 +5744,14 @@ EmbedFlags
.. autoclass:: EmbedFlags() .. autoclass:: EmbedFlags()
:members: :members:
InviteFlags
~~~~~~~~~~~~~~~~
.. attributetable:: InviteFlags
.. autoclass:: InviteFlags()
:members:
ForumTag ForumTag
~~~~~~~~~ ~~~~~~~~~

Loading…
Cancel
Save