Skip to content

Commit faf99a9

Browse files
committed
rate-limit prevention measures
1 parent 186b393 commit faf99a9

2 files changed

Lines changed: 75 additions & 23 deletions

File tree

src/commands/club.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,10 @@ async def remove_club_member(
7474
member: Member,
7575
):
7676
await interaction.response.defer(thinking=True)
77-
db_club = Club.get_or_none(Club.sith_id == club.id)
77+
db_club: Club = Club.get_or_none(Club.sith_id == club.id)
7878
if not db_club:
7979
await interaction.followup.send(f"Le club : {club.name} n'existe pas")
8080
return
81-
role_membre = utils.get(member.guild.roles, id=db_club.member_role_id)
8281
if (
8382
not interaction.user.guild_permissions.manage_roles
8483
and not interaction.user.get_role(db_club.president_role_id)
@@ -87,9 +86,19 @@ async def remove_club_member(
8786
"Seul le président du club et les admins peuvent retirer un membre"
8887
)
8988
return
90-
if role_membre not in member.roles:
89+
member_role = interaction.guild.get_role(db_club.member_role_id)
90+
board_roles = [db_club.president_role_id, db_club.treasurer_role_id]
91+
if member_role not in member.roles:
9192
await interaction.followup.send("Cet utilisateur n'est pas dans le club")
9293
return
94+
if any(member.get_role(r) for r in board_roles):
95+
await interaction.followup.send(
96+
"Cette commande ne peut pas être utilisée pour retirer "
97+
"le président ou le trésorier dun club.\n\n"
98+
"Utilisez plutôt `/club passation` ou `/club arret`."
99+
)
100+
return
101+
93102
await self.club_service.remove_member(db_club, member)
94103
await interaction.followup.send(
95104
f"{member.name} a été retiré du club :thumbs_up:"
@@ -109,7 +118,7 @@ async def add_club_member(
109118
):
110119
await interaction.response.defer(thinking=True)
111120
db_club = Club.get_or_none(Club.sith_id == club.id)
112-
role_membre = member.guild.get_role(db_club.member_role_id)
121+
role_membre = interaction.guild.get_role(db_club.member_role_id)
113122
if not db_club:
114123
await interaction.followup.send(f"Le club : {club.name} n'existe pas")
115124
return

src/services/club.py

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
from typing import TYPE_CHECKING
45
from urllib.parse import urljoin
56

@@ -35,6 +36,7 @@ def __init__(self, bot: AeBot):
3536
self._client = bot.client
3637
self._club_cache = {}
3738
self._bot = bot
39+
self._background_tasks = set()
3840

3941
async def search_club(
4042
self, current: str, *, only_existing: bool
@@ -135,13 +137,54 @@ async def add_member(self, club: Club, member: Member):
135137
async def remove_member(
136138
self, club: Club, member: Member, *, make_former: bool = True
137139
):
138-
role = utils.get(member.guild.roles, id=club.member_role_id)
139-
former = utils.get(member.guild.roles, id=club.former_member_role_id)
140-
await member.remove_roles(role, reason=f"{member.name} left club {club.name}")
141-
if make_former:
142-
await member.add_roles(
143-
former, reason=f"{member.name} left club {club.name}"
140+
"""Remove a member from the club.
141+
142+
Args:
143+
club: The club to remove the user from
144+
member: The member to remove
145+
make_former:
146+
if True, the member will receive
147+
a role indicating its former club membership
148+
"""
149+
await self.remove_members(club, [member], make_former=make_former)
150+
151+
async def remove_members(
152+
self,
153+
club: Club,
154+
members: list[Member] | tuple[Member] | set[Member],
155+
*,
156+
make_former: bool = True,
157+
):
158+
"""Remove multiple members from a club.
159+
160+
Args:
161+
club: The club to remove the user from
162+
members: The members to remove
163+
make_former:
164+
if True, the member will receive
165+
a role indicating its former club membership
166+
167+
Warnings:
168+
This method sleeps for two seconds between each member
169+
(in order to avoid rate-limit), so it may be a bad idea
170+
to await it.
171+
Favour an execution inside a detached async Task.
172+
"""
173+
role_ids = [club.member_role_id, club.president_role_id, club.treasurer_role_id]
174+
roles = [self._bot.watched_guild.get_role(r) for r in role_ids]
175+
former = self._bot.watched_guild.get_role(club.former_member_role_id)
176+
for member in members:
177+
if len(members) > 1:
178+
# if there is more than one member,
179+
# sleep a little bit to avoid rate limit
180+
await asyncio.sleep(2)
181+
await member.remove_roles(
182+
*roles, reason=f"{member.name} left club {club.name}"
144183
)
184+
if make_former:
185+
await member.add_roles(
186+
former, reason=f"{member.name} left club {club.name}"
187+
)
145188

146189
async def handover(
147190
self, club: ClubSchema, new_pres: Member, new_treso: Member, guild: Guild
@@ -176,25 +219,25 @@ async def stop_club(self, club: Club, guild: Guild):
176219
role_pres = utils.get(guild.roles, id=club.president_role_id)
177220
role_treso = utils.get(guild.roles, id=club.treasurer_role_id)
178221
role_member = utils.get(guild.roles, id=club.member_role_id)
179-
role_former = utils.get(guild.roles, id=club.former_member_role_id)
180-
old_member = {*role_pres.members, *role_treso.members, *role_member.members}
222+
old_members = {*role_pres.members, *role_treso.members, *role_member.members}
181223
category = utils.get(guild.categories, id=club.category_id)
182224
await self.move_to_bottom(category)
183225
await category.edit(name=club.name + " [inactif]")
184-
await self.move_to_bottom(category)
185-
186-
for e in old_member:
187-
await e.remove_roles(
188-
role_pres,
189-
role_treso,
190-
role_member,
191-
reason=f"Arrêt du club : {club.name}",
192-
)
193-
await e.add_roles(role_former, reason=f"Arrêt du club : {club.name}")
226+
# see https://docs.python.org/3/library/asyncio-task.html#creating-tasks
227+
task = asyncio.create_task(
228+
self.remove_members(club, old_members, make_former=True)
229+
)
230+
self._background_tasks.add(task)
231+
task.add_done_callback(self._background_tasks.discard)
194232

195233
@staticmethod
196234
async def move_to_bottom(category: CategoryChannel):
197-
"""Move this category after the last category belong to an active club."""
235+
"""Move this category after the last category belong to an active club.
236+
237+
Warnings:
238+
This method seems to have a high cost on discord's side.
239+
Using it a little bit too much is likely to end in rate-limit.
240+
"""
198241
guild = category.guild
199242
inactives = [c for c in guild.categories if c.name.endswith("[inactif]")]
200243
if not inactives:

0 commit comments

Comments
 (0)