Skip to content

Commit 2331612

Browse files
committed
Enhance webhook management features and update README
- Updated README to clarify webhook creation, editing, and deletion. - Bumped bot version to 1.11.0 in Dockerfile and code. - Added image handling imports and a new function for avatar processing. - Enhanced `create_webhook` command with optional avatar parameter. - Introduced `delete_webhook` and `edit_webhook` commands with permission checks. - Added `list_webhooks` command to display webhook details. - Implemented autocomplete for webhook parameters. - Updated `requirements.txt` to include `pillow` for image processing.
1 parent 6f8668e commit 2331612

File tree

4 files changed

+275
-20
lines changed

4 files changed

+275
-20
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WebhookCreator DiscordBot [![Discord Bot Invite](https://img.shields.io/badge/Invite-blue)](https://discord.com/oauth2/authorize?client_id=1251220473790861454)[![Discord Bots](https://top.gg/api/widget/servers/1251220473790861454.svg)](https://top.gg/bot/1251220473790861454)
22

3-
With this bot you can create application webhooks that can be used to send messages with buttons.
3+
With this bot you can create/edit/delete application webhooks that can be used to send messages with buttons.
44
Keep in mind that the webhooks get deleted, if you remove the bot from your server. (But it doesn't have to be online.)
55

66
You can use the bot to create webhooks for your server, or for other servers. (If you have the permission to create webhooks in that server.)

WebhookCreator/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev libc-dev l
2626
rm -rf /root/.cache/pip
2727

2828
LABEL maintainer="Discord: pika.pika.no.mi (970119359840284743)" \
29-
description="Discord bot for creating Webhooks." \
29+
description="Discord bot for managing Webhooks." \
3030
release=$BUILD_DATE \
31-
version="1.10.8" \
31+
version="1.11.0" \
3232
url="https://github.com/Serpensin/DiscordBots-WebhookCreator"
3333

3434
CMD ["python3", "main.py"]

WebhookCreator/main.py

Lines changed: 269 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import datetime
77
import discord
8+
import io
89
import json
910
import jsonschema
1011
import os
@@ -16,6 +17,7 @@
1617
from aiohttp import web
1718
from CustomModules import log_handler
1819
from dotenv import load_dotenv
20+
from PIL import Image
1921
from urllib.parse import urlparse
2022
from zipfile import ZIP_DEFLATED, ZipFile
2123

@@ -28,7 +30,7 @@
2830
if not os.path.exists(APP_FOLDER_NAME):
2931
os.makedirs(APP_FOLDER_NAME)
3032
ACTIVITY_FILE = os.path.join(APP_FOLDER_NAME, 'activity.json')
31-
BOT_VERSION = "1.10.8"
33+
BOT_VERSION = "1.11.0"
3234
TOKEN = os.getenv('TOKEN')
3335
OWNERID = os.getenv('OWNER_ID')
3436
SUPPORTID = 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
369403
class 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))
537570
async 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+
585618
if 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.')
600633
async 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.\nThis 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

WebhookCreator/requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ attrs
66
certifi
77
charset-normalizer
88
colorama
9-
discord.py
9+
discord.py>=2.0.0
1010
frozenlist
1111
idna
1212
jsonschema
1313
jsonschema-specifications
1414
multidict
15+
pillow
1516
propcache
1617
psutil
1718
pyrsistent
1819
python-dotenv
1920
referencing
2021
rpds-py
2122
sentry-sdk
23+
typing_extensions
2224
urllib3
2325
yarl

0 commit comments

Comments
 (0)