rolespace 0.1.0 → 0.2.2
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/package.json +1 -1
- package/rolespace.js +678 -47
package/package.json
CHANGED
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('
|
|
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.
|
|
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,350 @@ 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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return this.
|
|
122
|
-
|
|
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));
|
|
342
|
+
}
|
|
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));
|
|
123
348
|
}
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
*
|
|
131
|
-
*
|
|
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
|
+
/** Recent messages, oldest → newest. Pass `before` (a message id) to page backwards. */
|
|
384
|
+
async listMessages(serverId, channelId, opts) {
|
|
385
|
+
opts = opts || {};
|
|
386
|
+
const limit = opts.limit || 50;
|
|
387
|
+
let url = `/servers/${serverId}/channels/${channelId}/messages?limit=${limit}`;
|
|
388
|
+
if (opts.before) url += `&before=${encodeURIComponent(opts.before)}`;
|
|
389
|
+
const resp = await this.get(url);
|
|
390
|
+
return _unwrapList(resp).map(m => new RolespaceMessage(m));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Edit the bot's own message. Returns the updated message. */
|
|
394
|
+
async editMessage(serverId, channelId, messageId, newContent) {
|
|
395
|
+
return new RolespaceMessage(await this.patch(
|
|
396
|
+
`/servers/${serverId}/channels/${channelId}/messages/${messageId}`,
|
|
397
|
+
{ content: newContent }));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Delete a message (own message OR any with ManageMessages). */
|
|
401
|
+
deleteMessage(serverId, channelId, messageId) {
|
|
402
|
+
return this.del(`/servers/${serverId}/channels/${channelId}/messages/${messageId}`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Pin a message. Requires ManageMessages. */
|
|
406
|
+
pinMessage(serverId, channelId, messageId) {
|
|
407
|
+
return this.put(`/servers/${serverId}/channels/${channelId}/messages/${messageId}/pin`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Unpin a message. Requires ManageMessages. */
|
|
411
|
+
unpinMessage(serverId, channelId, messageId) {
|
|
412
|
+
return this.del(`/servers/${serverId}/channels/${channelId}/messages/${messageId}/pin`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** React to a message. Emoji is URL-encoded automatically. */
|
|
416
|
+
addReaction(serverId, channelId, messageId, emoji) {
|
|
417
|
+
return this.put(`/servers/${serverId}/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Remove the bot's own reaction. */
|
|
421
|
+
removeReaction(serverId, channelId, messageId, emoji) {
|
|
422
|
+
return this.del(`/servers/${serverId}/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---- Channel + category management ─────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Create a channel.
|
|
429
|
+
* @param {object} opts
|
|
430
|
+
* @param {string} opts.type text | voice | announcement | forum | rules (default "text")
|
|
431
|
+
* @param {number} [opts.categoryId]
|
|
432
|
+
* @param {string} [opts.topic]
|
|
433
|
+
* @param {boolean} [opts.isPrivate]
|
|
434
|
+
*/
|
|
435
|
+
async createChannel(serverId, name, opts) {
|
|
436
|
+
opts = opts || {};
|
|
437
|
+
return new RolespaceChannel(await this.post(`/servers/${serverId}/channels`, {
|
|
438
|
+
name,
|
|
439
|
+
type: opts.type || 'text',
|
|
440
|
+
categoryId: opts.categoryId,
|
|
441
|
+
topic: opts.topic,
|
|
442
|
+
isPrivate: !!opts.isPrivate,
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Rename and/or change a channel's topic. Pass null/undefined for fields to leave alone. */
|
|
447
|
+
updateChannel(serverId, channelId, opts) {
|
|
448
|
+
opts = opts || {};
|
|
449
|
+
return this.patch(`/servers/${serverId}/channels/${channelId}`, {
|
|
450
|
+
name: opts.name, topic: opts.topic,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Delete a channel and its contents. Requires ManageChannels. */
|
|
455
|
+
deleteChannel(serverId, channelId) {
|
|
456
|
+
return this.del(`/servers/${serverId}/channels/${channelId}`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** Create a category. Requires ManageChannels. */
|
|
460
|
+
createCategory(serverId, name) {
|
|
461
|
+
return this.post(`/servers/${serverId}/categories`, { name });
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Rename a category. */
|
|
465
|
+
updateCategory(serverId, categoryId, name) {
|
|
466
|
+
return this.patch(`/servers/${serverId}/categories/${categoryId}`, { name });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Delete a category. With deleteChannels=true, also deletes every channel inside. */
|
|
470
|
+
deleteCategory(serverId, categoryId, deleteChannels) {
|
|
471
|
+
return this.del(`/servers/${serverId}/categories/${categoryId}?deleteChannels=${deleteChannels ? 'true' : 'false'}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ---- Member moderation ────────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
/** Kick a member. Requires KickMembers. */
|
|
477
|
+
kickMember(serverId, userId, reason) {
|
|
478
|
+
let url = `/servers/${serverId}/members/${userId}`;
|
|
479
|
+
if (reason) url += `?reason=${encodeURIComponent(reason)}`;
|
|
480
|
+
return this.del(url);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Ban a member. Requires BanMembers. */
|
|
484
|
+
banMember(serverId, userId, reason) {
|
|
485
|
+
return this.post(`/servers/${serverId}/members/${userId}/ban`, { reason: reason || null });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Lift a ban. */
|
|
489
|
+
unbanMember(serverId, userId) {
|
|
490
|
+
return this.del(`/servers/${serverId}/members/${userId}/ban`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Set or clear a member's server nickname. Pass null/empty to clear. */
|
|
494
|
+
setNickname(serverId, userId, nickname) {
|
|
495
|
+
return this.patch(`/servers/${serverId}/members/${userId}/nickname`, { nickname: nickname || null });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Assign a role to a member. Requires ManageRoles. */
|
|
499
|
+
assignRole(serverId, userId, roleId) {
|
|
500
|
+
return this.put(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Remove a role from a member. */
|
|
504
|
+
removeRole(serverId, userId, roleId) {
|
|
505
|
+
return this.del(`/servers/${serverId}/members/${userId}/roles/${roleId}`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- Forum threads ────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
/** List threads in a forum channel. */
|
|
511
|
+
async listThreads(serverId, channelId) {
|
|
512
|
+
const resp = await this.get(`/servers/${serverId}/channels/${channelId}/threads`);
|
|
513
|
+
return _unwrapList(resp);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Fetch a thread + its posts. */
|
|
517
|
+
getThread(serverId, channelId, threadId) {
|
|
518
|
+
return this.get(`/servers/${serverId}/channels/${channelId}/threads/${threadId}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Start a new thread in a forum channel. */
|
|
522
|
+
createThread(serverId, channelId, title, content, tags) {
|
|
523
|
+
return this.post(`/servers/${serverId}/channels/${channelId}/threads`,
|
|
524
|
+
{ title, content, tags: tags ? Array.from(tags) : null });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Reply in a thread. */
|
|
528
|
+
replyToThread(serverId, channelId, threadId, content, replyToPostId) {
|
|
529
|
+
return this.post(`/servers/${serverId}/channels/${channelId}/threads/${threadId}/posts`,
|
|
530
|
+
{ content, replyToPostId: replyToPostId || null });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ---- Streams (read) ───────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
/** The bot's own channel status: live, viewers, title, game, HLS URL. */
|
|
536
|
+
myStream() { return this.get('/streams/me'); }
|
|
537
|
+
|
|
538
|
+
/** The current live directory (public). */
|
|
539
|
+
async liveStreams() { return _unwrapList(await this.get('/streams/live')); }
|
|
540
|
+
|
|
541
|
+
/** Public live status for any account. */
|
|
542
|
+
streamFor(accountId) { return this.get(`/streams/${accountId}`); }
|
|
543
|
+
|
|
544
|
+
/** The bot's stream chat moderators. Owner-only. */
|
|
545
|
+
async streamModerators() { return _unwrapList(await this.get('/streams/me/moderators')); }
|
|
546
|
+
|
|
547
|
+
/** The bot's stream chat bans + timeouts. Owner-only. */
|
|
548
|
+
async streamBans() { return _unwrapList(await this.get('/streams/me/bans')); }
|
|
549
|
+
|
|
550
|
+
// ---- Stream moderation ────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
/** Promote a chat moderator on the bot's channel. */
|
|
553
|
+
addStreamModerator(accountId) {
|
|
554
|
+
return this.post('/streams/me/moderators', { accountId });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Demote a chat moderator. */
|
|
558
|
+
removeStreamModerator(accountId) {
|
|
559
|
+
return this.del(`/streams/me/moderators/${accountId}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** Permanently ban a chatter. */
|
|
563
|
+
banStreamChatter(accountId, reason) {
|
|
564
|
+
return this.post('/streams/me/bans', { accountId, reason: reason || null });
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Temporarily ban a chatter for `durationSeconds`. */
|
|
568
|
+
timeoutStreamChatter(accountId, durationSeconds, reason) {
|
|
569
|
+
return this.post('/streams/me/timeouts', { accountId, durationSeconds, reason: reason || null });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** Lift a ban or timeout. */
|
|
573
|
+
liftStreamBan(accountId) {
|
|
574
|
+
return this.del(`/streams/me/bans/${accountId}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Set chat mode (subscribers only, URL allow). */
|
|
578
|
+
updateStreamChatSettings(allowUrls, subscribersOnly) {
|
|
579
|
+
return this.patch('/streams/me/chat-settings', { allowUrls, subscribersOnly });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ---- Webhooks (outgoing — event delivery) ─────────────────────────────
|
|
583
|
+
|
|
584
|
+
/** List the bot's outgoing (event-delivery) webhooks. */
|
|
585
|
+
async listOutgoingWebhooks() { return _unwrapList(await this.get('/webhooks/outgoing')); }
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Register an outgoing webhook. Response includes a one-time `secret` — store it.
|
|
589
|
+
* @param {string} targetType "server" or "stream"
|
|
590
|
+
* @param {number} targetId server id, or bot's own account id for stream target
|
|
591
|
+
* @param {string} url public HTTPS endpoint receiving POSTs
|
|
592
|
+
* @param {string[]} events e.g. ["message.created"] or ["stream.online", "stream.offline"]
|
|
593
|
+
*/
|
|
594
|
+
createOutgoingWebhook(targetType, targetId, url, events) {
|
|
595
|
+
return this.post('/webhooks/outgoing', { targetType, targetId, url, events: Array.from(events) });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Delete an outgoing webhook. */
|
|
599
|
+
deleteOutgoingWebhook(webhookId) {
|
|
600
|
+
return this.del(`/webhooks/outgoing/${webhookId}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ---- Webhooks (incoming — post-to-channel URL) ────────────────────────
|
|
604
|
+
|
|
605
|
+
/** List the bot's incoming webhooks. */
|
|
606
|
+
async listIncomingWebhooks() { return _unwrapList(await this.get('/webhooks/incoming')); }
|
|
607
|
+
|
|
608
|
+
/** Create an incoming webhook bound to a channel. Response includes the one-time POST URL with its token. */
|
|
609
|
+
createIncomingWebhook(serverId, channelId, name) {
|
|
610
|
+
return this.post('/webhooks/incoming', { serverId, channelId, name: name || null });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Delete an incoming webhook. */
|
|
614
|
+
deleteIncomingWebhook(webhookId) {
|
|
615
|
+
return this.del(`/webhooks/incoming/${webhookId}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Post to an incoming webhook URL — anonymous (no bot token; the URL token is auth).
|
|
620
|
+
* Static so you can call it without instantiating a Rolespace client:
|
|
621
|
+
*
|
|
622
|
+
* await Rolespace.postIncomingWebhook(url, { content: 'Deploy done!' });
|
|
623
|
+
*/
|
|
624
|
+
static async postIncomingWebhook(webhookUrl, payload) {
|
|
625
|
+
try {
|
|
626
|
+
const r = await fetch(webhookUrl, {
|
|
627
|
+
method: 'POST',
|
|
628
|
+
headers: { 'Content-Type': 'application/json' },
|
|
629
|
+
body: JSON.stringify(payload),
|
|
630
|
+
});
|
|
631
|
+
return r.ok;
|
|
632
|
+
} catch { return false; }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Send a direct message.
|
|
637
|
+
* sendDM(recipientId, "text")
|
|
638
|
+
* sendDM(recipientId, "text", embed[, embed...])
|
|
639
|
+
* sendDM(recipientId, embed)
|
|
640
|
+
* sendDM(recipientId, { content, embeds, ... })
|
|
641
|
+
*/
|
|
642
|
+
async sendDM(recipientId, ...rest) {
|
|
643
|
+
const payload = _buildMessagePayload(rest);
|
|
644
|
+
payload.recipientId = recipientId;
|
|
645
|
+
return new RolespaceMessage(await this.post('/dm', payload));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ---- Interaction polling ─────────────────────────────────────────────
|
|
649
|
+
/**
|
|
650
|
+
* Async iterator over interactions. Yields RolespaceInteraction instances.
|
|
132
651
|
*
|
|
133
652
|
* for await (const ix of rs.interactions()) {
|
|
134
|
-
*
|
|
653
|
+
* if (ix.customId === 'book') {
|
|
654
|
+
* await rs.respond(ix.id, { type: 'message', content: `Hi ${ix.user.displayName}!` });
|
|
655
|
+
* }
|
|
135
656
|
* }
|
|
136
657
|
*
|
|
137
658
|
* @param {object} [opts]
|
|
138
|
-
* @param {number} [opts.idleDelayMs=1000]
|
|
139
|
-
* @param {AbortSignal} [opts.signal]
|
|
659
|
+
* @param {number} [opts.idleDelayMs=1000]
|
|
660
|
+
* @param {AbortSignal} [opts.signal]
|
|
140
661
|
*/
|
|
141
662
|
async *interactions(opts) {
|
|
142
663
|
opts = opts || {};
|
|
@@ -145,31 +666,92 @@ class Rolespace {
|
|
|
145
666
|
while (!(opts.signal && opts.signal.aborted)) {
|
|
146
667
|
const page = await this.get(`/interactions?after=${after}`);
|
|
147
668
|
const data = (page && page.data) || [];
|
|
148
|
-
for (const ix of data) yield ix;
|
|
669
|
+
for (const ix of data) yield new RolespaceInteraction(ix);
|
|
149
670
|
if (page && typeof page.lastId === 'number') after = page.lastId;
|
|
150
671
|
if (data.length === 0) await new Promise(r => setTimeout(r, idle));
|
|
151
672
|
}
|
|
152
673
|
}
|
|
153
674
|
|
|
154
|
-
/** Respond to an interaction. `reply` is
|
|
675
|
+
/** Respond to an interaction. `reply` is { type: 'message'|'update'|'modal'|'ack', ... }. */
|
|
155
676
|
respond(interactionId, reply) {
|
|
156
677
|
return this.post(`/interactions/${interactionId}/callback`, reply);
|
|
157
678
|
}
|
|
158
679
|
|
|
159
|
-
// ----
|
|
680
|
+
// ---- Listening for new messages in a channel ───────────────────────────
|
|
160
681
|
/**
|
|
161
|
-
*
|
|
682
|
+
* Async iterator that yields new RolespaceMessage objects as they appear in a channel.
|
|
683
|
+
* Wraps the polling loop, cursor bookkeeping, and graceful error backoff so you can write:
|
|
162
684
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
685
|
+
* for await (const msg of rs.watchMessages(serverId, channelId, { ownAccountId: me.bot.id })) {
|
|
686
|
+
* if (msg.content.startsWith('!ping')) {
|
|
687
|
+
* await rs.sendMessage(serverId, channelId, 'pong');
|
|
688
|
+
* }
|
|
689
|
+
* }
|
|
167
690
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
691
|
+
* For high-volume / production bots, prefer outgoing webhooks (push) over polling.
|
|
692
|
+
* Polling is fine for low-traffic channels, dev/testing, or environments where you
|
|
693
|
+
* can't expose a public HTTP receiver.
|
|
694
|
+
*
|
|
695
|
+
* @param {number} serverId
|
|
696
|
+
* @param {number} channelId
|
|
697
|
+
* @param {object} [opts]
|
|
698
|
+
* @param {number} [opts.idleDelayMs=2000] Wait between polls.
|
|
699
|
+
* @param {number} [opts.batchSize=50] Max messages per poll.
|
|
700
|
+
* @param {string|Date} [opts.since] Don't yield messages older than this timestamp.
|
|
701
|
+
* Defaults to "right now" so existing history is skipped.
|
|
702
|
+
* @param {boolean} [opts.includeOwn=false] Yield messages posted by THIS bot. Default skips them
|
|
703
|
+
* to avoid common feedback loops.
|
|
704
|
+
* @param {number|null} [opts.ownAccountId] Required if includeOwn=false — pass `(await rs.me()).bot.id`.
|
|
705
|
+
* @param {AbortSignal} [opts.signal] Cancellation.
|
|
706
|
+
* @param {function} [opts.onError] Called with an Error on each polling failure.
|
|
172
707
|
*/
|
|
708
|
+
async *watchMessages(serverId, channelId, opts) {
|
|
709
|
+
opts = opts || {};
|
|
710
|
+
const idle = opts.idleDelayMs || 2000;
|
|
711
|
+
const batchSize = opts.batchSize || 50;
|
|
712
|
+
let lastSeenTs;
|
|
713
|
+
if (opts.since) {
|
|
714
|
+
lastSeenTs = (opts.since instanceof Date) ? opts.since.toISOString() : String(opts.since);
|
|
715
|
+
} else {
|
|
716
|
+
// Bootstrap with the most-recent message timestamp (or "now") so we DON'T replay history.
|
|
717
|
+
try {
|
|
718
|
+
const seed = await this.get(`/servers/${serverId}/channels/${channelId}/messages?limit=1`);
|
|
719
|
+
const seedData = (seed && seed.data) || [];
|
|
720
|
+
lastSeenTs = seedData.length > 0
|
|
721
|
+
? seedData[seedData.length - 1].timestamp
|
|
722
|
+
: new Date().toISOString();
|
|
723
|
+
} catch (err) {
|
|
724
|
+
if (opts.onError) opts.onError(err);
|
|
725
|
+
lastSeenTs = new Date().toISOString();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
while (!(opts.signal && opts.signal.aborted)) {
|
|
730
|
+
try {
|
|
731
|
+
const page = await this.get(`/servers/${serverId}/channels/${channelId}/messages?limit=${batchSize}`);
|
|
732
|
+
const msgs = (page && page.data) || [];
|
|
733
|
+
// API returns oldest → newest within the page; iterate in order so we yield in order too.
|
|
734
|
+
for (const m of msgs) {
|
|
735
|
+
if (!m || !m.timestamp || m.timestamp <= lastSeenTs) continue;
|
|
736
|
+
if (!opts.includeOwn && opts.ownAccountId
|
|
737
|
+
&& m.author && Number(m.author.id) === Number(opts.ownAccountId)) {
|
|
738
|
+
lastSeenTs = m.timestamp;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
yield new RolespaceMessage(m);
|
|
742
|
+
lastSeenTs = m.timestamp;
|
|
743
|
+
}
|
|
744
|
+
} catch (err) {
|
|
745
|
+
if (opts.onError) opts.onError(err);
|
|
746
|
+
// Back off harder on transient failures so we don't hammer a flaky API.
|
|
747
|
+
await new Promise(r => setTimeout(r, Math.min(30000, idle * 4)));
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
await new Promise(r => setTimeout(r, idle));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// ---- Webhook signature verification ──────────────────────────────────
|
|
173
755
|
static verifyWebhook(rawBody, signatureHeader, secret) {
|
|
174
756
|
if (!rawBody || !signatureHeader || !secret) return false;
|
|
175
757
|
const expected = 'sha256=' + crypto.createHmac('sha256', secret)
|
|
@@ -181,10 +763,59 @@ class Rolespace {
|
|
|
181
763
|
}
|
|
182
764
|
}
|
|
183
765
|
|
|
184
|
-
// Don't leak the bot token through stringification
|
|
185
|
-
// reaches for the client object).
|
|
766
|
+
// Don't leak the bot token through stringification.
|
|
186
767
|
Object.defineProperty(Rolespace.prototype, 'toJSON', {
|
|
187
768
|
value() { return { baseUrl: this._baseUrl, token: '[redacted]' }; },
|
|
188
769
|
});
|
|
189
770
|
|
|
190
|
-
|
|
771
|
+
// ═════════════════════════ helpers ═════════════════════════════════════
|
|
772
|
+
|
|
773
|
+
/** Unwrap { data: [...] } responses; pass through bare arrays. */
|
|
774
|
+
function _unwrapList(resp) {
|
|
775
|
+
if (Array.isArray(resp)) return resp;
|
|
776
|
+
if (resp && Array.isArray(resp.data)) return resp.data;
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Build a message-shaped payload from the variadic tail of sendMessage / sendDM.
|
|
782
|
+
* Accepts: ["text"], ["text", embed, ...], [embed], [embed, embed], [{content, embeds, ...}].
|
|
783
|
+
*/
|
|
784
|
+
function _buildMessagePayload(rest) {
|
|
785
|
+
if (rest.length === 0) return { content: '' };
|
|
786
|
+
|
|
787
|
+
// Single object payload — anything not a string and not a RolespaceEmbed.
|
|
788
|
+
if (rest.length === 1) {
|
|
789
|
+
const arg = rest[0];
|
|
790
|
+
if (typeof arg === 'string') return { content: arg };
|
|
791
|
+
if (arg instanceof RolespaceEmbed) return { content: '', embeds: [arg.toJSON()] };
|
|
792
|
+
if (arg && typeof arg === 'object') return arg; // raw payload object
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Mixed: string text + N embeds, or N embeds with no text (caller passed embeds directly).
|
|
796
|
+
let content = '';
|
|
797
|
+
const embeds = [];
|
|
798
|
+
for (const arg of rest) {
|
|
799
|
+
if (typeof arg === 'string') content = arg;
|
|
800
|
+
else if (arg instanceof RolespaceEmbed) embeds.push(arg.toJSON());
|
|
801
|
+
else if (arg && typeof arg === 'object') Object.assign({}, arg); // ignore stray objects to keep behavior predictable
|
|
802
|
+
}
|
|
803
|
+
return embeds.length > 0 ? { content, embeds } : { content };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
module.exports = {
|
|
807
|
+
Rolespace,
|
|
808
|
+
RolespaceError,
|
|
809
|
+
RolespaceEmbed,
|
|
810
|
+
// Response wrappers exported so callers can `instanceof`-check or extend.
|
|
811
|
+
RolespaceObject,
|
|
812
|
+
RolespaceMe,
|
|
813
|
+
RolespaceUser,
|
|
814
|
+
RolespaceMember,
|
|
815
|
+
RolespaceServer,
|
|
816
|
+
RolespaceCategory,
|
|
817
|
+
RolespaceChannel,
|
|
818
|
+
RolespaceRole,
|
|
819
|
+
RolespaceMessage,
|
|
820
|
+
RolespaceInteraction,
|
|
821
|
+
};
|