13 Commits

8 changed files with 240 additions and 20 deletions
+7 -1
View File
@@ -15,4 +15,10 @@
url = https://git.thewisewolf.dev/HoloTech/psqlmapper.git
[submodule "src/modules/profiles"]
path = src/modules/profiles
url = git@thewisewolf.dev:HoloTech/profiles-plugin.git
url = https://git.thewisewolf.dev/HoloTech/profiles-plugin.git
[submodule "src/modules/pluscampaign"]
path = src/modules/pluscampaign
url = https://git.thewisewolf.dev/CarmiCoven/pluscampaign-plugin.git
[submodule "src/modules/tracker"]
path = src/modules/tracker
url = https://git.thewisewolf.dev/HoloTech/twitch-eventtracker-plugin.git
+18
View File
@@ -18,6 +18,24 @@ BEGIN
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE OR REPLACE FUNCTION current_module_version(module_name TEXT)
RETURNS INTEGER
AS $$
SELECT
to_version
FROM version_history
WHERE
component = $1
ORDER BY _timestamp DESC
LIMIT 1;
$$ LANGUAGE SQL;
CREATE TABLE app_config(
appname TEXT PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- }}}
-- App metadata {{{
+3 -1
View File
@@ -6,7 +6,9 @@ active = [
".voicefix",
".messagelogger",
".voicelog",
".yarn"
".yarn",
".tracker",
".pluscampaign",
]
Submodule src/modules/tracker added at 2f4778d0b8
+194 -2
View File
@@ -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}")
)
+11 -11
View File
@@ -56,7 +56,7 @@ class LeoUI(View):
Currently exposes a hidden attribute of the underlying View.
May be reimplemented in future.
"""
return self._View__stopped
return self._BaseView__stopped
def to_components(self) -> List[Dict[str, Any]]:
"""
@@ -138,7 +138,7 @@ class LeoUI(View):
to include a pre_timeout task
which may optionally refresh and hence cancel the timeout.
"""
if self._View__stopped.done():
if self._BaseView__stopped.done():
# We are already stopped, nothing to do
return
@@ -160,17 +160,17 @@ class LeoUI(View):
# The timeout was removed entirely, silently walk away
return
if self._View__stopped.done():
if self._BaseView__stopped.done():
# We stopped while waiting for the pre timeout.
# Or maybe another thread timed us out
# Either way, we are done here
return
now = time.monotonic()
if self._View__timeout_expiry is not None and now < self._View__timeout_expiry:
if self._BaseView__timeout_expiry is not None and now < self._BaseView__timeout_expiry:
# The timeout was extended, make sure the timeout task is running then fade away
if self._View__timeout_task is None or self._View__timeout_task.done():
self._View__timeout_task = asyncio.create_task(self._View__timeout_task_impl())
if self._BaseView__timeout_task is None or self._BaseView__timeout_task.done():
self._BaseView__timeout_task = asyncio.create_task(self._BaseView__timeout_task_impl())
else:
# Actually timeout, and call the post-timeout task for cleanup.
self._really_timeout()
@@ -189,14 +189,14 @@ class LeoUI(View):
This copies View._dispatch_timeout, apart from the `on_timeout` dispatch,
which is now handled by `__dispatch_timeout`.
"""
if self._View__stopped.done():
if self._BaseView__stopped.done():
return
if self._View__cancel_callback:
self._View__cancel_callback(self)
self._View__cancel_callback = None
if self._BaseView__cancel_callback:
self._BaseView__cancel_callback(self)
self._BaseView__cancel_callback = None
self._View__stopped.set_result(True)
self._BaseView__stopped.set_result(True)
def _dispatch_item(self, *args, **kwargs):
"""Extending event dispatch to run in the instantiation context."""