mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-24 06:56:54 -05:00
Add an option to use a specific Discord client
This commit is contained in:
parent
b531fcced4
commit
4bc27e3ca9
|
|
@ -58,6 +58,9 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
describe: 'Stay connected to Discord while not playing',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
}).option('discord-user', {
|
||||
describe: 'Discord user ID (choose which Discord client to use when multiple are available)',
|
||||
type: 'string',
|
||||
}).option('splatnet2-monitor', {
|
||||
describe: 'Download new SplatNet 2 data when you are playing Splatoon 2 online',
|
||||
type: 'boolean',
|
||||
|
|
@ -142,6 +145,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
DiscordPresencePlayTime.DETAILED_PLAY_TIME_SINCE;
|
||||
|
||||
i.discord_preconnect = argv.discordPreconnect;
|
||||
if (argv.discordUser) i.discord_client_filter = (client, id) => client.user.id === argv.discordUser;
|
||||
|
||||
if (argv.splatnet2Monitor) {
|
||||
const storage = await initStorage(argv.dataPath);
|
||||
|
|
@ -208,6 +212,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
|
||||
i.presence_user = argv.friendNsaid ?? data?.nsoAccount.user.nsaId;
|
||||
i.discord_preconnect = argv.discordPreconnect;
|
||||
if (argv.discordUser) i.discord_client_filter = (client, id) => client.user.id === argv.discordUser;
|
||||
|
||||
console.warn('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
|
||||
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
|
||||
|
|
|
|||
43
src/cli/util/discord-rpc.ts
Normal file
43
src/cli/util/discord-rpc.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import createDebug from 'debug';
|
||||
import type { Arguments as ParentArguments } from '../../cli.js';
|
||||
import { DiscordRpcClient, getAllIpcSockets } from '../../discord/rpc.js';
|
||||
import { defaultTitle } from '../../discord/titles.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util.js';
|
||||
|
||||
const debug = createDebug('cli:util:discord-rpc');
|
||||
debug.enabled = true;
|
||||
|
||||
export const command = 'discord-rpc';
|
||||
export const desc = 'Search for Discord IPC sockets';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs;
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
const CLIENT_ID = defaultTitle.client;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const sockets = await getAllIpcSockets();
|
||||
|
||||
debug('Found %d Discord IPC sockets', sockets.length, sockets.map(s => s[0]));
|
||||
|
||||
for (const [id, socket] of sockets) {
|
||||
const client = new DiscordRpcClient({ transport: 'ipc', ipc_socket: socket });
|
||||
|
||||
await client.connect(CLIENT_ID);
|
||||
debug('[%d] Connected', id);
|
||||
|
||||
if (client.application) {
|
||||
debug('[%d] Application', id, client.application);
|
||||
}
|
||||
if (client.user) {
|
||||
debug('[%d] User', id, client.user);
|
||||
debug('[%d] User avatar', id,
|
||||
'https://cdn.discordapp.com/avatars/' + client.user.id + '/' + client.user.avatar + '.png');
|
||||
}
|
||||
|
||||
await client.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,3 +2,4 @@ export * as captureid from './captureid.js';
|
|||
export * as validateDiscordTitles from './validate-discord-titles.js';
|
||||
export * as exportDiscordTitles from './export-discord-titles.js';
|
||||
export * as discordActivity from './discord-activity.js';
|
||||
export * as discordRpc from './discord-rpc.js';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import createDebug from 'debug';
|
||||
import DiscordRPC from 'discord-rpc';
|
||||
import { ActiveEvent, CurrentUser, Friend, Presence, PresenceState, ZncErrorResponse } from '../api/znc-types.js';
|
||||
import { Loop, LoopResult } from '../util.js';
|
||||
import { DiscordRpcClient, findDiscordRpcClient } from '../discord/rpc.js';
|
||||
import { DiscordPresencePlayTime, DiscordPresenceContext, getDiscordPresence, getInactiveDiscordPresence } from '../discord/util.js';
|
||||
import { ZncNotifications } from './notify.js';
|
||||
import { ErrorResponse } from '../api/util.js';
|
||||
import { LoopResult } from '../util.js';
|
||||
import { getPresenceFromUrl } from '../api/znc-proxy.js';
|
||||
import { ActiveEvent, CurrentUser, Friend, Presence, PresenceState, ZncErrorResponse } from '../api/znc-types.js';
|
||||
import { ErrorResponse } from '../api/util.js';
|
||||
|
||||
const debug = createDebug('nxapi:nso:presence');
|
||||
const debugProxy = createDebug('nxapi:nso:presence:proxy');
|
||||
|
|
@ -56,7 +56,8 @@ export class ZncDiscordPresence extends ZncNotifications {
|
|||
return !!this.presence_user;
|
||||
}
|
||||
|
||||
rpc: {client: DiscordRPC.Client, id: string} | null = null;
|
||||
rpc: {client: DiscordRpcClient, id: string} | null = null;
|
||||
discord_client_filter: ((client: DiscordRpcClient, id?: number) => boolean) | undefined = undefined;
|
||||
title: {id: string; since: number} | null = null;
|
||||
i = 0;
|
||||
|
||||
|
|
@ -82,11 +83,17 @@ export class ZncDiscordPresence extends ZncNotifications {
|
|||
(online && 'name' in presence.game) ||
|
||||
(this.show_console_online && presence?.state === PresenceState.INACTIVE);
|
||||
|
||||
if (this.rpc && this.discord_client_filter && !this.discord_client_filter.call(null, this.rpc.client)) {
|
||||
const client = this.rpc.client;
|
||||
this.rpc = null;
|
||||
await client.destroy();
|
||||
}
|
||||
|
||||
if (!presence || !show_presence) {
|
||||
if (this.presence_enabled && this.discord_preconnect && !this.rpc) {
|
||||
debugDiscord('No presence but Discord preconnect enabled - connecting');
|
||||
const discordpresence = getInactiveDiscordPresence(PresenceState.OFFLINE, 0);
|
||||
const client = await this.createDiscordClient(discordpresence.id);
|
||||
const client = await this.createDiscordClient(discordpresence.id, this.discord_client_filter);
|
||||
this.rpc = {client, id: discordpresence.id};
|
||||
return;
|
||||
}
|
||||
|
|
@ -124,7 +131,7 @@ export class ZncDiscordPresence extends ZncNotifications {
|
|||
}
|
||||
|
||||
if (!this.rpc) {
|
||||
const client = await this.createDiscordClient(discordpresence.id);
|
||||
const client = await this.createDiscordClient(discordpresence.id, this.discord_client_filter);
|
||||
this.rpc = {client, id: discordpresence.id};
|
||||
}
|
||||
|
||||
|
|
@ -145,19 +152,22 @@ export class ZncDiscordPresence extends ZncNotifications {
|
|||
this.update_presence_errors = 0;
|
||||
}
|
||||
|
||||
async createDiscordClient(clientid: string) {
|
||||
let client: DiscordRPC.Client;
|
||||
async createDiscordClient(
|
||||
clientid: string,
|
||||
filter = (client: DiscordRpcClient, id: number) => true
|
||||
) {
|
||||
let client: DiscordRpcClient;
|
||||
let attempts = 0;
|
||||
let connected = false;
|
||||
let id;
|
||||
|
||||
while (attempts < 10) {
|
||||
if (attempts === 0) debugDiscord('RPC connecting', clientid);
|
||||
else debugDiscord('RPC connecting, attempt %d', attempts + 1, clientid);
|
||||
|
||||
try {
|
||||
client = new DiscordRPC.Client({transport: 'ipc'});
|
||||
await client.connect(clientid);
|
||||
debugDiscord('RPC connected', clientid, client.application, client.user);
|
||||
[id, client] = await findDiscordRpcClient(clientid, filter);
|
||||
debugDiscord('RPC connected', id, clientid, client.application, client.user);
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {}
|
||||
|
|
@ -180,15 +190,13 @@ export class ZncDiscordPresence extends ZncNotifications {
|
|||
|
||||
debugDiscord('RPC reconnecting, attempt %d', attempts + 1, clientid);
|
||||
try {
|
||||
const newclient = new DiscordRPC.Client({transport: 'ipc'});
|
||||
await newclient.connect(clientid);
|
||||
debugDiscord('RPC reconnected', clientid, newclient.application, newclient.user);
|
||||
[id, client] = await findDiscordRpcClient(clientid, filter);
|
||||
debugDiscord('RPC reconnected', id, clientid, client.application, client.user);
|
||||
|
||||
// @ts-expect-error
|
||||
client.transport.on('close', reconnect);
|
||||
|
||||
this.rpc.client = newclient;
|
||||
client = newclient;
|
||||
this.rpc.client = client;
|
||||
connected = true;
|
||||
break;
|
||||
} catch (err) {
|
||||
|
|
|
|||
186
src/discord/rpc.ts
Normal file
186
src/discord/rpc.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import * as net from 'net';
|
||||
import { createRequire } from 'module';
|
||||
import createDebug from 'debug';
|
||||
import fetch from 'node-fetch';
|
||||
import DiscordRPC from 'discord-rpc';
|
||||
|
||||
const debug = createDebug('nxapi:discord:rpc');
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const BaseIpcTransport = require('discord-rpc/src/transports/ipc.js');
|
||||
|
||||
export async function getDiscordRpcClients() {
|
||||
const sockets = await getAllIpcSockets();
|
||||
|
||||
return sockets.map(s => new DiscordRpcClient({transport: 'ipc', ipc_socket: s[1]}));
|
||||
}
|
||||
|
||||
export async function findDiscordRpcClient(
|
||||
clientid: string, filter: (client: DiscordRpcClient, id: number) => boolean
|
||||
) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const socket = await getIpcSocket(i);
|
||||
if (!socket) continue;
|
||||
|
||||
const client = new DiscordRpcClient({transport: 'ipc', ipc_socket: socket});
|
||||
await client.connect(clientid);
|
||||
|
||||
try {
|
||||
if (filter.call(null, client, i)) return [i, client] as const;
|
||||
|
||||
await client.destroy();
|
||||
} catch (err) {
|
||||
await client.destroy();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to find a matching Discord client');
|
||||
}
|
||||
|
||||
//
|
||||
// Patches discord-rpc to allow using a specific socket.
|
||||
//
|
||||
|
||||
export class DiscordRpcClient extends DiscordRPC.Client {
|
||||
constructor(options?: DiscordRPC.RPCClientOptions & {
|
||||
ipc_socket?: net.Socket;
|
||||
}) {
|
||||
super({
|
||||
transport: 'ipc',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (options?.transport ?? 'ipc' === 'ipc') {
|
||||
// @ts-expect-error
|
||||
this.transport = new IpcTransport(this);
|
||||
// @ts-expect-error
|
||||
if (options?.ipc_socket) this.transport.socket = options?.ipc_socket;
|
||||
// @ts-expect-error
|
||||
this.transport.on('message', this._onRpcMessage.bind(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IpcTransport extends BaseIpcTransport {
|
||||
client!: DiscordRpcClient;
|
||||
socket!: net.Socket | null;
|
||||
|
||||
onClose!: () => void;
|
||||
|
||||
async connect() {
|
||||
const socket = this.socket = this.socket ?? await getIpc();
|
||||
socket.on('close', this.onClose.bind(this));
|
||||
socket.on('error', this.onClose.bind(this));
|
||||
this.emit('open');
|
||||
socket.write(BaseIpcTransport.encode(OPCode.HANDSHAKE, {
|
||||
v: 1,
|
||||
// @ts-expect-error
|
||||
client_id: this.client.clientId,
|
||||
}));
|
||||
socket.pause();
|
||||
socket.on('readable', () => {
|
||||
// @ts-expect-error
|
||||
BaseIpcTransport.decode(socket, ({ op, data }) => {
|
||||
switch (op) {
|
||||
case OPCode.PING:
|
||||
this.send(data, OPCode.PONG);
|
||||
break;
|
||||
case OPCode.FRAME:
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.cmd === 'AUTHORIZE' && data.evt !== 'ERROR') {
|
||||
findEndpoint().then(endpoint => {
|
||||
// @ts-expect-error
|
||||
this.client.request.endpoint = endpoint;
|
||||
}).catch(e => this.client.emit('error', e));
|
||||
}
|
||||
// a@ts-expect-error
|
||||
this.emit('message', data);
|
||||
break;
|
||||
case OPCode.CLOSE:
|
||||
// a@ts-expect-error
|
||||
this.emit('close', data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enum OPCode {
|
||||
HANDSHAKE = 0,
|
||||
FRAME = 1,
|
||||
CLOSE = 2,
|
||||
PING = 3,
|
||||
PONG = 4,
|
||||
}
|
||||
|
||||
function getIpcPath(id: number) {
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\?\\pipe\\discord-ipc-${id}`;
|
||||
}
|
||||
const { env: { XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP } } = process;
|
||||
const prefix = XDG_RUNTIME_DIR || TMPDIR || TMP || TEMP || '/tmp';
|
||||
return `${prefix.replace(/\/$/, '')}/discord-ipc-${id}`;
|
||||
}
|
||||
|
||||
export function getIpc(id = 0) {
|
||||
return new Promise<net.Socket>((resolve, reject) => {
|
||||
const path = getIpcPath(id);
|
||||
const onerror = () => {
|
||||
if (id < 10) {
|
||||
resolve(getIpc(id + 1));
|
||||
} else {
|
||||
reject(new Error('Could not connect'));
|
||||
}
|
||||
};
|
||||
const sock = net.createConnection(path, () => {
|
||||
sock.removeListener('error', onerror);
|
||||
resolve(sock);
|
||||
});
|
||||
sock.once('error', onerror);
|
||||
});
|
||||
}
|
||||
|
||||
export function getIpcSocket(id: number) {
|
||||
return new Promise<net.Socket | null>((resolve, reject) => {
|
||||
const path = getIpcPath(id);
|
||||
const onerror = () => resolve(null);
|
||||
const sock = net.createConnection(path, () => {
|
||||
sock.removeListener('error', onerror);
|
||||
resolve(sock);
|
||||
});
|
||||
sock.once('error', onerror);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAllIpcSockets() {
|
||||
const promises: Promise<[number, net.Socket | null]>[] = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(getIpcSocket(i).then(s => [i, s]));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(s => s.filter(s => s[1]) as [number, net.Socket][]);
|
||||
}
|
||||
|
||||
async function findEndpoint(tries = 0): Promise<string> {
|
||||
if (tries > 30) {
|
||||
throw new Error('Could not find endpoint');
|
||||
}
|
||||
const endpoint = `http://127.0.0.1:${6463 + (tries % 10)}`;
|
||||
try {
|
||||
const r = await fetch(endpoint);
|
||||
if (r.status === 404) {
|
||||
return endpoint;
|
||||
}
|
||||
return findEndpoint(tries + 1);
|
||||
} catch (e) {
|
||||
return findEndpoint(tries + 1);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user