From a5bc8547f18276e67ead47ef829fbe91c047eab4 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Mon, 8 Jan 2024 18:06:42 +0100 Subject: [PATCH 01/25] Added member safety search information --- discord/enums.py | 5 +++ discord/guild.py | 65 +++++++++++++++++++++++++++++++- discord/http.py | 82 +++++++++++++++++++++++++++++++++++++++++ discord/member.py | 68 +++++++++++++++++++++++++++++++++- discord/types/member.py | 20 +++++++++- 5 files changed, 236 insertions(+), 4 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index ed498b8be190..e5b3593894ba 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -73,6 +73,7 @@ 'SKUType', 'EntitlementType', 'EntitlementOwnerType', + 'JoinType', ) if TYPE_CHECKING: @@ -804,6 +805,10 @@ class EntitlementOwnerType(Enum): guild = 1 user = 2 +class JoinType(Enum): + unknown = 0 + discovery = 3 + user_invite = 5 def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below diff --git a/discord/guild.py b/discord/guild.py index 82692ff73388..16c467bb8055 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -45,12 +45,13 @@ Tuple, Union, overload, + TypedDict, ) import warnings from . import utils, abc from .role import Role -from .member import Member, VoiceState +from .member import Member, VoiceState, MemberSearch from .emoji import Emoji from .errors import InvalidData from .permissions import PermissionOverwrite @@ -103,6 +104,8 @@ MISSING = utils.MISSING if TYPE_CHECKING: + from typing_extensions import Unpack + from .abc import Snowflake, SnowflakeTime from .types.guild import ( Ban as BanPayload, @@ -152,6 +155,14 @@ class _GuildLimit(NamedTuple): filesize: int +class MemberSearchQueries(TypedDict, total=False): + joined_after: datetime.datetime + users_ids: List[Union[Snowflake, int]] + roles: List[Union[Snowflake, int]] + timed_out_until: datetime.datetime + unusual_dms_until: datetime.datetime + + class Guild(Hashable): """Represents a Discord guild. @@ -4292,3 +4303,55 @@ async def create_automod_rule( ) return AutoModRule(data=data, guild=self, state=self._state) + + async def fetch_members_safety_information(self, *, limit: int = 15, **filters: Unpack[MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: + r"""|coro| + + Fetches all the members safety information with the applied filters. + + You must have :attr:`Permissions.manage_members` in order to + do this. + + Parameters + ---------- + limit: :class:`int` + The limit of members to fetch safety information. + **filters + Simple filters to sort members. If you provide + filters that none of the members satisfy, this + will return `None`. + + Simple filters table: + + +------------------+-----------------------------------------+ + | Parameter | Sort type | + +------------------+-----------------------------------------+ + | joined_after | Return users that joined after that date| + +------------------+-----------------------------------------+ + | timed_out_until | Return users timed out until that date | + +------------------+-----------------------------------------+ + | unusual_dms_until| Return users with unusual DM activities | + +------------------+-----------------------------------------+ + | users_ids | Return users that match any of the IDS | + +------------------+-----------------------------------------+ + | roles | Return users that have any of the roles | + +------------------+-----------------------------------------+ + + Raises + ------ + Forbidden + You do not have enough permissions to + fetch this information. + HTTPException + Fetching the information failed. + + Returns + ------- + Optional[Tuple[:class:`MemberSafety`, ...]] + The members that got fetched, or `None` if there + aren't or none of the filters were satisfied. + """ + + data = await self._state.http.get_guild_member_safety(self.id, limit, int(filters.pop('joined_after', self.created_at).timestamp()), self._state.self_id, **filters) + + return tuple([MemberSearch(data=member_data, guild=self, state=self._state) for member_data in data.get('members')]) if len(data.get('members')) > 0 else None diff --git a/discord/http.py b/discord/http.py index 69c5c779952a..6bf56e30ac56 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1550,6 +1550,23 @@ def get_members( def get_member(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberWithUser]: return self.request(Route('GET', '/guilds/{guild_id}/members/{member_id}', guild_id=guild_id, member_id=member_id)) + + def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake, after: int, bot_id: Snowflake) -> Response[member.MemberSearchResults]: + payload = {} + + payload['after'] = { + 'guild_joined_at': after, + 'user_id': str(bot_id) + } + payload['and_query'] = { + 'user_id': { + 'or_query': [str(member_id)] + } + } + payload['limit'] = 1 + payload['or_query'] = {} + + return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) def prune_members( self, @@ -1952,6 +1969,71 @@ def edit_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = None) -> Response[None]: return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) + + def get_guild_member_safety(self, guild_id: Snowflake, limit: int, joined_at: int, bot_id: int, **kwargs) -> Response[member.MemberSearchResults]: + payload = {} + payload['after'] = { + 'guild_joined_at': joined_at, + 'user_id': str(bot_id) + } + payload['limit'] = limit + payload['and_query'] = {} + payload['or_query'] = {} + + for key, value in kwargs.items(): + if value is Ellipsis: + continue + + if key == 'timed_out_until': + # This key's value is meant to be + # a datetime.datetime object + if 'safety_signals' not in payload['or_query'].keys(): + payload['or_query']['safety_signals'] = { + 'communication_disabled_until': { + 'range': { + 'gte': value.timestamp() + } + } + } + continue + + payload['or_query']['safety_signals']['communication_disabled_until'] = { + 'range': { + 'gte': value.timestamp() + } + } + + elif key == 'unusual_dms_until': + # This key's value is meant to be + # a datetime.datetime object + if 'safety_signals' not in payload['or_query'].keys(): + payload['or_query']['safety_signals'] = { + 'unusual_dm_activity_until': { + 'range': { + 'gte': value.timestamp() + } + } + } + continue + + payload['or_query']['safety_signals']['unusual_dm_activity_until'] = { + 'range': { + 'gte': value.timestamp() + } + } + + elif key == 'users_ids': + payload['and_query']['user_id'] = { + 'or_query': list((str(user.id) if not isinstance(user, int) else str(user)) for user in value) + } + + elif key == 'roles': + payload['and_query']['role_ids'] = { + 'and_query': list((str(role.id) if not isinstance(role, int) else str(role)) for role in value) + } + + + return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) # Guild scheduled event management diff --git a/discord/member.py b/discord/member.py index 71231e426ca1..09612f1aa25a 100644 --- a/discord/member.py +++ b/discord/member.py @@ -38,7 +38,7 @@ from .user import BaseUser, User, _UserTag from .activity import create_activity, ActivityTypes from .permissions import Permissions -from .enums import Status, try_enum +from .enums import Status, try_enum, JoinType from .errors import ClientException from .colour import Colour from .object import Object @@ -47,6 +47,7 @@ __all__ = ( 'VoiceState', 'Member', + 'MemberSearch', ) T = TypeVar('T', bound=type) @@ -65,6 +66,7 @@ MemberWithUser as MemberWithUserPayload, Member as MemberPayload, UserWithMember as UserWithMemberPayload, + MemberSearch as MemberSearchPayload, ) from .types.gateway import GuildMemberUpdateEvent from .types.user import User as UserPayload @@ -250,6 +252,37 @@ def general(self, *args, **kwargs): return cls +class MemberSearch: + """Represents a fetched member from member search. + + .. versionadded:: 2.4 + + Attributes + ---------- + member: :class:`Member` + The member this search if of. + source_invite_code: Optional[:class:`str`] + The Invite Code this user joined with. + join_type: :class:`JoinType` + The join type. + inviter: Optional[:class:`User`] + The inviter, or `None` if not cached or + not present. + """ + + __slots__ = ( + 'member', + 'source_invite_code', + 'join_type', + 'inviter', + ) + + def __init__(self, *, data: MemberSearchPayload, guild: Guild, state: ConnectionState) -> None: + self.member: Member = Member(data=data.get('member'), guild=guild, state=state) + self.source_invite_code: Optional[str] = data.get('source_invite_code') + self.join_type: JoinType = try_enum(JoinType, data.get('join_source_type')) + self.inviter: Optional[User] = state.get_user(int(data.get('inviter_id'))) if data.get('inviter_id') else None + @flatten_user class Member(discord.abc.Messageable, _UserTag): """Represents a Discord member to a :class:`Guild`. @@ -317,6 +350,7 @@ class Member(discord.abc.Messageable, _UserTag): 'pending', 'nick', 'timed_out_until', + 'unusual_dms_until', '_permissions', '_client_status', '_user', @@ -363,6 +397,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti self._permissions = None self.timed_out_until: Optional[datetime.datetime] = utils.parse_time(data.get('communication_disabled_until')) + self.unusual_dms_until: Optional[datetime.datetime] = utils.parse_time(data.get('unusual_dm_activity_until')) def __str__(self) -> str: return str(self._user) @@ -1011,6 +1046,37 @@ async def timeout( await self.edit(timed_out_until=timed_out_until, reason=reason) + async def fetch_safety_information(self) -> Optional[MemberSearch]: + r"""|coro| + + Fetches the safety information for this member. + + You must have :attr:`Permissions.manage_members` to + use this. + + Raises + ------ + Forbidden + You do not have permission to view member safety + information. + HTTPException + Fetching the information failed. + + Returns + ------- + Optional[:class:`MemberSafety`] + The member safety information, or `None` if there + isn't. + """ + + data = await self._state.http.get_member_safety_information(self.guild.id, self.id, int(self.guild.created_at.timestamp()), self._state.self_id) + member = data.get('members')[0] if len(data.get('members')) > 0 else None + + if not member: + return + + return MemberSearch(data=member, guild=self.guild, state=self._state) + async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None: r"""|coro| diff --git a/discord/types/member.py b/discord/types/member.py index ad9e49008a12..cc42aaec9999 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -22,10 +22,11 @@ DEALINGS IN THE SOFTWARE. """ -from typing import Optional, TypedDict -from .snowflake import SnowflakeList +from typing import Optional, TypedDict, Literal, List +from .snowflake import SnowflakeList, Snowflake from .user import User +JoinType = Literal[0, 3, 5] class Nickname(TypedDict): nick: str @@ -47,6 +48,7 @@ class Member(PartialMember, total=False): pending: bool permissions: str communication_disabled_until: str + unusual_dm_activity_until: Optional[str] class _OptionalMemberWithUser(PartialMember, total=False): @@ -64,3 +66,17 @@ class MemberWithUser(_OptionalMemberWithUser): class UserWithMember(User, total=False): member: _OptionalMemberWithUser + + +class MemberSearch(TypedDict): + member: MemberWithUser + source_invite_code: Optional[str] + join_source_type: JoinType + inviter_id: Optional[Snowflake] + + +class MemberSearchResults(TypedDict): + guild_id: Snowflake + members: List[MemberSearch] + page_result_count: int + total_result_count: int \ No newline at end of file From 69be2f1bf15224f57b56b8a52c892b6b85647c65 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Mon, 8 Jan 2024 18:38:12 +0100 Subject: [PATCH 02/25] Fixed Member Safety Information returning None --- discord/guild.py | 5 +---- discord/http.py | 8 ++++---- discord/member.py | 10 ++++++---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 16c467bb8055..8972c413c7c7 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -156,7 +156,6 @@ class _GuildLimit(NamedTuple): class MemberSearchQueries(TypedDict, total=False): - joined_after: datetime.datetime users_ids: List[Union[Snowflake, int]] roles: List[Union[Snowflake, int]] timed_out_until: datetime.datetime @@ -4326,8 +4325,6 @@ async def fetch_members_safety_information(self, *, limit: int = 15, **filters: +------------------+-----------------------------------------+ | Parameter | Sort type | +------------------+-----------------------------------------+ - | joined_after | Return users that joined after that date| - +------------------+-----------------------------------------+ | timed_out_until | Return users timed out until that date | +------------------+-----------------------------------------+ | unusual_dms_until| Return users with unusual DM activities | @@ -4352,6 +4349,6 @@ async def fetch_members_safety_information(self, *, limit: int = 15, **filters: aren't or none of the filters were satisfied. """ - data = await self._state.http.get_guild_member_safety(self.id, limit, int(filters.pop('joined_after', self.created_at).timestamp()), self._state.self_id, **filters) + data = await self._state.http.get_guild_member_safety(self.id, limit, self._state.self_id, **filters) return tuple([MemberSearch(data=member_data, guild=self, state=self._state) for member_data in data.get('members')]) if len(data.get('members')) > 0 else None diff --git a/discord/http.py b/discord/http.py index 6bf56e30ac56..4089b944ba64 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1551,11 +1551,11 @@ def get_members( def get_member(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberWithUser]: return self.request(Route('GET', '/guilds/{guild_id}/members/{member_id}', guild_id=guild_id, member_id=member_id)) - def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake, after: int, bot_id: Snowflake) -> Response[member.MemberSearchResults]: + def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake, bot_id: Snowflake) -> Response[member.MemberSearchResults]: payload = {} payload['after'] = { - 'guild_joined_at': after, + 'guild_joined_at': 1704734571433, 'user_id': str(bot_id) } payload['and_query'] = { @@ -1970,10 +1970,10 @@ def edit_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = None) -> Response[None]: return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) - def get_guild_member_safety(self, guild_id: Snowflake, limit: int, joined_at: int, bot_id: int, **kwargs) -> Response[member.MemberSearchResults]: + def get_guild_member_safety(self, guild_id: Snowflake, limit: int, bot_id: int, **kwargs) -> Response[member.MemberSearchResults]: payload = {} payload['after'] = { - 'guild_joined_at': joined_at, + 'guild_joined_at': 1704734571433, 'user_id': str(bot_id) } payload['limit'] = limit diff --git a/discord/member.py b/discord/member.py index 09612f1aa25a..5c7356ccf167 100644 --- a/discord/member.py +++ b/discord/member.py @@ -261,7 +261,7 @@ class MemberSearch: ---------- member: :class:`Member` The member this search if of. - source_invite_code: Optional[:class:`str`] + invite_code: Optional[:class:`str`] The Invite Code this user joined with. join_type: :class:`JoinType` The join type. @@ -272,14 +272,14 @@ class MemberSearch: __slots__ = ( 'member', - 'source_invite_code', + 'invite_code', 'join_type', 'inviter', ) def __init__(self, *, data: MemberSearchPayload, guild: Guild, state: ConnectionState) -> None: self.member: Member = Member(data=data.get('member'), guild=guild, state=state) - self.source_invite_code: Optional[str] = data.get('source_invite_code') + self.invite_code: Optional[str] = data.get('source_invite_code') self.join_type: JoinType = try_enum(JoinType, data.get('join_source_type')) self.inviter: Optional[User] = state.get_user(int(data.get('inviter_id'))) if data.get('inviter_id') else None @@ -1054,6 +1054,8 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: You must have :attr:`Permissions.manage_members` to use this. + .. versionadded:: 2.4 + Raises ------ Forbidden @@ -1069,7 +1071,7 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: isn't. """ - data = await self._state.http.get_member_safety_information(self.guild.id, self.id, int(self.guild.created_at.timestamp()), self._state.self_id) + data = await self._state.http.get_member_safety_information(self.guild.id, self.id, self._state.self_id) member = data.get('members')[0] if len(data.get('members')) > 0 else None if not member: From 99e64227f7153473e8ee50fcc1c21bbc6304ef0b Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Tue, 9 Jan 2024 19:05:03 +0100 Subject: [PATCH 03/25] Changed the docstring to show proper permissions --- discord/guild.py | 11 +++++++++-- discord/member.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 8972c413c7c7..a244849c1845 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4308,8 +4308,15 @@ async def fetch_members_safety_information(self, *, limit: int = 15, **filters: Fetches all the members safety information with the applied filters. - You must have :attr:`Permissions.manage_members` in order to - do this. + You must have __any__ of the following permissions: + + - :attr:`Permissions.administrator` + - :attr:`Permissions.manage_guild` + - :attr:`Permissions.manage_roles` + - :attr:`Permissions.manage_nicknames` + - :attr:`Permissions.ban_members` + - :attr:`Permissions.moderate_members` + - :attr:`Permissions.kick_members` Parameters ---------- diff --git a/discord/member.py b/discord/member.py index 5c7356ccf167..6638c217f035 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1051,8 +1051,15 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: Fetches the safety information for this member. - You must have :attr:`Permissions.manage_members` to - use this. + You must have __any__ of the following permissions: + + - :attr:`Permissions.administrator` + - :attr:`Permissions.manage_guild` + - :attr:`Permissions.manage_roles` + - :attr:`Permissions.manage_nicknames` + - :attr:`Permissions.ban_members` + - :attr:`Permissions.moderate_members` + - :attr:`Permissions.kick_members` .. versionadded:: 2.4 From 1acdffbe0301d774a8013cbf22a6a12506be7bb1 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 17 Mar 2024 20:00:59 +0100 Subject: [PATCH 04/25] Updated some things --- discord/enums.py | 18 +++++++- discord/guild.py | 33 ++++++++------ discord/http.py | 114 ++++++++++++++++++++++++---------------------- discord/member.py | 17 ++++--- 4 files changed, 104 insertions(+), 78 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index e5b3593894ba..9fffb3da95e8 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -73,7 +73,7 @@ 'SKUType', 'EntitlementType', 'EntitlementOwnerType', - 'JoinType', + 'MemberJoinType', ) if TYPE_CHECKING: @@ -805,11 +805,25 @@ class EntitlementOwnerType(Enum): guild = 1 user = 2 -class JoinType(Enum): +class MemberJoinType(Enum): unknown = 0 + bot = 1 + integration = 2 discovery = 3 + hub = 4 + invite = 5 + vanity = 6 + + # Aliases + app = 1 user_invite = 5 +class MemberSearchSortType(Enum): + new_guild_members = 1 + old_guild_members = 2 + new_discord_users = 3 + old_discord_users = 4 + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' diff --git a/discord/guild.py b/discord/guild.py index a244849c1845..73d8ce64db6e 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -62,6 +62,7 @@ from .channel import _threaded_guild_channel_factory from .enums import ( AuditLogAction, + MemberSearchSortType, VideoQualityMode, ChannelType, EntityType, @@ -155,11 +156,13 @@ class _GuildLimit(NamedTuple): filesize: int -class MemberSearchQueries(TypedDict, total=False): - users_ids: List[Union[Snowflake, int]] - roles: List[Union[Snowflake, int]] - timed_out_until: datetime.datetime - unusual_dms_until: datetime.datetime +class _MemberSearchQueries(TypedDict, total=False): + users: Snowflake + roles: Snowflake + unusual_activity: bool + quarantined: bool + timed_out: bool + unusual_dms: bool class Guild(Hashable): @@ -4303,7 +4306,7 @@ async def create_automod_rule( return AutoModRule(data=data, guild=self, state=self._state) - async def fetch_members_safety_information(self, *, limit: int = 15, **filters: Unpack[MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: + async def fetch_members_safety_information(self, *, limit: int = 250, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, **filters: Unpack[_MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: r"""|coro| Fetches all the members safety information with the applied filters. @@ -4322,21 +4325,25 @@ async def fetch_members_safety_information(self, *, limit: int = 15, **filters: ---------- limit: :class:`int` The limit of members to fetch safety information. + sort_type: :class:`MemberSearchSortType` + How to sort the results. **filters Simple filters to sort members. If you provide filters that none of the members satisfy, this will return `None`. - Simple filters table: - +------------------+-----------------------------------------+ | Parameter | Sort type | +------------------+-----------------------------------------+ - | timed_out_until | Return users timed out until that date | + | timed_out | Return users timed out until that date | + +------------------+-----------------------------------------+ + | unusual_dms | Return users with unusual DM activities | + +------------------+-----------------------------------------+ + | unusual_activity | Return users with unusual activity | +------------------+-----------------------------------------+ - | unusual_dms_until| Return users with unusual DM activities | + | quarantined | Returns users quarantined by AutoMod | +------------------+-----------------------------------------+ - | users_ids | Return users that match any of the IDS | + | users | Return users that match any of the IDS | +------------------+-----------------------------------------+ | roles | Return users that have any of the roles | +------------------+-----------------------------------------+ @@ -4356,6 +4363,6 @@ async def fetch_members_safety_information(self, *, limit: int = 15, **filters: aren't or none of the filters were satisfied. """ - data = await self._state.http.get_guild_member_safety(self.id, limit, self._state.self_id, **filters) + data = await self._state.http.get_guild_member_safety(self.id, limit, sort_type.value, **filters) - return tuple([MemberSearch(data=member_data, guild=self, state=self._state) for member_data in data.get('members')]) if len(data.get('members')) > 0 else None + return tuple([MemberSearch(data=member_data, guild=self, state=self._state) for member_data in data.get('members')]) if data.get('total_result_count') > 0 else None diff --git a/discord/http.py b/discord/http.py index 4089b944ba64..d2273d8d49f6 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1551,19 +1551,16 @@ def get_members( def get_member(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberWithUser]: return self.request(Route('GET', '/guilds/{guild_id}/members/{member_id}', guild_id=guild_id, member_id=member_id)) - def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake, bot_id: Snowflake) -> Response[member.MemberSearchResults]: - payload = {} - - payload['after'] = { - 'guild_joined_at': 1704734571433, - 'user_id': str(bot_id) + def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberSearchResults]: + payload: Dict[str, Union[int, Dict[str, Union[str, int, Dict]]]] = { + 'sort': 1, # This is the default value (Newest Guild Members First), and as it will only return 1 value it doesn't matter + 'limit': 250, # This value can't be changed } payload['and_query'] = { 'user_id': { 'or_query': [str(member_id)] } } - payload['limit'] = 1 payload['or_query'] = {} return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) @@ -1970,68 +1967,77 @@ def edit_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = None) -> Response[None]: return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) - def get_guild_member_safety(self, guild_id: Snowflake, limit: int, bot_id: int, **kwargs) -> Response[member.MemberSearchResults]: - payload = {} - payload['after'] = { - 'guild_joined_at': 1704734571433, - 'user_id': str(bot_id) + def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: int, **kwargs) -> Response[member.MemberSearchResults]: + gte: int = utils.time_snowflake(datetime.datetime.now(), high=False) + payload: Dict = { + 'sort': sort_type } + payload['limit'] = limit payload['and_query'] = {} payload['or_query'] = {} for key, value in kwargs.items(): - if value is Ellipsis: + if value is None or value is False: continue - if key == 'timed_out_until': - # This key's value is meant to be - # a datetime.datetime object - if 'safety_signals' not in payload['or_query'].keys(): - payload['or_query']['safety_signals'] = { - 'communication_disabled_until': { - 'range': { - 'gte': value.timestamp() - } - } - } - continue + if key == 'timed_out': + signals = payload['or_query'].get('safety_signals', None) + if not signals: + payload['or_query'].update(safety_signals={}) + signals = payload['or_query']['safety_signals'] - payload['or_query']['safety_signals']['communication_disabled_until'] = { - 'range': { - 'gte': value.timestamp() + signals.update( + communication_disabled_until={ + 'range': {'gte': guild_id} } - } - - elif key == 'unusual_dms_until': - # This key's value is meant to be - # a datetime.datetime object - if 'safety_signals' not in payload['or_query'].keys(): - payload['or_query']['safety_signals'] = { - 'unusual_dm_activity_until': { - 'range': { - 'gte': value.timestamp() - } - } + ) + + elif key == 'unusual_dms': + signals = payload['or_query'].get('safety_signals', None) + if not signals: + payload['or_query'].update(safety_signals={}) + signals = payload['or_query']['safety_signals'] + + signals.update( + unusual_dm_activity_until={ + 'range': {'gte': guild_id} } - continue + ) - payload['or_query']['safety_signals']['unusual_dm_activity_until'] = { - 'range': { - 'gte': value.timestamp() - } - } + elif key == 'unusual_activity': + signals = payload['or_query'].get('safety_signals', None) + if not signals: + payload['or_query'].update(safety_signals={}) + signals = payload['or_query']['safety_signals'] + + signals.update( + unusual_account_activity=value + ) + + elif key == 'quarantined': + signals = payload['or_query'].get('safety_signals', None) + if not signals: + payload['or_query'].update(safety_signals={}) + signals = payload['or_query']['safety_signals'] + + signals.update( + automod_quarantined_username=value + ) - elif key == 'users_ids': - payload['and_query']['user_id'] = { - 'or_query': list((str(user.id) if not isinstance(user, int) else str(user)) for user in value) - } + elif key == 'users': + payload['and_query'].update( + user_id={ + 'or_query': [str(user.id) for user in value] + } + ) elif key == 'roles': - payload['and_query']['role_ids'] = { - 'and_query': list((str(role.id) if not isinstance(role, int) else str(role)) for role in value) - } - + payload['and_query'].update( + role_ids={ + 'and_query': [str(role.id) for role in value] + } + ) return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) diff --git a/discord/member.py b/discord/member.py index 6638c217f035..37125f6f21b0 100644 --- a/discord/member.py +++ b/discord/member.py @@ -38,7 +38,7 @@ from .user import BaseUser, User, _UserTag from .activity import create_activity, ActivityTypes from .permissions import Permissions -from .enums import Status, try_enum, JoinType +from .enums import Status, MemberJoinType, try_enum from .errors import ClientException from .colour import Colour from .object import Object @@ -280,8 +280,8 @@ class MemberSearch: def __init__(self, *, data: MemberSearchPayload, guild: Guild, state: ConnectionState) -> None: self.member: Member = Member(data=data.get('member'), guild=guild, state=state) self.invite_code: Optional[str] = data.get('source_invite_code') - self.join_type: JoinType = try_enum(JoinType, data.get('join_source_type')) - self.inviter: Optional[User] = state.get_user(int(data.get('inviter_id'))) if data.get('inviter_id') else None + self.join_type: MemberJoinType = try_enum(MemberJoinType, data.get('join_source_type')) + self.inviter: Optional[User] = state.get_user(int(data.get('inviter_id'))) if data.get('inviter_id') else None # type: ignore # Checker complains that 'inviter_id' could be None @flatten_user class Member(discord.abc.Messageable, _UserTag): @@ -1046,7 +1046,7 @@ async def timeout( await self.edit(timed_out_until=timed_out_until, reason=reason) - async def fetch_safety_information(self) -> Optional[MemberSearch]: + async def fetch_safety_information(self, /) -> Optional[MemberSearch]: r"""|coro| Fetches the safety information for this member. @@ -1078,13 +1078,12 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: isn't. """ - data = await self._state.http.get_member_safety_information(self.guild.id, self.id, self._state.self_id) - member = data.get('members')[0] if len(data.get('members')) > 0 else None - - if not member: + data = await self._state.http.get_member_safety_information(self.guild.id, self.id) + + if data.get('total_result_count') <= 0: return - return MemberSearch(data=member, guild=self.guild, state=self._state) + return MemberSearch(data=data.get('members')[0], guild=self.guild, state=self._state) async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None: r"""|coro| From 5ad1684b771f2799e462edf553667b924239f1b5 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Wed, 20 Mar 2024 10:46:08 +0100 Subject: [PATCH 05/25] Added source invite filters --- discord/guild.py | 4 +--- discord/http.py | 7 +++++++ discord/member.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index d3b208607822..b40ae47e74e5 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -170,6 +170,7 @@ class _MemberSearchQueries(TypedDict, total=False): quarantined: bool timed_out: bool unusual_dms: bool + invite_codes: List[Union[str, Invite]] class Guild(Hashable): @@ -4404,7 +4405,6 @@ async def create_automod_rule( ) return AutoModRule(data=data, guild=self, state=self._state) -<<<<<<< HEAD async def fetch_members_safety_information(self, *, limit: int = 250, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, **filters: Unpack[_MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: r"""|coro| @@ -4466,7 +4466,6 @@ async def fetch_members_safety_information(self, *, limit: int = 250, sort_type: data = await self._state.http.get_guild_member_safety(self.id, limit, sort_type.value, **filters) return tuple([MemberSearch(data=member_data, guild=self, state=self._state) for member_data in data.get('members')]) if data.get('total_result_count') > 0 else None -======= @property def invites_paused_until(self) -> Optional[datetime.datetime]: @@ -4511,4 +4510,3 @@ def dms_paused(self) -> bool: return False return self.dms_paused_until > utils.utcnow() ->>>>>>> 0e016be42ca4f34bb89761261a1c7c12f4cc8c48 diff --git a/discord/http.py b/discord/http.py index a8a9c510cf9b..83a3275bc7fd 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2056,6 +2056,13 @@ def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: in } ) + elif key == 'invite_codes': + payload['and_query'].update( + source_invite_code={ + 'or_query': [invite.code for invite in value if hasattr(invite, 'code') else invite] + } + ) + return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) # Guild scheduled event management diff --git a/discord/member.py b/discord/member.py index 5c612d4394c5..c2a6e868459c 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1103,7 +1103,7 @@ async def fetch_safety_information(self, /) -> Optional[MemberSearch]: data = await self._state.http.get_member_safety_information(self.guild.id, self.id) - if data.get('total_result_count') <= 0: + if data.get('total_result_count') == 0: return return MemberSearch(data=data.get('members')[0], guild=self.guild, state=self._state) From 4a6098112c0f9ed3c27a4a6f614deadbe694825b Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Wed, 20 Mar 2024 10:49:13 +0100 Subject: [PATCH 06/25] Updated filters table in Guild.fetch_members_safety_information --- discord/guild.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index b40ae47e74e5..9e78e3ff5c20 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4421,6 +4421,8 @@ async def fetch_members_safety_information(self, *, limit: int = 250, sort_type: - :attr:`Permissions.moderate_members` - :attr:`Permissions.kick_members` + .. versionadded:: 2.4 + Parameters ---------- limit: :class:`int` @@ -4447,6 +4449,8 @@ async def fetch_members_safety_information(self, *, limit: int = 250, sort_type: +------------------+-----------------------------------------+ | roles | Return users that have any of the roles | +------------------+-----------------------------------------+ + | invite_codes | Return users joined via these invites | + +------------------+-----------------------------------------+ Raises ------ From fca336cdabe19aa0dbdd05669c3c5b5283d1b426 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:14:58 +0200 Subject: [PATCH 07/25] Rewritten MemberSearch --- discord/member.py | 60 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/discord/member.py b/discord/member.py index c2a6e868459c..59fd36cdf669 100644 --- a/discord/member.py +++ b/discord/member.py @@ -259,29 +259,60 @@ class MemberSearch: Attributes ---------- - member: :class:`Member` - The member this search if of. invite_code: Optional[:class:`str`] The Invite Code this user joined with. join_type: :class:`JoinType` The join type. - inviter: Optional[:class:`User`] - The inviter, or `None` if not cached or - not present. """ __slots__ = ( - 'member', + '_resolved_data', 'invite_code', 'join_type', - 'inviter', + '_inviter_id', + + '_guild', + '_state', ) def __init__(self, *, data: MemberSearchPayload, guild: Guild, state: ConnectionState) -> None: - self.member: Member = Member(data=data.get('member'), guild=guild, state=state) + self._resolved_data = data['member'] self.invite_code: Optional[str] = data.get('source_invite_code') - self.join_type: MemberJoinType = try_enum(MemberJoinType, data.get('join_source_type')) - self.inviter: Optional[User] = state.get_user(int(data.get('inviter_id'))) if data.get('inviter_id') else None # type: ignore # Checker complains that 'inviter_id' could be None + self.join_type: MemberJoinType = try_enum(MemberJoinType, data['join_source_type']) + try: + self._inviter_id = data['inviter_id'] + except KeyError: + self._inviter_id = None + + self._guild = guild + self._state = state + + def __repr__(self) -> str: + if self.inviter: + return f'' + return f'' + + @property + def resolved(self) -> Member: + """:class:`Member`: Returns the resolved member object this search if of.""" + return Member(data=self._resolved_data, guild=self._guild, state=self._state) + + @property + def inviter(self) -> Optional[Union[Member, User]]: + """Optional[Union[:class:`Member`, :class:`User`]]: Returns the resolved inviter, or ``None`` + if not cached. + """ + if not self.inviter_id: + return + return self._guild.get_member(self.inviter_id) or self._state.get_user(self.inviter_id) + + @property + def inviter_id(self) -> Optional[int]: + """Optional[:class:`int`]: Returns this member\'s inviter ID""" + if not self._inviter_id: + return + return int(self._inviter_id) + @flatten_user class Member(discord.abc.Messageable, _UserTag): @@ -1102,11 +1133,12 @@ async def fetch_safety_information(self, /) -> Optional[MemberSearch]: """ data = await self._state.http.get_member_safety_information(self.guild.id, self.id) - - if data.get('total_result_count') == 0: + + if data['total_result_count'] == 0: return - - return MemberSearch(data=data.get('members')[0], guild=self.guild, state=self._state) + + # We use data['members'][0] because there will only be 1 result + return MemberSearch(data=data['members'][0], guild=self.guild, state=self._state) async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None: r"""|coro| From 6a1c222be01763b5ac54df2db01a4d946420f1b0 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:15:20 +0200 Subject: [PATCH 08/25] fetch_members_safety_information -> fetch_safety_information --- discord/guild.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/guild.py b/discord/guild.py index 6e94afd61d90..05e0362456b6 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4406,7 +4406,7 @@ async def create_automod_rule( return AutoModRule(data=data, guild=self, state=self._state) - async def fetch_members_safety_information(self, *, limit: int = 250, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, **filters: Unpack[_MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: + async def fetch_safety_information(self, *, limit: int = 250, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, **filters: Unpack[_MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: r"""|coro| Fetches all the members safety information with the applied filters. From 39435d810947836af93687275e5b0a17a8c923d9 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:15:49 +0200 Subject: [PATCH 09/25] Edited get_guild_member_safety --- discord/http.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/discord/http.py b/discord/http.py index 83a3275bc7fd..43e14b545827 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1985,7 +1985,7 @@ def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: int, **kwargs) -> Response[member.MemberSearchResults]: - gte: int = utils.time_snowflake(datetime.datetime.now(), high=False) + gte: int = utils.time_snowflake(datetime.datetime.now(), high=True) + 1 payload: Dict = { 'sort': sort_type } @@ -2006,7 +2006,7 @@ def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: in signals.update( communication_disabled_until={ - 'range': {'gte': guild_id} + 'range': {'gte': gte} } ) @@ -2018,7 +2018,7 @@ def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: in signals.update( unusual_dm_activity_until={ - 'range': {'gte': guild_id} + 'range': {'gte': gte} } ) @@ -2059,7 +2059,10 @@ def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: in elif key == 'invite_codes': payload['and_query'].update( source_invite_code={ - 'or_query': [invite.code for invite in value if hasattr(invite, 'code') else invite] + 'or_query': [ + invite.code if not isinstance(invite, str) + else invite for invite in value + ] } ) From 50a5357878bf7914ae671ca9970128034ddfbd38 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:16:11 +0200 Subject: [PATCH 10/25] Added newline (pyright) --- discord/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/enums.py b/discord/enums.py index 4033253bc43b..0edbec50dee2 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -828,6 +828,7 @@ class MemberSearchSortType(Enum): new_discord_users = 3 old_discord_users = 4 + def create_unknown_value(cls: Type[E], val: Any) -> E: value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below name = f'unknown_{val}' From 7f6f40f3264694dbc279a7eb228bea8768dfcc4c Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:16:34 +0200 Subject: [PATCH 11/25] More newlines --- discord/enums.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/enums.py b/discord/enums.py index 0edbec50dee2..d039afeb956a 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -809,6 +809,7 @@ class EntitlementOwnerType(Enum): guild = 1 user = 2 + class MemberJoinType(Enum): unknown = 0 bot = 1 @@ -822,6 +823,7 @@ class MemberJoinType(Enum): app = 1 user_invite = 5 + class MemberSearchSortType(Enum): new_guild_members = 1 old_guild_members = 2 From b8eca3675d8566343c6a9bcefd28b83dd22b350b Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:41:11 +0200 Subject: [PATCH 12/25] Reworked get_guild_member_safety --- discord/http.py | 115 +++++++++++++++++++++--------------------------- 1 file changed, 49 insertions(+), 66 deletions(-) diff --git a/discord/http.py b/discord/http.py index 43e14b545827..23766623bb74 100644 --- a/discord/http.py +++ b/discord/http.py @@ -68,6 +68,7 @@ from .embeds import Embed from .message import Attachment from .flags import MessageFlags + from .invite import Invite from .types import ( appinfo, @@ -1984,7 +1985,21 @@ def edit_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = None) -> Response[None]: return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) - def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: int, **kwargs) -> Response[member.MemberSearchResults]: + def get_guild_member_safety( + self, + guild_id: Snowflake, + limit: int, + sort_type: int, + *, + users: Optional[Iterable[Snowflake]] = None, + roles: Optional[Iterable[Snowflake]] = None, + unusual_activity: Optional[bool] = None, + quarantined: Optional[bool] = None, + timed_out: Optional[bool] = None, + unusual_dms: Optional[bool] = None, + invite_codes: Optional[Iterable[Union[str, Invite]]] = None, + join_types: Optional[Iterable[int]] = None, + ) -> Response[member.MemberSearchResults]: gte: int = utils.time_snowflake(datetime.datetime.now(), high=True) + 1 payload: Dict = { 'sort': sort_type @@ -1993,78 +2008,46 @@ def get_guild_member_safety(self, guild_id: Snowflake, limit: int, sort_type: in payload['limit'] = limit payload['and_query'] = {} payload['or_query'] = {} + safety_signals = {} - for key, value in kwargs.items(): - if value is None or value is False: - continue + if users: + payload['and_query']['user_id'] = { + 'or_query': [str(user) for user in users] + } - if key == 'timed_out': - signals = payload['or_query'].get('safety_signals', None) - if not signals: - payload['or_query'].update(safety_signals={}) - signals = payload['or_query']['safety_signals'] + if roles: + payload['and_query']['role_ids'] = { + 'and_query': [str(role) for role in roles] + } - signals.update( - communication_disabled_until={ - 'range': {'gte': gte} - } - ) - - elif key == 'unusual_dms': - signals = payload['or_query'].get('safety_signals', None) - if not signals: - payload['or_query'].update(safety_signals={}) - signals = payload['or_query']['safety_signals'] - - signals.update( - unusual_dm_activity_until={ - 'range': {'gte': gte} - } - ) + if unusual_activity: + safety_signals['unusual_account_activity'] = unusual_activity - elif key == 'unusual_activity': - signals = payload['or_query'].get('safety_signals', None) - if not signals: - payload['or_query'].update(safety_signals={}) - signals = payload['or_query']['safety_signals'] - - signals.update( - unusual_account_activity=value - ) - - elif key == 'quarantined': - signals = payload['or_query'].get('safety_signals', None) - if not signals: - payload['or_query'].update(safety_signals={}) - signals = payload['or_query']['safety_signals'] - - signals.update( - automod_quarantined_username=value - ) + if quarantined: + safety_signals['automod_quarantined_username'] = quarantined - elif key == 'users': - payload['and_query'].update( - user_id={ - 'or_query': [str(user.id) for user in value] - } - ) + if timed_out: + safety_signals['communication_disabled_until'] = { + 'range': {'gte': gte} + } - elif key == 'roles': - payload['and_query'].update( - role_ids={ - 'and_query': [str(role.id) for role in value] - } - ) + if unusual_dms: + safety_signals['unusual_dm_activity_until'] = { + 'range': {'gte': gte} + } - elif key == 'invite_codes': - payload['and_query'].update( - source_invite_code={ - 'or_query': [ - invite.code if not isinstance(invite, str) - else invite for invite in value - ] - } - ) + if invite_codes: + payload['and_query']['source_invite_code'] = { + 'or_query': [ + getattr(invite, 'code', str(invite)) + for invite in invite_codes + ] + } + + if join_types: + payload['and_query']['join_source_type'] = { + 'or_query': join_types + } return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) From 4a6e0c6a79af6160bacebfbf48f6e27e5d218328 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:41:36 +0200 Subject: [PATCH 13/25] fetch_safety_information | Return Type: Tuple -> AsyncIterator --- discord/guild.py | 53 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 05e0362456b6..304fb63507a4 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -44,9 +44,9 @@ Optional, TYPE_CHECKING, Tuple, + TypedDict, Union, overload, - TypedDict, ) import warnings @@ -63,7 +63,6 @@ from .channel import _threaded_guild_channel_factory from .enums import ( AuditLogAction, - MemberSearchSortType, VideoQualityMode, ChannelType, EntityType, @@ -78,6 +77,8 @@ AutoModRuleEventType, ForumOrderType, ForumLayoutType, + MemberJoinType, + MemberSearchSortType, ) from .mixins import Hashable from .user import User @@ -164,13 +165,14 @@ class _GuildLimit(NamedTuple): class _MemberSearchQueries(TypedDict, total=False): - users: Snowflake - roles: Snowflake + users: Iterable[Snowflake] + roles: Iterable[Snowflake] unusual_activity: bool quarantined: bool timed_out: bool unusual_dms: bool - invite_codes: List[Union[str, Invite]] + invite_codes: Iterable[Union[str, Invite]] + join_types: Iterable[MemberJoinType] class Guild(Hashable): @@ -4406,10 +4408,19 @@ async def create_automod_rule( return AutoModRule(data=data, guild=self, state=self._state) - async def fetch_safety_information(self, *, limit: int = 250, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, **filters: Unpack[_MemberSearchQueries]) -> Optional[Tuple[MemberSearch, ...]]: - r"""|coro| - - Fetches all the members safety information with the applied filters. + async def fetch_safety_information( + self, + *, + limit: int = 250, + sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, + **filters: Unpack[_MemberSearchQueries] + ) -> AsyncIterator[MemberSearch]: + """Returns a :term:`asynchronous iterator` representing the members that were + obtained after the search. + + The ``limit`` parameter must be a :class:`int` + and represents the maxmimum amount of members to return, + not the ones that are expected. You must have __any__ of the following permissions: @@ -4451,6 +4462,8 @@ async def fetch_safety_information(self, *, limit: int = 250, sort_type: MemberS +------------------+-----------------------------------------+ | invite_codes | Return users joined via these invites | +------------------+-----------------------------------------+ + | join_types | Return users who join these ways | + +------------------+-----------------------------------------+ Raises ------ @@ -4467,9 +4480,27 @@ async def fetch_safety_information(self, *, limit: int = 250, sort_type: MemberS aren't or none of the filters were satisfied. """ - data = await self._state.http.get_guild_member_safety(self.id, limit, sort_type.value, **filters) + users = filters.get('users') + if users: + filters['users'] = [user.id for user in users] # type: ignore + + roles = filters.get('roles') + if roles: + filters['roles'] = [role.id for role in roles] # type: ignore + + join_types = filters.get('join_types') + if join_types: + filters['join_types'] = [join_type.value for join_type in join_types] # type: ignore + + data = await self._state.http.get_guild_member_safety( + self.id, + limit, + sort_type.value, + **filters, # type: ignore + ) - return tuple([MemberSearch(data=member_data, guild=self, state=self._state) for member_data in data.get('members')]) if data.get('total_result_count') > 0 else None + for result in data['members']: + yield MemberSearch(data=result, guild=self, state=self._state) @property def invites_paused_until(self) -> Optional[datetime.datetime]: From 24685631546e09cc40cf35787b68f728b63111a5 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 11:42:17 +0200 Subject: [PATCH 14/25] Updated fetch_safety_information docstring --- discord/guild.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 304fb63507a4..a9a8e0793853 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4473,11 +4473,10 @@ async def fetch_safety_information( HTTPException Fetching the information failed. - Returns - ------- - Optional[Tuple[:class:`MemberSafety`, ...]] - The members that got fetched, or `None` if there - aren't or none of the filters were satisfied. + Yields + ------ + :class:`MemberSearch` + The safety information of the members. """ users = filters.get('users') From 0457947e35419cb0825bb1379041569a6c52eea1 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 12:11:29 +0200 Subject: [PATCH 15/25] Added support for username search --- discord/http.py | 55 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/discord/http.py b/discord/http.py index 23766623bb74..6d6878aa5a3f 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1991,6 +1991,8 @@ def get_guild_member_safety( limit: int, sort_type: int, *, + after: Optional[Snowflake] = None, + before: Optional[Snowflake] = None, users: Optional[Iterable[Snowflake]] = None, roles: Optional[Iterable[Snowflake]] = None, unusual_activity: Optional[bool] = None, @@ -1999,9 +2001,12 @@ def get_guild_member_safety( unusual_dms: Optional[bool] = None, invite_codes: Optional[Iterable[Union[str, Invite]]] = None, join_types: Optional[Iterable[int]] = None, + usernames: Optional[Iterable[str]] = None, ) -> Response[member.MemberSearchResults]: - gte: int = utils.time_snowflake(datetime.datetime.now(), high=True) + 1 - payload: Dict = { + gte = utils.time_snowflake(datetime.datetime.now(), high=True) + 1 + users_gte: Optional[str] = str(after) if after else None + users_lte: Optional[str] = str(before) if before else None + payload: Dict[Any, Any] = { 'sort': sort_type } @@ -2011,9 +2016,44 @@ def get_guild_member_safety( safety_signals = {} if users: - payload['and_query']['user_id'] = { - 'or_query': [str(user) for user in users] - } + if 'user_id' in payload['and_query']: + payload['and_query']['user_id']['or_query'] = [ + str(user) for user in users + ] + else: + payload['and_query']['user_id'] = { + 'or_query': [str(user) for user in users] + } + + if users_gte: + if 'user_id' in payload['and_query']: + if 'range' in payload['and_query']['user_id']: + payload['and_query']['user_id']['range']['gte'] = users_gte # type: ignore + else: + payload['and_query']['user_id']['range'] = { # type: ignore + 'gte': users_gte + } + else: + payload['and_query']['user_id'] = { + 'range': { + 'gte': users_gte + } + } + + if users_lte: + if 'user_id' in payload['and_query']: + if 'range' in payload['and_query']['user_id']: + payload['and_query']['user_id']['range']['lte'] = users_lte # type: ignore + else: + payload['and_query']['user_id']['range'] = { # type: ignore + 'lte': users_lte + } + else: + payload['and_query']['user_id'] = { + 'range': { + 'lte': users_lte + } + } if roles: payload['and_query']['role_ids'] = { @@ -2049,6 +2089,11 @@ def get_guild_member_safety( 'or_query': join_types } + if usernames: + payload['and_query']['usernames'] = { + 'or_query': list(usernames) + } + return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) # Guild scheduled event management From abae9a65736549a4c6c2acb8a964df12142fc065 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 21 Apr 2024 12:12:26 +0200 Subject: [PATCH 16/25] Changes to fetch_safety_information + Added 'usernames' search filter / Reworked the function to exhaust the route until no more objects are recieved --- discord/guild.py | 50 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index a9a8e0793853..af3ff71cc57d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -173,6 +173,7 @@ class _MemberSearchQueries(TypedDict, total=False): unusual_dms: bool invite_codes: Iterable[Union[str, Invite]] join_types: Iterable[MemberJoinType] + usernames: Iterable[str] class Guild(Hashable): @@ -4412,15 +4413,16 @@ async def fetch_safety_information( self, *, limit: int = 250, + after: Optional[Snowflake] = None, + before: Optional[Snowflake] = None, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, **filters: Unpack[_MemberSearchQueries] ) -> AsyncIterator[MemberSearch]: """Returns a :term:`asynchronous iterator` representing the members that were obtained after the search. - The ``limit`` parameter must be a :class:`int` - and represents the maxmimum amount of members to return, - not the ones that are expected. + The ``after`` and ``before`` parameters must represent + a member and meet the :class:`abc.Snowflake` abc. You must have __any__ of the following permissions: @@ -4464,6 +4466,8 @@ async def fetch_safety_information( +------------------+-----------------------------------------+ | join_types | Return users who join these ways | +------------------+-----------------------------------------+ + | usernames | Return users with similar names | + +------------------+-----------------------------------------+ Raises ------ @@ -4491,15 +4495,39 @@ async def fetch_safety_information( if join_types: filters['join_types'] = [join_type.value for join_type in join_types] # type: ignore - data = await self._state.http.get_guild_member_safety( - self.id, - limit, - sort_type.value, - **filters, # type: ignore - ) + while limit > 0: + retrieve = min(limit, 250) + + state = self._state + after_id = after.id if after else None + before_id = before.id if before else None + + data = await state.http.get_guild_member_safety( + self.id, + limit=retrieve, + sort_type=sort_type.value, + after=after_id, + before=before_id, + **filters # type: ignore + ) + + results = data['members'] + + if len(results) == 0: + # No more members, break + break + + limit -= len(results) + after = Object(id=int(results[-1]['member']['user']['id'])) + + for result in reversed(results): + yield MemberSearch(data=result, guild=self, state=state) - for result in data['members']: - yield MemberSearch(data=result, guild=self, state=self._state) + if before_id: + if after.id > before_id: + # We cannot fetch users if the before requirements is less than + # the after requirement. + break @property def invites_paused_until(self) -> Optional[datetime.datetime]: From 2898271954d967790c4189edc6842cfa022a895e Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 12 May 2024 13:04:26 +0200 Subject: [PATCH 17/25] Rewrite get_guild_safety --- discord/guild.py | 108 +++++++++++++++++++++-------------------------- discord/http.py | 44 +++++++++---------- 2 files changed, 70 insertions(+), 82 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index af3ff71cc57d..55a320d71027 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -141,6 +141,7 @@ from .types.widget import EditWidgetSettings from .types.audit_log import AuditLogEvent from .message import EmojiInputType + from .invite import Invite VocalGuildChannel = Union[VoiceChannel, StageChannel] GuildChannel = Union[VocalGuildChannel, ForumChannel, TextChannel, CategoryChannel] @@ -164,18 +165,6 @@ class _GuildLimit(NamedTuple): filesize: int -class _MemberSearchQueries(TypedDict, total=False): - users: Iterable[Snowflake] - roles: Iterable[Snowflake] - unusual_activity: bool - quarantined: bool - timed_out: bool - unusual_dms: bool - invite_codes: Iterable[Union[str, Invite]] - join_types: Iterable[MemberJoinType] - usernames: Iterable[str] - - class Guild(Hashable): """Represents a Discord guild. @@ -4412,11 +4401,19 @@ async def create_automod_rule( async def fetch_safety_information( self, *, - limit: int = 250, - after: Optional[Snowflake] = None, - before: Optional[Snowflake] = None, + limit: int = MISSING, + after: Snowflake = MISSING, + before: Snowflake = MISSING, sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, - **filters: Unpack[_MemberSearchQueries] + timed_out: bool = MISSING, + unusual_dms: bool = MISSING, + unusual_activity: bool = MISSING, + quarantined: bool = MISSING, + users: Iterable[Snowflake] = MISSING, + roles: Iterable[Snowflake] = MISSING, + invites: Iterable[Union[str, Invite]] = MISSING, + join_types: Iterable[MemberJoinType] = MISSING, + usernames: Iterable[str] = MISSING, ) -> AsyncIterator[MemberSearch]: """Returns a :term:`asynchronous iterator` representing the members that were obtained after the search. @@ -4440,35 +4437,31 @@ async def fetch_safety_information( ---------- limit: :class:`int` The limit of members to fetch safety information. + after: :class:`abc.Snowflake` + Return members after this ID. + before: :class:`abc.Snowflake` + Return member before this ID. sort_type: :class:`MemberSearchSortType` - How to sort the results. - **filters - Simple filters to sort members. If you provide - filters that none of the members satisfy, this - will return `None`. - - +------------------+-----------------------------------------+ - | Parameter | Sort type | - +------------------+-----------------------------------------+ - | timed_out | Return users timed out until that date | - +------------------+-----------------------------------------+ - | unusual_dms | Return users with unusual DM activities | - +------------------+-----------------------------------------+ - | unusual_activity | Return users with unusual activity | - +------------------+-----------------------------------------+ - | quarantined | Returns users quarantined by AutoMod | - +------------------+-----------------------------------------+ - | users | Return users that match any of the IDS | - +------------------+-----------------------------------------+ - | roles | Return users that have any of the roles | - +------------------+-----------------------------------------+ - | invite_codes | Return users joined via these invites | - +------------------+-----------------------------------------+ - | join_types | Return users who join these ways | - +------------------+-----------------------------------------+ - | usernames | Return users with similar names | - +------------------+-----------------------------------------+ - + The order the members are returned. + timed_out: :class:`bool` + Whether to return timed out users or not. + unusual_dms: :class:`bool` + Whether to return members with the unusual DMs flag. + unusual_activity: :class:`bool` + Whether to return members with the unusual activity flag. + quarantined: :class:`bool` + Whether to return members that are quarantined by the AutoMod. + users: Iterable[:class:`abc.Snowflake`] + Returns members that match this ID. + roles: Iterable[:Class:`abc.Snowflake`] + Returns members with any of these roles. + invites: Iterable[Union[:class:`str`, :class:`Invite`]] + Returns members joined with these invites. + join_types: Iterable[:class:`MemberJoinType`] + Returns members that joined using these methods. + usernames: Iterable[:class:`str`] + Returns members named like any of these usernames. + Raises ------ Forbidden @@ -4483,24 +4476,12 @@ async def fetch_safety_information( The safety information of the members. """ - users = filters.get('users') - if users: - filters['users'] = [user.id for user in users] # type: ignore - - roles = filters.get('roles') - if roles: - filters['roles'] = [role.id for role in roles] # type: ignore - - join_types = filters.get('join_types') - if join_types: - filters['join_types'] = [join_type.value for join_type in join_types] # type: ignore - while limit > 0: retrieve = min(limit, 250) state = self._state - after_id = after.id if after else None - before_id = before.id if before else None + after_id = after.id if after is not MISSING else MISSING + before_id = before.id if before is not MISSING else MISSING data = await state.http.get_guild_member_safety( self.id, @@ -4508,7 +4489,16 @@ async def fetch_safety_information( sort_type=sort_type.value, after=after_id, before=before_id, - **filters # type: ignore + users=[user.id for user in users] if users is not MISSING else MISSING, + roles=[role.id for role in roles] if roles is not MISSING else MISSING, + unusual_activity=unusual_activity, + quarantined=quarantined, + timed_out=timed_out, + unusual_dms=unusual_dms, + invites=[invite if isinstance(invite, str) else invite.code for invite in invites] \ + if invites is not MISSING else MISSING, + join_types=[join_type.value for join_type in join_types] if join_types is not MISSING else MISSING, + usernames=usernames ) results = data['members'] diff --git a/discord/http.py b/discord/http.py index 6d6878aa5a3f..5f7fe542696a 100644 --- a/discord/http.py +++ b/discord/http.py @@ -68,8 +68,6 @@ from .embeds import Embed from .message import Attachment from .flags import MessageFlags - from .invite import Invite - from .types import ( appinfo, audit_log, @@ -1991,17 +1989,17 @@ def get_guild_member_safety( limit: int, sort_type: int, *, - after: Optional[Snowflake] = None, - before: Optional[Snowflake] = None, - users: Optional[Iterable[Snowflake]] = None, - roles: Optional[Iterable[Snowflake]] = None, - unusual_activity: Optional[bool] = None, - quarantined: Optional[bool] = None, - timed_out: Optional[bool] = None, - unusual_dms: Optional[bool] = None, - invite_codes: Optional[Iterable[Union[str, Invite]]] = None, - join_types: Optional[Iterable[int]] = None, - usernames: Optional[Iterable[str]] = None, + after: Snowflake = MISSING, + before: Snowflake = MISSING, + users: Iterable[Snowflake] = MISSING, + roles: Iterable[Snowflake] = MISSING, + unusual_activity: bool = MISSING, + quarantined: bool = MISSING, + timed_out: bool = MISSING, + unusual_dms: bool = MISSING, + invites: Iterable[str] = MISSING, + join_types: Iterable[int] = MISSING, + usernames: Iterable[str] = MISSING, ) -> Response[member.MemberSearchResults]: gte = utils.time_snowflake(datetime.datetime.now(), high=True) + 1 users_gte: Optional[str] = str(after) if after else None @@ -2015,7 +2013,7 @@ def get_guild_member_safety( payload['or_query'] = {} safety_signals = {} - if users: + if users is not MISSING: if 'user_id' in payload['and_query']: payload['and_query']['user_id']['or_query'] = [ str(user) for user in users @@ -2025,7 +2023,7 @@ def get_guild_member_safety( 'or_query': [str(user) for user in users] } - if users_gte: + if users_gte is not None: if 'user_id' in payload['and_query']: if 'range' in payload['and_query']['user_id']: payload['and_query']['user_id']['range']['gte'] = users_gte # type: ignore @@ -2040,7 +2038,7 @@ def get_guild_member_safety( } } - if users_lte: + if users_lte is not None: if 'user_id' in payload['and_query']: if 'range' in payload['and_query']['user_id']: payload['and_query']['user_id']['range']['lte'] = users_lte # type: ignore @@ -2055,32 +2053,32 @@ def get_guild_member_safety( } } - if roles: + if roles is not MISSING: payload['and_query']['role_ids'] = { 'and_query': [str(role) for role in roles] } - if unusual_activity: + if unusual_activity is not MISSING: safety_signals['unusual_account_activity'] = unusual_activity - if quarantined: + if quarantined is not MISSING: safety_signals['automod_quarantined_username'] = quarantined - if timed_out: + if timed_out is not MISSING: safety_signals['communication_disabled_until'] = { 'range': {'gte': gte} } - if unusual_dms: + if unusual_dms is not MISSING: safety_signals['unusual_dm_activity_until'] = { 'range': {'gte': gte} } - if invite_codes: + if invites is not MISSING: payload['and_query']['source_invite_code'] = { 'or_query': [ getattr(invite, 'code', str(invite)) - for invite in invite_codes + for invite in invites ] } From 3665d27ac29abd76bebba121791a9fc0bd92078d Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Wed, 29 May 2024 16:08:32 +0200 Subject: [PATCH 18/25] pog --- discord/guild.py | 3 +++ discord/http.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 9b1ba814d083..cd9a0a311466 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4480,6 +4480,9 @@ async def fetch_safety_information( The safety information of the members. """ + if limit is MISSING: + limit = self.member_count or self.approximate_member_count or 250 + while limit > 0: retrieve = min(limit, 250) diff --git a/discord/http.py b/discord/http.py index 38dd133ba480..7a978c867cda 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2070,14 +2070,14 @@ def get_guild_member_safety( if quarantined is not MISSING: safety_signals['automod_quarantined_username'] = quarantined - if timed_out is not MISSING: + if timed_out is True: safety_signals['communication_disabled_until'] = { - 'range': {'gte': gte} + 'range': {'gte': int(gte)} } - if unusual_dms is not MISSING: + if unusual_dms is True: safety_signals['unusual_dm_activity_until'] = { - 'range': {'gte': gte} + 'range': {'gte': int(gte)} } if invites is not MISSING: @@ -2088,16 +2088,19 @@ def get_guild_member_safety( ] } - if join_types: + if join_types is not MISSING: payload['and_query']['join_source_type'] = { 'or_query': join_types } - if usernames: + if usernames is not MISSING: payload['and_query']['usernames'] = { 'or_query': list(usernames) } + if len(safety_signals.values()) > 0: + payload['or_query']['safety_signals'] = safety_signals + return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) # Guild scheduled event management From 450eaf987d00e38485348b410485bd91c90851c5 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 28 Jul 2024 13:16:23 +0200 Subject: [PATCH 19/25] Some changes --- discord/guild.py | 2 +- discord/http.py | 2 +- discord/member.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index cd9a0a311466..eb5222311215 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4502,7 +4502,7 @@ async def fetch_safety_information( quarantined=quarantined, timed_out=timed_out, unusual_dms=unusual_dms, - invites=[invite if isinstance(invite, str) else invite.code for invite in invites] \ + invites=[invite if isinstance(invite, str) else invite.code for invite in invites] if invites is not MISSING else MISSING, join_types=[join_type.value for join_type in join_types] if join_types is not MISSING else MISSING, usernames=usernames diff --git a/discord/http.py b/discord/http.py index ea67330a6f59..22a506b58d7e 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1576,7 +1576,7 @@ def get_member(self, guild_id: Snowflake, member_id: Snowflake) -> Response[memb return self.request(Route('GET', '/guilds/{guild_id}/members/{member_id}', guild_id=guild_id, member_id=member_id)) def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberSearchResults]: - payload: Dict[str, Union[int, Dict[str, Union[str, int, Dict]]]] = { + payload = { 'sort': 1, # This is the default value (Newest Guild Members First), and as it will only return 1 value it doesn't matter 'limit': 250, # This value can't be changed } diff --git a/discord/member.py b/discord/member.py index ca968246fc58..a184edbe3bb1 100644 --- a/discord/member.py +++ b/discord/member.py @@ -1109,7 +1109,7 @@ async def timeout( await self.edit(timed_out_until=timed_out_until, reason=reason) - async def fetch_safety_information(self, /) -> Optional[MemberSearch]: + async def fetch_safety_information(self) -> Optional[MemberSearch]: r"""|coro| Fetches the safety information for this member. From 6fff459d82a3c6d7e8143d5d43c7e3653f466d83 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Sun, 28 Jul 2024 13:18:52 +0200 Subject: [PATCH 20/25] Docs things --- docs/api.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 44173d8310d9..0b63e740382d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5120,6 +5120,14 @@ PollAnswer .. autoclass:: PollAnswer() :members: +MemberSearch +~~~~~~~~~~~~ + +.. attributetable:: MemberSearch + +.. autoclass:: MemberSearch() + :members: + .. _discord_api_data: Data Classes From 210d5a163d9d69debbbc944a557f2561d5422555 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Fri, 9 Aug 2024 11:29:55 +0200 Subject: [PATCH 21/25] Update members-search + first docs commit --- discord/enums.py | 3 + discord/guild.py | 288 ++++++++++++++++++++++++++++++---------- discord/http.py | 143 +++----------------- discord/member.py | 54 +++----- discord/types/member.py | 8 +- docs/api.rst | 62 +++++++++ 6 files changed, 329 insertions(+), 229 deletions(-) diff --git a/discord/enums.py b/discord/enums.py index 4b7ba48f50cd..6a88e6867b21 100644 --- a/discord/enums.py +++ b/discord/enums.py @@ -75,6 +75,7 @@ 'EntitlementOwnerType', 'PollLayoutType', 'MemberJoinType', + 'MemberSearchSortType', ) @@ -844,9 +845,11 @@ class MemberJoinType(Enum): hub = 4 invite = 5 vanity = 6 + manual_verification = 7 # Aliases app = 1 + student_hub = 4 user_invite = 5 diff --git a/discord/guild.py b/discord/guild.py index 8864562bff18..f2f871ff9a0a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4390,65 +4390,81 @@ async def fetch_safety_information( self, *, limit: int = MISSING, - after: Snowflake = MISSING, + sort_type: MemberSearchSortType = MISSING, before: Snowflake = MISSING, - sort_type: MemberSearchSortType = MemberSearchSortType.new_guild_members, - timed_out: bool = MISSING, - unusual_dms: bool = MISSING, + after: Snowflake = MISSING, + user_ids: List[Snowflake] = MISSING, + usernames: List[str] = MISSING, + roles: List[Snowflake] = MISSING, + joined_guild_before: Union[Snowflake, datetime.datetime] = MISSING, + joined_guild_after: Union[Snowflake, datetime.datetime] = MISSING, + unusual_dms_until: datetime.datetime = MISSING, + timed_out_until: datetime.datetime = MISSING, unusual_activity: bool = MISSING, - quarantined: bool = MISSING, - users: Iterable[Snowflake] = MISSING, - roles: Iterable[Snowflake] = MISSING, - invites: Iterable[Union[str, Invite]] = MISSING, - join_types: Iterable[MemberJoinType] = MISSING, - usernames: Iterable[str] = MISSING, + automod_quarantined: bool = MISSING, + joined_discord_before: Union[Snowflake, datetime.datetime] = MISSING, + joined_discord_after: Union[Snowflake, datetime.datetime] = MISSING, + is_pending: bool = MISSING, + did_rejoin: bool = MISSING, + join_type: MemberJoinType = MISSING, + invite_code: str = MISSING, ) -> AsyncIterator[MemberSearch]: - """Returns a :term:`asynchronous iterator` representing the members that were - obtained after the search. + """Returns a :term:`asynchronous iterator` representing the members that were obtained after + the search. - The ``after`` and ``before`` parameters must represent - a member and meet the :class:`abc.Snowflake` abc. + The ``after`` and ``before`` parameters must represent a member and meet the + :class:`abc.Snowflake` abc. - You must have __any__ of the following permissions: - - - :attr:`Permissions.administrator` - - :attr:`Permissions.manage_guild` - - :attr:`Permissions.manage_roles` - - :attr:`Permissions.manage_nicknames` - - :attr:`Permissions.ban_members` - - :attr:`Permissions.moderate_members` - - :attr:`Permissions.kick_members` + You must have :attr:`Permissions.manage_guild` to do this. .. versionadded:: 2.4 Parameters ---------- limit: :class:`int` - The limit of members to fetch safety information. - after: :class:`abc.Snowflake` - Return members after this ID. + The maximum amount of :class:`MemberSearch` objects to return. Can be up to ``1000``. + Defaults to ``250``. + sort_type: :class:`.MemberSearchSortType` + How the results will be sorted. Defaults to :attr:`MemberSearchSortType.new_guild_members`. before: :class:`abc.Snowflake` - Return member before this ID. - sort_type: :class:`MemberSearchSortType` - The order the members are returned. - timed_out: :class:`bool` - Whether to return timed out users or not. - unusual_dms: :class:`bool` - Whether to return members with the unusual DMs flag. - unusual_activity: :class:`bool` - Whether to return members with the unusual activity flag. - quarantined: :class:`bool` - Whether to return members that are quarantined by the AutoMod. - users: Iterable[:class:`abc.Snowflake`] - Returns members that match this ID. - roles: Iterable[:Class:`abc.Snowflake`] + Return members before this object. + after: :class:`abc.Snowflake` + Return members after this object. + user_ids: List[:class:`abc.Snowflake`] + Returns members which IDs match with the ones provided. + usernames: List[:class:`str`] + Returns members which :attr:`Member.display_name`, :attr:`Member.name`, or + :attr:`Member.global_name` match with any of the items. + roles: List[:class:`abc.Snowflake`] Returns members with any of these roles. - invites: Iterable[Union[:class:`str`, :class:`Invite`]] - Returns members joined with these invites. - join_types: Iterable[:class:`MemberJoinType`] - Returns members that joined using these methods. - usernames: Iterable[:class:`str`] - Returns members named like any of these usernames. + joined_guild_before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] + Returns members that joined this guild before this object. If a `datetime.datetime` object is provided + it returns members that joined before that date. + joined_guild_after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] + Returns members that joined this guild after this object. If a `datetime.datetime` object is provided + it returns member that joined after that date. + unusual_dms_until: :class:`datetime.datetime` + Returns members which `Member.unusual_dms_until` attribute is less or equal to this. + timed_out_until: :class:`datetime.datetime` + Returns members which `Member.timed_out_until` attribute is less or equal to this. + unusual_activity: :class:`bool` + Returns members flagged with unusual account activity. + automod_quarantined: :class:`bool` + Returns members that have been indefinitely quarantined by an AutoMod Rule because of their name. + joined_discord_before: Union[:class.`abc.Snowflake`, :class:`datetime.datetime`] + Returns members that joined Discord before this object. If a `datetime.datetime` object is provided + it returns members that joined before that date. + joined_discord_after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] + Returns members that joined Discord after this object. If a `datetime.datetime` object is provided + it returns members that joined after that date. + is_pending: :class:`bool` + Returns members that have not yet passed the guild member verification. + did_rejoin: :class:`bool` + Returns members that have rejoined the guild. + join_type: :class:`MemberJoinType` + Returns members that have joined like the value provided. + invite_code: :class:`str` + Returns members that have joined using this invite code, or vanity code. Raises ------ @@ -4464,51 +4480,185 @@ async def fetch_safety_information( The safety information of the members. """ + def construct_range( + gte: Union[Snowflake, datetime.datetime], lte: Union[Snowflake, datetime.datetime], + ) -> Dict[str, Any]: + r = {} + if gte is not MISSING: + rgte = int(gte.timestamp()) if isinstance(gte, datetime.datetime) else gte.id + r['gte'] = rgte + if lte is not MISSING: + rlte = int(lte.timestamp()) if isinstance(lte, datetime.datetime) else lte.id + r['lte'] = rlte + return r + + def construct_safety_signals( + unusual_dm_activity_until: datetime.datetime, + communication_disabled_until: datetime.datetime, + unusual_account_activity: bool, + automod_quarantined_username: bool, + ) -> Dict[str, Any]: + r = {} + if unusual_dm_activity_until is not MISSING: + r['unusual_dm_activity_until'] = construct_range(unusual_dm_activity_until, MISSING) + if communication_disabled_until is not MISSING: + r['communication_disabled_until'] = construct_range( + communication_disabled_until, MISSING, + ) + if unusual_account_activity is not MISSING: + r['unusual_account_activity'] = unusual_account_activity + if automod_quarantined_username is not MISSING: + r['automod_quarantined_username'] = automod_quarantined_username + return r + + def construct_or_query( + value: List[Any], + ) -> Dict[str, List[Any]]: + return {'or_query': value} + + def construct_and_query( + value: List[Any], + ) -> Dict[str, List[Any]]: + return {'and_query': value} + + def set_or_update_query( + payload: Dict[str, Any], + query: Literal['or_query', 'and_query'], + key: str, + value: Any, + ) -> Dict[str, Any]: + query_data = payload[query] + if key in query_data: + query_value = query_data[key] + + if isinstance(query_value, list): + query_value.append(value) + elif isinstance(query_value, dict): + query_value.update(value) + else: + query_value += value + + query_data[key] = query_value + payload[query] = query_data + else: + payload[query][key] = value + return payload + if limit is MISSING: limit = self.member_count or self.approximate_member_count or 250 while limit > 0: - retrieve = min(limit, 250) + retrieve = min(limit, 1000) state = self._state after_id = after.id if after is not MISSING else MISSING before_id = before.id if before is not MISSING else MISSING - data = await state.http.get_guild_member_safety( + payload = { + 'limit': retrieve, + 'sort': sort_type.value, + 'and_query': {}, + 'or_query': {}, + } + + if after_id is not MISSING: + payload['after'] = after_id + if before_id is not MISSING: + payload['before'] = before_id + if any( + signal is not MISSING for signal in ( + timed_out_until, + unusual_activity, + unusual_dms_until, + automod_quarantined, + ) + ): + safety_signals_payload = construct_safety_signals( + unusual_dms_until, + timed_out_until, + unusual_activity, + automod_quarantined, + ) + set_or_update_query(payload, 'or_query', 'safety_signals', safety_signals_payload) + if user_ids is not MISSING: + user_ids_payload = construct_or_query([u.id for u in user_ids]) + set_or_update_query(payload, 'and_query', 'user_id', user_ids_payload) + if usernames is not MISSING: + usernames_payload = construct_or_query(usernames) + set_or_update_query(payload, 'and_query', 'usernames', usernames_payload) + if roles is not MISSING: + roles_payload = construct_and_query([r.id for r in roles]) + set_or_update_query(payload, 'and_query', 'role_ids', roles_payload) + if joined_guild_after is not MISSING: + set_or_update_query( + payload, + 'and_query', + 'guild_joined_at', + construct_range( + joined_guild_after, + MISSING, + ), + ) + if joined_guild_before is not MISSING: + set_or_update_query( + payload, + 'and_query', + 'guild_joined_at', + construct_range( + MISSING, + joined_guild_before, + ), + ) + if is_pending is not MISSING: + set_or_update_query( + payload, + 'and_query', + 'is_pending', + is_pending, + ) + if did_rejoin is not MISSING: + set_or_update_query( + payload, + 'and_query', + 'did_rejoin', + did_rejoin, + ) + if join_type is not MISSING: + set_or_update_query( + payload, + 'and_query', + 'join_source_type', + construct_or_query([join_type.value]), + ) + if invite_code is not MISSING: + set_or_update_query( + payload, + 'and_query', + 'source_invite_code', + construct_or_query([invite_code]), + ) + + data = await state.http.get_guild_members_safety_information( self.id, - limit=retrieve, - sort_type=sort_type.value, - after=after_id, - before=before_id, - users=[user.id for user in users] if users is not MISSING else MISSING, - roles=[role.id for role in roles] if roles is not MISSING else MISSING, - unusual_activity=unusual_activity, - quarantined=quarantined, - timed_out=timed_out, - unusual_dms=unusual_dms, - invites=[invite if isinstance(invite, str) else invite.code for invite in invites] - if invites is not MISSING else MISSING, - join_types=[join_type.value for join_type in join_types] if join_types is not MISSING else MISSING, - usernames=usernames + limit, + sort_type.value, + **payload, ) results = data['members'] - if len(results) == 0: + if not results: # No more members, break break limit -= len(results) after = Object(id=int(results[-1]['member']['user']['id'])) - for result in reversed(results): + for result in results: yield MemberSearch(data=result, guild=self, state=state) - if before_id: - if after.id > before_id: - # We cannot fetch users if the before requirements is less than - # the after requirement. - break + if before_id is not MISSING and after.id > before_id: + break @property def invites_paused_until(self) -> Optional[datetime.datetime]: diff --git a/discord/http.py b/discord/http.py index 22a506b58d7e..886f750b809b 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1574,20 +1574,6 @@ def get_members( def get_member(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberWithUser]: return self.request(Route('GET', '/guilds/{guild_id}/members/{member_id}', guild_id=guild_id, member_id=member_id)) - - def get_member_safety_information(self, guild_id: Snowflake, member_id: Snowflake) -> Response[member.MemberSearchResults]: - payload = { - 'sort': 1, # This is the default value (Newest Guild Members First), and as it will only return 1 value it doesn't matter - 'limit': 250, # This value can't be changed - } - payload['and_query'] = { - 'user_id': { - 'or_query': [str(member_id)] - } - } - payload['or_query'] = {} - - return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) def prune_members( self, @@ -1994,119 +1980,32 @@ def edit_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = None) -> Response[None]: return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) - def get_guild_member_safety( + def get_guild_members_safety_information( self, guild_id: Snowflake, limit: int, - sort_type: int, - *, - after: Snowflake = MISSING, - before: Snowflake = MISSING, - users: Iterable[Snowflake] = MISSING, - roles: Iterable[Snowflake] = MISSING, - unusual_activity: bool = MISSING, - quarantined: bool = MISSING, - timed_out: bool = MISSING, - unusual_dms: bool = MISSING, - invites: Iterable[str] = MISSING, - join_types: Iterable[int] = MISSING, - usernames: Iterable[str] = MISSING, + sort: int, + **kwargs: Any, ) -> Response[member.MemberSearchResults]: - gte = utils.time_snowflake(datetime.datetime.now(), high=True) + 1 - users_gte: Optional[str] = str(after) if after else None - users_lte: Optional[str] = str(before) if before else None - payload: Dict[Any, Any] = { - 'sort': sort_type + # These are just the base keys, other "subqueries" as "safety_signals" are constructed + # in `Guild.fetch_safety_information` + valid_keys = ( + 'limit', + 'sort', + 'or_query', + 'and_query', + 'before', + 'after', + ) + payload = { + 'limit': limit, + 'sort': sort, } - - payload['limit'] = limit - payload['and_query'] = {} - payload['or_query'] = {} - safety_signals = {} - - if users is not MISSING: - if 'user_id' in payload['and_query']: - payload['and_query']['user_id']['or_query'] = [ - str(user) for user in users - ] - else: - payload['and_query']['user_id'] = { - 'or_query': [str(user) for user in users] - } - - if users_gte is not None: - if 'user_id' in payload['and_query']: - if 'range' in payload['and_query']['user_id']: - payload['and_query']['user_id']['range']['gte'] = users_gte # type: ignore - else: - payload['and_query']['user_id']['range'] = { # type: ignore - 'gte': users_gte - } - else: - payload['and_query']['user_id'] = { - 'range': { - 'gte': users_gte - } - } - - if users_lte is not None: - if 'user_id' in payload['and_query']: - if 'range' in payload['and_query']['user_id']: - payload['and_query']['user_id']['range']['lte'] = users_lte # type: ignore - else: - payload['and_query']['user_id']['range'] = { # type: ignore - 'lte': users_lte - } - else: - payload['and_query']['user_id'] = { - 'range': { - 'lte': users_lte - } - } - - if roles is not MISSING: - payload['and_query']['role_ids'] = { - 'and_query': [str(role) for role in roles] - } - - if unusual_activity is not MISSING: - safety_signals['unusual_account_activity'] = unusual_activity - - if quarantined is not MISSING: - safety_signals['automod_quarantined_username'] = quarantined - - if timed_out is True: - safety_signals['communication_disabled_until'] = { - 'range': {'gte': int(gte)} - } - - if unusual_dms is True: - safety_signals['unusual_dm_activity_until'] = { - 'range': {'gte': int(gte)} - } - - if invites is not MISSING: - payload['and_query']['source_invite_code'] = { - 'or_query': [ - getattr(invite, 'code', str(invite)) - for invite in invites - ] - } - - if join_types is not MISSING: - payload['and_query']['join_source_type'] = { - 'or_query': join_types - } - - if usernames is not MISSING: - payload['and_query']['usernames'] = { - 'or_query': list(usernames) - } - - if len(safety_signals.values()) > 0: - payload['or_query']['safety_signals'] = safety_signals - - return self.request(Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), json=payload) + payload.update({k: v for k, v in kwargs.items() if k in valid_keys}) + return self.request( + Route('POST', '/guilds/{guild_id}/members-search', guild_id=guild_id), + json=payload, + ) # Guild scheduled event management diff --git a/discord/member.py b/discord/member.py index ecb9eaca1785..08e56ce375b6 100644 --- a/discord/member.py +++ b/discord/member.py @@ -266,36 +266,32 @@ class MemberSearch: """ __slots__ = ( - '_resolved_data', + 'resolved', 'invite_code', 'join_type', - '_inviter_id', + 'inviter_id', '_guild', '_state', ) def __init__(self, *, data: MemberSearchPayload, guild: Guild, state: ConnectionState) -> None: - self._resolved_data = data['member'] + self.resolved: Member = Member(data=data['member'], guild=guild, state=state) self.invite_code: Optional[str] = data.get('source_invite_code') self.join_type: MemberJoinType = try_enum(MemberJoinType, data['join_source_type']) try: - self._inviter_id = data['inviter_id'] + self.inviter_id = int(data['inviter_id']) except KeyError: - self._inviter_id = None + self.inviter_id = None self._guild = guild self._state = state def __repr__(self) -> str: - if self.inviter: - return f'' - return f'' - - @property - def resolved(self) -> Member: - """:class:`Member`: Returns the resolved member object this search if of.""" - return Member(data=self._resolved_data, guild=self._guild, state=self._state) + return ( + f'' + ) @property def inviter(self) -> Optional[Union[Member, User]]: @@ -306,13 +302,6 @@ def inviter(self) -> Optional[Union[Member, User]]: return return self._guild.get_member(self.inviter_id) or self._state.get_user(self.inviter_id) - @property - def inviter_id(self) -> Optional[int]: - """Optional[:class:`int`]: Returns this member\'s inviter ID""" - if not self._inviter_id: - return - return int(self._inviter_id) - @flatten_user class Member(discord.abc.Messageable, _UserTag): @@ -1140,15 +1129,7 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: Fetches the safety information for this member. - You must have __any__ of the following permissions: - - - :attr:`Permissions.administrator` - - :attr:`Permissions.manage_guild` - - :attr:`Permissions.manage_roles` - - :attr:`Permissions.manage_nicknames` - - :attr:`Permissions.ban_members` - - :attr:`Permissions.moderate_members` - - :attr:`Permissions.kick_members` + You must have :attr:`Permissions.manage_guild` to do this. .. versionadded:: 2.4 @@ -1167,13 +1148,16 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: isn't. """ - data = await self._state.http.get_member_safety_information(self.guild.id, self.id) - - if data['total_result_count'] == 0: - return + results = [ + m async for m in self.guild.fetch_safety_information( + limit=1, + user_ids=[self], + ) + ] - # We use data['members'][0] because there will only be 1 result - return MemberSearch(data=data['members'][0], guild=self.guild, state=self._state) + if not results: + return None + return results[0] async def add_roles(self, *roles: Snowflake, reason: Optional[str] = None, atomic: bool = True) -> None: r"""|coro| diff --git a/discord/types/member.py b/discord/types/member.py index fd4342e2e29b..bec805d44bfb 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -23,11 +23,12 @@ """ from typing import Optional, TypedDict, Literal, List + from .snowflake import SnowflakeList, Snowflake from .user import User, AvatarDecorationData from typing_extensions import NotRequired -JoinType = Literal[0, 3, 5] +JoinType = Literal[0, 1, 2, 3, 4, 5, 6, 7] class Nickname(TypedDict): nick: str @@ -72,13 +73,14 @@ class UserWithMember(User, total=False): class MemberSearch(TypedDict): member: MemberWithUser - source_invite_code: Optional[str] join_source_type: JoinType + source_invite_code: Optional[str] inviter_id: Optional[Snowflake] + integration_type: Optional[int] class MemberSearchResults(TypedDict): guild_id: Snowflake members: List[MemberSearch] page_result_count: int - total_result_count: int \ No newline at end of file + total_result_count: int diff --git a/docs/api.rst b/docs/api.rst index b089b816f1ae..e9ff9a578e14 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3663,6 +3663,67 @@ of :class:`enum.Enum`. A burst reaction, also known as a "super reaction". +.. class:: MemberJoinType + + Represents how a member joined a guild. + + .. versionadded:: 2.5 + + .. attribute:: unknown + + Member joined the guild by an unknown source. + + .. attribute:: bot + .. attribute:: app + + Member joined by a bot invite. + + .. attribute:: integration + + Member joined by an integration. + + .. attribute:: discovery + + Member joined by guild discovery. + + .. attribute:: hub + .. attribute:: student_hub + + Member joined by the Student Hub. + + .. attribute:: invite + .. attribute:: user_invite + + Member joined by a user invite. + + .. attribute:: manual_verification + + Member joined by manual verification. + + +.. class:: MemberSearchSortType + + How to sort the results of :meth:`Guild.fetch_safety_information`. + + .. versionadded:: 2.5 + + .. attribute:: new_guild_members + + Sort by guild join date in descending order (newest first). + + .. attribute:: old_guild_members + + Sort by guild join date in ascending order (oldest first). + + .. attribute:: new_discord_users + + Sort by account creation date in descending order (newest first). + + .. attribute:: old_discord_users + + Sort by account creation date in ascending order (oldest first). + + .. _discord-api-audit-logs: Audit Log Data @@ -5133,6 +5194,7 @@ MemberSearch .. autoclass:: MemberSearch() :members: + .. _discord_api_data: Data Classes From 09399166b7c9358c73be4849277e14d25b22bdbe Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Fri, 9 Aug 2024 13:57:19 +0200 Subject: [PATCH 22/25] Bump version to 2.5 + document MemberSearch attributes --- discord/guild.py | 2 +- discord/member.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index f2f871ff9a0a..6aeb7d19161c 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4417,7 +4417,7 @@ async def fetch_safety_information( You must have :attr:`Permissions.manage_guild` to do this. - .. versionadded:: 2.4 + .. versionadded:: 2.5 Parameters ---------- diff --git a/discord/member.py b/discord/member.py index 08e56ce375b6..63dbdd59c2c3 100644 --- a/discord/member.py +++ b/discord/member.py @@ -255,14 +255,18 @@ def general(self, *args, **kwargs): class MemberSearch: """Represents a fetched member from member search. - .. versionadded:: 2.4 + .. versionadded:: 2.5 Attributes ---------- + resolved: :class:`Member` + The resolved member of this search result. invite_code: Optional[:class:`str`] The Invite Code this user joined with. join_type: :class:`JoinType` The join type. + inviter_id: Optional[:class:`int`] + The ID of the user that invited this member to the guild, if available. """ __slots__ = ( @@ -1131,7 +1135,7 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: You must have :attr:`Permissions.manage_guild` to do this. - .. versionadded:: 2.4 + .. versionadded:: 2.5 Raises ------ From e366bcac12e8c8cdad16623e3de828b2ccb1016c Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Fri, 9 Aug 2024 13:59:04 +0200 Subject: [PATCH 23/25] Remove unneccessary imports and type: ignore --- discord/guild.py | 3 --- discord/member.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index 6aeb7d19161c..a6d2f618bf29 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -44,7 +44,6 @@ Optional, TYPE_CHECKING, Tuple, - TypedDict, Union, overload, ) @@ -107,8 +106,6 @@ MISSING = utils.MISSING if TYPE_CHECKING: - from typing_extensions import Unpack - from .abc import Snowflake, SnowflakeTime from .types.guild import ( Ban as BanPayload, diff --git a/discord/member.py b/discord/member.py index 63dbdd59c2c3..bb8f3a3bb597 100644 --- a/discord/member.py +++ b/discord/member.py @@ -284,7 +284,7 @@ def __init__(self, *, data: MemberSearchPayload, guild: Guild, state: Connection self.invite_code: Optional[str] = data.get('source_invite_code') self.join_type: MemberJoinType = try_enum(MemberJoinType, data['join_source_type']) try: - self.inviter_id = int(data['inviter_id']) + self.inviter_id = int(data['inviter_id']) # type: ignore except KeyError: self.inviter_id = None From 498fc88aa2e10494c06cddd23326df4f55af9f96 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Fri, 9 Aug 2024 14:01:22 +0200 Subject: [PATCH 24/25] Black --- discord/guild.py | 11 +++++++---- discord/http.py | 2 +- discord/member.py | 10 +++++----- discord/types/member.py | 1 + 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index a6d2f618bf29..dda2dd028286 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4382,7 +4382,7 @@ async def create_automod_rule( ) return AutoModRule(data=data, guild=self, state=self._state) - + async def fetch_safety_information( self, *, @@ -4478,7 +4478,8 @@ async def fetch_safety_information( """ def construct_range( - gte: Union[Snowflake, datetime.datetime], lte: Union[Snowflake, datetime.datetime], + gte: Union[Snowflake, datetime.datetime], + lte: Union[Snowflake, datetime.datetime], ) -> Dict[str, Any]: r = {} if gte is not MISSING: @@ -4500,7 +4501,8 @@ def construct_safety_signals( r['unusual_dm_activity_until'] = construct_range(unusual_dm_activity_until, MISSING) if communication_disabled_until is not MISSING: r['communication_disabled_until'] = construct_range( - communication_disabled_until, MISSING, + communication_disabled_until, + MISSING, ) if unusual_account_activity is not MISSING: r['unusual_account_activity'] = unusual_account_activity @@ -4563,7 +4565,8 @@ def set_or_update_query( if before_id is not MISSING: payload['before'] = before_id if any( - signal is not MISSING for signal in ( + signal is not MISSING + for signal in ( timed_out_until, unusual_activity, unusual_dms_until, diff --git a/discord/http.py b/discord/http.py index 886f750b809b..417485ef7f6f 100644 --- a/discord/http.py +++ b/discord/http.py @@ -1979,7 +1979,7 @@ def edit_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = def delete_stage_instance(self, channel_id: Snowflake, *, reason: Optional[str] = None) -> Response[None]: return self.request(Route('DELETE', '/stage-instances/{channel_id}', channel_id=channel_id), reason=reason) - + def get_guild_members_safety_information( self, guild_id: Snowflake, diff --git a/discord/member.py b/discord/member.py index bb8f3a3bb597..51b92f46fe25 100644 --- a/discord/member.py +++ b/discord/member.py @@ -254,7 +254,7 @@ def general(self, *args, **kwargs): class MemberSearch: """Represents a fetched member from member search. - + .. versionadded:: 2.5 Attributes @@ -274,7 +274,6 @@ class MemberSearch: 'invite_code', 'join_type', 'inviter_id', - '_guild', '_state', ) @@ -1130,7 +1129,7 @@ async def timeout( async def fetch_safety_information(self) -> Optional[MemberSearch]: r"""|coro| - + Fetches the safety information for this member. You must have :attr:`Permissions.manage_guild` to do this. @@ -1144,7 +1143,7 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: information. HTTPException Fetching the information failed. - + Returns ------- Optional[:class:`MemberSafety`] @@ -1153,7 +1152,8 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: """ results = [ - m async for m in self.guild.fetch_safety_information( + m + async for m in self.guild.fetch_safety_information( limit=1, user_ids=[self], ) diff --git a/discord/types/member.py b/discord/types/member.py index bec805d44bfb..96fd99f190d0 100644 --- a/discord/types/member.py +++ b/discord/types/member.py @@ -30,6 +30,7 @@ JoinType = Literal[0, 1, 2, 3, 4, 5, 6, 7] + class Nickname(TypedDict): nick: str From b625356706d9c62b0ea5a6f08ac05bd5dbf27ed4 Mon Sep 17 00:00:00 2001 From: Developer Anonymous Date: Fri, 9 Aug 2024 14:04:40 +0200 Subject: [PATCH 25/25] fix docs references --- discord/member.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/member.py b/discord/member.py index 51b92f46fe25..79bb4dacdd99 100644 --- a/discord/member.py +++ b/discord/member.py @@ -263,7 +263,7 @@ class MemberSearch: The resolved member of this search result. invite_code: Optional[:class:`str`] The Invite Code this user joined with. - join_type: :class:`JoinType` + join_type: :class:`MemberJoinType` The join type. inviter_id: Optional[:class:`int`] The ID of the user that invited this member to the guild, if available. @@ -1146,7 +1146,7 @@ async def fetch_safety_information(self) -> Optional[MemberSearch]: Returns ------- - Optional[:class:`MemberSafety`] + Optional[:class:`MemberSearch`] The member safety information, or `None` if there isn't. """