2525import asyncio
2626import base64
2727import binascii
28+ import datetime
2829import re
29- from typing import Any
30+ from textwrap import shorten
31+ from typing import TYPE_CHECKING , Any , Self , TypeAlias
3032
3133import discord
3234import yarl
3335from discord .ext import commands
3436
3537import core
38+ from constants import Channels
39+ from core .context import Interaction
40+ from core .utils import random_pastel_colour
3641
3742
43+ if TYPE_CHECKING :
44+ from types_ .papi import ModLogPayload , PythonistaAPIWebsocketPayload
45+
46+ ModLogType : TypeAlias = PythonistaAPIWebsocketPayload [ModLogPayload ]
47+
3848TOKEN_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+ }
3956
4057
4158def validate_token (token : str ) -> bool :
@@ -45,17 +62,47 @@ def validate_token(token: str) -> bool:
4562 user_id = int (base64 .b64decode (user_id + "==" , validate = True ))
4663 except (ValueError , binascii .Error ):
4764 return False
48- else :
49- return True
65+ return True
5066
5167
5268class GithubError (commands .CommandError ):
5369 pass
5470
5571
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+
56102class Moderation (commands .Cog ):
57103 def __init__ (self , bot : core .Bot , / ) -> None :
58104 self .bot = bot
105+ self .dpy_mod_cache : dict [int , discord .User | discord .Member ] = {}
59106 self ._req_lock = asyncio .Lock ()
60107
61108 async def github_request (
@@ -73,7 +120,7 @@ async def github_request(
73120
74121 hdrs = {
75122 "Accept" : "application/vnd.github.inertia-preview+json" ,
76- "User-Agent" : "RoboDanny DPYExclusive Cog" ,
123+ "User-Agent" : "PythonistaBot Moderation Cog" ,
77124 "Authorization" : f"token { api_key } " ,
78125 }
79126
@@ -148,6 +195,56 @@ async def find_discord_tokens(self, message: discord.Message) -> None:
148195 )
149196 await message .reply (msg )
150197
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+
151248
152249async def setup (bot : core .Bot ) -> None :
153250 await bot .add_cog (Moderation (bot ))
0 commit comments