@openmarket/rooms-client 0.1.0 → 0.3.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.d.ts +22 -5
- package/dist/agent/armed-mode.js +42 -6
- package/dist/client.d.ts +29 -3
- package/dist/client.js +41 -0
- package/dist/shared/rooms-protocol.d.ts +104 -3
- package/dist/shared/rooms-protocol.js +6 -0
- package/dist/social-client.d.ts +16 -1
- package/dist/social-client.js +89 -3
- package/dist/social-types.d.ts +21 -3
- package/dist/types.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +1 -1
- package/src/agent/armed-mode.ts +45 -4
- package/src/client.ts +69 -4
- package/src/shared/rooms-protocol.ts +112 -3
- package/src/social-client.ts +102 -2
- package/src/social-types.ts +22 -4
- package/src/types.ts +1 -0
- package/src/version.ts +2 -2
|
@@ -89,18 +89,35 @@ export interface ArmedRoomState {
|
|
|
89
89
|
}
|
|
90
90
|
/** A fresh armed state for a newly armed room. */
|
|
91
91
|
export declare function makeArmedRoomState(roomId: string, lastSeq?: number): ArmedRoomState;
|
|
92
|
+
/**
|
|
93
|
+
* A fresh armed state that CARRIES the cost/rate counters from a prior armed
|
|
94
|
+
* session of the same room. Disarm/rearm must not zero the cooldown, hourly
|
|
95
|
+
* window, or session budget: otherwise toggling /disarm + /arm bypasses every
|
|
96
|
+
* cost guard. Error and chain counters DO reset (rearming is an explicit
|
|
97
|
+
* operator action; a fresh process still starts with nothing retained).
|
|
98
|
+
*/
|
|
99
|
+
export declare function rearmedRoomState(roomId: string, retired: ArmedRoomState | null, lastSeq?: number): ArmedRoomState;
|
|
100
|
+
/** Seqs of the user's own posts in a window of entries; the reply-trigger
|
|
101
|
+
* check tests `inReplyTo` against this set. Pure. */
|
|
102
|
+
export declare function ownSeqsOf(entries: ReadonlyArray<RoomLedgerEntry>, ownUserId: string): Set<number>;
|
|
92
103
|
/**
|
|
93
104
|
* Whether a single incoming entry is a qualifying TRIGGER: a NEW human POST in
|
|
94
|
-
* the room that @mentions the user's own handle
|
|
95
|
-
* the user's own
|
|
105
|
+
* the room that @mentions the user's own handle, OR (when `ownSeqs` is given)
|
|
106
|
+
* directly replies to one of the user's own messages. Reply semantics match a
|
|
107
|
+
* mention (replying pings the author), so armed mode treats both as addressed
|
|
108
|
+
* to the user. Agent posts, system entries, the user's own posts, and
|
|
109
|
+
* non-mentions never trigger. Pure.
|
|
96
110
|
*/
|
|
97
|
-
export declare function isTriggeringEntry(entry: RoomLedgerEntry, ownHandle: string, ownUserId: string): boolean;
|
|
111
|
+
export declare function isTriggeringEntry(entry: RoomLedgerEntry, ownHandle: string, ownUserId: string, ownSeqs?: ReadonlySet<number>): boolean;
|
|
98
112
|
/**
|
|
99
113
|
* The newest qualifying trigger among entries strictly newer than
|
|
100
114
|
* `afterSeq`, or null if none. Used by the loop to decide whether the latest
|
|
101
|
-
* batch of room messages contains a reason to act.
|
|
115
|
+
* batch of room messages contains a reason to act. Reply triggers resolve
|
|
116
|
+
* against the user's own seqs within `entries` unless a wider `ownSeqs`
|
|
117
|
+
* window is supplied (the live single-entry path passes the room cache).
|
|
118
|
+
* Pure.
|
|
102
119
|
*/
|
|
103
|
-
export declare function findTrigger(entries: ReadonlyArray<RoomLedgerEntry>, ownHandle: string, ownUserId: string, afterSeq: number): RoomLedgerEntry | null;
|
|
120
|
+
export declare function findTrigger(entries: ReadonlyArray<RoomLedgerEntry>, ownHandle: string, ownUserId: string, afterSeq: number, ownSeqs?: ReadonlySet<number>): RoomLedgerEntry | null;
|
|
104
121
|
/** Autonomous posts inside the rolling hourly window as of `now`. */
|
|
105
122
|
export declare function postsInWindow(state: ArmedRoomState, now?: number): number;
|
|
106
123
|
/** Remaining cooldown (ms) before the next autonomous post is allowed; 0 when
|
package/dist/agent/armed-mode.js
CHANGED
|
@@ -83,12 +83,41 @@ export function makeArmedRoomState(roomId, lastSeq = 0) {
|
|
|
83
83
|
lastEvaluatedSeq: lastSeq,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* A fresh armed state that CARRIES the cost/rate counters from a prior armed
|
|
88
|
+
* session of the same room. Disarm/rearm must not zero the cooldown, hourly
|
|
89
|
+
* window, or session budget: otherwise toggling /disarm + /arm bypasses every
|
|
90
|
+
* cost guard. Error and chain counters DO reset (rearming is an explicit
|
|
91
|
+
* operator action; a fresh process still starts with nothing retained).
|
|
92
|
+
*/
|
|
93
|
+
export function rearmedRoomState(roomId, retired, lastSeq = 0) {
|
|
94
|
+
const state = makeArmedRoomState(roomId, lastSeq);
|
|
95
|
+
if (!retired || retired.roomId !== roomId)
|
|
96
|
+
return state;
|
|
97
|
+
state.postTimes = [...retired.postTimes];
|
|
98
|
+
state.postsThisSession = retired.postsThisSession;
|
|
99
|
+
state.tokensThisSession = retired.tokensThisSession;
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
/** Seqs of the user's own posts in a window of entries; the reply-trigger
|
|
103
|
+
* check tests `inReplyTo` against this set. Pure. */
|
|
104
|
+
export function ownSeqsOf(entries, ownUserId) {
|
|
105
|
+
const seqs = new Set();
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (entry.userId === ownUserId)
|
|
108
|
+
seqs.add(entry.seq);
|
|
109
|
+
}
|
|
110
|
+
return seqs;
|
|
111
|
+
}
|
|
86
112
|
/**
|
|
87
113
|
* Whether a single incoming entry is a qualifying TRIGGER: a NEW human POST in
|
|
88
|
-
* the room that @mentions the user's own handle
|
|
89
|
-
* the user's own
|
|
114
|
+
* the room that @mentions the user's own handle, OR (when `ownSeqs` is given)
|
|
115
|
+
* directly replies to one of the user's own messages. Reply semantics match a
|
|
116
|
+
* mention (replying pings the author), so armed mode treats both as addressed
|
|
117
|
+
* to the user. Agent posts, system entries, the user's own posts, and
|
|
118
|
+
* non-mentions never trigger. Pure.
|
|
90
119
|
*/
|
|
91
|
-
export function isTriggeringEntry(entry, ownHandle, ownUserId) {
|
|
120
|
+
export function isTriggeringEntry(entry, ownHandle, ownUserId, ownSeqs) {
|
|
92
121
|
if (entry.type !== RoomLedgerEntryType.POST)
|
|
93
122
|
return false;
|
|
94
123
|
if (entry.isAgent)
|
|
@@ -102,6 +131,10 @@ export function isTriggeringEntry(entry, ownHandle, ownUserId) {
|
|
|
102
131
|
return false;
|
|
103
132
|
if (me.length > 0 && fromHandle === me)
|
|
104
133
|
return false;
|
|
134
|
+
// A direct reply to one of our messages is addressed to us. The set only
|
|
135
|
+
// spans the cached window, so replies to long-scrolled-out posts stay inert.
|
|
136
|
+
if (entry.inReplyTo !== undefined && ownSeqs?.has(entry.inReplyTo))
|
|
137
|
+
return true;
|
|
105
138
|
// Must EXPLICITLY @mention the user's own handle. A bare-substring match (the
|
|
106
139
|
// old mentionsAgent) fired on ordinary words for short handles like "may" /
|
|
107
140
|
// "btc"; armed posting has no human review, so it triggers ONLY on an `@handle`
|
|
@@ -111,14 +144,17 @@ export function isTriggeringEntry(entry, ownHandle, ownUserId) {
|
|
|
111
144
|
/**
|
|
112
145
|
* The newest qualifying trigger among entries strictly newer than
|
|
113
146
|
* `afterSeq`, or null if none. Used by the loop to decide whether the latest
|
|
114
|
-
* batch of room messages contains a reason to act.
|
|
147
|
+
* batch of room messages contains a reason to act. Reply triggers resolve
|
|
148
|
+
* against the user's own seqs within `entries` unless a wider `ownSeqs`
|
|
149
|
+
* window is supplied (the live single-entry path passes the room cache).
|
|
150
|
+
* Pure.
|
|
115
151
|
*/
|
|
116
|
-
export function findTrigger(entries, ownHandle, ownUserId, afterSeq) {
|
|
152
|
+
export function findTrigger(entries, ownHandle, ownUserId, afterSeq, ownSeqs = ownSeqsOf(entries, ownUserId)) {
|
|
117
153
|
let found = null;
|
|
118
154
|
for (const entry of entries) {
|
|
119
155
|
if (entry.seq <= afterSeq)
|
|
120
156
|
continue;
|
|
121
|
-
if (isTriggeringEntry(entry, ownHandle, ownUserId))
|
|
157
|
+
if (isTriggeringEntry(entry, ownHandle, ownUserId, ownSeqs))
|
|
122
158
|
found = entry;
|
|
123
159
|
}
|
|
124
160
|
return found;
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RoomAccessMode, type
|
|
1
|
+
import { type RoomAccessMode, type RoomAttachmentInput, type RoomAuditLogPayload, type RoomAuthSuccessPayload, type RoomBackfillPayload, type RoomDmHistoryResultPayload, type RoomDmMessagePayload, type RoomDmOpenedPayload, type RoomFriendPresence, type RoomInvitedPayload, type RoomLedgerEntry, type RoomMemberRemovedPayload, type RoomMemberRoleChangedPayload, type RoomMeta, type RoomPinsPayload, type RoomPresencePayload, type RoomReactorsPayload, type RoomReadStatePayload, type RoomReportedPayload, type RoomRole, type RoomSearchResultsPayload, type RoomSearchScope, type RoomSpace, type RoomThreadPayload, type RoomTopic, type RoomTopicAttention, type RoomTopicResultPayload, type RoomTopicsResultPayload, type RoomUserStatus, type RoomWhoPayload } from "./types.js";
|
|
2
2
|
import { type RoomWebSocketCtor, RoomWsClient } from "./ws-client.js";
|
|
3
3
|
export interface RoomClientConfig {
|
|
4
4
|
url: string;
|
|
@@ -39,7 +39,7 @@ export interface RoomJoinOptions {
|
|
|
39
39
|
}
|
|
40
40
|
export interface RoomSayOptions extends RoomJoinOptions {
|
|
41
41
|
text?: string;
|
|
42
|
-
attachments?:
|
|
42
|
+
attachments?: RoomAttachmentInput[];
|
|
43
43
|
inReplyTo?: number;
|
|
44
44
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
45
45
|
topicId?: string;
|
|
@@ -208,6 +208,10 @@ export interface RoomDmSendRequest {
|
|
|
208
208
|
conversationId?: string;
|
|
209
209
|
content: string;
|
|
210
210
|
clientMsgId?: string;
|
|
211
|
+
/** A private DM-attachment key (dm-attachments/…) or a legacy public image
|
|
212
|
+
* URL. Stored on the message's imageUrl; the client signs keys on render.
|
|
213
|
+
* Additive — omitted for text-only sends. */
|
|
214
|
+
imageUrl?: string;
|
|
211
215
|
}
|
|
212
216
|
export interface RoomThreadRequest {
|
|
213
217
|
room: string;
|
|
@@ -251,13 +255,35 @@ export declare function requestRoomFriends(client: RoomWsClient): Promise<RoomFr
|
|
|
251
255
|
export declare function openRoomDmOnClient(client: RoomWsClient, request: RoomDmOpenRequest, timeoutMs?: number): Promise<RoomDmOpenedPayload>;
|
|
252
256
|
export declare function requestRoomDmHistoryOnClient(client: RoomWsClient, request: RoomDmHistoryRequest, timeoutMs?: number): Promise<RoomDmHistoryResultPayload>;
|
|
253
257
|
export declare function sendRoomDmOnClient(client: RoomWsClient, request: RoomDmSendRequest, timeoutMs?: number): Promise<RoomDmMessagePayload>;
|
|
258
|
+
/** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
|
|
259
|
+
* DM_MESSAGE_EDITED to both participants. */
|
|
260
|
+
export declare function editRoomDmOnClient(client: RoomWsClient, request: {
|
|
261
|
+
conversationId?: string;
|
|
262
|
+
username?: string;
|
|
263
|
+
messageId: string;
|
|
264
|
+
content: string;
|
|
265
|
+
}): void;
|
|
266
|
+
/** Soft-delete one of my DM messages. The relay fans out DM_MESSAGE_DELETED. */
|
|
267
|
+
export declare function deleteRoomDmOnClient(client: RoomWsClient, request: {
|
|
268
|
+
conversationId?: string;
|
|
269
|
+
username?: string;
|
|
270
|
+
messageId: string;
|
|
271
|
+
}): void;
|
|
272
|
+
/** Toggle a reaction on a DM message. The relay fans out DM_REACTION_UPDATED. */
|
|
273
|
+
export declare function reactRoomDmOnClient(client: RoomWsClient, request: {
|
|
274
|
+
conversationId?: string;
|
|
275
|
+
username?: string;
|
|
276
|
+
messageId: string;
|
|
277
|
+
emoji: string;
|
|
278
|
+
op: "add" | "remove";
|
|
279
|
+
}): void;
|
|
254
280
|
export declare function peekRoom(config: RoomClientConfig, options: RoomPeekOptions): Promise<RoomBackfillPayload>;
|
|
255
281
|
export declare function sayRoom(config: RoomClientConfig, options: RoomSayOptions): Promise<RoomSayResult>;
|
|
256
282
|
export declare function joinRoom(config: RoomClientConfig, options: RoomJoinOptions): Promise<RoomJoinedSession>;
|
|
257
283
|
/** Re-join a room on an already-connected client (used after a reconnect). */
|
|
258
284
|
export declare function rejoinRoomOnClient(client: RoomWsClient, options: RoomJoinOptions): Promise<RoomBackfillPayload>;
|
|
259
285
|
export declare function attachRejoinOnReconnect(client: RoomWsClient, options: RoomJoinOptions, latestSeq: () => number | undefined, onRejoined?: (backfill: RoomBackfillPayload) => void, onError?: (err: unknown) => void): () => void;
|
|
260
|
-
export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?:
|
|
286
|
+
export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string): string;
|
|
261
287
|
export declare function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void;
|
|
262
288
|
export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null): void;
|
|
263
289
|
/**
|
package/dist/client.js
CHANGED
|
@@ -465,6 +465,7 @@ export async function sendRoomDmOnClient(client, request, timeoutMs) {
|
|
|
465
465
|
clientMsgId,
|
|
466
466
|
...(request.username ? { username: request.username } : {}),
|
|
467
467
|
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
468
|
+
...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
|
|
468
469
|
},
|
|
469
470
|
timestamp: Date.now(),
|
|
470
471
|
});
|
|
@@ -473,6 +474,46 @@ export async function sendRoomDmOnClient(client, request, timeoutMs) {
|
|
|
473
474
|
msg.payload.message.clientMsgId === clientMsgId, timeoutMs, requestId);
|
|
474
475
|
return sent.payload;
|
|
475
476
|
}
|
|
477
|
+
/** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
|
|
478
|
+
* DM_MESSAGE_EDITED to both participants. */
|
|
479
|
+
export function editRoomDmOnClient(client, request) {
|
|
480
|
+
client.send({
|
|
481
|
+
type: RoomClientMessageType.DM_EDIT,
|
|
482
|
+
payload: {
|
|
483
|
+
messageId: request.messageId,
|
|
484
|
+
content: request.content,
|
|
485
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
486
|
+
...(request.username ? { username: request.username } : {}),
|
|
487
|
+
},
|
|
488
|
+
timestamp: Date.now(),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/** Soft-delete one of my DM messages. The relay fans out DM_MESSAGE_DELETED. */
|
|
492
|
+
export function deleteRoomDmOnClient(client, request) {
|
|
493
|
+
client.send({
|
|
494
|
+
type: RoomClientMessageType.DM_DELETE,
|
|
495
|
+
payload: {
|
|
496
|
+
messageId: request.messageId,
|
|
497
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
498
|
+
...(request.username ? { username: request.username } : {}),
|
|
499
|
+
},
|
|
500
|
+
timestamp: Date.now(),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/** Toggle a reaction on a DM message. The relay fans out DM_REACTION_UPDATED. */
|
|
504
|
+
export function reactRoomDmOnClient(client, request) {
|
|
505
|
+
client.send({
|
|
506
|
+
type: RoomClientMessageType.DM_REACT,
|
|
507
|
+
payload: {
|
|
508
|
+
messageId: request.messageId,
|
|
509
|
+
emoji: request.emoji,
|
|
510
|
+
op: request.op,
|
|
511
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
512
|
+
...(request.username ? { username: request.username } : {}),
|
|
513
|
+
},
|
|
514
|
+
timestamp: Date.now(),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
476
517
|
export async function peekRoom(config, options) {
|
|
477
518
|
if (!config.webSocketCtor) {
|
|
478
519
|
try {
|
|
@@ -38,6 +38,9 @@ export declare const RoomClientMessageType: {
|
|
|
38
38
|
readonly DM_HISTORY: "DM_HISTORY";
|
|
39
39
|
readonly DM_SEND: "DM_SEND";
|
|
40
40
|
readonly DM_READ: "DM_READ";
|
|
41
|
+
readonly DM_EDIT: "DM_EDIT";
|
|
42
|
+
readonly DM_DELETE: "DM_DELETE";
|
|
43
|
+
readonly DM_REACT: "DM_REACT";
|
|
41
44
|
readonly TOPIC_CREATE: "TOPIC_CREATE";
|
|
42
45
|
readonly TOPIC_RENAME: "TOPIC_RENAME";
|
|
43
46
|
readonly TOPIC_MOVE: "TOPIC_MOVE";
|
|
@@ -87,6 +90,9 @@ export declare const RoomServerMessageType: {
|
|
|
87
90
|
readonly DM_HISTORY: "DM_HISTORY";
|
|
88
91
|
readonly DM_MESSAGE: "DM_MESSAGE";
|
|
89
92
|
readonly DM_INBOX_UPDATED: "DM_INBOX_UPDATED";
|
|
93
|
+
readonly DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED";
|
|
94
|
+
readonly DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED";
|
|
95
|
+
readonly DM_REACTION_UPDATED: "DM_REACTION_UPDATED";
|
|
90
96
|
readonly TOPIC: "TOPIC";
|
|
91
97
|
readonly TOPICS: "TOPICS";
|
|
92
98
|
readonly TOPIC_UPDATED: "TOPIC_UPDATED";
|
|
@@ -211,13 +217,27 @@ export interface RoomLedgerEntry {
|
|
|
211
217
|
roomRole?: RoomRole | null | undefined;
|
|
212
218
|
}
|
|
213
219
|
export interface RoomAttachment {
|
|
214
|
-
/**
|
|
215
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
222
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
223
|
+
* store withholds the signable key until the scan clears, so render the
|
|
224
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
225
|
+
*/
|
|
226
|
+
key?: string | undefined;
|
|
216
227
|
name: string;
|
|
217
228
|
size: number;
|
|
218
229
|
mime: string;
|
|
219
230
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
220
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* A postable attachment reference. Outbound frames must always carry the
|
|
234
|
+
* stored key (senders reference their own uploads, which include it); only
|
|
235
|
+
* read views of pending entries may be keyless, and those must never be
|
|
236
|
+
* forwarded back into a post.
|
|
237
|
+
*/
|
|
238
|
+
export type RoomAttachmentInput = RoomAttachment & {
|
|
239
|
+
key: string;
|
|
240
|
+
};
|
|
221
241
|
/**
|
|
222
242
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
223
243
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -438,6 +458,11 @@ export interface RoomDmMessage {
|
|
|
438
458
|
/** Sealed DM: content is '' and the opaque envelope rides here. */
|
|
439
459
|
secret?: RoomLedgerSecretPayload | null | undefined;
|
|
440
460
|
metadata?: Record<string, unknown> | undefined;
|
|
461
|
+
/** Legacy public chat-image URL (renders as-is, no signing). */
|
|
462
|
+
imageUrl?: string | null | undefined;
|
|
463
|
+
/** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
|
|
464
|
+
* Additive: old messages carry none and render unchanged. */
|
|
465
|
+
attachments?: RoomAttachment[] | undefined;
|
|
441
466
|
}
|
|
442
467
|
export interface RoomDmOpenPayload {
|
|
443
468
|
requestId?: string;
|
|
@@ -459,6 +484,58 @@ export interface RoomDmSendPayload {
|
|
|
459
484
|
conversationId?: string;
|
|
460
485
|
content: string;
|
|
461
486
|
clientMsgId?: string;
|
|
487
|
+
/** DM-attachment key or legacy public image URL; stored on the message's
|
|
488
|
+
* imageUrl. Additive — text-only sends omit it. */
|
|
489
|
+
imageUrl?: string;
|
|
490
|
+
}
|
|
491
|
+
/** Client → server: edit one of my DM messages (sealed messages are rejected). */
|
|
492
|
+
export interface RoomDmEditPayload {
|
|
493
|
+
requestId?: string;
|
|
494
|
+
conversationId?: string;
|
|
495
|
+
username?: string;
|
|
496
|
+
messageId: string;
|
|
497
|
+
content: string;
|
|
498
|
+
}
|
|
499
|
+
/** Client → server: soft-delete one of my DM messages (tombstone in place). */
|
|
500
|
+
export interface RoomDmDeletePayload {
|
|
501
|
+
requestId?: string;
|
|
502
|
+
conversationId?: string;
|
|
503
|
+
username?: string;
|
|
504
|
+
messageId: string;
|
|
505
|
+
}
|
|
506
|
+
/** Server → both participants: a DM message was edited. */
|
|
507
|
+
export interface RoomDmMessageEditedPayload {
|
|
508
|
+
conversationId: string;
|
|
509
|
+
messageId: string;
|
|
510
|
+
content: string;
|
|
511
|
+
editedAt: string | number;
|
|
512
|
+
participant: RoomDmParticipant;
|
|
513
|
+
}
|
|
514
|
+
/** Server → both participants: a DM message was soft-deleted (tombstone). */
|
|
515
|
+
export interface RoomDmMessageDeletedPayload {
|
|
516
|
+
conversationId: string;
|
|
517
|
+
messageId: string;
|
|
518
|
+
deletedAt: string | number;
|
|
519
|
+
participant: RoomDmParticipant;
|
|
520
|
+
}
|
|
521
|
+
/** Client → server: toggle a reaction on a DM message. */
|
|
522
|
+
export interface RoomDmReactPayload {
|
|
523
|
+
requestId?: string;
|
|
524
|
+
conversationId?: string;
|
|
525
|
+
username?: string;
|
|
526
|
+
messageId: string;
|
|
527
|
+
emoji: string;
|
|
528
|
+
op: "add" | "remove";
|
|
529
|
+
}
|
|
530
|
+
/** Server → both participants: a DM message's aggregated reactions changed. */
|
|
531
|
+
export interface RoomDmReactionUpdatedPayload {
|
|
532
|
+
conversationId: string;
|
|
533
|
+
messageId: string;
|
|
534
|
+
reactions: Array<{
|
|
535
|
+
emoji: string;
|
|
536
|
+
users: string[];
|
|
537
|
+
}>;
|
|
538
|
+
participant: RoomDmParticipant;
|
|
462
539
|
}
|
|
463
540
|
export interface RoomDmReadPayload {
|
|
464
541
|
requestId?: string;
|
|
@@ -780,7 +857,7 @@ export type RoomClientMessage = {
|
|
|
780
857
|
payload: {
|
|
781
858
|
room: string;
|
|
782
859
|
text?: string;
|
|
783
|
-
attachments?:
|
|
860
|
+
attachments?: RoomAttachmentInput[];
|
|
784
861
|
inReplyTo?: number;
|
|
785
862
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
786
863
|
topicId?: string;
|
|
@@ -975,6 +1052,18 @@ export type RoomClientMessage = {
|
|
|
975
1052
|
type: typeof RoomClientMessageType.DM_READ;
|
|
976
1053
|
payload: RoomDmReadPayload;
|
|
977
1054
|
timestamp?: number;
|
|
1055
|
+
} | {
|
|
1056
|
+
type: typeof RoomClientMessageType.DM_EDIT;
|
|
1057
|
+
payload: RoomDmEditPayload;
|
|
1058
|
+
timestamp?: number;
|
|
1059
|
+
} | {
|
|
1060
|
+
type: typeof RoomClientMessageType.DM_DELETE;
|
|
1061
|
+
payload: RoomDmDeletePayload;
|
|
1062
|
+
timestamp?: number;
|
|
1063
|
+
} | {
|
|
1064
|
+
type: typeof RoomClientMessageType.DM_REACT;
|
|
1065
|
+
payload: RoomDmReactPayload;
|
|
1066
|
+
timestamp?: number;
|
|
978
1067
|
} | {
|
|
979
1068
|
type: typeof RoomClientMessageType.TOPIC_CREATE;
|
|
980
1069
|
payload: RoomTopicCreatePayload;
|
|
@@ -1165,6 +1254,18 @@ export type RoomServerMessage = {
|
|
|
1165
1254
|
type: typeof RoomServerMessageType.DM_INBOX_UPDATED;
|
|
1166
1255
|
payload: RoomDmInboxUpdatedPayload;
|
|
1167
1256
|
timestamp: number;
|
|
1257
|
+
} | {
|
|
1258
|
+
type: typeof RoomServerMessageType.DM_MESSAGE_EDITED;
|
|
1259
|
+
payload: RoomDmMessageEditedPayload;
|
|
1260
|
+
timestamp: number;
|
|
1261
|
+
} | {
|
|
1262
|
+
type: typeof RoomServerMessageType.DM_MESSAGE_DELETED;
|
|
1263
|
+
payload: RoomDmMessageDeletedPayload;
|
|
1264
|
+
timestamp: number;
|
|
1265
|
+
} | {
|
|
1266
|
+
type: typeof RoomServerMessageType.DM_REACTION_UPDATED;
|
|
1267
|
+
payload: RoomDmReactionUpdatedPayload;
|
|
1268
|
+
timestamp: number;
|
|
1168
1269
|
} | {
|
|
1169
1270
|
type: typeof RoomServerMessageType.TOPIC;
|
|
1170
1271
|
payload: RoomTopicResultPayload;
|
|
@@ -38,6 +38,9 @@ export const RoomClientMessageType = {
|
|
|
38
38
|
DM_HISTORY: "DM_HISTORY",
|
|
39
39
|
DM_SEND: "DM_SEND",
|
|
40
40
|
DM_READ: "DM_READ",
|
|
41
|
+
DM_EDIT: "DM_EDIT",
|
|
42
|
+
DM_DELETE: "DM_DELETE",
|
|
43
|
+
DM_REACT: "DM_REACT",
|
|
41
44
|
TOPIC_CREATE: "TOPIC_CREATE",
|
|
42
45
|
TOPIC_RENAME: "TOPIC_RENAME",
|
|
43
46
|
TOPIC_MOVE: "TOPIC_MOVE",
|
|
@@ -86,6 +89,9 @@ export const RoomServerMessageType = {
|
|
|
86
89
|
DM_HISTORY: "DM_HISTORY",
|
|
87
90
|
DM_MESSAGE: "DM_MESSAGE",
|
|
88
91
|
DM_INBOX_UPDATED: "DM_INBOX_UPDATED",
|
|
92
|
+
DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
|
|
93
|
+
DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
|
|
94
|
+
DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
|
|
89
95
|
TOPIC: "TOPIC",
|
|
90
96
|
TOPICS: "TOPICS",
|
|
91
97
|
TOPIC_UPDATED: "TOPIC_UPDATED",
|
package/dist/social-client.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { RoomDmConversation, RoomDmHistory, RoomDmSendResult, RoomDmUnreadConversation, RoomDoc, RoomDocAttribution, RoomDocChanges, RoomDocConflictHead, RoomDocEditingLease, RoomDocProvenance, RoomDocRevision, RoomDocRunRevertResult, RoomDocWriteResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequests, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomSocialConfig, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSuggestion, RoomSuggestionRequest } from "./social-types.js";
|
|
2
2
|
export type { RoomAttachment, RoomDmConversation, RoomDmHistory, RoomDmMessage, RoomDmParticipant, RoomDmSendResult, RoomDmUnreadConversation, RoomDocAttribution, RoomDocEditingLease, RoomDocMergeRegion, RoomDocRevisionSource, RoomDocRunRevertResult, RoomFollow, RoomFollowResult, RoomFriend, RoomFriendRequest, RoomFriendRequests, RoomPublicProfile, RoomRepliesReadResult, RoomRepliesResult, RoomReplyInboxItem, RoomSocialConfig, RoomSpaceRestoreAction, RoomSpaceRestoreActionKind, RoomSpaceRestorePlan, RoomSpaceRestoreReceipt, RoomSpaceRestoreSummary, RoomSuggestion, RoomSuggestionIntent, RoomSuggestionRequest, RoomSuggestionSource, } from "./social-types.js";
|
|
3
|
+
/** Largest per-file size any tier accepts (plus tier). The server enforces
|
|
4
|
+
* the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
|
|
5
|
+
* constant only pre-flights uploads no tier could ever accept. */
|
|
3
6
|
export declare const ROOM_ATTACHMENT_MAX_BYTES: number;
|
|
4
7
|
export declare class RoomSocialError extends Error {
|
|
5
8
|
readonly name = "RoomSocialError";
|
|
@@ -175,6 +178,12 @@ export interface RoomSpaceMember {
|
|
|
175
178
|
/** Named identity roles (RoomSpaceRole.roleId). */
|
|
176
179
|
roleIds?: string[];
|
|
177
180
|
joinedAt?: string;
|
|
181
|
+
/** The member's handle/username, resolved server-side from their room
|
|
182
|
+
* memberships. Absent when the store couldn't resolve a real name (never
|
|
183
|
+
* the raw userId). */
|
|
184
|
+
handle?: string;
|
|
185
|
+
/** Per-space display name; null or absent means the account username. */
|
|
186
|
+
nickname?: string | null;
|
|
178
187
|
}
|
|
179
188
|
export declare function getUserPrefs(config: RoomSocialConfig): Promise<Record<string, unknown>>;
|
|
180
189
|
/** Whole-blob replace (16KB cap server-side; last writer wins). */
|
|
@@ -321,6 +330,8 @@ export declare function removeSpaceMember(config: RoomSocialConfig, spaceId: str
|
|
|
321
330
|
removed?: boolean;
|
|
322
331
|
}>;
|
|
323
332
|
export declare function changeSpaceMemberRole(config: RoomSocialConfig, spaceId: string, targetUserId: string, role: "admin" | "member"): Promise<RoomSpaceMember>;
|
|
333
|
+
/** Set (string) or clear (null) a member's per-space nickname; the store validates and enforces uniqueness. */
|
|
334
|
+
export declare function setSpaceMemberNickname(config: RoomSocialConfig, spaceId: string, targetUserId: string, nickname: string | null): Promise<RoomSpaceMember>;
|
|
324
335
|
export declare function createRoomSpace(config: RoomSocialConfig, input: {
|
|
325
336
|
name: string;
|
|
326
337
|
access?: "open" | "invite";
|
|
@@ -329,7 +340,10 @@ export declare function restoreRoomDoc(config: RoomSocialConfig, docId: string,
|
|
|
329
340
|
note?: string;
|
|
330
341
|
asAgent?: boolean;
|
|
331
342
|
}): Promise<RoomDoc>;
|
|
332
|
-
export declare function getRoomAttachmentUrl(config: RoomSocialConfig, room: string, key: string
|
|
343
|
+
export declare function getRoomAttachmentUrl(config: RoomSocialConfig, room: string, key: string, opts?: {
|
|
344
|
+
name?: string;
|
|
345
|
+
download?: boolean;
|
|
346
|
+
}): Promise<{
|
|
333
347
|
url: string;
|
|
334
348
|
expiresAt: string | null;
|
|
335
349
|
}>;
|
|
@@ -355,6 +369,7 @@ export declare function socialRequest<T>(config: RoomSocialConfig, path: string,
|
|
|
355
369
|
body?: unknown;
|
|
356
370
|
signal?: AbortSignal;
|
|
357
371
|
formData?: FormData;
|
|
372
|
+
headers?: Record<string, string>;
|
|
358
373
|
}): Promise<T>;
|
|
359
374
|
export declare function cleanUsername(username: string): string;
|
|
360
375
|
export declare function cleanRoomId(room: string): string;
|
package/dist/social-client.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { resolveRoomsWsUrl } from "./client.js";
|
|
2
2
|
import { envString, ROOM_CHAT_API_URL } from "./endpoints.js";
|
|
3
|
-
|
|
3
|
+
/** Largest per-file size any tier accepts (plus tier). The server enforces
|
|
4
|
+
* the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
|
|
5
|
+
* constant only pre-flights uploads no tier could ever accept. */
|
|
6
|
+
export const ROOM_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
|
|
4
7
|
export class RoomSocialError extends Error {
|
|
5
8
|
name = "RoomSocialError";
|
|
6
9
|
status;
|
|
@@ -501,6 +504,11 @@ export async function changeSpaceMemberRole(config, spaceId, targetUserId, role)
|
|
|
501
504
|
const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(targetUserId)}/role`, { method: "PATCH", body: { role } });
|
|
502
505
|
return result.member;
|
|
503
506
|
}
|
|
507
|
+
/** Set (string) or clear (null) a member's per-space nickname; the store validates and enforces uniqueness. */
|
|
508
|
+
export async function setSpaceMemberNickname(config, spaceId, targetUserId, nickname) {
|
|
509
|
+
const result = await socialRequest(config, `/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(targetUserId)}/nickname`, { method: "PATCH", body: { nickname } });
|
|
510
|
+
return result.member;
|
|
511
|
+
}
|
|
504
512
|
export async function createRoomSpace(config, input) {
|
|
505
513
|
const result = await socialRequest(config, "/spaces", {
|
|
506
514
|
method: "POST",
|
|
@@ -512,9 +520,22 @@ export async function restoreRoomDoc(config, docId, input = {}) {
|
|
|
512
520
|
const result = await socialRequest(config, `/docs/${encodeURIComponent(docId)}/restore`, { method: "POST", body: input });
|
|
513
521
|
return result.doc;
|
|
514
522
|
}
|
|
515
|
-
export async function getRoomAttachmentUrl(config, room, key) {
|
|
523
|
+
export async function getRoomAttachmentUrl(config, room, key, opts = {}) {
|
|
524
|
+
// The object key identifies the object and stays in the query. The
|
|
525
|
+
// download filename and force-download flag ride REQUEST HEADERS, never
|
|
526
|
+
// the URL: a private-room filename in the query string leaked into the
|
|
527
|
+
// relay's access logs and error telemetry (Morgan, Sentry).
|
|
516
528
|
const params = new URLSearchParams({ key });
|
|
517
|
-
const
|
|
529
|
+
const headers = {};
|
|
530
|
+
// Percent-encode the name: Fetch/HTTP header values are byte strings, so a
|
|
531
|
+
// raw Unicode filename (报告.py, an emoji) throws a TypeError before the
|
|
532
|
+
// request is sent. The store decodes with decodeURIComponent.
|
|
533
|
+
if (opts.name)
|
|
534
|
+
headers["X-Om-Download-Name"] = encodeURIComponent(opts.name);
|
|
535
|
+
// Forces a Content-Disposition attachment even for inline mimes.
|
|
536
|
+
if (opts.download)
|
|
537
|
+
headers["X-Om-Download"] = "1";
|
|
538
|
+
const result = await socialRequest(config, `/rooms/${encodeURIComponent(cleanRoomId(room))}/attachment-url?${params.toString()}`, { method: "GET", headers });
|
|
518
539
|
if (!result.url) {
|
|
519
540
|
throw new RoomSocialError("Attachment sign failed", 502, result);
|
|
520
541
|
}
|
|
@@ -582,6 +603,7 @@ export async function socialRequest(config, path, init = {}) {
|
|
|
582
603
|
Authorization: `Bearer ${config.apiKey}`,
|
|
583
604
|
Accept: "application/json",
|
|
584
605
|
...(hasJsonBody ? { "Content-Type": "application/json" } : {}),
|
|
606
|
+
...(init.headers ?? {}),
|
|
585
607
|
},
|
|
586
608
|
...(hasJsonBody
|
|
587
609
|
? { body: JSON.stringify(init.body) }
|
|
@@ -653,6 +675,63 @@ function requestPath(url) {
|
|
|
653
675
|
function trimTrailingSlash(value) {
|
|
654
676
|
return value.replace(/\/+$/, "");
|
|
655
677
|
}
|
|
678
|
+
/** Source/config/text extensions declared as text/plain so the server's
|
|
679
|
+
* UTF-8 code-file lane accepts them. Markup extensions the server
|
|
680
|
+
* blocklists (html, svg, xml, ...) are deliberately absent: they fall
|
|
681
|
+
* through to application/octet-stream and the server rejects them. */
|
|
682
|
+
const CODE_TEXT_EXTENSIONS = new Set([
|
|
683
|
+
"js",
|
|
684
|
+
"jsx",
|
|
685
|
+
"ts",
|
|
686
|
+
"tsx",
|
|
687
|
+
"py",
|
|
688
|
+
"rb",
|
|
689
|
+
"go",
|
|
690
|
+
"rs",
|
|
691
|
+
"java",
|
|
692
|
+
"c",
|
|
693
|
+
"h",
|
|
694
|
+
"cc",
|
|
695
|
+
"cpp",
|
|
696
|
+
"hpp",
|
|
697
|
+
"cs",
|
|
698
|
+
"php",
|
|
699
|
+
"sh",
|
|
700
|
+
"bash",
|
|
701
|
+
"zsh",
|
|
702
|
+
"fish",
|
|
703
|
+
"ps1",
|
|
704
|
+
"sql",
|
|
705
|
+
"yaml",
|
|
706
|
+
"yml",
|
|
707
|
+
"toml",
|
|
708
|
+
"ini",
|
|
709
|
+
"cfg",
|
|
710
|
+
"conf",
|
|
711
|
+
"log",
|
|
712
|
+
"diff",
|
|
713
|
+
"patch",
|
|
714
|
+
"tex",
|
|
715
|
+
"r",
|
|
716
|
+
"jl",
|
|
717
|
+
"kt",
|
|
718
|
+
"kts",
|
|
719
|
+
"swift",
|
|
720
|
+
"scala",
|
|
721
|
+
"clj",
|
|
722
|
+
"cljs",
|
|
723
|
+
"ex",
|
|
724
|
+
"exs",
|
|
725
|
+
"erl",
|
|
726
|
+
"lua",
|
|
727
|
+
"pl",
|
|
728
|
+
"pm",
|
|
729
|
+
"dart",
|
|
730
|
+
"gradle",
|
|
731
|
+
"properties",
|
|
732
|
+
"bat",
|
|
733
|
+
"cmd",
|
|
734
|
+
]);
|
|
656
735
|
export function mimeFromFilename(filename) {
|
|
657
736
|
const lower = filename.toLowerCase();
|
|
658
737
|
if (lower.endsWith(".png"))
|
|
@@ -663,6 +742,10 @@ export function mimeFromFilename(filename) {
|
|
|
663
742
|
return "image/webp";
|
|
664
743
|
if (lower.endsWith(".gif"))
|
|
665
744
|
return "image/gif";
|
|
745
|
+
if (lower.endsWith(".mp4"))
|
|
746
|
+
return "video/mp4";
|
|
747
|
+
if (lower.endsWith(".webm"))
|
|
748
|
+
return "video/webm";
|
|
666
749
|
if (lower.endsWith(".pdf"))
|
|
667
750
|
return "application/pdf";
|
|
668
751
|
if (lower.endsWith(".txt"))
|
|
@@ -682,5 +765,8 @@ export function mimeFromFilename(filename) {
|
|
|
682
765
|
if (lower.endsWith(".pptx")) {
|
|
683
766
|
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
684
767
|
}
|
|
768
|
+
const dot = lower.lastIndexOf(".");
|
|
769
|
+
if (dot !== -1 && CODE_TEXT_EXTENSIONS.has(lower.slice(dot + 1)))
|
|
770
|
+
return "text/plain";
|
|
685
771
|
return "application/octet-stream";
|
|
686
772
|
}
|
package/dist/social-types.d.ts
CHANGED
|
@@ -44,16 +44,26 @@ export interface RoomFollowResult {
|
|
|
44
44
|
isNewFollow?: boolean;
|
|
45
45
|
}
|
|
46
46
|
export interface RoomAttachment {
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
49
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
50
|
+
* store withholds the signable key until the scan clears, so render the
|
|
51
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
52
|
+
*/
|
|
53
|
+
key?: string | undefined;
|
|
49
54
|
name: string;
|
|
50
55
|
size: number;
|
|
51
56
|
mime: string;
|
|
52
57
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
53
58
|
}
|
|
54
59
|
export interface RoomAttachmentUpload {
|
|
55
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The stored attachment plus its first signed URL, for immediate render.
|
|
62
|
+
* The uploader always receives the key; only member views of PENDING
|
|
63
|
+
* entries withhold it.
|
|
64
|
+
*/
|
|
56
65
|
attachment: RoomAttachment & {
|
|
66
|
+
key: string;
|
|
57
67
|
url: string;
|
|
58
68
|
expiresAt: string | null;
|
|
59
69
|
};
|
|
@@ -112,6 +122,14 @@ export interface RoomDmMessage {
|
|
|
112
122
|
/** Sealed DM: content is '' and the opaque envelope rides here. */
|
|
113
123
|
secret?: RoomLedgerSecretPayload | null;
|
|
114
124
|
metadata?: Record<string, unknown>;
|
|
125
|
+
/** DM image: a private dm-attachments/ key (signed on render) or a legacy
|
|
126
|
+
* public URL. Additive — old messages carry none. */
|
|
127
|
+
imageUrl?: string | null;
|
|
128
|
+
/** Aggregated reactions (per-emoji userId lists). Additive. */
|
|
129
|
+
reactions?: Array<{
|
|
130
|
+
emoji: string;
|
|
131
|
+
users: string[];
|
|
132
|
+
}>;
|
|
115
133
|
}
|
|
116
134
|
export interface RoomDmHistory {
|
|
117
135
|
conversationId: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { RoomAccessMode, RoomAttachment, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, 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, 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, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, 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, 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";
|
|
2
2
|
export { applyRoomPresenceDelta, isSystemRoomLedgerEntryType, ROOM_PRESENCE_SAMPLE_LIMIT, RoomClientMessageType, RoomLedgerEntryType, RoomServerMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION, } from "./shared/rooms-protocol.js";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.5.
|
|
2
|
-
export declare const RUNNER_VERSION = "0.5.
|
|
1
|
+
export declare const VERSION = "0.5.1";
|
|
2
|
+
export declare const RUNNER_VERSION = "0.5.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.5.
|
|
2
|
-
export const RUNNER_VERSION = "0.5.
|
|
1
|
+
export const VERSION = "0.5.1";
|
|
2
|
+
export const RUNNER_VERSION = "0.5.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmarket/rooms-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
@@ -126,15 +126,49 @@ export function makeArmedRoomState(roomId: string, lastSeq = 0): ArmedRoomState
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A fresh armed state that CARRIES the cost/rate counters from a prior armed
|
|
131
|
+
* session of the same room. Disarm/rearm must not zero the cooldown, hourly
|
|
132
|
+
* window, or session budget: otherwise toggling /disarm + /arm bypasses every
|
|
133
|
+
* cost guard. Error and chain counters DO reset (rearming is an explicit
|
|
134
|
+
* operator action; a fresh process still starts with nothing retained).
|
|
135
|
+
*/
|
|
136
|
+
export function rearmedRoomState(
|
|
137
|
+
roomId: string,
|
|
138
|
+
retired: ArmedRoomState | null,
|
|
139
|
+
lastSeq = 0,
|
|
140
|
+
): ArmedRoomState {
|
|
141
|
+
const state = makeArmedRoomState(roomId, lastSeq);
|
|
142
|
+
if (!retired || retired.roomId !== roomId) return state;
|
|
143
|
+
state.postTimes = [...retired.postTimes];
|
|
144
|
+
state.postsThisSession = retired.postsThisSession;
|
|
145
|
+
state.tokensThisSession = retired.tokensThisSession;
|
|
146
|
+
return state;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Seqs of the user's own posts in a window of entries; the reply-trigger
|
|
150
|
+
* check tests `inReplyTo` against this set. Pure. */
|
|
151
|
+
export function ownSeqsOf(entries: ReadonlyArray<RoomLedgerEntry>, ownUserId: string): Set<number> {
|
|
152
|
+
const seqs = new Set<number>();
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry.userId === ownUserId) seqs.add(entry.seq);
|
|
155
|
+
}
|
|
156
|
+
return seqs;
|
|
157
|
+
}
|
|
158
|
+
|
|
129
159
|
/**
|
|
130
160
|
* Whether a single incoming entry is a qualifying TRIGGER: a NEW human POST in
|
|
131
|
-
* the room that @mentions the user's own handle
|
|
132
|
-
* the user's own
|
|
161
|
+
* the room that @mentions the user's own handle, OR (when `ownSeqs` is given)
|
|
162
|
+
* directly replies to one of the user's own messages. Reply semantics match a
|
|
163
|
+
* mention (replying pings the author), so armed mode treats both as addressed
|
|
164
|
+
* to the user. Agent posts, system entries, the user's own posts, and
|
|
165
|
+
* non-mentions never trigger. Pure.
|
|
133
166
|
*/
|
|
134
167
|
export function isTriggeringEntry(
|
|
135
168
|
entry: RoomLedgerEntry,
|
|
136
169
|
ownHandle: string,
|
|
137
170
|
ownUserId: string,
|
|
171
|
+
ownSeqs?: ReadonlySet<number>,
|
|
138
172
|
): boolean {
|
|
139
173
|
if (entry.type !== RoomLedgerEntryType.POST) return false;
|
|
140
174
|
if (entry.isAgent) return false;
|
|
@@ -144,6 +178,9 @@ export function isTriggeringEntry(
|
|
|
144
178
|
const me = normalizeHandle(ownHandle).toLowerCase();
|
|
145
179
|
if (entry.userId === ownUserId) return false;
|
|
146
180
|
if (me.length > 0 && fromHandle === me) return false;
|
|
181
|
+
// A direct reply to one of our messages is addressed to us. The set only
|
|
182
|
+
// spans the cached window, so replies to long-scrolled-out posts stay inert.
|
|
183
|
+
if (entry.inReplyTo !== undefined && ownSeqs?.has(entry.inReplyTo)) return true;
|
|
147
184
|
// Must EXPLICITLY @mention the user's own handle. A bare-substring match (the
|
|
148
185
|
// old mentionsAgent) fired on ordinary words for short handles like "may" /
|
|
149
186
|
// "btc"; armed posting has no human review, so it triggers ONLY on an `@handle`
|
|
@@ -154,18 +191,22 @@ export function isTriggeringEntry(
|
|
|
154
191
|
/**
|
|
155
192
|
* The newest qualifying trigger among entries strictly newer than
|
|
156
193
|
* `afterSeq`, or null if none. Used by the loop to decide whether the latest
|
|
157
|
-
* batch of room messages contains a reason to act.
|
|
194
|
+
* batch of room messages contains a reason to act. Reply triggers resolve
|
|
195
|
+
* against the user's own seqs within `entries` unless a wider `ownSeqs`
|
|
196
|
+
* window is supplied (the live single-entry path passes the room cache).
|
|
197
|
+
* Pure.
|
|
158
198
|
*/
|
|
159
199
|
export function findTrigger(
|
|
160
200
|
entries: ReadonlyArray<RoomLedgerEntry>,
|
|
161
201
|
ownHandle: string,
|
|
162
202
|
ownUserId: string,
|
|
163
203
|
afterSeq: number,
|
|
204
|
+
ownSeqs: ReadonlySet<number> = ownSeqsOf(entries, ownUserId),
|
|
164
205
|
): RoomLedgerEntry | null {
|
|
165
206
|
let found: RoomLedgerEntry | null = null;
|
|
166
207
|
for (const entry of entries) {
|
|
167
208
|
if (entry.seq <= afterSeq) continue;
|
|
168
|
-
if (isTriggeringEntry(entry, ownHandle, ownUserId)) found = entry;
|
|
209
|
+
if (isTriggeringEntry(entry, ownHandle, ownUserId, ownSeqs)) found = entry;
|
|
169
210
|
}
|
|
170
211
|
return found;
|
|
171
212
|
}
|
package/src/client.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { authFromApiKey } from "./jwt.js";
|
|
|
7
7
|
import { consumeRoomSuggestionAttribution } from "./suggestion-attribution.js";
|
|
8
8
|
import {
|
|
9
9
|
type RoomAccessMode,
|
|
10
|
-
type
|
|
10
|
+
type RoomAttachmentInput,
|
|
11
11
|
type RoomAuditLogPayload,
|
|
12
12
|
type RoomAuthSuccessPayload,
|
|
13
13
|
type RoomBackfillPayload,
|
|
@@ -82,7 +82,7 @@ export interface RoomJoinOptions {
|
|
|
82
82
|
|
|
83
83
|
export interface RoomSayOptions extends RoomJoinOptions {
|
|
84
84
|
text?: string;
|
|
85
|
-
attachments?:
|
|
85
|
+
attachments?: RoomAttachmentInput[];
|
|
86
86
|
inReplyTo?: number;
|
|
87
87
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
88
88
|
topicId?: string;
|
|
@@ -261,7 +261,9 @@ export interface RoomJoinedSession {
|
|
|
261
261
|
client: RoomWsClient;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
function toRoomAttachmentPayloads(
|
|
264
|
+
function toRoomAttachmentPayloads(
|
|
265
|
+
attachments: readonly RoomAttachmentInput[],
|
|
266
|
+
): RoomAttachmentInput[] {
|
|
265
267
|
return attachments.map(({ key, name, size, mime }) => ({ key, name, size, mime }));
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -284,6 +286,10 @@ export interface RoomDmSendRequest {
|
|
|
284
286
|
conversationId?: string;
|
|
285
287
|
content: string;
|
|
286
288
|
clientMsgId?: string;
|
|
289
|
+
/** A private DM-attachment key (dm-attachments/…) or a legacy public image
|
|
290
|
+
* URL. Stored on the message's imageUrl; the client signs keys on render.
|
|
291
|
+
* Additive — omitted for text-only sends. */
|
|
292
|
+
imageUrl?: string;
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
export interface RoomThreadRequest {
|
|
@@ -931,6 +937,7 @@ export async function sendRoomDmOnClient(
|
|
|
931
937
|
clientMsgId,
|
|
932
938
|
...(request.username ? { username: request.username } : {}),
|
|
933
939
|
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
940
|
+
...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
|
|
934
941
|
},
|
|
935
942
|
timestamp: Date.now(),
|
|
936
943
|
});
|
|
@@ -946,6 +953,64 @@ export async function sendRoomDmOnClient(
|
|
|
946
953
|
return sent.payload;
|
|
947
954
|
}
|
|
948
955
|
|
|
956
|
+
/** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
|
|
957
|
+
* DM_MESSAGE_EDITED to both participants. */
|
|
958
|
+
export function editRoomDmOnClient(
|
|
959
|
+
client: RoomWsClient,
|
|
960
|
+
request: { conversationId?: string; username?: string; messageId: string; content: string },
|
|
961
|
+
): void {
|
|
962
|
+
client.send({
|
|
963
|
+
type: RoomClientMessageType.DM_EDIT,
|
|
964
|
+
payload: {
|
|
965
|
+
messageId: request.messageId,
|
|
966
|
+
content: request.content,
|
|
967
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
968
|
+
...(request.username ? { username: request.username } : {}),
|
|
969
|
+
},
|
|
970
|
+
timestamp: Date.now(),
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** Soft-delete one of my DM messages. The relay fans out DM_MESSAGE_DELETED. */
|
|
975
|
+
export function deleteRoomDmOnClient(
|
|
976
|
+
client: RoomWsClient,
|
|
977
|
+
request: { conversationId?: string; username?: string; messageId: string },
|
|
978
|
+
): void {
|
|
979
|
+
client.send({
|
|
980
|
+
type: RoomClientMessageType.DM_DELETE,
|
|
981
|
+
payload: {
|
|
982
|
+
messageId: request.messageId,
|
|
983
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
984
|
+
...(request.username ? { username: request.username } : {}),
|
|
985
|
+
},
|
|
986
|
+
timestamp: Date.now(),
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Toggle a reaction on a DM message. The relay fans out DM_REACTION_UPDATED. */
|
|
991
|
+
export function reactRoomDmOnClient(
|
|
992
|
+
client: RoomWsClient,
|
|
993
|
+
request: {
|
|
994
|
+
conversationId?: string;
|
|
995
|
+
username?: string;
|
|
996
|
+
messageId: string;
|
|
997
|
+
emoji: string;
|
|
998
|
+
op: "add" | "remove";
|
|
999
|
+
},
|
|
1000
|
+
): void {
|
|
1001
|
+
client.send({
|
|
1002
|
+
type: RoomClientMessageType.DM_REACT,
|
|
1003
|
+
payload: {
|
|
1004
|
+
messageId: request.messageId,
|
|
1005
|
+
emoji: request.emoji,
|
|
1006
|
+
op: request.op,
|
|
1007
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
1008
|
+
...(request.username ? { username: request.username } : {}),
|
|
1009
|
+
},
|
|
1010
|
+
timestamp: Date.now(),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
949
1014
|
export async function peekRoom(
|
|
950
1015
|
config: RoomClientConfig,
|
|
951
1016
|
options: RoomPeekOptions,
|
|
@@ -1087,7 +1152,7 @@ export function postRoomMessage(
|
|
|
1087
1152
|
room: string,
|
|
1088
1153
|
text: string,
|
|
1089
1154
|
inReplyTo?: number,
|
|
1090
|
-
attachments?:
|
|
1155
|
+
attachments?: RoomAttachmentInput[],
|
|
1091
1156
|
topicId?: string,
|
|
1092
1157
|
): string {
|
|
1093
1158
|
const clientMsgId = randomUUID();
|
|
@@ -38,6 +38,9 @@ export const RoomClientMessageType = {
|
|
|
38
38
|
DM_HISTORY: "DM_HISTORY",
|
|
39
39
|
DM_SEND: "DM_SEND",
|
|
40
40
|
DM_READ: "DM_READ",
|
|
41
|
+
DM_EDIT: "DM_EDIT",
|
|
42
|
+
DM_DELETE: "DM_DELETE",
|
|
43
|
+
DM_REACT: "DM_REACT",
|
|
41
44
|
TOPIC_CREATE: "TOPIC_CREATE",
|
|
42
45
|
TOPIC_RENAME: "TOPIC_RENAME",
|
|
43
46
|
TOPIC_MOVE: "TOPIC_MOVE",
|
|
@@ -90,6 +93,9 @@ export const RoomServerMessageType = {
|
|
|
90
93
|
DM_HISTORY: "DM_HISTORY",
|
|
91
94
|
DM_MESSAGE: "DM_MESSAGE",
|
|
92
95
|
DM_INBOX_UPDATED: "DM_INBOX_UPDATED",
|
|
96
|
+
DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
|
|
97
|
+
DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
|
|
98
|
+
DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
|
|
93
99
|
TOPIC: "TOPIC",
|
|
94
100
|
TOPICS: "TOPICS",
|
|
95
101
|
TOPIC_UPDATED: "TOPIC_UPDATED",
|
|
@@ -240,14 +246,27 @@ export interface RoomLedgerEntry {
|
|
|
240
246
|
}
|
|
241
247
|
|
|
242
248
|
export interface RoomAttachment {
|
|
243
|
-
/**
|
|
244
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
251
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
252
|
+
* store withholds the signable key until the scan clears, so render the
|
|
253
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
254
|
+
*/
|
|
255
|
+
key?: string | undefined;
|
|
245
256
|
name: string;
|
|
246
257
|
size: number;
|
|
247
258
|
mime: string;
|
|
248
259
|
scanStatus?: "pending" | "clean" | "held" | undefined;
|
|
249
260
|
}
|
|
250
261
|
|
|
262
|
+
/**
|
|
263
|
+
* A postable attachment reference. Outbound frames must always carry the
|
|
264
|
+
* stored key (senders reference their own uploads, which include it); only
|
|
265
|
+
* read views of pending entries may be keyless, and those must never be
|
|
266
|
+
* forwarded back into a post.
|
|
267
|
+
*/
|
|
268
|
+
export type RoomAttachmentInput = RoomAttachment & { key: string };
|
|
269
|
+
|
|
251
270
|
/**
|
|
252
271
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
253
272
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -527,6 +546,11 @@ export interface RoomDmMessage {
|
|
|
527
546
|
/** Sealed DM: content is '' and the opaque envelope rides here. */
|
|
528
547
|
secret?: RoomLedgerSecretPayload | null | undefined;
|
|
529
548
|
metadata?: Record<string, unknown> | undefined;
|
|
549
|
+
/** Legacy public chat-image URL (renders as-is, no signing). */
|
|
550
|
+
imageUrl?: string | null | undefined;
|
|
551
|
+
/** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
|
|
552
|
+
* Additive: old messages carry none and render unchanged. */
|
|
553
|
+
attachments?: RoomAttachment[] | undefined;
|
|
530
554
|
}
|
|
531
555
|
|
|
532
556
|
export interface RoomDmOpenPayload {
|
|
@@ -551,6 +575,61 @@ export interface RoomDmSendPayload {
|
|
|
551
575
|
conversationId?: string;
|
|
552
576
|
content: string;
|
|
553
577
|
clientMsgId?: string;
|
|
578
|
+
/** DM-attachment key or legacy public image URL; stored on the message's
|
|
579
|
+
* imageUrl. Additive — text-only sends omit it. */
|
|
580
|
+
imageUrl?: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Client → server: edit one of my DM messages (sealed messages are rejected). */
|
|
584
|
+
export interface RoomDmEditPayload {
|
|
585
|
+
requestId?: string;
|
|
586
|
+
conversationId?: string;
|
|
587
|
+
username?: string;
|
|
588
|
+
messageId: string;
|
|
589
|
+
content: string;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Client → server: soft-delete one of my DM messages (tombstone in place). */
|
|
593
|
+
export interface RoomDmDeletePayload {
|
|
594
|
+
requestId?: string;
|
|
595
|
+
conversationId?: string;
|
|
596
|
+
username?: string;
|
|
597
|
+
messageId: string;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Server → both participants: a DM message was edited. */
|
|
601
|
+
export interface RoomDmMessageEditedPayload {
|
|
602
|
+
conversationId: string;
|
|
603
|
+
messageId: string;
|
|
604
|
+
content: string;
|
|
605
|
+
editedAt: string | number;
|
|
606
|
+
participant: RoomDmParticipant;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Server → both participants: a DM message was soft-deleted (tombstone). */
|
|
610
|
+
export interface RoomDmMessageDeletedPayload {
|
|
611
|
+
conversationId: string;
|
|
612
|
+
messageId: string;
|
|
613
|
+
deletedAt: string | number;
|
|
614
|
+
participant: RoomDmParticipant;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Client → server: toggle a reaction on a DM message. */
|
|
618
|
+
export interface RoomDmReactPayload {
|
|
619
|
+
requestId?: string;
|
|
620
|
+
conversationId?: string;
|
|
621
|
+
username?: string;
|
|
622
|
+
messageId: string;
|
|
623
|
+
emoji: string;
|
|
624
|
+
op: "add" | "remove";
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Server → both participants: a DM message's aggregated reactions changed. */
|
|
628
|
+
export interface RoomDmReactionUpdatedPayload {
|
|
629
|
+
conversationId: string;
|
|
630
|
+
messageId: string;
|
|
631
|
+
reactions: Array<{ emoji: string; users: string[] }>;
|
|
632
|
+
participant: RoomDmParticipant;
|
|
554
633
|
}
|
|
555
634
|
|
|
556
635
|
export interface RoomDmReadPayload {
|
|
@@ -902,7 +981,7 @@ export type RoomClientMessage =
|
|
|
902
981
|
payload: {
|
|
903
982
|
room: string;
|
|
904
983
|
text?: string;
|
|
905
|
-
attachments?:
|
|
984
|
+
attachments?: RoomAttachmentInput[];
|
|
906
985
|
inReplyTo?: number;
|
|
907
986
|
/** Post directly into a topic; the room's linear stream is unaffected. */
|
|
908
987
|
topicId?: string;
|
|
@@ -1085,6 +1164,21 @@ export type RoomClientMessage =
|
|
|
1085
1164
|
payload: RoomDmReadPayload;
|
|
1086
1165
|
timestamp?: number;
|
|
1087
1166
|
}
|
|
1167
|
+
| {
|
|
1168
|
+
type: typeof RoomClientMessageType.DM_EDIT;
|
|
1169
|
+
payload: RoomDmEditPayload;
|
|
1170
|
+
timestamp?: number;
|
|
1171
|
+
}
|
|
1172
|
+
| {
|
|
1173
|
+
type: typeof RoomClientMessageType.DM_DELETE;
|
|
1174
|
+
payload: RoomDmDeletePayload;
|
|
1175
|
+
timestamp?: number;
|
|
1176
|
+
}
|
|
1177
|
+
| {
|
|
1178
|
+
type: typeof RoomClientMessageType.DM_REACT;
|
|
1179
|
+
payload: RoomDmReactPayload;
|
|
1180
|
+
timestamp?: number;
|
|
1181
|
+
}
|
|
1088
1182
|
| {
|
|
1089
1183
|
type: typeof RoomClientMessageType.TOPIC_CREATE;
|
|
1090
1184
|
payload: RoomTopicCreatePayload;
|
|
@@ -1317,6 +1411,21 @@ export type RoomServerMessage =
|
|
|
1317
1411
|
payload: RoomDmInboxUpdatedPayload;
|
|
1318
1412
|
timestamp: number;
|
|
1319
1413
|
}
|
|
1414
|
+
| {
|
|
1415
|
+
type: typeof RoomServerMessageType.DM_MESSAGE_EDITED;
|
|
1416
|
+
payload: RoomDmMessageEditedPayload;
|
|
1417
|
+
timestamp: number;
|
|
1418
|
+
}
|
|
1419
|
+
| {
|
|
1420
|
+
type: typeof RoomServerMessageType.DM_MESSAGE_DELETED;
|
|
1421
|
+
payload: RoomDmMessageDeletedPayload;
|
|
1422
|
+
timestamp: number;
|
|
1423
|
+
}
|
|
1424
|
+
| {
|
|
1425
|
+
type: typeof RoomServerMessageType.DM_REACTION_UPDATED;
|
|
1426
|
+
payload: RoomDmReactionUpdatedPayload;
|
|
1427
|
+
timestamp: number;
|
|
1428
|
+
}
|
|
1320
1429
|
| {
|
|
1321
1430
|
type: typeof RoomServerMessageType.TOPIC;
|
|
1322
1431
|
payload: RoomTopicResultPayload;
|
package/src/social-client.ts
CHANGED
|
@@ -62,7 +62,10 @@ export type {
|
|
|
62
62
|
RoomSuggestionSource,
|
|
63
63
|
} from "./social-types.js";
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
/** Largest per-file size any tier accepts (plus tier). The server enforces
|
|
66
|
+
* the per-tier caps (free 10MB/file, plus 25MB/file) and daily quotas; this
|
|
67
|
+
* constant only pre-flights uploads no tier could ever accept. */
|
|
68
|
+
export const ROOM_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024;
|
|
66
69
|
|
|
67
70
|
export class RoomSocialError extends Error {
|
|
68
71
|
override readonly name = "RoomSocialError";
|
|
@@ -658,6 +661,12 @@ export interface RoomSpaceMember {
|
|
|
658
661
|
/** Named identity roles (RoomSpaceRole.roleId). */
|
|
659
662
|
roleIds?: string[];
|
|
660
663
|
joinedAt?: string;
|
|
664
|
+
/** The member's handle/username, resolved server-side from their room
|
|
665
|
+
* memberships. Absent when the store couldn't resolve a real name (never
|
|
666
|
+
* the raw userId). */
|
|
667
|
+
handle?: string;
|
|
668
|
+
/** Per-space display name; null or absent means the account username. */
|
|
669
|
+
nickname?: string | null;
|
|
661
670
|
}
|
|
662
671
|
|
|
663
672
|
export async function getUserPrefs(config: RoomSocialConfig): Promise<Record<string, unknown>> {
|
|
@@ -1048,6 +1057,21 @@ export async function changeSpaceMemberRole(
|
|
|
1048
1057
|
return result.member;
|
|
1049
1058
|
}
|
|
1050
1059
|
|
|
1060
|
+
/** Set (string) or clear (null) a member's per-space nickname; the store validates and enforces uniqueness. */
|
|
1061
|
+
export async function setSpaceMemberNickname(
|
|
1062
|
+
config: RoomSocialConfig,
|
|
1063
|
+
spaceId: string,
|
|
1064
|
+
targetUserId: string,
|
|
1065
|
+
nickname: string | null,
|
|
1066
|
+
): Promise<RoomSpaceMember> {
|
|
1067
|
+
const result = await socialRequest<{ member: RoomSpaceMember }>(
|
|
1068
|
+
config,
|
|
1069
|
+
`/spaces/${encodeURIComponent(spaceId)}/members/${encodeURIComponent(targetUserId)}/nickname`,
|
|
1070
|
+
{ method: "PATCH", body: { nickname } },
|
|
1071
|
+
);
|
|
1072
|
+
return result.member;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1051
1075
|
export async function createRoomSpace(
|
|
1052
1076
|
config: RoomSocialConfig,
|
|
1053
1077
|
input: { name: string; access?: "open" | "invite" },
|
|
@@ -1076,12 +1100,24 @@ export async function getRoomAttachmentUrl(
|
|
|
1076
1100
|
config: RoomSocialConfig,
|
|
1077
1101
|
room: string,
|
|
1078
1102
|
key: string,
|
|
1103
|
+
opts: { name?: string; download?: boolean } = {},
|
|
1079
1104
|
): Promise<{ url: string; expiresAt: string | null }> {
|
|
1105
|
+
// The object key identifies the object and stays in the query. The
|
|
1106
|
+
// download filename and force-download flag ride REQUEST HEADERS, never
|
|
1107
|
+
// the URL: a private-room filename in the query string leaked into the
|
|
1108
|
+
// relay's access logs and error telemetry (Morgan, Sentry).
|
|
1080
1109
|
const params = new URLSearchParams({ key });
|
|
1110
|
+
const headers: Record<string, string> = {};
|
|
1111
|
+
// Percent-encode the name: Fetch/HTTP header values are byte strings, so a
|
|
1112
|
+
// raw Unicode filename (报告.py, an emoji) throws a TypeError before the
|
|
1113
|
+
// request is sent. The store decodes with decodeURIComponent.
|
|
1114
|
+
if (opts.name) headers["X-Om-Download-Name"] = encodeURIComponent(opts.name);
|
|
1115
|
+
// Forces a Content-Disposition attachment even for inline mimes.
|
|
1116
|
+
if (opts.download) headers["X-Om-Download"] = "1";
|
|
1081
1117
|
const result = await socialRequest<{ url?: string; expiresAt?: string | null }>(
|
|
1082
1118
|
config,
|
|
1083
1119
|
`/rooms/${encodeURIComponent(cleanRoomId(room))}/attachment-url?${params.toString()}`,
|
|
1084
|
-
{ method: "GET" },
|
|
1120
|
+
{ method: "GET", headers },
|
|
1085
1121
|
);
|
|
1086
1122
|
if (!result.url) {
|
|
1087
1123
|
throw new RoomSocialError("Attachment sign failed", 502, result);
|
|
@@ -1174,6 +1210,7 @@ export async function socialRequest<T>(
|
|
|
1174
1210
|
body?: unknown;
|
|
1175
1211
|
signal?: AbortSignal;
|
|
1176
1212
|
formData?: FormData;
|
|
1213
|
+
headers?: Record<string, string>;
|
|
1177
1214
|
} = {},
|
|
1178
1215
|
): Promise<T> {
|
|
1179
1216
|
const url = `${trimTrailingSlash(config.apiUrl)}${path}`;
|
|
@@ -1184,6 +1221,7 @@ export async function socialRequest<T>(
|
|
|
1184
1221
|
Authorization: `Bearer ${config.apiKey}`,
|
|
1185
1222
|
Accept: "application/json",
|
|
1186
1223
|
...(hasJsonBody ? { "Content-Type": "application/json" } : {}),
|
|
1224
|
+
...(init.headers ?? {}),
|
|
1187
1225
|
},
|
|
1188
1226
|
...(hasJsonBody
|
|
1189
1227
|
? { body: JSON.stringify(init.body) }
|
|
@@ -1263,12 +1301,72 @@ function trimTrailingSlash(value: string): string {
|
|
|
1263
1301
|
return value.replace(/\/+$/, "");
|
|
1264
1302
|
}
|
|
1265
1303
|
|
|
1304
|
+
/** Source/config/text extensions declared as text/plain so the server's
|
|
1305
|
+
* UTF-8 code-file lane accepts them. Markup extensions the server
|
|
1306
|
+
* blocklists (html, svg, xml, ...) are deliberately absent: they fall
|
|
1307
|
+
* through to application/octet-stream and the server rejects them. */
|
|
1308
|
+
const CODE_TEXT_EXTENSIONS = new Set([
|
|
1309
|
+
"js",
|
|
1310
|
+
"jsx",
|
|
1311
|
+
"ts",
|
|
1312
|
+
"tsx",
|
|
1313
|
+
"py",
|
|
1314
|
+
"rb",
|
|
1315
|
+
"go",
|
|
1316
|
+
"rs",
|
|
1317
|
+
"java",
|
|
1318
|
+
"c",
|
|
1319
|
+
"h",
|
|
1320
|
+
"cc",
|
|
1321
|
+
"cpp",
|
|
1322
|
+
"hpp",
|
|
1323
|
+
"cs",
|
|
1324
|
+
"php",
|
|
1325
|
+
"sh",
|
|
1326
|
+
"bash",
|
|
1327
|
+
"zsh",
|
|
1328
|
+
"fish",
|
|
1329
|
+
"ps1",
|
|
1330
|
+
"sql",
|
|
1331
|
+
"yaml",
|
|
1332
|
+
"yml",
|
|
1333
|
+
"toml",
|
|
1334
|
+
"ini",
|
|
1335
|
+
"cfg",
|
|
1336
|
+
"conf",
|
|
1337
|
+
"log",
|
|
1338
|
+
"diff",
|
|
1339
|
+
"patch",
|
|
1340
|
+
"tex",
|
|
1341
|
+
"r",
|
|
1342
|
+
"jl",
|
|
1343
|
+
"kt",
|
|
1344
|
+
"kts",
|
|
1345
|
+
"swift",
|
|
1346
|
+
"scala",
|
|
1347
|
+
"clj",
|
|
1348
|
+
"cljs",
|
|
1349
|
+
"ex",
|
|
1350
|
+
"exs",
|
|
1351
|
+
"erl",
|
|
1352
|
+
"lua",
|
|
1353
|
+
"pl",
|
|
1354
|
+
"pm",
|
|
1355
|
+
"dart",
|
|
1356
|
+
"gradle",
|
|
1357
|
+
"properties",
|
|
1358
|
+
"bat",
|
|
1359
|
+
"cmd",
|
|
1360
|
+
]);
|
|
1361
|
+
|
|
1266
1362
|
export function mimeFromFilename(filename: string): string {
|
|
1267
1363
|
const lower = filename.toLowerCase();
|
|
1268
1364
|
if (lower.endsWith(".png")) return "image/png";
|
|
1269
1365
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
1270
1366
|
if (lower.endsWith(".webp")) return "image/webp";
|
|
1271
1367
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
1368
|
+
if (lower.endsWith(".mp4")) return "video/mp4";
|
|
1369
|
+
if (lower.endsWith(".webm")) return "video/webm";
|
|
1272
1370
|
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
1273
1371
|
if (lower.endsWith(".txt")) return "text/plain";
|
|
1274
1372
|
if (lower.endsWith(".md") || lower.endsWith(".markdown")) return "text/markdown";
|
|
@@ -1283,5 +1381,7 @@ export function mimeFromFilename(filename: string): string {
|
|
|
1283
1381
|
if (lower.endsWith(".pptx")) {
|
|
1284
1382
|
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
1285
1383
|
}
|
|
1384
|
+
const dot = lower.lastIndexOf(".");
|
|
1385
|
+
if (dot !== -1 && CODE_TEXT_EXTENSIONS.has(lower.slice(dot + 1))) return "text/plain";
|
|
1286
1386
|
return "application/octet-stream";
|
|
1287
1387
|
}
|
package/src/social-types.ts
CHANGED
|
@@ -53,8 +53,13 @@ export interface RoomFollowResult {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export interface RoomAttachment {
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Object key under room-attachments/; clients sign it at render time.
|
|
58
|
+
* Absent while scanStatus is "pending" on entries served to members: the
|
|
59
|
+
* store withholds the signable key until the scan clears, so render the
|
|
60
|
+
* metadata as a placeholder and do not attempt signing without a key.
|
|
61
|
+
*/
|
|
62
|
+
key?: string | undefined;
|
|
58
63
|
name: string;
|
|
59
64
|
size: number;
|
|
60
65
|
mime: string;
|
|
@@ -62,8 +67,16 @@ export interface RoomAttachment {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export interface RoomAttachmentUpload {
|
|
65
|
-
/**
|
|
66
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The stored attachment plus its first signed URL, for immediate render.
|
|
72
|
+
* The uploader always receives the key; only member views of PENDING
|
|
73
|
+
* entries withhold it.
|
|
74
|
+
*/
|
|
75
|
+
attachment: RoomAttachment & {
|
|
76
|
+
key: string;
|
|
77
|
+
url: string;
|
|
78
|
+
expiresAt: string | null;
|
|
79
|
+
};
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
export interface RoomPublicProfile {
|
|
@@ -126,6 +139,11 @@ export interface RoomDmMessage {
|
|
|
126
139
|
/** Sealed DM: content is '' and the opaque envelope rides here. */
|
|
127
140
|
secret?: RoomLedgerSecretPayload | null;
|
|
128
141
|
metadata?: Record<string, unknown>;
|
|
142
|
+
/** DM image: a private dm-attachments/ key (signed on render) or a legacy
|
|
143
|
+
* public URL. Additive — old messages carry none. */
|
|
144
|
+
imageUrl?: string | null;
|
|
145
|
+
/** Aggregated reactions (per-emoji userId lists). Additive. */
|
|
146
|
+
reactions?: Array<{ emoji: string; users: string[] }>;
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
export interface RoomDmHistory {
|
package/src/types.ts
CHANGED
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.5.
|
|
2
|
-
export const RUNNER_VERSION = "0.5.
|
|
1
|
+
export const VERSION = "0.5.1";
|
|
2
|
+
export const RUNNER_VERSION = "0.5.1";
|