From 62d1982193b2ccdb894b8a0df891d2f1eb21cc66 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sat, 3 Jun 2023 21:35:24 +0100 Subject: [PATCH] Add support for challenges for Splatoon 3 presence --- src/cli/presence-server.ts | 61 +---------- src/discord/monitor/splatoon3.ts | 167 +++++++++++++++++++------------ 2 files changed, 108 insertions(+), 120 deletions(-) diff --git a/src/cli/presence-server.ts b/src/cli/presence-server.ts index c91b873..206d89b 100644 --- a/src/cli/presence-server.ts +++ b/src/cli/presence-server.ts @@ -6,8 +6,7 @@ import express, { Request, Response } from 'express'; import fetch from 'node-fetch'; import * as persist from 'node-persist'; import mkdirp from 'mkdirp'; -import { BankaraMatchMode, BankaraMatchSetting_schedule, CoopRule, CoopSetting_schedule, DetailFestRecordDetailResult, DetailVotingStatusResult, FestMatchSetting_schedule, FestRecordResult, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, FriendListResult, FriendOnlineState, Friend_friendList, GraphQLSuccessResponse, KnownRequestId, LeagueMatchSetting_schedule, RegularMatchSetting_schedule, StageScheduleResult, VsMode, XMatchSetting_schedule } from 'splatnet3-types/splatnet3'; -import StageScheduleQuery_730cd98 from 'splatnet3-types/graphql/730cd98e84f1030d3e9ac86b6f1aae13'; +import { BankaraMatchSetting_schedule, CoopRule, CoopSetting_schedule, DetailFestRecordDetailResult, DetailVotingStatusResult, FestMatchSetting_schedule, FestRecordResult, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, FriendListResult, FriendOnlineState, Friend_friendList, GraphQLSuccessResponse, KnownRequestId, LeagueMatchSetting_schedule, RegularMatchSetting_schedule, StageScheduleResult, XMatchSetting_schedule } from 'splatnet3-types/splatnet3'; import type { Arguments as ParentArguments } from '../cli.js'; import { product, version } from '../util/product.js'; import Users, { CoralUser } from '../common/users.js'; @@ -22,6 +21,7 @@ import { parseListenAddress } from '../util/net.js'; import { EventStreamResponse, HttpServer, ResponseError } from './util/http-server.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js'; import { getTitleIdFromEcUrl } from '../util/misc.js'; +import { getSettingForCoopRule, getSettingForVsMode } from '../discord/monitor/splatoon3.js'; const debug = createDebug('cli:presence-server'); const debugSplatnet3Proxy = createDebug('cli:presence-server:splatnet3-proxy'); @@ -839,7 +839,7 @@ class Server extends HttpServer { friend.onlineState === FriendOnlineState.VS_MODE_FIGHTING) && friend.vsMode ) { const schedules = await user.getSchedules(); - const vs_setting = this.getSettingForVsMode(schedules, friend.vsMode); + const vs_setting = getSettingForVsMode(schedules, friend.vsMode); const vs_stages = vs_setting?.vsStages.map(stage => ({ ...stage, image: schedules.vsStages.nodes.find(s => s.id === stage.id)?.originalImage ?? stage.image, @@ -858,11 +858,7 @@ class Server extends HttpServer { friend.onlineState === FriendOnlineState.COOP_MODE_FIGHTING ) { const schedules = await user.getSchedules(); - const coop_schedules = - friend.coopRule === CoopRule.BIG_RUN ? schedules.coopGroupingSchedule.bigRunSchedules : - friend.coopRule === CoopRule.TEAM_CONTEST ? schedules.coopGroupingSchedule.teamContestSchedules : - schedules.coopGroupingSchedule.regularSchedules; - const coop_setting = getSchedule(coop_schedules)?.setting; + const coop_setting = getSettingForCoopRule(schedules.coopGroupingSchedule, friend.coopRule as CoopRule); response.splatoon3_coop_setting = coop_setting ?? null; } @@ -899,31 +895,6 @@ class Server extends HttpServer { return null; } - getSettingForVsMode(schedules: StageScheduleResult, vs_mode: Pick) { - if (vs_mode.mode === 'REGULAR') { - return getSchedule(schedules.regularSchedules)?.regularMatchSetting; - } - if (vs_mode.mode === 'BANKARA') { - const settings = getSchedule(schedules.bankaraSchedules)?.bankaraMatchSettings; - if (vs_mode.id === 'VnNNb2RlLTI=') { - return settings?.find(s => s.mode === BankaraMatchMode.CHALLENGE); - } - if (vs_mode.id === 'VnNNb2RlLTUx') { - return settings?.find(s => s.mode === BankaraMatchMode.OPEN); - } - } - if (vs_mode.mode === 'FEST') { - return getSchedule(schedules.festSchedules)?.festMatchSetting; - } - if (vs_mode.mode === 'LEAGUE' && 'leagueSchedules' in schedules) { - return getSchedule((schedules as StageScheduleQuery_730cd98).leagueSchedules)?.leagueMatchSetting; - } - if (vs_mode.mode === 'X_MATCH') { - return getSchedule(schedules.xSchedules)?.xMatchSetting; - } - return null; - } - async handleUserFestVotingStatusHistoryRequest(req: Request, res: Response, presence_user_nsaid: string) { if (!this.record_fest_votes?.read) { throw new ResponseError(404, 'not_found', 'Not recording fest voting status history'); @@ -1317,27 +1288,3 @@ function replacer(key: string, value: any, data: unknown) { return value; } - -function getSplatoon3inkUrl(image_url: string) { - const url = new URL(image_url); - if (!url.hostname.endsWith('.nintendo.net')) return image_url; - const path = url.pathname.replace(/^\/resources\/prod\//, '/'); - return 'https://splatoon3.ink/assets/splatnet' + path; -} - -function getSchedule(schedules: T[] | {nodes: T[]}): T | null { - if ('nodes' in schedules) schedules = schedules.nodes; - const now = Date.now(); - - for (const schedule of schedules) { - const start = new Date(schedule.startTime); - const end = new Date(schedule.endTime); - - if (start.getTime() >= now) continue; - if (end.getTime() < now) continue; - - return schedule; - } - - return null; -} diff --git a/src/discord/monitor/splatoon3.ts b/src/discord/monitor/splatoon3.ts index f3135b8..bb2fe0a 100644 --- a/src/discord/monitor/splatoon3.ts +++ b/src/discord/monitor/splatoon3.ts @@ -1,7 +1,6 @@ import persist from 'node-persist'; import DiscordRPC from 'discord-rpc'; -import { BankaraMatchMode, BankaraMatchSetting, CoopRule, CoopSchedule_schedule, CoopSetting_schedule, DetailVotingStatusResult, FestMatchSetting, FestTeam_schedule, FestTeam_votingStatus, Fest_schedule, FriendListResult, FriendOnlineState, GraphQLSuccessResponse, LeagueMatchSetting, RegularMatchSetting, StageScheduleResult, VsSchedule_bankara, VsSchedule_fest, VsSchedule_league, VsSchedule_regular, VsSchedule_xMatch, XMatchSetting } from 'splatnet3-types/splatnet3'; -import StageScheduleQuery_730cd98 from 'splatnet3-types/graphql/730cd98e84f1030d3e9ac86b6f1aae13'; +import { BankaraMatchMode, CoopRule, CoopSetting_schedule, DetailVotingStatusResult, FestTeam_schedule, FestTeam_votingStatus, Fest_schedule, FriendListResult, FriendOnlineState, GraphQLSuccessResponse, StageScheduleResult, VsMode, VsSchedule_regular } from 'splatnet3-types/splatnet3'; import { Game } from '../../api/coral-types.js'; import SplatNet3Api from '../../api/splatnet3.js'; import { DiscordPresenceExternalMonitorsConfiguration } from '../../app/common/types.js'; @@ -16,6 +15,16 @@ import { DiscordPresenceContext, ErrorResult } from '../types.js'; const debug = createDebug('nxapi:discord:splatnet3'); +type VsSchedule_event = StageScheduleResult['eventSchedules']['nodes'][0]; +type LeagueMatchSetting_schedule = VsSchedule_event['leagueMatchSetting']; + +type VsSetting_schedule = + StageScheduleResult['regularSchedules']['nodes'][0]['regularMatchSetting'] | + StageScheduleResult['bankaraSchedules']['nodes'][0]['bankaraMatchSettings'][0] | + StageScheduleResult['eventSchedules']['nodes'][0]['leagueMatchSetting'] | + StageScheduleResult['xSchedules']['nodes'][0]['xMatchSetting'] | + StageScheduleResult['festSchedules']['nodes'][0]['festMatchSetting']; + export default class SplatNet3Monitor extends EmbeddedLoop { update_interval: number = 1 * 60; // 1 minute in seconds @@ -29,13 +38,9 @@ export default class SplatNet3Monitor extends EmbeddedLoop { friend: FriendListResult['friends']['nodes'][0] | null = null; regular_schedule: VsSchedule_regular | null = null; - anarchy_schedule: VsSchedule_bankara | null = null; - fest_schedule: VsSchedule_fest | null = null; - league_schedule: VsSchedule_league | null = null; - x_schedule: VsSchedule_xMatch | null = null; - coop_regular_schedule: CoopSchedule_schedule | null = null; - coop_big_run_schedule: CoopSchedule_schedule | null = null; - coop_team_contest_schedule: CoopSchedule_schedule | null = null; + vs_setting: VsSetting_schedule | null = null; + coop_setting: CoopSetting_schedule | null = null; + fest: Fest_schedule | null = null; fest_team_voting_status: FestTeam_votingStatus | null = null; fest_team: FestTeam_schedule | null = null; @@ -84,6 +89,7 @@ export default class SplatNet3Monitor extends EmbeddedLoop { this.splatnet = splatnet; this.data = data; } catch (err) { + debug('Error authenticating to SplatNet 3', err); const result = await this.discord_presence.handleError(err as Error); if (result === ErrorResult.RETRY) return this.init(); if (result === ErrorResult.STOP) return LoopResult.STOP; @@ -122,21 +128,19 @@ export default class SplatNet3Monitor extends EmbeddedLoop { this.friend = friend; - this.regular_schedule = this.getSchedule(this.cached_schedules?.data.regularSchedules.nodes ?? []); + this.regular_schedule = getSchedule(this.cached_schedules?.data.regularSchedules.nodes ?? []); if (!this.regular_schedule) { this.cached_schedules = await this.splatnet?.getSchedules() ?? null; - this.regular_schedule = this.getSchedule(this.cached_schedules?.data.regularSchedules.nodes ?? []); + this.regular_schedule = getSchedule(this.cached_schedules?.data.regularSchedules.nodes ?? []); } - this.anarchy_schedule = this.getSchedule(this.cached_schedules?.data.bankaraSchedules.nodes ?? []); - this.fest_schedule = this.getSchedule(this.cached_schedules?.data.festSchedules.nodes ?? []); - this.league_schedule = this.cached_schedules?.data && 'leagueSchedules' in this.cached_schedules.data ? - this.getSchedule((this.cached_schedules.data as StageScheduleQuery_730cd98).leagueSchedules.nodes ?? []) : null; - this.x_schedule = this.getSchedule(this.cached_schedules?.data.xSchedules.nodes ?? []); - this.coop_regular_schedule = this.getSchedule(this.cached_schedules?.data.coopGroupingSchedule.regularSchedules.nodes ?? []); - this.coop_big_run_schedule = this.getSchedule(this.cached_schedules?.data.coopGroupingSchedule.bigRunSchedules.nodes ?? []); - this.coop_team_contest_schedule = this.getSchedule(this.cached_schedules?.data.coopGroupingSchedule.teamContestSchedules.nodes ?? []); + this.vs_setting = this.cached_schedules && friend?.vsMode ? + getSettingForVsMode(this.cached_schedules.data, friend.vsMode) ?? null : null; + this.coop_setting = this.cached_schedules && friend?.coopRule ? + getSettingForCoopRule(this.cached_schedules.data.coopGroupingSchedule, + friend.coopRule as CoopRule) ?? null : null; + this.fest = this.cached_schedules?.data.currentFest ?? null; // Identify the user by their icon as the vote list doesn't have friend IDs @@ -155,22 +159,6 @@ export default class SplatNet3Monitor extends EmbeddedLoop { this.discord_presence.refreshPresence(); } - getSchedule(schedules: T[]): T | null { - const now = Date.now(); - - for (const schedule of schedules) { - const start = new Date(schedule.startTime); - const end = new Date(schedule.endTime); - - if (start.getTime() >= now) continue; - if (end.getTime() < now) continue; - - return schedule; - } - - return null; - } - async handleError(err: Error) { const result = await this.discord_presence.handleError(err as Error); if (result === ErrorResult.RETRY) return LoopResult.OK_SKIP_INTERVAL; @@ -183,6 +171,74 @@ export default class SplatNet3Monitor extends EmbeddedLoop { } } +export function getSettingForVsMode(schedules: StageScheduleResult, vs_mode: Pick) { + if (vs_mode.mode === 'REGULAR') { + return getSchedule(schedules.regularSchedules)?.regularMatchSetting; + } + if (vs_mode.mode === 'BANKARA') { + const settings = getSchedule(schedules.bankaraSchedules)?.bankaraMatchSettings; + + if (vs_mode.id === 'VnNNb2RlLTI=') { + return settings?.find(s => s.mode === BankaraMatchMode.CHALLENGE); + } + if (vs_mode.id === 'VnNNb2RlLTUx') { + return settings?.find(s => s.mode === BankaraMatchMode.OPEN); + } + } + if (vs_mode.mode === 'FEST') { + return getSchedule(schedules.festSchedules)?.festMatchSetting; + } + if (vs_mode.mode === 'LEAGUE') { + return getSchedule(schedules.eventSchedules)?.leagueMatchSetting; + } + if (vs_mode.mode === 'X_MATCH') { + return getSchedule(schedules.xSchedules)?.xMatchSetting; + } + return null; +} + +export function getSettingForCoopRule(schedules: StageScheduleResult['coopGroupingSchedule'], coop_rule: CoopRule) { + if (coop_rule === CoopRule.REGULAR) { + return getSchedule(schedules.regularSchedules)?.setting; + } + if (coop_rule === CoopRule.BIG_RUN) { + return getSchedule(schedules.bigRunSchedules)?.setting; + } + if (coop_rule === CoopRule.TEAM_CONTEST) { + return getSchedule(schedules.teamContestSchedules)?.setting; + } + return null; +} + +interface TimePeriod { + startTime: string; + endTime: string; +} +interface HasTimePeriods { + timePeriods: TimePeriod[]; +} + +export function getSchedule(schedules: T[] | {nodes: T[]}): T | null { + if ('nodes' in schedules) schedules = schedules.nodes; + const now = Date.now(); + + for (const schedule of schedules) { + const time_periods = 'timePeriods' in schedule ? schedule.timePeriods : [schedule] as [T & TimePeriod]; + + for (const time_period of time_periods) { + const start = new Date(time_period.startTime); + const end = new Date(time_period.endTime); + + if (start.getTime() >= now) continue; + if (end.getTime() < now) continue; + + return schedule; + } + } + + return null; +} + export interface SplatNet3MonitorConfig { storage: persist.LocalStorage; na_session_token: string; @@ -224,9 +280,7 @@ export function getConfigFromAppConfig( interface PresenceUrlResponse { splatoon3?: FriendListResult['friends']['nodes'][0] | null; splatoon3_fest_team?: (FestTeam_votingStatus & FestTeam_schedule) | null; - splatoon3_vs_setting?: - RegularMatchSetting | BankaraMatchSetting | FestMatchSetting | - LeagueMatchSetting | XMatchSetting | null; + splatoon3_vs_setting?: VsSetting_schedule | null; splatoon3_coop_setting?: CoopSetting_schedule | null; splatoon3_fest?: Fest_schedule | null; } @@ -254,33 +308,24 @@ export function callback(activity: DiscordRPC.Presence, game: Game, context?: Di friend.vsMode.id === 'VnNNb2RlLTc=' ? 'Splatfest Battle (Pro)' : // VsMode-7 friend.vsMode.id === 'VnNNb2RlLTg=' ? 'Tricolour Battle' : // VsMode-8 friend.vsMode.mode === 'FEST' ? 'Splatfest Battle' : - friend.vsMode.mode === 'LEAGUE' ? 'League Battle' : + friend.vsMode.id === 'VnNNb2RlLTQ=' ? 'Challenge' : // VsMode-4 + friend.vsMode.mode === 'LEAGUE' ? 'Challenge' : friend.vsMode.mode === 'X_MATCH' ? 'X Battle' : // VsMode-3 undefined; const setting = presence_proxy_data && 'splatoon3_vs_setting' in presence_proxy_data ? presence_proxy_data.splatoon3_vs_setting : - !monitor ? null : - friend.vsMode.mode === 'REGULAR' ? monitor.regular_schedule?.regularMatchSetting : - friend.vsMode.mode === 'BANKARA' ? - friend.vsMode.id === 'VnNNb2RlLTI=' ? - monitor.anarchy_schedule?.bankaraMatchSettings?.find(s => s.mode === BankaraMatchMode.CHALLENGE) : - friend.vsMode.id === 'VnNNb2RlLTUx' ? - monitor.anarchy_schedule?.bankaraMatchSettings?.find(s => s.mode === BankaraMatchMode.OPEN) : - null : - friend.vsMode.mode === 'FEST' ? - friend.vsMode.id === 'VnNNb2RlLTg=' ? null : - monitor.fest_schedule?.festMatchSetting : - friend.vsMode.mode === 'LEAGUE' ? monitor.league_schedule?.leagueMatchSetting : - friend.vsMode.mode === 'X_MATCH' ? monitor.x_schedule?.xMatchSetting : - null; + monitor?.vs_setting; activity.details = (mode_name ?? friend.vsMode.name) + (friend.vsMode.mode === 'FEST' && fest_team_voting_status ? ' - Team ' + fest_team_voting_status.teamName : '') + - (friend.vsMode.mode !== 'FEST' && setting ? ' - ' + setting.vsRule.name : '') + + (friend.vsMode.mode === 'LEAGUE' && setting && 'leagueMatchEvent' in setting ? + ': ' + (setting as LeagueMatchSetting_schedule).leagueMatchEvent.name : '') + + (friend.vsMode.mode !== 'FEST' && friend.vsMode.mode !== 'LEAGUE' && setting ? + ' - ' + setting.vsRule.name : '') + (friend.onlineState === FriendOnlineState.VS_MODE_MATCHING ? ' (matching)' : ''); if (friend.vsMode.id === 'VnNNb2RlLTg=' && fest) { @@ -337,17 +382,13 @@ export function callback(activity: DiscordRPC.Presence, game: Game, context?: Di activity.details = 'Salmon Run' + (friend.onlineState === FriendOnlineState.COOP_MODE_MATCHING ? ' (matching)' : ''); - const coop_setting = + const setting = presence_proxy_data && 'splatoon3_coop_setting' in presence_proxy_data ? presence_proxy_data.splatoon3_coop_setting : - monitor ? - friend.coopRule === CoopRule.BIG_RUN ? monitor.coop_big_run_schedule?.setting : - friend.coopRule === CoopRule.TEAM_CONTEST ? monitor.coop_team_contest_schedule?.setting : - monitor.coop_regular_schedule?.setting : - null; + monitor?.coop_setting; - if (coop_setting) { - const coop_stage_image = new URL(coop_setting.coopStage.image.url); + if (setting) { + const coop_stage_image = new URL(setting.coopStage.image.url); const match = coop_stage_image.pathname.match(/^\/resources\/prod\/(.+)$/); const proxy_stage_image = coop_stage_image.host === 'splatoon3.ink' ? coop_stage_image.href : @@ -356,7 +397,7 @@ export function callback(activity: DiscordRPC.Presence, game: Game, context?: Di if (proxy_stage_image) { activity.largeImageKey = proxy_stage_image; - activity.largeImageText = coop_setting.coopStage.name + + activity.largeImageText = setting.coopStage.name + ' | ' + product; } }