From 25c64bb3381a55111c6e5fca6f8b1986d86ed0a9 Mon Sep 17 00:00:00 2001 From: Interitio Date: Tue, 4 Aug 2026 11:53:07 +0300 Subject: [PATCH] feat (yarn): Add topvoice, upload, and topmoomin --- src/modules/yarn/cog.py | 196 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/src/modules/yarn/cog.py b/src/modules/yarn/cog.py index ea34b28..2fc3b4c 100644 --- a/src/modules/yarn/cog.py +++ b/src/modules/yarn/cog.py @@ -1,4 +1,5 @@ -from typing import Literal +from pathlib import Path +from typing import Literal, Optional from collections import defaultdict import datetime as dt from datetime import datetime, timedelta, UTC @@ -6,7 +7,7 @@ from datetime import datetime, timedelta, UTC from data.queries import ORDER import discord from discord.ext import commands as cmds -from discord import app_commands as appcmds +from discord import Attachment, app_commands as appcmds from meta import LionBot, LionCog, LionContext from meta.logger import log_wrap @@ -132,3 +133,194 @@ class YarnCog(LionCog): pages = paginate_list(lb_strings, block_length=page_len, title=title) await pager(ctx, pages) + + @cmds.hybrid_command(name="topvoice") + async def topvoice_cmd(self, ctx): + """ + Show top voice members by total time. + """ + target_channelid = 1383707078740279366 + since_stamp = 1769832959 + + voicelogger = ctx.bot.get_cog("VoiceLogCog") + session_data = voicelogger.data.voicelog_sessions + + query = ( + session_data.select_where( + VoiceLogSession.joined_at + >= datetime.fromtimestamp(since_stamp, tz=UTC), + guildid=ctx.guild.id, + channelid=target_channelid, + ) + .select( + userid="userid", + total_time="SUM(COALESCE(duration, EXTRACT(EPOCH FROM (NOW() - joined_at))))", + ) + .order_by("total_time", ORDER.DESC) + .group_by("userid") + .with_no_adapter() + ) + leaderboard = [(row["userid"], int(row["total_time"])) for row in await query] + + # Format for display and pager + # First collect names + names = {} + for uid, _ in leaderboard: + user = ctx.guild.get_member(uid) + if user is None: + try: + user = await ctx.guild.fetch_member(uid) + except discord.NotFound: + user = None + names[uid] = user.display_name if user else str(uid) + + lb_strings = [] + max_name_len = min((30, max(len(name) for name in names.values()))) + for i, (uid, total) in enumerate(leaderboard): + lb_strings.append( + "{:<{}}\t{:<9}".format( + names[uid], max_name_len, strfdur(total, short=False) + ) + ) + + page_len = 20 + title = "Voice Leaderboard" + pages = paginate_list(lb_strings, block_length=page_len, title=title) + + await pager(ctx, pages) + + @cmds.hybrid_command(name="topmoomin") + async def topmoomin_cmd(self, ctx): + target_channelid = 1383707078740279366 + since_stamp = 1777960800 + + # Build timestamps of all moomin times since the start + # Reverse time order + moomin_periods = [] + last_moomin_start = 1785823200 + moomin_dur = 60 * 30 + + mstart = last_moomin_start + while mstart >= since_stamp: + period = (mstart, mstart + moomin_dur) + moomin_periods.append(period) + mstart -= 24 * 60 * 60 + + # Get all voice sessions + + voicelogger = ctx.bot.get_cog("VoiceLogCog") + session_data = voicelogger.data.voicelog_sessions + + query = ( + session_data.select_where( + VoiceLogSession.joined_at + >= datetime.fromtimestamp(since_stamp, tz=UTC), + guildid=ctx.guild.id, + channelid=target_channelid, + ) + .select("userid", "joined_at", "duration") + .order_by("joined_at", ORDER.DESC) + .with_no_adapter() + ) + rows = await query + + # For each session, intersect it with all the moomin times, sum the intersection seconds. Key by userid + moomin_leaderboard = defaultdict(int) + for row in rows: + start_stamp = row["joined_at"].timestamp() + if row["duration"]: + end_stamp = start_stamp + row["duration"] + else: + end_stamp = utc_now().timestamp() + + # Intersection + for mstart, mend in moomin_periods: + if mend < start_stamp: + # Moomin periods go backwards + # Stop looking if the period ended before this voice session started + break + # Guarantee: mend >= start_stamp + if mstart <= end_stamp: + # Period starts before the end of the voice session + # and ends after the start of the session + intersection_start = max(start_stamp, mstart) + intersection_end = min(end_stamp, mend) + diff = intersection_end - intersection_start + moomin_leaderboard[row["userid"]] += int(diff) + else: + # Period starts after the end of the voice session, ignore + pass + + # Format for display and pager + leaderboard = sorted( + moomin_leaderboard.items(), key=lambda p: p[1], reverse=True + ) + + # First collect names + names = {} + users = {} + for uid, _ in leaderboard: + user = ctx.guild.get_member(uid) + if user is None: + try: + user = await ctx.guild.fetch_member(uid) + except discord.NotFound: + user = None + names[uid] = user.display_name if user else str(uid) + users[uid] = user + + lb_strings = [] + max_name_len = min((30, max(len(name) for name in names.values()))) + for i, (uid, total) in enumerate(leaderboard): + lb_strings.append( + "{:<{}}\t{:<9}".format( + names[uid], max_name_len, strfdur(total, short=False) + ) + ) + + page_len = 20 + title = "Moomin Leaderboard" + pages = paginate_list(lb_strings, block_length=page_len, title=title) + await pager(ctx, pages) + + if ctx.author.guild_permissions.administrator: + threshold = 20 * 60 + + role = ctx.guild.get_role(1534089417612857424) + if not role: + await ctx.reply("Couldn't find the role!") + return + earned_users = [] + for uid, dur in leaderboard: + if dur >= threshold: + if user := users.get(uid): + earned_users.append(user) + if role not in user.roles: + await user.add_roles(role, reason=f"Watched moomin for {dur}") + await ctx.reply( + f"Added moomin role to: {', '.join(u.mention for u in earned_users)} ", + allowed_mentions=None, + ) + + async def _upload_file(self, file: discord.Attachment, filename: str): + filedir = Path("/extension/www/lilac") + target = filedir / filename + + await file.save(target) + return f"https://lilac.thewisewolf.dev/provides/{filename}" + + @cmds.hybrid_group(name="upload") + @appcmds.default_permissions(administrator=True) + async def upload_group(self, ctx): + pass + + @upload_group.command(name="file") + @appcmds.default_permissions(administrator=True) + async def upload_file_group( + self, ctx, file: discord.Attachment, filename: Optional[str] = None + ): + await ctx.interaction.response.defer(thinking=True, ephemeral=True) + url = await self._upload_file(file, filename or file.filename) + await ctx.reply( + embed=discord.Embed(description=f"File now available at: {url}") + )