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 = {}
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 __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))
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
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 __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))
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
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()
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)
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
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
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)
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."))
: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"""
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)
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
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)
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()
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("✅")
def __init__(self, bot: Bot): self.bot = bot self.config = SubRedis(bot.db, "poll") self.errorlog = bot.errorlog
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)