Browse Source

Normalize upstream changes for the library

pull/10109/head
dolfies 5 months ago
parent
commit
ac641ea98b
  1. 1
      discord/audit_logs.py
  2. 47
      discord/client.py
  3. 3
      discord/flags.py
  4. 15
      discord/gateway.py
  5. 82
      discord/guild.py
  6. 22
      discord/http.py
  7. 80
      discord/message.py
  8. 11
      discord/permissions.py
  9. 2
      discord/player.py
  10. 2
      discord/raw_models.py
  11. 23
      discord/reaction.py
  12. 11
      discord/state.py
  13. 10
      discord/types/channel.py
  14. 7
      discord/types/guild.py
  15. 4
      discord/types/message.py
  16. 2
      discord/types/poll.py
  17. 2
      discord/types/voice.py
  18. 23
      discord/voice_client.py
  19. 98
      discord/voice_state.py
  20. 3
      discord/webhook/async_.py
  21. 45
      docs/api.rst

1
discord/audit_logs.py

@ -70,7 +70,6 @@ if TYPE_CHECKING:
from .types.invite import Invite as InvitePayload from .types.invite import Invite as InvitePayload
from .types.role import Role as RolePayload from .types.role import Role as RolePayload
from .types.snowflake import Snowflake from .types.snowflake import Snowflake
from .types.command import ApplicationCommandPermissions
from .types.automod import AutoModerationAction from .types.automod import AutoModerationAction
from .user import User from .user import User
from .webhook import Webhook from .webhook import Webhook

47
discord/client.py

@ -274,6 +274,11 @@ class Client:
set to is ``30.0`` seconds. set to is ``30.0`` seconds.
.. versionadded:: 2.0 .. versionadded:: 2.0
preferred_rtc_regions: List[:class:`str`]
A list of preferred RTC regions to connect to. This overrides Discord's suggested list.
.. versionadded:: 2.1
Attributes Attributes
----------- -----------
@ -533,13 +538,24 @@ class Client:
def preferred_rtc_regions(self) -> List[str]: def preferred_rtc_regions(self) -> List[str]:
"""List[:class:`str`]: Geo-ordered list of voice regions the connected client can use. """List[:class:`str`]: Geo-ordered list of voice regions the connected client can use.
This value is determined by Discord by default, but can be set to override it.
.. versionadded:: 2.0 .. versionadded:: 2.0
.. versionchanged:: 2.1 .. versionchanged:: 2.1
Rename from ``preferred_voice_regions`` to ``preferred_rtc_regions``. Rename from ``preferred_voice_regions`` to ``preferred_rtc_regions``.
""" """
return self._connection.preferred_rtc_regions return (
self._connection.overriden_rtc_regions
if self._connection.overriden_rtc_regions is not None
else self._connection.preferred_rtc_regions
)
@preferred_rtc_regions.setter
def preferred_rtc_regions(self, value: List[str]) -> None:
values = [str(x).lower() for x in value]
self._connection.overriden_rtc_regions = values
@property @property
def pending_payments(self) -> Sequence[Payment]: def pending_payments(self) -> Sequence[Payment]:
@ -1808,7 +1824,6 @@ class Client:
self_mute: bool = False, self_mute: bool = False,
self_deaf: bool = False, self_deaf: bool = False,
self_video: bool = False, self_video: bool = False,
preferred_region: Optional[str] = MISSING,
) -> None: ) -> None:
"""|coro| """|coro|
@ -1816,6 +1831,10 @@ class Client:
.. versionadded:: 2.0 .. versionadded:: 2.0
.. versionchanged:: 2.1
Removed the ``preferred_region`` parameter.
Parameters Parameters
----------- -----------
channel: Optional[:class:`~discord.abc.Snowflake`] channel: Optional[:class:`~discord.abc.Snowflake`]
@ -1827,19 +1846,12 @@ class Client:
self_video: :class:`bool` self_video: :class:`bool`
Indicates if the client is using video. Untested & unconfirmed Indicates if the client is using video. Untested & unconfirmed
(do not use). (do not use).
preferred_region: Optional[:class:`str`]
The preferred region to connect to.
""" """
state = self._connection state = self._connection
ws = self.ws ws = self.ws
channel_id = channel.id if channel else None channel_id = channel.id if channel else None
if preferred_region is None or channel_id is None: await ws.voice_state(None, channel_id, self_mute, self_deaf, self_video)
region = None
else:
region = str(preferred_region) if preferred_region else state.preferred_rtc_region
await ws.voice_state(None, channel_id, self_mute, self_deaf, self_video, preferred_region=region)
# Guild stuff # Guild stuff
@ -3092,13 +3104,18 @@ class Client:
data = await self.http.get_country_code() data = await self.http.get_country_code()
return data['country_code'] return data['country_code']
async def fetch_preferred_voice_regions(self) -> List[str]: async def fetch_preferred_rtc_regions(self) -> List[Tuple[str, List[str]]]:
"""|coro| """|coro|
Retrieves the preferred voice regions of the client. Retrieves the preferred RTC regions of the client.
.. versionadded:: 2.0 .. versionadded:: 2.0
.. versionchanged:: 2.1
Changed the name of the method from ``fetch_preferred_voice_regions`` to ``fetch_preferred_rtc_regions``.
The method now returns a list of tuples instead of a list of strings.
Raises Raises
------- -------
HTTPException HTTPException
@ -3106,11 +3123,11 @@ class Client:
Returns Returns
------- -------
List[:class:`str`] List[Tuple[:class:`str`, List[:class:`str`]]]
The preferred voice regions of the client. The region name and list of IPs for the closest voice regions.
""" """
data = await self.http.get_preferred_voice_regions() data = await self.http.get_preferred_voice_regions()
return [v['region'] for v in data] return [(v['region'], v['ips']) for v in data]
async def create_dm(self, user: Snowflake, /) -> DMChannel: async def create_dm(self, user: Snowflake, /) -> DMChannel:
"""|coro| """|coro|

3
discord/flags.py

@ -2750,8 +2750,6 @@ class AttachmentFlags(BaseFlags):
class RoleFlags(BaseFlags): class RoleFlags(BaseFlags):
r"""Wraps up the Discord Role flags r"""Wraps up the Discord Role flags
.. versionadded:: 2.1
.. container:: operations .. container:: operations
.. describe:: x == y .. describe:: x == y
@ -2795,6 +2793,7 @@ class RoleFlags(BaseFlags):
Returns whether any flag is set to ``True``. Returns whether any flag is set to ``True``.
.. versionadded:: 2.1
Attributes Attributes
----------- -----------

15
discord/gateway.py

@ -775,8 +775,6 @@ class DiscordWebSocket:
self_mute: bool = False, self_mute: bool = False,
self_deaf: bool = False, self_deaf: bool = False,
self_video: bool = False, self_video: bool = False,
*,
preferred_region: Optional[str] = None,
) -> None: ) -> None:
payload = { payload = {
'op': self.VOICE_STATE, 'op': self.VOICE_STATE,
@ -789,8 +787,8 @@ class DiscordWebSocket:
}, },
} }
if preferred_region is not None: if channel_id:
payload['d']['preferred_region'] = preferred_region payload['d'].update(self._connection._get_preferred_regions())
_log.debug('Updating %s voice state to %s.', guild_id or 'client', payload) _log.debug('Updating %s voice state to %s.', guild_id or 'client', payload)
await self.send_as_json(payload) await self.send_as_json(payload)
@ -1025,7 +1023,6 @@ class DiscordVoiceWebSocket:
await self.loop.sock_connect(state.socket, (state.endpoint_ip, state.voice_port)) await self.loop.sock_connect(state.socket, (state.endpoint_ip, state.voice_port))
state.ip, state.port = await self.discover_ip() state.ip, state.port = await self.discover_ip()
# there *should* always be at least one supported mode (xsalsa20_poly1305)
modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes] modes = [mode for mode in data['modes'] if mode in self._connection.supported_modes]
_log.debug('Received supported encryption modes: %s.', ', '.join(modes)) _log.debug('Received supported encryption modes: %s.', ', '.join(modes))
@ -1055,7 +1052,7 @@ class DiscordVoiceWebSocket:
_log.debug('Received IP discovery packet: %s.', recv) _log.debug('Received IP discovery packet: %s.', recv)
# the ip is ascii starting at the 8th byte and ending at the first null # The IP is ascii starting at the 8th byte and ending at the first null
ip_start = 8 ip_start = 8
ip_end = recv.index(0, ip_start) ip_end = recv.index(0, ip_start)
ip = recv[ip_start:ip_end].decode('ascii') ip = recv[ip_start:ip_end].decode('ascii')
@ -1084,9 +1081,9 @@ class DiscordVoiceWebSocket:
_log.debug('Received secret key for voice connection.') _log.debug('Received secret key for voice connection.')
self.secret_key = self._connection.secret_key = data['secret_key'] self.secret_key = self._connection.secret_key = data['secret_key']
# Send a speak command with the "not speaking" state. # Send a speak command with the "not speaking" state
# This also tells Discord our SSRC value, which Discord requires before # This also tells Discord our SSRC value, which Discord requires before
# sending any voice data (and is the real reason why we call this here). # sending any voice data (and is the real reason why we call this here)
await self.speak(SpeakingState.none) await self.speak(SpeakingState.none)
async def poll_event(self) -> None: async def poll_event(self) -> None:
@ -1098,7 +1095,7 @@ class DiscordVoiceWebSocket:
_log.debug('Voice received %s.', msg) _log.debug('Voice received %s.', msg)
raise ConnectionClosed(self.ws) from msg.data raise ConnectionClosed(self.ws) from msg.data
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING): elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING):
_log.debug('Voice received %s', msg) _log.debug('Voice received %s.', msg)
raise ConnectionClosed(self.ws, code=self._close_code) raise ConnectionClosed(self.ws, code=self._close_code)
async def close(self, code: int = 1000) -> None: async def close(self, code: int = 1000) -> None:

82
discord/guild.py

@ -377,9 +377,13 @@ class Guild(Hashable):
max_members: Optional[:class:`int`] max_members: Optional[:class:`int`]
The maximum amount of members for the guild. The maximum amount of members for the guild.
max_video_channel_users: Optional[:class:`int`] max_video_channel_users: Optional[:class:`int`]
The maximum amount of users in a video channel. The maximum amount of users in a video stream.
.. versionadded:: 1.4 .. versionadded:: 1.4
max_stage_video_channel_users: Optional[:class:`int`]
The maximum amount of users in a stage video stream.
.. versionadded:: 2.1
description: Optional[:class:`str`] description: Optional[:class:`str`]
The guild's description. The guild's description.
verification_level: :class:`VerificationLevel` verification_level: :class:`VerificationLevel`
@ -453,6 +457,7 @@ class Guild(Hashable):
'max_presences', 'max_presences',
'max_members', 'max_members',
'max_video_channel_users', 'max_video_channel_users',
'max_stage_video_channel_users',
'_premium_tier', '_premium_tier',
'premium_subscription_count', 'premium_subscription_count',
'preferred_locale', 'preferred_locale',
@ -478,6 +483,7 @@ class Guild(Hashable):
'_discovery_splash', '_discovery_splash',
'_rules_channel_id', '_rules_channel_id',
'_public_updates_channel_id', '_public_updates_channel_id',
'_safety_alerts_channel_id',
'_stage_instances', '_stage_instances',
'_scheduled_events', '_scheduled_events',
'_threads', '_threads',
@ -671,6 +677,7 @@ class Guild(Hashable):
self.max_presences: Optional[int] = guild.get('max_presences') self.max_presences: Optional[int] = guild.get('max_presences')
self.max_members: Optional[int] = guild.get('max_members') self.max_members: Optional[int] = guild.get('max_members')
self.max_video_channel_users: Optional[int] = guild.get('max_video_channel_users') self.max_video_channel_users: Optional[int] = guild.get('max_video_channel_users')
self.max_stage_video_channel_users: Optional[int] = guild.get('max_stage_video_channel_users')
self._premium_tier = guild.get('premium_tier') self._premium_tier = guild.get('premium_tier')
self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0 self.premium_subscription_count: int = guild.get('premium_subscription_count') or 0
self.vanity_url_code: Optional[str] = guild.get('vanity_url_code') self.vanity_url_code: Optional[str] = guild.get('vanity_url_code')
@ -681,6 +688,7 @@ class Guild(Hashable):
self._discovery_splash: Optional[str] = guild.get('discovery_splash') self._discovery_splash: Optional[str] = guild.get('discovery_splash')
self._rules_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'rules_channel_id') self._rules_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'rules_channel_id')
self._public_updates_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'public_updates_channel_id') self._public_updates_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'public_updates_channel_id')
self._safety_alerts_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'safety_alerts_channel_id')
self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id') self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id')
self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, guild.get('nsfw_level', 0)) self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, guild.get('nsfw_level', 0))
self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0)) self.mfa_level: MFALevel = try_enum(MFALevel, guild.get('mfa_level', 0))
@ -747,6 +755,16 @@ class Guild(Hashable):
def _extra_large(self) -> bool: def _extra_large(self) -> bool:
return self._member_count is not None and self._member_count >= 75000 return self._member_count is not None and self._member_count >= 75000
@property
def max_stage_video_users(self) -> Optional[int]:
"""Optional[:class:`int`]: The maximum amount of users in a stage video stream.
An alias for :attr:`max_stage_video_channel_users`.
.. versionadded:: 2.1
"""
return self.max_stage_video_channel_users
def is_hub(self) -> bool: def is_hub(self) -> bool:
""":class:`bool`: Whether the guild is a Student Hub. """:class:`bool`: Whether the guild is a Student Hub.
@ -1047,6 +1065,17 @@ class Guild(Hashable):
channel_id = self._public_updates_channel_id channel_id = self._public_updates_channel_id
return channel_id and self._channels.get(channel_id) # type: ignore return channel_id and self._channels.get(channel_id) # type: ignore
@property
def safety_alerts_channel(self) -> Optional[TextChannel]:
"""Optional[:class:`TextChannel`]: Return's the guild's channel used for safety alerts, if set.
For example, this is used for the raid protection setting. The guild must have the ``COMMUNITY`` feature.
.. versionadded:: 2.1
"""
channel_id = self._safety_alerts_channel_id
return channel_id and self._channels.get(channel_id) # type: ignore
@property @property
def afk_channel(self) -> Optional[VocalGuildChannel]: def afk_channel(self) -> Optional[VocalGuildChannel]:
"""Optional[:class:`VoiceChannel`]: Returns the guild channel AFK users are moved to. """Optional[:class:`VoiceChannel`]: Returns the guild channel AFK users are moved to.
@ -2184,12 +2213,14 @@ class Guild(Hashable):
preferred_locale: Locale = MISSING, preferred_locale: Locale = MISSING,
rules_channel: Optional[TextChannel] = MISSING, rules_channel: Optional[TextChannel] = MISSING,
public_updates_channel: Optional[TextChannel] = MISSING, public_updates_channel: Optional[TextChannel] = MISSING,
safety_alerts_channel: Optional[TextChannel] = MISSING,
premium_progress_bar_enabled: bool = MISSING, premium_progress_bar_enabled: bool = MISSING,
discoverable: bool = MISSING, discoverable: bool = MISSING,
invites_disabled: bool = MISSING, invites_disabled: bool = MISSING,
widget_enabled: bool = MISSING, widget_enabled: bool = MISSING,
widget_channel: Optional[Snowflake] = MISSING, widget_channel: Optional[Snowflake] = MISSING,
mfa_level: MFALevel = MISSING, mfa_level: MFALevel = MISSING,
raid_alerts_disabled: bool = MISSING,
invites_disabled_until: datetime = MISSING, invites_disabled_until: datetime = MISSING,
dms_disabled_until: datetime = MISSING, dms_disabled_until: datetime = MISSING,
) -> Guild: ) -> Guild:
@ -2278,6 +2309,12 @@ class Guild(Hashable):
public updates channel. public updates channel.
.. versionadded:: 1.4 .. versionadded:: 1.4
safety_alerts_channel: Optional[:class:`TextChannel`]
The new channel that is used for safety alerts. This is only available to
guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. Could be ``None`` for no
safety alerts channel.
.. versionadded:: 2.1
premium_progress_bar_enabled: :class:`bool` premium_progress_bar_enabled: :class:`bool`
Whether the premium AKA server boost level progress bar should be enabled for the guild. Whether the premium AKA server boost level progress bar should be enabled for the guild.
@ -2303,20 +2340,22 @@ class Guild(Hashable):
Note that you must be owner of the guild to do this. Note that you must be owner of the guild to do this.
.. versionadded:: 2.0 .. versionadded:: 2.0
reason: Optional[:class:`str`] raid_alerts_disabled: :class:`bool`
The reason for editing this guild. Shows up on the audit log. Whether the alerts for raid protection should be disabled for the guild.
.. versionadded:: 2.1
invites_disabled_until: Optional[:class:`datetime.datetime`] invites_disabled_until: Optional[:class:`datetime.datetime`]
The time when invites should be enabled again, or ``None`` to disable the action. The time when invites should be enabled again, or ``None`` to disable the action.
This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
.. versionadded:: 2.1 .. versionadded:: 2.1
dms_disabled_until: Optional[:class:`datetime.datetime`] dms_disabled_until: Optional[:class:`datetime.datetime`]
The time when direct messages should be allowed again, or ``None`` to disable the action. The time when direct messages should be allowed again, or ``None`` to disable the action.
This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`. This must be a timezone-aware datetime object. Consider using :func:`utils.utcnow`.
.. versionadded:: 2.1 .. versionadded:: 2.1
reason: Optional[:class:`str`]
The reason for editing this guild. Shows up on the audit log.
Raises Raises
------- -------
@ -2410,6 +2449,12 @@ class Guild(Hashable):
else: else:
fields['public_updates_channel_id'] = public_updates_channel.id fields['public_updates_channel_id'] = public_updates_channel.id
if safety_alerts_channel is not MISSING:
if safety_alerts_channel is None:
fields['safety_alerts_channel_id'] = safety_alerts_channel
else:
fields['safety_alerts_channel_id'] = safety_alerts_channel.id
if owner is not MISSING: if owner is not MISSING:
if self.owner_id != self._state.self_id: if self.owner_id != self._state.self_id:
raise ValueError('To transfer ownership you must be the owner of the guild') raise ValueError('To transfer ownership you must be the owner of the guild')
@ -2434,7 +2479,7 @@ class Guild(Hashable):
fields['system_channel_flags'] = system_channel_flags.value fields['system_channel_flags'] = system_channel_flags.value
if any(feat is not MISSING for feat in (community, discoverable, invites_disabled)): if any(feat is not MISSING for feat in (community, discoverable, invites_disabled, raid_alerts_disabled)):
features = set(self.features) features = set(self.features)
if community is not MISSING: if community is not MISSING:
@ -2460,6 +2505,12 @@ class Guild(Hashable):
else: else:
features.discard('INVITES_DISABLED') features.discard('INVITES_DISABLED')
if raid_alerts_disabled is not MISSING:
if raid_alerts_disabled:
features.add('RAID_ALERTS_DISABLED')
else:
features.discard('RAID_ALERTS_DISABLED')
fields['features'] = list(features) fields['features'] = list(features)
if premium_progress_bar_enabled is not MISSING: if premium_progress_bar_enabled is not MISSING:
@ -5212,6 +5263,10 @@ class Guild(Hashable):
.. versionadded:: 1.4 .. versionadded:: 1.4
.. versionchanged:: 2.1
Removed the ``preferred_region`` parameter.
Parameters Parameters
----------- -----------
channel: Optional[:class:`abc.Snowflake`] channel: Optional[:class:`abc.Snowflake`]
@ -5223,23 +5278,12 @@ class Guild(Hashable):
self_video: :class:`bool` self_video: :class:`bool`
Indicates if the client is using video. Untested & unconfirmed Indicates if the client is using video. Untested & unconfirmed
(do not use). (do not use).
preferred_region: Optional[:class:`str`]
The preferred region to connect to.
.. versionchanged:: 2.0
The type of this parameter has changed to :class:`str`.
""" """
state = self._state state = self._state
ws = state.ws ws = state.ws
channel_id = channel.id if channel else None channel_id = channel.id if channel else None
if preferred_region is None or channel_id is None: await ws.voice_state(self.id, channel_id, self_mute, self_deaf, self_video)
region = None
else:
region = str(preferred_region) if preferred_region else state.preferred_rtc_region
await ws.voice_state(self.id, channel_id, self_mute, self_deaf, self_video, preferred_region=region)
async def subscribe( async def subscribe(
self, *, typing: bool = MISSING, activities: bool = MISSING, threads: bool = MISSING, member_updates: bool = MISSING self, *, typing: bool = MISSING, activities: bool = MISSING, threads: bool = MISSING, member_updates: bool = MISSING
@ -5575,7 +5619,7 @@ class Guild(Hashable):
return utils.parse_time(self._incidents_data.get('dms_disabled_until')) return utils.parse_time(self._incidents_data.get('dms_disabled_until'))
@property @property
def dm_spam_detected_at(self) -> Optional[datetime.datetime]: def dm_spam_detected_at(self) -> Optional[datetime]:
""":class:`datetime.datetime`: Returns the time when DM spam was detected in the guild. """:class:`datetime.datetime`: Returns the time when DM spam was detected in the guild.
.. versionadded:: 2.1 .. versionadded:: 2.1
@ -5586,7 +5630,7 @@ class Guild(Hashable):
return utils.parse_time(self._incidents_data.get('dm_spam_detected_at')) return utils.parse_time(self._incidents_data.get('dm_spam_detected_at'))
@property @property
def raid_detected_at(self) -> Optional[datetime.datetime]: def raid_detected_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild. """Optional[:class:`datetime.datetime`]: Returns the time when a raid was detected in the guild.
.. versionadded:: 2.1 .. versionadded:: 2.1

22
discord/http.py

@ -1053,7 +1053,7 @@ class HTTPClient:
raise HTTPException(response, data) raise HTTPException(response, data)
async def get_preferred_voice_regions(self) -> List[dict]: async def get_preferred_voice_regions(self) -> List[guild.RTCRegion]:
async with self.__session.get('https://latency.discord.media/rtc') as resp: async with self.__session.get('https://latency.discord.media/rtc') as resp:
if resp.status == 200: if resp.status == 200:
return await resp.json() return await resp.json()
@ -1283,7 +1283,10 @@ class HTTPClient:
else: else:
return self.request(r, json=params.payload) return self.request(r, json=params.payload)
def add_reaction(self, channel_id: Snowflake, message_id: Snowflake, emoji: str) -> Response[None]: def add_reaction(
self, channel_id: Snowflake, message_id: Snowflake, emoji: str, type: message.ReactionType = 0
) -> Response[None]:
params = {'type': type}
return self.request( return self.request(
Route( Route(
'PUT', 'PUT',
@ -1291,31 +1294,36 @@ class HTTPClient:
channel_id=channel_id, channel_id=channel_id,
message_id=message_id, message_id=message_id,
emoji=emoji, emoji=emoji,
) ),
params=params,
) )
def remove_reaction( def remove_reaction(
self, channel_id: Snowflake, message_id: Snowflake, emoji: str, member_id: Snowflake self, channel_id: Snowflake, message_id: Snowflake, emoji: str, member_id: Snowflake, type: message.ReactionType = 0
) -> Response[None]: ) -> Response[None]:
return self.request( return self.request(
Route( Route(
'DELETE', 'DELETE',
'/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{member_id}', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{reaction_type}/{member_id}',
channel_id=channel_id, channel_id=channel_id,
message_id=message_id, message_id=message_id,
member_id=member_id, member_id=member_id,
emoji=emoji, emoji=emoji,
reaction_type=type,
) )
) )
def remove_own_reaction(self, channel_id: Snowflake, message_id: Snowflake, emoji: str) -> Response[None]: def remove_own_reaction(
self, channel_id: Snowflake, message_id: Snowflake, emoji: str, type: message.ReactionType = 0
) -> Response[None]:
return self.request( return self.request(
Route( Route(
'DELETE', 'DELETE',
'/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me', '/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{reaction_type}/@me',
channel_id=channel_id, channel_id=channel_id,
message_id=message_id, message_id=message_id,
emoji=emoji, emoji=emoji,
reaction_type=type,
) )
) )

80
discord/message.py

@ -47,12 +47,22 @@ from typing import (
overload, overload,
) )
from discord.types.components import MessageActionRow
from . import utils from . import utils
from .reaction import Reaction from .reaction import Reaction
from .emoji import Emoji from .emoji import Emoji
from .partial_emoji import PartialEmoji from .partial_emoji import PartialEmoji
from .calls import CallMessage from .calls import CallMessage
from .enums import MessageType, ChannelType, ApplicationCommandType, PurchaseNotificationType, MessageReferenceType, try_enum from .enums import (
MessageType,
ChannelType,
ApplicationCommandType,
PurchaseNotificationType,
MessageReferenceType,
ReactionType,
try_enum,
)
from .errors import HTTPException from .errors import HTTPException
from .components import _component_factory from .components import _component_factory
from .embeds import Embed from .embeds import Embed
@ -472,13 +482,29 @@ class DeletedReferencedMessage:
return self._parent.guild_id return self._parent.guild_id
class MessageSnapshot: class MessageSnapshot(Hashable):
"""Represents a message snapshot attached to a forwarded message. """Represents a message snapshot attached to a forwarded message.
.. container:: operations
.. describe:: x == y
Checks if the message snapshot is equal to another message snapshot.
.. describe:: x != y
Checks if the message snapshot is not equal to another message snapshot.
.. describe:: hash(x)
Returns the hash of the message snapshot.
.. versionadded:: 2.1 .. versionadded:: 2.1
Attributes Attributes
----------- -----------
id: :class:`int`
The ID of the forwarded message.
type: :class:`MessageType` type: :class:`MessageType`
The type of the forwarded message. The type of the forwarded message.
content: :class:`str` content: :class:`str`
@ -519,27 +545,29 @@ class MessageSnapshot:
cls, cls,
state: ConnectionState, state: ConnectionState,
message_snapshots: Optional[List[Dict[Literal['message'], MessageSnapshotPayload]]], message_snapshots: Optional[List[Dict[Literal['message'], MessageSnapshotPayload]]],
reference: MessageReference,
) -> List[Self]: ) -> List[Self]:
if not message_snapshots: if not message_snapshots:
return [] return []
return [cls(state, snapshot['message']) for snapshot in message_snapshots] return [cls(state, snapshot['message'], reference) for snapshot in message_snapshots]
def __init__(self, state: ConnectionState, data: MessageSnapshotPayload): def __init__(self, state: ConnectionState, data: MessageSnapshotPayload, reference: MessageReference):
self.type: MessageType = try_enum(MessageType, data['type']) self.type: MessageType = try_enum(MessageType, data['type'])
self.id: int = reference.message_id # type: ignore
self.content: str = data['content'] self.content: str = data['content']
self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']] self.embeds: List[Embed] = [Embed.from_dict(a) for a in data['embeds']]
self.attachments: List[Attachment] = [Attachment(data=a, state=state) for a in data['attachments']] self.attachments: List[Attachment] = [Attachment(data=a, state=state) for a in data['attachments']]
self.created_at: datetime.datetime = utils.parse_time(data['timestamp']) self.created_at: datetime.datetime = utils.parse_time(data['timestamp'])
self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp']) self._edited_timestamp: Optional[datetime.datetime] = utils.parse_time(data['edited_timestamp'])
self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0)) self.flags: MessageFlags = MessageFlags._from_value(data.get('flags', 0))
self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('stickers_items', [])] self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])]
self.components: List[MessageComponentType] = [] self.components: List[MessageActionRow] = []
for component_data in data.get('components', []): for component_data in data.get('components', []):
component = _component_factory(component_data) component = _component_factory(component_data)
if component is not None: if component is not None:
self.components.append(component) self.components.append(component) # type: ignore
self._state: ConnectionState = state self._state: ConnectionState = state
@ -577,16 +605,7 @@ class MessageSnapshot:
state = self._state state = self._state
return ( return (
utils.find( utils.find(
lambda m: ( lambda m: m.id == self.id,
m.created_at == self.created_at
and m.edited_at == self.edited_at
and m.content == self.content
and m.embeds == self.embeds
and m.components == self.components
and m.stickers == self.stickers
and m.attachments == self.attachments
and m.flags == self.flags
),
reversed(state._messages), reversed(state._messages),
) )
if state._messages if state._messages
@ -1200,7 +1219,7 @@ class PartialMessage(Hashable):
# pinned exists on PartialMessage for duck typing purposes # pinned exists on PartialMessage for duck typing purposes
self.pinned = False self.pinned = False
async def add_reaction(self, emoji: Union[EmojiInputType, Reaction], /) -> None: async def add_reaction(self, emoji: Union[EmojiInputType, Reaction], /, *, boost: bool = False) -> None:
"""|coro| """|coro|
Adds a reaction to the message. Adds a reaction to the message.
@ -1223,6 +1242,10 @@ class PartialMessage(Hashable):
------------ ------------
emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`] emoji: Union[:class:`Emoji`, :class:`Reaction`, :class:`PartialEmoji`, :class:`str`]
The emoji to react with. The emoji to react with.
boost: :class:`bool`
Whether to react with a super reaction.
.. versionadded:: 2.1
Raises Raises
-------- --------
@ -1236,9 +1259,9 @@ class PartialMessage(Hashable):
The emoji parameter is invalid. The emoji parameter is invalid.
""" """
emoji = convert_emoji_reaction(emoji) emoji = convert_emoji_reaction(emoji)
await self._state.http.add_reaction(self.channel.id, self.id, emoji) await self._state.http.add_reaction(self.channel.id, self.id, emoji, type=1 if boost else 0)
async def remove_reaction(self, emoji: Union[EmojiInputType, Reaction], member: Snowflake) -> None: async def remove_reaction(self, emoji: Union[EmojiInputType, Reaction], member: Snowflake, boost: bool = False) -> None:
"""|coro| """|coro|
Remove a reaction by the member from the message. Remove a reaction by the member from the message.
@ -1261,6 +1284,14 @@ class PartialMessage(Hashable):
The emoji to remove. The emoji to remove.
member: :class:`abc.Snowflake` member: :class:`abc.Snowflake`
The member for which to remove the reaction. The member for which to remove the reaction.
boost: :class:`bool`
Whether to remove a super reaction.
.. note::
Keep in mind that members can both react and super react with the same emoji.
.. versionadded:: 2.1
Raises Raises
-------- --------
@ -1276,9 +1307,9 @@ class PartialMessage(Hashable):
emoji = convert_emoji_reaction(emoji) emoji = convert_emoji_reaction(emoji)
if member.id == self._state.self_id: if member.id == self._state.self_id:
await self._state.http.remove_own_reaction(self.channel.id, self.id, emoji) await self._state.http.remove_own_reaction(self.channel.id, self.id, emoji, type=1 if boost else 0)
else: else:
await self._state.http.remove_reaction(self.channel.id, self.id, emoji, member.id) await self._state.http.remove_reaction(self.channel.id, self.id, emoji, member.id, type=1 if boost else 0)
async def clear_reaction(self, emoji: Union[EmojiInputType, Reaction]) -> None: async def clear_reaction(self, emoji: Union[EmojiInputType, Reaction]) -> None:
"""|coro| """|coro|
@ -1657,7 +1688,7 @@ class PartialMessage(Hashable):
.. versionadded:: 1.7 .. versionadded:: 1.7
type: :class:`MessageReferenceType` type: :class:`MessageReferenceType`
The type of message reference. The type of message reference. Default :attr:`MessageReferenceType.reply`.
.. versionadded:: 2.1 .. versionadded:: 2.1
@ -1967,7 +1998,6 @@ class Message(PartialMessage, Hashable):
self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])] self.stickers: List[StickerItem] = [StickerItem(data=d, state=state) for d in data.get('sticker_items', [])]
self.call: Optional[CallMessage] = None self.call: Optional[CallMessage] = None
self.interaction: Optional[Interaction] = None self.interaction: Optional[Interaction] = None
self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots'))
self.poll: Optional[Poll] = None self.poll: Optional[Poll] = None
try: try:
@ -2036,6 +2066,8 @@ class Message(PartialMessage, Hashable):
# The channel will be the correct type here # The channel will be the correct type here
ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore ref.resolved = self.__class__(channel=chan, data=resolved, state=state) # type: ignore
self.message_snapshots: List[MessageSnapshot] = MessageSnapshot._from_value(state, data.get('message_snapshots'), self.reference) # type: ignore
self.role_subscription: Optional[RoleSubscriptionInfo] = None self.role_subscription: Optional[RoleSubscriptionInfo] = None
try: try:
role_subscription = data['role_subscription_data'] role_subscription = data['role_subscription_data']

11
discord/permissions.py

@ -257,10 +257,7 @@ class Permissions(BaseFlags):
no longer part of the general permissions. no longer part of the general permissions.
.. versionchanged:: 2.1 .. versionchanged:: 2.1
Added :attr:`create_expressions` permission. Added :attr:`create_expressions` and :attr:`view_creator_monetization_analytics` permission.
.. versionchanged:: 2.1
Added :attr:`view_creator_monetization_analytics` permission.
""" """
return cls(0b0000_0000_0000_0000_0000_1010_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000) return cls(0b0000_0000_0000_0000_0000_1010_0000_0000_0111_0000_0000_1000_0000_0100_1011_0000)
@ -290,10 +287,7 @@ class Permissions(BaseFlags):
:attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions. :attr:`send_messages_in_threads` and :attr:`use_external_stickers` permissions.
.. versionchanged:: 2.1 .. versionchanged:: 2.1
Added :attr:`send_voice_messages` permission. Added :attr:`send_voice_messages`, :attr:`send_polls`, and :attr:`use_external_apps` permissions.
.. versionchanged:: 2.1
Added :attr:`send_polls` and :attr:`use_external_apps` permissions.
""" """
return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000) return cls(0b0000_0000_0000_0110_0100_0000_0111_1100_1000_0000_0000_0111_1111_1000_0100_0000)
@ -893,6 +887,7 @@ class PermissionOverwrite:
send_polls: Optional[bool] send_polls: Optional[bool]
create_polls: Optional[bool] create_polls: Optional[bool]
use_external_apps: Optional[bool] use_external_apps: Optional[bool]
view_creator_monetization_analytics: Optional[bool]
def __init__(self, **kwargs: Optional[bool]): def __init__(self, **kwargs: Optional[bool]):
self._values: Dict[str, Optional[bool]] = {} self._values: Dict[str, Optional[bool]] = {}

2
discord/player.py

@ -199,7 +199,7 @@ class FFmpegAudio(AudioSource):
self._pipe_reader_thread.start() self._pipe_reader_thread.start()
def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen: def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen:
_log.debug('Spawning ffmpeg process with command: %s', args) _log.debug('Spawning ffmpeg process with command: %s.', args)
process = None process = None
try: try:
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs) process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs)

2
discord/raw_models.py

@ -31,6 +31,8 @@ from .enums import ChannelType, ReactionType, ReadStateType, try_enum
from .utils import _get_as_snowflake from .utils import _get_as_snowflake
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Self
from .guild import Guild from .guild import Guild
from .member import Member from .member import Member
from .message import Message from .message import Message

23
discord/reaction.py

@ -77,11 +77,11 @@ class Reaction:
count: :class:`int` count: :class:`int`
Number of times this reaction was made. This is a sum of :attr:`normal_count` and :attr:`burst_count`. Number of times this reaction was made. This is a sum of :attr:`normal_count` and :attr:`burst_count`.
me: :class:`bool` me: :class:`bool`
If the user sent this reaction. If the user has reacted with this emoji.
message: :class:`Message` message: :class:`Message`
Message this reaction is for. Message this reaction is for.
me_burst: :class:`bool` me_burst: :class:`bool`
If the user sent this super reaction. If the user has super-reacted with this emoji.
.. versionadded:: 2.1 .. versionadded:: 2.1
normal_count: :class:`int` normal_count: :class:`int`
@ -105,11 +105,12 @@ class Reaction:
self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data['emoji']) self.emoji: Union[PartialEmoji, Emoji, str] = emoji or message._state.get_reaction_emoji(data['emoji'])
self.count: int = data.get('count', 1) self.count: int = data.get('count', 1)
self.me: bool = data['me'] self.me: bool = data['me']
details = data.get('count_details', {})
self.normal_count: int = details.get('normal', 0)
self.burst_count: int = details.get('burst', 0)
self.me_burst: bool = data.get('me_burst', False) self.me_burst: bool = data.get('me_burst', False)
details = data.get('count_details', {})
self.normal_count: int = details.get('normal', int(self.me))
self.burst_count: int = details.get('burst', int(self.me_burst))
# TODO: typeguard # TODO: typeguard
def is_custom_emoji(self) -> bool: def is_custom_emoji(self) -> bool:
""":class:`bool`: If this is a custom emoji.""" """:class:`bool`: If this is a custom emoji."""
@ -132,7 +133,7 @@ class Reaction:
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<Reaction emoji={self.emoji!r} me={self.me} count={self.count}>' return f'<Reaction emoji={self.emoji!r} me={self.me} count={self.count}>'
async def remove(self, user: Snowflake) -> None: async def remove(self, user: Snowflake, *, boost: bool = False) -> None:
"""|coro| """|coro|
Remove the reaction by the provided :class:`User` from the message. Remove the reaction by the provided :class:`User` from the message.
@ -147,6 +148,14 @@ class Reaction:
----------- -----------
user: :class:`abc.Snowflake` user: :class:`abc.Snowflake`
The user or member from which to remove the reaction. The user or member from which to remove the reaction.
boost: :class:`bool`
Whether to remove a super reaction.
.. note::
Keep in mind that members can both react and super react with the same emoji.
.. versionadded:: 2.1
Raises Raises
------- -------
@ -158,7 +167,7 @@ class Reaction:
The user you specified, or the reaction's message was not found. The user you specified, or the reaction's message was not found.
""" """
await self.message.remove_reaction(self.emoji, user) await self.message.remove_reaction(self.emoji, user, boost=boost)
async def clear(self) -> None: async def clear(self) -> None:
"""|coro| """|coro|

11
discord/state.py

@ -1016,6 +1016,7 @@ class ConnectionState:
self._status: Optional[str] = status self._status: Optional[str] = status
self._afk: bool = options.get('afk', False) self._afk: bool = options.get('afk', False)
self._idle_since: int = since self._idle_since: int = since
self.overriden_rtc_regions: Optional[List[str]] = options.get('preferred_rtc_regions', None)
if cache_flags._empty: if cache_flags._empty:
self.store_user = self.create_user self.store_user = self.create_user
@ -1131,10 +1132,6 @@ class ConnectionState:
def locale(self) -> str: def locale(self) -> str:
return str(getattr(self.user, 'locale', 'en-US')) return str(getattr(self.user, 'locale', 'en-US'))
@property
def preferred_rtc_region(self) -> str:
return self.preferred_rtc_regions[0] if self.preferred_rtc_regions else 'us-central'
@property @property
def voice_clients(self) -> List[VoiceProtocol]: def voice_clients(self) -> List[VoiceProtocol]:
return list(self._voice_clients.values()) return list(self._voice_clients.values())
@ -1176,6 +1173,12 @@ class ConnectionState:
def _remove_voice_client(self, guild_id: int) -> None: def _remove_voice_client(self, guild_id: int) -> None:
self._voice_clients.pop(guild_id, None) self._voice_clients.pop(guild_id, None)
def _get_preferred_regions(self) -> Dict[str, Union[List[str], str]]:
regions = self.overriden_rtc_regions if self.overriden_rtc_regions is not None else self.client.preferred_rtc_regions
if regions:
return {'preferred_regions': regions, 'preferred_region': regions[0]}
return {}
def _update_references(self, ws: DiscordWebSocket) -> None: def _update_references(self, ws: DiscordWebSocket) -> None:
for vc in self.voice_clients: for vc in self.voice_clients:
vc.main_ws = ws # type: ignore # Silencing the unknown attribute (ok at runtime). vc.main_ws = ws # type: ignore # Silencing the unknown attribute (ok at runtime).

10
discord/types/channel.py

@ -170,7 +170,15 @@ class MediaChannel(_BaseForumChannel):
GuildChannel = Union[ GuildChannel = Union[
TextChannel, NewsChannel, VoiceChannel, CategoryChannel, StageChannel, DirectoryChannel, ThreadChannel, ForumChannel, MediaChannel TextChannel,
NewsChannel,
VoiceChannel,
CategoryChannel,
StageChannel,
DirectoryChannel,
ThreadChannel,
ForumChannel,
MediaChannel,
] ]

7
discord/types/guild.py

@ -130,9 +130,11 @@ class Guild(UnavailableGuild, _GuildMedia):
max_members: NotRequired[int] max_members: NotRequired[int]
premium_subscription_count: NotRequired[int] premium_subscription_count: NotRequired[int]
max_video_channel_users: NotRequired[int] max_video_channel_users: NotRequired[int]
max_stage_video_channel_users: NotRequired[int]
# application_command_counts: ApplicationCommandCounts # application_command_counts: ApplicationCommandCounts
hub_type: Optional[Literal[0, 1, 2]] hub_type: Optional[Literal[0, 1, 2]]
incidents_data: Optional[IncidentData] incidents_data: Optional[IncidentData]
safety_alerts_channel_id: Optional[Snowflake]
class UserGuild(BaseGuild): class UserGuild(BaseGuild):
@ -201,3 +203,8 @@ class SupplementalGuild(UnavailableGuild):
class BulkBanUserResponse(TypedDict): class BulkBanUserResponse(TypedDict):
banned_users: Optional[List[Snowflake]] banned_users: Optional[List[Snowflake]]
failed_users: Optional[List[Snowflake]] failed_users: Optional[List[Snowflake]]
class RTCRegion(TypedDict):
region: str
ips: List[str]

4
discord/types/message.py

@ -180,8 +180,8 @@ class MessageSnapshot(TypedDict):
flags: NotRequired[int] flags: NotRequired[int]
mentions: List[UserWithMember] mentions: List[UserWithMember]
mention_roles: SnowflakeList mention_roles: SnowflakeList
stickers_items: NotRequired[List[StickerItem]] sticker_items: NotRequired[List[StickerItem]]
components: NotRequired[List[Component]] components: NotRequired[List[MessageActionRow]]
class Message(PartialMessage): class Message(PartialMessage):

2
discord/types/poll.py

@ -35,7 +35,7 @@ if TYPE_CHECKING:
from .emoji import PartialEmoji from .emoji import PartialEmoji
LayoutType = Literal[1] # 1 = Default LayoutType = Literal[1, 2] # 1 = Default
class PollMedia(TypedDict): class PollMedia(TypedDict):

2
discord/types/voice.py

@ -76,7 +76,7 @@ class VoiceRegion(TypedDict):
class VoiceServerUpdate(TypedDict): class VoiceServerUpdate(TypedDict):
token: str token: str
guild_id: Snowflake guild_id: NotRequired[Snowflake]
channel_id: Snowflake channel_id: Snowflake
endpoint: Optional[str] endpoint: Optional[str]

23
discord/voice_client.py

@ -47,6 +47,7 @@ if TYPE_CHECKING:
from .types.gateway import VoiceStateUpdateEvent as VoiceStateUpdatePayload from .types.gateway import VoiceStateUpdateEvent as VoiceStateUpdatePayload
from .types.voice import ( from .types.voice import (
GuildVoiceState as GuildVoiceStatePayload,
VoiceServerUpdate as VoiceServerUpdatePayload, VoiceServerUpdate as VoiceServerUpdatePayload,
TransportEncryptionModes, TransportEncryptionModes,
) )
@ -128,7 +129,9 @@ class VoiceProtocol:
""" """
raise NotImplementedError raise NotImplementedError
async def connect(self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False) -> None: async def connect(
self, *, timeout: float, reconnect: bool, self_deaf: bool = False, self_mute: bool = False, self_video: bool = False
) -> None:
"""|coro| """|coro|
An abstract method called when the client initiates the connection request. An abstract method called when the client initiates the connection request.
@ -302,9 +305,16 @@ class VoiceClient(VoiceProtocol):
async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None: async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None:
await self._connection.voice_server_update(data) await self._connection.voice_server_update(data)
async def connect(self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False) -> None: async def connect(
self, *, reconnect: bool, timeout: float, self_deaf: bool = False, self_mute: bool = False, self_video: bool = False
) -> None:
await self._connection.connect( await self._connection.connect(
reconnect=reconnect, timeout=timeout, self_deaf=self_deaf, self_mute=self_mute, resume=False reconnect=reconnect,
timeout=timeout,
self_deaf=self_deaf,
self_mute=self_mute,
self_video=self_video,
resume=False,
) )
def wait_until_connected(self, timeout: Optional[float] = 30.0) -> bool: def wait_until_connected(self, timeout: Optional[float] = 30.0) -> bool:
@ -541,10 +551,10 @@ class VoiceClient(VoiceProtocol):
@source.setter @source.setter
def source(self, value: AudioSource) -> None: def source(self, value: AudioSource) -> None:
if not isinstance(value, AudioSource): if not isinstance(value, AudioSource):
raise TypeError(f'expected AudioSource not {value.__class__.__name__}.') raise TypeError(f'expected AudioSource not {value.__class__.__name__}')
if self._player is None: if self._player is None:
raise ValueError('Not playing anything.') raise ValueError('Not playing anything')
self._player.set_source(value) self._player.set_source(value)
@ -567,7 +577,6 @@ class VoiceClient(VoiceProtocol):
opus.OpusError opus.OpusError
Encoding the data failed. Encoding the data failed.
""" """
self.checked_add('sequence', 1, 65535) self.checked_add('sequence', 1, 65535)
if encode: if encode:
encoded_data = self.encoder.encode(data, self.encoder.SAMPLES_PER_FRAME) encoded_data = self.encoder.encode(data, self.encoder.SAMPLES_PER_FRAME)
@ -577,6 +586,6 @@ class VoiceClient(VoiceProtocol):
try: try:
self._connection.send_packet(packet) self._connection.send_packet(packet)
except OSError: except OSError:
_log.debug('A packet has been dropped (seq: %s, timestamp: %s)', self.sequence, self.timestamp) _log.debug('A packet has been dropped (seq: %s, timestamp: %s).', self.sequence, self.timestamp)
self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME, 4294967295) self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME, 4294967295)

98
discord/voice_state.py

@ -148,7 +148,7 @@ class SocketReader(threading.Thread):
readable, _, _ = select.select([self.state.socket], [], [], 30) readable, _, _ = select.select([self.state.socket], [], [], 30)
except (ValueError, TypeError, OSError) as e: except (ValueError, TypeError, OSError) as e:
_log.debug( _log.debug(
"Select error handling socket in reader, this should be safe to ignore: %s: %s", e.__class__.__name__, e 'Select error handling socket in reader, this should be safe to ignore: %s: %s.', e.__class__.__name__, e
) )
# The socket is either closed or doesn't exist at the moment # The socket is either closed or doesn't exist at the moment
continue continue
@ -159,13 +159,13 @@ class SocketReader(threading.Thread):
try: try:
data = self.state.socket.recv(2048) data = self.state.socket.recv(2048)
except OSError: except OSError:
_log.debug('Error reading from socket in %s, this should be safe to ignore', self, exc_info=True) _log.debug('Error reading from socket in %s, this should be safe to ignore.', self, exc_info=True)
else: else:
for cb in self._callbacks: for cb in self._callbacks:
try: try:
cb(data) cb(data)
except Exception: except Exception:
_log.exception('Error calling %s in %s', cb, self) _log.exception('Error calling %s in %s.', cb, self)
class ConnectionFlowState(Enum): class ConnectionFlowState(Enum):
@ -195,6 +195,7 @@ class VoiceConnectionState:
self.reconnect: bool = True self.reconnect: bool = True
self.self_deaf: bool = False self.self_deaf: bool = False
self.self_mute: bool = False self.self_mute: bool = False
self.self_video: bool = False
self.token: Optional[str] = None self.token: Optional[str] = None
self.session_id: Optional[str] = None self.session_id: Optional[str] = None
self.endpoint: Optional[str] = None self.endpoint: Optional[str] = None
@ -226,7 +227,7 @@ class VoiceConnectionState:
@state.setter @state.setter
def state(self, state: ConnectionFlowState) -> None: def state(self, state: ConnectionFlowState) -> None:
if state is not self._state: if state is not self._state:
_log.debug('Connection state changed to %s', state.name) _log.debug('Voice connection state changed to %s.', state.name)
self._state = state self._state = state
self._state_event.set() self._state_event.set()
self._state_event.clear() self._state_event.clear()
@ -298,11 +299,12 @@ class VoiceConnectionState:
timeout=self.timeout, timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf, self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute, self_mute=(self.self_voice_state or self).self_mute,
self_video=(self.self_voice_state or self).self_video,
resume=False, resume=False,
wait=False, wait=False,
) )
else: else:
_log.debug('Ignoring unexpected voice_state_update event') _log.debug('Ignoring unexpected VOICE_STATE_UPDATE event.')
async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None: async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None:
previous_token = self.token previous_token = self.token
@ -310,13 +312,13 @@ class VoiceConnectionState:
previous_endpoint = self.endpoint previous_endpoint = self.endpoint
self.token = data['token'] self.token = data['token']
self.server_id = int(data['guild_id']) self.server_id = int(data.get('guild_id', data['channel_id']))
endpoint = data.get('endpoint') endpoint = data.get('endpoint')
if self.token is None or endpoint is None: if self.token is None or endpoint is None:
_log.warning( _log.warning(
'Awaiting endpoint... This requires waiting. ' 'Awaiting endpoint... This requires waiting. '
'If timeout occurred considering raising the timeout and reconnecting.' 'If timeout occurrs, considering raising the timeout and reconnecting.'
) )
return return
@ -337,15 +339,15 @@ class VoiceConnectionState:
self.state = ConnectionFlowState.got_both_voice_updates self.state = ConnectionFlowState.got_both_voice_updates
elif self.state is ConnectionFlowState.connected: elif self.state is ConnectionFlowState.connected:
_log.debug('Voice server update, closing old voice websocket') _log.debug('Got VOICE_SERVER_UPDATE, closing old voice gateway.')
await self.ws.close(4014) await self.ws.close(4014)
self.state = ConnectionFlowState.got_voice_server_update self.state = ConnectionFlowState.got_voice_server_update
elif self.state is not ConnectionFlowState.disconnected: elif self.state is not ConnectionFlowState.disconnected:
# eventual consistency # eventual consistency
if previous_token == self.token and previous_server_id == self.server_id: if previous_token == self.token and previous_server_id == self.server_id and previous_endpoint == self.endpoint:
return return
_log.debug('Unexpected server update event, attempting to handle') _log.debug('Unexpected VOICE_SERVER_UPDATE event, attempting to handle...')
await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update) await self.soft_disconnect(with_state=ConnectionFlowState.got_voice_server_update)
await self.connect( await self.connect(
@ -353,13 +355,22 @@ class VoiceConnectionState:
timeout=self.timeout, timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf, self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute, self_mute=(self.self_voice_state or self).self_mute,
self_video=(self.self_voice_state or self).self_video,
resume=False, resume=False,
wait=False, wait=False,
) )
self._create_socket() self._create_socket()
async def connect( async def connect(
self, *, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool, wait: bool = True self,
*,
reconnect: bool,
timeout: float,
self_deaf: bool,
self_mute: bool,
self_video: bool,
resume: bool,
wait: bool = True,
) -> None: ) -> None:
if self._connector: if self._connector:
self._connector.cancel() self._connector.cancel()
@ -372,7 +383,7 @@ class VoiceConnectionState:
self.timeout = timeout self.timeout = timeout
self.reconnect = reconnect self.reconnect = reconnect
self._connector = self.voice_client.loop.create_task( self._connector = self.voice_client.loop.create_task(
self._wrap_connect(reconnect, timeout, self_deaf, self_mute, resume), name='Voice connector' self._wrap_connect(reconnect, timeout, self_deaf, self_mute, self_video, resume), name='Voice connector'
) )
if wait: if wait:
await self._connector await self._connector
@ -381,30 +392,32 @@ class VoiceConnectionState:
try: try:
await self._connect(*args) await self._connect(*args)
except asyncio.CancelledError: except asyncio.CancelledError:
_log.debug('Cancelling voice connection') _log.debug('Cancelling voice connection.')
await self.soft_disconnect() await self.soft_disconnect()
raise raise
except asyncio.TimeoutError: except asyncio.TimeoutError:
_log.info('Timed out connecting to voice') _log.info('Timed out connecting to voice.')
await self.disconnect() await self.disconnect()
raise raise
except Exception: except Exception:
_log.exception('Error connecting to voice... disconnecting') _log.exception('Error connecting to voice. Disconnecting.')
await self.disconnect() await self.disconnect()
raise raise
async def _inner_connect(self, reconnect: bool, self_deaf: bool, self_mute: bool, resume: bool) -> None: async def _inner_connect(
self, reconnect: bool, self_deaf: bool, self_mute: bool, self_video: bool, resume: bool
) -> None:
for i in range(5): for i in range(5):
_log.info('Starting voice handshake... (connection attempt %d)', i + 1) _log.info('Starting voice handshake (connection attempt %d)...', i + 1)
await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute) await self._voice_connect(self_deaf=self_deaf, self_mute=self_mute, self_video=self_video)
# Setting this unnecessarily will break reconnecting # Setting this unnecessarily will break reconnecting
if self.state is ConnectionFlowState.disconnected: if self.state is ConnectionFlowState.disconnected:
self.state = ConnectionFlowState.set_guild_voice_state self.state = ConnectionFlowState.set_guild_voice_state
await self._wait_for_state(ConnectionFlowState.got_both_voice_updates) await self._wait_for_state(ConnectionFlowState.got_both_voice_updates)
_log.info('Voice handshake complete. Endpoint found: %s', self.endpoint) _log.info('Voice handshake complete. Endpoint found: %s.', self.endpoint)
try: try:
self.ws = await self._connect_websocket(resume) self.ws = await self._connect_websocket(resume)
@ -421,11 +434,15 @@ class VoiceConnectionState:
await self.disconnect() await self.disconnect()
raise raise
async def _connect(self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, resume: bool) -> None: async def _connect(
self, reconnect: bool, timeout: float, self_deaf: bool, self_mute: bool, self_video: bool, resume: bool
) -> None:
_log.info('Connecting to voice...') _log.info('Connecting to voice...')
await asyncio.wait_for( await asyncio.wait_for(
self._inner_connect(reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, resume=resume), self._inner_connect(
reconnect=reconnect, self_deaf=self_deaf, self_mute=self_mute, self_video=self_video, resume=resume
),
timeout=timeout, timeout=timeout,
) )
_log.info('Voice connection complete.') _log.info('Voice connection complete.')
@ -442,7 +459,7 @@ class VoiceConnectionState:
if self.ws: if self.ws:
await self.ws.close() await self.ws.close()
except Exception: except Exception:
_log.debug('Ignoring exception disconnecting from voice', exc_info=True) _log.debug('Ignoring exception disconnecting from voice.', exc_info=True)
finally: finally:
self.state = ConnectionFlowState.disconnected self.state = ConnectionFlowState.disconnected
self._socket_reader.pause() self._socket_reader.pause()
@ -471,7 +488,7 @@ class VoiceConnectionState:
try: try:
await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout) await asyncio.wait_for(self._disconnected.wait(), timeout=self.timeout)
except TimeoutError: except TimeoutError:
_log.debug('Timed out waiting for voice disconnection confirmation') _log.debug('Timed out waiting for voice disconnection confirmation.')
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
@ -489,7 +506,7 @@ class VoiceConnectionState:
if self.ws: if self.ws:
await self.ws.close() await self.ws.close()
except Exception: except Exception:
_log.debug('Ignoring exception soft disconnecting from voice', exc_info=True) _log.debug('Ignoring exception soft disconnecting from voice.', exc_info=True)
finally: finally:
self.state = with_state self.state = with_state
self._socket_reader.pause() self._socket_reader.pause()
@ -518,9 +535,13 @@ class VoiceConnectionState:
try: try:
await self.wait_async(timeout) await self.wait_async(timeout)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_log.warning('Timed out trying to move to channel %s in guild %s', channel.id, self.guild.id if self.guild else 'private') _log.warning(
'Timed out trying to move to channel %s in guild %s.',
channel.id,
self.guild.id if self.guild else '"private"',
)
if self.state is last_state: if self.state is last_state:
_log.debug('Reverting to previous state %s', previous_state.name) _log.debug('Reverting to previous voice state %s.', previous_state.name)
self.state = previous_state self.state = previous_state
def wait(self, timeout: Optional[float] = None) -> bool: def wait(self, timeout: Optional[float] = None) -> bool:
@ -536,11 +557,11 @@ class VoiceConnectionState:
self.socket.sendall(packet) self.socket.sendall(packet)
def add_socket_listener(self, callback: SocketReaderCallback) -> None: def add_socket_listener(self, callback: SocketReaderCallback) -> None:
_log.debug('Registering socket listener callback %s', callback) _log.debug('Registering voice socket listener callback %s.', callback)
self._socket_reader.register(callback) self._socket_reader.register(callback)
def remove_socket_listener(self, callback: SocketReaderCallback) -> None: def remove_socket_listener(self, callback: SocketReaderCallback) -> None:
_log.debug('Unregistering socket listener callback %s', callback) _log.debug('Unregistering voice socket listener callback %s.', callback)
self._socket_reader.unregister(callback) self._socket_reader.unregister(callback)
def _inside_runner(self) -> bool: def _inside_runner(self) -> bool:
@ -555,18 +576,22 @@ class VoiceConnectionState:
return return
await sane_wait_for([self._state_event.wait()], timeout=timeout) await sane_wait_for([self._state_event.wait()], timeout=timeout)
async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False) -> None: async def _voice_connect(self, *, self_deaf: bool = False, self_mute: bool = False, self_video: bool = False) -> None:
channel = self.voice_client.channel channel = self.voice_client.channel
if self.guild: if self.guild:
await self.guild.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute) await self.guild.change_voice_state(
channel=channel, self_deaf=self_deaf, self_mute=self_mute, self_video=self_video
)
else: else:
await self.voice_client._state.client.change_voice_state(channel=channel, self_deaf=self_deaf, self_mute=self_mute) await self.voice_client._state.client.change_voice_state(
channel=channel, self_deaf=self_deaf, self_mute=self_mute, self_video=self_video
)
async def _voice_disconnect(self) -> None: async def _voice_disconnect(self) -> None:
_log.info( _log.info(
'The voice handshake is being terminated for Channel ID %s (Guild ID %s)', 'The voice handshake is being terminated for channel ID %s (guild ID %s).',
self.voice_client.channel.id, self.voice_client.channel.id,
self.guild.id if self.guild else 'private', self.guild.id if self.guild else '"private"',
) )
self.state = ConnectionFlowState.disconnected self.state = ConnectionFlowState.disconnected
if self.guild: if self.guild:
@ -618,12 +643,12 @@ class VoiceConnectionState:
# 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():
_log.info('Disconnected from voice by discord, close code %d.', exc.code) _log.info('Disconnected from voice by Discord, close code %d.', exc.code)
await self.disconnect() await self.disconnect()
break break
# We may have been moved to a different channel # We may have been moved to a different channel
_log.info('Disconnected from voice by force... potentially reconnecting.') _log.info('Disconnected from voice by force. Potentially reconnecting...')
successful = await self._potential_reconnect() successful = await self._potential_reconnect()
if not successful: if not successful:
_log.info('Reconnect was unsuccessful, disconnecting from voice normally...') _log.info('Reconnect was unsuccessful, disconnecting from voice normally...')
@ -634,7 +659,7 @@ class VoiceConnectionState:
else: else:
continue 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:
await self.disconnect() await self.disconnect()
@ -651,6 +676,7 @@ class VoiceConnectionState:
timeout=self.timeout, timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf, self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute, self_mute=(self.self_voice_state or self).self_mute,
self_video=(self.self_voice_state or self).self_video,
resume=False, resume=False,
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:

3
discord/webhook/async_.py

@ -58,6 +58,7 @@ __all__ = (
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from datetime import datetime
from typing_extensions import Self from typing_extensions import Self
from types import TracebackType from types import TracebackType
@ -807,7 +808,7 @@ class BaseWebhook(Hashable):
return guild and guild.get_channel(self.channel_id) # type: ignore return guild and guild.get_channel(self.channel_id) # type: ignore
@property @property
def created_at(self) -> datetime.datetime: def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the webhook's creation time in UTC.""" """:class:`datetime.datetime`: Returns the webhook's creation time in UTC."""
return utils.snowflake_time(self.id) return utils.snowflake_time(self.id)

45
docs/api.rst

@ -1977,22 +1977,6 @@ of :class:`enum.Enum`.
.. versionadded:: 2.0 .. versionadded:: 2.0
.. class:: InviteType
Specifies the type of :class:`Invite`.
.. attribute:: guild
A guild invite.
.. attribute:: group_dm
A group DM invite.
.. attribute:: friend
A friend invite.
.. attribute:: guild_incident_alert_mode_enabled .. attribute:: guild_incident_alert_mode_enabled
The system message sent when security actions is enabled. The system message sent when security actions is enabled.
@ -2023,6 +2007,22 @@ of :class:`enum.Enum`.
.. versionadded:: 2.1 .. versionadded:: 2.1
.. class:: InviteType
Specifies the type of :class:`Invite`.
.. attribute:: guild
A guild invite.
.. attribute:: group_dm
A group DM invite.
.. attribute:: friend
A friend invite.
.. class:: UserFlags .. class:: UserFlags
Represents Discord User flags. Represents Discord User flags.
@ -8261,14 +8261,6 @@ DirectoryEntry
.. autoclass:: DirectoryEntry() .. autoclass:: DirectoryEntry()
:members: :members:
RoleFlags
~~~~~~~~~~
.. attributetable:: RoleFlags
.. autoclass:: RoleFlags
:members:
ForumTag ForumTag
~~~~~~~~~ ~~~~~~~~~
@ -8429,6 +8421,11 @@ Flags
.. autoclass:: ReadStateFlags() .. autoclass:: ReadStateFlags()
:members: :members:
.. attributetable:: RoleFlags
.. autoclass:: RoleFlags()
:members:
.. attributetable:: SKUFlags .. attributetable:: SKUFlags
.. autoclass:: SKUFlags() .. autoclass:: SKUFlags()

Loading…
Cancel
Save