diff --git a/bot.py b/bot.py index e2405a4..9fbb10a 100644 --- a/bot.py +++ b/bot.py @@ -22,6 +22,7 @@ import psutil import config import strings as S from core import economy, pb_client, sheets +from core.admin import is_bot_admin from core.member_sync import SyncResult from commands.dev_member_commands import register_dev_member_commands from commands.dev_member_runtime import handle_member_join, run_birthday_daily @@ -593,8 +594,8 @@ class HelpSelect(discord.ui.Select): @tree.command(name="help", description=S.CMD["help"]) async def cmd_help(interaction: discord.Interaction): - perms = interaction.user.guild_permissions if interaction.guild else None - is_admin = bool(perms and (perms.manage_roles or perms.manage_guild)) + member = interaction.user + is_admin = isinstance(member, discord.Member) and is_bot_admin(member) await interaction.response.send_message( embed=_help_embed("üldine"), view=HelpView(is_admin), ephemeral=True ) diff --git a/commands/dev_member_commands.py b/commands/dev_member_commands.py index dc13dbe..4cedbd7 100644 --- a/commands/dev_member_commands.py +++ b/commands/dev_member_commands.py @@ -8,6 +8,7 @@ import discord from discord import app_commands from core import sheets +from core.admin import bot_admin_check import strings as S from core.member_sync import announce_birthday, sync_member, today_local @@ -167,7 +168,7 @@ def register_dev_member_commands( @tree.command(name="check", description=S.CMD["check"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_roles=True) + @bot_admin_check() async def cmd_check(interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) @@ -281,7 +282,7 @@ def register_dev_member_commands( @tree.command(name="member", description=S.CMD["member"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_roles=True) + @bot_admin_check() async def cmd_member(interaction: discord.Interaction, user: discord.Member): row = sheets.find_member(user.id, user.name) if row is None: diff --git a/commands/economy_admin_commands.py b/commands/economy_admin_commands.py index 42d1b09..ad5f1de 100644 --- a/commands/economy_admin_commands.py +++ b/commands/economy_admin_commands.py @@ -8,6 +8,7 @@ import discord from discord import app_commands from core import economy +from core.admin import bot_admin_check import strings as S @@ -29,7 +30,7 @@ def register_economy_admin_commands( ) -> None: @tree.command(name="adminseason", description=S.CMD["adminseason"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() @app_commands.describe(top_n=S.OPT["adminseason_top_n"]) async def cmd_adminseason(interaction: discord.Interaction, top_n: int = 10): await interaction.response.defer(ephemeral=True) @@ -73,7 +74,7 @@ def register_economy_admin_commands( kogus=S.OPT["admincoins_kogus"], põhjus=S.OPT["admin_põhjus"], ) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_admincoins(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str): if kogus == 0: await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True) @@ -112,7 +113,7 @@ def register_economy_admin_commands( minutid=S.OPT["adminjail_minutid"], põhjus=S.OPT["admin_põhjus"], ) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminjail(interaction: discord.Interaction, kasutaja: discord.Member, minutid: int, põhjus: str): if minutid <= 0: await interaction.response.send_message(S.ERR["positive_duration"], ephemeral=True) @@ -133,7 +134,7 @@ def register_economy_admin_commands( @tree.command(name="adminunjail", description=S.CMD["adminunjail"]) @app_commands.guild_only() @app_commands.describe(kasutaja=S.OPT["admin_kasutaja"]) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminunjail(interaction: discord.Interaction, kasutaja: discord.Member): await economy.do_admin_unjail(kasutaja.id, interaction.user.id) await interaction.response.send_message( @@ -148,7 +149,7 @@ def register_economy_admin_commands( kasutaja=S.OPT["admin_kasutaja"], põhjus=S.OPT["admin_põhjus"], ) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminban(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str): if bot.user and kasutaja.id == bot.user.id: await interaction.response.send_message(S.ERR["admin_ban_bot"], ephemeral=True) @@ -164,7 +165,7 @@ def register_economy_admin_commands( @tree.command(name="adminunban", description=S.CMD["adminunban"]) @app_commands.guild_only() @app_commands.describe(kasutaja=S.OPT["admin_kasutaja"]) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminunban(interaction: discord.Interaction, kasutaja: discord.Member): await economy.do_admin_unban(kasutaja.id, interaction.user.id) await interaction.response.send_message( @@ -179,7 +180,7 @@ def register_economy_admin_commands( kasutaja=S.OPT["admin_kasutaja"], põhjus=S.OPT["admin_põhjus"], ) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminreset(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str): if bot.user and kasutaja.id == bot.user.id: await interaction.response.send_message(S.ERR["admin_reset_bot"], ephemeral=True) @@ -195,7 +196,7 @@ def register_economy_admin_commands( @tree.command(name="adminview", description=S.CMD["adminview"]) @app_commands.guild_only() @app_commands.describe(kasutaja=S.OPT["admin_kasutaja"]) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminview(interaction: discord.Interaction, kasutaja: discord.Member): res = await economy.do_admin_inspect(kasutaja.id) data = res["data"] @@ -238,7 +239,7 @@ def register_economy_admin_commands( kogus=S.OPT["adminexp_kogus"], põhjus=S.OPT["admin_põhjus"], ) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminexp(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str): if kogus == 0: await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True) @@ -281,7 +282,7 @@ def register_economy_admin_commands( ese=S.OPT["adminitem_ese"], tegevus=S.OPT["adminitem_tegevus"], ) - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_adminitem(interaction: discord.Interaction, kasutaja: discord.Member, ese: str, tegevus: str): action = tegevus.strip().lower() if action not in ("anna", "eemalda"): diff --git a/commands/ops_admin_commands.py b/commands/ops_admin_commands.py index 41d8ec7..d80ad67 100644 --- a/commands/ops_admin_commands.py +++ b/commands/ops_admin_commands.py @@ -14,6 +14,7 @@ import discord from discord import app_commands import strings as S +from core.admin import bot_admin_check def register_ops_admin_commands( @@ -32,7 +33,7 @@ def register_ops_admin_commands( ) -> None: @tree.command(name="status", description=S.CMD["status"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_status(interaction: discord.Interaction): mem = process.memory_info() cpu = process.cpu_percent(interval=0.1) @@ -95,7 +96,7 @@ def register_ops_admin_commands( @tree.command(name="sync", description=S.CMD["sync"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_sync(interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) tree.copy_global_to(guild=guild_obj) @@ -107,7 +108,7 @@ def register_ops_admin_commands( @tree.command(name="restart", description=S.CMD["restart"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_restart(interaction: discord.Interaction): restart_file.write_text(json.dumps({"channel_id": interaction.channel_id}), encoding="utf-8") await interaction.response.send_message(S.MSG_RESTARTING, ephemeral=True) @@ -117,7 +118,7 @@ def register_ops_admin_commands( @tree.command(name="shutdown", description=S.CMD["shutdown"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_shutdown(interaction: discord.Interaction): await interaction.response.send_message(S.MSG_SHUTTING_DOWN, ephemeral=True) log.info("/shutdown triggered by %s", interaction.user) @@ -125,7 +126,7 @@ def register_ops_admin_commands( @tree.command(name="pause", description=S.CMD["pause"]) @app_commands.guild_only() - @app_commands.default_permissions(manage_guild=True) + @bot_admin_check() async def cmd_pause(interaction: discord.Interaction): paused = not get_paused() set_paused(paused) diff --git a/config.py b/config.py index b3d04bb..39e3c80 100644 --- a/config.py +++ b/config.py @@ -58,3 +58,26 @@ PB_ECONOMY_COLLECTION_ECONOMY = ( PB_ECONOMY_COLLECTION = ( PB_ECONOMY_COLLECTION_ECONOMY if BOT_PROFILE == "economy" else PB_ECONOMY_COLLECTION_DEV ) + + +def _parse_admin_roles() -> dict[int, int]: + """Parse BOT_ADMIN_ROLES env var (format: guild_id:role_id,guild_id:role_id).""" + raw = os.getenv("BOT_ADMIN_ROLES", "").strip() + if not raw: + return {} + result: dict[int, int] = {} + for pair in raw.split(","): + pair = pair.strip() + if not pair: + continue + parts = pair.split(":") + if len(parts) != 2: + continue + try: + result[int(parts[0].strip())] = int(parts[1].strip()) + except ValueError: + continue + return result + + +BOT_ADMIN_ROLES: dict[int, int] = _parse_admin_roles() diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..e48886c --- /dev/null +++ b/core/admin.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import discord +from discord import app_commands + +import config + + +def is_bot_admin(member: discord.Member) -> bool: + """Return True if the member has the configured bot-admin role for their guild.""" + role_id = config.BOT_ADMIN_ROLES.get(member.guild.id) + if role_id is None: + return False + return any(r.id == role_id for r in member.roles) + + +def bot_admin_check(): + """Slash-command check decorator: raises MissingPermissions if not a bot admin.""" + def predicate(interaction: discord.Interaction) -> bool: + member = interaction.user + if not isinstance(member, discord.Member): + raise app_commands.MissingPermissions(["bot_admin"]) + if not is_bot_admin(member): + raise app_commands.MissingPermissions(["bot_admin"]) + return True + + return app_commands.check(predicate)