simple-discord-bot.js 0.1.3 → 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/README.md ADDED
@@ -0,0 +1,354 @@
1
+ simple-discord-bot.js
2
+
3
+ ""npm version" (https://img.shields.io/npm/v/simple-discord-bot.js.svg)" (https://www.npmjs.com/package/simple-discord-bot.js)
4
+ ""npm downloads" (https://img.shields.io/npm/dm/simple-discord-bot.js.svg)" (https://www.npmjs.com/package/simple-discord-bot.js)
5
+ ""License" (https://img.shields.io/npm/l/simple-discord-bot.js.svg)" (https://www.npmjs.com/package/simple-discord-bot.js)
6
+ ""Node.js" (https://img.shields.io/badge/Node.js-18%2B-brightgreen.svg)" (https://nodejs.org/)
7
+
8
+ A simple and lightweight Discord bot library for Node.js
9
+
10
+ Built to make Discord bot development easier while keeping the API simple and beginner friendly
11
+
12
+ ---
13
+
14
+ ✨ Features
15
+
16
+ - ⚡ Lightweight
17
+ - 🧩 Simple API
18
+ - 🤖 Discord bot support
19
+ - 💬 Message events
20
+ - ⚙️ Slash commands
21
+ - 📝 Slash command options
22
+ - 🎨 EmbedBuilder
23
+ - 🔐 Permission checking
24
+ - 💓 Gateway heartbeat
25
+ - 🛑 Clean bot shutdown
26
+ - 📦 Available through npm
27
+ - 🟢 Easy to learn
28
+
29
+ ---
30
+
31
+ 📦 Installation
32
+
33
+ Install the latest version with npm
34
+
35
+ npm install simple-discord-bot.js
36
+
37
+ ---
38
+
39
+ 🚀 Quick Start
40
+
41
+ Create a simple bot in just a few lines
42
+
43
+ const { Client } = require("simple-discord-bot.js")
44
+
45
+ const client = new Client()
46
+
47
+ client.command("ping", interaction => {
48
+ interaction.reply("Pong!")
49
+ }, {
50
+ description: "Replies with Pong!"
51
+ })
52
+
53
+ client.on("ready", () => {
54
+ console.log("Bot is online!")
55
+ })
56
+
57
+ client.login("YOUR_BOT_TOKEN")
58
+
59
+ Start your bot
60
+
61
+ node index.js
62
+
63
+ ---
64
+
65
+ ⚙️ Slash Commands
66
+
67
+ Create commands with "client.command()"
68
+
69
+ client.command("hello", interaction => {
70
+ interaction.reply("Hello!")
71
+ }, {
72
+ description: "Says hello"
73
+ })
74
+
75
+ ---
76
+
77
+ 🧩 Command Options
78
+
79
+ Commands can have options
80
+
81
+ client.command("say", interaction => {
82
+ interaction.reply(interaction.options.message)
83
+ }, {
84
+ description: "Make the bot say something",
85
+ options: [
86
+ {
87
+ name: "message",
88
+ description: "The message to send",
89
+ type: 3,
90
+ required: true
91
+ }
92
+ ]
93
+ })
94
+
95
+ The option can then be accessed with
96
+
97
+ interaction.options.message
98
+
99
+ ---
100
+
101
+ 🎨 Embeds
102
+
103
+ Create beautiful Discord embeds with "EmbedBuilder"
104
+
105
+ const { EmbedBuilder } = require("simple-discord-bot.js")
106
+
107
+ const embed = new EmbedBuilder()
108
+ .setTitle("Hello")
109
+ .setDescription("This is an embed!")
110
+ .setColor("5865F2")
111
+ .setFooter("Made with SimpleDiscord.js")
112
+ .setTimestamp()
113
+
114
+ interaction.reply(embed)
115
+
116
+ Embed Methods
117
+
118
+ Title
119
+
120
+ .setTitle("Hello")
121
+
122
+ Description
123
+
124
+ .setDescription("This is my description")
125
+
126
+ Color
127
+
128
+ Hex colors are supported
129
+
130
+ .setColor("FF0000")
131
+
132
+ URL
133
+
134
+ .setURL("https://example.com")
135
+
136
+ Footer
137
+
138
+ .setFooter("Made with SimpleDiscord.js")
139
+
140
+ Footer Icon
141
+
142
+ .setFooter(
143
+ "Made with SimpleDiscord.js",
144
+ "https://example.com/icon.png"
145
+ )
146
+
147
+ Author
148
+
149
+ .setAuthor("SimpleBot")
150
+
151
+ Author Icon
152
+
153
+ .setAuthor(
154
+ "SimpleBot",
155
+ "https://example.com/icon.png"
156
+ )
157
+
158
+ Thumbnail
159
+
160
+ .setThumbnail("https://example.com/image.png")
161
+
162
+ Image
163
+
164
+ .setImage("https://example.com/image.png")
165
+
166
+ Timestamp
167
+
168
+ .setTimestamp()
169
+
170
+ ---
171
+
172
+ 🔐 Permissions
173
+
174
+ Check Discord permissions easily
175
+
176
+ client.command("admin", interaction => {
177
+ if (!interaction.member?.permissions.has("Administrator")) {
178
+ return interaction.reply("You need Administrator")
179
+ }
180
+
181
+ interaction.reply("You have Administrator permission!")
182
+ }, {
183
+ description: "Administrator only command"
184
+ })
185
+
186
+ ---
187
+
188
+ 💬 Messages
189
+
190
+ Listen for messages
191
+
192
+ client.on("message", message => {
193
+ console.log(message.content)
194
+ })
195
+
196
+ Reply to messages
197
+
198
+ message.reply("Hello!")
199
+
200
+ ---
201
+
202
+ 📡 Events
203
+
204
+ SimpleDiscord.js currently supports:
205
+
206
+ Event| Description
207
+ "ready"| Bot is ready
208
+ "message"| A message was created
209
+ "error"| Gateway error
210
+ "disconnect"| Bot disconnected
211
+
212
+ Example
213
+
214
+ client.on("ready", () => {
215
+ console.log("Bot is online!")
216
+ })
217
+
218
+ ---
219
+
220
+ 🛠️ API
221
+
222
+ Client
223
+
224
+ const { Client } = require("simple-discord-bot.js")
225
+
226
+ const client = new Client()
227
+
228
+ "client.login(token)"
229
+
230
+ Connect your bot to Discord
231
+
232
+ client.login("YOUR_BOT_TOKEN")
233
+
234
+ "client.command(name, handler, options)"
235
+
236
+ Create a slash command
237
+
238
+ client.command(
239
+ "ping",
240
+ interaction => {
241
+ interaction.reply("Pong!")
242
+ },
243
+ {
244
+ description: "Replies with Pong!"
245
+ }
246
+ )
247
+
248
+ "client.destroy()"
249
+
250
+ Disconnect the bot
251
+
252
+ client.destroy()
253
+
254
+ ---
255
+
256
+ 📊 Current Status
257
+
258
+ SimpleDiscord.js is currently in early development
259
+
260
+ Current version:
261
+
262
+ 0.1.2
263
+
264
+ The API may change as the library develops
265
+
266
+ ---
267
+
268
+ 🗺️ Roadmap
269
+
270
+ ✅ Completed
271
+
272
+ - [x] Discord Gateway connection
273
+ - [x] Gateway heartbeat
274
+ - [x] Bot login
275
+ - [x] Clean shutdown
276
+ - [x] Ready event
277
+ - [x] Message events
278
+ - [x] Message replies
279
+ - [x] Slash commands
280
+ - [x] Slash command options
281
+ - [x] Permission checking
282
+ - [x] EmbedBuilder
283
+ - [x] npm publishing
284
+
285
+ 🚧 Planned
286
+
287
+ - [ ] Embed fields
288
+ - [ ] Buttons
289
+ - [ ] Select menus
290
+ - [ ] More interaction types
291
+ - [ ] Channel objects
292
+ - [ ] Guild objects
293
+ - [ ] User objects
294
+ - [ ] Message editing
295
+ - [ ] Message deletion
296
+ - [ ] More permission helpers
297
+ - [ ] Better error handling
298
+ - [ ] Automatic reconnection
299
+ - [ ] Gateway resume support
300
+ - [ ] More Discord API features
301
+
302
+ ---
303
+
304
+ 📚 Why SimpleDiscord.js?
305
+
306
+ Discord libraries can become complicated when you're just starting
307
+
308
+ SimpleDiscord.js focuses on keeping things straightforward
309
+
310
+ Instead of learning a huge API you can start with:
311
+
312
+ const { Client } = require("simple-discord-bot.js")
313
+
314
+ const client = new Client()
315
+
316
+ client.command("ping", interaction => {
317
+ interaction.reply("Pong!")
318
+ })
319
+
320
+ client.login("YOUR_BOT_TOKEN")
321
+
322
+ Simple
323
+
324
+ Lightweight
325
+
326
+ Beginner friendly
327
+
328
+ ---
329
+
330
+ 📦 npm
331
+
332
+ Install the package from npm
333
+
334
+ npm install simple-discord-bot.js
335
+
336
+ ---
337
+
338
+ 🤝 Contributing
339
+
340
+ Contributions and suggestions are welcome
341
+
342
+ If you find a bug or have an idea for a feature you can open an issue or submit a pull request
343
+
344
+ ---
345
+
346
+ 📄 License
347
+
348
+ MIT License
349
+
350
+ ---
351
+
352
+ Made with ❤️ and JavaScript
353
+
354
+ simple-discord-bot.js
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
@@ -46,15 +46,20 @@ class Permissions {
46
46
  has(permission) {
47
47
  const flag = PermissionFlags[permission]
48
48
 
49
- if (flag === undefined) {
49
+ if (!flag) {
50
50
  return false
51
51
  }
52
52
 
53
- if ((this.bitfield & PermissionFlags.Administrator) !== 0n) {
53
+ if (
54
+ (this.bitfield &
55
+ PermissionFlags.Administrator) !== 0n
56
+ ) {
54
57
  return true
55
58
  }
56
59
 
57
- return (this.bitfield & flag) !== 0n
60
+ return (
61
+ (this.bitfield & flag) !== 0n
62
+ )
58
63
  }
59
64
  }
60
65
 
@@ -64,113 +69,483 @@ class Client extends EventEmitter {
64
69
 
65
70
  this.token = null
66
71
  this.user = null
72
+ this.applicationId = null
67
73
  this.api = null
68
74
  this.gateway = new Gateway(this)
69
75
  this.commands = new Map()
76
+
70
77
  this.stopping = false
78
+ this.ready = false
79
+ this.readyAt = null
71
80
 
72
81
  this.on("rawMessage", data => {
73
- const message = new Message(this, data)
74
- this.emit("message", message)
82
+ const message =
83
+ new Message(this, data)
84
+
85
+ this.emit(
86
+ "message",
87
+ message
88
+ )
75
89
  })
76
90
 
77
91
  this.on("rawInteraction", data => {
78
- if (data.type !== 2) return
92
+ if (data.type === 2) {
93
+ this.handleCommandInteraction(
94
+ data
95
+ )
96
+
97
+ return
98
+ }
99
+
100
+ if (data.type === 3) {
101
+ this.handleComponentInteraction(
102
+ data
103
+ )
104
+ }
105
+ })
106
+
107
+ this.on("rawReady", async data => {
108
+ this.user = data.user
79
109
 
80
- const command = this.commands.get(data.data.name)
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
+ )
129
+ }
130
+ })
131
+
132
+ this.on("disconnect", () => {
133
+ this.ready = false
134
+ this.readyAt = null
135
+ })
81
136
 
82
- if (!command) return
137
+ process.on("SIGINT", () => {
138
+ if (this.stopping) return
83
139
 
84
- const member = data.member
140
+ this.stopping = true
85
141
 
86
- const interaction = {
87
- id: data.id,
88
- token: data.token,
142
+ this.destroy()
89
143
 
90
- commandName: data.data.name,
144
+ setTimeout(() => {
145
+ process.exit(0)
146
+ }, 1000)
147
+ })
148
+ }
91
149
 
92
- user: member?.user || data.user,
150
+ isReady() {
151
+ return this.ready
152
+ }
93
153
 
94
- member: member ? {
95
- ...member,
154
+ get uptime() {
155
+ if (!this.readyAt) {
156
+ return null
157
+ }
96
158
 
97
- permissions: new Permissions(member.permissions)
98
- } : null,
159
+ return Date.now() - this.readyAt
160
+ }
99
161
 
100
- channelId: data.channel_id,
162
+ createInteraction(data, type) {
163
+ const member = data.member
101
164
 
102
- guildId: data.guild_id || null,
165
+ const interaction = {
166
+ id: data.id,
167
+ token: data.token,
168
+ type: type,
103
169
 
104
- options: {},
170
+ user:
171
+ member?.user ||
172
+ data.user,
105
173
 
106
- reply: content => {
107
- const messageData = {}
174
+ member: member
175
+ ? {
176
+ ...member,
108
177
 
109
- if (typeof content === "string") {
110
- messageData.content = content
111
- } else if (
112
- content &&
113
- typeof content.toJSON === "function"
114
- ) {
115
- messageData.embeds = [content.toJSON()]
116
- } else if (content && typeof content === "object") {
117
- Object.assign(messageData, content)
178
+ permissions:
179
+ new Permissions(
180
+ member.permissions
181
+ )
118
182
  }
183
+ : null,
184
+
185
+ channelId:
186
+ data.channel_id,
187
+
188
+ guildId:
189
+ data.guild_id ||
190
+ null,
191
+
192
+ replied: false,
193
+ deferred: false,
119
194
 
120
- return this.api.request(
121
- "POST",
122
- `/interactions/${data.id}/${data.token}/callback`,
123
- {
124
- type: 4,
125
- data: messageData
126
- }
195
+ reply: async content => {
196
+ return this.replyToInteraction(
197
+ data,
198
+ content,
199
+ interaction
200
+ )
201
+ },
202
+
203
+ deferReply: async () => {
204
+ if (
205
+ interaction.replied ||
206
+ interaction.deferred
207
+ ) {
208
+ throw new Error(
209
+ "This interaction has already been responded to."
127
210
  )
128
211
  }
212
+
213
+ await this.api.request(
214
+ "POST",
215
+ `/interactions/${data.id}/${data.token}/callback`,
216
+ {
217
+ type: 5,
218
+ data: {}
219
+ }
220
+ )
221
+
222
+ interaction.deferred = true
223
+
224
+ return true
225
+ },
226
+
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
+ },
248
+
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
129
265
  }
266
+ }
267
+
268
+ return interaction
269
+ }
270
+
271
+ formatInteractionContent(content) {
272
+ const messageData = {}
273
+
274
+ if (typeof content === "string") {
275
+ messageData.content =
276
+ content
277
+ } else if (
278
+ content &&
279
+ typeof content.toJSON === "function"
280
+ ) {
281
+ messageData.embeds = [
282
+ content.toJSON()
283
+ ]
284
+ } else if (
285
+ content &&
286
+ typeof content === "object"
287
+ ) {
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
+ )
130
310
 
131
- for (const option of data.data.options || []) {
132
- interaction.options[option.name] = option.value
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
+ })
133
338
  }
339
+ } catch (error) {
340
+ this.emit(
341
+ "error",
342
+ error
343
+ )
344
+ }
345
+ }
134
346
 
135
- command.handler(interaction)
136
- })
347
+ handleComponentInteraction(data) {
348
+ const componentType =
349
+ data.data.component_type
137
350
 
138
- process.on("SIGINT", () => {
139
- if (this.stopping) return
351
+ let type = "selectMenu"
140
352
 
141
- this.stopping = true
353
+ if (componentType === 2) {
354
+ type = "button"
355
+ }
142
356
 
143
- console.log("Stopping bot...")
357
+ const interaction =
358
+ this.createInteraction(
359
+ data,
360
+ type
361
+ )
144
362
 
145
- this.destroy()
363
+ interaction.componentType =
364
+ componentType
146
365
 
147
- setTimeout(() => {
148
- process.exit(0)
149
- }, 1000)
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
+ )
392
+ }
393
+
394
+ const messageData =
395
+ this.formatInteractionContent(
396
+ content
397
+ )
398
+
399
+ return this.api.request(
400
+ "POST",
401
+ `/interactions/${data.id}/${data.token}/callback`,
402
+ {
403
+ type: 4,
404
+ data: messageData
405
+ }
406
+ ).then(result => {
407
+ if (interaction) {
408
+ interaction.replied = true
409
+ }
410
+
411
+ return result
150
412
  })
151
413
  }
152
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
447
+ }
448
+
153
449
  login(token) {
154
450
  this.token = token
155
451
  this.api = new API(token)
156
452
 
157
- return this.gateway.connect(token)
453
+ return this.gateway.connect(
454
+ token
455
+ )
158
456
  }
159
457
 
160
458
  destroy() {
161
459
  this.gateway.destroy()
162
- this.emit("disconnect")
460
+
461
+ this.ready = false
462
+ this.readyAt = null
463
+
464
+ this.emit(
465
+ "disconnect"
466
+ )
163
467
  }
164
468
 
165
- command(name, handler, options = {}) {
166
- this.commands.set(name, {
167
- command: {
168
- name: name,
169
- description: options.description || "No description",
170
- options: options.options || []
171
- },
172
- handler
173
- })
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)
174
549
  }
175
550
  }
176
551
 
package/gateway.js CHANGED
@@ -13,11 +13,13 @@ class Gateway {
13
13
  )
14
14
 
15
15
  this.ws.on("open", () => {
16
+ console.log("Connected to Discord Gateway")
17
+
16
18
  this.ws.send(JSON.stringify({
17
19
  op: 2,
18
20
  d: {
19
21
  token: token,
20
- intents: 33281,
22
+ intents: 33281 | 32768,
21
23
  properties: {
22
24
  os: "android",
23
25
  browser: "SimpleDiscord.js",
@@ -28,23 +30,36 @@ class Gateway {
28
30
  })
29
31
 
30
32
  this.ws.on("message", data => {
31
- const packet = JSON.parse(data)
33
+ try {
34
+ const packet = JSON.parse(data)
32
35
 
33
- if (packet.op === 10) {
34
- this.startHeartbeat(packet.d.heartbeat_interval)
35
- }
36
+ if (packet.op === 10) {
37
+ console.log("Gateway connected")
38
+ this.startHeartbeat(packet.d.heartbeat_interval)
39
+ }
36
40
 
37
- if (packet.t === "READY") {
38
- this.client.user = packet.d.user
39
- this.client.emit("ready", packet.d)
40
- }
41
+ if (packet.op === 9) {
42
+ console.error("Discord rejected the Gateway connection")
43
+ return
44
+ }
41
45
 
42
- if (packet.t === "MESSAGE_CREATE") {
43
- this.client.emit("rawMessage", packet.d)
44
- }
46
+ if (packet.t === "READY") {
47
+ this.client.user = packet.d.user
48
+ this.client.applicationId =
49
+ packet.d.application?.id ||
50
+ packet.d.user?.id
45
51
 
46
- if (packet.t === "INTERACTION_CREATE") {
47
- this.client.emit("rawInteraction", packet.d)
52
+ this.client.emit("rawReady", packet.d)
53
+ }
54
+ if (packet.t === "MESSAGE_CREATE") {
55
+ this.client.emit("rawMessage", packet.d)
56
+ }
57
+
58
+ if (packet.t === "INTERACTION_CREATE") {
59
+ this.client.emit("rawInteraction", packet.d)
60
+ }
61
+ } catch (error) {
62
+ this.client.emit("error", error)
48
63
  }
49
64
  })
50
65
 
@@ -57,6 +72,8 @@ class Gateway {
57
72
  this.ws = null
58
73
  this.client.emit("disconnect")
59
74
  })
75
+
76
+ return this.ws
60
77
  }
61
78
 
62
79
  startHeartbeat(interval) {
@@ -84,6 +101,7 @@ class Gateway {
84
101
 
85
102
  if (this.ws) {
86
103
  this.ws.close(1000, "Bot stopped")
104
+ this.ws = null
87
105
  }
88
106
  }
89
107
  }
package/index.js CHANGED
@@ -1,7 +1,15 @@
1
1
  const Client = require("./client")
2
2
  const EmbedBuilder = require("./structures/EmbedBuilder")
3
+ const ButtonBuilder = require("./structures/ButtonBuilder")
4
+ const SelectMenuBuilder = require("./structures/SelectMenuBuilder")
5
+ const ActionRowBuilder = require("./structures/ActionRowBuilder")
6
+ const API = require("./api")
3
7
 
4
8
  module.exports = {
5
9
  Client,
6
- EmbedBuilder
10
+ EmbedBuilder,
11
+ ButtonBuilder,
12
+ SelectMenuBuilder,
13
+ ActionRowBuilder,
14
+ DiscordAPIError: API.DiscordAPIError
7
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-discord-bot.js",
3
- "version": "0.1.3",
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
@@ -0,0 +1,50 @@
1
+ class ButtonBuilder {
2
+ constructor() {
3
+ this.data = {
4
+ type: 2
5
+ }
6
+ }
7
+
8
+ setCustomId(customId) {
9
+ this.data.custom_id = customId
10
+ return this
11
+ }
12
+
13
+ setLabel(label) {
14
+ this.data.label = label
15
+ return this
16
+ }
17
+
18
+ setStyle(style) {
19
+ const styles = {
20
+ primary: 1,
21
+ secondary: 2,
22
+ success: 3,
23
+ danger: 4,
24
+ link: 5
25
+ }
26
+
27
+ if (typeof style === "string") {
28
+ style = styles[style.toLowerCase()]
29
+ }
30
+
31
+ this.data.style = style
32
+ return this
33
+ }
34
+
35
+ setURL(url) {
36
+ this.data.url = url
37
+ return this
38
+ }
39
+
40
+ setDisabled(disabled = true) {
41
+ this.data.disabled = disabled
42
+ return this
43
+ }
44
+
45
+ toJSON() {
46
+ return this.data
47
+ }
48
+ }
49
+
50
+ module.exports = ButtonBuilder
@@ -82,6 +82,40 @@ class EmbedBuilder {
82
82
  return this
83
83
  }
84
84
 
85
+ addField(name, value, inline = false) {
86
+ if (!this.data.fields) {
87
+ this.data.fields = []
88
+ }
89
+
90
+ this.data.fields.push({
91
+ name: name,
92
+ value: value,
93
+ inline: inline
94
+ })
95
+
96
+ return this
97
+ }
98
+
99
+ addFields(...fields) {
100
+ for (const field of fields) {
101
+ if (
102
+ !field ||
103
+ typeof field.name !== "string" ||
104
+ typeof field.value !== "string"
105
+ ) {
106
+ continue
107
+ }
108
+
109
+ this.addField(
110
+ field.name,
111
+ field.value,
112
+ field.inline || false
113
+ )
114
+ }
115
+
116
+ return this
117
+ }
118
+
85
119
  toJSON() {
86
120
  return this.data
87
121
  }
@@ -1,22 +1,53 @@
1
1
  class Message {
2
2
  constructor(client, data) {
3
3
  this.client = client
4
-
5
4
  this.id = data.id
6
- this.content = data.content
5
+ this.content = data.content || ""
7
6
  this.channelId = data.channel_id
8
- this.guildId = data.guild_id
9
- this.author = data.author
7
+ this.guildId = data.guild_id || null
8
+ this.author = data.author || null
10
9
  }
11
10
 
12
- reply(content) {
13
- return this.client.api.request(
11
+ async reply(content) {
12
+ const data = await this.client.api.request(
14
13
  "POST",
15
14
  `/channels/${this.channelId}/messages`,
16
15
  {
17
16
  content: content
18
17
  }
19
18
  )
19
+
20
+ if (!data || !data.id) {
21
+ throw new Error(
22
+ "Discord did not return a valid message: " +
23
+ JSON.stringify(data)
24
+ )
25
+ }
26
+
27
+ return new Message(this.client, data)
28
+ }
29
+
30
+ async edit(content) {
31
+ const data = await this.client.api.request(
32
+ "PATCH",
33
+ `/channels/${this.channelId}/messages/${this.id}`,
34
+ {
35
+ content: content
36
+ }
37
+ )
38
+
39
+ this.content = data.content || content
40
+
41
+ return this
42
+ }
43
+
44
+ async delete() {
45
+ await this.client.api.request(
46
+ "DELETE",
47
+ `/channels/${this.channelId}/messages/${this.id}`
48
+ )
49
+
50
+ return true
20
51
  }
21
52
  }
22
53
 
@@ -0,0 +1,68 @@
1
+ class SelectMenuBuilder {
2
+ constructor() {
3
+ this.data = {
4
+ type: 3,
5
+ options: []
6
+ }
7
+ }
8
+
9
+ setCustomId(customId) {
10
+ this.data.custom_id = customId
11
+ return this
12
+ }
13
+
14
+ setPlaceholder(placeholder) {
15
+ this.data.placeholder = placeholder
16
+ return this
17
+ }
18
+
19
+ addOptions(...options) {
20
+ for (const option of options) {
21
+ if (!option || !option.label || !option.value) {
22
+ continue
23
+ }
24
+
25
+ const data = {
26
+ label: option.label,
27
+ value: option.value
28
+ }
29
+
30
+ if (option.description) {
31
+ data.description = option.description
32
+ }
33
+
34
+ if (option.emoji) {
35
+ data.emoji = option.emoji
36
+ }
37
+
38
+ if (option.default) {
39
+ data.default = true
40
+ }
41
+
42
+ this.data.options.push(data)
43
+ }
44
+
45
+ return this
46
+ }
47
+
48
+ setMinValues(min) {
49
+ this.data.min_values = min
50
+ return this
51
+ }
52
+
53
+ setMaxValues(max) {
54
+ this.data.max_values = max
55
+ return this
56
+ }
57
+
58
+ setDisabled(disabled = true) {
59
+ this.data.disabled = disabled
60
+ return this
61
+ }
62
+
63
+ toJSON() {
64
+ return this.data
65
+ }
66
+ }
67
+
68
+ module.exports = SelectMenuBuilder