Browse Source

Add support for community invites

pull/10386/head
Soheab 6 months ago
parent
commit
c082d3e16d
  1. 16
      discord/abc.py
  2. 8
      discord/enums.py
  3. 65
      discord/http.py
  4. 146
      discord/invite.py
  5. 11
      discord/types/invite.py
  6. 22
      docs/api.rst

16
discord/abc.py

@ -1283,6 +1283,8 @@ class GuildChannel:
target_user: Optional[User] = None, target_user: Optional[User] = None,
target_application_id: Optional[int] = None, target_application_id: Optional[int] = None,
guest: bool = False, guest: bool = False,
roles: Optional[List[Snowflake]] = None,
users: Optional[List[Snowflake]] = None,
) -> Invite: ) -> Invite:
"""|coro| """|coro|
@ -1325,6 +1327,18 @@ class GuildChannel:
Whether the invite is a guest invite. Whether the invite is a guest invite.
.. versionadded:: 2.6 .. versionadded:: 2.6
roles: Optional[List[:class:`~discord.abc.Snowflake`]]
A list of roles that should be granted to the users joining via this invite.
Requires :attr:`~discord.Permissions.manage_guild` permission.
.. versionadded:: 2.7
users: Optional[List[:class:`~discord.abc.Snowflake`]]
A list of user IDs that should be able to access this invite.
Requires :attr:`~discord.Permissions.manage_guild` permission.
.. versionadded:: 2.7
Raises Raises
------- -------
@ -1358,6 +1372,8 @@ class GuildChannel:
target_user_id=target_user.id if target_user else None, target_user_id=target_user.id if target_user else None,
target_application_id=target_application_id, target_application_id=target_application_id,
flags=flags.value if flags else None, flags=flags.value if flags else None,
role_ids=[role.id for role in roles or []],
user_ids=[user.id for user in users or []],
) )
return Invite.from_incomplete(data=data, state=self._state) return Invite.from_incomplete(data=data, state=self._state)

8
discord/enums.py

@ -87,6 +87,7 @@ __all__ = (
'MediaItemLoadingState', 'MediaItemLoadingState',
'CollectibleType', 'CollectibleType',
'NameplatePalette', 'NameplatePalette',
'InviteTargetUsersJobErrorStatus'
) )
@ -1002,6 +1003,13 @@ class NameplatePalette(Enum):
white = 'white' white = 'white'
class InviteTargetUsersJobErrorStatus(Enum):
unspecified = 0
pending = 1
completed = 2
failed = 3
def create_unknown_value(cls: Type[E], val: Any) -> E: def create_unknown_value(cls: Type[E], val: Any) -> E:
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'unknown_{val}' name = f'unknown_{val}'

65
discord/http.py

@ -1837,6 +1837,24 @@ class HTTPClient:
# Invite management # Invite management
def _generate_invite_multipart(
self,
*,
payload: dict[str, Any],
user_ids: List[Snowflake],
) -> MultipartParameters:
users ="Users\n" + "\n".join(str(user_id) for user_id in user_ids)
form = [
{'name': 'payload_json', 'value': utils._to_json(payload)},
{'name': 'target_users_file', 'value': users, 'filename': 'users.csv', 'content_type': 'text/csv'}
]
return MultipartParameters(
multipart=form,
payload={},
files=None,
)
def create_invite( def create_invite(
self, self,
channel_id: Snowflake, channel_id: Snowflake,
@ -1850,6 +1868,8 @@ class HTTPClient:
target_user_id: Optional[Snowflake] = None, target_user_id: Optional[Snowflake] = None,
target_application_id: Optional[Snowflake] = None, target_application_id: Optional[Snowflake] = None,
flags: Optional[int] = None, flags: Optional[int] = None,
role_ids: Optional[List[Snowflake]] = None,
user_ids: Optional[List[Snowflake]] = None,
) -> Response[invite.Invite]: ) -> Response[invite.Invite]:
r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id) r = Route('POST', '/channels/{channel_id}/invites', channel_id=channel_id)
payload = { payload = {
@ -1871,6 +1891,17 @@ class HTTPClient:
if flags: if flags:
payload['flags'] = flags payload['flags'] = flags
if role_ids:
payload["role_ids"] = list(map(str, role_ids))
if user_ids:
multipart_params = self._generate_invite_multipart(payload=payload, user_ids=user_ids)
return self.request(
r,
form=multipart_params.multipart,
reason=reason,
)
return self.request(r, reason=reason, json=payload) return self.request(r, reason=reason, json=payload)
def get_invite( def get_invite(
@ -1898,6 +1929,40 @@ class HTTPClient:
def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Response[invite.Invite]: def delete_invite(self, invite_id: str, *, reason: Optional[str] = None) -> Response[invite.Invite]:
return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason) return self.request(Route('DELETE', '/invites/{invite_id}', invite_id=invite_id), reason=reason)
def get_invite_target_users(
self,
invite_id: str,
) -> Response[str]:
return self.request(
Route('GET', '/invites/{invite_id}/target-users', invite_id=invite_id)
)
def edit_invite_target_users(
self,
invite_id: str,
user_ids: List[Snowflake],
) -> Response[None]:
multipart_params = self._generate_invite_multipart(
payload={},
user_ids=user_ids,
)
return self.request(
Route('PUT', '/invites/{invite_id}/target-users', invite_id=invite_id),
form=multipart_params.multipart,
)
def get_invite_target_users_job_status(
self,
invite_id: str,
) -> Response[invite.InviteTargetUsersJobStatus]:
return self.request(
Route(
"GET",
'/invites/{invite_id}/target-users/job-status',
invite_id=invite_id,
)
)
# Role management # Role management
def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]: def get_roles(self, guild_id: Snowflake) -> Response[List[role.Role]]:

146
discord/invite.py

@ -26,13 +26,22 @@ from __future__ import annotations
from typing import List, Optional, Union, TYPE_CHECKING from typing import List, Optional, Union, TYPE_CHECKING
from .asset import Asset from .asset import Asset
from .utils import parse_time, snowflake_time, _get_as_snowflake from .utils import parse_time, snowflake_time, _get_as_snowflake, MISSING
from .object import Object from .object import Object
from .mixins import Hashable from .mixins import Hashable
from .enums import ChannelType, NSFWLevel, VerificationLevel, InviteTarget, InviteType, try_enum from .enums import (
ChannelType,
NSFWLevel,
VerificationLevel,
InviteTarget,
InviteType,
InviteTargetUsersJobErrorStatus,
try_enum,
)
from .appinfo import PartialAppInfo from .appinfo import PartialAppInfo
from .scheduled_event import ScheduledEvent from .scheduled_event import ScheduledEvent
from .flags import InviteFlags from .flags import InviteFlags
from .role import Role
__all__ = ( __all__ = (
'PartialInviteChannel', 'PartialInviteChannel',
@ -47,6 +56,7 @@ if TYPE_CHECKING:
Invite as InvitePayload, Invite as InvitePayload,
InviteGuild as InviteGuildPayload, InviteGuild as InviteGuildPayload,
GatewayInvite as GatewayInvitePayload, GatewayInvite as GatewayInvitePayload,
InviteTargetUsersJobStatus as InviteTargetUsersJobStatusPayload,
) )
from .types.guild import GuildFeature from .types.guild import GuildFeature
from .types.channel import ( from .types.channel import (
@ -64,6 +74,47 @@ if TYPE_CHECKING:
import datetime import datetime
class InviteTargetUsersJobStatus:
"""Represents the status of an invite's target users job.
.. versionadded:: 2.7
Attributes
-----------
invite: :class:`Invite`
The invite this job status is for.
status: :class:`InviteTargetUsersJobStatus`
The status of the job.
total_users: :class:`int`
The total number of users in the job.
processed_users: :class:`int`
The number of users that have been processed so far.
created_at: :class:`datetime.datetime`
The time the job was created.
error_message: :class:`str`
The error message.
completed_at: Optional[:class:`datetime.datetime`]
The time the job was completed, if applicable.
"""
def __init__(self, *, invite: Invite, data: InviteTargetUsersJobStatusPayload) -> None:
self.invite: Invite = invite
self.status: InviteTargetUsersJobErrorStatus = try_enum(InviteTargetUsersJobErrorStatus, data['status'])
self.total_users: int = data['total_users']
self.processed_users: int = data['processed_users']
self.created_at: datetime.datetime = parse_time(data['created_at'])
self.error_message: str = data['error_message']
self.completed_at: Optional[datetime.datetime] = (
parse_time(data['completed_at']) if data.get('completed_at') else None
)
def __repr__(self) -> str:
return (
f'<InviteTargetUsersJobStatus invite={self.invite.code!r} status={self.status} '
f'total_users={self.total_users} processed_users={self.processed_users}>'
)
class PartialInviteChannel: class PartialInviteChannel:
"""Represents a "partial" invite channel. """Represents a "partial" invite channel.
@ -358,6 +409,9 @@ class Invite(Hashable):
The ID of the scheduled event associated with this invite, if any. The ID of the scheduled event associated with this invite, if any.
.. versionadded:: 2.0 .. versionadded:: 2.0
roles: List[:class:`Role`]
A list of roles the invite grants access to. Only available if the
invite's guild is cached.
""" """
__slots__ = ( __slots__ = (
@ -382,6 +436,7 @@ class Invite(Hashable):
'scheduled_event_id', 'scheduled_event_id',
'type', 'type',
'_flags', '_flags',
'roles',
) )
BASE = 'https://discord.gg' BASE = 'https://discord.gg'
@ -437,6 +492,13 @@ class Invite(Hashable):
self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None self.scheduled_event_id: Optional[int] = self.scheduled_event.id if self.scheduled_event else None
self._flags: int = data.get('flags', 0) self._flags: int = data.get('flags', 0)
if self.guild is not None and not isinstance(self.guild, (PartialInviteGuild, Object)):
self.roles: List[Role] = [
Role(state=self._state, guild=self.guild, data=role_data) for role_data in data.get('roles', [])
]
else:
self.roles: List[Role] = []
@classmethod @classmethod
def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self: def from_incomplete(cls, *, state: ConnectionState, data: InvitePayload) -> Self:
guild: Optional[Union[Guild, PartialInviteGuild]] guild: Optional[Union[Guild, PartialInviteGuild]]
@ -582,3 +644,83 @@ class Invite(Hashable):
data = await self._state.http.delete_invite(self.code, reason=reason) data = await self._state.http.delete_invite(self.code, reason=reason)
return self.from_incomplete(state=self._state, data=data) return self.from_incomplete(state=self._state, data=data)
async def fetch_target_users(self) -> list[int]:
"""|coro|
Fetches the target users CSV file for this invite.
Requires the :attr:`~Permissions.manage_guild` permission.
Returns
--------
List[:class:`int`]
A list of user IDs representing the target users.
Raises
-------
Forbidden
You do not have permissions to fetch target users.
NotFound
The invite is invalid or expired or the invite does not have target users.
HTTPException
Fetching the target users failed.
"""
string = await self._state.http.get_invite_target_users(self.code)
users = string.lstrip('Users\n').split('\n')
return [int(user_id) for user_id in users if user_id]
async def fetch_target_users_job_status(self) -> InviteTargetUsersJobStatus:
"""|coro|
Fetches the status of the target users job for this invite.
Requires the :attr:`~Permissions.manage_guild` permission.
Returns
--------
:class:`InviteTargetUsersJobStatus`
A dictionary containing the status of the target users job.
Raises
-------
Forbidden
You do not have permissions to fetch target users job status.
NotFound
The invite is invalid or expired or there is no ongoing target users job.
HTTPException
Fetching the target users job status failed.
"""
data = await self._state.http.get_invite_target_users_job_status(self.code)
return InviteTargetUsersJobStatus(invite=self, data=data)
async def edit(
self,
*,
users: List[Snowflake] = MISSING,
) -> None:
"""|coro|
Edits the invite.
Requires the :attr:`~Permissions.manage_guild` permission.
Parameters
-----------
users: List[:class:`~discord.abc.Snowflake`]
A list of users that should be able to use this invite.
Raises
-------
Forbidden
You do not have permissions to edit invites.
NotFound
The invite is invalid or expired.
HTTPException
Editing the invite failed.
"""
if users is not MISSING:
await self._state.http.edit_invite_target_users(self.code, user_ids=[user.id for user in users])

11
discord/types/invite.py

@ -33,6 +33,7 @@ from .guild import InviteGuild, _GuildPreviewUnique
from .channel import PartialChannel from .channel import PartialChannel
from .user import PartialUser from .user import PartialUser
from .appinfo import PartialAppInfo from .appinfo import PartialAppInfo
from .role import Role
InviteTargetType = Literal[1, 2] InviteTargetType = Literal[1, 2]
InviteType = Literal[0, 1, 2] InviteType = Literal[0, 1, 2]
@ -66,6 +67,7 @@ class Invite(IncompleteInvite, total=False):
type: InviteType type: InviteType
flags: NotRequired[int] flags: NotRequired[int]
expires_at: Optional[str] expires_at: Optional[str]
roles: NotRequired[list[Role]]
class InviteWithCounts(Invite, _GuildPreviewUnique): ... class InviteWithCounts(Invite, _GuildPreviewUnique): ...
@ -95,3 +97,12 @@ class GatewayInviteDelete(TypedDict):
GatewayInvite = Union[GatewayInviteCreate, GatewayInviteDelete] GatewayInvite = Union[GatewayInviteCreate, GatewayInviteDelete]
class InviteTargetUsersJobStatus(TypedDict):
status: int
total_users: int
processed_users: int
created_at: str
error_message: str
completed_at: Optional[str]

22
docs/api.rst

@ -4154,6 +4154,28 @@ of :class:`enum.Enum`.
The collectible nameplate palette is white. The collectible nameplate palette is white.
.. class:: InviteTargetUsersJobErrorStatus
Represents the error status of an invite target users job.
.. versionadded:: 2.7
.. attribute:: unspecified
The default value.
.. attribute:: processing
The job is still being processed.
.. attribute:: completed
The job has been completed successfully.
.. attribute:: failed
The job has failed, see `error_message` field for more details.
.. _discord-api-audit-logs: .. _discord-api-audit-logs:
Audit Log Data Audit Log Data

Loading…
Cancel
Save