@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.
Files changed (5) hide show
  1. package/README.md +1 -2
  2. package/docs.md +841 -473
  3. package/package.json +1 -2
  4. package/self.js +723 -802
  5. 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 3.3.0b
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
- module.exports = {
13
- CDNBaseOpt: {
14
- host: "cdn.discordapp.com",
15
- port: 443
16
- },
17
-
18
- APIBaseOpt: {
19
- host: "discord.com",
20
- port: 443
21
- },
22
-
23
- status: {
24
- PLAYING: 0,
25
- STREAMING: 1,
26
- LISTENING: 2,
27
- WATCHING: 3,
28
- CUSTOM: 4,
29
- COMPETING: 5
30
- },
31
-
32
- sleep: function(millis) {
33
- return new Promise(function(res) {
34
- setTimeout(res, millis);
35
- });
36
- },
37
-
38
- jsonEncode: function(jsonData) {
39
- const str = JSON.stringify(jsonData);
40
-
41
- return str.replace(/[\u007F-\uFFFF]/g, function(chr) {
42
- const code = chr.charCodeAt(0).toString(16);
43
-
44
- return "\\u" + new Array(4 - code.length).fill('0').join("") + code;
45
- });
46
- },
47
-
48
- sendWebhookMessage: function(webhookID, webhookToken, inputData) {
49
- const boundary = "X-SELFJS-BOUNDARY";
50
-
51
- let contentType = "application/json";
52
- if(inputData.attachments && inputData.attachments.length > 0)
53
- contentType = `multipart/form-data; boundary=${boundary}`;
54
-
55
- const webID = inputData ? webhookID : webhookID.split("//")[1].split('/')[3];
56
- const webTok = inputData ? webhookToken : webhookID.split("//")[1].split('/')[4];
57
- const inData = inputData ? inputData : webhookToken;
58
-
59
- let msgData = Buffer.from(module.exports.jsonEncode(inData));
60
-
61
- if(inputData.attachments && inputData.attachments.length > 0) {
62
- let msgFiles = [...inData.files];
63
-
64
- delete inData.files;
65
- msgData = Buffer.from(module.exports.jsonEncode(inData));
66
-
67
- msgData = Buffer.concat([Buffer.from("Content-Type: application/json\r\n\r\n"), msgData]);
68
- msgData = Buffer.concat([Buffer.from('Content-Disposition: form-data; name="payload_json"\r\n'), msgData]);
69
- msgData = Buffer.concat([Buffer.from(`--${boundary}\r\n`), msgData]);
70
- msgData = Buffer.concat([msgData, Buffer.from("\r\n")]);
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
+ };