/
member.py
442 lines (406 loc) · 16 KB
/
member.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
from datetime import datetime
from typing import List, Optional, Union
from .channel import Channel
from .flags import Permissions
from .misc import MISSING, DictSerializerMixin, Snowflake
from .role import Role
from .user import User
class Member(DictSerializerMixin):
"""
A class object representing the user of a guild, known as a "member."
.. note::
``pending`` and ``permissions`` only apply for members retroactively
requiring to verify rules via. membership screening or lack permissions
to speak.
:ivar User user: The user of the guild.
:ivar str nick: The nickname of the member.
:ivar Optional[str] avatar?: The hash containing the user's guild avatar, if applicable.
:ivar List[Role] roles: The list of roles of the member.
:ivar datetime joined_at: The timestamp the member joined the guild at.
:ivar datetime premium_since: The timestamp the member has been a server booster since.
:ivar bool deaf: Whether the member is deafened.
:ivar bool mute: Whether the member is muted.
:ivar Optional[bool] pending?: Whether the member is pending to pass membership screening.
:ivar Optional[Permissions] permissions?: Whether the member has permissions.
:ivar Optional[str] communication_disabled_until?: How long until they're unmuted, if any.
"""
__slots__ = (
"_json",
"user",
"nick",
"avatar",
"roles",
"joined_at",
"premium_since",
"deaf",
"mute",
"is_pending",
"pending",
"permissions",
"communication_disabled_until",
"hoisted_role",
"_client",
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.user = (
self.user
if isinstance(self.user, User)
else (User(**self.user) if self._json.get("user") else None)
)
self.joined_at = (
datetime.fromisoformat(self._json.get("joined_at"))
if self._json.get("joined_at")
else None
)
self.premium_since = (
datetime.fromisoformat(self._json.get("premium_since"))
if self._json.get("premium_since")
else None
)
self.permissions = (
Permissions(int(self._json.get("permissions")))
if self._json.get("permissions")
else None
)
if not self.avatar and self.user:
self.avatar = self.user.avatar
@property
def id(self) -> Snowflake:
"""
Returns the ID of the user.
:return: The ID of the user
:rtype: Snowflake
"""
return self.user.id if self.user else None
async def ban(
self,
guild_id: int,
reason: Optional[str] = None,
delete_message_days: Optional[int] = 0,
) -> None:
"""
Bans the member from a guild.
:param guild_id: The id of the guild to ban the member from
:type guild_id: int
:param reason?: The reason of the ban
:type reason: Optional[str]
:param delete_message_days?: Number of days to delete messages, from 0 to 7. Defaults to 0
:type delete_message_days: Optional[int]
"""
await self._client.create_guild_ban(
guild_id=guild_id,
user_id=int(self.user.id),
reason=reason,
delete_message_days=delete_message_days,
)
async def kick(
self,
guild_id: int,
reason: Optional[str] = None,
) -> None:
"""
Kicks the member from a guild.
:param guild_id: The id of the guild to kick the member from
:type guild_id: int
:param reason?: The reason for the kick
:type reason: Optional[str]
"""
if not self._client:
raise AttributeError("HTTPClient not found!")
await self._client.create_guild_kick(
guild_id=guild_id,
user_id=int(self.user.id),
reason=reason,
)
async def add_role(
self,
role: Union[Role, int],
guild_id: int,
reason: Optional[str],
) -> None:
"""
This method adds a role to a member.
:param role: The role to add. Either ``Role`` object or role_id
:type role: Union[Role, int]
:param guild_id: The id of the guild to add the roles to the member
:type guild_id: int
:param reason?: The reason why the roles are added
:type reason: Optional[str]
"""
if not self._client:
raise AttributeError("HTTPClient not found!")
if isinstance(role, Role):
await self._client.add_member_role(
guild_id=guild_id,
user_id=int(self.user.id),
role_id=int(role.id),
reason=reason,
)
else:
await self._client.add_member_role(
guild_id=guild_id,
user_id=int(self.user.id),
role_id=role,
reason=reason,
)
async def remove_role(
self,
role: Union[Role, int],
guild_id: int,
reason: Optional[str],
) -> None:
"""
This method removes a role from a member.
:param role: The role to remove. Either ``Role`` object or role_id
:type role: Union[Role, int]
:param guild_id: The id of the guild to remove the roles of the member
:type guild_id: int
:param reason?: The reason why the roles are removed
:type reason: Optional[str]
"""
if not self._client:
raise AttributeError("HTTPClient not found!")
if isinstance(role, Role):
await self._client.remove_member_role(
guild_id=guild_id,
user_id=int(self.user.id),
role_id=int(role.id),
reason=reason,
)
else:
await self._client.remove_member_role(
guild_id=guild_id,
user_id=int(self.user.id),
role_id=role,
reason=reason,
)
async def send(
self,
content: Optional[str] = MISSING,
*,
components: Optional[
Union[
"ActionRow", # noqa
"Button", # noqa
"SelectMenu", # noqa
List["ActionRow"], # noqa
List["Button"], # noqa
List["SelectMenu"], # noqa
]
] = MISSING,
tts: Optional[bool] = MISSING,
# attachments: Optional[List[Any]] = None, # TODO: post-v4: Replace with own file type.
embeds: Optional[Union["Embed", List["Embed"]]] = MISSING, # noqa
allowed_mentions: Optional["MessageInteraction"] = MISSING, # noqa
):
"""
Sends a DM to the member.
:param content?: The contents of the message as a string or string-converted value.
:type content: Optional[str]
:param components?: A component, or list of components for the message.
:type components: Optional[Union[ActionRow, Button, SelectMenu, List[Actionrow], List[Button], List[SelectMenu]]]
:param tts?: Whether the message utilizes the text-to-speech Discord programme or not.
:type tts: Optional[bool]
:param embeds?: An embed, or list of embeds for the message.
:type embeds: Optional[Union[Embed, List[Embed]]]
:param allowed_mentions?: The message interactions/mention limits that the message can refer to.
:type allowed_mentions: Optional[MessageInteraction]
:return: The sent message as an object.
:rtype: Message
"""
if not self._client:
raise AttributeError("HTTPClient not found!")
from ...models.component import ActionRow, Button, SelectMenu
from .message import Message
_content: str = "" if content is MISSING else content
_tts: bool = False if tts is MISSING else tts
# _file = None if file is None else file
# _attachments = [] if attachments else None
_embeds: list = (
[]
if not embeds or embeds is MISSING
else ([embed._json for embed in embeds] if isinstance(embeds, list) else [embeds._json])
)
_allowed_mentions: dict = {} if allowed_mentions is MISSING else allowed_mentions
if not components or components is MISSING:
_components = []
# TODO: Break this obfuscation pattern down to a "builder" method.
else:
_components: List[dict] = [{"type": 1, "components": []}]
if isinstance(components, list) and all(
isinstance(action_row, ActionRow) for action_row in components
):
_components = [
{
"type": 1,
"components": [
(
component._json
if component._json.get("custom_id") or component._json.get("url")
else []
)
for component in action_row.components
],
}
for action_row in components
]
elif isinstance(components, list) and all(
isinstance(component, (Button, SelectMenu)) for component in components
):
for component in components:
if isinstance(component, SelectMenu):
component._json["options"] = [
options._json if not isinstance(options, dict) else options
for options in component._json["options"]
]
_components = [
{
"type": 1,
"components": [
(
component._json
if component._json.get("custom_id") or component._json.get("url")
else []
)
for component in components
],
}
]
elif isinstance(components, list) and all(
isinstance(action_row, (list, ActionRow)) for action_row in components
):
_components = []
for action_row in components:
for component in (
action_row if isinstance(action_row, list) else action_row.components
):
if isinstance(component, SelectMenu):
component._json["options"] = [
option._json for option in component.options
]
_components.append(
{
"type": 1,
"components": [
(
component._json
if component._json.get("custom_id")
or component._json.get("url")
else []
)
for component in (
action_row
if isinstance(action_row, list)
else action_row.components
)
],
}
)
elif isinstance(components, ActionRow):
_components[0]["components"] = [
(
component._json
if component._json.get("custom_id") or component._json.get("url")
else []
)
for component in components.components
]
elif isinstance(components, Button):
_components[0]["components"] = (
[components._json]
if components._json.get("custom_id") or components._json.get("url")
else []
)
elif isinstance(components, SelectMenu):
components._json["options"] = [
options._json if not isinstance(options, dict) else options
for options in components._json["options"]
]
_components[0]["components"] = (
[components._json]
if components._json.get("custom_id") or components._json.get("url")
else []
)
# TODO: post-v4: Add attachments into Message obj.
payload = Message(
content=_content,
tts=_tts,
# file=file,
# attachments=_attachments,
embeds=_embeds,
components=_components,
allowed_mentions=_allowed_mentions,
)
channel = Channel(**await self._client.create_dm(recipient_id=int(self.user.id)))
res = await self._client.create_message(channel_id=int(channel.id), payload=payload._json)
return Message(**res, _client=self._client)
async def modify(
self,
guild_id: int,
nick: Optional[str] = MISSING,
roles: Optional[List[int]] = MISSING,
mute: Optional[bool] = MISSING,
deaf: Optional[bool] = MISSING,
channel_id: Optional[int] = MISSING,
communication_disabled_until: Optional[datetime.isoformat] = MISSING,
reason: Optional[str] = None,
) -> "Member":
"""
Modifies the member of a guild.
:param guild_id: The id of the guild to modify the member on
:type guild_id: int
:param nick?: The nickname of the member
:type nick: Optional[str]
:param roles?: A list of all role ids the member has
:type roles: Optional[List[int]]
:param mute?: whether the user is muted in voice channels
:type mute: Optional[bool]
:param deaf?: whether the user is deafened in voice channels
:type deaf: Optional[bool]
:param channel_id?: id of channel to move user to (if they are connected to voice)
:type channel_id: Optional[int]
:param communication_disabled_until?: when the user's timeout will expire and the user will be able to communicate in the guild again (up to 28 days in the future)
:type communication_disabled_until: Optional[datetime.isoformat]
:param reason?: The reason of the modifying
:type reason: Optional[str]
:return: The modified member object
:rtype: Member
"""
if not self._client:
raise AttributeError("HTTPClient not found!")
payload = {}
if nick is not MISSING:
payload["nick"] = nick
if roles is not MISSING:
payload["roles"] = roles
if channel_id is not MISSING:
payload["channel_id"] = channel_id
if mute is not MISSING:
payload["mute"] = mute
if deaf is not MISSING:
payload["deaf"] = deaf
if communication_disabled_until is not MISSING:
payload["communication_disabled_until"] = communication_disabled_until
res = await self._client.modify_member(
user_id=int(self.user.id),
guild_id=guild_id,
payload=payload,
reason=reason,
)
return Member(**res, _client=self._client)
async def add_to_thread(
self,
thread_id: int,
) -> None:
"""
Adds the member to a thread.
:param thread_id: The id of the thread to add the member to
:type thread_id: int
"""
if not self._client:
raise AttributeError("HTTPClient not found!")
await self._client.add_member_to_thread(
user_id=int(self.user.id),
thread_id=thread_id,
)