diff --git a/payphone/cogs/calls/embeds.py b/payphone/cogs/calls/embeds.py index 1afb0cc..97d1e5f 100644 --- a/payphone/cogs/calls/embeds.py +++ b/payphone/cogs/calls/embeds.py @@ -51,9 +51,11 @@ async def ReplyEmbed( embed = discord.Embed( color=constants.EMBED_COLOR, - description=formatting.trim(last_content.content) - if last_content.was_propagated or is_this_channel - else "*This message was censored.*", + description=( + formatting.trim(last_content.content) + if last_content.was_propagated or is_this_channel + else "*This message was censored.*" + ), author=discord.EmbedAuthor( name=f"Replied to {author_name}", icon_url=author_avatar_url, diff --git a/payphone/cogs/mods/cog.py b/payphone/cogs/mods/cog.py index 4cb428f..b2b0ad6 100644 --- a/payphone/cogs/mods/cog.py +++ b/payphone/cogs/mods/cog.py @@ -1,3 +1,5 @@ +from typing import Awaitable + from datetime import datetime, timezone, timedelta import sys @@ -13,7 +15,7 @@ from payphone import bot from payphone.db import models, utils from payphone.cogs import base from payphone.cogs.mods import embeds, views -from payphone.core import checks, converters, constants +from payphone.core import checks, converters, constants, formatting as fmt class Mods(base.Cog): @@ -291,29 +293,100 @@ class Mods(base.Cog): @commands.command() @commands.is_owner() - async def stats(self, ctx: commands.Context) -> None: + async def stats( + self, ctx: commands.Context, iterations: int = 5, wait: int = 5 + ) -> None: """ Shows all Payphone's stats. + + Options: + - ``: The amount of iterations (to display progress). Defaults to 5. + - ``: The amount of seconds to wait between iterations. Defaults to 5. """ stats = [ - f"On-call Channels: {await self.bot.redis.scard(constants.CHANNELS_ONCALL_SET)}", - f"Searching Channels: {await self.bot.redis.scard(constants.CHANNELS_SEARCH_QUEUE)}", - f"Messages: {await models.Message.all().count()}", - f"Message Contents: {await models.MessageContent.all().count()}", - f"Calls: {await models.Call.all().count()}", - f"Ongoing Calls: {await models.Call.filter(ended_at=None).count()}", - f"Bans: {await models.Ban.all().count()}", - f"Reports: {await models.Report.all().count()}", - f"On-review Reports: {await models.Report.filter(status=models.ReportStatus.ON_REVIEW).count()}", - f"Users: {await models.User.all().count()}", - f"Profiles: {await models.Profile.all().count()}", - f"Servers: {await models.Server.all().count()}", - f"Shards: {self.bot.shard_count}", - f"Current Shard: {ctx.guild.shard_id + 1}", + ( + "On-call Channels", + lambda: self.bot.redis.scard(constants.CHANNELS_ONCALL_SET), + ), + ( + "Searching Channels", + lambda: self.bot.redis.scard(constants.CHANNELS_SEARCH_QUEUE), + ), + ( + "Messages", + lambda: models.Message.all().count(), + ), + ( + "Message Contents", + lambda: models.MessageContent.all().count(), + ), + ("Calls", lambda: models.Call.all().count()), + ( + "Ongoing Calls", + lambda: models.Call.filter(ended_at=None).count(), + ), + ("Bans", lambda: models.Ban.all().count()), + ("Reports", lambda: models.Report.all().count()), + ( + "On-review Reports", + lambda: models.Report.filter( + status=models.ReportStatus.ON_REVIEW + ).count(), + ), + ("Users", lambda: models.User.all().count()), + ( + "Profiles", + lambda: models.Profile.all().count(), + ), + ("Servers", lambda: models.Server.all().count()), + ("Shards", lambda: self.bot.shard_count), + ("Current Shards", lambda: ctx.guild.shard_id + 1), ] - await ctx.send(f"```\n{"\n".join(stats)}\n```") + rows = [[stat[0]] for stat in stats] + rows_with_diff = [[stat[0]] for stat in stats] + + i = 0 + message = None + + while i < iterations: + i += 1 + + for stat_i, stat in enumerate(stats): + value = stat[1]() + + if isinstance(value, Awaitable): + value = await value + + rows[stat_i].append(value) + prev_value = rows[stat_i][-2] + + if isinstance(prev_value, str): + rows_with_diff[stat_i].append(value) + + else: + diff = value - prev_value + + rows_with_diff[stat_i].append( + str(value) + + (f" ({"+" if diff > 0 else ""}{diff})" if diff != 0 else "") + ) + + table = fmt.monospace_table( + rows_with_diff, + [""] + [f"{wait * j}s" for j in range(i)], + padding=2, + ) + + if message is None: + message = await ctx.send(f"```\n{table}\n```") + + else: + message = await message.edit(f"```\n{table}\n```") + + if i < iterations: + await asyncio.sleep(wait) def setup(b: bot.Payphone) -> None: diff --git a/payphone/cogs/mods/embeds.py b/payphone/cogs/mods/embeds.py index 2e19669..27dee0c 100644 --- a/payphone/cogs/mods/embeds.py +++ b/payphone/cogs/mods/embeds.py @@ -28,15 +28,19 @@ class BanNotificationEmbed(discord.Embed): ), discord.EmbedField( name="Expires At", - value=f" ()" - if ban.expires_at - else "*Does not expire.*", + value=( + f" ()" + if ban.expires_at + else "*Does not expire.*" + ), ), discord.EmbedField( name="Appeal", - value="*You haven't appealed this ban.*" - if ban.appeal is None - else ban.appeal, + value=( + "*You haven't appealed this ban.*" + if ban.appeal is None + else ban.appeal + ), ), ], author=bot.embed_author, diff --git a/payphone/cogs/mods/views.py b/payphone/cogs/mods/views.py index d8feb31..94329f3 100644 --- a/payphone/cogs/mods/views.py +++ b/payphone/cogs/mods/views.py @@ -186,7 +186,7 @@ class ReportView(discord.ui.View): await interaction.response.edit_message( content="✅ You're up to date! No reports left to check.", embed=None, - view=None + view=None, ) return @@ -218,7 +218,7 @@ class ReportView(discord.ui.View): await interaction.response.edit_message( content="✅ You're up to date! No reports left to check.", embed=None, - view=None + view=None, ) return diff --git a/payphone/cogs/users/cog.py b/payphone/cogs/users/cog.py index dcfe898..6011f9b 100644 --- a/payphone/cogs/users/cog.py +++ b/payphone/cogs/users/cog.py @@ -175,9 +175,11 @@ class Users(base.Cog): logs += f"- `{call_id}`: " participant_names = ", ".join( - call_rs.display_name - if call_rs.channel_id != data["user_rs"].channel_id - else call_rs.profile.name + ( + call_rs.display_name + if call_rs.channel_id != data["user_rs"].channel_id + else call_rs.profile.name + ) for call_rs in data["others_rs"] + [data["user_rs"]] ) diff --git a/payphone/core/censorship.py b/payphone/core/censorship.py index ecb6f88..39be68d 100644 --- a/payphone/core/censorship.py +++ b/payphone/core/censorship.py @@ -246,16 +246,16 @@ def moderate(message: str) -> str: match = next(PATTERN.finditer(message), None) if match is not None: - return message[match.start():match.end()] + return message[match.start() : match.end()] match = next(URL_PATTERN.finditer(message), None) if match is not None: - return message[match.start():match.end()] + return message[match.start() : match.end()] match = next(INVITES_PATTERN.finditer(message), None) if match is not None: - return message[match.start():match.end()] + return message[match.start() : match.end()] return None diff --git a/payphone/core/checks.py b/payphone/core/checks.py index 72c21bc..674b6be 100644 --- a/payphone/core/checks.py +++ b/payphone/core/checks.py @@ -27,9 +27,9 @@ def is_unbanned(): async def predicate(ctx: commands.Context) -> bool: if await ctx.bot.is_banned(ctx.author): raise exceptions.BannedError() - + return True - + return commands.check(predicate) @@ -45,7 +45,7 @@ def is_on_call(): if not is_on_call: raise exceptions.NotOnCallError() - + return True return commands.check(predicate) diff --git a/payphone/core/formatting.py b/payphone/core/formatting.py index 9814218..3296dff 100644 --- a/payphone/core/formatting.py +++ b/payphone/core/formatting.py @@ -56,6 +56,68 @@ def monospace_grid(values: list[str], space: int = 14) -> str: return "```" + "".join([v + " " * (space - len(v)) for v in values]) + "```" +def monospace_table( + rows: list[tuple], + headers: list | None = None, + column_separator: str = "|", + header_separator: str = "-", + intersection: str = "+", + padding: int = 1, +) -> str: + """Creates a monospace table. + + Args: + rows (list[tuple]): The rows. + headers (list | None, optional): The headers. + column_separator (str, optional): The column separator. Defaults to "|". + header_separator (str, optional): The header separator. Defaults to "-". + intersection (str, optional): The intersection character where column and row separators meet. + padding (int, optional): The padding that each column has at its sides. + + Returns: + str: The monospace table. + """ + + max_sizes = [ + max( + *( + [len(str(row[i])) for row in rows] + + ([len(headers[i])] if headers is not None else []) + ) + ) + for i in range(len(rows[0])) + ] + + def format_row(row: list) -> str: + return column_separator.join( + [ + (" " * padding) + + str(col) + + (" " * (max_sizes[i] - len(str(col)))) + + (" " * padding) + for i, col in enumerate(row) + ] + ) + + formatted_rows = [] + + for row in rows: + formatted_rows.append(format_row(row)) + + table = "\n".join(formatted_rows) + + if headers is not None: + header = format_row(headers) + + separator = intersection.join( + [header_separator * (size + padding * 2) for size in max_sizes] + ) + + table = f"{header}\n{separator}\n{table}" + + return table + + def clean( message: discord.Message, call_rss: list[models.CallUserChannelRelationship] ) -> str: @@ -75,9 +137,11 @@ def clean( for call_rs in call_rss: cleaned_message = cleaned_message.replace( f"<@{call_rs.user_id}>", - f"@**{call_rs.display_name}**" - if message.channel.id == call_rs.channel_id - else f"@**{call_rs.profile.name}**", + ( + f"@**{call_rs.display_name}**" + if message.channel.id == call_rs.channel_id + else f"@**{call_rs.profile.name}**" + ), ) for user in message.mentions: diff --git a/payphone/core/help_command.py b/payphone/core/help_command.py index f7bd20b..271e47a 100644 --- a/payphone/core/help_command.py +++ b/payphone/core/help_command.py @@ -82,9 +82,11 @@ class HelpCommand(commands.HelpCommand): [f" <{option.name}>" for option in command.options] if isinstance(command, discord.SlashCommand) else [ - f" [<{name}>]" - if typing_utils.is_optional(info.annotation) - else f" <{name}>" + ( + f" [<{name}>]" + if typing_utils.is_optional(info.annotation) + else f" <{name}>" + ) for name, info in command.params.items() if name not in ["self", "ctx"] ] diff --git a/payphone/db/setup.py b/payphone/db/setup.py index d9c3fcd..dcf9400 100644 --- a/payphone/db/setup.py +++ b/payphone/db/setup.py @@ -7,8 +7,7 @@ from payphone.core import config async def init() -> None: - """Initialize the database and it's connections. - """ + """Initialize the database and it's connections.""" await tortoise.Tortoise.init(config.TORTOISE_ORM, use_tz=True)