Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog/1510.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the new :meth:`Invite.fetch_target_users`, :meth:`Invite.update_target_users`, :meth:`Invite.fetch_target_users_job_status` methods as well as :attr:`Invite.roles`, :attr:`Invite.flags`, :class:`GuildInviteFlags`. Update :meth:`abc.GuildChannel.create_invite` to be able to specify ``roles`` and the ``target_users_file``.
26 changes: 24 additions & 2 deletions disnake/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import asyncio
import copy
from abc import ABC
from collections.abc import Callable, Mapping, Sequence
from collections.abc import Callable, Mapping, Sequence, Collection
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -1300,10 +1300,12 @@ async def create_invite(
unique: bool = True,
target_type: InviteTarget | None = None,
target_user: User | None = None,
target_users_file: File | None = None,
target_application: Snowflake | PartyType | None = None,
guild_scheduled_event: GuildScheduledEvent | None = None,
roles: Collection[Role] | None = None,
) -> Invite:
"""|coro|
r"""|coro|

Creates an instant invite from a text or voice channel.

Expand Down Expand Up @@ -1336,6 +1338,17 @@ async def create_invite(

.. versionadded:: 2.0

target_users_file: :class:`~disnake.File` | :data:`None`
A csv file with a list of users able to accept the invite.
This file must only have valid user ids separated by ``\n``.
A valid file content would look like this::

710570210159099984
1081815963990761542
... other user ids

.. versionadded:: |vnext|

target_application: :class:`.Snowflake` | :data:`None`
The ID of the embedded application for the invite, required if ``target_type`` is :attr:`.InviteTarget.embedded_application`.

Expand All @@ -1349,6 +1362,13 @@ async def create_invite(

.. versionadded:: 2.3

roles: :class:`list`\[:class:`.Role`] | :data:`None`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
roles: :class:`list`\[:class:`.Role`] | :data:`None`
roles: :class:`~collections.abc.Collection`\[:class:`.Role`] | :data:`None`

A list of roles added to the user upon accepting the invite.
You must have the :attr:`.Permissions.manage_roles` permission, and the roles must be
below the bot's top-most role.

.. versionadded:: |vnext|

reason: :class:`str` | :data:`None`
The reason for creating this invite. Shows up on the audit log.

Expand Down Expand Up @@ -1379,7 +1399,9 @@ async def create_invite(
unique=unique,
target_type=try_enum_to_int(target_type),
target_user_id=target_user.id if target_user else None,
target_users_file=target_users_file,
target_application_id=target_application.id if target_application else None,
role_ids=[r.id for r in roles] if roles else None,
)
invite = Invite.from_incomplete(data=data, state=self._state)
invite.guild_scheduled_event = guild_scheduled_event
Expand Down
75 changes: 75 additions & 0 deletions disnake/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"SKUFlags",
"ApplicationInstallTypes",
"InteractionContextTypes",
"GuildInviteFlags",
)

BF = TypeVar("BF", bound="BaseFlags")
Expand Down Expand Up @@ -2922,3 +2923,77 @@ def bot_dm(self) -> int:
def private_channel(self) -> int:
""":class:`bool`: Returns ``True`` if the command is usable in DMs and group DMs with other users."""
return 1 << 2


class GuildInviteFlags(BaseFlags):
"""Wraps up Discord Invite flags.

.. collapse:: operations

.. describe:: x == y

Checks if two GuildInviteFlags instances are equal.
.. describe:: x != y

Checks if two GuildInviteFlags instances are not equal.
.. describe:: x <= y

Checks if a GuildInviteFlags instance is a subset of another GuildInviteFlags instance.
.. describe:: x >= y

Checks if a GuildInviteFlags instance is a superset of another GuildInviteFlags instance.
.. describe:: x < y

Checks if a GuildInviteFlags instance is a strict subset of another GuildInviteFlags instance.
.. describe:: x > y

Checks if a GuildInviteFlags instance is a strict superset of another GuildInviteFlags instance.
.. describe:: x | y, x |= y

Returns a new GuildInviteFlags instance with all enabled flags from both x and y.
(Using ``|=`` will update in place).
.. describe:: x & y, x &= y

Returns a new GuildInviteFlags instance with only flags enabled on both x and y.
(Using ``&=`` will update in place).
.. describe:: x ^ y, x ^= y

Returns a new GuildInviteFlags instance with only flags enabled on one of x or y, but not both.
(Using ``^=`` will update in place).
.. describe:: ~x

Returns a new GuildInviteFlags instance with all flags from x inverted.
.. describe:: hash(x)

Returns the flag's hash.
.. describe:: iter(x)

Returns an iterator of ``(name, value)`` pairs. This allows it
to be, for example, constructed as a dict or a list of pairs.
Note that aliases are not shown.

Additionally supported are a few operations on class attributes.

.. describe:: GuildInviteFlags.y | GuildInviteFlags.z, GuildInviteFlags(y=True) | GuildInviteFlags.z

Returns a GuildInviteFlags instance with all provided flags enabled.

.. describe:: ~GuildInviteFlags.y

Returns a GuildInviteFlags instance with all flags except ``y`` inverted from their default value.

.. versionadded:: |vnext|

Attributes
----------
value: :class:`int`
The raw value. You should query flags via the properties
rather than using this raw value.
"""

__slots__ = ()

@flag_value
def is_guest_invite(self) -> int:
""":class:`bool`: Returns ``True`` if this invite is a guest invite for a voice channel."""
return 1 << 0
46 changes: 41 additions & 5 deletions disnake/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,28 @@ def set_attachments(payload: dict[str, Any], files: Sequence[File]) -> None:
payload["attachments"] = attachments


def to_multipart(payload: dict[str, Any], files: Sequence[File]) -> list[dict[str, Any]]:
def to_multipart(
payload: dict[str, Any], files: Sequence[File], *, is_csv: bool = False
) -> list[dict[str, Any]]:
"""Converts the payload and list of files to a multipart payload,
as specified by https://docs.discord.com/developers/reference#uploading-files
"""
multipart: list[dict[str, Any]] = []
for index, file in enumerate(files):
multipart.append(
multipart: list[dict[str, Any]]
if is_csv:
multipart = [
{"name": "target_users_file", "value": file.fp, "content_type": "text/csv"}
for file in files
]
else:
multipart = [
{
"name": f"files[{index}]",
"value": file.fp,
"filename": file.filename,
"content_type": "application/octet-stream",
}
)
for index, file in enumerate(files)
]

multipart.append({"name": "payload_json", "value": utils._to_json(payload)})
return multipart
Expand Down Expand Up @@ -1950,7 +1958,9 @@ def create_invite(
unique: bool = True,
target_type: invite.InviteTargetType | None = None,
target_user_id: Snowflake | None = None,
target_users_file: File | None = None,
target_application_id: Snowflake | None = None,
role_ids: list[Snowflake] | None = None,
) -> Response[invite.Invite]:
r = Route("POST", "/channels/{channel_id}/invites", channel_id=channel_id)
payload: dict[str, Any] = {
Expand All @@ -1969,6 +1979,16 @@ def create_invite(
if target_application_id:
payload["target_application_id"] = str(target_application_id)

if role_ids:
payload["role_ids"] = role_ids

if target_users_file:
return self.request(
r,
reason=reason,
form=to_multipart(payload=payload, files=[target_users_file], is_csv=True),
)

return self.request(r, reason=reason, json=payload)

def get_invite(
Expand All @@ -1988,6 +2008,22 @@ def get_invite(
Route("GET", "/invites/{invite_id}", invite_id=invite_id), params=params
)

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 update_invite_target_users(self, invite_id: str, *, file: File) -> Response[None]:
return self.request(
Route("PUT", "/invites/{invite_id}/target-users", invite_id=invite_id),
form=to_multipart(payload={}, files=[file], is_csv=True),
)

def get_invite_target_users_job_status(
self, invite_id: str
) -> Response[invite.TargetUsersJobPayload]:
return self.request(
Route("GET", "/invites/{invite_id}/target-users/job-status", invite_id=invite_id)
)

def invites_from(self, guild_id: Snowflake) -> Response[list[invite.Invite]]:
return self.request(Route("GET", "/guilds/{guild_id}/invites", guild_id=guild_id))

Expand Down
Loading
Loading