From ac921e3f16e5c689d56908905bdf59ee8bc0bbba Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Wed, 6 Apr 2022 22:41:17 +0100 Subject: [PATCH] Add a command to show/update presence permissions and simplify notify/presence updates --- src/cli/nso/index.ts | 1 + src/cli/nso/notify.ts | 118 ++++++++++++++++++---------- src/cli/nso/permissions.ts | 61 +++++++++++++++ src/cli/nso/presence.ts | 152 ++++++++++--------------------------- 4 files changed, 183 insertions(+), 149 deletions(-) create mode 100644 src/cli/nso/permissions.ts diff --git a/src/cli/nso/index.ts b/src/cli/nso/index.ts index f2d858b..202df7f 100644 --- a/src/cli/nso/index.ts +++ b/src/cli/nso/index.ts @@ -1,6 +1,7 @@ export * as token from './token.js'; export * as auth from './auth.js'; export * as user from './user.js'; +export * as permissions from './permissions.js'; export * as announcements from './announcements.js'; export * as webservices from './webservices.js'; export * as webservicetoken from './webservicetoken.js'; diff --git a/src/cli/nso/notify.ts b/src/cli/nso/notify.ts index 95c76a8..7a0d0b2 100644 --- a/src/cli/nso/notify.ts +++ b/src/cli/nso/notify.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import persist from 'node-persist'; import notifier from 'node-notifier'; import * as path from 'path'; -import { CurrentUser, Friend, Game, Presence, PresenceState, ZncErrorResponse, ZncSuccessResponse } from '../../api/znc-types.js'; +import { ActiveEvent, Announcements, CurrentUser, Friend, Game, Presence, PresenceState, WebServices, ZncErrorResponse } from '../../api/znc-types.js'; import ZncApi from '../../api/znc.js'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, getTitleIdFromEcUrl, getToken, hrduration, initStorage, Loop, LoopResult, SavedToken, YargsArguments } from '../../util.js'; @@ -146,31 +146,77 @@ export class ZncNotifications extends Loop { this.update_interval = argv.updateInterval; } - async init() { - const announcements = await this.nso.getAnnouncements(); - const friends = this.friend_notifications || !(this.nso instanceof ZncProxyApi) ? - await this.nso.getFriendList() : {result: {friends: []}}; + async fetch(req: ( + 'announcements' | 'friends' | {friend: string; presence?: boolean} | 'webservices' | 'event' | 'user' | null + )[]) { + const result: Partial<{ + announcements: Announcements; + friends: Friend[]; + webservices: WebServices; + activeevent: ActiveEvent; + user: CurrentUser; + }> = {}; + + const friends = req.filter(r => typeof r === 'object' && r && 'friend' in r) as + {friend: string; presence?: boolean}[]; + if (!(this.nso instanceof ZncProxyApi)) { - const webservices = await this.nso.getWebServices(); - const activeevent = await this.nso.getActiveEvent(); + if (req.includes('announcements')) req.push('friends', 'webservices', 'event'); + if (req.includes('webservices')) req.push('friends', 'event'); + if (req.includes('event')) req.push('friends', 'webservices'); } - let user: ZncSuccessResponse | null = null; + if (req.includes('announcements')) { + result.announcements = (await this.nso.getAnnouncements()).result; + } + if (req.includes('friends') || (friends && !(this.nso instanceof ZncProxyApi))) { + result.friends = (await this.nso.getFriendList()).result.friends; + } else if (friends && this.nso instanceof ZncProxyApi) { + result.friends = await Promise.all(friends.map(async r => { + const nso = this.nso as unknown as ZncProxyApi; - if (this.user_notifications) { - user = await this.nso.getCurrentUser(); + if (r.presence) { + const friend: Friend = { + id: 0, + nsaId: r.friend, + imageUri: '', + name: '', + isFriend: true, + isFavoriteFriend: false, + isServiceUser: true, + friendCreatedAt: 0, + presence: await nso.fetch('/friend/' + r.friend + '/presence'), + }; - await this.updateFriendsStatusForNotifications(this.friend_notifications ? - [user.result, ...friends.result.friends] : [user.result]); - } else if (this.friend_notifications) { - await this.updateFriendsStatusForNotifications(friends.result.friends); + return friend; + } + + return (await nso.fetch<{friend: Friend}>('/friend/' + r.friend)).friend; + })) + } + if (req.includes('webservices')) { + result.webservices = (await this.nso.getWebServices()).result; + } + if (req.includes('event')) { + result.activeevent = (await this.nso.getActiveEvent()).result; + } + if (req.includes('user')) { + result.user = (await this.nso.getCurrentUser()).result; } - if (this.splatnet2_monitors.size) { - if (!user) user = await this.nso.getCurrentUser(); + return result; + } - await this.updatePresenceForSplatNet2Monitors([user.result]); - } + async init() { + const {friends, user} = await this.fetch([ + 'announcements', + this.user_notifications ? 'user' : null, + this.friend_notifications ? 'friends' : null, + this.splatnet2_monitors.size ? 'user' : null, + ]); + + await this.updatePresenceForNotifications(user, friends, true); + await this.updatePresenceForSplatNet2Monitors([user!]); await new Promise(rs => setTimeout(rs, this.update_interval * 1000)); } @@ -327,6 +373,14 @@ export class ZncNotifications extends Loop { this.onlinefriends = newonlinefriends; } + async updatePresenceForNotifications( + user: CurrentUser | undefined, friends: Friend[] | undefined, initialRun?: boolean + ) { + await this.updateFriendsStatusForNotifications(([] as (CurrentUser | Friend)[]) + .concat(this.user_notifications && user ? [user] : []) + .concat(this.friend_notifications && friends ? friends : []), initialRun); + } + async updatePresenceForSplatNet2Monitors(friends: (CurrentUser | Friend)[]) { for (const friend of friends) { await this.updatePresenceForSplatNet2Monitor(friend.presence, friend.nsaId, friend.name); @@ -362,28 +416,14 @@ export class ZncNotifications extends Loop { } async update() { - debug('Updating presence'); + const {friends, user} = await this.fetch([ + this.user_notifications ? 'user' : null, + this.friend_notifications ? 'friends' : null, + this.splatnet2_monitors.size ? 'user' : null, + ]); - if (this.friend_notifications) { - if (!(this.nso instanceof ZncProxyApi)) await this.nso.getActiveEvent(); - const friends = this.friend_notifications || !(this.nso instanceof ZncProxyApi) ? - await this.nso.getFriendList() : {result: {friends: []}}; - if (!(this.nso instanceof ZncProxyApi)) await this.nso.getWebServices(); - - if (this.user_notifications) { - const user = await this.nso.getCurrentUser(); - - await this.updateFriendsStatusForNotifications([user.result, ...friends.result.friends]); - } else { - await this.updateFriendsStatusForNotifications(friends.result.friends); - } - } else { - const user = await this.nso.getCurrentUser(); - - await this.updateFriendsStatusForNotifications([user.result]); - } - - debug('Updated presence'); + await this.updatePresenceForNotifications(user, friends); + await this.updatePresenceForSplatNet2Monitors([user!]); } async handleError(err: ErrorResponse | NodeJS.ErrnoException): Promise { diff --git a/src/cli/nso/permissions.ts b/src/cli/nso/permissions.ts new file mode 100644 index 0000000..df6219c --- /dev/null +++ b/src/cli/nso/permissions.ts @@ -0,0 +1,61 @@ +import createDebug from 'debug'; +import { PresencePermissions } from '../../api/znc-types.js'; +import type { Arguments as ParentArguments } from '../nso.js'; +import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; + +const debug = createDebug('cli:nso:permissions'); + +export const command = 'permissions'; +export const desc = 'Get or update Nintendo Switch presence permissions'; + +export function builder(yargs: Argv) { + return yargs.option('user', { + describe: 'Nintendo Account ID', + type: 'string', + }).option('token', { + describe: 'Nintendo Account session token', + type: 'string', + }).option('json', { + describe: 'Output raw JSON', + type: 'boolean', + }).option('json-pretty-print', { + describe: 'Output pretty-printed JSON', + type: 'boolean', + }).option('presence', { + describe: 'New presence permission', + type: 'string', + }); +} + +type Arguments = YargsArguments>; + +export async function handler(argv: ArgumentsCamelCase) { + if (argv.presence && !['FRIENDS', 'FAVORITE_FRIENDS', 'SELF'].includes(argv.presence)) { + throw new Error('Invalid permissions'); + } + + const storage = await initStorage(argv.dataPath); + + const usernsid = argv.user ?? await storage.getItem('SelectedUser'); + const token: string = argv.token || + await storage.getItem('NintendoAccountToken.' + usernsid); + const {nso, data} = await getToken(storage, token, argv.zncProxyUrl); + + const permissions = await nso.getCurrentUserPermissions(); + + if (argv.presence) { + await nso.updateCurrentUserPermissions(argv.presence as PresencePermissions, + permissions.result.permissions.presence, permissions.result.etag); + } else { + if (argv.jsonPrettyPrint) { + console.log(JSON.stringify(permissions.result, null, 4)); + return; + } + if (argv.json) { + console.log(JSON.stringify(permissions.result)); + return; + } + + console.log('Presence is visible to %s', permissions.result.permissions.presence); + } +} diff --git a/src/cli/nso/presence.ts b/src/cli/nso/presence.ts index 8d097c2..9763fdc 100644 --- a/src/cli/nso/presence.ts +++ b/src/cli/nso/presence.ts @@ -1,14 +1,14 @@ +import * as path from 'path'; import createDebug from 'debug'; import persist from 'node-persist'; import DiscordRPC from 'discord-rpc'; import fetch from 'node-fetch'; -import { CurrentUser, Friend, Presence, PresenceState, ZncErrorResponse, ZncSuccessResponse } from '../../api/znc-types.js'; +import { CurrentUser, Presence, PresenceState, ZncErrorResponse } from '../../api/znc-types.js'; import ZncApi from '../../api/znc.js'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, getToken, initStorage, LoopResult, SavedToken, YargsArguments } from '../../util.js'; import { DiscordPresenceContext, getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js'; import { handleEnableSplatNet2Monitoring, ZncNotifications } from './notify.js'; -import ZncProxyApi from '../../api/znc-proxy.js'; import { ErrorResponse } from '../../index.js'; const debug = createDebug('cli:nso:presence'); @@ -176,14 +176,16 @@ export async function handler(argv: ArgumentsCamelCase) { } export class ZncDiscordPresence extends ZncNotifications { + presence_user: string | null; show_friend_code = false; force_friend_code: CurrentUser['links']['friendCode'] | undefined = undefined; show_console_online = false; - friend_nsaid: string | null; - constructor( - argv: ArgumentsCamelCase, + argv: Pick, + 'userNotifications' | 'friendNotifications' | 'updateInterval' | + 'friendCode' | 'showInactivePresence' | 'friendNsaid' + >, storage: persist.LocalStorage, token: string, nso: ZncApi, @@ -198,66 +200,34 @@ export class ZncDiscordPresence extends ZncNotifications { this.show_friend_code = !!this.force_friend_code || argv.friendCode === '' || argv.friendCode === '-'; this.show_console_online = argv.showInactivePresence; - this.friend_nsaid = argv.friendNsaid ?? null; + this.presence_user = argv.friendNsaid ?? data.nsoAccount.user.nsaId; } async init() { - const announcements = await this.nso.getAnnouncements(); - const friends = this.friend_notifications || !(this.nso instanceof ZncProxyApi) ? - await this.nso.getFriendList() : {result: {friends: this.friend_nsaid ? [this.user_notifications ? - (await this.nso.fetch<{friend: Friend}>('/friend/' + this.friend_nsaid)).friend : { - id: 0, - nsaId: this.friend_nsaid, - imageUri: '', - name: '', - isFriend: true, - isFavoriteFriend: false, - isServiceUser: true, - friendCreatedAt: 0, - presence: await this.nso.fetch('/friend/' + this.friend_nsaid + '/presence'), - }] : []}}; - if (!(this.nso instanceof ZncProxyApi)) { - await this.nso.getWebServices(); - await this.nso.getActiveEvent(); - } + const {friends, user} = await this.fetch([ + 'announcements', + this.presence_user ? + this.presence_user === this.data.nsoAccount.user.nsaId ? 'user' : + {friend: this.presence_user, presence: true} : null, + this.user_notifications ? 'user' : null, + this.friend_notifications ? 'friends' : null, + this.splatnet2_monitors.size ? 'user' : null, + ]); - let user: ZncSuccessResponse | null = null; - - if (this.friend_nsaid) { - const friend = friends.result.friends.find(f => f.nsaId === this.friend_nsaid); + if (this.presence_user && this.presence_user !== this.data.nsoAccount.user.nsaId) { + const friend = friends!.find(f => f.nsaId === this.presence_user); if (!friend) { - throw new Error('User "' + this.friend_nsaid + '" is not friends with this user'); + throw new Error('User "' + this.presence_user + '" is not friends with this user'); } - if (this.user_notifications && this.friend_notifications) { - await this.updateFriendsStatusForNotifications(friends.result.friends, true); - } else if (this.friend_notifications) { - await this.updateFriendsStatusForNotifications( - friends.result.friends.filter(f => f.nsaId !== this.friend_nsaid), true); - } else if (this.user_notifications && friend) { - await this.updateFriendsStatusForNotifications([friend], true); - } - - await this.updatePresence(friend.presence); + await this.updatePresenceForDiscord(friend.presence); } else { - user = await this.nso.getCurrentUser(); - - if (this.friend_notifications) { - await this.updateFriendsStatusForNotifications(this.user_notifications ? - [user.result, ...friends.result.friends] : friends.result.friends); - } else if (this.user_notifications) { - await this.updateFriendsStatusForNotifications([user.result]); - } - - await this.updatePresence(user.result.presence, user.result.links.friendCode); + await this.updatePresenceForDiscord(user!.presence, user!.links.friendCode); } - if (this.splatnet2_monitors.size) { - if (!user) user = await this.nso.getCurrentUser(); - - await this.updatePresenceForSplatNet2Monitors([user.result]); - } + await this.updatePresenceForNotifications(user, friends); + await this.updatePresenceForSplatNet2Monitors([user!]); await new Promise(rs => setTimeout(rs, this.update_interval * 1000)); } @@ -269,7 +239,7 @@ export class ZncDiscordPresence extends ZncNotifications { last_presence: Presence | null = null; friendcode: CurrentUser['links']['friendCode'] | undefined = undefined; - async updatePresence(presence: Presence | null, friendcode?: CurrentUser['links']['friendCode']) { + async updatePresenceForDiscord(presence: Presence | null, friendcode?: CurrentUser['links']['friendCode']) { this.last_presence = presence; this.friendcode = friendcode; @@ -293,7 +263,7 @@ export class ZncDiscordPresence extends ZncNotifications { const presencecontext: DiscordPresenceContext = { friendcode: this.show_friend_code ? this.force_friend_code ?? friendcode : undefined, znc_discord_presence: this, - nsaid: this.friend_nsaid ?? this.data.nsoAccount.user.nsaId, + nsaid: this.presence_user!, }; const discordpresence = 'name' in presence.game ? getDiscordPresence(presence.state, presence.game, presencecontext) : @@ -376,68 +346,30 @@ export class ZncDiscordPresence extends ZncNotifications { } async update() { - let user: CurrentUser | null = null; + const {friends, user} = await this.fetch([ + this.presence_user ? + this.presence_user === this.data.nsoAccount.user.nsaId ? 'user' : + {friend: this.presence_user, presence: true} : null, + this.user_notifications ? 'user' : null, + this.friend_notifications ? 'friends' : null, + this.splatnet2_monitors.size ? 'user' : null, + ]); - if (this.friend_nsaid) { - if (!(this.nso instanceof ZncProxyApi)) await this.nso.getActiveEvent(); - const friends = this.friend_notifications || !(this.nso instanceof ZncProxyApi) ? - await this.nso.getFriendList() : {result: {friends: this.friend_nsaid ? [this.user_notifications ? - (await this.nso.fetch<{friend: Friend}>('/friend/' + this.friend_nsaid)).friend : { - id: 0, - nsaId: this.friend_nsaid, - imageUri: '', - name: '', - isFriend: true, - isFavoriteFriend: false, - isServiceUser: true, - friendCreatedAt: 0, - presence: await this.nso.fetch('/friend/' + this.friend_nsaid + '/presence'), - }] : []}}; - if (!(this.nso instanceof ZncProxyApi)) await this.nso.getWebServices(); - - const friend = friends.result.friends.find(f => f.nsaId === this.friend_nsaid); - - if (this.user_notifications && this.friend_notifications) { - await this.updateFriendsStatusForNotifications(friends.result.friends); - } else if (this.friend_notifications) { - await this.updateFriendsStatusForNotifications( - friends.result.friends.filter(f => f.nsaId !== this.friend_nsaid)); - } else if (this.user_notifications && friend) { - await this.updateFriendsStatusForNotifications([friend]); - } + if (this.presence_user && this.presence_user !== this.data.nsoAccount.user.nsaId) { + const friend = friends!.find(f => f.nsaId === this.presence_user); if (!friend) { // Is the authenticated user no longer friends with this user? - await this.updatePresence(null); + await this.updatePresenceForDiscord(null); } else { - await this.updatePresence(friend.presence); + await this.updatePresenceForDiscord(friend.presence); } } else { - user = !(this.nso instanceof ZncProxyApi) ? - (await this.nso.getCurrentUser()).result : { - ...this.data.nsoAccount.user, - presence: await this.nso.fetch('/user/presence'), - }; - - if (this.friend_notifications) { - if (!(this.nso instanceof ZncProxyApi)) await this.nso.getActiveEvent(); - const friends = await this.nso.getFriendList(); - if (!(this.nso instanceof ZncProxyApi)) await this.nso.getWebServices(); - - await this.updateFriendsStatusForNotifications(this.user_notifications ? - [user, ...friends.result.friends] : friends.result.friends); - } else if (this.user_notifications) { - await this.updateFriendsStatusForNotifications([user]); - } - - await this.updatePresence(user.presence, user.links.friendCode); + await this.updatePresenceForDiscord(user!.presence, user!.links.friendCode); } - if (this.splatnet2_monitors.size) { - if (!user) user = (await this.nso.getCurrentUser()).result; - - await this.updatePresenceForSplatNet2Monitors([user]); - } + await this.updatePresenceForNotifications(user, friends); + await this.updatePresenceForSplatNet2Monitors([user!]); } update_presence_errors = 0; @@ -484,7 +416,7 @@ export class ZncProxyDiscordPresence extends ZncDiscordPresence { } const presence = await response.json() as Presence; - await this.updatePresence(presence); + await this.updatePresenceForDiscord(presence); await this.updatePresenceForSplatNet2Monitor(presence, this.presence_url); } }