novaapp-sdk 1.0.7 → 1.0.10

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 CHANGED
@@ -128,12 +128,53 @@ interface Interaction {
128
128
  commandName?: string | null;
129
129
  customId?: string | null;
130
130
  data?: unknown;
131
+ /** For SELECT_MENU interactions — the chosen option values. */
132
+ values?: string[];
133
+ /** For MODAL_SUBMIT interactions — field customId → submitted value. */
134
+ modalData?: Record<string, string>;
131
135
  userId: string;
132
136
  channelId: string;
133
137
  serverId?: string | null;
134
138
  triggerMsgId?: string | null;
135
139
  createdAt: string;
136
140
  }
141
+ /** A single text input field inside a bot modal. */
142
+ interface BotModalField {
143
+ /** Unique field ID — returned as key in `modalData` on MODAL_SUBMIT. */
144
+ customId: string;
145
+ /** Label displayed above the input. */
146
+ label: string;
147
+ /** `'short'` = single-line input, `'paragraph'` = multi-line textarea. */
148
+ type: 'short' | 'paragraph';
149
+ placeholder?: string;
150
+ required?: boolean;
151
+ minLength?: number;
152
+ maxLength?: number;
153
+ /** Optional pre-filled default value. */
154
+ value?: string;
155
+ }
156
+ /**
157
+ * Pass this as `modal` inside `client.interactions.respond()` to open a
158
+ * modal dialog on the user's client instead of sending a message.
159
+ *
160
+ * @example
161
+ * await client.interactions.respond(interaction.id, {
162
+ * modal: {
163
+ * title: 'Submit a report',
164
+ * customId: 'report_modal',
165
+ * fields: [
166
+ * { customId: 'reason', label: 'Reason', type: 'paragraph', required: true },
167
+ * ],
168
+ * },
169
+ * })
170
+ */
171
+ interface BotModalDefinition {
172
+ /** Title shown at the top of the modal dialog. */
173
+ title: string;
174
+ /** Custom ID passed back with the MODAL_SUBMIT interaction. */
175
+ customId: string;
176
+ fields: BotModalField[];
177
+ }
137
178
  interface SlashCommandOption {
138
179
  name: string;
139
180
  description: string;
@@ -210,6 +251,11 @@ interface RespondInteractionOptions {
210
251
  embed?: Embed;
211
252
  components?: MessageComponent[];
212
253
  ephemeral?: boolean;
254
+ /**
255
+ * Open a modal dialog on the user's client instead of sending a message.
256
+ * When set, all other fields are ignored.
257
+ */
258
+ modal?: BotModalDefinition;
213
259
  }
214
260
  interface FetchMessagesOptions {
215
261
  limit?: number;
@@ -222,6 +268,39 @@ interface PollInteractionsOptions {
222
268
  limit?: number;
223
269
  since?: string;
224
270
  }
271
+ /** A raw permission record stored for a specific scope (server, role, or channel). */
272
+ interface BotPermissionRecord {
273
+ id: string;
274
+ botId: string;
275
+ serverId: string;
276
+ /** `null` means this record is server-wide (not channel-scoped). */
277
+ channelId: string | null;
278
+ /** `null` means this record applies to all roles. */
279
+ roleId: string | null;
280
+ /** Key-value map of permission name → granted flag. */
281
+ permissions: Record<string, boolean>;
282
+ createdAt: string;
283
+ updatedAt: string;
284
+ }
285
+ /** Response from `client.permissions.get()`. */
286
+ interface PermissionsResult {
287
+ /**
288
+ * Merged, effective permissions for the requested scope.
289
+ * Merging order (highest wins): channel > role > server-wide.
290
+ */
291
+ permissions: Record<string, boolean>;
292
+ /** All raw permission records matching the query scope. */
293
+ records: BotPermissionRecord[];
294
+ }
295
+ /** Options accepted by `client.permissions.get()`. */
296
+ interface PermissionsQueryOptions {
297
+ /** The server to query permissions for (required). */
298
+ serverId: string;
299
+ /** Narrow the scope to a specific channel. */
300
+ channelId?: string;
301
+ /** Narrow the scope to a specific role. */
302
+ roleId?: string;
303
+ }
225
304
  interface NovaClientOptions {
226
305
  /** Your bot token — starts with "nova_bot_" */
227
306
  token: string;
@@ -419,7 +498,8 @@ declare class ServersAPI {
419
498
 
420
499
  declare class InteractionsAPI {
421
500
  private readonly http;
422
- constructor(http: HttpClient);
501
+ private readonly emitter;
502
+ constructor(http: HttpClient, emitter: EventEmitter);
423
503
  /**
424
504
  * Acknowledge an interaction without sending a visible response.
425
505
  * Shows a loading state to the user. You must follow up with `respond()`.
@@ -459,6 +539,84 @@ declare class InteractionsAPI {
459
539
  * }
460
540
  */
461
541
  poll(options?: PollInteractionsOptions): Promise<Interaction[]>;
542
+ /**
543
+ * Open a modal for the user who triggered an interaction and await their submission.
544
+ *
545
+ * Internally this:
546
+ * 1. Calls `respond(interactionId, { modal })` to push the modal to the client UI.
547
+ * 2. Listens for the next `interactionCreate` event whose type is `MODAL_SUBMIT`
548
+ * and whose `customId` matches your modal's `customId`.
549
+ * 3. Resolves with the submitted `Interaction` (containing `modalData`), or `null`
550
+ * if the user closes the modal without submitting within the timeout.
551
+ *
552
+ * @param interactionId - ID of the triggering interaction.
553
+ * @param modal - Modal definition (title, customId, fields).
554
+ * @param options.timeout - Max milliseconds to wait (default: 300 000 = 5 min).
555
+ *
556
+ * @example
557
+ * client.on('interactionCreate', async (interaction) => {
558
+ * if (interaction.commandName === 'report') {
559
+ * const submitted = await client.interactions.awaitModal(interaction.id, {
560
+ * title: 'Submit a report',
561
+ * customId: 'report_modal',
562
+ * fields: [
563
+ * { customId: 'reason', label: 'Reason', type: 'paragraph', required: true, maxLength: 500 },
564
+ * ],
565
+ * })
566
+ *
567
+ * if (!submitted) {
568
+ * // User dismissed the modal or timed out — nothing to do
569
+ * return
570
+ * }
571
+ *
572
+ * const reason = submitted.modalData?.reason
573
+ * await client.interactions.respond(submitted.id, {
574
+ * content: `Report received: ${reason}`,
575
+ * ephemeral: true,
576
+ * })
577
+ * }
578
+ * })
579
+ */
580
+ awaitModal(interactionId: string, modal: BotModalDefinition, options?: {
581
+ timeout?: number;
582
+ }): Promise<Interaction | null>;
583
+ }
584
+
585
+ declare class PermissionsAPI {
586
+ private readonly http;
587
+ constructor(http: HttpClient);
588
+ /**
589
+ * Fetch the bot's effective permissions for a given scope.
590
+ *
591
+ * Permissions are merged in priority order:
592
+ * 1. Server-wide base permissions
593
+ * 2. Role override (if `roleId` provided)
594
+ * 3. Channel override (if `channelId` provided, highest priority)
595
+ *
596
+ * `serverId` is required. `channelId` and `roleId` are optional filters.
597
+ *
598
+ * @example
599
+ * // Effective server-wide permissions
600
+ * const { permissions } = await client.permissions.get({
601
+ * serverId: 'server-id',
602
+ * })
603
+ * console.log(permissions) // { kick_members: true, ban_members: false, ... }
604
+ *
605
+ * @example
606
+ * // Scoped to a specific channel
607
+ * const { permissions } = await client.permissions.get({
608
+ * serverId: 'server-id',
609
+ * channelId: 'channel-id',
610
+ * })
611
+ *
612
+ * @example
613
+ * // Scoped to a specific role
614
+ * const { permissions } = await client.permissions.get({
615
+ * serverId: 'server-id',
616
+ * roleId: 'role-id',
617
+ * })
618
+ */
619
+ get(options: PermissionsQueryOptions): Promise<PermissionsResult>;
462
620
  }
463
621
 
464
622
  /**
@@ -494,6 +652,8 @@ declare class NovaClient extends EventEmitter {
494
652
  readonly servers: ServersAPI;
495
653
  /** Acknowledge and respond to interactions. */
496
654
  readonly interactions: InteractionsAPI;
655
+ /** Query the bot's effective permissions within a server, channel, or role scope. */
656
+ readonly permissions: PermissionsAPI;
497
657
  private socket;
498
658
  private readonly http;
499
659
  private readonly options;
@@ -536,4 +696,4 @@ declare class NovaClient extends EventEmitter {
536
696
  emit(event: string | symbol, ...args: unknown[]): boolean;
537
697
  }
538
698
 
539
- export { type Attachment, type BotApplication, type BotEvent, type BotEventType, type BotUser, CommandsAPI, type ContextCommandDefinition, type EditMessageOptions, type Embed, type EmbedField, type FetchMembersOptions, type FetchMessagesOptions, HttpClient, type Interaction, type InteractionType, InteractionsAPI, type Member, MembersAPI, type Message, type MessageComponent, MessagesAPI, NovaClient, type NovaClientEvents, type NovaClientOptions, type NovaServer, type PollInteractionsOptions, type PrefixCommandDefinition, type Reaction, type RegisteredContextCommand, type RegisteredPrefixCommand, type RegisteredSlashCommand, type RespondInteractionOptions, type SendMessageOptions, ServersAPI, type SlashCommandDefinition, type SlashCommandOption };
699
+ export { type Attachment, type BotApplication, type BotEvent, type BotEventType, type BotModalDefinition, type BotModalField, type BotPermissionRecord, type BotUser, CommandsAPI, type ContextCommandDefinition, type EditMessageOptions, type Embed, type EmbedField, type FetchMembersOptions, type FetchMessagesOptions, HttpClient, type Interaction, type InteractionType, InteractionsAPI, type Member, MembersAPI, type Message, type MessageComponent, MessagesAPI, NovaClient, type NovaClientEvents, type NovaClientOptions, type NovaServer, PermissionsAPI, type PermissionsQueryOptions, type PermissionsResult, type PollInteractionsOptions, type PrefixCommandDefinition, type Reaction, type RegisteredContextCommand, type RegisteredPrefixCommand, type RegisteredSlashCommand, type RespondInteractionOptions, type SendMessageOptions, ServersAPI, type SlashCommandDefinition, type SlashCommandOption };
package/dist/index.d.ts CHANGED
@@ -128,12 +128,53 @@ interface Interaction {
128
128
  commandName?: string | null;
129
129
  customId?: string | null;
130
130
  data?: unknown;
131
+ /** For SELECT_MENU interactions — the chosen option values. */
132
+ values?: string[];
133
+ /** For MODAL_SUBMIT interactions — field customId → submitted value. */
134
+ modalData?: Record<string, string>;
131
135
  userId: string;
132
136
  channelId: string;
133
137
  serverId?: string | null;
134
138
  triggerMsgId?: string | null;
135
139
  createdAt: string;
136
140
  }
141
+ /** A single text input field inside a bot modal. */
142
+ interface BotModalField {
143
+ /** Unique field ID — returned as key in `modalData` on MODAL_SUBMIT. */
144
+ customId: string;
145
+ /** Label displayed above the input. */
146
+ label: string;
147
+ /** `'short'` = single-line input, `'paragraph'` = multi-line textarea. */
148
+ type: 'short' | 'paragraph';
149
+ placeholder?: string;
150
+ required?: boolean;
151
+ minLength?: number;
152
+ maxLength?: number;
153
+ /** Optional pre-filled default value. */
154
+ value?: string;
155
+ }
156
+ /**
157
+ * Pass this as `modal` inside `client.interactions.respond()` to open a
158
+ * modal dialog on the user's client instead of sending a message.
159
+ *
160
+ * @example
161
+ * await client.interactions.respond(interaction.id, {
162
+ * modal: {
163
+ * title: 'Submit a report',
164
+ * customId: 'report_modal',
165
+ * fields: [
166
+ * { customId: 'reason', label: 'Reason', type: 'paragraph', required: true },
167
+ * ],
168
+ * },
169
+ * })
170
+ */
171
+ interface BotModalDefinition {
172
+ /** Title shown at the top of the modal dialog. */
173
+ title: string;
174
+ /** Custom ID passed back with the MODAL_SUBMIT interaction. */
175
+ customId: string;
176
+ fields: BotModalField[];
177
+ }
137
178
  interface SlashCommandOption {
138
179
  name: string;
139
180
  description: string;
@@ -210,6 +251,11 @@ interface RespondInteractionOptions {
210
251
  embed?: Embed;
211
252
  components?: MessageComponent[];
212
253
  ephemeral?: boolean;
254
+ /**
255
+ * Open a modal dialog on the user's client instead of sending a message.
256
+ * When set, all other fields are ignored.
257
+ */
258
+ modal?: BotModalDefinition;
213
259
  }
214
260
  interface FetchMessagesOptions {
215
261
  limit?: number;
@@ -222,6 +268,39 @@ interface PollInteractionsOptions {
222
268
  limit?: number;
223
269
  since?: string;
224
270
  }
271
+ /** A raw permission record stored for a specific scope (server, role, or channel). */
272
+ interface BotPermissionRecord {
273
+ id: string;
274
+ botId: string;
275
+ serverId: string;
276
+ /** `null` means this record is server-wide (not channel-scoped). */
277
+ channelId: string | null;
278
+ /** `null` means this record applies to all roles. */
279
+ roleId: string | null;
280
+ /** Key-value map of permission name → granted flag. */
281
+ permissions: Record<string, boolean>;
282
+ createdAt: string;
283
+ updatedAt: string;
284
+ }
285
+ /** Response from `client.permissions.get()`. */
286
+ interface PermissionsResult {
287
+ /**
288
+ * Merged, effective permissions for the requested scope.
289
+ * Merging order (highest wins): channel > role > server-wide.
290
+ */
291
+ permissions: Record<string, boolean>;
292
+ /** All raw permission records matching the query scope. */
293
+ records: BotPermissionRecord[];
294
+ }
295
+ /** Options accepted by `client.permissions.get()`. */
296
+ interface PermissionsQueryOptions {
297
+ /** The server to query permissions for (required). */
298
+ serverId: string;
299
+ /** Narrow the scope to a specific channel. */
300
+ channelId?: string;
301
+ /** Narrow the scope to a specific role. */
302
+ roleId?: string;
303
+ }
225
304
  interface NovaClientOptions {
226
305
  /** Your bot token — starts with "nova_bot_" */
227
306
  token: string;
@@ -419,7 +498,8 @@ declare class ServersAPI {
419
498
 
420
499
  declare class InteractionsAPI {
421
500
  private readonly http;
422
- constructor(http: HttpClient);
501
+ private readonly emitter;
502
+ constructor(http: HttpClient, emitter: EventEmitter);
423
503
  /**
424
504
  * Acknowledge an interaction without sending a visible response.
425
505
  * Shows a loading state to the user. You must follow up with `respond()`.
@@ -459,6 +539,84 @@ declare class InteractionsAPI {
459
539
  * }
460
540
  */
461
541
  poll(options?: PollInteractionsOptions): Promise<Interaction[]>;
542
+ /**
543
+ * Open a modal for the user who triggered an interaction and await their submission.
544
+ *
545
+ * Internally this:
546
+ * 1. Calls `respond(interactionId, { modal })` to push the modal to the client UI.
547
+ * 2. Listens for the next `interactionCreate` event whose type is `MODAL_SUBMIT`
548
+ * and whose `customId` matches your modal's `customId`.
549
+ * 3. Resolves with the submitted `Interaction` (containing `modalData`), or `null`
550
+ * if the user closes the modal without submitting within the timeout.
551
+ *
552
+ * @param interactionId - ID of the triggering interaction.
553
+ * @param modal - Modal definition (title, customId, fields).
554
+ * @param options.timeout - Max milliseconds to wait (default: 300 000 = 5 min).
555
+ *
556
+ * @example
557
+ * client.on('interactionCreate', async (interaction) => {
558
+ * if (interaction.commandName === 'report') {
559
+ * const submitted = await client.interactions.awaitModal(interaction.id, {
560
+ * title: 'Submit a report',
561
+ * customId: 'report_modal',
562
+ * fields: [
563
+ * { customId: 'reason', label: 'Reason', type: 'paragraph', required: true, maxLength: 500 },
564
+ * ],
565
+ * })
566
+ *
567
+ * if (!submitted) {
568
+ * // User dismissed the modal or timed out — nothing to do
569
+ * return
570
+ * }
571
+ *
572
+ * const reason = submitted.modalData?.reason
573
+ * await client.interactions.respond(submitted.id, {
574
+ * content: `Report received: ${reason}`,
575
+ * ephemeral: true,
576
+ * })
577
+ * }
578
+ * })
579
+ */
580
+ awaitModal(interactionId: string, modal: BotModalDefinition, options?: {
581
+ timeout?: number;
582
+ }): Promise<Interaction | null>;
583
+ }
584
+
585
+ declare class PermissionsAPI {
586
+ private readonly http;
587
+ constructor(http: HttpClient);
588
+ /**
589
+ * Fetch the bot's effective permissions for a given scope.
590
+ *
591
+ * Permissions are merged in priority order:
592
+ * 1. Server-wide base permissions
593
+ * 2. Role override (if `roleId` provided)
594
+ * 3. Channel override (if `channelId` provided, highest priority)
595
+ *
596
+ * `serverId` is required. `channelId` and `roleId` are optional filters.
597
+ *
598
+ * @example
599
+ * // Effective server-wide permissions
600
+ * const { permissions } = await client.permissions.get({
601
+ * serverId: 'server-id',
602
+ * })
603
+ * console.log(permissions) // { kick_members: true, ban_members: false, ... }
604
+ *
605
+ * @example
606
+ * // Scoped to a specific channel
607
+ * const { permissions } = await client.permissions.get({
608
+ * serverId: 'server-id',
609
+ * channelId: 'channel-id',
610
+ * })
611
+ *
612
+ * @example
613
+ * // Scoped to a specific role
614
+ * const { permissions } = await client.permissions.get({
615
+ * serverId: 'server-id',
616
+ * roleId: 'role-id',
617
+ * })
618
+ */
619
+ get(options: PermissionsQueryOptions): Promise<PermissionsResult>;
462
620
  }
463
621
 
464
622
  /**
@@ -494,6 +652,8 @@ declare class NovaClient extends EventEmitter {
494
652
  readonly servers: ServersAPI;
495
653
  /** Acknowledge and respond to interactions. */
496
654
  readonly interactions: InteractionsAPI;
655
+ /** Query the bot's effective permissions within a server, channel, or role scope. */
656
+ readonly permissions: PermissionsAPI;
497
657
  private socket;
498
658
  private readonly http;
499
659
  private readonly options;
@@ -536,4 +696,4 @@ declare class NovaClient extends EventEmitter {
536
696
  emit(event: string | symbol, ...args: unknown[]): boolean;
537
697
  }
538
698
 
539
- export { type Attachment, type BotApplication, type BotEvent, type BotEventType, type BotUser, CommandsAPI, type ContextCommandDefinition, type EditMessageOptions, type Embed, type EmbedField, type FetchMembersOptions, type FetchMessagesOptions, HttpClient, type Interaction, type InteractionType, InteractionsAPI, type Member, MembersAPI, type Message, type MessageComponent, MessagesAPI, NovaClient, type NovaClientEvents, type NovaClientOptions, type NovaServer, type PollInteractionsOptions, type PrefixCommandDefinition, type Reaction, type RegisteredContextCommand, type RegisteredPrefixCommand, type RegisteredSlashCommand, type RespondInteractionOptions, type SendMessageOptions, ServersAPI, type SlashCommandDefinition, type SlashCommandOption };
699
+ export { type Attachment, type BotApplication, type BotEvent, type BotEventType, type BotModalDefinition, type BotModalField, type BotPermissionRecord, type BotUser, CommandsAPI, type ContextCommandDefinition, type EditMessageOptions, type Embed, type EmbedField, type FetchMembersOptions, type FetchMessagesOptions, HttpClient, type Interaction, type InteractionType, InteractionsAPI, type Member, MembersAPI, type Message, type MessageComponent, MessagesAPI, NovaClient, type NovaClientEvents, type NovaClientOptions, type NovaServer, PermissionsAPI, type PermissionsQueryOptions, type PermissionsResult, type PollInteractionsOptions, type PrefixCommandDefinition, type Reaction, type RegisteredContextCommand, type RegisteredPrefixCommand, type RegisteredSlashCommand, type RespondInteractionOptions, type SendMessageOptions, ServersAPI, type SlashCommandDefinition, type SlashCommandOption };
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  MembersAPI: () => MembersAPI,
27
27
  MessagesAPI: () => MessagesAPI,
28
28
  NovaClient: () => NovaClient,
29
+ PermissionsAPI: () => PermissionsAPI,
29
30
  ServersAPI: () => ServersAPI
30
31
  });
31
32
  module.exports = __toCommonJS(index_exports);
@@ -272,8 +273,9 @@ var ServersAPI = class {
272
273
 
273
274
  // src/api/interactions.ts
274
275
  var InteractionsAPI = class {
275
- constructor(http) {
276
+ constructor(http, emitter) {
276
277
  this.http = http;
278
+ this.emitter = emitter;
277
279
  }
278
280
  /**
279
281
  * Acknowledge an interaction without sending a visible response.
@@ -322,6 +324,115 @@ var InteractionsAPI = class {
322
324
  const qs = params.toString();
323
325
  return this.http.get(`/interactions${qs ? `?${qs}` : ""}`);
324
326
  }
327
+ /**
328
+ * Open a modal for the user who triggered an interaction and await their submission.
329
+ *
330
+ * Internally this:
331
+ * 1. Calls `respond(interactionId, { modal })` to push the modal to the client UI.
332
+ * 2. Listens for the next `interactionCreate` event whose type is `MODAL_SUBMIT`
333
+ * and whose `customId` matches your modal's `customId`.
334
+ * 3. Resolves with the submitted `Interaction` (containing `modalData`), or `null`
335
+ * if the user closes the modal without submitting within the timeout.
336
+ *
337
+ * @param interactionId - ID of the triggering interaction.
338
+ * @param modal - Modal definition (title, customId, fields).
339
+ * @param options.timeout - Max milliseconds to wait (default: 300 000 = 5 min).
340
+ *
341
+ * @example
342
+ * client.on('interactionCreate', async (interaction) => {
343
+ * if (interaction.commandName === 'report') {
344
+ * const submitted = await client.interactions.awaitModal(interaction.id, {
345
+ * title: 'Submit a report',
346
+ * customId: 'report_modal',
347
+ * fields: [
348
+ * { customId: 'reason', label: 'Reason', type: 'paragraph', required: true, maxLength: 500 },
349
+ * ],
350
+ * })
351
+ *
352
+ * if (!submitted) {
353
+ * // User dismissed the modal or timed out — nothing to do
354
+ * return
355
+ * }
356
+ *
357
+ * const reason = submitted.modalData?.reason
358
+ * await client.interactions.respond(submitted.id, {
359
+ * content: `Report received: ${reason}`,
360
+ * ephemeral: true,
361
+ * })
362
+ * }
363
+ * })
364
+ */
365
+ awaitModal(interactionId, modal, options = {}) {
366
+ const ms = options.timeout ?? 3e5;
367
+ let listener = null;
368
+ let timer = null;
369
+ const waitPromise = new Promise((resolve) => {
370
+ timer = setTimeout(() => {
371
+ if (listener) this.emitter.off("interactionCreate", listener);
372
+ resolve(null);
373
+ }, ms);
374
+ listener = (interaction) => {
375
+ if (interaction.type === "MODAL_SUBMIT" && interaction.customId === modal.customId) {
376
+ if (timer) clearTimeout(timer);
377
+ if (listener) this.emitter.off("interactionCreate", listener);
378
+ resolve(interaction);
379
+ }
380
+ };
381
+ this.emitter.on("interactionCreate", listener);
382
+ });
383
+ return this.respond(interactionId, { modal }).then(
384
+ () => waitPromise,
385
+ (err) => {
386
+ if (timer) clearTimeout(timer);
387
+ if (listener) this.emitter.off("interactionCreate", listener);
388
+ return Promise.reject(err);
389
+ }
390
+ );
391
+ }
392
+ };
393
+
394
+ // src/api/permissions.ts
395
+ var PermissionsAPI = class {
396
+ constructor(http) {
397
+ this.http = http;
398
+ }
399
+ /**
400
+ * Fetch the bot's effective permissions for a given scope.
401
+ *
402
+ * Permissions are merged in priority order:
403
+ * 1. Server-wide base permissions
404
+ * 2. Role override (if `roleId` provided)
405
+ * 3. Channel override (if `channelId` provided, highest priority)
406
+ *
407
+ * `serverId` is required. `channelId` and `roleId` are optional filters.
408
+ *
409
+ * @example
410
+ * // Effective server-wide permissions
411
+ * const { permissions } = await client.permissions.get({
412
+ * serverId: 'server-id',
413
+ * })
414
+ * console.log(permissions) // { kick_members: true, ban_members: false, ... }
415
+ *
416
+ * @example
417
+ * // Scoped to a specific channel
418
+ * const { permissions } = await client.permissions.get({
419
+ * serverId: 'server-id',
420
+ * channelId: 'channel-id',
421
+ * })
422
+ *
423
+ * @example
424
+ * // Scoped to a specific role
425
+ * const { permissions } = await client.permissions.get({
426
+ * serverId: 'server-id',
427
+ * roleId: 'role-id',
428
+ * })
429
+ */
430
+ get(options) {
431
+ const params = new URLSearchParams({ serverId: options.serverId });
432
+ if (options.channelId) params.set("channelId", options.channelId);
433
+ if (options.roleId) params.set("roleId", options.roleId);
434
+ return this.http.get(`/permissions?${params.toString()}`);
435
+ }
325
436
  };
326
437
 
327
438
  // src/client.ts
@@ -356,11 +467,12 @@ var NovaClient = class extends import_node_events.EventEmitter {
356
467
  this.commands = new CommandsAPI(this.http);
357
468
  this.members = new MembersAPI(this.http);
358
469
  this.servers = new ServersAPI(this.http);
359
- this.interactions = new InteractionsAPI(this.http);
470
+ this.interactions = new InteractionsAPI(this.http, this);
471
+ this.permissions = new PermissionsAPI(this.http);
360
472
  this.on("error", () => {
361
473
  });
362
474
  const cleanup = () => this.disconnect();
363
- process.once("exit", cleanup);
475
+ process.once("beforeExit", cleanup);
364
476
  process.once("SIGINT", () => {
365
477
  cleanup();
366
478
  process.exit(0);
@@ -430,12 +542,28 @@ var NovaClient = class extends import_node_events.EventEmitter {
430
542
  */
431
543
  disconnect() {
432
544
  if (this.socket) {
545
+ const sock = this.socket;
546
+ this.socket = null;
433
547
  try {
434
- this.socket.io.reconnection(false);
548
+ sock.io.reconnection(false);
549
+ } catch {
550
+ }
551
+ try {
552
+ sock.io?.engine?.socket?.terminate?.();
553
+ } catch {
554
+ }
555
+ try {
556
+ sock.io?.engine?.socket?.destroy?.();
557
+ } catch {
558
+ }
559
+ try {
560
+ sock.io?.engine?.close?.();
561
+ } catch {
562
+ }
563
+ try {
564
+ sock.disconnect();
435
565
  } catch {
436
566
  }
437
- this.socket.disconnect();
438
- this.socket = null;
439
567
  }
440
568
  }
441
569
  /**
@@ -484,5 +612,6 @@ var NovaClient = class extends import_node_events.EventEmitter {
484
612
  MembersAPI,
485
613
  MessagesAPI,
486
614
  NovaClient,
615
+ PermissionsAPI,
487
616
  ServersAPI
488
617
  });
package/dist/index.mjs CHANGED
@@ -240,8 +240,9 @@ var ServersAPI = class {
240
240
 
241
241
  // src/api/interactions.ts
242
242
  var InteractionsAPI = class {
243
- constructor(http) {
243
+ constructor(http, emitter) {
244
244
  this.http = http;
245
+ this.emitter = emitter;
245
246
  }
246
247
  /**
247
248
  * Acknowledge an interaction without sending a visible response.
@@ -290,6 +291,115 @@ var InteractionsAPI = class {
290
291
  const qs = params.toString();
291
292
  return this.http.get(`/interactions${qs ? `?${qs}` : ""}`);
292
293
  }
294
+ /**
295
+ * Open a modal for the user who triggered an interaction and await their submission.
296
+ *
297
+ * Internally this:
298
+ * 1. Calls `respond(interactionId, { modal })` to push the modal to the client UI.
299
+ * 2. Listens for the next `interactionCreate` event whose type is `MODAL_SUBMIT`
300
+ * and whose `customId` matches your modal's `customId`.
301
+ * 3. Resolves with the submitted `Interaction` (containing `modalData`), or `null`
302
+ * if the user closes the modal without submitting within the timeout.
303
+ *
304
+ * @param interactionId - ID of the triggering interaction.
305
+ * @param modal - Modal definition (title, customId, fields).
306
+ * @param options.timeout - Max milliseconds to wait (default: 300 000 = 5 min).
307
+ *
308
+ * @example
309
+ * client.on('interactionCreate', async (interaction) => {
310
+ * if (interaction.commandName === 'report') {
311
+ * const submitted = await client.interactions.awaitModal(interaction.id, {
312
+ * title: 'Submit a report',
313
+ * customId: 'report_modal',
314
+ * fields: [
315
+ * { customId: 'reason', label: 'Reason', type: 'paragraph', required: true, maxLength: 500 },
316
+ * ],
317
+ * })
318
+ *
319
+ * if (!submitted) {
320
+ * // User dismissed the modal or timed out — nothing to do
321
+ * return
322
+ * }
323
+ *
324
+ * const reason = submitted.modalData?.reason
325
+ * await client.interactions.respond(submitted.id, {
326
+ * content: `Report received: ${reason}`,
327
+ * ephemeral: true,
328
+ * })
329
+ * }
330
+ * })
331
+ */
332
+ awaitModal(interactionId, modal, options = {}) {
333
+ const ms = options.timeout ?? 3e5;
334
+ let listener = null;
335
+ let timer = null;
336
+ const waitPromise = new Promise((resolve) => {
337
+ timer = setTimeout(() => {
338
+ if (listener) this.emitter.off("interactionCreate", listener);
339
+ resolve(null);
340
+ }, ms);
341
+ listener = (interaction) => {
342
+ if (interaction.type === "MODAL_SUBMIT" && interaction.customId === modal.customId) {
343
+ if (timer) clearTimeout(timer);
344
+ if (listener) this.emitter.off("interactionCreate", listener);
345
+ resolve(interaction);
346
+ }
347
+ };
348
+ this.emitter.on("interactionCreate", listener);
349
+ });
350
+ return this.respond(interactionId, { modal }).then(
351
+ () => waitPromise,
352
+ (err) => {
353
+ if (timer) clearTimeout(timer);
354
+ if (listener) this.emitter.off("interactionCreate", listener);
355
+ return Promise.reject(err);
356
+ }
357
+ );
358
+ }
359
+ };
360
+
361
+ // src/api/permissions.ts
362
+ var PermissionsAPI = class {
363
+ constructor(http) {
364
+ this.http = http;
365
+ }
366
+ /**
367
+ * Fetch the bot's effective permissions for a given scope.
368
+ *
369
+ * Permissions are merged in priority order:
370
+ * 1. Server-wide base permissions
371
+ * 2. Role override (if `roleId` provided)
372
+ * 3. Channel override (if `channelId` provided, highest priority)
373
+ *
374
+ * `serverId` is required. `channelId` and `roleId` are optional filters.
375
+ *
376
+ * @example
377
+ * // Effective server-wide permissions
378
+ * const { permissions } = await client.permissions.get({
379
+ * serverId: 'server-id',
380
+ * })
381
+ * console.log(permissions) // { kick_members: true, ban_members: false, ... }
382
+ *
383
+ * @example
384
+ * // Scoped to a specific channel
385
+ * const { permissions } = await client.permissions.get({
386
+ * serverId: 'server-id',
387
+ * channelId: 'channel-id',
388
+ * })
389
+ *
390
+ * @example
391
+ * // Scoped to a specific role
392
+ * const { permissions } = await client.permissions.get({
393
+ * serverId: 'server-id',
394
+ * roleId: 'role-id',
395
+ * })
396
+ */
397
+ get(options) {
398
+ const params = new URLSearchParams({ serverId: options.serverId });
399
+ if (options.channelId) params.set("channelId", options.channelId);
400
+ if (options.roleId) params.set("roleId", options.roleId);
401
+ return this.http.get(`/permissions?${params.toString()}`);
402
+ }
293
403
  };
294
404
 
295
405
  // src/client.ts
@@ -324,11 +434,12 @@ var NovaClient = class extends EventEmitter {
324
434
  this.commands = new CommandsAPI(this.http);
325
435
  this.members = new MembersAPI(this.http);
326
436
  this.servers = new ServersAPI(this.http);
327
- this.interactions = new InteractionsAPI(this.http);
437
+ this.interactions = new InteractionsAPI(this.http, this);
438
+ this.permissions = new PermissionsAPI(this.http);
328
439
  this.on("error", () => {
329
440
  });
330
441
  const cleanup = () => this.disconnect();
331
- process.once("exit", cleanup);
442
+ process.once("beforeExit", cleanup);
332
443
  process.once("SIGINT", () => {
333
444
  cleanup();
334
445
  process.exit(0);
@@ -398,12 +509,28 @@ var NovaClient = class extends EventEmitter {
398
509
  */
399
510
  disconnect() {
400
511
  if (this.socket) {
512
+ const sock = this.socket;
513
+ this.socket = null;
401
514
  try {
402
- this.socket.io.reconnection(false);
515
+ sock.io.reconnection(false);
516
+ } catch {
517
+ }
518
+ try {
519
+ sock.io?.engine?.socket?.terminate?.();
520
+ } catch {
521
+ }
522
+ try {
523
+ sock.io?.engine?.socket?.destroy?.();
524
+ } catch {
525
+ }
526
+ try {
527
+ sock.io?.engine?.close?.();
528
+ } catch {
529
+ }
530
+ try {
531
+ sock.disconnect();
403
532
  } catch {
404
533
  }
405
- this.socket.disconnect();
406
- this.socket = null;
407
534
  }
408
535
  }
409
536
  /**
@@ -451,5 +578,6 @@ export {
451
578
  MembersAPI,
452
579
  MessagesAPI,
453
580
  NovaClient,
581
+ PermissionsAPI,
454
582
  ServersAPI
455
583
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaapp-sdk",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "Official SDK for building bots on the Nova platform",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",