diff --git a/deps.ts b/deps.ts index 565f977..d5447b7 100644 --- a/deps.ts +++ b/deps.ts @@ -13,4 +13,5 @@ export * as path from "https://deno.land/std@0.160.0/path/mod.ts"; export { MultiProgressBar } from "https://deno.land/x/progress@v1.2.8/mod.ts"; export { Mutex } from "https://deno.land/x/semaphore@v1.1.1/mod.ts"; export type { DeepReadonly } from "https://deno.land/x/ts_essentials@v9.1.2/mod.ts"; -export * as MongoDB from "npm:mongodb"; \ No newline at end of file +export * as MongoDB from "npm:mongodb"; +export * as splatNet3Types from "npm:splatnet3-types/splatnet3"; diff --git a/s3si.ts b/s3si.ts index b49e85a..f7a6ed9 100644 --- a/s3si.ts +++ b/s3si.ts @@ -14,6 +14,7 @@ const parseArgs = (args: string[]) => { "monitor": ["m"], "skipMode": ["s", "skip-mode"], "withSummary": "with-summary", + "withStages": "with-stages", }, }); return parsed; @@ -34,6 +35,7 @@ Options: --skip-mode , -s Skip mode (default: null) ("vs", "coop") --with-summary Include summary in the output + --with-stages Include stage records in the output --help Show this help message and exit`, ); Deno.exit(0); diff --git a/scripts/convert-old-mongodb-battles.ts b/scripts/convert-old-mongodb-battles.ts index 4c95bbe..79041ae 100644 --- a/scripts/convert-old-mongodb-battles.ts +++ b/scripts/convert-old-mongodb-battles.ts @@ -3,7 +3,7 @@ import { DEFAULT_ENV } from "../src/env.ts"; import { MongoDBExporter } from "../src/exporters/mongodb.ts"; import { FileStateBackend, Profile } from "../src/state.ts"; -const OLD_BATTLES_END_DATE = new Date('2023-02-28T03:42:47.000+00:00'); +const OLD_BATTLES_END_DATE = new Date("2023-02-28T03:42:47.000+00:00"); const env = DEFAULT_ENV; const stateBackend = new FileStateBackend("./profile.json"); @@ -11,18 +11,18 @@ const profile = new Profile({ stateBackend, env }); await profile.readState(); if (!profile.state.mongoDbUri) { - console.error("MongoDB URI not set"); - Deno.exit(1); + console.error("MongoDB URI not set"); + Deno.exit(1); } const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri); const battlesCollection = mongoDbClient.db("splashcat").collection("battles"); const filter = { - "exportDate": { - "$lte": OLD_BATTLES_END_DATE, - }, - "gameId": undefined, + "exportDate": { + "$lte": OLD_BATTLES_END_DATE, + }, + "gameId": undefined, }; const cursor = battlesCollection.find(filter); @@ -32,16 +32,18 @@ const oldDocuments = await battlesCollection.countDocuments(filter); console.log(`Found ${oldDocuments} old battles to update...`); for await (const doc of cursor) { - const { splatNetData, _id } = doc; + const { splatNetData, _id } = doc; - const splatNetId = splatNetData.id; - const uniqueId = MongoDBExporter.getGameId(splatNetId); + const splatNetId = splatNetData.id; + const uniqueId = MongoDBExporter.getGameId(splatNetId); - await battlesCollection.updateOne({ _id }, { "$set": { - gameId: uniqueId, - }}); + await battlesCollection.updateOne({ _id }, { + "$set": { + gameId: uniqueId, + }, + }); - console.log(`Updated ${splatNetId} to ${uniqueId}`); + console.log(`Updated ${splatNetId} to ${uniqueId}`); } -console.log("Done!") \ No newline at end of file +console.log("Done!"); diff --git a/scripts/fix-stored-dates.ts b/scripts/fix-stored-dates.ts index 45a8138..6f90905 100644 --- a/scripts/fix-stored-dates.ts +++ b/scripts/fix-stored-dates.ts @@ -9,17 +9,17 @@ const profile = new Profile({ stateBackend, env }); await profile.readState(); if (!profile.state.mongoDbUri) { - console.error("MongoDB URI not set"); - Deno.exit(1); + console.error("MongoDB URI not set"); + Deno.exit(1); } const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri); const battlesCollection = mongoDbClient.db("splashcat").collection("battles"); const filter = { - "splatNetData.playedTime": { - $type: "string" - } + "splatNetData.playedTime": { + $type: "string", + }, }; const cursor = battlesCollection.find(filter); @@ -29,13 +29,19 @@ const oldDocuments = await battlesCollection.countDocuments(filter); console.log(`Found ${oldDocuments} old battles to update...`); for await (const doc of cursor) { - const { splatNetData, _id } = doc; + const { splatNetData, _id } = doc; - await battlesCollection.updateOne({ _id }, { "$set": { - "splatNetData.playedTime": new Date(splatNetData.playedTime), - }}); + await battlesCollection.updateOne({ _id }, { + "$set": { + "splatNetData.playedTime": new Date(splatNetData.playedTime), + }, + }); - console.log(`Updated ${splatNetData.playedTime} to ${new Date(splatNetData.playedTime)}`); + console.log( + `Updated ${splatNetData.playedTime} to ${new Date( + splatNetData.playedTime, + )}`, + ); } -console.log("Done!") \ No newline at end of file +console.log("Done!"); diff --git a/src/app.ts b/src/app.ts index 196282a..aec2a03 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,6 +17,7 @@ export type Opts = { noProgress: boolean; monitor: boolean; withSummary: boolean; + withStages: boolean; skipMode?: string; cache?: Cache; stateBackend?: StateBackend; @@ -29,6 +30,7 @@ export const DEFAULT_OPTS: Opts = { noProgress: false, monitor: false, withSummary: false, + withStages: true, env: DEFAULT_ENV, }; @@ -310,6 +312,34 @@ export class App { throw errors[0]; } } + + const stageExporters = exporters.filter((e) => e.exportStages); + if (!this.opts.withStages || stageExporters.length === 0) { + this.env.logger.log("Skip exporting stages."); + } else { + const stageRecords = await splatnet.getStageRecords(); + + await Promise.all( + stageExporters.map((e) => + showError( + this.env, + e.exportStages!(stageRecords.stageRecords.nodes), + ).then((result) => { + if (result.status === "success") { + this.env.logger.log(`Exported stages to ${result.url}`); + } else if (result.status === "skip") { + this.env.logger.log(`Skipped exporting stages to ${e.name}`); + } else { + const _never: never = result; + } + }) + .catch((err) => { + errors.push(err); + this.env.logger.error(`\nFailed to export to ${e.name}:`, err); + }) + ), + ); + } } async monitor() { while (true) { diff --git a/src/exporters/mongodb.ts b/src/exporters/mongodb.ts index d35caca..d9e4b4a 100644 --- a/src/exporters/mongodb.ts +++ b/src/exporters/mongodb.ts @@ -1,101 +1,113 @@ import { MongoDB } from "../../deps.ts"; import { AGENT_VERSION, NSOAPP_VERSION, S3SI_VERSION } from "../constant.ts"; -import { CoopHistoryDetail, ExportResult, Game, GameExporter, Summary, VsHistoryDetail } from "../types.ts"; +import { + CoopHistoryDetail, + ExportResult, + Game, + GameExporter, + Summary, + VsHistoryDetail, +} from "../types.ts"; import { parseHistoryDetailId } from "../utils.ts"; export class MongoDBExporter implements GameExporter { - name = "mongodb"; - mongoDbClient: MongoDB.MongoClient; - mongoDb: MongoDB.Db; - battlesCollection: MongoDB.Collection; - jobsCollection: MongoDB.Collection; - summariesCollection: MongoDB.Collection; - constructor(private mongoDbUri: string) { - this.mongoDbClient = new MongoDB.MongoClient(mongoDbUri); - this.mongoDb = this.mongoDbClient.db("splashcat"); - this.battlesCollection = this.mongoDb.collection("battles"); - this.jobsCollection = this.mongoDb.collection("jobs"); - this.summariesCollection = this.mongoDb.collection("summaries"); - } + name = "mongodb"; + mongoDbClient: MongoDB.MongoClient; + mongoDb: MongoDB.Db; + battlesCollection: MongoDB.Collection; + jobsCollection: MongoDB.Collection; + summariesCollection: MongoDB.Collection; + constructor(private mongoDbUri: string) { + this.mongoDbClient = new MongoDB.MongoClient(mongoDbUri); + this.mongoDb = this.mongoDbClient.db("splashcat"); + this.battlesCollection = this.mongoDb.collection("battles"); + this.jobsCollection = this.mongoDb.collection("jobs"); + this.summariesCollection = this.mongoDb.collection("summaries"); + } - static getGameId(id: string) { // very similar to the file exporter - const { uid, timestamp } = parseHistoryDetailId(id); + static getGameId(id: string) { // very similar to the file exporter + const { uid, timestamp } = parseHistoryDetailId(id); - return `${uid}_${timestamp}Z`; - } + return `${uid}_${timestamp}Z`; + } - async notExported({ type, list }: { type: Game["type"], list: string[] }): Promise { - const out: string[] = []; + async notExported( + { type, list }: { type: Game["type"]; list: string[] }, + ): Promise { + const out: string[] = []; - const collection = type === "CoopInfo" ? this.jobsCollection : this.battlesCollection; + const collection = type === "CoopInfo" + ? this.jobsCollection + : this.battlesCollection; - for (const id of list) { - const uniqueId = MongoDBExporter.getGameId(id); - const countNewStorage = await collection.countDocuments({ - gameId: uniqueId, - }); + for (const id of list) { + const uniqueId = MongoDBExporter.getGameId(id); + const countNewStorage = await collection.countDocuments({ + gameId: uniqueId, + }); - if (countNewStorage === 0) { - out.push(id); - } - } + if (countNewStorage === 0) { + out.push(id); + } + } - return out; - } + return out; + } - async exportGame(game: Game): Promise { - const uniqueId = MongoDBExporter.getGameId(game.detail.id); + async exportGame(game: Game): Promise { + const uniqueId = MongoDBExporter.getGameId(game.detail.id); - const common = { - // this seems like useful data to store... - // loosely modeled after FileExporterTypeCommon - nsoVersion: NSOAPP_VERSION, - agentVersion: AGENT_VERSION, - s3siVersion: S3SI_VERSION, - exportDate: new Date(), - }; + const common = { + // this seems like useful data to store... + // loosely modeled after FileExporterTypeCommon + nsoVersion: NSOAPP_VERSION, + agentVersion: AGENT_VERSION, + s3siVersion: S3SI_VERSION, + exportDate: new Date(), + }; - const splatNetData = { - ...game.detail, - playedTime: new Date(game.detail.playedTime), - }; + const splatNetData = { + ...game.detail, + playedTime: new Date(game.detail.playedTime), + }; - const body: - { - data: Game, - splatNetData: Omit<(VsHistoryDetail | CoopHistoryDetail), "playedTime"> & { playedTime: Date }, - gameId: string, - } & typeof common = { - ...common, - data: game, - splatNetData, - gameId: uniqueId, - }; + const body: { + data: Game; + splatNetData: + & Omit<(VsHistoryDetail | CoopHistoryDetail), "playedTime"> + & { playedTime: Date }; + gameId: string; + } & typeof common = { + ...common, + data: game, + splatNetData, + gameId: uniqueId, + }; - const isJob = game.type === "CoopInfo"; + const isJob = game.type === "CoopInfo"; - const collection = isJob ? this.jobsCollection : this.battlesCollection; + const collection = isJob ? this.jobsCollection : this.battlesCollection; - const result = await collection.insertOne(body); + const result = await collection.insertOne(body); - const objectId = result.insertedId; + const objectId = result.insertedId; - return { - status: "success", - url: `https://new.splatoon.catgirlin.space/battle/${objectId.toString()}`, - }; - } + return { + status: "success", + url: `https://new.splatoon.catgirlin.space/battle/${objectId.toString()}`, + }; + } - async exportSummary(summary: Summary): Promise { - const id = summary.uid; + async exportSummary(summary: Summary): Promise { + const id = summary.uid; - await this.summariesCollection.insertOne({ - summaryId: id, - ...summary, - }); + await this.summariesCollection.insertOne({ + summaryId: id, + ...summary, + }); - return { - status: "success", - }; - } + return { + status: "success", + }; + } } diff --git a/src/splatnet3.ts b/src/splatnet3.ts index 2eeaf87..8caee9f 100644 --- a/src/splatnet3.ts +++ b/src/splatnet3.ts @@ -257,6 +257,12 @@ export class Splatnet3 { CoopHistoryQuery, }; } + + async getStageRecords() { + const resp = await this.request(Queries.StageRecordQuery); + + return resp; + } } function getIdsFromGroups( diff --git a/src/types.ts b/src/types.ts index 23acd88..6c84183 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { splatNet3Types } from "../deps.ts"; import { RankState } from "./state.ts"; export enum Queries { @@ -15,6 +16,7 @@ export enum Queries { myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8", HistoryRecordQuery = "32b6771f94083d8f04848109b7300af5", ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd", + StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3", } export type VarsMap = { [Queries.HomeQuery]: []; @@ -34,6 +36,7 @@ export type VarsMap = { [Queries.myOutfitCommonDataEquipmentsQuery]: []; [Queries.HistoryRecordQuery]: []; [Queries.ConfigureAnalyticsQuery]: []; + [Queries.StageRecordQuery]: []; }; export type Image = { @@ -371,6 +374,9 @@ export type GameExporter = { ) => Promise; exportGame: (game: Game) => Promise; exportSummary?: (summary: Summary) => Promise; + exportStages?: ( + stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"], + ) => Promise; }; export type BankaraBattleHistories = { @@ -550,6 +556,7 @@ export type RespMap = { xMatchMaxLf: SimpleXRank; } | null; }; + [Queries.StageRecordQuery]: splatNet3Types.StageRecordResult; }; export type WeaponWithRatio = { weapon: {