diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db589ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +config.ini +.venv +.vscode +*.log diff --git a/config-example.ini b/config-example.ini new file mode 100644 index 0000000..01b0d4f --- /dev/null +++ b/config-example.ini @@ -0,0 +1,3 @@ +[ROK] +server_ID = 111 +kruh-XX = 6 diff --git a/main.py b/main.py new file mode 100644 index 0000000..b93a549 --- /dev/null +++ b/main.py @@ -0,0 +1,166 @@ +from configparser import ConfigParser +import logging + +import discord +from discord.ext import commands + +TOKEN = 'MTE1NDc0ODQ3MzE4MzMwNTc4OQ.Gz_Q7u.02sb2YNV_QQy7Bs19roXlB62mjoMKA6y8aubHU' # Bot token (replace with your own) +DEFAULT_CONFIG_FILE = 'config.ini' # Configuration file with server IDs and study group roles +DEFAULT_LOG_FILE = 'kruhobot.log' # Log file for the bot +HELP_MESSAGE = """Usage: `!grant_role kruh-## UKCO` +- Replace `##` with your study group number +- Replace `UKCO` with your UKCO (8-digit personal number).""" + +# Create an instance of the bot +intents = discord.Intents.default() +intents.members = True +intents.message_content = True + +bot = commands.Bot(command_prefix='!', intents=intents) + +logging.basicConfig(filename=DEFAULT_LOG_FILE, level=logging.INFO) +logger = logging.getLogger("Kruhobot") + +config = ConfigParser() +config.read(DEFAULT_CONFIG_FILE) + + +async def reply_error(ctx, message: str): + """ + Reply to the user with an error message. + :param ctx: The context of the command. + :param message: The error message. + """ + await reply(ctx, f'**Error:** {message}') + + +async def reply(ctx, message: str): + """ + Reply to the user. + :param ctx: The context of the command or the channel from the event handler. + :param message: The message to reply with. + """ + await ctx.send(message) + + +@bot.event +async def on_ready(): + logger.info(f'Logged in as {bot.user.name}') + + +@bot.command() +async def grant_role(ctx, study_group_name: str, ukco: str): + """ + Grant a role to the user based on the study group and UKCO. + :param ctx: The context of the command. + :param study_group_name: The name of the study group. + :param ukco: The UKCO of the user. + """ + # Check study group format + if not study_group_name.startswith('kruh-') or not study_group_name[5:].isdigit(): + await reply_error(ctx, 'Invalid study group format.') + await reply(ctx, HELP_MESSAGE) + return + + # Check UKCO format + if not ukco.isdigit() or not len(ukco) == 8: + await reply_error(ctx, 'Invalid UKCO format.') + await reply(ctx, HELP_MESSAGE) + return + + for guild_id in server_ids: + if study_group_name not in study_groups[guild_id]: # Check if the study group is valid for the server + continue + + guild = bot.get_guild(guild_id) + if guild is None: # Check if the guild exists + continue + + role = discord.utils.get(guild.roles, id=study_groups[guild_id][study_group_name]) + if role is None: # Check if the role exists + continue + + author = guild.get_member(ctx.author.id) + if author is None: # Check if the author is a member of the guild + continue + + await author.add_roles(role) + await reply(ctx, f'{author.mention} has been granted the [{role.name}] role on the [{guild.name}] server.') + logger.info(f'Granted role [{role.name}] to [{author.mention}] in [{guild.name}] corresponding to the study group [{study_group_name}].') + return + + await reply_error(ctx, 'Something went wrong. Please check your command and try again.') + logger.error(f'Failed to grant a role to [{author.mention}] corresponding to the study group [{study_group_name}].') + return + + +async def check_command(message): + # Split the message content into words + words = message.content.split() + + if len(words) == 0: + return False + + # Check if the message is a command + if not words[0].startswith('!'): + await reply(message.channel, HELP_MESSAGE) + return False + + # Check if the command is recognized + if not words[0] == '!grant_role': + await reply_error(message.channel, 'Unrecognized command.') + await reply(message.channel, HELP_MESSAGE) + return False + + # Check if the number of arguments is correct + if not len(words) == 3: + await reply_error(message.channel, 'Invalid number of arguments.') + await reply(message.channel, HELP_MESSAGE) + return False + + return True + + +@bot.event +async def on_message(message): + # Check if the message is a private message and not from the bot itself + if not isinstance(message.channel, discord.DMChannel) or message.author == bot.user: + return + + is_valid_command = await check_command(message) + if not is_valid_command: + return + + await bot.process_commands(message) + + +def init(config): + """ + Initialize the server IDs and study groups from the configuration file. + :param config: The configuration file. + :return: A tuple with server IDs and study groups. + """ + def get_section_server_id(section): + return int(config[section]["server_ID"]) + + server_ids = {get_section_server_id(section) for section in config.sections() if "server_ID" in config[section]} + study_groups = {server_id: dict() for server_id in server_ids} + + for section in config.sections(): + server_id = get_section_server_id(section) + if server_id is None: + continue + + # Study group names are in style "kruh-XX", where XX is a two-digit number + # Iterate over all from top to bottom + for (key, value) in config.items(section): + if key.startswith('kruh-'): + study_groups[server_id][key] = int(value) + + return server_ids, study_groups + + +server_ids, study_groups = init(config) + +# Run the bot +bot.run(TOKEN) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c8e4765 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +discord==2.3.2