Initial commit, based on VocalMaisBot code

This commit is contained in:
Elnath 2021-06-08 00:14:19 +02:00
commit 807728e82a
4 changed files with 169 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/venv/
.idea/
__pycache__/
token*
games.json*

112
SecretBot.py Executable file
View File

@ -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)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
discord.py

49
utils.py Normal file
View File

@ -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})"