From 126edd89e4ecf4c2efd44309a7de50733033d7e0 Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 26 Jun 2021 13:32:09 +0100 Subject: [PATCH] Seems good? --- server/src/lib/search/search.ts | 26 ++++--- .../server/router/api/v1/users/router.test.ts | 77 +++++++++++++++++++ .../src/server/router/api/v1/users/router.ts | 40 +++++++++- server/src/utils/user.ts | 10 +++ 4 files changed, 138 insertions(+), 15 deletions(-) create mode 100644 server/src/server/router/api/v1/users/router.test.ts diff --git a/server/src/lib/search/search.ts b/server/src/lib/search/search.ts index 8eb3e9ba4..41648c5f2 100644 --- a/server/src/lib/search/search.ts +++ b/server/src/lib/search/search.ts @@ -9,10 +9,11 @@ import { Playtypes, SongDocument, AnyChartDocument, + PrivateUserDocument, integer, } from "tachi-common"; import { EscapeStringRegexp } from "../../utils/misc"; -import { OMIT_PRIVATE_USER_RETURNS } from "../../utils/user"; +import { GetOnlineCutoff, OMIT_PRIVATE_USER_RETURNS } from "../../utils/user"; import { CONF_INFO } from "../setup/config"; const logger = CreateLogCtx(__filename); @@ -125,18 +126,21 @@ export function SearchSessions( * aren't allowed spaces in their name. In short, $text is very * poor at actually matching usernames. */ -export function SearchUsersRegExp(search: string) { +export function SearchUsersRegExp(search: string, matchOnline = false) { const regexEsc = EscapeStringRegexp(search.toLowerCase()); - return db.users.find( - { - usernameLowercase: { $regex: new RegExp(regexEsc, "u") }, - }, - { - limit: 25, - projection: OMIT_PRIVATE_USER_RETURNS, - } - ); + const matchQuery: FilterQuery = { + usernameLowercase: { $regex: new RegExp(regexEsc, "u") }, + }; + + if (matchOnline) { + matchQuery.lastSeen = { $gt: GetOnlineCutoff() }; + } + + return db.users.find(matchQuery, { + limit: 25, + projection: OMIT_PRIVATE_USER_RETURNS, + }); } /** diff --git a/server/src/server/router/api/v1/users/router.test.ts b/server/src/server/router/api/v1/users/router.test.ts new file mode 100644 index 000000000..1d4b48228 --- /dev/null +++ b/server/src/server/router/api/v1/users/router.test.ts @@ -0,0 +1,77 @@ +import t from "tap"; +import db from "../../../../../external/mongo/db"; +import mockApi from "../../../../../test-utils/mock-api"; +import ResetDBState from "../../../../../test-utils/resets"; +import { PrivateUserDocument, PublicUserDocument } from "tachi-common"; +import { CloseAllConnections } from "../../../../../test-utils/close-connections"; + +t.test("GET /api/v1/users", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should search users if search param is set.", async (t) => { + const res = await mockApi.get("/api/v1/users?search=zkldi"); + + t.equal(res.body.body.length, 1); + t.equal(res.body.body[0].username, "test_zkldi"); + + const res2 = await mockApi.get("/api/v1/users?search=nothing"); + + t.equal(res2.body.body.length, 0); + + const res3 = await mockApi.get("/api/v1/users?search=ZklDI"); + + t.equal(res3.body.body.length, 1); + t.equal(res3.body.body[0].username, "test_zkldi"); + + t.end(); + }); + + t.test("Should sanitise input for regex stuff.", async (t) => { + const res = await mockApi.get("/api/v1/users?search=.*"); + + t.equal(res.body.body.length, 0); + + t.end(); + }); + + t.test("Should restrict returns to only online users if online is set.", async (t) => { + const res = await mockApi.get("/api/v1/users?online=true"); + + t.equal(res.body.body.length, 0); + + await db.users.insert({ + usernameLowercase: "online_dude", + lastSeen: Date.now(), + id: 2, + } as PrivateUserDocument); + + const res2 = await mockApi.get("/api/v1/users?online=true"); + + t.equal(res2.body.body.length, 1); + t.equal(res2.body.body[0].usernameLowercase, "online_dude"); + + t.end(); + }); + + t.test("Should return users sorted by lastSeen if no arguments are passed.", async (t) => { + await db.users.insert({ + usernameLowercase: "online_dude", + lastSeen: Date.now(), + id: 2, + } as PrivateUserDocument); + + const res = await mockApi.get("/api/v1/users"); + + t.equal(res.body.body.length, 2); + t.strictSame( + res.body.body.map((e: PublicUserDocument) => e.usernameLowercase), + ["online_dude", "test_zkldi"] + ); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/v1/users/router.ts b/server/src/server/router/api/v1/users/router.ts index faa0399ad..6f5535903 100644 --- a/server/src/server/router/api/v1/users/router.ts +++ b/server/src/server/router/api/v1/users/router.ts @@ -1,17 +1,49 @@ import { Router } from "express"; import db from "../../../../../external/mongo/db"; +import { SearchUsersRegExp } from "../../../../../lib/search/search"; +import { IsString } from "../../../../../utils/misc"; +import { GetOnlineCutoff, OMIT_PRIVATE_USER_RETURNS } from "../../../../../utils/user"; import userIDRouter from "./_userID/router"; const router: Router = Router({ mergeParams: true }); /** - * List Users. - * @param online - Only return online users. - * @param username - Return users similar to this username. + * Search users. + * + * @param online - Restrict returned users to those who are online. + * @param search - Search for users where their name contains this string. If not present, returns + * users sorted by last appearance. + * * @name GET /api/v1/users */ router.get("/", async (req, res) => { - // i dont know yet. + const onlyOnline = !!req.query.online; + + let users; + + if (req.query.search) { + if (!IsString(req.query.search)) { + return res.status(400).json({ + success: false, + description: `Search parameter was invalid.`, + }); + } + users = await SearchUsersRegExp(req.query.search!, onlyOnline); + } else { + const query = onlyOnline ? { lastSeen: { $gt: GetOnlineCutoff() } } : {}; + + users = await db.users.find(query, { + sort: { lastSeen: -1 }, + limit: 100, + projection: OMIT_PRIVATE_USER_RETURNS, + }); + } + + return res.status(200).json({ + success: true, + description: `Returned ${users.length} users.`, + body: users, + }); }); router.use("/:userID", userIDRouter); diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts index a412262d1..8149add12 100644 --- a/server/src/utils/user.ts +++ b/server/src/utils/user.ts @@ -176,3 +176,13 @@ export async function GetUsersRanking(stats: UserGameStats) { outOf: aggRes[0].outOf as integer, }; } + +const FIVE_MINUTES = 1000 * 60 * 5; + +/** + * Returns the cutoff point for "being online" in tachi. This means the user + * has made any page request in the past 5 minutes. + */ +export function GetOnlineCutoff() { + return Date.now() - FIVE_MINUTES; +}