simple-discord-bot.js 0.1.4 → 0.2.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/api.js CHANGED
@@ -1,5 +1,18 @@
1
1
  const https = require("https")
2
2
 
3
+ class DiscordAPIError extends Error {
4
+ constructor(message, options = {}) {
5
+ super(message)
6
+
7
+ this.name = "DiscordAPIError"
8
+ this.code = options.code || null
9
+ this.status = options.status || null
10
+ this.method = options.method || null
11
+ this.path = options.path || null
12
+ this.errors = options.errors || null
13
+ }
14
+ }
15
+
3
16
  class API {
4
17
  constructor(token) {
5
18
  this.token = token
@@ -7,7 +20,10 @@ class API {
7
20
 
8
21
  request(method, path, body = null) {
9
22
  return new Promise((resolve, reject) => {
10
- const data = body ? JSON.stringify(body) : null
23
+ const data =
24
+ body !== null
25
+ ? JSON.stringify(body)
26
+ : null
11
27
 
12
28
  const headers = {
13
29
  Authorization: `Bot ${this.token}`,
@@ -15,7 +31,8 @@ class API {
15
31
  }
16
32
 
17
33
  if (data) {
18
- headers["Content-Length"] = Buffer.byteLength(data)
34
+ headers["Content-Length"] =
35
+ Buffer.byteLength(data)
19
36
  }
20
37
 
21
38
  const req = https.request({
@@ -31,25 +48,57 @@ class API {
31
48
  })
32
49
 
33
50
  res.on("end", () => {
51
+ let parsed
52
+
34
53
  try {
35
- resolve(JSON.parse(result))
54
+ parsed =
55
+ result
56
+ ? JSON.parse(result)
57
+ : null
36
58
  } catch {
37
- resolve(result)
59
+ parsed = result
38
60
  }
61
+
62
+ if (
63
+ res.statusCode < 200 ||
64
+ res.statusCode >= 300
65
+ ) {
66
+ const message =
67
+ parsed?.message ||
68
+ "Discord API request failed."
69
+
70
+ const error =
71
+ new DiscordAPIError(
72
+ message,
73
+ {
74
+ code: parsed?.code,
75
+ status: res.statusCode,
76
+ method,
77
+ path,
78
+ errors: parsed?.errors
79
+ }
80
+ )
81
+
82
+ reject(error)
83
+ return
84
+ }
85
+
86
+ resolve(parsed)
39
87
  })
40
88
  })
41
89
 
42
- req.on("error", reject)
43
-
44
- if (data) {
45
- req.write(data)
46
- }
90
+ req.on("error", error => {
91
+ reject(error)
92
+ })
47
93
 
48
- req.end()
94
+ req.end(data || undefined)
49
95
  })
50
96
  }
51
97
 
52
- registerCommands(applicationId, commands) {
98
+ registerCommands(
99
+ applicationId,
100
+ commands
101
+ ) {
53
102
  return this.request(
54
103
  "PUT",
55
104
  `/applications/${applicationId}/commands`,
@@ -58,4 +107,7 @@ class API {
58
107
  }
59
108
  }
60
109
 
110
+ API.DiscordAPIError =
111
+ DiscordAPIError
112
+
61
113
  module.exports = API
package/client.js CHANGED
@@ -51,12 +51,15 @@ class Permissions {
51
51
  }
52
52
 
53
53
  if (
54
- (this.bitfield & PermissionFlags.Administrator) !== 0n
54
+ (this.bitfield &
55
+ PermissionFlags.Administrator) !== 0n
55
56
  ) {
56
57
  return true
57
58
  }
58
59
 
59
- return (this.bitfield & flag) !== 0n
60
+ return (
61
+ (this.bitfield & flag) !== 0n
62
+ )
60
63
  }
61
64
  }
62
65
 
@@ -66,31 +69,76 @@ class Client extends EventEmitter {
66
69
 
67
70
  this.token = null
68
71
  this.user = null
72
+ this.applicationId = null
69
73
  this.api = null
70
74
  this.gateway = new Gateway(this)
71
75
  this.commands = new Map()
76
+
72
77
  this.stopping = false
78
+ this.ready = false
79
+ this.readyAt = null
73
80
 
74
81
  this.on("rawMessage", data => {
75
- const message = new Message(this, data)
76
- this.emit("message", message)
82
+ const message =
83
+ new Message(this, data)
84
+
85
+ this.emit(
86
+ "message",
87
+ message
88
+ )
77
89
  })
78
90
 
79
91
  this.on("rawInteraction", data => {
80
92
  if (data.type === 2) {
81
- this.handleCommandInteraction(data)
93
+ this.handleCommandInteraction(
94
+ data
95
+ )
96
+
82
97
  return
83
98
  }
84
99
 
85
100
  if (data.type === 3) {
86
- this.handleComponentInteraction(data)
101
+ this.handleComponentInteraction(
102
+ data
103
+ )
104
+ }
105
+ })
106
+
107
+ this.on("rawReady", async data => {
108
+ this.user = data.user
109
+
110
+ this.applicationId =
111
+ data.application?.id ||
112
+ data.user?.id
113
+
114
+ try {
115
+ await this.syncCommands()
116
+
117
+ this.ready = true
118
+ this.readyAt = Date.now()
119
+
120
+ this.emit(
121
+ "ready",
122
+ data
123
+ )
124
+ } catch (error) {
125
+ this.emit(
126
+ "error",
127
+ error
128
+ )
87
129
  }
88
130
  })
89
131
 
132
+ this.on("disconnect", () => {
133
+ this.ready = false
134
+ this.readyAt = null
135
+ })
136
+
90
137
  process.on("SIGINT", () => {
91
138
  if (this.stopping) return
92
139
 
93
140
  this.stopping = true
141
+
94
142
  this.destroy()
95
143
 
96
144
  setTimeout(() => {
@@ -99,101 +147,255 @@ class Client extends EventEmitter {
99
147
  })
100
148
  }
101
149
 
102
- handleCommandInteraction(data) {
103
- const command = this.commands.get(data.data.name)
150
+ isReady() {
151
+ return this.ready
152
+ }
104
153
 
105
- if (!command) return
154
+ get uptime() {
155
+ if (!this.readyAt) {
156
+ return null
157
+ }
158
+
159
+ return Date.now() - this.readyAt
160
+ }
106
161
 
162
+ createInteraction(data, type) {
107
163
  const member = data.member
108
164
 
109
165
  const interaction = {
110
166
  id: data.id,
111
167
  token: data.token,
112
- type: "command",
113
- commandName: data.data.name,
114
- user: member?.user || data.user,
168
+ type: type,
169
+
170
+ user:
171
+ member?.user ||
172
+ data.user,
115
173
 
116
174
  member: member
117
175
  ? {
118
176
  ...member,
119
- permissions: new Permissions(
120
- member.permissions
121
- )
177
+
178
+ permissions:
179
+ new Permissions(
180
+ member.permissions
181
+ )
122
182
  }
123
183
  : null,
124
184
 
125
- channelId: data.channel_id,
126
- guildId: data.guild_id || null,
127
- options: {},
128
-
129
- reply: content => {
130
- return this.replyToInteraction(data, content)
131
- }
132
- }
133
-
134
- for (const option of data.data.options || []) {
135
- interaction.options[option.name] = option.value
136
- }
185
+ channelId:
186
+ data.channel_id,
137
187
 
138
- command.handler(interaction)
139
- }
140
-
141
- handleComponentInteraction(data) {
142
- const componentType = data.data.component_type
188
+ guildId:
189
+ data.guild_id ||
190
+ null,
143
191
 
144
- let type = "selectMenu"
192
+ replied: false,
193
+ deferred: false,
145
194
 
146
- if (componentType === 2) {
147
- type = "button"
148
- }
195
+ reply: async content => {
196
+ return this.replyToInteraction(
197
+ data,
198
+ content,
199
+ interaction
200
+ )
201
+ },
149
202
 
150
- const interaction = {
151
- id: data.id,
152
- token: data.token,
153
- type: type,
154
- componentType: componentType,
155
- customId: data.data.custom_id,
203
+ deferReply: async () => {
204
+ if (
205
+ interaction.replied ||
206
+ interaction.deferred
207
+ ) {
208
+ throw new Error(
209
+ "This interaction has already been responded to."
210
+ )
211
+ }
212
+
213
+ await this.api.request(
214
+ "POST",
215
+ `/interactions/${data.id}/${data.token}/callback`,
216
+ {
217
+ type: 5,
218
+ data: {}
219
+ }
220
+ )
156
221
 
157
- user: data.member?.user || data.user,
222
+ interaction.deferred = true
158
223
 
159
- member: data.member
160
- ? {
161
- ...data.member,
162
- permissions: new Permissions(
163
- data.member.permissions
164
- )
165
- }
166
- : null,
224
+ return true
225
+ },
167
226
 
168
- channelId: data.channel_id,
169
- guildId: data.guild_id || null,
170
- values: data.data.values || [],
227
+ editReply: async content => {
228
+ if (
229
+ !interaction.replied &&
230
+ !interaction.deferred
231
+ ) {
232
+ throw new Error(
233
+ "You must reply or defer the interaction first."
234
+ )
235
+ }
236
+
237
+ const messageData =
238
+ this.formatInteractionContent(
239
+ content
240
+ )
241
+
242
+ return this.api.request(
243
+ "PATCH",
244
+ `/webhooks/${this.applicationId}/${data.token}/messages/@original`,
245
+ messageData
246
+ )
247
+ },
171
248
 
172
- reply: content => {
173
- return this.replyToInteraction(data, content)
249
+ deleteReply: async () => {
250
+ if (
251
+ !interaction.replied &&
252
+ !interaction.deferred
253
+ ) {
254
+ throw new Error(
255
+ "You must reply or defer the interaction first."
256
+ )
257
+ }
258
+
259
+ await this.api.request(
260
+ "DELETE",
261
+ `/webhooks/${this.applicationId}/${data.token}/messages/@original`
262
+ )
263
+
264
+ return true
174
265
  }
175
266
  }
176
267
 
177
- this.emit("interaction", interaction)
268
+ return interaction
178
269
  }
179
270
 
180
- replyToInteraction(data, content) {
271
+ formatInteractionContent(content) {
181
272
  const messageData = {}
182
273
 
183
274
  if (typeof content === "string") {
184
- messageData.content = content
275
+ messageData.content =
276
+ content
185
277
  } else if (
186
278
  content &&
187
279
  typeof content.toJSON === "function"
188
280
  ) {
189
- messageData.embeds = [content.toJSON()]
281
+ messageData.embeds = [
282
+ content.toJSON()
283
+ ]
190
284
  } else if (
191
285
  content &&
192
286
  typeof content === "object"
193
287
  ) {
194
- Object.assign(messageData, content)
288
+ Object.assign(
289
+ messageData,
290
+ content
291
+ )
292
+ }
293
+
294
+ return messageData
295
+ }
296
+
297
+ handleCommandInteraction(data) {
298
+ const command =
299
+ this.commands.get(
300
+ data.data.name
301
+ )
302
+
303
+ if (!command) return
304
+
305
+ const interaction =
306
+ this.createInteraction(
307
+ data,
308
+ "command"
309
+ )
310
+
311
+ interaction.commandName =
312
+ data.data.name
313
+
314
+ interaction.options = {}
315
+
316
+ for (
317
+ const option of
318
+ data.data.options || []
319
+ ) {
320
+ interaction.options[
321
+ option.name
322
+ ] = option.value
323
+ }
324
+
325
+ try {
326
+ const result =
327
+ command.handler(
328
+ interaction
329
+ )
330
+
331
+ if (result?.catch) {
332
+ result.catch(error => {
333
+ this.emit(
334
+ "error",
335
+ error
336
+ )
337
+ })
338
+ }
339
+ } catch (error) {
340
+ this.emit(
341
+ "error",
342
+ error
343
+ )
344
+ }
345
+ }
346
+
347
+ handleComponentInteraction(data) {
348
+ const componentType =
349
+ data.data.component_type
350
+
351
+ let type = "selectMenu"
352
+
353
+ if (componentType === 2) {
354
+ type = "button"
355
+ }
356
+
357
+ const interaction =
358
+ this.createInteraction(
359
+ data,
360
+ type
361
+ )
362
+
363
+ interaction.componentType =
364
+ componentType
365
+
366
+ interaction.customId =
367
+ data.data.custom_id
368
+
369
+ interaction.values =
370
+ data.data.values || []
371
+
372
+ this.emit(
373
+ "interaction",
374
+ interaction
375
+ )
376
+ }
377
+
378
+ replyToInteraction(
379
+ data,
380
+ content,
381
+ interaction = null
382
+ ) {
383
+ if (
384
+ interaction?.replied ||
385
+ interaction?.deferred
386
+ ) {
387
+ return Promise.reject(
388
+ new Error(
389
+ "This interaction has already been responded to."
390
+ )
391
+ )
195
392
  }
196
393
 
394
+ const messageData =
395
+ this.formatInteractionContent(
396
+ content
397
+ )
398
+
197
399
  return this.api.request(
198
400
  "POST",
199
401
  `/interactions/${data.id}/${data.token}/callback`,
@@ -201,31 +403,149 @@ class Client extends EventEmitter {
201
403
  type: 4,
202
404
  data: messageData
203
405
  }
204
- )
406
+ ).then(result => {
407
+ if (interaction) {
408
+ interaction.replied = true
409
+ }
410
+
411
+ return result
412
+ })
413
+ }
414
+
415
+ async syncCommands() {
416
+ if (!this.applicationId) {
417
+ throw new Error(
418
+ "Application ID is not available."
419
+ )
420
+ }
421
+
422
+ const commands = []
423
+
424
+ for (
425
+ const entry of
426
+ this.commands.values()
427
+ ) {
428
+ commands.push(
429
+ entry.command
430
+ )
431
+ }
432
+
433
+ const result =
434
+ await this.api.registerCommands(
435
+ this.applicationId,
436
+ commands
437
+ )
438
+
439
+ if (!Array.isArray(result)) {
440
+ throw new Error(
441
+ "Failed to register slash commands: " +
442
+ JSON.stringify(result)
443
+ )
444
+ }
445
+
446
+ return result
205
447
  }
206
448
 
207
449
  login(token) {
208
450
  this.token = token
209
451
  this.api = new API(token)
210
452
 
211
- return this.gateway.connect(token)
453
+ return this.gateway.connect(
454
+ token
455
+ )
212
456
  }
213
457
 
214
458
  destroy() {
215
459
  this.gateway.destroy()
216
- this.emit("disconnect")
460
+
461
+ this.ready = false
462
+ this.readyAt = null
463
+
464
+ this.emit(
465
+ "disconnect"
466
+ )
217
467
  }
218
468
 
219
- command(name, handler, options = {}) {
220
- this.commands.set(name, {
221
- command: {
222
- name: name,
223
- description:
224
- options.description || "No description",
225
- options: options.options || []
226
- },
227
- handler: handler
228
- })
469
+ command(
470
+ name,
471
+ handler,
472
+ options = {}
473
+ ) {
474
+ this.commands.set(
475
+ name,
476
+ {
477
+ command: {
478
+ name: name,
479
+
480
+ description:
481
+ options.description ||
482
+ "No description",
483
+
484
+ options:
485
+ options.options ||
486
+ []
487
+ },
488
+
489
+ handler: handler
490
+ }
491
+ )
492
+
493
+ return this
494
+ }
495
+
496
+ async removeCommand(name) {
497
+ const command =
498
+ this.commands.get(name)
499
+
500
+ if (!command) {
501
+ return false
502
+ }
503
+
504
+ if (!this.applicationId) {
505
+ throw new Error(
506
+ "Bot must be logged in before removing a Discord slash command."
507
+ )
508
+ }
509
+
510
+ const commands =
511
+ await this.api.request(
512
+ "GET",
513
+ `/applications/${this.applicationId}/commands`
514
+ )
515
+
516
+ if (!Array.isArray(commands)) {
517
+ throw new Error(
518
+ "Failed to retrieve Discord application commands."
519
+ )
520
+ }
521
+
522
+ const discordCommand =
523
+ commands.find(
524
+ command =>
525
+ command.name === name
526
+ )
527
+
528
+ if (discordCommand) {
529
+ await this.api.request(
530
+ "DELETE",
531
+ `/applications/${this.applicationId}/commands/${discordCommand.id}`
532
+ )
533
+ }
534
+
535
+ this.commands.delete(name)
536
+
537
+ return true
538
+ }
539
+
540
+ getCommand(name) {
541
+ return (
542
+ this.commands.get(name) ||
543
+ null
544
+ )
545
+ }
546
+
547
+ hasCommand(name) {
548
+ return this.commands.has(name)
229
549
  }
230
550
  }
231
551
 
package/gateway.js CHANGED
@@ -44,10 +44,13 @@ class Gateway {
44
44
  }
45
45
 
46
46
  if (packet.t === "READY") {
47
- this.client.user = packet.d.user
48
- this.client.emit("ready", packet.d)
49
- }
47
+ this.client.user = packet.d.user
48
+ this.client.applicationId =
49
+ packet.d.application?.id ||
50
+ packet.d.user?.id
50
51
 
52
+ this.client.emit("rawReady", packet.d)
53
+ }
51
54
  if (packet.t === "MESSAGE_CREATE") {
52
55
  this.client.emit("rawMessage", packet.d)
53
56
  }
package/index.js CHANGED
@@ -2,10 +2,14 @@ const Client = require("./client")
2
2
  const EmbedBuilder = require("./structures/EmbedBuilder")
3
3
  const ButtonBuilder = require("./structures/ButtonBuilder")
4
4
  const SelectMenuBuilder = require("./structures/SelectMenuBuilder")
5
+ const ActionRowBuilder = require("./structures/ActionRowBuilder")
6
+ const API = require("./api")
5
7
 
6
8
  module.exports = {
7
9
  Client,
8
10
  EmbedBuilder,
9
11
  ButtonBuilder,
10
- SelectMenuBuilder
11
- }
12
+ SelectMenuBuilder,
13
+ ActionRowBuilder,
14
+ DiscordAPIError: API.DiscordAPIError
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-discord-bot.js",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "A simple Discord bot library",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -0,0 +1,42 @@
1
+ class ActionRowBuilder {
2
+ constructor() {
3
+ this.data = {
4
+ type: 1,
5
+ components: []
6
+ }
7
+ }
8
+
9
+ addComponents(...components) {
10
+ for (const component of components) {
11
+ if (!component) continue
12
+
13
+ if (typeof component.toJSON === "function") {
14
+ this.data.components.push(
15
+ component.toJSON()
16
+ )
17
+ } else if (
18
+ typeof component === "object"
19
+ ) {
20
+ this.data.components.push(
21
+ component
22
+ )
23
+ }
24
+ }
25
+
26
+ return this
27
+ }
28
+
29
+ setComponents(...components) {
30
+ this.data.components = []
31
+
32
+ return this.addComponents(
33
+ ...components
34
+ )
35
+ }
36
+
37
+ toJSON() {
38
+ return this.data
39
+ }
40
+ }
41
+
42
+ module.exports = ActionRowBuilder