diff --git a/.env.template b/.env.template index 35647c5..f816416 100644 --- a/.env.template +++ b/.env.template @@ -23,6 +23,7 @@ MONGO_URI="mongodb://127.0.0.1:100" # needed by ./bot DISCORD_TOKEN="" +BOT_ADMINS="" # needed by ./portal CAS_LINK="https://cas.my-org.com/login" diff --git a/bot/main.py b/bot/main.py index cefe147..4827c56 100644 --- a/bot/main.py +++ b/bot/main.py @@ -6,6 +6,8 @@ - `get_users_from_discordid()`: Find users from DB given user ID - `is_verified()`: If a user is present in DB or not +- `is_bot_admin()`: If a user is a bot admin or not +- `is_author_bot_admin()`: If the author of the current message is a bot admin or not - `get_realname_from_discordid()`: Get a user's real name from their Discord ID. - `send_link()`: Send link for reattempting authentication. - `create_roles_if_missing()`: Adds missing roles to a server. @@ -15,11 +17,11 @@ - `post_verification()`: Handle role add/delete and nickname set post-verification of given user. - `verify_user()`: Implements `.verify`. - `backend_info()`: Logs server details for debug purposes -- `is_academic()`: Checks if server is for academic use. +- `is_academic_or_bot_admin()`: Checks if server is for academic use or if the author is a bot admin. - `query()`: Returns user details, uses Discord ID to find in DB. -- `query_error()`: Replies eror message if server is not academic. +- `query_error()`: Replies eror message if server is not academic or author is not a bot admin. - `roll()`: Returns user details, uses roll number to find in DB. -- `roll_error()`: Replies eror message if server is not academic. +- `roll_error()`: Replies eror message if server is not academic or author is not a bot admin. - `on_ready()`: Logs a message when the bot joins a server. - `main()`: Reads server config, loads DB and starts bot. @@ -57,6 +59,8 @@ SUBPATH = os.getenv("SUBPATH") BASE_URL = f"{PROTOCOL}://{HOST}{_PORT_AS_SUFFIX}{SUBPATH}" +BOT_ADMINS = [int(id) for id in os.getenv("BOT_ADMINS").split(",") if id != ""] + intent = discord.Intents.default() intent.message_content = True bot = commands.Bot(command_prefix=".", intents=intent) @@ -73,6 +77,15 @@ class DBEntry(TypedDict): view: bool +class CheckFailedException(commands.CommandError): + """Custom exception to help identify which check function failed.""" + + def __init__(self, check_name: str): + """Initialise with the name of the check.""" + super().__init__() + self.check_name = check_name + + def get_users_from_discordid(user_id: int): """ Finds users from the database, given their ID and returns @@ -89,6 +102,20 @@ def is_verified(user_id: int): return True if get_users_from_discordid(user_id) else False +def is_bot_admin(user_id: int): + """Checks if the user with the given discord ID is a bot admin or not.""" + return user_id in BOT_ADMINS + + +def is_author_bot_admin(ctx: commands.Context): + """Wrapper check function for `is_bot_admin`; checks if the user who invoked the command + is a bot admin or not.""" + author = ctx.message.author + if not is_bot_admin(author.id): + raise CheckFailedException("is_author_bot_admin") + return True + + def get_realname_from_discordid(user_id: int): """Returns the real name of the first user who matches the given ID.""" users = get_users_from_discordid(user_id) @@ -221,8 +248,10 @@ async def verify_user(ctx: commands.Context): @bot.hybrid_command(name="backend_info") +@commands.check(is_author_bot_admin) async def backend_info(ctx: commands.Context): """For debugging server info; sends details of the server.""" + uname = platform.uname() await ctx.reply( f"Here are the server details:\n" @@ -235,27 +264,45 @@ async def backend_info(ctx: commands.Context): ) -def is_academic(ctx: commands.Context): - """Checks if the server is an academic server.""" +@backend_info.error +async def backend_info_error(ctx: commands.Context, error: Exception): + """If the author of the message is not a bot admin then reply accordingly.""" + if ( + isinstance(error, CheckFailedException) + and error.check_name == "is_author_bot_admin" + ): + author = ctx.message.author + await ctx.reply(f"{author.mention} is not a bot admin.", ephemeral=True) + else: + await ctx.reply("Some checks failed.", ephemeral=True) + + +def is_academic_or_bot_admin(ctx: commands.Context): + """Checks if the server is an academic server or if the author is a bot admin.""" if ctx.guild is None: return False try: - return server_configs[ctx.guild.id]["is_academic"] - except KeyError: - return False + author = ctx.message.author + is_admin = is_bot_admin(author.id) + if not server_configs[ctx.guild.id]["is_academic"] and not is_admin: + raise CheckFailedException("is_academic_or_bot_admin") + return True + except KeyError as err: + raise CheckFailedException("server_config") from err @bot.hybrid_command(name="query") -@commands.check(is_academic) +@commands.check(is_academic_or_bot_admin) async def query( ctx: commands.Context, identifier: discord.User, ): """ - First checks if the server is an academic one. If so, finds the user who invoked the - command (by Discord ID) in the DB. If present, replies with their name, email and - roll number. Otherwise replies telling the user they are not registed with CAS. + First checks if the server is an academic one or if the author is a bot admin. + If so, finds the user mentioned (by Discord ID) in the command in the DB. + If present, replies with their name, email and roll number. Otherwise + replies telling the author that the mentioned user is not registed with CAS. """ if db is None: await ctx.reply( @@ -280,24 +327,37 @@ async def query( @query.error async def query_error(ctx: commands.Context, error: Exception): """ - For the `query` command, if the server is not academic, replies with error message. + For the `query` command, if the server is not academic and if the author is + not a bot admin, replies with an error message. """ - if isinstance(error, commands.CheckFailure): - await ctx.reply("This server is not for academic purposes.", ephemeral=True) + if ( + isinstance(error, CheckFailedException) + and error.check_name == "is_academic_or_bot_admin" + ): + author = ctx.message.author + await ctx.reply( + "This server is not for academic purposes " + f"and {author.mention} is not a bot admin.", + ephemeral=True, + ) + else: + await ctx.reply("Some checks failed.", ephemeral=True) @bot.hybrid_command(name="roll") -@commands.check(is_academic) +@commands.check(is_academic_or_bot_admin) async def roll( ctx: commands.Context, identifier: int, ): """ - First checks if the server is an academic one. If so, finds the user who invoked the - command in the DB. If present, replies with their name, email and - roll number. Otherwise replies telling the user they are not registed with CAS. + First checks if the server is an academic one or if the author is a bot admin. + If so, finds the user mentioned in the command in the DB. If present, replies + with their name, email and roll number. Otherwise replies telling the author + that the mentioned user is not registed with CAS. - Same as the `query` command, except this searches by roll number instead of Discord ID. + Same as the `query` command, except the user is mentioned by roll number + instead of Discord ID. """ if db is None: await ctx.reply( @@ -322,10 +382,21 @@ async def roll( @roll.error async def roll_error(ctx: commands.Context, error: Exception): """ - For the `roll` command, if the server is not academic, replies with error message. + For the `roll` command, if the server is not academic and if the author is + not a bot admin, replies with an error message. """ - if isinstance(error, commands.CheckFailure): - await ctx.reply("This server is not for academic purposes.", ephemeral=True) + author = ctx.message.author + if ( + isinstance(error, CheckFailedException) + and error.check_name == "is_academic_or_bot_admin" + ): + await ctx.reply( + "This server is not for academic purposes " + f"and {author.mention} is not a bot admin.", + ephemeral=True, + ) + else: + await ctx.reply("Some checks failed.", ephemeral=True) @bot.event diff --git a/server_config.ini b/server_config.ini index 394937d..41f7e7f 100644 --- a/server_config.ini +++ b/server_config.ini @@ -115,3 +115,4 @@ grantroles=Verified [Discord-CAS Test Server 2024] serverid=1239556898592788520 grantroles=Verified +is_academic=yes \ No newline at end of file