feat(bot): ✨ Add servers leaderboard.

This commit is contained in:
Nyeki.py
2024-10-19 00:39:33 -03:00
parent de65936073
commit 4e040ca8ad
6 changed files with 174 additions and 21 deletions
+9 -1
View File
@@ -19,6 +19,7 @@ class Payphone(commands.AutoShardedBot):
def __init__(self) -> None:
intents = discord.Intents.default()
intents.message_content = True
intents.guilds = True
if config.IS_DEBUG:
intents.members = True
@@ -280,7 +281,14 @@ class Payphone(commands.AutoShardedBot):
prefix = prefix.decode()
else:
server, _ = await models.Server.get_or_create(id=server_id)
server, _ = await models.Server.update_or_create(
(
{"name": message.guild.name}
if message is not None and message.guild is not None
else None
),
id=server_id,
)
await redis.set(key, server.prefix, ex=60 * 60)
+82 -3
View File
@@ -1,7 +1,10 @@
import discord
from discord.ext import commands
from datetime import datetime, timezone
from tortoise import transactions
import discord
from discord import errors
from discord.ext import commands, tasks
from tortoise import transactions, functions, exceptions
from payphone import bot
from payphone.db import models, utils
@@ -12,6 +15,82 @@ class Admin(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__channel", "call_rss__channel__server"
)
)
# ID to count
servers_count_map: dict[int, int] = {}
servers_map: dict[int, models.Server] = {
rs.channel.server_id: rs.channel.server
for call in calls
for rs in call.call_rss
}
for call in calls:
for rs in call.call_rss:
if rs.channel.server_id in servers_count_map:
servers_count_map[rs.channel.server.id] += 1
else:
servers_count_map[rs.channel.server_id] = 0
server_counts = list(servers_count_map.items())
server_counts.sort(key=lambda i: i[1], reverse=True)
for server_map in server_counts[:10]:
if servers_map[server_map[0]].name is None:
try:
servers_map[server_map[0]].name = (
await self.bot.fetch_guild(server_map[0])
).name
except errors.DiscordException:
pass
for i, server_map in enumerate(server_counts):
servers_map[server_map[0]].leaderboard_position = i + 1
servers_map[server_map[0]].leaderboard_calls_count = server_map[1]
await models.Server.bulk_update(
servers_map.values(),
["name", "leaderboard_position", "leaderboard_calls_count"],
500,
)
@commands.Cog.listener()
async def on_guild_update(
self, _before: discord.Guild, after: discord.Guild
) -> None:
"""Updates a server.
Args:
_before (discord.Guild): The server before it being updated.
after (discord.Guild): The server after it being updated.
"""
await models.Server.update_or_create({"name": after.name}, id=after.id)
@commands.group(invoke_without_command=True)
@commands.guild_only()
@commands.has_guild_permissions(manage_guild=True)
+34 -8
View File
@@ -471,36 +471,62 @@ class Users(base.Cog):
await ctx.send(embed=embed)
@commands.command(aliases=["l", "leader"])
async def leaderboard(self, ctx: commands.Context) -> None:
@commands.command(aliases=["l", "leader", "ranking"])
async def leaderboard(self, ctx: commands.Context, board: str = "users") -> None:
"""
Shows the leaderboard for the month.
Options:
- `[<board>]`: The board to show (users/servers). Defaults to users.
"""
users_board_options = ["u", "user", "users"]
server_board_options = ["s", "server", "servers", "g", "guild", "guilds"]
if board.lower() not in users_board_options + server_board_options:
await ctx.send(
f"❌ This leaderboard does not exist. The leaderboards are `servers` and `users`, for example `{ctx.clean_prefix}leaderboard users`."
)
return
table = ""
if board.lower() in users_board_options:
top_users = (
await models.User.filter(leaderboard_position__lte=10)
.prefetch_related("profiles")
.order_by("leaderboard_position")
.limit(10)
)
user_rows = [
f"**#{pos + 1} - {user.profiles[0].name}**{" (You!)" if user.id == ctx.author.id else ""}\nParticipated in {user.leaderboard_calls_count} calls."
for pos, user in enumerate(top_users)
]
table = "__**Top Users:**__\n\n" + "\n\n".join(user_rows)
else:
top_servers = (
await models.Server.filter(leaderboard_position__lte=10)
.order_by("leaderboard_position")
.limit(10)
)
server_rows = [
f"**#{pos + 1} - {server.name or "Updating..."}**{" (Here!)" if ctx.guild is not None and server.id == ctx.guild.id else ""}\nParticipated in {server.leaderboard_calls_count} calls."
for pos, server in enumerate(top_servers)
]
table = "__**Top Servers:**__\n\n" + "\n\n".join(server_rows)
embed = discord.Embed(
color=constants.EMBED_COLOR,
title=f"Monthly Leaderboard ({datetime.now(timezone.utc).strftime("%B")})",
description=(
"Payphone's monthly leaderboard is based on the amount of calls you've "
"participated in with more than 25 messages sent (to prevent spam). The stats are "
"resetted every month.\n"
"\n"
"__**Top Users:**__\n"
"participated in with more than 25 messages sent (to prevent spam).\n"
"\n"
)
+ "\n\n".join(user_rows),
+ table,
author=self.bot.embed_author,
footer=discord.EmbedFooter(
text="The leaderboard is updated every 15 minutes."
@@ -0,0 +1,15 @@
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "server" ADD "leaderboard_calls_count" INT NOT NULL DEFAULT 0;
ALTER TABLE "server" ADD "leaderboard_position" INT NOT NULL DEFAULT 0;
CREATE INDEX "idx_server_leaderb_47c354" ON "server" ("leaderboard_position");"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
DROP INDEX "idx_server_leaderb_47c354";
ALTER TABLE "server" DROP COLUMN "leaderboard_calls_count";
ALTER TABLE "server" DROP COLUMN "leaderboard_position";"""
@@ -0,0 +1,11 @@
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "server" ADD "name" VARCHAR(100);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "server" DROP COLUMN "name";"""
+14
View File
@@ -4,6 +4,7 @@ import enum
from tortoise import models, fields
import discord
from discord import webhook
import aiohttp
@@ -72,13 +73,26 @@ class Profile(models.Model):
return f"[{self.name}]({self.jump_url})"
@property
def embed_author(self) -> discord.EmbedAuthor:
"""Returns this profile as an EmbedAuthor object.
Returns:
discord.EmbedAuthor: The EmbedAuthor object.
"""
return discord.EmbedAuthor(name=self.name, icon_url=self.avatar_url)
class Meta:
unique_together = [("user_id", "server_id")]
class Server(models.Model):
id: int = fields.BigIntField(primary_key=True)
name: str = fields.CharField(max_length=100, null=True, default=None)
prefix: str = fields.CharField(max_length=5, default="p.")
leaderboard_position: int = fields.IntField(default=0, db_index=True)
leaderboard_calls_count: int = fields.IntField(default=0)
channels: fields.BackwardFKRelation["Channel"]
profiles: fields.BackwardFKRelation["Profile"]