Skip to content

Commit 92d7bb8

Browse files
committed
Add moderation commands
- /restrict - /unrestrict - /rename - /addgroup - /removegroup
1 parent 4271ce3 commit 92d7bb8

File tree

4 files changed

+309
-0
lines changed

4 files changed

+309
-0
lines changed

app/bot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async def load_cogs(self):
2424
await self.load_extension("app.extensions.search")
2525
await self.load_extension("app.extensions.rankings")
2626
await self.load_extension("app.extensions.pprecord")
27+
await self.load_extension("app.extensions.moderation")
2728
await self.tree.sync()
2829

2930
def run():

app/cog.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,31 @@ async def resolve_user(self, discord_id: int) -> DBUser | None:
4545
users.fetch_by_discord_id,
4646
discord_id
4747
)
48+
49+
async def resolve_user_by_id(self, user_id: int) -> DBUser | None:
50+
return await self.run_async(
51+
users.fetch_by_id,
52+
user_id
53+
)
4854

4955
async def resolve_user_by_name(self, username: str) -> DBUser | None:
5056
return await self.run_async(
5157
users.fetch_by_name_extended,
5258
username
5359
)
5460

61+
async def resolve_user_by_name_case_insensitive(self, username: str) -> DBUser | None:
62+
return await self.run_async(
63+
users.fetch_by_name_case_insensitive,
64+
username
65+
)
66+
67+
async def resolve_user_by_safe_name(self, username: str) -> DBUser | None:
68+
return await self.run_async(
69+
users.fetch_by_safe_name,
70+
username
71+
)
72+
5573
async def update_user(self, user_id: int, updates: dict) -> int:
5674
return await self.run_async(
5775
users.update,

app/extensions/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from .search import Search
1111
from .rankings import Rankings
1212
from .pprecord import PPRecord
13+
from .moderation import Moderation

app/extensions/moderation.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
2+
from app.common.database.objects import DBGroupEntry, DBUser, DBName, DBGroup
3+
from app.common.database.repositories import groups, names
4+
from app.common.constants import regexes
5+
from app.extensions.types import *
6+
from app.cog import BaseCog
7+
8+
from discord import app_commands, Interaction
9+
from discord.ext.commands import Bot
10+
from typing import List
11+
12+
class Moderation(BaseCog):
13+
@app_commands.command(name="restrict", description="Restrict a user")
14+
@app_commands.default_permissions(ban_members=True)
15+
async def restrict_user(
16+
self,
17+
interaction: Interaction,
18+
identifier: str,
19+
reason: str = "No reason."
20+
) -> None:
21+
if not (user := await self.resolve_user_from_identifier(identifier)):
22+
return await interaction.response.send_message(
23+
f"I could not find that user: `{identifier}`.",
24+
ephemeral=True
25+
)
26+
27+
if user.restricted:
28+
return await interaction.response.send_message(
29+
f"User `{user.name}` is already restricted.",
30+
ephemeral=True
31+
)
32+
33+
# Let bancho handle the restriction
34+
self.events.submit(
35+
'restrict',
36+
user_id=user.id,
37+
reason=reason
38+
)
39+
40+
await interaction.response.send_message(
41+
f"User `{user.name}` has been restricted."
42+
)
43+
44+
@app_commands.command(name="unrestrict", description="Unrestrict a user")
45+
@app_commands.default_permissions(ban_members=True)
46+
async def unrestrict_user(
47+
self,
48+
interaction: Interaction,
49+
identifier: str,
50+
restore_scores: bool = True
51+
) -> None:
52+
if not (user := await self.resolve_user_from_identifier(identifier)):
53+
return await interaction.response.send_message(
54+
f"I could not find that user: `{identifier}`.",
55+
ephemeral=True
56+
)
57+
58+
if not user.restricted:
59+
return await interaction.response.send_message(
60+
f"User `{user.name}` is not restricted.",
61+
ephemeral=True
62+
)
63+
64+
# Let bancho handle the unrestriction
65+
self.events.submit(
66+
'unrestrict',
67+
user_id=user.id,
68+
restore_scores=restore_scores
69+
)
70+
71+
await interaction.response.send_message(
72+
f"User `{user.name}` has been unrestricted."
73+
)
74+
75+
@app_commands.command(name="rename", description="Rename a user")
76+
@app_commands.default_permissions(moderate_members=True)
77+
async def rename_user(
78+
self,
79+
interaction: Interaction,
80+
identifier: str,
81+
new_name: str
82+
) -> None:
83+
new_name = new_name.strip()
84+
safe_name = new_name.lower().replace(" ", "_")
85+
86+
if len(new_name) < 3:
87+
return await interaction.response.send_message(
88+
"Usernames must be at least 3 characters long.",
89+
ephemeral=True
90+
)
91+
92+
if len(new_name) > 15:
93+
return await interaction.response.send_message(
94+
"Usernames cannot be longer than 15 characters.",
95+
ephemeral=True
96+
)
97+
98+
if new_name.lower().endswith("_old"):
99+
return await interaction.response.send_message(
100+
"Usernames cannot end with `_old`.",
101+
ephemeral=True
102+
)
103+
104+
if new_name.lower().startswith("deleteduser"):
105+
return await interaction.response.send_message(
106+
"This username is not allowed.",
107+
ephemeral=True
108+
)
109+
110+
name_match = regexes.USERNAME.match(new_name)
111+
112+
if not name_match:
113+
return await interaction.response.send_message(
114+
"This username contains invalid characters.",
115+
ephemeral=True
116+
)
117+
118+
if not (user := await self.resolve_user_from_identifier(identifier)):
119+
return await interaction.response.send_message(
120+
f"I could not find that user: `{identifier}`.",
121+
ephemeral=True
122+
)
123+
124+
if await self.resolve_user_by_safe_name(safe_name):
125+
return await interaction.response.send_message(
126+
"This username is already taken.",
127+
ephemeral=True
128+
)
129+
130+
if reserved_name := await self.fetch_reserved_name(new_name):
131+
if reserved_name.user_id != user.id:
132+
return await interaction.response.send_message(
133+
"This username is reserved.",
134+
ephemeral=True
135+
)
136+
137+
past_names = await self.fetch_name_history(user.id)
138+
max_name_changes = 4
139+
140+
if len(past_names) >= max_name_changes:
141+
# User has exceeded the maximum amount of name changes
142+
# We want to now find the oldest entry & un-reserve it, so
143+
# that other people can use it again.
144+
oldest_entry = sorted(past_names, key=lambda name: name.id)[0]
145+
146+
await self.update_name_history_entry(
147+
oldest_entry.id,
148+
{"reserved": False}
149+
)
150+
151+
await self.create_name_history_entry(user.id, user.name)
152+
await self.update_user(user.id, {"name": new_name, "safe_name": safe_name})
153+
154+
await interaction.response.send_message(
155+
(f"User `{user.name}` has been renamed to `{new_name}`.") +
156+
(" Their old name has been un-reserved." if len(past_names) >= max_name_changes else "")
157+
)
158+
159+
@app_commands.command(name="addgroup", description="Add a user to a group")
160+
@app_commands.default_permissions(administrator=True)
161+
async def add_to_group(
162+
self,
163+
interaction: Interaction,
164+
identifier: str,
165+
group: str
166+
) -> None:
167+
if not (user := await self.resolve_user_from_identifier(identifier)):
168+
return await interaction.response.send_message(
169+
f"I could not find that user: `{identifier}`.",
170+
ephemeral=True
171+
)
172+
173+
group_list = await self.fetch_groups()
174+
group_map = {g.name.lower(): g for g in group_list}
175+
group_map.update({g.short_name.lower(): g for g in group_list})
176+
177+
if group.lower() not in group_map:
178+
return await interaction.response.send_message(
179+
f"I could not find that group: `{group}`.",
180+
ephemeral=True
181+
)
182+
183+
group_object = group_map[group.lower()]
184+
185+
try:
186+
await self.create_group_entry(user.id, group_object.id)
187+
except Exception as e:
188+
return await interaction.response.send_message(
189+
f"User `{user.name}` is already in group `{group_object.name}`.",
190+
ephemeral=True
191+
)
192+
193+
await interaction.response.send_message(
194+
f"User `{user.name}` has been added to group `{group_object.name}`."
195+
)
196+
197+
@app_commands.command(name="removegroup", description="Remove a user from a group")
198+
@app_commands.default_permissions(administrator=True)
199+
async def remove_from_group(
200+
self,
201+
interaction: Interaction,
202+
identifier: str,
203+
group: str
204+
) -> None:
205+
if not (user := await self.resolve_user_from_identifier(identifier)):
206+
return await interaction.response.send_message(
207+
f"I could not find that user: `{identifier}`.",
208+
ephemeral=True
209+
)
210+
211+
group_list = await self.fetch_groups()
212+
group_map = {g.name.lower(): g for g in group_list}
213+
group_map.update({g.short_name.lower(): g for g in group_list})
214+
215+
if group.lower() not in group_map:
216+
return await interaction.response.send_message(
217+
f"I could not find that group: `{group}`.",
218+
ephemeral=True
219+
)
220+
221+
group_object = group_map[group.lower()]
222+
success = await self.delete_group_entry(user.id, group_object.id)
223+
224+
if not success:
225+
return await interaction.response.send_message(
226+
f"User `{user.name}` is not in group `{group_object.name}`.",
227+
ephemeral=True
228+
)
229+
230+
await interaction.response.send_message(
231+
f"User `{user.name}` has been removed from group `{group_object.name}`."
232+
)
233+
234+
async def fetch_groups(self) -> List[DBGroup]:
235+
return await self.run_async(
236+
groups.fetch_all
237+
)
238+
239+
async def create_group_entry(self, user_id: int, group_id: int) -> DBGroupEntry:
240+
return await self.run_async(
241+
groups.create_entry,
242+
user_id, group_id
243+
)
244+
245+
async def delete_group_entry(self, user_id: int, group_id: int) -> int:
246+
return await self.run_async(
247+
groups.delete_entry,
248+
user_id, group_id
249+
)
250+
251+
async def create_name_history_entry(self, user_id: int, old_name: str) -> DBName:
252+
return await self.run_async(
253+
names.create,
254+
user_id, old_name
255+
)
256+
257+
async def fetch_reserved_name(self, name: str) -> DBName | None:
258+
return await self.run_async(
259+
names.fetch_by_name_reserved,
260+
name
261+
)
262+
263+
async def fetch_name_history(self, user_id: int) -> List[DBName]:
264+
return await self.run_async(
265+
names.fetch_all,
266+
user_id
267+
)
268+
269+
async def update_name_history_entry(self, id: int, data: dict) -> int:
270+
return await self.run_async(
271+
names.update,
272+
id, data
273+
)
274+
275+
async def resolve_user_from_identifier(self, identifier: str) -> DBUser | None:
276+
if identifier.isnumeric():
277+
return await self.resolve_user_by_id(int(identifier))
278+
279+
if identifier.startswith("<@") and identifier.endswith(">"):
280+
discord_id = identifier[2:-1]
281+
discord_id = discord_id.strip("!")
282+
283+
if discord_id.isnumeric():
284+
return await self.resolve_user(int(discord_id))
285+
286+
return await self.resolve_user_by_name_case_insensitive(identifier)
287+
288+
async def setup(bot: Bot):
289+
await bot.add_cog(Moderation())

0 commit comments

Comments
 (0)