eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1{
2 "description": "Chat event vectors: ensures chat.js (web) and native (iOS/Mac) fold message/reaction/edit/delete events identically. Reactions are now encrypted events (type='reaction' stored like messages). Used by test/event-vectors.test.mjs and ios/.../EventVectorTests.swift.",
3 "app": "chat",
4 "source": "web/static/apps/chat.js",
5 "vectors": [
6 {
7 "name": "plain_text_message",
8 "description": "A simple text message with no attachments or reply.",
9 "decrypted_payload": {
10 "name": "Alice",
11 "text": "Hello everyone",
12 "ts": 1700000000000,
13 "uid": "user-alice-1"
14 },
15 "server_message": {
16 "id": 12345,
17 "created_at": 1700000000,
18 "blob": "encrypted(decrypted_payload)"
19 },
20 "expected_fold_event": {
21 "type": "chat.msg",
22 "name": "Alice",
23 "text": "Hello everyone",
24 "ts": 1700000000000,
25 "uid": "user-alice-1",
26 "attachments": []
27 },
28 "expected_native_fields": {
29 "name": "Alice",
30 "text": "Hello everyone",
31 "uid": "user-alice-1",
32 "isRenderableChat": true
33 }
34 },
35 {
36 "name": "message_with_attachments",
37 "description": "A message with an image and a file.",
38 "decrypted_payload": {
39 "name": "Bob",
40 "text": "Check these out",
41 "ts": 1700000001000,
42 "uid": "user-bob-1",
43 "attachments": [
44 {
45 "id": "blob-img-1",
46 "name": "photo.jpg",
47 "type": "image/jpeg",
48 "w": 1920,
49 "h": 1080,
50 "size": 524288
51 },
52 {
53 "id": "blob-file-1",
54 "name": "report.pdf",
55 "type": "application/pdf",
56 "size": 2097152
57 }
58 ]
59 },
60 "server_message": {
61 "id": 12346,
62 "created_at": 1700000001,
63 "blob": "encrypted(decrypted_payload)"
64 },
65 "expected_fold_event": {
66 "type": "chat.msg",
67 "name": "Bob",
68 "text": "Check these out",
69 "ts": 1700000001000,
70 "uid": "user-bob-1",
71 "attachments": [
72 {
73 "id": "blob-img-1",
74 "name": "photo.jpg",
75 "type": "image/jpeg",
76 "w": 1920,
77 "h": 1080,
78 "size": 524288
79 },
80 {
81 "id": "blob-file-1",
82 "name": "report.pdf",
83 "type": "application/pdf",
84 "size": 2097152
85 }
86 ]
87 }
88 },
89 {
90 "name": "message_with_reply",
91 "description": "A reply-to message with quoted context.",
92 "decrypted_payload": {
93 "name": "Charlie",
94 "text": "I agree!",
95 "ts": 1700000002000,
96 "uid": "user-charlie-1",
97 "replyTo": {
98 "id": 12345,
99 "name": "Alice",
100 "snippet": "Hello everyone"
101 }
102 },
103 "server_message": {
104 "id": 12347,
105 "created_at": 1700000002,
106 "blob": "encrypted(decrypted_payload)"
107 },
108 "expected_fold_event": {
109 "type": "chat.msg",
110 "name": "Charlie",
111 "text": "I agree!",
112 "ts": 1700000002000,
113 "uid": "user-charlie-1",
114 "replyTo": {
115 "id": 12345,
116 "name": "Alice",
117 "snippet": "Hello everyone"
118 },
119 "attachments": []
120 }
121 },
122 {
123 "name": "reply_in_thread_targets_root",
124 "description": "Replying to a message that is itself a reply continues that thread: the composer stages the thread's TOP message, so the payload's replyTo carries the ROOT's id/name/snippet β€” never the tapped reply's.",
125 "note": "Compose-side contract (web replyTarget in chat.js; native ChatFeedModel.replyTarget). Here Dana replied to message 12347 (Charlie's reply), but the staged replyTo names root 12345. The fold treats it like any reply; grouping is pinned by reply_thread_grouping.",
126 "decrypted_payload": {
127 "name": "Dana",
128 "text": "Same here!",
129 "ts": 1700000004000,
130 "uid": "user-dana-1",
131 "replyTo": {
132 "id": 12345,
133 "name": "Alice",
134 "snippet": "Hello everyone"
135 }
136 },
137 "server_message": {
138 "id": 12349,
139 "created_at": 1700000004,
140 "blob": "encrypted(decrypted_payload)"
141 },
142 "expected_fold_event": {
143 "type": "chat.msg",
144 "name": "Dana",
145 "text": "Same here!",
146 "ts": 1700000004000,
147 "uid": "user-dana-1",
148 "replyTo": {
149 "id": 12345,
150 "name": "Alice",
151 "snippet": "Hello everyone"
152 },
153 "attachments": []
154 }
155 },
156 {
157 "name": "reply_thread_grouping",
158 "description": "Reply threads group on the resolved ROOT message: new replies point straight at the root; a legacy chain (a reply quoting another reply) resolves transitively through loaded messages. The root's tally counts every message in its thread, and the first reply is the tally's jump target.",
159 "note": "Multi-message vector: fold the sequence, then derive the thread rollup (web viewItems replyRoot/replyCount in chat.js; native ChatFeedModel.threadRoots/refreshReplyCounts). Message 102 is a LEGACY chain β€” it quotes reply 101, not the root β€” and must still resolve to root 100.",
160 "message_sequence": [
161 {
162 "server_message": {
163 "id": 100,
164 "created_at": 1700000000
165 },
166 "decrypted_payload": {
167 "name": "Alice",
168 "text": "Hello everyone",
169 "ts": 1700000000000,
170 "uid": "user-alice-1"
171 }
172 },
173 {
174 "server_message": {
175 "id": 101,
176 "created_at": 1700000001
177 },
178 "decrypted_payload": {
179 "name": "Bob",
180 "text": "Hi Alice",
181 "ts": 1700000001000,
182 "uid": "user-bob-1",
183 "replyTo": {
184 "id": 100,
185 "name": "Alice",
186 "snippet": "Hello everyone"
187 }
188 }
189 },
190 {
191 "server_message": {
192 "id": 102,
193 "created_at": 1700000002
194 },
195 "decrypted_payload": {
196 "name": "Charlie",
197 "text": "Welcome!",
198 "ts": 1700000002000,
199 "uid": "user-charlie-1",
200 "replyTo": {
201 "id": 101,
202 "name": "Bob",
203 "snippet": "Hi Alice"
204 }
205 }
206 },
207 {
208 "server_message": {
209 "id": 103,
210 "created_at": 1700000003
211 },
212 "decrypted_payload": {
213 "name": "Dana",
214 "text": "Good to be here",
215 "ts": 1700000003000,
216 "uid": "user-dana-1",
217 "replyTo": {
218 "id": 100,
219 "name": "Alice",
220 "snippet": "Hello everyone"
221 }
222 }
223 }
224 ],
225 "expected_threads": {
226 "roots": {
227 "101": 100,
228 "102": 100,
229 "103": 100
230 },
231 "reply_counts": {
232 "100": 3
233 },
234 "first_reply": {
235 "100": 101
236 }
237 },
238 "expected_reply_target": {
239 "note": "Replying to ANY message in the thread (here the legacy chain reply 102) stages the root's id with a fresh snapshot of the root's name/text.",
240 "for_message": 102,
241 "replyTo": {
242 "id": 100,
243 "name": "Alice",
244 "snippet": "Hello everyone"
245 }
246 }
247 },
248 {
249 "name": "action_line",
250 "description": "A /me-style action line (e.g., 'added milk' from a to-do app).",
251 "decrypted_payload": {
252 "name": "Todo Bot",
253 "text": "added 'milk'",
254 "ts": 1700000003000,
255 "uid": "app:todo",
256 "action": true
257 },
258 "server_message": {
259 "id": 12348,
260 "created_at": 1700000003,
261 "blob": "encrypted(decrypted_payload)"
262 },
263 "expected_fold_event": {
264 "type": "chat.msg",
265 "name": "Todo Bot",
266 "text": "added 'milk'",
267 "ts": 1700000003000,
268 "uid": "app:todo",
269 "action": true,
270 "attachments": []
271 }
272 },
273 {
274 "name": "message_with_video",
275 "description": "A message with an embedded video (dimensions + duration for layout before blob loads).",
276 "decrypted_payload": {
277 "name": "Diana",
278 "text": "Check this video",
279 "ts": 1700000004000,
280 "uid": "user-diana-1",
281 "attachments": [
282 {
283 "id": "blob-vid-1",
284 "name": "clip.mp4",
285 "type": "video/mp4",
286 "w": 1280,
287 "h": 720,
288 "dur": 45.5,
289 "poster": "blob-vid-1-poster",
290 "size": 5242880
291 }
292 ]
293 },
294 "server_message": {
295 "id": 12349,
296 "created_at": 1700000004,
297 "blob": "encrypted(decrypted_payload)"
298 },
299 "expected_fold_event": {
300 "type": "chat.msg",
301 "name": "Diana",
302 "text": "Check this video",
303 "ts": 1700000004000,
304 "uid": "user-diana-1",
305 "attachments": [
306 {
307 "id": "blob-vid-1",
308 "name": "clip.mp4",
309 "type": "video/mp4",
310 "w": 1280,
311 "h": 720,
312 "dur": 45.5,
313 "poster": "blob-vid-1-poster",
314 "size": 5242880
315 }
316 ]
317 }
318 },
319 {
320 "name": "reaction_add",
321 "description": "A member reacting to a message with a single emoji. Reactions are encrypted events (type='reaction') that fold into the target message's reactions array.",
322 "decrypted_payload": {
323 "type": "reaction",
324 "targetSid": 12345,
325 "targetUid": "user-eve-1",
326 "emoji": "πŸ‘",
327 "targetText": "buy milk",
328 "name": "Alice",
329 "uid": "user-alice-1",
330 "ts": 1700000010000,
331 "on": true
332 },
333 "server_message": {
334 "id": 12353,
335 "created_at": 1700000010,
336 "blob": "encrypted(decrypted_payload)"
337 },
338 "expected_fold_event": {
339 "type": "chat.reaction",
340 "targetSid": 12345,
341 "emoji": "πŸ‘",
342 "uid": "user-alice-1",
343 "on": true
344 },
345 "expected_native_fields": {
346 "type": "reaction",
347 "targetSid": 12345,
348 "targetUid": "user-eve-1",
349 "emoji": "πŸ‘",
350 "targetText": "buy milk",
351 "uid": "user-alice-1",
352 "on": true
353 },
354 "note": "Reactions are encrypted events stored as messages. Clients decrypt and fold them into the target message's reactions array. Count and mine-flag are derived client-side from the accumulated reaction events. on=true adds the reaction; on=false removes it. The mine-flag is true if uid===me, false otherwise. targetUid is the reacted-to message's author, stamped by the reacting client so push handlers (sw.js, the iOS NSE) can show the notification only to the author; the fold ignores it. Omitted when the target is a legacy uid-less message β€” then no one is notified. targetText is a replySnippet of the reacted-to message, likewise stamped by the reacting client and likewise ignored by the fold: it exists so the chat list can say 'Alice reacted πŸ‘ to β€œbuy milk”' for a thread built from last_blob alone (format.js reactionLine / the Swift twin). Optional β€” reactions from clients that predate it carry none, and the line falls back to 'Alice reacted πŸ‘'."
355 },
356 {
357 "name": "reaction_remove",
358 "description": "A member removing their reaction from a message.",
359 "decrypted_payload": {
360 "type": "reaction",
361 "targetSid": 12346,
362 "targetUid": "user-eve-1",
363 "emoji": "❀️",
364 "targetText": "shipped it πŸŽ‰",
365 "name": "Bob",
366 "uid": "user-bob-1",
367 "ts": 1700000015000,
368 "on": false
369 },
370 "server_message": {
371 "id": 12354,
372 "created_at": 1700000015,
373 "blob": "encrypted(decrypted_payload)"
374 },
375 "expected_fold_event": {
376 "type": "chat.reaction",
377 "targetSid": 12346,
378 "emoji": "❀️",
379 "uid": "user-bob-1",
380 "on": false
381 },
382 "expected_native_fields": {
383 "type": "reaction",
384 "targetSid": 12346,
385 "targetUid": "user-eve-1",
386 "emoji": "❀️",
387 "targetText": "shipped it πŸŽ‰",
388 "uid": "user-bob-1",
389 "on": false
390 },
391 "note": "Removal is signaled by on=false. Clients fold both add and remove into the target message's reaction array; the current state is derived by filtering for on=true and counting. Removals never notify anyone (push handlers suppress on=false) β€” but they DO speak in the chat list ('Bob removed ❀️ from β€œshipped it πŸŽ‰β€'), because on reload this event is all the list has to render the row from."
392 },
393 {
394 "name": "edit",
395 "description": "A message edited by its author within the edit window.",
396 "original_payload": {
397 "name": "Eve",
398 "text": "typo here",
399 "ts": 1700000005000,
400 "uid": "user-eve-1"
401 },
402 "edited_payload": {
403 "name": "Eve",
404 "text": "no typo here",
405 "ts": 1700000005000,
406 "uid": "user-eve-1"
407 },
408 "server_message": {
409 "id": 12350,
410 "created_at": 1700000005,
411 "edited_at": 1700000010,
412 "blob": "encrypted(edited_payload)"
413 },
414 "expected_fold_event": {
415 "type": "chat.edit",
416 "msgId": 12350,
417 "text": "no typo here"
418 },
419 "note": "History/catch-up folds of an edited row (edited_at set) emit this SAME event, text included (#457): over a pre-seeded local store the chat.msg fold dedupes against the cached text, so this event is what carries an edit made while the device was away."
420 },
421 {
422 "name": "delete",
423 "description": "A message deleted by its author (hard-deleted server-side).",
424 "server_delete_frame": {
425 "type": "delete",
426 "id": 12351
427 },
428 "expected_fold_event": {
429 "type": "chat.delete",
430 "msgId": 12351
431 }
432 },
433 {
434 "name": "tombstone_delete",
435 "description": "A stored delete tombstone (#581/#583): the server hard-deleted message 12345 and left a fresh empty row naming it in tombstone_of. History pages and reconnect catch-up must fold it to the SAME event the live delete frame produces β€” one delete path, no gap-inference.",
436 "server_message": {
437 "id": 12360,
438 "created_at": 1751900000,
439 "blob": "",
440 "tombstone_of": 12345
441 },
442 "expected_fold_event": {
443 "type": "chat.delete",
444 "msgId": 12345
445 },
446 "note": "The tombstone's own id (12360) is deliberately fresh β€” above every client watermark β€” so forward-only after: sync always delivers it. Clients with no local copy of 12345 fold it as a no-op. Old clients that predate tombstone_of skip the row entirely (an empty blob fails decrypt)."
447 },
448 {
449 "name": "legacy_message_no_uid",
450 "description": "An older message (before uid was added) β€” both web and native fall back to matching by name.",
451 "decrypted_payload": {
452 "name": "Frank",
453 "text": "This is old",
454 "ts": 1699000000000
455 },
456 "server_message": {
457 "id": 12352,
458 "created_at": 1699000000,
459 "blob": "encrypted(decrypted_payload)"
460 },
461 "expected_fold_event": {
462 "type": "chat.msg",
463 "name": "Frank",
464 "text": "This is old",
465 "ts": 1699000000000,
466 "attachments": []
467 },
468 "note": "Both implementations should treat missing uid as valid and fall back to name-based matching for reply attribution."
469 },
470 {
471 "name": "message_with_transport_gossip",
472 "description": "A message whose payload carries the host-level _ckpt tamper-evidence gossip field (docs/tamper-evidence.md). _ckpt is transport metadata injected/stripped by the web host: folds must drop it (never render or store it), and native decode must tolerate and ignore it β€” it is not app or chat content and must never be re-emitted.",
473 "note": "Added with #573 slice 2. The web host strips _ckpt before folding; this vector pins that even an unstripped payload folds without it (the reduce allow-lists fields). Native: Codable ignores unknown keys β€” this vector documents that as contract, not accident.",
474 "decrypted_payload": {
475 "name": "Alice",
476 "text": "Hello with gossip",
477 "ts": 1700000000000,
478 "uid": "user-alice-1",
479 "_ckpt": [
480 9,
481 "b95fd584ded27920b2b8aa67add5b59269ac572f2924d1fb5a1387e650b5344e"
482 ]
483 },
484 "server_message": {
485 "id": 12399,
486 "created_at": 1700000000,
487 "blob": "encrypted(decrypted_payload)"
488 },
489 "expected_fold_event": {
490 "type": "chat.msg",
491 "name": "Alice",
492 "text": "Hello with gossip",
493 "ts": 1700000000000,
494 "uid": "user-alice-1",
495 "attachments": []
496 },
497 "expected_native_fields": {
498 "name": "Alice",
499 "text": "Hello with gossip",
500 "uid": "user-alice-1",
501 "isRenderableChat": true
502 }
503 },
504 {
505 "name": "admin_warning_message",
506 "description": "An admin warning (issue #578): an ordinary E2EE DM message whose payload carries warn:true. Clients render the same bubble with a distinct 'notice from an admin' framing; the flag rides inside the ciphertext, so the server never knows a warning from a hello.",
507 "decrypted_payload": {
508 "name": "Root",
509 "text": "A report was filed about your behaviour here. Please keep this space welcoming.",
510 "ts": 1700000300000,
511 "uid": "user-admin-1",
512 "warn": true
513 },
514 "server_message": {
515 "id": 12399,
516 "created_at": 1700000300,
517 "blob": "encrypted(decrypted_payload)"
518 },
519 "expected_fold_event": {
520 "type": "chat.msg",
521 "name": "Root",
522 "text": "A report was filed about your behaviour here. Please keep this space welcoming.",
523 "ts": 1700000300000,
524 "uid": "user-admin-1",
525 "warn": true,
526 "attachments": []
527 },
528 "expected_native_fields": {
529 "name": "Root",
530 "text": "A report was filed about your behaviour here. Please keep this space welcoming.",
531 "uid": "user-admin-1",
532 "warn": true,
533 "isRenderableChat": true
534 }
535 }
536 ]
537}