Skip to content
This repository was archived by the owner on Oct 2, 2023. It is now read-only.

Commit f7e9b42

Browse files
authored
Merge pull request #202 from PyDrocsid/feature/channel_hopping
Feature/channel hopping
2 parents 1025848 + ec451a1 commit f7e9b42

File tree

4 files changed

+149
-49
lines changed

4 files changed

+149
-49
lines changed

moderation/spam_detection/cog.py

Lines changed: 106 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,53 @@
11
import time
2+
from datetime import timedelta
23

3-
from discord import Embed, Member, VoiceState
4+
from discord import Embed, Forbidden, HTTPException, Member, VoiceState
45
from discord.ext import commands
56
from discord.ext.commands import Context, UserInputError, guild_only
67

78
from PyDrocsid.cog import Cog
8-
from PyDrocsid.command import reply
9-
from PyDrocsid.config import Contributor
9+
from PyDrocsid.command import CommandError, docs, reply
1010
from PyDrocsid.redis import redis
1111
from PyDrocsid.translations import t
1212

1313
from .colors import Colors
1414
from .permissions import SpamDetectionPermission
1515
from .settings import SpamDetectionSettings
16+
from ...contributor import Contributor
1617
from ...pubsub import send_alert, send_to_changelog
1718

1819

1920
tg = t.g
2021
t = 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+
2332
class 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)

moderation/spam_detection/permissions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ def description(self) -> str:
1111

1212
read = auto()
1313
write = auto()
14+
bypass = auto()

moderation/spam_detection/settings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33

44
class SpamDetectionSettings(Settings):
5-
max_hops = 0
5+
max_hops_alert = 0
6+
max_hops_warning = 0
7+
max_hops_temp_mute = 0
8+
temp_mute_duration = 10
Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,50 @@
1+
commands:
2+
spam_detection: view and change spam detection settings
3+
channel_hopping: edit spam detection settings
4+
alert: set the number of allowed hops before a alert-channel notification is used (0 for disabling)
5+
warning: set the number of allowed hops before a dm notification is used (0 for disabling)
6+
temp_mute: edit the channel hopping mute settings
7+
temp_mute_hops: set the number of allowed hops before a alert-channel notification is used (0 for disabling)
8+
temp_mute_duration: set the number of seconds a user is muted (0 for disabling)
9+
110
permissions:
211
read: read spam detection configuration
312
write: write spam detection configuration
13+
bypass: bypass the channel hopping check
414

515
spam_detection: Spam Detection
16+
617
max_x_hops:
7-
one: "Max.: {cnt} channel hop per minute"
8-
many: "Max.: {cnt} channel hops per minute"
18+
one: "Max.: `{cnt}` channel hop per minute"
19+
many: "Max.: `{cnt}` channel hops per minute"
20+
seconds_muted:
21+
one: "{cnt} second"
22+
many: "{cnt} seconds"
23+
924
member_id: Member ID
10-
channel_hopping: Channel Hopping Detection
25+
26+
channel_hopping: Channel hopping
27+
channel_hopping_warning: Warning via DM
28+
channel_hopping_alert: Alert message
29+
channel_hopping_mute: Temporare mute
30+
mute_duration: Mute duration
31+
reason: was timouted because of channelhopping
32+
33+
cant_mute: "Cannot mute {} ({}) for channel hopping because of missing permissions"
34+
1135
current_channel: Current Channel
12-
hop_amount_set: "The **maximum amount** of **channel hops per minute** has been **set to {}**."
13-
hop_detection_disabled: "**Channel Hopping Detection** has been **disabled**."
36+
1437
new_amount: New amount
15-
max_hops: Maximum channel hops per minute
38+
hop_amount_set: "The **maximum amount** of **channel hops per minute** has been **set to {}** for **{}**."
39+
mute_time_set: "The **muted time** for channel hooping has been **set to {} seconds**."
40+
hop_detection_disabled: "**Channel Hopping Detection** has been **disabled** for **{}**."
41+
change_types:
42+
warnings: warnings
43+
alerts: alerts
44+
mutes: mutes
45+
1646
hops_in_last_minute:
1747
one: "{cnt} hop in the last minute"
1848
many: "{cnt} hops in the last minute"
49+
50+
channel_hopping_warning_sent: "Please stop channel hopping!"

0 commit comments

Comments
 (0)