11import time
2+ from datetime import timedelta
23
3- from discord import Embed , Member , VoiceState
4+ from discord import Embed , Forbidden , HTTPException , Member , VoiceState
45from discord .ext import commands
56from discord .ext .commands import Context , UserInputError , guild_only
67
78from PyDrocsid .cog import Cog
8- from PyDrocsid .command import reply
9- from PyDrocsid .config import Contributor
9+ from PyDrocsid .command import CommandError , docs , reply
1010from PyDrocsid .redis import redis
1111from PyDrocsid .translations import t
1212
1313from .colors import Colors
1414from .permissions import SpamDetectionPermission
1515from .settings import SpamDetectionSettings
16+ from ...contributor import Contributor
1617from ...pubsub import send_alert , send_to_changelog
1718
1819
1920tg = t .g
2021t = t .spam_detection
2122
2223
24+ async def _send_changes (ctx : Context , amount : int , change_type : str , description ):
25+ description = description (amount , change_type ) if amount > 0 else t .hop_detection_disabled (change_type )
26+ embed = Embed (title = t .channel_hopping , description = description , colour = Colors .SpamDetection )
27+
28+ await reply (ctx , embed = embed )
29+ await send_to_changelog (ctx .guild , description )
30+
31+
2332class SpamDetectionCog (Cog , name = "Spam Detection" ):
24- CONTRIBUTORS = [Contributor .ce_phox , Contributor .Defelo ]
33+ CONTRIBUTORS = [Contributor .ce_phox , Contributor .Defelo , Contributor . NekoFanatic ]
2534
2635 async def on_voice_state_update (self , member : Member , before : VoiceState , after : VoiceState ):
2736 """
2837 Checks for channel-hopping
2938 """
3039
40+ if await SpamDetectionPermission .bypass .check_permissions (member ):
41+ return
42+
3143 if before .channel == after .channel :
3244 return
3345
34- max_hops : int = await SpamDetectionSettings .max_hops .get ()
35- if max_hops <= 0 :
46+ alert : int = await SpamDetectionSettings .max_hops_alert .get ()
47+ warning : int = await SpamDetectionSettings .max_hops_warning .get ()
48+ mute : int = await SpamDetectionSettings .max_hops_temp_mute .get ()
49+ duration : int = await SpamDetectionSettings .temp_mute_duration .get ()
50+ if alert == 0 and warning == 0 and mute == 0 :
3651 return
3752
3853 ts = time .time ()
@@ -41,30 +56,38 @@ async def on_voice_state_update(self, member: Member, before: VoiceState, after:
4156 await redis .expire (key , 60 )
4257 hops : int = await redis .zcount (key , "-inf" , "inf" )
4358
44- if hops <= max_hops :
45- return
46-
47- if await redis .exists (key := f"channel_hops_alert_sent:user={ member .id } " ):
48- return
49-
50- await redis .setex (key , 10 , 1 )
51-
52- embed = Embed (title = t .channel_hopping , color = Colors .SpamDetection , description = t .hops_in_last_minute (cnt = hops ))
53- embed .add_field (name = tg .member , value = member .mention )
54- embed .add_field (name = t .member_id , value = member .id )
55- embed .set_author (name = str (member ), icon_url = member .display_avatar .url )
56- if after .channel :
57- embed .add_field (name = t .current_channel , value = after .channel .name )
58-
59- await send_alert (member .guild , embed )
59+ if hops >= alert > 0 and not await redis .exists (key := f"channel_hops_alert_sent:user={ member .id } " ):
60+ await redis .setex (key , 10 , 1 )
61+ embed = Embed (
62+ title = t .channel_hopping , color = Colors .SpamDetection , description = t .hops_in_last_minute (cnt = hops )
63+ )
64+ embed .add_field (name = tg .member , value = member .mention )
65+ embed .add_field (name = t .member_id , value = str (member .id ))
66+ embed .set_author (name = str (member ), icon_url = member .display_avatar .url )
67+ if after .channel :
68+ embed .add_field (name = t .current_channel , value = after .channel .name )
69+ await send_alert (member .guild , embed )
70+
71+ if hops >= warning > 0 and not await redis .exists (key := f"channel_hops_warning_sent:user={ member .id } " ):
72+ await redis .setex (key , 10 , 1 )
73+ embed = Embed (title = t .channel_hopping_warning_sent , color = Colors .SpamDetection )
74+ try :
75+ await member .send (embed = embed )
76+ except (HTTPException , Forbidden ):
77+ pass
78+
79+ if hops >= mute > 0 and not await redis .exists (key := f"channel_hops_mute:user={ member .id } " ):
80+ try :
81+ await member .timeout_for (duration = timedelta (seconds = duration ), reason = t .reason )
82+ except Forbidden :
83+ await send_alert (member .guild , t .cant_mute (member .mention , member .id ))
84+ await redis .setex (key , 10 , 1 )
6085
6186 @commands .group (aliases = ["spam" , "sd" ])
6287 @SpamDetectionPermission .read .check
6388 @guild_only ()
89+ @docs (t .commands .spam_detection )
6490 async def spam_detection (self , ctx : Context ):
65- """
66- view and change spam detection settings
67- """
6891
6992 if ctx .subcommand_passed is not None :
7093 if ctx .invoked_subcommand is None :
@@ -73,26 +96,67 @@ async def spam_detection(self, ctx: Context):
7396
7497 embed = Embed (title = t .spam_detection , color = Colors .SpamDetection )
7598
76- if (max_hops := await SpamDetectionSettings .max_hops .get ()) <= 0 :
77- embed .add_field (name = t .channel_hopping , value = tg .disabled )
99+ if (dm_warning := await SpamDetectionSettings .max_hops_warning .get ()) <= 0 :
100+ embed .add_field (name = t .channel_hopping_warning , value = tg .disabled , inline = False )
101+ else :
102+ embed .add_field (name = t .channel_hopping_warning , value = t .max_x_hops (cnt = dm_warning ), inline = False )
103+
104+ if (alert := await SpamDetectionSettings .max_hops_alert .get ()) <= 0 :
105+ embed .add_field (name = t .channel_hopping_alert , value = tg .disabled , inline = False )
78106 else :
79- embed .add_field (name = t .channel_hopping , value = t .max_x_hops (cnt = max_hops ))
107+ embed .add_field (name = t .channel_hopping_alert , value = t .max_x_hops (cnt = alert ), inline = False )
108+
109+ if (mute_hops := await SpamDetectionSettings .max_hops_temp_mute .get ()) <= 0 :
110+ embed .add_field (name = t .channel_hopping_mute , value = tg .disabled , inline = False )
111+ else :
112+ embed .add_field (name = t .channel_hopping_mute , value = t .max_x_hops (cnt = mute_hops ), inline = False )
113+
114+ mute_duration = await SpamDetectionSettings .temp_mute_duration .get ()
115+ embed .add_field (name = t .mute_duration , value = t .seconds_muted (cnt = mute_duration ), inline = False )
80116
81117 await reply (ctx , embed = embed )
82118
83- @spam_detection .command (name = "hops " , aliases = ["h" ])
119+ @spam_detection .group (name = "channel_hopping " , aliases = ["ch" , "h" ])
84120 @SpamDetectionPermission .write .check
85- async def spam_detection_hops (self , ctx : Context , amount : int ):
86- """
87- Changes the number of maximum channel hops per minute allowed before an alert is issued
88- set this to 0 to disable channel hopping alerts
89- """
121+ @docs (t .commands .channel_hopping )
122+ async def channel_hopping (self , ctx : Context ):
90123
91- await SpamDetectionSettings .max_hops .set (amount )
92- embed = Embed (
93- title = t .channel_hopping ,
94- description = t .hop_amount_set (amount ) if amount > 0 else t .hop_detection_disabled ,
95- colour = Colors .SpamDetection ,
96- )
97- await reply (ctx , embed = embed )
98- await send_to_changelog (ctx .guild , t .hop_amount_set (amount ) if amount > 0 else t .hop_detection_disabled )
124+ if not ctx .subcommand_passed or not ctx .invoked_subcommand :
125+ raise UserInputError
126+
127+ @channel_hopping .command (name = "alert" , aliases = ["a" ])
128+ @docs (t .commands .alert )
129+ async def alert (self , ctx : Context , amount : int ):
130+
131+ await SpamDetectionSettings .max_hops_alert .set (max (amount , 0 ))
132+ await _send_changes (ctx , amount , t .change_types .alerts , t .hop_amount_set )
133+
134+ @channel_hopping .command (name = "warning" , aliases = ["warn" , "w" , "dm" ])
135+ @docs (t .commands .warning )
136+ async def warning (self , ctx : Context , amount : int ):
137+
138+ await SpamDetectionSettings .max_hops_warning .set (max (amount , 0 ))
139+ await _send_changes (ctx , amount , t .change_types .warnings , t .hop_amount_set )
140+
141+ @channel_hopping .group (name = "mute" , aliases = ["m" ])
142+ @docs (t .commands .temp_mute )
143+ async def mute (self , ctx : Context ):
144+
145+ if not ctx .subcommand_passed or not ctx .invoked_subcommand :
146+ raise UserInputError
147+
148+ @mute .command (name = "hops" , aliases = ["h" ])
149+ @docs (t .commands .temp_mute_hops )
150+ async def hops (self , ctx : Context , amount : int ):
151+
152+ await SpamDetectionSettings .max_hops_temp_mute .set (max (amount , 0 ))
153+ await _send_changes (ctx , amount , t .change_types .mutes , t .hop_amount_set )
154+
155+ @mute .command (name = "duration" , aliases = ["d" ])
156+ @docs (t .commands .temp_mute_duration )
157+ async def duration (self , ctx : Context , seconds : int ):
158+ if seconds not in range (1 , 28 * 24 * 60 * 60 ):
159+ raise CommandError (tg .invalid_duration )
160+
161+ await SpamDetectionSettings .temp_mute_duration .set (seconds )
162+ await _send_changes (ctx , seconds , t .change_types .mutes , t .mute_time_set )
0 commit comments