mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-16 06:03:11 +00:00
Due to a quirk in InteractionResponse.send_message not returning a message, all messages sent with an associated View would end up having no message_id set. When multiple instances of a View are responded to in a slash command context, this meant that the newest one would override the storage of the older one. Ultimately leading to the first view instance causing interaction failures. Since fetching the original message is an unacceptable solution to the problem due to incurred requests, the next best thing is to store an intermediate interaction_id as a stop gap to differentiate between the multiple instances. This change however, came with its own set of complications. Due to the interaction_id being an intermediate stop gap, the underlying storage of the view store had to be changed to accommodate the different way of accessing the data. Mainly, the interaction_id key had to be quick to swap and remove to another key. This solution attempts to change the interaction_id interim key with a full fledged message_id key when it receives one if it's possible. Note that the only way to obtain the interaction_id back from the component interaction is to retrieve it from the MessageInteraction data structure. This is because using the interaction_id of the button press would be a different interaction ID than the one set as an interim key. As a consequence, this stop gap only works for application command based interactions. I am not aware of this bug manifesting in component based interactions. This patch also fixes a bug with ViewStore.remove_view not working due to a bug being suppressed by a type: ignore comment. It also removes the older __verify_integrity helper method since clean-up is already done through View.stop() or View timeout. Hopefully in the near future, the `/callback` endpoint will return actual message data and this stop gap fix will no longer be necessary.
1673 lines
68 KiB
Python
1673 lines
68 KiB
Python
"""
|
|
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
|
|
from collections import deque, OrderedDict
|
|
import copy
|
|
import logging
|
|
from typing import (
|
|
Dict,
|
|
Optional,
|
|
TYPE_CHECKING,
|
|
Union,
|
|
Callable,
|
|
Any,
|
|
List,
|
|
TypeVar,
|
|
Coroutine,
|
|
Sequence,
|
|
Tuple,
|
|
Deque,
|
|
Literal,
|
|
overload,
|
|
)
|
|
import weakref
|
|
import inspect
|
|
|
|
import os
|
|
|
|
from .guild import Guild
|
|
from .activity import BaseActivity
|
|
from .user import User, ClientUser
|
|
from .emoji import Emoji
|
|
from .mentions import AllowedMentions
|
|
from .partial_emoji import PartialEmoji
|
|
from .message import Message
|
|
from .channel import *
|
|
from .channel import _channel_factory
|
|
from .raw_models import *
|
|
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 .object import Object
|
|
from .invite import Invite
|
|
from .integrations import _integration_factory
|
|
from .interactions import Interaction
|
|
from .ui.view import ViewStore, View
|
|
from .scheduled_event import ScheduledEvent
|
|
from .stage_instance import StageInstance
|
|
from .threads import Thread, ThreadMember
|
|
from .sticker import GuildSticker
|
|
|
|
if TYPE_CHECKING:
|
|
from .abc import PrivateChannel
|
|
from .message import MessageableChannel
|
|
from .guild import GuildChannel, VocalGuildChannel
|
|
from .http import HTTPClient
|
|
from .voice_client import VoiceProtocol
|
|
from .client import Client
|
|
from .gateway import DiscordWebSocket
|
|
from .app_commands import CommandTree
|
|
|
|
from .types.snowflake import Snowflake
|
|
from .types.activity import Activity as ActivityPayload
|
|
from .types.channel import DMChannel as DMChannelPayload
|
|
from .types.user import User as UserPayload, PartialUser as PartialUserPayload
|
|
from .types.emoji import Emoji as EmojiPayload, PartialEmoji as PartialEmojiPayload
|
|
from .types.sticker import GuildSticker as GuildStickerPayload
|
|
from .types.guild import Guild as GuildPayload
|
|
from .types.message import Message as MessagePayload, PartialMessage as PartialMessagePayload
|
|
from .types import gateway as gw
|
|
|
|
T = TypeVar('T')
|
|
Channel = Union[GuildChannel, VocalGuildChannel, PrivateChannel, PartialMessageable]
|
|
|
|
|
|
class ChunkRequest:
|
|
def __init__(
|
|
self,
|
|
guild_id: int,
|
|
loop: asyncio.AbstractEventLoop,
|
|
resolver: Callable[[int], Any],
|
|
*,
|
|
cache: bool = True,
|
|
) -> None:
|
|
self.guild_id: int = guild_id
|
|
self.resolver: Callable[[int], Any] = resolver
|
|
self.loop: asyncio.AbstractEventLoop = loop
|
|
self.cache: bool = cache
|
|
self.nonce: str = os.urandom(16).hex()
|
|
self.buffer: List[Member] = []
|
|
self.waiters: List[asyncio.Future[List[Member]]] = []
|
|
|
|
def add_members(self, members: List[Member]) -> None:
|
|
self.buffer.extend(members)
|
|
if self.cache:
|
|
guild = self.resolver(self.guild_id)
|
|
if guild is None:
|
|
return
|
|
|
|
for member in members:
|
|
existing = guild.get_member(member.id)
|
|
if existing is None or existing.joined_at is None:
|
|
guild._add_member(member)
|
|
|
|
async def wait(self) -> List[Member]:
|
|
future = self.loop.create_future()
|
|
self.waiters.append(future)
|
|
try:
|
|
return await future
|
|
finally:
|
|
self.waiters.remove(future)
|
|
|
|
def get_future(self) -> asyncio.Future[List[Member]]:
|
|
future = self.loop.create_future()
|
|
self.waiters.append(future)
|
|
return future
|
|
|
|
def done(self) -> None:
|
|
for future in self.waiters:
|
|
if not future.done():
|
|
future.set_result(self.buffer)
|
|
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
async def logging_coroutine(coroutine: Coroutine[Any, Any, T], *, info: str) -> Optional[T]:
|
|
try:
|
|
await coroutine
|
|
except Exception:
|
|
_log.exception('Exception occurred during %s', info)
|
|
|
|
|
|
class ConnectionState:
|
|
if TYPE_CHECKING:
|
|
_get_websocket: Callable[..., DiscordWebSocket]
|
|
_get_client: Callable[..., Client]
|
|
_parsers: Dict[str, Callable[[Dict[str, Any]], None]]
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
dispatch: Callable[..., Any],
|
|
handlers: Dict[str, Callable[..., Any]],
|
|
hooks: Dict[str, Callable[..., Coroutine[Any, Any, Any]]],
|
|
http: HTTPClient,
|
|
**options: Any,
|
|
) -> None:
|
|
# Set later, after Client.login
|
|
self.loop: asyncio.AbstractEventLoop = utils.MISSING
|
|
self.http: HTTPClient = http
|
|
self.max_messages: Optional[int] = options.get('max_messages', 1000)
|
|
if self.max_messages is not None and self.max_messages <= 0:
|
|
self.max_messages = 1000
|
|
|
|
self.dispatch: Callable[..., Any] = dispatch
|
|
self.handlers: Dict[str, Callable[..., Any]] = handlers
|
|
self.hooks: Dict[str, Callable[..., Coroutine[Any, Any, Any]]] = 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:
|
|
raise ValueError('guild_ready_timeout cannot be negative')
|
|
|
|
allowed_mentions = options.get('allowed_mentions')
|
|
|
|
if allowed_mentions is not None and not isinstance(allowed_mentions, AllowedMentions):
|
|
raise TypeError('allowed_mentions parameter must be AllowedMentions')
|
|
|
|
self.allowed_mentions: Optional[AllowedMentions] = allowed_mentions
|
|
self._chunk_requests: Dict[Union[int, str], ChunkRequest] = {}
|
|
|
|
activity = options.get('activity', None)
|
|
if activity:
|
|
if not isinstance(activity, BaseActivity):
|
|
raise TypeError('activity parameter must derive from BaseActivity.')
|
|
|
|
activity = activity.to_dict()
|
|
|
|
status = options.get('status', None)
|
|
if status:
|
|
if status is Status.offline:
|
|
status = 'invisible'
|
|
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.')
|
|
|
|
cache_flags = options.get('member_cache_flags', None)
|
|
if cache_flags is None:
|
|
cache_flags = MemberCacheFlags.from_intents(intents)
|
|
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
|
|
self._command_tree: Optional[CommandTree] = None
|
|
|
|
if not intents.members or cache_flags._empty:
|
|
self.store_user = self.store_user_no_intents # type: ignore # This reassignment is on purpose
|
|
|
|
self.parsers: Dict[str, Callable[[Any], None]]
|
|
self.parsers = parsers = {}
|
|
for attr, func in inspect.getmembers(self):
|
|
if attr.startswith('parse_'):
|
|
parsers[attr[6:].upper()] = func
|
|
|
|
self.clear()
|
|
|
|
def clear(self, *, views: bool = True) -> None:
|
|
self.user: Optional[ClientUser] = None
|
|
self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary()
|
|
self._emojis: Dict[int, Emoji] = {}
|
|
self._stickers: Dict[int, GuildSticker] = {}
|
|
self._guilds: Dict[int, Guild] = {}
|
|
if views:
|
|
self._view_store: ViewStore = ViewStore(self)
|
|
|
|
self._voice_clients: Dict[int, VoiceProtocol] = {}
|
|
|
|
# LRU of max size 128
|
|
self._private_channels: OrderedDict[int, PrivateChannel] = OrderedDict()
|
|
# extra dict to look up private channels by user id
|
|
self._private_channels_by_user: Dict[int, DMChannel] = {}
|
|
if self.max_messages is not None:
|
|
self._messages: Optional[Deque[Message]] = deque(maxlen=self.max_messages)
|
|
else:
|
|
self._messages: Optional[Deque[Message]] = None
|
|
|
|
def process_chunk_requests(self, guild_id: int, nonce: Optional[str], members: List[Member], complete: bool) -> None:
|
|
removed = []
|
|
for key, request in self._chunk_requests.items():
|
|
if request.guild_id == guild_id and request.nonce == nonce:
|
|
request.add_members(members)
|
|
if complete:
|
|
request.done()
|
|
removed.append(key)
|
|
|
|
for key in removed:
|
|
del self._chunk_requests[key]
|
|
|
|
def call_handlers(self, key: str, *args: Any, **kwargs: Any) -> None:
|
|
try:
|
|
func = self.handlers[key]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
func(*args, **kwargs)
|
|
|
|
async def call_hooks(self, key: str, *args: Any, **kwargs: Any) -> None:
|
|
try:
|
|
coro = self.hooks[key]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
await coro(*args, **kwargs)
|
|
|
|
@property
|
|
def self_id(self) -> Optional[int]:
|
|
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())
|
|
|
|
def _get_voice_client(self, guild_id: Optional[int]) -> Optional[VoiceProtocol]:
|
|
# the keys of self._voice_clients are ints
|
|
return self._voice_clients.get(guild_id) # type: ignore
|
|
|
|
def _add_voice_client(self, guild_id: int, voice: VoiceProtocol) -> None:
|
|
self._voice_clients[guild_id] = voice
|
|
|
|
def _remove_voice_client(self, guild_id: int) -> None:
|
|
self._voice_clients.pop(guild_id, None)
|
|
|
|
def _update_references(self, ws: DiscordWebSocket) -> None:
|
|
for vc in self.voice_clients:
|
|
vc.main_ws = ws # type: ignore # Silencing the unknown attribute (ok at runtime).
|
|
|
|
def store_user(self, data: Union[UserPayload, PartialUserPayload]) -> User:
|
|
# this way is 300% faster than `dict.setdefault`.
|
|
user_id = int(data['id'])
|
|
try:
|
|
return self._users[user_id]
|
|
except KeyError:
|
|
user = User(state=self, data=data)
|
|
if user.discriminator != '0000':
|
|
self._users[user_id] = user
|
|
return user
|
|
|
|
def store_user_no_intents(self, data: Union[UserPayload, PartialUserPayload]) -> User:
|
|
return User(state=self, data=data)
|
|
|
|
def create_user(self, data: Union[UserPayload, PartialUserPayload]) -> User:
|
|
return User(state=self, data=data)
|
|
|
|
def get_user(self, id: int) -> Optional[User]:
|
|
return self._users.get(id)
|
|
|
|
def store_emoji(self, guild: Guild, data: EmojiPayload) -> Emoji:
|
|
# the id will be present here
|
|
emoji_id = int(data['id']) # type: ignore
|
|
self._emojis[emoji_id] = emoji = Emoji(guild=guild, state=self, data=data)
|
|
return emoji
|
|
|
|
def store_sticker(self, guild: Guild, data: GuildStickerPayload) -> GuildSticker:
|
|
sticker_id = int(data['id'])
|
|
self._stickers[sticker_id] = sticker = GuildSticker(state=self, data=data)
|
|
return sticker
|
|
|
|
def store_view(self, view: View, message_id: Optional[int] = None, interaction_id: Optional[int] = None) -> None:
|
|
if interaction_id is not None:
|
|
self._view_store.remove_interaction_mapping(interaction_id)
|
|
self._view_store.add_view(view, message_id)
|
|
|
|
def prevent_view_updates_for(self, message_id: int) -> Optional[View]:
|
|
return self._view_store.remove_message_tracking(message_id)
|
|
|
|
@property
|
|
def persistent_views(self) -> Sequence[View]:
|
|
return self._view_store.persistent_views
|
|
|
|
@property
|
|
def guilds(self) -> List[Guild]:
|
|
return list(self._guilds.values())
|
|
|
|
def _get_guild(self, guild_id: Optional[int]) -> Optional[Guild]:
|
|
# the keys of self._guilds are ints
|
|
return self._guilds.get(guild_id) # type: ignore
|
|
|
|
def _add_guild(self, guild: Guild) -> None:
|
|
self._guilds[guild.id] = guild
|
|
|
|
def _remove_guild(self, guild: Guild) -> None:
|
|
self._guilds.pop(guild.id, None)
|
|
|
|
for emoji in guild.emojis:
|
|
self._emojis.pop(emoji.id, None)
|
|
|
|
for sticker in guild.stickers:
|
|
self._stickers.pop(sticker.id, None)
|
|
|
|
del guild
|
|
|
|
@property
|
|
def emojis(self) -> List[Emoji]:
|
|
return list(self._emojis.values())
|
|
|
|
@property
|
|
def stickers(self) -> List[GuildSticker]:
|
|
return list(self._stickers.values())
|
|
|
|
def get_emoji(self, emoji_id: Optional[int]) -> Optional[Emoji]:
|
|
# the keys of self._emojis are ints
|
|
return self._emojis.get(emoji_id) # type: ignore
|
|
|
|
def get_sticker(self, sticker_id: Optional[int]) -> Optional[GuildSticker]:
|
|
# the keys of self._stickers are ints
|
|
return self._stickers.get(sticker_id) # type: ignore
|
|
|
|
@property
|
|
def private_channels(self) -> List[PrivateChannel]:
|
|
return list(self._private_channels.values())
|
|
|
|
def _get_private_channel(self, channel_id: Optional[int]) -> Optional[PrivateChannel]:
|
|
try:
|
|
# the keys of self._private_channels are ints
|
|
value = self._private_channels[channel_id] # type: ignore
|
|
except KeyError:
|
|
return None
|
|
else:
|
|
# Type narrowing can't figure out that channel_id isn't None here
|
|
self._private_channels.move_to_end(channel_id) # type: ignore
|
|
return value
|
|
|
|
def _get_private_channel_by_user(self, user_id: Optional[int]) -> Optional[DMChannel]:
|
|
# the keys of self._private_channels are ints
|
|
return self._private_channels_by_user.get(user_id) # type: ignore
|
|
|
|
def _add_private_channel(self, channel: PrivateChannel) -> None:
|
|
channel_id = channel.id
|
|
self._private_channels[channel_id] = channel
|
|
|
|
if len(self._private_channels) > 128:
|
|
_, to_remove = self._private_channels.popitem(last=False)
|
|
if isinstance(to_remove, DMChannel) and to_remove.recipient:
|
|
self._private_channels_by_user.pop(to_remove.recipient.id, None)
|
|
|
|
if isinstance(channel, DMChannel) and channel.recipient:
|
|
self._private_channels_by_user[channel.recipient.id] = channel
|
|
|
|
def add_dm_channel(self, data: DMChannelPayload) -> DMChannel:
|
|
# self.user is *always* cached when this is called
|
|
channel = DMChannel(me=self.user, state=self, data=data) # type: ignore
|
|
self._add_private_channel(channel)
|
|
return channel
|
|
|
|
def _remove_private_channel(self, channel: PrivateChannel) -> None:
|
|
self._private_channels.pop(channel.id, None)
|
|
if isinstance(channel, DMChannel):
|
|
recipient = channel.recipient
|
|
if recipient is not None:
|
|
self._private_channels_by_user.pop(recipient.id, None)
|
|
|
|
def _get_message(self, msg_id: Optional[int]) -> Optional[Message]:
|
|
return utils.find(lambda m: m.id == msg_id, reversed(self._messages)) if self._messages else None
|
|
|
|
def _add_guild_from_data(self, data: GuildPayload) -> Guild:
|
|
guild = Guild(data=data, state=self)
|
|
self._add_guild(guild)
|
|
return guild
|
|
|
|
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)
|
|
|
|
def _get_guild_channel(
|
|
self, data: PartialMessagePayload, guild_id: Optional[int] = None
|
|
) -> Tuple[Union[Channel, Thread], Optional[Guild]]:
|
|
channel_id = int(data['channel_id'])
|
|
try:
|
|
guild_id = guild_id or int(data['guild_id'])
|
|
guild = self._get_guild(guild_id)
|
|
except KeyError:
|
|
channel = DMChannel._from_message(self, channel_id)
|
|
guild = None
|
|
else:
|
|
channel = guild and guild._resolve_channel(channel_id)
|
|
|
|
return channel or PartialMessageable(state=self, id=channel_id), guild
|
|
|
|
async def chunker(
|
|
self, guild_id: int, query: str = '', limit: int = 0, presences: bool = False, *, nonce: Optional[str] = None
|
|
) -> None:
|
|
ws = self._get_websocket(guild_id) # This is ignored upstream
|
|
await ws.request_chunks(guild_id, query=query, limit=limit, presences=presences, nonce=nonce)
|
|
|
|
async def query_members(
|
|
self, guild: Guild, query: Optional[str], limit: int, user_ids: Optional[List[int]], cache: bool, presences: bool
|
|
) -> List[Member]:
|
|
guild_id = guild.id
|
|
ws = self._get_websocket(guild_id)
|
|
if ws is None:
|
|
raise RuntimeError('Somehow do not have a websocket for this guild_id')
|
|
|
|
request = ChunkRequest(guild.id, self.loop, self._get_guild, cache=cache)
|
|
self._chunk_requests[request.nonce] = request
|
|
|
|
try:
|
|
# start the query operation
|
|
await ws.request_chunks(
|
|
guild_id, query=query, limit=limit, user_ids=user_ids, presences=presences, nonce=request.nonce
|
|
)
|
|
return await asyncio.wait_for(request.wait(), timeout=30.0)
|
|
except asyncio.TimeoutError:
|
|
_log.warning('Timed out waiting for chunks with query %r and limit %d for guild_id %d', query, limit, guild_id)
|
|
raise
|
|
|
|
async def _delay_ready(self) -> None:
|
|
try:
|
|
states = []
|
|
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):
|
|
future = await self.chunk_guild(guild, wait=False)
|
|
states.append((guild, future))
|
|
else:
|
|
if guild.unavailable is False:
|
|
self.dispatch('guild_available', guild)
|
|
else:
|
|
self.dispatch('guild_join', guild)
|
|
|
|
for guild, future in states:
|
|
timeout = self._chunk_timeout(guild)
|
|
|
|
try:
|
|
await asyncio.wait_for(future, timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
_log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
|
|
|
|
if guild.unavailable is False:
|
|
self.dispatch('guild_available', guild)
|
|
else:
|
|
self.dispatch('guild_join', guild)
|
|
|
|
# remove the state
|
|
try:
|
|
del self._ready_state
|
|
except AttributeError:
|
|
pass # already been deleted somehow
|
|
|
|
except asyncio.CancelledError:
|
|
pass
|
|
else:
|
|
# dispatch the event
|
|
self.call_handlers('ready')
|
|
self.dispatch('ready')
|
|
finally:
|
|
self._ready_task = None
|
|
|
|
def parse_ready(self, data: gw.ReadyEvent) -> None:
|
|
if self._ready_task is not None:
|
|
self._ready_task.cancel()
|
|
|
|
self._ready_state: asyncio.Queue[Guild] = asyncio.Queue()
|
|
self.clear(views=False)
|
|
self.user = user = ClientUser(state=self, data=data['user'])
|
|
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 = ApplicationFlags._from_value(application['flags'])
|
|
|
|
for guild_data in data['guilds']:
|
|
self._add_guild_from_data(guild_data) # type: ignore
|
|
|
|
self.dispatch('connect')
|
|
self._ready_task = asyncio.create_task(self._delay_ready())
|
|
|
|
def parse_resumed(self, data: gw.ResumedEvent) -> None:
|
|
self.dispatch('resumed')
|
|
|
|
def parse_message_create(self, data: gw.MessageCreateEvent) -> None:
|
|
channel, _ = self._get_guild_channel(data)
|
|
# channel would be the correct type here
|
|
message = Message(channel=channel, data=data, state=self) # type: ignore
|
|
self.dispatch('message', message)
|
|
if self._messages is not None:
|
|
self._messages.append(message)
|
|
# we ensure that the channel is either a TextChannel, VoiceChannel, or Thread
|
|
if channel and channel.__class__ in (TextChannel, VoiceChannel, Thread):
|
|
channel.last_message_id = message.id # type: ignore
|
|
|
|
def parse_message_delete(self, data: gw.MessageDeleteEvent) -> None:
|
|
raw = RawMessageDeleteEvent(data)
|
|
found = self._get_message(raw.message_id)
|
|
raw.cached_message = found
|
|
self.dispatch('raw_message_delete', raw)
|
|
if self._messages is not None and found is not None:
|
|
self.dispatch('message_delete', found)
|
|
self._messages.remove(found)
|
|
|
|
def parse_message_delete_bulk(self, data: gw.MessageDeleteBulkEvent) -> None:
|
|
raw = RawBulkMessageDeleteEvent(data)
|
|
if self._messages:
|
|
found_messages = [message for message in self._messages if message.id in raw.message_ids]
|
|
else:
|
|
found_messages = []
|
|
raw.cached_messages = found_messages
|
|
self.dispatch('raw_bulk_message_delete', raw)
|
|
if found_messages:
|
|
self.dispatch('bulk_message_delete', found_messages)
|
|
for msg in found_messages:
|
|
# self._messages won't be None here
|
|
self._messages.remove(msg) # type: ignore
|
|
|
|
def parse_message_update(self, data: gw.MessageUpdateEvent) -> None:
|
|
raw = RawMessageUpdateEvent(data)
|
|
message = self._get_message(raw.message_id)
|
|
if message is not None:
|
|
older_message = copy.copy(message)
|
|
raw.cached_message = older_message
|
|
self.dispatch('raw_message_edit', raw)
|
|
message._update(data)
|
|
# Coerce the `after` parameter to take the new updated Member
|
|
# ref: #5999
|
|
older_message.author = message.author
|
|
self.dispatch('message_edit', older_message, message)
|
|
else:
|
|
self.dispatch('raw_message_edit', raw)
|
|
|
|
if 'components' in data:
|
|
try:
|
|
entity_id = int(data['interaction']['id'])
|
|
except (KeyError, ValueError):
|
|
entity_id = raw.message_id
|
|
|
|
if self._view_store.is_message_tracked(entity_id):
|
|
self._view_store.update_from_message(entity_id, data['components'])
|
|
|
|
def parse_message_reaction_add(self, data: gw.MessageReactionAddEvent) -> None:
|
|
emoji = PartialEmoji.from_dict(data['emoji'])
|
|
emoji._state = self
|
|
raw = RawReactionActionEvent(data, emoji, 'REACTION_ADD')
|
|
|
|
member_data = data.get('member')
|
|
if member_data:
|
|
guild = self._get_guild(raw.guild_id)
|
|
if guild is not None:
|
|
raw.member = Member(data=member_data, guild=guild, state=self)
|
|
else:
|
|
raw.member = None
|
|
else:
|
|
raw.member = None
|
|
self.dispatch('raw_reaction_add', raw)
|
|
|
|
# rich interface here
|
|
message = self._get_message(raw.message_id)
|
|
if message is not None:
|
|
emoji = self._upgrade_partial_emoji(emoji)
|
|
reaction = message._add_reaction(data, emoji, raw.user_id)
|
|
user = raw.member or self._get_reaction_user(message.channel, raw.user_id)
|
|
|
|
if user:
|
|
self.dispatch('reaction_add', reaction, user)
|
|
|
|
def parse_message_reaction_remove_all(self, data: gw.MessageReactionRemoveAllEvent) -> None:
|
|
raw = RawReactionClearEvent(data)
|
|
self.dispatch('raw_reaction_clear', raw)
|
|
|
|
message = self._get_message(raw.message_id)
|
|
if message is not None:
|
|
old_reactions = message.reactions.copy()
|
|
message.reactions.clear()
|
|
self.dispatch('reaction_clear', message, old_reactions)
|
|
|
|
def parse_message_reaction_remove(self, data: gw.MessageReactionRemoveEvent) -> None:
|
|
emoji = PartialEmoji.from_dict(data['emoji'])
|
|
emoji._state = self
|
|
raw = RawReactionActionEvent(data, emoji, 'REACTION_REMOVE')
|
|
self.dispatch('raw_reaction_remove', raw)
|
|
|
|
message = self._get_message(raw.message_id)
|
|
if message is not None:
|
|
emoji = self._upgrade_partial_emoji(emoji)
|
|
try:
|
|
reaction = message._remove_reaction(data, emoji, raw.user_id)
|
|
except (AttributeError, ValueError): # eventual consistency lol
|
|
pass
|
|
else:
|
|
user = self._get_reaction_user(message.channel, raw.user_id)
|
|
if user:
|
|
self.dispatch('reaction_remove', reaction, user)
|
|
|
|
def parse_message_reaction_remove_emoji(self, data: gw.MessageReactionRemoveEmojiEvent) -> None:
|
|
emoji = PartialEmoji.from_dict(data['emoji'])
|
|
emoji._state = self
|
|
raw = RawReactionClearEmojiEvent(data, emoji)
|
|
self.dispatch('raw_reaction_clear_emoji', raw)
|
|
|
|
message = self._get_message(raw.message_id)
|
|
if message is not None:
|
|
try:
|
|
reaction = message._clear_emoji(emoji)
|
|
except (AttributeError, ValueError): # eventual consistency lol
|
|
pass
|
|
else:
|
|
if reaction:
|
|
self.dispatch('reaction_clear_emoji', reaction)
|
|
|
|
def parse_interaction_create(self, data: gw.InteractionCreateEvent) -> None:
|
|
interaction = Interaction(data=data, state=self)
|
|
if data['type'] in (2, 4) and self._command_tree: # application command and auto complete
|
|
self._command_tree._from_interaction(interaction)
|
|
elif data['type'] == 3: # interaction component
|
|
# These keys are always there for this interaction type
|
|
inner_data = data['data']
|
|
custom_id = inner_data['custom_id']
|
|
component_type = inner_data['component_type']
|
|
self._view_store.dispatch_view(component_type, custom_id, interaction)
|
|
elif data['type'] == 5: # modal submit
|
|
# These keys are always there for this interaction type
|
|
inner_data = data['data']
|
|
custom_id = inner_data['custom_id']
|
|
components = inner_data['components']
|
|
self._view_store.dispatch_modal(custom_id, interaction, components) # type: ignore
|
|
self.dispatch('interaction', interaction)
|
|
|
|
def parse_presence_update(self, data: gw.PresenceUpdateEvent) -> None:
|
|
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
|
# guild_id won't be None here
|
|
guild = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('PRESENCE_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
return
|
|
|
|
user = data['user']
|
|
member_id = int(user['id'])
|
|
member = guild.get_member(member_id)
|
|
if member is None:
|
|
_log.debug('PRESENCE_UPDATE referencing an unknown member ID: %s. Discarding', member_id)
|
|
return
|
|
|
|
old_member = Member._copy(member)
|
|
user_update = member._presence_update(data=data, user=user)
|
|
if user_update:
|
|
self.dispatch('user_update', user_update[0], user_update[1])
|
|
|
|
self.dispatch('presence_update', old_member, member)
|
|
|
|
def parse_user_update(self, data: gw.UserUpdateEvent) -> None:
|
|
if self.user:
|
|
self.user._update(data)
|
|
|
|
def parse_invite_create(self, data: gw.InviteCreateEvent) -> None:
|
|
invite = Invite.from_gateway(state=self, data=data)
|
|
self.dispatch('invite_create', invite)
|
|
|
|
def parse_invite_delete(self, data: gw.InviteDeleteEvent) -> None:
|
|
invite = Invite.from_gateway(state=self, data=data)
|
|
self.dispatch('invite_delete', invite)
|
|
|
|
def parse_channel_delete(self, data: gw.ChannelDeleteEvent) -> None:
|
|
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
|
|
channel_id = int(data['id'])
|
|
if guild is not None:
|
|
channel = guild.get_channel(channel_id)
|
|
if channel is not None:
|
|
guild._remove_channel(channel)
|
|
self.dispatch('guild_channel_delete', channel)
|
|
|
|
if channel.type in (ChannelType.voice, ChannelType.stage_voice):
|
|
for s in guild.scheduled_events:
|
|
if s.channel_id == channel.id:
|
|
guild._scheduled_events.pop(s.id)
|
|
self.dispatch('scheduled_event_delete', guild, s)
|
|
|
|
def parse_channel_update(self, data: gw.ChannelUpdateEvent) -> None:
|
|
channel_type = try_enum(ChannelType, data.get('type'))
|
|
channel_id = int(data['id'])
|
|
if channel_type is ChannelType.group:
|
|
channel = self._get_private_channel(channel_id)
|
|
if channel is not None:
|
|
old_channel = copy.copy(channel)
|
|
# the channel is a GroupChannel rather than PrivateChannel
|
|
channel._update_group(data) # type: ignore
|
|
self.dispatch('private_channel_update', old_channel, channel)
|
|
return
|
|
else:
|
|
_log.debug('CHANNEL_UPDATE referencing an unknown channel ID: %s. Discarding.', channel_id)
|
|
|
|
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
|
guild = self._get_guild(guild_id)
|
|
if guild is not None:
|
|
channel = guild.get_channel(channel_id)
|
|
if channel is not None:
|
|
old_channel = copy.copy(channel)
|
|
channel._update(guild, data) # type: ignore # the data payload varies based on the channel type.
|
|
self.dispatch('guild_channel_update', old_channel, channel)
|
|
else:
|
|
_log.debug('CHANNEL_UPDATE referencing an unknown channel ID: %s. Discarding.', channel_id)
|
|
else:
|
|
_log.debug('CHANNEL_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
|
|
def parse_channel_create(self, data: gw.ChannelCreateEvent) -> None:
|
|
factory, ch_type = _channel_factory(data['type'])
|
|
if factory is None:
|
|
_log.debug('CHANNEL_CREATE referencing an unknown channel type %s. Discarding.', data['type'])
|
|
return
|
|
|
|
guild_id = utils._get_as_snowflake(data, 'guild_id')
|
|
guild = self._get_guild(guild_id)
|
|
if guild is not None:
|
|
# the factory can't be a DMChannel or GroupChannel here
|
|
channel = factory(guild=guild, state=self, data=data) # type: ignore
|
|
guild._add_channel(channel) # type: ignore
|
|
self.dispatch('guild_channel_create', channel)
|
|
else:
|
|
_log.debug('CHANNEL_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
return
|
|
|
|
def parse_channel_pins_update(self, data: gw.ChannelPinsUpdateEvent) -> None:
|
|
channel_id = int(data['channel_id'])
|
|
try:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
except KeyError:
|
|
guild = None
|
|
channel = self._get_private_channel(channel_id)
|
|
else:
|
|
channel = guild and guild._resolve_channel(channel_id)
|
|
|
|
if channel is None:
|
|
_log.debug('CHANNEL_PINS_UPDATE referencing an unknown channel ID: %s. Discarding.', channel_id)
|
|
return
|
|
|
|
last_pin = utils.parse_time(data.get('last_pin_timestamp'))
|
|
|
|
if guild is None:
|
|
self.dispatch('private_channel_pins_update', channel, last_pin)
|
|
else:
|
|
self.dispatch('guild_channel_pins_update', channel, last_pin)
|
|
|
|
def parse_thread_create(self, data: gw.ThreadCreateEvent) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('THREAD_CREATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
|
return
|
|
|
|
thread = Thread(guild=guild, state=guild._state, data=data)
|
|
has_thread = guild.get_thread(thread.id)
|
|
guild._add_thread(thread)
|
|
if not has_thread:
|
|
if data.get('newly_created'):
|
|
if thread.parent.__class__ is ForumChannel:
|
|
thread.parent.last_message_id = thread.id # type: ignore
|
|
|
|
self.dispatch('thread_create', thread)
|
|
else:
|
|
self.dispatch('thread_join', thread)
|
|
|
|
def parse_thread_update(self, data: gw.ThreadUpdateEvent) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('THREAD_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
|
return
|
|
|
|
thread_id = int(data['id'])
|
|
thread = guild.get_thread(thread_id)
|
|
if thread is not None:
|
|
old = copy.copy(thread)
|
|
thread._update(data)
|
|
if thread.archived:
|
|
guild._remove_thread(thread)
|
|
self.dispatch('thread_update', old, thread)
|
|
else:
|
|
thread = Thread(guild=guild, state=guild._state, data=data)
|
|
if not thread.archived:
|
|
guild._add_thread(thread)
|
|
self.dispatch('thread_join', thread)
|
|
|
|
def parse_thread_delete(self, data: gw.ThreadDeleteEvent) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('THREAD_DELETE referencing an unknown guild ID: %s. Discarding', guild_id)
|
|
return
|
|
|
|
raw = RawThreadDeleteEvent(data)
|
|
raw.thread = thread = guild.get_thread(raw.thread_id)
|
|
self.dispatch('raw_thread_delete', raw)
|
|
|
|
if thread is not None:
|
|
guild._remove_thread(thread)
|
|
self.dispatch('thread_delete', thread)
|
|
|
|
def parse_thread_list_sync(self, data: gw.ThreadListSyncEvent) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('THREAD_LIST_SYNC referencing an unknown guild ID: %s. Discarding', guild_id)
|
|
return
|
|
|
|
try:
|
|
channel_ids = {int(i) for i in data['channel_ids']}
|
|
except KeyError:
|
|
# If not provided, then the entire guild is being synced
|
|
# So all previous thread data should be overwritten
|
|
previous_threads = guild._threads.copy()
|
|
guild._clear_threads()
|
|
else:
|
|
previous_threads = guild._filter_threads(channel_ids)
|
|
|
|
threads = {d['id']: guild._store_thread(d) for d in data.get('threads', [])}
|
|
|
|
for member in data.get('members', []):
|
|
try:
|
|
# note: member['id'] is the thread_id
|
|
thread = threads[member['id']]
|
|
except KeyError:
|
|
continue
|
|
else:
|
|
thread._add_member(ThreadMember(thread, member))
|
|
|
|
for thread in threads.values():
|
|
old = previous_threads.pop(thread.id, None)
|
|
if old is None:
|
|
self.dispatch('thread_join', thread)
|
|
|
|
for thread in previous_threads.values():
|
|
self.dispatch('thread_remove', thread)
|
|
|
|
def parse_thread_member_update(self, data: gw.ThreadMemberUpdate) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('THREAD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
|
return
|
|
|
|
thread_id = int(data['id'])
|
|
thread: Optional[Thread] = guild.get_thread(thread_id)
|
|
if thread is None:
|
|
_log.debug('THREAD_MEMBER_UPDATE referencing an unknown thread ID: %s. Discarding', thread_id)
|
|
return
|
|
|
|
member = ThreadMember(thread, data)
|
|
thread.me = member
|
|
|
|
def parse_thread_members_update(self, data: gw.ThreadMembersUpdate) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild: Optional[Guild] = self._get_guild(guild_id)
|
|
if guild is None:
|
|
_log.debug('THREAD_MEMBERS_UPDATE referencing an unknown guild ID: %s. Discarding', guild_id)
|
|
return
|
|
|
|
thread_id = int(data['id'])
|
|
thread: Optional[Thread] = guild.get_thread(thread_id)
|
|
if thread is None:
|
|
_log.debug('THREAD_MEMBERS_UPDATE referencing an unknown thread ID: %s. Discarding', thread_id)
|
|
return
|
|
|
|
added_members = [ThreadMember(thread, d) for d in data.get('added_members', [])]
|
|
removed_member_ids = [int(x) for x in data.get('removed_member_ids', [])]
|
|
self_id = self.self_id
|
|
for member in added_members:
|
|
if member.id != self_id:
|
|
thread._add_member(member)
|
|
self.dispatch('thread_member_join', member)
|
|
else:
|
|
thread.me = member
|
|
self.dispatch('thread_join', thread)
|
|
|
|
for member_id in removed_member_ids:
|
|
if member_id != self_id:
|
|
member = thread._pop_member(member_id)
|
|
if member is not None:
|
|
self.dispatch('thread_member_remove', member)
|
|
else:
|
|
self.dispatch('thread_remove', thread)
|
|
|
|
def parse_guild_member_add(self, data: gw.GuildMemberAddEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is None:
|
|
_log.debug('GUILD_MEMBER_ADD referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
return
|
|
|
|
member = Member(guild=guild, data=data, state=self)
|
|
if self.member_cache_flags.joined:
|
|
guild._add_member(member)
|
|
|
|
if guild._member_count is not None:
|
|
guild._member_count += 1
|
|
|
|
self.dispatch('member_join', member)
|
|
|
|
def parse_guild_member_remove(self, data: gw.GuildMemberRemoveEvent) -> None:
|
|
user = self.store_user(data['user'])
|
|
raw = RawMemberRemoveEvent(data, user)
|
|
|
|
guild = self._get_guild(raw.guild_id)
|
|
if guild is not None:
|
|
if guild._member_count is not None:
|
|
guild._member_count -= 1
|
|
|
|
member = guild.get_member(user.id)
|
|
if member is not None:
|
|
raw.user = member
|
|
guild._remove_member(member)
|
|
self.dispatch('member_remove', member)
|
|
else:
|
|
_log.debug('GUILD_MEMBER_REMOVE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
self.dispatch('raw_member_remove', raw)
|
|
|
|
def parse_guild_member_update(self, data: gw.GuildMemberUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
user = data['user']
|
|
user_id = int(user['id'])
|
|
if guild is None:
|
|
_log.debug('GUILD_MEMBER_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
return
|
|
|
|
member = guild.get_member(user_id)
|
|
if member is not None:
|
|
old_member = Member._copy(member)
|
|
member._update(data)
|
|
user_update = member._update_inner_user(user)
|
|
if user_update:
|
|
self.dispatch('user_update', user_update[0], user_update[1])
|
|
|
|
self.dispatch('member_update', old_member, member)
|
|
else:
|
|
if self.member_cache_flags.joined:
|
|
member = Member(data=data, guild=guild, state=self) # type: ignore # the data is not complete, contains a delta of values
|
|
|
|
# Force an update on the inner user if necessary
|
|
user_update = member._update_inner_user(user)
|
|
if user_update:
|
|
self.dispatch('user_update', user_update[0], user_update[1])
|
|
|
|
guild._add_member(member)
|
|
_log.debug('GUILD_MEMBER_UPDATE referencing an unknown member ID: %s. Discarding.', user_id)
|
|
|
|
def parse_guild_emojis_update(self, data: gw.GuildEmojisUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is None:
|
|
_log.debug('GUILD_EMOJIS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
return
|
|
|
|
before_emojis = guild.emojis
|
|
for emoji in before_emojis:
|
|
self._emojis.pop(emoji.id, None)
|
|
# guild won't be None here
|
|
guild.emojis = tuple(map(lambda d: self.store_emoji(guild, d), data['emojis']))
|
|
self.dispatch('guild_emojis_update', guild, before_emojis, guild.emojis)
|
|
|
|
def parse_guild_stickers_update(self, data: gw.GuildStickersUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is None:
|
|
_log.debug('GUILD_STICKERS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
return
|
|
|
|
before_stickers = guild.stickers
|
|
for emoji in before_stickers:
|
|
self._stickers.pop(emoji.id, None)
|
|
|
|
guild.stickers = tuple(map(lambda d: self.store_sticker(guild, d), data['stickers']))
|
|
self.dispatch('guild_stickers_update', guild, before_stickers, guild.stickers)
|
|
|
|
def _get_create_guild(self, data: gw.GuildCreateEvent) -> Guild:
|
|
if data.get('unavailable') is False:
|
|
# GUILD_CREATE with unavailable in the response
|
|
# usually means that the guild has become available
|
|
# and is therefore in the cache
|
|
guild = self._get_guild(int(data['id']))
|
|
if guild is not None:
|
|
guild.unavailable = False
|
|
guild._from_data(data)
|
|
return guild
|
|
|
|
return self._add_guild_from_data(data)
|
|
|
|
def is_guild_evicted(self, guild: Guild) -> bool:
|
|
return guild.id not in self._guilds
|
|
|
|
@overload
|
|
async def chunk_guild(self, guild: Guild, *, wait: Literal[True] = ..., cache: Optional[bool] = ...) -> List[Member]:
|
|
...
|
|
|
|
@overload
|
|
async def chunk_guild(
|
|
self, guild: Guild, *, wait: Literal[False] = ..., cache: Optional[bool] = ...
|
|
) -> asyncio.Future[List[Member]]:
|
|
...
|
|
|
|
async def chunk_guild(
|
|
self, guild: Guild, *, wait: bool = True, cache: Optional[bool] = None
|
|
) -> Union[List[Member], asyncio.Future[List[Member]]]:
|
|
cache = cache or self.member_cache_flags.joined
|
|
request = self._chunk_requests.get(guild.id)
|
|
if request is None:
|
|
self._chunk_requests[guild.id] = request = ChunkRequest(guild.id, self.loop, self._get_guild, cache=cache)
|
|
await self.chunker(guild.id, nonce=request.nonce)
|
|
|
|
if wait:
|
|
return await request.wait()
|
|
return request.get_future()
|
|
|
|
def _chunk_timeout(self, guild: Guild) -> float:
|
|
return max(5.0, (guild.member_count or 0) / 10000)
|
|
|
|
async def _chunk_and_dispatch(self, guild, unavailable):
|
|
timeout = self._chunk_timeout(guild)
|
|
|
|
try:
|
|
await asyncio.wait_for(self.chunk_guild(guild), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
_log.info('Somehow timed out waiting for chunks.')
|
|
|
|
if unavailable is False:
|
|
self.dispatch('guild_available', guild)
|
|
else:
|
|
self.dispatch('guild_join', guild)
|
|
|
|
def _add_ready_state(self, guild: Guild) -> bool:
|
|
try:
|
|
# Notify the on_ready state, if any, that this guild is complete.
|
|
self._ready_state.put_nowait(guild)
|
|
except AttributeError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def parse_guild_create(self, data: gw.GuildCreateEvent) -> None:
|
|
unavailable = data.get('unavailable')
|
|
if unavailable is True:
|
|
# joined a guild with unavailable == True so..
|
|
return
|
|
|
|
guild = self._get_create_guild(data)
|
|
|
|
if self._add_ready_state(guild):
|
|
return # We're waiting for the ready event, put the rest on hold
|
|
|
|
# check if it requires chunking
|
|
if self._guild_needs_chunking(guild):
|
|
asyncio.create_task(self._chunk_and_dispatch(guild, unavailable))
|
|
return
|
|
|
|
# Dispatch available if newly available
|
|
if unavailable is False:
|
|
self.dispatch('guild_available', guild)
|
|
else:
|
|
self.dispatch('guild_join', guild)
|
|
|
|
def parse_guild_update(self, data: gw.GuildUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['id']))
|
|
if guild is not None:
|
|
old_guild = copy.copy(guild)
|
|
guild._from_data(data)
|
|
self.dispatch('guild_update', old_guild, guild)
|
|
else:
|
|
_log.debug('GUILD_UPDATE referencing an unknown guild ID: %s. Discarding.', data['id'])
|
|
|
|
def parse_guild_delete(self, data: gw.GuildDeleteEvent) -> None:
|
|
guild = self._get_guild(int(data['id']))
|
|
if guild is None:
|
|
_log.debug('GUILD_DELETE referencing an unknown guild ID: %s. Discarding.', data['id'])
|
|
return
|
|
|
|
if data.get('unavailable', False):
|
|
# GUILD_DELETE with unavailable being True means that the
|
|
# guild that was available is now currently unavailable
|
|
guild.unavailable = True
|
|
self.dispatch('guild_unavailable', guild)
|
|
return
|
|
|
|
# do a cleanup of the messages cache
|
|
if self._messages is not None:
|
|
self._messages: Optional[Deque[Message]] = deque(
|
|
(msg for msg in self._messages if msg.guild != guild), maxlen=self.max_messages
|
|
)
|
|
|
|
self._remove_guild(guild)
|
|
self.dispatch('guild_remove', guild)
|
|
|
|
def parse_guild_ban_add(self, data: gw.GuildBanAddEvent) -> None:
|
|
# we make the assumption that GUILD_BAN_ADD is done
|
|
# before GUILD_MEMBER_REMOVE is called
|
|
# hence we don't remove it from cache or do anything
|
|
# strange with it, the main purpose of this event
|
|
# is mainly to dispatch to another event worth listening to for logging
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
try:
|
|
user = User(data=data['user'], state=self)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
member = guild.get_member(user.id) or user
|
|
self.dispatch('member_ban', guild, member)
|
|
|
|
def parse_guild_ban_remove(self, data: gw.GuildBanRemoveEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None and 'user' in data:
|
|
user = self.store_user(data['user'])
|
|
self.dispatch('member_unban', guild, user)
|
|
|
|
def parse_guild_role_create(self, data: gw.GuildRoleCreateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is None:
|
|
_log.debug('GUILD_ROLE_CREATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
return
|
|
|
|
role_data = data['role']
|
|
role = Role(guild=guild, data=role_data, state=self)
|
|
guild._add_role(role)
|
|
self.dispatch('guild_role_create', role)
|
|
|
|
def parse_guild_role_delete(self, data: gw.GuildRoleDeleteEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
role_id = int(data['role_id'])
|
|
try:
|
|
role = guild._remove_role(role_id)
|
|
except KeyError:
|
|
return
|
|
else:
|
|
self.dispatch('guild_role_delete', role)
|
|
else:
|
|
_log.debug('GUILD_ROLE_DELETE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_role_update(self, data: gw.GuildRoleUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
role_data = data['role']
|
|
role_id = int(role_data['id'])
|
|
role = guild.get_role(role_id)
|
|
if role is not None:
|
|
old_role = copy.copy(role)
|
|
role._update(role_data)
|
|
self.dispatch('guild_role_update', old_role, role)
|
|
else:
|
|
_log.debug('GUILD_ROLE_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_members_chunk(self, data: gw.GuildMembersChunkEvent) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild = self._get_guild(guild_id)
|
|
presences = data.get('presences', [])
|
|
|
|
if guild is None:
|
|
return
|
|
|
|
members = [Member(guild=guild, data=member, state=self) for member in data.get('members', [])]
|
|
_log.debug('Processed a chunk for %s members in guild ID %s.', len(members), guild_id)
|
|
|
|
if presences:
|
|
member_dict: Dict[Snowflake, Member] = {str(member.id): member for member in members}
|
|
for presence in presences:
|
|
user = presence['user']
|
|
member_id = user['id']
|
|
member = member_dict.get(member_id)
|
|
if member is not None:
|
|
member._presence_update(presence, user)
|
|
|
|
complete = data.get('chunk_index', 0) + 1 == data.get('chunk_count')
|
|
self.process_chunk_requests(guild_id, data.get('nonce'), members, complete)
|
|
|
|
def parse_guild_integrations_update(self, data: gw.GuildIntegrationsUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
self.dispatch('guild_integrations_update', guild)
|
|
else:
|
|
_log.debug('GUILD_INTEGRATIONS_UPDATE referencing an unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_integration_create(self, data: gw.IntegrationCreateEvent) -> None:
|
|
guild_id = int(data.pop('guild_id'))
|
|
guild = self._get_guild(guild_id)
|
|
if guild is not None:
|
|
cls, _ = _integration_factory(data['type'])
|
|
integration = cls(data=data, guild=guild)
|
|
self.dispatch('integration_create', integration)
|
|
else:
|
|
_log.debug('INTEGRATION_CREATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
|
|
def parse_integration_update(self, data: gw.IntegrationUpdateEvent) -> None:
|
|
guild_id = int(data.pop('guild_id'))
|
|
guild = self._get_guild(guild_id)
|
|
if guild is not None:
|
|
cls, _ = _integration_factory(data['type'])
|
|
integration = cls(data=data, guild=guild)
|
|
self.dispatch('integration_update', integration)
|
|
else:
|
|
_log.debug('INTEGRATION_UPDATE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
|
|
def parse_integration_delete(self, data: gw.IntegrationDeleteEvent) -> None:
|
|
guild_id = int(data['guild_id'])
|
|
guild = self._get_guild(guild_id)
|
|
if guild is not None:
|
|
raw = RawIntegrationDeleteEvent(data)
|
|
self.dispatch('raw_integration_delete', raw)
|
|
else:
|
|
_log.debug('INTEGRATION_DELETE referencing an unknown guild ID: %s. Discarding.', guild_id)
|
|
|
|
def parse_webhooks_update(self, data: gw.WebhooksUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is None:
|
|
_log.debug('WEBHOOKS_UPDATE referencing an unknown guild ID: %s. Discarding', data['guild_id'])
|
|
return
|
|
|
|
channel_id = utils._get_as_snowflake(data, 'channel_id')
|
|
channel = guild.get_channel(channel_id) # type: ignore # None is okay here
|
|
if channel is not None:
|
|
self.dispatch('webhooks_update', channel)
|
|
else:
|
|
_log.debug('WEBHOOKS_UPDATE referencing an unknown channel ID: %s. Discarding.', data['channel_id'])
|
|
|
|
def parse_stage_instance_create(self, data: gw.StageInstanceCreateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
stage_instance = StageInstance(guild=guild, state=self, data=data)
|
|
guild._stage_instances[stage_instance.id] = stage_instance
|
|
self.dispatch('stage_instance_create', stage_instance)
|
|
else:
|
|
_log.debug('STAGE_INSTANCE_CREATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_stage_instance_update(self, data: gw.StageInstanceUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
stage_instance = guild._stage_instances.get(int(data['id']))
|
|
if stage_instance is not None:
|
|
old_stage_instance = copy.copy(stage_instance)
|
|
stage_instance._update(data)
|
|
self.dispatch('stage_instance_update', old_stage_instance, stage_instance)
|
|
else:
|
|
_log.debug('STAGE_INSTANCE_UPDATE referencing unknown stage instance ID: %s. Discarding.', data['id'])
|
|
else:
|
|
_log.debug('STAGE_INSTANCE_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_stage_instance_delete(self, data: gw.StageInstanceDeleteEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
try:
|
|
stage_instance = guild._stage_instances.pop(int(data['id']))
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
self.dispatch('stage_instance_delete', stage_instance)
|
|
else:
|
|
_log.debug('STAGE_INSTANCE_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_scheduled_event_create(self, data: gw.GuildScheduledEventCreateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
scheduled_event = ScheduledEvent(state=self, data=data)
|
|
guild._scheduled_events[scheduled_event.id] = scheduled_event
|
|
self.dispatch('scheduled_event_create', scheduled_event)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_CREATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_scheduled_event_update(self, data: gw.GuildScheduledEventUpdateEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
scheduled_event = guild._scheduled_events.get(int(data['id']))
|
|
if scheduled_event is not None:
|
|
old_scheduled_event = copy.copy(scheduled_event)
|
|
scheduled_event._update(data)
|
|
self.dispatch('scheduled_event_update', old_scheduled_event, scheduled_event)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_UPDATE referencing unknown scheduled event ID: %s. Discarding.', data['id'])
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_UPDATE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_scheduled_event_delete(self, data: gw.GuildScheduledEventDeleteEvent) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
try:
|
|
scheduled_event = guild._scheduled_events.pop(int(data['id']))
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
self.dispatch('scheduled_event_delete', scheduled_event)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_DELETE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_scheduled_event_user_add(self, data: gw.GuildScheduledEventUserAdd) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
scheduled_event = guild._scheduled_events.get(int(data['guild_scheduled_event_id']))
|
|
if scheduled_event is not None:
|
|
user = self.get_user(int(data['user_id']))
|
|
if user is not None:
|
|
scheduled_event._add_user(user)
|
|
self.dispatch('scheduled_event_user_add', scheduled_event, user)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_USER_ADD referencing unknown user ID: %s. Discarding.', data['user_id'])
|
|
else:
|
|
_log.debug(
|
|
'SCHEDULED_EVENT_USER_ADD referencing unknown scheduled event ID: %s. Discarding.',
|
|
data['guild_scheduled_event_id'],
|
|
)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_USER_ADD referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_guild_scheduled_event_user_remove(self, data: gw.GuildScheduledEventUserRemove) -> None:
|
|
guild = self._get_guild(int(data['guild_id']))
|
|
if guild is not None:
|
|
scheduled_event = guild._scheduled_events.get(int(data['guild_scheduled_event_id']))
|
|
if scheduled_event is not None:
|
|
user = self.get_user(int(data['user_id']))
|
|
if user is not None:
|
|
scheduled_event._pop_user(user.id)
|
|
self.dispatch('scheduled_event_user_remove', scheduled_event, user)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_USER_REMOVE referencing unknown user ID: %s. Discarding.', data['user_id'])
|
|
else:
|
|
_log.debug(
|
|
'SCHEDULED_EVENT_USER_REMOVE referencing unknown scheduled event ID: %s. Discarding.',
|
|
data['guild_scheduled_event_id'],
|
|
)
|
|
else:
|
|
_log.debug('SCHEDULED_EVENT_USER_REMOVE referencing unknown guild ID: %s. Discarding.', data['guild_id'])
|
|
|
|
def parse_voice_state_update(self, data: gw.VoiceStateUpdateEvent) -> None:
|
|
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))
|
|
channel_id = utils._get_as_snowflake(data, 'channel_id')
|
|
flags = self.member_cache_flags
|
|
# self.user is *always* cached when this is called
|
|
self_id = self.user.id # type: ignore
|
|
if guild is not None:
|
|
if int(data['user_id']) == self_id:
|
|
voice = self._get_voice_client(guild.id)
|
|
if voice is not None:
|
|
coro = voice.on_voice_state_update(data)
|
|
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice state update handler'))
|
|
|
|
member, before, after = guild._update_voice_state(data, channel_id) # type: ignore
|
|
if member is not None:
|
|
if flags.voice:
|
|
if channel_id is None and flags._voice_only and member.id != self_id:
|
|
# Only remove from cache if we only have the voice flag enabled
|
|
guild._remove_member(member)
|
|
elif channel_id is not None:
|
|
guild._add_member(member)
|
|
|
|
self.dispatch('voice_state_update', member, before, after)
|
|
else:
|
|
_log.debug('VOICE_STATE_UPDATE referencing an unknown member ID: %s. Discarding.', data['user_id'])
|
|
|
|
def parse_voice_server_update(self, data: gw.VoiceServerUpdateEvent) -> None:
|
|
key_id = int(data['guild_id'])
|
|
|
|
vc = self._get_voice_client(key_id)
|
|
if vc is not None:
|
|
coro = vc.on_voice_server_update(data)
|
|
asyncio.create_task(logging_coroutine(coro, info='Voice Protocol voice server update handler'))
|
|
|
|
def parse_typing_start(self, data: gw.TypingStartEvent) -> None:
|
|
raw = RawTypingEvent(data)
|
|
raw.user = self.get_user(raw.user_id)
|
|
channel, guild = self._get_guild_channel(data)
|
|
|
|
if channel is not None:
|
|
if isinstance(channel, DMChannel):
|
|
channel.recipient = raw.user
|
|
elif guild is not None:
|
|
raw.user = guild.get_member(raw.user_id)
|
|
|
|
if raw.user is None:
|
|
member_data = data.get('member')
|
|
if member_data:
|
|
raw.user = Member(data=member_data, state=self, guild=guild)
|
|
|
|
if raw.user is not None:
|
|
self.dispatch('typing', channel, raw.user, raw.timestamp)
|
|
|
|
self.dispatch('raw_typing', raw)
|
|
|
|
def _get_reaction_user(self, channel: MessageableChannel, user_id: int) -> Optional[Union[User, Member]]:
|
|
if isinstance(channel, TextChannel):
|
|
return channel.guild.get_member(user_id)
|
|
return self.get_user(user_id)
|
|
|
|
def get_reaction_emoji(self, data: PartialEmojiPayload) -> Union[Emoji, PartialEmoji, str]:
|
|
emoji_id = utils._get_as_snowflake(data, 'id')
|
|
|
|
if not emoji_id:
|
|
# the name key will be a str
|
|
return data['name'] # type: ignore
|
|
|
|
try:
|
|
return self._emojis[emoji_id]
|
|
except KeyError:
|
|
return PartialEmoji.with_state(
|
|
self, animated=data.get('animated', False), id=emoji_id, name=data['name'] # type: ignore
|
|
)
|
|
|
|
def _upgrade_partial_emoji(self, emoji: PartialEmoji) -> Union[Emoji, PartialEmoji, str]:
|
|
emoji_id = emoji.id
|
|
if not emoji_id:
|
|
return emoji.name
|
|
try:
|
|
return self._emojis[emoji_id]
|
|
except KeyError:
|
|
return emoji
|
|
|
|
def get_channel(self, id: Optional[int]) -> Optional[Union[Channel, Thread]]:
|
|
if id is None:
|
|
return None
|
|
|
|
pm = self._get_private_channel(id)
|
|
if pm is not None:
|
|
return pm
|
|
|
|
for guild in self.guilds:
|
|
channel = guild._resolve_channel(id)
|
|
if channel is not None:
|
|
return channel
|
|
|
|
def create_message(self, *, channel: MessageableChannel, 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._ready_tasks: Dict[int, asyncio.Task[None]] = {}
|
|
self._ready_states: Dict[int, asyncio.Queue[Guild]] = {}
|
|
|
|
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)
|
|
|
|
def _add_ready_state(self, guild: Guild) -> bool:
|
|
try:
|
|
# Notify the on_ready state, if any, that this guild is complete.
|
|
self._ready_states[guild.shard_id].put_nowait(guild)
|
|
except KeyError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
async def _delay_ready(self) -> None:
|
|
await asyncio.gather(*self._ready_tasks.values())
|
|
|
|
# clear the current tasks
|
|
self._ready_task = None
|
|
self._ready_tasks = {}
|
|
|
|
# dispatch the event
|
|
self.call_handlers('ready')
|
|
self.dispatch('ready')
|
|
|
|
async def _delay_shard_ready(self, shard_id: int) -> None:
|
|
try:
|
|
states = []
|
|
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_states[shard_id].get(), timeout=self.guild_ready_timeout)
|
|
except asyncio.TimeoutError:
|
|
break
|
|
else:
|
|
if self._guild_needs_chunking(guild):
|
|
future = await self.chunk_guild(guild, wait=False)
|
|
states.append((guild, future))
|
|
else:
|
|
if guild.unavailable is False:
|
|
self.dispatch('guild_available', guild)
|
|
else:
|
|
self.dispatch('guild_join', guild)
|
|
|
|
for guild, future in states:
|
|
timeout = self._chunk_timeout(guild)
|
|
|
|
try:
|
|
await asyncio.wait_for(future, timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
_log.warning('Shard ID %s timed out waiting for chunks for guild_id %s.', guild.shard_id, guild.id)
|
|
|
|
if guild.unavailable is False:
|
|
self.dispatch('guild_available', guild)
|
|
else:
|
|
self.dispatch('guild_join', guild)
|
|
|
|
# remove the state
|
|
try:
|
|
del self._ready_states[shard_id]
|
|
except KeyError:
|
|
pass # already been deleted somehow
|
|
|
|
except asyncio.CancelledError:
|
|
pass
|
|
else:
|
|
# dispatch the event
|
|
self.dispatch('shard_ready', shard_id)
|
|
|
|
def parse_ready(self, data: gw.ReadyEvent) -> None:
|
|
if self._ready_task is not None:
|
|
self._ready_task.cancel()
|
|
|
|
shard_id = data['shard'][0] # shard_id, num_shards
|
|
|
|
if shard_id in self._ready_tasks:
|
|
self._ready_tasks[shard_id].cancel()
|
|
|
|
if shard_id not in self._ready_states:
|
|
self._ready_states[shard_id] = asyncio.Queue()
|
|
|
|
self.user: Optional[ClientUser]
|
|
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: Optional[int] = utils._get_as_snowflake(application, 'id')
|
|
self.application_flags: ApplicationFlags = ApplicationFlags._from_value(application['flags'])
|
|
|
|
for guild_data in data['guilds']:
|
|
self._add_guild_from_data(guild_data) # type: ignore # _add_guild_from_data requires a complete Guild payload
|
|
|
|
if self._messages:
|
|
self._update_message_references()
|
|
|
|
self.dispatch('connect')
|
|
self.dispatch('shard_connect', shard_id)
|
|
|
|
self._ready_tasks[shard_id] = asyncio.create_task(self._delay_shard_ready(shard_id))
|
|
|
|
# The delay task for every shard has been started
|
|
if len(self._ready_tasks) == len(self.shard_ids):
|
|
self._ready_task = asyncio.create_task(self._delay_ready())
|
|
|
|
def parse_resumed(self, data: gw.ResumedEvent) -> None:
|
|
self.dispatch('resumed')
|
|
self.dispatch('shard_resumed', data['__shard_id__']) # type: ignore # This is an internal discord.py key
|