@hieuxyz/rpc 1.0.7 → 1.0.9

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
@@ -43,7 +43,7 @@ DISCORD_USER_TOKEN="YOUR_DISCORD_USER_TOKEN_HERE"
43
43
 
44
44
  ```typescript
45
45
  import * as path from 'path';
46
- import { Client, RawImage, LocalImage, logger } from '@hieuxyz/rpc';
46
+ import { Client, LocalImage, logger } from '../src';
47
47
 
48
48
  async function start() {
49
49
  const token = process.env.DISCORD_USER_TOKEN;
@@ -54,7 +54,10 @@ async function start() {
54
54
  }
55
55
 
56
56
  // Initialize client with token
57
- const client = new Client({ token });
57
+ const client = new Client({
58
+ token,
59
+ alwaysReconnect: true,
60
+ });
58
61
 
59
62
  await client.run();
60
63
 
@@ -66,32 +69,30 @@ async function start() {
66
69
  .setType(0) // 0: Playing
67
70
  .setTimestamps(Date.now())
68
71
  .setParty(1, 5)
69
- .setLargeImage(new RawImage("mp:external/b7uybXM7LoJRB6_ig-65aX6dCHm2qGCEe8CiS5j7c2M/https/cdn.worldvectorlogo.com/logos/typescript.svg"), "TypeScript")
70
- .setSmallImage(new LocalImage(path.join(__dirname, 'vscode.png')), "VS Code")
71
- .setButtons([
72
- { label: "View on GitHub", url: "https://github.com/hieuxyz00/hieuxyz_rpc" }
73
- ]);
72
+ .setLargeImage("https://i.ibb.co/MDP0hfTM/typescript.png", "TypeScript")
73
+ .setSmallImage(new LocalImage(path.join(__dirname, 'vscode.png')), "VS Code");
74
74
 
75
75
  await client.rpc.build();
76
+ logger.info("Initial RPC has been set!");
76
77
 
77
- setTimeout(async () => {
78
- logger.info("Updating RPC details dynamically...");
78
+ setTimeout(() => {
79
+ logger.info("Clearing RPC and resetting builder...");
80
+ client.rpc.clear();
79
81
 
80
- // Change the necessary information
81
82
  client.rpc
82
- .setDetails("Reviewing pull requests")
83
- .setState("PR #01: Feature enhancement")
84
- .setParty(2, 5);
85
-
86
- await client.rpc.updateRPC();
83
+ .setName("On a break")
84
+ .setDetails("Thinking about the next feature")
85
+ .setLargeImage("mp:external/dZwPAoMNVxT5qYqecH3Mfgxv1RQEdtGBU8nAspOcAo4/https/c.tenor.com/fvuYGhI1vgUAAAAC/tenor.gif", "Coffee Time");
87
86
 
88
- logger.info("RPC has been dynamically updated!");
87
+ client.rpc.build();
88
+ logger.info("A new RPC has been set after clearing.");
89
89
 
90
- }, 15000);
90
+ }, 20000);
91
91
 
92
92
  process.on('SIGINT', () => {
93
93
  logger.info("SIGINT received. Closing connection...");
94
- client.close();
94
+ client.rpc.clear();
95
+ client.close(true);
95
96
  process.exit(0);
96
97
  });
97
98
  }
@@ -139,8 +140,11 @@ This is the main starting point.
139
140
  - `new Client(options)`: Create a new instance.
140
141
  - `options.token` (required): Your Discord user token.
141
142
  - `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`.
142
144
  - `client.run()`: Start connecting to Discord Gateway.
143
145
  - `client.rpc`: Access the instance of `HieuxyzRPC` to build the state.
146
+ - `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`.
144
148
 
145
149
  ### Class `HieuxyzRPC`
146
150
 
@@ -156,7 +160,8 @@ Main builder class for RPC.
156
160
  - `.setButtons(buttons[])`: Set buttons (up to 2).
157
161
  - `.setPlatform(platform)`: Lay the platform (`'desktop'`, `'xbox'`, `'ps5'`).
158
162
  - `.build()`: First RPC send.
159
- - `.updateRPC()`: Send updates to an existing RPC.(it just call build() lol)
163
+ - `.updateRPC()`: Send updates to an existing RPC.(it just call build() lol).
164
+ - `.clear()`: Clears the current Rich Presence from the user's profile and resets the builder to its default state.
160
165
 
161
166
  ### Types of images
162
167
 
@@ -48,8 +48,8 @@ export declare class Client {
48
48
  run(): Promise<void>;
49
49
  /**
50
50
  * Close the connection to Discord Gateway.
51
- * Terminate RPC and clean up resources.
52
- * If `alwaysReconnect` is true, the client will attempt to reconnect after this.
51
+ * @param {boolean} [force=false] - If true, the client will close permanently and will not attempt to reconnect,
52
+ * even if `alwaysReconnect` is enabled. Defaults to false.
53
53
  */
54
- close(): void;
54
+ close(force?: boolean): void;
55
55
  }
@@ -55,12 +55,12 @@ class Client {
55
55
  }
56
56
  /**
57
57
  * Close the connection to Discord Gateway.
58
- * Terminate RPC and clean up resources.
59
- * If `alwaysReconnect` is true, the client will attempt to reconnect after this.
58
+ * @param {boolean} [force=false] - If true, the client will close permanently and will not attempt to reconnect,
59
+ * even if `alwaysReconnect` is enabled. Defaults to false.
60
60
  */
61
- close() {
61
+ close(force = false) {
62
62
  this.rpc.stopBackgroundRenewal();
63
- this.websocket.close();
63
+ this.websocket.close(force);
64
64
  }
65
65
  }
66
66
  exports.Client = Client;
@@ -11,9 +11,12 @@ export declare class DiscordWebSocket {
11
11
  private ws;
12
12
  private sequence;
13
13
  private heartbeatInterval;
14
+ private heartbeatIntervalValue;
14
15
  private sessionId;
15
16
  private resumeGatewayUrl;
16
17
  private options;
18
+ private isReconnecting;
19
+ private permanentClose;
17
20
  private resolveReady;
18
21
  /**
19
22
  * A promise will be resolved when the Gateway connection is ready.
@@ -34,9 +37,9 @@ export declare class DiscordWebSocket {
34
37
  * If there was a previous session, it will try to resume.
35
38
  */
36
39
  connect(): void;
37
- private reconnect;
38
40
  private onMessage;
39
41
  private startHeartbeating;
42
+ private sendHeartbeat;
40
43
  private identify;
41
44
  private resume;
42
45
  /**
@@ -46,9 +49,10 @@ export declare class DiscordWebSocket {
46
49
  sendActivity(presence: PresenceUpdatePayload): void;
47
50
  private sendJson;
48
51
  /**
49
- * Close the WebSocket connection and clean up the resources.
52
+ * Closes the WebSocket connection.
53
+ * @param force If true, prevents any automatic reconnection attempts.
50
54
  */
51
- close(): void;
55
+ close(force?: boolean): void;
52
56
  private cleanupHeartbeat;
53
57
  private shouldReconnect;
54
58
  }
@@ -17,9 +17,12 @@ class DiscordWebSocket {
17
17
  ws = null;
18
18
  sequence = null;
19
19
  heartbeatInterval = null;
20
+ heartbeatIntervalValue = 0;
20
21
  sessionId = null;
21
22
  resumeGatewayUrl = null;
22
23
  options;
24
+ isReconnecting = false;
25
+ permanentClose = false;
23
26
  resolveReady = () => { };
24
27
  /**
25
28
  * A promise will be resolved when the Gateway connection is ready.
@@ -51,18 +54,40 @@ class DiscordWebSocket {
51
54
  * If there was a previous session, it will try to resume.
52
55
  */
53
56
  connect() {
57
+ if (this.isReconnecting) {
58
+ logger_1.logger.info("Connection attempt aborted: reconnection already in progress.");
59
+ return;
60
+ }
61
+ this.permanentClose = false;
62
+ this.isReconnecting = true;
54
63
  this.resetReadyPromise();
55
64
  const url = this.resumeGatewayUrl || "wss://gateway.discord.gg/?v=10&encoding=json";
56
65
  logger_1.logger.info(`Attempting to connect to ${url}...`);
57
66
  this.ws = new ws_1.default(url);
58
- this.ws.on('open', () => logger_1.logger.info(`Connected to Discord Gateway at ${url}.`));
67
+ this.ws.on('open', () => {
68
+ logger_1.logger.info(`Successfully connected to Discord Gateway at ${url}.`);
69
+ this.isReconnecting = false;
70
+ });
59
71
  this.ws.on('message', this.onMessage.bind(this));
60
72
  this.ws.on('close', (code, reason) => {
61
- logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString()}`);
73
+ logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString('utf-8')}`);
62
74
  this.cleanupHeartbeat();
75
+ if (this.permanentClose) {
76
+ logger_1.logger.info("Connection permanently closed by client. Not reconnecting.");
77
+ return;
78
+ }
79
+ if (this.isReconnecting)
80
+ return;
63
81
  if (this.shouldReconnect(code)) {
64
- logger_1.logger.info("Attempting to reconnect...");
65
- this.reconnect(code === 4004);
82
+ setTimeout(() => {
83
+ const canResume = code !== 4004 && !!this.sessionId;
84
+ if (!canResume) {
85
+ this.sessionId = null;
86
+ this.sequence = null;
87
+ this.resumeGatewayUrl = null;
88
+ }
89
+ this.connect();
90
+ }, 500);
66
91
  }
67
92
  else {
68
93
  logger_1.logger.info("Not attempting to reconnect based on close code and client options.");
@@ -72,15 +97,6 @@ class DiscordWebSocket {
72
97
  logger_1.logger.error(`WebSocket Error: ${err.message}`);
73
98
  });
74
99
  }
75
- reconnect(forceNewSession = false) {
76
- this.ws?.terminate();
77
- if (forceNewSession) {
78
- logger_1.logger.warn("Forcing a new session. Clearing previous session data.");
79
- this.sessionId = null;
80
- this.resumeGatewayUrl = null;
81
- }
82
- setTimeout(() => this.connect(), 3000);
83
- }
84
100
  onMessage(data) {
85
101
  const payload = JSON.parse(data.toString());
86
102
  if (payload.s) {
@@ -88,8 +104,10 @@ class DiscordWebSocket {
88
104
  }
89
105
  switch (payload.op) {
90
106
  case OpCode_1.OpCode.HELLO:
91
- this.startHeartbeating(payload.d.heartbeat_interval);
92
- if (this.sessionId) {
107
+ this.heartbeatIntervalValue = payload.d.heartbeat_interval;
108
+ logger_1.logger.info(`Received HELLO. Setting heartbeat interval to ${this.heartbeatIntervalValue}ms.`);
109
+ this.startHeartbeating();
110
+ if (this.sessionId && this.sequence) {
93
111
  this.resume();
94
112
  }
95
113
  else {
@@ -99,8 +117,8 @@ class DiscordWebSocket {
99
117
  case OpCode_1.OpCode.DISPATCH:
100
118
  if (payload.t === 'READY') {
101
119
  this.sessionId = payload.d.session_id;
102
- this.resumeGatewayUrl = payload.d.resume_gateway_url;
103
- logger_1.logger.info(`Session is READY. Session ID: ${this.sessionId}`);
120
+ this.resumeGatewayUrl = payload.d.resume_gateway_url + "/?v=10&encoding=json";
121
+ logger_1.logger.info(`Session READY. Session ID: ${this.sessionId}. Resume URL set.`);
104
122
  this.resolveReady();
105
123
  }
106
124
  else if (payload.t === 'RESUMED') {
@@ -112,28 +130,27 @@ class DiscordWebSocket {
112
130
  logger_1.logger.info("Heartbeat acknowledged.");
113
131
  break;
114
132
  case OpCode_1.OpCode.INVALID_SESSION:
115
- logger_1.logger.warn(`Invalid session received. Resumable: ${payload.d}`);
116
- if (payload.d === false) {
117
- this.ws?.close(4004, "Session not resumable");
133
+ logger_1.logger.warn(`Received INVALID_SESSION. Resumable: ${payload.d}`);
134
+ if (payload.d) {
135
+ this.ws?.close(4000, "Invalid session, attempting to resume.");
118
136
  }
119
137
  else {
120
- this.ws?.close(4000, "Session is resumable, retrying");
138
+ this.ws?.close(4004, "Invalid session, starting a new session.");
121
139
  }
122
140
  break;
123
141
  case OpCode_1.OpCode.RECONNECT:
124
- logger_1.logger.info("Gateway requested reconnect. Closing and reconnecting.");
125
- this.ws?.close(4000, "Reconnect request");
142
+ logger_1.logger.info("Gateway requested RECONNECT. Closing to reconnect and resume.");
143
+ this.ws?.close(4000, "Gateway requested reconnect.");
126
144
  break;
127
145
  default:
128
146
  break;
129
147
  }
130
148
  }
131
- startHeartbeating(interval) {
149
+ startHeartbeating() {
132
150
  this.cleanupHeartbeat();
133
151
  setTimeout(() => {
134
152
  if (this.ws?.readyState === ws_1.default.OPEN) {
135
- this.sendJson({ op: OpCode_1.OpCode.HEARTBEAT, d: this.sequence });
136
- logger_1.logger.info(`Initial heartbeat sent with sequence ${this.sequence}.`);
153
+ this.sendHeartbeat();
137
154
  }
138
155
  this.heartbeatInterval = setInterval(() => {
139
156
  if (this.ws?.readyState !== ws_1.default.OPEN) {
@@ -141,10 +158,15 @@ class DiscordWebSocket {
141
158
  this.cleanupHeartbeat();
142
159
  return;
143
160
  }
144
- this.sendJson({ op: OpCode_1.OpCode.HEARTBEAT, d: this.sequence });
145
- logger_1.logger.info(`Heartbeat sent with sequence ${this.sequence}.`);
146
- }, interval);
147
- }, interval * Math.random());
161
+ this.sendHeartbeat();
162
+ }, this.heartbeatIntervalValue);
163
+ }, this.heartbeatIntervalValue * Math.random());
164
+ }
165
+ sendHeartbeat() {
166
+ if (this.ws?.readyState !== ws_1.default.OPEN)
167
+ return;
168
+ this.sendJson({ op: OpCode_1.OpCode.HEARTBEAT, d: this.sequence });
169
+ logger_1.logger.info(`Heartbeat sent with sequence ${this.sequence}.`);
148
170
  }
149
171
  identify() {
150
172
  const identifyPayload = (0, identify_1.getIdentifyPayload)(this.token);
@@ -182,10 +204,21 @@ class DiscordWebSocket {
182
204
  }
183
205
  }
184
206
  /**
185
- * Close the WebSocket connection and clean up the resources.
207
+ * Closes the WebSocket connection.
208
+ * @param force If true, prevents any automatic reconnection attempts.
186
209
  */
187
- close() {
188
- this.ws?.close(1000, "Client closed connection");
210
+ close(force = false) {
211
+ if (force) {
212
+ logger_1.logger.info("Forcing permanent closure. Reconnects will be disabled.");
213
+ this.permanentClose = true;
214
+ }
215
+ else {
216
+ logger_1.logger.info("Closing connection manually...");
217
+ }
218
+ this.isReconnecting = false;
219
+ if (this.ws) {
220
+ this.ws.close(1000, "Client initiated closure");
221
+ }
189
222
  }
190
223
  cleanupHeartbeat() {
191
224
  if (this.heartbeatInterval) {
@@ -125,5 +125,12 @@ export declare class HieuxyzRPC {
125
125
  * @returns {Promise<void>}
126
126
  */
127
127
  updateRPC(): Promise<void>;
128
+ /**
129
+ * Clears the current Rich Presence from Discord and resets the builder state.
130
+ * This sends an empty activity payload to Discord and then resets all configured
131
+ * options (name, details, images, etc.) to their default values, allowing you
132
+ * to build a new presence from scratch.
133
+ */
134
+ clear(): void;
128
135
  }
129
136
  export {};
@@ -288,7 +288,7 @@ class HieuxyzRPC {
288
288
  since: 0,
289
289
  activities: [activity],
290
290
  status: this.status,
291
- afk: false,
291
+ afk: true,
292
292
  };
293
293
  this.websocket.sendActivity(presencePayload);
294
294
  }
@@ -300,5 +300,26 @@ class HieuxyzRPC {
300
300
  async updateRPC() {
301
301
  await this.build();
302
302
  }
303
+ /**
304
+ * Clears the current Rich Presence from Discord and resets the builder state.
305
+ * This sends an empty activity payload to Discord and then resets all configured
306
+ * options (name, details, images, etc.) to their default values, allowing you
307
+ * to build a new presence from scratch.
308
+ */
309
+ clear() {
310
+ const clearPayload = {
311
+ since: 0,
312
+ activities: [],
313
+ status: this.status,
314
+ afk: true,
315
+ };
316
+ this.websocket.sendActivity(clearPayload);
317
+ logger_1.logger.info("Rich Presence cleared from Discord.");
318
+ this.activity = {};
319
+ this.assets = {};
320
+ this.applicationId = '1416676323459469363'; // Reset to default
321
+ this.platform = 'desktop'; // Reset to default
322
+ logger_1.logger.info("RPC builder has been reset to its initial state.");
323
+ }
303
324
  }
304
325
  exports.HieuxyzRPC = HieuxyzRPC;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hieuxyz/rpc",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "A Discord Rich Presence library for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",