async def helper_role_remove(self, ctx, role: RoleConv, *args): logger.info( "Removing automatically managed helper role '%s' for guild '%s' (%d)", role.name, ctx.guild.name, ctx.guild.id, ) if len(args) == 1: if args[0] == "-h": helper_role = self.bot.sql.roles.get_pingable_role_from_original( ctx.guild, role) if not helper_role: role = None else: role = discord.utils.get(ctx.guild.roles, id=helper_role.id) else: raise CommandFailed("Unknown argument") channels = self.bot.sql.roles.get_channels_from_role(ctx.guild, role) if not channels or not role: raise CommandFailed( "Role was not pingable or did not exist to begin with") await self.role_unpingable(ctx, role, *channels) await self.role_unjoinable(ctx, role) await role.delete()
async def perform_mute(self, ctx, member: MemberConv, minutes: int, reason: str = None): logger.info( "Muting user '%s' (%d) for %d minutes", member.name, member.id, minutes ) if minutes <= 0: # Since muting prevents members from responding or petitioning staff, # a timed release is mandatory. Otherwise they might be forgotten # and muted forever. raise CommandFailed() roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.mute is None: raise CommandFailed(content="No configured mute role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure("I don't have permission to mute this user") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Muted", minutes, reason, past=True) await self.bot.punish.mute(ctx.guild, member, reason) # If a delayed event, schedule a Navi task if minutes: await self.remove_roles( ctx, member, minutes, PunishAction.RELIEVE_MUTE, reason )
async def perform_unjail(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.jail is None: raise CommandFailed(content="No configured jail role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to unjail this user") if roles.jail_role not in member.roles: raise CommandFailed(content="User is not jailed") if member != ctx.author: if member.top_role == ctx.author.top_role: raise CommandFailed( content= "You can not unjail a user with the same role as you") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Released", minutes, reason, past=True) if minutes: await self.remove_roles(ctx, member, minutes, PunishAction.RELIEVE_JAIL, reason) else: await self.bot.punish.unjail(ctx.guild, member, reason)
async def perform_jail(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.jail is None: raise CommandFailed(content="No configured jail role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to jail this user") if roles.jail_role in member.roles: raise CommandFailed(content="User is already jailed") # Check that users top role is not the same as the requesters top role. if member != ctx.author: if member.top_role == ctx.author.top_role: raise CommandFailed( content="You can not jail a user with the same role as you" ) minutes = max(minutes, 0) reason = self.build_reason(ctx, "Jailed", minutes, reason) await self.bot.punish.jail(ctx.guild, member, reason) # If a delayed event, schedule a Navi task if minutes: await self.remove_roles(ctx, member, minutes, PunishAction.RELIEVE_JAIL, reason)
async def log_send(self, ctx, path: str, content: str, *attributes: str): """ Manually send a journal event to test logging channels. The content must be a single argument, wrapped in quotes if it has spaces, and you can specify a number of journal attributes in the form KEY=VALUE. """ if path == "/": raise CommandFailed(content="Cannot broadcast on /") journal_attributes = {} for attribute in attributes: try: key, value = attribute.split("=") journal_attributes[key] = value except ValueError: raise CommandFailed( content="All attributes must be in the form KEY=VALUE" ) logger.info( "Sending manual journal event: '%s' (attrs: %s)", content, journal_attributes, ) self.bot.get_broadcaster(path).send( "", ctx.guild, content, **journal_attributes )
async def check_hashsums(*hashsums): if not hashsums: raise CommandFailed() if not all( map(lambda h: len(h) == 40 and HEXADECIMAL_REGEX.match(h), hashsums)): raise CommandFailed(content="SHA1 hashes are 40 hex digits long.")
async def remind_me(self, ctx, when: str, *, message: str): """ Request the bot remind you in the given time. """ timestamp = dateparser.parse(when) now = datetime.now() if timestamp is None: embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( f"Unknown date specification: `{escape_backticks(when)}`") raise CommandFailed(embed=embed) if now > timestamp: # First, try to see if a naive time specification put it in the past new_timestamp = dateparser.parse(f"in {when}") if new_timestamp is None or now > new_timestamp: time_since = fancy_timedelta(now - timestamp) embed = discord.Embed(colour=discord.Colour.red()) embed.description = f"Specified date was in the past: {time_since} ago" raise CommandFailed(embed=embed) # Was successful, replace it timestamp = new_timestamp # Check time assert timestamp > now duration = timestamp - now time_since = fancy_timedelta(duration) if duration > MAX_REMINDER_DURATION: embed = discord.Embed(colour=discord.Colour.red()) embed.description = f"Specified date is too far away: {time_since}" raise CommandFailed(embed=embed) logger.info( "Creating self-reminder SendMessageTask for '%s' (%d): %r", ctx.author.name, ctx.author.id, message, ) # Create navi task embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name=f"Reminder made {time_since} ago") embed.description = f"You asked to be reminded of:\n\n{message}" embed.timestamp = now self.bot.add_tasks( SendMessageTask( self.bot, None, ctx.author, timestamp, None, ctx.author, embed=embed, metadata={ "type": "reminder", "message": message }, ))
async def unload(self, ctx, cogname: str): """ Unloads the named cog. """ logger.info("Cog unload requested: %s", cogname) if cogname in Reloader.MANDATORY_COGS: logger.info("Cog cannot be unloaded because it is mandatory") embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Cannot unload") embed.description = "Cog cannot be unloaded because it is mandatory" content = f"Unable to unload cog {cogname} because it is mandatory" self.journal.send( "unload/fail", ctx.guild, content, icon="cog", cogname=cogname, reason="mandatory", ) raise CommandFailed(embed=embed) try: self.unload_cog(cogname) except Exception as error: logger.error("Unloading cog %s failed", cogname, exc_info=error) if isinstance(error, KeyError): # For no such cog errors (error, ) = error.args embed = discord.Embed(colour=discord.Colour.red(), description=f"```\n{error}\n```") embed.set_author(name="Unload failed") content = f"Error while trying to unload cog {cogname}" self.journal.send( "unload/fail", ctx.guild, content, icon="cog", cogname=cogname, reason="error", error=error, ) raise CommandFailed(embed=embed) else: logger.info("Unloaded cog: %s", cogname) embed = discord.Embed(colour=discord.Colour.green(), description=f"```\n{cogname}\n```") embed.set_author(name="Unloaded") content = f"Successfully unloaded cog {cogname}" self.journal.send("unload", ctx.guild, content, icon="cog", cogname=cogname) await ctx.send(embed=embed)
async def pinghelpers(self, ctx): """Pings helpers if used in the respective channel""" cooldown_time = self.bot.config.helper_ping_cooldown logger.info( "User '%s' (%d) is pinging the helper role in channel '%s' in guild '%s' (%d)", ctx.author, ctx.author.mention, ctx.channel, ctx.guild, ctx.guild.id, ) pingable_channels = self.bot.sql.roles.get_pingable_role_channels( ctx.guild) # this will return an empty list if there is nothing. channel_role = [(channel, role) for channel, role in pingable_channels if channel == ctx.channel] if not channel_role: embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Failed to ping helper role.") embed.description = f"There is no helper role set for this channel." raise CommandFailed(embed=embed) channel_user = (ctx.channel.id, ctx.author.id) cooldown = self.cooldowns.get(channel_user) if mod_perm(ctx) or not cooldown or cooldown <= datetime.now(): self.cooldowns[channel_user] = datetime.now() + timedelta( seconds=cooldown_time) # This will loop over the dictionary and remove expired entries. key_list = list(self.cooldowns.keys()) for k in key_list: if self.cooldowns[k] < datetime.now(): del self.cooldowns[k] # channel[0] will be the first tuple in the list. there will only be one, since the # channel's id is a primary key (tb_pingable_role_channel in roles.py). channel[0][1] is the role. await ctx.send( f"{channel_role[0][1].mention}, {ctx.author.mention} needs help." ) elif cooldown > datetime.now(): # convert deltatime into string: Hh, Mm, Ss time_remaining = cooldown - datetime.now() embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Failed to ping helper role.") embed.description = f"You can ping the helper role for this channel again in {fancy_timedelta(time_remaining)}" raise CommandFailed(embed=embed)
async def role_unpingable(self, ctx, role: RoleConv, *channels: TextChannelConv): logger.info( "Making role '%s' not pingable in guild '%s' (%d), channel(s) [%s]", role.name, ctx.guild.name, ctx.guild.id, self.str_channels(channels), ) if not channels: raise CommandFailed() # See role_pingable for an explanation channel_role = zip( *self.bot.sql.roles.get_pingable_role_channels(ctx.guild)) pingable_channels = next(channel_role, set()) exempt_channels = [] with self.bot.sql.transaction(): for channel in channels: if channel in pingable_channels: self.bot.sql.roles.remove_pingable_role_channel( ctx.guild, channel, role) else: exempt_channels.append(channel) if exempt_channels: embed = discord.Embed(colour=discord.Colour.dark_grey()) embed.set_author( name="Failed to make role unpingable in channels: ") descr = StringBuilder(sep=", ") for channel in exempt_channels: descr.write(channel.mention) embed.description = str(descr) await ctx.send(embed=embed) if set(exempt_channels) == set(channels): raise CommandFailed() # Send journal event content = f"Role was set as not pingable in channels: {self.str_channels(channels)}, except {self.str_channels(exempt_channels)}" self.journal.send( "pingable/remove", ctx.guild, content, icon="role", role=role, channels=channels, )
async def check_role(self, ctx, role): embed = discord.Embed(colour=discord.Colour.red()) if role.is_default(): embed.description = "@everyone role cannot be assigned for this purpose" raise CommandFailed(embed=embed) special_roles = self.bot.sql.settings.get_special_roles(ctx.guild) if role in special_roles: embed.description = "Cannot assign the same role for multiple purposes" raise CommandFailed(embed=embed) embed = permissions.elevated_role_embed(ctx.guild, role, "warning") if embed is not None: await ctx.send(embed=embed)
async def delete_filter(bot, filters, location, text): logger.info("Removing %r from server filter for '%s' (%d)", text, location.name, location.id) try: with bot.sql.transaction(): if bot.sql.filter.delete_filter(location, text): filters[location].pop(text, None) logger.debug("Succesfully removed filter") else: logger.debug("Filter was not present, deletion failed") raise CommandFailed() except Exception as error: logger.error("Error deleting filter", exc_info=error) raise CommandFailed()
async def perform_focus(self, ctx, member, minutes, reason): roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.focus is None: raise CommandFailed(content="No configured focus role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure( "I don't have permission to focus this user") if roles.focus_role in member.roles: raise CommandFailed(content="User is already focused") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Focus", minutes, reason) await self.bot.punish.focus(ctx.guild, member, reason)
async def add_filter(cog, filters, location, level, text): logger.info( "Adding %r to server filter '%s' for '%s' (%d)", text, level.value, location.name, location.id, ) try: with cog.bot.sql.transaction(): if text in filters[location]: cog.bot.sql.filter.update_filter(location, level, text) else: cog.bot.sql.filter.add_filter(location, level, text) except Exception as error: logger.error("Error adding filter", exc_info=error) raise CommandFailed() else: filter = Filter(text) filters[location][text] = (filter, level) if isinstance(location, discord.Guild): logger.debug("Checking all members against new guild text filter") cog.bot.loop.create_task( check_all_members_on_filter(cog, location, filter))
async def add_alt(self, ctx, first_user: UserConv, second_user: UserConv): """ Add a suspected alternate account for a user. """ logger.info( "Adding suspected alternate account pair for '%s' (%d) and '%s' (%d)", first_user.name, first_user.id, second_user.name, second_user.id, ) if first_user == second_user: embed = discord.Embed(colour=discord.Colour.red()) embed.description = "Both users are the same person!" raise CommandFailed(embed=embed) with self.bot.sql.transaction(): self.bot.sql.alias.add_possible_alt(ctx.guild, first_user, second_user) content = f"Added {first_user.mention} and {second_user.mention} as possible alt accounts." self.journal.send( "alt/add", ctx.guild, content, icon="item_add", users=[first_user, second_user], )
async def guestify(self, ctx): """ In the event that the bot is not properly assigning guest roles to users, this command can be used to manually apply it to all roleless users. """ roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.guest is None: prefix = self.bot.prefix(ctx.guild) embed = discord.Embed(colour=discord.Colour.red()) embed.description = ( f"No guest role set.\nYou can assign one using `{prefix}guest <role>`." ) raise CommandFailed(embed=embed) tasks = [] for member in ctx.guild.members: if member.top_role == ctx.guild.default_role: tasks.append( member.add_roles(roles.guest, reason="Manually assigning guest role", atomic=True)) await asyncio.gather(*tasks) embed = discord.Embed(colour=discord.Colour.dark_teal()) if tasks: embed.description = f"Added the {roles.guest.mention} role to `{len(tasks)}` member{plural(len(tasks))}." else: embed.description = "No roleless members found." await ctx.send(embed=embed)
async def unmute( self, ctx, member: MemberConv, minutes: int = 0, *, reason: str = None ): """ Unmutes the user, with an optional delay in minutes. Requires a mute role to be configured. Set 'minutes' to 0 to unmute immediately. """ logger.info( "Unmuting user '%s' (%d) in %d minutes", member.name, member.id, minutes ) roles = self.bot.sql.settings.get_special_roles(ctx.guild) if roles.mute is None: raise CommandFailed(content="No configured mute role") if member.top_role >= ctx.me.top_role: raise ManualCheckFailure("I don't have permission to unmute this user") minutes = max(minutes, 0) reason = self.build_reason(ctx, "Unmuted", minutes, reason, past=True) if minutes: await self.remove_roles( ctx, member, minutes, PunishAction.RELIEVE_MUTE, reason ) else: await self.bot.punish.unjail(ctx.guild, member, reason)
async def cleanup_user_cancel(self, ctx, code: str): """ Cancels a deletion code. """ try: print(self.delete_codes, code) guild, user, flag = self.delete_codes[code] # Check if the guilds match if guild != ctx.guild: raise KeyError except KeyError: embed = discord.Embed(colour=discord.Colour.red()) embed.title = "Complete user message purge" embed.description = ( "Invalid code provided, no deletion queue exists with that value." ) raise CommandFailed(embed=embed) # Actually perform the deletion flag.set() del self.delete_codes[code] # Send result embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.title = "Complete user message purge" embed.description = f"Deletion associated with {user.mention} cancelled." await ctx.send(embed=embed)
async def log_remove(self, ctx, path: str, channel: TextChannelConv): """ Removes a journal logger for the given path from the channel. """ logger.info( "Removing journal logger for channel #%s (%d) from path '%s'", channel.name, channel.id, path, ) listener = self.router.get(path, channel=channel) if listener is None: # No listener found raise CommandFailed( content=f"No output on `{path}` found for {channel.mention}" ) self.router.unregister(listener) with self.bot.sql.transaction(): self.bot.sql.journal.delete_journal_output(ctx.guild, channel, path) await channel.send(content=self.log_updated_message(channel)) content = f"Removed journal logger to {channel.mention} for `{path}`" self.journal.send( "channel/remove", ctx.guild, content, icon="journal", channel=channel, path=path, )
async def log_dm_remove(self, ctx, path: str): """ Removes a DM journal logger for the given path. """ logger.info( "Removing journal logger for user '%s' (%d) from path '%s'", ctx.author.name, ctx.author.id, path, ) user = self.bot.get_user(ctx.author.id) listener = self.router.get(path, user=user) if listener is None: # No listener found raise CommandFailed( content=f"No output on `{path}` found for {user_discrim(ctx.author)}" ) self.router.unregister(listener) with self.bot.sql.transaction(): self.bot.sql.journal.delete_journal_output(ctx.guild, user, path) await ctx.send(content=self.log_updated_message(user)) content = f"Removed journal logger to {user_discrim(ctx.author)} for `{path}`" self.journal.send( "user/remove", ctx.guild, content, icon="journal", user=ctx.author, path=path, )
async def channel_set(self, ctx, *channels: TextChannelConv): """ Overwrites the channel(s) in the restricted role channel list to exactly this. """ logger.info( "Setting channels to be used for role commands in guild '%s' (%d): [%s]", ctx.guild.name, ctx.guild.id, ", ".join(channel.name for channel in channels), ) if not channels: raise CommandFailed() # Write new channel list to database with self.bot.sql.transaction(): self.bot.sql.roles.remove_all_role_command_channels(ctx.guild) for channel in channels: self.bot.sql.roles.add_role_command_channel(ctx.guild, channel) # Send response embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Set channels to be used for adding roles") descr = StringBuilder(sep=", ") for channel in channels: descr.write(channel.mention) embed.description = str(descr) await ctx.send(embed=embed) # Send journal event self.channel_journal(ctx.guild)
async def prune(self, ctx, days: int = 7): """ Prunes users that have not used the !agree command for at least the given number of days. Defaults to seven days. """ pruned_members = await self.prune_member(ctx, days) # Check if prune_members is None as if it is there is not member role set # If there is no member role set pruning members makes no sense if pruned_members is None: error_message = ( "The server has no member role set, so pruning will have no effect" ) embed = discord.Embed( description=error_message, colour=discord.Colour.red() ) raise CommandFailed(embed=embed) content = f"Pruned {pruned_members} members" embed = discord.Embed(description=content, colour=discord.Colour.dark_teal()) await ctx.send(embed=embed) self.journal.send( "prune", ctx.guild, content, icon="snip", cause=ctx.author, members=pruned_members, )
async def filter_immunity_remove(self, ctx, *members: discord.Member): """ Removes a set of users from the server filter immunity list. """ if not members: raise CommandFailed() member_names = ", ".join( (f"'{member.name}' ({member.id})" for member in members)) logger.info( "Removing members to guild '%s' (%d) filter immunity list: %s", ctx.guild.name, ctx.guild.id, member_names, ) with self.bot.sql.transaction(): for member in members: logger.debug( "Removing member to filter immune: %s (%d)", member.display_name, member.id, ) self.bot.sql.filter.remove_filter_immune_user( ctx.guild, member) for member in members: content = f"Removed {member.name}#{member.discriminator} from filter immunity list" self.journal.send("immunity/remove", ctx.guild, content, icon="person")
async def role_unjoinable(self, ctx, *roles: RoleConv): """ Allows a moderator to remove roles from the self-assignable group. """ logger.info( "Removing joinable roles for guild '%s' (%d): [%s]", ctx.guild.name, ctx.guild.id, ", ".join(role.name for role in roles), ) if not roles: raise CommandFailed() # Remove roles from database with self.bot.sql.transaction(): for role in roles: self.bot.sql.roles.remove_assignable_role(ctx.guild, role) # Send response embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Made roles not joinable") descr = StringBuilder(sep=", ") for role in roles: descr.write(role.mention) embed.description = str(descr) await ctx.send(embed=embed) # Send journal event content = f"Roles were set as not joinable: {self.str_roles(roles)}" self.journal.send("joinable/remove", ctx.guild, content, icon="role", roles=roles)
def get_flags(flags): recursive = True for flag in flags: if flag == "-exact": recursive = False else: raise CommandFailed(content=f"No such flag: `{flag}`") return recursive
async def log_rename( self, ctx, old_channel: TextChannelConv, new_channel: TextChannelConv, path: str, *flags: str, ): """ Moves a journal logger from one channel to another. Accepts the optional flags: -exact, Don't recursively accept journal events from children. """ logger.info( "Moving journal logger from channel #%s (%d) to #%s (%d) for path '%s'", old_channel.name, old_channel.id, new_channel.name, new_channel.id, path, ) recursive = self.get_flags(flags) listener = self.router.get(path, channel=old_channel) if listener is None: # No listener found at old channel raise CommandFailed( content=f"No output on `{path}` found for {old_channel.mention}" ) listener.channel = new_channel logger.debug("Updating database for moved channel output") with self.bot.sql.transaction(): self.bot.sql.journal.delete_journal_output(ctx.guild, old_channel, path) self.bot.sql.journal.add_journal_output( ctx.guild, new_channel, path, recursive ) await asyncio.gather( old_channel.send(content=self.log_updated_message(old_channel)), new_channel.send(content=self.log_updated_message(new_channel)), ) content = f"Moved journal logger from {old_channel.mention} to {new_channel.mention} for `{path}`" self.journal.send( "channel/move", ctx.guild, content, icon="journal", old_channel=old_channel, new_channel=new_channel, path=path, recursive=recursive, )
async def role_joinable(self, ctx, *roles: RoleConv): """ Allows a moderator to add roles to the self-assignable group. """ logger.info( "Adding joinable roles for guild '%s' (%d): [%s]", ctx.guild.name, ctx.guild.id, ", ".join(role.name for role in roles), ) if not roles: raise CommandFailed() # Get special roles special_roles = self.bot.sql.settings.get_special_roles(ctx.guild) # Ensure none of the roles grant any permissions for role in roles: embed = permissions.elevated_role_embed(ctx.guild, role, "error") if embed is not None: raise ManualCheckFailure(embed=embed) for attr in ("member", "guest", "mute", "jail"): if role == getattr(special_roles, attr): embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Cannot add role as assignable") embed.description = ( f"{role.mention} cannot be self-assignable, " f"it is already used as the **{attr}** role!") raise ManualCheckFailure(embed=embed) # Get roles that are already assignable assignable_roles = self.bot.sql.roles.get_assignable_roles(ctx.guild) # Add roles to database with self.bot.sql.transaction(): for role in roles: if role not in assignable_roles: self.bot.sql.roles.add_assignable_role(ctx.guild, role) # Send response embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Made roles joinable") descr = StringBuilder(sep=", ") for role in roles: descr.write(role.mention) embed.description = str(descr) await ctx.send(embed=embed) # Send journal event content = f"Roles were set as joinable: {self.str_roles(roles)}" self.journal.send("joinable/add", ctx.guild, content, icon="role", roles=roles)
def check_roles(self, ctx, roles): if not roles: raise CommandFailed() assignable_roles = self.bot.sql.roles.get_assignable_roles(ctx.guild) for role in roles: if role not in assignable_roles: embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Role not assignable") embed.description = f"The role {role.mention} cannot be self-assigned" raise CommandFailed(embed=embed) if role >= ctx.me.top_role: embed = discord.Embed(colour=discord.Colour.red()) embed.set_author(name="Error assigning roles") embed.description = ( f"Cannot assign {role.mention}, which is above me in the hierarchy" ) raise ManualCheckFailure(embed=embed)
async def check_count(self, ctx, count): embed = discord.Embed(colour=discord.Colour.red()) max_count = self.bot.sql.settings.get_max_delete_messages(ctx.guild) if count < 1: embed.description = f"Invalid message count: {count}" raise CommandFailed(embed=embed) if is_discord_id(count): prefix = self.bot.prefix(ctx.guild) embed.description = ( "This looks like a Discord ID. If you want to delete all " f"messages up to a message ID, use `{prefix}cleanupid`.") raise CommandFailed(embed=embed) if count > max_count: embed.description = ( "Count too high. Maximum configured for this guild is " f"`{max_count}`.") raise CommandFailed(embed=embed)
async def sha1sum(self, ctx, *urls: str): """ Gives the SHA1 hashes of any files attached to the message. """ # Check all URLs links = [] for url in urls: match = URL_REGEX.match(url) if match is None: raise CommandFailed(content=f"Not a valid url: {url}") links.append(match[1]) links.extend(attach.url for attach in ctx.message.attachments) # Get list of "names" names = list(urls) names.extend(attach.filename for attach in ctx.message.attachments) # Send error if no URLS if not links: raise CommandFailed(content="No URLs listed or files attached.") # Download and check files contents = [] content = StringBuilder("Hashes:\n```") buffers = await download_links(links) for i, binio in enumerate(buffers): if binio is None: hashsum = SHA1_ERROR_MESSAGE else: hashsum = sha1(binio.getbuffer()).hexdigest() content.writeln(f"{hashsum} {names[i]}") if len(content) > 1920: contents.append(content) if i < len(buffers) - 1: content.clear() content.writeln("```") if len(content) > 4: content.writeln("```") contents.append(content) for content in contents: await ctx.send(content=str(content))