Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Members Search #9912

Draft
wants to merge 32 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a5bc854
Added member safety search information
DA-344 Jan 8, 2024
3c5c1dd
Merge branch 'Rapptz:master' into members-search
DA-344 Jan 8, 2024
69be2f1
Fixed Member Safety Information returning None
DA-344 Jan 8, 2024
62dd6d2
Merge branch 'members-search' of https:/DA-344/d.py into …
DA-344 Jan 8, 2024
99e6422
Changed the docstring to show proper permissions
DA-344 Jan 9, 2024
1acdffb
Updated some things
DA-344 Mar 17, 2024
e93f09e
Merge from https:/Rapptz/discord.py
DA-344 Mar 17, 2024
5ad1684
Added source invite filters
DA-344 Mar 20, 2024
4a60981
Updated filters table in Guild.fetch_members_safety_information
DA-344 Mar 20, 2024
dababbd
Merge branch 'master' of https:/Rapptz/discord.py into me…
DA-344 Apr 21, 2024
fca336c
Rewritten MemberSearch
DA-344 Apr 21, 2024
6a1c222
fetch_members_safety_information -> fetch_safety_information
DA-344 Apr 21, 2024
39435d8
Edited get_guild_member_safety
DA-344 Apr 21, 2024
50a5357
Added newline (pyright)
DA-344 Apr 21, 2024
7f6f40f
More newlines
DA-344 Apr 21, 2024
b8eca36
Reworked get_guild_member_safety
DA-344 Apr 21, 2024
4a6e0c6
fetch_safety_information | Return Type: Tuple -> AsyncIterator
DA-344 Apr 21, 2024
2468563
Updated fetch_safety_information docstring
DA-344 Apr 21, 2024
0457947
Added support for username search
DA-344 Apr 21, 2024
abae9a6
Changes to fetch_safety_information
DA-344 Apr 21, 2024
2898271
Rewrite get_guild_safety
DA-344 May 12, 2024
79b51a4
Merge master from https:/Rapptz/discord.py
DA-344 May 12, 2024
3665d27
pog
DA-344 May 29, 2024
c2f4775
Merge branch master from https:/Rapptz/discord.py
DA-344 May 29, 2024
450eaf9
Some changes
DA-344 Jul 28, 2024
6fff459
Docs things
DA-344 Jul 28, 2024
07faeec
Merge master to members-search
DA-344 Aug 7, 2024
210d5a1
Update members-search + first docs commit
DA-344 Aug 9, 2024
0939916
Bump version to 2.5 + document MemberSearch attributes
DA-344 Aug 9, 2024
e366bca
Remove unneccessary imports and type: ignore
DA-344 Aug 9, 2024
498fc88
Black
DA-344 Aug 9, 2024
b625356
fix docs references
DA-344 Aug 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions discord/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
'EntitlementType',
'EntitlementOwnerType',
'PollLayoutType',
'MemberJoinType',
'MemberSearchSortType',
)


Expand Down Expand Up @@ -835,6 +837,29 @@ class ReactionType(Enum):
burst = 1


class MemberJoinType(Enum):
unknown = 0
bot = 1
integration = 2
discovery = 3
hub = 4
invite = 5
vanity = 6
manual_verification = 7

# Aliases
app = 1
student_hub = 4
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}'
Expand Down
282 changes: 281 additions & 1 deletion discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

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
Expand All @@ -76,6 +76,8 @@
AutoModRuleEventType,
ForumOrderType,
ForumLayoutType,
MemberJoinType,
MemberSearchSortType,
)
from .mixins import Hashable
from .user import User
Expand Down Expand Up @@ -136,6 +138,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]
Expand Down Expand Up @@ -4380,6 +4383,283 @@ async def create_automod_rule(

return AutoModRule(data=data, guild=self, state=self._state)

async def fetch_safety_information(
self,
*,
limit: int = MISSING,
sort_type: MemberSearchSortType = MISSING,
before: Snowflake = 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,
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.

The ``after`` and ``before`` parameters must represent a member and meet the
:class:`abc.Snowflake` abc.

You must have :attr:`Permissions.manage_guild` to do this.

.. versionadded:: 2.5

Parameters
----------
limit: :class:`int`
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 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.
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
------
Forbidden
You do not have enough permissions to
fetch this information.
HTTPException
Fetching the information failed.

Yields
------
:class:`MemberSearch`
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, 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

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,
sort_type.value,
**payload,
)

results = data['members']

if not results:
# No more members, break
break

limit -= len(results)
after = Object(id=int(results[-1]['member']['user']['id']))

for result in results:
yield MemberSearch(data=result, guild=self, state=state)

if before_id is not MISSING and after.id > before_id:
break

@property
def invites_paused_until(self) -> Optional[datetime.datetime]:
"""Optional[:class:`datetime.datetime`]: If invites are paused, returns when
Expand Down
28 changes: 27 additions & 1 deletion discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
from .message import Attachment
from .flags import MessageFlags
from .poll import Poll

from .types import (
appinfo,
audit_log,
Expand Down Expand Up @@ -1981,6 +1980,33 @@ 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,
limit: int,
sort: int,
**kwargs: Any,
) -> Response[member.MemberSearchResults]:
# 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.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

@overload
Expand Down
Loading
Loading