diff --git a/src/api/coral-types.ts b/src/api/coral-types.ts index f0d0ae4..80218d9 100644 --- a/src/api/coral-types.ts +++ b/src/api/coral-types.ts @@ -108,16 +108,53 @@ export interface Friends { friends: Friend[]; } +/** /v4/Friend/List */ +export interface Friends_4 { + friends: Friend_4[]; + extractFriendsIds: string[]; +} + export interface Friend { id: number; nsaId: string; imageUri: string; + image2Uri: string; name: string; isFriend: boolean; isFavoriteFriend: boolean; isServiceUser: boolean; + isNew: boolean; friendCreatedAt: number; - presence: Presence; + route: FriendRoute; + presence: Presence | PresenceOnline | PresenceOffline; +} + +/** /v4/Friend/Show */ +export interface Friend_4 extends Friend { + isOnlineNotificationEnabled: boolean; + presence: PresenceOnline_4 | PresenceOffline; +} + +export interface FriendRoute { + appName: string; + /** In-game player name */ + userName: string; + shopUri: string; + imageUri: string; + // if not IN_APP all other properties are empty strings + channel: FriendRouteChannel; +} + +export enum FriendRouteChannel { + /** Added from friend code lookup on a Switch console or using coral */ + FRIEND_CODE = 'FRIEND_CODE', + /** Added from users you've played with */ + IN_APP = 'IN_APP', + /** Added from search for local users */ + NX_FACED = 'NX_FACED', + '3DS' = '3DS', + + // Wii U, Facebook, Twitter suggestions? } export interface Presence { @@ -128,7 +165,19 @@ export interface Presence { */ updatedAt: number; logoutAt: number; - game: Game | {}; + game: PresenceGame | {}; +} + +export interface PresenceOnline extends Presence { + state: PresenceState.ONLINE | PresenceState.PLAYING; + game: PresenceGame; +} +export interface PresenceOnline_4 extends PresenceOnline { + platform: PresencePlatform; +} +export interface PresenceOffline extends Presence { + state: PresenceState.OFFLINE | PresenceState.INACTIVE; + game: {}; } export enum PresenceState { @@ -146,6 +195,10 @@ export enum PresenceState { PLAYING = 'PLAYING', } +export enum PresencePlatform { + NINTENDO_SWITCH = 1, +} + export interface Game { name: string; imageUri: string; @@ -153,6 +206,9 @@ export interface Game { totalPlayTime: number; /** 0 if never played before */ firstPlayedAt: number; +} + +export interface PresenceGame extends Game { sysDescription: string; } @@ -167,7 +223,9 @@ export interface FriendCodeUser { id: number; nsaId: string; imageUri: string; + image2Uri: string; name: string; + isBlocking: boolean; extras: {}; } @@ -240,14 +298,16 @@ export interface User { id: number; nsaId: string; imageUri: string; + image2Uri: string; name: string; } -/** /v3/User/ShowSelf */ +/** /v4/User/ShowSelf */ export interface CurrentUser { id: number; nsaId: string; imageUri: string; + image2Uri: string; name: string; supportId: string; isChildRestricted: boolean; @@ -255,9 +315,7 @@ export interface CurrentUser { links: { nintendoAccount: { membership: { - active: { - active: boolean; - } | boolean; + active: boolean; }; }; friendCode: { @@ -267,11 +325,19 @@ export interface CurrentUser { }; }; permissions: { + playLog: PlayLogPermissions; presence: PresencePermissions; + friendRequestReception: boolean; }; - presence: Presence; + presence: PresenceOnline_4 | PresenceOffline; } +export enum PlayLogPermissions { + EVERYONE = 'EVERYONE', + FRIENDS = 'FRIENDS', + FAVORITE_FRIENDS = 'FAVORITE_FRIENDS', + SELF = 'SELF', +} export enum PresencePermissions { FRIENDS = 'FRIENDS', FAVORITE_FRIENDS = 'FAVORITE_FRIENDS', @@ -282,7 +348,9 @@ export enum PresencePermissions { export interface CurrentUserPermissions { etag: string; permissions: { + playLog: PlayLogPermissions; presence: PresencePermissions; + friendRequestReception: boolean; }; } @@ -295,3 +363,21 @@ export interface UpdateCurrentUserPermissionsParameter { }; etag: string; } + +/** /v4/User/PlayLog/Show */ +export type UserPlayLog = Game[]; + +/** /v4/FriendRequest/Received/List */ +export interface ReceivedFriendRequests { + friendRequests: unknown[]; +} + +/** /v4/FriendRequest/Sent/List */ +export interface SentFriendRequests { + friendRequests: unknown[]; +} + +/** /v3/User/Block/List */ +export interface BlockingUsers { + blockingUsers: unknown[]; +} diff --git a/src/app/browser/friend/index.tsx b/src/app/browser/friend/index.tsx index 00b8262..1d4ddd0 100644 --- a/src/app/browser/friend/index.tsx +++ b/src/app/browser/friend/index.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ActivityIndicator, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { CheckBox } from 'react-native-web'; -import { Friend, Game, Presence, PresencePermissions, PresenceState } from '../../../api/coral-types.js'; +import { Friend, Presence, PresenceGame, PresencePermissions, PresenceState } from '../../../api/coral-types.js'; import { getTitleIdFromEcUrl, hrduration } from '../../../util/misc.js'; import { Button } from '../components/index.js'; import { DEFAULT_ACCENT_COLOUR, TEXT_COLOUR_ACTIVE, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js'; @@ -174,7 +174,7 @@ function FriendPresence(props: { } function FriendPresenceGame(props: { - game: Game; + game: PresenceGame; }) { const theme = useColourScheme() === 'light' ? light : dark; const accent_colour = useAccentColour(); diff --git a/src/app/main/monitor.ts b/src/app/main/monitor.ts index fece6f0..1772c57 100644 --- a/src/app/main/monitor.ts +++ b/src/app/main/monitor.ts @@ -3,7 +3,7 @@ import { i18n } from 'i18next'; import { App } from './index.js'; import { showErrorDialog, tryGetNativeImageFromUrl } from './util.js'; import { DiscordPresenceConfiguration, DiscordPresenceExternalMonitorsConfiguration, DiscordPresenceSource, DiscordStatus } from '../common/types.js'; -import { CurrentUser, Friend, Game, CoralError } from '../../api/coral-types.js'; +import { CurrentUser, Friend, CoralError, PresenceGame } from '../../api/coral-types.js'; import { ErrorResponse } from '../../api/util.js'; import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../../common/presence.js'; import { NotificationManager } from '../../common/notify.js'; @@ -551,7 +551,7 @@ export class ElectronNotificationManager extends NotificationManager { } async onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; new Notification({ title: friend.name, @@ -570,7 +570,7 @@ export class ElectronNotificationManager extends NotificationManager { } async onFriendPlayingChangeTitle(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; new Notification({ title: friend.name, @@ -581,7 +581,7 @@ export class ElectronNotificationManager extends NotificationManager { } async onFriendTitleStateChange(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; new Notification({ title: friend.name, diff --git a/src/cli/nso/notify.ts b/src/cli/nso/notify.ts index 84d911b..57d12ed 100644 --- a/src/cli/nso/notify.ts +++ b/src/cli/nso/notify.ts @@ -7,7 +7,7 @@ import { initStorage } from '../../util/storage.js'; import { getToken } from '../../common/auth/coral.js'; import { getIksmToken } from '../../common/auth/splatnet2.js'; import { EmbeddedSplatNet2Monitor, NotificationManager, ZncNotifications } from '../../common/notify.js'; -import { CurrentUser, Friend, Game } from '../../api/coral-types.js'; +import { CurrentUser, Friend, PresenceGame } from '../../api/coral-types.js'; const debug = createDebug('cli:nso:notify'); @@ -133,7 +133,7 @@ export class TerminalNotificationManager extends NotificationManager { } onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; this.notifier.notify({ title: friend.name, @@ -153,7 +153,7 @@ export class TerminalNotificationManager extends NotificationManager { } onFriendPlayingChangeTitle(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; this.notifier.notify({ title: friend.name, @@ -165,7 +165,7 @@ export class TerminalNotificationManager extends NotificationManager { } onFriendTitleStateChange(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) { - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; this.notifier.notify({ title: friend.name, diff --git a/src/cli/util/discord-activity.ts b/src/cli/util/discord-activity.ts index e55b281..2d617e3 100644 --- a/src/cli/util/discord-activity.ts +++ b/src/cli/util/discord-activity.ts @@ -1,7 +1,7 @@ import process from 'node:process'; import { fetch } from 'undici'; import { getPresenceFromUrl } from '../../api/znc-proxy.js'; -import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState } from '../../api/coral-types.js'; +import { ActiveEvent, CurrentUser, Friend, Presence, PresenceGame, PresenceState } from '../../api/coral-types.js'; import type { Arguments as ParentArguments } from './index.js'; import { getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js'; import { DiscordPresenceContext, DiscordPresencePlayTime } from '../../discord/types.js'; @@ -122,7 +122,7 @@ async function getPresenceFromJson(json: string) { data.state === 'INACTIVE' ? PresenceState.INACTIVE : PresenceState.OFFLINE; - const game: Game | null = data.game && 'name' in data.game ? { + const game: PresenceGame | null = data.game && 'name' in data.game ? { name: typeof data.game.name === 'string' ? data.game.name : 'undefined', imageUri: typeof data.game.imageUri === 'string' ? data.game.imageUrl : null, shopUri: typeof data.game.shopUri === 'string' ? data.game.shopUri : null, diff --git a/src/common/auth/util.ts b/src/common/auth/util.ts index f2fb09c..fa7c02e 100644 --- a/src/common/auth/util.ts +++ b/src/common/auth/util.ts @@ -119,7 +119,7 @@ export class RateLimitError extends Error implements HasErrorDescription { export function checkMembershipActive(data: SavedToken) { const membership = data.nsoAccount.user.links.nintendoAccount.membership; - const active = typeof membership.active === 'object' ? membership.active.active : membership.active; + const active = typeof membership.active === 'object' ? (membership.active as typeof membership).active : membership.active; if (!active) throw new MembershipRequiredError('Nintendo Switch Online membership required'); } diff --git a/src/common/notify.ts b/src/common/notify.ts index b2a8b98..5ce62ba 100644 --- a/src/common/notify.ts +++ b/src/common/notify.ts @@ -1,6 +1,6 @@ import persist from 'node-persist'; import { CoralApiInterface } from '../api/coral.js'; -import { ActiveEvent, Announcements, CurrentUser, Friend, Game, Presence, PresenceState, WebServices, CoralError, GetActiveEventResult } from '../api/coral-types.js'; +import { ActiveEvent, Announcements, CurrentUser, Friend, Presence, PresenceState, WebServices, CoralError, GetActiveEventResult, FriendRouteChannel, PresenceGame } from '../api/coral-types.js'; import ZncProxyApi from '../api/znc-proxy.js'; import { ErrorResponse } from '../api/util.js'; import { SavedToken } from './auth/coral.js'; @@ -71,11 +71,20 @@ export class ZncNotifications extends Loop { id: 0, nsaId: r.friend, imageUri: '', + image2Uri: '', name: '', isFriend: true, isFavoriteFriend: false, isServiceUser: false, + isNew: false, friendCreatedAt: 0, + route: { + appName: '', + userName: '', + shopUri: '', + imageUri: '', + channel: FriendRouteChannel.FRIEND_CODE, + }, presence: await nso.fetch('/friend/' + r.friend + '/presence'), }; @@ -145,7 +154,7 @@ export class ZncNotifications extends Loop { const monitor = this.splatnet2_monitors.get(nsa_id); if (playing && monitor) { - const currenttitle = presence.game as Game; + const currenttitle = presence.game as PresenceGame; const titleid = getTitleIdFromEcUrl(currenttitle.shopUri); if (titleid && EmbeddedSplatNet2Monitor.title_ids.includes(titleid)) { @@ -233,7 +242,7 @@ export class NotificationManager { type = PresenceEvent.STATE_CHANGE; callback = 'onFriendOnline'; - const currenttitle = friend.presence.game as Game; + const currenttitle = friend.presence.game as PresenceGame; debugFriends('%s is now online%s%s, title %s %s - played for %s since %s', friend.name, friend.presence.state === PresenceState.ONLINE ? '' : ' (' + friend.presence.state + ')', @@ -250,7 +259,7 @@ export class NotificationManager { type = PresenceEvent.STATE_CHANGE; callback = 'onFriendOffline'; - const lasttitle = lastpresence.game as Game; + const lasttitle = lastpresence.game as PresenceGame; debugFriends('%s is now offline%s, was playing title %s %s, logout time %s', friend.name, friend.presence.state !== PresenceState.OFFLINE ? ' (console still online)' : '', @@ -262,8 +271,8 @@ export class NotificationManager { } } else if (wasonline && online) { // Friend is still online - const lasttitle = lastpresence.game as Game; - const currenttitle = friend.presence.game as Game; + const lasttitle = lastpresence.game as PresenceGame; + const currenttitle = friend.presence.game as PresenceGame; if (getTitleIdFromEcUrl(lasttitle.shopUri) !== getTitleIdFromEcUrl(currenttitle.shopUri)) { // Friend is playing a different title diff --git a/src/common/presence-embed.ts b/src/common/presence-embed.ts index 22095e9..745c577 100644 --- a/src/common/presence-embed.ts +++ b/src/common/presence-embed.ts @@ -4,7 +4,7 @@ import { Request } from 'express'; import { CoopRule, FestVoteState, FriendOnlineState, StageScheduleResult } from 'splatnet3-types/splatnet3'; import { dir } from '../util/product.js'; import createDebug from '../util/debug.js'; -import { Game, PresenceState } from '../api/coral-types.js'; +import { PresenceGame, PresenceState } from '../api/coral-types.js'; import { RawValueSymbol, htmlentities } from '../util/misc.js'; import { PresenceResponse } from '../cli/presence-server.js'; @@ -216,7 +216,7 @@ export function renderUserEmbedSvg( } function renderUserTitleEmbedPartialSvg( - game: Game, description: string | null | undefined, + game: PresenceGame, description: string | null | undefined, colours: PresenceEmbedThemeColours, font_family: string, ) { if (typeof description !== 'string') description = game.sysDescription; diff --git a/src/discord/util.ts b/src/discord/util.ts index 0686dcc..4d0039c 100644 --- a/src/discord/util.ts +++ b/src/discord/util.ts @@ -1,5 +1,5 @@ import DiscordRPC from 'discord-rpc'; -import { Game, PresenceState } from '../api/coral-types.js'; +import { PresenceGame, PresenceState } from '../api/coral-types.js'; import { defaultTitle, titles } from './titles.js'; import createDebug from '../util/debug.js'; import { product, version } from '../util/product.js'; @@ -9,7 +9,7 @@ import { DiscordPresence, DiscordPresenceContext, DiscordPresencePlayTime } from const debug = createDebug('nxapi:discord'); export function getDiscordPresence( - state: PresenceState, game: Game, context?: DiscordPresenceContext + state: PresenceState, game: PresenceGame, context?: DiscordPresenceContext ): DiscordPresence { const titleid = getTitleIdFromEcUrl(game.shopUri); const title = titles.find(t => t.id === titleid) || defaultTitle; @@ -80,7 +80,7 @@ export function getDiscordPresence( }; } -function getPlayTimeText(type: DiscordPresencePlayTime, game: Game) { +function getPlayTimeText(type: DiscordPresencePlayTime, game: PresenceGame) { if (type === DiscordPresencePlayTime.NINTENDO) { const days = Math.floor(Date.now() / 1000 / 86400) - Math.floor(game.firstPlayedAt / 86400); if (days <= 10) return getFirstPlayedText(game.firstPlayedAt); @@ -154,7 +154,7 @@ export function getInactiveDiscordPresence( }; } -export function getTitleConfiguration(game: Game, id: string) { +export function getTitleConfiguration(game: PresenceGame, id: string) { return titles.find(title => { if (title.id !== id) return false;