Skip to content

Commit 57f71ba

Browse files
Paillat-devSoheabCopilotJustaSqu1dplun1331
authored
feat: Implement Community Invites (#3044)
Co-authored-by: Soheab <33902984+Soheab@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: JustaSqu1d <89910983+JustaSqu1d@users.noreply.github.com> Co-authored-by: plun1331 <plun1331@gmail.com> Co-authored-by: Lala Sabathil <lala@pycord.dev>
1 parent bf8eb8d commit 57f71ba

11 files changed

Lines changed: 510 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ These changes are available on the `master` branch, but have not yet been releas
1212

1313
### Added
1414

15+
- Added support for community invites.
16+
([#3044](https://github.com/Pycord-Development/pycord/pull/3044))
1517
- Added `Member.colours` and `Member.colors` properties.
1618
([#3063](https://github.com/Pycord-Development/pycord/pull/3063))
1719
- Added `RadioGroup`, `CheckboxGroup`, and `Checkbox` for modals.

discord/abc.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from .invite import Invite
5151
from .iterators import HistoryIterator, MessagePinIterator
5252
from .mentions import AllowedMentions
53+
from .object import Object
5354
from .partial_emoji import PartialEmoji, _EmojiTag
5455
from .permissions import PermissionOverwrite, Permissions
5556
from .role import Role
@@ -1208,6 +1209,8 @@ async def create_invite(
12081209
target_type: InviteTarget | None = None,
12091210
target_user: User | None = None,
12101211
target_application_id: int | None = None,
1212+
roles: list[Role | Object] | None = None,
1213+
target_users_file: File | None = None,
12111214
) -> Invite:
12121215
"""|coro|
12131216
@@ -1259,6 +1262,20 @@ async def create_invite(
12591262
12601263
.. versionadded:: 2.0
12611264
1265+
roles: Optional[List[Union[:class:`.Role`, :class:`.Object`]]]
1266+
The roles to give a user when joining through this invite.
1267+
1268+
You must have the :attr:`~Permissions.manage_roles` permission to do this and roles cannot be higher than your own.
1269+
1270+
.. versionadded:: 2.8
1271+
1272+
target_users_file: Optional[:class:`File`]
1273+
A CSV file with a single column of user IDs for all the users able to accept this invite.
1274+
1275+
You can use :func:`utils.users_to_csv` to generate a virtual CSV file from a sequence of user IDs.
1276+
1277+
.. versionadded:: 2.8
1278+
12621279
Returns
12631280
-------
12641281
:class:`~discord.Invite`
@@ -1283,8 +1300,11 @@ async def create_invite(
12831300
target_type=target_type.value if target_type else None,
12841301
target_user_id=target_user.id if target_user else None,
12851302
target_application_id=target_application_id,
1303+
roles=[str(r.id) for r in roles] if roles else None,
1304+
target_users_file=target_users_file,
12861305
)
12871306
invite = Invite.from_incomplete(data=data, state=self._state)
1307+
12881308
if target_event:
12891309
invite.set_scheduled_event(target_event)
12901310
return invite

discord/enums.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"SubscriptionStatus",
8585
"SeparatorSpacingSize",
8686
"SelectDefaultValueType",
87+
"InviteTargetUsersJobStatusCode",
8788
)
8889

8990

@@ -1133,6 +1134,13 @@ class SelectDefaultValueType(Enum):
11331134
user = "user"
11341135

11351136

1137+
class InviteTargetUsersJobStatusCode(Enum):
1138+
unspecified = 0
1139+
processing = 1
1140+
completed = 2
1141+
failed = 3
1142+
1143+
11361144
T = TypeVar("T")
11371145

11381146

discord/http.py

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@
9292
welcome_screen,
9393
widget,
9494
)
95+
from .types.invite import (
96+
InviteTargetUsersJobStatus as InviteTargetUsersJobStatusPayload,
97+
)
9598
from .types.snowflake import Snowflake, SnowflakeList
9699
from .types.soundboard import SoundboardSound as SoundboardSoundPayload
97100

@@ -103,16 +106,19 @@
103106
API_VERSION: int = 10
104107

105108

106-
async def json_or_text(response: aiohttp.ClientResponse) -> dict[str, Any] | str:
107-
text = await response.text(encoding="utf-8")
109+
async def parse_response(
110+
response: aiohttp.ClientResponse,
111+
) -> dict[str, Any] | str | bytes:
108112
try:
109113
if response.headers["content-type"] == "application/json":
110-
return utils._from_json(text)
114+
return utils._from_json(await response.text(encoding="utf-8"))
115+
elif response.headers["content-type"] == "text/csv":
116+
return await response.read()
111117
except KeyError:
112118
# Thanks Cloudflare
113119
pass
114120

115-
return text
121+
return await response.text(encoding="utf-8")
116122

117123

118124
class Route:
@@ -281,7 +287,7 @@ async def request(
281287
await self._global_over.wait()
282288

283289
response: aiohttp.ClientResponse | None = None
284-
data: dict[str, Any] | str | None = None
290+
data: dict[str, Any] | str | bytes | None = None
285291
await lock.acquire()
286292
with MaybeUnlock(lock) as maybe_lock:
287293
for tries in range(5):
@@ -308,7 +314,7 @@ async def request(
308314
)
309315

310316
# even errors have text involved in them so this is safe to call
311-
data = await json_or_text(response)
317+
data = await parse_response(response)
312318

313319
# check if we have rate limit header information
314320
remaining = response.headers.get("X-Ratelimit-Remaining")
@@ -2046,9 +2052,10 @@ def create_invite(
20462052
target_type: invite.InviteTargetType | None = None,
20472053
target_user_id: Snowflake | None = None,
20482054
target_application_id: Snowflake | None = None,
2055+
roles: list[Snowflake] | None = None,
2056+
target_users_file: File | None = None,
20492057
) -> Response[invite.Invite]:
2050-
r = Route("POST", "/channels/{channel_id}/invites", channel_id=channel_id)
2051-
payload = {
2058+
payload: dict[str, Any] = {
20522059
"max_age": max_age,
20532060
"max_uses": max_uses,
20542061
"temporary": temporary,
@@ -2064,7 +2071,27 @@ def create_invite(
20642071
if target_application_id:
20652072
payload["target_application_id"] = str(target_application_id)
20662073

2067-
return self.request(r, reason=reason, json=payload)
2074+
if roles:
2075+
payload["role_ids"] = roles
2076+
2077+
route = Route("POST", "/channels/{channel_id}/invites", channel_id=channel_id)
2078+
2079+
if target_users_file is not None:
2080+
form = [
2081+
{
2082+
"name": "target_users_file",
2083+
"value": target_users_file.fp,
2084+
"filename": target_users_file.filename,
2085+
"content_type": "text/csv",
2086+
},
2087+
{
2088+
"name": "payload_json",
2089+
"value": utils._to_json(payload),
2090+
},
2091+
]
2092+
return self.request(route, reason=reason, form=form)
2093+
2094+
return self.request(route, reason=reason, json=payload)
20682095

20692096
def get_invite(
20702097
self,
@@ -2086,6 +2113,46 @@ def get_invite(
20862113
Route("GET", "/invites/{invite_id}", invite_id=invite_id), params=params
20872114
)
20882115

2116+
def get_invite_target_users(
2117+
self,
2118+
invite_id: str,
2119+
) -> Response[bytes]:
2120+
return self.request(
2121+
Route("GET", "/invites/{invite_id}/target-users", invite_id=invite_id)
2122+
)
2123+
2124+
def update_invite_target_users(
2125+
self,
2126+
invite_id: str,
2127+
*,
2128+
target_users_file: File,
2129+
) -> Response[invite.Invite]:
2130+
form = [
2131+
{
2132+
"name": "target_users_file",
2133+
"value": target_users_file.fp,
2134+
"filename": target_users_file.filename,
2135+
"content_type": "text/csv",
2136+
},
2137+
]
2138+
2139+
return self.request(
2140+
Route("PUT", "/invites/{invite_id}/target-users", invite_id=invite_id),
2141+
form=form,
2142+
)
2143+
2144+
def get_invite_target_users_job_status(
2145+
self,
2146+
invite_id: str,
2147+
) -> Response[InviteTargetUsersJobStatusPayload]:
2148+
return self.request(
2149+
Route(
2150+
"GET",
2151+
"/invites/{invite_id}/target-users/job-status",
2152+
invite_id=invite_id,
2153+
),
2154+
)
2155+
20892156
def invites_from(self, guild_id: Snowflake) -> Response[list[invite.Invite]]:
20902157
return self.request(
20912158
Route("GET", "/guilds/{guild_id}/invites", guild_id=guild_id)

0 commit comments

Comments
 (0)