@openmarket/rooms-client 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/armed-mode.js +7 -3
- package/dist/agent/clients/common.d.ts +5 -0
- package/dist/agent/topic-inbox.d.ts +6 -0
- package/dist/agent/topic-inbox.js +20 -5
- package/dist/shared/rooms-protocol.d.ts +82 -0
- package/dist/social-client.d.ts +174 -5
- package/dist/social-client.js +341 -5
- package/dist/social-types.d.ts +164 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent/armed-mode.ts +7 -3
- package/src/agent/clients/common.ts +5 -0
- package/src/agent/topic-inbox.ts +23 -5
- package/src/shared/rooms-protocol.ts +85 -0
- package/src/social-client.ts +567 -6
- package/src/social-types.ts +175 -1
- package/src/types.ts +3 -0
package/dist/social-client.js
CHANGED
|
@@ -419,10 +419,29 @@ export async function addSpaceMember(config, spaceId, input) {
|
|
|
419
419
|
const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/members`, { method: "POST", body: input });
|
|
420
420
|
return result.member;
|
|
421
421
|
}
|
|
422
|
-
/** Create (or refresh) a pending invitation. Membership needs their accept.
|
|
422
|
+
/** Create (or refresh) a pending invitation. Membership needs their accept.
|
|
423
|
+
* `expiresAt` (ISO date, future, at most 30 days out) puts a deadline on
|
|
424
|
+
* it; re-inviting without one clears any previous deadline. Fail-closed on
|
|
425
|
+
* servers older than 0.6.0: they persist the invite but drop the deadline,
|
|
426
|
+
* so a deadline the echo omits revokes the invite and throws. */
|
|
423
427
|
export async function createSpaceInvite(config, spaceId, input) {
|
|
424
428
|
const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/invites`, { method: "POST", body: input });
|
|
425
|
-
|
|
429
|
+
const invite = result.invite;
|
|
430
|
+
// Presence check, not equality: servers may normalize the ISO string.
|
|
431
|
+
// declineSpaceInvite doubles as the manager-side revoke (the store
|
|
432
|
+
// deletes the row for invitee OR manager), so the inviter can retract
|
|
433
|
+
// the invite it just created.
|
|
434
|
+
if (input.expiresAt !== undefined && invite && typeof invite.expiresAt !== "string") {
|
|
435
|
+
const details = { spaceId, inviteId: invite.inviteId, requestedExpiresAt: input.expiresAt };
|
|
436
|
+
try {
|
|
437
|
+
await declineSpaceInvite(config, invite.inviteId);
|
|
438
|
+
}
|
|
439
|
+
catch (revokeError) {
|
|
440
|
+
throw new RoomSocialError("Server does not support invite expiry, and revoking the invite failed: a standing (never-expiring) invite may be live", 502, { ...details, revokeError: revokeError.message });
|
|
441
|
+
}
|
|
442
|
+
throw new RoomSocialError("Server does not support invite expiry; the standing invite was revoked", 502, details);
|
|
443
|
+
}
|
|
444
|
+
return invite;
|
|
426
445
|
}
|
|
427
446
|
/** The caller's own pending invitations. */
|
|
428
447
|
export async function listMySpaceInvites(config) {
|
|
@@ -443,11 +462,46 @@ export async function declineSpaceInvite(config, inviteId) {
|
|
|
443
462
|
method: "POST",
|
|
444
463
|
});
|
|
445
464
|
}
|
|
446
|
-
/** Mint (or rotate) the shareable join code; plaintext comes back once.
|
|
447
|
-
|
|
448
|
-
|
|
465
|
+
/** Mint (or rotate) the shareable join code; plaintext comes back once.
|
|
466
|
+
* Optional caps: `expiresAt` (ISO date, future, at most 30 days out) and
|
|
467
|
+
* `maxUses` (1..10000). Rotation is a fresh grant: the new code starts at
|
|
468
|
+
* zero uses and takes exactly the caps given here (omitted caps clear the
|
|
469
|
+
* previous ones). Servers older than 0.6.0 echo neither cap field; they
|
|
470
|
+
* mint the code but drop the caps, so a requested cap the echo omits
|
|
471
|
+
* fail-closes: the fresh code is revoked and the call throws rather than
|
|
472
|
+
* hand back a live UNLIMITED code the caller believes capped. */
|
|
473
|
+
export async function setSpaceJoinCode(config, spaceId, options = {}) {
|
|
474
|
+
const body = {};
|
|
475
|
+
if (options.expiresAt !== undefined)
|
|
476
|
+
body.expiresAt = options.expiresAt;
|
|
477
|
+
if (options.maxUses !== undefined)
|
|
478
|
+
body.maxUses = options.maxUses;
|
|
479
|
+
const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/join-code`, {
|
|
449
480
|
method: "PUT",
|
|
481
|
+
// Capless mints keep the historical bodiless PUT on the wire.
|
|
482
|
+
...(Object.keys(body).length > 0 ? { body } : {}),
|
|
450
483
|
});
|
|
484
|
+
// Presence check for expiresAt (servers may normalize the ISO string),
|
|
485
|
+
// numeric equality for maxUses.
|
|
486
|
+
const droppedExpiry = body.expiresAt !== undefined && typeof result.expiresAt !== "string";
|
|
487
|
+
const droppedMaxUses = body.maxUses !== undefined && result.maxUses !== body.maxUses;
|
|
488
|
+
if (droppedExpiry || droppedMaxUses) {
|
|
489
|
+
// Details deliberately omit the plaintext code: RoomSocialError.details
|
|
490
|
+
// ends up in logs.
|
|
491
|
+
const details = {
|
|
492
|
+
spaceId,
|
|
493
|
+
requested: body,
|
|
494
|
+
echoed: { expiresAt: result.expiresAt ?? null, maxUses: result.maxUses ?? null },
|
|
495
|
+
};
|
|
496
|
+
try {
|
|
497
|
+
await clearSpaceJoinCode(config, spaceId);
|
|
498
|
+
}
|
|
499
|
+
catch (revokeError) {
|
|
500
|
+
throw new RoomSocialError("Server does not support join-code caps, and revoking the code failed: an uncapped join code may be live", 502, { ...details, revokeError: revokeError.message });
|
|
501
|
+
}
|
|
502
|
+
throw new RoomSocialError("Server does not support join-code caps; the uncapped code was revoked", 502, details);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
451
505
|
}
|
|
452
506
|
export async function clearSpaceJoinCode(config, spaceId) {
|
|
453
507
|
return socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/join-code`, {
|
|
@@ -651,6 +705,288 @@ export async function listRoomNotifications(config, options = {}) {
|
|
|
651
705
|
lastCursorId: result.lastCursorId ?? null,
|
|
652
706
|
};
|
|
653
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* Mark a room unread from `seq` (Discord-style): the room-wide read cursor
|
|
710
|
+
* drops to seq - 1 (server-clamped at 0) so the unread divider and pill
|
|
711
|
+
* return. The one sanctioned cursor-regression path; mention rows an
|
|
712
|
+
* earlier ack swept stay swept, and nothing is re-notified.
|
|
713
|
+
*/
|
|
714
|
+
export async function markRoomUnread(config, room, seq) {
|
|
715
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/read/unread`, { method: "POST", body: { seq } });
|
|
716
|
+
return { success: result.success === true };
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Set the caller's room-level attention over REST, the path that carries
|
|
720
|
+
* mute-with-duration: `untilMs` (epoch ms, valid only with 'mute') stamps
|
|
721
|
+
* the expiry that read state later surfaces as `muteUntil`; the mute lapses
|
|
722
|
+
* server-side and reads as 'default' afterwards. Every write settles the
|
|
723
|
+
* expiry, so follow/default (and a plain permanent mute) clear a previous
|
|
724
|
+
* one. The pinned ROOM_SET_ATTENTION frame (setRoomAttentionOnClient)
|
|
725
|
+
* predates `untilMs` and stays the live-session path for permanent
|
|
726
|
+
* postures; servers older than mute-with-duration strip the field and land
|
|
727
|
+
* the mute permanent, so a requested `untilMs` the echo omits fail-closes:
|
|
728
|
+
* the attention is reset and the call throws rather than leave an
|
|
729
|
+
* unbounded mute the caller believes timed. Echoes the persisted
|
|
730
|
+
* attention; `muteUntil` is the ISO expiry, null when none stands.
|
|
731
|
+
*/
|
|
732
|
+
export async function setRoomAttentionRest(config, room, attention, options = {}) {
|
|
733
|
+
const body = { attention };
|
|
734
|
+
if (options.untilMs !== undefined)
|
|
735
|
+
body.untilMs = options.untilMs;
|
|
736
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/attention`, {
|
|
737
|
+
method: "POST",
|
|
738
|
+
body,
|
|
739
|
+
});
|
|
740
|
+
const muteUntil = typeof result.muteUntil === "string" ? result.muteUntil : null;
|
|
741
|
+
if (options.untilMs !== undefined && muteUntil === null) {
|
|
742
|
+
// The undo cannot know the caller's prior attention, so it deliberately
|
|
743
|
+
// lands 'default' (the same neutral a lapsed timed mute reads as, see
|
|
744
|
+
// resolveTopicAttention) rather than leave the unbounded mute standing.
|
|
745
|
+
try {
|
|
746
|
+
await setRoomAttentionRest(config, room, "default");
|
|
747
|
+
}
|
|
748
|
+
catch (undoError) {
|
|
749
|
+
throw new RoomSocialError("Server does not support timed mutes, and undoing the mute failed: a permanent mute may be standing", 502, { room, undoError: undoError.message });
|
|
750
|
+
}
|
|
751
|
+
throw new RoomSocialError("Server does not support timed mutes; the permanent mute was undone (attention is now 'default')", 502, { room });
|
|
752
|
+
}
|
|
753
|
+
return { attention: result.attention ?? attention, muteUntil };
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Save a message for later (personal, per-user; nothing broadcasts).
|
|
757
|
+
* Idempotent: re-saving an already bookmarked event echoes the standing
|
|
758
|
+
* row with its original createdAt.
|
|
759
|
+
*/
|
|
760
|
+
export async function addRoomBookmark(config, room, seq) {
|
|
761
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/bookmarks`, { method: "POST", body: { seq } });
|
|
762
|
+
if (!result.bookmark) {
|
|
763
|
+
throw new RoomSocialError("Bookmark save returned no row", 502, result);
|
|
764
|
+
}
|
|
765
|
+
return result.bookmark;
|
|
766
|
+
}
|
|
767
|
+
/** Remove a bookmark. Idempotent, and deliberately NOT gated on room
|
|
768
|
+
* readability: cleanup keeps working after lost membership or a deleted
|
|
769
|
+
* target (`removed` is false when no row stood). */
|
|
770
|
+
/** Sequence numbers ride URL paths; a JavaScript caller can pass any value
|
|
771
|
+
* despite the type annotation, and fetch normalizes dot segments, so a seq
|
|
772
|
+
* of ".." would rewrite a bookmark DELETE onto the ROOM route (archive).
|
|
773
|
+
* Runtime-require a nonnegative safe integer before interpolating. */
|
|
774
|
+
function requireSeq(seq) {
|
|
775
|
+
if (!Number.isSafeInteger(seq) || seq < 0) {
|
|
776
|
+
throw new RoomSocialError("Invalid sequence number", 400, { seq: String(seq) });
|
|
777
|
+
}
|
|
778
|
+
return seq;
|
|
779
|
+
}
|
|
780
|
+
export async function removeRoomBookmark(config, room, seq) {
|
|
781
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/bookmarks/${requireSeq(seq)}`, { method: "DELETE" });
|
|
782
|
+
return { removed: result.removed === true };
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* The caller's bookmarks across every room (GET /users/bookmarks), newest
|
|
786
|
+
* first with the notification inbox's composite cursor: pass a page's
|
|
787
|
+
* lastCursor and lastCursorId back TOGETHER for the next page. Rows in
|
|
788
|
+
* rooms the caller can no longer read are subtracted server-side.
|
|
789
|
+
*/
|
|
790
|
+
export async function listUserBookmarks(config, options = {}) {
|
|
791
|
+
const params = new URLSearchParams();
|
|
792
|
+
if (options.limit !== undefined)
|
|
793
|
+
params.set("limit", String(options.limit));
|
|
794
|
+
if (options.cursor !== undefined)
|
|
795
|
+
params.set("cursor", options.cursor);
|
|
796
|
+
if (options.cursorId !== undefined)
|
|
797
|
+
params.set("cursorId", options.cursorId);
|
|
798
|
+
const init = {};
|
|
799
|
+
if (options.signal)
|
|
800
|
+
init.signal = options.signal;
|
|
801
|
+
const result = await socialRequest(config, `/users/bookmarks${params.size ? `?${params.toString()}` : ""}`, init);
|
|
802
|
+
return {
|
|
803
|
+
bookmarks: result.bookmarks ?? [],
|
|
804
|
+
hasMore: result.hasMore ?? false,
|
|
805
|
+
lastCursor: result.lastCursor ?? null,
|
|
806
|
+
lastCursorId: result.lastCursorId ?? null,
|
|
807
|
+
total: result.total ?? 0,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* The caller's user-block list (GET /users/blocks), newest first with the
|
|
812
|
+
* same composite-cursor paging as the notification inbox. Blocks are
|
|
813
|
+
* directional and the blocker is always the verified caller.
|
|
814
|
+
*/
|
|
815
|
+
export async function getUserBlocks(config, options = {}) {
|
|
816
|
+
const params = new URLSearchParams();
|
|
817
|
+
if (options.limit !== undefined)
|
|
818
|
+
params.set("limit", String(options.limit));
|
|
819
|
+
if (options.cursor !== undefined)
|
|
820
|
+
params.set("cursor", options.cursor);
|
|
821
|
+
if (options.cursorId !== undefined)
|
|
822
|
+
params.set("cursorId", options.cursorId);
|
|
823
|
+
const init = {};
|
|
824
|
+
if (options.signal)
|
|
825
|
+
init.signal = options.signal;
|
|
826
|
+
const result = await socialRequest(config, `/users/blocks${params.size ? `?${params.toString()}` : ""}`, init);
|
|
827
|
+
return {
|
|
828
|
+
blocks: result.blocks ?? [],
|
|
829
|
+
hasMore: result.hasMore ?? false,
|
|
830
|
+
lastCursor: result.lastCursor ?? null,
|
|
831
|
+
lastCursorId: result.lastCursorId ?? null,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/** Path-safety guard for block-target ids, NOT a format guard: the server
|
|
835
|
+
* accepts any trimmed non-empty id up to 128 chars (legacy and agent
|
|
836
|
+
* identities are not ObjectIds), so only the traversal class is refused
|
|
837
|
+
* here: empty ids and pure dot segments, which encodeURIComponent leaves
|
|
838
|
+
* alone and fetch would normalize onto /users/ itself. Everything else
|
|
839
|
+
* rides percent-encoded. */
|
|
840
|
+
function requireBlockUserId(userId) {
|
|
841
|
+
const trimmed = String(userId).trim();
|
|
842
|
+
if (trimmed === "" || trimmed === "." || trimmed === ".." || trimmed.length > 128) {
|
|
843
|
+
throw new RoomSocialError("Invalid user id", 400, { userId: String(userId) });
|
|
844
|
+
}
|
|
845
|
+
return trimmed;
|
|
846
|
+
}
|
|
847
|
+
/** Block a user by userId (PUT, idempotent: re-blocking echoes the standing
|
|
848
|
+
* row with its original createdAt). Server-enforced effects: DM doors shut
|
|
849
|
+
* both directions, friend requests refuse, and the blocked sender produces
|
|
850
|
+
* no notifications for the caller. Room visibility is unchanged. */
|
|
851
|
+
export async function blockUser(config, userId) {
|
|
852
|
+
const result = await socialRequest(config, `/users/blocks/${encodeURIComponent(requireBlockUserId(userId))}`, { method: "PUT" });
|
|
853
|
+
if (!result.block) {
|
|
854
|
+
throw new RoomSocialError("Block returned no row", 502, result);
|
|
855
|
+
}
|
|
856
|
+
return result.block;
|
|
857
|
+
}
|
|
858
|
+
/** Unblock a user (idempotent: unblocking a non-blocked user is a no-op). */
|
|
859
|
+
export async function unblockUser(config, userId) {
|
|
860
|
+
const result = await socialRequest(config, `/users/blocks/${encodeURIComponent(requireBlockUserId(userId))}`, { method: "DELETE" });
|
|
861
|
+
return { success: result.success === true };
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Post a poll message. There is no separate create route and no WS input
|
|
865
|
+
* path (the relay's post frame carries text only): a poll rides the normal
|
|
866
|
+
* REST append as the strict body's `poll` block. `options` are option TEXT
|
|
867
|
+
* (2..10 entries; ids are minted server-side) and `closesAt` is an ISO
|
|
868
|
+
* deadline that must sit in the future. `text` is the optional caption;
|
|
869
|
+
* absent is fine, the poll IS the content. Echoes the created store event
|
|
870
|
+
* row with its counts-only summary; the room hears the same message
|
|
871
|
+
* through the ordinary live echo. Additive (0.6.0 servers): older stores
|
|
872
|
+
* reject the unknown block.
|
|
873
|
+
*/
|
|
874
|
+
export async function createRoomPoll(config, room, input) {
|
|
875
|
+
const poll = {
|
|
876
|
+
question: input.question,
|
|
877
|
+
options: input.options,
|
|
878
|
+
};
|
|
879
|
+
if (input.multi !== undefined)
|
|
880
|
+
poll.multi = input.multi;
|
|
881
|
+
if (input.closesAt !== undefined)
|
|
882
|
+
poll.closesAt = input.closesAt;
|
|
883
|
+
const body = {
|
|
884
|
+
roomId: cleanRoomId(room),
|
|
885
|
+
poll,
|
|
886
|
+
};
|
|
887
|
+
if (input.text !== undefined)
|
|
888
|
+
body.text = input.text;
|
|
889
|
+
if (input.workspaceId !== undefined)
|
|
890
|
+
body.workspaceId = input.workspaceId;
|
|
891
|
+
if (input.topicId !== undefined)
|
|
892
|
+
body.topicId = input.topicId;
|
|
893
|
+
if (input.replyToSeq !== undefined)
|
|
894
|
+
body.replyToSeq = input.replyToSeq;
|
|
895
|
+
if (input.clientMsgId !== undefined)
|
|
896
|
+
body.clientMsgId = input.clientMsgId;
|
|
897
|
+
const result = await socialRequest(config, "/rooms/events", { method: "POST", body });
|
|
898
|
+
if (!result.event) {
|
|
899
|
+
throw new RoomSocialError("Poll create returned no event", 502, result);
|
|
900
|
+
}
|
|
901
|
+
return result.event;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Vote on a poll (the poll is keyed by its carrying message's seq). Echoes
|
|
905
|
+
* the store's public event row (RoomRestEvent, not the relay's mapped
|
|
906
|
+
* ledger entry) with the refreshed counts-only summary; the room hears the
|
|
907
|
+
* same update over ENTRY_UPDATED. Single-choice polls replace the caller's
|
|
908
|
+
* previous ballot; multi-choice polls union into it.
|
|
909
|
+
*/
|
|
910
|
+
export async function voteRoomPoll(config, room, seq, optionIds) {
|
|
911
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/polls/${requireSeq(seq)}/vote`, { method: "POST", body: { optionIds } });
|
|
912
|
+
if (!result.event) {
|
|
913
|
+
throw new RoomSocialError("Poll vote returned no event", 502, result);
|
|
914
|
+
}
|
|
915
|
+
return result.event;
|
|
916
|
+
}
|
|
917
|
+
/** Clear the caller's vote. Echoes the refreshed store event row. */
|
|
918
|
+
export async function unvoteRoomPoll(config, room, seq) {
|
|
919
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/polls/${requireSeq(seq)}/vote`, { method: "DELETE" });
|
|
920
|
+
if (!result.event) {
|
|
921
|
+
throw new RoomSocialError("Poll unvote returned no event", 502, result);
|
|
922
|
+
}
|
|
923
|
+
return result.event;
|
|
924
|
+
}
|
|
925
|
+
/** Close a poll (idempotent on an already-closed poll). Echoes the store
|
|
926
|
+
* event row with `poll.closed` set. */
|
|
927
|
+
export async function closeRoomPoll(config, room, seq) {
|
|
928
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/polls/${requireSeq(seq)}/close`, { method: "POST" });
|
|
929
|
+
if (!result.event) {
|
|
930
|
+
throw new RoomSocialError("Poll close returned no event", 502, result);
|
|
931
|
+
}
|
|
932
|
+
return result.event;
|
|
933
|
+
}
|
|
934
|
+
/** Edit a poll's question/options (author-only, and locked once any vote
|
|
935
|
+
* exists or the poll closes; at least one field required). Options are
|
|
936
|
+
* full replacement TEXT, like creation. */
|
|
937
|
+
/** Purge an event's frozen unfurl cards without editing or deleting the
|
|
938
|
+
* message. Author or a delete-any moderation power; the store unsets the
|
|
939
|
+
* cards, schedules thumbnail cleanup, and returns the mutated event for
|
|
940
|
+
* ENTRY_UPDATED fan-out. An edit that removes a card's URL from the text
|
|
941
|
+
* drops that card automatically; this route is the explicit purge for
|
|
942
|
+
* everything else (a capability URL that must go without rewording the
|
|
943
|
+
* message, a moderator acting on someone else's post). */
|
|
944
|
+
export async function unsetRoomEventUnfurls(config, room, seq) {
|
|
945
|
+
await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/events/${requireSeq(seq)}/unfurls`, { method: "DELETE" });
|
|
946
|
+
}
|
|
947
|
+
/** Edit an event's text over REST: the cold-replica-safe mirror of the WS
|
|
948
|
+
* EDIT frame, and the ONE shape the WS path cannot express: an empty text
|
|
949
|
+
* on a poll-bearing message legally clears the optional caption (the poll
|
|
950
|
+
* stays the content; the store rejects empty edits on plain messages).
|
|
951
|
+
* Viewers reconcile through the store's ENTRY_UPDATED fan-out exactly
|
|
952
|
+
* like the REST poll verbs. */
|
|
953
|
+
export async function editRoomEventRest(config, room, seq, text) {
|
|
954
|
+
await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/events/${requireSeq(seq)}`, { method: "PATCH", body: { text } });
|
|
955
|
+
}
|
|
956
|
+
export async function editRoomPoll(config, room, seq, patch) {
|
|
957
|
+
const body = {};
|
|
958
|
+
if (patch.question !== undefined)
|
|
959
|
+
body.question = patch.question;
|
|
960
|
+
if (patch.options !== undefined)
|
|
961
|
+
body.options = patch.options;
|
|
962
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/polls/${requireSeq(seq)}`, { method: "PATCH", body });
|
|
963
|
+
if (!result.event) {
|
|
964
|
+
throw new RoomSocialError("Poll edit returned no event", 502, result);
|
|
965
|
+
}
|
|
966
|
+
return result.event;
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Voter identities, on demand (the entry's summary carries counts only).
|
|
970
|
+
* Grouped per option; `optionId` narrows to one option and `limit` caps the
|
|
971
|
+
* identities per option (counts stay exact).
|
|
972
|
+
*/
|
|
973
|
+
export async function listRoomPollVoters(config, room, seq, options = {}) {
|
|
974
|
+
const params = new URLSearchParams();
|
|
975
|
+
if (options.optionId !== undefined)
|
|
976
|
+
params.set("optionId", options.optionId);
|
|
977
|
+
if (options.limit !== undefined)
|
|
978
|
+
params.set("limit", String(options.limit));
|
|
979
|
+
const init = {};
|
|
980
|
+
if (options.signal)
|
|
981
|
+
init.signal = options.signal;
|
|
982
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/polls/${requireSeq(seq)}/voters${params.size ? `?${params.toString()}` : ""}`, init);
|
|
983
|
+
return {
|
|
984
|
+
room: result.room ?? cleanRoomId(room),
|
|
985
|
+
seq: result.seq ?? seq,
|
|
986
|
+
voters: result.voters ?? [],
|
|
987
|
+
viewerOptionIds: result.viewerOptionIds ?? [],
|
|
988
|
+
};
|
|
989
|
+
}
|
|
654
990
|
export async function suggestRooms(config, request, options = {}) {
|
|
655
991
|
const init = {
|
|
656
992
|
method: "POST",
|
package/dist/social-types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RoomLedgerSecretPayload } from "./shared/rooms-protocol.js";
|
|
1
|
+
import type { RoomLedgerSecretPayload, RoomPollSummary, RoomTopicAttention, RoomUnfurl } from "./shared/rooms-protocol.js";
|
|
2
2
|
export interface RoomSocialConfig {
|
|
3
3
|
apiUrl: string;
|
|
4
4
|
apiKey: string;
|
|
@@ -209,6 +209,169 @@ export interface RoomNotificationInboxResult {
|
|
|
209
209
|
lastCursor: string | null;
|
|
210
210
|
lastCursorId: string | null;
|
|
211
211
|
}
|
|
212
|
+
/** One of the caller's user blocks, wire names verbatim from the store's
|
|
213
|
+
* block rows: the row id, the BLOCKED user's id, and when the block was
|
|
214
|
+
* created (ISO string). Blocks are directional; the blocker is always the
|
|
215
|
+
* verified caller and never appears on the row. */
|
|
216
|
+
export interface RoomUserBlock {
|
|
217
|
+
id: string;
|
|
218
|
+
blockedId: string;
|
|
219
|
+
createdAt: string;
|
|
220
|
+
}
|
|
221
|
+
/** A newest-first page of the caller's blocks plus the composite cursor
|
|
222
|
+
* (same paging contract as the notification inbox: feed lastCursor and
|
|
223
|
+
* lastCursorId back together; both null on an empty page). */
|
|
224
|
+
export interface RoomUserBlocksResult {
|
|
225
|
+
blocks: RoomUserBlock[];
|
|
226
|
+
hasMore: boolean;
|
|
227
|
+
lastCursor: string | null;
|
|
228
|
+
lastCursorId: string | null;
|
|
229
|
+
}
|
|
230
|
+
/** The row echoed by a bookmark save. `createdAt` is null only when a
|
|
231
|
+
* concurrent remove raced the save (the save still happened). */
|
|
232
|
+
export interface RoomBookmarkSaved {
|
|
233
|
+
roomId: string;
|
|
234
|
+
seq: number;
|
|
235
|
+
createdAt: string | null;
|
|
236
|
+
}
|
|
237
|
+
/** The store's REST poll summary: the ledger-entry wire shape plus the
|
|
238
|
+
* store's monotonic ordering revision (create = 0, bumped per mutation).
|
|
239
|
+
* The relay strips revision before entries reach the WS surface, so only
|
|
240
|
+
* REST consumers see it; use it to refuse applying a stale mutation echo
|
|
241
|
+
* over newer state. Optional because rows written before the field read
|
|
242
|
+
* without it. */
|
|
243
|
+
export type RoomRestPollSummary = RoomPollSummary & {
|
|
244
|
+
revision?: number | undefined;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* A persisted room event as the store's REST surfaces return it: the stored
|
|
248
|
+
* row with its wire names verbatim (roomId/senderId/sender/createdAt, store
|
|
249
|
+
* event kinds like 'message'), minus the server-internal fields the store's
|
|
250
|
+
* publicRoomEvent projection strips. This is NOT the relay's mapped
|
|
251
|
+
* RoomLedgerEntry (room/handle/ts, type 'post'); the two shapes never mix.
|
|
252
|
+
* Only the identity core is guaranteed: non-visible bookmark targets arrive
|
|
253
|
+
* as content-free tombstones, and older rows predate newer columns.
|
|
254
|
+
*/
|
|
255
|
+
export interface RoomRestEvent {
|
|
256
|
+
roomId: string;
|
|
257
|
+
seq: number;
|
|
258
|
+
/** The store's event kinds, not the relay's mapped entry types. */
|
|
259
|
+
type: "message" | "system_market" | "moderation" | "secret";
|
|
260
|
+
senderId: string;
|
|
261
|
+
/** Write-time sender snapshot (denormalized; never re-resolved). */
|
|
262
|
+
sender: {
|
|
263
|
+
userId: string;
|
|
264
|
+
username: string;
|
|
265
|
+
handle: string;
|
|
266
|
+
isAgent: boolean;
|
|
267
|
+
role?: string | null | undefined;
|
|
268
|
+
};
|
|
269
|
+
createdAt: string;
|
|
270
|
+
updatedAt: string;
|
|
271
|
+
/** Absent on content-free tombstones. */
|
|
272
|
+
text?: string | undefined;
|
|
273
|
+
/** The store's body blob. Room attachments live at body.attachments
|
|
274
|
+
* (sanitized for the viewer), never top-level like the ledger entry's. */
|
|
275
|
+
body?: Record<string, unknown> | undefined;
|
|
276
|
+
secret?: RoomLedgerSecretPayload | null | undefined;
|
|
277
|
+
cashtags?: string[] | undefined;
|
|
278
|
+
entities?: string[] | undefined;
|
|
279
|
+
reactions?: Array<{
|
|
280
|
+
emoji: string;
|
|
281
|
+
count: number;
|
|
282
|
+
}> | undefined;
|
|
283
|
+
/** Counts-only poll summary (additive; the same wire shape the ledger
|
|
284
|
+
* entry carries plus the store's ordering revision). */
|
|
285
|
+
poll?: RoomRestPollSummary | null | undefined;
|
|
286
|
+
/** Server-rendered link unfurl cards (additive). */
|
|
287
|
+
unfurls?: RoomUnfurl[] | undefined;
|
|
288
|
+
replyToSeq?: number | null | undefined;
|
|
289
|
+
topicId?: string | null | undefined;
|
|
290
|
+
replyCount?: number | undefined;
|
|
291
|
+
lastReplyAt?: string | null | undefined;
|
|
292
|
+
pinned?: boolean | undefined;
|
|
293
|
+
visibility?: "visible" | "soft_deleted" | "hidden" | "held" | undefined;
|
|
294
|
+
/** Present only when the viewer holds view-audit on the room. */
|
|
295
|
+
moderationState?: "clean" | "flagged" | "reported" | "actioned" | undefined;
|
|
296
|
+
clientMsgId?: string | null | undefined;
|
|
297
|
+
editedAt?: string | null | undefined;
|
|
298
|
+
deletedAt?: string | null | undefined;
|
|
299
|
+
deletedBy?: string | null | undefined;
|
|
300
|
+
}
|
|
301
|
+
/** One saved message on the cross-room bookmark list, with the bookmarked
|
|
302
|
+
* event hydrated server-side in the store's public row shape (non-visible
|
|
303
|
+
* targets arrive as content-free tombstones, exactly like the room's own
|
|
304
|
+
* read surfaces). */
|
|
305
|
+
export interface RoomBookmark {
|
|
306
|
+
roomId: string;
|
|
307
|
+
seq: number;
|
|
308
|
+
createdAt: string;
|
|
309
|
+
event: RoomRestEvent;
|
|
310
|
+
}
|
|
311
|
+
/** A newest-first bookmark page plus the composite cursor. `total` is the
|
|
312
|
+
* raw row count capped server-side: a bounded hint, not an exact readable
|
|
313
|
+
* total (rows in rooms the caller can no longer read are subtracted from
|
|
314
|
+
* pages but still count). */
|
|
315
|
+
export interface RoomBookmarksResult {
|
|
316
|
+
bookmarks: RoomBookmark[];
|
|
317
|
+
hasMore: boolean;
|
|
318
|
+
lastCursor: string | null;
|
|
319
|
+
lastCursorId: string | null;
|
|
320
|
+
total: number;
|
|
321
|
+
}
|
|
322
|
+
/** Voter identities for one poll option, fetched on demand (the entry's
|
|
323
|
+
* poll summary carries counts only). */
|
|
324
|
+
export interface RoomPollOptionVoters {
|
|
325
|
+
optionId: string;
|
|
326
|
+
users: Array<{
|
|
327
|
+
userId: string;
|
|
328
|
+
handle: string;
|
|
329
|
+
}>;
|
|
330
|
+
count: number;
|
|
331
|
+
}
|
|
332
|
+
export interface RoomPollVotersResult {
|
|
333
|
+
room: string;
|
|
334
|
+
seq: number;
|
|
335
|
+
voters: RoomPollOptionVoters[];
|
|
336
|
+
/** The authenticated caller's own current ballot, from an exact point
|
|
337
|
+
* read: independent of the bounded voter-identity window and of any
|
|
338
|
+
* optionId filter. Empty when the caller holds no ballot. This is the
|
|
339
|
+
* reload/recovery source of truth for rendering one's own vote. */
|
|
340
|
+
viewerOptionIds: string[];
|
|
341
|
+
}
|
|
342
|
+
/** Recipient-local daily push-suppression window (notify settings). */
|
|
343
|
+
export interface RoomNotifyQuietHours {
|
|
344
|
+
enabled: boolean;
|
|
345
|
+
/** Minutes after local midnight, 0..1439. */
|
|
346
|
+
startMinutes: number;
|
|
347
|
+
/** Minutes after local midnight, 0..1439. May wrap past midnight. */
|
|
348
|
+
endMinutes: number;
|
|
349
|
+
/** IANA time zone name, 1..64 chars (server-probed against Intl). */
|
|
350
|
+
tz: string;
|
|
351
|
+
}
|
|
352
|
+
/** Per-space override entry on the rooms notification settings. */
|
|
353
|
+
export interface RoomSpaceNotificationOverride {
|
|
354
|
+
suppressMassMentions?: boolean | undefined;
|
|
355
|
+
attention?: RoomTopicAttention | undefined;
|
|
356
|
+
/** Timed-mute expiry (ISO date). Meaningful only beside attention 'mute';
|
|
357
|
+
* normalization drops it otherwise. The settings surface echoes the
|
|
358
|
+
* CONFIGURED expiry, lapsed or not (a configuration read, not an
|
|
359
|
+
* effective-state read). */
|
|
360
|
+
muteUntil?: string | undefined;
|
|
361
|
+
}
|
|
362
|
+
/** The store-owned `user_settings.notificationSettings.rooms` shape behind
|
|
363
|
+
* GET/PUT /users/notification-settings. PUT is always the FULL object;
|
|
364
|
+
* absent fields take their contract defaults. */
|
|
365
|
+
export interface RoomsNotificationSettings {
|
|
366
|
+
dm: boolean;
|
|
367
|
+
mention: boolean;
|
|
368
|
+
reply: boolean;
|
|
369
|
+
/** Case-insensitive word-boundary keywords, capped at 50. */
|
|
370
|
+
keywords: string[];
|
|
371
|
+
spaceOverrides: Record<string, RoomSpaceNotificationOverride>;
|
|
372
|
+
/** Absent means no quiet-hours window is configured. */
|
|
373
|
+
quietHours?: RoomNotifyQuietHours | undefined;
|
|
374
|
+
}
|
|
212
375
|
/** A living markdown doc scoped to a space (head state; content on demand). */
|
|
213
376
|
export interface RoomDoc {
|
|
214
377
|
docId: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
|
|
1
|
+
export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPollOption, RoomPollSummary, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUnfurl, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
|
|
2
2
|
export { applyRoomPresenceDelta, isSystemRoomLedgerEntryType, ROOM_PRESENCE_SAMPLE_LIMIT, RoomClientMessageType, RoomLedgerEntryType, RoomServerMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION, } from "./shared/rooms-protocol.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmarket/rooms-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/agent/armed-mode.ts
CHANGED
|
@@ -184,7 +184,9 @@ export function isTriggeringEntry(
|
|
|
184
184
|
if (entry.type !== RoomLedgerEntryType.POST) return false;
|
|
185
185
|
if (entry.isAgent) return false;
|
|
186
186
|
if (isWebhookEntry(entry)) return false;
|
|
187
|
-
|
|
187
|
+
// A poll is substantive content even with no caption, and a captionless
|
|
188
|
+
// poll REPLY is addressed at us like any other reply.
|
|
189
|
+
if (!entry.text?.trim() && entry.poll == null) return false;
|
|
188
190
|
// Never trigger on our own message (we post as this handle/userId).
|
|
189
191
|
const fromHandle = normalizeHandle(entry.handle).toLowerCase();
|
|
190
192
|
const me = normalizeHandle(ownHandle).toLowerCase();
|
|
@@ -197,7 +199,9 @@ export function isTriggeringEntry(
|
|
|
197
199
|
// old mentionsAgent) fired on ordinary words for short handles like "may" /
|
|
198
200
|
// "btc"; armed posting has no human review, so it triggers ONLY on an `@handle`
|
|
199
201
|
// token. Empty handle never matches (mentionsAgentExplicit guards it).
|
|
200
|
-
|
|
202
|
+
// Mentions can live in the poll question too (member-authored content).
|
|
203
|
+
const mentionSource = [entry.text ?? "", entry.poll?.question ?? ""].filter(Boolean).join(" ");
|
|
204
|
+
return mentionsAgentExplicit(mentionSource, ownHandle);
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
/**
|
|
@@ -396,7 +400,7 @@ export function isHumanResetEntry(
|
|
|
396
400
|
const fromHandle = normalizeHandle(entry.handle).toLowerCase();
|
|
397
401
|
const me = normalizeHandle(ownHandle).toLowerCase();
|
|
398
402
|
if (me.length > 0 && fromHandle === me) return false;
|
|
399
|
-
return Boolean(entry.text?.trim());
|
|
403
|
+
return Boolean(entry.text?.trim()) || entry.poll != null;
|
|
400
404
|
}
|
|
401
405
|
|
|
402
406
|
/** Note a turn/post error against the kill switch; returns true once the room
|
|
@@ -24,6 +24,11 @@ export type AgentMessage =
|
|
|
24
24
|
export interface ToolDispatchResult {
|
|
25
25
|
isError?: true;
|
|
26
26
|
content: string;
|
|
27
|
+
/** Full-fidelity render payload for in-process SURFACES (e.g. the TUI's
|
|
28
|
+
* backtest card). Present only on the streamed copy channels consume —
|
|
29
|
+
* the agent runtime strips it before the result reaches the model's
|
|
30
|
+
* messages or the conversation store, so it never costs context. */
|
|
31
|
+
artifact?: unknown;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export interface LlmTool {
|