25
25
import asyncio
26
26
import base64
27
27
import binascii
28
+ import datetime
28
29
import re
29
- from typing import Any
30
+ from textwrap import shorten
31
+ from typing import TYPE_CHECKING , Any , Self , TypeAlias
30
32
31
33
import discord
32
34
import yarl
33
35
from discord .ext import commands
34
36
35
37
import core
38
+ from constants import Channels
39
+ from core .context import Interaction
40
+ from core .utils import random_pastel_colour
36
41
37
42
43
+ if TYPE_CHECKING :
44
+ from types_ .papi import ModLogPayload , PythonistaAPIWebsocketPayload
45
+
46
+ ModLogType : TypeAlias = PythonistaAPIWebsocketPayload [ModLogPayload ]
47
+
38
48
TOKEN_RE = re .compile (r"[a-zA-Z0-9_-]{23,28}\.[a-zA-Z0-9_-]{6,7}\.[a-zA-Z0-9_-]{27}" )
49
+ PROSE_LOOKUP = {
50
+ 1 : "banned" ,
51
+ 2 : "kicked" ,
52
+ 3 : "muted" ,
53
+ 4 : "unbanned" ,
54
+ 5 : "helpblocked" ,
55
+ }
39
56
40
57
41
58
def validate_token (token : str ) -> bool :
@@ -45,17 +62,47 @@ def validate_token(token: str) -> bool:
45
62
user_id = int (base64 .b64decode (user_id + "==" , validate = True ))
46
63
except (ValueError , binascii .Error ):
47
64
return False
48
- else :
49
- return True
65
+ return True
50
66
51
67
52
68
class GithubError (commands .CommandError ):
53
69
pass
54
70
55
71
72
+ class ModerationRespostView (discord .ui .View ):
73
+ message : discord .Message | discord .WebhookMessage
74
+
75
+ def __init__ (self , * , timeout : float | None = 180 , target_id : int , target_reason : str ) -> None :
76
+ super ().__init__ (timeout = timeout )
77
+ self .target : discord .Object = discord .Object (id = target_id , type = discord .Member )
78
+ self .target_reason : str = target_reason
79
+
80
+ def _disable_all_buttons (self ) -> None :
81
+ for item in self .children :
82
+ if isinstance (item , (discord .ui .Button , discord .ui .Select )):
83
+ item .disabled = True
84
+
85
+ async def on_timeout (self ) -> None :
86
+ self ._disable_all_buttons ()
87
+ await self .message .edit (view = self )
88
+
89
+ @discord .ui .button (label = "Ban" , emoji = "\U0001f528 " )
90
+ async def ban_button (self , interaction : Interaction , button : discord .ui .Button [Self ]) -> None :
91
+ assert interaction .guild
92
+ await interaction .response .defer (ephemeral = False )
93
+
94
+ reason = f"Banned due to grievances in discord.py: { self .target_reason !r} "
95
+ await interaction .guild .ban (
96
+ self .target ,
97
+ reason = shorten (reason , width = 128 , placeholder = "..." ),
98
+ )
99
+ await interaction .followup .send ("Banned." )
100
+
101
+
56
102
class Moderation (commands .Cog ):
57
103
def __init__ (self , bot : core .Bot , / ) -> None :
58
104
self .bot = bot
105
+ self .dpy_mod_cache : dict [int , discord .User | discord .Member ] = {}
59
106
self ._req_lock = asyncio .Lock ()
60
107
61
108
async def github_request (
@@ -73,7 +120,7 @@ async def github_request(
73
120
74
121
hdrs = {
75
122
"Accept" : "application/vnd.github.inertia-preview+json" ,
76
- "User-Agent" : "RoboDanny DPYExclusive Cog" ,
123
+ "User-Agent" : "PythonistaBot Moderation Cog" ,
77
124
"Authorization" : f"token { api_key } " ,
78
125
}
79
126
@@ -148,6 +195,56 @@ async def find_discord_tokens(self, message: discord.Message) -> None:
148
195
)
149
196
await message .reply (msg )
150
197
198
+ @commands .Cog .listener ()
199
+ async def on_papi_dpy_modlog (self , payload : ModLogType , / ) -> None :
200
+ moderation_payload = payload ["payload" ]
201
+ moderation_event = core .DiscordPyModerationEvent (moderation_payload ["moderation_event_type" ])
202
+
203
+ embed = discord .Embed (
204
+ title = f"Discord.py Moderation Event: { moderation_event .name .title ()} " ,
205
+ colour = random_pastel_colour (),
206
+ )
207
+
208
+ target_id = moderation_payload ["target_id" ]
209
+ target = await self .bot .get_or_fetch_user (target_id )
210
+
211
+ moderation_reason = moderation_payload ["reason" ]
212
+
213
+ moderator_id = moderation_payload ["author_id" ]
214
+ moderator = self .dpy_mod_cache .get (moderator_id ) or await self .bot .get_or_fetch_user (
215
+ moderator_id , cache = self .dpy_mod_cache
216
+ )
217
+
218
+ if moderator :
219
+ self .dpy_mod_cache [moderator .id ] = moderator
220
+ moderator_format = f"{ moderator .name } { PROSE_LOOKUP [moderation_event .value ]} "
221
+ embed .set_author (name = moderator .name , icon_url = moderator .display_avatar .url )
222
+ else :
223
+ moderator_format = f"Unknown Moderator with ID: { moderator_id } { PROSE_LOOKUP [moderation_event .value ]} "
224
+ embed .set_author (name = f"Unknown Moderator." )
225
+
226
+ if target :
227
+ target_format = target .name
228
+ embed .set_footer (text = f"{ target .name } | { target_id } " , icon_url = target .display_avatar .url )
229
+ else :
230
+ target_format = f"An unknown user with ID { target_id } "
231
+ embed .set_footer (text = f"Not Found | { target_id } " )
232
+ embed .add_field (name = "Reason" , value = moderation_reason or "No reason given." )
233
+
234
+ embed .description = moderator_format + target_format
235
+
236
+ when = datetime .datetime .fromisoformat (moderation_payload ["event_time" ])
237
+ embed .timestamp = when
238
+
239
+ guild = self .bot .get_guild (490948346773635102 )
240
+ assert guild
241
+
242
+ channel = guild .get_channel (Channels .DPY_MOD_LOGS )
243
+ assert isinstance (channel , discord .TextChannel ) # This is static
244
+
245
+ view = ModerationRespostView (timeout = 900 , target_id = target_id , target_reason = moderation_reason )
246
+ view .message = await channel .send (embed = embed , view = view )
247
+
151
248
152
249
async def setup (bot : core .Bot ) -> None :
153
250
await bot .add_cog (Moderation (bot ))
0 commit comments