@imaentity/selfjs 3.3.0-b → 4.0.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 +1 -2
- package/docs.md +841 -473
- package/package.json +1 -2
- package/self.js +723 -802
- package/changelog.log +0 -37
package/self.js
CHANGED
|
@@ -1,811 +1,732 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name SelfJS
|
|
3
3
|
* @description Breaking Discord's TOS to bot user accounts.
|
|
4
|
-
* @author
|
|
5
|
-
* @version
|
|
4
|
+
* @author Entity
|
|
5
|
+
* @version 4.0
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const EventEmitter = require("events");
|
|
8
9
|
const https = require("https");
|
|
9
10
|
const ws = require("ws");
|
|
10
|
-
const fs = require("fs");
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
for(const i in inData.attachments) {
|
|
73
|
-
const attach = inData.attachments[i];
|
|
74
|
-
|
|
75
|
-
msgData = Buffer.concat([msgData, Buffer.from(`--${boundary}\r\n`)]);
|
|
76
|
-
msgData = Buffer.concat([msgData, Buffer.from(`Content-Disposition: form-data; name="files[${i}]"; filename="${attach.filename}"\r\n`)]);
|
|
77
|
-
msgData = Buffer.concat([msgData, Buffer.from(`Content-Type: ${attach.content_type}\r\n\r\n`)]);
|
|
78
|
-
msgData = Buffer.concat([msgData, fs.existsSync(msgFiles[i]) ? fs.readFileSync(msgFiles[i], "") : Buffer.from("")]);
|
|
79
|
-
msgData = Buffer.concat([msgData, Buffer.from("\r\n")]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
msgData = Buffer.concat([msgData, Buffer.from(`--${boundary}--`)]);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const options = {
|
|
86
|
-
...module.exports.APIBaseOpt,
|
|
87
|
-
method: "POST",
|
|
88
|
-
path: `/api/webhooks/${webID}/${webTok}`,
|
|
89
|
-
headers: {
|
|
90
|
-
"Content-Type": contentType,
|
|
91
|
-
"Content-Length": Buffer.byteLength(msgData)
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
return new Promise(function(resolve) {
|
|
96
|
-
const req = https.request(options, function(res) {
|
|
97
|
-
const chunks = [];
|
|
98
|
-
|
|
99
|
-
res.on("data", function(chunk) {
|
|
100
|
-
chunks.push(chunk);
|
|
101
|
-
}).on("end", function() {
|
|
102
|
-
const data = Buffer.concat(chunks);
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
resolve(JSON.parse(data));
|
|
106
|
-
} catch(e) {
|
|
107
|
-
resolve({});
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
req.write(msgData);
|
|
113
|
-
req.end();
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
Client: class {
|
|
118
|
-
constructor() {
|
|
119
|
-
this.userID = null;
|
|
120
|
-
this.token = null;
|
|
121
|
-
this.socket = null;
|
|
122
|
-
this.sessionID = null;
|
|
123
|
-
this.onMessageFunction = null;
|
|
124
|
-
this.onEditFunction = null;
|
|
125
|
-
this.sequenceID = null;
|
|
126
|
-
this.resumeURL = null;
|
|
127
|
-
this.statusUpdateFunction = null;
|
|
128
|
-
this.messageDeleteFunction = null;
|
|
129
|
-
this.latency = null;
|
|
130
|
-
this.beforeHB = null;
|
|
131
|
-
this.hearbeatInterval = null;
|
|
132
|
-
this.receivedLastHB = true;
|
|
133
|
-
|
|
134
|
-
// Session, token, sequence
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async onSockMsg(payload) {
|
|
138
|
-
payload = JSON.parse(payload.toString());
|
|
139
|
-
|
|
140
|
-
if(payload.op == 10) {
|
|
141
|
-
if(logMsgs) console.log("[MainControlSocket] Hello ACK received");
|
|
142
|
-
|
|
143
|
-
if(logMsgs) console.log("[MainControlSocket] Sending heartbeat");
|
|
144
|
-
this.beforeHB = Date.now();
|
|
145
|
-
this.socket.send(module.exports.jsonEncode({op: 1, d: this.sequenceID}));
|
|
146
|
-
|
|
147
|
-
this.hearbeatInterval = setInterval(function() {
|
|
148
|
-
if(logMsgs) console.log("[MainControlSocket] Sending heartbeat");
|
|
149
|
-
this.beforeHB = Date.now();
|
|
150
|
-
this.socket.send(module.exports.jsonEncode({op: 1, d: this.sequenceID}));
|
|
151
|
-
|
|
152
|
-
if(!this.receivedLastHB) {
|
|
153
|
-
if(logMsgs) console.log("[MainControlSocket] Heartbeat ACK failed, reconnecting");
|
|
154
|
-
|
|
155
|
-
this.receivedLastHB = true;
|
|
156
|
-
this.reconnect(this.resumeURL, this.token, this.sessionID, this.sequenceID);
|
|
157
|
-
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.receivedLastHB = false;
|
|
162
|
-
}.bind(this), payload.d.heartbeat_interval);
|
|
163
|
-
} else if(payload.op == 11) {
|
|
164
|
-
if(logMsgs) console.log("[MainControlSocket] Heartbeat ACK received");
|
|
165
|
-
this.latency = Date.now() - this.beforeHB;
|
|
166
|
-
|
|
167
|
-
this.receivedLastHB = true;
|
|
168
|
-
} else if(payload.op == 0) {
|
|
169
|
-
this.sequenceID = payload.s;
|
|
170
|
-
|
|
171
|
-
if(payload.t == "READY") {
|
|
172
|
-
this.userID = payload.d.user.id;
|
|
173
|
-
this.sessionID = payload.d.session_id;
|
|
174
|
-
this.resumeURL = payload.d.resume_gateway_url;
|
|
175
|
-
|
|
176
|
-
if(logMsgs) console.log("[MainControlSocket] Ready message received");
|
|
177
|
-
resolve();
|
|
178
|
-
} else if(payload.t == "MESSAGE_CREATE") {
|
|
179
|
-
const ackData = module.exports.jsonEncode({token: null});
|
|
180
|
-
const options = {
|
|
181
|
-
...module.exports.APIBaseOpt,
|
|
182
|
-
method: "POST",
|
|
183
|
-
path: `/api/v10/channels/${payload.d.channel_id}/messages/${payload.d.id}/ack`,
|
|
184
|
-
headers: {
|
|
185
|
-
"Authorization": this.token,
|
|
186
|
-
"Content-Type": "application/json",
|
|
187
|
-
"Content-Length": ackData.length
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const req = https.request(options);
|
|
192
|
-
|
|
193
|
-
req.write(ackData);
|
|
194
|
-
req.end();
|
|
195
|
-
|
|
196
|
-
try {payload.d.author.self = payload.d.author.id == this.userID;} catch(e) {}
|
|
197
|
-
|
|
198
|
-
if(typeof this.onMessageFunction == "function") this.onMessageFunction(payload.d);
|
|
199
|
-
} else if(payload.t == "MESSAGE_UPDATE") {
|
|
200
|
-
try {payload.d.author.self = payload.d.author.id == this.userID;} catch(e) {}
|
|
201
|
-
|
|
202
|
-
if(typeof this.onEditFunction == "function") this.onEditFunction(payload.d);
|
|
203
|
-
} else if(payload.t == "PRESENCE_UPDATE") {
|
|
204
|
-
try {payload.d.author.self = payload.d.author.id == this.userID;} catch(e) {}
|
|
205
|
-
|
|
206
|
-
if(typeof this.statusUpdateFunction == "function") this.statusUpdateFunction(payload.d);
|
|
207
|
-
} else if(payload.t == "MESSAGE_DELETE") {
|
|
208
|
-
if(typeof this.onDeleteFunction == "function") this.onDeleteFunction(payload.d);
|
|
209
|
-
}
|
|
210
|
-
} else if(payload.op == 7) {
|
|
211
|
-
if(logMsgs) console.log("[MainControlSocket] Reconnect message received");
|
|
212
|
-
|
|
213
|
-
this.receivedLastHB = true;
|
|
214
|
-
this.reconnect(this.resumeURL, this.token, this.sessionID, this.sequenceID);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
reconnect(resumeURL, token, sessionID, sequence) {
|
|
219
|
-
this.socket.terminate();
|
|
220
|
-
if(this.hearbeatInterval)
|
|
221
|
-
clearInterval(this.hearbeatInterval);
|
|
222
|
-
|
|
223
|
-
this.socket = new ws(resumeURL);
|
|
224
|
-
|
|
225
|
-
this.socket.on("open", function() {
|
|
226
|
-
this.socket.send(module.exports.jsonEncode({
|
|
227
|
-
op: 6,
|
|
228
|
-
d: {
|
|
229
|
-
token: token,
|
|
230
|
-
session_id: sessionID,
|
|
231
|
-
seq: sequence
|
|
232
|
-
}
|
|
233
|
-
}));
|
|
234
|
-
}.bind(this));
|
|
235
|
-
|
|
236
|
-
this.socket.on("message", onSockMsg);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
login(token, isMobile = false, logMsgs = false) {
|
|
240
|
-
return new Promise(function(resolve) {
|
|
241
|
-
this.token = token;
|
|
242
|
-
this.socket = new ws("wss://gateway.discord.gg?v=10&encoding=json");
|
|
243
|
-
this.userID = null;
|
|
244
|
-
this.sessionID = null;
|
|
245
|
-
|
|
246
|
-
this.socket.on("open", function() {
|
|
247
|
-
if(logMsgs) console.log("[MainControlSocket] Sending hello");
|
|
248
|
-
|
|
249
|
-
this.socket.send(module.exports.jsonEncode({
|
|
250
|
-
op: 2,
|
|
251
|
-
d: {
|
|
252
|
-
token: this.token,
|
|
253
|
-
properties: {
|
|
254
|
-
$os: isMobile ? "linux" : "windows",
|
|
255
|
-
$browser: isMobile ? "Discord iOS" : "SelfJS",
|
|
256
|
-
$device: isMobile ? "discord.gblk" : "SelfJS"
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}));
|
|
260
|
-
}.bind(this));
|
|
261
|
-
|
|
262
|
-
this.socket.on("message", onSockMsg);
|
|
263
|
-
}.bind(this));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
makeRequest(options = {}, base = module.exports.APIBaseOpt) {
|
|
267
|
-
const path = options.path || '/';
|
|
268
|
-
const method = options.method || "GET";
|
|
269
|
-
const headers = options.headers || {};
|
|
270
|
-
const body = options.body || undefined;
|
|
271
|
-
|
|
272
|
-
const strBody = module.exports.jsonEncode(body) ?? "";
|
|
273
|
-
|
|
274
|
-
headers["Authorization"] = this.token;
|
|
275
|
-
headers["Content-Type"] = "application/json";
|
|
276
|
-
headers["Content-Length"] = strBody.length;
|
|
277
|
-
|
|
278
|
-
const reqOptions = {
|
|
279
|
-
...base,
|
|
280
|
-
path,
|
|
281
|
-
method,
|
|
282
|
-
headers
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
return new Promise(function(resolve) {
|
|
286
|
-
const req = https.request(reqOptions, function(res) {
|
|
287
|
-
const chunks = [];
|
|
288
|
-
|
|
289
|
-
res.on("data", function(chunk) {
|
|
290
|
-
chunks.push(chunk);
|
|
291
|
-
}).on("end", function() {
|
|
292
|
-
resolve(JSON.parse(Buffer.concat(chunks)));
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
if(strBody) req.write(strBody);
|
|
297
|
-
req.end();
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
onMessage(msgFunc) {
|
|
302
|
-
this.onMessageFunction = msgFunc;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
onMessageEdit(editFunc) {
|
|
306
|
-
this.onEditFunction = editFunc;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
onMessageDelete(deleteFunc) {
|
|
310
|
-
this.onDeleteFunction = deleteFunc;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
onStatusUpdate(statusFunc) {
|
|
314
|
-
this.statusUpdateFunction = statusFunc;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async getDMChannel(userID) {
|
|
318
|
-
return await this.makeRequest({
|
|
319
|
-
method: "POST",
|
|
320
|
-
path: "/api/v10/users/@me/channels",
|
|
321
|
-
body: {
|
|
322
|
-
recipients: [userID]
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async getRoles(serverID, userID) {
|
|
328
|
-
return (await this.makeRequest({
|
|
329
|
-
method: "GET",
|
|
330
|
-
path: `/api/v10/guilds/${serverID}/members/${userID}`,
|
|
331
|
-
})).guild_member.roles;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
uploadFile(channelID, fileName, isSpoiled = false, msgContent = "", messageID = null) {
|
|
335
|
-
const fileData = fs.readFileSync(fileName);
|
|
336
|
-
|
|
337
|
-
if(fileData.length >= 26214400) return null;
|
|
338
|
-
|
|
339
|
-
fileName = fileName.split('/').pop();
|
|
340
|
-
|
|
341
|
-
const requestData = module.exports.jsonEncode({
|
|
342
|
-
files: [
|
|
343
|
-
{
|
|
344
|
-
filename: fileName,
|
|
345
|
-
file_size: fileData.length,
|
|
346
|
-
id: '1'
|
|
347
|
-
}
|
|
348
|
-
]
|
|
349
|
-
});
|
|
350
|
-
const requestOptions = {
|
|
351
|
-
...module.exports.APIBaseOpt,
|
|
352
|
-
method: "POST",
|
|
353
|
-
path: `/api/v10/channels/${channelID}/attachments`,
|
|
354
|
-
headers: {
|
|
355
|
-
"Content-Type": "application/json",
|
|
356
|
-
"Authorization": this.token,
|
|
357
|
-
"Content-Length": requestData.length
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
return new Promise(function(resolve) {
|
|
362
|
-
const requestReq = https.request(requestOptions, function(requestRes) {
|
|
363
|
-
const requestChunks = [];
|
|
364
|
-
const fileTable = {
|
|
365
|
-
png: "image/png",
|
|
366
|
-
jpg: "image/jpeg",
|
|
367
|
-
jpeg: "image/jpeg",
|
|
368
|
-
js: "application/javascript",
|
|
369
|
-
json: "application/json",
|
|
370
|
-
html: "text/html",
|
|
371
|
-
mp4: "video/mp4",
|
|
372
|
-
mp3: "audio/mpeg",
|
|
373
|
-
css: "text/css",
|
|
374
|
-
c: "text/x-c",
|
|
375
|
-
cpp: "text/x-c",
|
|
376
|
-
h: "text/x-c",
|
|
377
|
-
hpp: "text/x-c",
|
|
378
|
-
bat: "application/x-msdownload",
|
|
379
|
-
exe: "application/x-msdownload",
|
|
380
|
-
asm: "text/x-asm",
|
|
381
|
-
webp: "image/webp",
|
|
382
|
-
webm: "video/webm",
|
|
383
|
-
mov: "video/quicktime",
|
|
384
|
-
mkv: "video/x-matroska",
|
|
385
|
-
java: "text/x-java-source",
|
|
386
|
-
class: "application/java-vm",
|
|
387
|
-
jar: "application/java-archive",
|
|
388
|
-
wav: "audio/x-wav",
|
|
389
|
-
zip: "application/zip",
|
|
390
|
-
rar: "application/x-rar-compressed",
|
|
391
|
-
txt: "text/plain",
|
|
392
|
-
log: "text/plain",
|
|
393
|
-
lua: "text/x-lua",
|
|
394
|
-
xml: "application/xml"
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
requestRes.on("data", function(chunk) {
|
|
398
|
-
requestChunks.push(chunk);
|
|
399
|
-
}).on("end", function() {
|
|
400
|
-
const requestResData = JSON.parse(Buffer.concat(requestChunks));
|
|
401
|
-
|
|
402
|
-
const fileOptions = {
|
|
403
|
-
host: "discord-attachments-uploads-prd.storage.googleapis.com",
|
|
404
|
-
port: 443,
|
|
405
|
-
method: "PUT",
|
|
406
|
-
path: `/${requestResData.attachments[0].upload_url.split('/').slice(3).join('/')}`,
|
|
407
|
-
headers: {
|
|
408
|
-
"Content-Type": fileTable[fileName.split('.').slice(-1)[0]] ?? false,
|
|
409
|
-
"Content-Length": fileData.length
|
|
410
|
-
}
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
const fileRequest = https.request(fileOptions, function(fileRes) {
|
|
414
|
-
const fileChunks = [];
|
|
415
|
-
|
|
416
|
-
fileRes.on("data", function(chunk) {
|
|
417
|
-
fileChunks.push(chunk);
|
|
418
|
-
}).on("end", function() {
|
|
419
|
-
let finalData = module.exports.jsonEncode({
|
|
420
|
-
content: msgContent,
|
|
421
|
-
attachments: [
|
|
422
|
-
{
|
|
423
|
-
id: '0',
|
|
424
|
-
filename: (isSpoiled ? "SPOILER_" : "") + fileName,
|
|
425
|
-
uploaded_filename: requestResData.attachments[0].upload_filename
|
|
426
|
-
}
|
|
427
|
-
]
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
if(messageID) {
|
|
431
|
-
finalData = JSON.parse(finalData);
|
|
432
|
-
finalData.message_reference = {
|
|
433
|
-
channel_id: channelID,
|
|
434
|
-
message_id: messageID
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
finalData = module.exports.jsonEncode(finalData);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const finalOptions = {
|
|
441
|
-
...module.exports.APIBaseOpt,
|
|
442
|
-
method: "POST",
|
|
443
|
-
path: `/api/v10/channels/${channelID}/messages`,
|
|
444
|
-
headers: {
|
|
445
|
-
"Content-Type": "application/json",
|
|
446
|
-
"Authorization": this.token,
|
|
447
|
-
"Content-Length": finalData.length
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
const finalReq = https.request(finalOptions, function(finalRes) {
|
|
452
|
-
const finalChunks = [];
|
|
453
|
-
|
|
454
|
-
finalRes.on("data", function(chunk) {
|
|
455
|
-
finalChunks.push(chunk);
|
|
456
|
-
}).on("end", function() {
|
|
457
|
-
resolve(JSON.parse(Buffer.concat(finalChunks)));
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
finalReq.write(finalData);
|
|
462
|
-
finalReq.end();
|
|
463
|
-
}.bind(this));
|
|
464
|
-
}.bind(this));
|
|
465
|
-
|
|
466
|
-
fileRequest.write(fileData);
|
|
467
|
-
fileRequest.end();
|
|
468
|
-
}.bind(this));
|
|
469
|
-
}.bind(this));
|
|
470
|
-
|
|
471
|
-
requestReq.write(requestData);
|
|
472
|
-
requestReq.end();
|
|
473
|
-
}.bind(this));
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
async search(channelID, options) {
|
|
477
|
-
options = options || {};
|
|
478
|
-
|
|
479
|
-
const pinned = options.pinned ?? null;
|
|
480
|
-
const author = options.author ?? null;
|
|
481
|
-
const mentions = options.mentions ?? null;
|
|
482
|
-
const has = options.has ?? null;
|
|
483
|
-
const minDate = options.minDate ?? null;
|
|
484
|
-
const maxDate = options.maxData ?? null;
|
|
485
|
-
const content = options.content ?? null;
|
|
486
|
-
const offset = options.offset ?? null;
|
|
487
|
-
|
|
488
|
-
let path = `/api/v10/channels/${channelID}/messages/search?`;
|
|
489
|
-
|
|
490
|
-
if(pinned != null) path += `pinned=${pinned}&`;
|
|
491
|
-
if(minDate != null) path += `min_id=${minDate}&`;
|
|
492
|
-
if(maxDate != null) path += `max_id=${maxDate}&`;
|
|
493
|
-
if(offset != null) path += `offset=${offset}&`;
|
|
494
|
-
if(content != null) path += `content=${encodeURI(content)}&`;
|
|
495
|
-
if(author != null) path += author.map((e) => `author_id=${e}`).join('&') + '&';
|
|
496
|
-
if(mentions != null) path += mentions.map((e) => `mentions=${e}`).join('&') + '&';
|
|
497
|
-
if(has != null) path += has.map((e) => `has=${e}`).join('&');
|
|
498
|
-
|
|
499
|
-
return await this.makeRequest({
|
|
500
|
-
method: "GET",
|
|
501
|
-
path: path
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
async getUserProfile(userID, mutualGuilds=false) {
|
|
506
|
-
return await this.makeRequest({
|
|
507
|
-
method: "GET",
|
|
508
|
-
path: `/api/v10/users/${userID}/profile?with_mutual_guilds=${mutualGuilds}`
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async getUserData(userID) {
|
|
513
|
-
return await this.makeRequest({
|
|
514
|
-
method: "GET",
|
|
515
|
-
path: `/api/v10/users/${userID}`
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
async setRolesForMemeber(serverID, userID, roleIDs) {
|
|
520
|
-
return await this.makeRequest({
|
|
521
|
-
method: "PATCH",
|
|
522
|
-
path: `/api/v10/guilds/${serverID}/members/${userID}`,
|
|
523
|
-
body: roleIDs
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async sendMessage(channelID, message) {
|
|
528
|
-
return await this.makeRequest({
|
|
529
|
-
method: "POST",
|
|
530
|
-
path: `/api/v10/channels/${channelID}/messages`,
|
|
531
|
-
body: {
|
|
532
|
-
content: message
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async replyToMessage(channelID, messageID, message) {
|
|
538
|
-
return await this.makeRequest({
|
|
539
|
-
method: "POST",
|
|
540
|
-
path: `/api/v10/channels/${channelID}/messages`,
|
|
541
|
-
body: {
|
|
542
|
-
content: message,
|
|
543
|
-
message_reference: {
|
|
544
|
-
channel_id: channelID,
|
|
545
|
-
message_id: messageID
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async createChannel(guildID, name, type, parentID = null) {
|
|
552
|
-
return await this.makeRequest({
|
|
553
|
-
method: "POST",
|
|
554
|
-
path: `/api/v10/guilds/${guildID}/channels`,
|
|
555
|
-
body: {
|
|
556
|
-
name,
|
|
557
|
-
type,
|
|
558
|
-
parent_id: parentID
|
|
559
|
-
}
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
async setChannelPermissions(channelID, userID, allow, deny) {
|
|
564
|
-
return await this.makeRequest({
|
|
565
|
-
method: "PUT",
|
|
566
|
-
path: `/api/v10/channels/${channelID}/permissions/${userID}`,
|
|
567
|
-
body: {
|
|
568
|
-
id: userID,
|
|
569
|
-
allow,
|
|
570
|
-
deny,
|
|
571
|
-
type: 1
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
async getMessages(channelID, limit) {
|
|
577
|
-
return await this.makeRequest({
|
|
578
|
-
method: "GET",
|
|
579
|
-
path: `/api/v10/channels/${channelID}/messages?limit=${limit}`
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async getUsers(guildID) {
|
|
584
|
-
return await this.makeRequest({
|
|
585
|
-
method: "GET",
|
|
586
|
-
path: `/api/v10/guilds/${guildID}/members`
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async removeFromChannel(channelID, userID) {
|
|
591
|
-
return await this.makeRequest({
|
|
592
|
-
method: "DELETE",
|
|
593
|
-
path: `/api/v10/channels/${channelID}/recipients/${userID}`
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
async leaveChannel(channelID) {
|
|
598
|
-
return await this.makeRequest({
|
|
599
|
-
method: "DELETE",
|
|
600
|
-
path: `/api/v10/channels/${channelID}`
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async ring(channelID, userIDs) {
|
|
605
|
-
return await this.makeRequest({
|
|
606
|
-
method: "POST",
|
|
607
|
-
path: `/api/v10/channels/${channelID}/call/ring`,
|
|
608
|
-
body: {
|
|
609
|
-
recipients: userIDs
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
async stopRinging(channelID, userIDs) {
|
|
615
|
-
return await this.makeRequest({
|
|
616
|
-
method: "POST",
|
|
617
|
-
path: `/api/v10/channels/${channelID}/call/stop-ringing`,
|
|
618
|
-
body: {
|
|
619
|
-
recipients: userIDs
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
async addToChannel(channelID, userID) {
|
|
625
|
-
return await this.makeRequest({
|
|
626
|
-
method: "PUT",
|
|
627
|
-
path: `/api/v10/channels/${channelID}/recipients/${userID}`,
|
|
628
|
-
body: "junkValue"
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
async getAvatar(userID, size = 256) {
|
|
633
|
-
let avatarID = null;
|
|
634
|
-
|
|
635
|
-
try {
|
|
636
|
-
avatarID = await this.getUserData(userID);
|
|
637
|
-
avatarID = avatarID.avatar;
|
|
638
|
-
} catch(e) {
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return await this.makeRequest({
|
|
643
|
-
method: "GET",
|
|
644
|
-
path: `/avatars/${userID}/${avatarID}.png?size=${size}`
|
|
645
|
-
}, module.exports.CDNBaseOpt);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
async addFriend(userID) {
|
|
649
|
-
return await this.makeRequest({
|
|
650
|
-
method: "PUT",
|
|
651
|
-
path: `/api/v10/users/@me/relationships/${userID}`,
|
|
652
|
-
body: "junkValue"
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
async createServer(options) {
|
|
657
|
-
options = options || {};
|
|
658
|
-
|
|
659
|
-
const name = options.name ?? "Server";
|
|
660
|
-
const icon = options.icon ?? null;
|
|
661
|
-
|
|
662
|
-
return await this.makeRequest({
|
|
663
|
-
method: "POST",
|
|
664
|
-
path: "/api/v10/guilds",
|
|
665
|
-
body: {
|
|
666
|
-
channels: [],
|
|
667
|
-
guild_template_code: null,
|
|
668
|
-
system_channel_id: null,
|
|
669
|
-
name,
|
|
670
|
-
icon,
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async editMessage(channelID, messageID, newMessage) {
|
|
676
|
-
return await this.makeRequest({
|
|
677
|
-
method: "PATCH",
|
|
678
|
-
path: `/api/v10/channels/${channelID}/messages/${messageID}`,
|
|
679
|
-
body: {
|
|
680
|
-
content: newMessage
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
async removeFriend(userID) {
|
|
686
|
-
return await this.makeRequest({
|
|
687
|
-
method: "DELETE",
|
|
688
|
-
path: `/api/v10/users/@me/relationships/${userID}`
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
async renameChannel(channelID, channelName) {
|
|
693
|
-
return await this.makeRequest({
|
|
694
|
-
method: "PATCH",
|
|
695
|
-
path: `/api/v10/channels/${channelID}`,
|
|
696
|
-
body: {
|
|
697
|
-
name: channelName
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
async block(userID) {
|
|
703
|
-
return await this.makeRequest({
|
|
704
|
-
method: "PUT",
|
|
705
|
-
path: `/api/v10/users/@me/relationships/${userID}`,
|
|
706
|
-
body: {
|
|
707
|
-
type: 2
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
unblock(userID) {
|
|
713
|
-
return new Promise(function(resolve) {
|
|
714
|
-
this.removeFriend(userID).then(resolve);
|
|
715
|
-
}.bind(this));
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async deleteMessage(channelID, messageID) {
|
|
719
|
-
return await this.makeRequest({
|
|
720
|
-
method: "DELETE",
|
|
721
|
-
path: `/api/v10/channels/${channelID}/messages/${messageID}`
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
setStatus(status, activities, afk = false) {
|
|
726
|
-
return new Promise(function (resolve) {
|
|
727
|
-
this.socket.send(module.exports.jsonEncode({
|
|
728
|
-
op: 3,
|
|
729
|
-
d: {
|
|
730
|
-
since: Date.now(),
|
|
731
|
-
status,
|
|
732
|
-
activities,
|
|
733
|
-
afk
|
|
734
|
-
}
|
|
735
|
-
}));
|
|
736
|
-
|
|
737
|
-
setTimeout(resolve, 1000);
|
|
738
|
-
}.bind(this));
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
async createGroupChat(userIDs) {
|
|
742
|
-
return await this.makeRequest({
|
|
743
|
-
method: "POST",
|
|
744
|
-
path: `/api/v10/users/@me/channels`,
|
|
745
|
-
body: {
|
|
746
|
-
recipients: userIDs
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
async startTyping(channelID) {
|
|
752
|
-
return await this.makeRequest({
|
|
753
|
-
method: "POST",
|
|
754
|
-
path: `/api/v10/channels/${channelID}/typing`,
|
|
755
|
-
body: "junkValue"
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
async pinMessage(channelID, messageID) {
|
|
760
|
-
return await this.makeRequest({
|
|
761
|
-
method: "PUT",
|
|
762
|
-
path: `/api/v10/channels/${channelID}/pins/${messageID}`,
|
|
763
|
-
body: "junkValue"
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
async unpinMessage(channelID, messageID) {
|
|
768
|
-
return await this.makeRequest({
|
|
769
|
-
method: "DELETE",
|
|
770
|
-
path: `/api/v10/channels/${channelID}/pins/${messageID}`
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
async editNote(userID, note) {
|
|
775
|
-
return await this.makeRequest({
|
|
776
|
-
method: "PUT",
|
|
777
|
-
path: `/api/v10/users/@me/notes/${userID}`,
|
|
778
|
-
body: {
|
|
779
|
-
note
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
async getChannelData(channelID) {
|
|
785
|
-
return await this.makeRequest({
|
|
786
|
-
method: "GET",
|
|
787
|
-
path: `/api/v10/channels/${channelID}`
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
async transferOwnership(channelID, userID) {
|
|
792
|
-
return await this.makeRequest({
|
|
793
|
-
method: "PATCH",
|
|
794
|
-
path: `/api/v10/channels/${channelID}`,
|
|
795
|
-
body: {
|
|
796
|
-
owner: userID
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
async refreshURL(url) {
|
|
802
|
-
return await this.makeRequest({
|
|
803
|
-
method: "POST",
|
|
804
|
-
path: "/api/v10/attachments/refresh-urls",
|
|
805
|
-
body: {
|
|
806
|
-
attachment_urls: [url]
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
}
|
|
12
|
+
const DISCORD_EPOCH = 1420070400000n;
|
|
13
|
+
const SELF_VERSION = "v4.0";
|
|
14
|
+
const WS_ENDPOINT = "wss://gateway.discord.gg?v=10&encoding=json";
|
|
15
|
+
const LOGIN_PROPS = {
|
|
16
|
+
os: process.platform,
|
|
17
|
+
browser: "SelfJS",
|
|
18
|
+
device: "NodeJS"
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const Status = {
|
|
22
|
+
PLAYING : 0,
|
|
23
|
+
STREAMING : 1,
|
|
24
|
+
LISTENING : 2,
|
|
25
|
+
WATCHING : 3,
|
|
26
|
+
CUSTOM_STATUS : 4,
|
|
27
|
+
COMPETING : 5
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const OPCODES = {
|
|
31
|
+
GATEWAY: {
|
|
32
|
+
DISPATCH : 0,
|
|
33
|
+
HEARTBEAT : 1,
|
|
34
|
+
IDENTIFY : 2,
|
|
35
|
+
PRESENCE_UPDATE : 3,
|
|
36
|
+
VOICE_STATE_UPDATE : 4,
|
|
37
|
+
RESUME : 6,
|
|
38
|
+
RECONNECT : 7,
|
|
39
|
+
REQUEST_GUILD_MEMBERS : 8,
|
|
40
|
+
INVALID_SESSION : 9,
|
|
41
|
+
HELLO : 10,
|
|
42
|
+
HEARTBEAT_ACK : 11,
|
|
43
|
+
REQUEST_SOUNDBOARD_SOUNDS : 31
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// fuck this stupid ass DAVE protocol
|
|
47
|
+
VOICE: {
|
|
48
|
+
IDENTIFY : 0,
|
|
49
|
+
SELECT_PROTOCOL : 1,
|
|
50
|
+
READY : 2,
|
|
51
|
+
HEARTBEAT : 3,
|
|
52
|
+
SESSION_DESCRIPTION : 4,
|
|
53
|
+
SPEAKING : 5,
|
|
54
|
+
HEARTBEAT_ACK : 6,
|
|
55
|
+
RESUME : 7,
|
|
56
|
+
HELLO : 8,
|
|
57
|
+
RESUMED : 9,
|
|
58
|
+
CLIENTS_CONNECT : 11,
|
|
59
|
+
CLIENT_DISCONNECT : 13,
|
|
60
|
+
DAVE_PREPARE_TRANSITION : 21,
|
|
61
|
+
DAVE_EXECUTE_TRANSITION : 22,
|
|
62
|
+
DAVE_TRANSITION_READY : 23,
|
|
63
|
+
DAVE_PREPARE_EPOCH : 24,
|
|
64
|
+
DAVE_MLS_EXTERNAL_SENDER : 25,
|
|
65
|
+
DAVE_MLS_KEY_PACKAGE : 26,
|
|
66
|
+
DAVE_MLS_PROPOSALS : 27,
|
|
67
|
+
DAVE_MLS_COMMIT_WELCOME : 28,
|
|
68
|
+
DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION : 29,
|
|
69
|
+
DAVE_MLS_WELCOME : 30,
|
|
70
|
+
DAVE_MLS_INVALID_COMMIT_WELCOME : 31
|
|
71
|
+
}
|
|
811
72
|
};
|
|
73
|
+
|
|
74
|
+
// Formats JSON in a way that makes the Discord API
|
|
75
|
+
// able to accept random unicode characters
|
|
76
|
+
function unison(jsonData) {
|
|
77
|
+
return JSON.Stringify(jsonData)?.replace(
|
|
78
|
+
/[\u007F-\uFFFF]/g,
|
|
79
|
+
c => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rawRequest(options={}) {
|
|
84
|
+
const host = options.host ?? "discord.com";
|
|
85
|
+
const port = options.port ?? 443;
|
|
86
|
+
const headers = options.headers ?? {};
|
|
87
|
+
const isJson = options.isJson ?? true;
|
|
88
|
+
const query = options.query ?? [];
|
|
89
|
+
const path = !options.useCustomPath ?
|
|
90
|
+
`/api/v10${options.path}` : options.path;
|
|
91
|
+
|
|
92
|
+
let body = options.body;
|
|
93
|
+
if(isJson && body != null) {
|
|
94
|
+
body = unison(body);
|
|
95
|
+
headers["Content-Type"] = "application/json";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
headers["Content-Length"] = body != null ? Buffer.byteLength(body) : 0;
|
|
99
|
+
|
|
100
|
+
if(!options.path || !options.method)
|
|
101
|
+
throw new Error(".path and .method properties are required!");
|
|
102
|
+
|
|
103
|
+
const rawQuery = Object.entries(query)
|
|
104
|
+
.filter(e => e[0] != null && e[1] != null)
|
|
105
|
+
.map(function(e) {
|
|
106
|
+
const key = encodeURIComponent(e[0]);
|
|
107
|
+
if(!Array.isArray(e[1])) return `${key}=${encodeURIComponent(e[1])}`;
|
|
108
|
+
return e[1].map(v => `${key}=${v}`).join('&');
|
|
109
|
+
}).join('&');
|
|
110
|
+
|
|
111
|
+
const reqOptions = {
|
|
112
|
+
host,
|
|
113
|
+
port,
|
|
114
|
+
path: `${path}${rawQuery != "" ? '?' : ""}${rawQuery}`,
|
|
115
|
+
headers,
|
|
116
|
+
method: options.method
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return new Promise(function(resolve) {
|
|
120
|
+
const req = https.request(reqOptions, function(res) {
|
|
121
|
+
const chunks = [];
|
|
122
|
+
|
|
123
|
+
res.on("data", c => chunks.push(c));
|
|
124
|
+
res.on("end", function() {
|
|
125
|
+
const data = Buffer.concat(chunks);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
if(isJson) resolve(JSON.parse(data));
|
|
129
|
+
} catch {}
|
|
130
|
+
|
|
131
|
+
resolve(data);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if(body != null) req.write(body);
|
|
136
|
+
req.end();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const POST = (options) => rawRequest({...options, method: "POST"});
|
|
141
|
+
const GET = (options) => rawRequest({...options, method: "GET"});
|
|
142
|
+
const PATCH = (options) => rawRequest({...options, method: "PATCH"});
|
|
143
|
+
const DELETE = (options) => rawRequest({...options, method: "DELETE"});
|
|
144
|
+
const PUT = (options) => rawRequest({...options, method: "PUT"});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validates the provided token and returns some user data from the websocket
|
|
148
|
+
* Only use this function if you need basic user data, but dont need to login
|
|
149
|
+
* @param {String} token The token to validate
|
|
150
|
+
* @returns {Promise<Object|null>} User data returned from the websocket, null if invalid
|
|
151
|
+
*/
|
|
152
|
+
async function validateToken(token) {
|
|
153
|
+
const socket = new ws(WS_ENDPOINT);
|
|
154
|
+
if(!token) return null;
|
|
155
|
+
|
|
156
|
+
socket.on("open", function() {
|
|
157
|
+
socket.send(unison({
|
|
158
|
+
op: OPCODES.GATEWAY.IDENTIFY,
|
|
159
|
+
d: {token, properties: LOGIN_PROPS}
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return await new Promise(function(resolve) {
|
|
164
|
+
socket.on("error", function() {
|
|
165
|
+
socket.close();
|
|
166
|
+
resolve(null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
socket.on("message", function(data) {
|
|
170
|
+
data = JSON.parse(data.toString());
|
|
171
|
+
if(data.op == OPCODES.GATEWAY.HELLO)
|
|
172
|
+
socket.send(unison({op: OPCODES.GATEWAY.HEARTBEAT, d: 0}));
|
|
173
|
+
|
|
174
|
+
if(data.op == OPCODES.GATEWAY.DISPATCH && data.t == "READY") {
|
|
175
|
+
socket.close();
|
|
176
|
+
resolve(data.d.user);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if(data.op == OPCODES.GATEWAY.INVALID_SESSION) {
|
|
180
|
+
socket.close();
|
|
181
|
+
resolve(null);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Creates a new token from the provided account credientials if possible
|
|
189
|
+
* Let's the user handle mfa if required by the account
|
|
190
|
+
* @param {Object} options Options for getting a token
|
|
191
|
+
* @param {String} options.email The email to use for finding a token
|
|
192
|
+
* @param {String} options.password The password to use for getting a token
|
|
193
|
+
* @returns {Promise<Object|null>} An object containing a token, mfa methods and a callback, or null
|
|
194
|
+
*/
|
|
195
|
+
async function createToken(options) {
|
|
196
|
+
const {email, password} = options;
|
|
197
|
+
if(!email || !password) return null;
|
|
198
|
+
|
|
199
|
+
const loginData = await POST({
|
|
200
|
+
path: "/auth/login",
|
|
201
|
+
body: {login: email, password}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if(!loginData.user_id) return null;
|
|
205
|
+
if(!loginData.mfa) return {
|
|
206
|
+
token: loginData.token,
|
|
207
|
+
user_id: loginData.user_id,
|
|
208
|
+
mfaRequired: false
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
user_id: loginData.user_id,
|
|
213
|
+
mfaRequired: true,
|
|
214
|
+
mfaMethods: Object.entries(loginData)
|
|
215
|
+
.filter(([k, v]) => k != "mfa" && v === true)
|
|
216
|
+
.map(([k, _v]) => k),
|
|
217
|
+
|
|
218
|
+
confirmMFA: async function(mfaMethod, data) {
|
|
219
|
+
if(mfaMethod == "mfa" || loginData[mfaMethod] !== true)
|
|
220
|
+
return null;
|
|
221
|
+
|
|
222
|
+
if(mfaMethod == "sms")
|
|
223
|
+
throw new Error("SMS MFA is currently unsupported");
|
|
224
|
+
|
|
225
|
+
return POST({
|
|
226
|
+
path: `/auth/mfa/${mfaMethod}`,
|
|
227
|
+
body: {
|
|
228
|
+
...data,
|
|
229
|
+
login_instance_id: loginData.login_instance_id,
|
|
230
|
+
ticket: loginData.ticket,
|
|
231
|
+
gift_code_sku_id: null,
|
|
232
|
+
login_source: null
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Converts a discord snowflake to a UTC timestamp
|
|
241
|
+
* @param {String} snowflake The discord snowflake to convert
|
|
242
|
+
* @returns {Number} The resultant UTC timestamp
|
|
243
|
+
*/
|
|
244
|
+
function snowflakeToUTC(snowflake) {
|
|
245
|
+
return Number((BigInt(snowflake) >> 22n) + DISCORD_EPOCH);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Converts a UTC timestamp to a discord snowflake
|
|
250
|
+
* Importantly, the resultant snowflake does not contain process id, worker id, or sequence values
|
|
251
|
+
* @param {Number} timestamp The UTC timestamp to convert
|
|
252
|
+
* @returns {String} The resultant snowflake
|
|
253
|
+
*/
|
|
254
|
+
function UTCToSnowflake(timestamp) {
|
|
255
|
+
return ((BigInt(timestamp) - DISCORD_EPOCH) << 22n).toString();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class Client extends EventEmitter {
|
|
259
|
+
#socket = null;
|
|
260
|
+
#sessionID = null;
|
|
261
|
+
#sequenceID = null;
|
|
262
|
+
#resumeURL = null;
|
|
263
|
+
#heartbeatInterval = null;
|
|
264
|
+
#lastHeartbeatSent = null;
|
|
265
|
+
#heartbeatIntTime = null;
|
|
266
|
+
#properties = null;
|
|
267
|
+
#debugLogs = null;
|
|
268
|
+
#POST = null;
|
|
269
|
+
#GET = null;
|
|
270
|
+
#PATCH = null;
|
|
271
|
+
#DELETE = null;
|
|
272
|
+
#PUT = null;
|
|
273
|
+
#handleHeartbeatACK = null;
|
|
274
|
+
#identifiedClient = null;
|
|
275
|
+
#intents = null;
|
|
276
|
+
|
|
277
|
+
constructor(options={}) {
|
|
278
|
+
super();
|
|
279
|
+
|
|
280
|
+
this.#properties = options.properties ?? LOGIN_PROPS;
|
|
281
|
+
this.#debugLogs = options.debugLogs ?? true;
|
|
282
|
+
this.#intents = options.intents ?? null;
|
|
283
|
+
|
|
284
|
+
this.token = null;
|
|
285
|
+
this.user = null;
|
|
286
|
+
this.latency = null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#log(...args) {
|
|
290
|
+
if(!this.#debugLogs) return;
|
|
291
|
+
console.log(`(SelfJS ${SELF_VERSION}) [MainControlSocket]`, ...args);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#handleHeartbeat(interval) {
|
|
295
|
+
if(this.#heartbeatInterval)
|
|
296
|
+
clearInterval(this.#heartbeatInterval);
|
|
297
|
+
|
|
298
|
+
this.#heartbeatIntTime = interval;
|
|
299
|
+
|
|
300
|
+
let heartbeatFailed = false;
|
|
301
|
+
function sendHeartbeat() {
|
|
302
|
+
if(heartbeatFailed) {
|
|
303
|
+
this.#log("Heartbeat failed, attempting reconnect");
|
|
304
|
+
this.#reconnect();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.#socket.send(unison({op: OPCODES.GATEWAY.HEARTBEAT, d: this.#sequenceID}));
|
|
309
|
+
this.#lastHeartbeatSent = Date.now();
|
|
310
|
+
heartbeatFailed = true;
|
|
311
|
+
|
|
312
|
+
this.#log("Sent heartbeat");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.#heartbeatInterval = setInterval(sendHeartbeat.bind(this), interval);
|
|
316
|
+
sendHeartbeat.call(this);
|
|
317
|
+
|
|
318
|
+
return () => {
|
|
319
|
+
this.latency = Date.now() - this.#lastHeartbeatSent;
|
|
320
|
+
heartbeatFailed = false;
|
|
321
|
+
|
|
322
|
+
this.#log("Heartbeat acknowledged, updated latency");
|
|
323
|
+
|
|
324
|
+
if(this.#identifiedClient) return;
|
|
325
|
+
this.#identifiedClient = true;
|
|
326
|
+
|
|
327
|
+
this.#socket.send(unison({
|
|
328
|
+
op: OPCODES.GATEWAY.IDENTIFY,
|
|
329
|
+
d: {
|
|
330
|
+
token: this.token,
|
|
331
|
+
properties: this.#properties,
|
|
332
|
+
...(this.#intents != null && {intents: this.#intents}) // what the fuck
|
|
333
|
+
}
|
|
334
|
+
}));
|
|
335
|
+
|
|
336
|
+
this.#log("Sent identifier packet");
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#reconnect() {
|
|
341
|
+
this.#socket.removeAllListeners("close");
|
|
342
|
+
this.#socket.terminate();
|
|
343
|
+
|
|
344
|
+
this.#socket = new ws(this.#resumeURL);
|
|
345
|
+
this.#socket.on("open", function() {
|
|
346
|
+
this.#socket.send(unison({
|
|
347
|
+
op: OPCODES.GATEWAY.RESUME,
|
|
348
|
+
d: {token: this.token, session_id: this.#sessionID, seq: this.#sequenceID}
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
this.#log("Requested session resume");
|
|
352
|
+
}.bind(this));
|
|
353
|
+
|
|
354
|
+
this.#socket.on("error", console.error);
|
|
355
|
+
this.#socket.on("close", this.#handleClose.bind(this));
|
|
356
|
+
this.#socket.on("message", this.#handlePacket.bind(this));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#handlePacket(data) {
|
|
360
|
+
const packet = JSON.parse(data.toString());
|
|
361
|
+
switch(packet.op) {
|
|
362
|
+
case OPCODES.GATEWAY.HELLO:
|
|
363
|
+
this.#handleHeartbeatACK = this.#handleHeartbeat(packet.d.heartbeat_interval);
|
|
364
|
+
break;
|
|
365
|
+
|
|
366
|
+
case OPCODES.GATEWAY.HEARTBEAT: {
|
|
367
|
+
this.#log("Received heartbeat timer reset");
|
|
368
|
+
this.#handleHeartbeatACK = this.#handleHeartbeat(this.#heartbeatIntTime);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
case OPCODES.GATEWAY.HEARTBEAT_ACK:
|
|
373
|
+
this.#handleHeartbeatACK();
|
|
374
|
+
break;
|
|
375
|
+
|
|
376
|
+
case OPCODES.GATEWAY.INVALID_SESSION: {
|
|
377
|
+
this.#log("Session invalidated");
|
|
378
|
+
|
|
379
|
+
if(!packet.d) {
|
|
380
|
+
this.disconnect();
|
|
381
|
+
this.emit("INVALID_SESSION");
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.#reconnect();
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
case OPCODES.GATEWAY.RECONNECT: {
|
|
390
|
+
this.#log("Received reconnect request");
|
|
391
|
+
this.#reconnect();
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case OPCODES.GATEWAY.DISPATCH:
|
|
396
|
+
this.#handleDispatch(packet);
|
|
397
|
+
break;
|
|
398
|
+
|
|
399
|
+
default: {
|
|
400
|
+
const opcodeName = Object.keys(OPCODES.GATEWAY)[Object.values(OPCODES.GATEWAY).indexOf(packet.op)];
|
|
401
|
+
this.#log(`Received unknown opcode: ${opcodeName ?? packet.op}`);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
#handleClose(code) {
|
|
408
|
+
this.#log(`Connection closed with code: ${code}`);
|
|
409
|
+
this.emit("DISCONNECT");
|
|
410
|
+
|
|
411
|
+
const reconnectableCodes = [
|
|
412
|
+
4000, 4001, 4002, 4003,
|
|
413
|
+
4005, 4007, 4008, 4009
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
if(!reconnectableCodes.includes(code)) {
|
|
417
|
+
this.login(this.token);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.#reconnect();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#handleDispatch(packet) {
|
|
425
|
+
this.#sequenceID = packet.s;
|
|
426
|
+
|
|
427
|
+
let ackMessage = true;
|
|
428
|
+
switch(packet.t) {
|
|
429
|
+
case "READY": {
|
|
430
|
+
this.#resumeURL = packet.d.resume_gateway_url;
|
|
431
|
+
this.#sessionID = packet.d.session_id;
|
|
432
|
+
this.user = packet.d.user;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
case "MESSAGE_CREATE": {
|
|
437
|
+
packet.d.preventACK = () => ackMessage = false;
|
|
438
|
+
packet.d.author.self = this.user.id == packet.d.author.id;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.emit(packet.t, packet.d);
|
|
444
|
+
if(
|
|
445
|
+
packet.d?.author?.id == this.user.id ||
|
|
446
|
+
packet.t != "MESSAGE_CREATE" ||
|
|
447
|
+
!ackMessage
|
|
448
|
+
) return;
|
|
449
|
+
|
|
450
|
+
this.ackMessage(packet.d);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
login(token) {
|
|
454
|
+
this.#POST = (options) => POST ({...options, headers: {"Authorization": token, ...options.headers}});
|
|
455
|
+
this.#GET = (options) => GET ({...options, headers: {"Authorization": token, ...options.headers}});
|
|
456
|
+
this.#PATCH = (options) => PATCH ({...options, headers: {"Authorization": token, ...options.headers}});
|
|
457
|
+
this.#DELETE = (options) => DELETE({...options, headers: {"Authorization": token, ...options.headers}});
|
|
458
|
+
this.#PUT = (options) => PUT ({...options, headers: {"Authorization": token, ...options.headers}});
|
|
459
|
+
|
|
460
|
+
this.token = token;
|
|
461
|
+
|
|
462
|
+
this.#identifiedClient = false;
|
|
463
|
+
this.#socket = new ws(this.#resumeURL ?? WS_ENDPOINT);
|
|
464
|
+
|
|
465
|
+
this.#socket.on("open", () => this.#log("Initialized socket"));
|
|
466
|
+
this.#socket.on("error", console.error);
|
|
467
|
+
this.#socket.on("close", this.#handleClose.bind(this));
|
|
468
|
+
this.#socket.on("message", this.#handlePacket.bind(this));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// temp, while other shits being added
|
|
472
|
+
_attachMethod(options={}) {
|
|
473
|
+
this[options.methodName] = function(...args) {
|
|
474
|
+
options.method(this.#socket, {
|
|
475
|
+
POST : this.#POST,
|
|
476
|
+
GET : this.#GET,
|
|
477
|
+
PATCH : this.#PATCH,
|
|
478
|
+
DELETE : this.#DELETE,
|
|
479
|
+
PUT : this.#PUT
|
|
480
|
+
}, ...args);
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Acknowledges that a message was sent, and removes the notification for it
|
|
486
|
+
* @param {Object} message The message being acknowledged
|
|
487
|
+
* @param {String} message.channel_id The id of the channel which the message was sent in
|
|
488
|
+
* @param {String} message.id The id of the message that was sent
|
|
489
|
+
* @returns {Promise<Object>} The exact response from discord
|
|
490
|
+
*/
|
|
491
|
+
ackMessage(message) {
|
|
492
|
+
return POST({path: `/channels/${message.channel_id}/messages/${message.id}/ack`});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async #uploadFiles(msg) {
|
|
496
|
+
const files = msg.files.map(function(file, idx) {
|
|
497
|
+
return {
|
|
498
|
+
id: (idx + 1).toString(),
|
|
499
|
+
file_size: Buffer.byteLength(file.data),
|
|
500
|
+
...file
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const attachments = [];
|
|
505
|
+
const uploadSources = await this.#POST({
|
|
506
|
+
path: `/channels/${msg.channel_id}/attachments`,
|
|
507
|
+
body: {files}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
for(let i = 0; i < uploadSources.attachments.length; i++) {
|
|
511
|
+
const source = uploadSources.attachments[i];
|
|
512
|
+
const url = new URL(source.upload_url);
|
|
513
|
+
|
|
514
|
+
await PUT({
|
|
515
|
+
host: url.host,
|
|
516
|
+
port: url.port,
|
|
517
|
+
path: url.pathname + url.search,
|
|
518
|
+
useCustomPath: true, isJson: false,
|
|
519
|
+
body: msg.files[i].data,
|
|
520
|
+
headers: {
|
|
521
|
+
"Content-Type": "application/octet-stream"
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
attachments.push({
|
|
526
|
+
id: source.id,
|
|
527
|
+
uploaded_filename: source.upload_filename,
|
|
528
|
+
filename: `${msg.files[i].spoiled ? "SPOILER_" : ""}${files[i].filename}`,
|
|
529
|
+
...files[i]
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return attachments;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Sends a message in a specified channel, with optional file attachments
|
|
538
|
+
* @param {Object} message The message object to send
|
|
539
|
+
* @param {String} message.channel_id The id of the channel to send the message in
|
|
540
|
+
* @param {String} [message.content] The content for the message, if not specified, a files list must be
|
|
541
|
+
* @param {Array<Object>} [message.files] A list of files to send, if not specified, message content must be
|
|
542
|
+
* @param {Object} [message.message_reference] An optional message to reply to
|
|
543
|
+
* @param {String} message.message_reference.id The id of the message to reply to
|
|
544
|
+
* @param {String} message.message_reference.channel_id The channel id of the message to reply to, must be the same as message.channel_id
|
|
545
|
+
* @returns {Promise<Object>} The message object as stored on the server
|
|
546
|
+
*/
|
|
547
|
+
async sendMessage(message) {
|
|
548
|
+
if(message.files) message.attachments = await this.#uploadFiles(message);
|
|
549
|
+
if(message.message_reference) message.message_reference.message_id = message.message_reference.id;
|
|
550
|
+
|
|
551
|
+
return this.#POST({
|
|
552
|
+
path: `/channels/${message.channel_id}/messages`,
|
|
553
|
+
body: message
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Edits a message in a specified channel, with optional file attachments
|
|
559
|
+
* @param {Object} message The message object to edit
|
|
560
|
+
* @param {String} message.id The id of the message to edit
|
|
561
|
+
* @param {String} message.channel_id The id of the channel to edit the message in
|
|
562
|
+
* @param {String} [message.content] The content for the message, if not specified, a files list must be
|
|
563
|
+
* @param {Array<Object>} [message.files] A list of files to edit, if not specified, message content must be
|
|
564
|
+
* @returns {Promise<Object>} The message object as stored on the server
|
|
565
|
+
*/
|
|
566
|
+
async editMessage(message) {
|
|
567
|
+
if(message.files) message.attachments = await this.#uploadFiles(message);
|
|
568
|
+
|
|
569
|
+
return this.#PATCH({
|
|
570
|
+
path: `/channels/${message.channel_id}/messages/${message.id}`,
|
|
571
|
+
body: message
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Gets the channels active in the DM list of the user, channels can be both DMs and group chats
|
|
577
|
+
* @returns {Promise<Object>} The list of channels open in the user's DM list
|
|
578
|
+
*/
|
|
579
|
+
getOpenChannels() { return this.#GET({path: "/users/@me/channels"}); }
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Closes the current session and disconnects from discord
|
|
583
|
+
* Once this is done a client will no longer see events
|
|
584
|
+
* And will need to create a new session via the login function
|
|
585
|
+
* @param {number} [code] The close code to send to discord, 1000 if not provided
|
|
586
|
+
* @returns {void}
|
|
587
|
+
*/
|
|
588
|
+
disconnect(code=1000) {
|
|
589
|
+
this.#handleHeartbeatACK = null;
|
|
590
|
+
if (this.#heartbeatInterval)
|
|
591
|
+
clearInterval(this.#heartbeatInterval);
|
|
592
|
+
|
|
593
|
+
this.#socket.removeAllListeners("close");
|
|
594
|
+
this.#socket.close(code);
|
|
595
|
+
this.#socket = null;
|
|
596
|
+
|
|
597
|
+
this.emit("DISCONNECT");
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Sets the current activities, and status of the user
|
|
602
|
+
* @param {Object} options The options used to set the user data
|
|
603
|
+
* @param {"online" | "idle" | "dnd" | "invisible"} options.status Used to set the status of the client
|
|
604
|
+
* @param {Array<{
|
|
605
|
+
* type: number,
|
|
606
|
+
* name: String
|
|
607
|
+
* }>} options.activities Used to provide a list of activities
|
|
608
|
+
*
|
|
609
|
+
* @returns {void}
|
|
610
|
+
*/
|
|
611
|
+
setStatus(options) {
|
|
612
|
+
for(const activity of options.activities) {
|
|
613
|
+
if(activity.type == Status.CUSTOM_STATUS) {
|
|
614
|
+
activity.state = activity.name;
|
|
615
|
+
activity.name = "Custom Status";
|
|
616
|
+
} else if(activity.type == Status.STREAMING)
|
|
617
|
+
activity.details = activity.name;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
this.#socket.send(unison({
|
|
621
|
+
op: OPCODES.GATEWAY.PRESENCE_UPDATE,
|
|
622
|
+
d: {
|
|
623
|
+
afk: false,
|
|
624
|
+
since: Date.now(),
|
|
625
|
+
...options
|
|
626
|
+
}
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Logs out the current token
|
|
632
|
+
* @returns {Promise<Object>} The result of the logout request
|
|
633
|
+
*/
|
|
634
|
+
logout() {
|
|
635
|
+
this.disconnect();
|
|
636
|
+
return this.#POST({path: "/auth/logout"});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Adds a reaction to a specified message
|
|
641
|
+
* @param {Object} options The options specifying which message and reaction to add
|
|
642
|
+
* @param {String} options.emoji The emoji reaction to add to the message
|
|
643
|
+
* @param {String} options.channel_id The id of the channel the message is in
|
|
644
|
+
* @param {String} options.message_id The id of the message to add the reaction to
|
|
645
|
+
* @returns {Promise<Object>} The response from the server
|
|
646
|
+
*/
|
|
647
|
+
addReaction(options) {
|
|
648
|
+
const emoji = encodeURIComponent(options.emoji);
|
|
649
|
+
|
|
650
|
+
return this.#PUT({
|
|
651
|
+
path: `/channels/${options.channel_id}/messages/${options.message_id}/reactions/${emoji}/@me`
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Removes a reaction from a specified message
|
|
657
|
+
* @param {Object} options The options specifying which message and reaction to remove
|
|
658
|
+
* @param {String} options.emoji The emoji reaction to remove from the message
|
|
659
|
+
* @param {String} options.channel_id The id of the channel the message is in
|
|
660
|
+
* @param {String} options.message_id The id of the message to remove the reaction from
|
|
661
|
+
* @returns {Promise<Object>} The response from the server
|
|
662
|
+
*/
|
|
663
|
+
removeReaction(options) {
|
|
664
|
+
const emoji = encodeURIComponent(options.emoji);
|
|
665
|
+
|
|
666
|
+
return this.#DELETE({
|
|
667
|
+
path: `/channels/${options.channel_id}/messages/${options.message_id}/reactions/${emoji}/@me`
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Searches for messages in a certain channel
|
|
673
|
+
* @param {Object} options The options specifying what to search for and in which channel
|
|
674
|
+
* @param {String} options.channel_id The channel id to search in
|
|
675
|
+
* @param {String} [options.content] Only return messages that include this content
|
|
676
|
+
* @param {Array<String>} [options.authors] Only return messages from these user ids
|
|
677
|
+
* @param {Array<String>} [options.mentions] Only return messages that mention one these user ids
|
|
678
|
+
* @param {Array<"image" | "video" | "link" | "file" | "embed" | "sound" | "poll" | "sticker" | "snapshot">} [options.contentTypes] Searches for messages that contain one of these content types
|
|
679
|
+
* @param {Boolean} [options.pinned] Only included pinned messages in the search results
|
|
680
|
+
* @param {Array<"user" | "bot" | "webhook">} [options.authorTypes] Only return messages from these author types
|
|
681
|
+
* @param {"new" | "old" | "relevance"} [options.sort] Sort resulting messages in this order
|
|
682
|
+
* @param {Number} [options.offset] Skip this number of messages in the returned data
|
|
683
|
+
* @param {Number} [options.after] Only return messages after this utc timestamp
|
|
684
|
+
* @param {Number} [options.before] Only return messages before this utc timestamp
|
|
685
|
+
* @returns {Promise<Object>} The result of the search, contains both channel and message data
|
|
686
|
+
*/
|
|
687
|
+
search(options) {
|
|
688
|
+
const minDate = options.after ? UTCToSnowflake(options.after) : null;
|
|
689
|
+
const maxDate = options.before ? UTCToSnowflake(options.before) : null;
|
|
690
|
+
|
|
691
|
+
return this.#GET({
|
|
692
|
+
path: `/channels/${options.channel_id}/messages/search`,
|
|
693
|
+
query: {
|
|
694
|
+
sort_by: options.sort == "relevance" ? "relevance" : "timestamp",
|
|
695
|
+
sort_order: options.sort == "old" ? "asc" : "desc",
|
|
696
|
+
offset: options.offset ?? 0,
|
|
697
|
+
author_id: options.authors,
|
|
698
|
+
author_type: options.authorTypes,
|
|
699
|
+
pinned: options.pinned,
|
|
700
|
+
mentions: options.mentions,
|
|
701
|
+
has: options.contentTypes,
|
|
702
|
+
content: options.content,
|
|
703
|
+
min_id: minDate,
|
|
704
|
+
max_id: maxDate
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Gets recent messages in a certain channel
|
|
711
|
+
* @param {Object} options The options specifying what channel to get messages in and how many
|
|
712
|
+
* @param {String} options.channel_id The channel id to get messages from
|
|
713
|
+
* @param {Number} [options.limit] The max amount of messages to return
|
|
714
|
+
* @param {String} [options.before] Only return messages before this message id
|
|
715
|
+
* @returns {Promise<Array>} The list of messages in order from newest to oldest
|
|
716
|
+
*/
|
|
717
|
+
getMessages(options) {
|
|
718
|
+
return this.#GET({
|
|
719
|
+
path: `/channels/${options.channel_id}/messages`,
|
|
720
|
+
query: {limit: options.limit, before: options.before}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
module.exports = {
|
|
726
|
+
Status,
|
|
727
|
+
validateToken,
|
|
728
|
+
createToken,
|
|
729
|
+
UTCToSnowflake,
|
|
730
|
+
snowflakeToUTC,
|
|
731
|
+
Client
|
|
732
|
+
};
|