mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-27 17:38:11 +03:00
175 lines
4.7 KiB
TypeScript
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,
|
|
});
|
|
};
|
|
}
|