feat: Discord's local RPC servers
arRPC (https://github.com/OpenAsar/arrpc)
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
'use strict';
|
||||
|
||||
const { EventEmitter } = require('node:events');
|
||||
const ProcessServer = require('./process/index.js');
|
||||
const IPCServer = require('./transports/ipc.js');
|
||||
const WSServer = require('./transports/websocket.js');
|
||||
const { RichPresence } = require('../../structures/RichPresence.js');
|
||||
const { NitroType } = require('../Constants.js');
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const checkUrl = url => /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/.test(url);
|
||||
|
||||
let socketId = 0;
|
||||
module.exports = class RPCServer extends EventEmitter {
|
||||
constructor(client, debug = false) {
|
||||
super();
|
||||
Object.defineProperty(this, 'client', { value: client });
|
||||
return (async () => {
|
||||
this.debug = debug;
|
||||
this.onConnection = this.onConnection.bind(this);
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
|
||||
const handlers = {
|
||||
connection: this.onConnection,
|
||||
message: this.onMessage,
|
||||
close: this.onClose,
|
||||
};
|
||||
|
||||
this.ipc = await new IPCServer(handlers, this.debug);
|
||||
this.ws = await new WSServer(handlers, this.debug);
|
||||
this.process = await new ProcessServer(handlers, this.debug);
|
||||
|
||||
return this;
|
||||
})();
|
||||
}
|
||||
|
||||
onConnection(socket) {
|
||||
socket.send({
|
||||
cmd: 'DISPATCH',
|
||||
evt: 'READY',
|
||||
|
||||
data: {
|
||||
v: 1,
|
||||
// Needed otherwise some stuff errors out parsing json strictly
|
||||
user: {
|
||||
// Mock user data using arRPC app/bot
|
||||
id: this.client?.user?.id ?? '1045800378228281345',
|
||||
username: this.client?.user?.username ?? 'arRPC',
|
||||
discriminator: this.client?.user?.discriminator ?? '0000',
|
||||
avatar: this.client?.user?.avatar,
|
||||
flags: this.client?.user?.flags?.bitfield ?? 0,
|
||||
premium_type: this.client?.user?.nitroType ? NitroType[this.client?.user?.nitroType] : 0,
|
||||
},
|
||||
config: {
|
||||
api_endpoint: '//discord.com/api',
|
||||
cdn_host: 'cdn.discordapp.com',
|
||||
environment: 'production',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
socket.socketId = socketId++;
|
||||
|
||||
this.emit('connection', socket);
|
||||
}
|
||||
|
||||
onClose(socket) {
|
||||
this.emit('activity', {
|
||||
activity: null,
|
||||
pid: socket.lastPid,
|
||||
socketId: socket.socketId.toString(),
|
||||
});
|
||||
|
||||
this.emit('close', socket);
|
||||
}
|
||||
|
||||
async onMessage(socket, { cmd, args, nonce }) {
|
||||
this.emit('message', { socket, cmd, args, nonce });
|
||||
|
||||
switch (cmd) {
|
||||
case 'SET_ACTIVITY':
|
||||
if (!socket.clientInfo || !socket.clientAssets) {
|
||||
// https://discord.com/api/v9/oauth2/applications/:id/rpc
|
||||
socket.clientInfo = await this.client.api.oauth2.applications(socket.clientId).rpc.get();
|
||||
socket.clientAssets = await this.client.api.oauth2.applications(socket.clientId).assets.get();
|
||||
}
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { activity, pid } = args; // Translate given parameters into what discord dispatch expects
|
||||
|
||||
if (!activity) {
|
||||
return this.emit('activity', {
|
||||
activity: null,
|
||||
pid,
|
||||
socketId: socket.socketId.toString(),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { buttons, timestamps, instance, assets } = activity;
|
||||
|
||||
socket.lastPid = pid ?? socket.lastPid;
|
||||
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const metadata = {};
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const extra = {};
|
||||
if (buttons) {
|
||||
// Map buttons into expected metadata
|
||||
metadata.button_urls = buttons.map(x => x.url);
|
||||
extra.buttons = buttons.map(x => x.label);
|
||||
}
|
||||
|
||||
if (assets?.large_image) {
|
||||
if (checkUrl(assets.large_image)) {
|
||||
assets.large_image = assets.large_image
|
||||
.replace('https://cdn.discordapp.com/', 'mp:')
|
||||
.replace('http://cdn.discordapp.com/', 'mp:')
|
||||
.replace('https://media.discordapp.net/', 'mp:')
|
||||
.replace('http://media.discordapp.net/', 'mp:');
|
||||
if (!assets.large_image.startsWith('mp:')) {
|
||||
// Fetch
|
||||
const data = await RichPresence.getExternal(this.client, socket.clientId, assets.large_image);
|
||||
assets.large_image = data[0].external_asset_path;
|
||||
}
|
||||
}
|
||||
if (/^[0-9]{17,19}$/.test(assets.large_image)) {
|
||||
// ID Assets
|
||||
}
|
||||
if (
|
||||
assets.large_image.startsWith('mp:') ||
|
||||
assets.large_image.startsWith('youtube:') ||
|
||||
assets.large_image.startsWith('spotify:')
|
||||
) {
|
||||
// Image
|
||||
}
|
||||
if (assets.large_image.startsWith('external/')) {
|
||||
assets.large_image = `mp:${assets.large_image}`;
|
||||
} else {
|
||||
const l = socket.clientAssets.find(o => o.name == assets.large_image);
|
||||
if (l) assets.large_image = l.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (assets?.small_image) {
|
||||
if (checkUrl(assets.small_image)) {
|
||||
assets.small_image = assets.small_image
|
||||
.replace('https://cdn.discordapp.com/', 'mp:')
|
||||
.replace('http://cdn.discordapp.com/', 'mp:')
|
||||
.replace('https://media.discordapp.net/', 'mp:')
|
||||
.replace('http://media.discordapp.net/', 'mp:');
|
||||
if (!assets.small_image.startsWith('mp:')) {
|
||||
// Fetch
|
||||
const data = await RichPresence.getExternal(this.client, socket.clientId, assets.small_image);
|
||||
assets.small_image = data[0].external_asset_path;
|
||||
}
|
||||
}
|
||||
if (/^[0-9]{17,19}$/.test(assets.small_image)) {
|
||||
// ID Assets
|
||||
}
|
||||
if (
|
||||
assets.small_image.startsWith('mp:') ||
|
||||
assets.small_image.startsWith('youtube:') ||
|
||||
assets.small_image.startsWith('spotify:')
|
||||
) {
|
||||
// Image
|
||||
}
|
||||
if (assets.small_image.startsWith('external/')) {
|
||||
assets.small_image = `mp:${assets.small_image}`;
|
||||
} else {
|
||||
const l = socket.clientAssets.find(o => o.name == assets.small_image);
|
||||
if (l) assets.small_image = l.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamps) {
|
||||
for (const x in timestamps) {
|
||||
// Translate s -> ms timestamps
|
||||
if (Date.now().toString().length - timestamps[x].toString().length > 2) {
|
||||
timestamps[x] = Math.floor(1000 * timestamps[x]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('activity', {
|
||||
activity: {
|
||||
application_id: socket.clientId,
|
||||
type: 0,
|
||||
name: socket.clientInfo.name,
|
||||
metadata,
|
||||
assets,
|
||||
flags: instance ? 1 << 0 : 0,
|
||||
...activity,
|
||||
...extra,
|
||||
},
|
||||
pid,
|
||||
socketId: socket.socketId.toString(),
|
||||
});
|
||||
|
||||
socket.send?.({
|
||||
cmd,
|
||||
data: null,
|
||||
evt: null,
|
||||
nonce,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case 'GUILD_TEMPLATE_BROWSER':
|
||||
case 'INVITE_BROWSER':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { code } = args;
|
||||
socket.send({
|
||||
cmd,
|
||||
data: {
|
||||
code,
|
||||
},
|
||||
nonce,
|
||||
});
|
||||
|
||||
this.emit(cmd === 'INVITE_BROWSER' ? 'invite' : 'guild-template', code);
|
||||
break;
|
||||
|
||||
case 'DEEP_LINK':
|
||||
this.emit('link', args.params);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user