break: remove arRPC
This commit is contained in:
		@@ -1,229 +0,0 @@
 | 
			
		||||
'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;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,102 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
 | 
			
		||||
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}]`, ...args);
 | 
			
		||||
const { setInterval } = require('node:timers');
 | 
			
		||||
const process = require('process');
 | 
			
		||||
const DetectableDB = require('./detectable.json');
 | 
			
		||||
const Natives = require('./native/index.js');
 | 
			
		||||
 | 
			
		||||
const Native = Natives[process.platform];
 | 
			
		||||
 | 
			
		||||
const timestamps = {},
 | 
			
		||||
  names = {},
 | 
			
		||||
  pids = {};
 | 
			
		||||
module.exports = class ProcessServer {
 | 
			
		||||
  constructor(handlers, debug = false) {
 | 
			
		||||
    this.debug = debug;
 | 
			
		||||
    if (!Native) return; // Log('unsupported platform:', process.platform);
 | 
			
		||||
 | 
			
		||||
    this.handlers = handlers;
 | 
			
		||||
 | 
			
		||||
    this.scan = this.scan.bind(this);
 | 
			
		||||
 | 
			
		||||
    this.scan();
 | 
			
		||||
    setInterval(this.scan, 5000).unref();
 | 
			
		||||
 | 
			
		||||
    if (this.debug) log('started');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async scan() {
 | 
			
		||||
    const processes = await Native.getProcesses();
 | 
			
		||||
    const ids = [];
 | 
			
		||||
 | 
			
		||||
    for (const [pid, _path] of processes) {
 | 
			
		||||
      const path = _path.toLowerCase().replaceAll('\\', '/');
 | 
			
		||||
      const toCompare = [path.split('/').pop(), path.split('/').slice(-2).join('/')];
 | 
			
		||||
 | 
			
		||||
      for (const p of toCompare.slice()) {
 | 
			
		||||
        // Add more possible tweaked paths for less false negatives
 | 
			
		||||
        toCompare.push(p.replace('64', '')); // Remove 64bit identifiers-ish
 | 
			
		||||
        toCompare.push(p.replace('.x64', ''));
 | 
			
		||||
        toCompare.push(p.replace('x64', ''));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const { executables, id, name } of DetectableDB) {
 | 
			
		||||
        if (executables?.some(x => !x.isLauncher && toCompare.some(y => x.name === y))) {
 | 
			
		||||
          names[id] = name;
 | 
			
		||||
          pids[id] = pid;
 | 
			
		||||
 | 
			
		||||
          ids.push(id);
 | 
			
		||||
          if (!timestamps[id]) {
 | 
			
		||||
            // eslint-disable-next-line max-depth
 | 
			
		||||
            if (this.debug) log('detected game!', name);
 | 
			
		||||
            timestamps[id] = Date.now();
 | 
			
		||||
 | 
			
		||||
            this.handlers.message(
 | 
			
		||||
              {
 | 
			
		||||
                socketId: id,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                cmd: 'SET_ACTIVITY',
 | 
			
		||||
                args: {
 | 
			
		||||
                  activity: {
 | 
			
		||||
                    application_id: id,
 | 
			
		||||
                    name,
 | 
			
		||||
                    timestamps: {
 | 
			
		||||
                      start: timestamps[id],
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                  pid,
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const id in timestamps) {
 | 
			
		||||
      if (!ids.includes(id)) {
 | 
			
		||||
        if (this.debug) log('lost game!', names[id]);
 | 
			
		||||
        delete timestamps[id];
 | 
			
		||||
 | 
			
		||||
        this.handlers.message(
 | 
			
		||||
          {
 | 
			
		||||
            socketId: id,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            cmd: 'SET_ACTIVITY',
 | 
			
		||||
            args: {
 | 
			
		||||
              activity: null,
 | 
			
		||||
              pid: pids[id],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If (this.debug) log(`finished scan in ${(performance.now() - startTime).toFixed(2)}ms`);
 | 
			
		||||
    // process.stdout.write(`\r${' '.repeat(100)}\r[${rgb(88, 101, 242, 'arRPC')} > ${rgb(237, 66, 69, 'process')}] scanned (took ${(performance.now() - startTime).toFixed(2)}ms)`);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const linux = require('./linux.js');
 | 
			
		||||
const win32 = require('./win32.js');
 | 
			
		||||
module.exports = { win32, linux };
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const { exec } = require('child_process');
 | 
			
		||||
const { readlink } = require('fs/promises');
 | 
			
		||||
 | 
			
		||||
const getProcesses = () =>
 | 
			
		||||
  new Promise(res =>
 | 
			
		||||
    exec(`ps a -o "%p;%c;%a"`, async (e, out) => {
 | 
			
		||||
      res(
 | 
			
		||||
        (
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            out
 | 
			
		||||
              .toString()
 | 
			
		||||
              .split('\n')
 | 
			
		||||
              .slice(1, -1)
 | 
			
		||||
 | 
			
		||||
              .map(async x => {
 | 
			
		||||
                const split = x.trim().split(';');
 | 
			
		||||
                // If (split.length === 1) return;
 | 
			
		||||
 | 
			
		||||
                const pid = parseInt(split[0].trim());
 | 
			
		||||
                /* Unused
 | 
			
		||||
                const cmd = split[1].trim();
 | 
			
		||||
                const argv = split.slice(2).join(';').trim();
 | 
			
		||||
                */
 | 
			
		||||
 | 
			
		||||
                const path = await readlink(`/proc/${pid}/exe`).catch(() => {}); // Read path from /proc/{pid}/exe symlink
 | 
			
		||||
 | 
			
		||||
                return [pid, path];
 | 
			
		||||
              }),
 | 
			
		||||
          )
 | 
			
		||||
        ).filter(x => x && x[1]),
 | 
			
		||||
      );
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
module.exports = { getProcesses };
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const { exec } = require('child_process');
 | 
			
		||||
 | 
			
		||||
const getProcesses = () =>
 | 
			
		||||
  new Promise(res =>
 | 
			
		||||
    exec(`wmic process get ProcessID,ExecutablePath /format:csv`, (e, out) => {
 | 
			
		||||
      res(
 | 
			
		||||
        out
 | 
			
		||||
          .toString()
 | 
			
		||||
          .split('\r\n')
 | 
			
		||||
          .slice(2)
 | 
			
		||||
 | 
			
		||||
          .map(x => {
 | 
			
		||||
            // eslint-disable-next-line newline-per-chained-call
 | 
			
		||||
            const parsed = x.trim().split(',').slice(1).reverse();
 | 
			
		||||
            parsed[0] = parseInt(parsed[0]) || parsed[0]; // Pid to int
 | 
			
		||||
            return parsed;
 | 
			
		||||
          })
 | 
			
		||||
          .filter(x => x[1]),
 | 
			
		||||
      );
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
module.exports = { getProcesses };
 | 
			
		||||
@@ -1,281 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
 | 
			
		||||
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(254, 231, 92, 'ipc')}]`, ...args);
 | 
			
		||||
 | 
			
		||||
const { Buffer } = require('buffer');
 | 
			
		||||
const { unlinkSync } = require('fs');
 | 
			
		||||
const { createServer, createConnection } = require('net');
 | 
			
		||||
const { setTimeout } = require('node:timers');
 | 
			
		||||
const { join } = require('path');
 | 
			
		||||
const { platform, env } = require('process');
 | 
			
		||||
 | 
			
		||||
const SOCKET_PATH =
 | 
			
		||||
  platform === 'win32'
 | 
			
		||||
    ? '\\\\?\\pipe\\discord-ipc'
 | 
			
		||||
    : join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || '/tmp', 'discord-ipc');
 | 
			
		||||
 | 
			
		||||
// Enums for various constants
 | 
			
		||||
const Types = {
 | 
			
		||||
  // Types of packets
 | 
			
		||||
  HANDSHAKE: 0,
 | 
			
		||||
  FRAME: 1,
 | 
			
		||||
  CLOSE: 2,
 | 
			
		||||
  PING: 3,
 | 
			
		||||
  PONG: 4,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CloseCodes = {
 | 
			
		||||
  // Codes for closures
 | 
			
		||||
  CLOSE_NORMAL: 1000,
 | 
			
		||||
  CLOSE_UNSUPPORTED: 1003,
 | 
			
		||||
  CLOSE_ABNORMAL: 1006,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ErrorCodes = {
 | 
			
		||||
  // Codes for errors
 | 
			
		||||
  INVALID_CLIENTID: 4000,
 | 
			
		||||
  INVALID_ORIGIN: 4001,
 | 
			
		||||
  RATELIMITED: 4002,
 | 
			
		||||
  TOKEN_REVOKED: 4003,
 | 
			
		||||
  INVALID_VERSION: 4004,
 | 
			
		||||
  INVALID_ENCODING: 4005,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let uniqueId = 0;
 | 
			
		||||
 | 
			
		||||
const encode = (type, data) => {
 | 
			
		||||
  data = JSON.stringify(data);
 | 
			
		||||
  const dataSize = Buffer.byteLength(data);
 | 
			
		||||
 | 
			
		||||
  const buf = Buffer.alloc(dataSize + 8);
 | 
			
		||||
  buf.writeInt32LE(type, 0); // Type
 | 
			
		||||
  buf.writeInt32LE(dataSize, 4); // Data size
 | 
			
		||||
  buf.write(data, 8, dataSize); // Data
 | 
			
		||||
 | 
			
		||||
  return buf;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const read = socket => {
 | 
			
		||||
  let resp = socket.read(8);
 | 
			
		||||
  if (!resp) return;
 | 
			
		||||
 | 
			
		||||
  resp = Buffer.from(resp);
 | 
			
		||||
  const type = resp.readInt32LE(0);
 | 
			
		||||
  const dataSize = resp.readInt32LE(4);
 | 
			
		||||
 | 
			
		||||
  if (type < 0 || type >= Object.keys(Types).length) throw new Error('invalid type');
 | 
			
		||||
 | 
			
		||||
  let data = socket.read(dataSize);
 | 
			
		||||
  if (!data) throw new Error('failed reading data');
 | 
			
		||||
 | 
			
		||||
  data = JSON.parse(Buffer.from(data).toString());
 | 
			
		||||
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case Types.PING:
 | 
			
		||||
      socket.emit('ping', data);
 | 
			
		||||
      socket.write(encode(Types.PONG, data));
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case Types.PONG:
 | 
			
		||||
      socket.emit('pong', data);
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case Types.HANDSHAKE:
 | 
			
		||||
      if (socket._handshook) throw new Error('already handshook');
 | 
			
		||||
 | 
			
		||||
      socket._handshook = true;
 | 
			
		||||
      socket.emit('handshake', data);
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case Types.FRAME:
 | 
			
		||||
      if (!socket._handshook) throw new Error('need to handshake first');
 | 
			
		||||
 | 
			
		||||
      socket.emit('request', data);
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case Types.CLOSE:
 | 
			
		||||
      socket.end();
 | 
			
		||||
      socket.destroy();
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  read(socket);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const socketIsAvailable = async socket => {
 | 
			
		||||
  socket.pause();
 | 
			
		||||
  socket.on('readable', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      read(socket);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // Debug: log('error whilst reading', e);
 | 
			
		||||
      socket.end(
 | 
			
		||||
        encode(Types.CLOSE, {
 | 
			
		||||
          code: CloseCodes.CLOSE_UNSUPPORTED,
 | 
			
		||||
          message: e.message,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
      socket.destroy();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const stop = () => {
 | 
			
		||||
    try {
 | 
			
		||||
      socket.end();
 | 
			
		||||
      socket.destroy();
 | 
			
		||||
    } catch {
 | 
			
		||||
      // Debug
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const possibleOutcomes = Promise.race([
 | 
			
		||||
    new Promise(res => socket.on('error', res)), // Errored
 | 
			
		||||
    // eslint-disable-next-line prefer-promise-reject-errors
 | 
			
		||||
    new Promise((res, rej) => socket.on('pong', () => rej('socket ponged'))), // Ponged
 | 
			
		||||
    // eslint-disable-next-line prefer-promise-reject-errors
 | 
			
		||||
    new Promise((res, rej) => setTimeout(() => rej('timed out'), 1000).unref()), // Timed out
 | 
			
		||||
  ]).then(
 | 
			
		||||
    () => true,
 | 
			
		||||
    e => e,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  socket.write(encode(Types.PING, ++uniqueId));
 | 
			
		||||
 | 
			
		||||
  const outcome = await possibleOutcomes;
 | 
			
		||||
  stop();
 | 
			
		||||
  // Debug: log('checked if socket is available:', outcome === true, outcome === true ? '' : `- reason: ${outcome}`);
 | 
			
		||||
 | 
			
		||||
  return outcome === true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const getAvailableSocket = async (tries = 0) => {
 | 
			
		||||
  if (tries > 9) {
 | 
			
		||||
    throw new Error('ran out of tries to find socket', tries);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const path = `${SOCKET_PATH}-${tries}`;
 | 
			
		||||
  const socket = createConnection(path);
 | 
			
		||||
 | 
			
		||||
  // Debug: log('checking', path);
 | 
			
		||||
 | 
			
		||||
  if (await socketIsAvailable(socket)) {
 | 
			
		||||
    if (platform !== 'win32') {
 | 
			
		||||
      try {
 | 
			
		||||
        unlinkSync(path);
 | 
			
		||||
      } catch {
 | 
			
		||||
        // Debug
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return path;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Debug: log(`not available, trying again (attempt ${tries + 1})`);
 | 
			
		||||
  return getAvailableSocket(tries + 1);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = class IPCServer {
 | 
			
		||||
  constructor(handers, debug = false) {
 | 
			
		||||
    // eslint-disable-next-line no-async-promise-executor
 | 
			
		||||
    return new Promise(async res => {
 | 
			
		||||
      this.debug = debug;
 | 
			
		||||
      this.handlers = handers;
 | 
			
		||||
 | 
			
		||||
      this.onConnection = this.onConnection.bind(this);
 | 
			
		||||
      this.onMessage = this.onMessage.bind(this);
 | 
			
		||||
 | 
			
		||||
      const server = createServer(this.onConnection);
 | 
			
		||||
      server.on('error', e => {
 | 
			
		||||
        if (this.debug) log('server error', e);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const socketPath = await getAvailableSocket();
 | 
			
		||||
      server.listen(socketPath, () => {
 | 
			
		||||
        if (this.debug) log('listening at', socketPath);
 | 
			
		||||
        this.server = server;
 | 
			
		||||
 | 
			
		||||
        res(this);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onConnection(socket) {
 | 
			
		||||
    if (this.debug) log('new connection!');
 | 
			
		||||
 | 
			
		||||
    socket.pause();
 | 
			
		||||
    socket.on('readable', () => {
 | 
			
		||||
      try {
 | 
			
		||||
        read(socket);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        if (this.debug) log('error whilst reading', e);
 | 
			
		||||
 | 
			
		||||
        socket.end(
 | 
			
		||||
          encode(Types.CLOSE, {
 | 
			
		||||
            code: CloseCodes.CLOSE_UNSUPPORTED,
 | 
			
		||||
            message: e.message,
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.once('handshake', params => {
 | 
			
		||||
      if (this.debug) log('handshake:', params);
 | 
			
		||||
 | 
			
		||||
      const ver = parseInt(params.v ?? 1);
 | 
			
		||||
      const clientId = params.client_id ?? '';
 | 
			
		||||
      // Encoding is always json for ipc
 | 
			
		||||
 | 
			
		||||
      socket.close = (code = CloseCodes.CLOSE_NORMAL, message = '') => {
 | 
			
		||||
        socket.end(
 | 
			
		||||
          encode(Types.CLOSE, {
 | 
			
		||||
            code,
 | 
			
		||||
            message,
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
        socket.destroy();
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      if (ver !== 1) {
 | 
			
		||||
        if (this.debug) log('unsupported version requested', ver);
 | 
			
		||||
 | 
			
		||||
        socket.close(ErrorCodes.INVALID_VERSION);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (clientId === '') {
 | 
			
		||||
        if (this.debug) log('client id required');
 | 
			
		||||
 | 
			
		||||
        socket.close(ErrorCodes.INVALID_CLIENTID);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      socket.on('error', e => {
 | 
			
		||||
        if (this.debug) log('socket error', e);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      socket.on('close', e => {
 | 
			
		||||
        if (this.debug) log('socket closed', e);
 | 
			
		||||
        this.handlers.close(socket);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      socket.on('request', this.onMessage.bind(this, socket));
 | 
			
		||||
 | 
			
		||||
      socket._send = socket.send;
 | 
			
		||||
      socket.send = msg => {
 | 
			
		||||
        if (this.debug) log('sending', msg);
 | 
			
		||||
        socket.write(encode(Types.FRAME, msg));
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      socket.clientId = clientId;
 | 
			
		||||
 | 
			
		||||
      this.handlers.connection(socket);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMessage(socket, msg) {
 | 
			
		||||
    if (this.debug) log('message', msg);
 | 
			
		||||
    this.handlers.message(socket, msg);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -1,128 +0,0 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const rgb = (r, g, b, msg) => `\x1b[38;2;${r};${g};${b}m${msg}\x1b[0m`;
 | 
			
		||||
const log = (...args) => console.log(`[${rgb(88, 101, 242, 'arRPC')} > ${rgb(235, 69, 158, 'websocket')}]`, ...args);
 | 
			
		||||
 | 
			
		||||
const { createServer } = require('http');
 | 
			
		||||
const { parse } = require('querystring');
 | 
			
		||||
const { WebSocketServer } = require('ws');
 | 
			
		||||
 | 
			
		||||
const portRange = [6463, 6472]; // Ports available/possible: 6463-6472
 | 
			
		||||
 | 
			
		||||
module.exports = class WSServer {
 | 
			
		||||
  constructor(handlers, debug = false) {
 | 
			
		||||
    return (async () => {
 | 
			
		||||
      this.debug = debug;
 | 
			
		||||
 | 
			
		||||
      this.handlers = handlers;
 | 
			
		||||
 | 
			
		||||
      this.onConnection = this.onConnection.bind(this);
 | 
			
		||||
      this.onMessage = this.onMessage.bind(this);
 | 
			
		||||
 | 
			
		||||
      let port = portRange[0];
 | 
			
		||||
 | 
			
		||||
      let http, wss;
 | 
			
		||||
      while (port <= portRange[1]) {
 | 
			
		||||
        if (this.debug) log('trying port', port);
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          await new Promise(res => {
 | 
			
		||||
            http = createServer();
 | 
			
		||||
            http.on('error', e => {
 | 
			
		||||
              // Log('http error', e);
 | 
			
		||||
 | 
			
		||||
              if (e.code === 'EADDRINUSE') {
 | 
			
		||||
                if (this.debug) log(port, 'in use!');
 | 
			
		||||
                res(false);
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            wss = new WebSocketServer({ server: http });
 | 
			
		||||
            // eslint-disable-next-line no-unused-vars
 | 
			
		||||
            wss.on('error', e => {
 | 
			
		||||
              // Debug: Log('wss error', e);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            wss.on('connection', this.onConnection);
 | 
			
		||||
 | 
			
		||||
            http.listen(port, '127.0.0.1', () => {
 | 
			
		||||
              if (this.debug) log('listening on', port);
 | 
			
		||||
 | 
			
		||||
              this.http = http;
 | 
			
		||||
              this.wss = wss;
 | 
			
		||||
 | 
			
		||||
              res(true);
 | 
			
		||||
            });
 | 
			
		||||
          })
 | 
			
		||||
        ) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        port++;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return this;
 | 
			
		||||
    })();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onConnection(socket, req) {
 | 
			
		||||
    const params = parse(req.url.split('?')[1]);
 | 
			
		||||
    const ver = parseInt(params.v ?? 1);
 | 
			
		||||
    const encoding = params.encoding ?? 'json'; // Json | etf (erlpack)
 | 
			
		||||
    const clientId = params.client_id ?? '';
 | 
			
		||||
 | 
			
		||||
    const origin = req.headers.origin ?? '';
 | 
			
		||||
 | 
			
		||||
    if (this.debug) log(`new connection! origin:`, origin, JSON.parse(JSON.stringify(params)));
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      origin !== '' &&
 | 
			
		||||
      !['https://discord.com', 'https://ptb.discord.com', 'https://canary.discord.com/'].includes(origin)
 | 
			
		||||
    ) {
 | 
			
		||||
      if (this.debug) log('disallowed origin', origin);
 | 
			
		||||
 | 
			
		||||
      socket.close();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (encoding !== 'json') {
 | 
			
		||||
      if (this.debug) log('unsupported encoding requested', encoding);
 | 
			
		||||
 | 
			
		||||
      socket.close();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (ver !== 1) {
 | 
			
		||||
      if (this.debug) log('unsupported version requested', ver);
 | 
			
		||||
 | 
			
		||||
      socket.close();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket.clientId = clientId;
 | 
			
		||||
    socket.encoding = encoding;
 | 
			
		||||
 | 
			
		||||
    socket.on('error', e => {
 | 
			
		||||
      if (this.debug) log('socket error', e);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on('close', (e, r) => {
 | 
			
		||||
      if (this.debug) log('socket closed', e, r);
 | 
			
		||||
      this.handlers.close(socket);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    socket.on('message', this.onMessage.bind(this, socket));
 | 
			
		||||
 | 
			
		||||
    socket._send = socket.send;
 | 
			
		||||
    socket.send = msg => {
 | 
			
		||||
      if (this.debug) log('sending', msg);
 | 
			
		||||
      socket._send(JSON.stringify(msg));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.handlers.connection(socket);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMessage(socket, msg) {
 | 
			
		||||
    if (this.debug) log('message', JSON.parse(msg));
 | 
			
		||||
    this.handlers.message(socket, JSON.parse(msg));
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user