@hieuxyz/rpc 1.1.0 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/npm/l/@hieuxyz/rpc.svg)](https://github.com/hieuxyz/rpc/blob/main/LICENSE)
5
5
  [![Downloads](https://img.shields.io/npm/dt/@hieuxyz/rpc.svg)](https://www.npmjs.com/package/@hieuxyz/rpc)
6
6
 
7
- An easy-to-use Discord Rich Presence (RPC) library built for the Node.js environment using TypeScript. This library is designed to simplify the creation and management of custom RPC states for Discord user accounts.
7
+ An easy-to-use and powerful Discord Rich Presence (RPC) library built for the Node.js environment using TypeScript. This library is designed to simplify the creation and management of custom RPC states for Discord user accounts.
8
8
 
9
9
  > [!WARNING]
10
10
  > **I don't take any responsibility for blocked Discord accounts that used this module.**
@@ -17,7 +17,6 @@ An easy-to-use Discord Rich Presence (RPC) library built for the Node.js environ
17
17
 
18
18
  - **Flexible Builder Pattern:** Easily build your RPC state with intuitive chainable methods.
19
19
  - **Easy to use:** The `Client` class abstracts away all the complex connection and setup logic, letting you get started with just a few lines of code.
20
- - **Auto Resume:** Built-in auto reconnect and session recovery mechanism in case of network failure, ensuring your RPC is always stable.
21
20
 
22
21
  ## Install
23
22
 
@@ -65,17 +64,21 @@ async function start() {
65
64
  .setName("Visual Studio Code")
66
65
  .setDetails("Developing a new library")
67
66
  .setState("Workspace: @hieuxyz/rpc")
68
- .setPlatform('desktop') // 'desktop', 'xbox', 'ps5', etc.
67
+ .setPlatform('desktop')
69
68
  .setType(0) // 0: Playing
70
69
  .setTimestamps(Date.now())
71
70
  .setParty(1, 5)
72
71
  .setLargeImage("https://i.ibb.co/MDP0hfTM/typescript.png", "TypeScript")
73
- .setSmallImage(new LocalImage(path.join(__dirname, 'vscode.png')), "VS Code");
72
+ .setSmallImage(new LocalImage(path.join(__dirname, 'vscode.png')), "VS Code")
73
+ .setButtons([
74
+ { label: 'View on GitHub', url: 'https://github.com/hieuxyz00/hieuxyz_rpc' },
75
+ { label: 'View on NPM', url: 'https://www.npmjs.com/package/@hieuxyz/rpc' }
76
+ ]);
74
77
 
75
78
  await client.rpc.build();
76
79
  logger.info("Initial RPC has been set!");
77
80
 
78
- setTimeout(() => {
81
+ setTimeout(async () => {
79
82
  logger.info("Clearing RPC and resetting builder...");
80
83
  client.rpc.clear();
81
84
 
@@ -84,7 +87,7 @@ async function start() {
84
87
  .setDetails("Thinking about the next feature")
85
88
  .setLargeImage("mp:external/dZwPAoMNVxT5qYqecH3Mfgxv1RQEdtGBU8nAspOcAo4/https/c.tenor.com/fvuYGhI1vgUAAAAC/tenor.gif", "Coffee Time");
86
89
 
87
- client.rpc.build();
90
+ await client.rpc.build();
88
91
  logger.info("A new RPC has been set after clearing.");
89
92
 
90
93
  }, 20000);
@@ -102,11 +105,32 @@ start().catch(err => {
102
105
  });
103
106
  ```
104
107
 
108
+ ## Advanced Usage
109
+
110
+ ### Client Spoofing
111
+
112
+ You can make it appear as though you are using Discord from a different device (e.g., mobile) by providing the `properties` option during client initialization.
113
+
114
+ ```typescript
115
+ import { Client } from '@hieuxyz/rpc';
116
+
117
+ const client = new Client({
118
+ token: "YOUR_TOKEN",
119
+ properties: {
120
+ os: 'Android',
121
+ browser: 'Discord Android',
122
+ device: 'Android16',
123
+ }
124
+ });
125
+
126
+ // ...
127
+ ```
128
+
105
129
  ## Get Token ?
106
130
 
107
131
  - Based: [findByProps](https://discord.com/channels/603970300668805120/1085682686607249478/1085682686607249478)
108
132
 
109
- <strong>Run code (Discord Console - [Ctrl + Shift + I])</strong>
133
+ **Run code (Discord Console - [Ctrl + Shift + I])**
110
134
 
111
135
  ```js
112
136
  window.webpackChunkdiscord_app.push([
@@ -140,16 +164,22 @@ This is the main starting point.
140
164
  - `new Client(options)`: Create a new instance.
141
165
  - `options.token` (required): Your Discord user token.
142
166
  - `options.apiBaseUrl` (optional): Override the default image proxy service URL.
143
- - `options.alwaysReconnect` (optional): If `true`, the client will attempt to reconnect even after a normal close (e.g., from `client.close()` or a Discord-initiated close). Defaults to `false`.
167
+ - `options.alwaysReconnect` (optional): If `true`, the client will attempt to reconnect even after a normal close. Defaults to `false`.
168
+ - `options.properties` (optional): An object to spoof client properties (OS, browser, device).
169
+ - `options.connectionTimeout` (optional): Timeout in milliseconds for the initial connection. Defaults to `30000`.
144
170
  - `client.run()`: Start connecting to Discord Gateway.
145
171
  - `client.rpc`: Access the instance of `HieuxyzRPC` to build the state.
146
172
  - `client.close(force?: boolean)`: Closes the connection to the Discord Gateway.
147
- - `force` (optional, boolean): If set to `true`, the client will close permanently and will not attempt to reconnect, overriding the `alwaysReconnect` option. Defaults to `false`.
173
+ - `force` (optional, boolean): If `true`, the client closes permanently and will not reconnect.
148
174
 
149
175
  ### Class `HieuxyzRPC`
150
176
 
151
177
  Main builder class for RPC.
152
178
 
179
+ #### Getter Properties
180
+ - `.largeImageUrl`: Returns the resolved URL for the large image, or `null`.
181
+ - `.smallImageUrl`: Returns the resolved URL for the small image, or `null`.
182
+
153
183
  #### Setter Methods
154
184
  - `.setName(string)`: Sets the activity name (first line).
155
185
  - `.setDetails(string)`: Sets the activity details (second line).
@@ -158,33 +188,37 @@ Main builder class for RPC.
158
188
  - `.setParty(current, max)`: Sets the party information.
159
189
  - `.setLargeImage(RpcImage, text?)`: Sets the large image and its tooltip text.
160
190
  - `.setSmallImage(RpcImage, text?)`: Sets the small image and its tooltip text.
161
- - `.setButtons(buttons[])`: Sets up to two clickable buttons.
191
+ - `.setButtons(buttons[])`: Sets up to two clickable buttons. Each button is an object `{ label: string, url: string }`.
192
+ - `.setSecrets({ join?, spectate?, match? })`: Sets secrets for game invites.
193
+ - `.setSyncId(string)`: Sets the sync ID, used for features like Spotify track syncing.
194
+ - `.setFlags(number)`: Sets activity flags (e.g., for instanced games). Use the `ActivityFlags` enum.
162
195
  - `.setPlatform(platform)`: Sets the platform (`'desktop'`, `'xbox'`, etc.).
163
196
  - `.setInstance(boolean)`: Marks the activity as a specific, joinable instance.
164
197
  - `.setApplicationId(string)`: Sets a custom Application ID.
165
198
  - `.setStatus('online' | ...)`: Sets the user's presence status.
166
199
 
167
200
  #### Clearer Methods
168
- - `.clearDetails()`: Removes the activity details.
169
- - `.clearState()`: Removes the activity state.
170
- - `.clearTimestamps()`: Removes the timestamps.
171
- - `.clearParty()`: Removes the party information.
172
- - `.clearLargeImage()`: Removes the large image and its text.
173
- - `.clearSmallImage()`: Removes the small image and its text.
201
+ - `.clearDetails()`: Removes activity details.
202
+ - `.clearState()`: Removes activity state.
203
+ - `.clearTimestamps()`: Removes timestamps.
204
+ - `.clearParty()`: Removes party information.
205
+ - `.clearLargeImage()`: Removes the large image and text.
206
+ - `.clearSmallImage()`: Removes the small image and text.
174
207
  - `.clearButtons()`: Removes all buttons.
208
+ - `.clearSecrets()`: Removes all secrets.
175
209
  - `.clearInstance()`: Removes the instance flag.
176
210
 
177
211
  #### Core Methods
178
212
  - `.build()`: Builds and sends the presence payload to Discord.
179
- - `.updateRPC()`: Builds and sends an updated presence payload. (Alias for `build()`).
180
- - `.clear()`: Clears the entire Rich Presence from the user's profile and resets the builder to its default state.
213
+ - `.updateRPC()`: Alias for `build()`.
214
+ - `.clear()`: Clears the Rich Presence from the user's profile and resets the builder.
181
215
 
182
216
  ### Types of images
183
217
 
184
- - `new ExternalImage(url)`: Use image from an external URL (will be proxy).
218
+ - `new ExternalImage(url)`: Use an image from an external URL (will be proxied).
185
219
  - `new LocalImage(filePath, fileName?)`: Upload a photo from your device.
186
- - `new RawImage(assetKey)`: Use an existing asset key directly.
187
- - `new DiscordImage(key)`: Use assets already on Discord.
220
+ - `new RawImage(assetKey)`: Use an existing asset key directly (e.g., `spotify:track_id`).
221
+ - `new DiscordImage(key)`: Use assets already on Discord (e.g., `mp:attachments/...`).
188
222
 
189
223
  ## Author
190
224
 
@@ -1,4 +1,5 @@
1
1
  import { HieuxyzRPC } from './rpc/HieuxyzRPC';
2
+ import { ClientProperties } from './gateway/entities/identify';
2
3
  /**
3
4
  * Option to initialize Client.
4
5
  */
@@ -12,6 +13,16 @@ export interface ClientOptions {
12
13
  * Defaults to false.
13
14
  */
14
15
  alwaysReconnect?: boolean;
16
+ /**
17
+ * (Optional) Client properties to send to Discord gateway.
18
+ * Used for client spoofing (e.g., appearing as on mobile).
19
+ */
20
+ properties?: ClientProperties;
21
+ /**
22
+ * (Optional) The timeout in milliseconds for the initial gateway connection.
23
+ * Defaults to 30000 (30 seconds).
24
+ */
25
+ connectionTimeout?: number;
15
26
  }
16
27
  /**
17
28
  * The main Client class for interacting with Discord Rich Presence.
@@ -28,7 +39,7 @@ export interface ClientOptions {
28
39
  export declare class Client {
29
40
  /**
30
41
  * Provides access to RPC constructor methods.
31
- * Use this property to set your Rich Presence state details.
42
+ * Use this to set your Rich Presence state details.
32
43
  */
33
44
  readonly rpc: HieuxyzRPC;
34
45
  private readonly websocket;
@@ -36,7 +47,7 @@ export declare class Client {
36
47
  private readonly token;
37
48
  /**
38
49
  * Create a new Client instance.
39
- * @param options - Options to configure the client.
50
+ * @param {ClientOptions} options - Options to configure the client.
40
51
  * @throws {Error} If no token is provided in the options.
41
52
  */
42
53
  constructor(options: ClientOptions);
@@ -20,7 +20,7 @@ const logger_1 = require("./utils/logger");
20
20
  class Client {
21
21
  /**
22
22
  * Provides access to RPC constructor methods.
23
- * Use this property to set your Rich Presence state details.
23
+ * Use this to set your Rich Presence state details.
24
24
  */
25
25
  rpc;
26
26
  websocket;
@@ -28,7 +28,7 @@ class Client {
28
28
  token;
29
29
  /**
30
30
  * Create a new Client instance.
31
- * @param options - Options to configure the client.
31
+ * @param {ClientOptions} options - Options to configure the client.
32
32
  * @throws {Error} If no token is provided in the options.
33
33
  */
34
34
  constructor(options) {
@@ -39,6 +39,8 @@ class Client {
39
39
  this.imageService = new ImageService_1.ImageService(options.apiBaseUrl);
40
40
  this.websocket = new DiscordWebSocket_1.DiscordWebSocket(this.token, {
41
41
  alwaysReconnect: options.alwaysReconnect ?? false,
42
+ properties: options.properties,
43
+ connectionTimeout: options.connectionTimeout,
42
44
  });
43
45
  this.rpc = new HieuxyzRPC_1.HieuxyzRPC(this.websocket, this.imageService);
44
46
  }
@@ -1,6 +1,9 @@
1
+ import { ClientProperties } from './entities/identify';
1
2
  import { PresenceUpdatePayload } from './entities/types';
2
3
  interface DiscordWebSocketOptions {
3
4
  alwaysReconnect: boolean;
5
+ properties?: ClientProperties;
6
+ connectionTimeout?: number;
4
7
  }
5
8
  /**
6
9
  * Manage WebSocket connections to Discord Gateway.
@@ -17,6 +20,7 @@ export declare class DiscordWebSocket {
17
20
  private options;
18
21
  private isReconnecting;
19
22
  private permanentClose;
23
+ private connectTimeout;
20
24
  private resolveReady;
21
25
  /**
22
26
  * A promise will be resolved when the Gateway connection is ready.
@@ -25,8 +29,8 @@ export declare class DiscordWebSocket {
25
29
  readyPromise: Promise<void>;
26
30
  /**
27
31
  * Create a DiscordWebSocket instance.
28
- * @param token - Discord user token for authentication.
29
- * @param options - Configuration options for the WebSocket client.
32
+ * @param {string} token - Discord user token for authentication.
33
+ * @param {DiscordWebSocketOptions} options - Configuration options for the WebSocket client.
30
34
  * @throws {Error} If the token is invalid.
31
35
  */
32
36
  constructor(token: string, options: DiscordWebSocketOptions);
@@ -37,6 +41,7 @@ export declare class DiscordWebSocket {
37
41
  * If there was a previous session, it will try to resume.
38
42
  */
39
43
  connect(): void;
44
+ private handleClose;
40
45
  private onMessage;
41
46
  private startHeartbeating;
42
47
  private sendHeartbeat;
@@ -44,13 +49,13 @@ export declare class DiscordWebSocket {
44
49
  private resume;
45
50
  /**
46
51
  * Send presence update payload to Gateway.
47
- * @param presence - Payload update status to send.
52
+ * @param {PresenceUpdatePayload} presence - Payload update status to send.
48
53
  */
49
54
  sendActivity(presence: PresenceUpdatePayload): void;
50
55
  private sendJson;
51
56
  /**
52
57
  * Closes the WebSocket connection.
53
- * @param force If true, prevents any automatic reconnection attempts.
58
+ * @param {boolean} force If true, prevents any automatic reconnection attempts.
54
59
  */
55
60
  close(force?: boolean): void;
56
61
  private cleanupHeartbeat;
@@ -57,6 +57,7 @@ class DiscordWebSocket {
57
57
  options;
58
58
  isReconnecting = false;
59
59
  permanentClose = false;
60
+ connectTimeout = null;
60
61
  resolveReady = () => { };
61
62
  /**
62
63
  * A promise will be resolved when the Gateway connection is ready.
@@ -65,8 +66,8 @@ class DiscordWebSocket {
65
66
  readyPromise;
66
67
  /**
67
68
  * Create a DiscordWebSocket instance.
68
- * @param token - Discord user token for authentication.
69
- * @param options - Configuration options for the WebSocket client.
69
+ * @param {string} token - Discord user token for authentication.
70
+ * @param {DiscordWebSocketOptions} options - Configuration options for the WebSocket client.
70
71
  * @throws {Error} If the token is invalid.
71
72
  */
72
73
  constructor(token, options) {
@@ -74,7 +75,11 @@ class DiscordWebSocket {
74
75
  throw new Error('Invalid token provided.');
75
76
  }
76
77
  this.token = token;
77
- this.options = options;
78
+ this.options = {
79
+ alwaysReconnect: options.alwaysReconnect ?? false,
80
+ properties: options.properties,
81
+ connectionTimeout: options.connectionTimeout ?? 30000,
82
+ };
78
83
  this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve));
79
84
  }
80
85
  resetReadyPromise() {
@@ -99,39 +104,53 @@ class DiscordWebSocket {
99
104
  const url = this.resumeGatewayUrl || 'wss://gateway.discord.gg/?v=10&encoding=json';
100
105
  logger_1.logger.info(`Attempting to connect to ${url}...`);
101
106
  this.ws = new ws_1.default(url);
107
+ this.connectTimeout = setTimeout(() => {
108
+ logger_1.logger.error('Connection timed out. Terminating connection attempt.');
109
+ if (this.ws) {
110
+ this.ws.terminate();
111
+ }
112
+ }, this.options.connectionTimeout);
102
113
  this.ws.on('open', () => {
103
114
  logger_1.logger.info(`Successfully connected to Discord Gateway at ${url}.`);
104
115
  this.isReconnecting = false;
105
- });
106
- this.ws.on('message', this.onMessage.bind(this));
107
- this.ws.on('close', (code, reason) => {
108
- logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString('utf-8')}`);
109
- this.cleanupHeartbeat();
110
- if (this.permanentClose) {
111
- logger_1.logger.info('Connection permanently closed by client. Not reconnecting.');
112
- return;
113
- }
114
- if (this.isReconnecting)
115
- return;
116
- if (this.shouldReconnect(code)) {
117
- setTimeout(() => {
118
- const canResume = code !== 4004 && !!this.sessionId;
119
- if (!canResume) {
120
- this.sessionId = null;
121
- this.sequence = null;
122
- this.resumeGatewayUrl = null;
123
- }
124
- this.connect();
125
- }, 500);
126
- }
127
- else {
128
- logger_1.logger.info('Not attempting to reconnect based on close code and client options.');
116
+ if (this.connectTimeout) {
117
+ clearTimeout(this.connectTimeout);
118
+ this.connectTimeout = null;
129
119
  }
130
120
  });
121
+ this.ws.on('message', this.onMessage.bind(this));
122
+ this.ws.on('close', this.handleClose.bind(this));
131
123
  this.ws.on('error', (err) => {
132
- logger_1.logger.error(`WebSocket Error: ${err.message}`);
124
+ if (err.message !== 'WebSocket was closed before the connection was established') {
125
+ logger_1.logger.error(`WebSocket Error: ${err.message}`);
126
+ }
133
127
  });
134
128
  }
129
+ handleClose(code, reason) {
130
+ logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString('utf-8')}`);
131
+ this.cleanupHeartbeat();
132
+ if (this.connectTimeout) {
133
+ clearTimeout(this.connectTimeout);
134
+ this.connectTimeout = null;
135
+ }
136
+ this.isReconnecting = false;
137
+ if (code === 4004) {
138
+ this.sessionId = null;
139
+ this.sequence = null;
140
+ this.resumeGatewayUrl = null;
141
+ }
142
+ if (this.permanentClose) {
143
+ logger_1.logger.info('Connection permanently closed by client. Not reconnecting.');
144
+ return;
145
+ }
146
+ if (this.shouldReconnect(code)) {
147
+ logger_1.logger.info('Attempting to reconnect in 5 seconds...');
148
+ setTimeout(() => this.connect(), 5000);
149
+ }
150
+ else {
151
+ logger_1.logger.info('Not attempting to reconnect based on close code and client options.');
152
+ }
153
+ }
135
154
  onMessage(data, isBinary) {
136
155
  let decompressedData;
137
156
  if (isBinary) {
@@ -146,6 +165,10 @@ class DiscordWebSocket {
146
165
  }
147
166
  switch (payload.op) {
148
167
  case OpCode_1.OpCode.HELLO:
168
+ if (this.connectTimeout) {
169
+ clearTimeout(this.connectTimeout);
170
+ this.connectTimeout = null;
171
+ }
149
172
  this.heartbeatIntervalValue = payload.d.heartbeat_interval;
150
173
  logger_1.logger.info(`Received HELLO. Setting heartbeat interval to ${this.heartbeatIntervalValue}ms.`);
151
174
  this.startHeartbeating();
@@ -211,7 +234,7 @@ class DiscordWebSocket {
211
234
  logger_1.logger.info(`Heartbeat sent with sequence ${this.sequence}.`);
212
235
  }
213
236
  identify() {
214
- const identifyPayload = (0, identify_1.getIdentifyPayload)(this.token);
237
+ const identifyPayload = (0, identify_1.getIdentifyPayload)(this.token, this.options.properties);
215
238
  this.sendJson({ op: OpCode_1.OpCode.IDENTIFY, d: identifyPayload });
216
239
  logger_1.logger.info('Identify payload sent.');
217
240
  }
@@ -231,7 +254,7 @@ class DiscordWebSocket {
231
254
  }
232
255
  /**
233
256
  * Send presence update payload to Gateway.
234
- * @param presence - Payload update status to send.
257
+ * @param {PresenceUpdatePayload} presence - Payload update status to send.
235
258
  */
236
259
  sendActivity(presence) {
237
260
  this.sendJson({ op: OpCode_1.OpCode.PRESENCE_UPDATE, d: presence });
@@ -247,7 +270,7 @@ class DiscordWebSocket {
247
270
  }
248
271
  /**
249
272
  * Closes the WebSocket connection.
250
- * @param force If true, prevents any automatic reconnection attempts.
273
+ * @param {boolean} force If true, prevents any automatic reconnection attempts.
251
274
  */
252
275
  close(force = false) {
253
276
  if (force) {
@@ -257,9 +280,13 @@ class DiscordWebSocket {
257
280
  else {
258
281
  logger_1.logger.info('Closing connection manually...');
259
282
  }
260
- this.isReconnecting = false;
261
283
  if (this.ws) {
262
- this.ws.close(1000, 'Client initiated closure');
284
+ if (this.ws.readyState === ws_1.default.OPEN) {
285
+ this.ws.close(1000, 'Client initiated closure');
286
+ }
287
+ else {
288
+ this.ws.terminate();
289
+ }
263
290
  }
264
291
  }
265
292
  cleanupHeartbeat() {
@@ -269,7 +296,9 @@ class DiscordWebSocket {
269
296
  }
270
297
  }
271
298
  shouldReconnect(code) {
272
- const fatalErrorCodes = [4010, 4011, 4013, 4014];
299
+ if (code === 1006)
300
+ return true;
301
+ const fatalErrorCodes = [4004, 4010, 4011, 4013, 4014];
273
302
  if (fatalErrorCodes.includes(code)) {
274
303
  logger_1.logger.error(`Fatal WebSocket error received (code: ${code}). Will not reconnect.`);
275
304
  return false;
@@ -1,2 +1,13 @@
1
1
  import { IdentifyPayload } from './types';
2
- export declare function getIdentifyPayload(token: string): IdentifyPayload;
2
+ /**
3
+ * @typedef {object} ClientProperties
4
+ * @property {string} [os] - The operating system. (e.g., 'Windows', 'Android')
5
+ * @property {string} [browser] - The browser or client. (e.g., 'Discord Client', 'Discord Android')
6
+ * @property {string} [device] - The device. (e.g., 'Android16')
7
+ */
8
+ export interface ClientProperties {
9
+ os?: string;
10
+ browser?: string;
11
+ device?: string;
12
+ }
13
+ export declare function getIdentifyPayload(token: string, properties?: ClientProperties): IdentifyPayload;
@@ -1,16 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getIdentifyPayload = getIdentifyPayload;
4
- function getIdentifyPayload(token) {
4
+ function getIdentifyPayload(token, properties) {
5
+ const defaultProperties = {
6
+ os: 'Windows',
7
+ browser: 'Discord Client',
8
+ device: 'hieuxyz©rpc',
9
+ };
5
10
  return {
6
11
  token: token,
7
12
  capabilities: 65,
8
13
  largeThreshold: 50,
9
- properties: {
10
- os: 'Windows',
11
- browser: 'Discord Client',
12
- device: 'hieuxyz©rpc',
13
- },
14
+ properties: { ...defaultProperties, ...properties },
14
15
  compress: true,
15
16
  };
16
17
  }
@@ -15,15 +15,16 @@ export interface GatewayPayload {
15
15
  s?: number | null;
16
16
  t?: string | null;
17
17
  }
18
+ export interface IdentifyProperties {
19
+ os: string;
20
+ browser: string;
21
+ device: string;
22
+ }
18
23
  export interface IdentifyPayload {
19
24
  token: string;
20
25
  capabilities: number;
21
26
  largeThreshold: number;
22
- properties: {
23
- os: string;
24
- browser: string;
25
- device: string;
26
- };
27
+ properties: IdentifyProperties;
27
28
  compress: boolean;
28
29
  }
29
30
  export interface Activity {
@@ -34,6 +35,8 @@ export interface Activity {
34
35
  state?: string;
35
36
  platform?: string;
36
37
  instance?: boolean;
38
+ flags?: number;
39
+ sync_id?: string;
37
40
  party?: {
38
41
  id?: string;
39
42
  size?: [number, number];
@@ -48,6 +51,11 @@ export interface Activity {
48
51
  small_image?: string;
49
52
  small_text?: string;
50
53
  };
54
+ secrets?: {
55
+ join?: string;
56
+ spectate?: string;
57
+ match?: string;
58
+ };
51
59
  buttons?: string[];
52
60
  metadata?: {
53
61
  button_urls?: string[];
@@ -2,10 +2,27 @@ import { DiscordWebSocket } from '../gateway/DiscordWebSocket';
2
2
  import { SettableActivityType } from '../gateway/entities/types';
3
3
  import { ImageService } from './ImageService';
4
4
  import { RpcImage } from './RpcImage';
5
+ /**
6
+ * Flags for activities, used with `.setFlags()`.
7
+ * @enum {number}
8
+ */
9
+ export declare enum ActivityFlags {
10
+ INSTANCE = 1,
11
+ JOIN = 2,
12
+ SPECTATE = 4,
13
+ JOIN_REQUEST = 8,
14
+ SYNC = 16,
15
+ PLAY = 32
16
+ }
5
17
  interface RpcButton {
6
18
  label: string;
7
19
  url: string;
8
20
  }
21
+ interface RpcSecrets {
22
+ join?: string;
23
+ spectate?: string;
24
+ match?: string;
25
+ }
9
26
  export type DiscordPlatform = 'desktop' | 'android' | 'ios' | 'samsung' | 'xbox' | 'ps4' | 'ps5' | 'embedded';
10
27
  /**
11
28
  * Class built for creating and managing Discord Rich Presence states.
@@ -26,89 +43,120 @@ export declare class HieuxyzRPC {
26
43
  private resolvedAssetsCache;
27
44
  private renewalInterval;
28
45
  constructor(websocket: DiscordWebSocket, imageService: ImageService);
46
+ /**
47
+ * Returns the URL of the large image asset, if available.
48
+ * @type {string | null}
49
+ * @readonly
50
+ */
51
+ get largeImageUrl(): string | null;
52
+ /**
53
+ * Returns the URL of the small image asset, if available.
54
+ * @type {string | null}
55
+ * @readonly
56
+ */
57
+ get smallImageUrl(): string | null;
58
+ private _resolveAssetUrl;
29
59
  private _toRpcImage;
30
60
  private cleanupNulls;
31
61
  private sanitize;
32
62
  /**
33
63
  * Name the operation (first line of RPC).
34
- * @param name - Name to display.
64
+ * @param {string} name - Name to display.
35
65
  * @returns {this}
36
66
  */
37
67
  setName(name: string): this;
38
68
  /**
39
69
  * Set details for the operation (second line of RPC).
40
- * @param details - Details to display.
70
+ * @param {string} details - Details to display.
41
71
  * @returns {this}
42
72
  */
43
73
  setDetails(details: string): this;
44
74
  /**
45
75
  * Set the state for the operation (third line of the RPC).
46
- * @param state - State to display.
76
+ * @param {string} state - State to display.
47
77
  * @returns {this}
48
78
  */
49
79
  setState(state: string): this;
50
80
  /**
51
81
  * Set the activity type.
52
- * @param type - The type of activity (e.g. 0, 'playing', or ActivityType.Playing).
82
+ * @param {SettableActivityType} type - The type of activity (e.g. 0, 'playing', or ActivityType.Playing).
53
83
  * @returns {this}
54
84
  */
55
85
  setType(type: SettableActivityType): this;
56
86
  /**
57
87
  * Set a start and/or end timestamp for the activity.
58
- * @param start - Unix timestamp (milliseconds) for start time.
59
- * @param end - Unix timestamp (milliseconds) for the end time.
88
+ * @param {number} [start] - Unix timestamp (milliseconds) for start time.
89
+ * @param {number} [end] - Unix timestamp (milliseconds) for the end time.
60
90
  * @returns {this}
61
91
  */
62
92
  setTimestamps(start?: number, end?: number): this;
63
93
  /**
64
94
  * Set party information for the activity.
65
- * @param currentSize - Current number of players.
66
- * @param maxSize - Maximum number of players.
95
+ * @param {number} currentSize - Current number of players.
96
+ * @param {number} maxSize - Maximum number of players.
67
97
  * @returns {this}
68
98
  */
69
99
  setParty(currentSize: number, maxSize: number): this;
70
100
  /**
71
101
  * Set large image and its caption text.
72
- * @param source - Image source (URL, asset key, or RpcImage object).
73
- * @param text - Text displayed when hovering over image.
102
+ * @param {string | RpcImage} source - Image source (URL, asset key, or RpcImage object).
103
+ * @param {string} [text] - Text displayed when hovering over image.
74
104
  * @returns {this}
75
105
  */
76
106
  setLargeImage(source: string | RpcImage, text?: string): this;
77
107
  /**
78
108
  * Set the small image and its caption text.
79
- * @param source - Image source (URL, asset key, or RpcImage object).
80
- * @param text - Text displayed when hovering over image.
109
+ * @param {string | RpcImage} source - Image source (URL, asset key, or RpcImage object).
110
+ * @param {string} [text] - Text displayed when hovering over image.
81
111
  * @returns {this}
82
112
  */
83
113
  setSmallImage(source: string | RpcImage, text?: string): this;
84
114
  /**
85
115
  * Set clickable buttons for RPC (up to 2).
86
- * @param buttons - An array of button objects.
116
+ * @param {RpcButton[]} buttons - An array of button objects, each with a `label` and `url`.
87
117
  * @returns {this}
88
118
  */
89
119
  setButtons(buttons: RpcButton[]): this;
120
+ /**
121
+ * Set secrets for joining, spectating, and matching games.
122
+ * @param {RpcSecrets} secrets - An object with join, spectate, and/or match secrets.
123
+ * @returns {this}
124
+ */
125
+ setSecrets(secrets: RpcSecrets): this;
126
+ /**
127
+ * Set the sync_id, typically used for Spotify track synchronization.
128
+ * @param {string} syncId - The synchronization ID.
129
+ * @returns {this}
130
+ */
131
+ setSyncId(syncId: string): this;
132
+ /**
133
+ * Set activity flags. Use the ActivityFlags enum for convenience.
134
+ * @param {number} flags - A number representing the bitwise flags.
135
+ * @returns {this}
136
+ */
137
+ setFlags(flags: number): this;
90
138
  /**
91
139
  * Set custom application ID for RPC.
92
- * @param id - Discord app ID (must be an 18 or 19 digit number string).
140
+ * @param {string} id - Discord app ID (must be an 18 or 19 digit number string).
93
141
  * @throws {Error} If ID is invalid.
94
142
  * @returns {this}
95
143
  */
96
144
  setApplicationId(id: string): this;
97
145
  /**
98
146
  * Set the user's status (e.g. online, idle, dnd).
99
- * @param status - Desired state.
147
+ * @param {'online' | 'dnd' | 'idle' | 'invisible' | 'offline'} status - Desired state.
100
148
  * @returns {this}
101
149
  */
102
150
  setStatus(status: 'online' | 'dnd' | 'idle' | 'invisible' | 'offline'): this;
103
151
  /**
104
152
  * Set the platform on which the activity is running.
105
- * @param platform - Platform (e.g. 'desktop', 'xbox').
153
+ * @param {DiscordPlatform} platform - Platform (e.g. 'desktop', 'xbox').
106
154
  * @returns {this}
107
155
  */
108
156
  setPlatform(platform: DiscordPlatform): this;
109
157
  /**
110
158
  * Marks the activity as a joinable instance for the game.
111
- * @param instance - Whether this activity is a specific instance.
159
+ * @param {boolean} instance - Whether this activity is a specific instance.
112
160
  * @returns {this}
113
161
  */
114
162
  setInstance(instance: boolean): this;
@@ -117,6 +165,7 @@ export declare class HieuxyzRPC {
117
165
  clearTimestamps(): this;
118
166
  clearParty(): this;
119
167
  clearButtons(): this;
168
+ clearSecrets(): this;
120
169
  clearInstance(): this;
121
170
  clearLargeImage(): this;
122
171
  clearSmallImage(): this;
@@ -1,9 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HieuxyzRPC = void 0;
3
+ exports.HieuxyzRPC = exports.ActivityFlags = void 0;
4
4
  const types_1 = require("../gateway/entities/types");
5
5
  const RpcImage_1 = require("./RpcImage");
6
6
  const logger_1 = require("../utils/logger");
7
+ /**
8
+ * Flags for activities, used with `.setFlags()`.
9
+ * @enum {number}
10
+ */
11
+ var ActivityFlags;
12
+ (function (ActivityFlags) {
13
+ ActivityFlags[ActivityFlags["INSTANCE"] = 1] = "INSTANCE";
14
+ ActivityFlags[ActivityFlags["JOIN"] = 2] = "JOIN";
15
+ ActivityFlags[ActivityFlags["SPECTATE"] = 4] = "SPECTATE";
16
+ ActivityFlags[ActivityFlags["JOIN_REQUEST"] = 8] = "JOIN_REQUEST";
17
+ ActivityFlags[ActivityFlags["SYNC"] = 16] = "SYNC";
18
+ ActivityFlags[ActivityFlags["PLAY"] = 32] = "PLAY";
19
+ })(ActivityFlags || (exports.ActivityFlags = ActivityFlags = {}));
7
20
  /**
8
21
  * Class built for creating and managing Discord Rich Presence states.
9
22
  */
@@ -13,7 +26,7 @@ class HieuxyzRPC {
13
26
  activity = {};
14
27
  assets = {};
15
28
  status = 'online';
16
- applicationId = '1416676323459469363'; // Default ID, can be changed
29
+ applicationId = '1416676323459469363';
17
30
  platform = 'desktop';
18
31
  /**
19
32
  * Cache for resolved image assets to avoid re-uploading or re-fetching.
@@ -27,6 +40,49 @@ class HieuxyzRPC {
27
40
  this.imageService = imageService;
28
41
  this.startBackgroundRenewal();
29
42
  }
43
+ /**
44
+ * Returns the URL of the large image asset, if available.
45
+ * @type {string | null}
46
+ * @readonly
47
+ */
48
+ get largeImageUrl() {
49
+ if (!this.assets.large_image)
50
+ return null;
51
+ const cacheKey = this.assets.large_image.getCacheKey();
52
+ const resolvedAsset = this.resolvedAssetsCache.get(cacheKey);
53
+ return resolvedAsset ? this._resolveAssetUrl(resolvedAsset) : null;
54
+ }
55
+ /**
56
+ * Returns the URL of the small image asset, if available.
57
+ * @type {string | null}
58
+ * @readonly
59
+ */
60
+ get smallImageUrl() {
61
+ if (!this.assets.small_image)
62
+ return null;
63
+ const cacheKey = this.assets.small_image.getCacheKey();
64
+ const resolvedAsset = this.resolvedAssetsCache.get(cacheKey);
65
+ return resolvedAsset ? this._resolveAssetUrl(resolvedAsset) : null;
66
+ }
67
+ _resolveAssetUrl(assetKey) {
68
+ if (assetKey.startsWith('mp:')) {
69
+ return `https://media.discordapp.net/${assetKey.substring(3)}`;
70
+ }
71
+ if (assetKey.startsWith('spotify:')) {
72
+ return `https://i.scdn.co/image/${assetKey.substring(8)}`;
73
+ }
74
+ if (assetKey.startsWith('youtube:')) {
75
+ return `https://i.ytimg.com/vi/${assetKey.substring(8)}/hqdefault.jpg`;
76
+ }
77
+ if (assetKey.startsWith('twitch:')) {
78
+ return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${assetKey.substring(7)}.png`;
79
+ }
80
+ // For assets uploaded to a Discord application
81
+ if (this.applicationId && !assetKey.startsWith('http')) {
82
+ return `https://cdn.discordapp.com/app-assets/${this.applicationId}/${assetKey}.png`;
83
+ }
84
+ return null;
85
+ }
30
86
  _toRpcImage(source) {
31
87
  if (typeof source !== 'string') {
32
88
  return source;
@@ -60,7 +116,7 @@ class HieuxyzRPC {
60
116
  }
61
117
  /**
62
118
  * Name the operation (first line of RPC).
63
- * @param name - Name to display.
119
+ * @param {string} name - Name to display.
64
120
  * @returns {this}
65
121
  */
66
122
  setName(name) {
@@ -69,7 +125,7 @@ class HieuxyzRPC {
69
125
  }
70
126
  /**
71
127
  * Set details for the operation (second line of RPC).
72
- * @param details - Details to display.
128
+ * @param {string} details - Details to display.
73
129
  * @returns {this}
74
130
  */
75
131
  setDetails(details) {
@@ -78,7 +134,7 @@ class HieuxyzRPC {
78
134
  }
79
135
  /**
80
136
  * Set the state for the operation (third line of the RPC).
81
- * @param state - State to display.
137
+ * @param {string} state - State to display.
82
138
  * @returns {this}
83
139
  */
84
140
  setState(state) {
@@ -87,7 +143,7 @@ class HieuxyzRPC {
87
143
  }
88
144
  /**
89
145
  * Set the activity type.
90
- * @param type - The type of activity (e.g. 0, 'playing', or ActivityType.Playing).
146
+ * @param {SettableActivityType} type - The type of activity (e.g. 0, 'playing', or ActivityType.Playing).
91
147
  * @returns {this}
92
148
  */
93
149
  setType(type) {
@@ -109,8 +165,8 @@ class HieuxyzRPC {
109
165
  }
110
166
  /**
111
167
  * Set a start and/or end timestamp for the activity.
112
- * @param start - Unix timestamp (milliseconds) for start time.
113
- * @param end - Unix timestamp (milliseconds) for the end time.
168
+ * @param {number} [start] - Unix timestamp (milliseconds) for start time.
169
+ * @param {number} [end] - Unix timestamp (milliseconds) for the end time.
114
170
  * @returns {this}
115
171
  */
116
172
  setTimestamps(start, end) {
@@ -119,8 +175,8 @@ class HieuxyzRPC {
119
175
  }
120
176
  /**
121
177
  * Set party information for the activity.
122
- * @param currentSize - Current number of players.
123
- * @param maxSize - Maximum number of players.
178
+ * @param {number} currentSize - Current number of players.
179
+ * @param {number} maxSize - Maximum number of players.
124
180
  * @returns {this}
125
181
  */
126
182
  setParty(currentSize, maxSize) {
@@ -129,8 +185,8 @@ class HieuxyzRPC {
129
185
  }
130
186
  /**
131
187
  * Set large image and its caption text.
132
- * @param source - Image source (URL, asset key, or RpcImage object).
133
- * @param text - Text displayed when hovering over image.
188
+ * @param {string | RpcImage} source - Image source (URL, asset key, or RpcImage object).
189
+ * @param {string} [text] - Text displayed when hovering over image.
134
190
  * @returns {this}
135
191
  */
136
192
  setLargeImage(source, text) {
@@ -141,8 +197,8 @@ class HieuxyzRPC {
141
197
  }
142
198
  /**
143
199
  * Set the small image and its caption text.
144
- * @param source - Image source (URL, asset key, or RpcImage object).
145
- * @param text - Text displayed when hovering over image.
200
+ * @param {string | RpcImage} source - Image source (URL, asset key, or RpcImage object).
201
+ * @param {string} [text] - Text displayed when hovering over image.
146
202
  * @returns {this}
147
203
  */
148
204
  setSmallImage(source, text) {
@@ -153,7 +209,7 @@ class HieuxyzRPC {
153
209
  }
154
210
  /**
155
211
  * Set clickable buttons for RPC (up to 2).
156
- * @param buttons - An array of button objects.
212
+ * @param {RpcButton[]} buttons - An array of button objects, each with a `label` and `url`.
157
213
  * @returns {this}
158
214
  */
159
215
  setButtons(buttons) {
@@ -162,9 +218,36 @@ class HieuxyzRPC {
162
218
  this.activity.metadata = { button_urls: validButtons.map((b) => b.url) };
163
219
  return this;
164
220
  }
221
+ /**
222
+ * Set secrets for joining, spectating, and matching games.
223
+ * @param {RpcSecrets} secrets - An object with join, spectate, and/or match secrets.
224
+ * @returns {this}
225
+ */
226
+ setSecrets(secrets) {
227
+ this.activity.secrets = secrets;
228
+ return this;
229
+ }
230
+ /**
231
+ * Set the sync_id, typically used for Spotify track synchronization.
232
+ * @param {string} syncId - The synchronization ID.
233
+ * @returns {this}
234
+ */
235
+ setSyncId(syncId) {
236
+ this.activity.sync_id = syncId;
237
+ return this;
238
+ }
239
+ /**
240
+ * Set activity flags. Use the ActivityFlags enum for convenience.
241
+ * @param {number} flags - A number representing the bitwise flags.
242
+ * @returns {this}
243
+ */
244
+ setFlags(flags) {
245
+ this.activity.flags = flags;
246
+ return this;
247
+ }
165
248
  /**
166
249
  * Set custom application ID for RPC.
167
- * @param id - Discord app ID (must be an 18 or 19 digit number string).
250
+ * @param {string} id - Discord app ID (must be an 18 or 19 digit number string).
168
251
  * @throws {Error} If ID is invalid.
169
252
  * @returns {this}
170
253
  */
@@ -177,7 +260,7 @@ class HieuxyzRPC {
177
260
  }
178
261
  /**
179
262
  * Set the user's status (e.g. online, idle, dnd).
180
- * @param status - Desired state.
263
+ * @param {'online' | 'dnd' | 'idle' | 'invisible' | 'offline'} status - Desired state.
181
264
  * @returns {this}
182
265
  */
183
266
  setStatus(status) {
@@ -186,7 +269,7 @@ class HieuxyzRPC {
186
269
  }
187
270
  /**
188
271
  * Set the platform on which the activity is running.
189
- * @param platform - Platform (e.g. 'desktop', 'xbox').
272
+ * @param {DiscordPlatform} platform - Platform (e.g. 'desktop', 'xbox').
190
273
  * @returns {this}
191
274
  */
192
275
  setPlatform(platform) {
@@ -195,7 +278,7 @@ class HieuxyzRPC {
195
278
  }
196
279
  /**
197
280
  * Marks the activity as a joinable instance for the game.
198
- * @param instance - Whether this activity is a specific instance.
281
+ * @param {boolean} instance - Whether this activity is a specific instance.
199
282
  * @returns {this}
200
283
  */
201
284
  setInstance(instance) {
@@ -223,6 +306,10 @@ class HieuxyzRPC {
223
306
  this.activity.metadata = undefined;
224
307
  return this;
225
308
  }
309
+ clearSecrets() {
310
+ this.activity.secrets = undefined;
311
+ return this;
312
+ }
226
313
  clearInstance() {
227
314
  this.activity.instance = undefined;
228
315
  return this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hieuxyz/rpc",
3
- "version": "1.1.0",
3
+ "version": "1.1.11",
4
4
  "description": "A Discord Rich Presence library for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,10 @@
16
16
  "lint:fix": "eslint \"src/**/*.ts\" --fix",
17
17
  "format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\""
18
18
  },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/hieuxyz00/hieuxyz_rpc.git"
22
+ },
19
23
  "keywords": [
20
24
  "discord",
21
25
  "rpc",
@@ -23,6 +27,10 @@
23
27
  ],
24
28
  "author": "hieuxyz",
25
29
  "license": "ISC",
30
+ "bugs": {
31
+ "url": "https://github.com/hieuxyz00/hieuxyz_rpc/issues"
32
+ },
33
+ "homepage": "https://github.com/hieuxyz00/hieuxyz_rpc#readme",
26
34
  "dependencies": {
27
35
  "axios": "^1.12.2",
28
36
  "ws": "^8.18.3"