#!/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 from GameFiles import GamesFile logger = logging.getLogger("SecretBot") class SecretBot(commands.Cog): def __init__(self, games_file: GamesFile): super().__init__() self.bot = commands.Bot( "!", help_command = commands.MinimalHelpCommand(), intents = discord.Intents(guild_messages = True, guilds = True, members = 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) self.games_file = games_file 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.bot.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(read_messages = True, send_messages = True, manage_channels = True, manage_roles = True, mention_everyone = True, manage_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 @staticmethod async def check_is_administrator(ctx: commands.Context): """ If the user that sent the command is not an administrator of the server, notify them and raise utils.CheckFailDoNotNotify. """ if not utils.is_administrator(ctx.author): await ctx.reply(f":dragon_face: You have no power here!") raise utils.CheckFailDoNotNotify async def check_is_administrator_or_gm(self, ctx: commands.Context): """ If the user that sent the command is not either an administrator of the server or a gm of the game, notify them and raise utils.CheckFailDoNotNotify. """ game = self.games_file[ctx.guild] if not (utils.is_administrator(ctx.author) or (game.is_started() and game.is_gm(ctx.author))): await ctx.reply(f":dragon_face: You have no power here!") raise utils.CheckFailDoNotNotify @commands.command(help = "See if I'm alive") async def ping(self, ctx: commands.Context): await ctx.reply(":ping_pong:", mention_author = True) @commands.command("StartGame") async def start_game(self, ctx: commands.Context, gm_role: discord.Role, player_role: discord.Role): await self.check_is_administrator(ctx) game = self.games_file[ctx.guild] if game.is_started(): await ctx.reply(":x: a game is already running") else: await ctx.guild.get_member(self.bot.user.id).add_roles(gm_role) await game.start(gm_role, player_role) await ctx.reply(":white_check_mark: Game started!") @commands.command("DeleteGame", help = "Delete a running game and all of its associated channels") async def delete_game(self, ctx: commands.Context): game = self.games_file[ctx.guild] if game.is_started(): await self.check_is_administrator_or_gm(ctx) gm_role = game.get_gm_role() await game.delete() await ctx.guild.get_member(self.bot.user.id).remove_roles(gm_role) await ctx.reply(":white_check_mark: Game deleted!") else: await ctx.reply(":x: Game is not running") @commands.command("StartVote") async def start_vote(self, ctx: commands.Context, president: discord.Member, chancellor: discord.Member): await self.check_is_administrator_or_gm(ctx) game = self.games_file[ctx.guild] if not game.is_started(): await ctx.reply(":x: Game is not running") return if game.is_vote_running(): await ctx.reply(":x: A vote is already running") return await game.start_vote(president, chancellor) await ctx.message.delete() async def cast_vote(self, ctx: commands.Context, vote: bool): game = self.games_file[ctx.guild] if game.is_started() and game.is_vote_running(): if ctx.author.id in game.get_players_id(): if ctx.channel == game.get_player_channel(ctx.author): await game.cast_vote(ctx.author, vote) await ctx.reply(":pencil: Your vote has been registered. Thank you citizen.") else: await ctx.reply(":x: Please cast your vote in your dedicated channel.") else: await ctx.reply(":x: Only players can cast votes") else: await ctx.reply(":x: There isn't a vote running") @commands.command("ja") async def vote_yes(self, ctx: commands.Context): await self.cast_vote(ctx, True) @commands.command("nein") async def vote_no(self, ctx: commands.Context): await self.cast_vote(ctx, False) @commands.command("StopTheCount") async def stop_vote(self, ctx: commands.Context): await self.check_is_administrator_or_gm(ctx) game = self.games_file[ctx.guild] if not game.is_started(): await ctx.reply(":x: Game is not running") return if not game.is_vote_running(): await ctx.reply(":x: No vote is running") return await game.stop_vote() await ctx.message.delete() 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() games_file = GamesFile(ARGS.games_file) bot = SecretBot(games_file) bot.run(token)