rolespace 0.2.1 → 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 +326 -0
package/package.json
CHANGED
package/rolespace.js
CHANGED
|
@@ -380,6 +380,258 @@ class Rolespace {
|
|
|
380
380
|
return new RolespaceMessage(await this.get(`/servers/${serverId}/channels/${channelId}/messages/${messageId}`));
|
|
381
381
|
}
|
|
382
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
|
+
|
|
383
635
|
/**
|
|
384
636
|
* Send a direct message.
|
|
385
637
|
* sendDM(recipientId, "text")
|
|
@@ -425,6 +677,80 @@ class Rolespace {
|
|
|
425
677
|
return this.post(`/interactions/${interactionId}/callback`, reply);
|
|
426
678
|
}
|
|
427
679
|
|
|
680
|
+
// ---- Listening for new messages in a channel ───────────────────────────
|
|
681
|
+
/**
|
|
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:
|
|
684
|
+
*
|
|
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
|
+
* }
|
|
690
|
+
*
|
|
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.
|
|
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
|
+
|
|
428
754
|
// ---- Webhook signature verification ──────────────────────────────────
|
|
429
755
|
static verifyWebhook(rawBody, signatureHeader, secret) {
|
|
430
756
|
if (!rawBody || !signatureHeader || !secret) return false;
|