diff --git a/discord/guild.py b/discord/guild.py index 10bc3aea2..52e4c19ee 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -150,17 +150,12 @@ class Guild(Hashable): All stickers that the guild owns. .. versionadded:: 2.0 - region: :class:`VoiceRegion` - The region the guild belongs on. There is a chance that the region - will be a :class:`str` if the value is not recognised by the enumerator. afk_timeout: :class:`int` The timeout to get sent to the AFK channel. - afk_channel: Optional[:class:`VoiceChannel`] - The channel that denotes the AFK channel. ``None`` if it doesn't exist. id: :class:`int` The guild's ID. owner_id: :class:`int` - The guild owner's ID. Use :attr:`Guild.owner` instead. + The guild owner's ID. unavailable: :class:`bool` Indicates if the guild is unavailable. If this is ``True`` then the reliability of other attributes outside of :attr:`Guild.id` is slim and they might @@ -274,6 +269,8 @@ class Guild(Hashable): '_public_updates_channel_id', '_stage_instances', '_threads', + '_online_count', + '_subscribing' ) _PREMIUM_GUILD_LIMITS: ClassVar[Dict[Optional[int], _GuildLimit]] = { @@ -285,10 +282,12 @@ class Guild(Hashable): } def __init__(self, *, data: GuildPayload, state: ConnectionState): + self._roles: Dict[int, Role] = {} self._channels: Dict[int, GuildChannel] = {} self._members: Dict[int, Member] = {} self._voice_states: Dict[int, VoiceState] = {} self._threads: Dict[int, Thread] = {} + self._stage_instances: Dict[int, StageInstance] = {} self._state: ConnectionState = state self._from_data(data) @@ -349,7 +348,7 @@ class Guild(Hashable): user_id = int(data['user_id']) channel = self.get_channel(channel_id) try: - # check if we should remove the voice state from cache + # Check if we should remove the voice state from cache if channel is None: after = self._voice_states.pop(user_id) else: @@ -358,7 +357,7 @@ class Guild(Hashable): before = copy.copy(after) after._update(data, channel) except KeyError: - # if we're here then we're getting added into the cache + # If we're here then add it into the cache after = VoiceState(data=data, channel=channel) before = VoiceState(data=data, channel=None) self._voice_states[user_id] = after @@ -373,59 +372,63 @@ class Guild(Hashable): return member, before, after def _add_role(self, role: Role, /) -> None: - # roles get added to the bottom (position 1, pos 0 is @everyone) - # so since self.roles has the @everyone role, we can't increment - # its position because it's stuck at position 0. Luckily x += False - # is equivalent to adding 0. So we cast the position to a bool and - # increment it. for r in self._roles.values(): r.position += not r.is_default() self._roles[role.id] = role def _remove_role(self, role_id: int, /) -> Role: - # this raises KeyError if it fails.. role = self._roles.pop(role_id) - # since it didn't, we can change the positions now - # basically the same as above except we only decrement - # the position if we're above the role we deleted. for r in self._roles.values(): r.position -= r.position > role.position return role def _from_data(self, guild: GuildPayload) -> None: - # according to Stan, this is always available even if the guild is unavailable - # I don't have this guarantee when someone updates the guild. - member_count = guild.get('member_count', None) + member_count = guild.get('member_count') if member_count is not None: self._member_count: int = member_count + self.id: int = int(guild['id']) self.name: str = guild.get('name') - self.region: VoiceRegion = try_enum(VoiceRegion, guild.get('region')) self.verification_level: VerificationLevel = try_enum(VerificationLevel, guild.get('verification_level')) self.default_notifications: NotificationLevel = try_enum( NotificationLevel, guild.get('default_message_notifications') ) self.explicit_content_filter: ContentFilter = try_enum(ContentFilter, guild.get('explicit_content_filter', 0)) self.afk_timeout: int = guild.get('afk_timeout') - self._icon: Optional[str] = guild.get('icon') - self._banner: Optional[str] = guild.get('banner') self.unavailable: bool = guild.get('unavailable', False) - self.id: int = int(guild['id']) - self._roles: Dict[int, Role] = {} - state = self._state # speed up attribute access + + state = self._state # Speed up attribute access + for r in guild.get('roles', []): role = Role(guild=self, data=r, state=state) self._roles[role.id] = role + for c in guild.get('channels', []): + factory, _ = _guild_channel_factory(c['type']) + if factory: + self._add_channel(factory(guild=self, data=c, state=state)) + + for t in guild.get('threads', []): + self._add_thread(Thread(guild=self, state=self._state, data=t)) + + for s in guild.get('stage_instances', []): + stage_instance = StageInstance(guild=self, data=s, state=state) + self._stage_instances[stage_instance.id] = stage_instance + + for vs in guild.get('voice_states', []): + self._update_voice_state(vs, int(vs['channel_id'])) + self.mfa_level: MFALevel = guild.get('mfa_level') self.emojis: Tuple[Emoji, ...] = tuple(map(lambda d: state.store_emoji(self, d), guild.get('emojis', []))) self.stickers: Tuple[GuildSticker, ...] = tuple( map(lambda d: state.store_sticker(self, d), guild.get('stickers', [])) ) self.features: List[GuildFeature] = guild.get('features', []) + self._icon: Optional[str] = guild.get('icon') + self._banner: Optional[str] = guild.get('banner') self._splash: Optional[str] = guild.get('splash') self._system_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'system_channel_id') self.description: Optional[str] = guild.get('description') @@ -439,54 +442,29 @@ class Guild(Hashable): self._discovery_splash: Optional[str] = guild.get('discovery_splash') self._rules_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'rules_channel_id') self._public_updates_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'public_updates_channel_id') + self._afk_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'afk_channel_id') + self._widget_channel_id: Optional[int] = utils._get_as_snowflake(guild, 'widget_channel_id') self.nsfw_level: NSFWLevel = try_enum(NSFWLevel, guild.get('nsfw_level', 0)) + self._online_count = None - self._stage_instances: Dict[int, StageInstance] = {} - for s in guild.get('stage_instances', []): - stage_instance = StageInstance(guild=self, data=s, state=state) - self._stage_instances[stage_instance.id] = stage_instance - - cache_joined = self._state.member_cache_flags.joined - self_id = self._state.self_id - for mdata in guild.get('members', []): - member = Member(data=mdata, guild=self, state=state) - if cache_joined or member.id == self_id: - self._add_member(member) - - self._sync(guild) - self._large: Optional[bool] = None if member_count is None else self._member_count >= 250 - - self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') - self.afk_channel: Optional[VocalGuildChannel] = self.get_channel(utils._get_as_snowflake(guild, 'afk_channel_id')) # type: ignore - - for obj in guild.get('voice_states', []): - self._update_voice_state(obj, int(obj['channel_id'])) - - # TODO: refactor/remove? - def _sync(self, data: GuildPayload) -> None: - try: - self._large = data['large'] - except KeyError: - pass + for mdata in guild.get('merged_members', []): + try: + member = Member(data=mdata, guild=self, state=state) + except KeyError: + continue + self._add_member(member) empty_tuple = tuple() - for presence in data.get('presences', []): - user_id = int(presence['user']['id']) + for presence in guild.get('merged_presences', []): + user_id = int(presence['user_id']) member = self.get_member(user_id) if member is not None: - member._presence_update(presence, empty_tuple) # type: ignore + member._presence_update(presence, empty_tuple) - if 'channels' in data: - channels = data['channels'] - for c in channels: - factory, ch_type = _guild_channel_factory(c['type']) - if factory: - self._add_channel(factory(guild=self, data=c, state=self._state)) # type: ignore + large = None if member_count is None else member_count >= 250 + self._large: Optional[bool] = guild.get('large', large) - if 'threads' in data: - threads = data['threads'] - for thread in threads: - self._add_thread(Thread(guild=self, state=self._state, data=thread)) + self.owner_id: Optional[int] = utils._get_as_snowflake(guild, 'owner_id') @property def channels(self) -> List[GuildChannel]: @@ -543,7 +521,7 @@ class Guild(Hashable): This is essentially used to get the member version of yourself. """ self_id = self._state.user.id - # The self member is *always* cached + # We are *always* cached return self.get_member(self_id) # type: ignore @property @@ -625,7 +603,7 @@ class Guild(Hashable): Returns -------- Optional[Union[:class:`Thread`, :class:`.abc.GuildChannel`]] - The returned channel or thread or ``None`` if not found. + The returned channel, thread, or ``None`` if not found. """ return self._channels.get(channel_id) or self._threads.get(channel_id) @@ -704,6 +682,25 @@ class Guild(Hashable): channel_id = self._public_updates_channel_id return channel_id and self._channels.get(channel_id) # type: ignore + @property + def afk_channel(self) -> Optional[VocalGuildChannel]: + """Optional[:class:`VoiceChannel`]: Returns the guild channel AFK users are moved to. + + If no channel is set, then this returns ``None``. + """ + channel_id = self._afk_channel_id + return channel_id and self._channels.get(channel_id) # type: ignore + + @property + def widget_channel(self) -> Optional[GuildChannel]: + """Optional[:class:`TextChannel`]: Returns the channel the + widget will generate an invite to by default. + + If no channel is set, then this returns ``None``. + """ + channel_id = self._widget_channel_id + return channel_id and self._channels.get(channel_id) # type: ignore + @property def emoji_limit(self) -> int: """:class:`int`: The maximum number of emoji slots this guild has.""" @@ -796,19 +793,6 @@ class Guild(Hashable): return role return None - @property - def self_role(self) -> Optional[Role]: - """Optional[:class:`Role`]: Gets the role associated with this client's user, if any. - - .. versionadded:: 1.6 - """ - self_id = self._state.self_id - for role in self._roles.values(): - tags = role.tags - if tags and tags.bot_id == self_id: - return role - return None - @property def stage_instances(self) -> List[StageInstance]: """List[:class:`StageInstance`]: Returns a :class:`list` of the guild's stage instances that @@ -879,6 +863,13 @@ class Guild(Hashable): """ return self._member_count + @property + def online_count(self) -> Optional[int]: + """Optional[:class:`int`]: Returns the online member count. + This only exists after the first GUILD_MEMBER_LIST_UPDATE. + """ + return self._online_count + @property def chunked(self) -> bool: """:class:`bool`: Returns a boolean indicating if the guild is "chunked". @@ -926,16 +917,10 @@ class Guild(Hashable): then ``None`` is returned. """ - result = None members = self.members + if len(name) > 5 and name[-5] == '#': - # The 5 length is checking to see if #0000 is in the string, - # as a#0000 has a length of 6, the minimum for a potential - # discriminator lookup. potential_discriminator = name[-4:] - - # do the actual lookup and return if found - # if it isn't found then we'll do a full name lookup below. result = utils.get(members, name=name[:-5], discriminator=potential_discriminator) if result is not None: return result @@ -1296,7 +1281,7 @@ class Guild(Hashable): .. note:: - You cannot leave the guild that you own, you must delete it instead + You cannot leave a guild that you own, you must delete it instead via :meth:`delete`. Raises @@ -1346,6 +1331,7 @@ class Guild(Hashable): preferred_locale: str = MISSING, rules_channel: Optional[TextChannel] = MISSING, public_updates_channel: Optional[TextChannel] = MISSING, + features: List[str] = MISSING, ) -> Guild: r"""|coro| @@ -1369,7 +1355,7 @@ class Guild(Hashable): The new name of the guild. description: Optional[:class:`str`] The new description of the guild. Could be ``None`` for no description. - This is only available to guilds that contain ``PUBLIC`` in :attr:`Guild.features`. + This is only available to guilds that contain ``COMMUNITY`` in :attr:`Guild.features`. icon: :class:`bytes` A :term:`py:bytes-like object` representing the icon. Only PNG/JPEG is supported. GIF is only available to guilds that contain ``ANIMATED_ICON`` in :attr:`Guild.features`. @@ -1444,6 +1430,7 @@ class Guild(Hashable): mentioned in :meth:`Client.fetch_guild` and may not have full data. """ + # TODO: see what fields are sent no matter if they're changed or not http = self._state.http if vanity_code is not MISSING: @@ -1992,7 +1979,7 @@ class Guild(Hashable): """ await self._state.http.create_integration(self.id, type, id) - async def integrations(self) -> List[Integration]: + async def integrations(self, *, with_applications=True) -> List[Integration]: """|coro| Returns a list of all integrations attached to the guild. @@ -2002,6 +1989,11 @@ class Guild(Hashable): .. versionadded:: 1.4 + Parameters + ----------- + with_applications: :class:`bool` + Whether to include applications. + Raises ------- Forbidden @@ -2014,7 +2006,7 @@ class Guild(Hashable): List[:class:`Integration`] The list of integrations that are attached to the guild. """ - data = await self._state.http.get_all_integrations(self.id) + data = await self._state.http.get_all_integrations(self.id, with_applications) def convert(d): factory, _ = _integration_factory(d['type']) @@ -2784,7 +2776,7 @@ class Guild(Hashable): *, limit: int = 5, user_ids: Optional[List[int]] = None, - presences: bool = False, + presences: bool = True, cache: bool = True, ) -> List[Member]: """|coro| @@ -2805,7 +2797,7 @@ class Guild(Hashable): a number between 5 and 100. presences: :class:`bool` Whether to request for presences to be provided. This defaults - to ``False``. + to ``True``. .. versionadded:: 1.6 @@ -2878,7 +2870,8 @@ class Guild(Hashable): preferred_region: Optional[:class:`VoiceRegion`] The preferred region to connect to. """ - ws = self._state._get_websocket(self.id) + state = self._state + ws = state._get_websocket(self.id) channel_id = channel.id if channel else None if preferred_region is None or channel_id is None: