rolespace 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/rolespace.js +356 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rolespace",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Official Rolespace bot SDK for Node.js",
5
5
  "main": "rolespace.js",
6
6
  "engines": { "node": ">=18" },
package/rolespace.js CHANGED
@@ -5,25 +5,42 @@
5
5
  * Requires Node 18+ (uses built-in fetch + crypto).
6
6
  *
7
7
  * Quick start:
8
- * const { Rolespace } = require('./rolespace');
8
+ * const { Rolespace, RolespaceEmbed } = require('rolespace');
9
9
  * const rs = Rolespace.fromEnv(); // reads ROLESPACE_BOT_TOKEN
10
10
  * const me = await rs.me();
11
11
  * console.log(`Logged in as ${me.bot.username}`);
12
12
  *
13
+ * const msg = await rs.sendMessage(serverId, channelId, 'Hello!');
14
+ * console.log(msg.id, msg.content);
15
+ *
16
+ * const card = new RolespaceEmbed()
17
+ * .withTitle('Patch 2.4')
18
+ * .withColor('#5f85f7')
19
+ * .addField('Author', '@lynn', true);
20
+ * await rs.sendMessage(serverId, channelId, 'Heads up:', card);
21
+ *
13
22
  * What this SDK gives you that raw fetch doesn't:
23
+ * - Strongly-typed wrapper classes: msg.content instead of msg.content
24
+ * (yes, JS is loose anyway — but you get IntelliSense in TypeScript via JSDoc,
25
+ * plus the wrapper protects from missing fields throwing.)
26
+ * - RolespaceEmbed builder so you don't hand-shape JSON for rich cards
14
27
  * - Token is loaded from env by default (no hardcoded tokens in source)
15
28
  * - 429 rate-limit retries with exponential backoff + Retry-After
16
29
  * - Async iterator over interactions (no manual polling loop)
17
30
  * - Constant-time webhook signature verification (Rolespace.verifyWebhook)
18
31
  * - TLS verification is enforced; can only be disabled with an explicit, scary opt-in
19
32
  * - Authorization header is never logged
33
+ *
34
+ * Escape hatch: every typed wrapper exposes `.raw` — the underlying parsed JSON
35
+ * object — so any field the server adds before this SDK is updated still works:
36
+ * const newField = msg.raw.brandNewField;
20
37
  */
21
38
  'use strict';
22
39
 
23
40
  const crypto = require('crypto');
24
41
 
25
42
  const DEFAULT_BASE = 'https://rolespace.net';
26
- const SDK_VERSION = '0.1.0';
43
+ const SDK_VERSION = '0.2.0';
27
44
 
28
45
  class RolespaceError extends Error {
29
46
  constructor(message, status, body) {
@@ -34,13 +51,209 @@ class RolespaceError extends Error {
34
51
  }
35
52
  }
36
53
 
54
+ // ═══════════════════ Response wrapper base class ═══════════════════════
55
+ // All typed response wrappers extend this. Subclasses just declare getters
56
+ // over `this._raw` for the fields they care about. Missing fields return
57
+ // undefined (or sensible defaults from coerce helpers) instead of throwing.
58
+
59
+ class RolespaceObject {
60
+ constructor(raw) {
61
+ // Store the underlying parsed JSON so callers can always reach unmodelled fields.
62
+ Object.defineProperty(this, '_raw', { value: raw || {}, enumerable: false });
63
+ }
64
+ /** The underlying parsed JSON. Use for fields not yet modelled. */
65
+ get raw() { return this._raw; }
66
+
67
+ /** Sensible toJSON so JSON.stringify(wrapper) gives back the original shape. */
68
+ toJSON() { return this._raw; }
69
+ }
70
+
71
+ // ═══════════════════════════ /me ═══════════════════════════════════════
72
+
73
+ class RolespaceMe extends RolespaceObject {
74
+ /** The bot account — author of anything the bot does. */
75
+ get bot() { return new RolespaceUser(this._raw.bot || {}); }
76
+ /** The human owner, if visible. */
77
+ get owner() { return this._raw.owner ? new RolespaceUser(this._raw.owner) : null; }
78
+ /** Granted OAuth-style scopes. */
79
+ get scopes() { return (this._raw.application && this._raw.application.scopes) || []; }
80
+ /** Numeric id of the application registration. */
81
+ get applicationId() { return (this._raw.application && this._raw.application.id) || 0; }
82
+ }
83
+
84
+ // ═══════════════════════ Users / members ═══════════════════════════════
85
+
86
+ class RolespaceUser extends RolespaceObject {
87
+ /** Numeric account id. Same id everywhere in the API. */
88
+ get id() { return this._raw.id ?? this._raw.userId ?? 0; }
89
+ get username() { return this._raw.username || ''; }
90
+ get displayName() { return this._raw.displayName || ''; }
91
+ get nickname() { return this._raw.nickname || null; }
92
+ get avatarUrl() { return this._raw.avatar || this._raw.avatarUrl || null; }
93
+ }
94
+
95
+ class RolespaceMember extends RolespaceUser {
96
+ /** True if this member owns the server. */
97
+ get isOwner() { return !!this._raw.isOwner; }
98
+ /** Role ids assigned to this member in this server. */
99
+ get roleIds() { return this._raw.roleIds || []; }
100
+ }
101
+
102
+ // ═══════════════════════════ Servers ═══════════════════════════════════
103
+
104
+ class RolespaceServer extends RolespaceObject {
105
+ get id() { return this._raw.id || 0; }
106
+ get name() { return this._raw.name || ''; }
107
+ get description() { return this._raw.description || null; }
108
+ get iconUrl() { return this._raw.iconUrl || null; }
109
+ get bannerUrl() { return this._raw.bannerUrl || null; }
110
+ get isPublic() { return !!this._raw.isPublic; }
111
+ get ownerId() { return this._raw.ownerId || 0; }
112
+ get memberCount() { return this._raw.memberCount || 0; }
113
+ get createdAt() { return this._raw.createdAt || null; }
114
+ get categories() { return (this._raw.categories || []).map(c => new RolespaceCategory(c)); }
115
+ /** Flatten the category tree into a single channel list. */
116
+ allChannels() {
117
+ const out = [];
118
+ for (const cat of this.categories) for (const ch of cat.channels) out.push(ch);
119
+ return out;
120
+ }
121
+ }
122
+
123
+ class RolespaceCategory extends RolespaceObject {
124
+ get id() { return this._raw.id || 0; }
125
+ get name() { return this._raw.name || ''; }
126
+ get position() { return this._raw.position || 0; }
127
+ get channels() { return (this._raw.channels || []).map(c => new RolespaceChannel(c)); }
128
+ }
129
+
130
+ class RolespaceChannel extends RolespaceObject {
131
+ get id() { return this._raw.id || 0; }
132
+ get serverId() { return this._raw.serverId || 0; }
133
+ get categoryId() { return this._raw.categoryId || null; }
134
+ get categoryName() { return this._raw.categoryName || null; }
135
+ get name() { return this._raw.name || ''; }
136
+ get topic() { return this._raw.topic || null; }
137
+ /** Lowercase string: text, voice, announcement, forum, rules. */
138
+ get type() { return this._raw.type || ''; }
139
+ get position() { return this._raw.position || 0; }
140
+ get isNsfw() { return !!this._raw.isNsfw; }
141
+ get isPrivate() { return !!this._raw.isPrivate; }
142
+ }
143
+
144
+ class RolespaceRole extends RolespaceObject {
145
+ get id() { return this._raw.id || 0; }
146
+ get name() { return this._raw.name || ''; }
147
+ get color() { return this._raw.color || null; }
148
+ get position() { return this._raw.position || 0; }
149
+ get isEveryone() { return !!this._raw.isEveryone; }
150
+ get isDefault() { return !!this._raw.isDefault; }
151
+ get permissions() { return this._raw.permissions || {}; }
152
+ }
153
+
154
+ // ═══════════════════════════ Messages ══════════════════════════════════
155
+
156
+ class RolespaceMessage extends RolespaceObject {
157
+ /** String id (GUID). Message ids are strings, NOT numeric. */
158
+ get id() { return this._raw.id || ''; }
159
+ get channelId() { return this._raw.channelId || 0; }
160
+ get serverId() { return this._raw.serverId || 0; }
161
+ get content() { return this._raw.content || ''; }
162
+ get timestamp() { return this._raw.timestamp || null; }
163
+ get editedAt() { return this._raw.editedAt || null; }
164
+ get isPinned() { return !!this._raw.isPinned; }
165
+ get replyToMessageId() { return this._raw.replyToMessageId || null; }
166
+ get author() { return new RolespaceUser(this._raw.author || {}); }
167
+ get reactions() { return this._raw.reactions || []; }
168
+ get attachments() { return this._raw.attachments || []; }
169
+ }
170
+
171
+ // ═══════════════════════ Interactions ══════════════════════════════════
172
+
173
+ class RolespaceInteraction extends RolespaceObject {
174
+ /** Numeric interaction id. Pass to client.respond(...). */
175
+ get id() { return this._raw.id || 0; }
176
+ /** One of 'button', 'select', 'modal_submit'. */
177
+ get type() { return this._raw.type || ''; }
178
+ /** The customId the bot set on the component. Use this to dispatch. */
179
+ get customId() { return this._raw.customId || ''; }
180
+ get serverId() { return this._raw.serverId || 0; }
181
+ get channelId() { return this._raw.channelId || 0; }
182
+ /** String id of the source message. */
183
+ get messageId() { return this._raw.messageId || null; }
184
+ /** Who clicked / submitted. */
185
+ get user() { return new RolespaceUser(this._raw.user || {}); }
186
+ /** Extra payload — shape varies by type. */
187
+ get data() { return this._raw.data || null; }
188
+ get createdAt() { return this._raw.createdAt || null; }
189
+ }
190
+
191
+ // ════════════════════════ Embeds (rich cards) ══════════════════════════
192
+ //
193
+ // Outbound builder. Two styles work — pick whichever reads better:
194
+ //
195
+ // // Plain-object style:
196
+ // const card = new RolespaceEmbed({ title: 'Hi', color: '#5f85f7' });
197
+ // card.addField('Author', '@lynn', true);
198
+ //
199
+ // // Fluent builder style:
200
+ // const card = new RolespaceEmbed()
201
+ // .withTitle('Hi')
202
+ // .withColor('#5f85f7')
203
+ // .addField('Author', '@lynn', true);
204
+ //
205
+ // Pass to sendMessage as the last (or only) argument.
206
+
207
+ class RolespaceEmbed {
208
+ constructor(initial) {
209
+ // Copy whatever the caller passed in; everything optional.
210
+ Object.assign(this, initial || {});
211
+ }
212
+
213
+ withTitle(title) { this.title = title; return this; }
214
+ withUrl(url) { this.url = url; return this; }
215
+ withDescription(desc) { this.description = desc; return this; }
216
+ /** Hex string like "#5f85f7". */
217
+ withColor(hex) { this.color = hex; return this; }
218
+ /** flag: optional "nsfw" | "triggering" | "spoiler" — render blurred behind a click-to-reveal cover. */
219
+ withImage(url, flag) { this.image = url; if (flag) this.imageFlag = flag; return this; }
220
+ withThumbnail(url) { this.thumbnail = url; return this; }
221
+ withAuthor(name, iconUrl) {
222
+ this.author = iconUrl ? { name, iconUrl } : { name };
223
+ return this;
224
+ }
225
+ withFooter(text) { this.footer = text; return this; }
226
+
227
+ /** Append an inline name/value field. Up to 25 per embed. */
228
+ addField(name, value, inline = false) {
229
+ (this.fields ||= []).push({ name, value, inline });
230
+ return this;
231
+ }
232
+
233
+ /** Append a gallery image. Up to 24 per embed. */
234
+ addGalleryImage(url, flag) {
235
+ (this.gallery ||= []).push(flag ? { url, flag } : url);
236
+ return this;
237
+ }
238
+
239
+ /** Used by JSON.stringify — strips internal stuff. (None here, but kept for future.) */
240
+ toJSON() {
241
+ const out = {};
242
+ for (const k of Object.keys(this)) {
243
+ if (this[k] !== undefined && this[k] !== null) out[k] = this[k];
244
+ }
245
+ return out;
246
+ }
247
+ }
248
+
249
+ // ════════════════════════ Main client ══════════════════════════════════
250
+
37
251
  class Rolespace {
38
252
  /**
39
253
  * @param {object} opts
40
254
  * @param {string} opts.token - Bot token (rsp_*). Required.
41
255
  * @param {string} [opts.baseUrl] - API base URL (default: https://rolespace.net).
42
256
  * @param {number} [opts.maxRetries] - Max 429 retries before giving up (default: 5).
43
- * @param {boolean} [opts.dangerouslyDisableTls] - Opt-out of cert verification. Do not use.
44
257
  */
45
258
  constructor(opts) {
46
259
  if (!opts || typeof opts.token !== 'string' || !opts.token.startsWith('rsp_')) {
@@ -49,25 +262,16 @@ class Rolespace {
49
262
  this._token = opts.token;
50
263
  this._baseUrl = (opts.baseUrl || DEFAULT_BASE).replace(/\/+$/, '');
51
264
  this._maxRetries = Number.isInteger(opts.maxRetries) ? opts.maxRetries : 5;
52
- this._dangerouslyDisableTls = opts.dangerouslyDisableTls === true;
53
- if (this._dangerouslyDisableTls) {
54
- // Mirror the user agent of `requests`/`HttpClient` — make this visible.
55
- // We don't actually disable TLS unless they ALSO set NODE_TLS_REJECT_UNAUTHORIZED=0,
56
- // because we refuse to do it for them. This flag only suppresses our warning.
57
- }
58
265
  }
59
266
 
60
267
  /** Build a client from env vars. Reads ROLESPACE_BOT_TOKEN and optional ROLESPACE_API_BASE. */
61
268
  static fromEnv() {
62
269
  const token = process.env.ROLESPACE_BOT_TOKEN;
63
- if (!token) {
64
- throw new Error('Rolespace.fromEnv: set ROLESPACE_BOT_TOKEN in your environment');
65
- }
270
+ if (!token) throw new Error('Rolespace.fromEnv: set ROLESPACE_BOT_TOKEN in your environment');
66
271
  return new Rolespace({ token, baseUrl: process.env.ROLESPACE_API_BASE });
67
272
  }
68
273
 
69
- // ---- HTTP primitives ----
70
- /** Make a raw request. Most callers should use get/post/patch/del or the typed helpers. */
274
+ // ---- HTTP primitives ─────────────────────────────────────────────────
71
275
  async request(method, path, body) {
72
276
  const url = path.startsWith('http') ? path : this._baseUrl + (path.startsWith('/') ? path : '/api/v1/' + path);
73
277
  const headers = {
@@ -84,7 +288,6 @@ class Rolespace {
84
288
  let attempt = 0;
85
289
  while (true) {
86
290
  const res = await fetch(url, init);
87
- // 429 → wait Retry-After (or exponential backoff) and try again.
88
291
  if (res.status === 429 && attempt < this._maxRetries) {
89
292
  const ra = parseFloat(res.headers.get('retry-after') || '0');
90
293
  const waitMs = ra > 0 ? ra * 1000 : Math.min(30000, 500 * Math.pow(2, attempt));
@@ -111,32 +314,98 @@ class Rolespace {
111
314
  put(path, body) { return this.request('PUT', path, body ?? {}); }
112
315
  del(path) { return this.request('DELETE', path); }
113
316
 
114
- // ---- Typed convenience helpers ----
115
- me() { return this.get('/me'); }
116
- servers() { return this.get('/servers'); }
117
- server(id) { return this.get(`/servers/${id}`); }
118
- serverChannels(id) { return this.get(`/servers/${id}/channels`); }
119
- serverMembers(id) { return this.get(`/servers/${id}/members`); }
120
- sendMessage(serverId, channelId, payload) {
121
- return this.post(`/servers/${serverId}/channels/${channelId}/messages`,
122
- typeof payload === 'string' ? { content: payload } : payload);
317
+ // ---- Typed convenience helpers ───────────────────────────────────────
318
+ // Each wraps the raw response in a typed class so callers get autocompleted
319
+ // accessors (msg.content) instead of raw dict access (msg.content but also
320
+ // msg.weirdMisspelling that silently returns undefined).
321
+
322
+ /** The authenticated application + bot identity + owner + scopes. */
323
+ async me() {
324
+ return new RolespaceMe(await this.get('/me'));
325
+ }
326
+
327
+ /** All servers the bot has been added to (summary objects). */
328
+ async servers() {
329
+ const resp = await this.get('/servers');
330
+ return _unwrapList(resp).map(s => new RolespaceServer(s));
331
+ }
332
+
333
+ /** One server with its categories and visible channels. */
334
+ async server(id) {
335
+ return new RolespaceServer(await this.get(`/servers/${id}`));
336
+ }
337
+
338
+ /** Flat list of channels the bot can view in the server. */
339
+ async serverChannels(id) {
340
+ const resp = await this.get(`/servers/${id}/channels`);
341
+ return _unwrapList(resp).map(c => new RolespaceChannel(c));
123
342
  }
124
- sendDM(recipientId, payload) {
125
- return this.post('/dm', { recipientId, ...(typeof payload === 'string' ? { content: payload } : payload) });
343
+
344
+ /** All members of the server. */
345
+ async serverMembers(id) {
346
+ const resp = await this.get(`/servers/${id}/members`);
347
+ return _unwrapList(resp).map(m => new RolespaceMember(m));
348
+ }
349
+
350
+ /** One member of the server. */
351
+ async serverMember(serverId, userId) {
352
+ return new RolespaceMember(await this.get(`/servers/${serverId}/members/${userId}`));
353
+ }
354
+
355
+ /** All roles in the server, highest position first. */
356
+ async serverRoles(id) {
357
+ const resp = await this.get(`/servers/${id}/roles`);
358
+ return _unwrapList(resp).map(r => new RolespaceRole(r));
126
359
  }
127
360
 
128
- // ---- Interaction polling ----
129
361
  /**
130
- * Async iterator over interactions. Resolves the polling loop, backoff, and
131
- * cursor management for you. Use with `for await`:
362
+ * Send a message.
363
+ *
364
+ * Signatures:
365
+ * sendMessage(serverId, channelId, "plain text")
366
+ * sendMessage(serverId, channelId, "text", embed) // RolespaceEmbed
367
+ * sendMessage(serverId, channelId, "text", embed1, embed2) // multiple embeds
368
+ * sendMessage(serverId, channelId, embed) // embed only, no text
369
+ * sendMessage(serverId, channelId, { content, embeds, components, replyToMessageId })
370
+ *
371
+ * Returns a RolespaceMessage.
372
+ */
373
+ async sendMessage(serverId, channelId, ...rest) {
374
+ const payload = _buildMessagePayload(rest);
375
+ return new RolespaceMessage(await this.post(`/servers/${serverId}/channels/${channelId}/messages`, payload));
376
+ }
377
+
378
+ /** Fetch a single message by id. */
379
+ async getMessage(serverId, channelId, messageId) {
380
+ return new RolespaceMessage(await this.get(`/servers/${serverId}/channels/${channelId}/messages/${messageId}`));
381
+ }
382
+
383
+ /**
384
+ * Send a direct message.
385
+ * sendDM(recipientId, "text")
386
+ * sendDM(recipientId, "text", embed[, embed...])
387
+ * sendDM(recipientId, embed)
388
+ * sendDM(recipientId, { content, embeds, ... })
389
+ */
390
+ async sendDM(recipientId, ...rest) {
391
+ const payload = _buildMessagePayload(rest);
392
+ payload.recipientId = recipientId;
393
+ return new RolespaceMessage(await this.post('/dm', payload));
394
+ }
395
+
396
+ // ---- Interaction polling ─────────────────────────────────────────────
397
+ /**
398
+ * Async iterator over interactions. Yields RolespaceInteraction instances.
132
399
  *
133
400
  * for await (const ix of rs.interactions()) {
134
- * await rs.respond(ix.id, { type: 'message', content: 'hi', ephemeral: true });
401
+ * if (ix.customId === 'book') {
402
+ * await rs.respond(ix.id, { type: 'message', content: `Hi ${ix.user.displayName}!` });
403
+ * }
135
404
  * }
136
405
  *
137
406
  * @param {object} [opts]
138
- * @param {number} [opts.idleDelayMs=1000] - Wait between empty polls.
139
- * @param {AbortSignal} [opts.signal] - Optional cancellation.
407
+ * @param {number} [opts.idleDelayMs=1000]
408
+ * @param {AbortSignal} [opts.signal]
140
409
  */
141
410
  async *interactions(opts) {
142
411
  opts = opts || {};
@@ -145,31 +414,18 @@ class Rolespace {
145
414
  while (!(opts.signal && opts.signal.aborted)) {
146
415
  const page = await this.get(`/interactions?after=${after}`);
147
416
  const data = (page && page.data) || [];
148
- for (const ix of data) yield ix;
417
+ for (const ix of data) yield new RolespaceInteraction(ix);
149
418
  if (page && typeof page.lastId === 'number') after = page.lastId;
150
419
  if (data.length === 0) await new Promise(r => setTimeout(r, idle));
151
420
  }
152
421
  }
153
422
 
154
- /** Respond to an interaction. `reply` is one of: { type: 'message' | 'update' | 'modal' | 'ack', ... }. */
423
+ /** Respond to an interaction. `reply` is { type: 'message'|'update'|'modal'|'ack', ... }. */
155
424
  respond(interactionId, reply) {
156
425
  return this.post(`/interactions/${interactionId}/callback`, reply);
157
426
  }
158
427
 
159
- // ---- Webhook signature verification ----
160
- /**
161
- * Verify an X-Rolespace-Signature header against a raw request body.
162
- *
163
- * IMPORTANT: pass the RAW body buffer/string, NOT the parsed JSON. If your
164
- * server parsed the JSON first the byte order changed and the signature
165
- * will never match. With Express, use `app.use(express.raw({ type: '*\/*' }))`
166
- * on the webhook route.
167
- *
168
- * @param {Buffer|string} rawBody
169
- * @param {string} signatureHeader - Value of X-Rolespace-Signature (e.g. "sha256=abc...")
170
- * @param {string} secret - Shared signing secret you got when you registered the webhook.
171
- * @returns {boolean}
172
- */
428
+ // ---- Webhook signature verification ──────────────────────────────────
173
429
  static verifyWebhook(rawBody, signatureHeader, secret) {
174
430
  if (!rawBody || !signatureHeader || !secret) return false;
175
431
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
@@ -181,10 +437,59 @@ class Rolespace {
181
437
  }
182
438
  }
183
439
 
184
- // Don't leak the bot token through stringification (e.g. when a logger
185
- // reaches for the client object).
440
+ // Don't leak the bot token through stringification.
186
441
  Object.defineProperty(Rolespace.prototype, 'toJSON', {
187
442
  value() { return { baseUrl: this._baseUrl, token: '[redacted]' }; },
188
443
  });
189
444
 
190
- module.exports = { Rolespace, RolespaceError };
445
+ // ═════════════════════════ helpers ═════════════════════════════════════
446
+
447
+ /** Unwrap { data: [...] } responses; pass through bare arrays. */
448
+ function _unwrapList(resp) {
449
+ if (Array.isArray(resp)) return resp;
450
+ if (resp && Array.isArray(resp.data)) return resp.data;
451
+ return [];
452
+ }
453
+
454
+ /**
455
+ * Build a message-shaped payload from the variadic tail of sendMessage / sendDM.
456
+ * Accepts: ["text"], ["text", embed, ...], [embed], [embed, embed], [{content, embeds, ...}].
457
+ */
458
+ function _buildMessagePayload(rest) {
459
+ if (rest.length === 0) return { content: '' };
460
+
461
+ // Single object payload — anything not a string and not a RolespaceEmbed.
462
+ if (rest.length === 1) {
463
+ const arg = rest[0];
464
+ if (typeof arg === 'string') return { content: arg };
465
+ if (arg instanceof RolespaceEmbed) return { content: '', embeds: [arg.toJSON()] };
466
+ if (arg && typeof arg === 'object') return arg; // raw payload object
467
+ }
468
+
469
+ // Mixed: string text + N embeds, or N embeds with no text (caller passed embeds directly).
470
+ let content = '';
471
+ const embeds = [];
472
+ for (const arg of rest) {
473
+ if (typeof arg === 'string') content = arg;
474
+ else if (arg instanceof RolespaceEmbed) embeds.push(arg.toJSON());
475
+ else if (arg && typeof arg === 'object') Object.assign({}, arg); // ignore stray objects to keep behavior predictable
476
+ }
477
+ return embeds.length > 0 ? { content, embeds } : { content };
478
+ }
479
+
480
+ module.exports = {
481
+ Rolespace,
482
+ RolespaceError,
483
+ RolespaceEmbed,
484
+ // Response wrappers exported so callers can `instanceof`-check or extend.
485
+ RolespaceObject,
486
+ RolespaceMe,
487
+ RolespaceUser,
488
+ RolespaceMember,
489
+ RolespaceServer,
490
+ RolespaceCategory,
491
+ RolespaceChannel,
492
+ RolespaceRole,
493
+ RolespaceMessage,
494
+ RolespaceInteraction,
495
+ };