rewrite: Profile, Stats, Leaderboard.

This commit is contained in:
2023-05-14 12:31:43 +03:00
parent 16bc05d39b
commit 7f79009ac7
13 changed files with 1447 additions and 51 deletions

View File

@@ -5,6 +5,7 @@ from data import NULL
from meta import LionBot
from gui.cards import WeeklyGoalCard, MonthlyGoalCard
from gui.base import CardMode
from tracking.text.data import TextTrackerData
from ..data import StatsData
from ..lib import extract_weekid, extract_monthid, apply_week_offset, apply_month_offset
@@ -41,7 +42,15 @@ async def get_goals_card(
key = {'guildid': guildid or 0, 'userid': userid, 'monthid': periodid}
# Extract goals and tasks
goals = await goal_model.fetch(*key.values())
# TODO: Major data model fixy fixy here
if guildid:
goals = await goal_model.fetch_or_create(*key.values())
else:
goals = await goal_model.fetch(*key.values())
if not goals:
from collections import defaultdict
goals = defaultdict(lambda: -1)
task_rows = await tasks_model.fetch_where(**key)
tasks = [(i, row.content, bool(row.completed)) for i, row in enumerate(task_rows)]
@@ -58,11 +67,17 @@ async def get_goals_card(
tasks_completed = results[0]['total'] if results else 0
# Set and compute correct middle goal column
# if mode in (CardMode.VOICE, CardMode.STUDY):
if True:
if mode in (CardMode.VOICE, CardMode.STUDY):
model = data.VoiceSessionStats
middle_completed = (await model.study_times_between(guildid or None, userid, start, end))[0]
middle_goal = goals['study_goal']
elif mode is CardMode.TEXT:
model = TextTrackerData.TextSessions
middle_goal = goals['message_goal']
if guildid:
middle_completed = (await model.member_messages_between(guildid, userid, start, end))[0]
else:
middle_completed = (await model.user_messages_between(userid, start, end))[0]
# Compute schedule session progress
# TODO

View File

@@ -0,0 +1,74 @@
from meta import LionBot
from gui.cards import LeaderboardCard
from gui.base import CardMode
async def get_leaderboard_card(
bot: LionBot, highlightid: int, guildid: int,
mode: CardMode,
entry_data: list[tuple[int, int, int]], # userid, position, time
):
"""
Render a leaderboard card with given parameters.
"""
guild = bot.get_guild(guildid)
if guild is None:
raise ValueError("Attempting to build leaderboard for non-existent guild!")
# Need to do two passes here in case we need to do a db request for the avatars or names
avatars = {}
names = {}
missing = []
for userid, _, _ in entry_data:
hash = name = None
if guild and (member := guild.get_member(userid)):
hash = member.avatar.key
name = member.display_name
elif (user := bot.get_user(userid)):
hash = user.avatar.key
name = user.name
elif (user_data := bot.core.data.User._cache_.get((userid,))):
hash = user_data.avatar_hash
name = user_data.name
if hash:
avatars[userid] = hash
names[userid] = name or 'Unknown'
else:
missing.append(userid)
if missing:
# We were unable to retrieve information for some userids
# Bulk-fetch missing users from data
data = await bot.core.data.User.fetch_where(userid=missing)
for user_data in data:
avatars[user_data.userid] = user_data.avatar_hash
names[user_data.userid] = user_data.name or 'Unknown'
missing.remove(user_data.userid)
if missing:
# Some of the users were missing from data
# This should be impossible (by FKEY constraints on sessions)
# But just in case...
for userid in missing:
avatars[userid] = None
names[userid] = str(userid)
highlight = None
entries = []
for userid, position, duration in entry_data:
entries.append(
(userid, position, duration, names[userid], (userid, avatars[userid]))
)
if userid == highlightid:
highlight = position
# Request Card
card = LeaderboardCard(
skin={'mode': mode},
server_name=guild.name,
entries=entries,
highlight=highlight
)
return card

View File

@@ -6,6 +6,7 @@ from data import ORDER
from meta import LionBot
from gui.cards import MonthlyStatsCard
from gui.base import CardMode
from tracking.text.data import TextTrackerData
from ..data import StatsData
from ..lib import apply_month_offset
@@ -34,8 +35,23 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
[0]*(calendar.monthrange(month.year, month.month)[1]) for month in months
]
# TODO: Select model based on card mode
model = data.VoiceSessionStats
if mode is CardMode.VOICE:
model = data.VoiceSessionStats
req = model.study_times_between
reqkey = (guildid or None, userid,)
elif mode is CardMode.TEXT:
model = TextTrackerData.TextSessions
if guildid:
req = model.member_messages_between
reqkey = (guildid, userid,)
else:
req = model.user_messages_between
reqkey = (userid,)
else:
# TODO: ANKI
model = data.VoiceSessionStats
req = model.study_times_between
reqkey = (guildid or None, userid,)
# Get first session
query = model.table.select_where().order_by('start_time', ORDER.ASC).limit(1)
@@ -50,10 +66,10 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
longest_streak = 0
else:
first_day = first_session.replace(hour=0, minute=0, second=0, microsecond=0)
first_month = first_day.replace(day=1)
# first_month = first_day.replace(day=1)
# Build list of day starts up to now, or end of requested month
requests = []
requests = [first_day]
end_of_req = target_end if offset else today
day = first_day
while day <= end_of_req:
@@ -61,7 +77,7 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
requests.append(day)
# Request times between requested days
day_stats = await model.study_times_between(guildid or None, userid, *requests)
day_stats = await req(*reqkey, *requests)
# Compute current streak and longest streak
current_streak = 0
@@ -79,7 +95,10 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
if day < months[0]:
break
i = offsets[(day.year, day.month)]
monthly[i][day.day - 1] = stat / 3600
if mode in (CardMode.VOICE, CardMode.STUDY):
monthly[i][day.day - 1] = stat / 3600
else:
monthly[i][day.day - 1] = stat
# Get member profile
if user:

View File

@@ -0,0 +1,91 @@
from typing import Optional, TYPE_CHECKING
from datetime import datetime, timedelta
import discord
from meta import LionBot
from gui.cards import ProfileCard
from modules.ranks.cog import RankCog
from modules.ranks.utils import format_stat_range
if TYPE_CHECKING:
from ..cog import StatsCog
async def get_profile_card(bot: LionBot, userid: int, guildid: int):
ranks: Optional[RankCog] = bot.get_cog('RankCog')
stats: Optional[StatsCog] = bot.get_cog('StatsCog')
if ranks is None or stats is None:
return
guild = bot.get_guild(guildid)
if guild is None:
return
lion = await bot.core.lions.fetch_member(guildid, userid)
luser = lion.luser
member = await lion.fetch_member()
if member:
username = (member.display_name, '#' + str(member.discriminator))
avatar = member.avatar.key
else:
username = (lion.data.display_name, "#????")
avatar = luser.data.avatar_hash
profile_badges = await stats.data.ProfileTag.fetch_tags(guildid, userid)
# Fetch current and next guild rank
season_rank = await ranks.get_member_rank(guildid, userid)
rank_type = lion.lguild.config.get('rank_type').value
crank = season_rank.current_rank
nrank = season_rank.next_rank
if crank:
roleid = crank.roleid
role = guild.get_role(roleid)
name = role.name if role else str(role.id)
minimum = crank.required
maximum = nrank.required if nrank else None
rangestr = format_stat_range(rank_type, minimum, maximum)
if maximum is None:
rangestr = f"{rangestr}"
current_rank = (name, rangestr)
if maximum:
rank_progress = (season_rank.stat - minimum) / (maximum - minimum)
else:
rank_progress = 1
else:
current_rank = None
rank_progress = 0
if nrank:
roleid = nrank.roleid
role = guild.get_role(roleid)
name = role.name if role else str(role.id)
minimum = nrank.required
guild_ranks = await ranks.get_guild_ranks(guildid)
nnrank = next((rank for rank in guild_ranks if rank.required > nrank.required), None)
maximum = nnrank.required if nnrank else None
rangestr = format_stat_range(rank_type, minimum, maximum)
if maximum is None:
rangestr = f"{rangestr}"
next_rank = (name, rangestr)
else:
next_rank = None
achievements = (0, 1)
card = ProfileCard(
user=username,
avatar=(userid, avatar),
coins=lion.data.coins, gems=luser.data.gems, gifts=0,
profile_badges=profile_badges,
achievements=achievements,
current_rank=current_rank,
rank_progress=rank_progress,
next_rank=next_rank
)
return card

View File

@@ -5,15 +5,16 @@ import discord
from meta import LionBot
from gui.cards import StatsCard
from gui.base import CardMode
from ..data import StatsData
async def get_stats_card(bot: LionBot, userid: int, guildid: int):
async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode):
data: StatsData = bot.get_cog('StatsCog').data
# TODO: Workouts
# TODO: Leaderboard rankings
# TODO: Leaderboard rankings for this season or all time
guildid = guildid or 0
lion = await bot.core.lions.fetch_member(guildid, userid)
@@ -31,20 +32,40 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int):
)
# Extract the study times for each period
study_times = await data.VoiceSessionStats.study_times_since(guildid, userid, *period_timestamps)
print("Study times", study_times)
if mode in (CardMode.STUDY, CardMode.VOICE):
model = data.VoiceSessionStats
refkey = (guildid, userid)
ref_since = model.study_times_since
ref_between = model.study_times_between
elif mode is CardMode.TEXT:
if guildid:
model = data.MemberExp
refkey = (guildid, userid)
else:
model = data.UserExp
refkey = (userid,)
ref_since = model.xp_since
ref_between = model.xp_between
else:
# TODO ANKI
model = data.VoiceSessionStats
refkey = (guildid, userid)
ref_since = model.study_times_since
ref_between = model.study_times_between
study_times = await ref_since(*refkey, *period_timestamps)
print("Period study times: ", study_times)
# Calculate streak data by requesting times per day
# First calculate starting timestamps for each day
days = list(range(0, today.day + 2))
day_timestamps = [month_start + timedelta(days=day - 1) for day in days]
study_times = await data.VoiceSessionStats.study_times_between(guildid, userid, *day_timestamps)
print("Study times", study_times)
study_times_month = await ref_between(*refkey, *day_timestamps)
# Then extract streak tuples
streaks = []
streak_start = None
for day, stime in zip(days, study_times):
for day, stime in zip(days, study_times_month):
stime = stime or 0
if stime > 0 and streak_start is None:
streak_start = day
@@ -59,5 +80,6 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int):
list(reversed(study_times)),
100,
streaks,
skin={'mode': mode}
)
return card

View File

@@ -5,6 +5,7 @@ from data import ORDER
from meta import LionBot
from gui.cards import WeeklyStatsCard
from gui.base import CardMode
from tracking.text.data import TextTrackerData
from ..data import StatsData
@@ -20,13 +21,27 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
user = await bot.fetch_user(userid)
today = lion.today
week_start = today - timedelta(days=today.weekday()) - timedelta(weeks=offset)
days = [week_start + timedelta(i) for i in range(-7, 7 if offset else (today.weekday() + 1))]
days = [week_start + timedelta(i) for i in range(-7, 8 if offset else (today.weekday() + 2))]
# TODO: Select statistics model based on mode
model = data.VoiceSessionStats
if mode is CardMode.VOICE:
model = data.VoiceSessionStats
day_stats = await model.study_times_between(guildid or None, userid, *days)
day_stats = list(map(lambda n: n // 3600, day_stats))
elif mode is CardMode.TEXT:
model = TextTrackerData.TextSessions
if guildid:
day_stats = await model.member_messages_between(guildid, userid, *days)
else:
day_stats = await model.user_messages_between(userid, *days)
else:
# TODO: ANKI
model = data.VoiceSessionStats
day_stats = await model.study_times_between(guildid or None, userid, *days)
day_stats = list(map(lambda n: n // 3600, day_stats))
# Get user session rows
query = model.table.select_where()
query = model.table.select_where(model.start_time >= days[0])
if guildid:
query = query.where(userid=userid, guildid=guildid).order_by('start_time', ORDER.ASC)
else:
@@ -34,7 +49,6 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
sessions = await query
# Extract quantities per-day
day_stats = await model.study_times_between(guildid or None, userid, *days)
for i in range(14 - len(day_stats)):
day_stats.append(0)
@@ -49,9 +63,9 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
timezone=str(lion.timezone),
now=lion.now.timestamp(),
week=week_start.timestamp(),
daily=tuple(map(lambda n: n/3600, day_stats)),
daily=tuple(map(int, day_stats)),
sessions=[
(int(session['start_time'].timestamp()), int(session['end_time'].timestamp()))
(int(session['start_time'].timestamp()), int(session['start_time'].timestamp() + int(session['duration'])))
for session in sessions
],
skin={'mode': mode}