@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/dist/index.mjs ADDED
@@ -0,0 +1,519 @@
1
+ import { ActivityType, ChannelType, ChannelType as ChannelType$1, GatewayDispatchEvents, GatewayDispatchEvents as GatewayDispatchEvents$1, GatewayOpcodes, Routes } from "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 === ChannelType$1.GuildText || this.type === ChannelType$1.DM || this.type === ChannelType$1.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 GatewayDispatchEvents$1.MessageCreate:
118
+ client.emit("messageCreate", new Message(client, await client.fetchChannel(d.channel_id), d));
119
+ break;
120
+ case GatewayDispatchEvents$1.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: GatewayOpcodes.Heartbeat,
238
+ d: s
239
+ }));
240
+ }, d.heartbeat_interval);
241
+ this.#ws?.send(JSON.stringify({
242
+ op: 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(`${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(`${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(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: 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
+ export { ActivityType, Channel, ChannelType, Client, ClientIntents, EventHandler, GatewayDispatchEvents, Message, User, handleClientEvent };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@wlix/ceres",
3
+ "description": "Node.js library for creating Discord bots",
4
+ "version": "0.0.1",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "url": "https://github.com/e60m5ss/ceres"
10
+ },
11
+ "author": {
12
+ "name": "e60m5ss",
13
+ "url": "https://github.com/e60m5ss"
14
+ },
15
+ "keywords": [
16
+ "api",
17
+ "bot",
18
+ "discord",
19
+ "discord-bot",
20
+ "discord-client",
21
+ "websocket",
22
+ "ws"
23
+ ],
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.mjs",
26
+ "types": "./dist/index.d.ts",
27
+ "type": "module",
28
+ "prettier": {
29
+ "jsxSingleQuote": false,
30
+ "printWidth": 90,
31
+ "semi": true,
32
+ "singleQuote": false,
33
+ "tabWidth": 4,
34
+ "trailingComma": "es5",
35
+ "useTabs": false
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.2.0",
39
+ "prettier": "^3.8.1",
40
+ "tsdown": "^0.20.1",
41
+ "typedoc": "^0.28.16",
42
+ "typescript": "^5.9.3"
43
+ },
44
+ "dependencies": {
45
+ "discord-api-types": "^0.38.38"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "docgen": "typedoc ./src",
50
+ "format": "prettier ./src --write --ignore-path=.prettierignore",
51
+ "lint": "tsc --noEmit; prettier ./src --check --ignore-path=.prettierignore",
52
+ "prepublish": "tsdown"
53
+ }
54
+ }