├── .eslintrc.js
├── .github
├── FUNDING.yml
└── workflows
│ └── docs.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── LICENSE
├── README.md
├── docgen.json
├── example
├── README.md
├── index.html
├── main.js
├── renderer.js
└── style.css
├── jsdoc.json
├── package.json
├── src
├── client.js
├── constants.js
├── index.js
├── transports
│ ├── index.js
│ ├── ipc.js
│ └── websocket.js
└── util.js
├── test
└── rp.js
└── webpack.config.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | root: true,
5 | extends: 'airbnb-base',
6 | parser: 'babel-eslint',
7 | parserOptions: {
8 | ecmaVersion: 2018,
9 | sourceType: 'script',
10 | },
11 | env: {
12 | es6: true,
13 | node: true,
14 | },
15 | overrides: [
16 | {
17 | files: ['*.jsx'],
18 | parserOptions: {
19 | sourceType: 'module',
20 | ecmaFeatures: { jsx: true },
21 | },
22 | },
23 | {
24 | files: ['*.mjs'],
25 | parserOptions: { sourceType: 'module' },
26 | env: {
27 | node: true,
28 | },
29 | rules: {
30 | 'no-restricted-globals': ['error', 'require'],
31 | },
32 | },
33 | {
34 | files: ['*.web.js'],
35 | env: { browser: true },
36 | },
37 | ],
38 | rules: {
39 | 'strict': ['error', 'global'],
40 | 'indent': ['error', 2, {
41 | SwitchCase: 1,
42 | FunctionDeclaration: {
43 | parameters: 'first',
44 | },
45 | FunctionExpression: {
46 | parameters: 'first',
47 | },
48 | CallExpression: {
49 | arguments: 'first',
50 | },
51 | }],
52 | 'no-bitwise': 'off',
53 | 'no-iterator': 'off',
54 | 'global-require': 'off',
55 | 'quote-props': ['error', 'consistent-as-needed'],
56 | 'brace-style': ['error', '1tbs', { allowSingleLine: false }],
57 | 'curly': ['error', 'all'],
58 | 'no-param-reassign': 'off',
59 | 'arrow-parens': ['error', 'always'],
60 | 'no-multi-assign': 'off',
61 | 'no-underscore-dangle': 'off',
62 | 'no-restricted-syntax': 'off',
63 | 'object-curly-newline': 'off',
64 | 'prefer-const': ['error', { destructuring: 'all' }],
65 | 'class-methods-use-this': 'off',
66 | 'implicit-arrow-linebreak': 'off',
67 | 'lines-between-class-members': 'off',
68 | 'import/no-dynamic-require': 'off',
69 | 'import/no-extraneous-dependencies': ['error', {
70 | devDependencies: true,
71 | }],
72 | 'import/extensions': 'off',
73 | 'import/prefer-default-export': 'off',
74 | 'import/no-unresolved': 'off',
75 | },
76 | globals: {
77 | WebAssembly: false,
78 | BigInt: false,
79 | BigInt64Array: false,
80 | BigUint64Array: false,
81 | URL: false,
82 | Atomics: false,
83 | SharedArrayBuffer: false,
84 | globalThis: false,
85 | FinalizationGroup: false,
86 | WeakRef: false,
87 | queueMicrotask: false,
88 | },
89 | };
90 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [devsnek] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with a single custom sponsorship URL
13 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-node@v1
12 | with:
13 | node-version: '15'
14 | - run: "npm install"
15 | - run: "npm run docs"
16 | - uses: JamesIves/github-pages-deploy-action@3.7.1
17 | with:
18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 | BRANCH: docs
20 | FOLDER: docs-out
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**
2 | package-lock.json
3 | browser.js
4 | test/auth.js
5 | docs-out
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | example
3 | test
4 | package-lock.json
5 | shrinkwrap.yml
6 | docs.json
7 | docgen.json
8 | jsdoc.json
9 | webpack.config.js
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 devsnek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | # Discord.js RPC Extension
15 |
16 | ### [Documentation](https://discord.js.org/#/docs/rpc/)
17 |
18 | ### [Rich Presence Example](https://github.com/discordjs/RPC/blob/master/example)
19 |
20 | ### __Browser__ Example
21 |
22 | ```javascript
23 | const clientId = '287406016902594560';
24 | const scopes = ['rpc', 'rpc.api', 'messages.read'];
25 |
26 | const client = new RPC.Client({ transport: 'websocket' });
27 |
28 | client.on('ready', () => {
29 | console.log('Logged in as', client.application.name);
30 | console.log('Authed for user', client.user.username);
31 |
32 | client.selectVoiceChannel('81384788862181376');
33 | });
34 |
35 | // Log in to RPC with client id
36 | client.login({ clientId, scopes });
37 | ```
38 |
--------------------------------------------------------------------------------
/docgen.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "General",
4 | "files": [
5 | {
6 | "path": "../README.md",
7 | "name": "Welcome",
8 | "id": "welcome"
9 | }
10 | ]
11 | }
12 | ]
13 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | To run this example simply run `npm run example`
2 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BOOP TEH SNEK (RPC Example)
6 |
7 |
8 |
9 | BOOP TEH SNEK
10 |
11 | 0 BOOPS
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/example/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* eslint-disable no-console */
4 |
5 | const { app, BrowserWindow } = require('electron');
6 | const path = require('path');
7 | const url = require('url');
8 | const DiscordRPC = require('../');
9 |
10 | let mainWindow;
11 |
12 | function createWindow() {
13 | mainWindow = new BrowserWindow({
14 | width: 340,
15 | height: 380,
16 | resizable: false,
17 | titleBarStyle: 'hidden',
18 | webPreferences: {
19 | nodeIntegration: true,
20 | },
21 | });
22 |
23 | mainWindow.loadURL(url.format({
24 | pathname: path.join(__dirname, 'index.html'),
25 | protocol: 'file:',
26 | slashes: true,
27 | }));
28 |
29 | mainWindow.on('closed', () => {
30 | mainWindow = null;
31 | });
32 | }
33 |
34 | app.on('ready', createWindow);
35 |
36 | app.on('window-all-closed', () => {
37 | app.quit();
38 | });
39 |
40 | app.on('activate', () => {
41 | if (mainWindow === null) {
42 | createWindow();
43 | }
44 | });
45 |
46 | // Set this to your Client ID.
47 | const clientId = '280984871685062656';
48 |
49 | // Only needed if you want to use spectate, join, or ask to join
50 | DiscordRPC.register(clientId);
51 |
52 | const rpc = new DiscordRPC.Client({ transport: 'ipc' });
53 | const startTimestamp = new Date();
54 |
55 | async function setActivity() {
56 | if (!rpc || !mainWindow) {
57 | return;
58 | }
59 |
60 | const boops = await mainWindow.webContents.executeJavaScript('window.boops');
61 |
62 | // You'll need to have snek_large and snek_small assets uploaded to
63 | // https://discord.com/developers/applications//rich-presence/assets
64 | rpc.setActivity({
65 | details: `booped ${boops} times`,
66 | state: 'in slither party',
67 | startTimestamp,
68 | largeImageKey: 'snek_large',
69 | largeImageText: 'tea is delicious',
70 | smallImageKey: 'snek_small',
71 | smallImageText: 'i am my own pillows',
72 | instance: false,
73 | });
74 | }
75 |
76 | rpc.on('ready', () => {
77 | setActivity();
78 |
79 | // activity can only be set every 15 seconds
80 | setInterval(() => {
81 | setActivity();
82 | }, 15e3);
83 | });
84 |
85 | rpc.login({ clientId }).catch(console.error);
86 |
--------------------------------------------------------------------------------
/example/renderer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* eslint-env browser */
4 |
5 | const { webFrame } = require('electron');
6 |
7 | const snek = document.getElementById('snek');
8 | const counter = document.getElementById('boops');
9 |
10 | webFrame.setVisualZoomLevelLimits(1, 1);
11 | webFrame.setLayoutZoomLevelLimits(0, 0);
12 |
13 | window.boops = 0;
14 | function boop() {
15 | window.boops += 1;
16 | counter.innerHTML = `${window.boops} BOOPS`;
17 | }
18 |
19 | snek.onmousedown = () => {
20 | snek.style['font-size'] = '550%';
21 | boop();
22 | };
23 |
24 | snek.onmouseup = () => {
25 | snek.style['font-size'] = '500%';
26 | };
27 |
--------------------------------------------------------------------------------
/example/style.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | margin: 0px;
3 | height: 100%;
4 | cursor: default;
5 | font-family: sans-serif;
6 | -webkit-touch-callout: none;
7 | -webkit-user-select: none;
8 | -khtml-user-select: none;
9 | -moz-user-select: none;
10 | -ms-user-select: none;
11 | user-select: none;
12 | -webkit-app-region: drag;
13 | }
14 |
15 | h1 {
16 | text-align: center;
17 | margin: auto;
18 | padding-top: 1em;
19 | }
20 |
21 | #game {
22 | height: 60%;
23 | display: grid;
24 | -webkit-app-region: no-drag;
25 | }
26 |
27 | #snek {
28 | cursor: pointer;
29 | font-size: 500%;
30 | margin: auto;
31 | }
32 |
33 | #boops {
34 | text-align: center;
35 | -webkit-app-region: no-drag;
36 | }
37 |
--------------------------------------------------------------------------------
/jsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["node_modules/jsdoc-strip-async-await"]
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discord-rpc",
3 | "version": "4.0.1",
4 | "description": "A simple RPC client for Discord",
5 | "keywords": [
6 | "discord",
7 | "rpc",
8 | "rich presence",
9 | "remote procedural call"
10 | ],
11 | "main": "src/index.js",
12 | "jsdelivr": "browser.js",
13 | "unpkg": "browser.js",
14 | "author": "snek ",
15 | "license": "MIT",
16 | "homepage": "https://github.com/discordjs/RPC#readme",
17 | "bugs": {
18 | "url": "https://github.com/discordjs/RPC/issues"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/discordjs/RPC.git"
23 | },
24 | "scripts": {
25 | "lint": "eslint src test --ext=js",
26 | "docs": "mkdir -p docs-out && docgen --source src --output docs-out/master.json --jsdoc jsdoc.json --custom docgen.json",
27 | "example": "electron example/main.js",
28 | "build:browser": "webpack-cli",
29 | "prepublishOnly": "npm run lint && npm run build:browser"
30 | },
31 | "dependencies": {
32 | "node-fetch": "^2.6.1",
33 | "ws": "^7.3.1"
34 | },
35 | "optionalDependencies": {
36 | "register-scheme": "github:devsnek/node-register-scheme"
37 | },
38 | "devDependencies": {
39 | "babel-eslint": "^10.0.3",
40 | "discord.js-docgen": "github:discordjs/docgen",
41 | "electron": "^7.1.9",
42 | "eslint": "^6.1.0",
43 | "eslint-config-airbnb-base": "14.0.0",
44 | "eslint-plugin-import": "^2.18.2",
45 | "jsdoc-strip-async-await": "^0.1.0",
46 | "webpack": "^4.40.0",
47 | "webpack-cli": "^3.3.8"
48 | },
49 | "browser": {
50 | "net": false,
51 | "ws": false,
52 | "uws": false,
53 | "erlpack": false,
54 | "electron": false,
55 | "register-scheme": false,
56 | "./src/transports/IPC.js": false
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/client.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const EventEmitter = require('events');
4 | const { setTimeout, clearTimeout } = require('timers');
5 | const fetch = require('node-fetch');
6 | const transports = require('./transports');
7 | const { RPCCommands, RPCEvents, RelationshipTypes } = require('./constants');
8 | const { pid: getPid, uuid } = require('./util');
9 |
10 | function subKey(event, args) {
11 | return `${event}${JSON.stringify(args)}`;
12 | }
13 |
14 | /**
15 | * @typedef {RPCClientOptions}
16 | * @extends {ClientOptions}
17 | * @prop {string} transport RPC transport. one of `ipc` or `websocket`
18 | */
19 |
20 | /**
21 | * The main hub for interacting with Discord RPC
22 | * @extends {BaseClient}
23 | */
24 | class RPCClient extends EventEmitter {
25 | /**
26 | * @param {RPCClientOptions} [options] Options for the client.
27 | * You must provide a transport
28 | */
29 | constructor(options = {}) {
30 | super();
31 |
32 | this.options = options;
33 |
34 | this.accessToken = null;
35 | this.clientId = null;
36 |
37 | /**
38 | * Application used in this client
39 | * @type {?ClientApplication}
40 | */
41 | this.application = null;
42 |
43 | /**
44 | * User used in this application
45 | * @type {?User}
46 | */
47 | this.user = null;
48 |
49 | const Transport = transports[options.transport];
50 | if (!Transport) {
51 | throw new TypeError('RPC_INVALID_TRANSPORT', options.transport);
52 | }
53 |
54 | this.fetch = (method, path, { data, query } = {}) =>
55 | fetch(`${this.fetch.endpoint}${path}${query ? new URLSearchParams(query) : ''}`, {
56 | method,
57 | body: data,
58 | headers: {
59 | Authorization: `Bearer ${this.accessToken}`,
60 | },
61 | }).then(async (r) => {
62 | const body = await r.json();
63 | if (!r.ok) {
64 | const e = new Error(r.status);
65 | e.body = body;
66 | throw e;
67 | }
68 | return body;
69 | });
70 |
71 | this.fetch.endpoint = 'https://discord.com/api';
72 |
73 | /**
74 | * Raw transport userd
75 | * @type {RPCTransport}
76 | * @private
77 | */
78 | this.transport = new Transport(this);
79 | this.transport.on('message', this._onRpcMessage.bind(this));
80 |
81 | /**
82 | * Map of nonces being expected from the transport
83 | * @type {Map}
84 | * @private
85 | */
86 | this._expecting = new Map();
87 |
88 | this._connectPromise = undefined;
89 | }
90 |
91 | /**
92 | * Search and connect to RPC
93 | */
94 | connect(clientId) {
95 | if (this._connectPromise) {
96 | return this._connectPromise;
97 | }
98 | this._connectPromise = new Promise((resolve, reject) => {
99 | this.clientId = clientId;
100 | const timeout = setTimeout(() => reject(new Error('RPC_CONNECTION_TIMEOUT')), 10e3);
101 | timeout.unref();
102 | this.once('connected', () => {
103 | clearTimeout(timeout);
104 | resolve(this);
105 | });
106 | this.transport.once('close', () => {
107 | this._expecting.forEach((e) => {
108 | e.reject(new Error('connection closed'));
109 | });
110 | this.emit('disconnected');
111 | reject(new Error('connection closed'));
112 | });
113 | this.transport.connect().catch(reject);
114 | });
115 | return this._connectPromise;
116 | }
117 |
118 | /**
119 | * @typedef {RPCLoginOptions}
120 | * @param {string} clientId Client ID
121 | * @param {string} [clientSecret] Client secret
122 | * @param {string} [accessToken] Access token
123 | * @param {string} [rpcToken] RPC token
124 | * @param {string} [tokenEndpoint] Token endpoint
125 | * @param {string[]} [scopes] Scopes to authorize with
126 | */
127 |
128 | /**
129 | * Performs authentication flow. Automatically calls Client#connect if needed.
130 | * @param {RPCLoginOptions} options Options for authentication.
131 | * At least one property must be provided to perform login.
132 | * @example client.login({ clientId: '1234567', clientSecret: 'abcdef123' });
133 | * @returns {Promise}
134 | */
135 | async login(options = {}) {
136 | let { clientId, accessToken } = options;
137 | await this.connect(clientId);
138 | if (!options.scopes) {
139 | this.emit('ready');
140 | return this;
141 | }
142 | if (!accessToken) {
143 | accessToken = await this.authorize(options);
144 | }
145 | return this.authenticate(accessToken);
146 | }
147 |
148 | /**
149 | * Request
150 | * @param {string} cmd Command
151 | * @param {Object} [args={}] Arguments
152 | * @param {string} [evt] Event
153 | * @returns {Promise}
154 | * @private
155 | */
156 | request(cmd, args, evt) {
157 | return new Promise((resolve, reject) => {
158 | const nonce = uuid();
159 | this.transport.send({ cmd, args, evt, nonce });
160 | this._expecting.set(nonce, { resolve, reject });
161 | });
162 | }
163 |
164 | /**
165 | * Message handler
166 | * @param {Object} message message
167 | * @private
168 | */
169 | _onRpcMessage(message) {
170 | if (message.cmd === RPCCommands.DISPATCH && message.evt === RPCEvents.READY) {
171 | if (message.data.user) {
172 | this.user = message.data.user;
173 | }
174 | this.emit('connected');
175 | } else if (this._expecting.has(message.nonce)) {
176 | const { resolve, reject } = this._expecting.get(message.nonce);
177 | if (message.evt === 'ERROR') {
178 | const e = new Error(message.data.message);
179 | e.code = message.data.code;
180 | e.data = message.data;
181 | reject(e);
182 | } else {
183 | resolve(message.data);
184 | }
185 | this._expecting.delete(message.nonce);
186 | } else {
187 | this.emit(message.evt, message.data);
188 | }
189 | }
190 |
191 | /**
192 | * Authorize
193 | * @param {Object} options options
194 | * @returns {Promise}
195 | * @private
196 | */
197 | async authorize({ scopes, clientSecret, rpcToken, redirectUri, prompt } = {}) {
198 | if (clientSecret && rpcToken === true) {
199 | const body = await this.fetch('POST', '/oauth2/token/rpc', {
200 | data: new URLSearchParams({
201 | client_id: this.clientId,
202 | client_secret: clientSecret,
203 | }),
204 | });
205 | rpcToken = body.rpc_token;
206 | }
207 |
208 | const { code } = await this.request('AUTHORIZE', {
209 | scopes,
210 | client_id: this.clientId,
211 | prompt,
212 | rpc_token: rpcToken,
213 | });
214 |
215 | const response = await this.fetch('POST', '/oauth2/token', {
216 | data: new URLSearchParams({
217 | client_id: this.clientId,
218 | client_secret: clientSecret,
219 | code,
220 | grant_type: 'authorization_code',
221 | redirect_uri: redirectUri,
222 | }),
223 | });
224 |
225 | return response.access_token;
226 | }
227 |
228 | /**
229 | * Authenticate
230 | * @param {string} accessToken access token
231 | * @returns {Promise}
232 | * @private
233 | */
234 | authenticate(accessToken) {
235 | return this.request('AUTHENTICATE', { access_token: accessToken })
236 | .then(({ application, user }) => {
237 | this.accessToken = accessToken;
238 | this.application = application;
239 | this.user = user;
240 | this.emit('ready');
241 | return this;
242 | });
243 | }
244 |
245 |
246 | /**
247 | * Fetch a guild
248 | * @param {Snowflake} id Guild ID
249 | * @param {number} [timeout] Timeout request
250 | * @returns {Promise}
251 | */
252 | getGuild(id, timeout) {
253 | return this.request(RPCCommands.GET_GUILD, { guild_id: id, timeout });
254 | }
255 |
256 | /**
257 | * Fetch all guilds
258 | * @param {number} [timeout] Timeout request
259 | * @returns {Promise>}
260 | */
261 | getGuilds(timeout) {
262 | return this.request(RPCCommands.GET_GUILDS, { timeout });
263 | }
264 |
265 | /**
266 | * Get a channel
267 | * @param {Snowflake} id Channel ID
268 | * @param {number} [timeout] Timeout request
269 | * @returns {Promise}
270 | */
271 | getChannel(id, timeout) {
272 | return this.request(RPCCommands.GET_CHANNEL, { channel_id: id, timeout });
273 | }
274 |
275 | /**
276 | * Get all channels
277 | * @param {Snowflake} [id] Guild ID
278 | * @param {number} [timeout] Timeout request
279 | * @returns {Promise>}
280 | */
281 | async getChannels(id, timeout) {
282 | const { channels } = await this.request(RPCCommands.GET_CHANNELS, {
283 | timeout,
284 | guild_id: id,
285 | });
286 | return channels;
287 | }
288 |
289 | /**
290 | * @typedef {CertifiedDevice}
291 | * @prop {string} type One of `AUDIO_INPUT`, `AUDIO_OUTPUT`, `VIDEO_INPUT`
292 | * @prop {string} uuid This device's Windows UUID
293 | * @prop {object} vendor Vendor information
294 | * @prop {string} vendor.name Vendor's name
295 | * @prop {string} vendor.url Vendor's url
296 | * @prop {object} model Model information
297 | * @prop {string} model.name Model's name
298 | * @prop {string} model.url Model's url
299 | * @prop {string[]} related Array of related product's Windows UUIDs
300 | * @prop {boolean} echoCancellation If the device has echo cancellation
301 | * @prop {boolean} noiseSuppression If the device has noise suppression
302 | * @prop {boolean} automaticGainControl If the device has automatic gain control
303 | * @prop {boolean} hardwareMute If the device has a hardware mute
304 | */
305 |
306 | /**
307 | * Tell discord which devices are certified
308 | * @param {CertifiedDevice[]} devices Certified devices to send to discord
309 | * @returns {Promise}
310 | */
311 | setCertifiedDevices(devices) {
312 | return this.request(RPCCommands.SET_CERTIFIED_DEVICES, {
313 | devices: devices.map((d) => ({
314 | type: d.type,
315 | id: d.uuid,
316 | vendor: d.vendor,
317 | model: d.model,
318 | related: d.related,
319 | echo_cancellation: d.echoCancellation,
320 | noise_suppression: d.noiseSuppression,
321 | automatic_gain_control: d.automaticGainControl,
322 | hardware_mute: d.hardwareMute,
323 | })),
324 | });
325 | }
326 |
327 | /**
328 | * @typedef {UserVoiceSettings}
329 | * @prop {Snowflake} id ID of the user these settings apply to
330 | * @prop {?Object} [pan] Pan settings, an object with `left` and `right` set between
331 | * 0.0 and 1.0, inclusive
332 | * @prop {?number} [volume=100] The volume
333 | * @prop {bool} [mute] If the user is muted
334 | */
335 |
336 | /**
337 | * Set the voice settings for a user, by id
338 | * @param {Snowflake} id ID of the user to set
339 | * @param {UserVoiceSettings} settings Settings
340 | * @returns {Promise}
341 | */
342 | setUserVoiceSettings(id, settings) {
343 | return this.request(RPCCommands.SET_USER_VOICE_SETTINGS, {
344 | user_id: id,
345 | pan: settings.pan,
346 | mute: settings.mute,
347 | volume: settings.volume,
348 | });
349 | }
350 |
351 | /**
352 | * Move the user to a voice channel
353 | * @param {Snowflake} id ID of the voice channel
354 | * @param {Object} [options] Options
355 | * @param {number} [options.timeout] Timeout for the command
356 | * @param {boolean} [options.force] Force this move. This should only be done if you
357 | * have explicit permission from the user.
358 | * @returns {Promise}
359 | */
360 | selectVoiceChannel(id, { timeout, force = false } = {}) {
361 | return this.request(RPCCommands.SELECT_VOICE_CHANNEL, { channel_id: id, timeout, force });
362 | }
363 |
364 | /**
365 | * Move the user to a text channel
366 | * @param {Snowflake} id ID of the voice channel
367 | * @param {Object} [options] Options
368 | * @param {number} [options.timeout] Timeout for the command
369 | * have explicit permission from the user.
370 | * @returns {Promise}
371 | */
372 | selectTextChannel(id, { timeout } = {}) {
373 | return this.request(RPCCommands.SELECT_TEXT_CHANNEL, { channel_id: id, timeout });
374 | }
375 |
376 | /**
377 | * Get current voice settings
378 | * @returns {Promise}
379 | */
380 | getVoiceSettings() {
381 | return this.request(RPCCommands.GET_VOICE_SETTINGS)
382 | .then((s) => ({
383 | automaticGainControl: s.automatic_gain_control,
384 | echoCancellation: s.echo_cancellation,
385 | noiseSuppression: s.noise_suppression,
386 | qos: s.qos,
387 | silenceWarning: s.silence_warning,
388 | deaf: s.deaf,
389 | mute: s.mute,
390 | input: {
391 | availableDevices: s.input.available_devices,
392 | device: s.input.device_id,
393 | volume: s.input.volume,
394 | },
395 | output: {
396 | availableDevices: s.output.available_devices,
397 | device: s.output.device_id,
398 | volume: s.output.volume,
399 | },
400 | mode: {
401 | type: s.mode.type,
402 | autoThreshold: s.mode.auto_threshold,
403 | threshold: s.mode.threshold,
404 | shortcut: s.mode.shortcut,
405 | delay: s.mode.delay,
406 | },
407 | }));
408 | }
409 |
410 | /**
411 | * Set current voice settings, overriding the current settings until this session disconnects.
412 | * This also locks the settings for any other rpc sessions which may be connected.
413 | * @param {Object} args Settings
414 | * @returns {Promise}
415 | */
416 | setVoiceSettings(args) {
417 | return this.request(RPCCommands.SET_VOICE_SETTINGS, {
418 | automatic_gain_control: args.automaticGainControl,
419 | echo_cancellation: args.echoCancellation,
420 | noise_suppression: args.noiseSuppression,
421 | qos: args.qos,
422 | silence_warning: args.silenceWarning,
423 | deaf: args.deaf,
424 | mute: args.mute,
425 | input: args.input ? {
426 | device_id: args.input.device,
427 | volume: args.input.volume,
428 | } : undefined,
429 | output: args.output ? {
430 | device_id: args.output.device,
431 | volume: args.output.volume,
432 | } : undefined,
433 | mode: args.mode ? {
434 | type: args.mode.type,
435 | auto_threshold: args.mode.autoThreshold,
436 | threshold: args.mode.threshold,
437 | shortcut: args.mode.shortcut,
438 | delay: args.mode.delay,
439 | } : undefined,
440 | });
441 | }
442 |
443 | /**
444 | * Capture a shortcut using the client
445 | * The callback takes (key, stop) where `stop` is a function that will stop capturing.
446 | * This `stop` function must be called before disconnecting or else the user will have
447 | * to restart their client.
448 | * @param {Function} callback Callback handling keys
449 | * @returns {Promise}
450 | */
451 | captureShortcut(callback) {
452 | const subid = subKey(RPCEvents.CAPTURE_SHORTCUT_CHANGE);
453 | const stop = () => {
454 | this._subscriptions.delete(subid);
455 | return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'STOP' });
456 | };
457 | this._subscriptions.set(subid, ({ shortcut }) => {
458 | callback(shortcut, stop);
459 | });
460 | return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'START' })
461 | .then(() => stop);
462 | }
463 |
464 | /**
465 | * Sets the presence for the logged in user.
466 | * @param {object} args The rich presence to pass.
467 | * @param {number} [pid] The application's process ID. Defaults to the executing process' PID.
468 | * @returns {Promise}
469 | */
470 | setActivity(args = {}, pid = getPid()) {
471 | let timestamps;
472 | let assets;
473 | let party;
474 | let secrets;
475 | if (args.startTimestamp || args.endTimestamp) {
476 | timestamps = {
477 | start: args.startTimestamp,
478 | end: args.endTimestamp,
479 | };
480 | if (timestamps.start instanceof Date) {
481 | timestamps.start = Math.round(timestamps.start.getTime());
482 | }
483 | if (timestamps.end instanceof Date) {
484 | timestamps.end = Math.round(timestamps.end.getTime());
485 | }
486 | if (timestamps.start > 2147483647000) {
487 | throw new RangeError('timestamps.start must fit into a unix timestamp');
488 | }
489 | if (timestamps.end > 2147483647000) {
490 | throw new RangeError('timestamps.end must fit into a unix timestamp');
491 | }
492 | }
493 | if (
494 | args.largeImageKey || args.largeImageText
495 | || args.smallImageKey || args.smallImageText
496 | ) {
497 | assets = {
498 | large_image: args.largeImageKey,
499 | large_text: args.largeImageText,
500 | small_image: args.smallImageKey,
501 | small_text: args.smallImageText,
502 | };
503 | }
504 | if (args.partySize || args.partyId || args.partyMax) {
505 | party = { id: args.partyId };
506 | if (args.partySize || args.partyMax) {
507 | party.size = [args.partySize, args.partyMax];
508 | }
509 | }
510 | if (args.matchSecret || args.joinSecret || args.spectateSecret) {
511 | secrets = {
512 | match: args.matchSecret,
513 | join: args.joinSecret,
514 | spectate: args.spectateSecret,
515 | };
516 | }
517 |
518 | return this.request(RPCCommands.SET_ACTIVITY, {
519 | pid,
520 | activity: {
521 | state: args.state,
522 | details: args.details,
523 | timestamps,
524 | assets,
525 | party,
526 | secrets,
527 | buttons: args.buttons,
528 | instance: !!args.instance,
529 | },
530 | });
531 | }
532 |
533 | /**
534 | * Clears the currently set presence, if any. This will hide the "Playing X" message
535 | * displayed below the user's name.
536 | * @param {number} [pid] The application's process ID. Defaults to the executing process' PID.
537 | * @returns {Promise}
538 | */
539 | clearActivity(pid = getPid()) {
540 | return this.request(RPCCommands.SET_ACTIVITY, {
541 | pid,
542 | });
543 | }
544 |
545 | /**
546 | * Invite a user to join the game the RPC user is currently playing
547 | * @param {User} user The user to invite
548 | * @returns {Promise}
549 | */
550 | sendJoinInvite(user) {
551 | return this.request(RPCCommands.SEND_ACTIVITY_JOIN_INVITE, {
552 | user_id: user.id || user,
553 | });
554 | }
555 |
556 | /**
557 | * Request to join the game the user is playing
558 | * @param {User} user The user whose game you want to request to join
559 | * @returns {Promise}
560 | */
561 | sendJoinRequest(user) {
562 | return this.request(RPCCommands.SEND_ACTIVITY_JOIN_REQUEST, {
563 | user_id: user.id || user,
564 | });
565 | }
566 |
567 | /**
568 | * Reject a join request from a user
569 | * @param {User} user The user whose request you wish to reject
570 | * @returns {Promise}
571 | */
572 | closeJoinRequest(user) {
573 | return this.request(RPCCommands.CLOSE_ACTIVITY_JOIN_REQUEST, {
574 | user_id: user.id || user,
575 | });
576 | }
577 |
578 | createLobby(type, capacity, metadata) {
579 | return this.request(RPCCommands.CREATE_LOBBY, {
580 | type,
581 | capacity,
582 | metadata,
583 | });
584 | }
585 |
586 | updateLobby(lobby, { type, owner, capacity, metadata } = {}) {
587 | return this.request(RPCCommands.UPDATE_LOBBY, {
588 | id: lobby.id || lobby,
589 | type,
590 | owner_id: (owner && owner.id) || owner,
591 | capacity,
592 | metadata,
593 | });
594 | }
595 |
596 | deleteLobby(lobby) {
597 | return this.request(RPCCommands.DELETE_LOBBY, {
598 | id: lobby.id || lobby,
599 | });
600 | }
601 |
602 | connectToLobby(id, secret) {
603 | return this.request(RPCCommands.CONNECT_TO_LOBBY, {
604 | id,
605 | secret,
606 | });
607 | }
608 |
609 | sendToLobby(lobby, data) {
610 | return this.request(RPCCommands.SEND_TO_LOBBY, {
611 | id: lobby.id || lobby,
612 | data,
613 | });
614 | }
615 |
616 | disconnectFromLobby(lobby) {
617 | return this.request(RPCCommands.DISCONNECT_FROM_LOBBY, {
618 | id: lobby.id || lobby,
619 | });
620 | }
621 |
622 | updateLobbyMember(lobby, user, metadata) {
623 | return this.request(RPCCommands.UPDATE_LOBBY_MEMBER, {
624 | lobby_id: lobby.id || lobby,
625 | user_id: user.id || user,
626 | metadata,
627 | });
628 | }
629 |
630 | getRelationships() {
631 | const types = Object.keys(RelationshipTypes);
632 | return this.request(RPCCommands.GET_RELATIONSHIPS)
633 | .then((o) => o.relationships.map((r) => ({
634 | ...r,
635 | type: types[r.type],
636 | })));
637 | }
638 |
639 | /**
640 | * Subscribe to an event
641 | * @param {string} event Name of event e.g. `MESSAGE_CREATE`
642 | * @param {Object} [args] Args for event e.g. `{ channel_id: '1234' }`
643 | * @returns {Promise