55import asyncio
66import datetime
77import discord
8+ import io
89import json
910import jsonschema
1011import os
1617from aiohttp import web
1718from CustomModules import log_handler
1819from dotenv import load_dotenv
20+ from PIL import Image
1921from urllib .parse import urlparse
2022from zipfile import ZIP_DEFLATED , ZipFile
2123
2830if not os .path .exists (APP_FOLDER_NAME ):
2931 os .makedirs (APP_FOLDER_NAME )
3032ACTIVITY_FILE = os .path .join (APP_FOLDER_NAME , 'activity.json' )
31- BOT_VERSION = "1.10.8 "
33+ BOT_VERSION = "1.11.0 "
3234TOKEN = os .getenv ('TOKEN' )
3335OWNERID = os .getenv ('OWNER_ID' )
3436SUPPORTID = os .getenv ('SUPPORT_SERVER' )
@@ -364,6 +366,38 @@ async def function():
364366 except asyncio .CancelledError :
365367 pass
366368
369+ async def process_avatar_file (attachment : discord .Attachment ) -> bytes :
370+ ALLOWED_FORMATS = ("jpg" , "jpeg" , "png" , "gif" , "webp" , "avif" )
371+
372+ MIN_SIZE = 128
373+ ext = attachment .filename .split ("." )[- 1 ].lower ()
374+ if ext not in ALLOWED_FORMATS :
375+ raise ValueError (f"Invalid file format. Allowed: { ', ' .join (ALLOWED_FORMATS )} " )
376+
377+ file_bytes = await attachment .read ()
378+
379+ try :
380+ img = Image .open (io .BytesIO (file_bytes ))
381+ img .verify ()
382+ except Exception :
383+ raise ValueError ("The uploaded file is not a valid image." )
384+
385+ img = Image .open (io .BytesIO (file_bytes )).convert ("RGBA" )
386+
387+ width , height = img .size
388+ min_dim = min (width , height )
389+ left = (width - min_dim ) // 2
390+ top = (height - min_dim ) // 2
391+ img = img .crop ((left , top , left + min_dim , top + min_dim ))
392+
393+ if min_dim < MIN_SIZE :
394+ img = img .resize ((MIN_SIZE , MIN_SIZE ), Image .Resampling .LANCZOS )
395+
396+ output = io .BytesIO ()
397+ img .save (output , format = "PNG" )
398+ output .seek (0 )
399+ return output .read ()
400+
367401
368402##Owner Commands
369403class Owner ():
@@ -531,7 +565,6 @@ async def shutdown(message):
531565
532566
533567##Bot Commands----------------------------------------
534- #Bot Information
535568@tree .command (name = 'botinfo' , description = 'Get information about the bot.' )
536569@discord .app_commands .checks .cooldown (1 , 60 , key = lambda i : (i .user .id ))
537570async def botinfo (interaction : discord .Interaction ):
@@ -581,7 +614,7 @@ async def botinfo(interaction: discord.Interaction):
581614 embed .add_field (name = "RAM" , value = f"{ ram_real } MB" , inline = True )
582615
583616 await interaction .edit_original_response (embed = embed )
584- #Support Invite
617+
585618if support_available :
586619 @tree .command (name = 'support' , description = 'Get invite to our support server.' )
587620 @discord .app_commands .checks .cooldown (1 , 60 , key = lambda i : (i .user .id ))
@@ -595,41 +628,261 @@ async def support(interaction: discord.Interaction):
595628 await interaction .followup .send (await Functions .create_support_invite (interaction ), ephemeral = True )
596629 else :
597630 await interaction .response .send_message ('You are already in our support server!' , ephemeral = True )
598- #Ping
631+
599632@tree .command (name = 'ping' , description = 'Test, if the bot is responding.' )
600633async def ping (interaction : discord .Interaction ):
601634 before = time .monotonic ()
602635 await interaction .response .send_message ('Pong!' )
603636 ping = (time .monotonic () - before ) * 1000
604637 await interaction .edit_original_response (content = f'Pong! `{ int (ping )} ms`' )
638+
639+
605640##Main Commands----------------------------------------
606- #Create Webhook
607- @tree .command (name = 'create_webhook' , description = 'Create a webhook.' )
641+ @tree .command (name = 'create_webhook' , description = 'Create a webhook.' )
608642@discord .app_commands .checks .cooldown (1 , 60 , key = lambda i : (i .channel .id ))
609- @discord .app_commands .describe (name = 'Name of the webhook.' , channel = 'Channel the webhook should be created in.' )
610- async def create_webhook (interaction : discord .Interaction , name : str , channel : discord .TextChannel ):
643+ @discord .app_commands .describe (
644+ name = 'Name of the webhook.' ,
645+ channel = 'Channel the webhook should be created in.' ,
646+ avatar_file = 'Upload an optional avatar image (Discord-supported formats only).'
647+ )
648+ async def create_webhook (
649+ interaction : discord .Interaction ,
650+ name : str ,
651+ channel : discord .TextChannel ,
652+ avatar_file : discord .Attachment | None = None
653+ ):
611654 if name .lower () in ['discord' , 'wumpus' ]:
612655 await interaction .response .send_message ('Please choose a different name for your webhook.' , ephemeral = True )
613656 return
657+
614658 if not channel .permissions_for (interaction .user ).manage_webhooks :
615- await interaction .response .send_message (f'You need the permission "Manage Webhooks" for { channel .mention } to use this command!' , ephemeral = True )
659+ await interaction .response .send_message (
660+ f'You need the permission "Manage Webhooks" for { channel .mention } to use this command!' ,
661+ ephemeral = True
662+ )
616663 return
617664 if not channel .permissions_for (interaction .guild .me ).manage_webhooks :
618- await interaction .response .send_message (f'I need the permission "Manage Webhooks" for { channel .mention } to use this command!' , ephemeral = True )
665+ await interaction .response .send_message (
666+ f'I need the permission "Manage Webhooks" for { channel .mention } to use this command!' ,
667+ ephemeral = True
668+ )
619669 return
620- if len (name ) < 1 or len (name ) > 80 or name .strip () == '' :
621- name = 'WebhookCreator'
670+
671+ name = name if name and 0 < len (name .strip ()) < 80 else "WebhookCreator"
672+
673+ avatar_bytes = None
674+ if avatar_file :
675+ try :
676+ avatar_bytes = await Functions .process_avatar_file (avatar_file )
677+ except Exception as e :
678+ await interaction .response .send_message (f"Failed to process avatar: { e } " , ephemeral = True )
679+ return
680+
622681 try :
623- webhook = await channel .create_webhook (name = name , reason = f'Created by { interaction .user .name } #{ interaction .user .discriminator } ({ interaction .user .id } )' )
624- await interaction .response .send_message (f'Webhook for channel { channel .mention } :\n { webhook .url } ' , ephemeral = True )
682+ webhook = await channel .create_webhook (
683+ name = name ,
684+ avatar = avatar_bytes ,
685+ reason = f'Created by { interaction .user .name } ({ interaction .user .id } )'
686+ )
687+ await interaction .response .send_message (
688+ f'Webhook for channel { channel .mention } :\n { webhook .url } \n \n '
689+ '!!!Make sure to save it, since you WILL NOT be able to see it again!!!' ,
690+ ephemeral = True
691+ )
625692 except discord .errors .HTTPException as e :
626693 if e .code == 30007 :
627- await interaction .response .send_message (f'You reached the maximum amount of webhooks in this guild.\n This is a limit, imposed by discord, which I can\' t change.' , ephemeral = True )
694+ await interaction .response .send_message (
695+ 'You reached the maximum amount of webhooks in this guild.\n '
696+ 'This is a limit, imposed by Discord, which I can\' t change.' ,
697+ ephemeral = True
698+ )
628699 else :
629- _message = f'Error while creating webhook: { e } '
700+ _message = f'Error while creating webhook: { e } '
630701 await interaction .response .send_message (_message , ephemeral = True )
631702 program_logger .error (_message )
632703
704+ @tree .command (name = 'delete_webhook' , description = 'Delete a wbhook from a server.' )
705+ @discord .app_commands .checks .cooldown (1 , 60 , key = lambda i : (i .user .id ))
706+ @discord .app_commands .describe (webhook = 'Select the webhook you want to delete. -> Name: Creator (Channel)' )
707+ async def delete_webhook (interaction : discord .Interaction , webhook : str ):
708+ await interaction .response .defer (ephemeral = True )
709+ if not interaction .guild :
710+ await interaction .followup .send ('This command can only be used in a server.' , ephemeral = True )
711+ return
712+ try :
713+ webhook_id = int (webhook .split ('|' )[0 ])
714+ except Exception :
715+ await interaction .followup .send ('Invalid selection.' , ephemeral = True )
716+ return
717+
718+ webhooks = await interaction .guild .webhooks ()
719+ for wh in webhooks :
720+ if wh .id == webhook_id :
721+ break
722+ else :
723+ await interaction .followup .send ('Could not find the selected webhook in this server.' , ephemeral = True )
724+ return
725+
726+ channel = wh .channel
727+ if channel is None :
728+ await interaction .followup .send ('The channel of this webhook no longer exists.' , ephemeral = True )
729+ return
730+ if not channel .permissions_for (interaction .user ).manage_webhooks :
731+ await interaction .followup .send (f'You need the permission "Manage Webhooks" for { channel .mention } to use this command!' , ephemeral = True )
732+ return
733+ if not channel .permissions_for (interaction .guild .me ).manage_webhooks :
734+ await interaction .followup .send ("I don't have permission to delete webhooks in that channel." , ephemeral = True )
735+ return
736+ try :
737+ await wh .delete (reason = f'Deleted from { interaction .user } ({ interaction .user .id } )' )
738+ await interaction .followup .send (f'Webhook **{ wh .name } ** in { channel .mention } got deleted.' , ephemeral = True )
739+ except Exception as e :
740+ program_logger .error (f'Error while deleting webhook: { e } ' )
741+ await interaction .followup .send (f'Error during deletion: { e } ' , ephemeral = True )
742+
743+ @tree .command (name = "edit_webhook" , description = "Edit a webhook's name, channel, or avatar." )
744+ @discord .app_commands .checks .cooldown (1 , 60 , key = lambda i : (i .user .id ))
745+ @discord .app_commands .describe (
746+ webhook = "Select the webhook you want to edit." ,
747+ new_name = "New name for the webhook (optional)." ,
748+ new_channel = "New channel for the webhook (optional)." ,
749+ avatar_file = "Upload a new avatar image (optional, only Discord-supported formats)."
750+ )
751+ async def edit_webhook (
752+ interaction : discord .Interaction ,
753+ webhook : str ,
754+ new_name : str | None = None ,
755+ new_channel : discord .TextChannel | None = None ,
756+ avatar_file : discord .Attachment | None = None
757+ ):
758+ await interaction .response .defer (ephemeral = True )
759+
760+ if not interaction .guild :
761+ await interaction .followup .send ("This command can only be used in a server." , ephemeral = True )
762+ return
763+
764+ try :
765+ webhook_id = int (webhook .split ("|" )[0 ])
766+ except Exception :
767+ await interaction .followup .send ("Invalid selection." , ephemeral = True )
768+ return
769+
770+ webhooks = await interaction .guild .webhooks ()
771+ for wh in webhooks :
772+ if wh .id == webhook_id :
773+ break
774+ else :
775+ await interaction .followup .send ("Webhook not found." , ephemeral = True )
776+ return
777+
778+ src_channel = wh .channel
779+ tgt_channel = new_channel or wh .channel
780+ if not src_channel or not tgt_channel :
781+ await interaction .followup .send ("Source or target channel no longer exists." , ephemeral = True )
782+ return
783+
784+ if not (src_channel .permissions_for (interaction .user ).manage_webhooks and
785+ tgt_channel .permissions_for (interaction .user ).manage_webhooks ):
786+ await interaction .followup .send (
787+ "You don't have Manage Webhooks permission in the source or target channel." ,
788+ ephemeral = True
789+ )
790+ return
791+
792+ bot_member = interaction .guild .me
793+ if not (src_channel .permissions_for (bot_member ).manage_webhooks and
794+ tgt_channel .permissions_for (bot_member ).manage_webhooks ):
795+ await interaction .followup .send (
796+ "I don't have Manage Webhooks permission in the source or target channel." ,
797+ ephemeral = True
798+ )
799+ return
800+
801+ if avatar_file :
802+ avatar_bytes = None
803+ try :
804+ avatar_bytes = await Functions .process_avatar_file (avatar_file )
805+ except Exception as e :
806+ program_logger .error (f"Error processing avatar file: { e } " )
807+ await interaction .followup .send (f"Failed to process avatar: { e } " , ephemeral = True )
808+ return
809+
810+ try :
811+ await wh .edit (
812+ name = new_name if new_name and 0 < len (new_name ) < 80 else wh .name ,
813+ channel = tgt_channel ,
814+ avatar = avatar_bytes if avatar_file else wh .avatar
815+ )
816+ await interaction .followup .send (f"Webhook **{ wh .name } ** updated successfully." , ephemeral = True )
817+ except Exception as e :
818+ program_logger .error (f"Error while editing webhook: { e } " )
819+ await interaction .followup .send (f"Failed to edit webhook: { e } " , ephemeral = True )
820+
821+ @tree .command (name = "list_webhooks" , description = "List all webhooks of this server with details." )
822+ @discord .app_commands .checks .has_permissions (administrator = True )
823+ async def list_webhooks (interaction : discord .Interaction ):
824+ if not interaction .guild :
825+ await interaction .response .send_message ("This command can only be used in a server." , ephemeral = True )
826+ return
827+
828+ webhooks = await interaction .guild .webhooks ()
829+ valid_webhooks = [wh for wh in webhooks if wh .token ]
830+
831+ if not valid_webhooks :
832+ await interaction .response .send_message ("No usable webhooks found in this server." , ephemeral = True )
833+ return
834+
835+ lines = []
836+ for wh in valid_webhooks :
837+ creator = wh .user .mention if wh .user else "N/A"
838+ channel = wh .channel .mention if wh .channel else "N/A"
839+ created = discord .utils .format_dt (wh .created_at , style = "R" )
840+
841+ lines .append (
842+ f"**{ wh .name } ** (ID: `{ wh .id } `)\n "
843+ f"• Channel: { channel } \n "
844+ f"• Creator: { creator } \n "
845+ f"• Created: { created } \n "
846+ f"• [Webhook URL]({ wh .url } )\n "
847+ f"⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺⸺"
848+ )
849+
850+ message_chunks = []
851+ current = ""
852+ for line in lines :
853+ if len (current ) + len (line ) > 1900 :
854+ message_chunks .append (current )
855+ current = ""
856+ current += line + "\n "
857+ if current :
858+ message_chunks .append (current )
859+
860+ await interaction .response .send_message (message_chunks [0 ], ephemeral = True )
861+ for chunk in message_chunks [1 :]:
862+ await interaction .followup .send (chunk , ephemeral = True )
863+
864+
865+ # Autocomplete for Webhook-Parameter
866+ @delete_webhook .autocomplete ('webhook' )
867+ @edit_webhook .autocomplete ('webhook' )
868+ async def webhook_autocomplete (interaction : discord .Interaction , current : str ):
869+ if not interaction .guild :
870+ return []
871+ webhooks = await interaction .guild .webhooks ()
872+ choices = []
873+ for wh in webhooks :
874+ if not wh .token :
875+ continue
876+ owner = wh .user .name if wh .user else 'N/A'
877+ channel_name = wh .channel .name if wh .channel else 'N/A'
878+ label = f"{ wh .name } : { owner } ({ channel_name } )"
879+ value = f"{ wh .id } |{ wh .name } "
880+ if current .lower () in label .lower ():
881+ choices .append (discord .app_commands .Choice (name = label , value = value ))
882+ if len (choices ) >= 25 :
883+ break
884+ return choices
885+
633886
634887
635888
0 commit comments