diff --git a/discord/__init__.py b/discord/__init__.py index 1e74cf910..9a6f6e97f 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -2,15 +2,15 @@ Discord API Wrapper ~~~~~~~~~~~~~~~~~~~ -A basic wrapper for the Discord API. +A basic wrapper for the Discord user API. :copyright: (c) 2015-present Rapptz :license: MIT, see LICENSE for more details. """ -__title__ = 'discord' -__author__ = 'Rapptz' +__title__ = 'discord.py-self' +__author__ = 'Dolfies' __license__ = 'MIT' __copyright__ = 'Copyright 2015-present Rapptz' __version__ = '2.0.0a' @@ -47,7 +47,6 @@ from . import utils, opus, abc, ui from .enums import * from .embeds import * from .mentions import * -from .shard import * from .player import * from .webhook import * from .voice_client import * @@ -61,14 +60,13 @@ from .components import * from .threads import * -class VersionInfo(NamedTuple): +class _VersionInfo(NamedTuple): major: int minor: int micro: int - releaselevel: Literal["alpha", "beta", "candidate", "final"] + releaselevel: Literal['alpha', 'beta', 'candidate', 'final'] serial: int - -version_info: VersionInfo = VersionInfo(major=2, minor=0, micro=0, releaselevel='alpha', serial=0) +version_info: _VersionInfo = _VersionInfo(major=2, minor=0, micro=0, releaselevel='alpha', serial=0) logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/discord/abc.py b/discord/abc.py index fd2dc4bb9..a2ca1e489 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -1633,8 +1633,6 @@ class Connectable(Protocol): Connects to voice and creates a :class:`VoiceClient` to establish your connection to the voice server. - This requires :attr:`Intents.voice_states`. - Parameters ----------- timeout: :class:`float` diff --git a/discord/client.py b/discord/client.py index b6198d109..f2389d640 100644 --- a/discord/client.py +++ b/discord/client.py @@ -44,7 +44,6 @@ from .enums import ChannelType from .mentions import AllowedMentions from .errors import * from .enums import Status, VoiceRegion -from .flags import ApplicationFlags, Intents from .gateway import * from .activity import ActivityTypes, BaseActivity, create_activity from .voice_client import VoiceClient @@ -133,29 +132,15 @@ class Client: Proxy URL. proxy_auth: Optional[:class:`aiohttp.BasicAuth`] An object that represents proxy HTTP Basic Authorization. - shard_id: Optional[:class:`int`] - Integer starting at ``0`` and less than :attr:`.shard_count`. - shard_count: Optional[:class:`int`] - The total number of shards. - application_id: :class:`int` - The client's application ID. - intents: :class:`Intents` - The intents that you want to enable for the session. This is a way of - disabling and enabling certain gateway events from triggering and being sent. - If not given, defaults to a regularly constructed :class:`Intents` class. - - .. versionadded:: 1.5 member_cache_flags: :class:`MemberCacheFlags` Allows for finer control over how the library caches members. - If not given, defaults to cache as much as possible with the - currently selected intents. + If not given, defaults to cache as much as possible. .. versionadded:: 1.5 chunk_guilds_at_startup: :class:`bool` Indicates if :func:`.on_ready` should be delayed to chunk all guilds at start-up if necessary. This operation is incredibly slow for large - amounts of guilds. The default is ``True`` if :attr:`Intents.members` - is ``True``. + amounts of guilds. The default is ``True``. .. versionadded:: 1.5 status: Optional[:class:`.Status`] @@ -171,11 +156,6 @@ class Client: WebSocket in the case of not receiving a HEARTBEAT_ACK. Useful if processing the initial packets take too long to the point of disconnecting you. The default timeout is 60 seconds. - guild_ready_timeout: :class:`float` - The maximum number of seconds to wait for the GUILD_CREATE stream to end before - preparing the member cache and firing READY. The default timeout is 2 seconds. - - .. versionadded:: 1.4 assume_unsync_clock: :class:`bool` Whether to assume the system clock is unsynced. This applies to the ratelimit handling code. If this is set to ``True``, the default, then the library uses the time to reset @@ -206,12 +186,10 @@ class Client: loop: Optional[asyncio.AbstractEventLoop] = None, **options: Any, ): - # self.ws is set in the connect method + # Set in the connect method self.ws: DiscordWebSocket = None # type: ignore self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() if loop is None else loop self._listeners: Dict[str, List[Tuple[asyncio.Future, Callable[..., bool]]]] = {} - self.shard_id: Optional[int] = options.get('shard_id') - self.shard_count: Optional[int] = options.get('shard_count') connector: Optional[aiohttp.BaseConnector] = options.pop('connector', None) proxy: Optional[str] = options.pop('proxy', None) @@ -229,7 +207,6 @@ class Client: self._enable_debug_events: bool = options.pop('enable_debug_events', False) self._connection: ConnectionState = self._get_state(**options) - self._connection.shard_count = self.shard_count self._closed: bool = False self._ready: asyncio.Event = asyncio.Event() self._connection._get_websocket = self._get_websocket @@ -237,11 +214,11 @@ class Client: if VoiceClient.warn_nacl: VoiceClient.warn_nacl = False - _log.warning("PyNaCl is not installed, voice will NOT be supported") + _log.warning('PyNaCl is not installed, voice will NOT be supported.') - # internals + # Internals - def _get_websocket(self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None) -> DiscordWebSocket: + def _get_websocket(self, guild_id: Optional[int] = None) -> DiscordWebSocket: return self.ws def _get_state(self, **options: Any) -> ConnectionState: @@ -305,13 +282,7 @@ class Client: @property def private_channels(self) -> List[PrivateChannel]: - """List[:class:`.abc.PrivateChannel`]: The private channels that the connected client is participating on. - - .. note:: - - This returns only up to 128 most recent private channels due to an internal working - on how Discord deals with private channels. - """ + """List[:class:`.abc.PrivateChannel`]: The private channels that the connected client is participating on.""" return self._connection.private_channels @property @@ -322,26 +293,6 @@ class Client: """ return self._connection.voice_clients - @property - def application_id(self) -> Optional[int]: - """Optional[:class:`int`]: The client's application ID. - - If this is not passed via ``__init__`` then this is retrieved - through the gateway when an event contains the data. Usually - after :func:`~discord.on_connect` is called. - - .. versionadded:: 2.0 - """ - return self._connection.application_id - - @property - def application_flags(self) -> ApplicationFlags: - """:class:`~discord.ApplicationFlags`: The client's application flags. - - .. versionadded:: 2.0 - """ - return self._connection.application_flags # type: ignore - def is_ready(self) -> bool: """:class:`bool`: Specifies if the client's internal cache is ready for use.""" return self._ready.is_set() @@ -363,7 +314,7 @@ class Client: return asyncio.create_task(wrapped, name=f'discord.py: {event_name}') def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None: - _log.debug('Dispatching event %s', event) + _log.debug('Dispatching event %s.', event) method = 'on_' + event listeners = self._listeners.get(event) @@ -414,49 +365,51 @@ class Client: print(f'Ignoring exception in {event_method}', file=sys.stderr) traceback.print_exc() - # hooks + # Hooks - async def _call_before_identify_hook(self, shard_id: Optional[int], *, initial: bool = False) -> None: - # This hook is an internal hook that actually calls the public one. + async def _call_before_identify_hook(self, *, initial: bool = False) -> None: + # This hook is an internal hook that actually calls the public one # It allows the library to have its own hook without stepping on the - # toes of those who need to override their own hook. - await self.before_identify_hook(shard_id, initial=initial) + # toes of those who need to override their own hook + await self.before_identify_hook(initial=initial) - async def before_identify_hook(self, shard_id: Optional[int], *, initial: bool = False) -> None: + async def before_identify_hook(self, *, initial: bool = False) -> None: """|coro| A hook that is called before IDENTIFYing a session. This is useful if you wish to have more control over the synchronization of multiple IDENTIFYing clients. - The default implementation sleeps for 5 seconds. + The default implementation does nothing. .. versionadded:: 1.4 Parameters ------------ - shard_id: :class:`int` - The shard ID that requested being IDENTIFY'd initial: :class:`bool` Whether this IDENTIFY is the first initial IDENTIFY. """ - if not initial: - await asyncio.sleep(5.0) + pass - # login state management + # Login state management async def login(self, token: str) -> None: """|coro| Logs in the client with the specified credentials. + .. warning:: + + Logging on with a user token is unfortunately against the Discord + `Terms of Service `_ + and doing so might potentially get your account banned. + Use this at your own risk. Parameters ----------- token: :class:`str` - The authentication token. Do not prefix this token with - anything as the library will do it for you. + The authentication token. Raises ------ @@ -468,7 +421,7 @@ class Client: passing status code. """ - _log.info('logging in using static token') + _log.info('Logging in using static token.') data = await self.http.static_login(token.strip()) self._connection.user = ClientUser(state=self._connection, data=data) @@ -486,8 +439,8 @@ class Client: reconnect: :class:`bool` If we should attempt reconnecting, either due to internet failure or a specific failure on Discord's part. Certain - disconnects that lead to bad state will not be handled (such as - invalid sharding payloads or bad tokens). + disconnects that lead to bad state will not be handled + (such as bad tokens). Raises ------- @@ -501,7 +454,6 @@ class Client: backoff = ExponentialBackoff() ws_params = { 'initial': True, - 'shard_id': self.shard_id, } while not self.is_closed(): try: @@ -526,7 +478,7 @@ class Client: if not reconnect: await self.close() if isinstance(exc, ConnectionClosed) and exc.code == 1000: - # clean close, don't re-raise this + # Clean close, don't re-raise this return raise @@ -539,12 +491,10 @@ class Client: continue # We should only get this when an unhandled close code happens, - # such as a clean disconnect (1000) or a bad state (bad token, no sharding, etc) - # sometimes, discord sends us 1000 for unknown reasons so we should reconnect + # such as a clean disconnect (1000) or a bad state (bad token, etc) + # Sometimes, discord sends us 1000 for unknown reasons so we should reconnect # regardless and rely on is_closed instead if isinstance(exc, ConnectionClosed): - if exc.code == 4014: - raise PrivilegedIntentsRequired(exc.shard_id) from None if exc.code != 1000: await self.close() raise @@ -553,8 +503,8 @@ class Client: _log.exception("Attempting a reconnect in %.2fs", retry) await asyncio.sleep(retry) # Always try to RESUME the connection - # If the connection is not RESUME-able then the gateway will invalidate the session. - # This is apparently what the official Discord client does. + # If the connection is not RESUME-able then the gateway will invalidate the session + # This is apparently what the official Discord client does ws_params.update(sequence=self.ws.sequence, resume=True, session=self.ws.session_id) async def close(self) -> None: @@ -571,7 +521,7 @@ class Client: try: await voice.disconnect(force=True) except Exception: - # if an error happens during disconnects, disregard it. + # If an error happens during disconnects, disregard it pass if self.ws is not None and self.ws.open: @@ -665,7 +615,7 @@ class Client: # I am unsure why this gets raised here but suppress it anyway return None - # properties + # Properties def is_closed(self) -> bool: """:class:`bool`: Indicates if the websocket connection is closed.""" @@ -686,7 +636,7 @@ class Client: # ConnectionState._activity is typehinted as ActivityPayload, we're passing Dict[str, Any] self._connection._activity = value.to_dict() # type: ignore else: - raise TypeError('activity must derive from BaseActivity.') + raise TypeError('activity must derive from BaseActivity') @property def status(self): @@ -706,7 +656,7 @@ class Client: elif isinstance(value, Status): self._connection._status = str(value) else: - raise TypeError('status must derive from Status.') + raise TypeError('status must derive from Status') @property def allowed_mentions(self) -> Optional[AllowedMentions]: @@ -723,15 +673,7 @@ class Client: else: raise TypeError(f'allowed_mentions must be AllowedMentions not {value.__class__!r}') - @property - def intents(self) -> Intents: - """:class:`~discord.Intents`: The intents configured for this connection. - - .. versionadded:: 1.5 - """ - return self._connection.intents - - # helpers/getters + # Helpers/Getters @property def users(self) -> List[User]: @@ -900,7 +842,7 @@ class Client: for guild in self.guilds: yield from guild.members - # listeners/waiters + # Listeners/Waiters async def wait_until_ready(self) -> None: """|coro| @@ -1013,7 +955,7 @@ class Client: listeners.append((future, check)) return asyncio.wait_for(future, timeout) - # event registration + # Event registration def event(self, coro: Coro) -> Coro: """A decorator that registers an event to listen to. @@ -1198,8 +1140,7 @@ class Client: .. note:: - Using this, you will **not** receive :attr:`.Guild.channels`, :attr:`.Guild.members`, - :attr:`.Member.activity` and :attr:`.Member.voice` per :class:`.Member`. + Using this, you will **not** receive :attr:`.Guild.channels` and :attr:`.Guild.members`. .. note:: @@ -1237,8 +1178,6 @@ class Client: Creates a :class:`.Guild`. - Bot accounts in more than 10 guilds are not allowed to create guilds. - Parameters ---------- name: :class:`str` @@ -1410,26 +1349,6 @@ class Client: return Widget(state=self._connection, data=data) - async def application_info(self) -> AppInfo: - """|coro| - - Retrieves the bot's application information. - - Raises - ------- - :exc:`.HTTPException` - Retrieving the information failed somehow. - - Returns - -------- - :class:`.AppInfo` - The bot's application information. - """ - data = await self.http.application_info() - if 'rpc_origins' not in data: - data['rpc_origins'] = None - return AppInfo(self._connection, data) - async def fetch_user(self, user_id: int, /) -> User: """|coro| @@ -1439,7 +1358,7 @@ class Client: .. note:: - This method is an API call. If you have :attr:`discord.Intents.members` and member cache enabled, consider :meth:`get_user` instead. + This method is an API call. If you have member cache enabled, consider :meth:`get_user` instead. Parameters ----------- @@ -1598,45 +1517,3 @@ class Client: data = await state.http.start_private_message(user.id) return state.add_dm_channel(data) - - def add_view(self, view: View, *, message_id: Optional[int] = None) -> None: - """Registers a :class:`~discord.ui.View` for persistent listening. - - This method should be used for when a view is comprised of components - that last longer than the lifecycle of the program. - - .. versionadded:: 2.0 - - Parameters - ------------ - view: :class:`discord.ui.View` - The view to register for dispatching. - message_id: Optional[:class:`int`] - The message ID that the view is attached to. This is currently used to - refresh the view's state during message update events. If not given - then message update events are not propagated for the view. - - Raises - ------- - TypeError - A view was not passed. - ValueError - The view is not persistent. A persistent view has no timeout - and all their components have an explicitly provided custom_id. - """ - - if not isinstance(view, View): - raise TypeError(f'expected an instance of View not {view.__class__!r}') - - if not view.is_persistent(): - raise ValueError('View is not persistent. Items need to have a custom_id set and View must have no timeout') - - self._connection.store_view(view, message_id) - - @property - def persistent_views(self) -> Sequence[View]: - """Sequence[:class:`.View`]: A sequence of persistent views added to the client. - - .. versionadded:: 2.0 - """ - return self._connection.persistent_views diff --git a/discord/errors.py b/discord/errors.py index bc2398d55..e344c9f8f 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -49,9 +49,6 @@ __all__ = ( 'InvalidData', 'InvalidArgument', 'LoginFailure', - 'ConnectionClosed', - 'PrivilegedIntentsRequired', - 'InteractionResponded', ) @@ -217,61 +214,12 @@ class ConnectionClosed(ClientException): The close code of the websocket. reason: :class:`str` The reason provided for the closure. - shard_id: Optional[:class:`int`] - The shard ID that got closed if applicable. """ - def __init__(self, socket: ClientWebSocketResponse, *, shard_id: Optional[int], code: Optional[int] = None): + def __init__(self, socket: ClientWebSocketResponse, *, code: Optional[int] = None): # This exception is just the same exception except # reconfigured to subclass ClientException for users self.code: int = code or socket.close_code or -1 # aiohttp doesn't seem to consistently provide close reason self.reason: str = '' - self.shard_id: Optional[int] = shard_id - super().__init__(f'Shard ID {self.shard_id} WebSocket closed with {self.code}') - - -class PrivilegedIntentsRequired(ClientException): - """Exception that's raised when the gateway is requesting privileged intents - but they're not ticked in the developer page yet. - - Go to https://discord.com/developers/applications/ and enable the intents - that are required. Currently these are as follows: - - - :attr:`Intents.members` - - :attr:`Intents.presences` - - Attributes - ----------- - shard_id: Optional[:class:`int`] - The shard ID that got closed if applicable. - """ - - def __init__(self, shard_id: Optional[int]): - self.shard_id: Optional[int] = shard_id - msg = ( - 'Shard ID %s is requesting privileged intents that have not been explicitly enabled in the ' - 'developer portal. It is recommended to go to https://discord.com/developers/applications/ ' - 'and explicitly enable the privileged intents within your application\'s page. If this is not ' - 'possible, then consider disabling the privileged intents instead.' - ) - super().__init__(msg % shard_id) - - -class InteractionResponded(ClientException): - """Exception that's raised when sending another interaction response using - :class:`InteractionResponse` when one has already been done before. - - An interaction can only respond once. - - .. versionadded:: 2.0 - - Attributes - ----------- - interaction: :class:`Interaction` - The interaction that's already been responded to. - """ - - def __init__(self, interaction: Interaction): - self.interaction: Interaction = interaction - super().__init__('This interaction has already been responded to before') + super().__init__(f'WebSocket closed with {self.code}') diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index b4da61001..0c14c5389 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -57,7 +57,6 @@ __all__ = ( 'when_mentioned', 'when_mentioned_or', 'Bot', - 'AutoShardedBot', ) MISSING: Any = discord.utils.MISSING @@ -137,7 +136,7 @@ class BotBase(GroupMixin): self.strip_after_prefix = options.get('strip_after_prefix', False) if self.owner_id and self.owner_ids: - raise TypeError('Both owner_id and owner_ids are set.') + raise TypeError('Both owner_id and owner_ids are set') if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection): raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}') @@ -315,24 +314,22 @@ class BotBase(GroupMixin): # type-checker doesn't distinguish between functions and methods return await discord.utils.async_all(f(ctx) for f in data) # type: ignore - async def is_owner(self, user: discord.User) -> bool: + def is_owner(self, user: discord.User) -> bool: """|coro| Checks if a :class:`~discord.User` or :class:`~discord.Member` is the owner of this bot. - If an :attr:`owner_id` is not set, it is fetched automatically - through the use of :meth:`~.Bot.application_info`. - - .. versionchanged:: 1.3 - The function also checks if the application is team-owned if - :attr:`owner_ids` is not set. - Parameters ----------- user: :class:`.abc.User` The user to check for. + Raises + ------- + AttributeError + Owners aren't set. + Returns -------- :class:`bool` @@ -344,14 +341,7 @@ class BotBase(GroupMixin): elif self.owner_ids: return user.id in self.owner_ids else: - - app = await self.application_info() # type: ignore - if app.team: - self.owner_ids = ids = {m.id for m in app.team.members} - return user.id in ids - else: - self.owner_id = owner_id = app.owner.id - return user.id == owner_id + raise AttributeError('Owners aren\'t set.') def before_invoke(self, coro: CFT) -> CFT: """A decorator that registers a coroutine as a pre-invoke hook. @@ -1086,8 +1076,7 @@ class Bot(BotBase, discord.Client): information on implementing a help command, see :ref:`ext_commands_help_command`. owner_id: Optional[:class:`int`] The user ID that owns the bot. If this is not set and is then queried via - :meth:`.is_owner` then it is fetched automatically using - :meth:`~.Bot.application_info`. + :meth:`.is_owner` then it will error. owner_ids: Optional[Collection[:class:`int`]] The user IDs that owns the bot. This is similar to :attr:`owner_id`. If this is not set and the application is team based, then it is @@ -1104,9 +1093,3 @@ class Bot(BotBase, discord.Client): .. versionadded:: 1.7 """ pass - -class AutoShardedBot(BotBase, discord.AutoShardedClient): - """This is similar to :class:`.Bot` except that it is inherited from - :class:`discord.AutoShardedClient` instead. - """ - pass diff --git a/discord/flags.py b/discord/flags.py index fb468c50b..391ec3b37 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -32,7 +32,6 @@ __all__ = ( 'SystemChannelFlags', 'MessageFlags', 'PublicUserFlags', - 'Intents', 'MemberCacheFlags', 'ApplicationFlags', ) @@ -415,458 +414,6 @@ class PublicUserFlags(BaseFlags): return [public_flag for public_flag in UserFlags if self._has_flag(public_flag.value)] -@fill_with_flags() -class Intents(BaseFlags): - r"""Wraps up a Discord gateway intent flag. - - Similar to :class:`Permissions`\, the properties provided are two way. - You can set and retrieve individual bits using the properties as if they - were regular bools. - - To construct an object you can pass keyword arguments denoting the flags - to enable or disable. - - This is used to disable certain gateway features that are unnecessary to - run your bot. To make use of this, it is passed to the ``intents`` keyword - argument of :class:`Client`. - - .. versionadded:: 1.5 - - .. container:: operations - - .. describe:: x == y - - Checks if two flags are equal. - .. describe:: x != y - - Checks if two flags are not equal. - .. describe:: hash(x) - - Return 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. - - Attributes - ----------- - value: :class:`int` - The raw value. You should query flags via the properties - rather than using this raw value. - """ - - __slots__ = () - - def __init__(self, **kwargs: bool): - self.value = self.DEFAULT_VALUE - for key, value in kwargs.items(): - if key not in self.VALID_FLAGS: - raise TypeError(f'{key!r} is not a valid flag name.') - setattr(self, key, value) - - @classmethod - def all(cls: Type[Intents]) -> Intents: - """A factory method that creates a :class:`Intents` with everything enabled.""" - bits = max(cls.VALID_FLAGS.values()).bit_length() - value = (1 << bits) - 1 - self = cls.__new__(cls) - self.value = value - return self - - @classmethod - def none(cls: Type[Intents]) -> Intents: - """A factory method that creates a :class:`Intents` with everything disabled.""" - self = cls.__new__(cls) - self.value = self.DEFAULT_VALUE - return self - - @classmethod - def default(cls: Type[Intents]) -> Intents: - """A factory method that creates a :class:`Intents` with everything enabled - except :attr:`presences` and :attr:`members`. - """ - self = cls.all() - self.presences = False - self.members = False - return self - - @flag_value - def guilds(self): - """:class:`bool`: Whether guild related events are enabled. - - This corresponds to the following events: - - - :func:`on_guild_join` - - :func:`on_guild_remove` - - :func:`on_guild_available` - - :func:`on_guild_unavailable` - - :func:`on_guild_channel_update` - - :func:`on_guild_channel_create` - - :func:`on_guild_channel_delete` - - :func:`on_guild_channel_pins_update` - - This also corresponds to the following attributes and classes in terms of cache: - - - :attr:`Client.guilds` - - :class:`Guild` and all its attributes. - - :meth:`Client.get_channel` - - :meth:`Client.get_all_channels` - - It is highly advisable to leave this intent enabled for your bot to function. - """ - return 1 << 0 - - @flag_value - def members(self): - """:class:`bool`: Whether guild member related events are enabled. - - This corresponds to the following events: - - - :func:`on_member_join` - - :func:`on_member_remove` - - :func:`on_member_update` - - :func:`on_user_update` - - This also corresponds to the following attributes and classes in terms of cache: - - - :meth:`Client.get_all_members` - - :meth:`Client.get_user` - - :meth:`Guild.chunk` - - :meth:`Guild.fetch_members` - - :meth:`Guild.get_member` - - :attr:`Guild.members` - - :attr:`Member.roles` - - :attr:`Member.nick` - - :attr:`Member.premium_since` - - :attr:`User.name` - - :attr:`User.avatar` - - :attr:`User.discriminator` - - For more information go to the :ref:`member intent documentation `. - - .. note:: - - Currently, this requires opting in explicitly via the developer portal as well. - Bots in over 100 guilds will need to apply to Discord for verification. - """ - return 1 << 1 - - @flag_value - def bans(self): - """:class:`bool`: Whether guild ban related events are enabled. - - This corresponds to the following events: - - - :func:`on_member_ban` - - :func:`on_member_unban` - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return 1 << 2 - - @flag_value - def emojis(self): - """:class:`bool`: Alias of :attr:`.emojis_and_stickers`. - - .. versionchanged:: 2.0 - Changed to an alias. - """ - return 1 << 3 - - @alias_flag_value - def emojis_and_stickers(self): - """:class:`bool`: Whether guild emoji and sticker related events are enabled. - - .. versionadded:: 2.0 - - This corresponds to the following events: - - - :func:`on_guild_emojis_update` - - :func:`on_guild_stickers_update` - - This also corresponds to the following attributes and classes in terms of cache: - - - :class:`Emoji` - - :class:`GuildSticker` - - :meth:`Client.get_emoji` - - :meth:`Client.get_sticker` - - :meth:`Client.emojis` - - :meth:`Client.stickers` - - :attr:`Guild.emojis` - - :attr:`Guild.stickers` - """ - return 1 << 3 - - @flag_value - def integrations(self): - """:class:`bool`: Whether guild integration related events are enabled. - - This corresponds to the following events: - - - :func:`on_guild_integrations_update` - - :func:`on_integration_create` - - :func:`on_integration_update` - - :func:`on_raw_integration_delete` - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return 1 << 4 - - @flag_value - def webhooks(self): - """:class:`bool`: Whether guild webhook related events are enabled. - - This corresponds to the following events: - - - :func:`on_webhooks_update` - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return 1 << 5 - - @flag_value - def invites(self): - """:class:`bool`: Whether guild invite related events are enabled. - - This corresponds to the following events: - - - :func:`on_invite_create` - - :func:`on_invite_delete` - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return 1 << 6 - - @flag_value - def voice_states(self): - """:class:`bool`: Whether guild voice state related events are enabled. - - This corresponds to the following events: - - - :func:`on_voice_state_update` - - This also corresponds to the following attributes and classes in terms of cache: - - - :attr:`VoiceChannel.members` - - :attr:`VoiceChannel.voice_states` - - :attr:`Member.voice` - - .. note:: - - This intent is required to connect to voice. - """ - return 1 << 7 - - @flag_value - def presences(self): - """:class:`bool`: Whether guild presence related events are enabled. - - This corresponds to the following events: - - - :func:`on_presence_update` - - This also corresponds to the following attributes and classes in terms of cache: - - - :attr:`Member.activities` - - :attr:`Member.status` - - :attr:`Member.raw_status` - - For more information go to the :ref:`presence intent documentation `. - - .. note:: - - Currently, this requires opting in explicitly via the developer portal as well. - Bots in over 100 guilds will need to apply to Discord for verification. - """ - return 1 << 8 - - @alias_flag_value - def messages(self): - """:class:`bool`: Whether guild and direct message related events are enabled. - - This is a shortcut to set or get both :attr:`guild_messages` and :attr:`dm_messages`. - - This corresponds to the following events: - - - :func:`on_message` (both guilds and DMs) - - :func:`on_message_edit` (both guilds and DMs) - - :func:`on_message_delete` (both guilds and DMs) - - :func:`on_raw_message_delete` (both guilds and DMs) - - :func:`on_raw_message_edit` (both guilds and DMs) - - This also corresponds to the following attributes and classes in terms of cache: - - - :class:`Message` - - :attr:`Client.cached_messages` - - Note that due to an implicit relationship this also corresponds to the following events: - - - :func:`on_reaction_add` (both guilds and DMs) - - :func:`on_reaction_remove` (both guilds and DMs) - - :func:`on_reaction_clear` (both guilds and DMs) - """ - return (1 << 9) | (1 << 12) - - @flag_value - def guild_messages(self): - """:class:`bool`: Whether guild message related events are enabled. - - See also :attr:`dm_messages` for DMs or :attr:`messages` for both. - - This corresponds to the following events: - - - :func:`on_message` (only for guilds) - - :func:`on_message_edit` (only for guilds) - - :func:`on_message_delete` (only for guilds) - - :func:`on_raw_message_delete` (only for guilds) - - :func:`on_raw_message_edit` (only for guilds) - - This also corresponds to the following attributes and classes in terms of cache: - - - :class:`Message` - - :attr:`Client.cached_messages` (only for guilds) - - Note that due to an implicit relationship this also corresponds to the following events: - - - :func:`on_reaction_add` (only for guilds) - - :func:`on_reaction_remove` (only for guilds) - - :func:`on_reaction_clear` (only for guilds) - """ - return 1 << 9 - - @flag_value - def dm_messages(self): - """:class:`bool`: Whether direct message related events are enabled. - - See also :attr:`guild_messages` for guilds or :attr:`messages` for both. - - This corresponds to the following events: - - - :func:`on_message` (only for DMs) - - :func:`on_message_edit` (only for DMs) - - :func:`on_message_delete` (only for DMs) - - :func:`on_raw_message_delete` (only for DMs) - - :func:`on_raw_message_edit` (only for DMs) - - This also corresponds to the following attributes and classes in terms of cache: - - - :class:`Message` - - :attr:`Client.cached_messages` (only for DMs) - - Note that due to an implicit relationship this also corresponds to the following events: - - - :func:`on_reaction_add` (only for DMs) - - :func:`on_reaction_remove` (only for DMs) - - :func:`on_reaction_clear` (only for DMs) - """ - return 1 << 12 - - @alias_flag_value - def reactions(self): - """:class:`bool`: Whether guild and direct message reaction related events are enabled. - - This is a shortcut to set or get both :attr:`guild_reactions` and :attr:`dm_reactions`. - - This corresponds to the following events: - - - :func:`on_reaction_add` (both guilds and DMs) - - :func:`on_reaction_remove` (both guilds and DMs) - - :func:`on_reaction_clear` (both guilds and DMs) - - :func:`on_raw_reaction_add` (both guilds and DMs) - - :func:`on_raw_reaction_remove` (both guilds and DMs) - - :func:`on_raw_reaction_clear` (both guilds and DMs) - - This also corresponds to the following attributes and classes in terms of cache: - - - :attr:`Message.reactions` (both guild and DM messages) - """ - return (1 << 10) | (1 << 13) - - @flag_value - def guild_reactions(self): - """:class:`bool`: Whether guild message reaction related events are enabled. - - See also :attr:`dm_reactions` for DMs or :attr:`reactions` for both. - - This corresponds to the following events: - - - :func:`on_reaction_add` (only for guilds) - - :func:`on_reaction_remove` (only for guilds) - - :func:`on_reaction_clear` (only for guilds) - - :func:`on_raw_reaction_add` (only for guilds) - - :func:`on_raw_reaction_remove` (only for guilds) - - :func:`on_raw_reaction_clear` (only for guilds) - - This also corresponds to the following attributes and classes in terms of cache: - - - :attr:`Message.reactions` (only for guild messages) - """ - return 1 << 10 - - @flag_value - def dm_reactions(self): - """:class:`bool`: Whether direct message reaction related events are enabled. - - See also :attr:`guild_reactions` for guilds or :attr:`reactions` for both. - - This corresponds to the following events: - - - :func:`on_reaction_add` (only for DMs) - - :func:`on_reaction_remove` (only for DMs) - - :func:`on_reaction_clear` (only for DMs) - - :func:`on_raw_reaction_add` (only for DMs) - - :func:`on_raw_reaction_remove` (only for DMs) - - :func:`on_raw_reaction_clear` (only for DMs) - - This also corresponds to the following attributes and classes in terms of cache: - - - :attr:`Message.reactions` (only for DM messages) - """ - return 1 << 13 - - @alias_flag_value - def typing(self): - """:class:`bool`: Whether guild and direct message typing related events are enabled. - - This is a shortcut to set or get both :attr:`guild_typing` and :attr:`dm_typing`. - - This corresponds to the following events: - - - :func:`on_typing` (both guilds and DMs) - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return (1 << 11) | (1 << 14) - - @flag_value - def guild_typing(self): - """:class:`bool`: Whether guild and direct message typing related events are enabled. - - See also :attr:`dm_typing` for DMs or :attr:`typing` for both. - - This corresponds to the following events: - - - :func:`on_typing` (only for guilds) - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return 1 << 11 - - @flag_value - def dm_typing(self): - """:class:`bool`: Whether guild and direct message typing related events are enabled. - - See also :attr:`guild_typing` for guilds or :attr:`typing` for both. - - This corresponds to the following events: - - - :func:`on_typing` (only for DMs) - - This does not correspond to any attributes or classes in the library in terms of cache. - """ - return 1 << 14 - - @fill_with_flags() class MemberCacheFlags(BaseFlags): """Controls the library's cache policy when it comes to members. @@ -875,11 +422,6 @@ class MemberCacheFlags(BaseFlags): Note that the bot's own member is always cached. This class is passed to the ``member_cache_flags`` parameter in :class:`Client`. - Due to a quirk in how Discord works, in order to ensure proper cleanup - of cache resources it is recommended to have :attr:`Intents.members` - enabled. Otherwise the library cannot know when a member leaves a guild and - is thus unable to cleanup after itself. - To construct an object you can pass keyword arguments denoting the flags to enable or disable. @@ -944,8 +486,6 @@ class MemberCacheFlags(BaseFlags): def voice(self): """:class:`bool`: Whether to cache members that are in voice. - This requires :attr:`Intents.voice_states`. - Members that leave voice are no longer cached. """ return 1 @@ -955,43 +495,10 @@ class MemberCacheFlags(BaseFlags): """:class:`bool`: Whether to cache members that joined the guild or are chunked as part of the initial log in flow. - This requires :attr:`Intents.members`. - Members that leave the guild are no longer cached. """ return 2 - @classmethod - def from_intents(cls: Type[MemberCacheFlags], intents: Intents) -> MemberCacheFlags: - """A factory method that creates a :class:`MemberCacheFlags` based on - the currently selected :class:`Intents`. - - Parameters - ------------ - intents: :class:`Intents` - The intents to select from. - - Returns - --------- - :class:`MemberCacheFlags` - The resulting member cache flags. - """ - - self = cls.none() - if intents.members: - self.joined = True - if intents.voice_states: - self.voice = True - - return self - - def _verify_intents(self, intents: Intents): - if self.voice and not intents.voice_states: - raise ValueError('MemberCacheFlags.voice requires Intents.voice_states') - - if self.joined and not intents.members: - raise ValueError('MemberCacheFlags.joined requires Intents.members') - @property def _voice_only(self): return self.value == 1 diff --git a/discord/gateway.py b/discord/gateway.py index aa0c6ba06..92128893c 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -52,8 +52,7 @@ __all__ = ( class ReconnectWebSocket(Exception): """Signals to safely reconnect the websocket.""" - def __init__(self, shard_id, *, resume=True): - self.shard_id = shard_id + def __init__(self, *, resume=True): self.resume = resume self.op = 'RESUME' if resume else 'IDENTIFY' @@ -71,7 +70,6 @@ class GatewayRatelimiter: self.window = 0.0 self.per = per self.lock = asyncio.Lock() - self.shard_id = None def is_ratelimited(self): current = time.time() @@ -101,7 +99,7 @@ class GatewayRatelimiter: async with self.lock: delta = self.get_delay() if delta: - _log.warning('WebSocket in shard ID %s is ratelimited, waiting %.2f seconds', self.shard_id, delta) + _log.warning('WebSocket is ratelimited, waiting %.2f seconds.', delta) await asyncio.sleep(delta) @@ -109,16 +107,14 @@ class KeepAliveHandler(threading.Thread): def __init__(self, *args, **kwargs): ws = kwargs.pop('ws', None) interval = kwargs.pop('interval', None) - shard_id = kwargs.pop('shard_id', None) threading.Thread.__init__(self, *args, **kwargs) self.ws = ws self._main_thread_id = ws.thread_id self.interval = interval self.daemon = True - self.shard_id = shard_id - self.msg = 'Keeping shard ID %s websocket alive with sequence %s.' - self.block_msg = 'Shard ID %s heartbeat blocked for more than %s seconds.' - self.behind_msg = 'Can\'t keep up, shard ID %s websocket is %.1fs behind.' + self.msg = 'Keeping websocket alive with sequence %s.' + self.block_msg = 'Heartbeat blocked for more than %s seconds.' + self.behind_msg = 'Can\'t keep up, websocket is %.1fs behind.' self._stop_ev = threading.Event() self._last_ack = time.perf_counter() self._last_send = time.perf_counter() @@ -129,7 +125,7 @@ class KeepAliveHandler(threading.Thread): def run(self): while not self._stop_ev.wait(self.interval): if self._last_recv + self.heartbeat_timeout < time.perf_counter(): - _log.warning("Shard ID %s has stopped responding to the gateway. Closing and restarting.", self.shard_id) + _log.warning('Gateway has stopped responding. Closing and restarting.') coro = self.ws.close(4000) f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) @@ -142,7 +138,7 @@ class KeepAliveHandler(threading.Thread): return data = self.get_payload() - _log.debug(self.msg, self.shard_id, data['d']) + _log.debug(self.msg, data['d']) coro = self.ws.send_heartbeat(data) f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) try: @@ -161,7 +157,7 @@ class KeepAliveHandler(threading.Thread): else: stack = ''.join(traceback.format_stack(frame)) msg = f'{self.block_msg}\nLoop thread traceback (most recent call last):\n{stack}' - _log.warning(msg, self.shard_id, total) + _log.warning(msg, total) except Exception: self.stop() @@ -185,15 +181,15 @@ class KeepAliveHandler(threading.Thread): self._last_ack = ack_time self.latency = ack_time - self._last_send if self.latency > 10: - _log.warning(self.behind_msg, self.shard_id, self.latency) + _log.warning(self.behind_msg, self.latency) class VoiceKeepAliveHandler(KeepAliveHandler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.recent_ack_latencies = deque(maxlen=20) - self.msg = 'Keeping shard ID %s voice websocket alive with timestamp %s.' - self.block_msg = 'Shard ID %s voice heartbeat blocked for more than %s seconds' - self.behind_msg = 'High socket latency, shard ID %s heartbeat is %.1fs behind' + self.msg = 'Keeping voice websocket alive with timestamp %s.' + self.block_msg = 'Voice heartbeat blocked for more than %s seconds.' + self.behind_msg = 'High socket latency, voice websocket is %.1fs behind.' def get_payload(self): return { @@ -300,7 +296,7 @@ class DiscordWebSocket: pass @classmethod - async def from_client(cls, client, *, initial=False, gateway=None, shard_id=None, session=None, sequence=None, resume=False): + async def from_client(cls, client, *, initial=False, gateway=None, session=None, sequence=None, resume=False): """Creates a main websocket for Discord from a :class:`Client`. This is for internal use only. @@ -317,9 +313,6 @@ class DiscordWebSocket: ws.gateway = gateway ws.call_hooks = client._connection.call_hooks ws._initial_identify = initial - ws.shard_id = shard_id - ws._rate_limiter.shard_id = shard_id - ws.shard_count = client._connection.shard_count ws.session_id = session ws.sequence = sequence ws._max_heartbeat_timeout = client._connection.heartbeat_timeout @@ -386,9 +379,6 @@ class DiscordWebSocket: } } - if self.shard_id is not None and self.shard_count is not None: - payload['d']['shard'] = [self.shard_id, self.shard_count] - state = self._connection if state._activity is not None or state._status is not None: payload['d']['presence'] = { @@ -398,12 +388,9 @@ class DiscordWebSocket: 'afk': False } - if state._intents is not None: - payload['d']['intents'] = state._intents.value - - await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify) + await self.call_hooks('before_identify', initial=self._initial_identify) await self.send_as_json(payload) - _log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id) + _log.info('Gateway has sent the IDENTIFY payload.') async def resume(self): """Sends the RESUME packet.""" @@ -417,7 +404,7 @@ class DiscordWebSocket: } await self.send_as_json(payload) - _log.info('Shard ID %s has sent the RESUME payload.', self.shard_id) + _log.info('Gateway has sent the RESUME payload.') async def received_message(self, msg, /): if type(msg) is bytes: @@ -432,7 +419,7 @@ class DiscordWebSocket: self.log_receive(msg) msg = utils._from_json(msg) - _log.debug('For Shard ID %s: WebSocket Event: %s', self.shard_id, msg) + _log.debug('WebSocket Event: %s.', msg) event = msg.get('t') if event: self._dispatch('socket_event_type', event) @@ -448,12 +435,12 @@ class DiscordWebSocket: if op != self.DISPATCH: if op == self.RECONNECT: - # "reconnect" can only be handled by the Client + # RECONNECT can only be handled by the Client # so we terminate our connection and raise an - # internal exception signalling to reconnect. + # internal exception signalling to reconnect _log.debug('Received RECONNECT opcode.') await self.close() - raise ReconnectWebSocket(self.shard_id) + raise ReconnectWebSocket if op == self.HEARTBEAT_ACK: if self._keep_alive: @@ -468,7 +455,7 @@ class DiscordWebSocket: if op == self.HELLO: interval = data['heartbeat_interval'] / 1000.0 - self._keep_alive = KeepAliveHandler(ws=self, interval=interval, shard_id=self.shard_id) + self._keep_alive = KeepAliveHandler(ws=self, interval=interval) # send a heartbeat immediately await self.send_as_json(self._keep_alive.get_payload()) self._keep_alive.start() @@ -477,13 +464,13 @@ class DiscordWebSocket: if op == self.INVALIDATE_SESSION: if data is True: await self.close() - raise ReconnectWebSocket(self.shard_id) + raise ReconnectWebSocket self.sequence = None self.session_id = None - _log.info('Shard ID %s session has been invalidated.', self.shard_id) + _log.info('Gateway session has been invalidated.') await self.close(code=1000) - raise ReconnectWebSocket(self.shard_id, resume=False) + raise ReconnectWebSocket(resume=False) _log.warning('Unknown OP code %s.', op) return @@ -492,17 +479,13 @@ class DiscordWebSocket: self._trace = trace = data.get('_trace', []) self.sequence = msg['s'] self.session_id = data['session_id'] - # pass back shard ID to ready handler - data['__shard_id__'] = self.shard_id - _log.info('Shard ID %s has connected to Gateway: %s (Session ID: %s).', - self.shard_id, ', '.join(trace), self.session_id) + _log.info('Connected to Gateway: %s (Session ID: %s).', + ', '.join(trace), self.session_id) elif event == 'RESUMED': self._trace = trace = data.get('_trace', []) - # pass back the shard ID to the resumed handler - data['__shard_id__'] = self.shard_id - _log.info('Shard ID %s has successfully RESUMED session %s under trace %s.', - self.shard_id, self.session_id, ', '.join(trace)) + _log.info('Gateway has successfully RESUMED session %s under trace %s.', + self.session_id, ', '.join(trace)) try: func = self._discord_parsers[event] @@ -574,15 +557,15 @@ class DiscordWebSocket: if isinstance(e, asyncio.TimeoutError): _log.info('Timed out receiving packet. Attempting a reconnect.') - raise ReconnectWebSocket(self.shard_id) from None + raise ReconnectWebSocket from None code = self._close_code or self.socket.close_code if self._can_handle_close(): _log.info('Websocket closed with %s, attempting a reconnect.', code) - raise ReconnectWebSocket(self.shard_id) from None + raise ReconnectWebSocket from None else: _log.info('Websocket closed with %s, cannot reconnect.', code) - raise ConnectionClosed(self.socket, shard_id=self.shard_id, code=code) from None + raise ConnectionClosed(self.socket, code=code) from None async def debug_send(self, data, /): await self._rate_limiter.block() @@ -598,7 +581,7 @@ class DiscordWebSocket: await self.send(utils._to_json(data)) except RuntimeError as exc: if not self._can_handle_close(): - raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc + raise ConnectionClosed(self.socket) from exc async def send_heartbeat(self, data): # This bypasses the rate limit handling code since it has a higher priority @@ -606,7 +589,7 @@ class DiscordWebSocket: await self.socket.send_str(utils._to_json(data)) except RuntimeError as exc: if not self._can_handle_close(): - raise ConnectionClosed(self.socket, shard_id=self.shard_id) from exc + raise ConnectionClosed(self.socket) from exc async def change_presence(self, *, activity=None, status=None, since=0.0): if activity is not None: @@ -898,10 +881,10 @@ class DiscordVoiceWebSocket: await self.received_message(utils._from_json(msg.data)) elif msg.type is aiohttp.WSMsgType.ERROR: _log.debug('Received %s', msg) - raise ConnectionClosed(self.ws, shard_id=None) from msg.data + raise ConnectionClosed(self.ws) from msg.data elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING): _log.debug('Received %s', msg) - raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) + raise ConnectionClosed(self.ws, code=self._close_code) async def close(self, code=1000): if self._keep_alive is not None: diff --git a/discord/guild.py b/discord/guild.py index 41545f773..b2132960f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -339,7 +339,6 @@ class Guild(Hashable): attrs = ( ('id', self.id), ('name', self.name), - ('shard_id', self.shard_id), ('chunked', self.chunked), ('member_count', getattr(self, '_member_count', None)), ) @@ -875,8 +874,7 @@ class Guild(Hashable): .. warning:: - Due to a Discord limitation, in order for this attribute to remain up-to-date and - accurate, it requires :attr:`Intents.members` to be specified. + Due to a Discord limitation, this may not always be up-to-date and accurate. """ return self._member_count @@ -896,14 +894,6 @@ class Guild(Hashable): return False return count == len(self._members) - @property - def shard_id(self) -> int: - """:class:`int`: Returns the shard ID for this guild if applicable.""" - count = self._state.shard_count - if count is None: - return 0 - return (self.id >> 22) % count - @property def created_at(self) -> datetime.datetime: """:class:`datetime.datetime`: Returns the guild's creation time in UTC.""" @@ -1631,60 +1621,6 @@ class Guild(Hashable): return threads - # TODO: Remove Optional typing here when async iterators are refactored - def fetch_members(self, *, limit: int = 1000, after: Optional[SnowflakeTime] = None) -> MemberIterator: - """Retrieves an :class:`.AsyncIterator` that enables receiving the guild's members. In order to use this, - :meth:`Intents.members` must be enabled. - - .. note:: - - This method is an API call. For general usage, consider :attr:`members` instead. - - .. versionadded:: 1.3 - - All parameters are optional. - - Parameters - ---------- - limit: Optional[:class:`int`] - The number of members to retrieve. Defaults to 1000. - Pass ``None`` to fetch all members. Note that this is potentially slow. - after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] - Retrieve members after this date or object. - If a datetime is provided, it is recommended to use a UTC aware datetime. - If the datetime is naive, it is assumed to be local time. - - Raises - ------ - ClientException - The members intent is not enabled. - HTTPException - Getting the members failed. - - Yields - ------ - :class:`.Member` - The member with the member data parsed. - - Examples - -------- - - Usage :: - - async for member in guild.fetch_members(limit=150): - print(member.name) - - Flattening into a list :: - - members = await guild.fetch_members(limit=150).flatten() - # members is now a list of Member... - """ - - if not self._state._intents.members: - raise ClientException('Intents.members must be enabled to use this.') - - return MemberIterator(self, limit=limit, after=after) - async def fetch_member(self, member_id: int, /) -> Member: """|coro| @@ -1692,7 +1628,7 @@ class Guild(Hashable): .. note:: - This method is an API call. If you have :attr:`Intents.members` and member cache enabled, consider :meth:`get_member` instead. + This method is an API call. If you have member cache, consider :meth:`get_member` instead. Parameters ----------- @@ -2822,7 +2758,7 @@ class Guild(Hashable): """|coro| Requests all members that belong to this guild. In order to use this, - :meth:`Intents.members` must be enabled. + you must have certain permissions. This is a websocket operation and can be slow. @@ -2836,12 +2772,9 @@ class Guild(Hashable): Raises ------- ClientException - The members intent is not enabled. + Insufficient permissions. """ - if not self._state._intents.members: - raise ClientException('Intents.members must be enabled to use this.') - if not self._state.is_guild_evicted(self): return await self._state.chunk_guild(self, cache=cache) @@ -2891,8 +2824,6 @@ class Guild(Hashable): The query timed out waiting for the members. ValueError Invalid parameters were passed to the function - ClientException - The presences intent is not enabled. Returns -------- @@ -2900,9 +2831,6 @@ class Guild(Hashable): The list of members that have matched the query. """ - if presences and not self._state._intents.presences: - raise ClientException('Intents.presences must be enabled to use this.') - if query is None: if query == '': raise ValueError('Cannot pass empty query string.') diff --git a/discord/shard.py b/discord/shard.py deleted file mode 100644 index edbdebf4f..000000000 --- a/discord/shard.py +++ /dev/null @@ -1,546 +0,0 @@ -""" -The MIT License (MIT) - -Copyright (c) 2015-present Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -from __future__ import annotations - -import asyncio -import logging - -import aiohttp - -from .state import AutoShardedConnectionState -from .client import Client -from .backoff import ExponentialBackoff -from .gateway import * -from .errors import ( - ClientException, - HTTPException, - GatewayNotFound, - ConnectionClosed, - PrivilegedIntentsRequired, -) - -from .enums import Status - -from typing import TYPE_CHECKING, Any, Callable, Tuple, Type, Optional, List, Dict, TypeVar - -if TYPE_CHECKING: - from .gateway import DiscordWebSocket - from .activity import BaseActivity - from .enums import Status - - EI = TypeVar('EI', bound='EventItem') - -__all__ = ( - 'AutoShardedClient', - 'ShardInfo', -) - -_log = logging.getLogger(__name__) - - -class EventType: - close = 0 - reconnect = 1 - resume = 2 - identify = 3 - terminate = 4 - clean_close = 5 - - -class EventItem: - __slots__ = ('type', 'shard', 'error') - - def __init__(self, etype: int, shard: Optional['Shard'], error: Optional[Exception]) -> None: - self.type: int = etype - self.shard: Optional['Shard'] = shard - self.error: Optional[Exception] = error - - def __lt__(self: EI, other: EI) -> bool: - if not isinstance(other, EventItem): - return NotImplemented - return self.type < other.type - - def __eq__(self: EI, other: EI) -> bool: - if not isinstance(other, EventItem): - return NotImplemented - return self.type == other.type - - def __hash__(self) -> int: - return hash(self.type) - - -class Shard: - def __init__(self, ws: DiscordWebSocket, client: AutoShardedClient, queue_put: Callable[[EventItem], None]) -> None: - self.ws: DiscordWebSocket = ws - self._client: Client = client - self._dispatch: Callable[..., None] = client.dispatch - self._queue_put: Callable[[EventItem], None] = queue_put - self.loop: asyncio.AbstractEventLoop = self._client.loop - self._disconnect: bool = False - self._reconnect = client._reconnect - self._backoff: ExponentialBackoff = ExponentialBackoff() - self._task: Optional[asyncio.Task] = None - self._handled_exceptions: Tuple[Type[Exception], ...] = ( - OSError, - HTTPException, - GatewayNotFound, - ConnectionClosed, - aiohttp.ClientError, - asyncio.TimeoutError, - ) - - @property - def id(self) -> int: - # DiscordWebSocket.shard_id is set in the from_client classmethod - return self.ws.shard_id # type: ignore - - def launch(self) -> None: - self._task = self.loop.create_task(self.worker()) - - def _cancel_task(self) -> None: - if self._task is not None and not self._task.done(): - self._task.cancel() - - async def close(self) -> None: - self._cancel_task() - await self.ws.close(code=1000) - - async def disconnect(self) -> None: - await self.close() - self._dispatch('shard_disconnect', self.id) - - async def _handle_disconnect(self, e: Exception) -> None: - self._dispatch('disconnect') - self._dispatch('shard_disconnect', self.id) - if not self._reconnect: - self._queue_put(EventItem(EventType.close, self, e)) - return - - if self._client.is_closed(): - return - - if isinstance(e, OSError) and e.errno in (54, 10054): - # If we get Connection reset by peer then always try to RESUME the connection. - exc = ReconnectWebSocket(self.id, resume=True) - self._queue_put(EventItem(EventType.resume, self, exc)) - return - - if isinstance(e, ConnectionClosed): - if e.code == 4014: - self._queue_put(EventItem(EventType.terminate, self, PrivilegedIntentsRequired(self.id))) - return - if e.code != 1000: - self._queue_put(EventItem(EventType.close, self, e)) - return - - retry = self._backoff.delay() - _log.error('Attempting a reconnect for shard ID %s in %.2fs', self.id, retry, exc_info=e) - await asyncio.sleep(retry) - self._queue_put(EventItem(EventType.reconnect, self, e)) - - async def worker(self) -> None: - while not self._client.is_closed(): - try: - await self.ws.poll_event() - except ReconnectWebSocket as e: - etype = EventType.resume if e.resume else EventType.identify - self._queue_put(EventItem(etype, self, e)) - break - except self._handled_exceptions as e: - await self._handle_disconnect(e) - break - except asyncio.CancelledError: - break - except Exception as e: - self._queue_put(EventItem(EventType.terminate, self, e)) - break - - async def reidentify(self, exc: ReconnectWebSocket) -> None: - self._cancel_task() - self._dispatch('disconnect') - self._dispatch('shard_disconnect', self.id) - _log.info('Got a request to %s the websocket at Shard ID %s.', exc.op, self.id) - try: - coro = DiscordWebSocket.from_client( - self._client, - resume=exc.resume, - shard_id=self.id, - session=self.ws.session_id, - sequence=self.ws.sequence, - ) - self.ws = await asyncio.wait_for(coro, timeout=60.0) - except self._handled_exceptions as e: - await self._handle_disconnect(e) - except asyncio.CancelledError: - return - except Exception as e: - self._queue_put(EventItem(EventType.terminate, self, e)) - else: - self.launch() - - async def reconnect(self) -> None: - self._cancel_task() - try: - coro = DiscordWebSocket.from_client(self._client, shard_id=self.id) - self.ws = await asyncio.wait_for(coro, timeout=60.0) - except self._handled_exceptions as e: - await self._handle_disconnect(e) - except asyncio.CancelledError: - return - except Exception as e: - self._queue_put(EventItem(EventType.terminate, self, e)) - else: - self.launch() - - -class ShardInfo: - """A class that gives information and control over a specific shard. - - You can retrieve this object via :meth:`AutoShardedClient.get_shard` - or :attr:`AutoShardedClient.shards`. - - .. versionadded:: 1.4 - - Attributes - ------------ - id: :class:`int` - The shard ID for this shard. - shard_count: Optional[:class:`int`] - The shard count for this cluster. If this is ``None`` then the bot has not started yet. - """ - - __slots__ = ('_parent', 'id', 'shard_count') - - def __init__(self, parent: Shard, shard_count: Optional[int]) -> None: - self._parent: Shard = parent - self.id: int = parent.id - self.shard_count: Optional[int] = shard_count - - def is_closed(self) -> bool: - """:class:`bool`: Whether the shard connection is currently closed.""" - return not self._parent.ws.open - - async def disconnect(self) -> None: - """|coro| - - Disconnects a shard. When this is called, the shard connection will no - longer be open. - - If the shard is already disconnected this does nothing. - """ - if self.is_closed(): - return - - await self._parent.disconnect() - - async def reconnect(self) -> None: - """|coro| - - Disconnects and then connects the shard again. - """ - if not self.is_closed(): - await self._parent.disconnect() - await self._parent.reconnect() - - async def connect(self) -> None: - """|coro| - - Connects a shard. If the shard is already connected this does nothing. - """ - if not self.is_closed(): - return - - await self._parent.reconnect() - - @property - def latency(self) -> float: - """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds for this shard.""" - return self._parent.ws.latency - - def is_ws_ratelimited(self) -> bool: - """:class:`bool`: Whether the websocket is currently rate limited. - - This can be useful to know when deciding whether you should query members - using HTTP or via the gateway. - - .. versionadded:: 1.6 - """ - return self._parent.ws.is_ratelimited() - - -class AutoShardedClient(Client): - """A client similar to :class:`Client` except it handles the complications - of sharding for the user into a more manageable and transparent single - process bot. - - When using this client, you will be able to use it as-if it was a regular - :class:`Client` with a single shard when implementation wise internally it - is split up into multiple shards. This allows you to not have to deal with - IPC or other complicated infrastructure. - - It is recommended to use this client only if you have surpassed at least - 1000 guilds. - - If no :attr:`.shard_count` is provided, then the library will use the - Bot Gateway endpoint call to figure out how many shards to use. - - If a ``shard_ids`` parameter is given, then those shard IDs will be used - to launch the internal shards. Note that :attr:`.shard_count` must be provided - if this is used. By default, when omitted, the client will launch shards from - 0 to ``shard_count - 1``. - - Attributes - ------------ - shard_ids: Optional[List[:class:`int`]] - An optional list of shard_ids to launch the shards with. - """ - - if TYPE_CHECKING: - _connection: AutoShardedConnectionState - - def __init__(self, *args: Any, loop: Optional[asyncio.AbstractEventLoop] = None, **kwargs: Any) -> None: - kwargs.pop('shard_id', None) - self.shard_ids: Optional[List[int]] = kwargs.pop('shard_ids', None) - super().__init__(*args, loop=loop, **kwargs) - - if self.shard_ids is not None: - if self.shard_count is None: - raise ClientException('When passing manual shard_ids, you must provide a shard_count.') - elif not isinstance(self.shard_ids, (list, tuple)): - raise ClientException('shard_ids parameter must be a list or a tuple.') - - # instead of a single websocket, we have multiple - # the key is the shard_id - self.__shards = {} - self._connection._get_websocket = self._get_websocket - self._connection._get_client = lambda: self - self.__queue = asyncio.PriorityQueue() - - def _get_websocket(self, guild_id: Optional[int] = None, *, shard_id: Optional[int] = None) -> DiscordWebSocket: - if shard_id is None: - # guild_id won't be None if shard_id is None and shard_count won't be None here - shard_id = (guild_id >> 22) % self.shard_count # type: ignore - return self.__shards[shard_id].ws - - def _get_state(self, **options: Any) -> AutoShardedConnectionState: - return AutoShardedConnectionState( - dispatch=self.dispatch, - handlers=self._handlers, - hooks=self._hooks, - http=self.http, - loop=self.loop, - **options, - ) - - @property - def latency(self) -> float: - """:class:`float`: Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. - - This operates similarly to :meth:`Client.latency` except it uses the average - latency of every shard's latency. To get a list of shard latency, check the - :attr:`latencies` property. Returns ``nan`` if there are no shards ready. - """ - if not self.__shards: - return float('nan') - return sum(latency for _, latency in self.latencies) / len(self.__shards) - - @property - def latencies(self) -> List[Tuple[int, float]]: - """List[Tuple[:class:`int`, :class:`float`]]: A list of latencies between a HEARTBEAT and a HEARTBEAT_ACK in seconds. - - This returns a list of tuples with elements ``(shard_id, latency)``. - """ - return [(shard_id, shard.ws.latency) for shard_id, shard in self.__shards.items()] - - def get_shard(self, shard_id: int) -> Optional[ShardInfo]: - """Optional[:class:`ShardInfo`]: Gets the shard information at a given shard ID or ``None`` if not found.""" - try: - parent = self.__shards[shard_id] - except KeyError: - return None - else: - return ShardInfo(parent, self.shard_count) - - @property - def shards(self) -> Dict[int, ShardInfo]: - """Mapping[int, :class:`ShardInfo`]: Returns a mapping of shard IDs to their respective info object.""" - return {shard_id: ShardInfo(parent, self.shard_count) for shard_id, parent in self.__shards.items()} - - async def launch_shard(self, gateway: str, shard_id: int, *, initial: bool = False) -> None: - try: - coro = DiscordWebSocket.from_client(self, initial=initial, gateway=gateway, shard_id=shard_id) - ws = await asyncio.wait_for(coro, timeout=180.0) - except Exception: - _log.exception('Failed to connect for shard_id: %s. Retrying...', shard_id) - await asyncio.sleep(5.0) - return await self.launch_shard(gateway, shard_id) - - # keep reading the shard while others connect - self.__shards[shard_id] = ret = Shard(ws, self, self.__queue.put_nowait) - ret.launch() - - async def launch_shards(self) -> None: - if self.shard_count is None: - self.shard_count, gateway = await self.http.get_bot_gateway() - else: - gateway = await self.http.get_gateway() - - self._connection.shard_count = self.shard_count - - shard_ids = self.shard_ids or range(self.shard_count) - self._connection.shard_ids = shard_ids - - for shard_id in shard_ids: - initial = shard_id == shard_ids[0] - await self.launch_shard(gateway, shard_id, initial=initial) - - self._connection.shards_launched.set() - - async def connect(self, *, reconnect: bool = True) -> None: - self._reconnect = reconnect - await self.launch_shards() - - while not self.is_closed(): - item = await self.__queue.get() - if item.type == EventType.close: - await self.close() - if isinstance(item.error, ConnectionClosed): - if item.error.code != 1000: - raise item.error - if item.error.code == 4014: - raise PrivilegedIntentsRequired(item.shard.id) from None - return - elif item.type in (EventType.identify, EventType.resume): - await item.shard.reidentify(item.error) - elif item.type == EventType.reconnect: - await item.shard.reconnect() - elif item.type == EventType.terminate: - await self.close() - raise item.error - elif item.type == EventType.clean_close: - return - - async def close(self) -> None: - """|coro| - - Closes the connection to Discord. - """ - if self.is_closed(): - return - - self._closed = True - - for vc in self.voice_clients: - try: - await vc.disconnect(force=True) - except Exception: - pass - - to_close = [asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values()] - if to_close: - await asyncio.wait(to_close) - - await self.http.close() - self.__queue.put_nowait(EventItem(EventType.clean_close, None, None)) - - async def change_presence( - self, - *, - activity: Optional[BaseActivity] = None, - status: Optional[Status] = None, - shard_id: int = None, - ) -> None: - """|coro| - - Changes the client's presence. - - Example: :: - - game = discord.Game("with the API") - await client.change_presence(status=discord.Status.idle, activity=game) - - .. versionchanged:: 2.0 - Removed the ``afk`` keyword-only parameter. - - Parameters - ---------- - activity: Optional[:class:`BaseActivity`] - The activity being done. ``None`` if no currently active activity is done. - status: Optional[:class:`Status`] - Indicates what status to change to. If ``None``, then - :attr:`Status.online` is used. - shard_id: Optional[:class:`int`] - The shard_id to change the presence to. If not specified - or ``None``, then it will change the presence of every - shard the bot can see. - - Raises - ------ - InvalidArgument - If the ``activity`` parameter is not of proper type. - """ - - if status is None: - status_value = 'online' - status_enum = Status.online - elif status is Status.offline: - status_value = 'invisible' - status_enum = Status.offline - else: - status_enum = status - status_value = str(status) - - if shard_id is None: - for shard in self.__shards.values(): - await shard.ws.change_presence(activity=activity, status=status_value) - - guilds = self._connection.guilds - else: - shard = self.__shards[shard_id] - await shard.ws.change_presence(activity=activity, status=status_value) - guilds = [g for g in self._connection.guilds if g.shard_id == shard_id] - - activities = () if activity is None else (activity,) - for guild in guilds: - me = guild.me - if me is None: - continue - - # Member.activities is typehinted as Tuple[ActivityType, ...], we may be setting it as Tuple[BaseActivity, ...] - me.activities = activities # type: ignore - me.status = status_enum - - def is_ws_ratelimited(self) -> bool: - """:class:`bool`: Whether the websocket is currently rate limited. - - This can be useful to know when deciding whether you should query members - using HTTP or via the gateway. - - This implementation checks if any of the shards are rate limited. - For more granular control, consider :meth:`ShardInfo.is_ws_ratelimited`. - - .. versionadded:: 1.6 - """ - return any(shard.ws.is_ratelimited() for shard in self.__shards.values()) diff --git a/discord/state.py b/discord/state.py index 2534e7aac..89198213f 100644 --- a/discord/state.py +++ b/discord/state.py @@ -49,7 +49,7 @@ from .member import Member from .role import Role from .enums import ChannelType, try_enum, Status from . import utils -from .flags import ApplicationFlags, Intents, MemberCacheFlags +from .flags import MemberCacheFlags from .object import Object from .invite import Invite from .integrations import _integration_factory @@ -164,9 +164,7 @@ class ConnectionState: self.dispatch: Callable = dispatch self.handlers: Dict[str, Callable] = handlers self.hooks: Dict[str, Callable] = hooks - self.shard_count: Optional[int] = None self._ready_task: Optional[asyncio.Task] = None - self.application_id: Optional[int] = utils._get_as_snowflake(options, 'application_id') self.heartbeat_timeout: float = options.get('heartbeat_timeout', 60.0) self.guild_ready_timeout: float = options.get('guild_ready_timeout', 2.0) if self.guild_ready_timeout < 0: @@ -194,37 +192,20 @@ class ConnectionState: else: status = str(status) - intents = options.get('intents', None) - if intents is not None: - if not isinstance(intents, Intents): - raise TypeError(f'intents parameter must be Intent not {type(intents)!r}') - else: - intents = Intents.default() - - if not intents.guilds: - _log.warning('Guilds intent seems to be disabled. This may cause state related issues.') - - self._chunk_guilds: bool = options.get('chunk_guilds_at_startup', intents.members) - - # Ensure these two are set properly - if not intents.members and self._chunk_guilds: - raise ValueError('Intents.members must be enabled to chunk guilds at startup.') + self._chunk_guilds: bool = options.get('chunk_guilds_at_startup', True) cache_flags = options.get('member_cache_flags', None) if cache_flags is None: - cache_flags = MemberCacheFlags.from_intents(intents) + cache_flags = MemberCacheFlags.all() else: if not isinstance(cache_flags, MemberCacheFlags): raise TypeError(f'member_cache_flags parameter must be MemberCacheFlags not {type(cache_flags)!r}') - cache_flags._verify_intents(intents) - self.member_cache_flags: MemberCacheFlags = cache_flags self._activity: Optional[ActivityPayload] = activity self._status: Optional[str] = status - self._intents: Intents = intents - if not intents.members or cache_flags._empty: + if cache_flags._empty: self.store_user = self.create_user # type: ignore self.deref_user = self.deref_user_no_intents # type: ignore @@ -300,12 +281,6 @@ class ConnectionState: u = self.user return u.id if u else None - @property - def intents(self) -> Intents: - ret = Intents.none() - ret.value = self._intents.value - return ret - @property def voice_clients(self) -> List[VoiceProtocol]: return list(self._voice_clients.values()) @@ -460,7 +435,7 @@ class ConnectionState: def _guild_needs_chunking(self, guild: Guild) -> bool: # If presences are enabled then we get back the old guild.large behaviour - return self._chunk_guilds and not guild.chunked and not (self._intents.presences and not guild.large) + return self._chunk_guilds and not guild.chunked and not (True and not guild.large) def _get_guild_channel(self, data: MessagePayload) -> Tuple[Union[Channel, Thread], Optional[Guild]]: channel_id = int(data['channel_id']) @@ -523,7 +498,7 @@ class ConnectionState: try: await asyncio.wait_for(future, timeout=5.0) except asyncio.TimeoutError: - _log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id) + _log.warning('Timed out waiting for chunks for guild_id %s.', guild.id) if guild.unavailable is False: self.dispatch('guild_available', guild) @@ -1395,136 +1370,3 @@ class ConnectionState: self, *, channel: Union[TextChannel, Thread, DMChannel, GroupChannel, PartialMessageable], data: MessagePayload ) -> Message: return Message(state=self, channel=channel, data=data) - - -class AutoShardedConnectionState(ConnectionState): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.shard_ids: Union[List[int], range] = [] - self.shards_launched: asyncio.Event = asyncio.Event() - - def _update_message_references(self) -> None: - # self._messages won't be None when this is called - for msg in self._messages: # type: ignore - if not msg.guild: - continue - - new_guild = self._get_guild(msg.guild.id) - if new_guild is not None and new_guild is not msg.guild: - channel_id = msg.channel.id - channel = new_guild._resolve_channel(channel_id) or Object(id=channel_id) - # channel will either be a TextChannel, Thread or Object - msg._rebind_cached_references(new_guild, channel) # type: ignore - - async def chunker( - self, - guild_id: int, - query: str = '', - limit: int = 0, - presences: bool = False, - *, - shard_id: Optional[int] = None, - nonce: Optional[str] = None, - ) -> None: - ws = self._get_websocket(guild_id, shard_id=shard_id) - await ws.request_chunks(guild_id, query=query, limit=limit, presences=presences, nonce=nonce) - - async def _delay_ready(self) -> None: - await self.shards_launched.wait() - processed = [] - max_concurrency = len(self.shard_ids) * 2 - current_bucket = [] - while True: - # this snippet of code is basically waiting N seconds - # until the last GUILD_CREATE was sent - try: - guild = await asyncio.wait_for(self._ready_state.get(), timeout=self.guild_ready_timeout) - except asyncio.TimeoutError: - break - else: - if self._guild_needs_chunking(guild): - _log.debug('Guild ID %d requires chunking, will be done in the background.', guild.id) - if len(current_bucket) >= max_concurrency: - try: - await utils.sane_wait_for(current_bucket, timeout=max_concurrency * 70.0) - except asyncio.TimeoutError: - fmt = 'Shard ID %s failed to wait for chunks from a sub-bucket with length %d' - _log.warning(fmt, guild.shard_id, len(current_bucket)) - finally: - current_bucket = [] - - # Chunk the guild in the background while we wait for GUILD_CREATE streaming - future = asyncio.ensure_future(self.chunk_guild(guild)) - current_bucket.append(future) - else: - future = self.loop.create_future() - future.set_result([]) - - processed.append((guild, future)) - - guilds = sorted(processed, key=lambda g: g[0].shard_id) - for shard_id, info in itertools.groupby(guilds, key=lambda g: g[0].shard_id): - children, futures = zip(*info) - # 110 reqs/minute w/ 1 req/guild plus some buffer - timeout = 61 * (len(children) / 110) - try: - await utils.sane_wait_for(futures, timeout=timeout) - except asyncio.TimeoutError: - _log.warning( - 'Shard ID %s failed to wait for chunks (timeout=%.2f) for %d guilds', shard_id, timeout, len(guilds) - ) - for guild in children: - if guild.unavailable is False: - self.dispatch('guild_available', guild) - else: - self.dispatch('guild_join', guild) - - self.dispatch('shard_ready', shard_id) - - # remove the state - try: - del self._ready_state - except AttributeError: - pass # already been deleted somehow - - # regular users cannot shard so we won't worry about it here. - - # clear the current task - self._ready_task = None - - # dispatch the event - self.call_handlers('ready') - self.dispatch('ready') - - def parse_ready(self, data) -> None: - if not hasattr(self, '_ready_state'): - self._ready_state = asyncio.Queue() - - self.user = user = ClientUser(state=self, data=data['user']) - # self._users is a list of Users, we're setting a ClientUser - self._users[user.id] = user # type: ignore - - if self.application_id is None: - try: - application = data['application'] - except KeyError: - pass - else: - self.application_id = utils._get_as_snowflake(application, 'id') - self.application_flags = ApplicationFlags._from_value(application['flags']) - - for guild_data in data['guilds']: - self._add_guild_from_data(guild_data) - - if self._messages: - self._update_message_references() - - self.dispatch('connect') - self.dispatch('shard_connect', data['__shard_id__']) - - if self._ready_task is None: - self._ready_task = asyncio.create_task(self._delay_ready()) - - def parse_resumed(self, data) -> None: - self.dispatch('resumed') - self.dispatch('shard_resumed', data['__shard_id__']) diff --git a/discord/template.py b/discord/template.py index 30af3a4d9..455073937 100644 --- a/discord/template.py +++ b/discord/template.py @@ -52,10 +52,6 @@ class _PartialTemplateState: self.__state = state self.http = _FriendlyHttpAttributeErrorHelper() - @property - def shard_count(self): - return self.__state.shard_count - @property def user(self): return self.__state.user