@wirecat/cli-core 0.381.0 → 0.383.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/CHANGELOG.md +50 -0
- package/dist/attachments/documents.d.ts +3 -0
- package/dist/attachments/documents.js +17 -0
- package/dist/cli/bot/watch.js +3 -0
- package/dist/cli/messenger/chat-answers.d.ts +2 -0
- package/dist/cli/messenger/chat-answers.js +2 -0
- package/dist/cli/messenger/chat-lists.d.ts +2 -0
- package/dist/cli/messenger/context.d.ts +2 -2
- package/dist/cli/messenger/context.js +2 -1
- package/dist/cli/messenger/conversation-answers.d.ts +1 -0
- package/dist/cli/messenger/message-answers.d.ts +1 -0
- package/dist/cli/messenger/message-items.js +1 -0
- package/dist/cli/messenger/message-writes.d.ts +1 -0
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/stored.d.ts +3 -1
- package/dist/cli/messenger/stored.js +12 -5
- package/dist/cli/messenger/watch-command.js +2 -0
- package/dist/documents/blocks.d.ts +45 -0
- package/dist/documents/blocks.js +48 -0
- package/dist/documents/index.d.ts +5 -0
- package/dist/documents/index.js +4 -0
- package/dist/documents/markdown-blocks.d.ts +3 -0
- package/dist/documents/markdown-blocks.js +123 -0
- package/dist/documents/push-model.d.ts +89 -0
- package/dist/documents/push-model.js +1 -0
- package/dist/documents.d.ts +1 -0
- package/dist/documents.js +1 -0
- package/dist/domain/models.d.ts +31 -4
- package/dist/entities/chat.d.ts +4 -0
- package/dist/entities/chat.js +4 -0
- package/dist/entities/contact.d.ts +4 -0
- package/dist/entities/evidence.d.ts +8 -0
- package/dist/entities/index.d.ts +17 -0
- package/dist/entities/message.d.ts +2 -0
- package/dist/entities/message.js +2 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/meetings/answers.d.ts +1 -0
- package/dist/meetings/index.d.ts +1 -1
- package/dist/meetings/index.js +1 -1
- package/dist/meetings/item.d.ts +10 -0
- package/dist/meetings/item.js +34 -18
- package/dist/ports/traits.d.ts +6 -0
- package/dist/services/chats.js +16 -2
- package/dist/services/deps.d.ts +2 -0
- package/dist/services/deps.js +2 -0
- package/dist/services/documents/block-diff.d.ts +24 -0
- package/dist/services/documents/block-diff.js +91 -0
- package/dist/services/documents/push.d.ts +60 -0
- package/dist/services/documents/push.js +218 -0
- package/dist/services/inbox.d.ts +1 -0
- package/dist/services/inbox.js +10 -3
- package/dist/store/keys.d.ts +1 -1
- package/dist/store/messages.d.ts +7 -1
- package/dist/store/messages.js +16 -3
- package/dist/store/sqlite/chat-rekey.js +9 -1
- package/dist/store/sqlite/chats.d.ts +2 -2
- package/dist/store/sqlite/chats.js +37 -8
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/meeting-hit-query.js +18 -3
- package/dist/store/sqlite/messages.d.ts +9 -2
- package/dist/store/sqlite/messages.js +29 -2
- package/dist/store/sqlite/migrations.generated.js +16 -0
- package/dist/store/sqlite/reads.d.ts +50 -0
- package/dist/store/sqlite/reads.js +3 -1
- package/dist/store/sqlite/schema.d.ts +96 -0
- package/dist/store/sqlite/schema.js +6 -0
- package/dist/store/sqlite/search.d.ts +33 -0
- package/dist/store/sqlite/values.d.ts +4 -0
- package/dist/store/sqlite/values.js +3 -0
- package/package.json +5 -1
|
@@ -137,6 +137,7 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
137
137
|
participantsCount: chat.participantsCount ?? null,
|
|
138
138
|
metadata: mergedMetadata(stored?.metadata ?? null, chat.providerMetadata),
|
|
139
139
|
membershipState: chat.membershipState === "member" ? null : (chat.membershipState ?? null),
|
|
140
|
+
...stateOf(chat, now()),
|
|
140
141
|
createdAt: now(),
|
|
141
142
|
updatedAt: now(),
|
|
142
143
|
})
|
|
@@ -147,16 +148,33 @@ export const upsertChat = (context, accountKey, chat) => {
|
|
|
147
148
|
...(chat.parentChatId === undefined ? {} : { parentChatId: sql `excluded.parent_chat_id` }),
|
|
148
149
|
scope: sql `coalesce(excluded.scope, ${chats.scope})`,
|
|
149
150
|
...(chat.title === undefined ? {} : { title: sql `excluded.title` }),
|
|
150
|
-
...(chat.unreadCount === undefined
|
|
151
|
+
...(chat.unreadCount === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { unreadCount: sql `excluded.unread_count`, unreadCountedAt: sql `excluded.unread_counted_at` }),
|
|
151
154
|
...(chat.lastMessageAt === undefined ? {} : { lastMessageAt: sql `excluded.last_message_at` }),
|
|
152
155
|
...(chat.participantsCount === undefined ? {} : { participantsCount: sql `excluded.participants_count` }),
|
|
153
156
|
metadata: sql `excluded.metadata`,
|
|
154
157
|
...membershipSet(chat.membershipState),
|
|
158
|
+
...(chat.muted === undefined ? {} : { muted: sql `excluded.muted`, mutedUntil: sql `excluded.muted_until` }),
|
|
159
|
+
...(chat.archived === undefined ? {} : { archived: sql `excluded.archived` }),
|
|
160
|
+
...(chat.markedUnread === undefined ? {} : { markedUnread: sql `excluded.marked_unread` }),
|
|
155
161
|
updatedAt: sql `excluded.updated_at`,
|
|
156
162
|
},
|
|
157
163
|
})
|
|
158
164
|
.run();
|
|
159
165
|
};
|
|
166
|
+
const bit = (value) => (value === undefined ? null : value ? 1 : 0);
|
|
167
|
+
/**
|
|
168
|
+
* The count is taken as of the chat's newest message where the update names it, else as of now: a message the store
|
|
169
|
+
* saves later and sent after that point is one the count does not hold yet.
|
|
170
|
+
*/
|
|
171
|
+
const stateOf = (chat, at) => ({
|
|
172
|
+
unreadCountedAt: chat.unreadCount === undefined ? null : (toMs(chat.lastMessageAt) ?? at),
|
|
173
|
+
muted: bit(chat.muted),
|
|
174
|
+
mutedUntil: chat.muted === true ? toMs(chat.mutedUntil) : null,
|
|
175
|
+
archived: bit(chat.archived),
|
|
176
|
+
markedUnread: bit(chat.markedUnread),
|
|
177
|
+
});
|
|
160
178
|
const membershipSet = (state) => {
|
|
161
179
|
if (state === undefined)
|
|
162
180
|
return {};
|
|
@@ -286,7 +304,7 @@ export const purgeChats = ({ orm }, pks) => {
|
|
|
286
304
|
orm.delete(messages).where(inArray(messages.chatId, pks)).run();
|
|
287
305
|
orm.delete(chats).where(inArray(chats.id, pks)).run();
|
|
288
306
|
};
|
|
289
|
-
export const chatsWith = ({ orm }, accountKey, key, memberId) => {
|
|
307
|
+
export const chatsWith = ({ orm, now }, accountKey, key, memberId) => {
|
|
290
308
|
const rows = orm
|
|
291
309
|
.select({ chat: chats })
|
|
292
310
|
.from(chats)
|
|
@@ -300,7 +318,7 @@ export const chatsWith = ({ orm }, accountKey, key, memberId) => {
|
|
|
300
318
|
.orderBy(...byRecency)
|
|
301
319
|
.all()
|
|
302
320
|
.map(({ chat }) => chat);
|
|
303
|
-
return toChats(orm, rows);
|
|
321
|
+
return toChats(orm, rows, now());
|
|
304
322
|
};
|
|
305
323
|
const chatsWhere = (accountKey, { where, includeLeft }) => and(eq(chats.accountId, accountKey), includeLeft ? undefined : notLeft, where === undefined ? undefined : bound(where));
|
|
306
324
|
/**
|
|
@@ -324,7 +342,7 @@ export const CHAT_COLUMNS = {
|
|
|
324
342
|
};
|
|
325
343
|
/** `title:*book*` and `title:book` ask the same: the title holds "book". */
|
|
326
344
|
export const titlePart = (value) => value.replace(/^\*+|\*+$/g, "").trim();
|
|
327
|
-
export const listChats = ({ orm }, accountKey, { limit, offset = 0, ...filter }) => {
|
|
345
|
+
export const listChats = ({ orm, now }, accountKey, { limit, offset = 0, ...filter }) => {
|
|
328
346
|
const rows = orm
|
|
329
347
|
.select()
|
|
330
348
|
.from(chats)
|
|
@@ -333,10 +351,10 @@ export const listChats = ({ orm }, accountKey, { limit, offset = 0, ...filter })
|
|
|
333
351
|
.limit(limit === undefined ? -1 : limit + 1)
|
|
334
352
|
.offset(offset)
|
|
335
353
|
.all();
|
|
336
|
-
return { items: toChats(orm, rows.slice(0, limit)), hasMore: limit !== undefined && rows.length > limit };
|
|
354
|
+
return { items: toChats(orm, rows.slice(0, limit), now()), hasMore: limit !== undefined && rows.length > limit };
|
|
337
355
|
};
|
|
338
356
|
export const countChats = ({ orm }, accountKey, filter) => Number(orm.select({ n: sql `count(*)` }).from(chats).where(chatsWhere(accountKey, filter)).get()?.n);
|
|
339
|
-
const toChats = (orm, rows) => {
|
|
357
|
+
const toChats = (orm, rows, at) => {
|
|
340
358
|
const ids = [...new Set(rows.flatMap((row) => (row.parentChatId === null ? [] : [row.parentChatId])))];
|
|
341
359
|
const parents = new Map(ids.length === 0
|
|
342
360
|
? []
|
|
@@ -346,9 +364,10 @@ const toChats = (orm, rows) => {
|
|
|
346
364
|
.where(inArray(chats.id, ids))
|
|
347
365
|
.all()
|
|
348
366
|
.map((row) => [row.pk, row.externalId]));
|
|
349
|
-
return rows.map((row) => toChat(row, row.parentChatId === null ? undefined : parents.get(row.parentChatId)));
|
|
367
|
+
return rows.map((row) => toChat(row, at, row.parentChatId === null ? undefined : parents.get(row.parentChatId)));
|
|
350
368
|
};
|
|
351
|
-
const
|
|
369
|
+
const flag = (stored) => (stored === null ? undefined : stored === 1);
|
|
370
|
+
const toChat = (row, at, parentId) => ({
|
|
352
371
|
id: row.externalId,
|
|
353
372
|
title: row.title,
|
|
354
373
|
kind: row.kind,
|
|
@@ -361,7 +380,17 @@ const toChat = (row, parentId) => ({
|
|
|
361
380
|
membershipState: row.membershipState,
|
|
362
381
|
providerMetadata: parsed(row.metadata),
|
|
363
382
|
}),
|
|
383
|
+
...muteOf(row, at),
|
|
384
|
+
...present({ archived: flag(row.archived), markedUnread: flag(row.markedUnread) }),
|
|
364
385
|
});
|
|
386
|
+
// A mute is read against the clock, so one that ran out needs no write to end.
|
|
387
|
+
const muteOf = ({ muted, mutedUntil }, at) => {
|
|
388
|
+
if (muted === null)
|
|
389
|
+
return {};
|
|
390
|
+
if (muted !== 1 || (mutedUntil !== null && mutedUntil <= at))
|
|
391
|
+
return { muted: false };
|
|
392
|
+
return mutedUntil === null ? { muted: true } : { muted: true, mutedUntil: toIso(mutedUntil) };
|
|
393
|
+
};
|
|
365
394
|
const metadataChatPk = ({ database }, key, chatId) => {
|
|
366
395
|
const row = database
|
|
367
396
|
.prepare("SELECT c.id FROM chats c JOIN accounts a ON a.id=c.account_id WHERE a.provider=? AND a.external_id=? AND c.external_id=?")
|
|
@@ -32,6 +32,8 @@ export const MANIFEST = [
|
|
|
32
32
|
{ name: "20261019115025_version-25-chat-revisions", version: 25, minCompatible: 1 },
|
|
33
33
|
{ name: "20261019115027_version-25-chat-revisions-triggers", version: 25, minCompatible: 1 },
|
|
34
34
|
{ name: "20261021214318_version-26-message-reactions", version: 26, minCompatible: 1 },
|
|
35
|
+
{ name: "20261021221808_version-27-message-delivery", version: 27, minCompatible: 1 },
|
|
36
|
+
{ name: "20261021224625_version-28-chat-state", version: 28, minCompatible: 1 },
|
|
35
37
|
];
|
|
36
38
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
37
39
|
const migrations = [];
|
|
@@ -21,7 +21,7 @@ const speakers = (database, accountId) => {
|
|
|
21
21
|
found.set(key, speaker);
|
|
22
22
|
};
|
|
23
23
|
const participants = database
|
|
24
|
-
.prepare(`SELECT p.id,p.identity_id,p.display_name,i.name AS identity_name,l.person_id,person.name AS person_name
|
|
24
|
+
.prepare(`SELECT p.id,p.identity_id,p.display_name,p.email,i.external_id AS identity_key,i.name AS identity_name,l.person_id,person.name AS person_name
|
|
25
25
|
FROM meeting_participants p JOIN meetings m ON m.id=p.meeting_id
|
|
26
26
|
LEFT JOIN identities i ON i.id=p.identity_id LEFT JOIN identity_links l ON l.identity_id=p.identity_id
|
|
27
27
|
LEFT JOIN persons person ON person.id=l.person_id WHERE m.account_id=? AND m.deleted_at IS NULL`)
|
|
@@ -34,11 +34,26 @@ const speakers = (database, accountId) => {
|
|
|
34
34
|
const others = personNames.all(Number(row.person_id)).map((one) => (one.name === null ? null : String(one.name)));
|
|
35
35
|
add(`person:${row.person_id}`, String(row.person_name ?? label), [...names, ...others], Number(row.id));
|
|
36
36
|
}
|
|
37
|
-
else if (row.identity_id !== null &&
|
|
38
|
-
|
|
37
|
+
else if (row.identity_id !== null &&
|
|
38
|
+
row.identity_id !== undefined &&
|
|
39
|
+
!String(row.identity_key).startsWith("name:"))
|
|
40
|
+
add(`identity:${row.identity_id}`, row.email ? `${label} <${row.email}>` : label, names, Number(row.id));
|
|
41
|
+
// A participant known only by name is keyed per meeting (`name:…@account/occurrence:n`); across meetings the name is all there is.
|
|
39
42
|
else
|
|
40
43
|
add(`name:${fold(label)}`, label, names, Number(row.id));
|
|
41
44
|
}
|
|
45
|
+
// A name heard in one meeting is the identified speaker of that name, when exactly one has it.
|
|
46
|
+
for (const [key, speaker] of found) {
|
|
47
|
+
if (!key.startsWith("name:"))
|
|
48
|
+
continue;
|
|
49
|
+
const named = [...found].filter(([other, one]) => !other.startsWith("name:") && one.names.has(key.slice(5)));
|
|
50
|
+
const [only] = named;
|
|
51
|
+
if (named.length !== 1 || !only)
|
|
52
|
+
continue;
|
|
53
|
+
for (const participant of speaker.participants)
|
|
54
|
+
only[1].participants.add(participant);
|
|
55
|
+
found.delete(key);
|
|
56
|
+
}
|
|
42
57
|
const unlinked = database
|
|
43
58
|
.prepare(`SELECT DISTINCT r.speaker_name AS name FROM meeting_transcript_rows r
|
|
44
59
|
JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id JOIN meetings m ON m.id=t.meeting_id
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type { Id, Message, PersonReaction, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
1
|
+
import type { Delivery, Id, Message, PersonReaction, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
2
2
|
import type { AccountKey, DeletionScope } from "../types.js";
|
|
3
3
|
import type { StoreContext } from "./open.js";
|
|
4
|
-
export declare const upsertMessage: (context: StoreContext, key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) =>
|
|
4
|
+
export declare const upsertMessage: (context: StoreContext, key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* One more unread for a message from someone else that the store first saw now, sent after the messenger's count was
|
|
7
|
+
* taken. A chat the messenger never gave a count for is left alone: the store would count its whole history.
|
|
8
|
+
*/
|
|
9
|
+
export declare const countUnread: ({ database }: StoreContext, chatKey: number, message: Message) => void;
|
|
5
10
|
/**
|
|
6
11
|
* The owner's ruling (NEED-393 A): a deleted message keeps its row, so a later sync cannot bring it
|
|
7
12
|
* back, and leaves no text — not in the row, the search copy, the edit history, a transcript or a vector.
|
|
@@ -11,6 +16,8 @@ export declare const tombstone: (context: StoreContext, pk: number) => number;
|
|
|
11
16
|
export declare const saveReactions: (context: StoreContext, chatKey: number, messageId: Id, reactions: Reactions) => boolean;
|
|
12
17
|
/** Answers whether the message is held; a deleted one keeps no star. */
|
|
13
18
|
export declare const saveStar: ({ orm }: StoreContext, chatKey: number, messageId: Id, starred: boolean) => boolean;
|
|
19
|
+
/** Answers whether the owner's own message is held. A receipt is for a message the owner sent, so another's keeps none. */
|
|
20
|
+
export declare const saveDelivery: ({ orm }: StoreContext, chatKey: number, messageId: Id, delivery: Delivery) => boolean;
|
|
14
21
|
export declare const markDeleted: (context: StoreContext, accountKey: number, messageIds: Id[], chatId: Id | undefined, among?: DeletionScope) => number;
|
|
15
22
|
/** A vote older than the one held for that voter is dropped, and so is any vote in a deleted poll. */
|
|
16
23
|
export declare const saveVote: (context: StoreContext, accountKey: number, provider: Provider, chatKey: number, messageId: Id, { person, answers, votedAt }: PollVote) => void;
|
|
@@ -7,7 +7,7 @@ import { folded } from "./folded.js";
|
|
|
7
7
|
import { identityPk } from "./identities.js";
|
|
8
8
|
import { attachments, chats, messageRevisions, messages, messageTranscripts } from "./schema.js";
|
|
9
9
|
import { intoVoiceAttachment } from "./transcripts.js";
|
|
10
|
-
import { json, parsed, toMs } from "./values.js";
|
|
10
|
+
import { DELIVERY, json, parsed, toMs } from "./values.js";
|
|
11
11
|
import { purgeVectorsOf } from "./vectors.js";
|
|
12
12
|
const FIELDS = [
|
|
13
13
|
"threadExternalId",
|
|
@@ -120,7 +120,7 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
|
|
|
120
120
|
const revived = found?.deletedAt != null && seenAt !== undefined && found.deletedAt < seenAt;
|
|
121
121
|
// A deleted message leaves no text behind, and a sync that still carries it does not bring it back.
|
|
122
122
|
if (found?.deletedAt != null && !revived)
|
|
123
|
-
return;
|
|
123
|
+
return false;
|
|
124
124
|
let pk;
|
|
125
125
|
if (!found) {
|
|
126
126
|
pk = Number(statements.insert.get({
|
|
@@ -189,6 +189,12 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
|
|
|
189
189
|
.set({ starred: message.starred ? 1 : 0 })
|
|
190
190
|
.where(eq(messages.id, pk))
|
|
191
191
|
.run();
|
|
192
|
+
if (message.delivery !== undefined)
|
|
193
|
+
orm
|
|
194
|
+
.update(messages)
|
|
195
|
+
.set({ delivery: raised(message.delivery) })
|
|
196
|
+
.where(and(eq(messages.id, pk), eq(messages.outgoing, 1)))
|
|
197
|
+
.run();
|
|
192
198
|
const root = message.threadRootId ?? message.threadId;
|
|
193
199
|
if (root !== undefined) {
|
|
194
200
|
context.database
|
|
@@ -222,6 +228,19 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
|
|
|
222
228
|
});
|
|
223
229
|
if (Number(heardFirst) > 0)
|
|
224
230
|
intoVoiceAttachment(context, pk);
|
|
231
|
+
return !found;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* One more unread for a message from someone else that the store first saw now, sent after the messenger's count was
|
|
235
|
+
* taken. A chat the messenger never gave a count for is left alone: the store would count its whole history.
|
|
236
|
+
*/
|
|
237
|
+
export const countUnread = ({ database }, chatKey, message) => {
|
|
238
|
+
if (message.outgoing || message.editedAt !== null)
|
|
239
|
+
return;
|
|
240
|
+
database
|
|
241
|
+
.prepare(`UPDATE chats SET unread_count = unread_count + 1
|
|
242
|
+
WHERE id = ? AND unread_count IS NOT NULL AND unread_counted_at IS NOT NULL AND unread_counted_at < ?`)
|
|
243
|
+
.run(chatKey, Date.parse(message.timestamp));
|
|
225
244
|
};
|
|
226
245
|
/**
|
|
227
246
|
* The owner's ruling (NEED-393 A): a deleted message keeps its row, so a later sync cannot bring it
|
|
@@ -268,6 +287,14 @@ export const saveStar = ({ orm }, chatKey, messageId, starred) => orm
|
|
|
268
287
|
.where(and(eq(messages.chatId, chatKey), eq(messages.externalId, messageId), isNull(messages.deletedAt)))
|
|
269
288
|
.returning({ pk: messages.id })
|
|
270
289
|
.all().length > 0;
|
|
290
|
+
const raised = (delivery) => sql `max(coalesce(delivery, 0), ${DELIVERY[delivery]})`;
|
|
291
|
+
/** Answers whether the owner's own message is held. A receipt is for a message the owner sent, so another's keeps none. */
|
|
292
|
+
export const saveDelivery = ({ orm }, chatKey, messageId, delivery) => orm
|
|
293
|
+
.update(messages)
|
|
294
|
+
.set({ delivery: raised(delivery) })
|
|
295
|
+
.where(and(eq(messages.chatId, chatKey), eq(messages.externalId, messageId), eq(messages.outgoing, 1), isNull(messages.deletedAt)))
|
|
296
|
+
.returning({ pk: messages.id })
|
|
297
|
+
.all().length > 0;
|
|
271
298
|
export const markDeleted = (context, accountKey, messageIds, chatId, among) => {
|
|
272
299
|
const { orm } = context;
|
|
273
300
|
const chatKey = chatId === undefined ? undefined : findChatPk(context, accountKey, chatId);
|
|
@@ -561,5 +561,21 @@ export const GENERATED = [
|
|
|
561
561
|
"CREATE TABLE `message_reactions` (\n\t`id` integer PRIMARY KEY,\n\t`chat_id` integer NOT NULL,\n\t`message_external_id` text NOT NULL,\n\t`identity_id` integer NOT NULL,\n\t`own` integer NOT NULL,\n\t`reaction` text,\n\t`reacted_at` integer NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\tCONSTRAINT `fk_message_reactions_chat_id_chats_id_fk` FOREIGN KEY (`chat_id`) REFERENCES `chats`(`id`),\n\tCONSTRAINT `fk_message_reactions_identity_id_identities_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities`(`id`),\n\tCONSTRAINT `message_reactions_chat_id_message_external_id_identity_id_unique` UNIQUE(`chat_id`,`message_external_id`,`identity_id`)\n);",
|
|
562
562
|
"CREATE INDEX `message_reactions_by_identity_id` ON `message_reactions` (`identity_id`);"
|
|
563
563
|
]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"name": "20261021221808_version-27-message-delivery",
|
|
567
|
+
"statements": [
|
|
568
|
+
"ALTER TABLE `messages` ADD `delivery` integer;"
|
|
569
|
+
]
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"name": "20261021224625_version-28-chat-state",
|
|
573
|
+
"statements": [
|
|
574
|
+
"ALTER TABLE `chats` ADD `muted` integer;",
|
|
575
|
+
"ALTER TABLE `chats` ADD `muted_until` integer;",
|
|
576
|
+
"ALTER TABLE `chats` ADD `archived` integer;",
|
|
577
|
+
"ALTER TABLE `chats` ADD `marked_unread` integer;",
|
|
578
|
+
"ALTER TABLE `chats` ADD `unread_counted_at` integer;"
|
|
579
|
+
]
|
|
564
580
|
}
|
|
565
581
|
];
|
|
@@ -266,6 +266,22 @@ export declare const MESSAGE_FIELDS: {
|
|
|
266
266
|
identity: undefined;
|
|
267
267
|
generated: undefined;
|
|
268
268
|
}, {}>;
|
|
269
|
+
delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
270
|
+
name: string;
|
|
271
|
+
tableName: "messages";
|
|
272
|
+
dataType: "number int53";
|
|
273
|
+
data: number;
|
|
274
|
+
driverParam: number;
|
|
275
|
+
notNull: false;
|
|
276
|
+
hasDefault: false;
|
|
277
|
+
isPrimaryKey: false;
|
|
278
|
+
isAutoincrement: false;
|
|
279
|
+
hasRuntimeDefault: false;
|
|
280
|
+
enumValues: undefined;
|
|
281
|
+
baseColumn: never;
|
|
282
|
+
identity: undefined;
|
|
283
|
+
generated: undefined;
|
|
284
|
+
}, {}>;
|
|
269
285
|
};
|
|
270
286
|
export interface MessageRow {
|
|
271
287
|
pk: number;
|
|
@@ -286,6 +302,7 @@ export interface MessageRow {
|
|
|
286
302
|
providerMetadata: string | null;
|
|
287
303
|
mentions: string | null;
|
|
288
304
|
starred: number | null;
|
|
305
|
+
delivery: number | null;
|
|
289
306
|
}
|
|
290
307
|
export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("drizzle-orm/sqlite-core").SQLiteAsyncSelectBase<"messages", "sync", unknown, {
|
|
291
308
|
pk: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
@@ -546,6 +563,22 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
|
|
|
546
563
|
identity: undefined;
|
|
547
564
|
generated: undefined;
|
|
548
565
|
}, {}>;
|
|
566
|
+
delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
567
|
+
name: string;
|
|
568
|
+
tableName: "messages";
|
|
569
|
+
dataType: "number int53";
|
|
570
|
+
data: number;
|
|
571
|
+
driverParam: number;
|
|
572
|
+
notNull: false;
|
|
573
|
+
hasDefault: false;
|
|
574
|
+
isPrimaryKey: false;
|
|
575
|
+
isAutoincrement: false;
|
|
576
|
+
hasRuntimeDefault: false;
|
|
577
|
+
enumValues: undefined;
|
|
578
|
+
baseColumn: never;
|
|
579
|
+
identity: undefined;
|
|
580
|
+
generated: undefined;
|
|
581
|
+
}, {}>;
|
|
549
582
|
}, "partial", Record<"messages", "not-null"> & {
|
|
550
583
|
chats: "not-null";
|
|
551
584
|
} & {
|
|
@@ -569,6 +602,7 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
|
|
|
569
602
|
providerMetadata: string | null;
|
|
570
603
|
mentions: string | null;
|
|
571
604
|
starred: number | null;
|
|
605
|
+
delivery: number | null;
|
|
572
606
|
}[], {
|
|
573
607
|
pk: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
574
608
|
name: string;
|
|
@@ -828,6 +862,22 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
|
|
|
828
862
|
identity: undefined;
|
|
829
863
|
generated: undefined;
|
|
830
864
|
}, {}>;
|
|
865
|
+
delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
866
|
+
name: string;
|
|
867
|
+
tableName: "messages";
|
|
868
|
+
dataType: "number int53";
|
|
869
|
+
data: number;
|
|
870
|
+
driverParam: number;
|
|
871
|
+
notNull: false;
|
|
872
|
+
hasDefault: false;
|
|
873
|
+
isPrimaryKey: false;
|
|
874
|
+
isAutoincrement: false;
|
|
875
|
+
hasRuntimeDefault: false;
|
|
876
|
+
enumValues: undefined;
|
|
877
|
+
baseColumn: never;
|
|
878
|
+
identity: undefined;
|
|
879
|
+
generated: undefined;
|
|
880
|
+
}, {}>;
|
|
831
881
|
}>, never>;
|
|
832
882
|
export declare const newestFirst: SQL<unknown>[];
|
|
833
883
|
export declare const before: (sentAt: number, pk: number) => SQL<unknown>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { and, asc, desc, eq, gt, gte, inArray, isNull, lte, or, sql } from "./drizzle/core.js";
|
|
3
3
|
import { attachments, chats, identities, messageRevisions, messages } from "./schema.js";
|
|
4
|
-
import { parsed, present, toIso, toMs } from "./values.js";
|
|
4
|
+
import { deliveryOf, parsed, present, toIso, toMs } from "./values.js";
|
|
5
5
|
/**
|
|
6
6
|
* One flat row for every read, aliased where tables share a column name, so the same mapper serves
|
|
7
7
|
* a plain select and a subquery (`find` per chat).
|
|
@@ -25,6 +25,7 @@ export const MESSAGE_FIELDS = {
|
|
|
25
25
|
providerMetadata: messages.metadata,
|
|
26
26
|
mentions: messages.mentions,
|
|
27
27
|
starred: messages.starred,
|
|
28
|
+
delivery: messages.delivery,
|
|
28
29
|
};
|
|
29
30
|
export const selectMessages = ({ orm }) => orm
|
|
30
31
|
.select(MESSAGE_FIELDS)
|
|
@@ -236,6 +237,7 @@ const toMessage = (row, attachments) => {
|
|
|
236
237
|
reactions: parsed(row.reactions) ?? null,
|
|
237
238
|
...present({ providerMetadata: parsed(row.providerMetadata), mentions: parsed(row.mentions) }),
|
|
238
239
|
...(row.starred === 1 ? { starred: true } : {}),
|
|
240
|
+
...present({ delivery: deliveryOf(row.delivery) }),
|
|
239
241
|
};
|
|
240
242
|
};
|
|
241
243
|
/**
|
|
@@ -2596,6 +2596,86 @@ export declare const chats: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
2596
2596
|
identity: undefined;
|
|
2597
2597
|
generated: undefined;
|
|
2598
2598
|
}, {}>;
|
|
2599
|
+
muted: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2600
|
+
name: string;
|
|
2601
|
+
tableName: "chats";
|
|
2602
|
+
dataType: "number int53";
|
|
2603
|
+
data: number;
|
|
2604
|
+
driverParam: number;
|
|
2605
|
+
notNull: false;
|
|
2606
|
+
hasDefault: false;
|
|
2607
|
+
isPrimaryKey: false;
|
|
2608
|
+
isAutoincrement: false;
|
|
2609
|
+
hasRuntimeDefault: false;
|
|
2610
|
+
enumValues: undefined;
|
|
2611
|
+
baseColumn: never;
|
|
2612
|
+
identity: undefined;
|
|
2613
|
+
generated: undefined;
|
|
2614
|
+
}, {}>;
|
|
2615
|
+
mutedUntil: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2616
|
+
name: string;
|
|
2617
|
+
tableName: "chats";
|
|
2618
|
+
dataType: "number int53";
|
|
2619
|
+
data: number;
|
|
2620
|
+
driverParam: number;
|
|
2621
|
+
notNull: false;
|
|
2622
|
+
hasDefault: false;
|
|
2623
|
+
isPrimaryKey: false;
|
|
2624
|
+
isAutoincrement: false;
|
|
2625
|
+
hasRuntimeDefault: false;
|
|
2626
|
+
enumValues: undefined;
|
|
2627
|
+
baseColumn: never;
|
|
2628
|
+
identity: undefined;
|
|
2629
|
+
generated: undefined;
|
|
2630
|
+
}, {}>;
|
|
2631
|
+
archived: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2632
|
+
name: string;
|
|
2633
|
+
tableName: "chats";
|
|
2634
|
+
dataType: "number int53";
|
|
2635
|
+
data: number;
|
|
2636
|
+
driverParam: number;
|
|
2637
|
+
notNull: false;
|
|
2638
|
+
hasDefault: false;
|
|
2639
|
+
isPrimaryKey: false;
|
|
2640
|
+
isAutoincrement: false;
|
|
2641
|
+
hasRuntimeDefault: false;
|
|
2642
|
+
enumValues: undefined;
|
|
2643
|
+
baseColumn: never;
|
|
2644
|
+
identity: undefined;
|
|
2645
|
+
generated: undefined;
|
|
2646
|
+
}, {}>;
|
|
2647
|
+
markedUnread: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2648
|
+
name: string;
|
|
2649
|
+
tableName: "chats";
|
|
2650
|
+
dataType: "number int53";
|
|
2651
|
+
data: number;
|
|
2652
|
+
driverParam: number;
|
|
2653
|
+
notNull: false;
|
|
2654
|
+
hasDefault: false;
|
|
2655
|
+
isPrimaryKey: false;
|
|
2656
|
+
isAutoincrement: false;
|
|
2657
|
+
hasRuntimeDefault: false;
|
|
2658
|
+
enumValues: undefined;
|
|
2659
|
+
baseColumn: never;
|
|
2660
|
+
identity: undefined;
|
|
2661
|
+
generated: undefined;
|
|
2662
|
+
}, {}>;
|
|
2663
|
+
unreadCountedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
2664
|
+
name: string;
|
|
2665
|
+
tableName: "chats";
|
|
2666
|
+
dataType: "number int53";
|
|
2667
|
+
data: number;
|
|
2668
|
+
driverParam: number;
|
|
2669
|
+
notNull: false;
|
|
2670
|
+
hasDefault: false;
|
|
2671
|
+
isPrimaryKey: false;
|
|
2672
|
+
isAutoincrement: false;
|
|
2673
|
+
hasRuntimeDefault: false;
|
|
2674
|
+
enumValues: undefined;
|
|
2675
|
+
baseColumn: never;
|
|
2676
|
+
identity: undefined;
|
|
2677
|
+
generated: undefined;
|
|
2678
|
+
}, {}>;
|
|
2599
2679
|
};
|
|
2600
2680
|
dialect: 'sqlite';
|
|
2601
2681
|
}>;
|
|
@@ -3365,6 +3445,22 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
3365
3445
|
identity: undefined;
|
|
3366
3446
|
generated: undefined;
|
|
3367
3447
|
}, {}>;
|
|
3448
|
+
delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
3449
|
+
name: string;
|
|
3450
|
+
tableName: "messages";
|
|
3451
|
+
dataType: "number int53";
|
|
3452
|
+
data: number;
|
|
3453
|
+
driverParam: number;
|
|
3454
|
+
notNull: false;
|
|
3455
|
+
hasDefault: false;
|
|
3456
|
+
isPrimaryKey: false;
|
|
3457
|
+
isAutoincrement: false;
|
|
3458
|
+
hasRuntimeDefault: false;
|
|
3459
|
+
enumValues: undefined;
|
|
3460
|
+
baseColumn: never;
|
|
3461
|
+
identity: undefined;
|
|
3462
|
+
generated: undefined;
|
|
3463
|
+
}, {}>;
|
|
3368
3464
|
};
|
|
3369
3465
|
dialect: 'sqlite';
|
|
3370
3466
|
}>;
|
|
@@ -261,6 +261,11 @@ export const chats = sqliteTable("chats", {
|
|
|
261
261
|
createdAt: integer("created_at").notNull(),
|
|
262
262
|
parentChatId: integer("parent_chat_id").references(() => chats.id),
|
|
263
263
|
scope: text("scope"),
|
|
264
|
+
muted: integer("muted"),
|
|
265
|
+
mutedUntil: integer("muted_until"),
|
|
266
|
+
archived: integer("archived"),
|
|
267
|
+
markedUnread: integer("marked_unread"),
|
|
268
|
+
unreadCountedAt: integer("unread_counted_at"),
|
|
264
269
|
}, (table) => [
|
|
265
270
|
unique().on(table.accountId, table.externalId),
|
|
266
271
|
index("chats_by_recency").on(table.accountId, desc(table.lastMessageAt)),
|
|
@@ -348,6 +353,7 @@ export const messages = sqliteTable("messages", {
|
|
|
348
353
|
updatedAt: integer("updated_at").notNull(),
|
|
349
354
|
threadRootId: integer("thread_root_id").references(() => messages.id),
|
|
350
355
|
starred: integer("starred"),
|
|
356
|
+
delivery: integer("delivery"),
|
|
351
357
|
}, (table) => [
|
|
352
358
|
unique().on(table.chatId, table.externalId),
|
|
353
359
|
index("messages_by_time").on(table.chatId, desc(table.sentAt)),
|
|
@@ -264,6 +264,22 @@ export declare const newestHits: (context: StoreContext, where: SQL | undefined,
|
|
|
264
264
|
identity: undefined;
|
|
265
265
|
generated: undefined;
|
|
266
266
|
}, {}>;
|
|
267
|
+
delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
268
|
+
name: string;
|
|
269
|
+
tableName: "messages";
|
|
270
|
+
dataType: "number int53";
|
|
271
|
+
data: number;
|
|
272
|
+
driverParam: number;
|
|
273
|
+
notNull: false;
|
|
274
|
+
hasDefault: false;
|
|
275
|
+
isPrimaryKey: false;
|
|
276
|
+
isAutoincrement: false;
|
|
277
|
+
hasRuntimeDefault: false;
|
|
278
|
+
enumValues: undefined;
|
|
279
|
+
baseColumn: never;
|
|
280
|
+
identity: undefined;
|
|
281
|
+
generated: undefined;
|
|
282
|
+
}, {}>;
|
|
267
283
|
chatTitle: SQL.Aliased<string | null>;
|
|
268
284
|
provider: SQL.Aliased<string>;
|
|
269
285
|
accountNativeId: SQL.Aliased<string>;
|
|
@@ -292,6 +308,7 @@ export declare const newestHits: (context: StoreContext, where: SQL | undefined,
|
|
|
292
308
|
providerMetadata: string | null;
|
|
293
309
|
mentions: string | null;
|
|
294
310
|
starred: number | null;
|
|
311
|
+
delivery: number | null;
|
|
295
312
|
chatTitle: string | null;
|
|
296
313
|
provider: string;
|
|
297
314
|
accountNativeId: string;
|
|
@@ -554,6 +571,22 @@ export declare const newestHits: (context: StoreContext, where: SQL | undefined,
|
|
|
554
571
|
identity: undefined;
|
|
555
572
|
generated: undefined;
|
|
556
573
|
}, {}>;
|
|
574
|
+
delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
575
|
+
name: string;
|
|
576
|
+
tableName: "messages";
|
|
577
|
+
dataType: "number int53";
|
|
578
|
+
data: number;
|
|
579
|
+
driverParam: number;
|
|
580
|
+
notNull: false;
|
|
581
|
+
hasDefault: false;
|
|
582
|
+
isPrimaryKey: false;
|
|
583
|
+
isAutoincrement: false;
|
|
584
|
+
hasRuntimeDefault: false;
|
|
585
|
+
enumValues: undefined;
|
|
586
|
+
baseColumn: never;
|
|
587
|
+
identity: undefined;
|
|
588
|
+
generated: undefined;
|
|
589
|
+
}, {}>;
|
|
557
590
|
chatTitle: SQL.Aliased<string | null>;
|
|
558
591
|
provider: SQL.Aliased<string>;
|
|
559
592
|
accountNativeId: SQL.Aliased<string>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Delivery } from "../../domain/models.js";
|
|
1
2
|
export declare const toMs: (iso: string | null | undefined) => number | null;
|
|
2
3
|
export declare const toIso: (ms: unknown) => string | null;
|
|
3
4
|
export declare const json: (value: unknown) => string | null;
|
|
@@ -5,3 +6,6 @@ export declare const parsed: <T>(text: unknown) => T | undefined;
|
|
|
5
6
|
export declare const present: <T extends Record<string, unknown>>(entries: T) => {
|
|
6
7
|
[k: string]: unknown;
|
|
7
8
|
};
|
|
9
|
+
/** `messages.delivery` keeps a level, so a receipt can only raise it. */
|
|
10
|
+
export declare const DELIVERY: Record<Delivery, number>;
|
|
11
|
+
export declare const deliveryOf: (stored: number | null) => Delivery | undefined;
|
|
@@ -8,3 +8,6 @@ export const toIso = (ms) => (typeof ms === "number" ? new Date(ms).toISOString(
|
|
|
8
8
|
export const json = (value) => (value === undefined ? null : JSON.stringify(value));
|
|
9
9
|
export const parsed = (text) => typeof text === "string" ? JSON.parse(text) : undefined;
|
|
10
10
|
export const present = (entries) => Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== null && value !== undefined));
|
|
11
|
+
/** `messages.delivery` keeps a level, so a receipt can only raise it. */
|
|
12
|
+
export const DELIVERY = { delivered: 1, read: 2 };
|
|
13
|
+
export const deliveryOf = (stored) => stored === DELIVERY.read ? "read" : stored === DELIVERY.delivered ? "delivered" : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.383.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -182,6 +182,9 @@
|
|
|
182
182
|
"liquidjs": "10.30.0",
|
|
183
183
|
"lossless-json": "4.3.0",
|
|
184
184
|
"mammoth": "1.13.0",
|
|
185
|
+
"mdast-util-from-markdown": "2.1.0",
|
|
186
|
+
"mdast-util-gfm": "3.1.0",
|
|
187
|
+
"micromark-extension-gfm": "3.0.0",
|
|
185
188
|
"ogg-opus-decoder": "1.7.5",
|
|
186
189
|
"psl": "1.15.0",
|
|
187
190
|
"qrcode-generator": "2.0.4",
|
|
@@ -200,6 +203,7 @@
|
|
|
200
203
|
"@cspell/dict-ru_ru": "2.3.3",
|
|
201
204
|
"@modelcontextprotocol/client": "2.2.0",
|
|
202
205
|
"@napi-rs/canvas": "1.0.10",
|
|
206
|
+
"@types/mdast": "4.0.4",
|
|
203
207
|
"@types/node": "^22.20.4",
|
|
204
208
|
"@types/pngjs": "6.0.5",
|
|
205
209
|
"@vitest/coverage-v8": "^5.0.2",
|