11from discord import (
22 Attachment ,
33 Forbidden ,
4+ HTTPException ,
45 Interaction ,
56 Member ,
7+ Message ,
68 NotFound ,
79 Object ,
810 Role ,
@@ -204,11 +206,13 @@ async def proxy(
204206 @app_commands .default_permissions (administrator = True )
205207 @app_commands .checks .has_permissions (administrator = True )
206208 @app_commands .command (
207- description = "Purge messages in current channel" , extras = {"category" : "Console" }
209+ description = "Purge messages in current channel with advanced filters " , extras = {"category" : "Console" }
208210 )
209211 @app_commands .describe (
210- limit = "Maximum number of messages to purge (default: 1)" ,
212+ limit = "Number of targeted messages to delete (default: 1)" ,
211213 member = "Only purge messages from this member (optional)" ,
214+ role = "Only purge messages from members with this role (optional)" ,
215+ message_id = "Target a single specific message ID to delete (optional)" ,
212216 before = "Purge messages before this message ID (optional)" ,
213217 after = "Purge messages after this message ID (optional)" ,
214218 )
@@ -217,11 +221,13 @@ async def purge(
217221 interaction : Interaction ,
218222 limit : int = 1 ,
219223 member : Member | User | None = None ,
224+ role : Role | None = None ,
225+ message_id : str | None = None ,
220226 before : str | None = None ,
221227 after : str | None = None ,
222228 ):
223229 """
224- Purge messages in the current channel with better handling of before/after fields .
230+ Purge messages in the current channel with dynamic criteria scanning .
225231 """
226232 try :
227233 await interaction .response .defer (ephemeral = True )
@@ -239,30 +245,75 @@ async def purge(
239245 ephemeral = True ,
240246 )
241247
242- # Convert before and after to discord.Message objects if provided
248+ # Convert before, after, and target message IDs to integers/ objects if provided
243249 before_msg = None
244250 after_msg = None
251+ target_msg_id = None
252+
245253 try :
246254 if before :
247255 before_msg = await channel .fetch_message (int (before ))
248256 if after :
249257 after_msg = await channel .fetch_message (int (after ))
258+ if message_id :
259+ target_msg_id = int (message_id )
250260 except Exception as e :
251261 return await interaction .followup .send (
252262 embed = EmbedX .error (f"Invalid message ID provided: { e } " ),
253263 ephemeral = True ,
254264 )
255265
256- # Purge messages
266+ # Keep track of how many matching messages we've actually approved for deletion
267+ deleted_count = 0
268+
269+ def purge_check (msg : Message ) -> bool :
270+ nonlocal deleted_count
271+
272+ # If we already approved the requested number of deletions, stop matching
273+ if deleted_count >= limit :
274+ return False
275+
276+ # Filter by exact Message ID
277+ if target_msg_id and msg .id != target_msg_id :
278+ return False
279+
280+ # Filter by Member
281+ if member and msg .author .id != member .id :
282+ return False
283+
284+ # Filter by Role (Checks if author is a Member and has the role)
285+ if role :
286+ if not isinstance (msg .author , Member ) or role not in msg .author .roles :
287+ return False
288+
289+ # If it passed all filters, it's a match! Increment our target counter.
290+ deleted_count += 1
291+ return True
292+
293+ # Set an upper search ceiling. If looking for a specific message ID,
294+ # or a rare user, we might need to sweep through hundreds of records.
295+ # 1000 is usually a safe maximum history sweep boundary per command call.
296+ search_history_limit = 1000 if (member or role or target_msg_id ) else limit
297+
298+ # Run the purge with our flexible criteria checklist
257299 deleted = await channel .purge (
258- limit = limit ,
259- check = ( lambda msg : msg . author == member ) if member else MISSING ,
300+ limit = search_history_limit ,
301+ check = purge_check ,
260302 before = before_msg ,
261303 after = after_msg ,
262304 )
305+
306+ # Construct a dynamic success message based on used parameters
307+ filter_details = []
308+ if member : filter_details .append (f"by { member .mention } " )
309+ if role : filter_details .append (f"from roles matching { role .name } " )
310+ if target_msg_id : filter_details .append (f"with ID `{ target_msg_id } `" )
311+
312+ suffix = f" matching { " and " .join (filter_details )} " if filter_details else ""
313+
263314 await interaction .followup .send (
264315 embed = EmbedX .success (
265- f"Purged { len (deleted )} message(s){ f" by { member . mention } " if member else "" } ."
316+ f"Successfully purged { len (deleted )} message(s){ suffix } ."
266317 ),
267318 ephemeral = True ,
268319 )
@@ -345,4 +396,110 @@ async def role(self, interaction: Interaction, member: Member, role: Role):
345396 @role .error
346397 async def role_error (self , interaction : Interaction , error : app_commands .AppCommandError ):
347398 if isinstance (error , app_commands .errors .MissingPermissions ):
348- await interaction .response .send_message ("⛔ You need 'Manage Roles' permissions to use this command." , ephemeral = True )
399+ await interaction .response .send_message ("⛔ You need 'Manage Roles' permissions to use this command." , ephemeral = True )
400+
401+
402+ #----------------------------------------------------------------------------------------------------
403+ # * Server
404+ #----------------------------------------------------------------------------------------------------
405+ @app_commands .command (
406+ name = "server" ,
407+ description = "View or update server configuration settings."
408+ )
409+ @app_commands .describe (
410+ name = "Change the server's name" ,
411+ icon = "Upload a new server icon (Image file)" ,
412+ description = "Change the server's description (for public/discoverable servers)" ,
413+ private_profile = "Set if the server profile is private (True/False)"
414+ )
415+ @app_commands .checks .has_permissions (administrator = True )
416+ async def server_cmd (
417+ self ,
418+ interaction : Interaction ,
419+ name : str | None = None ,
420+ icon : Attachment | None = None ,
421+ description : str | None = None ,
422+ private_profile : bool | None = None
423+ ):
424+ guild = interaction .guild
425+
426+ # Guard clause if used outside a server (DM)
427+ if not guild :
428+ await interaction .response .send_message ("This command can only be used in a server." , ephemeral = True )
429+ return
430+
431+ # 1. IF NO ARGUMENTS PASSED: Return current server info
432+ if name is None and icon is None and description is None and private_profile is None :
433+ embed = EmbedX .info (
434+ title = f"Server Info" ,
435+ )
436+ if guild .icon :
437+ embed .set_thumbnail (url = guild .icon .url )
438+
439+ embed .add_field (name = "ID" , value = guild .id , inline = True )
440+ embed .add_field (name = "Name" , value = guild .name , inline = False )
441+ embed .add_field (name = "Owner" , value = guild .owner , inline = True )
442+ embed .add_field (name = "Members" , value = guild .member_count , inline = True )
443+ embed .add_field (name = "Description" , value = guild .description or "(No description)" , inline = False )
444+
445+ # Discord doesn't natively have a "private profile" boolean for guilds,
446+ # but we can check features like 'DISCOVERABLE' or display custom state.
447+ is_discoverable = "DISCOVERABLE" in guild .features
448+ embed .add_field (name = "Publicly Discoverable?" , value = "Yes" if is_discoverable else "No" , inline = True )
449+
450+ await interaction .response .send_message (embed = embed )
451+ return
452+
453+ # 2. IF ARGUMENTS PASSED: Update the server settings
454+ await interaction .response .defer (ephemeral = True ) # Defer since API calls take time
455+ changes = []
456+
457+ try :
458+ # Update Name
459+ if name is not None :
460+ await guild .edit (name = name )
461+ changes .append (f"• **Name** updated to: `{ name } `" )
462+
463+ # Update Description
464+ if description is not None :
465+ await guild .edit (description = description )
466+ changes .append (f"• **Description** updated to: `{ description } `" )
467+
468+ # Update Icon
469+ if icon is not None :
470+ if icon .content_type and icon .content_type .startswith ("image/" ):
471+ icon_bytes = await icon .read ()
472+ await guild .edit (icon = icon_bytes )
473+ changes .append ("• **Server Icon** updated successfully." )
474+ else :
475+ await interaction .followup .send ("❌ Provided file for the icon must be an image." , ephemeral = True )
476+ return
477+
478+ # Update Private Profile
479+ # Note: Native discord servers don't have a "private_profile" toggle.
480+ # This toggle changes community discovery features if the server qualifies.
481+ if private_profile is not None :
482+ if private_profile :
483+ if "DISCOVERABLE" in guild .features :
484+ await guild .remove_features ("DISCOVERABLE" )
485+ changes .append ("• **Private Profile**: Enabled (Removed from public discovery if applicable)." )
486+ else :
487+ # Enabling discoverability usually requires meeting explicit Discord requirements,
488+ # but we attempt to add it here.
489+ try :
490+ await guild .add_features ("DISCOVERABLE" )
491+ changes .append ("• **Private Profile**: Disabled (Added to public discovery)." )
492+ except HTTPException :
493+ changes .append ("• **Private Profile**: Failed to disable. (Server may not meet Discord's community discovery requirements)." )
494+
495+ # Send success confirmation
496+ result_embed = EmbedX .success (
497+ title = "Server Settings Updated" ,
498+ description = "\n " .join (changes ),
499+ )
500+ await interaction .followup .send (embed = result_embed , ephemeral = True )
501+
502+ except Forbidden :
503+ await interaction .followup .send ("❌ I do not have the required permissions (`Manage Server`) to change these settings." , ephemeral = True )
504+ except HTTPException as e :
505+ await interaction .followup .send (f"❌ An error occurred while updating settings: { e } " , ephemeral = True )
0 commit comments