1
+ import contextlib
1
2
import re
2
3
from asyncio import TimeoutError as AsyncTimeoutError
3
4
from random import choice
4
- from typing import Optional , Union
5
+ from typing import Literal , Optional , Union
5
6
6
7
import aiohttp
7
8
import discord
9
+ from red_commons .logging import getLogger
8
10
from redbot .core import commands
9
11
from redbot .core .i18n import Translator , cog_i18n
10
12
from redbot .core .utils import chat_formatting as chat
11
13
from redbot .core .utils .mod import get_audit_reason
12
14
from redbot .core .utils .predicates import MessagePredicate
13
15
14
- try :
15
- from redbot import json # support of Draper's branch
16
- except ImportError :
17
- import json
18
-
19
16
_ = Translator ("AdminUtils" , __file__ )
20
17
21
18
EMOJI_RE = re .compile (r"(<(a)?:[a-zA-Z0-9_]+:([0-9]+)>)" )
22
19
20
+ CHANNEL_REASONS = {
21
+ discord .CategoryChannel : _ ("You are not allowed to edit this category." ),
22
+ discord .TextChannel : _ ("You are not allowed to edit this channel." ),
23
+ discord .VoiceChannel : _ ("You are not allowed to edit this channel." ),
24
+ discord .StageChannel : _ ("You are not allowed to edit this channel." ),
25
+ }
26
+
27
+
28
+ async def check_regions (ctx ):
29
+ """Check if regions list is populated"""
30
+ return ctx .cog .regions
31
+
23
32
24
33
@cog_i18n (_ )
25
34
class AdminUtils (commands .Cog ):
26
35
"""Useful commands for server administrators."""
27
36
28
- __version__ = "2.5.11 "
37
+ __version__ = "3.0.0 "
29
38
30
39
# noinspection PyMissingConstructor
31
40
def __init__ (self , bot ):
32
41
self .bot = bot
33
- self .session = aiohttp .ClientSession (json_serialize = json .dumps )
42
+ self .session = aiohttp .ClientSession ()
43
+ self .log = getLogger ("red.fixator10-cogs.adminutils" )
44
+ self .regions = []
45
+
46
+ async def cog_load (self ):
47
+ try :
48
+ regions = await self .bot .http .request (discord .http .Route ("GET" , "/voice/regions" ))
49
+ self .regions = [region ["id" ] for region in regions ]
50
+ except Exception as e :
51
+ self .log .warning (
52
+ "Unable to get list of rtc_regions. [p]restartvoice command will be unavailable" ,
53
+ exc_info = e ,
54
+ )
34
55
35
- def cog_unload (self ):
36
- self .bot . loop . create_task ( self . session .close () )
56
+ async def cog_unload (self ):
57
+ await self .session .close ()
37
58
38
59
def format_help_for_context (self , ctx : commands .Context ) -> str : # Thanks Sinbad!
39
60
pre_processed = super ().format_help_for_context (ctx )
@@ -45,20 +66,19 @@ async def red_delete_data_for_user(self, **kwargs):
45
66
@staticmethod
46
67
def check_channel_permission (
47
68
ctx : commands .Context ,
48
- channel_or_category : Union [discord .TextChannel , discord .CategoryChannel ],
69
+ channel_or_category : Union [
70
+ discord .TextChannel ,
71
+ discord .CategoryChannel ,
72
+ discord .VoiceChannel ,
73
+ discord .StageChannel ,
74
+ ],
49
75
) -> bool :
50
76
"""
51
77
Check user's permission in a channel, to be sure he can edit it.
52
78
"""
53
- mc = channel_or_category .permissions_for (ctx .author ).manage_channels
54
- if mc :
79
+ if channel_or_category .permissions_for (ctx .author ).manage_channels :
55
80
return True
56
- reason = (
57
- _ ("You are not allowed to edit this channel." )
58
- if not isinstance (channel_or_category , discord .CategoryChannel )
59
- else _ ("You are not allowed to edit in this category." )
60
- )
61
- raise commands .UserFeedbackCheckFailure (reason )
81
+ raise commands .UserFeedbackCheckFailure (CHANNEL_REASONS .get (type (channel_or_category )))
62
82
63
83
@commands .command (name = "prune" )
64
84
@commands .guild_only ()
@@ -92,10 +112,8 @@ async def cleanup_users(self, ctx, days: Optional[int] = 1, *roles: discord.Role
92
112
).format (to_kick = to_kick , days = days , roles = roles_text if roles else "" )
93
113
)
94
114
)
95
- try :
115
+ with contextlib . suppress ( AsyncTimeoutError ) :
96
116
await self .bot .wait_for ("message" , check = pred , timeout = 30 )
97
- except AsyncTimeoutError :
98
- pass
99
117
if ctx .assume_yes or pred .result :
100
118
cleanup = await ctx .guild .prune_members (
101
119
days = days , reason = get_audit_reason (ctx .author ), roles = roles or None
@@ -113,23 +131,20 @@ async def cleanup_users(self, ctx, days: Optional[int] = 1, *roles: discord.Role
113
131
114
132
@commands .command ()
115
133
@commands .guild_only ()
116
- @commands .admin_or_permissions (manage_guild = True )
117
- @commands .bot_has_permissions (manage_guild = True )
118
- async def restartvoice (self , ctx : commands .Context ):
119
- """Change server's voice region to random and back
134
+ @commands .check (check_regions )
135
+ @commands .admin_or_permissions (manage_channels = True )
136
+ @commands .bot_has_permissions (manage_channels = True )
137
+ async def restartvoice (
138
+ self , ctx : commands .Context , channel : Union [discord .VoiceChannel , discord .StageChannel ]
139
+ ):
140
+ """Change voice channel's region to random and back
120
141
121
142
Useful to reinitate all voice connections"""
122
- current_region = ctx .guild .region
123
- random_region = choice (
124
- [
125
- r
126
- for r in discord .VoiceRegion
127
- if not r .value .startswith ("vip" ) and current_region != r
128
- ]
129
- )
130
- await ctx .guild .edit (region = random_region )
131
- await ctx .guild .edit (
132
- region = current_region ,
143
+ current_region = channel .rtc_region
144
+ random_region = choice ([r for r in self .regions if current_region != r ])
145
+ await channel .edit (rtc_region = random_region )
146
+ await channel .edit (
147
+ rtc_region = current_region ,
133
148
reason = get_audit_reason (ctx .author , _ ("Voice restart" )),
134
149
)
135
150
await ctx .tick ()
@@ -142,8 +157,8 @@ async def restartvoice(self, ctx: commands.Context):
142
157
async def massmove (
143
158
self ,
144
159
ctx : commands .Context ,
145
- from_channel : discord .VoiceChannel ,
146
- to_channel : discord .VoiceChannel = None ,
160
+ from_channel : Union [ discord .VoiceChannel , discord . StageChannel ] ,
161
+ to_channel : Union [ discord .VoiceChannel , discord . StageChannel ] = None ,
147
162
):
148
163
"""Move all members from one voice channel to another
149
164
@@ -171,6 +186,7 @@ async def massmove(
171
186
continue
172
187
await ctx .send (_ ("Finished moving users. {} members could not be moved." ).format (fails ))
173
188
189
+ # TODO: Stickers?
174
190
@commands .group ()
175
191
@commands .guild_only ()
176
192
@commands .admin_or_permissions (manage_emojis = True )
@@ -207,8 +223,6 @@ async def emoji_add(self, ctx, name: str, url: str, *roles: discord.Role):
207
223
else None ,
208
224
),
209
225
)
210
- except discord .InvalidArgument :
211
- await ctx .send (chat .error (_ ("This image type is unsupported, or link is incorrect" )))
212
226
except discord .HTTPException as e :
213
227
await ctx .send (chat .error (_ ("An error occurred on adding an emoji: {}" ).format (e )))
214
228
else :
@@ -251,13 +265,6 @@ async def emote_steal(
251
265
),
252
266
)
253
267
await ctx .tick ()
254
- except discord .InvalidArgument :
255
- await ctx .send (
256
- _ (
257
- "This image type is not supported anymore or Discord returned incorrect data. Try again later."
258
- )
259
- )
260
- return
261
268
except discord .HTTPException as e :
262
269
await ctx .send (chat .error (_ ("An error occurred on adding an emoji: {}" ).format (e )))
263
270
@@ -301,6 +308,7 @@ async def emoji_remove(self, ctx: commands.Context, *, emoji: discord.Emoji):
301
308
await emoji .delete (reason = get_audit_reason (ctx .author ))
302
309
await ctx .tick ()
303
310
311
+ # TODO: Threads?
304
312
@commands .group ()
305
313
@commands .guild_only ()
306
314
@commands .admin_or_permissions (manage_channels = True )
@@ -360,8 +368,8 @@ async def channel_create_voice(
360
368
Use double quotes if category has spaces
361
369
362
370
Examples:
363
- `[p]channel add voice "The Zoo" Awesome Channel` will create under the "The Zoo" category.
364
- `[p]channel add voice Awesome Channel` will create under no category, at the top.
371
+ `[p]channel add voice "The Zoo" Awesome Channel` will create voice channel under the "The Zoo" category.
372
+ `[p]channel add voice Awesome Channel` will create stage channel under no category, at the top.
365
373
"""
366
374
if category :
367
375
self .check_channel_permission (ctx , category )
@@ -376,11 +384,41 @@ async def channel_create_voice(
376
384
else :
377
385
await ctx .tick ()
378
386
387
+ @channel_create .command (name = "stage" )
388
+ async def channel_create_stage (
389
+ self ,
390
+ ctx : commands .Context ,
391
+ category : Optional [discord .CategoryChannel ] = None ,
392
+ * ,
393
+ name : str ,
394
+ ):
395
+ """Create a stage channel
396
+
397
+ You can create the channel under a category if passed, else it is created under no category
398
+ Use double quotes if category has spaces
399
+
400
+ Examples:
401
+ `[p]channel add voice "The Zoo" Awesome Channel` will create voice channel under the "The Zoo" category.
402
+ `[p]channel add voice Awesome Channel` will create stage channel under no category, at the top.
403
+ """
404
+ if category :
405
+ self .check_channel_permission (ctx , category )
406
+ try :
407
+ await ctx .guild .create_stage_channel (
408
+ name , category = category , reason = get_audit_reason (ctx .author )
409
+ )
410
+ except discord .Forbidden :
411
+ await ctx .send (chat .error (_ ("I can't create channel in this category" )))
412
+ except discord .HTTPException as e :
413
+ await ctx .send (chat .error (_ ("I am unable to create a channel: {}" ).format (e )))
414
+ else :
415
+ await ctx .tick ()
416
+
379
417
@channel .command (name = "rename" )
380
418
async def channel_rename (
381
419
self ,
382
420
ctx : commands .Context ,
383
- channel : Union [discord .TextChannel , discord .VoiceChannel ],
421
+ channel : Union [discord .TextChannel , discord .VoiceChannel , discord . StageChannel ],
384
422
* ,
385
423
name : str ,
386
424
):
@@ -403,7 +441,10 @@ async def channel_rename(
403
441
404
442
@channel .command (name = "delete" , aliases = ["remove" ])
405
443
async def channel_delete (
406
- self , ctx : commands .Context , * , channel : Union [discord .TextChannel , discord .VoiceChannel ]
444
+ self ,
445
+ ctx : commands .Context ,
446
+ * ,
447
+ channel : Union [discord .TextChannel , discord .VoiceChannel , discord .StageChannel ],
407
448
):
408
449
"""Remove a channel from server
409
450
@@ -421,10 +462,8 @@ async def channel_delete(
421
462
).format (channel = channel .mention )
422
463
)
423
464
)
424
- try :
465
+ with contextlib . suppress ( AsyncTimeoutError ) :
425
466
await self .bot .wait_for ("message" , check = pred , timeout = 30 )
426
- except AsyncTimeoutError :
427
- pass
428
467
if ctx .assume_yes or pred .result :
429
468
try :
430
469
await channel .delete (reason = get_audit_reason (ctx .author ))
0 commit comments