Files
zkldi_Tachi/server/src/lib/activity/activity.ts
T
2022-11-11 18:46:00 +00:00

175 lines
4.7 KiB
TypeScript

import db from "external/mongo/db";
import { GetRelevantSongsAndCharts } from "utils/db";
import { DedupeArr } from "utils/misc";
import { GetGPT } from "utils/req-tachi-data";
import { GetUsersWithIDs } from "utils/user";
import type { Request, Response } from "express-serve-static-core";
import type { FilterQuery } from "mongodb";
import type { ClassAchievementDocument, Game, ScoreDocument, SessionDocument } from "tachi-common";
export type ActivityConstraint = FilterQuery<
ClassAchievementDocument & ScoreDocument & SessionDocument
>;
/**
* Retrieves recent activity for this group of users for this GPT.
*
* At the moment, this retrieves the following events:
* - Recent Sessions
* - Recent Highlighted Scores
* - Achieved Classes
*
* It will support:
* - Recently achieved goals
* - Recently achieved quests
*
* To get the set of things we want to fetch, we fetch the first N sessions we see.
* This sets our "upper bound" for how far back we want to look -- when the Nth session
* started.
*
* Optionally, startFrom can be passed, which will start this activity search from that
* point in time.
*
* @bug - With the way `startFrom` works, its possible to "skip over" sessions that have
* the **exact** same timestamp, but didn't fall into the previous limit.
*
* for an array of imagined timestamps with sessions=3, followed by startFrom=3
* i.e. [1, 2, 3] 3, 3, 3 [4, 5, 6]
*/
export async function GetRecentActivity(
// todo: is it possible to make this game agnostic?
game: Game,
query: ActivityConstraint,
sessions = 30,
startFrom: number | null = null
) {
const baseQuery = query;
const initialSessionQuery = {
...baseQuery,
// start from anytime if startFrom is omitted, otherwise, cap at the start.
timeStarted: { $lt: startFrom === null ? Infinity : startFrom },
};
const recentSessions = await db.sessions.find(initialSessionQuery, {
sort: {
timeStarted: -1,
},
limit: sessions,
});
// find the earliest point in the sessions we just fetched.
// if we found no sessions, set this to now, which means we'll fetch no highlighted
// scores.
// (it's not possible to have no sessions *and* have scores with timestamps)
const earliestSession = recentSessions.at(-1)?.timeStarted ?? Date.now();
const timeConstraint =
startFrom !== null
? { $lt: startFrom, $gte: earliestSession }
: {
$gte: earliestSession,
};
const achievedClasses = await db["class-achievements"].find(
{
...baseQuery,
timeAchieved: timeConstraint,
},
{
sort: {
timeAchieved: -1,
},
}
);
const recentlyHighlightedScores = await db.scores.find(
{
...baseQuery,
highlight: true,
timeAchieved: timeConstraint,
},
{
sort: {
timeAchieved: -1,
},
}
);
const { songs, charts } = await GetRelevantSongsAndCharts(recentlyHighlightedScores, game);
const userIDs = DedupeArr([
...recentSessions.map((e) => e.userID),
...recentlyHighlightedScores.map((e) => e.userID),
...achievedClasses.map((e) => e.userID),
]);
const users = await GetUsersWithIDs(userIDs);
return {
recentSessions,
recentlyHighlightedScores,
songs,
charts,
achievedClasses,
users,
};
}
/**
* Utility for creating an express handler for activity-related endpoints. These endpoints
* are all *remarkably* similar, but with slightly different initial constraints.
*
* This creates a function that you should call inside another route.
*/
export function CreateActivityRouteHandler(query: ActivityConstraint) {
return async (req: Request, res: Response) => {
const { game } = GetGPT(req);
const qSessions = req.query.sessions;
const qStartTime = req.query.startTime;
if (qSessions !== undefined && typeof qSessions !== "string") {
return res.status(400).json({
success: false,
description: `Invalid 'sessions'.`,
});
}
if (qStartTime !== undefined && typeof qStartTime !== "string") {
return res.status(400).json({
success: false,
description: `Invalid 'startTime'.`,
});
}
// defaulting to 30 seems sensible.
const sessions = qSessions ? Number(qSessions) : 30;
if (sessions > 100 || sessions < 10 || Number.isNaN(sessions)) {
return res.status(400).json({
success: false,
description: `Invalid sessions, got ${sessions}, which wasn't between 10 and 100.`,
});
}
const startTime = qStartTime ? Number(qStartTime) : null;
if (Number.isNaN(startTime)) {
return res.status(400).json({
success: false,
description: `Invalid startTime, got a non number.`,
});
}
const recentActivity = await GetRecentActivity(game, query, sessions, startTime);
return res.status(200).json({
success: true,
description: `Retrieved activity.`,
body: recentActivity,
});
};
}