mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-10-24 18:45:55 -05:00
Initial commit
This commit is contained in:
61
src/cli/announcements.ts
Normal file
61
src/cli/announcements.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:announcements');
|
||||
|
||||
export const command = 'announcements';
|
||||
export const desc = 'List Nintendo Switch Online app announcements';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
console.log('Listing announcements');
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
const announcements = await nso.getAnnouncements();
|
||||
const friends = await nso.getFriendList();
|
||||
const webservices = await nso.getWebServices();
|
||||
const activeevent = await nso.getActiveEvent();
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'Priority',
|
||||
'Date',
|
||||
'Display end date',
|
||||
],
|
||||
});
|
||||
|
||||
for (const announcement of announcements.result) {
|
||||
table.push([
|
||||
announcement.announcementId,
|
||||
announcement.title.substr(0, 60),
|
||||
announcement.priority,
|
||||
new Date(announcement.distributionDate * 1000).toISOString(),
|
||||
new Date(announcement.forceDisplayEndDate * 1000).toISOString(),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
78
src/cli/friends.ts
Normal file
78
src/cli/friends.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import { PresenceState } from '../api/znc-types.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:friends');
|
||||
|
||||
export const command = 'friends';
|
||||
export const desc = 'List Nintendo Switch friends';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
console.log('Listing friends');
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
const announcements = await nso.getAnnouncements();
|
||||
const friends = await nso.getFriendList();
|
||||
const webservices = await nso.getWebServices();
|
||||
const activeevent = await nso.getActiveEvent();
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'NA ID',
|
||||
'Name',
|
||||
'Status',
|
||||
'Favourite?',
|
||||
'Added at',
|
||||
],
|
||||
});
|
||||
|
||||
for (const friend of friends.result.friends) {
|
||||
const online = friend.presence.state === PresenceState.ONLINE ||
|
||||
friend.presence.state === PresenceState.PLAYING;
|
||||
const hours = 'name' in friend.presence.game ? Math.floor(friend.presence.game.totalPlayTime / 60) : 0;
|
||||
const minutes = 'name' in friend.presence.game ? friend.presence.game.totalPlayTime - (hours * 60) : 0;
|
||||
|
||||
table.push([
|
||||
friend.id,
|
||||
friend.nsaId,
|
||||
friend.name,
|
||||
online ?
|
||||
'name' in friend.presence.game ?
|
||||
'Playing ' + friend.presence.game.name +
|
||||
'; played for ' + (hours || !minutes ? hours + ' hour' + (hours === 1 ? '' : 's') : '') +
|
||||
(minutes ? ', ' + minutes + ' minute' + (minutes === 1 ? '' : 's'): '') +
|
||||
' since ' + new Date(friend.presence.game.firstPlayedAt * 1000).toLocaleDateString('en-GB') :
|
||||
'Online' :
|
||||
friend.presence.logoutAt ?
|
||||
'Last seen ' + new Date(friend.presence.logoutAt * 1000).toISOString() :
|
||||
'Offline',
|
||||
friend.isFavoriteFriend ? 'Yes' : 'No',
|
||||
new Date(friend.friendCreatedAt * 1000).toISOString(),
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
9
src/cli/index.ts
Normal file
9
src/cli/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * as token from './token.js';
|
||||
export * as users from './users.js';
|
||||
export * as user from './user.js';
|
||||
export * as announcements from './announcements.js';
|
||||
export * as webservices from './webservices.js';
|
||||
export * as webservicetoken from './webservicetoken.js';
|
||||
export * as friends from './friends.js';
|
||||
export * as presence from './presence.js';
|
||||
export * as notify from './notify.js';
|
||||
220
src/cli/notify.ts
Normal file
220
src/cli/notify.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import notifier from 'node-notifier';
|
||||
import { CurrentUser, Friend, Game, PresenceState } from '../api/znc-types.js';
|
||||
import ZncApi from '../api/znc.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getTitleIdFromEcUrl, getToken, initStorage, SavedToken, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:notify');
|
||||
const debugFriends = createDebug('cli:notify:friends');
|
||||
|
||||
export const command = 'notify';
|
||||
export const desc = 'Show notifications when friends come online without starting Discord Rich Presence';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('user-notifications', {
|
||||
describe: 'Show notification for your own user',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('friend-notifications', {
|
||||
describe: 'Show notification for friends',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
}).option('update-interval', {
|
||||
describe: 'Update interval in seconds',
|
||||
type: 'number',
|
||||
default: 30,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
if (!argv.userNotifications && !argv.friendNotifications) {
|
||||
throw new Error('Must enable either user or friend notifications');
|
||||
}
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
const i = new ZncNotifications(argv, storage, token, nso, data);
|
||||
|
||||
console.log('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
|
||||
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
|
||||
|
||||
await i.init();
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
}
|
||||
}
|
||||
|
||||
export class ZncNotifications {
|
||||
constructor(
|
||||
readonly argv: ArgumentsCamelCase<Arguments>,
|
||||
public storage: persist.LocalStorage,
|
||||
public token: string,
|
||||
public nso: ZncApi,
|
||||
public data: Omit<SavedToken, 'expires_at'>,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
const announcements = await this.nso.getAnnouncements();
|
||||
const friends = await this.nso.getFriendList();
|
||||
const webservices = await this.nso.getWebServices();
|
||||
const activeevent = await this.nso.getActiveEvent();
|
||||
|
||||
if (this.argv.userNotifications) {
|
||||
const user = await this.nso.getCurrentUser();
|
||||
|
||||
await this.updateFriendsStatusForNotifications(this.argv.friendNotifications ?
|
||||
[user.result, ...friends.result.friends] : [user.result]);
|
||||
} else {
|
||||
await this.updateFriendsStatusForNotifications(friends.result.friends);
|
||||
}
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.argv.updateInterval * 1000));
|
||||
}
|
||||
|
||||
onlinefriends: (CurrentUser | Friend)[] = [];
|
||||
|
||||
async updateFriendsStatusForNotifications(friends: (CurrentUser | Friend)[], initialRun?: boolean) {
|
||||
const newonlinefriends: (CurrentUser | Friend)[] = [];
|
||||
|
||||
for (const friend of friends) {
|
||||
const lastpresence = this.onlinefriends.find(f => f.id === friend.id)?.presence;
|
||||
const online = friend.presence.state === PresenceState.ONLINE ||
|
||||
friend.presence.state === PresenceState.PLAYING;
|
||||
|
||||
if (!lastpresence && online) {
|
||||
// Friend has come online
|
||||
const currenttitle = friend.presence.game as Game;
|
||||
|
||||
debugFriends('%s is now online, title %s %s', friend.name,
|
||||
currenttitle.name, JSON.stringify(currenttitle.sysDescription));
|
||||
|
||||
notifier.notify({
|
||||
title: friend.name,
|
||||
message: 'Playing ' + currenttitle.name +
|
||||
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
|
||||
icon: currenttitle.imageUri,
|
||||
});
|
||||
|
||||
newonlinefriends.push(friend);
|
||||
} else if (lastpresence && !online) {
|
||||
// Friend has gone offline
|
||||
const lasttitle = lastpresence.game as Game;
|
||||
|
||||
notifier.notify({
|
||||
title: friend.name,
|
||||
message: 'Offline',
|
||||
});
|
||||
|
||||
debugFriends('%s is now offline, was playing title %s %s', friend.name,
|
||||
lasttitle.name, JSON.stringify(lasttitle.sysDescription));
|
||||
} else if (lastpresence && online) {
|
||||
// Friend is still online
|
||||
const lasttitle = lastpresence.game as Game;
|
||||
const currenttitle = friend.presence.game as Game;
|
||||
|
||||
if (getTitleIdFromEcUrl(lasttitle.shopUri) !== getTitleIdFromEcUrl(currenttitle.shopUri)) {
|
||||
// Friend is playing a different title
|
||||
|
||||
debugFriends('%s is now playing %s %s, was playing %s %s',
|
||||
friend.name,
|
||||
currenttitle.name, JSON.stringify(currenttitle.sysDescription),
|
||||
lasttitle.name, JSON.stringify(lasttitle.sysDescription));
|
||||
|
||||
notifier.notify({
|
||||
title: friend.name,
|
||||
message: 'Playing ' + currenttitle.name +
|
||||
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
|
||||
icon: currenttitle.imageUri,
|
||||
});
|
||||
} else if (
|
||||
lastpresence.state !== friend.presence.state ||
|
||||
lasttitle.sysDescription !== currenttitle.sysDescription
|
||||
) {
|
||||
// Title state changed
|
||||
|
||||
debugFriends('%s title %s state changed, now %s %s, was %s %s',
|
||||
friend.name, currenttitle.name,
|
||||
friend.presence.state, JSON.stringify(currenttitle.sysDescription),
|
||||
lastpresence.state, JSON.stringify(lasttitle.sysDescription));
|
||||
|
||||
notifier.notify({
|
||||
title: friend.name,
|
||||
message: 'Playing ' + currenttitle.name +
|
||||
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
|
||||
icon: currenttitle.imageUri,
|
||||
});
|
||||
}
|
||||
|
||||
newonlinefriends.push(friend);
|
||||
}
|
||||
}
|
||||
|
||||
this.onlinefriends = newonlinefriends;
|
||||
}
|
||||
|
||||
async update() {
|
||||
debug('Updating presence');
|
||||
|
||||
if (this.argv.friendNotifications) {
|
||||
const activeevent = await this.nso.getActiveEvent();
|
||||
const friends = await this.nso.getFriendList();
|
||||
const webservices = await this.nso.getWebServices();
|
||||
|
||||
if (this.argv.userNotifications) {
|
||||
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');
|
||||
}
|
||||
|
||||
async loop() {
|
||||
try {
|
||||
await this.update();
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.argv.updateInterval * 1000));
|
||||
} catch (err) {
|
||||
// @ts-expect-error
|
||||
if (err?.data?.status === 9404) {
|
||||
// Token expired
|
||||
debug('Renewing token');
|
||||
|
||||
const data = await this.nso.renewToken(this.token);
|
||||
|
||||
const existingToken: SavedToken = {
|
||||
...data,
|
||||
expires_at: Date.now() + (data.credential.expiresIn * 1000),
|
||||
};
|
||||
|
||||
await this.storage.setItem('NsoToken.' + this.token, existingToken);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
277
src/cli/presence.ts
Normal file
277
src/cli/presence.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import DiscordRPC from 'discord-rpc';
|
||||
import { CurrentUser, Presence, PresenceState } from '../api/znc-types.js';
|
||||
import ZncApi from '../api/znc.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getDiscordPresence, getTitleIdFromEcUrl, getToken, initStorage, SavedToken, YargsArguments } from '../util.js';
|
||||
import { ZncNotifications } from './notify.js';
|
||||
|
||||
const debug = createDebug('cli:presence');
|
||||
const debugFriends = createDebug('cli:presence:friends');
|
||||
const debugDiscord = createDebug('cli:presence:discordrpc');
|
||||
|
||||
export const command = 'presence';
|
||||
export const desc = 'Start Discord Rich Presence';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('friend-naid', {
|
||||
describe: 'Friend\'s Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('friend-code', {
|
||||
describe: 'Friend code',
|
||||
type: 'string',
|
||||
}).option('user-notifications', {
|
||||
describe: 'Show notification for your own user',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('friend-notifications', {
|
||||
describe: 'Show notification for friends',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('update-interval', {
|
||||
describe: 'Update interval in seconds',
|
||||
type: 'number',
|
||||
default: 30,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
const i = new ZncDiscordPresence(argv, storage, token, nso, data);
|
||||
|
||||
console.log('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
|
||||
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
|
||||
|
||||
await i.init();
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
}
|
||||
}
|
||||
|
||||
class ZncDiscordPresence extends ZncNotifications {
|
||||
forceFriendCode: CurrentUser['links']['friendCode'] | undefined;
|
||||
|
||||
constructor(
|
||||
readonly argv: ArgumentsCamelCase<Arguments>,
|
||||
storage: persist.LocalStorage,
|
||||
token: string,
|
||||
nso: ZncApi,
|
||||
data: Omit<SavedToken, 'expires_at'>,
|
||||
) {
|
||||
super(argv, storage, token, nso, data);
|
||||
|
||||
let match;
|
||||
this.forceFriendCode =
|
||||
(match = (this.argv.friendCode as string)?.match(/^(SW-)?(\d{4})-?(\d{4})-?(\d{4})$/)) ?
|
||||
{id: match[2] + '-' + match[3] + '-' + match[4], regenerable: false, regenerableAt: 0} : undefined;
|
||||
}
|
||||
|
||||
async init() {
|
||||
const announcements = await this.nso.getAnnouncements();
|
||||
const friends = await this.nso.getFriendList();
|
||||
const webservices = await this.nso.getWebServices();
|
||||
const activeevent = await this.nso.getActiveEvent();
|
||||
|
||||
if (this.argv.friendNaid) {
|
||||
const friend = friends.result.friends.find(f => f.nsaId === this.argv.friendNaid);
|
||||
|
||||
if (!friend) {
|
||||
throw new Error('User "' + this.argv.friendNaid + '" is not friends with this user');
|
||||
}
|
||||
|
||||
if (this.argv.userNotifications && this.argv.friendNotifications) {
|
||||
await this.updateFriendsStatusForNotifications(friends.result.friends, true);
|
||||
} else if (this.argv.friendNotifications) {
|
||||
await this.updateFriendsStatusForNotifications(
|
||||
friends.result.friends.filter(f => f.nsaId !== this.argv.friendNaid), true);
|
||||
} else if (this.argv.userNotifications && friend) {
|
||||
await this.updateFriendsStatusForNotifications([friend], true);
|
||||
}
|
||||
|
||||
await this.updatePresence(friend.presence);
|
||||
} else {
|
||||
const user = await this.nso.getCurrentUser();
|
||||
|
||||
if (this.argv.friendNotifications) {
|
||||
await this.updateFriendsStatusForNotifications(this.argv.userNotifications ?
|
||||
[user.result, ...friends.result.friends] : friends.result.friends);
|
||||
} else if (this.argv.userNotifications) {
|
||||
await this.updateFriendsStatusForNotifications([user.result]);
|
||||
}
|
||||
|
||||
await this.updatePresence(user.result.presence, user.result.links.friendCode);
|
||||
}
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.argv.updateInterval * 1000));
|
||||
}
|
||||
|
||||
rpc: {client: DiscordRPC.Client, id: string} | null = null;
|
||||
title: {id: string; since: number} | null = null;
|
||||
i = 0;
|
||||
|
||||
async updatePresence(presence: Presence | null, friendcode?: CurrentUser['links']['friendCode']) {
|
||||
debug('Presence %d state=%s, updatedAt=%s, logoutAt=%s', this.i++,
|
||||
presence?.state,
|
||||
new Date((presence?.updatedAt ?? 0) * 1000).toString(),
|
||||
new Date((presence?.logoutAt ?? 0) * 1000).toString());
|
||||
if (presence && 'name' in presence.game) {
|
||||
debug('Title %s, id=%s, totalPlayTime=%d, firstPlayedAt=%s, sysDescription=%s',
|
||||
presence.game.name,
|
||||
getTitleIdFromEcUrl(presence.game.shopUri),
|
||||
presence.game.totalPlayTime,
|
||||
new Date((presence.game.firstPlayedAt ?? 0) * 1000).toString(),
|
||||
JSON.stringify(presence.game.sysDescription));
|
||||
}
|
||||
|
||||
const online = presence?.state === PresenceState.ONLINE || presence?.state === PresenceState.PLAYING;
|
||||
|
||||
if (online && 'name' in presence.game) {
|
||||
const discordpresence = getDiscordPresence(presence.game,
|
||||
this.argv.friendCode === '' || this.argv.friendCode === '-' ? friendcode : this.forceFriendCode);
|
||||
|
||||
if (this.rpc && this.rpc.id !== discordpresence.id) {
|
||||
const client = this.rpc.client;
|
||||
this.rpc = null;
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
if (!this.rpc) {
|
||||
const client = new DiscordRPC.Client({transport: 'ipc'});
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (attempts === 0) debugDiscord('RPC connecting');
|
||||
else debugDiscord('RPC connecting, attempt %d', attempts + 1);
|
||||
|
||||
try {
|
||||
await client.connect(discordpresence.id);
|
||||
debugDiscord('RPC connected');
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
||||
attempts++;
|
||||
await new Promise(rs => setTimeout(rs, 5000));
|
||||
}
|
||||
|
||||
if (!connected) throw new Error('Failed to connect to Discord');
|
||||
|
||||
// @ts-expect-error
|
||||
client.transport.on('close', async () => {
|
||||
if (this.rpc?.client !== client) return;
|
||||
|
||||
console.warn('[discordrpc] RPC client disconnected, attempting to reconnect');
|
||||
debugDiscord('RPC client disconnected');
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (this.rpc?.client !== client) return;
|
||||
|
||||
debugDiscord('RPC reconnecting, attempt %d', attempts + 1);
|
||||
try {
|
||||
await client.connect(discordpresence.id);
|
||||
console.warn('[discordrpc] RPC reconnected');
|
||||
debugDiscord('RPC reconnected');
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
||||
attempts++;
|
||||
await new Promise(rs => setTimeout(rs, 5000));
|
||||
}
|
||||
|
||||
if (!connected) throw new Error('Failed to reconnect to Discord');
|
||||
|
||||
throw new Error('Discord disconnected');
|
||||
});
|
||||
|
||||
this.rpc = {client, id: discordpresence.id};
|
||||
}
|
||||
|
||||
if (discordpresence.title) {
|
||||
if (discordpresence.title !== this.title?.id) {
|
||||
this.title = {id: discordpresence.title, since: Date.now()};
|
||||
}
|
||||
|
||||
if (discordpresence.showTimestamp) {
|
||||
discordpresence.presence.startTimestamp = this.title.since;
|
||||
}
|
||||
} else {
|
||||
this.title = null;
|
||||
}
|
||||
|
||||
this.rpc.client.setActivity(discordpresence.presence);
|
||||
}
|
||||
|
||||
if (!presence || !online || !('name' in presence.game)) {
|
||||
if (this.rpc) {
|
||||
const client = this.rpc.client;
|
||||
this.rpc = null;
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
this.title = null;
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
if (this.argv.friendNaid) {
|
||||
const activeevent = await this.nso.getActiveEvent();
|
||||
const friends = await this.nso.getFriendList();
|
||||
const webservices = await this.nso.getWebServices();
|
||||
|
||||
const friend = friends.result.friends.find(f => f.nsaId === this.argv.friendNaid);
|
||||
|
||||
if (this.argv.userNotifications && this.argv.friendNotifications) {
|
||||
await this.updateFriendsStatusForNotifications(friends.result.friends);
|
||||
} else if (this.argv.friendNotifications) {
|
||||
await this.updateFriendsStatusForNotifications(
|
||||
friends.result.friends.filter(f => f.nsaId !== this.argv.friendNaid));
|
||||
} else if (this.argv.userNotifications && friend) {
|
||||
await this.updateFriendsStatusForNotifications([friend]);
|
||||
}
|
||||
|
||||
if (!friend) {
|
||||
// Is the authenticated user no longer friends with this user?
|
||||
await this.updatePresence(null);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updatePresence(friend.presence);
|
||||
} else {
|
||||
const user = await this.nso.getCurrentUser();
|
||||
|
||||
if (this.argv.friendNotifications) {
|
||||
const activeevent = await this.nso.getActiveEvent();
|
||||
const friends = await this.nso.getFriendList();
|
||||
const webservices = await this.nso.getWebServices();
|
||||
|
||||
await this.updateFriendsStatusForNotifications(this.argv.userNotifications ?
|
||||
[user.result, ...friends.result.friends] : friends.result.friends);
|
||||
} else if (this.argv.userNotifications) {
|
||||
await this.updateFriendsStatusForNotifications([user.result]);
|
||||
}
|
||||
|
||||
await this.updatePresence(user.result.presence, user.result.links.friendCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/cli/token.ts
Normal file
64
src/cli/token.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as util from 'util';
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:token');
|
||||
|
||||
export const command = 'token [token]';
|
||||
export const desc = 'Set the default Nintendo Account session token';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('token', {
|
||||
describe: 'Nintendo Account session token (it is recommended this is not set and you enter it interactively)',
|
||||
type: 'string',
|
||||
}).option('auth', {
|
||||
describe: 'Authenticate immediately',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
}).option('select', {
|
||||
describe: 'Set as default user (default: true if only user)',
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
if (!argv.token) {
|
||||
const read = await import('read');
|
||||
// @ts-expect-error
|
||||
const prompt = util.promisify(read.default as typeof read);
|
||||
|
||||
argv.token = await prompt({
|
||||
prompt: `Token: `,
|
||||
silent: true,
|
||||
output: process.stderr,
|
||||
});
|
||||
}
|
||||
|
||||
await storage.setItem('SessionToken', argv.token);
|
||||
|
||||
if (argv.auth) {
|
||||
const {nso, data} = await getToken(storage, argv.token);
|
||||
|
||||
console.log('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
|
||||
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
|
||||
|
||||
await storage.setItem('NintendoAccountToken.' + data.user.id, argv.token);
|
||||
|
||||
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.add(data.user.id);
|
||||
await storage.setItem('NintendoAccountIds', [...users]);
|
||||
|
||||
if ('select' in argv ? argv.select : users.size === 1) {
|
||||
await storage.setItem('SelectedUser', data.user.id);
|
||||
|
||||
console.log('Set as default user');
|
||||
}
|
||||
} else {
|
||||
console.log('Saved token');
|
||||
}
|
||||
}
|
||||
44
src/cli/user.ts
Normal file
44
src/cli/user.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:user');
|
||||
|
||||
export const command = 'user';
|
||||
export const desc = 'Get the authenticated Nintendo Account';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
}).option('force-refresh', {
|
||||
describe: 'Always fetch Nintendo Switch user data (not including Nintendo Account user data)',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
if (argv.forceRefresh && 'expires_at' in data) {
|
||||
const user = await nso.getCurrentUser();
|
||||
|
||||
console.log('Nintendo Account', data.user);
|
||||
console.log('Nintendo Switch user', user.result);
|
||||
} else {
|
||||
console.log('Nintendo Account', data.user);
|
||||
console.log('Nintendo Switch user', data.nsoAccount.user);
|
||||
}
|
||||
}
|
||||
95
src/cli/users.ts
Normal file
95
src/cli/users.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { Argv, initStorage, SavedToken } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:users');
|
||||
|
||||
export const command = 'users <command>';
|
||||
export const desc = 'Manage authenticated Nintendo Accounts';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.command('list', 'Lists known Nintendo Accounts', () => {}, async argv => {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const users: string[] | undefined = await storage.getItem('NintendoAccountIds');
|
||||
const selected: string | undefined = await storage.getItem('SelectedUser');
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Screen name',
|
||||
'Nickname',
|
||||
'Country',
|
||||
'Nintendo Switch username',
|
||||
],
|
||||
});
|
||||
|
||||
for (const userid of users ?? []) {
|
||||
const token: string | undefined = await storage.getItem('NintendoAccountToken.' + userid);
|
||||
if (!token) continue;
|
||||
const cache: SavedToken | undefined = await storage.getItem('NsoToken.' + token);
|
||||
if (!cache) continue;
|
||||
|
||||
table.push([
|
||||
cache.user.id + (selected === cache.user.id ? ' *' : ''),
|
||||
cache.user.screenName,
|
||||
cache.user.nickname,
|
||||
cache.user.country,
|
||||
cache.nsoAccount.user.name,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!table.length) {
|
||||
console.log('No Nintendo Accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}).command('set <user>', 'Sets the default Nintendo Account', yargs => {
|
||||
return yargs.positional('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
});
|
||||
}, async argv => {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const token: string | undefined = await storage.getItem('NintendoAccountToken.' + argv.user);
|
||||
|
||||
if (!token) {
|
||||
console.error('No session token for this user. Set a Nintendo Account session token by running `nintendo-znc token --select`.');
|
||||
throw new Error('Unknown user');
|
||||
}
|
||||
|
||||
await storage.setItem('SelectedUser', argv.user);
|
||||
}).command('forget <user>', 'Removes all data for a Nintendo Account', yargs => {
|
||||
return yargs.positional('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
});
|
||||
}, async argv => {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const selected: string | undefined = await storage.getItem('SelectedUser');
|
||||
const token: string | undefined = await storage.getItem('NintendoAccountToken.' + argv.user);
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Unknown user');
|
||||
}
|
||||
|
||||
if (selected === argv.user) {
|
||||
await storage.removeItem('SelectedUser');
|
||||
await storage.removeItem('SessionToken');
|
||||
}
|
||||
|
||||
await storage.removeItem('NintendoAccountToken.' + argv.user);
|
||||
await storage.removeItem('NsoToken.' + token);
|
||||
|
||||
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
|
||||
users.delete(argv.user);
|
||||
await storage.setItem('NintendoAccountIds', [...users]);
|
||||
});
|
||||
}
|
||||
57
src/cli/webservices.ts
Normal file
57
src/cli/webservices.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import createDebug from 'debug';
|
||||
// @ts-expect-error
|
||||
import Table from 'cli-table/lib/index.js';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:announcements');
|
||||
|
||||
export const command = 'webservices';
|
||||
export const desc = 'List Nintendo Switch Online web services';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
console.log('Listing web services');
|
||||
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
const announcements = await nso.getAnnouncements();
|
||||
const friends = await nso.getFriendList();
|
||||
const webservices = await nso.getWebServices();
|
||||
const activeevent = await nso.getActiveEvent();
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Name',
|
||||
'URL',
|
||||
],
|
||||
});
|
||||
|
||||
for (const webservice of webservices.result) {
|
||||
table.push([
|
||||
webservice.id,
|
||||
webservice.name,
|
||||
webservice.uri,
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
61
src/cli/webservicetoken.ts
Normal file
61
src/cli/webservicetoken.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import createDebug from 'debug';
|
||||
import fetch from 'node-fetch';
|
||||
import type { Arguments as ParentArguments } from '../cli.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
|
||||
|
||||
const debug = createDebug('cli:announcements');
|
||||
|
||||
export const command = 'webservicetoken <id>';
|
||||
export const desc = 'Get a token for a web service';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('id', {
|
||||
describe: 'Web service ID',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
}).option('token', {
|
||||
describe: 'Nintendo Account session token',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
||||
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
|
||||
const token: string = argv.token ||
|
||||
await storage.getItem('NintendoAccountToken.' + usernsid) ||
|
||||
await storage.getItem('SessionToken');
|
||||
const {nso, data} = await getToken(storage, token);
|
||||
|
||||
const announcements = await nso.getAnnouncements();
|
||||
const friends = await nso.getFriendList();
|
||||
const webservices = await nso.getWebServices();
|
||||
const activeevent = await nso.getActiveEvent();
|
||||
|
||||
const webservice = webservices.result.find(w => '' + w.id === argv.id);
|
||||
|
||||
if (!webservice) {
|
||||
throw new Error('Invalid web service');
|
||||
}
|
||||
|
||||
const webserviceToken = await nso.getWebServiceToken(argv.id, data.credential.accessToken);
|
||||
|
||||
// https://app.splatoon2.nintendo.net/?lang=en-GB&na_country=GB&na_lang=en-GB
|
||||
const url = new URL(webservice.uri);
|
||||
url.search = new URLSearchParams({
|
||||
lang: data.user.language,
|
||||
na_country: data.user.country,
|
||||
na_lang: data.user.language,
|
||||
}).toString();
|
||||
|
||||
console.log('Web service', {
|
||||
name: webservice.name,
|
||||
url: url.toString(),
|
||||
}, webserviceToken.result);
|
||||
}
|
||||
Reference in New Issue
Block a user