diff --git a/server/package.json b/server/package.json index e8d4a4ccd..7973f1e64 100644 --- a/server/package.json +++ b/server/package.json @@ -73,7 +73,7 @@ "redis": "3.1.2", "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", - "tachi-common": "0.1.54", + "tachi-common": "0.2.1", "typescript": "4.3.4", "winston": "3.3.3" }, diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 6c609071e..050d47915 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -54,7 +54,7 @@ specifiers: rimraf: 3.0.2 safe-json-stringify: 1.2.0 supertest: 6.1.3 - tachi-common: 0.1.54 + tachi-common: 0.2.1 tap: 15.0.9 ts-node: 10.0.0 tsconfig-paths: 3.10.1 @@ -88,7 +88,7 @@ dependencies: redis: 3.1.2 rimraf: 3.0.2 safe-json-stringify: 1.2.0 - tachi-common: 0.1.54_ts-node@10.0.0+typescript@4.3.4 + tachi-common: 0.2.1_ts-node@10.0.0+typescript@4.3.4 typescript: 4.3.4 winston: 3.3.3 @@ -3681,8 +3681,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.1.54_ts-node@10.0.0+typescript@4.3.4: - resolution: {integrity: sha512-m2l1zLk6YiLckvaXcu4n0cdrFo54XA0eeq+IrfbIp5N9qHf4ACkTPCVu206KPLiaRq2Dr4LMugv+XxZyKIo98w==} + /tachi-common/0.2.1_ts-node@10.0.0+typescript@4.3.4: + resolution: {integrity: sha512-4Dc9K2o9s9ZDKiETLoegKM+rNC/D7BCTxdKeZnHJX8hcWo4lcTcKZbH51NHLb+hfDTGFYTgfHvm3nXSEcrQm9A==} dependencies: monk: 7.3.4 tap: 15.0.9_ts-node@10.0.0+typescript@4.3.4 diff --git a/server/src/external/redis/redis-IPC.test.ts b/server/src/external/redis/redis-IPC.test.ts index 45d0ea9d4..e29167d37 100644 --- a/server/src/external/redis/redis-IPC.test.ts +++ b/server/src/external/redis/redis-IPC.test.ts @@ -2,7 +2,12 @@ import t from "tap"; import { RedisPub, RedisSub } from "./redis-IPC"; -t.test("Basic PUB/SUB testing", (t) => { +/** + * Note: The functions these things use have been stubbed out. However, the tests still run and + * they also still work. We'll skip them though, anyway. + */ + +t.skip("Basic PUB/SUB testing", (t) => { t.setTimeout(2000); RedisSub("test" as any, (d) => { t.equal(d.userID, 1); diff --git a/server/src/external/redis/redis-IPC.ts b/server/src/external/redis/redis-IPC.ts index 6e37859b3..d50d5eaf4 100644 --- a/server/src/external/redis/redis-IPC.ts +++ b/server/src/external/redis/redis-IPC.ts @@ -1,10 +1,24 @@ import redis from "redis"; -import { RedisIPCChannels, RedisIPCData } from "tachi-common"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; +/** + * This code has been stubbed out! It doesn't really have a use at the moment. + * At some point, though, we might actually need this. (Maybe for a logging framework) + * + * This was used to communicate with other processes running on the same host as this server. + * The idea was the use it for the discord bot, however, we have webhooks now that do this (better). + * Thanks! + */ + const logger = CreateLogCtx(__filename); +// Stub types. In the future, we may actually use this. +export type RedisIPCChannels = ""; +export type RedisIPCData = { + "": unknown; +}; + type RedisSubCallback = (data: RedisIPCData[T]) => void; type SubCallbacks = { diff --git a/server/src/lib/score-import/framework/goals/goals.ts b/server/src/lib/score-import/framework/goals/goals.ts index 88c4809fb..08f2c2d14 100644 --- a/server/src/lib/score-import/framework/goals/goals.ts +++ b/server/src/lib/score-import/framework/goals/goals.ts @@ -2,7 +2,7 @@ import { integer, Game, GoalDocument, UserGoalDocument } from "tachi-common"; import { EvaluateGoalForUser } from "lib/achievables/goals"; import db from "external/mongo/db"; import { KtLogger } from "lib/logger/logger"; -import { RedisPub } from "external/redis/redis-IPC"; +import { EmitWebhookEvent } from "lib/webhooks/webhooks"; /** * Update a user's progress on all of their set goals. @@ -111,11 +111,14 @@ export async function ProcessGoal( // if this is a newly-achieved goal if (res.achieved && !userGoal.achieved) { - RedisPub("goal-achieved", { - userID, - goalID: goal.goalID, - old: oldData, - new: newData, + EmitWebhookEvent({ + type: "goal-achieved/v1", + content: { + userID, + goalID: goal.goalID, + old: oldData, + new: newData, + }, }); } diff --git a/server/src/lib/score-import/framework/milestones/milestones.ts b/server/src/lib/score-import/framework/milestones/milestones.ts index c1f7a2dbf..ada8b96e6 100644 --- a/server/src/lib/score-import/framework/milestones/milestones.ts +++ b/server/src/lib/score-import/framework/milestones/milestones.ts @@ -11,7 +11,7 @@ import { import db from "external/mongo/db"; import { BulkWriteUpdateOneOperation } from "mongodb"; import { CalculateMilestoneOutOf, GetGoalIDsFromMilestone } from "utils/milestone"; -import { RedisPub } from "external/redis/redis-IPC"; +import { EmitWebhookEvent } from "lib/webhooks/webhooks"; /** * Processes and updates a user's milestones from their Goal Import Info (i.e. what is returned @@ -131,9 +131,12 @@ export async function UpdateUsersMilestones( } if (achieved && !userMilestone.achieved) { - RedisPub("milestone-achieved", { - userID, - ...milestoneInfo, + EmitWebhookEvent({ + type: "milestone-achieved/v1", + content: { + userID, + ...milestoneInfo, + }, }); } } diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.ts b/server/src/lib/score-import/framework/user-game-stats/classes.ts index dd50e7cdb..8f1c41002 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.ts @@ -10,6 +10,7 @@ import { import { ReturnClassIfGreater } from "utils/class"; import { RedisPub } from "external/redis/redis-IPC"; import { ClassHandler, ScoreClasses } from "./types"; +import { EmitWebhookEvent } from "lib/webhooks/webhooks"; type ClassHandlerMap = { [G in Game]: @@ -152,7 +153,7 @@ export function CalculateClassDeltas( }; } - RedisPub("class-update", { userID, ...delta }); + EmitWebhookEvent({ type: "class-update/v1", content: { userID, ...delta } }); deltas.push(delta); } diff --git a/server/src/lib/webhooks/webhooks.ts b/server/src/lib/webhooks/webhooks.ts new file mode 100644 index 000000000..7ae364dc4 --- /dev/null +++ b/server/src/lib/webhooks/webhooks.ts @@ -0,0 +1,37 @@ +import db from "external/mongo/db"; +import CreateLogCtx from "lib/logger/logger"; +import { WebhookEvents } from "tachi-common"; + +const logger = CreateLogCtx(__filename); + +// @todo make use of aggressive caching here? +export async function GetWebhookUrlInfo() { + const urls = await db["oauth2-clients"].find( + { webhookUri: { $ne: null } }, + { projection: { webhookUri: 1, clientSecret: 1 } } + ); + + return urls; +} + +/** + * Emits a webhook event to all registered client webhooks on this tachi-server install. + */ +export async function EmitWebhookEvent(content: WebhookEvents) { + const webhookUrls = await GetWebhookUrlInfo(); + + logger.verbose(`Emitting webhook event ${content.type} to ${webhookUrls.length} clients.`); + + // We don't actually care about the response of these. Just fire them and forget. + for (const client of webhookUrls) { + // we know this to be non-null because of GetWebhookUrlInfo. + fetch(client.webhookUri!, { + method: "POST", + body: JSON.stringify(content), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${client.clientSecret}`, + }, + }); + } +} diff --git a/server/src/utils/class.ts b/server/src/utils/class.ts index 4c788cdb8..d1bf93ba4 100644 --- a/server/src/utils/class.ts +++ b/server/src/utils/class.ts @@ -2,8 +2,8 @@ import { Game, integer, IDStrings, Playtypes, UserGameStats } from "tachi-common import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { GameClassSets } from "tachi-common/js/game-classes"; -import { RedisPub } from "external/redis/redis-IPC"; import { CreateGameSettings } from "lib/game-settings/create-game-settings"; +import { EmitWebhookEvent } from "lib/webhooks/webhooks"; const logger = CreateLogCtx(__filename); @@ -70,15 +70,21 @@ export async function UpdateClassIfGreater( } if (isGreater === null) { - RedisPub("class-update", { userID, new: classVal, old: null, set: classSet }); + EmitWebhookEvent({ + type: "class-update/v1", + content: { userID, new: classVal, old: null, set: classSet }, + }); return null; } else { - RedisPub("class-update", { - userID, - new: classVal, - old: userGameStats!.classes[classSet]!, - set: classSet, + EmitWebhookEvent({ + type: "class-update/v1", + content: { + userID, + new: classVal, + old: userGameStats!.classes[classSet]!, + set: classSet, + }, }); return true;