From 807728e82a009605a0e0187b6aecf288bb324434 Mon Sep 17 00:00:00 2001 From: Elnath Date: Tue, 8 Jun 2021 00:14:19 +0200 Subject: [PATCH] Initial commit, based on VocalMaisBot code --- .gitignore | 7 +++ SecretBot.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + utils.py | 49 +++++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 .gitignore create mode 100755 SecretBot.py create mode 100644 requirements.txt create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb2fd04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/venv/ +.idea/ +__pycache__/ + +token* +games.json* + diff --git a/SecretBot.py b/SecretBot.py new file mode 100755 index 0000000..308e3d1 --- /dev/null +++ b/SecretBot.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +import argparse +import logging +import sys +from pathlib import Path + +import discord +import discord.utils +from discord.ext import commands + +import utils + +logger = logging.getLogger("SecretBot") + + +class SecretBot(commands.Cog): + + def __init__(self): + super().__init__() + self.bot = commands.Bot( + "!", + help_command = commands.MinimalHelpCommand(), + intents = discord.Intents(guild_messages = True, guilds = True), + allowed_mentions = discord.AllowedMentions.none(), # By default we do not allow mentions, as we will white-list them on each required message + ) + self.bot.add_cog(self) + + def run(self, token): + self.bot.run(token) + + def get_command_usage_message(self, command: commands.Command, prepend = "Usage: ") -> str: + command_elements = [str(cmd) for cmd in command.parents] + [command.name] + return f"{prepend}`{self.command_prefix}{' '.join(command_elements)} {command.signature}`" + + @commands.Cog.listener() + async def on_ready(self): + logger.info("Connected and ready!") + logger.info(f"Logged in as {utils.to_string(self.bot.user)}") + + oauth_url = discord.utils.oauth_url( + self.bot.user.id, + discord.Permissions(manage_channels = True, read_messages = True, send_messages = True) + ) + logger.info(f"You can invite the bot to your server using the following link: {oauth_url}") + + @commands.Cog.listener() + async def on_command_error(self, ctx: commands.Context, error: commands.CommandError): + if isinstance(error, commands.CommandNotFound): + return await ctx.reply(f":x: The requested command does not exist") + elif isinstance(error, commands.MissingRequiredArgument): + return await ctx.reply(f":x: Missing required argument {error.param}. {self.get_command_usage_message(ctx.command)}") + elif isinstance(error, commands.TooManyArguments): + return await ctx.reply(f":x: too many arguments for command {ctx.command}. {self.get_command_usage_message(ctx.command)}") + elif isinstance(error, commands.BadArgument): + if hasattr(error, "argument"): + return await ctx.reply(f":x: I could not correctly understand argument {error.argument}. {self.get_command_usage_message(ctx.command)}") + else: + return await ctx.reply(f":x: I could not correctly understand one of the arguments. {self.get_command_usage_message(ctx.command)}") + elif isinstance(error, commands.ArgumentParsingError): + return await ctx.reply(f":x: Error when parsing the command: {error}") + elif isinstance(error, commands.CheckFailure): + if isinstance(error, utils.CheckFailDoNotNotify): # A check failed, but the check function asked that we do not notify the user + return + else: + return await ctx.reply(f":x: this command can not be run: {error}") + else: + logger.error(f"Error when running command '{ctx.message.content}' by {utils.to_string(ctx.author)} in {utils.to_string(ctx.guild)}", exc_info = error) + await ctx.reply(f":x: There was an error with the command :dizzy_face:") + + def cog_check(self, ctx: commands.Context): + # We silently ignore messages from bots, since we do not want bots to command us + if ctx.author.bot or ctx.message.is_system(): + logger.info(f"[{utils.to_string(ctx.guild)}] Ignored command message from bot or system: {ctx.message.content}") + raise utils.CheckFailDoNotNotify + return True + + @commands.command(help = "See if I'm alive") + async def ping(self, ctx: commands.Context): + await ctx.reply(":ping_pong:", mention_author = True) + + +if __name__ == '__main__': + argparser = argparse.ArgumentParser(description = "Secret Hitler helper bot", formatter_class = argparse.ArgumentDefaultsHelpFormatter) + argparser.add_argument("-t", "--token-file", default = "token", help = "File where the discord bot token is stored") + argparser.add_argument("-g", "--games-file", default = "games.json", help = "File used to store game information so that the bot can be stopped safely") + argparser.add_argument("-v", "--verbose", action = "count", default = 0, help = "Specify once to print bot debug messages, specify twice to print discord.py debug messages as well") + + ARGS = argparser.parse_args() + + logging.basicConfig( + format = "%(asctime)s %(name)s [%(levelname)s] %(message)s" + ) + + if ARGS.verbose == 0: + logging.root.setLevel(logging.INFO) + elif ARGS.verbose == 1: + logging.root.setLevel(logging.DEBUG) + logging.getLogger("discord").setLevel(logging.INFO) + else: + logging.root.setLevel(logging.DEBUG) + + logger.info(f"Using discord.py version {discord.__version__}") + + token_file_path = Path(ARGS.token_file).absolute() + if not token_file_path.is_file(): + logger.error(f"Token file {token_file_path} does not exist") + sys.exit(1) + with token_file_path.open("r") as token_file: + token = token_file.readline().strip() + + bot = SecretBot() + bot.run(token) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..844f49a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +discord.py diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..07affaa --- /dev/null +++ b/utils.py @@ -0,0 +1,49 @@ +import discord +import discord.ext.commands as commands +from functools import singledispatch + + +class CheckFailDoNotNotify(commands.CheckFailure): + """ + A custom exception that we can raise inside command check functions if the check fails, but the global error handling should not notify the user about the error. + For example it can be used if the check function already sent the user a customised error message. + """ + pass + +@singledispatch +def to_string(obj) -> str: + """ + Convert the given discord object to a string, for logging purposes. + + See functools.singledispatch + """ + return str(obj) + + +@to_string.register(discord.Object) +@to_string.register(discord.abc.Snowflake) +def _(obj) -> str: + return f"<{obj.id}>" + + +@to_string.register(discord.abc.User) +def _(user) -> str: + return f"{user.name}#{user.discriminator}({user.id})" + + +@to_string.register(discord.Guild) +def _(guild) -> str: + return f"{guild.name}({guild.id})" + + +# Even though these types are all subclasses of discord.abc.GuildChannel, it does not work if we register that class directly +@to_string.register(discord.TextChannel) +@to_string.register(discord.VoiceChannel) +@to_string.register(discord.CategoryChannel) +def _(channel) -> str: + return f"{channel.name}({channel.id})" + + +@to_string.register(discord.Role) +def _(role) -> str: + return f"@{role.name}({role.id})"