diff --git a/src/babel/cog.py b/src/babel/cog.py index 9a22151f..e25e945c 100644 --- a/src/babel/cog.py +++ b/src/babel/cog.py @@ -13,7 +13,7 @@ from discord.ui.button import ButtonStyle from meta import LionBot, LionCog, LionContext from meta.errors import UserInputError from utils.ui import AButton, AsComponents -from wards import low_management +from wards import low_management_ward from .translator import ctx_locale, ctx_translator, SOURCE_LOCALE from . import babel @@ -115,7 +115,8 @@ class BabelCog(LionCog): force_language=LocaleSettings.ForceLocale._display_name ) @appcmds.guild_only() # Can be removed when attached as a subcommand - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def cmd_configure_language(self, ctx: LionContext, language: Optional[str] = None, force_language: Optional[appcmds.Choice[int]] = None): diff --git a/src/core/config.py b/src/core/config.py index e0bcfd9a..2f14fc37 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -28,6 +28,7 @@ class ConfigCog(LionCog): name=_p('group:configure', "configure"), ) @appcmds.guild_only + @appcmds.default_permissions(manage_guild=True) async def configure_group(self, ctx: LionContext): """ Bare command group, has no function. diff --git a/src/modules/config/cog.py b/src/modules/config/cog.py index 22f34bb3..56926d4a 100644 --- a/src/modules/config/cog.py +++ b/src/modules/config/cog.py @@ -25,6 +25,7 @@ class DashCog(LionCog): description="At-a-glance view of the server's configuration." ) @appcmds.guild_only + @appcmds.default_permissions(manage_guild=True) async def dashboard_cmd(self, ctx: LionContext): ui = GuildDashboard(self.bot, ctx.guild, ctx.author.id, ctx.channel.id) await ui.run(ctx.interaction) diff --git a/src/modules/config/general.py b/src/modules/config/general.py index 3073a95b..4b57c844 100644 --- a/src/modules/config/general.py +++ b/src/modules/config/general.py @@ -13,7 +13,7 @@ from discord import app_commands as appcmds from meta import LionBot, LionCog, LionContext, ctx_bot from meta.errors import UserInputError -from wards import low_management +from wards import low_management_ward from settings import ModelData from settings.setting_types import TimezoneSetting from settings.groups import SettingGroup @@ -101,7 +101,8 @@ class GeneralSettingsCog(LionCog): timezone=GeneralSettings.Timezone._desc ) @appcmds.guild_only() - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def cmd_configure_general(self, ctx: LionContext, timezone: Optional[str] = None): t = self.bot.translator.t diff --git a/src/modules/economy/cog.py b/src/modules/economy/cog.py index ccec2031..082a1bd1 100644 --- a/src/modules/economy/cog.py +++ b/src/modules/economy/cog.py @@ -11,7 +11,7 @@ from data import ORDER from utils.ui import Confirm, Pager from utils.lib import error_embed, MessageArgs, utc_now -from wards import low_management +from wards import low_management_ward from constants import MAX_COINS from . import babel, logger @@ -801,7 +801,8 @@ class Economy(LionCog): appcmds.Choice(name=EconomySettings.AllowTransfers._outputs[False], value=0), ] ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_economy(self, ctx: LionContext, allow_transfers: Optional[appcmds.Choice[int]] = None, coins_per_xp: Optional[appcmds.Range[int, 0, 2**15]] = None): diff --git a/src/modules/meta/cog.py b/src/modules/meta/cog.py index c31e3535..0a7932b2 100644 --- a/src/modules/meta/cog.py +++ b/src/modules/meta/cog.py @@ -31,6 +31,6 @@ class MetaCog(LionCog): ctx.bot, ctx.author, ctx.guild, - show_admin=await low_management(ctx), + show_admin=await low_management(ctx.bot, ctx.author), ) await ui.run(ctx.interaction) diff --git a/src/modules/pomodoro/cog.py b/src/modules/pomodoro/cog.py index 57cc5fd7..387ee853 100644 --- a/src/modules/pomodoro/cog.py +++ b/src/modules/pomodoro/cog.py @@ -11,7 +11,7 @@ from meta.logger import log_wrap from meta.sharding import THIS_SHARD from utils.lib import utc_now -from wards import low_management +from wards import low_management_ward from . import babel, logger from .data import TimerData @@ -807,7 +807,8 @@ class TimerCog(LionCog): @appcmds.describe( pomodoro_channel=TimerSettings.PomodoroChannel._desc ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_pomodoro_command(self, ctx: LionContext, pomodoro_channel: Optional[discord.VoiceChannel | discord.TextChannel] = None): t = self.bot.translator.t diff --git a/src/modules/ranks/cog.py b/src/modules/ranks/cog.py index a91e9cea..6cffbe99 100644 --- a/src/modules/ranks/cog.py +++ b/src/modules/ranks/cog.py @@ -9,7 +9,7 @@ from discord.app_commands.transformers import AppCommandOptionType from cachetools import LRUCache from meta import LionBot, LionContext, LionCog -from wards import high_management +from wards import high_management_ward from core.data import RankType from utils.ui import ChoicedEnum, Transformed from utils.lib import utc_now, replace_multiple @@ -435,7 +435,8 @@ class RankCog(LionCog): # ---------- Commands ---------- @cmds.hybrid_command(name=_p('cmd:ranks', "ranks")) - @cmds.check(high_management) + @appcmds.default_permissions(administrator=True) + @high_management_ward async def ranks_cmd(self, ctx: LionContext): """ Command to access the Rank Overview UI. @@ -472,7 +473,8 @@ class RankCog(LionCog): dm_ranks=RankSettings.DMRanks._desc, rank_channel=RankSettings.RankChannel._desc, ) - @cmds.check(high_management) + @appcmds.default_permissions(administrator=True) + @high_management_ward async def configure_ranks_cmd(self, ctx: LionContext, rank_type: Optional[Transformed[RankTypeChoice, AppCommandOptionType.string]] = None, dm_ranks: Optional[bool] = None, diff --git a/src/modules/ranks/ui/config.py b/src/modules/ranks/ui/config.py index 2d7a554a..06c8a8ed 100644 --- a/src/modules/ranks/ui/config.py +++ b/src/modules/ranks/ui/config.py @@ -5,11 +5,11 @@ from discord.ui.select import select, ChannelSelect, Select, SelectOption from discord.ui.button import button, Button, ButtonStyle from meta import LionBot -from wards import i_high_management +from wards import high_management_iward from core.data import RankType from utils.ui import ConfigUI, DashboardSection -from utils.lib import MessageArgs +from utils.lib import MessageArgs, error_embed from ..settings import RankSettings from .. import babel, logger @@ -31,7 +31,20 @@ class RankConfigUI(ConfigUI): super().__init__(bot, guildid, channelid, **kwargs) async def interaction_check(self, interaction: discord.Interaction) -> bool: - return await i_high_management(interaction) + passed = await high_management_iward(interaction) + if passed: + return True + else: + await interaction.response.send_message( + embed=error_embed( + self.bot.translator.t(_p( + 'ui:rankconfigui|check|not_permitted', + "You have insufficient server permissions to use this UI!" + )) + ), + ephemeral=True + ) + return False # ----- UI Components ----- diff --git a/src/modules/rooms/cog.py b/src/modules/rooms/cog.py index 96c45fc7..f727f7b9 100644 --- a/src/modules/rooms/cog.py +++ b/src/modules/rooms/cog.py @@ -16,7 +16,7 @@ from utils.ui import Confirm from constants import MAX_COINS from core.data import CoreData -from wards import low_management +from wards import low_management_ward from . import babel, logger from .data import RoomData @@ -888,7 +888,8 @@ class RoomCog(LionCog): @appcmds.describe( **{setting.setting_id: setting._desc for setting in RoomSettings.model_settings} ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_rooms_cmd(self, ctx: LionContext, rooms_category: Optional[discord.CategoryChannel] = None, rooms_price: Optional[Range[int, 0, MAX_COINS]] = None, diff --git a/src/modules/shop/cog.py b/src/modules/shop/cog.py index 290bbdf2..644d9297 100644 --- a/src/modules/shop/cog.py +++ b/src/modules/shop/cog.py @@ -72,6 +72,7 @@ from discord import app_commands as appcmds from meta import LionBot, LionCog, LionContext from utils import ui from utils.lib import error_embed +from wards import low_management_ward from . import babel @@ -106,6 +107,9 @@ class Shopping(LionCog): @cmds.hybrid_group( name=_p('group:editshop', 'editshop') ) + @appcmds.guild_only + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def editshop_group(self, ctx: LionContext): return diff --git a/src/modules/statistics/cog.py b/src/modules/statistics/cog.py index 11fe46bb..4225d597 100644 --- a/src/modules/statistics/cog.py +++ b/src/modules/statistics/cog.py @@ -9,7 +9,7 @@ from discord.ui.button import ButtonStyle from meta import LionBot, LionCog, LionContext from utils.lib import error_embed from utils.ui import LeoUI, AButton -from wards import low_management +from wards import low_management_ward from . import babel from .data import StatsData @@ -97,7 +97,8 @@ class StatsCog(LionCog): "Time from which to start counting activity for rank badges and season leadeboards." ) ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_statistics_cmd(self, ctx: LionContext, season_start: Optional[str] = None): t = self.bot.translator.t diff --git a/src/modules/sysadmin/blacklists.py b/src/modules/sysadmin/blacklists.py index 72d0a06b..095dd6c2 100644 --- a/src/modules/sysadmin/blacklists.py +++ b/src/modules/sysadmin/blacklists.py @@ -20,7 +20,7 @@ from meta.app import shard_talk from utils.ui import ChoicedEnum, Transformed, FastModal, LeoUI, error_handler_for, ModalRetryUI from utils.lib import EmbedField, tabulate, MessageArgs, parse_ids, error_embed -from wards import sys_admin +from wards import sys_admin_ward logger = logging.getLogger(__name__) @@ -168,7 +168,7 @@ class Blacklists(LionCog): name="blacklist", description="Display and modify the user and guild blacklists." ) - @cmds.check(sys_admin) + @sys_admin_ward async def blacklist_cmd( self, ctx: LionContext, diff --git a/src/modules/sysadmin/dash.py b/src/modules/sysadmin/dash.py index 2a672f6c..85fea47b 100644 --- a/src/modules/sysadmin/dash.py +++ b/src/modules/sysadmin/dash.py @@ -7,7 +7,7 @@ import discord.ext.commands as cmds from meta import LionBot, LionCog, LionContext from meta.app import appname -from wards import sys_admin +from wards import sys_admin_ward from settings.groups import SettingGroup @@ -23,7 +23,7 @@ class LeoSettings(LionCog): @cmds.hybrid_group( name="leo" ) - @cmds.check(sys_admin) + @sys_admin_ward async def leo_group(self, ctx: LionContext): """ Base command group for global leo-only functions. @@ -35,7 +35,7 @@ class LeoSettings(LionCog): name='dashboard', description="Global setting dashboard" ) - @cmds.check(sys_admin) + @sys_admin_ward async def dash_cmd(self, ctx: LionContext): embed = discord.Embed( title="System Admin Dashboard", @@ -56,7 +56,7 @@ class LeoSettings(LionCog): name='configure', description="Leo Configuration Group" ) - @cmds.check(sys_admin) + @sys_admin_ward async def leo_configure_group(self, ctx: LionContext): """ Base command group for global configuration of Leo. diff --git a/src/modules/sysadmin/exec_cog.py b/src/modules/sysadmin/exec_cog.py index d768d4f5..3c7c6df5 100644 --- a/src/modules/sysadmin/exec_cog.py +++ b/src/modules/sysadmin/exec_cog.py @@ -249,7 +249,7 @@ class Exec(LionCog): self.talk_async = shard_talk.register_route('exec')(_async) async def cog_check(self, ctx: LionContext) -> bool: # type: ignore - return await sys_admin(ctx) + return await sys_admin(ctx.bot, ctx.author.id) @commands.hybrid_command( name=_('async'), diff --git a/src/modules/sysadmin/presence.py b/src/modules/sysadmin/presence.py index 70ddf17c..ef742503 100644 --- a/src/modules/sysadmin/presence.py +++ b/src/modules/sysadmin/presence.py @@ -21,7 +21,7 @@ from settings.data import ModelData from settings.setting_types import EnumSetting, StringSetting from settings.groups import SettingGroup -from wards import sys_admin +from wards import sys_admin_ward from . import babel @@ -376,7 +376,7 @@ class PresenceCtrl(LionCog): name="presence", description="Globally set the bot status and activity." ) - @cmds.check(sys_admin) + @sys_admin_ward @appcmds.describe( status="Online status (online | idle | dnd | offline)", type="Activity type (watching | listening | playing | streaming)", diff --git a/src/modules/tasklist/cog.py b/src/modules/tasklist/cog.py index 937720f9..785477d5 100644 --- a/src/modules/tasklist/cog.py +++ b/src/modules/tasklist/cog.py @@ -12,7 +12,7 @@ from utils.lib import utc_now, error_embed from utils.ui import ChoicedEnum, Transformed from data import Condition, NULL -from wards import low_management +from wards import low_management_ward from . import babel, logger from .data import TasklistData @@ -712,7 +712,8 @@ class TasklistCog(LionCog): reward=TasklistSettings.task_reward._desc, reward_limit=TasklistSettings.task_reward_limit._desc ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_tasklist_cmd(self, ctx: LionContext, reward: Optional[int] = None, reward_limit: Optional[int] = None): diff --git a/src/tracking/text/cog.py b/src/tracking/text/cog.py index 15c186b0..01876c84 100644 --- a/src/tracking/text/cog.py +++ b/src/tracking/text/cog.py @@ -15,7 +15,7 @@ from meta.sharding import THIS_SHARD from meta.app import appname from utils.lib import utc_now, error_embed -from wards import low_management +from wards import low_management_ward from . import babel, logger from .data import TextTrackerData @@ -260,7 +260,8 @@ class TextTrackerCog(LionCog): xp_per_period=TextTrackerSettings.XPPerPeriod._desc, word_xp=TextTrackerSettings.WordXP._desc, ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_text_tracking_cmd(self, ctx: LionContext, xp_per_period: Optional[appcmds.Range[int, 0, 2**15]] = None, word_xp: Optional[appcmds.Range[int, 0, 2**15]] = None): diff --git a/src/tracking/voice/cog.py b/src/tracking/voice/cog.py index 68fa47ea..1b5f665e 100644 --- a/src/tracking/voice/cog.py +++ b/src/tracking/voice/cog.py @@ -14,7 +14,7 @@ from meta.sharding import THIS_SHARD from utils.lib import utc_now, error_embed from core.lion_guild import VoiceMode -from wards import low_management +from wards import low_management_ward from . import babel, logger from .data import VoiceTrackerData @@ -625,7 +625,8 @@ class VoiceTrackerCog(LionCog): hourly_live_bonus=VoiceTrackerSettings.HourlyLiveBonus._desc, daily_voice_cap=VoiceTrackerSettings.DailyVoiceCap._desc, ) - @cmds.check(low_management) + @appcmds.default_permissions(manage_guild=True) + @low_management_ward async def configure_voice_tracking_cmd(self, ctx: LionContext, hourly_reward: Optional[int] = None, # TODO: Change these to Ranges hourly_live_bonus: Optional[int] = None, diff --git a/src/utils/ui/config.py b/src/utils/ui/config.py index 3a2823f4..99e91bd5 100644 --- a/src/utils/ui/config.py +++ b/src/utils/ui/config.py @@ -6,7 +6,8 @@ from discord.ui.button import button, Button, ButtonStyle from meta import conf, LionBot from meta.errors import UserInputError -from wards import i_low_management +from utils.lib import error_embed +from wards import low_management_iward from babel.translator import ctx_translator, LazyStr from ..lib import tabulate @@ -55,7 +56,20 @@ class ConfigUI(LeoUI): """ Default requirement for a Config UI is low management (i.e. manage_guild permissions). """ - return await i_low_management(interaction) + passed = await low_management_iward(interaction) + if passed: + return True + else: + await interaction.response.send_message( + embed=error_embed( + self.bot.translator.t(_p( + 'ui:configui|check|not_permitted', + "You have insufficient server permissions to use this UI!" + )) + ), + ephemeral=True + ) + return False async def cleanup(self): self._listening.pop(self.channelid, None) diff --git a/src/wards.py b/src/wards.py index 0c1229e4..ec76b7f3 100644 --- a/src/wards.py +++ b/src/wards.py @@ -1,47 +1,98 @@ +from typing import Optional import discord +from discord.ext.commands.errors import CheckFailure +import discord.ext.commands as cmds -from meta.LionContext import LionContext -from meta import conf +from babel.translator import LocalBabel + +from meta import conf, LionContext, LionBot + +babel = LocalBabel('wards') +_p = babel._p -# Interaction Wards - -async def i_sys_admin(interaction: discord.Interaction) -> bool: +# Raw checks, return True/False depending on whether they pass +async def sys_admin(bot: LionBot, userid: int): """ Checks whether the context author is listed in the configuration file as a bot admin. """ - admins = conf.bot.getintlist('admins') - return interaction.user.id in admins + admins = bot.config.bot.getintlist('admins') + return userid in admins -async def i_high_management(interaction: discord.Interaction) -> bool: - if await i_sys_admin(interaction): +async def high_management(bot: LionBot, member: discord.Member): + if await sys_admin(bot, member.id): return True + return member.guild_permissions.administrator + + +async def low_management(bot: LionBot, member: discord.Member): + if await high_management(bot, member): + return True + return member.guild_permissions.manage_guild + + +# Interaction Wards, also return True/False + +async def sys_admin_iward(interaction: discord.Interaction) -> bool: + return await sys_admin(interaction.client, interaction.user.id) + + +async def high_management_iward(interaction: discord.Interaction) -> bool: if not interaction.guild: return False - return interaction.user.guild_permissions.administrator + return await high_management(interaction.client, interaction.user) -async def i_low_management(interaction: discord.Interaction) -> bool: - if await i_high_management(interaction): - return True +async def low_management_iward(interaction: discord.Interaction) -> bool: if not interaction.guild: return False - return interaction.user.guild_permissions.manage_guild + return await low_management(interaction.client, interaction.user) -async def sys_admin(ctx: LionContext) -> bool: - admins = ctx.bot.config.bot.getintlist('admins') - return ctx.author.id in admins +# Command Wards, raise CheckFailure with localised error message - -async def high_management(ctx: LionContext) -> bool: - if await sys_admin(ctx): +@cmds.check +async def sys_admin_ward(ctx: LionContext) -> bool: + passed = await sys_admin(ctx.bot, ctx.author.id) + if passed: return True + else: + raise CheckFailure( + ctx.bot.translator.t(_p( + 'ward:sys_admin|failed', + "You must be a bot owner to do this!" + )) + ) + + +@cmds.check +async def high_management_ward(ctx: LionContext) -> bool: if not ctx.guild: return False - return ctx.author.guild_permissions.administrator + passed = await high_management(ctx.bot, ctx.author) + if passed: + return True + else: + raise CheckFailure( + ctx.bot.translator.t(_p( + 'ward:high_management|failed', + "You must have the `ADMINISTRATOR` permission in this server to do this!" + )) + ) -async def low_management(ctx: LionContext) -> bool: - return (await high_management(ctx)) or ctx.author.guild_permissions.manage_guild +@cmds.check +async def low_management_ward(ctx: LionContext) -> bool: + if not ctx.guild: + return False + passed = await low_management(ctx.bot, ctx.author) + if passed: + return True + else: + raise CheckFailure( + ctx.bot.translator.t(_p( + 'ward:low_management|failed', + "You must have the `MANAGE_GUILD` permission in this server to do this!" + )) + )