@hieuxyz/rpc 1.0.9 → 1.1.0
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 +29 -13
- package/dist/hieuxyz/Client.js +3 -3
- package/dist/hieuxyz/gateway/DiscordWebSocket.js +69 -27
- package/dist/hieuxyz/gateway/entities/identify.d.ts +1 -1
- package/dist/hieuxyz/gateway/entities/identify.js +5 -4
- package/dist/hieuxyz/gateway/entities/types.d.ts +3 -1
- package/dist/hieuxyz/rpc/HieuxyzRPC.d.ts +19 -4
- package/dist/hieuxyz/rpc/HieuxyzRPC.js +61 -14
- package/dist/hieuxyz/rpc/ImageService.js +2 -2
- package/dist/hieuxyz/rpc/RpcImage.d.ts +1 -1
- package/dist/hieuxyz/rpc/RpcImage.js +1 -1
- package/package.json +12 -4
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, LocalImage, logger } from '
|
|
46
|
+
import { Client, LocalImage, logger } from '@hieuxyz/rpc';
|
|
47
47
|
|
|
48
48
|
async function start() {
|
|
49
49
|
const token = process.env.DISCORD_USER_TOKEN;
|
|
@@ -150,18 +150,34 @@ This is the main starting point.
|
|
|
150
150
|
|
|
151
151
|
Main builder class for RPC.
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
- `.
|
|
155
|
-
- `.
|
|
156
|
-
- `.
|
|
157
|
-
- `.
|
|
158
|
-
- `.
|
|
159
|
-
- `.
|
|
160
|
-
- `.
|
|
161
|
-
- `.
|
|
162
|
-
- `.
|
|
163
|
-
- `.
|
|
164
|
-
- `.
|
|
153
|
+
#### Setter Methods
|
|
154
|
+
- `.setName(string)`: Sets the activity name (first line).
|
|
155
|
+
- `.setDetails(string)`: Sets the activity details (second line).
|
|
156
|
+
- `.setState(string)`: Sets the activity state (third line).
|
|
157
|
+
- `.setTimestamps(start?, end?)`: Sets the start and/or end times.
|
|
158
|
+
- `.setParty(current, max)`: Sets the party information.
|
|
159
|
+
- `.setLargeImage(RpcImage, text?)`: Sets the large image and its tooltip text.
|
|
160
|
+
- `.setSmallImage(RpcImage, text?)`: Sets the small image and its tooltip text.
|
|
161
|
+
- `.setButtons(buttons[])`: Sets up to two clickable buttons.
|
|
162
|
+
- `.setPlatform(platform)`: Sets the platform (`'desktop'`, `'xbox'`, etc.).
|
|
163
|
+
- `.setInstance(boolean)`: Marks the activity as a specific, joinable instance.
|
|
164
|
+
- `.setApplicationId(string)`: Sets a custom Application ID.
|
|
165
|
+
- `.setStatus('online' | ...)`: Sets the user's presence status.
|
|
166
|
+
|
|
167
|
+
#### 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.
|
|
174
|
+
- `.clearButtons()`: Removes all buttons.
|
|
175
|
+
- `.clearInstance()`: Removes the instance flag.
|
|
176
|
+
|
|
177
|
+
#### Core Methods
|
|
178
|
+
- `.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.
|
|
165
181
|
|
|
166
182
|
### Types of images
|
|
167
183
|
|
package/dist/hieuxyz/Client.js
CHANGED
|
@@ -33,7 +33,7 @@ class Client {
|
|
|
33
33
|
*/
|
|
34
34
|
constructor(options) {
|
|
35
35
|
if (!options.token) {
|
|
36
|
-
throw new Error(
|
|
36
|
+
throw new Error('Tokens are required to connect to Discord.');
|
|
37
37
|
}
|
|
38
38
|
this.token = options.token;
|
|
39
39
|
this.imageService = new ImageService_1.ImageService(options.apiBaseUrl);
|
|
@@ -49,9 +49,9 @@ class Client {
|
|
|
49
49
|
*/
|
|
50
50
|
async run() {
|
|
51
51
|
this.websocket.connect();
|
|
52
|
-
logger_1.logger.info(
|
|
52
|
+
logger_1.logger.info('Waiting for Discord session to be ready...');
|
|
53
53
|
await this.websocket.readyPromise;
|
|
54
|
-
logger_1.logger.info(
|
|
54
|
+
logger_1.logger.info('Client is ready to send Rich Presence updates.');
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
57
|
* Close the connection to Discord Gateway.
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.DiscordWebSocket = void 0;
|
|
7
40
|
const ws_1 = __importDefault(require("ws"));
|
|
41
|
+
const zlib = __importStar(require("zlib"));
|
|
8
42
|
const logger_1 = require("../utils/logger");
|
|
9
43
|
const identify_1 = require("./entities/identify");
|
|
10
44
|
const OpCode_1 = require("./entities/OpCode");
|
|
@@ -37,17 +71,18 @@ class DiscordWebSocket {
|
|
|
37
71
|
*/
|
|
38
72
|
constructor(token, options) {
|
|
39
73
|
if (!this.isTokenValid(token)) {
|
|
40
|
-
throw new Error(
|
|
74
|
+
throw new Error('Invalid token provided.');
|
|
41
75
|
}
|
|
42
76
|
this.token = token;
|
|
43
77
|
this.options = options;
|
|
44
|
-
this.readyPromise = new Promise(resolve => (this.resolveReady = resolve));
|
|
78
|
+
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve));
|
|
45
79
|
}
|
|
46
80
|
resetReadyPromise() {
|
|
47
|
-
this.readyPromise = new Promise(resolve => (this.resolveReady = resolve));
|
|
81
|
+
this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve));
|
|
48
82
|
}
|
|
49
83
|
isTokenValid(token) {
|
|
50
|
-
return /^[a-zA-Z0-9_-]{24}\.[a-zA-Z0-9_-]{6}\.[a-zA-Z0-9_-]{38}$/.test(token) ||
|
|
84
|
+
return (/^[a-zA-Z0-9_-]{24}\.[a-zA-Z0-9_-]{6}\.[a-zA-Z0-9_-]{38}$/.test(token) ||
|
|
85
|
+
/^mfa\.[a-zA-Z0-9_-]{84}$/.test(token));
|
|
51
86
|
}
|
|
52
87
|
/**
|
|
53
88
|
* Initiate connection to Discord Gateway.
|
|
@@ -55,13 +90,13 @@ class DiscordWebSocket {
|
|
|
55
90
|
*/
|
|
56
91
|
connect() {
|
|
57
92
|
if (this.isReconnecting) {
|
|
58
|
-
logger_1.logger.info(
|
|
93
|
+
logger_1.logger.info('Connection attempt aborted: reconnection already in progress.');
|
|
59
94
|
return;
|
|
60
95
|
}
|
|
61
96
|
this.permanentClose = false;
|
|
62
97
|
this.isReconnecting = true;
|
|
63
98
|
this.resetReadyPromise();
|
|
64
|
-
const url = this.resumeGatewayUrl ||
|
|
99
|
+
const url = this.resumeGatewayUrl || 'wss://gateway.discord.gg/?v=10&encoding=json';
|
|
65
100
|
logger_1.logger.info(`Attempting to connect to ${url}...`);
|
|
66
101
|
this.ws = new ws_1.default(url);
|
|
67
102
|
this.ws.on('open', () => {
|
|
@@ -73,7 +108,7 @@ class DiscordWebSocket {
|
|
|
73
108
|
logger_1.logger.warn(`Connection closed: ${code} - ${reason.toString('utf-8')}`);
|
|
74
109
|
this.cleanupHeartbeat();
|
|
75
110
|
if (this.permanentClose) {
|
|
76
|
-
logger_1.logger.info(
|
|
111
|
+
logger_1.logger.info('Connection permanently closed by client. Not reconnecting.');
|
|
77
112
|
return;
|
|
78
113
|
}
|
|
79
114
|
if (this.isReconnecting)
|
|
@@ -90,15 +125,22 @@ class DiscordWebSocket {
|
|
|
90
125
|
}, 500);
|
|
91
126
|
}
|
|
92
127
|
else {
|
|
93
|
-
logger_1.logger.info(
|
|
128
|
+
logger_1.logger.info('Not attempting to reconnect based on close code and client options.');
|
|
94
129
|
}
|
|
95
130
|
});
|
|
96
131
|
this.ws.on('error', (err) => {
|
|
97
132
|
logger_1.logger.error(`WebSocket Error: ${err.message}`);
|
|
98
133
|
});
|
|
99
134
|
}
|
|
100
|
-
onMessage(data) {
|
|
101
|
-
|
|
135
|
+
onMessage(data, isBinary) {
|
|
136
|
+
let decompressedData;
|
|
137
|
+
if (isBinary) {
|
|
138
|
+
decompressedData = zlib.inflateSync(data).toString('utf-8');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
decompressedData = data.toString('utf-8');
|
|
142
|
+
}
|
|
143
|
+
const payload = JSON.parse(decompressedData);
|
|
102
144
|
if (payload.s) {
|
|
103
145
|
this.sequence = payload.s;
|
|
104
146
|
}
|
|
@@ -117,30 +159,30 @@ class DiscordWebSocket {
|
|
|
117
159
|
case OpCode_1.OpCode.DISPATCH:
|
|
118
160
|
if (payload.t === 'READY') {
|
|
119
161
|
this.sessionId = payload.d.session_id;
|
|
120
|
-
this.resumeGatewayUrl = payload.d.resume_gateway_url +
|
|
162
|
+
this.resumeGatewayUrl = payload.d.resume_gateway_url + '/?v=10&encoding=json';
|
|
121
163
|
logger_1.logger.info(`Session READY. Session ID: ${this.sessionId}. Resume URL set.`);
|
|
122
164
|
this.resolveReady();
|
|
123
165
|
}
|
|
124
166
|
else if (payload.t === 'RESUMED') {
|
|
125
|
-
logger_1.logger.info(
|
|
167
|
+
logger_1.logger.info('The session has been successfully resumed.');
|
|
126
168
|
this.resolveReady();
|
|
127
169
|
}
|
|
128
170
|
break;
|
|
129
171
|
case OpCode_1.OpCode.HEARTBEAT_ACK:
|
|
130
|
-
logger_1.logger.info(
|
|
172
|
+
logger_1.logger.info('Heartbeat acknowledged.');
|
|
131
173
|
break;
|
|
132
174
|
case OpCode_1.OpCode.INVALID_SESSION:
|
|
133
175
|
logger_1.logger.warn(`Received INVALID_SESSION. Resumable: ${payload.d}`);
|
|
134
176
|
if (payload.d) {
|
|
135
|
-
this.ws?.close(4000,
|
|
177
|
+
this.ws?.close(4000, 'Invalid session, attempting to resume.');
|
|
136
178
|
}
|
|
137
179
|
else {
|
|
138
|
-
this.ws?.close(4004,
|
|
180
|
+
this.ws?.close(4004, 'Invalid session, starting a new session.');
|
|
139
181
|
}
|
|
140
182
|
break;
|
|
141
183
|
case OpCode_1.OpCode.RECONNECT:
|
|
142
|
-
logger_1.logger.info(
|
|
143
|
-
this.ws?.close(4000,
|
|
184
|
+
logger_1.logger.info('Gateway requested RECONNECT. Closing to reconnect and resume.');
|
|
185
|
+
this.ws?.close(4000, 'Gateway requested reconnect.');
|
|
144
186
|
break;
|
|
145
187
|
default:
|
|
146
188
|
break;
|
|
@@ -154,7 +196,7 @@ class DiscordWebSocket {
|
|
|
154
196
|
}
|
|
155
197
|
this.heartbeatInterval = setInterval(() => {
|
|
156
198
|
if (this.ws?.readyState !== ws_1.default.OPEN) {
|
|
157
|
-
logger_1.logger.warn(
|
|
199
|
+
logger_1.logger.warn('Heartbeat skipped: WebSocket is not open.');
|
|
158
200
|
this.cleanupHeartbeat();
|
|
159
201
|
return;
|
|
160
202
|
}
|
|
@@ -171,21 +213,21 @@ class DiscordWebSocket {
|
|
|
171
213
|
identify() {
|
|
172
214
|
const identifyPayload = (0, identify_1.getIdentifyPayload)(this.token);
|
|
173
215
|
this.sendJson({ op: OpCode_1.OpCode.IDENTIFY, d: identifyPayload });
|
|
174
|
-
logger_1.logger.info(
|
|
216
|
+
logger_1.logger.info('Identify payload sent.');
|
|
175
217
|
}
|
|
176
218
|
resume() {
|
|
177
219
|
if (!this.sessionId || this.sequence === null) {
|
|
178
|
-
logger_1.logger.error(
|
|
220
|
+
logger_1.logger.error('Attempted to resume without session ID or sequence. Falling back to identify.');
|
|
179
221
|
this.identify();
|
|
180
222
|
return;
|
|
181
223
|
}
|
|
182
224
|
const resumePayload = {
|
|
183
225
|
token: this.token,
|
|
184
226
|
session_id: this.sessionId,
|
|
185
|
-
seq: this.sequence
|
|
227
|
+
seq: this.sequence,
|
|
186
228
|
};
|
|
187
229
|
this.sendJson({ op: OpCode_1.OpCode.RESUME, d: resumePayload });
|
|
188
|
-
logger_1.logger.info(
|
|
230
|
+
logger_1.logger.info('Resume payload sent.');
|
|
189
231
|
}
|
|
190
232
|
/**
|
|
191
233
|
* Send presence update payload to Gateway.
|
|
@@ -193,14 +235,14 @@ class DiscordWebSocket {
|
|
|
193
235
|
*/
|
|
194
236
|
sendActivity(presence) {
|
|
195
237
|
this.sendJson({ op: OpCode_1.OpCode.PRESENCE_UPDATE, d: presence });
|
|
196
|
-
logger_1.logger.info(
|
|
238
|
+
logger_1.logger.info('Presence update sent.');
|
|
197
239
|
}
|
|
198
240
|
sendJson(data) {
|
|
199
241
|
if (this.ws?.readyState === ws_1.default.OPEN) {
|
|
200
242
|
this.ws.send(JSON.stringify(data));
|
|
201
243
|
}
|
|
202
244
|
else {
|
|
203
|
-
logger_1.logger.warn(
|
|
245
|
+
logger_1.logger.warn('Attempted to send data while WebSocket was not open.');
|
|
204
246
|
}
|
|
205
247
|
}
|
|
206
248
|
/**
|
|
@@ -209,15 +251,15 @@ class DiscordWebSocket {
|
|
|
209
251
|
*/
|
|
210
252
|
close(force = false) {
|
|
211
253
|
if (force) {
|
|
212
|
-
logger_1.logger.info(
|
|
254
|
+
logger_1.logger.info('Forcing permanent closure. Reconnects will be disabled.');
|
|
213
255
|
this.permanentClose = true;
|
|
214
256
|
}
|
|
215
257
|
else {
|
|
216
|
-
logger_1.logger.info(
|
|
258
|
+
logger_1.logger.info('Closing connection manually...');
|
|
217
259
|
}
|
|
218
260
|
this.isReconnecting = false;
|
|
219
261
|
if (this.ws) {
|
|
220
|
-
this.ws.close(1000,
|
|
262
|
+
this.ws.close(1000, 'Client initiated closure');
|
|
221
263
|
}
|
|
222
264
|
}
|
|
223
265
|
cleanupHeartbeat() {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { IdentifyPayload } from
|
|
1
|
+
import { IdentifyPayload } from './types';
|
|
2
2
|
export declare function getIdentifyPayload(token: string): IdentifyPayload;
|
|
@@ -5,11 +5,12 @@ function getIdentifyPayload(token) {
|
|
|
5
5
|
return {
|
|
6
6
|
token: token,
|
|
7
7
|
capabilities: 65,
|
|
8
|
+
largeThreshold: 50,
|
|
8
9
|
properties: {
|
|
9
|
-
os:
|
|
10
|
-
browser:
|
|
11
|
-
device:
|
|
10
|
+
os: 'Windows',
|
|
11
|
+
browser: 'Discord Client',
|
|
12
|
+
device: 'hieuxyz©rpc',
|
|
12
13
|
},
|
|
13
|
-
compress:
|
|
14
|
+
compress: true,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpCode } from
|
|
1
|
+
import { OpCode } from './OpCode';
|
|
2
2
|
export declare enum ActivityType {
|
|
3
3
|
Playing = 0,
|
|
4
4
|
Streaming = 1,
|
|
@@ -18,6 +18,7 @@ export interface GatewayPayload {
|
|
|
18
18
|
export interface IdentifyPayload {
|
|
19
19
|
token: string;
|
|
20
20
|
capabilities: number;
|
|
21
|
+
largeThreshold: number;
|
|
21
22
|
properties: {
|
|
22
23
|
os: string;
|
|
23
24
|
browser: string;
|
|
@@ -32,6 +33,7 @@ export interface Activity {
|
|
|
32
33
|
details?: string;
|
|
33
34
|
state?: string;
|
|
34
35
|
platform?: string;
|
|
36
|
+
instance?: boolean;
|
|
35
37
|
party?: {
|
|
36
38
|
id?: string;
|
|
37
39
|
size?: [number, number];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { DiscordWebSocket } from
|
|
2
|
-
import { SettableActivityType } from
|
|
3
|
-
import { ImageService } from
|
|
4
|
-
import { RpcImage } from
|
|
1
|
+
import { DiscordWebSocket } from '../gateway/DiscordWebSocket';
|
|
2
|
+
import { SettableActivityType } from '../gateway/entities/types';
|
|
3
|
+
import { ImageService } from './ImageService';
|
|
4
|
+
import { RpcImage } from './RpcImage';
|
|
5
5
|
interface RpcButton {
|
|
6
6
|
label: string;
|
|
7
7
|
url: string;
|
|
@@ -27,6 +27,7 @@ export declare class HieuxyzRPC {
|
|
|
27
27
|
private renewalInterval;
|
|
28
28
|
constructor(websocket: DiscordWebSocket, imageService: ImageService);
|
|
29
29
|
private _toRpcImage;
|
|
30
|
+
private cleanupNulls;
|
|
30
31
|
private sanitize;
|
|
31
32
|
/**
|
|
32
33
|
* Name the operation (first line of RPC).
|
|
@@ -105,6 +106,20 @@ export declare class HieuxyzRPC {
|
|
|
105
106
|
* @returns {this}
|
|
106
107
|
*/
|
|
107
108
|
setPlatform(platform: DiscordPlatform): this;
|
|
109
|
+
/**
|
|
110
|
+
* Marks the activity as a joinable instance for the game.
|
|
111
|
+
* @param instance - Whether this activity is a specific instance.
|
|
112
|
+
* @returns {this}
|
|
113
|
+
*/
|
|
114
|
+
setInstance(instance: boolean): this;
|
|
115
|
+
clearDetails(): this;
|
|
116
|
+
clearState(): this;
|
|
117
|
+
clearTimestamps(): this;
|
|
118
|
+
clearParty(): this;
|
|
119
|
+
clearButtons(): this;
|
|
120
|
+
clearInstance(): this;
|
|
121
|
+
clearLargeImage(): this;
|
|
122
|
+
clearSmallImage(): this;
|
|
108
123
|
private getExpiryTime;
|
|
109
124
|
private renewAssetIfNeeded;
|
|
110
125
|
private startBackgroundRenewal;
|
|
@@ -42,7 +42,7 @@ class HieuxyzRPC {
|
|
|
42
42
|
return new RpcImage_1.ExternalImage(source);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
catch
|
|
45
|
+
catch {
|
|
46
46
|
logger_1.logger.warn(`Could not parse "${source}" into a valid URL. Treating as RawImage.`);
|
|
47
47
|
return new RpcImage_1.RawImage(source);
|
|
48
48
|
}
|
|
@@ -52,6 +52,9 @@ class HieuxyzRPC {
|
|
|
52
52
|
}
|
|
53
53
|
return new RpcImage_1.RawImage(source);
|
|
54
54
|
}
|
|
55
|
+
cleanupNulls(obj) {
|
|
56
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== null && v !== undefined));
|
|
57
|
+
}
|
|
55
58
|
sanitize(str, length = 128) {
|
|
56
59
|
return str.length > length ? str.substring(0, length) : str;
|
|
57
60
|
}
|
|
@@ -155,8 +158,8 @@ class HieuxyzRPC {
|
|
|
155
158
|
*/
|
|
156
159
|
setButtons(buttons) {
|
|
157
160
|
const validButtons = buttons.slice(0, 2);
|
|
158
|
-
this.activity.buttons = validButtons.map(b => this.sanitize(b.label, 32));
|
|
159
|
-
this.activity.metadata = { button_urls: validButtons.map(b => b.url) };
|
|
161
|
+
this.activity.buttons = validButtons.map((b) => this.sanitize(b.label, 32));
|
|
162
|
+
this.activity.metadata = { button_urls: validButtons.map((b) => b.url) };
|
|
160
163
|
return this;
|
|
161
164
|
}
|
|
162
165
|
/**
|
|
@@ -167,7 +170,7 @@ class HieuxyzRPC {
|
|
|
167
170
|
*/
|
|
168
171
|
setApplicationId(id) {
|
|
169
172
|
if (!/^\d{18,19}$/.test(id)) {
|
|
170
|
-
throw new Error(
|
|
173
|
+
throw new Error('The app ID must be an 18 or 19 digit number.');
|
|
171
174
|
}
|
|
172
175
|
this.applicationId = id;
|
|
173
176
|
return this;
|
|
@@ -190,6 +193,50 @@ class HieuxyzRPC {
|
|
|
190
193
|
this.platform = platform;
|
|
191
194
|
return this;
|
|
192
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Marks the activity as a joinable instance for the game.
|
|
198
|
+
* @param instance - Whether this activity is a specific instance.
|
|
199
|
+
* @returns {this}
|
|
200
|
+
*/
|
|
201
|
+
setInstance(instance) {
|
|
202
|
+
this.activity.instance = instance;
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
clearDetails() {
|
|
206
|
+
this.activity.details = undefined;
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
clearState() {
|
|
210
|
+
this.activity.state = undefined;
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
clearTimestamps() {
|
|
214
|
+
this.activity.timestamps = undefined;
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
clearParty() {
|
|
218
|
+
this.activity.party = undefined;
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
clearButtons() {
|
|
222
|
+
this.activity.buttons = undefined;
|
|
223
|
+
this.activity.metadata = undefined;
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
clearInstance() {
|
|
227
|
+
this.activity.instance = undefined;
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
clearLargeImage() {
|
|
231
|
+
this.assets.large_image = undefined;
|
|
232
|
+
this.assets.large_text = undefined;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
clearSmallImage() {
|
|
236
|
+
this.assets.small_image = undefined;
|
|
237
|
+
this.assets.small_text = undefined;
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
193
240
|
getExpiryTime(assetKey) {
|
|
194
241
|
if (!assetKey.startsWith('mp:attachments'))
|
|
195
242
|
return null;
|
|
@@ -201,14 +248,14 @@ class HieuxyzRPC {
|
|
|
201
248
|
return parseInt(expiresTimestamp, 16) * 1000;
|
|
202
249
|
}
|
|
203
250
|
}
|
|
204
|
-
catch
|
|
251
|
+
catch {
|
|
205
252
|
logger_1.logger.error(`Could not parse asset URL for expiry check: ${assetKey}`);
|
|
206
253
|
}
|
|
207
254
|
return null;
|
|
208
255
|
}
|
|
209
256
|
async renewAssetIfNeeded(cacheKey, assetKey) {
|
|
210
257
|
const expiryTimeMs = this.getExpiryTime(assetKey);
|
|
211
|
-
if (expiryTimeMs && expiryTimeMs <
|
|
258
|
+
if (expiryTimeMs && expiryTimeMs < Date.now() + 3600000) {
|
|
212
259
|
logger_1.logger.info(`Asset ${cacheKey} is expiring soon. Renewing...`);
|
|
213
260
|
const assetId = assetKey.split('mp:attachments/')[1];
|
|
214
261
|
const newAsset = await this.imageService.renewImage(assetId);
|
|
@@ -225,7 +272,7 @@ class HieuxyzRPC {
|
|
|
225
272
|
clearInterval(this.renewalInterval);
|
|
226
273
|
}
|
|
227
274
|
this.renewalInterval = setInterval(async () => {
|
|
228
|
-
logger_1.logger.info(
|
|
275
|
+
logger_1.logger.info('Running background asset renewal check...');
|
|
229
276
|
for (const [cacheKey, assetKey] of this.resolvedAssetsCache.entries()) {
|
|
230
277
|
await this.renewAssetIfNeeded(cacheKey, assetKey);
|
|
231
278
|
}
|
|
@@ -238,14 +285,14 @@ class HieuxyzRPC {
|
|
|
238
285
|
if (this.renewalInterval) {
|
|
239
286
|
clearInterval(this.renewalInterval);
|
|
240
287
|
this.renewalInterval = null;
|
|
241
|
-
logger_1.logger.info(
|
|
288
|
+
logger_1.logger.info('Stopped background asset renewal process.');
|
|
242
289
|
}
|
|
243
290
|
}
|
|
244
291
|
async resolveImage(image) {
|
|
245
292
|
if (!image)
|
|
246
293
|
return undefined;
|
|
247
294
|
const cacheKey = image.getCacheKey();
|
|
248
|
-
|
|
295
|
+
const cachedAsset = this.resolvedAssetsCache.get(cacheKey);
|
|
249
296
|
if (cachedAsset) {
|
|
250
297
|
return await this.renewAssetIfNeeded(cacheKey, cachedAsset);
|
|
251
298
|
}
|
|
@@ -267,16 +314,16 @@ class HieuxyzRPC {
|
|
|
267
314
|
if (small_image)
|
|
268
315
|
finalAssets.small_image = small_image;
|
|
269
316
|
const finalActivity = { ...this.activity };
|
|
270
|
-
finalActivity.assets =
|
|
317
|
+
finalActivity.assets = large_image || small_image ? finalAssets : undefined;
|
|
271
318
|
finalActivity.application_id = this.applicationId;
|
|
272
319
|
finalActivity.platform = this.platform;
|
|
273
320
|
if (!finalActivity.name) {
|
|
274
|
-
finalActivity.name =
|
|
321
|
+
finalActivity.name = 'hieuxyzRPC';
|
|
275
322
|
}
|
|
276
323
|
if (typeof finalActivity.type === 'undefined') {
|
|
277
324
|
finalActivity.type = types_1.ActivityType.Playing;
|
|
278
325
|
}
|
|
279
|
-
return finalActivity;
|
|
326
|
+
return this.cleanupNulls(finalActivity);
|
|
280
327
|
}
|
|
281
328
|
/**
|
|
282
329
|
* Build the final Rich Presence payload and send it to Discord.
|
|
@@ -314,12 +361,12 @@ class HieuxyzRPC {
|
|
|
314
361
|
afk: true,
|
|
315
362
|
};
|
|
316
363
|
this.websocket.sendActivity(clearPayload);
|
|
317
|
-
logger_1.logger.info(
|
|
364
|
+
logger_1.logger.info('Rich Presence cleared from Discord.');
|
|
318
365
|
this.activity = {};
|
|
319
366
|
this.assets = {};
|
|
320
367
|
this.applicationId = '1416676323459469363'; // Reset to default
|
|
321
368
|
this.platform = 'desktop'; // Reset to default
|
|
322
|
-
logger_1.logger.info(
|
|
369
|
+
logger_1.logger.info('RPC builder has been reset to its initial state.');
|
|
323
370
|
}
|
|
324
371
|
}
|
|
325
372
|
exports.HieuxyzRPC = HieuxyzRPC;
|
|
@@ -90,8 +90,8 @@ class ImageService {
|
|
|
90
90
|
form.append('file_name', fileName);
|
|
91
91
|
const response = await this.apiClient.post('/upload', form, {
|
|
92
92
|
headers: {
|
|
93
|
-
...form.getHeaders()
|
|
94
|
-
}
|
|
93
|
+
...form.getHeaders(),
|
|
94
|
+
},
|
|
95
95
|
});
|
|
96
96
|
if (response.data && response.data.id) {
|
|
97
97
|
return response.data.id;
|
|
@@ -51,6 +51,6 @@ export declare class LocalImage extends RpcImage {
|
|
|
51
51
|
export declare class RawImage extends RpcImage {
|
|
52
52
|
private assetKey;
|
|
53
53
|
constructor(assetKey: string);
|
|
54
|
-
resolve(
|
|
54
|
+
resolve(__imageService: ImageService): Promise<string | undefined>;
|
|
55
55
|
getCacheKey(): string;
|
|
56
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hieuxyz/rpc",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A Discord Rich Presence library for Node.js",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
|
-
"dev": "node -r ts-node/register --env-file=.env examples/main.ts"
|
|
14
|
+
"dev": "node -r ts-node/register --env-file=.env examples/main.ts",
|
|
15
|
+
"lint": "eslint \"src/**/*.ts\" --report-unused-disable-directives --max-warnings 0",
|
|
16
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
17
|
+
"format": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\""
|
|
15
18
|
},
|
|
16
19
|
"keywords": [
|
|
17
20
|
"discord",
|
|
@@ -25,9 +28,14 @@
|
|
|
25
28
|
"ws": "^8.18.3"
|
|
26
29
|
},
|
|
27
30
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^24.
|
|
31
|
+
"@types/node": "^24.6.2",
|
|
29
32
|
"@types/ws": "^8.18.1",
|
|
33
|
+
"eslint": "^9.36.0",
|
|
34
|
+
"eslint-config-prettier": "^10.1.8",
|
|
35
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
36
|
+
"prettier": "^3.6.2",
|
|
30
37
|
"ts-node": "^10.9.2",
|
|
31
|
-
"typescript": "^5.9.
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"typescript-eslint": "^8.45.0"
|
|
32
40
|
}
|
|
33
41
|
}
|