index — Deutsche-Haus-Bot @ 5d944a38981410cc7b4fb7aa4f8c8b9dc1899251

Discord bot to dynamically create voice chats for clubs that boosters can create

added deletion and restructured a good bit
split up especially voice state into more functions
added better error reporting
added better logging
crispy-caesus 114518720+crispy-caesus@users.noreply.github.com
Tue, 05 Nov 2024 23:08:20 +0100
commit

5d944a38981410cc7b4fb7aa4f8c8b9dc1899251

parent

37717140a8d7764245b33f73ab0b52389095fd46

5 files changed, 313 insertions(+), 136 deletions(-)

jump to
M bot.pybot.py

@@ -1,6 +1,8 @@

import discord import discord.ext.commands import asyncio +from varname import nameof +import time import bot_token import db as database

@@ -28,38 +30,121 @@ await logic.on_guild_join(guild.id)

print(f"LOG: guild {guild} joined") +# ====================== DB INIT ========================= # +async def init_db(ctx)-> database.Database: + return database.Database(f"{ctx.guild.id}.db") + + +# ======================== ERROR HANDLING ============================= # + +async def dm(ctx, message: str, err): + user = await bot.fetch_user(621759962280099840) + if user: + messageDraft = message + if err != None: + messageDraft += "\n```{err}```" + try: + await user.send(messageDraft) + await ctx.send("Der Entwickler wurde kontaktiert und wird sich sobald wie möglich darum kümmern") + except discord.Forbidden: + print(f"{ctx.guild.id} | ERROR: Can't DM user") + ctx.send("Bitte kontaktiere die Serverleitung") + else: + print(f"{ctx.guild.id} | ERROR: Can't find user to DM") + ctx.send("Bitte kontaktiere die Serverleitung") + + +async def error(ctx, message: str, err = None): + decoratedMessage = f"{ctx.guild.id} | ERROR: {message}\n{err}" + print(decoratedMessage) + await dm(ctx, decoratedMessage, err=err) + + ctx.send(":x: Interner Fehler") + + +async def log(ctx, message: str): + decoratedMessage = f"{ctx.guild.id} | LOG: {message}" + print(decoratedMessage) + + +async def is_admin(ctx): + if not ctx.author.guild_permissions.administrator: + ctx.respond(":x: Du musst Administrator sein, um diesen Command auszuführen") + return False + else: + return True + + + # ======================= SETUP ========================= # @bot.slash_command(description="Gibt alle Commands zurück, die für die Initialisation des Bots nötig sind") async def setup(ctx): - if not ctx.author.guild_permissions.administrator: - ctx.respond("Du musst Administrator sein, um diesen Command auszuführen") + if await is_admin(ctx) == False: return - await ctx.respond(await logic.setup(ctx.guild.id)) + + db = await init_db(ctx) + ids = await db.get_discord_ids() + + checks = [] + for id in ids: + if id[2] == None: + checks.append("❌") + else: + checks.append("✅") + + await ctx.respond( \ + f"{checks[0]} Setze die Booster Rolle mit `/{nameof(setze_booster_rolle)}`\n" \ + f"{checks[1]} Setze den Verteiler Channel mit `/{nameof(setze_verteiler_channel)}`\n" \ + f"{checks[2]} Setze die Kategorie, in der die Clubs erstellt werden sollen mit `/{nameof(setze_club_kategorie)}`\n" \ + f"{checks[3]} Setze die Rolle, welche als Clubrollenheader dient mit `/{nameof(setze_clubrollenheader_rolle)}`" \ + ) + return + @bot.slash_command(description="Setzt die Booster Rolle intern im Bot") async def setze_booster_rolle(ctx, booster_rolle): - if not ctx.author.guild_permissions.administrator: - ctx.respond("Du musst Administrator sein, um diesen Command auszuführen") + if await is_admin(ctx) == False: return + await ctx.respond(await logic.add_booster_role(ctx.guild.id, int(booster_rolle[3:-1]))) - print(f"LOG: {ctx.author} added {booster_rolle} as booster role") + await log(ctx, f"{ctx.author} added {booster_rolle} as booster role") + return + @bot.slash_command(description="Setzt den Verteiler Channel intern im Bot") async def setze_verteiler_channel(ctx, verteiler_channel_id): - if not ctx.author.guild_permissions.administrator: - ctx.respond("Du musst Administrator sein, um diesen Command auszuführen") + if await is_admin(ctx) == False: return + await ctx.respond(await logic.add_distributor_channel(ctx.guild.id, int(verteiler_channel_id))) - print(f"LOG: {ctx.author} added {verteiler_channel_id} as distributor channel id") + await log(ctx, f"{ctx.author} added {verteiler_channel_id} as distributor channel id") + return + @bot.slash_command(description="Setzt die Kategorie, in der die Clubs erstellt werden sollen") async def setze_club_kategorie(ctx, kategorie_id): - if not ctx.author.guild_permissions.administrator: - ctx.respond("Du musst Administrator sein, um diesen Command auszuführen") + if await is_admin(ctx) == False: return + await ctx.respond(await logic.add_club_category(ctx.guild.id, int(kategorie_id))) - print(f"LOG: {ctx.author} added {kategorie_id} as booster role") + await log(ctx, f"{ctx.author} added {kategorie_id} as booster role") + + +@bot.slash_command(description="Setzt die Rolle für den Clubrollen-Header") +async def setze_clubrollenheader_rolle(ctx, clubrollenheader_rolle: str): + if await is_admin(ctx) == False: + return + + clubrollenheader_rolle = clubrollenheader_rolle.strip() + + db = await init_db(ctx) + err = await db.add_id("club_role_header_role_id", int(clubrollenheader_rolle[3:-1])) + if err != None: + await error(ctx, "Database error when adding club role header role id", err) + return + + await ctx.respond(f"✅ Clubrollenheader {clubrollenheader_rolle} hinzugefügt")

@@ -93,7 +178,7 @@ return

else: await ctx.guild.create_role(name = response[0], color = response[1], mentionable = False) - print(f"LOG: club {kanalname} created by {ctx.author}") + await log(ctx, f"club {kanalname} created by {ctx.author}") role = await role_converter.convert(ctx, rollenname)

@@ -105,7 +190,7 @@

db = database.Database(f"{ctx.guild.id}.db") await db.add_member(ctx.author.id, ctx.author.id) - print(f"LOG: role {role} added to {ctx.author}") + await log(ctx, f"Role {role} added to {ctx.author}") # ==================== EDIT CLUBS ====================== #

@@ -117,7 +202,7 @@

# ========================= dsfsf ======================= # @bot.slash_command(description="Fügt Member zu eigenem Club hinzu") async def mitglied_hinzufuegen(ctx, member): - db = database.Database(f"{ctx.guild.id}.db") + db = await init_db(ctx) response = await db.add_member(member[2:-1], ctx.author.id) if response != None: await ctx.respond(response)

@@ -134,7 +219,7 @@

await member.add_roles(role) await ctx.respond(":white_check_mark:") - print(f"LOG: added {member} to {role}") + await log(ctx, f"Added {member} to {role}") @bot.command(description="Sends the bot's latency.") # this decorator makes a slash command

@@ -143,114 +228,190 @@ await ctx.respond(f"Pong! Latency is {bot.latency}")

# ==================== distributor vc ======================= # -distributor_vcs = [] +async def send_club_list(ctx, user, db_response): + clubs = "" + + for i in range(len(db_response)): + clubs += f"\n**{i+1}.** {db_response[i][0]}" # create a new numbered line for every club + + if clubs.strip() == "": + await ctx.send(f":x: {user.name}, du bist in keinen Clubs") # if no clubs + return + else: + await ctx.send(f" {user.name}, welchen Club-Kanal willst du öffnen?" + clubs) + +async def get_and_check_user_response(ctx, user, db_response): + def check(m): + return m.author == user and m.channel == ctx + + cycle = 1 + while cycle < 3: + + try: + response = await bot.wait_for('message', check=check, timeout=30.0) + except asyncio.TimeoutError: + await ctx.send(":x: Zu spät (joine dem Kanal noch einmal, um die Dialogauswahl wieder zu erhalten)") + return + else: + try: + int(response.content) + except ValueError: + await ctx.send(":x: Konnte nicht in ganze Zahl umwandeln") + continue + if int(response.content) > len(db_response) and int(response.content) <= 0: + await ctx.send(":x: Nicht zulässige Zahl") + else: + return response + + cycle += 1 + + ctx.send(":x: Zu viele Versuche, trete dem Channel erneut bei, um noch einmal auszuwählen") + return None + + +async def gather_arguments_for_channel_creation(ctx, db, db_response, userResponse): + channel_name = db_response[int(userResponse.content)-1][0] + + new_channel_category_id = await db.get_discord_id("new_channel_category_id") + category = discord.utils.get(ctx.guild.categories, id=new_channel_category_id) + if category is None: + await error(ctx, f"Distributor-Channel: Can't find new channel category ({new_channel_category_id}) on Discord") + + roleId = db_response[int(userResponse.content)-1][1] + role = discord.utils.get(ctx.guild.roles, id=roleId) + if role is None: + await error(ctx, f"Distributor-Channel: Can't find club role ({roleId}) on Discord") + + bot_member = ctx.guild.me + + clubOwnerId = await db.get_owner_by_club_id(db_response[int(userResponse.content)-1][2]) + club_owner = discord.utils.get(ctx.guild.members, id=clubOwnerId) + if club_owner is None: + await error(ctx, f"Distributor-Channel: Can't find club owner ({clubOwnerId}) on Discord") + + return (channel_name, category, role, bot_member, club_owner) + + +async def create_permission_overwrites(ctx, role, bot_member, club_owner): + bot_overwrites = discord.PermissionOverwrite ( + move_members = True, + view_channel = True, + manage_channels = True + ) -@bot.event -async def on_voice_state_update(user, before, after): - if before.channel != after.channel: + club_member_overwrites = discord.PermissionOverwrite ( + view_channel = True + ) - if after.channel: - db = database.Database(f"{after.channel.guild.id}.db") + club_owner_overwrites = discord.PermissionOverwrite ( + manage_channels = True, + mute_members = True, + deafen_members = True, + move_members = True + ) - + default_overwrites = discord.PermissionOverwrite ( + view_channel = False + ) - if after.channel.id == await db.get_discord_id("distributor_channel_id"): - print(f"LOG: {user.name} joined {after.channel.name}") - #print(f"LOG: channel members: {after.channel.members}") - - + overwrites = { + ctx.guild.default_role: default_overwrites, + bot_member: bot_overwrites, + club_owner: club_owner_overwrites, + role: club_member_overwrites + } + return overwrites - db_response = await db.get_channel_name_role_name_by_member(user.id) - message = "Welchen Club-Kanal willst du öffnen?" - for i in range(len(db_response)): - message += f"\n**{i+1}.** {db_response[i][0]}" - await after.channel.send(message) - - def check(m): - return m.author == user and m.channel == after.channel - +async def distributor_channel(user, after, db): + ctx = after.channel # ctx is distributor channel + await log(ctx, f"{user.name} joined distributor channel {ctx.name}") + db_response = await db.get_channel_name_role_name_by_member(user.id) + + await send_club_list(ctx, user, db_response) + userResponse = await get_and_check_user_response(ctx, user, db_response) + if userResponse == None: + return - done = False - while not done: + channel_name, category, role, bot_member, club_owner = await gather_arguments_for_channel_creation(ctx, db, db_response, userResponse) + overwrites = await create_permission_overwrites(ctx, role, bot_member, club_owner) - try: - response = await bot.wait_for('message', check=check, timeout=30.0) - except asyncio.TimeoutError: - await after.channel.send(":x: Zu spät (joine dem Kanal noch einmal, um die Dialogauswahl wieder zu erhalten)") - return - else: - try: - int(response.content) - except ValueError: - await after.channel.send(":x: Konnte nicht in ganze Zahl umwandeln") - continue - if int(response.content) <= len(db_response) and int(response.content) > 0: - channel_name = db_response[int(response.content)-1][0] + voice_channel = await category.create_voice_channel( + name = channel_name, + overwrites=overwrites + ) + await log(ctx, f"New channel {voice_channel} created") + distributor_vcs.append(voice_channel.id) + await log(ctx, f"The List of existing club channels is now: {distributor_vcs}") + + if user.voice: + await user.move_to(voice_channel) + await log(ctx, f"Moved {user} into {voice_channel}") - category = discord.utils.get(after.channel.guild.categories, id=await db.get_discord_id("new_channel_category_id")) - if category is None: - print("ERROR: Can't find set new_channel_category on server") - role = discord.utils.get(after.channel.guild.roles, id=db_response[int(response.content)-1][1]) - if role is None: - print("ERROR: Can't find club role") +distributor_vcs = [] - bot_member = after.channel.guild.me - club_owner = discord.utils.get(after.channel.guild.members, id=await db.get_owner_by_club_id(db_response[int(response.content)-1][2])) +@bot.event +async def on_voice_state_update(user, before, after): + if before.channel != after.channel: # actually moved channels in some way + if after.channel: # moved and didn't leave + db = await init_db(after.channel) + if after.channel.id == await db.get_discord_id("distributor_channel_id"): + await distributor_channel(user, after, db) - bot_overwrites = discord.PermissionOverwrite ( - move_members = True, - view_channel = True, - manage_channels = True - ) - club_member_overwrites = discord.PermissionOverwrite ( - view_channel = True - ) - club_owner_overwrites = discord.PermissionOverwrite ( - manage_channels = True, - mute_members = True, - deafen_members = True, - move_members = True - ) + if before.channel and before.channel.id in distributor_vcs: # if old channel exists and is a distributor vc + if len(before.channel.members) == 0: + await before.channel.delete(reason="Niemand ist mehr verbunden") + distributor_vcs.remove(before.channel.id) + await log(before.channel, f"Deleted {before.channel} because it was empty") - default_overwrites = discord.PermissionOverwrite ( - view_channel = False - ) + +@bot.slash_command(description="Löscht eigenen Club") +async def club_löschen(ctx, owner = None): + db = database.Database(f"{ctx.guild.id}.db") + if owner == None: + owner = ctx.author.id + else: + owner = owner[2:-1] + + club_role_id = await db.select_role_id_by_owner(owner) - overwrites = { - after.channel.guild.default_role: default_overwrites, - bot_member: bot_overwrites, - club_owner: club_owner_overwrites, - role: club_member_overwrites - } + if club_role_id == None: + await ctx.respond(":x: Der angegebene Benutzer besitzt keinen Club") + return + + err = await db.delete_club(owner) + if err != None: + await error(ctx, "Club-Deletion: Club from {owner} could not be deleted from the db", err) + return + else: + print(f"LOG: club from {owner} has been deleted from the db") + role = discord.utils.get(ctx.guild.roles, id=club_role_id) + + # Check if the role exists + if role == None: + await error(ctx, f"Club-Deletion: Role {club_role_id} could not be found") + return + + # Attempt to delete the role + try: + await role.delete() + await ctx.respond(f":white_check_mark: Rolle {role.name} und Club von <@{owner}> wurden gelöscht") + print(f"LOG: role '{role.name}' has been deleted by {ctx.author}") + except discord.Forbidden: + await error(ctx, f"Club-Deletion: No permission to delete role ({club_role_id})") + return + except discord.HTTPException as e: + await error(ctx, "Club-Deletion: Error when deleting role", e) + return - voice_channel = await category.create_voice_channel( - name = channel_name, - overwrites=overwrites - ) - print(f"LOG: new channel {voice_channel} created") - distributor_vcs.append(voice_channel.id) - print(f"LOG: The List of existing club channels is now: {distributor_vcs}") - done = True - else: - await after.channel.send(":x: Nicht zulässige Zahl") - if user.voice: - await user.move_to(voice_channel) - print(f"LOG: moved {user} into {voice_channel}") - - if before.channel and before.channel.id in distributor_vcs: - if len(before.channel.members) == 0: - await before.channel.delete(reason="Niemand ist mehr verbunden") - distributor_vcs.remove(before.channel.id) - print(f"LOG: deleted {before.channel} because it was empty") bot.run(bot_token.token)
M db.pydb.py

@@ -34,8 +34,15 @@ await db.commit()

await db.execute("""CREATE TABLE IF NOT EXISTS ids ( id INTEGER PRIMARY KEY, id_type TEXT UNIQUE NOT NULL, - discord_id INTEGER NOT NULL);""") + discord_id INTEGER);""") await db.commit() + + async def create_id_rows(self): + id_types = ["booster_role_id", "distributor_channel_id", "new_channel_category_id", "club_role_header_role_id"] + for id_type in id_types: + async with aiosqlite.connect(self.db_name) as db: + await db.execute("""INSERT INTO ids (id_type) VALUES(?);""", (id_type,)) + await db.commit() # ========================= SETUP ============================ #

@@ -48,14 +55,24 @@

async def add_id(self, id_type: str, discord_id: int): async with aiosqlite.connect(self.db_name) as db: try: - await db.execute("""INSERT INTO ids (id_type, discord_id) - VALUES(?,?);""", (id_type, discord_id)) + await db.execute("""UPDATE ids + SET discord_id = ? + WHERE id_type = ?;""", (discord_id, id_type)) await db.commit() except Error as e: print(e) - return(":x: Error!") + return(e) return(None) + async def get_discord_ids(self): + async with aiosqlite.connect(self.db_name) as db: + async with db.execute("SELECT * FROM ids;") as cursor: + result = [] + async for row in cursor: + result.append(row) + return result + + # =========================== CREATE CLUB =========================== # async def get_booster_role_id(self):

@@ -87,7 +104,7 @@ return row

async def create_club(self, channel_name: str, owner: int, role_id: int, role_name: str): - print(f"DB: create_club received:\n channel_name: {channel_name}\n owner: {owner}\n role_id: {role_id}") + #print(f"DB: create_club received:\n channel_name: {channel_name}\n owner: {owner}\n role_id: {role_id}") args = (channel_name, owner, role_id, role_name) sql = """INSERT INTO clubs (channel_name,owner_id,role_id,role_name) VALUES(?,?,?,?);"""

@@ -103,7 +120,7 @@ # ============================ EDIT CLUB ========================== #

async def club_edit(self, owner_id: int, column, value): - print(f"DB: updating club:\n owner_id: {owner_id}\n column: {column}\n value: {value}") + #print(f"DB: updating club:\n owner_id: {owner_id}\n column: {column}\n value: {value}") async with aiosqlite.connect(self.db_name) as db: try: await db.execute(f"UPDATE clubs SET {column} = ? WHERE owner_id = ?;", (value, owner_id))

@@ -116,7 +133,7 @@

# ============================ add member ================================== # async def add_member(self, member: int, owner: int): - print(f"DB: add_member received:\n member: {member}\n owner: {owner}") + #print(f"DB: add_member received:\n member: {member}\n owner: {owner}") sql = """INSERT INTO members (user_id, club_id) VALUES(?,?);""" try:

@@ -190,3 +207,7 @@ result = await cursor.fetchone()

return (result[0]) + async def delete_club(self, owner_id): + async with aiosqlite.connect(self.db_name) as db: + await db.execute("DELETE FROM clubs WHERE owner_id = ?", (owner_id,)) + await db.commit()
M logic.pylogic.py

@@ -10,30 +10,6 @@ await db.create_tables()

# ========================= SETUP ========================== # -async def setup(guild_id): - db = database.Database(f"{guild_id}.db") - booster_role_id = await db.get_discord_id("booster_role_id") - if booster_role_id == None: - booster_check = "❌" - else: - booster_check = "✅" - distributor_channel_id = await db.get_discord_id("distributor_channel_id") - if distributor_channel_id == None: - distributor_check = "❌" - else: - distributor_check = "✅" - new_channel_category_id = await db.get_discord_id("new_channel_category_id") - if new_channel_category_id == None: - category_check = "❌" - else: - category_check = "✅" - - return( \ - f"{booster_check} Setze die Booster Rolle mit `/setze_booster_rolle`\n" \ - f"{distributor_check} Setze den Verteiler Channel mit `/setze_verteiler_channel`\n" \ - f"{category_check} Setze die Kategorie, in der die Clubs erstellt werden sollen mit `/setze_club_kategorie`" \ - ) - async def add_booster_role(guild_id: int, booster_role_id: int)-> str: db = database.Database(f"{guild_id}.db") response = await db.add_id("booster_role_id", booster_role_id)
M requirements.txtrequirements.txt

@@ -1,3 +1,3 @@

-aiosqlite==0.20.0 -discord.py==2.4.0 -emoji==2.13.1 +aiosqlite>=0.20.0 +discord.py>=2.4.0 +emoji>=2.13.1
A test.py

@@ -0,0 +1,19 @@

+import db as database +import asyncio + +async def error_test(hi="", hey=""): + print(hi, hey) + + + +async def db_reset(): + db = database.Database("1170646611236487208.db") + await db.create_tables() + await db.create_id_rows() + + +async def main(): + await error_test("hallo", "was geht") + + +asyncio.run(main())