1111from config import settings
1212from db .core .models import GroupMadeMember
1313from exceptions import ApplicantRoleDoesNotExistError , GuestRoleDoesNotExistError
14- from utils import CommandChecks , TeXBotApplicationContext , TeXBotBaseCog
14+ from utils import CommandChecks , TeXBotBaseCog
1515from utils .msl import (
1616 fetch_community_group_members_count ,
1717 is_id_a_community_group_member ,
2222 from logging import Logger
2323 from typing import Final
2424
25+ from utils import TeXBotApplicationContext
26+
2527__all__ : "Sequence[str]" = (
2628 "MakeMemberCommandCog" ,
2729 "MakeMemberModalCommandCog" ,
5658)
5759
5860
59- class MakeMemberCommandCog (TeXBotBaseCog ):
61+ class MakeMemberBaseCog (TeXBotBaseCog ):
62+ """Base cog class for make member interactions."""
63+
64+ async def _perform_make_member (
65+ self , user : discord .User | discord .Member , raw_group_member_id : str
66+ ) -> tuple [bool , str ]:
67+ """Perform the actions to make a user a member."""
68+ member_role : discord .Role = await self .bot .member_role
69+ discord_member : discord .Member = await self .bot .get_main_guild_member (user )
70+
71+ INVALID_GROUP_MEMBER_ID_MESSAGE : Final [str ] = (
72+ f"{ raw_group_member_id !r} is not a valid { self .bot .group_member_id_type } ID."
73+ )
74+
75+ if not re .fullmatch (r"\A\d{7}\Z" , raw_group_member_id ):
76+ return False , INVALID_GROUP_MEMBER_ID_MESSAGE
77+
78+ try :
79+ group_member_id : int = int (raw_group_member_id )
80+ except ValueError :
81+ return False , INVALID_GROUP_MEMBER_ID_MESSAGE
82+
83+ if member_role in discord_member .roles :
84+ return (False , (
85+ ":information_source: No changes made. "
86+ "You're already a member - why are you trying this again? :information_source:"
87+ ))
88+
89+ if await GroupMadeMember .objects .filter (
90+ hashed_group_member_id = GroupMadeMember .hash_group_member_id (
91+ group_member_id , self .bot .group_member_id_type
92+ )
93+ ).aexists ():
94+ return False , "This student ID has already been used."
95+
96+ if not await is_id_a_community_group_member (member_id = group_member_id ):
97+ return False , (
98+ f"You must be a member of { self .bot .group_full_name } "
99+ "to use this command.\n "
100+ f"The provided { _GROUP_MEMBER_ID_ARGUMENT_NAME } must match "
101+ f"the { self .bot .group_member_id_type } ID "
102+ f"that you purchased your { self .bot .group_short_name } membership with."
103+ )
104+
105+ await discord_member .add_roles (
106+ member_role , reason = f"{ discord_member } used TeX-Bot to become a member"
107+ )
108+
109+ try :
110+ await GroupMadeMember .objects .acreate (group_member_id = raw_group_member_id ) # type: ignore[misc]
111+ except ValidationError as create_group_made_member_error :
112+ error_is_already_exists : bool = (
113+ "hashed_group_member_id" in create_group_made_member_error .message_dict
114+ and any (
115+ "already exists" in error
116+ for error in create_group_made_member_error .message_dict [
117+ "hashed_group_member_id"
118+ ]
119+ )
120+ )
121+ if not error_is_already_exists :
122+ raise
123+
124+ try :
125+ guest_role : discord .Role = await self .bot .guest_role
126+ except GuestRoleDoesNotExistError :
127+ logger .warning (
128+ '"/make-member" command used but the "Guest" role does not exist. '
129+ 'Some user\' s may now have the "Member" role without the "Guest" role. '
130+ 'Use the "/ensure-members-inducted" command to fix this issue.'
131+ )
132+ else :
133+ if guest_role not in discord_member .roles :
134+ await discord_member .add_roles (
135+ guest_role ,
136+ reason = f"{ discord_member } used TeX-Bot to become a member." ,
137+ )
138+
139+ try :
140+ applicant_role : discord .Role = await self .bot .applicant_role
141+ except ApplicantRoleDoesNotExistError :
142+ pass
143+ else :
144+ if applicant_role in discord_member .roles :
145+ await discord_member .remove_roles (
146+ applicant_role ,
147+ reason = f"{ discord_member } used TeX-Bot to become a member." ,
148+ )
149+
150+ return True , ""
151+
152+
153+ class MakeMemberCommandCog (MakeMemberBaseCog ):
60154 """Cog class that defines the "/make-member" command and its call-back method."""
61155
62156 @discord .slash_command (
@@ -105,112 +199,14 @@ async def make_member(
105199 has purchased a valid membership to your community group,
106200 then gives the member the "Member" role.
107201 """
108- # NOTE: Shortcut accessors are placed at the top of the function so that the exceptions they raise are displayed before any further errors may be sent
109- member_role : discord .Role = await self .bot .member_role
110- interaction_member : discord .Member = await ctx .bot .get_main_guild_member (ctx .user )
111-
112- INVALID_GROUP_MEMBER_ID_MESSAGE : Final [str ] = (
113- f"{ raw_group_member_id !r} is not a valid { self .bot .group_member_id_type } ID."
114- )
115-
116- if not re .fullmatch (r"\A\d{7}\Z" , raw_group_member_id ):
117- await self .command_send_error (ctx , message = (INVALID_GROUP_MEMBER_ID_MESSAGE ))
118- return
119-
120- try :
121- group_member_id : int = int (raw_group_member_id )
122- except ValueError :
123- await self .command_send_error (ctx , message = INVALID_GROUP_MEMBER_ID_MESSAGE )
124- return
125-
126202 await ctx .defer (ephemeral = True )
127- async with ctx .typing ():
128- if member_role in interaction_member .roles :
129- await ctx .followup .send (
130- content = (
131- ":information_source: No changes made. You're already a member "
132- "- why are you trying this again? :information_source:"
133- ),
134- ephemeral = True ,
135- )
136- return
137203
138- if await GroupMadeMember .objects .filter (
139- hashed_group_member_id = GroupMadeMember .hash_group_member_id (
140- group_member_id , self .bot .group_member_id_type
141- )
142- ).aexists ():
143- await ctx .followup .send (
144- content = (
145- ":information_source: No changes made. This student ID has already "
146- f"been used. Please contact a {
147- await self .bot .get_mention_string (self .bot .committee_role )
148- } member if this is an error. :information_source:"
149- ),
150- ephemeral = True ,
151- )
152- return
153-
154- if not await is_id_a_community_group_member (member_id = group_member_id ):
155- await self .command_send_error (
156- ctx ,
157- message = (
158- f"You must be a member of { self .bot .group_full_name } "
159- "to use this command.\n "
160- f"The provided { _GROUP_MEMBER_ID_ARGUMENT_NAME } must match "
161- f"the { self .bot .group_member_id_type } ID "
162- f"that you purchased your { self .bot .group_short_name } membership with."
163- ),
164- )
165- return
166-
167- # NOTE: The "Member" role must be added to the user **before** the "Guest" role to ensure that the welcome message does not include the suggestion to purchase membership
168- await interaction_member .add_roles (
169- member_role , reason = f'{ ctx .user } used TeX Bot slash-command: "/make-member"'
204+ with ctx .typing ():
205+ _ , message = await self ._perform_make_member (
206+ user = ctx .user , raw_group_member_id = raw_group_member_id
170207 )
171208
172- try :
173- await GroupMadeMember .objects .acreate (group_member_id = raw_group_member_id ) # type: ignore[misc]
174- except ValidationError as create_group_made_member_error :
175- error_is_already_exists : bool = (
176- "hashed_group_member_id" in create_group_made_member_error .message_dict
177- and any (
178- "already exists" in error
179- for error in create_group_made_member_error .message_dict [
180- "hashed_group_member_id"
181- ]
182- )
183- )
184- if not error_is_already_exists :
185- raise
186-
187- await ctx .followup .send (content = "Successfully made you a member!" , ephemeral = True )
188-
189- try :
190- guest_role : discord .Role = await self .bot .guest_role
191- except GuestRoleDoesNotExistError :
192- logger .warning (
193- '"/make-member" command used but the "Guest" role does not exist. '
194- 'Some user\' s may now have the "Member" role without the "Guest" role. '
195- 'Use the "/ensure-members-inducted" command to fix this issue.'
196- )
197- else :
198- if guest_role not in interaction_member .roles :
199- await interaction_member .add_roles (
200- guest_role ,
201- reason = f'{ ctx .user } used TeX Bot slash-command: "/make-member"' ,
202- )
203- applicant_role : discord .Role | None
204- try :
205- applicant_role = await ctx .bot .applicant_role
206- except ApplicantRoleDoesNotExistError :
207- applicant_role = None
208-
209- if applicant_role and applicant_role in interaction_member .roles :
210- await interaction_member .remove_roles (
211- applicant_role ,
212- reason = f'{ ctx .user } used TeX Bot slash-command: "/make-member"' ,
213- )
209+ await ctx .followup .send (content = message , ephemeral = True )
214210
215211
216212class MemberCountCommandCog (TeXBotBaseCog ):
@@ -232,7 +228,7 @@ async def member_count(self, ctx: "TeXBotApplicationContext") -> None:
232228 )
233229
234230
235- class MakeMemberModalActual (Modal ):
231+ class MakeMemberModalActual (Modal , MakeMemberBaseCog ):
236232 """A discord.Modal containing a the input box for make member user interaction."""
237233
238234 @override
@@ -257,26 +253,25 @@ async def callback(self, interaction: discord.Interaction) -> None:
257253 )
258254 return
259255
260- try :
261- student_id : int = int (raw_student_id )
262- except ValueError :
256+ if not interaction .user :
263257 await interaction .response .send_message (
264- content = "Student ID must be a number." , ephemeral = True
258+ content = "Something went wrong, contact a committee member if this persists." ,
259+ ephemeral = True ,
265260 )
266- return
267-
268- if await is_id_a_community_group_member (member_id = student_id ):
269- await MakeMemberModalCommandCog .give_member_role (
270- self = MakeMemberModalCommandCog (bot = interaction .client ), # type: ignore[arg-type]
271- interaction = interaction ,
261+ logger .debug (
262+ "Interaction user was unexpectedly None in MakeMemberModal. Interaction: %s" ,
263+ interaction .data ,
272264 )
273- await interaction .response .send_message (content = "Action complete." )
274265 return
275266
276- await interaction .response .send_message (
277- content = "Student ID not found." , ephemeral = True
267+ await interaction .response .defer (ephemeral = True )
268+
269+ _ , message = await self ._perform_make_member (
270+ user = interaction .user , raw_group_member_id = raw_student_id
278271 )
279272
273+ await interaction .followup .send (content = message , ephemeral = True )
274+
280275
281276class OpenMemberVerifyModalView (View ):
282277 """A discord.View containing a button to open a new member verification modal."""
@@ -293,28 +288,14 @@ async def verify_new_member_button_callback( # type: ignore[misc]
293288 await interaction .response .send_modal (MakeMemberModalActual ())
294289
295290
296- class MakeMemberModalCommandCog (TeXBotBaseCog ):
291+ class MakeMemberModalCommandCog (MakeMemberBaseCog ):
297292 """Cog class that defines the "/make-member-modal" command and its call-back method."""
298293
299294 @TeXBotBaseCog .listener ()
300295 async def on_ready (self ) -> None :
301296 """Add OpenMemberVerifyModalView to the bot's list of permanent views."""
302297 self .bot .add_view (OpenMemberVerifyModalView ())
303298
304- async def give_member_role (self , interaction : discord .Interaction ) -> None :
305- """Give the member role to the user who interacted with the modal."""
306- if not isinstance (interaction .user , discord .Member ):
307- await self .command_send_error (
308- ctx = TeXBotApplicationContext (bot = interaction .client , interaction = interaction ), # type: ignore[arg-type]
309- message = "User is not a member." ,
310- )
311- return
312-
313- await interaction .user .add_roles (
314- await self .bot .member_role ,
315- reason = f'{ interaction .user } used TeX Bot modal: "Make Member"' ,
316- )
317-
318299 async def _open_make_new_member_modal (
319300 self ,
320301 button_callback_channel : discord .TextChannel | discord .DMChannel ,
0 commit comments