4 Commits

6 changed files with 210 additions and 13 deletions
+3
View File
@@ -19,3 +19,6 @@
[submodule "src/modules/pluscampaign"] [submodule "src/modules/pluscampaign"]
path = src/modules/pluscampaign path = src/modules/pluscampaign
url = https://git.thewisewolf.dev/CarmiCoven/pluscampaign-plugin.git 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
+1
View File
@@ -7,6 +7,7 @@ active = [
".messagelogger", ".messagelogger",
".voicelog", ".voicelog",
".yarn", ".yarn",
".tracker",
".pluscampaign", ".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 from collections import defaultdict
import datetime as dt import datetime as dt
from datetime import datetime, timedelta, UTC from datetime import datetime, timedelta, UTC
@@ -6,7 +7,7 @@ from datetime import datetime, timedelta, UTC
from data.queries import ORDER from data.queries import ORDER
import discord import discord
from discord.ext import commands as cmds 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 import LionBot, LionCog, LionContext
from meta.logger import log_wrap from meta.logger import log_wrap
@@ -132,3 +133,194 @@ class YarnCog(LionCog):
pages = paginate_list(lb_strings, block_length=page_len, title=title) pages = paginate_list(lb_strings, block_length=page_len, title=title)
await pager(ctx, pages) 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}")
)
+10 -10
View File
@@ -138,7 +138,7 @@ class LeoUI(View):
to include a pre_timeout task to include a pre_timeout task
which may optionally refresh and hence cancel the timeout. 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 # We are already stopped, nothing to do
return return
@@ -160,17 +160,17 @@ class LeoUI(View):
# The timeout was removed entirely, silently walk away # The timeout was removed entirely, silently walk away
return return
if self._View__stopped.done(): if self._BaseView__stopped.done():
# We stopped while waiting for the pre timeout. # We stopped while waiting for the pre timeout.
# Or maybe another thread timed us out # Or maybe another thread timed us out
# Either way, we are done here # Either way, we are done here
return return
now = time.monotonic() 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 # 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(): if self._BaseView__timeout_task is None or self._BaseView__timeout_task.done():
self._View__timeout_task = asyncio.create_task(self._View__timeout_task_impl()) self._BaseView__timeout_task = asyncio.create_task(self._BaseView__timeout_task_impl())
else: else:
# Actually timeout, and call the post-timeout task for cleanup. # Actually timeout, and call the post-timeout task for cleanup.
self._really_timeout() self._really_timeout()
@@ -189,14 +189,14 @@ class LeoUI(View):
This copies View._dispatch_timeout, apart from the `on_timeout` dispatch, This copies View._dispatch_timeout, apart from the `on_timeout` dispatch,
which is now handled by `__dispatch_timeout`. which is now handled by `__dispatch_timeout`.
""" """
if self._View__stopped.done(): if self._BaseView__stopped.done():
return return
if self._View__cancel_callback: if self._BaseView__cancel_callback:
self._View__cancel_callback(self) self._BaseView__cancel_callback(self)
self._View__cancel_callback = None self._BaseView__cancel_callback = None
self._View__stopped.set_result(True) self._BaseView__stopped.set_result(True)
def _dispatch_item(self, *args, **kwargs): def _dispatch_item(self, *args, **kwargs):
"""Extending event dispatch to run in the instantiation context.""" """Extending event dispatch to run in the instantiation context."""