mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-07 18:15:51 -05:00
Show title name (details) under name in Discord
This commit is contained in:
parent
56086c0651
commit
be21d69a4c
|
|
@ -13,7 +13,7 @@ import { CurrentUser, Friend, Game, PresencePlatform, PresenceState, WebService
|
|||
import { NintendoAccountSessionTokenJwtPayload, NintendoAccountUser } from '../../api/na.js';
|
||||
import { DiscordPresence } from '../../discord/types.js';
|
||||
import { getDiscordRpcClients } from '../../discord/rpc.js';
|
||||
import { defaultTitle } from '../../discord/titles.js';
|
||||
import { default_client } from '../../discord/titles.js';
|
||||
import type { FriendProps } from '../browser/friend/index.js';
|
||||
import type { DiscordSetupProps } from '../browser/discord/index.js';
|
||||
import type { AddFriendProps } from '../browser/add-friend/index.js';
|
||||
|
|
@ -181,7 +181,7 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) {
|
|||
|
||||
for (const client of await getDiscordRpcClients()) {
|
||||
try {
|
||||
await client.connect(defaultTitle.client);
|
||||
await client.connect(default_client);
|
||||
if (client.user && !users.find(u => u.id === client.user!.id)) users.push(client.user);
|
||||
} finally {
|
||||
await client.destroy();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Arguments as ParentArguments } from './index.js';
|
||||
import { DiscordRpcClient, getAllIpcSockets } from '../../discord/rpc.js';
|
||||
import { defaultTitle } from '../../discord/titles.js';
|
||||
import { default_client } from '../../discord/titles.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
|
||||
|
|
@ -16,8 +16,6 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
const CLIENT_ID = defaultTitle.client;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const sockets = await getAllIpcSockets();
|
||||
|
||||
|
|
@ -26,7 +24,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
for (const [id, socket] of sockets) {
|
||||
const client = new DiscordRpcClient({ transport: 'ipc', ipc_socket: socket });
|
||||
|
||||
await client.connect(CLIENT_ID);
|
||||
await client.connect(default_client);
|
||||
debug('[%d] Connected', id);
|
||||
|
||||
if (client.application) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { fetch } from 'undici';
|
|||
import type { Arguments as ParentArguments } from './index.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { titles as unsorted_titles } from '../../discord/titles.js';
|
||||
import { default_client, titles as unsorted_titles } from '../../discord/titles.js';
|
||||
import { DiscordApplicationRpc, getDiscordApplicationRpc } from './discord-activity.js';
|
||||
import { Title } from '../../discord/types.js';
|
||||
|
||||
|
|
@ -153,13 +153,13 @@ async function getGroupedTitlesJson(exclude_discord_configuration = false, inclu
|
|||
}[] = [];
|
||||
|
||||
for (const title of titles) {
|
||||
let client = clients.find(c => c.id === title.client);
|
||||
let client = clients.find(c => c.id === (title.client ?? default_client));
|
||||
|
||||
if (!client) {
|
||||
const application = await getDiscordApplicationRpc(title.client);
|
||||
const application = await getDiscordApplicationRpc(title.client ?? default_client);
|
||||
|
||||
client = {
|
||||
id: title.client,
|
||||
id: title.client ?? default_client,
|
||||
application,
|
||||
titles: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
warn('Invalid title ID, must be lowercase hex', title.id);
|
||||
else warn('Invalid title ID', title.id);
|
||||
}
|
||||
if (!title.client.match(/^\d{16,}$/)) warn('Invalid Discord client ID', title.id, title.client);
|
||||
if (title.client && !title.client.match(/^\d{16,}$/)) warn('Invalid Discord client ID', title.id, title.client);
|
||||
|
||||
if (has_errors) continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,29 @@ export async function findDiscordRpcClient(
|
|||
// Patches discord-rpc to allow using a specific socket.
|
||||
//
|
||||
|
||||
export interface DiscordRpcClient {
|
||||
/**
|
||||
* Request
|
||||
* @param {string} cmd Command
|
||||
* @param {Object} [args={}] Arguments
|
||||
* @param {string} [evt] Event
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
request(cmd: string, args?: object, evt?: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
declare module 'discord-rpc' {
|
||||
interface Presence {
|
||||
name?: string;
|
||||
statusDisplayType?: DiscordApiActivityStatusDisplayType;
|
||||
stateUrl?: string;
|
||||
detailsUrl?: string;
|
||||
largeImageUrl?: string;
|
||||
smallImageUrl?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class DiscordRpcClient extends DiscordRPC.Client {
|
||||
constructor(options?: DiscordRPC.RPCClientOptions & {
|
||||
ipc_socket?: net.Socket;
|
||||
|
|
@ -74,6 +97,149 @@ export class DiscordRpcClient extends DiscordRPC.Client {
|
|||
this.transport.on('message', this._onRpcMessage.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
setActivity(args: DiscordRPC.Presence, pid = process.pid) {
|
||||
const activity: DiscordRpcActivity = {
|
||||
name: args.name,
|
||||
type: DiscordApiActivityType.PLAYING,
|
||||
status_display_type: args.statusDisplayType,
|
||||
state: args.state,
|
||||
state_url: args.stateUrl,
|
||||
details: args.details,
|
||||
details_url: args.detailsUrl,
|
||||
buttons: args.buttons,
|
||||
instance: !!args.instance,
|
||||
};
|
||||
|
||||
if (args.startTimestamp || args.endTimestamp) {
|
||||
activity.timestamps = {
|
||||
start: args.startTimestamp instanceof Date ? Math.round(args.startTimestamp.getTime()) : args.startTimestamp,
|
||||
end: args.endTimestamp instanceof Date ? Math.round(args.endTimestamp.getTime()) : args.endTimestamp,
|
||||
};
|
||||
if (typeof activity.timestamps.start === 'number' && activity.timestamps.start > 2147483647000) {
|
||||
throw new RangeError('timestamps.start must fit into a unix timestamp');
|
||||
}
|
||||
if (typeof activity.timestamps.end === 'number' && activity.timestamps.end > 2147483647000) {
|
||||
throw new RangeError('timestamps.end must fit into a unix timestamp');
|
||||
}
|
||||
}
|
||||
|
||||
if (args.largeImageKey || args.largeImageText ||
|
||||
args.smallImageKey || args.smallImageText
|
||||
) {
|
||||
activity.assets = {
|
||||
large_image: args.largeImageKey,
|
||||
large_text: args.largeImageText,
|
||||
large_url: args.largeImageUrl,
|
||||
small_image: args.smallImageKey,
|
||||
small_text: args.smallImageText,
|
||||
small_url: args.smallImageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (args.partySize || args.partyId || args.partyMax) {
|
||||
activity.party = {
|
||||
id: args.partyId,
|
||||
size: args.partySize || args.partyMax ? [args.partySize ?? 0, args.partyMax ?? 0] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (args.matchSecret || args.joinSecret || args.spectateSecret) {
|
||||
activity.secrets = {
|
||||
match: args.matchSecret,
|
||||
join: args.joinSecret,
|
||||
spectate: args.spectateSecret,
|
||||
};
|
||||
}
|
||||
|
||||
return this.setActivityRaw(activity, pid);
|
||||
}
|
||||
|
||||
setActivityRaw(activity: DiscordRpcActivity, pid = process.pid) {
|
||||
debug('set activity', activity);
|
||||
|
||||
return this.request('SET_ACTIVITY', {
|
||||
pid,
|
||||
activity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type DiscordRpcActivity = Partial<Omit<DiscordApiActivity, 'created_at' | 'application_id' | 'emoji'>>;
|
||||
|
||||
interface DiscordApiActivity {
|
||||
name: string;
|
||||
type: DiscordApiActivityType;
|
||||
url?: string;
|
||||
created_at: number;
|
||||
timestamps?: DiscordApiActivityTimestamps;
|
||||
application_id?: string;
|
||||
status_display_type?: DiscordApiActivityStatusDisplayType;
|
||||
details?: string;
|
||||
details_url?: string;
|
||||
state?: string;
|
||||
state_url?: string;
|
||||
emoji?: DiscordApiActivityEmoji;
|
||||
party?: DiscordApiActivityParty;
|
||||
assets?: DiscordApiActivityAssets;
|
||||
secrets?: DiscordApiActivitySecrets;
|
||||
instance?: boolean;
|
||||
flags?: number;
|
||||
buttons?: DiscordApiActivityButton[];
|
||||
}
|
||||
enum DiscordApiActivityType {
|
||||
PLAYING = 0,
|
||||
STREAMING = 1,
|
||||
LISTENING = 2,
|
||||
WATCHING = 3,
|
||||
CUSTOM = 4,
|
||||
COMPETING = 5,
|
||||
}
|
||||
export enum DiscordApiActivityStatusDisplayType {
|
||||
NAME = 0,
|
||||
STATE = 1,
|
||||
DETAILS = 2,
|
||||
}
|
||||
interface DiscordApiActivityTimestamps {
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
interface DiscordApiActivityEmoji {
|
||||
name: string;
|
||||
id?: string;
|
||||
animated?: boolean;
|
||||
}
|
||||
interface DiscordApiActivityParty {
|
||||
id?: string;
|
||||
size?: [number, number];
|
||||
}
|
||||
interface DiscordApiActivityAssets {
|
||||
large_image?: string;
|
||||
large_text?: string;
|
||||
large_url?: string;
|
||||
small_image?: string;
|
||||
small_text?: string;
|
||||
small_url?: string;
|
||||
}
|
||||
interface DiscordApiActivitySecrets {
|
||||
join?: string;
|
||||
spectate?: string;
|
||||
match?: string;
|
||||
}
|
||||
enum DiscordApiActivityFlags {
|
||||
INSTANCE = 1 << 0,
|
||||
JOIN = 1 << 1,
|
||||
SPECTATE = 1 << 2,
|
||||
JOIN_REQUEST = 1 << 3,
|
||||
SYNC = 1 << 4,
|
||||
PLAY = 1 << 5,
|
||||
PARTY_PRIVACY_FRIENDS = 1 << 6,
|
||||
PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7,
|
||||
EMBEDDED = 1 << 8,
|
||||
}
|
||||
interface DiscordApiActivityButton {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
class IpcTransport extends BaseIpcTransport {
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@ import { Title } from './types.js';
|
|||
import * as publishers from './titles/index.js';
|
||||
import { PresencePlatform } from '../api/coral-types.js';
|
||||
|
||||
export const defaultTitle: Title = {
|
||||
id: '0000000000000000',
|
||||
client: '950883021165330493',
|
||||
titleName: true,
|
||||
showPlayingOnline: true,
|
||||
showActiveEvent: true,
|
||||
};
|
||||
|
||||
export const platform_clients: Record<PresencePlatform, string> = {
|
||||
[PresencePlatform.NX]: '950883021165330493',
|
||||
[PresencePlatform.OUNCE]: '1358060657957928970',
|
||||
};
|
||||
|
||||
export const default_client = platform_clients[PresencePlatform.NX];
|
||||
|
||||
export const defaultTitle: Title = {
|
||||
id: '0000000000000000',
|
||||
titleName: true,
|
||||
showPlayingOnline: true,
|
||||
showActiveEvent: true,
|
||||
};
|
||||
|
||||
export const titles: Title[] = [];
|
||||
|
||||
for (const [publisher, m] of Object.entries(publishers)) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ export interface DiscordPresence {
|
|||
type SystemModuleTitleId = `01000000000000${string}`;
|
||||
type SystemDataTitleId = `01000000000008${string}`;
|
||||
type SystemAppletTitleId = `0100000000001${string}`;
|
||||
type ApplicationTitleId = `0100${string}${'0' | '2' | '4' | '6' | '8' | 'a' | 'c' | 'e'}000`;
|
||||
type ApplicationTitleIdNx = `0100${string}${'0' | '2' | '4' | '6' | '8' | 'a' | 'c' | 'e'}000`;
|
||||
type ApplicationTitleIdOunce = `0400${string}${'0' | '2' | '4' | '6' | '8' | 'a' | 'c' | 'e'}000`;
|
||||
type ApplicationTitleId = ApplicationTitleIdNx | ApplicationTitleIdOunce;
|
||||
|
||||
export interface Title<M extends ExternalMonitor = ExternalMonitor> {
|
||||
/**
|
||||
|
|
@ -39,12 +41,18 @@ export interface Title<M extends ExternalMonitor = ExternalMonitor> {
|
|||
/**
|
||||
* Discord client ID
|
||||
*/
|
||||
client: string;
|
||||
client?: string;
|
||||
|
||||
/**
|
||||
* Activity name. This overrides the application's name that will appear under the user's name after "Playing ".
|
||||
*/
|
||||
setActivityName?: boolean;
|
||||
activityName?: string;
|
||||
|
||||
/**
|
||||
* Title name to show in Discord. This is *not* the name that will appear under the user's name after "Playing ".
|
||||
*
|
||||
* If this is set to true the title's name from znc will be used.
|
||||
* If this is set to true the title's name from coral will be used.
|
||||
* If this is set to false (default) no title name will be set. This should be used when a specific Discord client for the title is used.
|
||||
* If this is set to a string it will be used as the title name.
|
||||
*
|
||||
|
|
@ -52,7 +60,7 @@ export interface Title<M extends ExternalMonitor = ExternalMonitor> {
|
|||
*/
|
||||
titleName?: string | boolean;
|
||||
/**
|
||||
* By default the title's icon from znc will be used. (No icons need to be uploaded to Discord.)
|
||||
* By default the title's icon from coral will be used. (No icons need to be uploaded to Discord.)
|
||||
*/
|
||||
largeImageKey?: string;
|
||||
largeImageText?: string;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import DiscordRPC from 'discord-rpc';
|
||||
import { PresenceGame, PresenceState } from '../api/coral-types.js';
|
||||
import { defaultTitle, platform_clients, titles } from './titles.js';
|
||||
import { PresenceGame, PresencePlatform, PresenceState } from '../api/coral-types.js';
|
||||
import { default_client, defaultTitle, platform_clients, titles } from './titles.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { product, version } from '../util/product.js';
|
||||
import { getTitleIdFromEcUrl, hrduration } from '../util/misc.js';
|
||||
import { DiscordPresence, DiscordPresenceContext, DiscordPresencePlayTime } from './types.js';
|
||||
import { DiscordApiActivityStatusDisplayType } from './rpc.js';
|
||||
|
||||
const debug = createDebug('nxapi:discord');
|
||||
|
||||
|
|
@ -44,9 +45,18 @@ export function getDiscordPresence(
|
|||
|
||||
const activity = new DiscordActivity();
|
||||
|
||||
if (title.setActivityName) {
|
||||
activity.name = title.activityName ?? game.name;
|
||||
} else if (title.titleName) {
|
||||
// If this is set it/the title name is used as the details field
|
||||
activity.statusDisplayType = DiscordApiActivityStatusDisplayType.DETAILS;
|
||||
}
|
||||
|
||||
activity.details = text[0];
|
||||
activity.state = text[1];
|
||||
|
||||
activity.platform = context?.platform;
|
||||
|
||||
activity.setLargeImage(title.largeImageKey ?? game.imageUri, title.largeImageText);
|
||||
|
||||
if (title.smallImageKey) {
|
||||
|
|
@ -78,7 +88,7 @@ export function getDiscordPresence(
|
|||
return {
|
||||
id: (title !== defaultTitle ? title : null)?.client ||
|
||||
(typeof context?.platform !== 'undefined' && platform_clients[context.platform]) ||
|
||||
defaultTitle.client,
|
||||
defaultTitle.client || default_client,
|
||||
title: titleid,
|
||||
config: title,
|
||||
activity,
|
||||
|
|
@ -87,6 +97,8 @@ export function getDiscordPresence(
|
|||
}
|
||||
|
||||
export class DiscordActivity implements DiscordRPC.Presence {
|
||||
name?: string = undefined;
|
||||
statusDisplayType?: DiscordApiActivityStatusDisplayType | undefined;
|
||||
details?: string = undefined;
|
||||
state?: string = undefined;
|
||||
largeImageKey?: string = undefined;
|
||||
|
|
@ -95,13 +107,24 @@ export class DiscordActivity implements DiscordRPC.Presence {
|
|||
smallImageText?: string = undefined;
|
||||
buttons: { label: string; url: string; }[] = [];
|
||||
|
||||
platform?: PresencePlatform;
|
||||
|
||||
constructor() {
|
||||
//
|
||||
}
|
||||
|
||||
get large_image_default_text() {
|
||||
let text = product;
|
||||
|
||||
if (this.platform === PresencePlatform.NX) text = 'Playing on Nintendo Switch | ' + text;
|
||||
if (this.platform === PresencePlatform.OUNCE) text = 'Playing on Nintendo Switch 2 | ' + text;
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
setLargeImage(key: string, text?: string) {
|
||||
this.largeImageKey = key;
|
||||
this.largeImageText = text ? text + ' | ' + product : product;
|
||||
this.largeImageText = text ? text + ' | ' + this.large_image_default_text : this.large_image_default_text;
|
||||
}
|
||||
|
||||
setSmallImage(key: string, text?: string) {
|
||||
|
|
@ -174,7 +197,7 @@ export function getInactiveDiscordPresence(
|
|||
): DiscordPresence {
|
||||
return {
|
||||
id: (typeof context?.platform !== 'undefined' && platform_clients[context.platform]) ||
|
||||
defaultTitle.client,
|
||||
defaultTitle.client || default_client,
|
||||
title: null,
|
||||
activity: {
|
||||
state: 'Not playing',
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user