Compare commits
12 Commits
8ccd92633c
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d307726a58 | |||
| 25c64bb338 | |||
| d4e3c49ef2 | |||
| d17da09572 | |||
| 48a5d83e8b | |||
| 0395a9b4d3 | |||
| 47c50c29b0 | |||
| ff78f7d5bf | |||
| 384820370a | |||
| 52cbd68778 | |||
| 447672d972 | |||
| a098106468 |
+7
-1
@@ -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,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 {{{
|
||||
|
||||
@@ -6,7 +6,9 @@ active = [
|
||||
".voicefix",
|
||||
".messagelogger",
|
||||
".voicelog",
|
||||
".yarn"
|
||||
".yarn",
|
||||
".tracker",
|
||||
".pluscampaign",
|
||||
]
|
||||
|
||||
|
||||
|
||||
Submodule
+1
Submodule src/modules/pluscampaign added at 97bdf8e8e3
+1
-1
Submodule src/modules/profiles updated: c9b6ce8f60...8818263d88
Submodule
+1
Submodule src/modules/tracker added at 2f4778d0b8
+194
-2
@@ -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
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user