feat(bot): ✨ Create p.leaderboard command.
This commit is contained in:
+174
-3
@@ -1,19 +1,65 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, pages
|
||||
from discord.ext import commands, pages, tasks
|
||||
from discord.utils import find
|
||||
|
||||
from tortoise import transactions, functions
|
||||
from tortoise import transactions, functions, expressions, exceptions
|
||||
|
||||
from payphone import bot
|
||||
from payphone.db import models, utils
|
||||
from payphone.cogs import base
|
||||
from payphone.core import constants
|
||||
from payphone.core import constants, formatting as fmt
|
||||
|
||||
|
||||
class Users(base.Cog):
|
||||
def __init__(self, bot_instance: bot.Payphone):
|
||||
self.bot = bot_instance
|
||||
|
||||
self.update_leaderboard.add_exception_type(exceptions.ConfigurationError)
|
||||
self.update_leaderboard.start()
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
self.update_leaderboard.cancel()
|
||||
|
||||
@tasks.loop(minutes=15)
|
||||
async def update_leaderboard(self) -> None:
|
||||
"""Updates the leaderboard."""
|
||||
|
||||
calls = (
|
||||
await models.Call.annotate(messages_count=functions.Count("messages"))
|
||||
.filter(
|
||||
started_at__month=datetime.now(timezone.utc).month,
|
||||
started_at__year=datetime.now(timezone.utc).year,
|
||||
messages_count__gte=25,
|
||||
)
|
||||
.prefetch_related("call_rss", "call_rss__user")
|
||||
)
|
||||
|
||||
# ID to count
|
||||
users_count_map: dict[int, int] = {}
|
||||
|
||||
users_map: dict[int, models.User] = {
|
||||
rs.user.id: rs.user for call in calls for rs in call.call_rss
|
||||
}
|
||||
|
||||
for call in calls:
|
||||
for rs in call.call_rss:
|
||||
if rs.user_id in users_count_map:
|
||||
users_count_map[rs.user_id] += 1
|
||||
|
||||
else:
|
||||
users_count_map[rs.user_id] = 0
|
||||
|
||||
user_counts = list(users_count_map.items())
|
||||
|
||||
user_counts.sort(key=lambda i: i[1])
|
||||
|
||||
for i, user_id in enumerate(user_counts):
|
||||
users_map[user_id[0]].leaderboard_position = i + 1
|
||||
|
||||
await models.User.bulk_update(users_map.values(), ["leaderboard_position"], 500)
|
||||
|
||||
@commands.command()
|
||||
@transactions.atomic()
|
||||
async def mask(self, ctx: commands.Context, toggle: bool | None = None) -> None:
|
||||
@@ -247,6 +293,131 @@ class Users(base.Cog):
|
||||
|
||||
await paginator.send(ctx)
|
||||
|
||||
@commands.command()
|
||||
@transactions.atomic()
|
||||
async def me(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
Shows your statistics.
|
||||
"""
|
||||
|
||||
user, profile = await utils.get_user(ctx.author)
|
||||
|
||||
messages_count = await models.Message.filter(user=user).count()
|
||||
|
||||
channel_ids = (
|
||||
await models.CallUserChannelRelationship.filter(user=user)
|
||||
.distinct()
|
||||
.values_list("channel_id", flat=True)
|
||||
)
|
||||
call_ids = (
|
||||
await models.CallUserChannelRelationship.filter(user=user)
|
||||
.distinct()
|
||||
.values_list("call_id", flat=True)
|
||||
)
|
||||
|
||||
people_met_count = await models.CallUserChannelRelationship.filter(
|
||||
call_id__in=call_ids, channel_id__not_in=channel_ids
|
||||
).count()
|
||||
ban = (
|
||||
await models.Ban.filter(
|
||||
expressions.Q(
|
||||
expires_at__gt=datetime.now(timezone.utc),
|
||||
expires_at=None,
|
||||
join_type="OR",
|
||||
),
|
||||
user_id=user.id,
|
||||
)
|
||||
.limit(1)
|
||||
.first()
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=constants.EMBED_COLOR,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
fields=[
|
||||
discord.EmbedField(
|
||||
name="Messages", value=str(messages_count), inline=True
|
||||
),
|
||||
discord.EmbedField(name="Calls", value=len(call_ids), inline=True),
|
||||
discord.EmbedField(
|
||||
name="People Met", value=people_met_count, inline=True
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Joined At",
|
||||
value=f"<t:{int(user.joined_at.timestamp())}:d> <t:{int(user.joined_at.timestamp())}:T>",
|
||||
inline=True,
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Leaderboard",
|
||||
value=(
|
||||
f"#{user.leaderboard_position} (Monthly)"
|
||||
if user.leaderboard_position is not None
|
||||
else "*No calls yet.*"
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
discord.EmbedField(
|
||||
name="Banned",
|
||||
value=(
|
||||
"*Not banned.*"
|
||||
if ban is None
|
||||
else (
|
||||
"*Permanent.*"
|
||||
if ban.expires_at is None
|
||||
else f"<t:{int(ban.expires_at.timestamp())}:F> (<t:{int(ban.expires_at.timestamp())}:R>)"
|
||||
)
|
||||
),
|
||||
inline=True,
|
||||
),
|
||||
],
|
||||
author=discord.EmbedAuthor(name=profile.name, icon_url=profile.avatar_url),
|
||||
footer=discord.EmbedFooter(
|
||||
text="Thank you for using Payphone! Leaderboard updates every 15 minutes."
|
||||
),
|
||||
thumbnail=profile.avatar_url,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(aliases=["l", "leader"])
|
||||
async def leaderboard(self, ctx: commands.Context) -> None:
|
||||
"""
|
||||
Shows the leaderboard for the month.
|
||||
"""
|
||||
|
||||
top_users = (
|
||||
await models.User.filter(leaderboard_position__lte=10)
|
||||
.prefetch_related("profiles")
|
||||
.order_by("leaderboard_position")
|
||||
.limit(10)
|
||||
)
|
||||
|
||||
rows = [
|
||||
[f"#{pos + 1}", fmt.trim(user.profiles[0].name, 10)]
|
||||
for pos, user in enumerate(top_users)
|
||||
]
|
||||
|
||||
if not any(map(lambda u: u.id == ctx.author.id, top_users)):
|
||||
user, profile = await utils.get_user(ctx.author)
|
||||
|
||||
if user.leaderboard_position is not None:
|
||||
rows.append([f"#{user.leaderboard_position}", fmt.trim(profile.name, 10)])
|
||||
|
||||
table = fmt.monospace_table(rows)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=constants.EMBED_COLOR,
|
||||
title=f"Monthly Leaderboard ({datetime.now(timezone.utc).strftime("%B")})",
|
||||
description=f"```\n{table}\n```",
|
||||
author=self.bot.embed_author,
|
||||
footer=discord.EmbedFooter(
|
||||
text="The leaderboard is updated every 15 minutes."
|
||||
),
|
||||
thumbnail=self.bot.user.display_avatar.url,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(b: bot.Payphone) -> None:
|
||||
"""Load the cog into the bot.
|
||||
|
||||
+29
-14
@@ -83,22 +83,37 @@ def monospace_table(
|
||||
str: The monospace table.
|
||||
"""
|
||||
|
||||
max_sizes = [
|
||||
max(
|
||||
*(
|
||||
[
|
||||
(
|
||||
len(str(row[i]))
|
||||
if not isinstance(row[i], tuple)
|
||||
else (sum(len(str(item)) for item in row[i]) + 1)
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
+ ([len(headers[i])] if headers is not None else [])
|
||||
max_sizes = (
|
||||
[
|
||||
max(
|
||||
*(
|
||||
[
|
||||
(
|
||||
len(str(row[i]))
|
||||
if not isinstance(row[i], tuple)
|
||||
else (sum(len(str(item)) for item in row[i]) + 1)
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
+ ([len(headers[i])] if headers is not None else [])
|
||||
)
|
||||
)
|
||||
for i in range(len(rows[0]))
|
||||
]
|
||||
if len(rows) >= 2
|
||||
else (
|
||||
[
|
||||
(
|
||||
len(str(col))
|
||||
if not isinstance(col, tuple)
|
||||
else (sum(len(str(item)) for item in col) + 1)
|
||||
)
|
||||
for col in rows[0]
|
||||
]
|
||||
if len(rows) > 0
|
||||
else []
|
||||
)
|
||||
for i in range(len(rows[0]))
|
||||
]
|
||||
)
|
||||
|
||||
def format_row(row: list) -> str:
|
||||
return column_separator.join(
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "calluserchannelrelationship" ADD "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
ALTER TABLE "user" ADD "leaderboard_position" INT;
|
||||
CREATE INDEX "idx_callusercha_created_e72939" ON "calluserchannelrelationship" ("created_at");
|
||||
CREATE INDEX "idx_user_leaderb_1dcee5" ON "user" ("leaderboard_position");"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
DROP INDEX "idx_callusercha_created_e72939";
|
||||
DROP INDEX "idx_user_leaderb_1dcee5";
|
||||
ALTER TABLE "user" DROP COLUMN "leaderboard_position";
|
||||
ALTER TABLE "calluserchannelrelationship" DROP COLUMN "created_at";"""
|
||||
@@ -0,0 +1,11 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" ADD "joined_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "user" DROP COLUMN "joined_at";"""
|
||||
@@ -18,6 +18,10 @@ class User(models.Model):
|
||||
is_moderator: bool = fields.BooleanField(default=False)
|
||||
profiles: fields.BackwardFKRelation["Profile"]
|
||||
call_rss: fields.BackwardFKRelation["CallUserChannelRelationship"]
|
||||
leaderboard_position: int | None = fields.IntField(
|
||||
default=None, null=True, db_index=True
|
||||
)
|
||||
joined_at: datetime = fields.DatetimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Call(models.Model):
|
||||
@@ -130,6 +134,7 @@ class CallUserChannelRelationship(models.Model):
|
||||
)
|
||||
is_masked: bool = fields.BooleanField()
|
||||
mask: int = fields.BigIntField()
|
||||
created_at: datetime = fields.DatetimeField(auto_now_add=True, db_index=True)
|
||||
|
||||
@property
|
||||
def mask_name(self) -> str:
|
||||
|
||||
+4
-12
@@ -5,9 +5,7 @@ import discord
|
||||
from payphone.db import models
|
||||
|
||||
|
||||
async def get_channel(
|
||||
channel: discord.TextChannel, for_update: bool = False
|
||||
) -> models.Channel:
|
||||
async def get_channel(channel: discord.TextChannel, for_update: bool = False) -> models.Channel:
|
||||
"""Returns a channel from the database, creating or updating it if it already exists.
|
||||
|
||||
Args:
|
||||
@@ -18,15 +16,9 @@ async def get_channel(
|
||||
models.Channel: The channel row.
|
||||
"""
|
||||
|
||||
model = models.Channel
|
||||
|
||||
if for_update:
|
||||
model = model.select_for_update()
|
||||
|
||||
row = await model.get_or_none(id=channel.id)
|
||||
|
||||
if row is None:
|
||||
row = await models.Channel.create(id=channel.id, server_id=channel.guild.id)
|
||||
row, _ = await models.Channel.update_or_create(
|
||||
id=channel.id, server=channel.guild.id
|
||||
)
|
||||
|
||||
channel_name = channel.name.replace("discord", "d").replace("wumpus", "w")
|
||||
webhook_name = f"Payphone #{channel_name}"
|
||||
|
||||
Reference in New Issue
Block a user