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: def __init__(self) -> None:
intents = discord.Intents.default() intents = discord.Intents.default()
intents.message_content = True intents.message_content = True
intents.guilds = True
if config.IS_DEBUG: if config.IS_DEBUG:
intents.members = True intents.members = True
@@ -280,7 +281,14 @@ class Payphone(commands.AutoShardedBot):
prefix = prefix.decode() prefix = prefix.decode()
else: 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) await redis.set(key, server.prefix, ex=60 * 60)
+82 -3
View File
@@ -1,7 +1,10 @@
import discord from datetime import datetime, timezone
from discord.ext import commands
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 import bot
from payphone.db import models, utils from payphone.db import models, utils
@@ -12,6 +15,82 @@ class Admin(base.Cog):
def __init__(self, bot_instance: bot.Payphone): def __init__(self, bot_instance: bot.Payphone):
self.bot = bot_instance 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.group(invoke_without_command=True)
@commands.guild_only() @commands.guild_only()
@commands.has_guild_permissions(manage_guild=True) @commands.has_guild_permissions(manage_guild=True)
+34 -8
View File
@@ -471,36 +471,62 @@ class Users(base.Cog):
await ctx.send(embed=embed) await ctx.send(embed=embed)
@commands.command(aliases=["l", "leader"]) @commands.command(aliases=["l", "leader", "ranking"])
async def leaderboard(self, ctx: commands.Context) -> None: async def leaderboard(self, ctx: commands.Context, board: str = "users") -> None:
""" """
Shows the leaderboard for the month. 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 = ( top_users = (
await models.User.filter(leaderboard_position__lte=10) await models.User.filter(leaderboard_position__lte=10)
.prefetch_related("profiles") .prefetch_related("profiles")
.order_by("leaderboard_position") .order_by("leaderboard_position")
.limit(10) .limit(10)
) )
user_rows = [ user_rows = [
f"**#{pos + 1} - {user.profiles[0].name}**{" (You!)" if user.id == ctx.author.id else ""}\nParticipated in {user.leaderboard_calls_count} calls." 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) 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( embed = discord.Embed(
color=constants.EMBED_COLOR, color=constants.EMBED_COLOR,
title=f"Monthly Leaderboard ({datetime.now(timezone.utc).strftime("%B")})", title=f"Monthly Leaderboard ({datetime.now(timezone.utc).strftime("%B")})",
description=( description=(
"Payphone's monthly leaderboard is based on the amount of calls you've " "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 " "participated in with more than 25 messages sent (to prevent spam).\n"
"resetted every month.\n"
"\n"
"__**Top Users:**__\n"
"\n" "\n"
) )
+ "\n\n".join(user_rows), + table,
author=self.bot.embed_author, author=self.bot.embed_author,
footer=discord.EmbedFooter( footer=discord.EmbedFooter(
text="The leaderboard is updated every 15 minutes." 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 from tortoise import models, fields
import discord
from discord import webhook from discord import webhook
import aiohttp import aiohttp
@@ -72,13 +73,26 @@ class Profile(models.Model):
return f"[{self.name}]({self.jump_url})" 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: class Meta:
unique_together = [("user_id", "server_id")] unique_together = [("user_id", "server_id")]
class Server(models.Model): class Server(models.Model):
id: int = fields.BigIntField(primary_key=True) 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.") 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"] channels: fields.BackwardFKRelation["Channel"]
profiles: fields.BackwardFKRelation["Profile"] profiles: fields.BackwardFKRelation["Profile"]