Ruby-Cogs/levelup/dashboard/integration.py
Valerie 477974d53c
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run
Upload 2 Cogs & Update README
2025-05-23 01:30:53 -04:00

205 lines
7.5 KiB
Python

import asyncio
import logging
import typing as t
from pathlib import Path
import discord
from redbot.core import commands
from redbot.core.i18n import Translator
from ..abc import MixinMeta
from ..common import formatter
_ = Translator("LevelUp", __file__)
log = logging.getLogger("red.levelup.dashboard")
root = Path(__file__).parent
static = root / "static"
templates = root / "templates"
def dashboard_page(*args: t.Any, **kwargs: t.Any) -> t.Callable[[t.Any], t.Any]:
def decorator(func: t.Callable) -> t.Callable[[t.Any], t.Any]:
func.__dashboard_decorator_params__ = (args, kwargs)
return func
return decorator
class DashboardIntegration(MixinMeta):
@commands.Cog.listener()
async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None:
log.info("Dashboard cog added, registering third party.")
dashboard_cog.rpc.third_parties_handler.add_third_party(self)
logging.getLogger("werkzeug").setLevel(logging.WARNING)
async def get_dashboard_leaderboard(
self,
user: discord.User,
guild: discord.Guild,
lbtype: t.Literal["lb", "weekly"],
stat: t.Literal["exp", "messages", "voice", "stars"] = "exp",
**kwargs,
):
"""
kwargs = {
"user_id": int,
"guild_id": int,
"method": str, # GET or POST
"request_url": str,
"cxrf_token": str,
"wtf_csrf_secret_key": bytes,
"extra_kwargs": MultiDict,
"data": {
"form": ImmutableMultiDict,
"json": ImmutableMultiDict,
}
"lang_code": str,
"Form": FlaskForm,
"DpyObjectConverter": DpyObjectConverter,
"get_sorted_channels": Callable,
"get_sorted_roles": Callable,
"Pagination": Pagination,
}
"""
conf = self.db.get_conf(guild)
if lbtype == "weekly":
if not conf.weeklysettings.on:
return {
"status": 1,
"error_title": _("Weekly stats disabled"),
"error_message": _("Weekly stats are disabled for this guild."),
}
if not conf.users_weekly:
return {
"status": 1,
"error_title": _("No Weekly stats available"),
"error_message": _("There is no data for the weekly leaderboard yet, please chat a bit first."),
}
source_path = templates / "leaderboard.html"
js_path = static / "js" / "leaderboard.js"
css_path = static / "css" / "leaderboard.css"
# Inject JS and CSS into the HTML source for full page loads
source = (
f"<style>\n{css_path.read_text()}\n</style>\n\n"
+ source_path.read_text().strip()
+ f"\n\n<script>\n{js_path.read_text()}\n</script>"
)
"""
{
"title": str,
"description": str,
"stat": str,
"stats": [{"position": int, "name": str, "id": int, "stat": str}]
"total": str,
"type": leaderboard type, // lb or weekly
"user_position": int, // Index of the user in the leaderboard
}
"""
res: dict = await asyncio.to_thread(
formatter.get_leaderboard,
bot=self.bot,
guild=guild,
db=self.db,
stat=stat,
lbtype=lbtype,
is_global=False,
member=guild.get_member(user.id),
use_displayname=True,
dashboard=True,
)
data = {
"user_id": user.id,
"users": res["stats"],
"stat": stat,
"total": res["description"].replace("`", ""),
"type": lbtype,
"page": int(kwargs["extra_kwargs"].get("page", 1)),
}
content = {
"status": 0,
"web_content": {
"source": source,
"data": data,
"stat": stat,
"statname": res["stat"],
"expanded": True,
},
}
return content
@dashboard_page(name="leaderboard", description="Display the guild leaderboard.")
async def leaderboard_page(
self, user: discord.User, guild: discord.Guild, stat: str = None, **kwargs
) -> t.Dict[str, t.Any]:
stat = stat if stat is not None and stat in {"exp", "messages", "voice", "stars"} else "exp"
return await self.get_dashboard_leaderboard(user, guild, "lb", stat, **kwargs)
@dashboard_page(name="weekly", description="Display the guild weekly leaderboard.")
async def weekly_page(
self, user: discord.User, guild: discord.Guild, stat: str = None, **kwargs
) -> t.Dict[str, t.Any]:
stat = stat if stat is not None and stat in {"exp", "messages", "voice", "stars"} else "exp"
return await self.get_dashboard_leaderboard(user, guild, "weekly", stat, **kwargs)
# @dashboard_page(name="settings", description="Configure the leveling system.", methods=("GET", "POST"))
async def cog_settings(self, user: discord.User, guild: discord.Guild, **kwargs):
import wtforms # pip install WTForms
from flask_wtf import FlaskForm # pip install Flask-WTF
log.info(f"Getting settings for {guild.name} by {user.name}")
member = guild.get_member(user.id)
if not member:
log.warning(f"Member {user.name} not found in guild {guild.name}")
return {
"status": 1,
"error_title": _("Member not found"),
"error_message": _("You are not a member of this guild."),
}
if not await self.bot.is_admin(member):
log.warning(f"Member {user.name} is not an admin in guild {guild.name}")
return {
"status": 1,
"error_title": _("Insufficient permissions"),
"error_message": _("You need to be an admin to access this page."),
}
conf = self.db.get_conf(guild)
class SettingsForm(kwargs["Form"]):
def __init__(self):
super().__init__(prefix="levelup_settings_form_")
# General settings
enabled = wtforms.BooleanField(_("Enabled:"), default=conf.enabled)
algo_base = wtforms.IntegerField(
_("Algorithm Base:"), default=conf.algorithm.base, validators=[wtforms.validators.InputRequired()]
)
algo_multiplier = wtforms.FloatField(
_("Algorithm Multiplier:"), default=conf.algorithm.exp, validators=[wtforms.validators.InputRequired()]
)
# Submit button
submit = wtforms.SubmitField(_("Save Settings"))
form: FlaskForm = SettingsForm()
# Handle form submission
if form.validate_on_submit() and await form.validate_dpy_converters():
log.info(f"Form validated for {guild.name} by {user.name}")
conf.enabled = form.enabled.data
conf.algorithm.base = form.algo_base.data or 100
conf.algorithm.exp = form.algo_multiplier.data or 2.0
self.save()
return {
"status": 0,
"notifications": [{"message": _("Settings saved"), "category": "success"}],
"redirect_url": kwargs["request_url"],
}
source = (templates / "settings.html").read_text()
return {
"status": 0,
"web_content": {"source": source, "settings_form": form},
}