from random import seed, randint from datetime import datetime seed(datetime.now()) colours = [ Colour.teal(), Colour.dark_teal(), Colour.green(), Colour.dark_green(), Colour.blue(), Colour.dark_blue(), Colour.purple(), Colour.dark_purple(), Colour.magenta(), Colour.dark_magenta(), Colour.gold(), Colour.dark_gold(), Colour.orange(), Colour.dark_orange(), Colour.red(), Colour.dark_red(), Colour.lighter_grey(), Colour.light_grey(), Colour.dark_grey(), Colour.darker_grey(), Colour.blurple(), Colour.greyple(), Colour.from_rgb(randint(0, 255), randint(0, 255), randint(0, 255)) ]
async def clean_cancel(self, ctx: Context) -> None: """If there is an ongoing cleaning process, attempt to immediately cancel it.""" self.cleaning = False embed = Embed(color=Colour.blurple(), description="Clean interrupted.") await ctx.send(embed=embed, delete_after=10)
async def guildinfo(self, ctx: Context, *, guild_id: int = None) -> None: """Returns info about a guild""" if guild_id is not None and await self.bot.is_owner(ctx.author): guild = self.bot.get_guild(guild_id) if guild is None: return await ctx.send(f'Invalid Guild ID given.') else: guild = ctx.guild created = guild.created_at features = ", ".join(guild.features) id = guild.id owner = guild.owner ownerdn = guild.owner.display_name boostlvl = guild.premium_tier boostlen = guild.premium_subscription_count #lists of server elements rolelist = guild.roles cats = guild.categories chans = guild.text_channels #Dictionary of flags regionFlag = { 'amsterdam': ":flag_nl: - Amsterdam", 'brazil': ":flag_br: - Brazil", 'eu_central': ":flag_eu: - Central Europe", 'eu-central': "", 'eu_west': ":flag_eu: - West Europe", 'eu-west': "", 'europe': ":flag_eu: - Europe", 'frankfurt': ":flag_de: - Frankfurt", 'hongkong': ":flag_ch: - Hong Kong", 'india': ":flag_in: - India", 'japan': ":flag_jap: - Japan", 'london': ":flag_uk: - London", 'russia': ":flag_ru: - Russia", 'singapore': ":flag_au: - Singapore", 'southafrica': ":flag_za: - South Africa", 'sydney': ":flag_au: - Sydney", 'us_central': ":flag_us: - US Central", 'us-central': "", 'us_east': ":flag_us: - US East", 'us-east': "", 'us_south': ":flag_us: - US South", 'us-south': "", 'us_west': ":flag_us: - US West", 'us-west': "" } region = guild.region embed = discord.Embed(title=str(guild.name) + "'s information", colour=Colour.blurple()) embed.add_field(name=":id:", value=id) embed.add_field(name=":date: Guild Created On", value=created.strftime("%A %d %B %Y %H:%M")) embed.add_field(name=":bust_in_silhouette: Owner", value=str(owner) + " aka " + str(ownerdn)) embed.add_field(name=":telephone_receiver: Voice Region", value=" ".join([regionFlag[n] for n in region])) #embed.add_field(name=":telephone_receiver: Voice Region", value=region) embed.add_field(name="Nitro Level", value=str(boostlvl) + "/" + str(3)) embed.add_field(name="# of current boosts", value=str(boostlen) + "/" + str(30)) if boostlen > 2: embed.add_field(name=".. needed for lvl 1", value="Already unlocked") else: embed.add_field(name=".. needed for lvl 1", value=str(2 - boostlen)) if boostlen > 15: embed.add_field(name=".. needed for lvl 2", value="Already unlocked") else: embed.add_field(name=".. needed for lvl 2", value=str(15 - boostlen)) if boostlen > 30: embed.add_field(name=".. needed for lvl 3", value="Already unlocked") else: embed.add_field(name=".. needed for lvl 3", value=str(30 - boostlen)) embed.set_thumbnail(url=guild.icon_url) if boostlen > 30: embed.set_footer(text="Max Level reached", icon_url="") else: embed.set_footer(text=str(30 - boostlen) + " boosts to go for max boost level", icon_url="") await ctx.send(embed=embed)
fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) else: fields.append(await self.basic_user_infraction_counts(user)) # Let's build the embed now embed = Embed( title=name, description=" ".join(badges) ) for field_name, field_content in fields: embed.add_field(name=field_name, value=field_content, inline=False) embed.set_thumbnail(url=user.avatar_url_as(static_format="png")) embed.colour = user.colour if user.colour != Colour.default() else Colour.blurple() return embed async def basic_user_infraction_counts(self, user: FetchedMember) -> Tuple[str, str]: """Gets the total and active infraction counts for the given `member`.""" infractions = await self.bot.api_client.get( 'bot/infractions', params={ 'hidden': 'False', 'user__id': str(user.id) } ) total_infractions = len(infractions) active_infractions = sum(infraction['active'] for infraction in infractions)
async def on_message_edit(self, msg_before: discord.Message, msg_after: discord.Message) -> None: """Log message edit event to message change log.""" if (not msg_before.guild or msg_before.guild.id != GuildConstant.id or msg_before.channel.id in GuildConstant.ignored or msg_before.author.bot): return self._cached_edits.append(msg_before.id) if msg_before.content == msg_after.content: return author = msg_before.author channel = msg_before.channel channel_name = f"{channel.category}/#{channel.name}" if channel.category else f"#{channel.name}" # Getting the difference per words and group them by type - add, remove, same # Note that this is intended grouping without sorting diff = difflib.ndiff(msg_before.clean_content.split(), msg_after.clean_content.split()) diff_groups = tuple( (diff_type, tuple(s[2:] for s in diff_words)) for diff_type, diff_words in itertools.groupby(diff, key=lambda s: s[0])) content_before: t.List[str] = [] content_after: t.List[str] = [] for index, (diff_type, words) in enumerate(diff_groups): sub = ' '.join(words) if diff_type == '-': content_before.append(f"[{sub}](http://o.hi)") elif diff_type == '+': content_after.append(f"[{sub}](http://o.hi)") elif diff_type == ' ': if len(words) > 2: sub = ( f"{words[0] if index > 0 else ''}" " ... " f"{words[-1] if index < len(diff_groups) - 1 else ''}") content_before.append(sub) content_after.append(sub) response = (f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** {channel_name} (`{channel.id}`)\n" f"**Message ID:** `{msg_before.id}`\n" "\n" f"**Before**:\n{' '.join(content_before)}\n" f"**After**:\n{' '.join(content_after)}\n" "\n" f"[Jump to message]({msg_after.jump_url})") if msg_before.edited_at: # Message was previously edited, to assist with self-bot detection, use the edited_at # datetime as the baseline and create a human-readable delta between this edit event # and the last time the message was edited timestamp = msg_before.edited_at delta = humanize_delta( relativedelta(msg_after.edited_at, msg_before.edited_at)) footer = f"Last edited {delta} ago" else: # Message was not previously edited, use the created_at datetime as the baseline, no # delta calculation needed timestamp = msg_before.created_at footer = None await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited", response, channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer)
async def user_info(self, ctx: Context, user: Member = None, hidden: bool = False): """ Returns info about a user. """ # Do a role check if this is being executed on # someone other than the caller if user and user != ctx.author: if not with_role_check(ctx, *MODERATION_ROLES): raise BadArgument( "You do not have permission to use this command on users other than yourself." ) # Non-moderators may only do this in #bot-commands and can't see # hidden infractions. if not with_role_check(ctx, *STAFF_ROLES): if not ctx.channel.id == Channels.bot: raise MissingPermissions("You can't do that here!") # Hide hidden infractions for users without a moderation role hidden = False # Validates hidden input hidden = str(hidden) if user is None: user = ctx.author # User information created = time_since(user.created_at, max_units=3) name = f"{user.name}#{user.discriminator}" if user.nick: name = f"{user.nick} ({name})" # Member information joined = time_since(user.joined_at, precision="days") # You're welcome, Volcyyyyyyyyyyyyyyyy roles = ", ".join(role.mention for role in user.roles if role.name != "@everyone") # Infractions api_response = await self.bot.http_session.get( url=URLs.site_infractions_user.format(user_id=user.id), params={"hidden": hidden}, headers=self.headers) infractions = await api_response.json() infr_total = 0 infr_active = 0 # At least it's readable. for infr in infractions: if infr["active"]: infr_active += 1 infr_total += 1 # Let's build the embed now embed = Embed(title=name, description=textwrap.dedent(f""" **User Information** Created: {created} Profile: {user.mention} ID: {user.id} **Member Information** Joined: {joined} Roles: {roles or None} **Infractions** Total: {infr_total} Active: {infr_active} """)) embed.set_thumbnail(url=user.avatar_url_as(format="png")) embed.colour = user.top_role.colour if roles else Colour.blurple() await ctx.send(embed=embed)
class Information(Cog): """A cog with commands for generating embeds with server info, such as server stats and user info.""" def __init__(self, bot: Bot): self.bot = bot @staticmethod def role_can_read(channel: GuildChannel, role: Role) -> bool: """Return True if `role` can read messages in `channel`.""" overwrites = channel.overwrites_for(role) return overwrites.read_messages is True def get_staff_channel_count(self, guild: Guild) -> int: """ Get the number of channels that are staff-only. We need to know two things about a channel: - Does the @everyone role have explicit read deny permissions? - Do staff roles have explicit read allow permissions? If the answer to both of these questions is yes, it's a staff channel. """ channel_ids = set() for channel in guild.channels: if channel.type is ChannelType.category: continue everyone_can_read = self.role_can_read(channel, guild.default_role) for role in constants.STAFF_ROLES: role_can_read = self.role_can_read(channel, guild.get_role(role)) if role_can_read and not everyone_can_read: channel_ids.add(channel.id) break return len(channel_ids) @staticmethod def get_channel_type_counts(guild: Guild) -> str: """Return the total amounts of the various types of channels in `guild`.""" channel_counter = Counter(c.type for c in guild.channels) channel_type_list = [] for channel, count in channel_counter.items(): channel_type = str(channel).title() channel_type_list.append(f"{channel_type} channels: {count}") channel_type_list = sorted(channel_type_list) return "\n".join(channel_type_list) @has_any_role(*constants.STAFF_ROLES) @command(name="roles") async def roles_info(self, ctx: Context) -> None: """Returns a list of all roles and their corresponding IDs.""" # Sort the roles alphabetically and remove the @everyone role roles = sorted(ctx.guild.roles[1:], key=lambda role: role.name) # Build a list role_list = [] for role in roles: role_list.append(f"`{role.id}` - {role.mention}") # Build an embed embed = Embed( title= f"Role information (Total {len(roles)} role{'s' * (len(role_list) > 1)})", colour=Colour.blurple()) await LinePaginator.paginate(role_list, ctx, embed, empty=False) @has_any_role(*constants.STAFF_ROLES) @command(name="role") async def role_info(self, ctx: Context, *roles: Union[Role, str]) -> None: """ Return information on a role or list of roles. To specify multiple roles just add to the arguments, delimit roles with spaces in them using quotation marks. """ parsed_roles = set() failed_roles = set() all_roles = {role.id: role.name for role in ctx.guild.roles} for role_name in roles: if isinstance(role_name, Role): # Role conversion has already succeeded parsed_roles.add(role_name) continue match = fuzzywuzzy.process.extractOne(role_name, all_roles, score_cutoff=80, scorer=fuzzywuzzy.fuzz.ratio) if not match: failed_roles.add(role_name) continue # `match` is a (role name, score, role id) tuple role = ctx.guild.get_role(match[2]) parsed_roles.add(role) if failed_roles: await ctx.send( f":x: Could not retrieve the following roles: {', '.join(failed_roles)}" ) for role in parsed_roles: h, s, v = colorsys.rgb_to_hsv(*role.colour.to_rgb()) embed = Embed( title=f"{role.name} info", colour=role.colour, ) embed.add_field(name="ID", value=role.id, inline=True) embed.add_field(name="Colour (RGB)", value=f"#{role.colour.value:0>6x}", inline=True) embed.add_field(name="Colour (HSV)", value=f"{h:.2f} {s:.2f} {v}", inline=True) embed.add_field(name="Member count", value=len(role.members), inline=True) embed.add_field(name="Position", value=role.position) embed.add_field(name="Permission code", value=role.permissions.value, inline=True) await ctx.send(embed=embed) @command(name="server", aliases=["server_info", "guild", "guild_info"]) async def server_info(self, ctx: Context) -> None: """Returns an embed full of server information.""" created = time_since(ctx.guild.created_at, precision="days") features = ", ".join(ctx.guild.features) region = ctx.guild.region roles = len(ctx.guild.roles) member_count = ctx.guild.member_count channel_counts = self.get_channel_type_counts(ctx.guild) # How many of each user status? py_invite = await self.bot.fetch_invite(constants.Guild.invite) online_presences = py_invite.approximate_presence_count offline_presences = py_invite.approximate_member_count - online_presences embed = Embed(colour=Colour.blurple()) # How many staff members and staff channels do we have? staff_member_count = len( ctx.guild.get_role(constants.Roles.helpers).members) staff_channel_count = self.get_staff_channel_count(ctx.guild) # Because channel_counts lacks leading whitespace, it breaks the dedent if it's inserted directly by the # f-string. While this is correctly formatted by Discord, it makes unit testing difficult. To keep the # formatting without joining a tuple of strings we can use a Template string to insert the already-formatted # channel_counts after the dedent is made. embed.description = Template( textwrap.dedent(f""" **Server information** Created: {created} Voice region: {region} Features: {features} **Channel counts** $channel_counts Staff channels: {staff_channel_count} **Member counts** Members: {member_count:,} Staff members: {staff_member_count} Roles: {roles} **Member statuses** {constants.Emojis.status_online} {online_presences:,} {constants.Emojis.status_offline} {offline_presences:,} """)).substitute({"channel_counts": channel_counts}) embed.set_thumbnail(url=ctx.guild.icon_url) await ctx.send(embed=embed) @command(name="user", aliases=["user_info", "member", "member_info"]) async def user_info(self, ctx: Context, user: FetchedMember = None) -> None: """Returns info about a user.""" if user is None: user = ctx.author # Do a role check if this is being executed on someone other than the caller elif user != ctx.author and await has_no_roles_check( ctx, *constants.MODERATION_ROLES): await ctx.send( "You may not use this command on users other than yourself.") return # Will redirect to #bot-commands if it fails. if in_whitelist_check(ctx, roles=constants.STAFF_ROLES): embed = await self.create_user_embed(ctx, user) await ctx.send(embed=embed) async def create_user_embed(self, ctx: Context, user: FetchedMember) -> Embed: """Creates an embed containing information on the `user`.""" on_server = bool(ctx.guild.get_member(user.id)) created = time_since(user.created_at, max_units=3) name = str(user) if on_server and user.nick: name = f"{user.nick} ({name})" badges = [] for badge, is_set in user.public_flags: if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}", None)): badges.append(emoji) activity = await self.user_messages(user) if on_server: joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) membership = { "Joined": joined, "Verified": not user.pending, "Roles": roles or None } if not is_mod_channel(ctx.channel): membership.pop("Verified") membership = textwrap.dedent("\n".join( [f"{key}: {value}" for key, value in membership.items()])) else: roles = None membership = "The user is not a member of the server" fields = [ ("User information", textwrap.dedent(f""" Created: {created} Profile: {user.mention} ID: {user.id} """).strip()), ("Member information", membership), ] # Show more verbose output in moderation channels for infractions and nominations if is_mod_channel(ctx.channel): fields.append(activity) fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) else: fields.append(await self.basic_user_infraction_counts(user)) # Let's build the embed now embed = Embed(title=name, description=" ".join(badges)) for field_name, field_content in fields: embed.add_field(name=field_name, value=field_content, inline=False) embed.set_thumbnail(url=user.avatar_url_as(static_format="png")) embed.colour = user.top_role.colour if roles else Colour.blurple() return embed
async def help_(self, ctx, second_help: str = None): cogs = sorted([ cog for cog in self.bot.cogs.keys() if cog not in ['ErrorHandler', 'Tavern', 'Events'] ]) pages = [] page = 1 cmd_names = [cmd.name for cmd in self.bot.commands] if not second_help: for cog_name in cogs: cog = self.bot.get_cog(cog_name) commands = [ cmd for cmd in cog.get_commands() if not cmd.hidden or cmd.name == 'help' ] message = cog.description + '\n' for cmd in commands: if cmd.name == 'subreddit': for sub_cmd in cmd.walk_commands(): message += f' \n **{self.config["prefix"]}{sub_cmd}** \n *{sub_cmd.help}*' else: message += f' \n **{self.config["prefix"]}{cmd}** \n *{cmd.help}*' help_embed = Embed(title=cog_name, colour=Colour.blurple(), description=message) help_embed.set_footer(text=f'Page: {page}/{len(cogs)}') help_embed.set_author(name=f'{ctx.author}', icon_url=ctx.author.avatar_url) pages.append(help_embed) page = page + 1 embed = Paginator(embed=False, timeout=90, use_defaults=True, extra_pages=pages, length=1) await embed.start(ctx) else: if second_help.lower() in cmd_names: cmd = self.bot.get_command(second_help) embed = Embed(title=cmd.name, colour=Colour.blurple()) value = '' if cmd.aliases: for alias in cmd.aliases: value += f'{str(alias)}, ' value = value[0:-2] value = value + '.' else: value = None embed.add_field(name="Aliases", value=f'*{value}*', inline=False) params_list = list(cmd.params.keys()) req_params = [] for value in params_list: req_params.append(value) req_params.remove('self') req_params.remove('ctx') param_message = 'Required parameters are:\n**' if req_params: for parm in req_params: param_message += parm + '\n' embed.add_field(name='Usage', value=param_message + '**', inline=False) else: embed.add_field(name='Usage', value=param_message + 'None**', inline=False) embed.set_author(name=f'{ctx.author}', icon_url=ctx.author.avatar_url) return await ctx.send(embed=embed) else: return await ctx.send( f"{str(second_help)} command does not exist!")
fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) else: fields.append(await self.basic_user_infraction_counts(user)) # Let's build the embed now embed = Embed( title=name, description=" ".join(badges) ) for field_name, field_content in fields: embed.add_field(name=field_name, value=field_content, inline=False) embed.set_thumbnail(url=user.avatar_url_as(static_format="png")) embed.colour = user.top_role.colour if roles else Colour.blurple() return embed async def basic_user_infraction_counts(self, user: FetchedMember) -> Tuple[str, str]: """Gets the total and active infraction counts for the given `member`.""" infractions = await self.bot.api_client.get( 'bot/infractions', params={ 'hidden': 'False', 'user__id': str(user.id) } ) total_infractions = len(infractions) active_infractions = sum(infraction['active'] for infraction in infractions)
async def poll_new_posts(self) -> None: """Periodically search for new subreddit posts.""" while True: await asyncio.sleep(RedditConfig.request_delay) for subreddit in RedditConfig.subreddits: # Make a HEAD request to the subreddit head_response = await self.bot.http_session.head( url=f"{self.URL}/{subreddit}/new.rss", headers=self.HEADERS) content_length = head_response.headers["content-length"] # If the content is the same size as before, assume there's no new posts. if content_length == self.prev_lengths.get(subreddit, None): continue self.prev_lengths[subreddit] = content_length # Now we can actually fetch the new data posts = await self.fetch_posts(f"{subreddit}/new") new_posts = [] # Only show new posts if we've checked before. if subreddit in self.last_ids: for post in posts: data = post["data"] # Convert the ID to an integer for easy comparison. int_id = int(data["id"], 36) # If we've already seen this post, finish checking if int_id <= self.last_ids[subreddit]: break embed_data = { "title": textwrap.shorten(data["title"], width=64, placeholder="..."), "text": textwrap.shorten(data["selftext"], width=128, placeholder="..."), "url": self.URL + data["permalink"], "author": data["author"] } new_posts.append(embed_data) self.last_ids[subreddit] = int(posts[0]["data"]["id"], 36) # Send all of the new posts as spicy embeds for data in new_posts: embed = Embed() embed.title = data["title"] embed.url = data["url"] embed.description = data["text"] embed.set_footer( text=f"Posted by u/{data['author']} in {subreddit}") embed.colour = Colour.blurple() await self.reddit_channel.send(embed=embed) log.trace( f"Sent {len(new_posts)} new {subreddit} posts to channel {self.reddit_channel.id}." )
async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None: """Log message edit event to modlog.""" if (not before.guild or before.guild.id != GuildConstant.id or before.channel.id in GuildConstant.ignored or before.author.bot): return self._cached_edits.append(before.id) if before.content == after.content: return author = before.author channel = before.channel if channel.category: before_response = ( f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{before.clean_content}") after_response = ( f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{after.clean_content}") else: before_response = ( f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{before.clean_content}") after_response = ( f"**Author:** {author.name}#{author.discriminator} (`{author.id}`)\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{after.clean_content}") if before.edited_at: timestamp = before.edited_at delta = humanize_delta(relativedelta(after.edited_at, before.edit)) footer = f"Last edited {delta} ago" else: timestamp = before.created_at footer = None await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, channel_id=Channels.modlog, timestamp_override=timestamp, footer=footer) await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited (After)", after_response, channel_id=Channels.modlog, timestamp_override=timestamp, footer=footer)
async def tasks_command(self, ctx: Context, status: str = None, task_list: str = None): """ Get a list of tasks, optionally on a specific list or with a specific status Provide "*" for the status to match everything except for "Closed". When specifying a list you may use the list name on its own, but it is preferable to give the project name as well - for example, "Bot/Cogs". This is case-insensitive. """ params = {} embed = Embed(colour=Colour.blurple()) embed.set_author( name="ClickUp Tasks", icon_url="https://clickup.com/landing/favicons/favicon-32x32.png", url=f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/") if task_list: if task_list in self.lists: params["list_ids[]"] = self.lists[task_list] else: log.warning( f"{ctx.author} requested '{task_list}', but that list is unknown. Rejecting request." ) embed.description = f"Unknown list: {task_list}" embed.colour = Colour.red() return await ctx.send(embed=embed) if status and status != "*": params["statuses[]"] = status response = await self.bot.http_session.get( GET_TASKS_URL.format(team_id=CLICKUP_TEAM), headers=HEADERS, params=params) result = await response.json() if "err" in result: log.error( "ClickUp responded to the task list request with an error!\n" f"error code: '{result['ECODE']}'\n" f"error: {result['err']}") embed.description = f"`{result['ECODE']}`: {result['err']}" embed.colour = Colour.red() else: tasks = result["tasks"] if not tasks: log.debug( f"{ctx.author} requested a list of ClickUp tasks, but no ClickUp tasks were found." ) embed.description = "No tasks found." embed.colour = Colour.red() else: lines = [] for task in tasks: task_url = f"http://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/t/{task['id']}" id_fragment = f"[`#{task['id']: <5}`]({task_url})" status = f"{task['status']['status'].title()}" lines.append( f"{id_fragment} ({status})\n\u00BB {task['name']}") log.debug( f"{ctx.author} requested a list of ClickUp tasks. Returning list." ) return await LinePaginator.paginate(lines, ctx, embed, max_size=750) return await ctx.send(embed=embed)
async def task_command(self, ctx: Context, task_id: str): """ Get a task and return information specific to it """ if task_id.startswith("#"): task_id = task_id[1:] embed = Embed(colour=Colour.blurple()) embed.set_author( name=f"ClickUp Task: #{task_id}", icon_url="https://clickup.com/landing/favicons/favicon-32x32.png", url= f"https://app.clickup.com/{CLICKUP_TEAM}/{CLICKUP_SPACE}/t/{task_id}" ) params = MultiDict() params.add("statuses[]", "Open") params.add("statuses[]", "in progress") params.add("statuses[]", "review") params.add("statuses[]", "Closed") response = await self.bot.http_session.get( GET_TASKS_URL.format(team_id=CLICKUP_TEAM), headers=HEADERS, params=params) result = await response.json() if "err" in result: log.error( "ClickUp responded to the get task request with an error!\n" f"error code: '{result['ECODE']}'\n" f"error: {result['err']}") embed.description = f"`{result['ECODE']}`: {result['err']}" embed.colour = Colour.red() else: task = None for task_ in result["tasks"]: if task_["id"] == task_id: task = task_ break if task is None: log.warning( f"{ctx.author} requested the task '#{task_id}', but it could not be found." ) embed.description = f"Unable to find task with ID `#{task_id}`:" embed.colour = Colour.red() else: status = task['status']['status'].title() project, list_ = self.lists[task['list']['id']].split("/", 1) list_ = f"{project.title()}/{list_.title()}" first_line = f"**{list_}** \u00BB *{task['name']}* \n**Status**: {status}" if task.get("tags"): tags = ", ".join(tag["name"].title() for tag in task["tags"]) first_line += f" / **Tags**: {tags}" lines = [first_line] if task.get("text_content"): text = task["text_content"] if len(text) >= 1500: text = text[:1497] + "..." lines.append(text) if task.get("assignees"): assignees = ", ".join(user["username"] for user in task["assignees"]) lines.append(f"**Assignees**\n{assignees}") log.debug( f"{ctx.author} requested the task '#{task_id}'. Returning the task data." ) return await LinePaginator.paginate(lines, ctx, embed, max_size=1500) return await ctx.send(embed=embed)
async def on_guild_channel_update(self, before: GUILD_CHANNEL, after: GuildChannel) -> None: """Log channel update event to mod log.""" if before.guild.id != GuildConstant.id: return if before.id in self._ignored[Event.guild_channel_update]: self._ignored[Event.guild_channel_update].remove(before.id) return # Two channel updates are sent for a single edit: 1 for topic and 1 for category change. # TODO: remove once support is added for ignoring multiple occurrences for the same channel. help_categories = (Categories.help_available, Categories.help_dormant, Categories.help_in_use) if after.category and after.category.id in help_categories: return diff = DeepDiff(before, after) changes = [] done = [] diff_values = diff.get("values_changed", {}) diff_values.update(diff.get("type_changes", {})) for key, value in diff_values.items(): if not key: # Not sure why, but it happens continue key = key[5:] # Remove "root." prefix if "[" in key: key = key.split("[", 1)[0] if "." in key: key = key.split(".", 1)[0] if key in done or key in CHANNEL_CHANGES_SUPPRESSED: continue if key in CHANNEL_CHANGES_UNSUPPORTED: changes.append(f"**{key.title()}** updated") else: new = value["new_value"] old = value["old_value"] # Discord does not treat consecutive backticks ("``") as an empty inline code block, so the markdown # formatting is broken when `new` and/or `old` are empty values. "None" is used for these cases so # formatting is preserved. changes.append( f"**{key.title()}:** `{old or 'None'}` **→** `{new or 'None'}`" ) done.append(key) if not changes: return message = "" for item in sorted(changes): message += f"{Emojis.bullet} {item}\n" if after.category: message = f"**{after.category}/#{after.name} (`{after.id}`)**\n{message}" else: message = f"**#{after.name}** (`{after.id}`)\n{message}" await self.send_log_message(Icons.hash_blurple, Colour.blurple(), "Channel updated", message)
async def character_cmd(self, ctx): """View your character or create one.""" db_connection = await dbconnection() cursor = await db_connection.cursor() user_id = ctx.author.id channel = ctx.channel select_characters = "SELECT * FROM `character` WHERE user_id = '%s'" val = user_id await cursor.execute(select_characters, (val, )) result = await cursor.fetchall() if is_empty(result) is True: await ctx.send( "Starting character creation process, what do you want your character to be called?" ) try: def check(m): return m.channel == channel and ctx.author == m.author msg = await self.bot.wait_for('message', timeout=15.0, check=check) except asyncio.TimeoutError: return await ctx.send( 'No response. Character creation stopped.') character_name = msg.content await ctx.send('Give me a short background on your character.') try: def check(m): return m.channel == channel and ctx.author == m.author msg = await self.bot.wait_for('message', timeout=120.0, check=check) except asyncio.TimeoutError: return await ctx.send( 'No response. Character creation stopped.') character_description = msg.content # insert the data the user has submitted sql = "INSERT INTO `character`(`name`, user_id, description) VALUES(%s, %s, %s)" val = (character_name, user_id, character_description) await cursor.execute(sql, val) # insert the standard data that is set on default sql = "INSERT INTO inventory(character_id, gold) VALUES(%s, %s)" val = (user_id, 100) await cursor.execute(sql, val) sql = "INSERT INTO jobs(character_id, job) VALUES(%s, %s)" val = (user_id, 'miner') await cursor.execute(sql, val) try: await db_connection.commit() await cursor.close() db_connection.close() return await ctx.send( 'Succesfully created your character! Use !character to acces it.' ) except: await cursor.close() db_connection.close() return await ctx.send( 'Could not create your character. Something went wrong.') else: pages = [] select_character = "SELECT * FROM `character` " \ "LEFT JOIN inventory ON `character`.user_id = inventory.character_id " \ "LEFT JOIN jobs ON inventory.character_id = jobs.character_id " \ "AND jobs.current_job = %s " \ "WHERE `character`.user_id = '%s'" val = ('true', user_id) await cursor.execute(select_character, val) results = await cursor.fetchall() columns = [desc[0] for desc in cursor.description] result = [] for row in results: row = dict(zip(columns, row)) result.append(row) embed = Embed(title=result[0]['name'], colour=Colour.blurple(), description=result[0]['description']) embed.add_field(name="Details", value=f"**Current Job:** {result[0]['job']} \n") ores = ['stone', 'coal', 'iron', 'ruby'] desc = '' for x in ores: if result[0][x] > 0: desc += f"**{x}:** {result[0][x]} \n" embed.add_field(name='Inventory', value=f"**Gold:** {result[0]['gold']}\n{desc}", inline=False) pages.append(embed) desc = f"**Skill Shards:** {result[0]['skillshard']}" embed = Embed(title="Skills", colour=Colour.blurple(), description=desc) pages.append(embed) await cursor.close() db_connection.close() embed = Paginator(embed=False, timeout=90, use_defaults=True, extra_pages=pages, length=1) await embed.start(ctx)
async def on_voice_state_update(self, member: discord.Member, before: discord.VoiceState, after: discord.VoiceState) -> None: """Log member voice state changes to the voice log channel.""" if (member.guild.id != GuildConstant.id or (before.channel and before.channel.id in GuildConstant.modlog_blacklist)): return if member.id in self._ignored[Event.voice_state_update]: self._ignored[Event.voice_state_update].remove(member.id) return # Exclude all channel attributes except the name. diff = DeepDiff( before, after, exclude_paths=("root.session_id", "root.afk"), exclude_regex_paths=r"root\.channel\.(?!name)", ) # A type change seems to always take precedent over a value change. Furthermore, it will # include the value change along with the type change anyway. Therefore, it's OK to # "overwrite" values_changed; in practice there will never even be anything to overwrite. diff_values = { **diff.get("values_changed", {}), **diff.get("type_changes", {}) } icon = Icons.voice_state_blue colour = Colour.blurple() changes = [] for attr, values in diff_values.items(): if not attr: # Not sure why, but it happens. continue old = values["old_value"] new = values["new_value"] attr = attr[5:] # Remove "root." prefix. attr = VOICE_STATE_ATTRIBUTES.get( attr, attr.replace("_", " ").capitalize()) changes.append(f"**{attr}:** `{old}` **→** `{new}`") # Set the embed icon and colour depending on which attribute changed. if any(name in attr for name in ("Channel", "deaf", "mute")): if new is None or new is True: # Left a channel or was muted/deafened. icon = Icons.voice_state_red colour = Colours.soft_red elif old is None or old is True: # Joined a channel or was unmuted/undeafened. icon = Icons.voice_state_green colour = Colours.soft_green if not changes: return member_str = escape_markdown(str(member)) message = "\n".join(f"{Emojis.bullet} {item}" for item in sorted(changes)) message = f"**{member_str}** (`{member.id}`)\n{message}" await self.send_log_message( icon_url=icon, colour=colour, title="Voice state updated", text=message, thumbnail=member.avatar_url_as(static_format="png"), channel_id=Channels.voice_log)
async def on_message_edit(self, before: discord.Message, after: discord.Message) -> None: """Log message edit event to message change log.""" if (not before.guild or before.guild.id != GuildConstant.id or before.channel.id in GuildConstant.ignored or before.author.bot): return self._cached_edits.append(before.id) if before.content == after.content: return author = before.author channel = before.channel if channel.category: before_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{before.clean_content}") after_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{after.clean_content}") else: before_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{before.clean_content}") after_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{before.id}`\n" "\n" f"{after.clean_content}") if before.edited_at: # Message was previously edited, to assist with self-bot detection, use the edited_at # datetime as the baseline and create a human-readable delta between this edit event # and the last time the message was edited timestamp = before.edited_at delta = humanize_delta( relativedelta(after.edited_at, before.edited_at)) footer = f"Last edited {delta} ago" else: # Message was not previously edited, use the created_at datetime as the baseline, no # delta calculation needed timestamp = before.created_at footer = None await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, channel_id=Channels.message_log, timestamp_override=timestamp, footer=footer) await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited (After)", after_response, channel_id=Channels.message_log, timestamp_override=after.edited_at)
async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: """Log member update event to user log.""" if before.guild.id != GuildConstant.id: return if before.id in self._ignored[Event.member_update]: self._ignored[Event.member_update].remove(before.id) return diff = DeepDiff(before, after) changes = [] done = [] diff_values = {} diff_values.update(diff.get("values_changed", {})) diff_values.update(diff.get("type_changes", {})) diff_values.update(diff.get("iterable_item_removed", {})) diff_values.update(diff.get("iterable_item_added", {})) diff_user = DeepDiff(before._user, after._user) diff_values.update(diff_user.get("values_changed", {})) diff_values.update(diff_user.get("type_changes", {})) diff_values.update(diff_user.get("iterable_item_removed", {})) diff_values.update(diff_user.get("iterable_item_added", {})) for key, value in diff_values.items(): if not key: # Not sure why, but it happens continue key = key[5:] # Remove "root." prefix if "[" in key: key = key.split("[", 1)[0] if "." in key: key = key.split(".", 1)[0] if key in done or key in MEMBER_CHANGES_SUPPRESSED: continue if key == "_roles": new_roles = after.roles old_roles = before.roles for role in old_roles: if role not in new_roles: changes.append( f"**Role removed:** {role.name} (`{role.id}`)") for role in new_roles: if role not in old_roles: changes.append( f"**Role added:** {role.name} (`{role.id}`)") else: new = value.get("new_value") old = value.get("old_value") if new and old: changes.append( f"**{key.title()}:** `{old}` **->** `{new}`") done.append(key) if before.name != after.name: changes.append( f"**Username:** `{before.name}` **->** `{after.name}`") if before.discriminator != after.discriminator: changes.append( f"**Discriminator:** `{before.discriminator}` **->** `{after.discriminator}`" ) if before.display_name != after.display_name: changes.append( f"**Display name:** `{before.display_name}` **->** `{after.display_name}`" ) if not changes: return message = "" for item in sorted(changes): message += f"{Emojis.bullet} {item}\n" message = f"**{after}** (`{after.id}`)\n{message}" await self.send_log_message( Icons.user_update, Colour.blurple(), "Member updated", message, thumbnail=after.avatar_url_as(static_format="png"), channel_id=Channels.userlog)
async def on_raw_message_edit( self, event: discord.RawMessageUpdateEvent) -> None: """Log raw message edit event to message change log.""" try: channel = self.bot.get_channel(int(event.data["channel_id"])) message = await channel.fetch_message(event.message_id) except discord.NotFound: # Was deleted before we got the event return if (not message.guild or message.guild.id != GuildConstant.id or message.channel.id in GuildConstant.ignored or message.author.bot): return await asyncio.sleep(1) # Wait here in case the normal event was fired if event.message_id in self._cached_edits: # It was in the cache and the normal event was fired, so we can just ignore it self._cached_edits.remove(event.message_id) return author = message.author channel = message.channel if channel.category: before_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" "This message was not cached, so the message content cannot be displayed." ) after_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" f"{message.clean_content}") else: before_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" "This message was not cached, so the message content cannot be displayed." ) after_response = ( f"**Author:** {author} (`{author.id}`)\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" "\n" f"{message.clean_content}") await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited (Before)", before_response, channel_id=Channels.message_log) await self.send_log_message(Icons.message_edit, Colour.blurple(), "Message edited (After)", after_response, channel_id=Channels.message_log)
async def superstarify(self, ctx: Context, member: Member, duration: str, *, forced_nick: str = None): """ This command will force a random superstar name (like Taylor Swift) to be the user's nickname for a specified duration. If a forced_nick is provided, it will use that instead. :param ctx: Discord message context :param ta: If provided, this function shows data for that specific tag. If not provided, this function shows the caller a list of all tags. """ log.debug( f"Attempting to superstarify {member.display_name} for {duration}. " f"forced_nick is set to {forced_nick}." ) embed = Embed() embed.colour = Colour.blurple() params = { "user_id": str(member.id), "duration": duration } if forced_nick: params["forced_nick"] = forced_nick response = await self.bot.http_session.post( URLs.site_superstarify_api, headers=self.headers, json=params ) response = await response.json() if "error_message" in response: log.warning( "Encountered the following error when trying to superstarify the user:\n" f"{response.get('error_message')}" ) embed.colour = Colour.red() embed.title = random.choice(NEGATIVE_REPLIES) embed.description = response.get("error_message") return await ctx.send(embed=embed) else: forced_nick = response.get('forced_nick') end_time = response.get("end_timestamp") image_url = response.get("image_url") embed.title = "Congratulations!" embed.description = ( f"Your previous nickname, **{member.display_name}**, was so bad that we have decided to change it. " f"Your new nickname will be **{forced_nick}**.\n\n" f"You will be unable to change your nickname until \n**{end_time}**.\n\n" "If you're confused by this, please read our " f"[official nickname policy]({NICKNAME_POLICY_URL})." ) embed.set_image(url=image_url) # Log to the mod_log channel log.trace("Logging to the #mod-log channel. This could fail because of channel permissions.") mod_log = self.bot.get_channel(Channels.modlog) await mod_log.send( f":middle_finger: {member.name}#{member.discriminator} (`{member.id}`) " f"has been superstarified by **{ctx.author.name}**. Their new nickname is `{forced_nick}`. " f"They will not be able to change their nickname again until **{end_time}**" ) await self.moderation.notify_infraction( user=member, infr_type="Superstarify", duration=duration, reason=f"Your nickname didn't comply with our [nickname policy]({NICKNAME_POLICY_URL})." ) # Change the nick and return the embed log.debug("Changing the users nickname and sending the embed.") await member.edit(nick=forced_nick) await ctx.send(embed=embed)
async def get_command(self, ctx: Context, *, tag_name: TagNameConverter=None): """ Get a list of all tags or a specified tag. :param ctx: Discord message context :param tag_name: If provided, this function shows data for that specific tag. If not provided, this function shows the caller a list of all tags. """ def _command_on_cooldown(tag_name) -> bool: """ Check if the command is currently on cooldown. The cooldown duration is set in constants.py. This works on a per-tag, per-channel basis. :param tag_name: The name of the command to check. :return: True if the command is cooling down. Otherwise False. """ now = time.time() cooldown_conditions = ( tag_name and tag_name in self.tag_cooldowns and (now - self.tag_cooldowns[tag_name]["time"]) < Cooldowns.tags and self.tag_cooldowns[tag_name]["channel"] == ctx.channel.id ) if cooldown_conditions: return True return False if _command_on_cooldown(tag_name): time_left = Cooldowns.tags - (time.time() - self.tag_cooldowns[tag_name]["time"]) log.warning(f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. " f"Cooldown ends in {time_left:.1f} seconds.") return tags = [] embed = Embed() embed.colour = Colour.red() tag_data = await self.get_tag_data(tag_name) # If we found something, prepare that data if tag_data: embed.colour = Colour.blurple() if tag_name: log.debug(f"{ctx.author} requested the tag '{tag_name}'") embed.title = tag_name if ctx.channel.id not in TEST_CHANNELS: self.tag_cooldowns[tag_name] = { "time": time.time(), "channel": ctx.channel.id } else: embed.title = "**Current tags**" if isinstance(tag_data, list): log.debug(f"{ctx.author} requested a list of all tags") tags = [f"**»** {tag['tag_name']}" for tag in tag_data] tags = sorted(tags) else: embed.description = tag_data['tag_content'] if tag_data['image_url'] is not None: embed.set_image(url=tag_data['image_url']) # If not, prepare an error message. else: embed.colour = Colour.red() if isinstance(tag_data, dict): log.warning(f"{ctx.author} requested the tag '{tag_name}', but it could not be found.") embed.description = f"**{tag_name}** is an unknown tag name. Please check the spelling and try again." else: log.warning(f"{ctx.author} requested a list of all tags, but the tags database was empty!") embed.description = "**There are no tags in the database!**" if tag_name: embed.set_footer(text="To show a list of all tags, use !tags.") embed.title = "Tag not found." # Paginate if this is a list of all tags if tags: log.debug(f"Returning a paginated list of all tags.") return await LinePaginator.paginate( (lines for lines in tags), ctx, embed, footer_text="To show a tag, type !tags <tagname>.", empty=False, max_lines=15 ) return await ctx.send(embed=embed)
async def server_info(self, ctx: Context): """ Returns an embed full of server information. """ created = time_since(ctx.guild.created_at, precision="days") features = ", ".join(ctx.guild.features) region = ctx.guild.region # How many of each type of channel? roles = len(ctx.guild.roles) channels = ctx.guild.channels text_channels = 0 category_channels = 0 voice_channels = 0 for channel in channels: if type(channel) == TextChannel: text_channels += 1 elif type(channel) == CategoryChannel: category_channels += 1 elif type(channel) == VoiceChannel: voice_channels += 1 # How many of each user status? member_count = ctx.guild.member_count members = ctx.guild.members online = 0 dnd = 0 idle = 0 offline = 0 for member in members: if str(member.status) == "online": online += 1 elif str(member.status) == "offline": offline += 1 elif str(member.status) == "idle": idle += 1 elif str(member.status) == "dnd": dnd += 1 embed = Embed(colour=Colour.blurple(), description=textwrap.dedent(f""" **Server information** Created: {created} Voice region: {region} Features: {features} **Counts** Members: {member_count:,} Roles: {roles} Text: {text_channels} Voice: {voice_channels} Channel categories: {category_channels} **Members** {Emojis.status_online} {online} {Emojis.status_idle} {idle} {Emojis.status_dnd} {dnd} {Emojis.status_offline} {offline} """)) embed.set_thumbnail(url=ctx.guild.icon_url) await ctx.send(embed=embed)
async def zen(self, ctx: Context, *, search_value: Union[int, str, None] = None) -> None: """ Show the Zen of Python. Without any arguments, the full Zen will be produced. If an integer is provided, the line with that index will be produced. If a string is provided, the line which matches best will be produced. """ embed = Embed( colour=Colour.blurple(), title="The Zen of Python", description=ZEN_OF_PYTHON ) if search_value is None: embed.title += ", by Tim Peters" await ctx.send(embed=embed) return zen_lines = ZEN_OF_PYTHON.splitlines() # handle if it's an index int if isinstance(search_value, int): upper_bound = len(zen_lines) - 1 lower_bound = -1 * upper_bound if not (lower_bound <= search_value <= upper_bound): raise BadArgument(f"Please provide an index between {lower_bound} and {upper_bound}.") embed.title += f" (line {search_value % len(zen_lines)}):" embed.description = zen_lines[search_value] await ctx.send(embed=embed) return # Try to handle first exact word due difflib.SequenceMatched may use some other similar word instead # exact word. for i, line in enumerate(zen_lines): for word in line.split(): if word.lower() == search_value.lower(): embed.title += f" (line {i}):" embed.description = line await ctx.send(embed=embed) return # handle if it's a search string and not exact word matcher = difflib.SequenceMatcher(None, search_value.lower()) best_match = "" match_index = 0 best_ratio = 0 for index, line in enumerate(zen_lines): matcher.set_seq2(line.lower()) # the match ratio needs to be adjusted because, naturally, # longer lines will have worse ratios than shorter lines when # fuzzy searching for keywords. this seems to work okay. adjusted_ratio = (len(line) - 5) ** 0.5 * matcher.ratio() if adjusted_ratio > best_ratio: best_ratio = adjusted_ratio best_match = line match_index = index if not best_match: raise BadArgument("I didn't get a match! Please try again with a different search term.") embed.title += f" (line {match_index}):" embed.description = best_match await ctx.send(embed=embed)
async def member_info(self, ctx: Context, *, user: discord.Member) -> None: """Returns info about a member.""" roles = "" activities = "" joined = user.joined_at name = user.name gstatus = user.status wstatus = user.web_status dstatus = user.desktop_status mstatus = user.mobile_status bot = user.bot nick = user.nick boostsince = user.premium_since pc = user.is_on_pc() web = user.is_on_web() mobile = user.is_on_mobile() top = user.top_role #List of users roles for i in range(len(user.roles)): roles += str(user.roles[i].mention) + ", " embed = discord.Embed(title=str(user.display_name) + "'s Information", colour=Colour.blurple()) embed.add_field(name="Member joined on", value=joined.strftime("%A %d %B %Y %H:%M")) embed.add_field(name="Members Nickname", value=nick) if boostsince == None: embed.add_field(name="Boosted server since", value="Never boosted") else: embed.add_field(name="Boosted server since", value=boostsince.strftime("%A %d %B %Y %H:%M")) embed.add_field(name=":robot: Bot?", value=bot) embed.add_field(name="On PC?", value=pc) embed.add_field(name="On Web?", value=web) embed.add_field(name=":iphone: On Mobile?", value=mobile) embed.add_field(name=":computer: Desktop App", value=dstatus) embed.add_field(name="Web/Browser App", value=wstatus) embed.add_field(name=":iphone: Android/iOS App", value=mstatus) embed.add_field(name="Roles Member is in", value=roles) embed.add_field(name="Highest role of Member", value=top) embed.set_thumbnail(url=user.avatar_url) await ctx.send(embed=embed)
class Information(Cog): """A cog with commands for generating embeds with server info, such as server stats and user info.""" def __init__(self, bot: Bot): self.bot = bot @staticmethod def role_can_read(channel: GuildChannel, role: Role) -> bool: """Return True if `role` can read messages in `channel`.""" overwrites = channel.overwrites_for(role) return overwrites.read_messages is True def get_staff_channel_count(self, guild: Guild) -> int: """ Get the number of channels that are staff-only. We need to know two things about a channel: - Does the @everyone role have explicit read deny permissions? - Do staff roles have explicit read allow permissions? If the answer to both of these questions is yes, it's a staff channel. """ channel_ids = set() for channel in guild.channels: if channel.type is ChannelType.category: continue everyone_can_read = self.role_can_read(channel, guild.default_role) for role in constants.STAFF_ROLES: role_can_read = self.role_can_read(channel, guild.get_role(role)) if role_can_read and not everyone_can_read: channel_ids.add(channel.id) break return len(channel_ids) @staticmethod def get_channel_type_counts(guild: Guild) -> str: """Return the total amounts of the various types of channels in `guild`.""" channel_counter = Counter(c.type for c in guild.channels) channel_type_list = [] for channel, count in channel_counter.items(): channel_type = str(channel).title() channel_type_list.append(f"{channel_type} channels: {count}") channel_type_list = sorted(channel_type_list) return "\n".join(channel_type_list) @with_role(*constants.MODERATION_ROLES) @command(name="roles") async def roles_info(self, ctx: Context) -> None: """Returns a list of all roles and their corresponding IDs.""" # Sort the roles alphabetically and remove the @everyone role roles = sorted(ctx.guild.roles[1:], key=lambda role: role.name) # Build a list role_list = [] for role in roles: role_list.append(f"`{role.id}` - {role.mention}") # Build an embed embed = Embed( title= f"Role information (Total {len(roles)} role{'s' * (len(role_list) > 1)})", colour=Colour.blurple()) await LinePaginator.paginate(role_list, ctx, embed, empty=False) @with_role(*constants.MODERATION_ROLES) @command(name="role") async def role_info(self, ctx: Context, *roles: Union[Role, str]) -> None: """ Return information on a role or list of roles. To specify multiple roles just add to the arguments, delimit roles with spaces in them using quotation marks. """ parsed_roles = [] failed_roles = [] for role_name in roles: if isinstance(role_name, Role): # Role conversion has already succeeded parsed_roles.append(role_name) continue role = utils.find(lambda r: r.name.lower() == role_name.lower(), ctx.guild.roles) if not role: failed_roles.append(role_name) continue parsed_roles.append(role) if failed_roles: await ctx.send( f":x: Could not retrieve the following roles: {', '.join(failed_roles)}" ) for role in parsed_roles: h, s, v = colorsys.rgb_to_hsv(*role.colour.to_rgb()) embed = Embed( title=f"{role.name} info", colour=role.colour, ) embed.add_field(name="ID", value=role.id, inline=True) embed.add_field(name="Colour (RGB)", value=f"#{role.colour.value:0>6x}", inline=True) embed.add_field(name="Colour (HSV)", value=f"{h:.2f} {s:.2f} {v}", inline=True) embed.add_field(name="Member count", value=len(role.members), inline=True) embed.add_field(name="Position", value=role.position) embed.add_field(name="Permission code", value=role.permissions.value, inline=True) await ctx.send(embed=embed) @command(name="server", aliases=["server_info", "guild", "guild_info"]) async def server_info(self, ctx: Context) -> None: """Returns an embed full of server information.""" created = time_since(ctx.guild.created_at, precision="days") features = ", ".join(ctx.guild.features) region = ctx.guild.region roles = len(ctx.guild.roles) member_count = ctx.guild.member_count channel_counts = self.get_channel_type_counts(ctx.guild) # How many of each user status? statuses = Counter(member.status for member in ctx.guild.members) embed = Embed(colour=Colour.blurple()) # How many staff members and staff channels do we have? staff_member_count = len( ctx.guild.get_role(constants.Roles.helpers).members) staff_channel_count = self.get_staff_channel_count(ctx.guild) # Because channel_counts lacks leading whitespace, it breaks the dedent if it's inserted directly by the # f-string. While this is correctly formated by Discord, it makes unit testing difficult. To keep the formatting # without joining a tuple of strings we can use a Template string to insert the already-formatted channel_counts # after the dedent is made. embed.description = Template( textwrap.dedent(f""" **Server information** Created: {created} Voice region: {region} Features: {features} **Channel counts** $channel_counts Staff channels: {staff_channel_count} **Member counts** Members: {member_count:,} Staff members: {staff_member_count} Roles: {roles} **Member statuses** {constants.Emojis.status_online} {statuses[Status.online]:,} {constants.Emojis.status_idle} {statuses[Status.idle]:,} {constants.Emojis.status_dnd} {statuses[Status.dnd]:,} {constants.Emojis.status_offline} {statuses[Status.offline]:,} """)).substitute({"channel_counts": channel_counts}) embed.set_thumbnail(url=ctx.guild.icon_url) await ctx.send(embed=embed) @command(name="user", aliases=["user_info", "member", "member_info"]) async def user_info(self, ctx: Context, user: Member = None) -> None: """Returns info about a user.""" if user is None: user = ctx.author # Do a role check if this is being executed on someone other than the caller elif user != ctx.author and not with_role_check( ctx, *constants.MODERATION_ROLES): await ctx.send( "You may not use this command on users other than yourself.") return # Non-staff may only do this in #bot-commands if not with_role_check(ctx, *constants.STAFF_ROLES): if not ctx.channel.id == constants.Channels.bot_commands: raise InWhitelistCheckFailure(constants.Channels.bot_commands) embed = await self.create_user_embed(ctx, user) await ctx.send(embed=embed) async def create_user_embed(self, ctx: Context, user: Member) -> Embed: """Creates an embed containing information on the `user`.""" created = time_since(user.created_at, max_units=3) # Custom status custom_status = '' for activity in user.activities: if isinstance(activity, CustomActivity): state = "" if activity.name: state = escape_markdown(activity.name) emoji = "" if activity.emoji: # If an emoji is unicode use the emoji, else write the emote like :abc: if not activity.emoji.id: emoji += activity.emoji.name + " " else: emoji += f"`:{activity.emoji.name}:` " custom_status = f'Status: {emoji}{state}\n' name = str(user) if user.nick: name = f"{user.nick} ({name})" badges = [] for badge, is_set in user.public_flags: if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}", None)): badges.append(emoji) joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) desktop_status = STATUS_EMOTES.get(user.desktop_status, constants.Emojis.status_online) web_status = STATUS_EMOTES.get(user.web_status, constants.Emojis.status_online) mobile_status = STATUS_EMOTES.get(user.mobile_status, constants.Emojis.status_online) fields = [("User information", textwrap.dedent(f""" Created: {created} Profile: {user.mention} ID: {user.id} {custom_status} """).strip()), ("Member information", textwrap.dedent(f""" Joined: {joined} Roles: {roles or None} """).strip()), ("Status", textwrap.dedent(f""" {desktop_status} Desktop {web_status} Web {mobile_status} Mobile """).strip())] # Show more verbose output in moderation channels for infractions and nominations if ctx.channel.id in constants.MODERATION_CHANNELS: fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) else: fields.append(await self.basic_user_infraction_counts(user)) # Let's build the embed now embed = Embed(title=name, description=" ".join(badges)) for field_name, field_content in fields: embed.add_field(name=field_name, value=field_content, inline=False) embed.set_thumbnail(url=user.avatar_url_as(static_format="png")) embed.colour = user.top_role.colour if roles else Colour.blurple() return embed