novaapp-sdk 1.0.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.mjs ADDED
@@ -0,0 +1,430 @@
1
+ // src/client.ts
2
+ import { io } from "socket.io-client";
3
+ import { EventEmitter } from "events";
4
+
5
+ // src/http.ts
6
+ var HttpClient = class {
7
+ constructor(baseUrl, token) {
8
+ this.baseUrl = baseUrl.replace(/\/$/, "");
9
+ this.token = token;
10
+ }
11
+ async request(method, path, body) {
12
+ const url = `${this.baseUrl}/api/bot${path}`;
13
+ const res = await fetch(url, {
14
+ method,
15
+ headers: {
16
+ Authorization: `Bearer ${this.token}`,
17
+ "Content-Type": "application/json",
18
+ "User-Agent": "nova-bot-sdk/1.0.0"
19
+ },
20
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
21
+ });
22
+ if (!res.ok) {
23
+ let message = res.statusText;
24
+ try {
25
+ const json = await res.json();
26
+ if (json.error) message = json.error;
27
+ } catch {
28
+ }
29
+ const err = new Error(`Nova API error ${res.status}: ${message}`);
30
+ err.status = res.status;
31
+ throw err;
32
+ }
33
+ if (res.status === 204) return void 0;
34
+ return res.json();
35
+ }
36
+ get(path) {
37
+ return this.request("GET", path);
38
+ }
39
+ post(path, body) {
40
+ return this.request("POST", path, body);
41
+ }
42
+ patch(path, body) {
43
+ return this.request("PATCH", path, body);
44
+ }
45
+ delete(path) {
46
+ return this.request("DELETE", path);
47
+ }
48
+ };
49
+
50
+ // src/api/messages.ts
51
+ var MessagesAPI = class {
52
+ constructor(http) {
53
+ this.http = http;
54
+ }
55
+ /**
56
+ * Send a message to a channel.
57
+ *
58
+ * @example
59
+ * await client.messages.send('channel-id', { content: 'Hello!' })
60
+ * await client.messages.send('channel-id', {
61
+ * embed: { title: 'Report', description: 'Everything is fine.' }
62
+ * })
63
+ */
64
+ send(channelId, options) {
65
+ return this.http.post("/messages", { channelId, ...options });
66
+ }
67
+ /**
68
+ * Edit a message previously sent by this bot.
69
+ */
70
+ edit(messageId, options) {
71
+ return this.http.patch(`/messages/${messageId}`, options);
72
+ }
73
+ /**
74
+ * Delete a message previously sent by this bot.
75
+ */
76
+ delete(messageId) {
77
+ return this.http.delete(`/messages/${messageId}`);
78
+ }
79
+ /**
80
+ * Fetch recent messages from a channel.
81
+ */
82
+ fetch(channelId, options = {}) {
83
+ const params = new URLSearchParams();
84
+ if (options.limit) params.set("limit", String(options.limit));
85
+ if (options.before) params.set("before", options.before);
86
+ const qs = params.toString();
87
+ return this.http.get(`/channels/${channelId}/messages${qs ? `?${qs}` : ""}`);
88
+ }
89
+ /**
90
+ * Send a typing indicator in a channel (appears for ~5 seconds).
91
+ */
92
+ typing(channelId) {
93
+ return this.http.post(`/channels/${channelId}/typing`);
94
+ }
95
+ };
96
+
97
+ // src/api/commands.ts
98
+ var CommandsAPI = class {
99
+ constructor(http) {
100
+ this.http = http;
101
+ }
102
+ // ── Slash Commands ────────────────────────────────────────────────────────
103
+ /**
104
+ * Register (or fully replace) slash commands.
105
+ * Pass `guildId` to register guild-specific commands; omit for global.
106
+ *
107
+ * @example
108
+ * await client.commands.setSlash([
109
+ * { name: 'ping', description: 'Responds with pong!' },
110
+ * { name: 'ban', description: 'Ban a user', options: [
111
+ * { name: 'user', description: 'User to ban', type: 'USER', required: true }
112
+ * ]}
113
+ * ])
114
+ */
115
+ setSlash(commands, guildId) {
116
+ return this.http.post("/commands", { commands, guildId });
117
+ }
118
+ /**
119
+ * Fetch all registered slash commands.
120
+ */
121
+ getSlash(guildId) {
122
+ const qs = guildId ? `?guildId=${encodeURIComponent(guildId)}` : "";
123
+ return this.http.get(`/commands${qs}`);
124
+ }
125
+ /**
126
+ * Delete a slash command by name.
127
+ */
128
+ deleteSlash(name) {
129
+ return this.http.delete(`/commands/${encodeURIComponent(name)}`);
130
+ }
131
+ // ── Prefix Commands ───────────────────────────────────────────────────────
132
+ /**
133
+ * Register (or fully replace) prefix commands.
134
+ *
135
+ * @example
136
+ * await client.commands.setPrefix([
137
+ * { prefix: '!', name: 'help', description: 'Show help' },
138
+ * { prefix: '!', name: 'kick', description: 'Kick a user' },
139
+ * ])
140
+ */
141
+ setPrefix(commands) {
142
+ return this.http.post("/prefix-commands", commands);
143
+ }
144
+ /**
145
+ * Fetch all registered prefix commands.
146
+ */
147
+ getPrefix() {
148
+ return this.http.get("/prefix-commands");
149
+ }
150
+ /**
151
+ * Delete a prefix command.
152
+ */
153
+ deletePrefix(prefix, name) {
154
+ return this.http.delete(
155
+ `/prefix-commands/${encodeURIComponent(prefix)}/${encodeURIComponent(name)}`
156
+ );
157
+ }
158
+ // ── Context Menu Commands ─────────────────────────────────────────────────
159
+ /**
160
+ * Register (or fully replace) context menu commands.
161
+ *
162
+ * @example
163
+ * await client.commands.setContext([
164
+ * { name: 'Report message', target: 'MESSAGE' },
165
+ * { name: 'View profile', target: 'USER' },
166
+ * ])
167
+ */
168
+ setContext(commands) {
169
+ return this.http.post("/context-commands", commands);
170
+ }
171
+ /**
172
+ * Fetch all registered context menu commands.
173
+ */
174
+ getContext() {
175
+ return this.http.get("/context-commands");
176
+ }
177
+ /**
178
+ * Delete a context menu command by name.
179
+ */
180
+ deleteContext(name) {
181
+ return this.http.delete(`/context-commands/${encodeURIComponent(name)}`);
182
+ }
183
+ };
184
+
185
+ // src/api/members.ts
186
+ var MembersAPI = class {
187
+ constructor(http) {
188
+ this.http = http;
189
+ }
190
+ /**
191
+ * Fetch members of a server the bot is in.
192
+ *
193
+ * @example
194
+ * const members = await client.members.list('server-id', { limit: 50 })
195
+ */
196
+ list(serverId, options = {}) {
197
+ const params = new URLSearchParams();
198
+ if (options.limit) params.set("limit", String(options.limit));
199
+ const qs = params.toString();
200
+ return this.http.get(`/servers/${serverId}/members${qs ? `?${qs}` : ""}`);
201
+ }
202
+ /**
203
+ * Kick a member from a server.
204
+ * Bots cannot kick owners or admins (403 will be thrown).
205
+ *
206
+ * @example
207
+ * await client.members.kick('server-id', 'user-id')
208
+ */
209
+ kick(serverId, userId) {
210
+ return this.http.post(`/servers/${serverId}/members/${userId}/kick`);
211
+ }
212
+ /**
213
+ * Ban a member from a server.
214
+ * Bots cannot ban owners or admins (403 will be thrown).
215
+ *
216
+ * @example
217
+ * await client.members.ban('server-id', 'user-id', 'Spamming')
218
+ */
219
+ ban(serverId, userId, reason) {
220
+ return this.http.post(`/servers/${serverId}/members/${userId}/ban`, { reason });
221
+ }
222
+ };
223
+
224
+ // src/api/servers.ts
225
+ var ServersAPI = class {
226
+ constructor(http) {
227
+ this.http = http;
228
+ }
229
+ /**
230
+ * Fetch all servers the bot is currently a member of.
231
+ *
232
+ * @example
233
+ * const servers = await client.servers.list()
234
+ * console.log(`Bot is in ${servers.length} servers`)
235
+ */
236
+ list() {
237
+ return this.http.get("/servers");
238
+ }
239
+ };
240
+
241
+ // src/api/interactions.ts
242
+ var InteractionsAPI = class {
243
+ constructor(http) {
244
+ this.http = http;
245
+ }
246
+ /**
247
+ * Acknowledge an interaction without sending a visible response.
248
+ * Shows a loading state to the user. You must follow up with `respond()`.
249
+ *
250
+ * @example
251
+ * client.on('interactionCreate', async (interaction) => {
252
+ * await client.interactions.ack(interaction.id)
253
+ * // ... do work ...
254
+ * await client.interactions.respond(interaction.id, { content: 'Done!' })
255
+ * })
256
+ */
257
+ ack(interactionId) {
258
+ return this.http.post(`/interactions/${interactionId}/ack`);
259
+ }
260
+ /**
261
+ * Respond to an interaction with a message.
262
+ * Set `ephemeral: true` to only show the response to the triggering user.
263
+ *
264
+ * @example
265
+ * await client.interactions.respond(interaction.id, {
266
+ * content: 'Pong!',
267
+ * ephemeral: true,
268
+ * })
269
+ */
270
+ respond(interactionId, options) {
271
+ return this.http.post(
272
+ `/interactions/${interactionId}/respond`,
273
+ options
274
+ );
275
+ }
276
+ /**
277
+ * Poll for pending (unacknowledged) interactions.
278
+ * Useful when not using the WebSocket gateway.
279
+ *
280
+ * @example
281
+ * const pending = await client.interactions.poll({ limit: 20 })
282
+ * for (const i of pending) {
283
+ * await client.interactions.respond(i.id, { content: 'Got it!' })
284
+ * }
285
+ */
286
+ poll(options = {}) {
287
+ const params = new URLSearchParams();
288
+ if (options.limit) params.set("limit", String(options.limit));
289
+ if (options.since) params.set("since", options.since);
290
+ const qs = params.toString();
291
+ return this.http.get(`/interactions${qs ? `?${qs}` : ""}`);
292
+ }
293
+ };
294
+
295
+ // src/client.ts
296
+ var EVENT_MAP = {
297
+ "message.created": "messageCreate",
298
+ "message.edited": "messageUpdate",
299
+ "message.deleted": "messageDelete",
300
+ "message.reaction_added": "reactionAdd",
301
+ "message.reaction_removed": "reactionRemove",
302
+ "user.joined_server": "memberAdd",
303
+ "user.left_server": "memberRemove",
304
+ "user.started_typing": "typingStart"
305
+ };
306
+ var NovaClient = class extends EventEmitter {
307
+ constructor(options) {
308
+ super();
309
+ /** The authenticated bot application. Available after `ready` fires. */
310
+ this.botUser = null;
311
+ this.socket = null;
312
+ if (!options.token) {
313
+ throw new Error("[nova-bot-sdk] A bot token is required.");
314
+ }
315
+ if (!options.token.startsWith("nova_bot_")) {
316
+ console.warn('[nova-bot-sdk] Warning: token does not start with "nova_bot_". Are you sure this is a bot token?');
317
+ }
318
+ this.options = {
319
+ token: options.token,
320
+ baseUrl: options.baseUrl ?? "https://api.nova.chat"
321
+ };
322
+ this.http = new HttpClient(this.options.baseUrl, this.options.token);
323
+ this.messages = new MessagesAPI(this.http);
324
+ this.commands = new CommandsAPI(this.http);
325
+ this.members = new MembersAPI(this.http);
326
+ this.servers = new ServersAPI(this.http);
327
+ this.interactions = new InteractionsAPI(this.http);
328
+ }
329
+ /**
330
+ * Connect to the Nova WebSocket gateway.
331
+ * Resolves when the `ready` event is received.
332
+ *
333
+ * @example
334
+ * await client.connect()
335
+ */
336
+ connect() {
337
+ return new Promise((resolve, reject) => {
338
+ const gatewayUrl = this.options.baseUrl.replace(/\/$/, "") + "/bot-gateway";
339
+ this.socket = io(gatewayUrl, {
340
+ path: "/socket.io",
341
+ transports: ["websocket"],
342
+ auth: { botToken: this.options.token },
343
+ reconnection: true,
344
+ reconnectionAttempts: Infinity,
345
+ reconnectionDelay: 1e3,
346
+ reconnectionDelayMax: 3e4
347
+ });
348
+ const onConnectError = (err) => {
349
+ this.emit("error", { code: 0, message: err.message });
350
+ reject(err);
351
+ };
352
+ this.socket.once("connect_error", onConnectError);
353
+ this.socket.once("bot:ready", (data) => {
354
+ this.socket.off("connect_error", onConnectError);
355
+ this.botUser = { ...data.bot, scopes: data.scopes };
356
+ this.emit("ready", this.botUser);
357
+ resolve();
358
+ });
359
+ this.socket.on("interaction:created", (interaction) => {
360
+ this.emit("interactionCreate", interaction);
361
+ });
362
+ this.socket.on("bot:event", (event) => {
363
+ this.emit("event", event);
364
+ const shorthand = EVENT_MAP[event.type];
365
+ if (shorthand) {
366
+ this.emit(shorthand, event.data);
367
+ }
368
+ });
369
+ this.socket.on("bot:error", (err) => {
370
+ this.emit("error", err);
371
+ });
372
+ this.socket.on("disconnect", (reason) => {
373
+ this.emit("disconnect", reason);
374
+ });
375
+ });
376
+ }
377
+ /**
378
+ * Disconnect from the gateway and clean up.
379
+ */
380
+ disconnect() {
381
+ this.socket?.disconnect();
382
+ this.socket = null;
383
+ }
384
+ /**
385
+ * Send a message via the WebSocket gateway (lower latency than HTTP).
386
+ * Requires the `messages.write` scope.
387
+ *
388
+ * @example
389
+ * client.wsSend('channel-id', 'Hello from the gateway!')
390
+ */
391
+ wsSend(channelId, content) {
392
+ if (!this.socket?.connected) {
393
+ throw new Error("[nova-bot-sdk] Not connected. Call client.connect() first.");
394
+ }
395
+ this.socket.emit("bot:message:send", { channelId, content });
396
+ }
397
+ /**
398
+ * Start a typing indicator via WebSocket.
399
+ */
400
+ wsTypingStart(channelId) {
401
+ this.socket?.emit("bot:typing:start", { channelId });
402
+ }
403
+ /**
404
+ * Stop a typing indicator via WebSocket.
405
+ */
406
+ wsTypingStop(channelId) {
407
+ this.socket?.emit("bot:typing:stop", { channelId });
408
+ }
409
+ on(event, listener) {
410
+ return super.on(event, listener);
411
+ }
412
+ once(event, listener) {
413
+ return super.once(event, listener);
414
+ }
415
+ off(event, listener) {
416
+ return super.off(event, listener);
417
+ }
418
+ emit(event, ...args) {
419
+ return super.emit(event, ...args);
420
+ }
421
+ };
422
+ export {
423
+ CommandsAPI,
424
+ HttpClient,
425
+ InteractionsAPI,
426
+ MembersAPI,
427
+ MessagesAPI,
428
+ NovaClient,
429
+ ServersAPI
430
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "novaapp-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Official SDK for building bots on the Nova platform",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md"],
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
18
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": ["nova", "bot", "sdk", "discord-like", "chat"],
22
+ "author": "Nova",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "socket.io-client": "^4.8.1"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "tsup": "^8.0.0",
30
+ "typescript": "^5.7.3"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ }
35
+ }