@wlix/ceres 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Andrew (e60m5ss / wlix)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ <h1 align="center">@wlix/ceres</h1>
2
+
3
+ > Node.js library for creating Discord bots
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @wlix/ceres
9
+ # or
10
+ yarn add @wlix/ceres
11
+ # or
12
+ pnpm add @wlix/ceres
13
+ # or
14
+ bun add @wlix/ceres
15
+ ```
16
+
17
+ ## Example
18
+
19
+ ```js
20
+ import { Client, ClientIntents } from "@wlix/ceres";
21
+
22
+ const client = new Client({
23
+ // replace TOKEN with the bot's token
24
+ // https://discord.com/developers/applications
25
+ token: "TOKEN",
26
+ intents: [
27
+ // include client intents here
28
+ ClientIntents.Guilds,
29
+ ClientIntents.GuildMembers,
30
+ ClientIntents.GuildMessages,
31
+ ],
32
+ });
33
+
34
+ client.once("ready", () => {
35
+ console.log("Client has been connected to Discord");
36
+ });
37
+
38
+ client.on("messageCreate", (message) => {
39
+ if (message.author.bot) return;
40
+
41
+ if (message.content === "!ping") {
42
+ message.reply("Pong!");
43
+ }
44
+ });
45
+
46
+ // in case any issues arise
47
+ client.on("debug", console.log);
48
+ client.on("error", console.log);
49
+
50
+ // connect to Discord
51
+ client.connect();
52
+ ```
53
+
54
+ ## Contributing
55
+
56
+ Pull requests are always welcomed. For more major changes, please open an issue to discuss what you wish to change.
57
+
58
+ ## License
59
+
60
+ [MIT](LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,543 @@
1
+ let discord_api_types_v10 = require("discord-api-types/v10");
2
+
3
+ //#region src/structures/EventHandler.ts
4
+ /**
5
+ * Class representing a typed EventHandler supporting generics
6
+ */
7
+ var EventHandler = class {
8
+ /**
9
+ * All stored listeners
10
+ */
11
+ #listeners = {};
12
+ /**
13
+ * Listen for an event
14
+ * @param event Event name
15
+ * @param listener Callback once event is emitted
16
+ */
17
+ on(event, listener) {
18
+ if (!this.#listeners[event]) this.#listeners[event] = [];
19
+ this.#listeners[event].push(listener);
20
+ return this;
21
+ }
22
+ /**
23
+ * Listen for an event once
24
+ * @param event Event name
25
+ * @param listener Callback once event is emitted
26
+ */
27
+ once(event, listener) {
28
+ const wrapped = (...args) => {
29
+ listener(...args);
30
+ this.off(event, wrapped);
31
+ };
32
+ this.on(event, wrapped);
33
+ return this;
34
+ }
35
+ /**
36
+ * Remove a registered event listener
37
+ * @param event Event name
38
+ * @param listener Callback function to remove
39
+ */
40
+ off(event, listener) {
41
+ if (!this.#listeners[event]) return this;
42
+ this.#listeners[event] = this.#listeners[event].filter((l) => l !== listener);
43
+ return this;
44
+ }
45
+ /**
46
+ * Emit an event
47
+ * @param event Event name
48
+ * @param args Event arguments
49
+ */
50
+ emit(event, ...args) {
51
+ if (!this.#listeners[event]) return false;
52
+ for (const listener of this.#listeners[event]) listener(...args);
53
+ return true;
54
+ }
55
+ };
56
+
57
+ //#endregion
58
+ //#region src/structures/Channel.ts
59
+ /**
60
+ * Class representing a base Channel
61
+ */
62
+ var Channel = class {
63
+ /**
64
+ * The Client associated with this Channel
65
+ */
66
+ client;
67
+ /**
68
+ * The Discord channel ID
69
+ */
70
+ id;
71
+ /**
72
+ * The Discord channel name
73
+ */
74
+ name;
75
+ /**
76
+ * The type of this channel
77
+ */
78
+ type;
79
+ /**
80
+ * The ID of the guild this channel is in
81
+ */
82
+ guildId;
83
+ /**
84
+ * Instantiate a new Channel (internal)
85
+ * @param client Associated client
86
+ * @param data API channel data
87
+ */
88
+ constructor(client, data) {
89
+ this.client = client;
90
+ this.id = data.id;
91
+ this.type = data.type;
92
+ if ("guild_id" in data) this.guildId = data.guild_id ?? void 0;
93
+ if ("name" in data) this.name = data.name ?? void 0;
94
+ }
95
+ /**
96
+ * Whether this channel is a guild text channel, a direct message, or a guild announcement channel
97
+ */
98
+ isText() {
99
+ return this.type === discord_api_types_v10.ChannelType.GuildText || this.type === discord_api_types_v10.ChannelType.DM || this.type === discord_api_types_v10.ChannelType.GuildAnnouncement;
100
+ }
101
+ /**
102
+ * Send a message in this channel
103
+ * @param props Message data
104
+ * @returns Created message object
105
+ */
106
+ send(props) {
107
+ return this.client.createMessage(this.id, typeof props === "string" ? { content: props } : props);
108
+ }
109
+ };
110
+
111
+ //#endregion
112
+ //#region src/utils/client-events.ts
113
+ async function handleClientEvent(client, packet) {
114
+ const { t, d } = packet;
115
+ client.emit("debug", `[@wlix/ceres Client debug]: Received ${t} event`);
116
+ switch (t) {
117
+ case discord_api_types_v10.GatewayDispatchEvents.MessageCreate:
118
+ client.emit("messageCreate", new Message(client, await client.fetchChannel(d.channel_id), d));
119
+ break;
120
+ case discord_api_types_v10.GatewayDispatchEvents.Ready:
121
+ client.user = new User(client, d.user);
122
+ client.emit("ready", client);
123
+ break;
124
+ }
125
+ }
126
+
127
+ //#endregion
128
+ //#region src/utils/intents.ts
129
+ const ClientIntents = {
130
+ AutoModerationConfiguration: 1 << 20,
131
+ AutoModerationExecution: 1 << 21,
132
+ DirectMessagePolls: 1 << 25,
133
+ DirectMessageReactions: 8192,
134
+ DirectMessages: 4096,
135
+ DirectMessageTyping: 16384,
136
+ GuildExpressions: 8,
137
+ GuildIntegrations: 16,
138
+ GuildInvites: 64,
139
+ GuildMembers: 2,
140
+ GuildMessagePolls: 1 << 24,
141
+ GuildMessageReactions: 1024,
142
+ GuildMessages: 512,
143
+ GuildMessageTyping: 2048,
144
+ GuildModeration: 4,
145
+ GuildPresences: 256,
146
+ Guilds: 1,
147
+ GuildScheduledEvents: 65536,
148
+ GuildVoiceStates: 128,
149
+ GuildWebhooks: 32,
150
+ MessageContent: 32768
151
+ };
152
+
153
+ //#endregion
154
+ //#region src/structures/Client.ts
155
+ /**
156
+ * Class representing a Client connecting to Discord's API
157
+ * @extends EventHandler
158
+ */
159
+ var Client = class Client extends EventHandler {
160
+ /**
161
+ * The URL to connect to when initializing the gateway
162
+ */
163
+ static gatewayUrl = "wss://gateway.discord.gg/?v=10&encoding=json";
164
+ /**
165
+ * The URL to connect to when contacting the API
166
+ */
167
+ static baseApiUrl = "https://discord.com/api/v10";
168
+ /**
169
+ * The client's intents
170
+ */
171
+ intents;
172
+ /**
173
+ * Whether the client is ready
174
+ */
175
+ ready = false;
176
+ /**
177
+ * The client's token
178
+ */
179
+ token;
180
+ /**
181
+ * The client's user
182
+ */
183
+ user = null;
184
+ /**
185
+ * A simple cache for Discord objects
186
+ */
187
+ #cache = {
188
+ channels: /* @__PURE__ */ new Map(),
189
+ users: /* @__PURE__ */ new Map()
190
+ };
191
+ /**
192
+ * The client's presence
193
+ */
194
+ #presence;
195
+ /**
196
+ * The client's heartbeat interval
197
+ */
198
+ #heartbeatInterval;
199
+ /**
200
+ * The client's websocket
201
+ */
202
+ #ws;
203
+ /**
204
+ * Instantiate a new Client
205
+ * @param options Client options
206
+ * @param options.token Client token
207
+ */
208
+ constructor(options) {
209
+ super();
210
+ if (!options?.token) throw new Error("[@wlix/ceres Client]: Client token must be provided as a non-empty string");
211
+ if (!options?.intents) throw new Error("[@wlix/ceres Client]: Client intents must be provided");
212
+ this.token = options.token.toLowerCase().startsWith("bot ") ? options.token : "Bot " + options.token;
213
+ this.intents = options.intents.reduce((bitfield, i) => bitfield | i, 0);
214
+ if (options.presence) this.#presence = options.presence;
215
+ }
216
+ async #apiFetch(path, options) {
217
+ const res = await fetch(`${Client.baseApiUrl}${path}`, {
218
+ ...options,
219
+ headers: {
220
+ Authorization: this.token,
221
+ "Content-Type": "application/json",
222
+ ...options?.headers ?? {}
223
+ }
224
+ });
225
+ if (!res.ok) throw new Error(`[@wlix/ceres] Discord API error: ${res.status} ${res.statusText}`);
226
+ return res.json();
227
+ }
228
+ /**
229
+ * Handle incoming packets
230
+ * @param packet Packet data
231
+ */
232
+ #handlePacket(packet) {
233
+ const { t, op, d, s } = packet;
234
+ if (op === 10) {
235
+ this.#heartbeatInterval = setInterval(() => {
236
+ this.#ws?.send(JSON.stringify({
237
+ op: discord_api_types_v10.GatewayOpcodes.Heartbeat,
238
+ d: s
239
+ }));
240
+ }, d.heartbeat_interval);
241
+ this.#ws?.send(JSON.stringify({
242
+ op: discord_api_types_v10.GatewayOpcodes.Identify,
243
+ d: {
244
+ token: this.token,
245
+ intents: this.intents,
246
+ properties: {
247
+ $os: "linux",
248
+ $browser: "@wlix/ceres",
249
+ $device: "@wlix/ceres"
250
+ },
251
+ presence: this.#presence ? {
252
+ status: this.#presence.status,
253
+ since: null,
254
+ afk: false,
255
+ activities: this.#presence.activities ?? []
256
+ } : {
257
+ status: "online",
258
+ since: null,
259
+ afk: false,
260
+ activities: []
261
+ }
262
+ }
263
+ }));
264
+ }
265
+ if (packet.t) handleClientEvent(this, packet);
266
+ }
267
+ /**
268
+ * Whether the client is ready
269
+ */
270
+ isReady() {
271
+ return this.user !== null;
272
+ }
273
+ /**
274
+ * Returns the client's user presence
275
+ */
276
+ get presence() {
277
+ return this.#presence ?? null;
278
+ }
279
+ /**
280
+ * Update the client's token (only when not connected)
281
+ * @param token New token
282
+ */
283
+ setToken(token) {
284
+ if (this.#ws) throw new Error("[@wlix/ceres Client]: Cannot change token while connected, disconnect first");
285
+ if (!token || typeof token !== "string") throw new Error("[@wlix/ceres Client]: Client token must be provided as a non-empty string");
286
+ this.token = token.toLowerCase().startsWith("bot ") ? token : "Bot " + token;
287
+ this.emit("debug", "[@wlix/ceres Client debug]: Client token was updated");
288
+ }
289
+ /**
290
+ * Connects the Client to the Discord gateway
291
+ * @returns Client instance
292
+ */
293
+ async connect() {
294
+ this.emit("debug", "[@wlix/ceres Client debug]: Connecting to Discord gateway");
295
+ this.#ws = new WebSocket(Client.gatewayUrl);
296
+ this.#ws.onopen = () => {
297
+ this.emit("debug", "[@wlix/ceres Client debug]: WebSocket has been opened");
298
+ };
299
+ this.#ws.onmessage = (message) => {
300
+ this.#handlePacket(JSON.parse(message.data));
301
+ };
302
+ this.#ws.onerror = (error) => {
303
+ this.emit("debug", `[@wlix/ceres Client debug]: WebSocket error: ${error}`);
304
+ };
305
+ return this;
306
+ }
307
+ /**
308
+ * Disconnects the client from the Discord gateway
309
+ */
310
+ disconnect() {
311
+ if (this.#heartbeatInterval) {
312
+ clearInterval(this.#heartbeatInterval);
313
+ this.#heartbeatInterval = void 0;
314
+ }
315
+ if (this.#ws) {
316
+ this.#ws.close(1e3, "Client disconnect");
317
+ this.#ws = void 0;
318
+ }
319
+ this.emit("debug", "[@wlix/ceres Client debug]: Disconnected from Discord gateway");
320
+ this.emit("disconnect");
321
+ }
322
+ /**
323
+ * Fetch a Discord channel by ID
324
+ * @param id Channel ID
325
+ * @returns Channel object or null
326
+ */
327
+ async fetchChannel(id) {
328
+ if (this.#cache.channels.has(id)) return this.#cache.channels.get(id);
329
+ try {
330
+ const channel = new Channel(this, await this.#apiFetch(`${discord_api_types_v10.Routes.channel(id)}`));
331
+ this.#cache.channels.set(id, channel);
332
+ return channel;
333
+ } catch (error) {
334
+ this.emit("error", error);
335
+ throw error;
336
+ }
337
+ }
338
+ /**
339
+ * Fetch a user by ID
340
+ * @param id User ID
341
+ * @returns User object or null
342
+ */
343
+ async fetchUser(id) {
344
+ if (this.#cache.users.has(id)) return this.#cache.users.get(id);
345
+ try {
346
+ const user = new User(this, await this.#apiFetch(`${discord_api_types_v10.Routes.user(id)}`));
347
+ this.#cache.users.set(id, user);
348
+ return user;
349
+ } catch (error) {
350
+ this.emit("error", error);
351
+ throw error;
352
+ }
353
+ }
354
+ /**
355
+ * Sends a new message as the Client
356
+ * @returns Created message or null
357
+ */
358
+ async createMessage(channelId, props) {
359
+ if (!this.isReady()) return null;
360
+ try {
361
+ const data = await this.#apiFetch(discord_api_types_v10.Routes.channelMessages(channelId), {
362
+ method: "POST",
363
+ body: typeof props === "string" ? JSON.stringify({ content: props }) : JSON.stringify({
364
+ content: props.content,
365
+ nonce: props.nonce,
366
+ tts: props.tts,
367
+ embeds: props.embeds,
368
+ allowed_mentions: props.allowedMentions,
369
+ message_reference: props.messageReference,
370
+ components: props.components,
371
+ sticker_ids: props.stickerIds,
372
+ attachments: props.attachments,
373
+ flags: props.flags,
374
+ enforce_nonce: props.enforceNonce
375
+ })
376
+ });
377
+ return new Message(this, await this.fetchChannel(data.channel_id), data);
378
+ } catch (error) {
379
+ this.emit("error", error);
380
+ throw error;
381
+ }
382
+ }
383
+ /**
384
+ * Update the client's user presence
385
+ * @param presence Presence data
386
+ */
387
+ setPresence(presence) {
388
+ this.#presence = presence;
389
+ if (this.#ws) this.#ws.send(JSON.stringify({
390
+ op: discord_api_types_v10.GatewayOpcodes.PresenceUpdate,
391
+ d: {
392
+ status: presence.status,
393
+ since: null,
394
+ afk: false,
395
+ activities: []
396
+ }
397
+ }));
398
+ }
399
+ };
400
+
401
+ //#endregion
402
+ //#region src/structures/Message.ts
403
+ /**
404
+ * Class representing a Message
405
+ */
406
+ var Message = class {
407
+ /**
408
+ * The Client associated with this Message
409
+ */
410
+ client;
411
+ /**
412
+ * The Discord message ID
413
+ */
414
+ id;
415
+ /**
416
+ * The Discord message content
417
+ */
418
+ content;
419
+ /**
420
+ * The User who sent this message
421
+ */
422
+ author;
423
+ /**
424
+ * The channel where this message is
425
+ */
426
+ channel;
427
+ /**
428
+ * The ID of the channel where this message is
429
+ */
430
+ channelId;
431
+ /**
432
+ * A Date representing when this message was sent
433
+ */
434
+ timestamp;
435
+ /**
436
+ * Instantiate a new Message (internal)
437
+ * @param client Associated client
438
+ * @param data API message data
439
+ */
440
+ constructor(client, channel, data) {
441
+ this.client = client;
442
+ this.id = data.id;
443
+ this.content = data.content;
444
+ this.author = new User(client, data.author);
445
+ this.channelId = data.channel_id;
446
+ this.timestamp = new Date(data.timestamp);
447
+ this.channel = channel;
448
+ }
449
+ /**
450
+ * Reply to a message
451
+ * @param props Message reply props
452
+ * @returns Replied message
453
+ */
454
+ async reply(props) {
455
+ return this.client.createMessage(this.channelId, typeof props === "string" ? {
456
+ content: props,
457
+ messageReference: { message_id: this.id }
458
+ } : {
459
+ ...props,
460
+ messageReference: { message_id: this.id }
461
+ });
462
+ }
463
+ };
464
+
465
+ //#endregion
466
+ //#region src/structures/User.ts
467
+ /**
468
+ * Class representing a User
469
+ */
470
+ var User = class {
471
+ /**
472
+ * The Client associated with this User
473
+ */
474
+ client;
475
+ /**
476
+ * The Discord user ID
477
+ */
478
+ id;
479
+ /**
480
+ * The Discord user username
481
+ */
482
+ username;
483
+ /**
484
+ * The Discord user's discriminator, or "0" if they have none
485
+ */
486
+ discriminator = "0";
487
+ /**
488
+ * Whether this Discord user is a bot
489
+ */
490
+ bot;
491
+ /**
492
+ * If the user has a discriminator (i.e., it's not "0"), returns username#discriminator
493
+ * Else, returns just the username
494
+ */
495
+ get tag() {
496
+ return this.discriminator === "0" ? this.username : `${this.username}#${this.discriminator}`;
497
+ }
498
+ /**
499
+ * Represent the User as a mention (e.g., <@USERID>)
500
+ */
501
+ toString() {
502
+ return `<@${this.id}>`;
503
+ }
504
+ /**
505
+ * Instantiate a new User (internal)
506
+ * @param client Associated client
507
+ * @param data API user data
508
+ */
509
+ constructor(client, data) {
510
+ this.client = client;
511
+ this.id = data.id;
512
+ this.username = data.username;
513
+ this.discriminator = data.discriminator;
514
+ this.bot = data.bot ?? false;
515
+ }
516
+ };
517
+
518
+ //#endregion
519
+ Object.defineProperty(exports, 'ActivityType', {
520
+ enumerable: true,
521
+ get: function () {
522
+ return discord_api_types_v10.ActivityType;
523
+ }
524
+ });
525
+ exports.Channel = Channel;
526
+ Object.defineProperty(exports, 'ChannelType', {
527
+ enumerable: true,
528
+ get: function () {
529
+ return discord_api_types_v10.ChannelType;
530
+ }
531
+ });
532
+ exports.Client = Client;
533
+ exports.ClientIntents = ClientIntents;
534
+ exports.EventHandler = EventHandler;
535
+ Object.defineProperty(exports, 'GatewayDispatchEvents', {
536
+ enumerable: true,
537
+ get: function () {
538
+ return discord_api_types_v10.GatewayDispatchEvents;
539
+ }
540
+ });
541
+ exports.Message = Message;
542
+ exports.User = User;
543
+ exports.handleClientEvent = handleClientEvent;