예제 #1
0
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "maps")

        self.listen_for_maps = bool(self.config.get("listen_for_maps")) or False
        self.buffer_channel = self.bot.get_channel(id=434551085185630218)
        self.loaded_maps = {}
예제 #2
0
파일: c4.py 프로젝트: SirThane/BattleMaps
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "c4")

        self.sessions = dict()
        self.timeout = 120
        self.timeout_incr = 1

        # Valid reactions
        self._game_reactions = [
            str(Emoji.one),
            str(Emoji.two),
            str(Emoji.three),
            str(Emoji.four),
            str(Emoji.five),
            str(Emoji.six),
            str(Emoji.seven),
            str(Emoji.x),
            str(Emoji.refresh)
        ]

        # Default chips
        self.empty_chip = AWBW_EMOJIS["ne"]
        self.p1_chip = AWBW_EMOJIS["os"]
        self.p2_chip = AWBW_EMOJIS["bm"]
예제 #3
0
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "player")

        self.errorlog = bot.errorlog

        self.sessions = dict()

        self.bot.loop.create_task(self._init_all_sessions())
        self.bot.loop.create_task(self.cog_reload_cronjob(24 * 60 * 60))
예제 #4
0
    def __init__(self, bot: Bot):
        self.bot = bot

        self.config = SubRedis(bot.db, "admin")
        self.config_bot = SubRedis(bot.db, "config")

        self.errorlog = bot.errorlog

        self.delete_after = 30
        self.say_dest = None
예제 #5
0
파일: gh.py 프로젝트: SirThane/BattleMaps
    def __init__(self, bot: Bot):

        self.bot = bot
        self.config = SubRedis(bot.db, "github")

        self.errorlog = bot.errorlog

        self.gh_client = self.try_auth()

        if self.gh_client:
            self._user = self.user = self.gh_client.get_user()
            self._repo = self.repo = self.user.get_repo(self.bot.APP_NAME)
예제 #6
0
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "timer")

        self.errorlog = bot.errorlog

        bot.loop.create_task(self._init_timed_events(bot))
예제 #7
0
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "modlog")

        self.errorlog = bot.errorlog

        # init a local cache of logged Guilds and their configs
        cache = dict()
        for key in self.config.scan_iter("guilds*"):
            *_, guild_id = key.split(":")
            try:
                cache[int(guild_id)] = self.config.hgetall(key)
            except TypeError:
                # Guild ID not found
                self.config.delete(key)

        self._config_cache = cache
예제 #8
0
파일: repl.py 프로젝트: SirThane/BattleMaps
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "REPL")

        self.errorlog = bot.errorlog

        self.ret = None
        self._env_store = dict()
예제 #9
0
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "selfroles")

        self.errorlog = bot.errorlog

        self._selfroles = {g.id: [] for g in bot.guilds}

        for key in self.config.scan_iter(match=f"{self.config}:*"):
            guild = bot.get_guild(int(key.split(":")[-1]))
            if not guild:
                continue
            self._selfroles[guild.id] = []
            r_ids = [int(r_id) for r_id in self.config.smembers(key)]
            for role in guild.roles:
                if role.id in r_ids:
                    self._selfroles[guild.id].append(role.id)
예제 #10
0
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "events")

        self.errorlog = bot.errorlog

        self.awbw = bot.get_guild(313453805150928906)  # AWBW Guild
        self.channel = bot.get_channel(313453805150928906)  # AWBW General
        self.notifchannel = bot.get_channel(
            637877898560143363)  # BattleMaps-Notifs

        if self.awbw:
            self.sad_andy = get(self.awbw.emojis,
                                id=325608374526017536)  # :sad_andy: emoji
예제 #11
0
    def __init__(
            self,
            source,
            config: SubRedis,
            volume: float = 0.15,
            requester: Optional[User] = None,
            **kwargs
    ):

        if not volume:
            volume = float(config.hget("config:defaults", "volume"))

        super().__init__(FFmpegPCMAudio(source, **kwargs), volume)

        self.requester = requester
        self._frames = 0
예제 #12
0
class Admin(Cog):
    """Administrative Commands"""
    def __init__(self, bot: Bot):
        self.bot = bot

        self.config = SubRedis(bot.db, "admin")
        self.config_bot = SubRedis(bot.db, "config")

        self.errorlog = bot.errorlog

        self.delete_after = 30
        self.say_dest = None

    @staticmethod
    def color(ctx: Context):
        """Color for embeds"""

        if ctx.guild:
            return ctx.guild.me.color
        else:
            return None

    """ ######################
         Managing Bot Modules
        ###################### """

    @sudo()
    @group(name="module", aliases=["cog", "mod"], invoke_without_command=True)
    async def module(self, ctx: Context):
        """Base command for managing bot modules

        Use without subcommand to list currently loaded modules"""

        modules = {
            module.__module__: cog
            for cog, module in self.bot.cogs.items()
        }
        space = len(max(modules.keys(), key=len))

        fmt = "\n".join([
            f"{module}{' ' * (space - len(module))} : {cog}"
            for module, cog in modules.items()
        ])

        em = Embed(title="Administration: Currently Loaded Modules",
                   description=f"```py\n{fmt}\n```",
                   color=0x00FF00)
        await ctx.send(embed=em)

    @sudo()
    @module.command(name="load", usage="(module name)")
    async def load(self, ctx: Context, module: str, verbose: bool = False):
        """load a module

        If `verbose=True` is included at the end, error tracebacks will
        be sent to the errorlog channel"""

        module = f"cogs.{module}"
        verbose_error = None

        try:
            self.bot.load_extension(module)

        except ExtensionNotFound as error:
            em = Embed(title="Administration: Load Module Failed",
                       description=f"**__ExtensionNotFound__**\n"
                       f"No module `{module}` found in cogs directory",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error.original

        except ExtensionAlreadyLoaded as error:
            em = Embed(title="Administration: Load Module Failed",
                       description=f"**__ExtensionAlreadyLoaded__**\n"
                       f"Module `{module}` is already loaded",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except NoEntryPointError as error:
            em = Embed(title="Administration: Load Module Failed",
                       description=f"**__NoEntryPointError__**\n"
                       f"Module `{module}` does not define a `setup` function",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except ExtensionFailed as error:
            if isinstance(error.original, TypeError):
                em = Embed(
                    title="Administration: Load Module Failed",
                    description=f"**__ExtensionFailed__**\n"
                    f"The cog loaded by `{module}` must be a subclass of discord.ext.commands.Cog",
                    color=0xFF0000)
            else:
                em = Embed(
                    title="Administration: Load Module Failed",
                    description=f"**__ExtensionFailed__**\n"
                    f"An execution error occurred during module `{module}`'s setup function",
                    color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error.original

        except Exception as error:
            em = Embed(title="Administration: Load Module Failed",
                       description=f"**__{type(error).__name__}__**\n"
                       f"```py\n"
                       f"{error}\n"
                       f"```",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        else:
            em = Embed(title="Administration: Load Module",
                       description=f"Module `{module}` loaded successfully",
                       color=0x00FF00)
            await ctx.send(embed=em, delete_after=self.delete_after)

        finally:
            if verbose and verbose_error:
                await self.errorlog.send(verbose_error, ctx)

    @sudo()
    @module.command(name="unload", usage="(module name)")
    async def unload(self, ctx: Context, module: str, verbose: bool = False):
        """Unload a module

        If `verbose=True` is included at the end, error tracebacks will
        be sent to the errorlog channel"""

        module = f"cogs.{module}"
        verbose_error = None

        try:
            self.bot.unload_extension(module)

        except ExtensionNotLoaded as error:
            em = Embed(title="Administration: Unload Module Failed",
                       description=f"**__ExtensionNotLoaded__**\n"
                       f"Module `{module}` is not loaded",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except Exception as error:
            em = Embed(title="Administration: Unload Module Failed",
                       description=f"**__{type(error).__name__}__**\n"
                       f"```py\n"
                       f"{error}\n"
                       f"```",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        else:
            em = Embed(title="Administration: Unload Module",
                       description=f"Module `{module}` unloaded successfully",
                       color=0x00FF00)
            await ctx.send(embed=em, delete_after=self.delete_after)

        finally:
            if verbose and verbose_error:
                await self.errorlog.send(verbose_error, ctx)

    @sudo()
    @module.command(name="reload", usage="(module name)")
    async def reload(self, ctx: Context, module: str, verbose: bool = False):
        """Reload a module

        If `verbose=True` is included at the end, error tracebacks will
        be sent to the errorlog channel"""

        module = f"cogs.{module}"
        verbose_error = None

        try:
            self.bot.reload_extension(module)

        except ExtensionNotLoaded as error:
            em = Embed(title="Administration: Reload Module Failed",
                       description=f"**__ExtensionNotLoaded__**\n"
                       f"Module `{module}` is not loaded",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except ExtensionNotFound as error:
            em = Embed(title="Administration: Reload Module Failed",
                       description=f"**__ExtensionNotFound__**\n"
                       f"No module `{module}` found in cogs directory",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error.original

        except NoEntryPointError as error:
            em = Embed(title="Administration: Reload Module Failed",
                       description=f"**__NoEntryPointError__**\n"
                       f"Module `{module}` does not define a `setup` function",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except ExtensionFailed as error:
            if isinstance(error.original, TypeError):
                em = Embed(
                    title="Administration: Reload Module Failed",
                    description=f"**__ExtensionFailed__**\n"
                    f"The cog loaded by `{module}` must be a subclass of discord.ext.commands.Cog",
                    color=0xFF0000)
            else:
                em = Embed(
                    title="Administration: Reload Module Failed",
                    description=f"**__ExtensionFailed__**\n"
                    f"An execution error occurred during module `{module}`'s setup function",
                    color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error.original

        except Exception as error:
            em = Embed(title="Administration: Reload Module Failed",
                       description=f"**__{type(error).__name__}__**\n"
                       f"```py\n"
                       f"{error}\n"
                       f"```",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        else:
            em = Embed(title="Administration: Reload Module",
                       description=f"Module `{module}` reloaded successfully",
                       color=0x00FF00)
            await ctx.send(embed=em, delete_after=self.delete_after)

        finally:
            if verbose and verbose_error:
                await self.errorlog.send(verbose_error, ctx)

    @sudo()
    @module.group(name="init",
                  aliases=["initial"],
                  invoke_without_command=True)
    async def init(self, ctx: Context):
        """Get list of modules currently set as initial cogs"""

        modules = dict()
        failed = dict()

        for init_module in self.config_bot.lrange('initial_cogs', 0, -1):
            try:
                module = import_module(f"cogs.{init_module}")
                module_setup = getattr(module, "setup")
                modules[init_module] = module_setup.__doc__
            except Exception as error:
                failed[
                    init_module] = error  # TODO: Capture error details of failed cogs

        space = len(max(modules.keys(), key=lambda x: len(x)))
        fmt = "\n".join([
            f"{module}{' ' * (space - len(module))} : {cog}"
            for module, cog in modules.items()
        ])

        em = Embed(
            title="Administration: Initial Modules",
            description=f"Modules currently set to be loaded at startup\n"
            f"```py\n"
            f"{fmt}\n"
            f"```",
            color=0x00FF00)

        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @init.command(name="add", usage="(module name)")
    async def add(self, ctx: Context, module: str, verbose: bool = False):
        """Sets a module to be loaded on startup

        Must be a valid cog with setup function
        Will check with `importlib.import_module` before setting

        If `verbose=True` is included at the end, error tracebacks will
        be sent to the errorlog channel"""

        verbose_error = None
        lib = None
        module_setup = None

        init_modules = self.config_bot.lrange("initial_cogs", 0, -1)
        if module in init_modules:
            em = Embed(title="Administration: Initial Module Add Failed",
                       description=f"**__ExtensionAlreadyLoaded__**\n"
                       f"Module `{module}` is already initial module",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            return

        try:

            # Basic checks for valid cog
            # If we can import it and if it has the setup entry point
            lib = import_module(f"cogs.{module}")
            module_setup = getattr(lib, "setup")

        except ImportError as error:
            em = Embed(title="Administration: Initial Module Add Failed",
                       description=f"**__ExtensionNotfound__**\n"
                       f"No module `{module}` found in cogs directory",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except AttributeError as error:
            em = Embed(title="Administration: Initial Module Add Failed",
                       description=f"**__NoEntryPointError__**\n"
                       f"Module `{module}` does not define a `setup` function",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        except Exception as error:
            em = Embed(title="Administration: Initial Module Add Failed",
                       description=f"**__{type(error).__name__}__**"
                       f"{error}",
                       color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)
            verbose_error = error

        else:
            self.config_bot.lpush("initial_cogs", module)
            em = Embed(
                title="Administration: Initial Module Add",
                description=f"Module `{module}` added to initial modules",
                color=0x00FF00)
            await ctx.send(embed=em, delete_after=self.delete_after)

        finally:
            if verbose and verbose_error:
                await self.errorlog.send(verbose_error, ctx)

            # We don't actually need them, so remove
            del lib
            del module_setup

    @sudo()
    @init.command(name="rem",
                  aliases=["del", "delete", "remove"],
                  usage="(module name)")
    async def rem(self, ctx: Context, module: str):
        """Removes a module from initial modules"""

        # Get current list of initial cogs
        init_modules = self.config_bot.lrange("initial_cogs", 0, -1)

        if module in init_modules:
            self.config_bot.lrem("initial_cogs", 0, module)
            em = Embed(
                title="Administration: Initial Module Remove",
                description=f"Module `{module}` removed from initial modules",
                color=0x00FF00)
            await ctx.send(embed=em, delete_after=self.delete_after)

        else:
            em = Embed(
                title="Administration: Initial Module Remove Failed",
                description=f"Module `{module}` is not an initial module",
                color=0xFF0000)
            await ctx.send(embed=em, delete_after=self.delete_after)

    """ ######################
         General Use Commands
        ###################### """

    @sudo()
    @group(name="say", invoke_without_command=True)
    async def say(self, ctx: Context, *, msg: str = ""):
        """Makes the bot send a message

        If self.say_dest is set, it will send the message there
        If it is not, it will send to ctx.channel"""

        dest: Messageable = self.say_dest if self.say_dest else ctx.channel
        await dest.send(msg)

    @sudo()
    @say.command(name="in")
    async def say_in(self, ctx: Context, dest: str = None):
        """Sets the destination for messages from `[p]say`"""

        if dest:
            try:
                self.say_dest: TextChannel = await GlobalTextChannelConverter(
                ).convert(ctx, dest)
            except BadArgument as error:
                em = Embed(
                    title="Invalid Channel Identifier",
                    description=f"**__{type(error).__name__}__**: {str(error)}",
                    color=0xFF0000)
                await ctx.send(embed=em, delete_after=self.delete_after)
            else:
                em = Embed(title="Administration: Set `say` Destination",
                           description=f"__Say destination set__\n"
                           f"Guild: {self.say_dest.guild.name}\n"
                           f"Channel: {self.say_dest.mention}\n"
                           f"ID: {self.say_dest.id}",
                           color=0x00FF00)
                await ctx.send(embed=em, delete_after=self.delete_after)
        else:
            self.say_dest = None
            em = Embed(title="Administration: Set `say` Destination",
                       description=f"Say destination has been unset",
                       color=0x00FF00)
            await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @command(name='invite')
    async def invite(self, ctx: Context):
        """Sends an OAuth bot invite URL"""

        em = Embed(title=f'OAuth URL for {self.bot.user.name}',
                   description=f'[Click Here]'
                   f'({oauth_url(self.bot.app_info.id)}) '
                   f'to invite {self.bot.user.name} to your guild.',
                   color=self.color(ctx))
        await ctx.send(embed=em)

    """ ###############################################
         Change Custom Status Message and Online State
        ############################################### """

    @sudo()
    @group(name='status', invoke_without_command=True)
    async def status(self, ctx: Context):
        """Changes the status and state"""
        pass

    @sudo()
    @status.command(name="online")
    async def online(self, ctx: Context):
        """Changes online status to Online"""
        await self.bot.change_presence(status=Status.online)

        em = Embed(title="Administration: Change Online Status",
                   description="Status changed to `online`",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="dnd", aliases=["do_not_disturb"])
    async def dnd(self, ctx: Context):
        """Changes online status to Do Not Disturb"""
        await self.bot.change_presence(status=Status.dnd)

        em = Embed(title="Administration: Change Online Status",
                   description="Status changed to `dnd`",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="idle")
    async def idle(self, ctx: Context):
        """Changes online status to Idle"""
        await self.bot.change_presence(status=Status.idle)

        em = Embed(title="Administration: Change Online Status",
                   description="Status changed to `idle`",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="invisible", aliases=["offline"])
    async def invisible(self, ctx: Context):
        """Changes online status to Invisible"""
        await self.bot.change_presence(status=Status.invisible)

        em = Embed(title="Administration: Change Online Status",
                   description="Status changed to `invisible`",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="remove", aliases=["rem", "del", "delete", "stop"])
    async def remove(self, ctx: Context):
        """Removes status message"""
        activity = Activity(name=None)
        await self.bot.change_presence(activity=activity)

        em = Embed(title="Administration: Status Message Removed",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="playing", aliases=["game"])
    async def playing(self, ctx: Context, *, status: str):
        """Changes status to `Playing (status)`

        Will also change status header to `Playing A Game`"""
        activity = Activity(name=status, type=ActivityType.playing)
        await self.bot.change_presence(activity=activity)

        em = Embed(title="Administration: Status Message Set",
                   description=f"**Playing A Game\n**"
                   f"Playing {status}",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="streaming")
    async def streaming(self, ctx: Context, *, status: str):
        """Changes status to `Playing (status)`

        Will also change status header to `Live on Twitch`"""
        activity = Activity(name=status, type=ActivityType.streaming)
        await self.bot.change_presence(activity=activity)

        em = Embed(title="Administration: Status Message Set",
                   description=f"**Live On Twitch\n**"
                   f"Playing {status}",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="listening")
    async def listening(self, ctx: Context, *, status: str):
        """Changes status to `Listening to (status)`"""
        activity = Activity(name=status, type=ActivityType.listening)
        await self.bot.change_presence(activity=activity)

        em = Embed(title="Administration: Status Message Set",
                   description=f"Listening to {status}",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @status.command(name="watching")
    async def watching(self, ctx: Context, *, status: str):
        """Changes status to `Watching (status)`"""
        activity = Activity(name=status, type=ActivityType.watching)
        await self.bot.change_presence(activity=activity)

        em = Embed(title="Administration: Status Message Set",
                   description=f"Watching {status}",
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    """ #########################
         Guild-Specific Prefixes
        ######################### """

    @group(name="prefix", invoke_without_command=True)
    async def prefix(self, ctx: Context):
        """Manage bot prefixes

        `Displays current prefix settings"""

        if ctx.guild:
            guild_prefix = self.config_bot.hget("prefix:guild", ctx.guild.id)

            if guild_prefix:
                guild_prefix = f"`{guild_prefix}`"
            else:
                guild_prefix = f"Not set for `{ctx.guild.name}`"

        else:
            guild_prefix = None

        em = Embed(title="Administration: Prefix Settings",
                   color=self.color(ctx))

        em.add_field(
            name="Default Prefix:",
            value=
            f"`{self.config_bot.hget('prefix:config', 'default_prefix')}`",
            inline=False)

        em.add_field(
            name="When Mentioned:",
            value=
            f"`{self.config_bot.hget('prefix:config', 'when_mentioned')}`",
            inline=False)

        if guild_prefix:
            em.add_field(name="Guild Prefix:",
                         value=guild_prefix,
                         inline=False)

        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @prefix.command(name="default")
    async def default(self, ctx: Context, prefix: str = None):
        """Show or change default prefix"""

        if prefix:
            self.config_bot.hset("prefix:config", "default_prefix", prefix)
            em = Embed(title="Administration: Default Prefix",
                       description=f"Default prefix changed to `{prefix}`",
                       color=self.color(ctx))

        else:
            default_prefix = self.config_bot.hget("prefix:config",
                                                  "default_prefix")
            em = Embed(title="Administration: Default Prefix",
                       description=
                       f"Default prefix currently set to `{default_prefix}`",
                       color=self.color(ctx))

        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @prefix.command(name="mention")
    async def mention(self, ctx: Context, enabled: bool = None):
        """Show or change `when_mentioned` prefix option

        `[p]prefix mention` to toggle current setting
        `[p]prefix mention [True|False]` to set setting"""

        if enabled is None:
            enabled = not self.config_bot.hget("prefix:config",
                                               "when_mentioned")

        self.config_bot.hset("prefix:config", "when_mentioned", str(enabled))

        em = Embed(
            title="Administration: Mention As Prefix",
            description=
            f"`when_mentioned` is now {'en' if enabled else 'dis'}abled",
            color=self.color(ctx))

        await ctx.send(embed=em, delete_after=self.delete_after)

    @has_guild_permissions(manage_guild=True)
    @prefix.command(name="guild")
    async def guild(self, ctx: Context, *, prefix: str = None):
        """Change guild-specific prefix"""

        current_guild_prefix = self.config_bot.hget("prefix:guild",
                                                    f"{ctx.guild.id}")

        if prefix:
            if current_guild_prefix == prefix:
                em = Embed(
                    title="Administration: Guild-Specific Prefix",
                    description=f"No changes to make.\n"
                    f"Prefix for guild `{ctx.guild.name}` is currently set to `{prefix}`",
                    color=self.color(ctx))

            else:
                self.config_bot.hset("prefix:guild", f"{ctx.guild.id}", prefix)
                em = Embed(
                    title="Administration: Guild-Specific Prefix",
                    description=
                    f"Prefix for guild `{ctx.guild.name}` set to `{prefix}`",
                    color=self.color(ctx))

        else:
            self.config_bot.hdel("prefix:guild", f"{ctx.guild.id}")
            em = Embed(
                title="Administration: Guild-Specific Prefix",
                description=f"Prefix for guild `{ctx.guild.name}` unset",
                color=self.color(ctx))

        await ctx.send(embed=em, delete_after=self.delete_after)

    """ #########################
         Updating and Restarting
        ######################### """

    @staticmethod
    def gitpull() -> str:
        """Uses os.popen to `git pull`"""
        resp = popen("git pull").read()
        resp = f"```diff\n{resp}\n```"
        return resp

    @sudo()
    @command(name="pull")
    async def pull(self, ctx: Context):
        """Updates bot repo from master"""

        em = Embed(title="Administration: Git Pull",
                   description=self.gitpull(),
                   color=0x00FF00)
        await ctx.send(embed=em, delete_after=self.delete_after)

    @sudo()
    @group(name='restart', aliases=["kill", "f"], invoke_without_command=True)
    async def _restart(self, ctx: Context):
        """Restarts the bot"""

        em = Embed(title="Administration: Restart",
                   description=f"{ctx.author.mention} initiated bot restart.",
                   color=0x00FF00)

        await ctx.send(embed=em, delete_after=self.delete_after)
        await self.bot.logout()

    @sudo()
    @_restart.command(name="pull")
    async def restart_pull(self, ctx: Context):
        """Updates repo from origin master and restarts"""

        em = Embed(
            title="Administration: Git Pull and Restart",
            description=
            f"{ctx.author.mention} initiated bot code update and restart.\n{self.gitpull()}",
            color=0x00FF00)

        await ctx.send(embed=em)
        await self.bot.logout()

    """ ######
         Logs
        ###### """

    @staticmethod
    def get_tail(file: str, lines: int):
        """Get the tail of the specified log file"""

        # # Too many lines will not display in embed.
        # if 0 > lines or lines > 20:
        #     lines = 5

        # Get log file name from repo name from name of cwd
        repo = split(getcwd())[1]

        # Use linux `tail` to read logs
        ret = popen(f"tail -{lines} ~/.pm2/logs/{repo}-{file}.log").read()

        # Format into string with characters for diff markdown highlighting
        head = "+ " if file == "out" else "- "
        ret = "\n".join([f"{head}{line}" for line in ret.split("\n")][:-1])

        return ret

    @sudo()
    @group(name="tail", aliases=["logs"], invoke_without_command=True)
    async def tail(self, ctx: Context, lines: int = 5):
        """Get logs for stdout and stderr"""

        err = self.get_tail("error", lines)
        out = self.get_tail("out", lines)

        em = Embed(title="Administration: Tail", color=0x00FF00)
        em.add_field(name="Error", value=f"```diff\n{err}\n```", inline=False)
        em.add_field(name="Out", value=f"```diff\n{out}\n```", inline=False)

        for embed in em.split():
            await ctx.send(embed=embed)
            await sleep(0.1)

    @sudo()
    @tail.command(name="out")
    async def out(self, ctx: Context, lines: int = 5):
        """Get stdout logs"""

        out = self.get_tail("out", lines)

        em = Embed(title="Administration: Tail", color=0x00FF00)
        em.add_field(name="Out", value=f"```diff\n{out}\n```", inline=False)

        for embed in em.split():
            await ctx.send(embed=embed)
            await sleep(0.1)

    @sudo()
    @tail.command(name="err", aliases=["error"])
    async def err(self, ctx: Context, lines: int = 5):
        """Get stdout logs"""

        err = self.get_tail("error", lines)

        em = Embed(title="Administration: Tail", color=0x00FF00)
        em.add_field(name="Error", value=f"```diff\n{err}\n```", inline=False)

        for embed in em.split():
            await ctx.send(embed=embed)
            await sleep(0.1)
예제 #13
0
class SelfRoles(Cog):
    """Commands for managing and assigning selfroles"""
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "selfroles")

        self.errorlog = bot.errorlog

        self._selfroles = {g.id: [] for g in bot.guilds}

        for key in self.config.scan_iter(match=f"{self.config}:*"):
            guild = bot.get_guild(int(key.split(":")[-1]))
            if not guild:
                continue
            self._selfroles[guild.id] = []
            r_ids = [int(r_id) for r_id in self.config.smembers(key)]
            for role in guild.roles:
                if role.id in r_ids:
                    self._selfroles[guild.id].append(role.id)

    @guild_only()
    @command(name="iam")
    async def iam(self, ctx: Context, *, role) -> None:
        """Add a self-assignable role

        If the `role` is configured as an assignable
        selfrole, you can use this command to assign
        the role to yourself.

        `[p]iam role`
        `[p]iam @role`
        `[p]iam role id`

        You can also use a country's short code.
        e.g. `[p]iam os`"""

        if role.lower() in ROLE_SHORTNAME.keys():
            role = str(ROLE_SHORTNAME[role.lower()])

        try:
            role = await RoleConverter().convert(ctx, role)

        except BadArgument:
            await ctx.send(
                embed=Embed(color=ctx.guild.me.colour,
                            title="⚠ Selfroles",
                            description=f"Could not recognize role."))

        else:
            if role in ctx.author.roles:
                await ctx.send(embed=Embed(
                    color=ctx.guild.me.colour,
                    title="⚠ Selfroles",
                    description="You already have this role assigned."))
            elif role.id in self._selfroles.get(ctx.guild.id):
                for author_role in ctx.author.roles:
                    if author_role.id in self._selfroles[ctx.guild.id]:
                        await ctx.author.remove_roles(
                            author_role,
                            reason=ctx.message.content,
                            atomic=True)
                        await sleep(0.5)
                await ctx.author.add_roles(role,
                                           reason=ctx.message.content,
                                           atomic=True)
                await ctx.send(embed=Embed(
                    color=role.colour,
                    title="Role Assigned",
                    description=f"Congratulations, {ctx.author.mention}!"
                    f" You now have the **{role.mention}** "
                    f"role."))
            else:
                await ctx.send(embed=Embed(
                    color=0xFF0000,
                    title="⚠ Selfroles",
                    description="That role is not self-assignable."))

    @guild_only()
    @group(name="selfroles", aliases=["selfrole"], invoke_without_command=True)
    async def selfroles(self, ctx: Context):
        """View all selfroles"""

        r_ids = self._selfroles.get(ctx.guild.id)

        if r_ids:
            roles = []
            for role in ctx.guild.roles:
                if role.id in r_ids:
                    roles.append(role.mention)

            roles = "\n".join(roles)

            await ctx.send(embed=Embed(
                color=ctx.guild.me.colour,
                title="Selfroles",
                description=f"The following roles are self-assignable:\n"
                f"{roles}"
                f"\n"
                f"\n"
                f"See `?help iam` for assigning selfroles."))

        else:
            await ctx.send(embed=Embed(
                color=ctx.guild.me.colour,
                title="Selfroles",
                description="There are currently no assignable selfroles.\n"
                "Staff may configure selfroles with `?selfrole add`."))

    @awbw_staff()
    @selfroles.command(name="add")
    async def _add(self, ctx: Context, *, role) -> None:
        """Configures a role as a selfrole"""

        r_ids = [i for i in self._selfroles.get(ctx.guild.id)]

        try:
            role = await RoleConverter().convert(ctx, role)
        except BadArgument:
            await ctx.send(
                embed=Embed(color=ctx.guild.me.colour,
                            title="⚠ Selfroles Management",
                            description=f"Could not recognize role."))
        else:
            if role.id in r_ids:
                await ctx.send(embed=Embed(
                    color=ctx.guild.me.colour,
                    title="⚠ Selfroles Management",
                    description=
                    f"Role {role.mention} is already configured as a selfrole."
                ))
            else:
                self.config.sadd(f"{self.config}:{ctx.guild.id}", str(role.id))
                self._selfroles[ctx.guild.id].append(role.id)
                await ctx.send(embed=Embed(
                    color=ctx.guild.me.colour,
                    title="Selfroles Management",
                    description=
                    f"Role {role.mention} has been added to selfroles."))

    @awbw_staff()
    @selfroles.command(name="rem", aliases=["del", "remove", "delete"])
    async def _rem(self, ctx: Context, *, role):
        """Removes a role from selfroles"""

        r_ids = [i for i in self._selfroles.get(ctx.guild.id)]

        try:
            role = await RoleConverter().convert(ctx, role)

        except BadArgument:
            await ctx.send(
                embed=Embed(color=ctx.guild.me.colour,
                            title="⚠ Selfroles Management",
                            description=f"Could not recognize role."))

        else:
            if role.id in r_ids:
                self.config.srem(f"{self.config}:{ctx.guild.id}", role.id)
                self._selfroles[ctx.guild.id].remove(role.id)
                await ctx.send(embed=Embed(
                    color=ctx.guild.me.colour,
                    title="Selfroles Management",
                    description=
                    f"Role {role.mention} has been removed  from selfroles."))
            else:
                await ctx.send(embed=Embed(
                    color=ctx.guild.me.colour,
                    title="⚠ Selfroles Management",
                    description=
                    f"Role {role.mention} is not configured as a selfrole."))
예제 #14
0
파일: main.py 프로젝트: SirThane/BattleMaps
:file ./redis.json
{
  "db": {
    "host": "localhost",                    # Server address hosting Redis DB
    "port": 6379,                           # Port for accessing Redis
    "db": 0,                                # Redis DB number storing app configs
    "decode_responses": true                # decode_responses must be bool true
  }
}
"""

try:
    with open("redis.json", "r+") as redis_conf:
        conf = load(redis_conf)["db"]
        root = StrictRedis(**conf)
        db = SubRedis(root, APP_NAME)
except FileNotFoundError:
    raise FileNotFoundError("redis.json not found in running directory")

config = SubRedis(db, "config")

if not config.hget("prefix:config", "default_prefix"):
    config.hset("prefix:config", "default_prefix", "!")

if not config.hget("prefix:config", "when_mentioned"):
    config.hset("prefix:config", "when_mentioned", "False")


def command_prefix(client: Bot, msg: Message) -> List[str]:
    """Callable to determine guild-specific prefix or default"""
예제 #15
0
파일: c4.py 프로젝트: SirThane/BattleMaps
class ConnectFour(Cog):
    """Play a game of Connect Four

    See `[p]help c4` manual for individual
    commands for more information.

    The classic game of Connect Four.
    Use these commands to play a game
    of Connect Four with another user.
    You can have multiple concurrent
    games, one per channel."""
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "c4")

        self.sessions = dict()
        self.timeout = 120
        self.timeout_incr = 1

        # Valid reactions
        self._game_reactions = [
            str(Emoji.one),
            str(Emoji.two),
            str(Emoji.three),
            str(Emoji.four),
            str(Emoji.five),
            str(Emoji.six),
            str(Emoji.seven),
            str(Emoji.x),
            str(Emoji.refresh)
        ]

        # Default chips
        self.empty_chip = AWBW_EMOJIS["ne"]
        self.p1_chip = AWBW_EMOJIS["os"]
        self.p2_chip = AWBW_EMOJIS["bm"]

    def session(self, channel: TextChannel) -> Optional[ConnectFourSession]:
        """Returns an active ConnectFourSession if there is a running game in a channel"""
        return self.sessions.get(channel.id, None)

    def channel_check(self, channel: TextChannel) -> bool:
        """Returns true if C4 is enabled in channel"""
        return str(channel.id) in self.config.smembers("allowed_channels")

    @staticmethod
    async def member_check(ctx: Context, member: str) -> Optional[Member]:
        """Attempt to convert an argument an argument to :class:`Member`"""
        try:
            return await MemberConverter().convert(ctx, member)
        except (BadArgument, NoPrivateMessage, TypeError):
            return None

    @staticmethod
    def get_member_chip(roles: List[Role],
                        skip: str = None) -> Tuple[str, str]:
        for ctry, role_id in AWBW_ROLES.items():
            if role_id in [r.id for r in roles] and ctry != skip:
                return ctry, AWBW_EMOJIS[ctry]
        else:
            ctry = choice(
                [ctry for ctry in AWBW_EMOJIS.keys() if ctry != skip])
            return ctry, AWBW_EMOJIS[ctry]

    @staticmethod
    async def send_message(channel: TextChannel,
                           msg: str = "Error",
                           level: int = MsgLevel.info) -> None:
        """Formats a message as embed"""

        em = Embed(description=f"{MSG_ICON[level.value]}  {msg}",
                   color=MSG_COLOR[level.value])
        msg = await channel.send(embed=em)
        await sleep(5)
        await msg.delete()

    async def init_game_message(self, channel: TextChannel,
                                session: ConnectFourSession,
                                board: Embed) -> Message:
        """Sends game board to channel and sets message on session"""

        msg = await channel.send(embed=board)

        for react in self._game_reactions:
            await msg.add_reaction(str(react))
            await sleep(0.1)

        session.msg = msg

        return msg

    async def send_board(self, channel: TextChannel) -> Message:
        """Prepare game Embed and update message"""

        session = self.session(channel)

        em = Embed(
            title=
            f"{session.p1.chip}{session.p1.name} {Emoji.vs} {session.p2.name}{session.p2.chip}",
            description=f"\n"
            f"{Emoji.one}{Emoji.two}{Emoji.three}{Emoji.four}{Emoji.five}{Emoji.six}{Emoji.seven}\n"
            f"{session.draw_board}")

        if session.state == State.init:
            em.description = f"New game! Turn: 1\n" \
                             f"{em.description}\n" \
                             f"\n" \
                             f"{session.current_player.mention}'s turn: {session.current_player.chip}"
            em.colour = session.colour

            return await self.init_game_message(channel, session, em)

        elif session.state == State.active:
            em.description = f"Turn: {(session.turn + 2) // 2}\n" \
                             f"{em.description}\n" \
                             f"\n" \
                             f"{session.current_player.mention}'s turn: {session.current_player.chip}"
            em.colour = session.colour

        elif session.state == State.draw:
            self.sessions.pop(channel.id)
            em.description = f"Game ended in a Draw.\n" \
                             f"{em.description}"
            em.colour = Colour.dark_grey()
            await session.msg.clear_reactions()

        elif session.state == State.forfeit:
            self.sessions.pop(channel.id)
            em.description = f"Game Over. {session.current_player.name} Forfeits.\n" \
                             f"{em.description}"
            em.colour = Colour.dark_grey()
            await session.msg.clear_reactions()

        elif session.state == State.timeout:
            self.sessions.pop(channel.id)
            em.description = f"Time Out. {session.current_player.name} Forfeits.\n" \
                             f"{em.description}"
            em.colour = Colour.dark_grey()
            await session.msg.clear_reactions()

        elif session.state == State.won:
            self.sessions.pop(channel.id)
            em.description = f"Game Over!\n{session.current_player.name} wins! {Emoji.tada}\n" \
                             f"{em.description}"
            em.colour = 0xFDFF00
            await session.msg.clear_reactions()

        # TODO: check I can edit the message (retrievable), if not, init message
        return await session.msg.edit(embed=em)

    @group(name="c4", invoke_without_command=True)
    async def c4(self, ctx: Context, *, member=None):
        """Connect Four

        `[p]c4 @user` to start a game with
        another user in the current channel."""

        if not member:
            return await self.bot.help_command.send_help_for(
                ctx, ctx.command, "You need another player to start")

        try:
            member = await self.member_check(ctx, member)
        except BadArgument:
            return await self.member_check(ctx, member)

        if not self.channel_check(ctx.channel):
            return await self.send_message(
                ctx.channel,
                msg=
                f"{ctx.author.mention}: Connect Four is not enabled in this channel",
                level=MsgLevel.error)

        elif self.session(ctx.channel):
            return await self.send_message(
                ctx.channel,
                msg=
                f"{ctx.author.mention}: There is already an active session in this channel",
                level=MsgLevel.warning)

        elif member.id == ctx.author.id:
            return await self.send_message(
                ctx.channel,
                msg=
                f"{ctx.author.mention}: You cannot start a game with yourself.",
                level=MsgLevel.warning)

        elif member.bot:
            return await self.send_message(
                ctx.channel,
                msg=f"{ctx.author.mention}: You cannot play against bots.",
                level=MsgLevel.warning)

        elif member.id == ctx.author.id:
            return await self.send_message(
                ctx.channel,
                msg=
                f"{ctx.author.mention}: You cannot start a game with yourself.",
                level=MsgLevel.warning)

        else:
            p2_ctry, p2_chip = self.get_member_chip(ctx.author.roles)
            _, p1_chip = self.get_member_chip(member.roles, p2_ctry)

            self.sessions[ctx.channel.id] = ConnectFourSession(
                p1=member,
                p1_chip=p1_chip,
                p2=ctx.author,
                p2_chip=p2_chip,
                empty=self.empty_chip)
            await self.send_board(ctx.channel)

    @c4.command(name="help", hidden=True)
    async def c4_help(self, ctx):
        """Shortcut to send help manual for ConnectFour"""
        await self.bot.help_command.send_help_for(
            ctx, self.bot.get_cog("ConnectFour"))

    @c4.command(name="board")
    async def c4_board(self, ctx: Context):
        """Resend the current game board"""
        session = self.session(ctx.channel)
        if not session:
            return await self.send_message(
                ctx.channel,
                msg=
                f"{ctx.author.mention} There is no active game in this channel.",
                level=MsgLevel.warning)
        elif ctx.author.id not in session.players.keys():
            return
        else:
            await self.init_game_message(ctx.channel, session,
                                         session.msg.embeds[0])

    @sudo()
    @c4.command(name="enable", hidden=True)
    async def c4_enable(self, ctx: Context, *, chan: TextChannel = None):
        """Enable Connect Four on a channel

        Run without an argument to enable on the current channel
        Pass a channel as an argument to enable on that channel"""

        if not chan:
            chan = ctx.channel

        if self.config.sadd("allowed_channels", chan.id):
            await self.send_message(
                ctx.channel,
                msg="Connect Four successfully enabled on channel.",
                level=MsgLevel.info)

        else:
            await self.send_message(
                ctx.channel,
                msg="Connect Four already enabled on channel.",
                level=MsgLevel.warning)

    @sudo()
    @c4.command(name="disable", hidden=True)
    async def c4_disable(self, ctx: Context, *, chan: TextChannel = None):
        """Disable Connect Four on a channel

        Run without an argument to disabled on the current channel
        Pass a channel as an argument to disable on that channel"""

        if not chan:
            chan = ctx.channel

        if self.config.srem("allowed_channels", chan.id):
            await self.send_message(
                ctx.channel,
                msg="Connect Four successfully disabled on channel.",
                level=MsgLevel.info)

        else:
            await self.send_message(
                ctx.channel,
                msg="Connect Four already disabled on channel.",
                level=MsgLevel.warning)

    @sudo()
    @c4.command(name="games", hidden=True)
    async def c4_games(self, ctx: Context):
        """Shows the number of running sessions"""
        sessions = '\n'.join([str(s) for s in self.sessions.values()])
        await self.send_message(
            ctx.channel,
            msg=f"Total running games: {len(self.sessions.keys())}\n"
            f"\n"
            f"{sessions}",
            level=MsgLevel.info)

    @sudo()
    @c4.command(name="kill", hidden=True)
    async def c4_kill(self, ctx: Context, *, _all: bool = False):
        """Administrative kill command

        This will kill a running game in the current channel"""
        if not _all:
            if self.sessions.pop(ctx.channel.id, None):
                await self.send_message(
                    ctx.channel,
                    msg="Current game in this channel has been terminated.",
                    level=MsgLevel.info)
            else:
                await self.send_message(ctx.channel,
                                        msg="No active game in this channel.",
                                        level=MsgLevel.warning)
        else:
            await self.send_message(
                ctx.channel,
                msg=
                f"All running games have been terminated. (Total: {len(self.sessions.keys())})",
                level=MsgLevel.info)
            self.sessions = dict()

    @sudo()
    @c4.command(name="killall", hidden=True)
    async def c4_killall(self, ctx: Context):
        """Administrative kill command

        This will kill all running games in all channels"""

        ctx.message.content = f"{self.bot.command_prefix}kill True"
        await self.bot.process_commands(ctx.message)

    @Cog.listener(name="on_reaction_add")
    async def on_reaction_add(self, reaction: Reaction, user: Union[User,
                                                                    Member]):

        await sleep(0.1)

        if not self.channel_check(reaction.message.channel):
            # Channel is not watched for Connect Four
            return

        if user.id == self.bot.user.id:
            # Ignore the bot's own reactions
            return

        # It's a watched channel, so try to get an active session
        session = self.session(reaction.message.channel)

        if not session:
            # No session in channel
            return

        if reaction.message.id != session.msg.id:
            # Message is not an active session
            return

        if reaction.emoji not in self._game_reactions:
            # Not a valid game reaction
            return

        # It is a valid game reaction on an active session in a watched channel
        # Remove reaction, then act based on reaction
        await reaction.remove(user)

        if user.id not in session.players:
            # Not a player
            return

        # Currently the other player's turn
        if user.id != session.current_player.id:

            # Still allow quitting
            if reaction.emoji == str(Emoji.x):
                session.turn -= 1
                session.forfeit()
                return await self.send_board(reaction.message.channel)

            # And resending the board
            elif reaction.emoji == str(Emoji.refresh):
                await session.msg.delete()
                return await self.init_game_message(reaction.message.channel,
                                                    session,
                                                    session.msg.embeds[0])

            # Otherwise needs to be player's turn
            else:
                return await self.send_message(
                    reaction.message.channel,
                    msg=f"{user.mention}: It is not your turn.",
                    level=MsgLevel.warning)

        # Resend the current game board
        if reaction.emoji == str(Emoji.refresh):
            await session.msg.delete()
            return await self.init_game_message(reaction.message.channel,
                                                session, session.msg.embeds[0])

        if reaction.emoji == str(Emoji.one):
            try:
                session.play(user, 1)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.two):
            try:
                session.play(user, 2)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.three):
            try:
                session.play(user, 3)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.four):
            try:
                session.play(user, 4)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.five):
            try:
                session.play(user, 5)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.six):
            try:
                session.play(user, 6)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.seven):
            try:
                session.play(user, 7)
            except ValueError:
                await self.send_message(
                    reaction.message.channel,
                    msg="That column is full. Select another.",
                    level=MsgLevel.error)
            return await self.send_board(reaction.message.channel)

        if reaction.emoji == str(Emoji.x):
            session.forfeit()
            return await self.send_board(reaction.message.channel)

    @Cog.listener(name="on_timer_update")
    async def on_timer_update(self, sec: int):
        """Timer event that triggers every 1 second"""

        # Only check statuses every `self.timeout_incr` seconds
        if sec % self.timeout_incr == 0:

            # Get all sessions and start a list of ones that timed out
            sessions = self.sessions.values()
            to_expire = list()

            # Increment the time on all
            for session in sessions:
                session.timeout += self.timeout_incr

                # Timeout reached
                # Add to list of expired sessions
                if session.timeout == self.timeout:
                    to_expire.append(session)

            # Set game over condition on all expired sessions to timeout and send
            for session in to_expire:
                session.expire()
                await self.send_board(session.msg.channel)
예제 #16
0
파일: gh.py 프로젝트: SirThane/BattleMaps
class GitHub(Cog):
    """GitHub"""

    def __init__(self, bot: Bot):

        self.bot = bot
        self.config = SubRedis(bot.db, "github")

        self.errorlog = bot.errorlog

        self.gh_client = self.try_auth()

        if self.gh_client:
            self._user = self.user = self.gh_client.get_user()
            self._repo = self.repo = self.user.get_repo(self.bot.APP_NAME)

    def cog_check(self, ctx: Context) -> bool:
        return ctx.

    def try_auth(self) -> Optional[Github]:
        """Create Github client object and attempt hitting API with token"""

        token = self.config.get("token")

        if token:
            try:
                gh_client = Github(token)
                _ = gh_client.get_rate_limit()
                return gh_client

            except BadCredentialsException:
                return None

    @group(name="gh", invoke_without_command=True)
    @is_authed
    async def gh(self, ctx: Context):
        """GitHub

        Something"""
        user = f"[{self.user.name}]({self.user.html_url})" if self.user else "None Selected"
        repo = f"[{self.repo.name}]({self.repo.html_url})" if self.repo else "None Selected"

        em = Embed(
            title="GitHub",
            description=f"**__Current Selections:__**\n"
                        f"__User:__ {user}\n"
                        f"__Repository:__ {repo}",
            color=Colour.green()
        )

        await ctx.send(embed=em)

    @gh.command(name="auth")
    async def auth(self, ctx: Context):
        """Authenticate With GitHub Token

        Something something"""
        pass

    @gh.command(name="user")
    async def user(self, ctx: Context, *, user: str = None):
        """User

        Selects a GitHub user account and shows a brief of the profile.
        `[p]gh user <username>` will select a user account
        `[p]gh user self` will select your user account
        `[p]gh user` will display the currently selected user"""

        try:
            if user == "self":
                self.user = user = self.gh_client.get_user()
            elif user:
                self.user = user = self.gh_client.get_user(user)
            else:
                if self.user:
                    user = self.user
                else:
                    return await self.bot.send_help_for(
                        ctx.command,
                        "No account is currently selected."
                    )

            if self.repo.owner.name != self.user.name:
                self.repo = None

            repos = len(list(user.get_repos()))
            gists = len(list(user.get_gists()))
            stars = len(list(user.get_gists()))

            em = Embed(
                title=f"{user.login}'s Public GitHub Profile",
                description=f"*\"{user.bio}\"*\n\n"
                            f"{Emoji.repo} [Repositories]({user.html_url}?tab=repositories): {repos}\n"
                            f"{Emoji.gist} [Gists](https://gist.github.com/{user.login}): {gists}\n"
                            f"{Emoji.star} [Starred Repositories]({user.html_url}?tab=stars): {stars}",
                color=Colour.green()
            )
            em.set_author(name=user.name, url=user.html_url, icon_url=user.avatar_url)

        except:
            em = Embed(
                title="GitHub: Error",
                description="Unable to load user or user not found",
                color=Colour.red()
            )

        await ctx.send(embed=em)

    @gh.command(name="repo")
    async def repo(self, ctx: Context, *, name: str = None):
        pass

    @gh.group(name="issues", invoke_without_command=True, aliases=["issue"])
    async def issues(self, ctx: Context, state: str = "all"):
        """Issues

        View and manage issues on a repo"""

        state = state.lower()
        if state not in ("open", "closed", "all"):
            return await self.bot.send_help_for(ctx.command, "Valid states: `open`, `closed`, `all`")

        em = Embed()

        for issue in self.repo.get_issues(state=state):
            pass
예제 #17
0
class AdvanceWars(Cog):
    """
    Commands for working with Advance Wars maps.
    Will currently support AWS map files and
    AWBW maps, both text, file, and links.
    Other features soon to come™

    Maps can be automatically loaded from AWS,
    CSV, and AWBW links if `Map Listen` is turned
    on. See `[p]help map listen` for more details.
    """

    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "maps")

        self.listen_for_maps = bool(self.config.get("listen_for_maps")) or False
        self.buffer_channel = self.bot.get_channel(id=434551085185630218)
        self.loaded_maps = {}

    """
        #################################
        # General use commands for maps #
        #################################
    """

    @group(name="map", invoke_without_command=True, usage="[map file or link]")
    async def _map(self, ctx: Context, arg: str = "", *, args: str = ""):
        """The base command for working with maps

        This command will retrieve map information
        on a loaded map.

        This command will load a map if you do not
        already have one loaded.

        See `[p]help map load` for more information
        on loading maps."""

        if ctx.author.id in self.loaded_maps.keys():

            # Subcommands available to ctx.author
            avail_cmds = []

            for cmd in ctx.command.walk_commands():
                if await cmd.can_run(ctx):
                    avail_cmds.append(cmd.name)

            if arg in avail_cmds:
                ctx.message.content = f"{self._inv(ctx)} {arg} {args}"
                ctx = await self.bot.get_context(ctx.message)
                await self.bot.invoke(ctx)

            else:
                ctx.message.content = f"{self._inv(ctx)} info {arg} {args}"
                ctx = await self.bot.get_context(ctx.message)
                await self.bot.invoke(ctx)

        else:
            ctx.message.content = f"{self._inv(ctx)} load {arg} {args}"
            ctx = await self.bot.get_context(ctx.message)
            await self.bot.invoke(ctx)

    @_map.command(name="load", usage="[title]")
    async def _load(self, ctx: Context, title: str = None, *, _: str = ""):
        """Load a map to be worked with

        This command will load a map to be worked with.

        You can load maps through 5 different methods:
        __AWS File__
        `[p]map load` with attached AWS file

        __AWBW Map CSV File__
        `[p]map load` with attached CSV or TXT file

        __AWBW Map Link__
        `[p]map load http://awbw.amarriner.com/prevmaps.php?maps_id=12345`

        __AWBW Map ID__
        `[p]map load 12345`

        __AWBW Text Style Map__
        `[p]map load "Example Map"`
        ```
        1,2,3,4,5
        6,7,8,9,10
        11,12,13,14,15
        16,17,18,19,20
        21,22,23,24,25
        ```
        For text maps, the first line must be the title.
        For multi-word titles, you must wrap the title
        in quotes (" " or ' '). You will receive an
        error if not all rows have the same number of
        columns. Markdown boxes are not necessary, but
        may make typing text maps easier.

        Text maps stay loaded as long as you're working
        with them with a time out of 5 minutes. After 5
        minutes with no activity, maps are automatically
        unloaded and must loaded again to continue
        working with them."""

        awmap = await CheckMap.check(ctx.message, title, verify=True)

        if not awmap:
            raise InvalidMapError

        await self.em_load(ctx.channel, awmap)
        await self.timed_store(ctx.author, awmap)

    @_map.group(name="draw", invoke_without_command=False, aliases=["mod"])
    async def draw(self, ctx: Context):
        pass

    @draw.command(name="terr", aliases=["terrain"])
    async def terr(self, ctx: Context, terr: Union[str, int], ctry: Union[str, int], *, coords):
        """Modifies the terrain contents of a list of tiles

        """
        awmap = self.get_loaded_map(ctx.author)
        if not awmap:
            raise NoLoadedMapError
        coords = [list(coords.split(" ")[i:2 + i]) for i in range(0, len(coords.split(" ")), 2)]
        terr, ctry = int(terr), int(ctry)
        for coord in coords:
            x, y = coord
            x, y = int(x), int(y)
            awmap.mod_terr(x, y, terr, ctry)
        await self.timed_store(ctx.author, awmap)
        await self.em_load(ctx.channel, awmap)

    @_map.command(name="download", usage=" ")
    async def download(self, ctx: Context, *, _: str = ""):
        """Download your currently loaded map

        Use this command when you have a map loaded
        with `[p]map load` to get download links for
        all supported map formats.

        Currently supported formats:
        `Advance Wars Series Map Editor File (*.aws)`
        `Advance Wars By Web Text Map File (*.csv)`"""
        awmap = self.get_loaded_map(ctx.author)

        if not awmap:
            raise NoLoadedMapError

        await self.em_download(ctx.channel, awmap)

    @_map.command(name="info", usage=" ", enabled=False)
    async def info(self, ctx: Context, *, _: str = ""):
        """Something something information about a map"""
        raise UnimplementedError

    """
        ##########################
        # Some Utility Functions #
        ##########################
    """

    async def timed_store(self, user: Member, awmap: AWMap) -> None:
        """
        Stores an AWMap by user ID with a expiry of 5 minutes.
        Loading another map will reset the timer. Stored in
        `self.loaded_maps` dict with structure:

        user.id: {
            "awmap": awmap,
            "ts": datetime.utcnow()
            }

        :param user: `discord.Member` instance of command author
        :param awmap: `AWMap` instance of map loaded by `user`

        :returns: `None` (Does not return)
        """
        ts = datetime.utcnow()
        self.loaded_maps[user.id] = {
            "ts": ts,
            "awmap": awmap
        }
        await sleep(300)
        if self.loaded_maps[user.id]["ts"] == ts:
            self.loaded_maps.pop(user.id)

    def get_loaded_map(self, user: Member) -> Union[AWMap, None]:
        """Will retrieve loaded map object for a given user.

        :param user: `discord.Member` instance for user

        :return: `AWMap` object or `None` if no loaded map"""

        store = self.loaded_maps.get(user.id)

        if store:
            return store["awmap"]
        else:
            return None

    async def get_hosted_file(self, file: File) -> str:
        """Sends a message to Discord containing a file to
        return the file URL hosted on Discord

        :param file: `discord.File` object containing the file to host

        :return: `str` URL of hosted file"""
        msg = await self.buffer_channel.send(file=file)
        return msg.attachments[0].url

    async def get_aws(self, awmap: AWMap) -> str:
        """Uses `AWMap`'s `to_aws` parameter to export a map
        as AWS file then returns a link to the file hosted on
        Discord using `get_hosted_file` method

        :param awmap: `AWMap` instance of map to export

        :return: `str` URL of hosted AWS file"""

        if awmap.title:
            title = awmap.title
        else:
            title = "Untitled"

        attachment = File(
            fp=BytesIO(awmap.to_aws),
            filename=f"{title}.aws"
        )
        url = await self.get_hosted_file(attachment)

        return url

    async def get_awbw(self, awmap: AWMap) -> str:
        """Uses `AWMap`'s `to_awbw` parameter to export a map
        to an AWBW CSV text file then returns a link to the file
        hosted on Discord using `get_hosted_file` method

        :param awmap: `AWMap` instance of map to export

        :return: `str` URL of hosted CSV file"""

        if awmap.title:
            title = awmap.title
        else:
            title = "Untitled"

        attachment = File(
            fp=BytesIO(awmap.to_awbw.encode("utf-8")),
            filename=f"{title}.csv"
        )
        url = await self.get_hosted_file(attachment)

        return url

    async def get_minimap(self, awmap: AWMap) -> str:
        """Uses `AWMap`'s `minimap` parameter to generate a
        `PIL` image of a minimap representing the loaded map
        then returns a link to the file hosted on Discord
        using the `get_hosted_file` method

        :param awmap: `AWMap` instance of map

        :return: `str` URL of hosted minimap image"""

        if awmap.title:
            title = awmap.title
        else:
            title = "[Untitled]"

        attachment = File(fp=awmap.minimap, filename=f"{title}.gif")
        url = await self.get_hosted_file(attachment)

        return url

    """
        ######################################
        # Some Special Utilities for Discord #
        ######################################
    """

    @staticmethod
    def _color(channel) -> int:
        if isinstance(channel, DMChannel):
            return 0
        else:
            return channel.guild.me.colour.value

    @staticmethod
    def _inv(ctx: Context) -> str:
        return f"{ctx.prefix}{ctx.invoked_with}"

    """
        #####################
        # Formatting Embeds #
        #####################
    """

    def base_embed(self, channel, awmap: AWMap) -> Embed:
        """Formats and returns the base embed with map
        title and author."""

        if awmap.awbw_id:
            a_url = f"http://awbw.amarriner.com/profile.php?username={quote(awmap.author)}"
            if awmap.author == "[Unknown]":
                author = "Design Map by [Unknown]"
            else:
                author = f"Design map by [{awmap.author}]({a_url})"
            m_url = f"http://awbw.amarriner.com/prevmaps.php?maps_id={awmap.awbw_id}"
        else:
            author = f"Design map by {awmap.author}"
            m_url = ""

        return Embed(color=self._color(channel), title=awmap.title, description=author, url=m_url)

    async def em_load(self, channel, awmap: AWMap):
        """Formats and sends an embed to `channel` appropriate
        for when a map is loaded for a user."""

        em = self.base_embed(channel, awmap)

        image_url = await self.get_minimap(awmap)
        em.set_image(url=image_url)

        return await channel.send(embed=em)

    async def em_download(self, channel, awmap: AWMap):
        """Formats and sends an embed to `channel` containing
        downloads for the supported map types."""

        em = self.base_embed(channel, awmap)

        aws = await self.get_aws(awmap)
        csv = await self.get_awbw(awmap)
        thumb = await self.get_minimap(awmap)

        em.add_field(name="Downloads", value=f"[AWS]({aws})\n[AWBW CSV]({csv})")
        em.set_thumbnail(url=thumb)

        return await channel.send(embed=em)

    """
        ###############################################
        # Administrative Commands for AdvanceWars cog #
        ###############################################
    """

    @sudo()
    @_map.command(name="listen", hidden=True, usage="[on / off]")
    async def listen(self, ctx: Context, *, arg: str = ""):
        """Toggle active listening for maps

        With this option turned on, all messages
        will be checked for valid maps. Messages
        containing valid maps will be treated as
        a `[p]map load` command."""

        if arg.strip(" ").lower() in "on yes true y t 1".split(" "):
            self.config.set("listen_for_maps", "True")
            self.listen_for_maps = True

        elif arg.strip(" ").lower() in "off no false n f 0".split(" "):
            self.config.set("listen_for_maps", "False")
            self.listen_for_maps = False

        em = Embed(color=self._color(ctx.channel), title="BattleMaps Config",
                   description=f"Active Listen For Maps: `{self.listen_for_maps}`")
        await ctx.send(embed=em)

    @sudo()
    @_map.command(name="viewallmaps", hidden=True, aliases=["vam"])
    async def viewallmaps(self, ctx: Context):
        """View all currently loaded maps.

        Administrative command that will display
        Map titles and user IDs for all currently
        loaded maps"""
        em = Embed(color=self._color(ctx.channel), title="All Currently Loaded Maps",
                   description="\n".join(f"{k} @ {v['ts']}: {v['awmap'].title}" for k, v in self.loaded_maps.items()))
        await ctx.send(embed=em)

    # @checks.sudo()
    # @_map.command(name="pull", hidden=True, aliases=["update"], enabled=False)
    # async def _pull(self, ctx: Context):
    #     """Update the converter core
    #
    #     Uses `pull` method to update AWSMapConvert
    #     and reload the cog."""
    #     if self.pull():
    #         await ctx.send("Converter core updated.\nReloading cog.")
    #         self.bot.remove_cog("AdvanceWars")
    #         self.bot.add_cog(AdvanceWars(self.bot))
    #     else:
    #         await ctx.send("Converter core already up-to-date.")

    # @staticmethod
    # def pull() -> bool:
    #     """Git Pull the AWSMapConverter. If it is not
    #     found, will Git Clone
    #
    #     :returns bool: `True` if converter was updated
    #     successfully. `False` otherwise"""
    #
    #     cmd = "git -C AWSMapConverter --no-pager pull".split(" ")
    #     params = {
    #         "universal_newlines":   True,
    #         "cwd":                  os.getcwd(),
    #         "stdout":               subprocess.PIPE,
    #         "stderr":               subprocess.STDOUT
    #     }
    #
    #     ret = subprocess.run(cmd, **params)
    #     if "up-to-date" in ret.stdout:
    #         return False
    #     elif "fatal: Not a git repository" in ret.stdout:
    #         cmd = "git clone --no-pager" \
    #               "https://github.com/SirThane/AWSMapConverter.git".split(" ")
    #         subprocess.run(cmd, **params)
    #         return True
    #     else:
    #         return True

    @Cog.listener("on_message")
    async def on_message(self, msg: Message):
        if self.listen_for_maps and not msg.author.bot:
            if not any([msg.content.startswith(prefix) for prefix in self.bot.command_prefix(self.bot, msg)]):
                awmap = await CheckMap.check(msg, skips=["msg_csv", "id"])
                if awmap:
                    await self.em_load(msg.channel, awmap)
                    await self.timed_store(msg.author, awmap)
예제 #18
0
class Player(Cog):
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "player")

        self.errorlog = bot.errorlog

        self.sessions = dict()

        self.bot.loop.create_task(self._init_all_sessions())
        self.bot.loop.create_task(self.cog_reload_cronjob(24 * 60 * 60))

    """ ##############################################
         Setup, Session Initialization, And Breakdown
        ############################################## """

    def get_session(self, guild: Guild) -> Optional[Session]:
        return self.sessions.get(guild)

    async def init_session(self,
                           guild: Guild,
                           voice: VoiceChannel,
                           log: TextChannel = None,
                           run_forever: bool = True,
                           **session_config):
        session = Session(self.bot,
                          self.config,
                          self,
                          voice,
                          log=log,
                          run_forever=run_forever,
                          **session_config)

        # Add session to sessions and start voice
        self.sessions[guild] = session
        await session.session_task()

        # When voice has ended, disconnect and remove session
        await session.voice.disconnect()
        self.sessions.pop(guild)

    async def _init_all_sessions(self):
        """Read configs from db and init all sessions"""

        # Cannot start sessions before bot is logged in and ready
        await self.bot.wait_until_ready()

        for init_session_config in self.config.scan_iter("sessions*"):
            session_config = self.config.hgetall(init_session_config)

            guild = self.bot.get_guild(int(session_config.pop("guild_id")))
            voice = self.bot.get_channel(int(session_config.pop("voice")))

            l_id = session_config.pop("log", None)
            if l_id:
                log = self.bot.get_channel(int(l_id))
            else:
                log = None

            self.bot.loop.create_task(
                self.init_session(guild,
                                  voice,
                                  log=log,
                                  run_forever=True,
                                  **session_config))

    async def cog_reload_cronjob(self, secs: int):
        """Async background task added in `__init__` to reload cog after `secs` seconds"""

        await sleep(secs)
        self.bot.remove_cog("Player")

        # Small delay to avoid race condition
        await sleep(2)
        self.bot.add_cog(Player(self.bot))

    def cog_unload(self):
        """Stop voice on all sessions to cleanly leave session loop and disconnect voice"""
        for session in self.sessions.values():
            session.stop()

    """ ##################
         Requesting Songs
        ################## """

    @group(name="request",
           aliases=["play"],
           invoke_without_command=True,
           enabled=True)
    @check(user_is_in_voice_channel)
    @check(user_has_required_permissions)
    async def request(self, ctx: Context, *, request):
        """Adds a YouTube video to the requests queue.

        request: YouTube search query.
        """

        if isinstance(request, str):
            try:
                request = YouTubeTrack(request,
                                       self.config,
                                       requester=ctx.author)
            except Exception as error:
                await self.bot.errorlog.send(error, ctx)
                raise CommandError(
                    f"An error occurred trying to load YouTubeTrack `{request}`"
                )

        session = self.get_session(ctx.guild)

        if session is None:
            session = self.sessions[ctx.guild] = Session(
                self.bot, self.config, self, ctx.author.voice.channel)

        await ctx.send(**request.request_message)
        session.queue.add_request(request)

    @request.command(name="mp3")
    async def request_mp3(self, ctx: Context, *, request):
        """Adds a local MP3 file to the requests queue.

        request: Local track search query.
        """

        try:
            request = MP3Track(request, config=self.config)
        except Exception as error:
            await self.bot.errorlog.send(error, ctx)
            raise CommandError(
                f"An error occurred trying to load MP3Track `{request}`")

        await ctx.invoke(self.request, request=request)

    @request.command(name="youtube")
    async def request_youtube(self, ctx: Context, *, request):
        """Adds a YouTube video to the requests queue.

        request: YouTube search query.
        """

        try:
            request = YouTubeTrack(request, self.config, requester=ctx.author)
        except Exception as error:
            await self.bot.errorlog.send(error, ctx)
            raise CommandError(
                f"An error occurred trying to load YouTubeTrack `{request}`")

        await ctx.invoke(self.request, request=request)

    """ ################
         Queue Commands
        ################ """

    @command(name="skip")
    @check(session_is_running)
    @check(user_is_listening)
    async def skip(self, ctx: Context):
        """Skips the currently playing track."""

        session = self.get_session(ctx.guild)

        if ctx.author in session.skip_requests:
            raise CommandError("You have already requested to skip.")

        session.skip_requests.append(ctx.author)
        skips_needed = len(list(session.listeners)) // 2 + 1

        if len(session.skip_requests) >= skips_needed:
            session.voice.stop()

        else:
            em = Embed(colour=Colour.dark_green(),
                       title="Skip video",
                       description=f"You currently need "
                       f"**{skips_needed - len(session.skip_requests)}** "
                       f"more votes to skip this track.")

            await ctx.send(embed=em)

    @command(name='repeat')
    @check(session_is_running)
    @check(user_is_listening)
    async def repeat(self, ctx: Context):
        """Repeats the currently playing track."""

        session = self.get_session(ctx.guild)

        if ctx.author in session.repeat_requests:
            raise CommandError('You have already requested to repeat.')

        session.repeat_requests.append(ctx.author)
        repeats_needed = len(list(session.listeners)) // 2 + 1

        if len(session.repeat_requests) >= repeats_needed:
            session.queue.add_request(session.current_track, at_start=True)

        else:
            em = Embed(colour=Colour.dark_green(),
                       title='Repeat track',
                       description=f'You currently need '
                       f'**{repeats_needed - len(session.repeat_requests)}** '
                       f'more votes to repeat this track.')

            await ctx.send(embed=em)

    @command(name='playing', aliases=['now'])
    @check(session_is_running)
    async def playing(self, ctx: Context):
        """Retrieves information on the currently playing track."""

        session = self.get_session(ctx.guild)

        play_time = session.current_track.play_time
        track_length = session.current_track.length

        play_time_str = str(timedelta(seconds=play_time))
        length_str = str(timedelta(seconds=track_length))

        seek_length = 50
        seek_distance = round(seek_length * play_time / track_length)

        message = session.current_track.playing_message
        message['embed'].add_field(
            name=f'{play_time_str} / {length_str}',
            value=
            f'`{"-" * seek_distance}|{"-" * (seek_length - seek_distance)}`',
            inline=False)

        await ctx.send(**message)

    @command(name="queue", aliases=["upcoming"])
    @check(session_is_running)
    async def queue(self, ctx: Context):

        session = self.get_session(ctx.guild)

        em = Embed(colour=Colour.dark_green(), title="Upcoming requests")

        for index, track in enumerate(session.queue.requests[:10], 1):
            em.add_field(name=f"{index} - Requested by {track.requester}",
                         value=track.information)

        if not em.fields:
            em.description = "There are currently no requests"

        await ctx.send(embed=em)

    """ ################
         Admin Commands
        ################ """

    @sudo()
    @command(name="stop")
    async def stop(self, ctx: Context):
        session = self.get_session(ctx.guild)
        if session:
            session.stop()

    @sudo()
    @command(name="start")
    async def start(self, ctx: Context):

        session = self.get_session(ctx.guild)
        if session:
            session.stop()
            await sleep(0.5)

        session_config = self.config.hgetall(f"sessions:{ctx.guild.id}")

        if session_config:

            guild = self.bot.get_guild(int(session_config.pop("guild_id")))
            voice = self.bot.get_channel(int(session_config.pop("voice")))

            l_id = session_config.pop("log", None)
            if l_id:
                log = self.bot.get_channel(int(l_id))
            else:
                log = None

            self.bot.loop.create_task(
                self.init_session(guild,
                                  voice,
                                  log=log,
                                  run_forever=True,
                                  **session_config))

        else:
            raise CommandError(f"Player not configured for {ctx.guild.name}")

    """ ########
         Events
        ######## """

    @Cog.listener()
    async def on_voice_state_update(self, member: Member, _: VoiceState,
                                    after: VoiceState):

        session = self.get_session(member.guild)

        if session is not None:
            if after is None and member in session.skip_requests:
                session.skip_requests.remove(member)

            if session.voice is not None:
                session.check_listeners()
예제 #19
0
class ModLogs(Cog):
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "modlog")

        self.errorlog = bot.errorlog

        # init a local cache of logged Guilds and their configs
        cache = dict()
        for key in self.config.scan_iter("guilds*"):
            *_, guild_id = key.split(":")
            try:
                cache[int(guild_id)] = self.config.hgetall(key)
            except TypeError:
                # Guild ID not found
                self.config.delete(key)

        self._config_cache = cache

    @property
    def active_guilds(self) -> List[int]:  # TODO: Use this
        return list(self._config_cache.keys())

    def _is_tracked(self, guild: Guild, priority_event: bool):
        """Perform a simple check before running each event so that we don't waste time trying to log"""
        if not guild:  # DMs
            return False
        elif guild.id not in self._config_cache.keys():
            return False
        elif priority_event and self._config_cache[guild.id].get(
                "priority_modlog") is None:
            return False
        elif not priority_event and self._config_cache[guild.id].get(
                "default_modlog") is None:
            return False
        else:
            return True

    def _create_guild_config(self, guild: Guild):
        config = {
            "priority_modlog": "None",
            "default_modlog": "None",
        }

        self.config.hmset(f"guilds:{guild.id}", config)
        self._config_cache[int(guild.id)] = config
        return config

    def get_guild_config(self, guild: Guild):
        """
        Get the guild's config, or create it if it doesn't exist.

        Expected format should be:
        {
            "priority_modlog": "id",
            "default_modlog": "id",
        }

        either modlog can be `None`, which just results in the event being discarded.
        :param guild: The tracked guild
        :return: guild config dict, or None if it doesn't exist
        """

        try:
            return self._config_cache.get(guild.id)
        except KeyError:
            # Let's just build it up anyway
            return self.config.hgetall(f"guilds:{guild.id}")

    def em_base(self, user: Union[Member, User], log_title: str,
                color: int) -> Embed:
        """Do basic formatting on the embed"""

        em = Embed(description=f"*{log_title}*", color=color)

        user_repr = f"{user.name}#{user.discriminator}   (ID: {user.id})"
        em.set_author(name=user_repr, icon_url=user.avatar_url)

        em.set_footer(text=self._get_timestamp())

        return em

    @staticmethod
    def _get_timestamp() -> str:
        """Returns a formatted timestamp based on server region"""

        dt = timezone("UTC").localize(
            datetime.utcnow()).strftime("%b. %d, %Y#%H:%M UTC")
        date, time = dt.split("#")
        return f"Event Timestamp: 📅 {date}  🕒 {time}"

    async def log_event(self,
                        embed: Embed,
                        guild: Guild,
                        priority: bool = False,
                        **kwargs) -> None:
        """Have to use this backwards-ass method because it throws http exceptions."""

        guild_config = self.get_guild_config(guild)

        if priority:
            priority_modlog = int(guild_config.get("priority_modlog", 0))
            dest = self.bot.get_channel(priority_modlog)
        else:
            default_modlog = int(guild_config.get("default_modlog", 0))
            dest = self.bot.get_channel(default_modlog)

        if not dest:
            return

        try:
            for i, page in enumerate(embed.split()):
                if i:
                    await sleep(0.1)
                await dest.send(embed=page, **kwargs)
        except HTTPException as error:
            await self.errorlog.send(error)

    async def _get_last_audit_action(
        self, guild: Guild, action: int, member: Union[Member, User]
    ) -> Tuple[bool, bool, Optional[User], Optional[str]]:
        """Find the first Audit Log entry matching the action type.

        Will only search up to 10 log entries and only up to 5 seconds ago.

        Returns Tuple

        bool:           If log entry was found
        bool:           If exception is encountered (to adjust embed message)
        Optional[User]: The moderator that used moderation action or None
        Optional[str]:  The reason given for moderation action or None"""

        # Allow time so audit logs will be available
        await sleep(0.5)

        # Only search last 10 seconds of audit logs
        timeframe = datetime.utcnow() - timedelta(seconds=10.0)

        try:

            # Only search last 10 audit log entries
            # Action should be at the top of the stack
            for log_entry in await guild.audit_logs(
                    action=action, limit=10, oldest_first=False).flatten():

                # after kwarg of Guild.audit_logs does not appear to work
                # Manually compare datetimes
                if log_entry.target.id == member.id and log_entry.created_at > timeframe:

                    # Get mod and reason
                    # Should always get mod
                    # Reason is optional
                    mod = getattr(log_entry, "user", None)
                    reason = getattr(log_entry, "reason", None)

                    return True, False, mod, reason

            # Could not find audit log entry
            # member_remove was voluntary leave
            else:
                return False, False, None, None

        # Do not have access to audit logs
        except Forbidden as error:
            print(error)
            return False, True, None, None

        # Catch any unknown errors and log them
        # We need this method to return so event still logs
        except Exception as error:
            await self.errorlog.send(error)
            return False, True, None, None

    """ ###################
         Registered Events
        ################### """

    @Cog.listener(name="on_member_ban")
    async def on_member_ban(self, guild: Guild, user: Union[Member, User],
                            *args):
        """Event called when a user is banned.
        User does not need to currently be a member to be banned."""

        if not self._is_tracked(guild, EventPriority.ban):
            return

        # Event is sometimes called with 3 arguments
        # Capture occurrence
        await self.errorlog.send(
            Exception(f"Additional arguments sent to `on_member_ban`: {args}"))

        em = self.em_base(user,
                          f"User {user.mention} ({user.name}) was banned",
                          EventColors.ban.value)

        # Attempt to retrieve unban reason and mod that unbanned from Audit Log
        found, errored, mod, reason = await self._get_last_audit_action(
            guild, AuditLogAction.ban, user)

        # Audit log action found
        # Add details
        if found and not errored:
            em.add_field(
                name="Banned By",
                value=f"{mod.mention}\n({mod.name}#{mod.discriminator})")
            em.add_field(
                name="Reason",
                value=reason if reason is not None else "No reason given")

        # Cannot access audit log or HTTP error prevented access
        elif errored and not found:
            em.add_field(name="Banned By",
                         value="Unknown\nAudit Log inaccessible")
            em.add_field(name="Reason",
                         value="Irretrievable\nAudit Log inaccessible")

        # No audit log entry found for ban
        else:
            em.add_field(name="Banned By",
                         value="Unknown\nAudit Log missing data")
            em.add_field(
                name="Reason",
                value="Irretrievable\nAudit Log missing data or no reason given"
            )

        # If banned user was a member of the server, capture roles
        if isinstance(user, Member):
            roles = "\n".join([
                f"{role.mention} ({role.name})"
                for role in sorted(user.roles, reverse=True)
                if role.name != "@everyone"
            ])
            em.add_field(name="Roles",
                         value=roles if roles else "User had no roles")

        await self.log_event(em, guild, priority=EventPriority.ban)

    @Cog.listener(name="on_member_unban")
    async def on_member_unban(self, guild: Guild, user: User, *args):
        """Event called when a user is unbanned"""

        if not self._is_tracked(guild, EventPriority.unban):
            return

        # Event is sometimes called with 3 arguments
        # Capture occurrence
        await self.errorlog.send(
            Exception(
                f"Additional arguments sent to `on_member_unban`: {args}"))

        em = self.em_base(user,
                          f"User {user.mention} ({user.name}) was unbanned",
                          EventColors.unban.value)

        # Attempt to retrieve unban reason and mod that unbanned from Audit Log
        found, errored, mod, reason = await self._get_last_audit_action(
            guild, AuditLogAction.unban, user)

        # Audit log action found
        # Add details
        if found and not errored:
            em.add_field(
                name="Unbanned By",
                value=f"{mod.mention}\n({mod.name}#{mod.discriminator})")
            em.add_field(
                name="Reason",
                value=reason if reason is not None else "No reason given")

        # Cannot access audit log or HTTP error prevented access
        elif errored and not found:
            em.add_field(name="Unbanned By",
                         value="Unknown\nAudit Log inaccessible")
            em.add_field(name="Reason",
                         value="Irretrievable\nAudit Log inaccessible")

        # No audit log entry found for ban
        else:
            em.add_field(name="Unbanned By",
                         value="Unknown\nAudit Log missing data")
            em.add_field(
                name="Reason",
                value="Irretrievable\nAudit Log missing data or no reason given"
            )

        await self.log_event(em, guild, priority=EventPriority.unban)

    @Cog.listener(name="on_member_join")
    async def on_member_join(self, member: Member):
        """Event called when a member joins the guild"""

        if not self._is_tracked(member.guild, EventPriority.join):
            return

        em = self.em_base(member,
                          f"User {member.mention} ({member.name}) joined",
                          EventColors.join.value)

        em.add_field(name="Account Creation Timestamp",
                     value=self._get_timestamp())

        await self.log_event(em, member.guild, priority=EventPriority.join)

    @Cog.listener(name="on_member_remove")
    async def on_member_remove(self, member: Member):
        """Event called when a member is removed from the guild

        This event will be called if the member leaves, is kicked, or is banned"""

        if not self._is_tracked(member.guild, EventPriority.leave):
            return

        # Stop if ban. Will be handled in on_member_ban
        found, *_ = await self._get_last_audit_action(member.guild,
                                                      AuditLogAction.ban,
                                                      member)
        if found:
            return

        # Attempt to retrieve kic reason and mod that kicked from Audit Log
        found, errored, mod, reason = await self._get_last_audit_action(
            member.guild, AuditLogAction.kick, member)

        # Kick found in audit log
        if found and not errored:
            leave_type = EventPriority.kick

            em = self.em_base(
                member, f"User {member.mention} ({member.name}) was kicked",
                EventColors.kick.value)

            em.add_field(
                name="Kicked By",
                value=f"{mod.mention}\n({mod.name}#{mod.discriminator})")

            em.add_field(name="Reason",
                         value=reason if reason else "No reason given")

        # Cannot access audit log or HTTP error prevented access
        elif errored and not found:
            print("errored and not found")
            leave_type = EventPriority.kick
            em = self.em_base(member,
                              f"User {member.name} may have been kicked",
                              EventColors.kick.value)
            em.description = f"{em.description}\n\nAudit Log inaccessible\n" \
                             f"Unable to determine if member remove was kick or leave"
            em.add_field(name="Kicked By",
                         value="Unknown\nAudit Log inaccessible")
            em.add_field(name="Reason",
                         value="Irretrievable\nAudit Log inaccessible")

        # Successfully accessed audit log and found no kick
        # Presume voluntary leave
        else:
            leave_type = EventPriority.leave

            em = self.em_base(member,
                              f"User {member.mention} ({member.name}) left",
                              EventColors.kick.value)

        roles = "\n".join([
            f"{role.mention} ({role.name})"
            for role in sorted(member.roles, reverse=True)
            if role.name != "@everyone"
        ])

        em.add_field(name="Roles",
                     value=roles if roles else "User had no roles")

        await self.log_event(em, member.guild, priority=leave_type)

    @Cog.listener(name="on_message_delete")
    async def on_message_delete(self, msg: Message):
        """Event called when a message is deleted"""

        if not self._is_tracked(msg.guild, EventPriority.delete):
            return

        modlog_channels = [
            int(channel_id)
            for channel_id in self.get_guild_config(msg.guild).values()
        ]

        # If message deleted from modlog, record event with header only
        if msg.channel.id in modlog_channels:

            description = f"\n\n{msg.embeds[0].description}" if msg.embeds else ""
            description = escape_markdown(
                description.replace("Modlog message deleted\n\n", ""))

            em = self.em_base(msg.author,
                              f"Modlog message deleted{description}",
                              EventColors.delete.value)

            return await self.log_event(em, msg.guild, EventPriority.delete)

        # Otherwise, ignore bot's deleted embed-only (help pages, et.) messages
        elif msg.author.id == self.bot.user.id and not msg.content:
            return

        em = self.em_base(
            msg.author,
            f"Message by {msg.author.mention} ({msg.author.name}) deleted",
            EventColors.delete.value)

        em.description = f"{em.description}\n\nChannel: {msg.channel.mention} ({msg.channel.name})"

        if msg.content:
            chunks = [
                msg.content[i:i + 1024]
                for i in range(0, len(msg.content), 1024)
            ]
            for i, chunk in enumerate(chunks):
                em.add_field(name=f"🗑 Content [{i + 1}/{len(chunks)}]",
                             value=chunk)

        else:
            em.add_field(name="🗑 Content [0/0]",
                         value="Message had no content")

        # Try to re-download attached images if possible. The proxy url doesn't 404 immediately unlike the
        # regular URL, so it may be possible to download from it before it goes down as well.
        reupload = None

        if msg.attachments:
            temp_image = BytesIO()
            attachment = msg.attachments[0]
            if attachment.size > 5000000:
                # caching is important and all, but this will just cause more harm than good
                return

            try:
                await download_image(msg.attachments[0].proxy_url, temp_image)
                reupload = File(temp_image,
                                filename="reupload.{}".format(
                                    attachment.filename))

                em.description = f"{em.description}\n\n**Attachment Included Above**"

            except Exception as error:
                await self.errorlog.send(error)
                reupload = None

                em.description = f"{em.description}\n\n**Attachment Reupload Failed (See Error Log)**"

        await self.log_event(em,
                             msg.guild,
                             priority=EventPriority.delete,
                             file=reupload)

    @Cog.listener(name="on_bulk_message_delete")
    async def on_bulk_message_delete(self, msgs: List[Message]):
        """Event called when messages are bulk deleted"""

        # Bulk delete event triggered with no messages or messages not found in cache
        if not msgs:
            return

        if not self._is_tracked(msgs[0].guild, EventPriority.delete):
            return

        # modlog_channels = [int(channel_id) for channel_id in self.get_guild_config(msgs[0].guild).values()]
        #
        # # If messages deleted from modlog, record event with headers only
        # if msgs[0].channel.id in modlog_channels:
        #
        #     description = f"\n\n{msg.embeds[0].description}" if msg.embeds else ""
        #     description = escape_markdown(description.replace("Modlog message deleted\n\n", ""))
        #
        #     em = self.em_base(
        #         msg.author,
        #         f"Modlog messages deleted{description}",
        #         EventColors.delete.value
        #     )
        #
        #     return await self.log_event(em, msg.guild, EventPriority.delete)

        em = self.em_base(self.bot.user, f"Messages bulk deleted",
                          EventColors.bulk_delete.value)

        em.description = f"{em.description}\n\nChannel: {msgs[0].channel.mention} ({msgs[0].channel.name})"

        for i, msg in enumerate(msgs):
            content = f"__Content:__ {escape_markdown(msg.content)}" if msg.content else "Message had no content"

            if msg.attachments:
                content = f"{content}\n__Attachments:__ {', '.join([file.filename for file in msg.attachments])}"

            if msg.embeds:
                embed = f"__Embed Title:__ {escape_markdown(msg.embeds[0].title)}\n" \
                        f"__Embed Description:__ {escape_markdown(msg.embeds[0].description)}"
                content = f"{content}\n{embed}"

            content = [
                content[i:1024 + i] for i in range(0, len(content), 1024)
            ]

            for page in content:
                em.add_field(
                    name=
                    f"{msg.author.name}#{msg.author.discriminator} [{i + 1}/{len(msgs)}]",
                    value=page)

        await self.log_event(em, msgs[0].guild, EventPriority.delete)

        # msgs_raw = list()
        #
        # for msg in msgs:
        #     msgs_raw.append(
        #         f"**__{msg.author.name}#{msg.author.discriminator}__** ({msg.author.id})\n"
        #         f"{escape_markdown(msg.content)}"
        #     )
        #
        # msg_stream = "\n".join(msgs_raw).split("\n")
        #
        # field_values = list()
        # current = ""
        #
        # for line in msg_stream:
        #
        #     if len(current) + len(line) < 1024:
        #         current = f"{current}\n{line}"
        #
        #     else:
        #         field_values.append(current)
        #         current = line
        #
        # else:
        #     field_values.append(current)
        #
        # total = len(field_values)
        # field_groups = [field_values[i:25 + i] for i in range(0, len(field_values), 25)]
        #
        # for n, field_group in enumerate(field_groups):
        #     page = em.copy()
        #     if len(field_groups) > 1:
        #         if n < 1:
        #             page.set_footer("")
        #         else:
        #             page.title = ""
        #             page.description = ""
        #             page.set_author(name="", url="", icon_url="")
        #
        #     for i, msg_raw in enumerate(field_group):
        #         page.add_field(
        #             name=f"🗑 Messages [{(n + 1) * (i + 1)}/{total}]",
        #             value=msg_raw
        #         )
        #
        #     await self.log_event(page, msgs[0].guild, EventPriority.delete)

    @Cog.listener(name="on_message_edit")
    async def on_message_edit(self, before: Message, after: Message):
        """Event called when a message is edited"""

        if not self._is_tracked(before.guild, EventPriority.edit):
            return

        if before.author.id == self.bot.user.id:
            return

        if before.content == after.content or isinstance(
                before.channel, DMChannel):
            return

        em = self.em_base(
            before.author,
            f"Message by {before.author.mention} ({before.author.name}) edited",
            EventColors.edit.value)

        em.description = f"{em.description}\n\nChannel: {before.channel.mention} ({before.channel.name})"

        if before.content:
            chunks = [
                before.content[i:i + 1024]
                for i in range(0, len(before.content), 1024)
            ]
            for i, chunk in enumerate(chunks):
                em.add_field(name=f"🗑 Before [{i + 1}/{len(chunks)}]",
                             value=chunk,
                             inline=False)
        else:
            em.add_field(name="🗑 Before [0/0]",
                         value="Message had no content",
                         inline=False)
        if after.content:
            chunks = [
                after.content[i:i + 1024]
                for i in range(0, len(after.content), 1024)
            ]
            for i, chunk in enumerate(chunks):
                em.add_field(name=f"💬 After [{i + 1}/{len(chunks)}]",
                             value=chunk,
                             inline=False)

        await self.log_event(em, before.guild, priority=EventPriority.edit)

    @Cog.listener(name="on_member_update")
    async def on_member_update(self, before: Member, after: Member):
        """Event called when a user's member profile is changed"""

        if not self._is_tracked(before.guild, EventPriority.update):
            return

        if before.name != after.name or before.discriminator != after.discriminator:
            em = self.em_base(
                after,
                f"Member {before.mention} ({before.name}#{before.discriminator}) "
                f"changed their name to {after.name}#{after.discriminator}",
                EventColors.name_change.value)

            await self.log_event(em,
                                 before.guild,
                                 priority=EventPriority.update)

        if before.roles != after.roles:
            added, removed = None, None

            for role in before.roles:
                if role not in after.roles:
                    removed = f"{role.mention}\n({role.name})"

            for role in after.roles:
                if role not in before.roles:
                    added = f"{role.mention}\n({role.name})"

            found, errored, mod, _ = await self._get_last_audit_action(
                after.guild, AuditLogAction.member_role_update, after)

            if added:

                em = self.em_base(
                    after,
                    f"Member {after.mention} ({after.name}) roles changed",
                    EventColors.role_added.value)

                em.add_field(name="Role Added", value=added)

                if found:
                    em.add_field(name="Mod Responsible",
                                 value=f"{mod.mention}\n({mod.name})")

                await self.log_event(em,
                                     before.guild,
                                     priority=EventPriority.update)

            if removed:

                em = self.em_base(
                    after,
                    f"Member {after.mention} ({after.name}) roles changed",
                    EventColors.role_removed.value)

                em.add_field(name="Role Removed", value=removed)

                if found:
                    em.add_field(name="Mod Responsible",
                                 value=f"{mod.mention}\n({mod.name})")

                await self.log_event(em,
                                     before.guild,
                                     priority=EventPriority.update)

        if before.nick != after.nick:

            if after.nick is None:
                em = self.em_base(
                    after,
                    f"Member {before.mention} ({before.name}) reset their nickname",
                    EventColors.nickname_change.value)

            else:
                em = self.em_base(
                    after,
                    f"Member {before.mention} ({before.name}) changed their nickname "
                    f"from {before.nick} to {after.nick}",
                    EventColors.nickname_change.value)

            await self.log_event(em,
                                 after.guild,
                                 priority=EventPriority.update)

    """ ##########
         Commands
        ########## """

    @sudo()
    @group(name="modlog", invoke_without_command=True)
    async def modlog(
            self,
            ctx: Context):  # TODO: List enabled guilds with their channels
        pass

    @sudo()
    @modlog.command(name="enable")
    async def enable(self, ctx: Context, *, guild: int = None):
        """Enable logging on a Guild

        You must also set default and/or priority log channels
        with `[p]modlog set (default/priority)`"""

        if guild is None:
            guild = ctx.guild
        else:
            guild = self.bot.get_guild(guild)

        if not guild:
            return await ctx.message.add_reaction("âš ")

        self._create_guild_config(guild)

        await ctx.message.add_reaction("✅")

    @sudo()
    @modlog.command(name="disable")
    async def disable(self, ctx: Context, guild: int = None):
        """Disable logging on a Guild

        Guild and its config will be removed from the database"""

        if guild is None:
            guild = ctx.guild
        else:
            guild = self.bot.get_guild(guild)

        if not guild:
            return await ctx.message.add_reaction("âš ")

        if guild.id not in self.active_guilds:
            return await ctx.message.add_reaction("âš ")

        self._config_cache.pop(guild.id)
        self.config.delete(f"guilds:{guild.id}")

        await ctx.message.add_reaction("✅")

    @sudo()
    @modlog.group(name="set", invoke_without_command=True)
    async def _set(self, ctx: Context):  # TODO: Show guilds and configs?
        pass

    @sudo()
    @_set.command(name="default")
    async def default(self,
                      ctx: Context,
                      *,
                      guild: int = None,
                      channel: int = None):
        """Set modlog Channel for "default" messages

        `guild` must be a tracked Guild
        `channel` does not necessarily need to be a Channel in `guild`"""

        if not guild:
            guild = ctx.guild
        else:
            guild = self.bot.get_guild(guild)
        if not guild:
            return await ctx.message.add_reaction("âš ")

        if guild.id not in self.active_guilds:
            return await ctx.message.add_reaction("âš ")

        if not channel:
            channel = ctx.channel
        else:
            channel = self.bot.get_channel(channel)
        if not channel:
            return await ctx.message.add_reaction("âš ")

        config = self.get_guild_config(guild)
        config["default_modlog"] = str(channel.id)

        self.config.hmset(f"guilds:{guild.id}", config)
        self._config_cache[guild.id] = config

        await ctx.message.add_reaction("✅")

    @sudo()
    @_set.command(name="priority")
    async def priority(self,
                       ctx: Context,
                       *,
                       guild: int = None,
                       channel: int = None):
        """Set modlog channel for "priority" messages

        `guild` must be a tracked Guild
        `channel` does not necessarily need to be a Channel in `guild`"""

        if not guild:
            guild = ctx.guild
        else:
            guild = self.bot.get_guild(guild)
        if not guild:
            return await ctx.message.add_reaction("âš ")

        if guild.id not in self.active_guilds:
            return await ctx.message.add_reaction("âš ")

        if not channel:
            channel = ctx.channel
        else:
            channel = self.bot.get_channel(channel)
        if not channel:
            return await ctx.message.add_reaction("âš ")

        config = self.get_guild_config(guild)
        config["priority_modlog"] = str(channel.id)

        self.config.hmset(f"guilds:{guild.id}", config)
        self._config_cache[guild.id] = config

        await ctx.message.add_reaction("✅")
예제 #20
0
파일: poll.py 프로젝트: SirThane/BattleMaps
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "poll")

        self.errorlog = bot.errorlog
예제 #21
0
파일: poll.py 프로젝트: SirThane/BattleMaps
class Poll(Cog):
    """User-created polls"""
    def __init__(self, bot: Bot):
        self.bot = bot
        self.config = SubRedis(bot.db, "poll")

        self.errorlog = bot.errorlog

    @check(has_permission)
    @group(name="poll", invoke_without_command=True)
    async def poll(self, ctx: Context):
        await ctx.send("Placeholder")

    """ #######
         Setup
        ####### """

    @check(has_permission)
    @poll.command(name="view")
    async def view(self, ctx: Context):
        pass

    @check(has_permission)
    @poll.command(name="create", aliases=["new"])
    async def create(self, ctx: Context, *, name: str):
        pass

    @check(has_permission)
    @poll.command(name="discard", aliases=["drop"])
    async def discard(self, ctx: Context):
        pass

    @check(has_permission)
    @poll.command(name="name", aliases=["title"])
    async def name(self, ctx: Context):
        pass

    @check(has_permission)
    @poll.command(name="add_option", aliases=["add"])
    async def add_option(self, ctx: Context):
        pass

    @check(has_permission)
    @poll.command(name="remove_option",
                  aliases=["remove", "delete_option", "delete", "rem", "del"])
    async def remove_option(self, ctx: Context):
        pass

    """ #########
         Running
        ######### """

    @check(has_permission)
    @poll.command(name="start")
    async def start(self, ctx: Context, *, channel: TextChannel):
        pass

    @check(has_permission)
    @poll.command(name="view")
    async def view(self, ctx: Context):
        pass

    """ ################
         Administrative
        ################ """

    @has_guild_permissions(manage_guild=True)
    @poll.command(name="add_role", hidden=True)
    async def add_role(self, ctx: Context, role: Role):

        if self.config.sismember(f"allowed_roles:{ctx.guild.id}", role.id):
            em = Embed(title="Polls Administration",
                       description=
                       f"{role.mention} is already allowed to conduct polls.",
                       color=Color.red())
            await ctx.send(embed=em, delete_after=5)

        else:
            self.config.sadd(f"allowed_roles:{ctx.guild.id}", role.id)
            em = Embed(
                title="Polls Administration",
                description=f"{role.mention} is now allowed to conduct polls.",
                color=Color.green())
            await ctx.send(embed=em, delete_after=5)

    @has_guild_permissions(manage_guild=True)
    @poll.command(name="remove_role", aliases=["rem_role"], hidden=True)
    async def remove_role(self, ctx: Context, role: Role):

        if self.config.sismember(f"allowed_roles:{ctx.guild.id}", role.id):
            self.config.srem(f"allowed_roles:{ctx.guild.id}", role.id)
            em = Embed(
                title="Polls Administration",
                description=
                f"{role.mention} is no longer allowed to conduct polls.",
                color=Color.green())
            await ctx.send(embed=em, delete_after=5)

        else:
            em = Embed(
                title="Polls Administration",
                description=f"{role.mention} is not allowed to conduct polls.",
                color=Color.red())
            await ctx.send(embed=em, delete_after=5)

    @has_guild_permissions(manage_guild=True)
    @poll.command(name="list_roles", aliases=["roles"], hidden=True)
    async def list_roles(self, ctx: Context):
        role_ids = sorted(
            self.config.smembers(f"allowed_roles:{ctx.guild.id}"))

        if role_ids:
            roles = "\n".join([
                ctx.guild.get_role(int(role_id)).mention
                for role_id in role_ids
            ])
            em = Embed(
                title="Polls Administration",
                description=
                f"The following roles are allowed to conduct polls:\n{roles}",
                color=Color.green())
            await ctx.send(embed=em, delete_after=5)

        else:
            em = Embed(
                title="Polls Administration",
                description=
                "There are currently no roles allowed to conduct polls.",
                color=Color.red())
            await ctx.send(embed=em, delete_after=5)