novaapp-sdk 1.1.0 → 1.3.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/dist/index.d.mts +1876 -284
- package/dist/index.d.ts +1876 -284
- package/dist/index.js +2290 -482
- package/dist/index.mjs +2269 -481
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,22 +22,42 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
ActionRowBuilder: () => ActionRowBuilder,
|
|
24
24
|
ButtonBuilder: () => ButtonBuilder,
|
|
25
|
+
ChannelsAPI: () => ChannelsAPI,
|
|
26
|
+
Collection: () => Collection,
|
|
25
27
|
CommandsAPI: () => CommandsAPI,
|
|
28
|
+
Cooldown: () => Cooldown,
|
|
29
|
+
CooldownManager: () => CooldownManager,
|
|
26
30
|
EmbedBuilder: () => EmbedBuilder,
|
|
27
31
|
HttpClient: () => HttpClient,
|
|
28
32
|
InteractionOptions: () => InteractionOptions,
|
|
29
33
|
InteractionsAPI: () => InteractionsAPI,
|
|
34
|
+
Logger: () => Logger,
|
|
30
35
|
MembersAPI: () => MembersAPI,
|
|
36
|
+
MessageBuilder: () => MessageBuilder,
|
|
31
37
|
MessagesAPI: () => MessagesAPI,
|
|
32
38
|
ModalBuilder: () => ModalBuilder,
|
|
39
|
+
NovaChannel: () => NovaChannel,
|
|
33
40
|
NovaClient: () => NovaClient,
|
|
34
41
|
NovaInteraction: () => NovaInteraction,
|
|
42
|
+
NovaMember: () => NovaMember,
|
|
43
|
+
NovaMessage: () => NovaMessage,
|
|
44
|
+
Paginator: () => Paginator,
|
|
45
|
+
Permissions: () => Permissions,
|
|
35
46
|
PermissionsAPI: () => PermissionsAPI,
|
|
47
|
+
PermissionsBitfield: () => PermissionsBitfield,
|
|
48
|
+
PollBuilder: () => PollBuilder,
|
|
49
|
+
ReactionsAPI: () => ReactionsAPI,
|
|
36
50
|
SelectMenuBuilder: () => SelectMenuBuilder,
|
|
37
51
|
ServersAPI: () => ServersAPI,
|
|
38
52
|
SlashCommandBuilder: () => SlashCommandBuilder,
|
|
39
53
|
SlashCommandOptionBuilder: () => SlashCommandOptionBuilder,
|
|
40
|
-
TextInputBuilder: () => TextInputBuilder
|
|
54
|
+
TextInputBuilder: () => TextInputBuilder,
|
|
55
|
+
countdown: () => countdown,
|
|
56
|
+
formatDuration: () => formatDuration,
|
|
57
|
+
formatRelative: () => formatRelative,
|
|
58
|
+
parseTimestamp: () => parseTimestamp,
|
|
59
|
+
sleep: () => sleep,
|
|
60
|
+
withTimeout: () => withTimeout
|
|
41
61
|
});
|
|
42
62
|
module.exports = __toCommonJS(index_exports);
|
|
43
63
|
|
|
@@ -135,6 +155,70 @@ var MessagesAPI = class {
|
|
|
135
155
|
typing(channelId) {
|
|
136
156
|
return this.http.post(`/channels/${channelId}/typing`);
|
|
137
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Fetch a single message by ID.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* const msg = await client.messages.fetchOne('message-id')
|
|
163
|
+
*/
|
|
164
|
+
fetchOne(messageId) {
|
|
165
|
+
return this.http.get(`/messages/${messageId}`);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Pin a message in its channel.
|
|
169
|
+
* The bot must have the `messages.manage` scope.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* await client.messages.pin('message-id')
|
|
173
|
+
*/
|
|
174
|
+
pin(messageId) {
|
|
175
|
+
return this.http.post(`/messages/${messageId}/pin`);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Unpin a message.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* await client.messages.unpin('message-id')
|
|
182
|
+
*/
|
|
183
|
+
unpin(messageId) {
|
|
184
|
+
return this.http.delete(`/messages/${messageId}/pin`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Fetch all pinned messages in a channel.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* const pins = await client.messages.fetchPinned('channel-id')
|
|
191
|
+
*/
|
|
192
|
+
fetchPinned(channelId) {
|
|
193
|
+
return this.http.get(`/channels/${channelId}/pins`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Add a reaction to a message.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* await client.messages.addReaction('message-id', '👍')
|
|
200
|
+
*/
|
|
201
|
+
addReaction(messageId, emoji) {
|
|
202
|
+
return this.http.post(`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Remove the bot's reaction from a message.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* await client.messages.removeReaction('message-id', '👍')
|
|
209
|
+
*/
|
|
210
|
+
removeReaction(messageId, emoji) {
|
|
211
|
+
return this.http.delete(`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Fetch all reactions on a message.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* const reactions = await client.messages.fetchReactions('message-id')
|
|
218
|
+
*/
|
|
219
|
+
fetchReactions(messageId) {
|
|
220
|
+
return this.http.get(`/messages/${messageId}/reactions`);
|
|
221
|
+
}
|
|
138
222
|
};
|
|
139
223
|
|
|
140
224
|
// src/api/commands.ts
|
|
@@ -262,6 +346,61 @@ var MembersAPI = class {
|
|
|
262
346
|
ban(serverId, userId, reason) {
|
|
263
347
|
return this.http.post(`/servers/${serverId}/members/${userId}/ban`, { reason });
|
|
264
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Unban a previously banned user.
|
|
351
|
+
* Requires the `members.ban` scope.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* await client.members.unban('server-id', 'user-id')
|
|
355
|
+
*/
|
|
356
|
+
unban(serverId, userId) {
|
|
357
|
+
return this.http.delete(`/servers/${serverId}/bans/${userId}`);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Fetch the ban list for a server.
|
|
361
|
+
* Requires the `members.ban` scope.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* const bans = await client.members.listBans('server-id')
|
|
365
|
+
* for (const ban of bans) {
|
|
366
|
+
* console.log(`${ban.username} — ${ban.reason ?? 'No reason'}`)
|
|
367
|
+
* }
|
|
368
|
+
*/
|
|
369
|
+
listBans(serverId) {
|
|
370
|
+
return this.http.get(`/servers/${serverId}/bans`);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Send a direct message to a user (DM).
|
|
374
|
+
* Requires the `messages.write` scope.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* await client.members.dm('user-id', { content: 'Hello!' })
|
|
378
|
+
* await client.members.dm('user-id', 'Hello from the bot!')
|
|
379
|
+
*/
|
|
380
|
+
dm(userId, options) {
|
|
381
|
+
const body = typeof options === "string" ? { content: options } : options;
|
|
382
|
+
return this.http.post(`/users/${userId}/dm`, body);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Add a role to a member.
|
|
386
|
+
* Requires the `members.roles` scope.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* await client.members.addRole('server-id', 'user-id', 'role-id')
|
|
390
|
+
*/
|
|
391
|
+
addRole(serverId, userId, roleId) {
|
|
392
|
+
return this.http.post(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Remove a role from a member.
|
|
396
|
+
* Requires the `members.roles` scope.
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* await client.members.removeRole('server-id', 'user-id', 'role-id')
|
|
400
|
+
*/
|
|
401
|
+
removeRole(serverId, userId, roleId) {
|
|
402
|
+
return this.http.delete(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
|
|
403
|
+
}
|
|
265
404
|
};
|
|
266
405
|
|
|
267
406
|
// src/api/servers.ts
|
|
@@ -279,6 +418,17 @@ var ServersAPI = class {
|
|
|
279
418
|
list() {
|
|
280
419
|
return this.http.get("/servers");
|
|
281
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Fetch all roles in a server.
|
|
423
|
+
* Results are sorted by position (lowest first).
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* const roles = await client.servers.listRoles('server-id')
|
|
427
|
+
* const adminRole = roles.find(r => r.name === 'Admin')
|
|
428
|
+
*/
|
|
429
|
+
listRoles(serverId) {
|
|
430
|
+
return this.http.get(`/servers/${serverId}/roles`);
|
|
431
|
+
}
|
|
282
432
|
};
|
|
283
433
|
|
|
284
434
|
// src/api/interactions.ts
|
|
@@ -445,6 +595,172 @@ var PermissionsAPI = class {
|
|
|
445
595
|
}
|
|
446
596
|
};
|
|
447
597
|
|
|
598
|
+
// src/api/channels.ts
|
|
599
|
+
var ChannelsAPI = class {
|
|
600
|
+
constructor(http) {
|
|
601
|
+
this.http = http;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Fetch all channels in a server the bot is a member of.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* const channels = await client.channels.list('server-id')
|
|
608
|
+
* const textChannels = channels.filter(c => c.type === 'TEXT')
|
|
609
|
+
*/
|
|
610
|
+
list(serverId) {
|
|
611
|
+
return this.http.get(`/servers/${serverId}/channels`);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Fetch a single channel by ID.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* const channel = await client.channels.fetch('channel-id')
|
|
618
|
+
* console.log(channel.name, channel.type)
|
|
619
|
+
*/
|
|
620
|
+
fetch(channelId) {
|
|
621
|
+
return this.http.get(`/channels/${channelId}`);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Create a new channel in a server.
|
|
625
|
+
* Requires the `channels.manage` scope.
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* const channel = await client.channels.create('server-id', {
|
|
629
|
+
* name: 'announcements',
|
|
630
|
+
* type: 'ANNOUNCEMENT',
|
|
631
|
+
* topic: 'Official announcements only',
|
|
632
|
+
* })
|
|
633
|
+
*/
|
|
634
|
+
create(serverId, options) {
|
|
635
|
+
return this.http.post(`/servers/${serverId}/channels`, options);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Edit an existing channel.
|
|
639
|
+
* Requires the `channels.manage` scope.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* await client.channels.edit('channel-id', { topic: 'New topic!' })
|
|
643
|
+
*/
|
|
644
|
+
edit(channelId, options) {
|
|
645
|
+
return this.http.patch(`/channels/${channelId}`, options);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Delete a channel.
|
|
649
|
+
* Requires the `channels.manage` scope.
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* await client.channels.delete('channel-id')
|
|
653
|
+
*/
|
|
654
|
+
delete(channelId) {
|
|
655
|
+
return this.http.delete(`/channels/${channelId}`);
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Fetch messages from a channel.
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* const messages = await client.channels.fetchMessages('channel-id', { limit: 50 })
|
|
662
|
+
*/
|
|
663
|
+
fetchMessages(channelId, options = {}) {
|
|
664
|
+
const params = new URLSearchParams();
|
|
665
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
666
|
+
if (options.before) params.set("before", options.before);
|
|
667
|
+
const qs = params.toString();
|
|
668
|
+
return this.http.get(`/channels/${channelId}/messages${qs ? `?${qs}` : ""}`);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Fetch all pinned messages in a channel.
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* const pins = await client.channels.fetchPins('channel-id')
|
|
675
|
+
*/
|
|
676
|
+
fetchPins(channelId) {
|
|
677
|
+
return this.http.get(`/channels/${channelId}/pins`);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Send a typing indicator in a channel.
|
|
681
|
+
* Displayed to users for ~5 seconds.
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* await client.channels.startTyping('channel-id')
|
|
685
|
+
*/
|
|
686
|
+
startTyping(channelId) {
|
|
687
|
+
return this.http.post(`/channels/${channelId}/typing`);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/api/reactions.ts
|
|
692
|
+
var ReactionsAPI = class {
|
|
693
|
+
constructor(http) {
|
|
694
|
+
this.http = http;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Add a reaction to a message.
|
|
698
|
+
* Use a plain emoji character or a custom emoji ID.
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* await client.reactions.add('message-id', '👍')
|
|
702
|
+
* await client.reactions.add('message-id', '🎉')
|
|
703
|
+
*/
|
|
704
|
+
add(messageId, emoji) {
|
|
705
|
+
const encoded = encodeURIComponent(emoji);
|
|
706
|
+
return this.http.post(`/messages/${messageId}/reactions/${encoded}`);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Remove the bot's reaction from a message.
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* await client.reactions.remove('message-id', '👍')
|
|
713
|
+
*/
|
|
714
|
+
remove(messageId, emoji) {
|
|
715
|
+
const encoded = encodeURIComponent(emoji);
|
|
716
|
+
return this.http.delete(`/messages/${messageId}/reactions/${encoded}`);
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Remove all reactions from a message.
|
|
720
|
+
* Requires the `messages.manage` scope.
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* await client.reactions.removeAll('message-id')
|
|
724
|
+
*/
|
|
725
|
+
removeAll(messageId) {
|
|
726
|
+
return this.http.delete(`/messages/${messageId}/reactions`);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Remove all reactions of a specific emoji from a message.
|
|
730
|
+
* Requires the `messages.manage` scope.
|
|
731
|
+
*
|
|
732
|
+
* @example
|
|
733
|
+
* await client.reactions.removeEmoji('message-id', '👍')
|
|
734
|
+
*/
|
|
735
|
+
removeEmoji(messageId, emoji) {
|
|
736
|
+
const encoded = encodeURIComponent(emoji);
|
|
737
|
+
return this.http.delete(`/messages/${messageId}/reactions/${encoded}/all`);
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Fetch all reactions on a message, broken down by emoji.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* const reactions = await client.reactions.fetch('message-id')
|
|
744
|
+
* for (const r of reactions) {
|
|
745
|
+
* console.log(`${r.emoji} — ${r.count} reactions from ${r.users.map(u => u.username).join(', ')}`)
|
|
746
|
+
* }
|
|
747
|
+
*/
|
|
748
|
+
fetch(messageId) {
|
|
749
|
+
return this.http.get(`/messages/${messageId}/reactions`);
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Fetch reactions for a specific emoji on a message.
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* const detail = await client.reactions.fetchEmoji('message-id', '👍')
|
|
756
|
+
* console.log(`${detail.count} thumbs ups`)
|
|
757
|
+
*/
|
|
758
|
+
fetchEmoji(messageId, emoji) {
|
|
759
|
+
const encoded = encodeURIComponent(emoji);
|
|
760
|
+
return this.http.get(`/messages/${messageId}/reactions/${encoded}`);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
448
764
|
// src/structures/NovaInteraction.ts
|
|
449
765
|
var InteractionOptions = class {
|
|
450
766
|
constructor(data) {
|
|
@@ -624,651 +940,2143 @@ var NovaInteraction = class _NovaInteraction {
|
|
|
624
940
|
}
|
|
625
941
|
};
|
|
626
942
|
|
|
627
|
-
// src/
|
|
628
|
-
var
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
this.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
this.
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
this.
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
process.exit(0);
|
|
672
|
-
});
|
|
673
|
-
process.once("SIGTERM", () => {
|
|
674
|
-
cleanup();
|
|
675
|
-
process.exit(0);
|
|
676
|
-
});
|
|
943
|
+
// src/structures/NovaMessage.ts
|
|
944
|
+
var NovaMessage = class _NovaMessage {
|
|
945
|
+
constructor(raw, messages, reactions) {
|
|
946
|
+
this.id = raw.id;
|
|
947
|
+
this.content = raw.content;
|
|
948
|
+
this.channelId = raw.channelId;
|
|
949
|
+
this.author = raw.author;
|
|
950
|
+
this.embed = raw.embed ?? null;
|
|
951
|
+
this.components = raw.components ?? [];
|
|
952
|
+
this.replyToId = raw.replyToId ?? null;
|
|
953
|
+
this.attachments = raw.attachments ?? [];
|
|
954
|
+
this.reactions = raw.reactions ?? [];
|
|
955
|
+
this.createdAt = new Date(raw.createdAt);
|
|
956
|
+
this.editedAt = raw.editedAt ? new Date(raw.editedAt) : null;
|
|
957
|
+
this._messages = messages;
|
|
958
|
+
this._reactions = reactions;
|
|
959
|
+
}
|
|
960
|
+
// ─── Type guards ─────────────────────────────────────────────────────────────
|
|
961
|
+
/** Returns true if the message was sent by a bot. */
|
|
962
|
+
isFromBot() {
|
|
963
|
+
return this.author.isBot === true;
|
|
964
|
+
}
|
|
965
|
+
/** Returns true if the message has an embed. */
|
|
966
|
+
hasEmbed() {
|
|
967
|
+
return this.embed !== null;
|
|
968
|
+
}
|
|
969
|
+
/** Returns true if the message has interactive components. */
|
|
970
|
+
hasComponents() {
|
|
971
|
+
return this.components.length > 0;
|
|
972
|
+
}
|
|
973
|
+
/** Returns true if the message has been edited. */
|
|
974
|
+
isEdited() {
|
|
975
|
+
return this.editedAt !== null;
|
|
976
|
+
}
|
|
977
|
+
// ─── Reactions ────────────────────────────────────────────────────────────────
|
|
978
|
+
/**
|
|
979
|
+
* Add a reaction to this message.
|
|
980
|
+
*
|
|
981
|
+
* @example
|
|
982
|
+
* await msg.react('👍')
|
|
983
|
+
* await msg.react('🎉')
|
|
984
|
+
*/
|
|
985
|
+
react(emoji) {
|
|
986
|
+
return this._reactions.add(this.id, emoji);
|
|
677
987
|
}
|
|
678
988
|
/**
|
|
679
|
-
*
|
|
680
|
-
* Automatically routes `interactionCreate` events whose `commandName` matches.
|
|
989
|
+
* Remove the bot's reaction from this message.
|
|
681
990
|
*
|
|
682
991
|
* @example
|
|
683
|
-
*
|
|
684
|
-
* await interaction.reply('Pong! 🏓')
|
|
685
|
-
* })
|
|
992
|
+
* await msg.removeReaction('👍')
|
|
686
993
|
*/
|
|
687
|
-
|
|
688
|
-
this.
|
|
689
|
-
return this;
|
|
994
|
+
removeReaction(emoji) {
|
|
995
|
+
return this._reactions.remove(this.id, emoji);
|
|
690
996
|
}
|
|
997
|
+
// ─── Messaging ────────────────────────────────────────────────────────────────
|
|
691
998
|
/**
|
|
692
|
-
*
|
|
999
|
+
* Reply to this message.
|
|
693
1000
|
*
|
|
694
1001
|
* @example
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
* })
|
|
1002
|
+
* await msg.reply('Got it!')
|
|
1003
|
+
* await msg.reply({ embed: new EmbedBuilder().setTitle('Result').toJSON() })
|
|
698
1004
|
*/
|
|
699
|
-
|
|
700
|
-
this.
|
|
701
|
-
return this;
|
|
1005
|
+
reply(options) {
|
|
1006
|
+
const opts = typeof options === "string" ? { content: options, replyToId: this.id } : { ...options, replyToId: this.id };
|
|
1007
|
+
return this._messages.send(this.channelId, opts).then((raw) => new _NovaMessage(raw, this._messages, this._reactions));
|
|
702
1008
|
}
|
|
703
1009
|
/**
|
|
704
|
-
*
|
|
1010
|
+
* Edit this message.
|
|
1011
|
+
* Only works if the bot is the author.
|
|
705
1012
|
*
|
|
706
1013
|
* @example
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* await interaction.reply(`You picked: ${chosen}`)
|
|
710
|
-
* })
|
|
1014
|
+
* await msg.edit('Updated content')
|
|
1015
|
+
* await msg.edit({ content: 'Updated', embed: { title: 'New embed' } })
|
|
711
1016
|
*/
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return this;
|
|
1017
|
+
edit(options) {
|
|
1018
|
+
const opts = typeof options === "string" ? { content: options } : options;
|
|
1019
|
+
return this._messages.edit(this.id, opts).then((raw) => new _NovaMessage(raw, this._messages, this._reactions));
|
|
715
1020
|
}
|
|
716
1021
|
/**
|
|
717
|
-
*
|
|
718
|
-
*
|
|
1022
|
+
* Delete this message.
|
|
1023
|
+
* Only works if the bot is the author.
|
|
719
1024
|
*
|
|
720
1025
|
* @example
|
|
721
|
-
* await
|
|
1026
|
+
* await msg.delete()
|
|
722
1027
|
*/
|
|
723
|
-
|
|
724
|
-
return
|
|
725
|
-
const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
|
|
726
|
-
this.socket = (0, import_socket.io)(gatewayUrl, {
|
|
727
|
-
path: "/socket.io",
|
|
728
|
-
transports: ["websocket"],
|
|
729
|
-
auth: { botToken: this.options.token },
|
|
730
|
-
reconnection: true,
|
|
731
|
-
reconnectionAttempts: Infinity,
|
|
732
|
-
reconnectionDelay: 1e3,
|
|
733
|
-
reconnectionDelayMax: 3e4
|
|
734
|
-
});
|
|
735
|
-
const onConnectError = (err) => {
|
|
736
|
-
this.socket?.disconnect();
|
|
737
|
-
this.socket = null;
|
|
738
|
-
reject(new Error(`[nova-bot-sdk] Gateway connection failed: ${err.message}`));
|
|
739
|
-
};
|
|
740
|
-
this.socket.once("connect_error", onConnectError);
|
|
741
|
-
this.socket.once("bot:ready", (data) => {
|
|
742
|
-
this.socket.off("connect_error", onConnectError);
|
|
743
|
-
this.botUser = {
|
|
744
|
-
...data.bot,
|
|
745
|
-
scopes: data.scopes,
|
|
746
|
-
// Flatten botUser fields for convenience so bot.username works
|
|
747
|
-
username: data.bot.botUser?.username ?? data.bot.name,
|
|
748
|
-
displayName: data.bot.botUser?.displayName ?? data.bot.name
|
|
749
|
-
};
|
|
750
|
-
this.emit("ready", this.botUser);
|
|
751
|
-
resolve();
|
|
752
|
-
});
|
|
753
|
-
this.socket.on("interaction:created", (raw) => {
|
|
754
|
-
const interaction = new NovaInteraction(raw, this.interactions);
|
|
755
|
-
this.emit("interactionCreate", interaction);
|
|
756
|
-
const run = (h) => {
|
|
757
|
-
if (h) Promise.resolve(h(interaction)).catch((e) => this.emit("error", e));
|
|
758
|
-
};
|
|
759
|
-
if (interaction.isCommand() && interaction.commandName) {
|
|
760
|
-
run(this._commandHandlers.get(interaction.commandName));
|
|
761
|
-
} else if (interaction.isButton() && interaction.customId) {
|
|
762
|
-
run(this._buttonHandlers.get(interaction.customId));
|
|
763
|
-
} else if (interaction.isSelectMenu() && interaction.customId) {
|
|
764
|
-
run(this._selectHandlers.get(interaction.customId));
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
this.socket.on("bot:event", (event) => {
|
|
768
|
-
this.emit("event", event);
|
|
769
|
-
const shorthand = EVENT_MAP[event.type];
|
|
770
|
-
if (shorthand) {
|
|
771
|
-
this.emit(shorthand, event.data);
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
this.socket.on("bot:error", (err) => {
|
|
775
|
-
this.emit("error", err);
|
|
776
|
-
});
|
|
777
|
-
this.socket.on("disconnect", (reason) => {
|
|
778
|
-
this.emit("disconnect", reason);
|
|
779
|
-
});
|
|
780
|
-
});
|
|
1028
|
+
delete() {
|
|
1029
|
+
return this._messages.delete(this.id);
|
|
781
1030
|
}
|
|
782
1031
|
/**
|
|
783
|
-
*
|
|
1032
|
+
* Pin this message in its channel.
|
|
1033
|
+
*
|
|
1034
|
+
* @example
|
|
1035
|
+
* await msg.pin()
|
|
784
1036
|
*/
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const sock = this.socket;
|
|
788
|
-
this.socket = null;
|
|
789
|
-
try {
|
|
790
|
-
sock.io.reconnection(false);
|
|
791
|
-
} catch {
|
|
792
|
-
}
|
|
793
|
-
try {
|
|
794
|
-
sock.io?.engine?.socket?.terminate?.();
|
|
795
|
-
} catch {
|
|
796
|
-
}
|
|
797
|
-
try {
|
|
798
|
-
sock.io?.engine?.socket?.destroy?.();
|
|
799
|
-
} catch {
|
|
800
|
-
}
|
|
801
|
-
try {
|
|
802
|
-
sock.io?.engine?.close?.();
|
|
803
|
-
} catch {
|
|
804
|
-
}
|
|
805
|
-
try {
|
|
806
|
-
sock.disconnect();
|
|
807
|
-
} catch {
|
|
808
|
-
}
|
|
809
|
-
}
|
|
1037
|
+
pin() {
|
|
1038
|
+
return this._messages.pin(this.id);
|
|
810
1039
|
}
|
|
811
1040
|
/**
|
|
812
|
-
*
|
|
813
|
-
* Requires the `messages.write` scope.
|
|
1041
|
+
* Unpin this message.
|
|
814
1042
|
*
|
|
815
1043
|
* @example
|
|
816
|
-
*
|
|
1044
|
+
* await msg.unpin()
|
|
817
1045
|
*/
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
|
|
821
|
-
}
|
|
822
|
-
this.socket.emit("bot:message:send", { channelId, content });
|
|
1046
|
+
unpin() {
|
|
1047
|
+
return this._messages.unpin(this.id);
|
|
823
1048
|
}
|
|
824
1049
|
/**
|
|
825
|
-
*
|
|
1050
|
+
* Re-fetch the latest version of this message from the server.
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* const fresh = await msg.fetch()
|
|
826
1054
|
*/
|
|
827
|
-
|
|
828
|
-
this.
|
|
1055
|
+
async fetch() {
|
|
1056
|
+
const raw = await this._messages.fetchOne(this.id);
|
|
1057
|
+
return new _NovaMessage(raw, this._messages, this._reactions);
|
|
829
1058
|
}
|
|
830
1059
|
/**
|
|
831
|
-
*
|
|
1060
|
+
* Get a URL to this message (deep link).
|
|
832
1061
|
*/
|
|
833
|
-
|
|
834
|
-
this.
|
|
835
|
-
}
|
|
836
|
-
on(event, listener) {
|
|
837
|
-
return super.on(event, listener);
|
|
838
|
-
}
|
|
839
|
-
once(event, listener) {
|
|
840
|
-
return super.once(event, listener);
|
|
1062
|
+
get url() {
|
|
1063
|
+
return `/channels/${this.channelId}/messages/${this.id}`;
|
|
841
1064
|
}
|
|
842
|
-
|
|
843
|
-
|
|
1065
|
+
/**
|
|
1066
|
+
* Return the raw message object.
|
|
1067
|
+
*/
|
|
1068
|
+
toJSON() {
|
|
1069
|
+
return {
|
|
1070
|
+
id: this.id,
|
|
1071
|
+
content: this.content,
|
|
1072
|
+
channelId: this.channelId,
|
|
1073
|
+
author: this.author,
|
|
1074
|
+
embed: this.embed,
|
|
1075
|
+
components: this.components,
|
|
1076
|
+
replyToId: this.replyToId,
|
|
1077
|
+
attachments: this.attachments,
|
|
1078
|
+
reactions: this.reactions,
|
|
1079
|
+
createdAt: this.createdAt.toISOString(),
|
|
1080
|
+
editedAt: this.editedAt?.toISOString() ?? null
|
|
1081
|
+
};
|
|
844
1082
|
}
|
|
845
|
-
|
|
846
|
-
return
|
|
1083
|
+
toString() {
|
|
1084
|
+
return `NovaMessage(${this.id}): ${this.content.slice(0, 50)}${this.content.length > 50 ? "\u2026" : ""}`;
|
|
847
1085
|
}
|
|
848
1086
|
};
|
|
849
1087
|
|
|
850
|
-
// src/
|
|
851
|
-
var
|
|
852
|
-
constructor() {
|
|
853
|
-
this.
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
this.
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
this.
|
|
863
|
-
|
|
864
|
-
|
|
1088
|
+
// src/structures/NovaChannel.ts
|
|
1089
|
+
var NovaChannel = class _NovaChannel {
|
|
1090
|
+
constructor(raw, channels, messages) {
|
|
1091
|
+
this.id = raw.id;
|
|
1092
|
+
this.name = raw.name;
|
|
1093
|
+
this.type = raw.type;
|
|
1094
|
+
this.serverId = raw.serverId;
|
|
1095
|
+
this.topic = raw.topic;
|
|
1096
|
+
this.position = raw.position;
|
|
1097
|
+
this.slowMode = raw.slowMode ?? 0;
|
|
1098
|
+
this.createdAt = new Date(raw.createdAt);
|
|
1099
|
+
this._channels = channels;
|
|
1100
|
+
this._messages = messages;
|
|
1101
|
+
}
|
|
1102
|
+
// ─── Type guards ────────────────────────────────────────────────────────────
|
|
1103
|
+
/** `true` for text channels. */
|
|
1104
|
+
isText() {
|
|
1105
|
+
return this.type === "TEXT";
|
|
1106
|
+
}
|
|
1107
|
+
/** `true` for voice channels. */
|
|
1108
|
+
isVoice() {
|
|
1109
|
+
return this.type === "VOICE";
|
|
1110
|
+
}
|
|
1111
|
+
/** `true` for announcement channels. */
|
|
1112
|
+
isAnnouncement() {
|
|
1113
|
+
return this.type === "ANNOUNCEMENT";
|
|
1114
|
+
}
|
|
1115
|
+
/** `true` for forum channels. */
|
|
1116
|
+
isForum() {
|
|
1117
|
+
return this.type === "FORUM";
|
|
1118
|
+
}
|
|
1119
|
+
/** `true` for stage channels. */
|
|
1120
|
+
isStage() {
|
|
1121
|
+
return this.type === "STAGE";
|
|
1122
|
+
}
|
|
1123
|
+
/** `true` if slow-mode is enabled on this channel. */
|
|
1124
|
+
hasSlowMode() {
|
|
1125
|
+
return this.slowMode > 0;
|
|
1126
|
+
}
|
|
1127
|
+
// ─── Messaging ──────────────────────────────────────────────────────────────
|
|
865
1128
|
/**
|
|
866
|
-
*
|
|
867
|
-
*
|
|
1129
|
+
* Send a message to this channel.
|
|
1130
|
+
* Accepts a plain string or a full options object.
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* await channel.send('Hello!')
|
|
1134
|
+
* await channel.send({ content: 'Hi', embed: { title: 'Stats' } })
|
|
868
1135
|
*/
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
return this;
|
|
872
|
-
}
|
|
873
|
-
/** Make the title a clickable hyperlink. */
|
|
874
|
-
setUrl(url) {
|
|
875
|
-
this._data.url = url;
|
|
876
|
-
return this;
|
|
877
|
-
}
|
|
878
|
-
/** Small image shown in the top-right corner. */
|
|
879
|
-
setThumbnail(url) {
|
|
880
|
-
this._data.thumbnail = url;
|
|
881
|
-
return this;
|
|
882
|
-
}
|
|
883
|
-
/** Large image shown below the fields. */
|
|
884
|
-
setImage(url) {
|
|
885
|
-
this._data.image = url;
|
|
886
|
-
return this;
|
|
1136
|
+
send(options) {
|
|
1137
|
+
const body = typeof options === "string" ? { content: options } : options;
|
|
1138
|
+
return this._messages.send(this.id, body);
|
|
887
1139
|
}
|
|
888
|
-
/**
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* Fetch recent messages from this channel.
|
|
1142
|
+
*
|
|
1143
|
+
* @example
|
|
1144
|
+
* const messages = await channel.fetchMessages({ limit: 50 })
|
|
1145
|
+
*/
|
|
1146
|
+
fetchMessages(options = {}) {
|
|
1147
|
+
return this._channels.fetchMessages(this.id, options);
|
|
892
1148
|
}
|
|
893
1149
|
/**
|
|
894
|
-
*
|
|
895
|
-
*
|
|
1150
|
+
* Fetch all pinned messages in this channel.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* const pins = await channel.fetchPins()
|
|
896
1154
|
*/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
this._data.timestamp = d.toISOString();
|
|
900
|
-
return this;
|
|
1155
|
+
fetchPins() {
|
|
1156
|
+
return this._channels.fetchPins(this.id);
|
|
901
1157
|
}
|
|
902
|
-
/**
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1158
|
+
/**
|
|
1159
|
+
* Start a typing indicator (shows "Bot is typing…" for ~5 seconds).
|
|
1160
|
+
*
|
|
1161
|
+
* @example
|
|
1162
|
+
* await channel.startTyping()
|
|
1163
|
+
*/
|
|
1164
|
+
startTyping() {
|
|
1165
|
+
return this._channels.startTyping(this.id);
|
|
906
1166
|
}
|
|
1167
|
+
// ─── Management ─────────────────────────────────────────────────────────────
|
|
907
1168
|
/**
|
|
908
|
-
*
|
|
909
|
-
*
|
|
1169
|
+
* Edit this channel's properties.
|
|
1170
|
+
* Requires the `channels.manage` scope.
|
|
1171
|
+
*
|
|
1172
|
+
* @example
|
|
1173
|
+
* await channel.edit({ name: 'general-2', topic: 'The second general channel' })
|
|
910
1174
|
*/
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
return this;
|
|
1175
|
+
async edit(options) {
|
|
1176
|
+
const raw = await this._channels.edit(this.id, options);
|
|
1177
|
+
return new _NovaChannel(raw, this._channels, this._messages);
|
|
915
1178
|
}
|
|
916
|
-
/**
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1179
|
+
/**
|
|
1180
|
+
* Delete this channel.
|
|
1181
|
+
* Requires the `channels.manage` scope.
|
|
1182
|
+
*
|
|
1183
|
+
* @example
|
|
1184
|
+
* await channel.delete()
|
|
1185
|
+
*/
|
|
1186
|
+
delete() {
|
|
1187
|
+
return this._channels.delete(this.id);
|
|
921
1188
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1189
|
+
// ─── Serialisation ──────────────────────────────────────────────────────────
|
|
1190
|
+
/**
|
|
1191
|
+
* Returns the channel as a mention-style string: `#name`.
|
|
1192
|
+
*/
|
|
1193
|
+
toString() {
|
|
1194
|
+
return `#${this.name}`;
|
|
926
1195
|
}
|
|
927
|
-
/**
|
|
1196
|
+
/** Returns the raw channel data. */
|
|
928
1197
|
toJSON() {
|
|
929
1198
|
return {
|
|
930
|
-
|
|
931
|
-
|
|
1199
|
+
id: this.id,
|
|
1200
|
+
name: this.name,
|
|
1201
|
+
type: this.type,
|
|
1202
|
+
serverId: this.serverId,
|
|
1203
|
+
topic: this.topic,
|
|
1204
|
+
position: this.position,
|
|
1205
|
+
slowMode: this.slowMode,
|
|
1206
|
+
createdAt: this.createdAt.toISOString()
|
|
932
1207
|
};
|
|
933
1208
|
}
|
|
934
1209
|
};
|
|
935
1210
|
|
|
936
|
-
// src/
|
|
937
|
-
var
|
|
938
|
-
constructor() {
|
|
939
|
-
this.
|
|
940
|
-
this.
|
|
941
|
-
this.
|
|
942
|
-
this.
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
this.
|
|
947
|
-
|
|
1211
|
+
// src/structures/NovaMember.ts
|
|
1212
|
+
var NovaMember = class {
|
|
1213
|
+
constructor(raw, serverId, members) {
|
|
1214
|
+
this.userId = raw.user.id;
|
|
1215
|
+
this.serverId = serverId;
|
|
1216
|
+
this.username = raw.user.username;
|
|
1217
|
+
this.displayName = raw.user.displayName;
|
|
1218
|
+
this.avatar = raw.user.avatar;
|
|
1219
|
+
this.role = raw.role;
|
|
1220
|
+
this.status = raw.user.status;
|
|
1221
|
+
this.isBot = raw.user.isBot;
|
|
1222
|
+
this.joinedAt = new Date(raw.joinedAt);
|
|
1223
|
+
this._members = members;
|
|
1224
|
+
}
|
|
1225
|
+
// ─── Type guards ────────────────────────────────────────────────────────────
|
|
1226
|
+
/** `true` if this member is the server owner. */
|
|
1227
|
+
isOwner() {
|
|
1228
|
+
return this.role === "OWNER";
|
|
1229
|
+
}
|
|
1230
|
+
/** `true` if this member is an admin or the server owner. */
|
|
1231
|
+
isAdmin() {
|
|
1232
|
+
return this.role === "ADMIN" || this.role === "OWNER";
|
|
1233
|
+
}
|
|
1234
|
+
/** `true` if this member is a regular (non-privileged) member. */
|
|
1235
|
+
isRegularMember() {
|
|
1236
|
+
return this.role === "MEMBER";
|
|
1237
|
+
}
|
|
1238
|
+
/** `true` if the user is currently online. */
|
|
1239
|
+
isOnline() {
|
|
1240
|
+
return this.status === "ONLINE";
|
|
1241
|
+
}
|
|
1242
|
+
/** `true` if the user is idle / away. */
|
|
1243
|
+
isIdle() {
|
|
1244
|
+
return this.status === "IDLE";
|
|
1245
|
+
}
|
|
1246
|
+
/** `true` if the user is set to Do Not Disturb. */
|
|
1247
|
+
isDND() {
|
|
1248
|
+
return this.status === "DND";
|
|
1249
|
+
}
|
|
1250
|
+
/** `true` if the user appears offline. */
|
|
1251
|
+
isOffline() {
|
|
1252
|
+
return this.status === "OFFLINE";
|
|
1253
|
+
}
|
|
1254
|
+
// ─── Actions ────────────────────────────────────────────────────────────────
|
|
1255
|
+
/**
|
|
1256
|
+
* Kick this member from the server.
|
|
1257
|
+
* Bots cannot kick owners or admins (throws 403).
|
|
1258
|
+
*
|
|
1259
|
+
* @example
|
|
1260
|
+
* await member.kick()
|
|
1261
|
+
*/
|
|
1262
|
+
kick() {
|
|
1263
|
+
return this._members.kick(this.serverId, this.userId);
|
|
948
1264
|
}
|
|
949
|
-
/**
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1265
|
+
/**
|
|
1266
|
+
* Ban this member from the server with an optional reason.
|
|
1267
|
+
* Bots cannot ban owners or admins (throws 403).
|
|
1268
|
+
*
|
|
1269
|
+
* @example
|
|
1270
|
+
* await member.ban('Repeated rule violations')
|
|
1271
|
+
*/
|
|
1272
|
+
ban(reason) {
|
|
1273
|
+
return this._members.ban(this.serverId, this.userId, reason);
|
|
953
1274
|
}
|
|
954
1275
|
/**
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
*
|
|
1276
|
+
* Send this user a direct message.
|
|
1277
|
+
* Requires the `messages.write` scope.
|
|
1278
|
+
*
|
|
1279
|
+
* @example
|
|
1280
|
+
* await member.dm('Welcome to the server!')
|
|
1281
|
+
* await member.dm({ content: 'Hello', embed: { title: 'Rules' } })
|
|
961
1282
|
*/
|
|
962
|
-
|
|
963
|
-
this.
|
|
964
|
-
return this;
|
|
1283
|
+
dm(options) {
|
|
1284
|
+
return this._members.dm(this.userId, options);
|
|
965
1285
|
}
|
|
966
|
-
/**
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* Assign a custom role to this member.
|
|
1288
|
+
* Requires the `members.roles` scope.
|
|
1289
|
+
*
|
|
1290
|
+
* @example
|
|
1291
|
+
* await member.addRole('role-id')
|
|
1292
|
+
*/
|
|
1293
|
+
addRole(roleId) {
|
|
1294
|
+
return this._members.addRole(this.serverId, this.userId, roleId);
|
|
970
1295
|
}
|
|
971
1296
|
/**
|
|
972
|
-
*
|
|
973
|
-
*
|
|
1297
|
+
* Remove a custom role from this member.
|
|
1298
|
+
* Requires the `members.roles` scope.
|
|
1299
|
+
*
|
|
1300
|
+
* @example
|
|
1301
|
+
* await member.removeRole('role-id')
|
|
974
1302
|
*/
|
|
975
|
-
|
|
976
|
-
this.
|
|
977
|
-
this._style = "link";
|
|
978
|
-
return this;
|
|
1303
|
+
removeRole(roleId) {
|
|
1304
|
+
return this._members.removeRole(this.serverId, this.userId, roleId);
|
|
979
1305
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1306
|
+
// ─── Serialisation ──────────────────────────────────────────────────────────
|
|
1307
|
+
/**
|
|
1308
|
+
* Returns a mention-style string: `@displayName`.
|
|
1309
|
+
*/
|
|
1310
|
+
toString() {
|
|
1311
|
+
return `@${this.displayName}`;
|
|
984
1312
|
}
|
|
1313
|
+
/** Returns the raw member data. */
|
|
985
1314
|
toJSON() {
|
|
986
1315
|
return {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1316
|
+
role: this.role,
|
|
1317
|
+
joinedAt: this.joinedAt.toISOString(),
|
|
1318
|
+
user: {
|
|
1319
|
+
id: this.userId,
|
|
1320
|
+
username: this.username,
|
|
1321
|
+
displayName: this.displayName,
|
|
1322
|
+
avatar: this.avatar,
|
|
1323
|
+
status: this.status,
|
|
1324
|
+
isBot: this.isBot
|
|
1325
|
+
}
|
|
994
1326
|
};
|
|
995
1327
|
}
|
|
996
1328
|
};
|
|
997
1329
|
|
|
998
|
-
// src/
|
|
999
|
-
var
|
|
1000
|
-
constructor() {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
this.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
this.
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1330
|
+
// src/client.ts
|
|
1331
|
+
var NovaClient = class extends import_node_events.EventEmitter {
|
|
1332
|
+
constructor(options) {
|
|
1333
|
+
super();
|
|
1334
|
+
/** The authenticated bot application. Available after `ready` fires. */
|
|
1335
|
+
this.botUser = null;
|
|
1336
|
+
this.socket = null;
|
|
1337
|
+
this._cronTimers = [];
|
|
1338
|
+
// ── Command / component routing ───────────────────────────────────────────────────
|
|
1339
|
+
this._commandHandlers = /* @__PURE__ */ new Map();
|
|
1340
|
+
this._buttonHandlers = /* @__PURE__ */ new Map();
|
|
1341
|
+
this._selectHandlers = /* @__PURE__ */ new Map();
|
|
1342
|
+
this._modalHandlers = /* @__PURE__ */ new Map();
|
|
1343
|
+
if (!options.token) {
|
|
1344
|
+
throw new Error("[nova-bot-sdk] A bot token is required.");
|
|
1345
|
+
}
|
|
1346
|
+
if (!options.token.startsWith("nova_bot_")) {
|
|
1347
|
+
console.warn('[nova-bot-sdk] Warning: token does not start with "nova_bot_". Are you sure this is a bot token?');
|
|
1348
|
+
}
|
|
1349
|
+
this.options = {
|
|
1350
|
+
token: options.token,
|
|
1351
|
+
baseUrl: options.baseUrl ?? "https://novachatapp.com"
|
|
1352
|
+
};
|
|
1353
|
+
this.http = new HttpClient(this.options.baseUrl, this.options.token);
|
|
1354
|
+
this.messages = new MessagesAPI(this.http);
|
|
1355
|
+
this.commands = new CommandsAPI(this.http);
|
|
1356
|
+
this.members = new MembersAPI(this.http);
|
|
1357
|
+
this.servers = new ServersAPI(this.http);
|
|
1358
|
+
this.interactions = new InteractionsAPI(this.http, this);
|
|
1359
|
+
this.permissions = new PermissionsAPI(this.http);
|
|
1360
|
+
this.channels = new ChannelsAPI(this.http);
|
|
1361
|
+
this.reactions = new ReactionsAPI(this.http);
|
|
1362
|
+
this.on("error", () => {
|
|
1363
|
+
});
|
|
1364
|
+
const cleanup = () => this.disconnect();
|
|
1365
|
+
process.once("beforeExit", cleanup);
|
|
1366
|
+
process.once("SIGINT", () => {
|
|
1367
|
+
cleanup();
|
|
1368
|
+
process.exit(0);
|
|
1369
|
+
});
|
|
1370
|
+
process.once("SIGTERM", () => {
|
|
1371
|
+
cleanup();
|
|
1372
|
+
process.exit(0);
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Register a handler for a slash or prefix command by name.
|
|
1377
|
+
* Automatically routes `interactionCreate` events whose `commandName` matches.
|
|
1378
|
+
*
|
|
1379
|
+
* @example
|
|
1380
|
+
* client.command('ping', async (interaction) => {
|
|
1381
|
+
* await interaction.reply('Pong! 🏓')
|
|
1382
|
+
* })
|
|
1383
|
+
*/
|
|
1384
|
+
command(name, handler) {
|
|
1385
|
+
this._commandHandlers.set(name, handler);
|
|
1386
|
+
return this;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Register a handler for a button by its `customId`.
|
|
1390
|
+
*
|
|
1391
|
+
* @example
|
|
1392
|
+
* client.button('confirm_delete', async (interaction) => {
|
|
1393
|
+
* await interaction.replyEphemeral('Deleted.')
|
|
1394
|
+
* })
|
|
1395
|
+
*/
|
|
1396
|
+
button(customId, handler) {
|
|
1397
|
+
this._buttonHandlers.set(customId, handler);
|
|
1398
|
+
return this;
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Register a handler for a select menu by its `customId`.
|
|
1402
|
+
*
|
|
1403
|
+
* @example
|
|
1404
|
+
* client.selectMenu('colour_pick', async (interaction) => {
|
|
1405
|
+
* const chosen = interaction.values[0]
|
|
1406
|
+
* await interaction.reply(`You picked: ${chosen}`)
|
|
1407
|
+
* })
|
|
1408
|
+
*/
|
|
1409
|
+
selectMenu(customId, handler) {
|
|
1410
|
+
this._selectHandlers.set(customId, handler);
|
|
1411
|
+
return this;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Register a handler for a modal submission by its `customId`.
|
|
1415
|
+
* Called automatically when the user submits a modal with that `customId`.
|
|
1416
|
+
*
|
|
1417
|
+
* @example
|
|
1418
|
+
* client.modal('report_modal', async (interaction) => {
|
|
1419
|
+
* const reason = interaction.modalData.reason
|
|
1420
|
+
* await interaction.replyEphemeral(`Report received: ${reason}`)
|
|
1421
|
+
* })
|
|
1422
|
+
*/
|
|
1423
|
+
modal(customId, handler) {
|
|
1424
|
+
this._modalHandlers.set(customId, handler);
|
|
1425
|
+
return this;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Connect to the Nova WebSocket gateway.
|
|
1429
|
+
* Resolves when the `ready` event is received.
|
|
1430
|
+
*
|
|
1431
|
+
* @example
|
|
1432
|
+
* await client.connect()
|
|
1433
|
+
*/
|
|
1434
|
+
connect() {
|
|
1435
|
+
return new Promise((resolve, reject) => {
|
|
1436
|
+
const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
|
|
1437
|
+
this.socket = (0, import_socket.io)(gatewayUrl, {
|
|
1438
|
+
path: "/socket.io",
|
|
1439
|
+
transports: ["websocket"],
|
|
1440
|
+
auth: { botToken: this.options.token },
|
|
1441
|
+
reconnection: true,
|
|
1442
|
+
reconnectionAttempts: Infinity,
|
|
1443
|
+
reconnectionDelay: 1e3,
|
|
1444
|
+
reconnectionDelayMax: 3e4
|
|
1445
|
+
});
|
|
1446
|
+
const onConnectError = (err) => {
|
|
1447
|
+
this.socket?.disconnect();
|
|
1448
|
+
this.socket = null;
|
|
1449
|
+
reject(new Error(`[nova-bot-sdk] Gateway connection failed: ${err.message}`));
|
|
1450
|
+
};
|
|
1451
|
+
this.socket.once("connect_error", onConnectError);
|
|
1452
|
+
this.socket.once("bot:ready", (data) => {
|
|
1453
|
+
this.socket.off("connect_error", onConnectError);
|
|
1454
|
+
this.botUser = {
|
|
1455
|
+
...data.bot,
|
|
1456
|
+
scopes: data.scopes,
|
|
1457
|
+
// Flatten botUser fields for convenience so bot.username works
|
|
1458
|
+
username: data.bot.botUser?.username ?? data.bot.name,
|
|
1459
|
+
displayName: data.bot.botUser?.displayName ?? data.bot.name
|
|
1460
|
+
};
|
|
1461
|
+
this.emit("ready", this.botUser);
|
|
1462
|
+
resolve();
|
|
1463
|
+
});
|
|
1464
|
+
this.socket.on("interaction:created", (raw) => {
|
|
1465
|
+
const interaction = new NovaInteraction(raw, this.interactions);
|
|
1466
|
+
this.emit("interactionCreate", interaction);
|
|
1467
|
+
const run = (h) => {
|
|
1468
|
+
if (h) Promise.resolve(h(interaction)).catch((e) => this.emit("error", e));
|
|
1469
|
+
};
|
|
1470
|
+
if (interaction.isCommand() && interaction.commandName) {
|
|
1471
|
+
run(this._commandHandlers.get(interaction.commandName));
|
|
1472
|
+
} else if (interaction.isButton() && interaction.customId) {
|
|
1473
|
+
run(this._buttonHandlers.get(interaction.customId));
|
|
1474
|
+
} else if (interaction.isSelectMenu() && interaction.customId) {
|
|
1475
|
+
run(this._selectHandlers.get(interaction.customId));
|
|
1476
|
+
} else if (interaction.isModalSubmit() && interaction.customId) {
|
|
1477
|
+
run(this._modalHandlers.get(interaction.customId));
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
this.socket.on("bot:event", (event) => {
|
|
1481
|
+
this.emit("event", event);
|
|
1482
|
+
switch (event.type) {
|
|
1483
|
+
case "message.created":
|
|
1484
|
+
case "message.edited": {
|
|
1485
|
+
const raw = event.data;
|
|
1486
|
+
const wrapped = new NovaMessage(raw, this.messages, this.reactions);
|
|
1487
|
+
if (event.type === "message.created") this.emit("messageCreate", wrapped);
|
|
1488
|
+
else this.emit("messageUpdate", wrapped);
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
case "message.deleted":
|
|
1492
|
+
this.emit("messageDelete", event.data);
|
|
1493
|
+
break;
|
|
1494
|
+
case "message.reaction_added":
|
|
1495
|
+
this.emit("reactionAdd", event.data);
|
|
1496
|
+
break;
|
|
1497
|
+
case "message.reaction_removed":
|
|
1498
|
+
this.emit("reactionRemove", event.data);
|
|
1499
|
+
break;
|
|
1500
|
+
case "user.joined_server":
|
|
1501
|
+
this.emit("memberAdd", event.data);
|
|
1502
|
+
break;
|
|
1503
|
+
case "user.left_server":
|
|
1504
|
+
this.emit("memberRemove", event.data);
|
|
1505
|
+
break;
|
|
1506
|
+
case "user.started_typing":
|
|
1507
|
+
this.emit("typingStart", event.data);
|
|
1508
|
+
break;
|
|
1509
|
+
case "message.pinned":
|
|
1510
|
+
this.emit("messagePinned", event.data);
|
|
1511
|
+
break;
|
|
1512
|
+
case "channel.created":
|
|
1513
|
+
this.emit("channelCreate", event.data);
|
|
1514
|
+
break;
|
|
1515
|
+
case "channel.updated":
|
|
1516
|
+
this.emit("channelUpdate", event.data);
|
|
1517
|
+
break;
|
|
1518
|
+
case "channel.deleted":
|
|
1519
|
+
this.emit("channelDelete", event.data);
|
|
1520
|
+
break;
|
|
1521
|
+
case "role.created":
|
|
1522
|
+
this.emit("roleCreate", event.data);
|
|
1523
|
+
break;
|
|
1524
|
+
case "role.deleted":
|
|
1525
|
+
this.emit("roleDelete", event.data);
|
|
1526
|
+
break;
|
|
1527
|
+
case "user.voice_joined":
|
|
1528
|
+
this.emit("voiceJoin", event.data);
|
|
1529
|
+
break;
|
|
1530
|
+
case "user.voice_left":
|
|
1531
|
+
this.emit("voiceLeave", event.data);
|
|
1532
|
+
break;
|
|
1533
|
+
case "user.banned":
|
|
1534
|
+
this.emit("memberBanned", event.data);
|
|
1535
|
+
break;
|
|
1536
|
+
case "user.unbanned":
|
|
1537
|
+
this.emit("memberUnbanned", event.data);
|
|
1538
|
+
break;
|
|
1539
|
+
case "user.updated_profile":
|
|
1540
|
+
this.emit("memberUpdate", event.data);
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
this.socket.on("bot:error", (err) => {
|
|
1545
|
+
this.emit("error", err);
|
|
1546
|
+
});
|
|
1547
|
+
this.socket.on("disconnect", (reason) => {
|
|
1548
|
+
this.emit("disconnect", reason);
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Register a recurring task that fires at a set interval.
|
|
1554
|
+
* All cron tasks are automatically cancelled on `disconnect()`.
|
|
1555
|
+
*
|
|
1556
|
+
* @param intervalMs - How often to run the task (in milliseconds).
|
|
1557
|
+
* @param fn - Async or sync function to call on each tick.
|
|
1558
|
+
* @returns A cancel function — call it to stop this specific task.
|
|
1559
|
+
*
|
|
1560
|
+
* @example
|
|
1561
|
+
* // Check for new announcements every 30 seconds
|
|
1562
|
+
* client.cron(30_000, async () => {
|
|
1563
|
+
* const messages = await client.messages.fetch(channelId, { limit: 5 })
|
|
1564
|
+
* // do something...
|
|
1565
|
+
* })
|
|
1566
|
+
*/
|
|
1567
|
+
cron(intervalMs, fn) {
|
|
1568
|
+
const id = setInterval(() => {
|
|
1569
|
+
try {
|
|
1570
|
+
const result = fn();
|
|
1571
|
+
if (result && typeof result.catch === "function") {
|
|
1572
|
+
;
|
|
1573
|
+
result.catch((e) => this.emit("error", e));
|
|
1574
|
+
}
|
|
1575
|
+
} catch (e) {
|
|
1576
|
+
this.emit("error", e);
|
|
1577
|
+
}
|
|
1578
|
+
}, intervalMs);
|
|
1579
|
+
this._cronTimers.push(id);
|
|
1580
|
+
return () => {
|
|
1581
|
+
clearInterval(id);
|
|
1582
|
+
this._cronTimers = this._cronTimers.filter((t) => t !== id);
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Set the bot's presence status.
|
|
1587
|
+
* Broadcasts to all servers the bot is in via WebSocket.
|
|
1588
|
+
*
|
|
1589
|
+
* @example
|
|
1590
|
+
* client.setStatus('DND') // Do Not Disturb
|
|
1591
|
+
* client.setStatus('IDLE') // Away
|
|
1592
|
+
* client.setStatus('OFFLINE') // Appear offline
|
|
1593
|
+
* client.setStatus('ONLINE') // Back online
|
|
1594
|
+
*/
|
|
1595
|
+
setStatus(status) {
|
|
1596
|
+
this.socket?.emit("bot:status", { status });
|
|
1597
|
+
}
|
|
1598
|
+
// ─── Rich-wrapper helpers ────────────────────────────────────────────────────
|
|
1599
|
+
/**
|
|
1600
|
+
* Fetch a single channel and return it as a rich `NovaChannel` wrapper.
|
|
1601
|
+
*
|
|
1602
|
+
* @example
|
|
1603
|
+
* const channel = await client.fetchChannel('channel-id')
|
|
1604
|
+
* await channel.send('Hello!')
|
|
1605
|
+
*/
|
|
1606
|
+
async fetchChannel(channelId) {
|
|
1607
|
+
const raw = await this.channels.fetch(channelId);
|
|
1608
|
+
return new NovaChannel(raw, this.channels, this.messages);
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Fetch all channels in a server and return them as `NovaChannel` wrappers.
|
|
1612
|
+
*
|
|
1613
|
+
* @example
|
|
1614
|
+
* const channels = await client.fetchChannels('server-id')
|
|
1615
|
+
* const textChannels = channels.filter(c => c.isText())
|
|
1616
|
+
*/
|
|
1617
|
+
async fetchChannels(serverId) {
|
|
1618
|
+
const list = await this.channels.list(serverId);
|
|
1619
|
+
return list.map((raw) => new NovaChannel(raw, this.channels, this.messages));
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Fetch all members in a server and return them as `NovaMember` wrappers.
|
|
1623
|
+
*
|
|
1624
|
+
* @example
|
|
1625
|
+
* const members = await client.fetchMembers('server-id')
|
|
1626
|
+
* const bots = members.filter(m => m.isBot)
|
|
1627
|
+
*/
|
|
1628
|
+
async fetchMembers(serverId) {
|
|
1629
|
+
const list = await this.members.list(serverId);
|
|
1630
|
+
return list.map((raw) => new NovaMember(raw, serverId, this.members));
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Fetch a single member from a server and return them as a `NovaMember` wrapper.
|
|
1634
|
+
* Throws if the user is not found in the server.
|
|
1635
|
+
*
|
|
1636
|
+
* @example
|
|
1637
|
+
* const member = await client.fetchMember('server-id', 'user-id')
|
|
1638
|
+
* await member.dm('Welcome!')
|
|
1639
|
+
*/
|
|
1640
|
+
async fetchMember(serverId, userId) {
|
|
1641
|
+
const list = await this.members.list(serverId);
|
|
1642
|
+
const raw = list.find((m) => m.user.id === userId);
|
|
1643
|
+
if (!raw) throw new Error(`[nova-bot-sdk] Member ${userId} not found in server ${serverId}`);
|
|
1644
|
+
return new NovaMember(raw, serverId, this.members);
|
|
1645
|
+
}
|
|
1646
|
+
// ─── Event fence ──────────────────────────────────────────────────────────────
|
|
1647
|
+
/**
|
|
1648
|
+
* Wait for a specific event to be emitted, optionally filtered.
|
|
1649
|
+
* Rejects after `timeoutMs` (default 30 s) if the condition never fires.
|
|
1650
|
+
*
|
|
1651
|
+
* @example
|
|
1652
|
+
* // Wait for any message in a specific channel
|
|
1653
|
+
* const msg = await client.waitFor('messageCreate', m => m.channelId === channelId)
|
|
1654
|
+
*
|
|
1655
|
+
* // Wait for a button click with a timeout
|
|
1656
|
+
* const i = await client.waitFor('interactionCreate', i => i.isButton(), 60_000)
|
|
1657
|
+
*/
|
|
1658
|
+
waitFor(event, filter, timeoutMs = 3e4) {
|
|
1659
|
+
return new Promise((resolve, reject) => {
|
|
1660
|
+
const timer = setTimeout(() => {
|
|
1661
|
+
this.off(event, handler);
|
|
1662
|
+
reject(new Error(`[nova-bot-sdk] waitFor('${String(event)}') timed out after ${timeoutMs}ms`));
|
|
1663
|
+
}, timeoutMs);
|
|
1664
|
+
const handler = (...args) => {
|
|
1665
|
+
const arg = args[0];
|
|
1666
|
+
if (!filter || filter(arg)) {
|
|
1667
|
+
clearTimeout(timer);
|
|
1668
|
+
this.off(event, handler);
|
|
1669
|
+
resolve(arg);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
this.on(event, handler);
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Disconnect from the gateway and clean up.
|
|
1677
|
+
*/
|
|
1678
|
+
disconnect() {
|
|
1679
|
+
for (const id of this._cronTimers) clearInterval(id);
|
|
1680
|
+
this._cronTimers = [];
|
|
1681
|
+
if (this.socket) {
|
|
1682
|
+
const sock = this.socket;
|
|
1683
|
+
this.socket = null;
|
|
1684
|
+
try {
|
|
1685
|
+
sock.io.reconnection(false);
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
try {
|
|
1689
|
+
sock.io?.engine?.socket?.terminate?.();
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
sock.io?.engine?.socket?.destroy?.();
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
try {
|
|
1697
|
+
sock.io?.engine?.close?.();
|
|
1698
|
+
} catch {
|
|
1699
|
+
}
|
|
1700
|
+
try {
|
|
1701
|
+
sock.disconnect();
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Send a message via the WebSocket gateway (lower latency than HTTP).
|
|
1708
|
+
* Requires the `messages.write` scope.
|
|
1709
|
+
*
|
|
1710
|
+
* @example
|
|
1711
|
+
* client.wsSend('channel-id', 'Hello from the gateway!')
|
|
1712
|
+
*/
|
|
1713
|
+
wsSend(channelId, content) {
|
|
1714
|
+
if (!this.socket?.connected) {
|
|
1715
|
+
throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
|
|
1716
|
+
}
|
|
1717
|
+
this.socket.emit("bot:message:send", { channelId, content });
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Start a typing indicator via WebSocket.
|
|
1721
|
+
*/
|
|
1722
|
+
wsTypingStart(channelId) {
|
|
1723
|
+
this.socket?.emit("bot:typing:start", { channelId });
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Stop a typing indicator via WebSocket.
|
|
1727
|
+
*/
|
|
1728
|
+
wsTypingStop(channelId) {
|
|
1729
|
+
this.socket?.emit("bot:typing:stop", { channelId });
|
|
1730
|
+
}
|
|
1731
|
+
on(event, listener) {
|
|
1732
|
+
return super.on(event, listener);
|
|
1733
|
+
}
|
|
1734
|
+
once(event, listener) {
|
|
1735
|
+
return super.once(event, listener);
|
|
1736
|
+
}
|
|
1737
|
+
off(event, listener) {
|
|
1738
|
+
return super.off(event, listener);
|
|
1739
|
+
}
|
|
1740
|
+
emit(event, ...args) {
|
|
1741
|
+
return super.emit(event, ...args);
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
// src/builders/EmbedBuilder.ts
|
|
1746
|
+
var EmbedBuilder = class {
|
|
1747
|
+
constructor() {
|
|
1748
|
+
this._data = {};
|
|
1749
|
+
}
|
|
1750
|
+
/** Set the embed title (shown in bold at the top). */
|
|
1751
|
+
setTitle(title) {
|
|
1752
|
+
this._data.title = title;
|
|
1753
|
+
return this;
|
|
1754
|
+
}
|
|
1755
|
+
/** Set the main description text (supports markdown). */
|
|
1756
|
+
setDescription(description) {
|
|
1757
|
+
this._data.description = description;
|
|
1758
|
+
return this;
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Set the accent colour.
|
|
1762
|
+
* @param color Hex string — e.g. `'#5865F2'` or `'5865F2'`
|
|
1763
|
+
*/
|
|
1764
|
+
setColor(color) {
|
|
1765
|
+
this._data.color = color.startsWith("#") ? color : `#${color}`;
|
|
1766
|
+
return this;
|
|
1767
|
+
}
|
|
1768
|
+
/** Make the title a clickable hyperlink. */
|
|
1769
|
+
setUrl(url) {
|
|
1770
|
+
this._data.url = url;
|
|
1771
|
+
return this;
|
|
1772
|
+
}
|
|
1773
|
+
/** Small image shown in the top-right corner. */
|
|
1774
|
+
setThumbnail(url) {
|
|
1775
|
+
this._data.thumbnail = url;
|
|
1776
|
+
return this;
|
|
1777
|
+
}
|
|
1778
|
+
/** Large image shown below the fields. */
|
|
1779
|
+
setImage(url) {
|
|
1780
|
+
this._data.image = url;
|
|
1781
|
+
return this;
|
|
1782
|
+
}
|
|
1783
|
+
/** Footer text shown at the very bottom of the embed. */
|
|
1784
|
+
setFooter(text) {
|
|
1785
|
+
this._data.footer = text;
|
|
1786
|
+
return this;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Add a timestamp to the footer line.
|
|
1790
|
+
* @param date Defaults to `new Date()`.
|
|
1791
|
+
*/
|
|
1792
|
+
setTimestamp(date) {
|
|
1793
|
+
const d = date instanceof Date ? date : date != null ? new Date(date) : /* @__PURE__ */ new Date();
|
|
1794
|
+
this._data.timestamp = d.toISOString();
|
|
1795
|
+
return this;
|
|
1796
|
+
}
|
|
1797
|
+
/** Author name + optional icon shown above the title. */
|
|
1798
|
+
setAuthor(author) {
|
|
1799
|
+
this._data.author = author;
|
|
1800
|
+
return this;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Add a single field.
|
|
1804
|
+
* @param inline Pass `true` to render this field side-by-side with adjacent inline fields.
|
|
1805
|
+
*/
|
|
1806
|
+
addField(name, value, inline) {
|
|
1807
|
+
if (!this._data.fields) this._data.fields = [];
|
|
1808
|
+
this._data.fields.push({ name, value, inline });
|
|
1809
|
+
return this;
|
|
1810
|
+
}
|
|
1811
|
+
/** Add multiple fields at once. */
|
|
1812
|
+
addFields(...fields) {
|
|
1813
|
+
if (!this._data.fields) this._data.fields = [];
|
|
1814
|
+
this._data.fields.push(...fields);
|
|
1815
|
+
return this;
|
|
1816
|
+
}
|
|
1817
|
+
/** Replace all fields. */
|
|
1818
|
+
setFields(fields) {
|
|
1819
|
+
this._data.fields = [...fields];
|
|
1820
|
+
return this;
|
|
1821
|
+
}
|
|
1822
|
+
/** Serialise to a plain `Embed` object you can pass to any API call. */
|
|
1823
|
+
toJSON() {
|
|
1824
|
+
return {
|
|
1825
|
+
...this._data,
|
|
1826
|
+
fields: this._data.fields ? [...this._data.fields] : void 0
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// src/builders/ButtonBuilder.ts
|
|
1832
|
+
var ButtonBuilder = class {
|
|
1833
|
+
constructor() {
|
|
1834
|
+
this._customId = "";
|
|
1835
|
+
this._label = "";
|
|
1836
|
+
this._style = "primary";
|
|
1837
|
+
this._disabled = false;
|
|
1838
|
+
}
|
|
1839
|
+
/** Custom ID returned in the `BUTTON_CLICK` interaction. Not required for `link` style buttons. */
|
|
1840
|
+
setCustomId(customId) {
|
|
1841
|
+
this._customId = customId;
|
|
1842
|
+
return this;
|
|
1843
|
+
}
|
|
1844
|
+
/** Text displayed on the button. */
|
|
1845
|
+
setLabel(label) {
|
|
1846
|
+
this._label = label;
|
|
1847
|
+
return this;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Visual style:
|
|
1851
|
+
* - `primary` — blurple / brand colour
|
|
1852
|
+
* - `secondary` — grey
|
|
1853
|
+
* - `success` — green
|
|
1854
|
+
* - `danger` — red
|
|
1855
|
+
* - `link` — grey, navigates to a URL instead of creating an interaction
|
|
1856
|
+
*/
|
|
1857
|
+
setStyle(style) {
|
|
1858
|
+
this._style = style;
|
|
1859
|
+
return this;
|
|
1860
|
+
}
|
|
1861
|
+
/** Emoji shown to the left of the label (unicode or custom e.g. `'👋'`). */
|
|
1862
|
+
setEmoji(emoji) {
|
|
1863
|
+
this._emoji = emoji;
|
|
1864
|
+
return this;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* URL for `link` style buttons.
|
|
1868
|
+
* Sets the style to `'link'` automatically.
|
|
1869
|
+
*/
|
|
1870
|
+
setUrl(url) {
|
|
1871
|
+
this._url = url;
|
|
1872
|
+
this._style = "link";
|
|
1873
|
+
return this;
|
|
1874
|
+
}
|
|
1875
|
+
/** Prevent users from clicking the button. */
|
|
1876
|
+
setDisabled(disabled = true) {
|
|
1877
|
+
this._disabled = disabled;
|
|
1878
|
+
return this;
|
|
1879
|
+
}
|
|
1880
|
+
toJSON() {
|
|
1881
|
+
return {
|
|
1882
|
+
type: "button",
|
|
1883
|
+
customId: this._customId,
|
|
1884
|
+
label: this._label || void 0,
|
|
1885
|
+
style: this._style,
|
|
1886
|
+
emoji: this._emoji,
|
|
1887
|
+
url: this._url,
|
|
1888
|
+
disabled: this._disabled || void 0
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
// src/builders/SelectMenuBuilder.ts
|
|
1894
|
+
var SelectMenuBuilder = class {
|
|
1895
|
+
constructor() {
|
|
1896
|
+
this._customId = "";
|
|
1897
|
+
this._disabled = false;
|
|
1898
|
+
this._options = [];
|
|
1899
|
+
}
|
|
1900
|
+
/** Custom ID returned in the `SELECT_MENU` interaction. */
|
|
1901
|
+
setCustomId(customId) {
|
|
1902
|
+
this._customId = customId;
|
|
1903
|
+
return this;
|
|
1904
|
+
}
|
|
1905
|
+
/** Greyed-out hint shown when nothing is selected. */
|
|
1906
|
+
setPlaceholder(placeholder) {
|
|
1907
|
+
this._placeholder = placeholder;
|
|
1908
|
+
return this;
|
|
1909
|
+
}
|
|
1015
1910
|
/** Prevent users from interacting with the menu. */
|
|
1016
1911
|
setDisabled(disabled = true) {
|
|
1017
1912
|
this._disabled = disabled;
|
|
1018
1913
|
return this;
|
|
1019
1914
|
}
|
|
1020
|
-
/** Add a single option. */
|
|
1915
|
+
/** Add a single option. */
|
|
1916
|
+
addOption(option) {
|
|
1917
|
+
this._options.push(option);
|
|
1918
|
+
return this;
|
|
1919
|
+
}
|
|
1920
|
+
/** Add multiple options at once. */
|
|
1921
|
+
addOptions(...options) {
|
|
1922
|
+
this._options.push(...options);
|
|
1923
|
+
return this;
|
|
1924
|
+
}
|
|
1925
|
+
/** Replace all options. */
|
|
1926
|
+
setOptions(options) {
|
|
1927
|
+
this._options.splice(0, this._options.length, ...options);
|
|
1928
|
+
return this;
|
|
1929
|
+
}
|
|
1930
|
+
toJSON() {
|
|
1931
|
+
return {
|
|
1932
|
+
type: "select",
|
|
1933
|
+
customId: this._customId,
|
|
1934
|
+
placeholder: this._placeholder,
|
|
1935
|
+
disabled: this._disabled || void 0,
|
|
1936
|
+
options: [...this._options]
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
|
|
1941
|
+
// src/builders/ActionRowBuilder.ts
|
|
1942
|
+
var ActionRowBuilder = class {
|
|
1943
|
+
constructor() {
|
|
1944
|
+
this._components = [];
|
|
1945
|
+
}
|
|
1946
|
+
/** Add a single component (button or select menu). */
|
|
1947
|
+
addComponent(component) {
|
|
1948
|
+
this._components.push(component);
|
|
1949
|
+
return this;
|
|
1950
|
+
}
|
|
1951
|
+
/** Add multiple components at once. */
|
|
1952
|
+
addComponents(...components) {
|
|
1953
|
+
this._components.push(...components);
|
|
1954
|
+
return this;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Serialise to a flat `MessageComponent[]` array — the format accepted by all API calls.
|
|
1958
|
+
*/
|
|
1959
|
+
toJSON() {
|
|
1960
|
+
return this._components.map((c) => c.toJSON());
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
// src/builders/ModalBuilder.ts
|
|
1965
|
+
var ModalBuilder = class {
|
|
1966
|
+
constructor() {
|
|
1967
|
+
this._title = "";
|
|
1968
|
+
this._customId = "";
|
|
1969
|
+
this._fields = [];
|
|
1970
|
+
}
|
|
1971
|
+
/** Title shown at the top of the modal dialog. */
|
|
1972
|
+
setTitle(title) {
|
|
1973
|
+
this._title = title;
|
|
1974
|
+
return this;
|
|
1975
|
+
}
|
|
1976
|
+
/** Custom ID passed back with the `MODAL_SUBMIT` interaction. */
|
|
1977
|
+
setCustomId(customId) {
|
|
1978
|
+
this._customId = customId;
|
|
1979
|
+
return this;
|
|
1980
|
+
}
|
|
1981
|
+
/** Add a single text-input field. */
|
|
1982
|
+
addField(field) {
|
|
1983
|
+
this._fields.push("toJSON" in field ? field.toJSON() : field);
|
|
1984
|
+
return this;
|
|
1985
|
+
}
|
|
1986
|
+
/** Add multiple fields at once. */
|
|
1987
|
+
addFields(...fields) {
|
|
1988
|
+
for (const f of fields) this.addField(f);
|
|
1989
|
+
return this;
|
|
1990
|
+
}
|
|
1991
|
+
toJSON() {
|
|
1992
|
+
if (!this._title) throw new Error("ModalBuilder: title is required \u2014 call .setTitle()");
|
|
1993
|
+
if (!this._customId) throw new Error("ModalBuilder: customId is required \u2014 call .setCustomId()");
|
|
1994
|
+
if (this._fields.length === 0) throw new Error("ModalBuilder: at least one field is required \u2014 call .addField()");
|
|
1995
|
+
return { title: this._title, customId: this._customId, fields: [...this._fields] };
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
// src/builders/TextInputBuilder.ts
|
|
2000
|
+
var TextInputBuilder = class {
|
|
2001
|
+
constructor() {
|
|
2002
|
+
this._data = {
|
|
2003
|
+
customId: "",
|
|
2004
|
+
label: "",
|
|
2005
|
+
type: "short"
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
/** Unique ID for this field — the key in `interaction.modalData` on submit. */
|
|
2009
|
+
setCustomId(customId) {
|
|
2010
|
+
this._data.customId = customId;
|
|
2011
|
+
return this;
|
|
2012
|
+
}
|
|
2013
|
+
/** Label shown above the input inside the modal. */
|
|
2014
|
+
setLabel(label) {
|
|
2015
|
+
this._data.label = label;
|
|
2016
|
+
return this;
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Input style:
|
|
2020
|
+
* - `'short'` — single-line text input
|
|
2021
|
+
* - `'paragraph'` — multi-line textarea
|
|
2022
|
+
*/
|
|
2023
|
+
setStyle(style) {
|
|
2024
|
+
this._data.type = style;
|
|
2025
|
+
return this;
|
|
2026
|
+
}
|
|
2027
|
+
/** Greyed-out hint text shown when the field is empty. */
|
|
2028
|
+
setPlaceholder(placeholder) {
|
|
2029
|
+
this._data.placeholder = placeholder;
|
|
2030
|
+
return this;
|
|
2031
|
+
}
|
|
2032
|
+
/** Whether the user must fill in this field before submitting. */
|
|
2033
|
+
setRequired(required = true) {
|
|
2034
|
+
this._data.required = required;
|
|
2035
|
+
return this;
|
|
2036
|
+
}
|
|
2037
|
+
/** Minimum number of characters required. */
|
|
2038
|
+
setMinLength(min) {
|
|
2039
|
+
this._data.minLength = min;
|
|
2040
|
+
return this;
|
|
2041
|
+
}
|
|
2042
|
+
/** Maximum number of characters allowed. */
|
|
2043
|
+
setMaxLength(max) {
|
|
2044
|
+
this._data.maxLength = max;
|
|
2045
|
+
return this;
|
|
2046
|
+
}
|
|
2047
|
+
/** Pre-filled default value. */
|
|
2048
|
+
setValue(value) {
|
|
2049
|
+
this._data.value = value;
|
|
2050
|
+
return this;
|
|
2051
|
+
}
|
|
2052
|
+
toJSON() {
|
|
2053
|
+
return { ...this._data };
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
// src/builders/SlashCommandBuilder.ts
|
|
2058
|
+
var SlashCommandOptionBuilder = class {
|
|
2059
|
+
constructor(type) {
|
|
2060
|
+
this._data = { name: "", description: "", type };
|
|
2061
|
+
}
|
|
2062
|
+
/** Internal option name (lowercase, no spaces). Shown after `/command ` in the client UI. */
|
|
2063
|
+
setName(name) {
|
|
2064
|
+
this._data.name = name;
|
|
2065
|
+
return this;
|
|
2066
|
+
}
|
|
2067
|
+
/** Short human-readable description shown in the command picker. */
|
|
2068
|
+
setDescription(description) {
|
|
2069
|
+
this._data.description = description;
|
|
2070
|
+
return this;
|
|
2071
|
+
}
|
|
2072
|
+
/** Whether users must supply this option before sending the command. */
|
|
2073
|
+
setRequired(required = true) {
|
|
2074
|
+
this._data.required = required;
|
|
2075
|
+
return this;
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Restrict the option to specific values.
|
|
2079
|
+
* The picker will show these as autocomplete suggestions.
|
|
2080
|
+
*/
|
|
2081
|
+
addChoice(name, value) {
|
|
2082
|
+
if (!this._data.choices) this._data.choices = [];
|
|
2083
|
+
this._data.choices.push({ name, value });
|
|
2084
|
+
return this;
|
|
2085
|
+
}
|
|
2086
|
+
/** Set all allowed choices at once. */
|
|
2087
|
+
setChoices(choices) {
|
|
2088
|
+
this._data.choices = [...choices];
|
|
2089
|
+
return this;
|
|
2090
|
+
}
|
|
2091
|
+
toJSON() {
|
|
2092
|
+
return { ...this._data };
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
var SlashCommandBuilder = class {
|
|
2096
|
+
constructor() {
|
|
2097
|
+
this._data = {
|
|
2098
|
+
name: "",
|
|
2099
|
+
description: "",
|
|
2100
|
+
options: []
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
/** Command name — lowercase, no spaces (e.g. `'ban'`, `'server-info'`). */
|
|
2104
|
+
setName(name) {
|
|
2105
|
+
this._data.name = name;
|
|
2106
|
+
return this;
|
|
2107
|
+
}
|
|
2108
|
+
/** Short description shown in the command picker UI. */
|
|
2109
|
+
setDescription(description) {
|
|
2110
|
+
this._data.description = description;
|
|
2111
|
+
return this;
|
|
2112
|
+
}
|
|
2113
|
+
/** Add a text (string) option. */
|
|
2114
|
+
addStringOption(fn) {
|
|
2115
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("STRING")).toJSON());
|
|
2116
|
+
return this;
|
|
2117
|
+
}
|
|
2118
|
+
/** Add an integer (whole number) option. */
|
|
2119
|
+
addIntegerOption(fn) {
|
|
2120
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("INTEGER")).toJSON());
|
|
2121
|
+
return this;
|
|
2122
|
+
}
|
|
2123
|
+
/** Add a boolean (true/false) option. */
|
|
2124
|
+
addBooleanOption(fn) {
|
|
2125
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("BOOLEAN")).toJSON());
|
|
2126
|
+
return this;
|
|
2127
|
+
}
|
|
2128
|
+
/** Add a user-mention option (returns a user ID string). */
|
|
2129
|
+
addUserOption(fn) {
|
|
2130
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("USER")).toJSON());
|
|
2131
|
+
return this;
|
|
2132
|
+
}
|
|
2133
|
+
/** Add a channel-mention option (returns a channel ID string). */
|
|
2134
|
+
addChannelOption(fn) {
|
|
2135
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("CHANNEL")).toJSON());
|
|
2136
|
+
return this;
|
|
2137
|
+
}
|
|
2138
|
+
/** Add a role-mention option (returns a role ID string). */
|
|
2139
|
+
addRoleOption(fn) {
|
|
2140
|
+
this._data.options.push(fn(new SlashCommandOptionBuilder("ROLE")).toJSON());
|
|
2141
|
+
return this;
|
|
2142
|
+
}
|
|
2143
|
+
toJSON() {
|
|
2144
|
+
if (!this._data.name) throw new Error("SlashCommandBuilder: name is required \u2014 call .setName()");
|
|
2145
|
+
if (!this._data.description) throw new Error("SlashCommandBuilder: description is required \u2014 call .setDescription()");
|
|
2146
|
+
return { ...this._data, options: [...this._data.options ?? []] };
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
// src/builders/PollBuilder.ts
|
|
2151
|
+
var PollBuilder = class {
|
|
2152
|
+
constructor() {
|
|
2153
|
+
this._question = "";
|
|
2154
|
+
this._options = [];
|
|
2155
|
+
this._allowMultiple = false;
|
|
2156
|
+
this._duration = null;
|
|
2157
|
+
this._anonymous = false;
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Set the poll question.
|
|
2161
|
+
*
|
|
2162
|
+
* @example
|
|
2163
|
+
* builder.setQuestion('Best programming language?')
|
|
2164
|
+
*/
|
|
2165
|
+
setQuestion(question) {
|
|
2166
|
+
this._question = question;
|
|
2167
|
+
return this;
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Add a single answer option.
|
|
2171
|
+
*
|
|
2172
|
+
* @example
|
|
2173
|
+
* builder.addOption({ label: 'TypeScript', emoji: '🔷' })
|
|
2174
|
+
*/
|
|
1021
2175
|
addOption(option) {
|
|
2176
|
+
if (this._options.length >= 10) throw new Error("Polls can have at most 10 options");
|
|
1022
2177
|
this._options.push(option);
|
|
1023
2178
|
return this;
|
|
1024
2179
|
}
|
|
1025
|
-
/**
|
|
1026
|
-
|
|
1027
|
-
|
|
2180
|
+
/**
|
|
2181
|
+
* Add multiple answer options at once.
|
|
2182
|
+
*
|
|
2183
|
+
* @example
|
|
2184
|
+
* builder.addOptions([
|
|
2185
|
+
* { label: 'Yes', emoji: '✅' },
|
|
2186
|
+
* { label: 'No', emoji: '❌' },
|
|
2187
|
+
* ])
|
|
2188
|
+
*/
|
|
2189
|
+
addOptions(options) {
|
|
2190
|
+
for (const o of options) this.addOption(o);
|
|
1028
2191
|
return this;
|
|
1029
2192
|
}
|
|
1030
|
-
/**
|
|
1031
|
-
|
|
1032
|
-
|
|
2193
|
+
/**
|
|
2194
|
+
* Allow users to select more than one option.
|
|
2195
|
+
* Default: `false` (single choice).
|
|
2196
|
+
*/
|
|
2197
|
+
setAllowMultiple(yes = true) {
|
|
2198
|
+
this._allowMultiple = yes;
|
|
2199
|
+
return this;
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Set the poll duration in **seconds**. Pass `null` to make it permanent.
|
|
2203
|
+
*
|
|
2204
|
+
* @example
|
|
2205
|
+
* builder.setDuration(60 * 60 * 24) // expires in 24 hours
|
|
2206
|
+
* builder.setDuration(null) // no expiry
|
|
2207
|
+
*/
|
|
2208
|
+
setDuration(seconds) {
|
|
2209
|
+
this._duration = seconds;
|
|
2210
|
+
return this;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Hide which users voted for which option.
|
|
2214
|
+
* Default: `false` (votes are public).
|
|
2215
|
+
*/
|
|
2216
|
+
setAnonymous(yes = true) {
|
|
2217
|
+
this._anonymous = yes;
|
|
1033
2218
|
return this;
|
|
1034
2219
|
}
|
|
2220
|
+
/** Build the poll definition object. */
|
|
1035
2221
|
toJSON() {
|
|
2222
|
+
if (!this._question.trim()) throw new Error("PollBuilder: question is required");
|
|
2223
|
+
if (this._options.length < 2) throw new Error("PollBuilder: at least 2 options are required");
|
|
1036
2224
|
return {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
2225
|
+
question: this._question,
|
|
2226
|
+
options: this._options,
|
|
2227
|
+
allowMultiple: this._allowMultiple,
|
|
2228
|
+
duration: this._duration,
|
|
2229
|
+
anonymous: this._anonymous
|
|
1042
2230
|
};
|
|
1043
2231
|
}
|
|
1044
2232
|
};
|
|
1045
2233
|
|
|
1046
|
-
// src/builders/
|
|
1047
|
-
var
|
|
2234
|
+
// src/builders/MessageBuilder.ts
|
|
2235
|
+
var MessageBuilder = class {
|
|
1048
2236
|
constructor() {
|
|
1049
2237
|
this._components = [];
|
|
1050
2238
|
}
|
|
1051
|
-
/**
|
|
1052
|
-
|
|
1053
|
-
|
|
2239
|
+
/**
|
|
2240
|
+
* Set the text content of the message.
|
|
2241
|
+
*
|
|
2242
|
+
* @example
|
|
2243
|
+
* builder.setContent('Hello world!')
|
|
2244
|
+
*/
|
|
2245
|
+
setContent(content) {
|
|
2246
|
+
this._content = content;
|
|
1054
2247
|
return this;
|
|
1055
2248
|
}
|
|
1056
|
-
/**
|
|
1057
|
-
|
|
2249
|
+
/**
|
|
2250
|
+
* Attach an embed. Accepts an `EmbedBuilder` or raw `Embed` object.
|
|
2251
|
+
*
|
|
2252
|
+
* @example
|
|
2253
|
+
* builder.setEmbed(new EmbedBuilder().setTitle('Result'))
|
|
2254
|
+
* builder.setEmbed({ title: 'Result', color: '#57F287' })
|
|
2255
|
+
*/
|
|
2256
|
+
setEmbed(embed) {
|
|
2257
|
+
this._embed = "toJSON" in embed && typeof embed.toJSON === "function" ? embed.toJSON() : embed;
|
|
2258
|
+
return this;
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Remove any embed from this message.
|
|
2262
|
+
*/
|
|
2263
|
+
clearEmbed() {
|
|
2264
|
+
this._embed = void 0;
|
|
2265
|
+
return this;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Add an `ActionRowBuilder` (or raw component array) as a component row.
|
|
2269
|
+
*
|
|
2270
|
+
* @example
|
|
2271
|
+
* builder.addRow(
|
|
2272
|
+
* new ActionRowBuilder().addComponent(btn1).addComponent(btn2)
|
|
2273
|
+
* )
|
|
2274
|
+
*/
|
|
2275
|
+
addRow(row) {
|
|
2276
|
+
const components = Array.isArray(row) ? row : row.toJSON();
|
|
1058
2277
|
this._components.push(...components);
|
|
1059
2278
|
return this;
|
|
1060
2279
|
}
|
|
1061
2280
|
/**
|
|
1062
|
-
*
|
|
2281
|
+
* Replace all existing component rows.
|
|
2282
|
+
*/
|
|
2283
|
+
setComponents(components) {
|
|
2284
|
+
this._components = components;
|
|
2285
|
+
return this;
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Set the message this is replying to.
|
|
2289
|
+
*
|
|
2290
|
+
* @example
|
|
2291
|
+
* builder.setReplyTo(message.id)
|
|
2292
|
+
*/
|
|
2293
|
+
setReplyTo(messageId) {
|
|
2294
|
+
this._replyToId = messageId;
|
|
2295
|
+
return this;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Attach a poll to the message.
|
|
2299
|
+
* Accepts a `PollBuilder` or raw `PollDefinition`.
|
|
2300
|
+
*
|
|
2301
|
+
* @example
|
|
2302
|
+
* builder.setPoll(
|
|
2303
|
+
* new PollBuilder()
|
|
2304
|
+
* .setQuestion('Favourite language?')
|
|
2305
|
+
* .addOptions([{ label: 'TypeScript' }, { label: 'Python' }])
|
|
2306
|
+
* )
|
|
2307
|
+
*/
|
|
2308
|
+
setPoll(poll) {
|
|
2309
|
+
this._poll = "toJSON" in poll && typeof poll.toJSON === "function" ? poll.toJSON() : poll;
|
|
2310
|
+
return this;
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Build the message options object, ready to pass to `client.messages.send()`.
|
|
2314
|
+
*
|
|
2315
|
+
* @throws if neither `content` nor `embed` nor `poll` is set.
|
|
2316
|
+
*/
|
|
2317
|
+
toJSON() {
|
|
2318
|
+
if (!this._content && !this._embed && !this._poll) {
|
|
2319
|
+
throw new Error("MessageBuilder: message must have content, an embed, or a poll");
|
|
2320
|
+
}
|
|
2321
|
+
return {
|
|
2322
|
+
...this._content !== void 0 ? { content: this._content } : {},
|
|
2323
|
+
...this._embed !== void 0 ? { embed: this._embed } : {},
|
|
2324
|
+
...this._components.length > 0 ? { components: this._components } : {},
|
|
2325
|
+
...this._replyToId !== void 0 ? { replyToId: this._replyToId } : {},
|
|
2326
|
+
...this._poll !== void 0 ? { poll: this._poll } : {}
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
// src/utils/Collection.ts
|
|
2332
|
+
var Collection = class _Collection extends Map {
|
|
2333
|
+
/**
|
|
2334
|
+
* The first value stored (insertion order), or `undefined` if empty.
|
|
2335
|
+
*/
|
|
2336
|
+
first() {
|
|
2337
|
+
return this.values().next().value;
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* The first `n` values stored (insertion order).
|
|
2341
|
+
*/
|
|
2342
|
+
firstN(n) {
|
|
2343
|
+
const result = [];
|
|
2344
|
+
for (const v of this.values()) {
|
|
2345
|
+
if (result.length >= n) break;
|
|
2346
|
+
result.push(v);
|
|
2347
|
+
}
|
|
2348
|
+
return result;
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* The last value stored, or `undefined` if empty.
|
|
2352
|
+
*/
|
|
2353
|
+
last() {
|
|
2354
|
+
let last;
|
|
2355
|
+
for (const v of this.values()) last = v;
|
|
2356
|
+
return last;
|
|
2357
|
+
}
|
|
2358
|
+
/**
|
|
2359
|
+
* The last `n` values stored (insertion order).
|
|
2360
|
+
*/
|
|
2361
|
+
lastN(n) {
|
|
2362
|
+
return this.toArray().slice(-n);
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2365
|
+
* Returns a random value from the collection, or `undefined` if empty.
|
|
2366
|
+
*/
|
|
2367
|
+
random() {
|
|
2368
|
+
const arr = this.toArray();
|
|
2369
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* Find the first value that passes the predicate.
|
|
2373
|
+
*
|
|
2374
|
+
* @example
|
|
2375
|
+
* const bot = members.find(m => m.user.isBot)
|
|
2376
|
+
*/
|
|
2377
|
+
find(fn) {
|
|
2378
|
+
for (const [k, v] of this) {
|
|
2379
|
+
if (fn(v, k, this)) return v;
|
|
2380
|
+
}
|
|
2381
|
+
return void 0;
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Find the key of the first value that passes the predicate.
|
|
2385
|
+
*/
|
|
2386
|
+
findKey(fn) {
|
|
2387
|
+
for (const [k, v] of this) {
|
|
2388
|
+
if (fn(v, k, this)) return k;
|
|
2389
|
+
}
|
|
2390
|
+
return void 0;
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Returns `true` if at least one value satisfies the predicate.
|
|
2394
|
+
*/
|
|
2395
|
+
some(fn) {
|
|
2396
|
+
for (const [k, v] of this) {
|
|
2397
|
+
if (fn(v, k, this)) return true;
|
|
2398
|
+
}
|
|
2399
|
+
return false;
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Returns `true` if every value satisfies the predicate.
|
|
2403
|
+
*/
|
|
2404
|
+
every(fn) {
|
|
2405
|
+
for (const [k, v] of this) {
|
|
2406
|
+
if (!fn(v, k, this)) return false;
|
|
2407
|
+
}
|
|
2408
|
+
return true;
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Filter to a new Collection containing only values that pass the predicate.
|
|
2412
|
+
*
|
|
2413
|
+
* @example
|
|
2414
|
+
* const admins = members.filter(m => m.role === 'ADMIN')
|
|
2415
|
+
*/
|
|
2416
|
+
filter(fn) {
|
|
2417
|
+
const result = new _Collection();
|
|
2418
|
+
for (const [k, v] of this) {
|
|
2419
|
+
if (fn(v, k, this)) result.set(k, v);
|
|
2420
|
+
}
|
|
2421
|
+
return result;
|
|
2422
|
+
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Map each value to a new array.
|
|
2425
|
+
*
|
|
2426
|
+
* @example
|
|
2427
|
+
* const names = members.map(m => m.user.username)
|
|
2428
|
+
*/
|
|
2429
|
+
map(fn) {
|
|
2430
|
+
const result = [];
|
|
2431
|
+
for (const [k, v] of this) result.push(fn(v, k, this));
|
|
2432
|
+
return result;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Map to a new Collection with transformed values.
|
|
2436
|
+
*
|
|
2437
|
+
* @example
|
|
2438
|
+
* const names = channels.mapValues(c => c.name.toUpperCase())
|
|
2439
|
+
*/
|
|
2440
|
+
mapValues(fn) {
|
|
2441
|
+
const result = new _Collection();
|
|
2442
|
+
for (const [k, v] of this) result.set(k, fn(v, k, this));
|
|
2443
|
+
return result;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Reduce the collection to a single value.
|
|
2447
|
+
*
|
|
2448
|
+
* @example
|
|
2449
|
+
* const totalReactions = messages.reduce((sum, m) => sum + m.reactions.length, 0)
|
|
2450
|
+
*/
|
|
2451
|
+
reduce(fn, initial) {
|
|
2452
|
+
let acc = initial;
|
|
2453
|
+
for (const [k, v] of this) acc = fn(acc, v, k, this);
|
|
2454
|
+
return acc;
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Returns values as an array (insertion order).
|
|
2458
|
+
*/
|
|
2459
|
+
toArray() {
|
|
2460
|
+
return [...this.values()];
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Returns keys as an array (insertion order).
|
|
2464
|
+
*/
|
|
2465
|
+
keyArray() {
|
|
2466
|
+
return [...this.keys()];
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Sort and return a new Collection.
|
|
2470
|
+
* Callback works like `Array.prototype.sort`.
|
|
2471
|
+
*
|
|
2472
|
+
* @example
|
|
2473
|
+
* const sorted = channels.sort((a, b) => a.position - b.position)
|
|
2474
|
+
*/
|
|
2475
|
+
sort(fn) {
|
|
2476
|
+
const entries = [...this.entries()].sort(
|
|
2477
|
+
([ak, av], [bk, bv]) => fn ? fn(av, bv, ak, bk) : 0
|
|
2478
|
+
);
|
|
2479
|
+
return new _Collection(entries);
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Split into two Collections based on a predicate.
|
|
2483
|
+
* Returns `[passing, failing]`.
|
|
2484
|
+
*
|
|
2485
|
+
* @example
|
|
2486
|
+
* const [bots, humans] = members.partition(m => m.user.isBot)
|
|
2487
|
+
*/
|
|
2488
|
+
partition(fn) {
|
|
2489
|
+
const pass = new _Collection();
|
|
2490
|
+
const fail = new _Collection();
|
|
2491
|
+
for (const [k, v] of this) {
|
|
2492
|
+
if (fn(v, k, this)) pass.set(k, v);
|
|
2493
|
+
else fail.set(k, v);
|
|
2494
|
+
}
|
|
2495
|
+
return [pass, fail];
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* Merge this collection with one or more others.
|
|
2499
|
+
* Later collections overwrite duplicate keys.
|
|
2500
|
+
*/
|
|
2501
|
+
merge(...others) {
|
|
2502
|
+
const result = new _Collection(this);
|
|
2503
|
+
for (const other of others) {
|
|
2504
|
+
for (const [k, v] of other) result.set(k, v);
|
|
2505
|
+
}
|
|
2506
|
+
return result;
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Serialize to a plain `Record` (requires string keys).
|
|
2510
|
+
*/
|
|
2511
|
+
toJSON() {
|
|
2512
|
+
const obj = {};
|
|
2513
|
+
for (const [k, v] of this) obj[String(k)] = v;
|
|
2514
|
+
return obj;
|
|
2515
|
+
}
|
|
2516
|
+
toString() {
|
|
2517
|
+
return `Collection(${this.size})`;
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// src/utils/Cooldown.ts
|
|
2522
|
+
var Cooldown = class {
|
|
2523
|
+
/**
|
|
2524
|
+
* @param durationMs - Default cooldown duration in milliseconds.
|
|
2525
|
+
*/
|
|
2526
|
+
constructor(durationMs) {
|
|
2527
|
+
this._durations = /* @__PURE__ */ new Map();
|
|
2528
|
+
this._last = /* @__PURE__ */ new Map();
|
|
2529
|
+
this._defaultMs = durationMs;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Check remaining cooldown for a user.
|
|
2533
|
+
* Returns `0` if they are not on cooldown (free to proceed),
|
|
2534
|
+
* or the **milliseconds remaining** if they are on cooldown.
|
|
2535
|
+
*
|
|
2536
|
+
* @example
|
|
2537
|
+
* const ms = cooldown.check(userId)
|
|
2538
|
+
* if (ms > 0) return interaction.replyEphemeral(`Wait ${Cooldown.format(ms)}!`)
|
|
2539
|
+
*/
|
|
2540
|
+
check(userId) {
|
|
2541
|
+
const last = this._last.get(userId);
|
|
2542
|
+
if (last === void 0) return 0;
|
|
2543
|
+
const duration = this._durations.get(userId) ?? this._defaultMs;
|
|
2544
|
+
const remaining = duration - (Date.now() - last);
|
|
2545
|
+
return remaining > 0 ? remaining : 0;
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Mark a user as having just used the command, starting their cooldown.
|
|
2549
|
+
* Optionally override the cooldown duration for this specific user.
|
|
2550
|
+
*
|
|
2551
|
+
* @example
|
|
2552
|
+
* cooldown.use(userId) // uses default duration
|
|
2553
|
+
* cooldown.use(userId, 10_000) // 10 second cooldown for this use
|
|
2554
|
+
*/
|
|
2555
|
+
use(userId, durationMs) {
|
|
2556
|
+
this._last.set(userId, Date.now());
|
|
2557
|
+
if (durationMs !== void 0) this._durations.set(userId, durationMs);
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Immediately reset (clear) the cooldown for a user.
|
|
2561
|
+
*
|
|
2562
|
+
* @example
|
|
2563
|
+
* cooldown.reset(userId) // user can use the command again immediately
|
|
2564
|
+
*/
|
|
2565
|
+
reset(userId) {
|
|
2566
|
+
this._last.delete(userId);
|
|
2567
|
+
this._durations.delete(userId);
|
|
2568
|
+
}
|
|
2569
|
+
/**
|
|
2570
|
+
* Reset all active cooldowns.
|
|
2571
|
+
*/
|
|
2572
|
+
resetAll() {
|
|
2573
|
+
this._last.clear();
|
|
2574
|
+
this._durations.clear();
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Returns a list of all users currently on cooldown.
|
|
2578
|
+
*
|
|
2579
|
+
* @example
|
|
2580
|
+
* const active = cooldown.activeCooldowns()
|
|
2581
|
+
* // [{ userId: 'abc', remainingMs: 3200 }, ...]
|
|
2582
|
+
*/
|
|
2583
|
+
activeCooldowns() {
|
|
2584
|
+
const result = [];
|
|
2585
|
+
for (const [userId] of this._last) {
|
|
2586
|
+
const ms = this.check(userId);
|
|
2587
|
+
if (ms > 0) result.push({ userId, remainingMs: ms });
|
|
2588
|
+
}
|
|
2589
|
+
return result;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Format milliseconds into a human-readable string.
|
|
2593
|
+
*
|
|
2594
|
+
* @example
|
|
2595
|
+
* Cooldown.format(3_600_000) // '1h 0m 0s'
|
|
2596
|
+
* Cooldown.format(90_000) // '1m 30s'
|
|
2597
|
+
* Cooldown.format(4_000) // '4s'
|
|
1063
2598
|
*/
|
|
1064
|
-
|
|
1065
|
-
|
|
2599
|
+
static format(ms) {
|
|
2600
|
+
const totalSecs = Math.ceil(ms / 1e3);
|
|
2601
|
+
const hours = Math.floor(totalSecs / 3600);
|
|
2602
|
+
const mins = Math.floor(totalSecs % 3600 / 60);
|
|
2603
|
+
const secs = totalSecs % 60;
|
|
2604
|
+
if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
|
|
2605
|
+
if (mins > 0) return `${mins}m ${secs}s`;
|
|
2606
|
+
return `${secs}s`;
|
|
1066
2607
|
}
|
|
1067
2608
|
};
|
|
1068
|
-
|
|
1069
|
-
// src/builders/ModalBuilder.ts
|
|
1070
|
-
var ModalBuilder = class {
|
|
2609
|
+
var CooldownManager = class {
|
|
1071
2610
|
constructor() {
|
|
1072
|
-
this.
|
|
1073
|
-
this._customId = "";
|
|
1074
|
-
this._fields = [];
|
|
2611
|
+
this._map = /* @__PURE__ */ new Map();
|
|
1075
2612
|
}
|
|
1076
|
-
/**
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
2613
|
+
/**
|
|
2614
|
+
* Get or create a named cooldown.
|
|
2615
|
+
*/
|
|
2616
|
+
get(name, durationMs = 3e3) {
|
|
2617
|
+
if (!this._map.has(name)) this._map.set(name, new Cooldown(durationMs));
|
|
2618
|
+
return this._map.get(name);
|
|
1080
2619
|
}
|
|
1081
|
-
/**
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
2620
|
+
/**
|
|
2621
|
+
* Check a named cooldown for a user.
|
|
2622
|
+
* Creates the cooldown if it doesn't exist yet.
|
|
2623
|
+
* Returns `0` if free, or remaining ms if on cooldown.
|
|
2624
|
+
*
|
|
2625
|
+
* @example
|
|
2626
|
+
* const ms = cooldowns.check('ban', userId, 30_000)
|
|
2627
|
+
*/
|
|
2628
|
+
check(name, userId, durationMs = 3e3) {
|
|
2629
|
+
return this.get(name, durationMs).check(userId);
|
|
1085
2630
|
}
|
|
1086
|
-
/**
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
2631
|
+
/**
|
|
2632
|
+
* Mark a user as having used a named command.
|
|
2633
|
+
*
|
|
2634
|
+
* @example
|
|
2635
|
+
* cooldowns.use('ban', userId, 30_000)
|
|
2636
|
+
*/
|
|
2637
|
+
use(name, userId, durationMs) {
|
|
2638
|
+
this.get(name, durationMs).use(userId, durationMs);
|
|
1090
2639
|
}
|
|
1091
|
-
/**
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
2640
|
+
/**
|
|
2641
|
+
* Reset a user's cooldown on a named command.
|
|
2642
|
+
*/
|
|
2643
|
+
reset(name, userId) {
|
|
2644
|
+
this._map.get(name)?.reset(userId);
|
|
1095
2645
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
2646
|
+
/**
|
|
2647
|
+
* Reset all cooldowns for all commands.
|
|
2648
|
+
*/
|
|
2649
|
+
resetAll() {
|
|
2650
|
+
for (const c of this._map.values()) c.resetAll();
|
|
1101
2651
|
}
|
|
1102
2652
|
};
|
|
1103
2653
|
|
|
1104
|
-
// src/
|
|
1105
|
-
var
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
2654
|
+
// src/utils/Logger.ts
|
|
2655
|
+
var Logger = class {
|
|
2656
|
+
/**
|
|
2657
|
+
* @param name - Name shown in square brackets before every message.
|
|
2658
|
+
* @param enableDebug - When `false` (default), `.debug()` calls are silent.
|
|
2659
|
+
*/
|
|
2660
|
+
constructor(name, enableDebug = false) {
|
|
2661
|
+
this._prefix = name;
|
|
2662
|
+
this._debug = enableDebug;
|
|
1112
2663
|
}
|
|
1113
|
-
/**
|
|
1114
|
-
|
|
1115
|
-
this.
|
|
2664
|
+
/** Enable or disable debug output at runtime. */
|
|
2665
|
+
setDebug(enabled) {
|
|
2666
|
+
this._debug = enabled;
|
|
1116
2667
|
return this;
|
|
1117
2668
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
this._data.label = label;
|
|
1121
|
-
return this;
|
|
2669
|
+
_timestamp() {
|
|
2670
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
1122
2671
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
*/
|
|
1128
|
-
setStyle(style) {
|
|
1129
|
-
this._data.type = style;
|
|
1130
|
-
return this;
|
|
2672
|
+
_fmt(level, color, ...args) {
|
|
2673
|
+
const ts = this._timestamp();
|
|
2674
|
+
const tag = `\x1B[90m${ts}\x1B[0m ${color}[${level}]\x1B[0m \x1B[36m[${this._prefix}]\x1B[0m`;
|
|
2675
|
+
return `${tag} ${args.map((a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)).join(" ")}`;
|
|
1131
2676
|
}
|
|
1132
|
-
/**
|
|
1133
|
-
|
|
1134
|
-
this.
|
|
1135
|
-
return this;
|
|
2677
|
+
/** General info message. */
|
|
2678
|
+
info(...args) {
|
|
2679
|
+
console.log(this._fmt("INFO ", "\x1B[34m", ...args));
|
|
1136
2680
|
}
|
|
1137
|
-
/**
|
|
1138
|
-
|
|
1139
|
-
this.
|
|
1140
|
-
return this;
|
|
2681
|
+
/** Warning — something unexpected but non-fatal. */
|
|
2682
|
+
warn(...args) {
|
|
2683
|
+
console.warn(this._fmt("WARN ", "\x1B[33m", ...args));
|
|
1141
2684
|
}
|
|
1142
|
-
/**
|
|
1143
|
-
|
|
1144
|
-
this.
|
|
1145
|
-
return this;
|
|
2685
|
+
/** Error — something went wrong. */
|
|
2686
|
+
error(...args) {
|
|
2687
|
+
console.error(this._fmt("ERROR", "\x1B[31m", ...args));
|
|
1146
2688
|
}
|
|
1147
|
-
/**
|
|
1148
|
-
|
|
1149
|
-
this.
|
|
1150
|
-
return this;
|
|
2689
|
+
/** Success / positive confirmation. */
|
|
2690
|
+
success(...args) {
|
|
2691
|
+
console.log(this._fmt("OK ", "\x1B[32m", ...args));
|
|
1151
2692
|
}
|
|
1152
|
-
/**
|
|
1153
|
-
|
|
1154
|
-
this.
|
|
1155
|
-
return this;
|
|
2693
|
+
/** Debug — only printed when `enableDebug` is true. */
|
|
2694
|
+
debug(...args) {
|
|
2695
|
+
if (this._debug) console.log(this._fmt("DEBUG", "\x1B[35m", ...args));
|
|
1156
2696
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
2697
|
+
/**
|
|
2698
|
+
* Log an incoming command interaction.
|
|
2699
|
+
*
|
|
2700
|
+
* @example
|
|
2701
|
+
* log.command('ping', interaction.userId)
|
|
2702
|
+
* // [MyBot] [CMD] /ping ← user:abc123
|
|
2703
|
+
*/
|
|
2704
|
+
command(name, userId) {
|
|
2705
|
+
console.log(this._fmt("CMD ", "\x1B[36m", `/${name} \x1B[90m\u2190 user:${userId}`));
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Log a gateway event.
|
|
2709
|
+
*
|
|
2710
|
+
* @example
|
|
2711
|
+
* log.event('messageCreate')
|
|
2712
|
+
*/
|
|
2713
|
+
event(type, extra = "") {
|
|
2714
|
+
console.log(this._fmt("EVT ", "\x1B[35m", type + (extra ? ` \x1B[90m${extra}` : "")));
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Log that a command handler threw an error.
|
|
2718
|
+
*
|
|
2719
|
+
* @example
|
|
2720
|
+
* log.commandError('ban', err)
|
|
2721
|
+
*/
|
|
2722
|
+
commandError(name, err) {
|
|
2723
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2724
|
+
console.error(this._fmt("ERROR", "\x1B[31m", `${name} threw: ${msg}`));
|
|
2725
|
+
if (err instanceof Error && err.stack) {
|
|
2726
|
+
console.error("\x1B[90m" + err.stack + "\x1B[0m");
|
|
2727
|
+
}
|
|
1159
2728
|
}
|
|
1160
2729
|
};
|
|
1161
2730
|
|
|
1162
|
-
// src/
|
|
1163
|
-
var
|
|
1164
|
-
constructor(
|
|
1165
|
-
this.
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
2731
|
+
// src/utils/Paginator.ts
|
|
2732
|
+
var Paginator = class {
|
|
2733
|
+
constructor(fetchFn) {
|
|
2734
|
+
this.fetchFn = fetchFn;
|
|
2735
|
+
this._cursor = null;
|
|
2736
|
+
this._done = false;
|
|
2737
|
+
this._totalFetched = 0;
|
|
2738
|
+
}
|
|
2739
|
+
/** Whether all pages have been consumed. */
|
|
2740
|
+
get done() {
|
|
2741
|
+
return this._done;
|
|
2742
|
+
}
|
|
2743
|
+
/** Total number of items fetched so far (across all pages). */
|
|
2744
|
+
get totalFetched() {
|
|
2745
|
+
return this._totalFetched;
|
|
1171
2746
|
}
|
|
1172
|
-
/**
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
2747
|
+
/**
|
|
2748
|
+
* Fetch the next page of results.
|
|
2749
|
+
* Returns an empty array when there are no more pages.
|
|
2750
|
+
*
|
|
2751
|
+
* @example
|
|
2752
|
+
* const page1 = await paginator.fetchPage()
|
|
2753
|
+
* const page2 = await paginator.fetchPage()
|
|
2754
|
+
*/
|
|
2755
|
+
async fetchPage() {
|
|
2756
|
+
if (this._done) return [];
|
|
2757
|
+
const { items, cursor } = await this.fetchFn(this._cursor);
|
|
2758
|
+
this._totalFetched += items.length;
|
|
2759
|
+
this._cursor = cursor;
|
|
2760
|
+
if (!cursor || items.length === 0) this._done = true;
|
|
2761
|
+
return items;
|
|
1176
2762
|
}
|
|
1177
|
-
/**
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
2763
|
+
/**
|
|
2764
|
+
* Fetch **all** remaining pages and return a flat array.
|
|
2765
|
+
*
|
|
2766
|
+
* ⚠️ Use with caution on very large collections.
|
|
2767
|
+
*
|
|
2768
|
+
* @example
|
|
2769
|
+
* const allMembers = await paginator.fetchAll()
|
|
2770
|
+
*/
|
|
2771
|
+
async fetchAll() {
|
|
2772
|
+
const all = [];
|
|
2773
|
+
while (!this._done) {
|
|
2774
|
+
const page = await this.fetchPage();
|
|
2775
|
+
all.push(...page);
|
|
2776
|
+
}
|
|
2777
|
+
return all;
|
|
1181
2778
|
}
|
|
1182
2779
|
/**
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
2780
|
+
* Fetch up to `n` items total (across however many pages are needed).
|
|
2781
|
+
*
|
|
2782
|
+
* @example
|
|
2783
|
+
* const first100 = await paginator.fetchN(100)
|
|
1185
2784
|
*/
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
this.
|
|
1189
|
-
|
|
2785
|
+
async fetchN(n) {
|
|
2786
|
+
const result = [];
|
|
2787
|
+
while (!this._done && result.length < n) {
|
|
2788
|
+
const page = await this.fetchPage();
|
|
2789
|
+
result.push(...page.slice(0, n - result.length));
|
|
2790
|
+
}
|
|
2791
|
+
return result;
|
|
1190
2792
|
}
|
|
1191
|
-
/**
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
2793
|
+
/**
|
|
2794
|
+
* Async-iterate over every item, one page at a time.
|
|
2795
|
+
*
|
|
2796
|
+
* @example
|
|
2797
|
+
* for await (const msg of paginator) {
|
|
2798
|
+
* process(msg)
|
|
2799
|
+
* }
|
|
2800
|
+
*/
|
|
2801
|
+
async *[Symbol.asyncIterator]() {
|
|
2802
|
+
while (!this._done) {
|
|
2803
|
+
const page = await this.fetchPage();
|
|
2804
|
+
for (const item of page) yield item;
|
|
2805
|
+
}
|
|
1195
2806
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
2807
|
+
/**
|
|
2808
|
+
* Reset the paginator so you can iterate from the beginning again.
|
|
2809
|
+
*
|
|
2810
|
+
* @example
|
|
2811
|
+
* await paginator.fetchAll()
|
|
2812
|
+
* paginator.reset()
|
|
2813
|
+
* const againFirst = await paginator.fetchPage()
|
|
2814
|
+
*/
|
|
2815
|
+
reset() {
|
|
2816
|
+
this._cursor = null;
|
|
2817
|
+
this._done = false;
|
|
2818
|
+
this._totalFetched = 0;
|
|
1198
2819
|
}
|
|
1199
2820
|
};
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
2821
|
+
|
|
2822
|
+
// src/utils/PermissionsBitfield.ts
|
|
2823
|
+
var Permissions = {
|
|
2824
|
+
/** Read messages in channels. */
|
|
2825
|
+
MESSAGES_READ: "messages.read",
|
|
2826
|
+
/** Send messages in channels. */
|
|
2827
|
+
MESSAGES_WRITE: "messages.write",
|
|
2828
|
+
/** Edit / delete any message (not just the bot's own). */
|
|
2829
|
+
MESSAGES_MANAGE: "messages.manage",
|
|
2830
|
+
/** Create, edit and delete channels. */
|
|
2831
|
+
CHANNELS_MANAGE: "channels.manage",
|
|
2832
|
+
/** Kick members from the server. */
|
|
2833
|
+
MEMBERS_KICK: "members.kick",
|
|
2834
|
+
/** Ban and unban members from the server. */
|
|
2835
|
+
MEMBERS_BAN: "members.ban",
|
|
2836
|
+
/** Assign and remove roles on members. */
|
|
2837
|
+
MEMBERS_ROLES: "members.roles",
|
|
2838
|
+
/** Edit server settings. */
|
|
2839
|
+
SERVERS_MANAGE: "servers.manage"
|
|
2840
|
+
};
|
|
2841
|
+
var _PermissionsBitfield = class _PermissionsBitfield {
|
|
2842
|
+
constructor(perms = {}) {
|
|
2843
|
+
this._perms = { ...perms };
|
|
1207
2844
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
2845
|
+
// ─── Checking ──────────────────────────────────────────────────────────────
|
|
2846
|
+
/**
|
|
2847
|
+
* Returns `true` if the given permission is explicitly granted.
|
|
2848
|
+
*
|
|
2849
|
+
* @example
|
|
2850
|
+
* if (!perms.has(Permissions.MESSAGES_WRITE)) {
|
|
2851
|
+
* throw new Error('Bot cannot write messages here.')
|
|
2852
|
+
* }
|
|
2853
|
+
*/
|
|
2854
|
+
has(permission) {
|
|
2855
|
+
return this._perms[permission] === true;
|
|
1212
2856
|
}
|
|
1213
|
-
/**
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
2857
|
+
/**
|
|
2858
|
+
* Returns `true` if **all** listed permissions are granted.
|
|
2859
|
+
*
|
|
2860
|
+
* @example
|
|
2861
|
+
* perms.hasAll('messages.read', 'messages.write') // true
|
|
2862
|
+
*/
|
|
2863
|
+
hasAll(...permissions) {
|
|
2864
|
+
return permissions.every((p) => this.has(p));
|
|
1217
2865
|
}
|
|
1218
|
-
/**
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
2866
|
+
/**
|
|
2867
|
+
* Returns `true` if **at least one** of the listed permissions is granted.
|
|
2868
|
+
*
|
|
2869
|
+
* @example
|
|
2870
|
+
* perms.hasAny('channels.manage', 'servers.manage') // true if either is set
|
|
2871
|
+
*/
|
|
2872
|
+
hasAny(...permissions) {
|
|
2873
|
+
return permissions.some((p) => this.has(p));
|
|
1222
2874
|
}
|
|
1223
|
-
/**
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
2875
|
+
/**
|
|
2876
|
+
* Returns the list of permissions that are **not** granted.
|
|
2877
|
+
*
|
|
2878
|
+
* @example
|
|
2879
|
+
* const missing = perms.missing('messages.write', 'members.kick')
|
|
2880
|
+
* if (missing.length) throw new Error(`Missing: ${missing.join(', ')}`)
|
|
2881
|
+
*/
|
|
2882
|
+
missing(...permissions) {
|
|
2883
|
+
return permissions.filter((p) => !this.has(p));
|
|
1227
2884
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
2885
|
+
// ─── Building ──────────────────────────────────────────────────────────────
|
|
2886
|
+
/**
|
|
2887
|
+
* Return a **new** `PermissionsBitfield` with the given permission granted.
|
|
2888
|
+
*
|
|
2889
|
+
* @example
|
|
2890
|
+
* const updated = perms.grant('channels.manage')
|
|
2891
|
+
*/
|
|
2892
|
+
grant(...permissions) {
|
|
2893
|
+
const next = { ...this._perms };
|
|
2894
|
+
for (const p of permissions) next[p] = true;
|
|
2895
|
+
return new _PermissionsBitfield(next);
|
|
1232
2896
|
}
|
|
1233
|
-
/**
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
2897
|
+
/**
|
|
2898
|
+
* Return a **new** `PermissionsBitfield` with the given permission denied.
|
|
2899
|
+
*
|
|
2900
|
+
* @example
|
|
2901
|
+
* const restricted = perms.deny('members.kick')
|
|
2902
|
+
*/
|
|
2903
|
+
deny(...permissions) {
|
|
2904
|
+
const next = { ...this._perms };
|
|
2905
|
+
for (const p of permissions) next[p] = false;
|
|
2906
|
+
return new _PermissionsBitfield(next);
|
|
1237
2907
|
}
|
|
1238
|
-
/**
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
2908
|
+
/**
|
|
2909
|
+
* Merge another record or `PermissionsBitfield` on top of this one.
|
|
2910
|
+
* The incoming values **override** any existing ones.
|
|
2911
|
+
*
|
|
2912
|
+
* @example
|
|
2913
|
+
* const merged = serverPerms.merge(channelOverrides)
|
|
2914
|
+
*/
|
|
2915
|
+
merge(other) {
|
|
2916
|
+
const otherRecord = other instanceof _PermissionsBitfield ? other.toRecord() : other;
|
|
2917
|
+
return new _PermissionsBitfield({ ...this._perms, ...otherRecord });
|
|
1242
2918
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
2919
|
+
// ─── Inspection ────────────────────────────────────────────────────────────
|
|
2920
|
+
/**
|
|
2921
|
+
* Returns an array of the permission keys that are **currently granted**.
|
|
2922
|
+
*
|
|
2923
|
+
* @example
|
|
2924
|
+
* perms.toArray() // ['messages.read', 'messages.write']
|
|
2925
|
+
*/
|
|
2926
|
+
toArray() {
|
|
2927
|
+
return Object.entries(this._perms).filter(([, v]) => v).map(([k]) => k);
|
|
1247
2928
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
2929
|
+
/**
|
|
2930
|
+
* Returns the raw `Record<string, boolean>` map.
|
|
2931
|
+
*/
|
|
2932
|
+
toRecord() {
|
|
2933
|
+
return { ...this._perms };
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Pretty-print the granted permissions.
|
|
2937
|
+
*
|
|
2938
|
+
* @example
|
|
2939
|
+
* console.log(String(perms)) // 'PermissionsBitfield[messages.read, messages.write]'
|
|
2940
|
+
*/
|
|
2941
|
+
toString() {
|
|
2942
|
+
const granted = this.toArray();
|
|
2943
|
+
return granted.length > 0 ? `PermissionsBitfield[${granted.join(", ")}]` : "PermissionsBitfield[none]";
|
|
2944
|
+
}
|
|
2945
|
+
// ─── Static helpers ─────────────────────────────────────────────────────────
|
|
2946
|
+
/** Create a `PermissionsBitfield` from an existing record. */
|
|
2947
|
+
static from(perms) {
|
|
2948
|
+
return new _PermissionsBitfield(perms);
|
|
1252
2949
|
}
|
|
1253
2950
|
};
|
|
2951
|
+
/** A `PermissionsBitfield` with all known permissions granted. */
|
|
2952
|
+
_PermissionsBitfield.ALL = new _PermissionsBitfield({
|
|
2953
|
+
[Permissions.MESSAGES_READ]: true,
|
|
2954
|
+
[Permissions.MESSAGES_WRITE]: true,
|
|
2955
|
+
[Permissions.MESSAGES_MANAGE]: true,
|
|
2956
|
+
[Permissions.CHANNELS_MANAGE]: true,
|
|
2957
|
+
[Permissions.MEMBERS_KICK]: true,
|
|
2958
|
+
[Permissions.MEMBERS_BAN]: true,
|
|
2959
|
+
[Permissions.MEMBERS_ROLES]: true,
|
|
2960
|
+
[Permissions.SERVERS_MANAGE]: true
|
|
2961
|
+
});
|
|
2962
|
+
/** A `PermissionsBitfield` with no permissions granted. */
|
|
2963
|
+
_PermissionsBitfield.NONE = new _PermissionsBitfield({});
|
|
2964
|
+
var PermissionsBitfield = _PermissionsBitfield;
|
|
2965
|
+
|
|
2966
|
+
// src/utils/time.ts
|
|
2967
|
+
function sleep(ms) {
|
|
2968
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2969
|
+
}
|
|
2970
|
+
function withTimeout(promise, ms, message) {
|
|
2971
|
+
return new Promise((resolve, reject) => {
|
|
2972
|
+
const id = setTimeout(() => {
|
|
2973
|
+
reject(new Error(message ?? `[nova-bot-sdk] Operation timed out after ${ms}ms`));
|
|
2974
|
+
}, ms);
|
|
2975
|
+
promise.then(
|
|
2976
|
+
(v) => {
|
|
2977
|
+
clearTimeout(id);
|
|
2978
|
+
resolve(v);
|
|
2979
|
+
},
|
|
2980
|
+
(e) => {
|
|
2981
|
+
clearTimeout(id);
|
|
2982
|
+
reject(e);
|
|
2983
|
+
}
|
|
2984
|
+
);
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
var SECOND = 1e3;
|
|
2988
|
+
var MINUTE = 60 * SECOND;
|
|
2989
|
+
var HOUR = 60 * MINUTE;
|
|
2990
|
+
var DAY = 24 * HOUR;
|
|
2991
|
+
var WEEK = 7 * DAY;
|
|
2992
|
+
function formatDuration(ms) {
|
|
2993
|
+
if (ms < SECOND) return "0s";
|
|
2994
|
+
const weeks = Math.floor(ms / WEEK);
|
|
2995
|
+
const days = Math.floor(ms % WEEK / DAY);
|
|
2996
|
+
const hours = Math.floor(ms % DAY / HOUR);
|
|
2997
|
+
const minutes = Math.floor(ms % HOUR / MINUTE);
|
|
2998
|
+
const seconds = Math.floor(ms % MINUTE / SECOND);
|
|
2999
|
+
const parts = [];
|
|
3000
|
+
if (weeks) parts.push(`${weeks}w`);
|
|
3001
|
+
if (days) parts.push(`${days}d`);
|
|
3002
|
+
if (hours) parts.push(`${hours}h`);
|
|
3003
|
+
if (minutes) parts.push(`${minutes}m`);
|
|
3004
|
+
if (seconds) parts.push(`${seconds}s`);
|
|
3005
|
+
return parts.join(" ");
|
|
3006
|
+
}
|
|
3007
|
+
function formatRelative(timestamp) {
|
|
3008
|
+
const ts = timestamp instanceof Date ? timestamp.getTime() : typeof timestamp === "string" ? new Date(timestamp).getTime() : timestamp;
|
|
3009
|
+
const diff = Date.now() - ts;
|
|
3010
|
+
const abs = Math.abs(diff);
|
|
3011
|
+
const past = diff > 0;
|
|
3012
|
+
const fmt = (n, unit) => past ? `${n} ${unit}${n !== 1 ? "s" : ""} ago` : `in ${n} ${unit}${n !== 1 ? "s" : ""}`;
|
|
3013
|
+
if (abs < 5e3) return "just now";
|
|
3014
|
+
if (abs < MINUTE) return fmt(Math.round(abs / SECOND), "second");
|
|
3015
|
+
if (abs < 2 * MINUTE) return past ? "a minute ago" : "in a minute";
|
|
3016
|
+
if (abs < HOUR) return fmt(Math.round(abs / MINUTE), "minute");
|
|
3017
|
+
if (abs < 2 * HOUR) return past ? "an hour ago" : "in an hour";
|
|
3018
|
+
if (abs < DAY) return fmt(Math.round(abs / HOUR), "hour");
|
|
3019
|
+
if (abs < 2 * DAY) return past ? "yesterday" : "tomorrow";
|
|
3020
|
+
if (abs < WEEK) return fmt(Math.round(abs / DAY), "day");
|
|
3021
|
+
if (abs < 4 * WEEK) return fmt(Math.round(abs / WEEK), "week");
|
|
3022
|
+
return new Date(ts).toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
3023
|
+
}
|
|
3024
|
+
function parseTimestamp(value) {
|
|
3025
|
+
if (value == null) return null;
|
|
3026
|
+
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
|
|
3027
|
+
const d = new Date(value);
|
|
3028
|
+
return isNaN(d.getTime()) ? null : d;
|
|
3029
|
+
}
|
|
3030
|
+
function countdown(target) {
|
|
3031
|
+
const targetMs = target instanceof Date ? target.getTime() : typeof target === "string" ? new Date(target).getTime() : target;
|
|
3032
|
+
const total = Math.max(0, targetMs - Date.now());
|
|
3033
|
+
return {
|
|
3034
|
+
total,
|
|
3035
|
+
weeks: Math.floor(total / WEEK),
|
|
3036
|
+
days: Math.floor(total % WEEK / DAY),
|
|
3037
|
+
hours: Math.floor(total % DAY / HOUR),
|
|
3038
|
+
minutes: Math.floor(total % HOUR / MINUTE),
|
|
3039
|
+
seconds: Math.floor(total % MINUTE / SECOND)
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
1254
3042
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1255
3043
|
0 && (module.exports = {
|
|
1256
3044
|
ActionRowBuilder,
|
|
1257
3045
|
ButtonBuilder,
|
|
3046
|
+
ChannelsAPI,
|
|
3047
|
+
Collection,
|
|
1258
3048
|
CommandsAPI,
|
|
3049
|
+
Cooldown,
|
|
3050
|
+
CooldownManager,
|
|
1259
3051
|
EmbedBuilder,
|
|
1260
3052
|
HttpClient,
|
|
1261
3053
|
InteractionOptions,
|
|
1262
3054
|
InteractionsAPI,
|
|
3055
|
+
Logger,
|
|
1263
3056
|
MembersAPI,
|
|
3057
|
+
MessageBuilder,
|
|
1264
3058
|
MessagesAPI,
|
|
1265
3059
|
ModalBuilder,
|
|
3060
|
+
NovaChannel,
|
|
1266
3061
|
NovaClient,
|
|
1267
3062
|
NovaInteraction,
|
|
3063
|
+
NovaMember,
|
|
3064
|
+
NovaMessage,
|
|
3065
|
+
Paginator,
|
|
3066
|
+
Permissions,
|
|
1268
3067
|
PermissionsAPI,
|
|
3068
|
+
PermissionsBitfield,
|
|
3069
|
+
PollBuilder,
|
|
3070
|
+
ReactionsAPI,
|
|
1269
3071
|
SelectMenuBuilder,
|
|
1270
3072
|
ServersAPI,
|
|
1271
3073
|
SlashCommandBuilder,
|
|
1272
3074
|
SlashCommandOptionBuilder,
|
|
1273
|
-
TextInputBuilder
|
|
3075
|
+
TextInputBuilder,
|
|
3076
|
+
countdown,
|
|
3077
|
+
formatDuration,
|
|
3078
|
+
formatRelative,
|
|
3079
|
+
parseTimestamp,
|
|
3080
|
+
sleep,
|
|
3081
|
+
withTimeout
|
|
1274
3082
|
});
|