async def role_show(self, ctx): """ Shows all self-assignable roles. """ await self.check_channel(ctx) assignable_roles = sorted(self.bot.sql.roles.get_assignable_roles( ctx.guild), key=lambda r: r.name) if not assignable_roles: prefix = self.bot.prefix(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_purple()) embed.set_author(name="No self-assignable roles") embed.description = ( f"Moderators can use the `{prefix}role joinable/unjoinable` " "commands to change this list!") await ctx.send(embed=embed) return embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Self-assignable roles") descr = StringBuilder(sep=", ") for role in assignable_roles: descr.write(role.mention) embed.description = str(descr) await ctx.send(embed=embed)
async def message_violator(): logger.debug("Sending message to user who violated the filter") response = StringBuilder() response.write( f"The message you posted in {message.channel.mention} contains or links to a file " ) response.writeln( f"that violates a {filter_type.value} content filter: `{hashsum.hex()}`." ) response.writeln(f"Your original link: <{url}>") if reupload: response.writeln( "The filtered file has been attached to this message.") if severity >= FilterType.JAIL.level: if roles.jail is not None: response.writeln( "This offense is serious enough to warrant immediate revocation of posting privileges.\n" f"As such, you have been assigned the `{roles.jail.name}` role, until a moderator clears you." ) kwargs = {} if reupload: response.writeln( "In case the link is broken, the file has been attached below:" ) filename = os.path.basename(urlparse(url).path) kwargs["file"] = discord.File(binio.getbuffer(), filename=filename) kwargs["content"] = str(response) await message.author.send(**kwargs)
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)
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 pingable_show(self, ctx): """ Shows all channels where a role is pingable. """ logger.info( "Displaying pingable channels and roles in guild '%s' (%d)", ctx.guild.name, ctx.guild.id, ) # r[0] == current channel. channel_role = sorted( self.bot.sql.roles.get_pingable_role_channels(ctx.guild), key=lambda r: r[0].name, ) if not channel_role: prefix = self.bot.prefix(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_purple()) embed.set_author(name="No pingable roles in this guild") embed.description = ( f"Moderators can use the `{prefix}role pingable/unpingable` " "commands to change this list!") await ctx.send(embed=embed) return embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Pingable roles (channel, role)") descr = StringBuilder(sep="\n") for channel, role in channel_role: descr.write(f"{channel.mention}: {role.mention}") embed.description = str(descr) await ctx.send(embed=embed)
def build_reason(ctx, action, minutes, reason, past=False): full_reason = StringBuilder(f"{action} by {user_discrim(ctx.author)}") if minutes: full_reason.write( f" {'for' if past else 'in'} {minutes} minute{plural(minutes)}" ) if reason: full_reason.write(f" with reason: {reason}") return str(full_reason)
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 uinfo_add_voice(embed, user): if getattr(user, "voice", None): mute = user.voice.mute or user.voice.self_mute deaf = user.voice.deaf or user.voice.self_deaf states = StringBuilder(sep=" ") if mute: states.write("muted") if deaf: states.write("deafened") state = str(states) if states else "active" embed.add_field(name="Voice", value=state)
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, )
def build_regex(text, groups): # Build similar character tree chars = {} pattern = StringBuilder() for group in groups: pattern.write("[") char = group["character"] pattern.write(re.escape(char)) for homoglyph in group["homoglyphs"]: pattern.write(re.escape(homoglyph["c"])) pattern.write("]") chars[char] = str(pattern) pattern.clear() # Create pattern for char in text: pattern.write(chars.get(char, char)) return str(pattern)
async def channel_show(self, ctx): """ Lists all channels that are allowed to be used for role commands. """ all_channels = self.bot.sql.roles.get_role_command_channels(ctx.guild) prefix = self.bot.prefix(ctx.guild) if all_channels: embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Permitted channels") descr = StringBuilder(sep=", ") for channel in all_channels: descr.write(channel.mention) embed.description = str(descr) else: embed = discord.Embed(colour=discord.Colour.dark_purple()) embed.set_author(name="All channels are permitted") embed.description = ( f"There are no restricted role channels set, so `{prefix}role add/remove` commands " "can be used anywhere.") await ctx.send(embed=embed)
async def reactions(self, ctx, *, message: MessageConv): """ Displays all reactions on a message. """ logger.info("Displaying reactions for message ID %d", message.id) if not message.reactions: embed = discord.Embed(colour=discord.Colour.dark_purple()) embed.description = "This message has no reactions." await ctx.send(embed=embed) return descr = StringBuilder() for reaction in message.reactions: if descr: descr.writeln() descr.write(reaction.emoji) async for user in reaction.users(): descr.write(f" {user.mention}") if len(descr) > 1800: embed = discord.Embed(colour=discord.Colour.teal()) embed.description = str(descr) await ctx.send(embed=embed) descr.clear() descr.write(reaction.emoji) if descr: embed = discord.Embed(colour=discord.Colour.teal()) embed.description = str(descr) await ctx.send(embed=embed)
async def tracker_blacklist_show(self, ctx): """ Shows all blacklist entries for this guild. """ blacklist = self.bot.sql.settings.get_tracking_blacklist(ctx.guild) if not blacklist.blacklisted_users and not blacklist.blacklisted_channels: prefix = self.bot.prefix(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_purple()) embed.set_author(name="No blacklist entries") embed.description = ( f"Moderators can use the `{prefix}trackerblacklist add/remove` " "commands to change this list!") await ctx.send(embed=embed) return embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Blacklist entries") if blacklist.blacklisted_channels: channel_msg = StringBuilder(sep=", ") for channel_id in blacklist.blacklisted_channels: channel = discord.utils.get(ctx.guild.channels, id=channel_id) channel_msg.write(channel.mention) embed.add_field(name="Blacklisted channels", value=channel_msg) if blacklist.blacklisted_users: user_msg = StringBuilder(sep=", ") for user_id in blacklist.blacklisted_users: user = discord.utils.get(chain(ctx.guild.members, ctx.bot.users), id=user_id) user_msg.write(user.mention) embed.add_field(name="Blacklisted channels", value=user_msg) await ctx.send(embed=embed)
async def channel_delete(self, ctx, *channels: TextChannelConv): """ Removes the channel(s) from the restricted role channel list. """ logger.info( "Removing channels to be used for role commands in guild '%s' (%d): [%s]", ctx.guild.name, ctx.guild.id, ", ".join(channel.mention for channel in channels), ) if not channels: raise CommandFailed() # Remove channels from database with self.bot.sql.transaction(): for channel in channels: self.bot.sql.roles.remove_role_command_channel( ctx.guild, channel) # Send response embed = discord.Embed(colour=discord.Colour.dark_teal()) embed.set_author(name="Removed channels to be used for adding roles") all_channels = self.bot.sql.roles.get_role_command_channels(ctx.guild) descr = StringBuilder(sep=", ") for channel in channels: descr.write(channel.mention) embed.add_field(name="Removed", value=str(descr)) descr.clear() for channel in all_channels: descr.write(channel.mention) embed.add_field(name="Remaining", value=str(descr) or "(none)") await ctx.send(embed=embed) # Send journal event self.channel_journal(ctx.guild)
async def reapply_show(self, ctx): """ Lists all roles that are reappliable. Reappliable roles, in addition to all punishment and self-assignable roles, are automatically reapplied when the member rejoins the guild. """ reapply_roles = self.bot.sql.settings.get_reapply_roles(ctx.guild) special_roles = self.bot.sql.settings.get_special_roles(ctx.guild) embed = discord.Embed(colour=discord.Colour.dark_teal()) descr = StringBuilder(sep=", ") has_roles = False # Manually set for role in sorted(reapply_roles, key=lambda r: r.name): descr.write(role.mention) if descr: embed.add_field(name="Manually designated", value=str(descr)) has_roles = True else: embed.add_field(name="Manually designated", value="(none)") # Punishment roles descr.clear() if special_roles.mute_role is not None: descr.write(special_roles.mute_role.mention) if special_roles.jail_role is not None: descr.write(special_roles.jail_role.mention) if descr: embed.add_field(name="Punishment roles", value=str(descr)) has_roles = True # Self-assignable roles if "SelfAssignableRoles" in self.bot.cogs: assignable_roles = self.bot.sql.roles.get_assignable_roles( ctx.guild) if assignable_roles: embed.add_field( name="Self-assignable roles", value=", ".join(role.mention for role in sorted(assignable_roles, key=lambda r: r.name)), ) has_roles = True # Send final embed if has_roles: embed.title = "\N{MILITARY MEDAL} Roles which are automatically reapplied" else: embed.colour = discord.Colour.dark_purple() await ctx.send(embed=embed)
def fancy_timedelta(delta): """ Formats a fancy time difference. When given a datetime object, it calculates the difference from the present. """ if isinstance(delta, datetime): delta = abs(datetime.now() - delta) result = StringBuilder(sep=" ") years, days = divmod(delta.days, 365) months, days = divmod(days, 30) weeks, days = divmod(days, 7) hours, seconds = divmod(delta.seconds, 3600) minutes, seconds = divmod(seconds, 60) seconds += delta.microseconds / 1e6 seconds = round(seconds, 2) if years: result.write(f"{years}y") if months: result.write(f"{months}mo") if weeks: result.write(f"{weeks}w") if days: result.write(f"{days}d") if hours: result.write(f"{hours}h") if minutes: result.write(f"{minutes}m") if seconds: result.write(f"{seconds}s") return str(result)
async def role_pingable(self, ctx, role: RoleConv, *channels: TextChannelConv, original=None): logger.info( "Making role '%s' 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() # self.bot.sql.roles.get_pingable_role_channels(ctx.guild) gets a set # of tuples (channel, role). # Zip converts it into a set of channels and a set of roles. channel_role = zip( *self.bot.sql.roles.get_pingable_role_channels(ctx.guild)) # this makes a list of all channels and rows, currently it's a channel and row pair. # next gets the next item from the zip iterator which is the set of channels. # if the iterator is exhausted i.e there's no pingable channels, the default value set() will be used. pingable_channels = next(channel_role, set()) # channels that were already in the database will not be added, user # will be informed. exempt_channels = [] with self.bot.sql.transaction(): for channel in channels: if channel not in pingable_channels: self.bot.sql.roles.add_pingable_role_channel( ctx.guild, channel, role, original) else: exempt_channels.append(channel) if exempt_channels: embed = discord.Embed(colour=discord.Colour.dark_grey()) embed.set_author(name="Failed to make role pingable in channels: ") descr = StringBuilder(sep=", ") for channel in exempt_channels: descr.write(channel.mention) embed.description = str(descr) await ctx.send(embed=embed) # Did not put the embed in CommandFailed. All channels must fail # to be added for the entire command to 'fail', imo. if set(exempt_channels) == set(channels): raise CommandFailed() # Send journal event content = f"Role was set as pingable in channels: {self.str_channels(channels)}, except {self.str_channels(exempt_channels)}" self.journal.send( "pingable/add", ctx.guild, content, icon="role", role=role, channels=channels, )
async def member_update(self, before, after): """ Handles update of member information. """ changes = MemberChanges() timestamp = datetime.now() if before.avatar != after.avatar: logger.info( "Member '%s' (%d) has changed their profile picture (%s)", before.name, before.id, after.avatar, ) changes.avatar_url = after.avatar_url if before.name != after.name: logger.info( "Member '%s' (%d) has changed name to '%s'", before.name, before.id, after.name, ) changes.username = after.name if before.nick != after.nick and after.nick is not None: logger.info( "Member '%s' (%d) has changed nick to '%s'", before.display_name, before.id, after.nick, ) changes.nickname = after.nick # Check if there were any changes if not changes: return attrs = StringBuilder(sep=", ") with self.bot.sql.transaction(): if changes.avatar_url is not None: avatar, avatar_ext = await self._download_avatar( changes.avatar_url) self.bot.sql.alias.add_avatar(before, timestamp, avatar, avatar_ext) attrs.write(f"avatar: {changes.avatar_url}") if changes.username is not None: self.bot.sql.alias.add_username(before, timestamp, changes.username) attrs.write(f"name: {changes.username}") if changes.nickname is not None: self.bot.sql.alias.add_nickname(before, timestamp, changes.nickname) attrs.write(f"nick: {changes.nickname}") content = f"{user_discrim(before)} updated {attrs}" self.journal.send( "member/update", before.guild, content, icon="person", before=before, after=after, changes=changes, )
def unicode_repr(s): """ Similar to repr(), but always escapes characters that aren't "readable". That is, any characters not in READABLE_CHAR_SET. """ result = StringBuilder('"') for ch in s: if ch == "\n": result.write("\\n") elif ch == "\t": result.write("\\t") elif ch == '"': result.write('\\"') elif ch in READABLE_CHAR_SET: result.write(ch) else: num = ord(ch) if num < 0x100: result.write(f"\\x{num:02x}") elif num < 0x10000: result.write(f"\\u{num:04x}") elif num < 0x100000000: result.write(f"\\U{num:08x}") else: raise ValueError(f"Character {ch!r} (ord {num:x}) too big for escaping") result.write('"') return str(result)