2
2
import textwrap
3
3
import typing as t
4
4
5
- import arrow
6
5
import discord
7
6
from discord .ext import commands
8
7
from discord .ext .commands import Context
26
25
27
26
log = get_logger (__name__ )
28
27
28
+ NO_DURATION_INFRACTIONS = ("note" , "warning" , "kick" )
29
+
30
+ FAILED_DM_SYMBOL = constants .Emojis .failmail
31
+ HIDDEN_INFRACTION_SYMBOL = "🕵️"
32
+ EDITED_DURATION_SYMBOL = "✏️"
33
+
34
+ SYMBOLS_GUIDE = f"""
35
+ Symbols guide:
36
+ \u2003 { FAILED_DM_SYMBOL } - The infraction DM failed to deliver.
37
+ \u2003 { HIDDEN_INFRACTION_SYMBOL } - The infraction is hidden.
38
+ \u2003 { EDITED_DURATION_SYMBOL } - The duration was edited.
39
+ """
40
+
29
41
30
42
class ModManagement (commands .Cog ):
31
43
"""Management of infractions."""
@@ -35,6 +47,16 @@ class ModManagement(commands.Cog):
35
47
def __init__ (self , bot : Bot ):
36
48
self .bot = bot
37
49
50
+ # Add the symbols guide to the help embeds of the appropriate commands.
51
+ for command in (
52
+ self .infraction_group ,
53
+ self .infraction_search_group ,
54
+ self .search_reason ,
55
+ self .search_user ,
56
+ self .search_by_actor
57
+ ):
58
+ command .help += f"\n { SYMBOLS_GUIDE } "
59
+
38
60
@property
39
61
def mod_log (self ) -> ModLog :
40
62
"""Get currently loaded ModLog cog instance."""
@@ -58,10 +80,10 @@ async def infraction_group(self, ctx: Context, infraction: Infraction = None) ->
58
80
return
59
81
60
82
embed = discord .Embed (
61
- title = f"Infraction # { infraction [ 'id' ] } " ,
83
+ title = f"{ self . format_infraction_title ( infraction ) } " ,
62
84
colour = discord .Colour .orange ()
63
85
)
64
- await self .send_infraction_list (ctx , embed , [infraction ])
86
+ await self .send_infraction_list (ctx , embed , [infraction ], ignore_fields = ( "id" ,) )
65
87
66
88
@infraction_group .command (name = "resend" , aliases = ("send" , "rs" , "dm" ))
67
89
async def infraction_resend (self , ctx : Context , infraction : Infraction ) -> None :
@@ -287,7 +309,8 @@ async def search_user(self, ctx: Context, user: MemberOrUser | discord.Object) -
287
309
title = f"Infractions for { user_str } ({ formatted_infraction_count } total)" ,
288
310
colour = discord .Colour .orange ()
289
311
)
290
- await self .send_infraction_list (ctx , embed , infraction_list )
312
+ prefix = f"{ user .mention } - { user .id } "
313
+ await self .send_infraction_list (ctx , embed , infraction_list , prefix , ("user" ,))
291
314
292
315
@infraction_search_group .command (name = "reason" , aliases = ("match" , "regex" , "re" ))
293
316
async def search_reason (self , ctx : Context , reason : str ) -> None :
@@ -304,10 +327,12 @@ async def search_reason(self, ctx: Context, reason: str) -> None:
304
327
305
328
formatted_infraction_count = self .format_infraction_count (len (infraction_list ))
306
329
embed = discord .Embed (
307
- title = f"Infractions matching ` { reason } ` ({ formatted_infraction_count } total)" ,
330
+ title = f"Infractions with matching context ({ formatted_infraction_count } total)" ,
308
331
colour = discord .Colour .orange ()
309
332
)
310
- await self .send_infraction_list (ctx , embed , infraction_list )
333
+ if len (reason ) > 500 :
334
+ reason = reason [:500 ] + "..."
335
+ await self .send_infraction_list (ctx , embed , infraction_list , reason )
311
336
312
337
# endregion
313
338
# region: Search for infractions by given actor
@@ -348,7 +373,8 @@ async def search_by_actor(
348
373
colour = discord .Colour .orange ()
349
374
)
350
375
351
- await self .send_infraction_list (ctx , embed , infraction_list )
376
+ prefix = f"{ actor .mention } - { actor .id } "
377
+ await self .send_infraction_list (ctx , embed , infraction_list , prefix , ("actor" ,))
352
378
353
379
# endregion
354
380
# region: Utility functions
@@ -369,94 +395,93 @@ async def send_infraction_list(
369
395
self ,
370
396
ctx : Context ,
371
397
embed : discord .Embed ,
372
- infractions : t .Iterable [dict [str , t .Any ]]
398
+ infractions : t .Iterable [dict [str , t .Any ]],
399
+ prefix : str = "" ,
400
+ ignore_fields : tuple [str , ...] = ()
373
401
) -> None :
374
402
"""Send a paginated embed of infractions for the specified user."""
375
403
if not infractions :
376
404
await ctx .send (":warning: No infractions could be found for that query." )
377
405
return
378
406
379
- lines = tuple (
380
- self .infraction_to_string (infraction )
381
- for infraction in infractions
382
- )
407
+ lines = [self .infraction_to_string (infraction , ignore_fields ) for infraction in infractions ]
383
408
384
409
await LinePaginator .paginate (
385
410
lines ,
386
411
ctx = ctx ,
387
412
embed = embed ,
413
+ prefix = f"{ prefix } \n " ,
388
414
empty = True ,
389
415
max_lines = 3 ,
390
416
max_size = 1000
391
417
)
392
418
393
- def infraction_to_string (self , infraction : dict [str , t .Any ]) -> str :
419
+ def infraction_to_string (self , infraction : dict [str , t .Any ], ignore_fields : tuple [ str , ...] ) -> str :
394
420
"""Convert the infraction object to a string representation."""
395
- active = infraction ["active" ]
396
- user = infraction ["user" ]
397
421
expires_at = infraction ["expires_at" ]
398
422
inserted_at = infraction ["inserted_at" ]
399
423
last_applied = infraction ["last_applied" ]
400
- created = time .discord_timestamp (inserted_at )
401
- applied = time .discord_timestamp (last_applied )
402
- duration_edited = arrow .get (last_applied ) > arrow .get (inserted_at )
403
- dm_sent = infraction ["dm_sent" ]
404
424
jump_url = infraction ["jump_url" ]
405
425
406
- # Format the user string.
407
- if user_obj := self .bot .get_user (user ["id" ]):
408
- # The user is in the cache.
409
- user_str = messages .format_user (user_obj )
410
- else :
411
- # Use the user data retrieved from the DB.
412
- name = escape_markdown (user ["name" ])
413
- user_str = f"<@{ user ['id' ]} > ({ name } #{ user ['discriminator' ]:04} )"
426
+ title = ""
427
+ if "id" not in ignore_fields :
428
+ title = f"**{ self .format_infraction_title (infraction )} **"
414
429
415
- if active :
416
- remaining = time .until_expiration (expires_at )
417
- else :
418
- remaining = "Inactive"
430
+ symbols = []
431
+ if not infraction ["hidden" ] and infraction ["dm_sent" ] is False :
432
+ symbols .append (FAILED_DM_SYMBOL )
433
+ if infraction ["hidden" ]:
434
+ symbols .append (HIDDEN_INFRACTION_SYMBOL )
435
+ if inserted_at != infraction ["last_applied" ]:
436
+ symbols .append (EDITED_DURATION_SYMBOL )
437
+ symbols = " " .join (symbols )
419
438
420
- if expires_at is None :
421
- duration = "*Permanent*"
422
- else :
423
- duration = time .humanize_delta (last_applied , expires_at )
439
+ user_str = ""
440
+ if "user" not in ignore_fields :
441
+ user_str = "For " + self .format_user_from_record (infraction ["user" ])
424
442
425
- # Notice if infraction expiry was edited.
426
- if duration_edited :
427
- duration + = f" (edited { applied } ) "
443
+ actor_str = ""
444
+ if "actor" not in ignore_fields :
445
+ actor_str = f"By <@ { infraction [ 'actor' ][ 'id' ] } > "
428
446
429
- # Format `dm_sent`
430
- if dm_sent is None :
431
- dm_sent_text = "N/A"
432
- else :
433
- dm_sent_text = "Yes" if dm_sent else "No"
447
+ issued = "Issued " + time .discord_timestamp (inserted_at )
448
+
449
+ duration = ""
450
+ if infraction ["type" ] not in NO_DURATION_INFRACTIONS :
451
+ if expires_at is None :
452
+ duration = "*Permanent*"
453
+ else :
454
+ duration = time .humanize_delta (last_applied , expires_at )
455
+ if infraction ["active" ]:
456
+ duration = f"{ duration } (Expires { time .format_relative (expires_at )} )"
457
+ duration = f"Duration: { duration } "
434
458
435
459
if jump_url is None :
436
460
# Infraction was issued prior to jump urls being stored in the database
437
461
# or infraction was issued in ModMail category.
438
- jump_url = "N/A "
462
+ context = f"**Context**: { infraction [ 'reason' ] or '*None*' } "
439
463
else :
440
- jump_url = f"[Click here.]({ jump_url } )"
441
-
442
- lines = textwrap .dedent (f"""
443
- { "**===============**" if active else "===============" }
444
- Status: { "__**Active**__" if active else "Inactive" }
445
- User: { user_str }
446
- Type: **{ infraction ["type" ]} **
447
- DM Sent: { dm_sent_text }
448
- Shadow: { infraction ["hidden" ]}
449
- Created: { created }
450
- Expires: { remaining }
451
- Duration: { duration }
452
- Actor: <@{ infraction ["actor" ]["id" ]} >
453
- ID: `{ infraction ["id" ]} `
454
- Jump URL: { jump_url }
455
- Reason: { infraction ["reason" ] or "*None*" }
456
- { "**===============**" if active else "===============" }
457
- """ )
458
-
459
- return lines .strip ()
464
+ context = f"**[Context]({ jump_url } )**: { infraction ['reason' ] or '*None*' } "
465
+
466
+ return "\n " .join (part for part in (title , symbols , user_str , actor_str , issued , duration , context ) if part )
467
+
468
+ def format_user_from_record (self , user : dict ) -> str :
469
+ """Create a formatted user string from its DB record."""
470
+ if user_obj := self .bot .get_user (user ["id" ]):
471
+ # The user is in the cache.
472
+ return messages .format_user (user_obj )
473
+
474
+ # Use the user data retrieved from the DB.
475
+ name = escape_markdown (user ["name" ])
476
+ return f"<@{ user ['id' ]} > ({ name } #{ user ['discriminator' ]:04} )"
477
+
478
+ @staticmethod
479
+ def format_infraction_title (infraction : Infraction ) -> str :
480
+ """Format the infraction title."""
481
+ title = infraction ["type" ].replace ("_" , " " ).title ()
482
+ if infraction ["active" ]:
483
+ title = f"__Active__ { title } "
484
+ return f"{ title } #{ infraction ['id' ]} "
460
485
461
486
# endregion
462
487
0 commit comments