Skip to content

Commit 54b704f

Browse files
committed
Implement member verification and join requests
1 parent 1add8a0 commit 54b704f

11 files changed

Lines changed: 1087 additions & 0 deletions

File tree

discord/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from .subscription import *
7575
from .presences import *
7676
from .primary_guild import *
77+
from .member_verification import *
7778
from .onboarding import *
7879
from .collectible import *
7980

discord/client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .template import Template
5656
from .widget import Widget
5757
from .guild import Guild, GuildPreview
58+
from .member_verification import JoinRequest
5859
from .emoji import Emoji
5960
from .channel import _threaded_channel_factory, PartialMessageable
6061
from .enums import ChannelType, EntitlementOwnerType
@@ -109,6 +110,7 @@
109110
RawThreadMembersUpdate,
110111
RawThreadUpdateEvent,
111112
RawTypingEvent,
113+
RawJoinRequestDeleteEvent,
112114
RawPollVoteActionEvent,
113115
)
114116
from .reaction import Reaction
@@ -1473,6 +1475,38 @@ async def wait_for(
14731475
timeout: Optional[float] = ...,
14741476
) -> AuditLogEntry: ...
14751477

1478+
# Member Verification
1479+
1480+
@overload
1481+
async def wait_for(
1482+
self,
1483+
event: Literal['join_request_create', 'join_request_update'],
1484+
/,
1485+
*,
1486+
check: Optional[Callable[[JoinRequest], bool]] = ...,
1487+
timeout: Optional[float] = ...,
1488+
) -> JoinRequest: ...
1489+
1490+
@overload
1491+
async def wait_for(
1492+
self,
1493+
event: Literal['join_request_delete'],
1494+
/,
1495+
*,
1496+
check: Optional[Callable[[Guild, User], bool]] = ...,
1497+
timeout: Optional[float] = ...,
1498+
) -> Tuple[Guild, User]: ...
1499+
1500+
@overload
1501+
async def wait_for(
1502+
self,
1503+
event: Literal['raw_join_request_delete'],
1504+
/,
1505+
*,
1506+
check: Optional[Callable[[RawJoinRequestDeleteEvent], bool]] = ...,
1507+
timeout: Optional[float] = ...,
1508+
) -> RawJoinRequestDeleteEvent: ...
1509+
14761510
# Integrations
14771511

14781512
@overload

discord/enums.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
'StatusDisplayType',
8484
'OnboardingPromptType',
8585
'OnboardingMode',
86+
'MemberVerificationFieldType',
87+
'JoinRequestStatus',
8688
'SeparatorSpacing',
8789
'MediaItemLoadingState',
8890
'CollectibleType',
@@ -976,6 +978,26 @@ class OnboardingMode(Enum):
976978
advanced = 1
977979

978980

981+
class MemberVerificationFieldType(Enum):
982+
terms = 'TERMS'
983+
text_input = 'TEXT_INPUT'
984+
paragraph = 'PARAGRAPH'
985+
multiple_choice = 'MULTIPLE_CHOICE'
986+
987+
def __str__(self) -> str:
988+
return self.value
989+
990+
991+
class JoinRequestStatus(Enum):
992+
started = 'STARTED'
993+
submitted = 'SUBMITTED'
994+
rejected = 'REJECTED'
995+
approved = 'APPROVED'
996+
997+
def __str__(self) -> str:
998+
return self.value
999+
1000+
9791001
class SeparatorSpacing(Enum):
9801002
small = 1
9811003
large = 2

discord/guild.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
PrivacyLevel,
6868
try_enum,
6969
VerificationLevel,
70+
JoinRequestStatus,
7071
ContentFilter,
7172
NotificationLevel,
7273
NSFWLevel,
@@ -91,6 +92,7 @@
9192
from .file import File
9293
from .audit_logs import AuditLogEntry
9394
from .object import OLDEST_OBJECT, Object
95+
from .member_verification import JoinRequest, MemberVerification, MemberVerificationFormField
9496
from .onboarding import Onboarding
9597
from .welcome_screen import WelcomeScreen, WelcomeChannel
9698
from .automod import AutoModRule, AutoModTrigger, AutoModRuleAction
@@ -136,6 +138,7 @@
136138
ForumChannel as ForumChannelPayload,
137139
)
138140
from .types.integration import IntegrationType
141+
from .types.member_verification import JoinRequest as JoinRequestPayload
139142
from .types.snowflake import SnowflakeList
140143
from .types.widget import EditWidgetSettings
141144
from .types.audit_log import AuditLogEvent
@@ -3964,6 +3967,252 @@ async def edit_welcome_screen(
39643967
data = await self._state.http.edit_welcome_screen(self.id, reason=reason, **fields)
39653968
return WelcomeScreen(data=data, guild=self)
39663969

3970+
async def member_verification(self) -> MemberVerification:
3971+
"""|coro|
3972+
3973+
Fetches the member verification gate for this guild.
3974+
3975+
.. versionadded:: 2.8
3976+
3977+
Raises
3978+
-------
3979+
NotFound
3980+
The guild does not have member verification enabled.
3981+
Forbidden
3982+
You do not have permissions to fetch the member verification.
3983+
HTTPException
3984+
Fetching the member verification failed.
3985+
3986+
Returns
3987+
--------
3988+
:class:`MemberVerification`
3989+
The member verification that was fetched.
3990+
"""
3991+
data = await self._state.http.get_member_verification(self.id)
3992+
return MemberVerification(data=data, guild=self)
3993+
3994+
async def edit_member_verification(
3995+
self,
3996+
*,
3997+
enabled: bool = MISSING,
3998+
form_fields: Sequence[MemberVerificationFormField] = MISSING,
3999+
description: Optional[str] = MISSING,
4000+
bulk_action: JoinRequestStatus = MISSING,
4001+
reason: Optional[str] = None,
4002+
) -> MemberVerification:
4003+
"""|coro|
4004+
4005+
Edits the member verification gate for this guild.
4006+
4007+
You must have :attr:`~Permissions.manage_guild` to do this.
4008+
4009+
All parameters are optional.
4010+
4011+
.. versionadded:: 2.8
4012+
4013+
Parameters
4014+
-----------
4015+
enabled: :class:`bool`
4016+
Whether the member verification gate is enabled.
4017+
form_fields: List[:class:`MemberVerificationFormField`]
4018+
The questions the user must answer. There can be up to 5 questions.
4019+
4020+
Using a field type other than :attr:`MemberVerificationFieldType.terms`
4021+
requires the guild to have the ``MEMBER_VERIFICATION_MANUAL_APPROVAL`` feature.
4022+
description: Optional[:class:`str`]
4023+
A description of what the guild is about. Can be up to 300 characters long.
4024+
bulk_action: :class:`JoinRequestStatus`
4025+
What to do with the pending join requests when disabling the gate.
4026+
Only :attr:`JoinRequestStatus.approved` and :attr:`JoinRequestStatus.rejected`
4027+
can be used. Defaults to approving.
4028+
reason: Optional[:class:`str`]
4029+
The reason for editing the member verification. Shows up on the audit log.
4030+
4031+
Raises
4032+
-------
4033+
ValueError
4034+
An invalid ``bulk_action`` was passed.
4035+
Forbidden
4036+
You do not have permissions to edit the member verification.
4037+
HTTPException
4038+
Editing the member verification failed.
4039+
4040+
Returns
4041+
--------
4042+
:class:`MemberVerification`
4043+
The newly updated member verification.
4044+
"""
4045+
payload: Dict[str, Any] = {}
4046+
if enabled is not MISSING:
4047+
payload['enabled'] = enabled
4048+
if form_fields is not MISSING:
4049+
payload['form_fields'] = [field.to_dict() for field in form_fields]
4050+
if description is not MISSING:
4051+
payload['description'] = description
4052+
if bulk_action is not MISSING:
4053+
if bulk_action not in (JoinRequestStatus.approved, JoinRequestStatus.rejected):
4054+
raise ValueError('bulk_action must be either JoinRequestStatus.approved or JoinRequestStatus.rejected')
4055+
payload['bulk_action'] = bulk_action.value
4056+
4057+
data = await self._state.http.edit_member_verification(self.id, reason=reason, **payload)
4058+
return MemberVerification(data=data, guild=self)
4059+
4060+
async def join_requests(
4061+
self,
4062+
*,
4063+
status: JoinRequestStatus = JoinRequestStatus.submitted,
4064+
limit: Optional[int] = 100,
4065+
before: SnowflakeTime = MISSING,
4066+
after: SnowflakeTime = MISSING,
4067+
oldest_first: bool = MISSING,
4068+
) -> AsyncIterator[JoinRequest]:
4069+
"""Retrieves an :term:`asynchronous iterator` of the guild's :class:`JoinRequest`\\s.
4070+
4071+
You must have :attr:`~Permissions.kick_members` to do this.
4072+
4073+
.. note::
4074+
4075+
Join requests with a status of :attr:`JoinRequestStatus.submitted` are ordered
4076+
and paginated by the request itself, whereas actioned join requests are ordered
4077+
and paginated by :attr:`JoinRequest.actioned_at`.
4078+
4079+
.. versionadded:: 2.8
4080+
4081+
Examples
4082+
---------
4083+
4084+
Usage ::
4085+
4086+
async for request in guild.join_requests():
4087+
print(request.user, request.status)
4088+
4089+
Flattening into a list ::
4090+
4091+
requests = [request async for request in guild.join_requests()]
4092+
4093+
Parameters
4094+
-----------
4095+
status: :class:`JoinRequestStatus`
4096+
The status of the join requests to retrieve.
4097+
Defaults to :attr:`JoinRequestStatus.submitted`.
4098+
4099+
:attr:`JoinRequestStatus.started` cannot be used.
4100+
limit: Optional[:class:`int`]
4101+
The number of join requests to retrieve. If ``None``, retrieves every
4102+
join request with the given status. Note that this is potentially slow.
4103+
before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
4104+
Retrieve join requests before this date or join request.
4105+
If a datetime is provided, it is recommended to use a UTC aware datetime.
4106+
If the datetime is naive, it is assumed to be local time.
4107+
after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
4108+
Retrieve join requests after this date or join request.
4109+
If a datetime is provided, it is recommended to use a UTC aware datetime.
4110+
If the datetime is naive, it is assumed to be local time.
4111+
oldest_first: :class:`bool`
4112+
If set to ``True``, return join requests in oldest->newest order.
4113+
Defaults to ``True`` if ``after`` is specified, otherwise ``False``.
4114+
4115+
Raises
4116+
-------
4117+
ValueError
4118+
:attr:`JoinRequestStatus.started` was passed as the ``status``.
4119+
Forbidden
4120+
You do not have permissions to fetch the join requests.
4121+
HTTPException
4122+
Fetching the join requests failed.
4123+
4124+
Yields
4125+
-------
4126+
:class:`JoinRequest`
4127+
The join request that was fetched.
4128+
"""
4129+
if status is JoinRequestStatus.started:
4130+
raise ValueError('Join requests with a status of JoinRequestStatus.started cannot be queried')
4131+
4132+
if isinstance(before, datetime.datetime):
4133+
before = Object(id=utils.time_snowflake(before, high=False))
4134+
if isinstance(after, datetime.datetime):
4135+
after = Object(id=utils.time_snowflake(after, high=True))
4136+
4137+
state = self._state
4138+
endpoint = state.http.get_join_requests
4139+
4140+
def _cursor(request: JoinRequestPayload, *, high: bool) -> int:
4141+
if status is JoinRequestStatus.submitted:
4142+
return int(request['id'])
4143+
# actioned_at exists as a snowflake but it's deprecated
4144+
return utils.time_snowflake(
4145+
utils.parse_time(request['reviewed_at']), # pyright: ignore[reportTypedDictNotRequiredAccess]
4146+
high=high,
4147+
)
4148+
4149+
async def _before_strategy(retrieve: int, before: Optional[Snowflake], limit: Optional[int]):
4150+
before_id = before.id if before else None
4151+
data = (await endpoint(self.id, status.value, limit=retrieve, before=before_id))['guild_join_requests']
4152+
4153+
if data:
4154+
if limit is not None:
4155+
limit -= len(data)
4156+
4157+
before = Object(id=_cursor(data[-1], high=True))
4158+
4159+
return data, before, limit
4160+
4161+
async def _after_strategy(retrieve: int, after: Optional[Snowflake], limit: Optional[int]):
4162+
after_id = after.id if after else None
4163+
data = (await endpoint(self.id, status.value, limit=retrieve, after=after_id))['guild_join_requests']
4164+
4165+
if data:
4166+
if limit is not None:
4167+
limit -= len(data)
4168+
4169+
after = Object(id=_cursor(data[-1], high=False))
4170+
4171+
return data, after, limit
4172+
4173+
if oldest_first is MISSING:
4174+
reverse = after is not MISSING
4175+
else:
4176+
reverse = oldest_first
4177+
4178+
predicate = None
4179+
4180+
if reverse:
4181+
strategy, state_ = _after_strategy, after or OLDEST_OBJECT
4182+
if before is not MISSING:
4183+
predicate = lambda r: _cursor(r, high=False) < before.id
4184+
else:
4185+
strategy, state_ = _before_strategy, before
4186+
if after is not MISSING:
4187+
predicate = lambda r: _cursor(r, high=True) > after.id
4188+
4189+
seen: Set[int] = set()
4190+
4191+
while True:
4192+
retrieve = 100 if limit is None else min(limit, 100)
4193+
if retrieve < 1:
4194+
return
4195+
4196+
data, state_, limit = await strategy(retrieve, state_, limit)
4197+
4198+
# Terminate loop on next iteration; there's no data left after this
4199+
if len(data) < 100:
4200+
limit = 0
4201+
4202+
if data:
4203+
# Everything actioned in the same millisecond (somewhat common)
4204+
# as the page boundary is returned again on the next page
4205+
boundary = _cursor(data[-1], high=False)
4206+
fresh = [request for request in data if int(request['id']) not in seen]
4207+
seen = {int(request['id']) for request in data if _cursor(request, high=False) == boundary}
4208+
data = fresh
4209+
4210+
if predicate:
4211+
data = filter(predicate, data)
4212+
4213+
for raw_request in data:
4214+
yield JoinRequest(data=raw_request, state=state)
4215+
39674216
async def kick(self, user: Snowflake, *, reason: Optional[str] = None) -> None:
39684217
"""|coro|
39694218

0 commit comments

Comments
 (0)