@wirecat/cli-core 0.477.0 → 0.479.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 +27 -0
- package/dist/cli/messenger/catch-up-answers.d.ts +2 -0
- package/dist/cli/messenger/catch-up-answers.js +2 -1
- package/dist/cli/messenger/context.d.ts +4 -1
- package/dist/cli/messenger/context.js +4 -0
- package/dist/cli/messenger/message-items.d.ts +1 -1
- package/dist/cli/messenger/message-items.js +2 -2
- package/dist/cli/messenger/search-answers.js +5 -1
- package/dist/domain/models.d.ts +6 -0
- package/dist/domain/models.js +2 -0
- package/dist/entities/hit.d.ts +2 -0
- package/dist/entities/hit.js +2 -0
- package/dist/entities/index.d.ts +8 -1
- package/dist/entities/index.js +2 -0
- package/dist/ports/messenger.d.ts +2 -2
- package/dist/services/person-profile.js +8 -3
- package/dist/services/pictures.d.ts +2 -1
- package/dist/services/pictures.js +2 -0
- package/dist/services/search-all.d.ts +3 -1
- package/dist/services/search-all.js +2 -0
- package/dist/store/messages.d.ts +3 -1
- package/dist/store/messages.js +4 -0
- package/dist/store/people.d.ts +2 -0
- package/dist/store/people.js +1 -0
- package/dist/store/sqlite/identities.d.ts +1 -0
- package/dist/store/sqlite/identities.js +10 -1
- package/dist/store/sqlite/lucene-fields.d.ts +7 -0
- package/dist/store/sqlite/lucene-fields.js +7 -0
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/mention-drain.js +6 -6
- package/dist/store/sqlite/mentions.d.ts +1 -1
- package/dist/store/sqlite/mentions.js +4 -1
- package/dist/store/sqlite/messages.d.ts +2 -1
- package/dist/store/sqlite/messages.js +10 -0
- package/dist/store/sqlite/migrations.generated.js +13 -0
- package/dist/store/sqlite/schema.d.ts +16 -0
- package/dist/store/sqlite/schema.js +1 -0
- package/dist/store/types.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.479.0 — 29.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`StoredMessages.reactors(chatId, messageId)`** gives an adapter the reactions the store holds for a message, so a
|
|
15
|
+
messenger that sends reactions one person at a time (WhatsApp) can implement `MessageReactions.reactors` from the
|
|
16
|
+
store. One `Reactor` per person (`person`, `reaction`, `reactedAt`), newest first; a person's newest reaction wins
|
|
17
|
+
and one they took back is not listed. It is optional on the type, so an adapter that builds its own `StoredMessages` still compiles. It covers only what the store received. No schema change, store version stays 37.
|
|
18
|
+
- **A person's phone is saved in the store** (store version 37, additive, minCompatible stays 34). `contacts sync`
|
|
19
|
+
saves the phone an address book entry carries (`AddressBookEntry`, a `Member` with an optional `phone`), and an
|
|
20
|
+
online `contacts show` saves the phone the profile shows, so `contacts show --offline` answers it. A read that
|
|
21
|
+
shows no phone never erases a saved one.
|
|
22
|
+
- **A shared contact card's phone is a mention** of field `contact`, beside `file_text` and `transcript`:
|
|
23
|
+
`mentions list kind:phone` and `phone:` search find it, and `mentions rebuild` reads cards stored before.
|
|
24
|
+
|
|
25
|
+
## 0.478.0 — 29.10.2026
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **`sender_is_me` on `inbox`, `review`, `search messages` and `search all`**, as 0.473.0 promised. The catch-up
|
|
30
|
+
answers and the search hits are built field by field, not from `messageItem`, and left it out; only `messages list`
|
|
31
|
+
and the other `messageItem` answers had it. A `search all` hit carries it for a message or a mail, and the picture
|
|
32
|
+
lane's hits of `search messages` carry it too. Every answer applies the one rule: `outgoing`, or the sender id is
|
|
33
|
+
the reading account's own.
|
|
34
|
+
|
|
8
35
|
## 0.477.0 — 29.10.2026
|
|
9
36
|
|
|
10
37
|
### Added
|
|
@@ -20,6 +20,7 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
|
|
|
20
20
|
chat_title: string | null | undefined;
|
|
21
21
|
timestamp: string;
|
|
22
22
|
sender_name: string | null;
|
|
23
|
+
sender_is_me: boolean;
|
|
23
24
|
outgoing: boolean | null;
|
|
24
25
|
text: string;
|
|
25
26
|
transcript?: string | undefined;
|
|
@@ -74,6 +75,7 @@ export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined,
|
|
|
74
75
|
chat_title: string | null | undefined;
|
|
75
76
|
timestamp: string;
|
|
76
77
|
sender_name: string | null;
|
|
78
|
+
sender_is_me: boolean;
|
|
77
79
|
outgoing: boolean | null;
|
|
78
80
|
text: string;
|
|
79
81
|
transcript?: string | undefined;
|
|
@@ -34,13 +34,14 @@ export const catchUpQuery = (words, registry, command) => {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
const summaryOf = (message, reader) => {
|
|
37
|
-
const { ref, chat_id, chat_title, timestamp, sender_name, outgoing, text, transcript, attachments } = messageItem(message, reader);
|
|
37
|
+
const { ref, chat_id, chat_title, timestamp, sender_name, sender_is_me, outgoing, text, transcript, attachments } = messageItem(message, reader);
|
|
38
38
|
return {
|
|
39
39
|
ref,
|
|
40
40
|
chat_id,
|
|
41
41
|
chat_title,
|
|
42
42
|
timestamp,
|
|
43
43
|
sender_name,
|
|
44
|
+
sender_is_me,
|
|
44
45
|
outgoing,
|
|
45
46
|
text,
|
|
46
47
|
...(transcript === undefined ? {} : { transcript }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import type { AdminRight, GroupSettingKey, Id, Message, Page, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
|
|
2
|
+
import type { AdminRight, GroupSettingKey, Id, Message, Page, PollVote, PrivacyKey, Provider, Reactor } from "../../domain/models.js";
|
|
3
3
|
import type { MessengerAdapter } from "../../ports/messenger.js";
|
|
4
4
|
import type { EventSink, Warn } from "../../runs/events.js";
|
|
5
5
|
import { type Asker, type SendGuard } from "../../sends/guard.js";
|
|
@@ -31,6 +31,9 @@ export interface StoredMessages {
|
|
|
31
31
|
wroteTo(personId: Id): Promise<boolean>;
|
|
32
32
|
/** Each voter's newest vote in the poll the message carries, newest first; a retracted vote has no answers. */
|
|
33
33
|
pollVotes(chatId: Id, messageId: Id): Promise<PollVote[]>;
|
|
34
|
+
/** Each person's newest reaction the store received for the message, newest first; a taken-back one is not listed.
|
|
35
|
+
* Optional so an adapter that builds its own `StoredMessages` still compiles. */
|
|
36
|
+
reactors?(chatId: Id, messageId: Id): Promise<Reactor[]>;
|
|
34
37
|
}
|
|
35
38
|
/** `listen` opens a connection that receives updates — only `watch` asks; the rest stay quiet. */
|
|
36
39
|
export interface ConnectOptions {
|
|
@@ -85,6 +85,10 @@ export const storedMessages = (provider, store, account) => {
|
|
|
85
85
|
const owner = key();
|
|
86
86
|
return owner ? (await store()).messages.pollVotes(owner, chatId, messageId) : [];
|
|
87
87
|
},
|
|
88
|
+
reactors: async (chatId, messageId) => {
|
|
89
|
+
const owner = key();
|
|
90
|
+
return owner ? (await store()).messages.reactors(owner, chatId, messageId) : [];
|
|
91
|
+
},
|
|
88
92
|
};
|
|
89
93
|
};
|
|
90
94
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as v from "valibot";
|
|
2
|
-
import type
|
|
2
|
+
import { type Message, type MessageHit } from "../../domain/models.js";
|
|
3
3
|
import type { message as messageSchema } from "../../entities/message.js";
|
|
4
4
|
import type { MessengerAdapter } from "../../ports/messenger.js";
|
|
5
5
|
import type { AccountKey } from "../../store/store.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { formatLocator, parseLocator } from "../../domain/locator.js";
|
|
3
|
+
import { senderIsMe, } from "../../domain/models.js";
|
|
3
4
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, one]) => one !== undefined));
|
|
4
5
|
const attachmentItem = (attachment) => defined({
|
|
5
6
|
kind: attachment.kind,
|
|
@@ -50,8 +51,7 @@ export const messageItem = (message, reader) => {
|
|
|
50
51
|
sender_name: message.senderName,
|
|
51
52
|
sender_username: message.senderUsername,
|
|
52
53
|
sender_is_chat: message.senderIsChat,
|
|
53
|
-
|
|
54
|
-
sender_is_me: message.outgoing === true || (message.senderId !== null && String(message.senderId) === account),
|
|
54
|
+
sender_is_me: senderIsMe(message, account),
|
|
55
55
|
mentions: message.mentions,
|
|
56
56
|
timestamp: message.timestamp,
|
|
57
57
|
edited_at: message.editedAt,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatLocator, parseLocator } from "../../domain/locator.js";
|
|
2
|
+
import { senderIsMe } from "../../domain/models.js";
|
|
2
3
|
import { DEFAULT_TEXT_MODEL } from "../../embeddings/models.js";
|
|
3
4
|
import { excerpt } from "../../services/excerpt.js";
|
|
4
5
|
import { MAX_SEARCH_OFFSET } from "../../services/messages-archive-search.js";
|
|
@@ -28,6 +29,7 @@ export const messageHitItem = (hit, query = "") => {
|
|
|
28
29
|
chat_title: hit.chatTitle ?? undefined,
|
|
29
30
|
timestamp: hit.timestamp,
|
|
30
31
|
sender_name: hit.senderName,
|
|
32
|
+
sender_is_me: senderIsMe(hit, at.account),
|
|
31
33
|
excerpt: hit.excerpt ?? excerpt(hit.text, query),
|
|
32
34
|
matched_in: hit.matchedIn,
|
|
33
35
|
score: hit.score ?? undefined,
|
|
@@ -165,10 +167,11 @@ const pictureItems = (found) => {
|
|
|
165
167
|
const shown = new Set(found.items.map(({ locator }) => locator));
|
|
166
168
|
return (found.pictures ?? [])
|
|
167
169
|
.filter(({ locator }) => !shown.has(locator))
|
|
168
|
-
.map(({ locator, timestamp, senderName, score }) => ({
|
|
170
|
+
.map(({ locator, timestamp, senderName, senderIsMe, score }) => ({
|
|
169
171
|
ref: locator,
|
|
170
172
|
timestamp,
|
|
171
173
|
sender_name: senderName,
|
|
174
|
+
sender_is_me: senderIsMe,
|
|
172
175
|
matched_in: "picture",
|
|
173
176
|
score,
|
|
174
177
|
untrusted: ["sender_name"],
|
|
@@ -226,6 +229,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
226
229
|
url: item.url,
|
|
227
230
|
excerpt: item.text,
|
|
228
231
|
sender_name: item.from?.name,
|
|
232
|
+
sender_is_me: item.senderIsMe,
|
|
229
233
|
sender_ref: item.from?.ref,
|
|
230
234
|
found_by: item.foundBy,
|
|
231
235
|
untrusted: ALL_HIT_UNTRUSTED[item.kind],
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -366,6 +366,8 @@ export interface Message {
|
|
|
366
366
|
*/
|
|
367
367
|
delivery?: Delivery;
|
|
368
368
|
}
|
|
369
|
+
/** MAX reports the owner's own messages with no sender id, so `outgoing` counts as well as the id. */
|
|
370
|
+
export declare const senderIsMe: (message: Pick<Message, "outgoing" | "senderId">, account: string) => boolean;
|
|
369
371
|
/** `delivered` reached the other person's phone; `read` they opened it (a voice note or view-once file played counts). */
|
|
370
372
|
export type Delivery = "delivered" | "read";
|
|
371
373
|
/**
|
|
@@ -572,6 +574,10 @@ export interface Contact {
|
|
|
572
574
|
}
|
|
573
575
|
/** Somebody in a chat, as much of them as a chat card shows. */
|
|
574
576
|
export type Member = Pick<Contact, "id" | "name" | "username">;
|
|
577
|
+
/** A contact of the messenger's address book: the phone is there when the messenger shows it to the owner. */
|
|
578
|
+
export type AddressBookEntry = Member & {
|
|
579
|
+
phone?: string | null;
|
|
580
|
+
};
|
|
575
581
|
/** A topic of a forum group; its messages carry this id as `threadId`. */
|
|
576
582
|
export interface Topic {
|
|
577
583
|
id: Id;
|
package/dist/domain/models.js
CHANGED
|
@@ -9,5 +9,7 @@ export const FOLDER_KINDS = ["contacts", "non-contacts", "groups", "channels", "
|
|
|
9
9
|
export const FOLDER_SKIPS = ["muted", "read", "archived"];
|
|
10
10
|
/** What an admin may do, in max-cli's words; a messenger maps them onto its own. */
|
|
11
11
|
export const ADMIN_RIGHTS = ["read", "members", "admins", "info", "pin", "link", "post", "edit", "delete"];
|
|
12
|
+
/** MAX reports the owner's own messages with no sender id, so `outgoing` counts as well as the id. */
|
|
13
|
+
export const senderIsMe = (message, account) => message.outgoing === true || (message.senderId !== null && String(message.senderId) === account);
|
|
12
14
|
/** Kinds of attachment `chats media` filters by. */
|
|
13
15
|
export const MEDIA_KINDS = ["photo", "video", "file", "audio", "link"];
|
package/dist/entities/hit.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export declare const hit: v.StrictObjectSchema<{
|
|
|
13
13
|
readonly timestamp: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
14
14
|
readonly excerpt: v.StringSchema<undefined>;
|
|
15
15
|
readonly sender_name: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
16
|
+
/** A message's or a mail's: whether the owner wrote it. */
|
|
17
|
+
readonly sender_is_me: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
16
18
|
/** The writer's `person:` reference, where the store has one. */
|
|
17
19
|
readonly sender_ref: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
18
20
|
readonly found_by: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<["title", "words", "meaning"], undefined>, undefined>, undefined>;
|
package/dist/entities/hit.js
CHANGED
|
@@ -14,6 +14,8 @@ export const hit = v.strictObject({
|
|
|
14
14
|
timestamp: v.nullable(time),
|
|
15
15
|
excerpt: text,
|
|
16
16
|
sender_name: v.optional(v.nullable(text)),
|
|
17
|
+
/** A message's or a mail's: whether the owner wrote it. */
|
|
18
|
+
sender_is_me: v.optional(v.boolean()),
|
|
17
19
|
/** The writer's `person:` reference, where the store has one. */
|
|
18
20
|
sender_ref: v.optional(v.nullable(reference)),
|
|
19
21
|
found_by: v.optional(v.array(v.picklist(["title", "words", "meaning"]))),
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -1141,6 +1141,7 @@ export declare const ENTITIES: {
|
|
|
1141
1141
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
1142
1142
|
readonly chat_title: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
1143
1143
|
readonly sender_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
1144
|
+
readonly sender_is_me: v.BooleanSchema<undefined>;
|
|
1144
1145
|
readonly timestamp: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
1145
1146
|
readonly excerpt: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
1146
1147
|
readonly matched_in: v.OptionalSchema<v.PicklistSchema<["text", "transcript", "filename", "file", "context", "picture"], undefined>, undefined>;
|
|
@@ -1623,12 +1624,13 @@ export declare const ENTITIES: {
|
|
|
1623
1624
|
}, undefined>, undefined>;
|
|
1624
1625
|
}, undefined>, undefined>;
|
|
1625
1626
|
readonly provider_metadata: v.OptionalSchema<v.RecordSchema<v.StringSchema<undefined>, v.UnknownSchema, undefined>, undefined>;
|
|
1626
|
-
}, "attachments" | "chat_id" | "chat_title" | "outgoing" | "ref" | "sender_name" | "text" | "timestamp" | "transcript">;
|
|
1627
|
+
}, "attachments" | "chat_id" | "chat_title" | "outgoing" | "ref" | "sender_is_me" | "sender_name" | "text" | "timestamp" | "transcript">;
|
|
1627
1628
|
readonly "~standard": v.StandardProps<{
|
|
1628
1629
|
ref: string;
|
|
1629
1630
|
chat_id: string;
|
|
1630
1631
|
chat_title?: string | null | undefined;
|
|
1631
1632
|
sender_name: string | null;
|
|
1633
|
+
sender_is_me: boolean;
|
|
1632
1634
|
timestamp: string;
|
|
1633
1635
|
text: string;
|
|
1634
1636
|
transcript?: string | undefined;
|
|
@@ -1671,6 +1673,7 @@ export declare const ENTITIES: {
|
|
|
1671
1673
|
chat_id: string;
|
|
1672
1674
|
chat_title?: string | null | undefined;
|
|
1673
1675
|
sender_name: string | null;
|
|
1676
|
+
sender_is_me: boolean;
|
|
1674
1677
|
timestamp: string;
|
|
1675
1678
|
text: string;
|
|
1676
1679
|
transcript?: string | undefined;
|
|
@@ -1714,6 +1717,7 @@ export declare const ENTITIES: {
|
|
|
1714
1717
|
chat_id: string;
|
|
1715
1718
|
chat_title?: string | null | undefined;
|
|
1716
1719
|
sender_name: string | null;
|
|
1720
|
+
sender_is_me: boolean;
|
|
1717
1721
|
timestamp: string;
|
|
1718
1722
|
text: string;
|
|
1719
1723
|
transcript?: string | undefined;
|
|
@@ -1758,6 +1762,7 @@ export declare const ENTITIES: {
|
|
|
1758
1762
|
chat_id: string;
|
|
1759
1763
|
chat_title?: string | null | undefined;
|
|
1760
1764
|
sender_name: string | null;
|
|
1765
|
+
sender_is_me: boolean;
|
|
1761
1766
|
timestamp: string;
|
|
1762
1767
|
text: string;
|
|
1763
1768
|
transcript?: string | undefined;
|
|
@@ -1801,6 +1806,7 @@ export declare const ENTITIES: {
|
|
|
1801
1806
|
chat_id: string;
|
|
1802
1807
|
chat_title?: string | null | undefined;
|
|
1803
1808
|
sender_name: string | null;
|
|
1809
|
+
sender_is_me: boolean;
|
|
1804
1810
|
timestamp: string;
|
|
1805
1811
|
text: string;
|
|
1806
1812
|
transcript?: string | undefined;
|
|
@@ -4469,6 +4475,7 @@ export declare const ENTITIES: {
|
|
|
4469
4475
|
readonly timestamp: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
4470
4476
|
readonly excerpt: v.StringSchema<undefined>;
|
|
4471
4477
|
readonly sender_name: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4478
|
+
readonly sender_is_me: v.OptionalSchema<v.BooleanSchema<undefined>, undefined>;
|
|
4472
4479
|
readonly sender_ref: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4473
4480
|
readonly found_by: v.OptionalSchema<v.ArraySchema<v.PicklistSchema<["title", "words", "meaning"], undefined>, undefined>, undefined>;
|
|
4474
4481
|
readonly offset_ms: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>, undefined>;
|
package/dist/entities/index.js
CHANGED
|
@@ -108,6 +108,7 @@ export const ENTITIES = {
|
|
|
108
108
|
"chat_title",
|
|
109
109
|
"timestamp",
|
|
110
110
|
"sender_name",
|
|
111
|
+
"sender_is_me",
|
|
111
112
|
"excerpt",
|
|
112
113
|
"matched_in",
|
|
113
114
|
"score",
|
|
@@ -126,6 +127,7 @@ export const ENTITIES = {
|
|
|
126
127
|
"chat_title",
|
|
127
128
|
"timestamp",
|
|
128
129
|
"sender_name",
|
|
130
|
+
"sender_is_me",
|
|
129
131
|
"outgoing",
|
|
130
132
|
"text",
|
|
131
133
|
"transcript",
|
|
@@ -3,7 +3,7 @@ import type { CounterField, CounterObservations } from "../domain/counters.js";
|
|
|
3
3
|
import type { HtmlFormatting, MarkdownFormatting, TextSpan } from "../domain/formatting.js";
|
|
4
4
|
import type { Markup } from "../domain/markdown.js";
|
|
5
5
|
import type { MessagePermalink } from "../domain/message-link.js";
|
|
6
|
-
import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PersonReaction, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, Reactor, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
|
|
6
|
+
import type { Account, AccountSession, AddressBookEntry, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PersonReaction, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, Reactor, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
|
|
7
7
|
/** One attachment's bytes, fetched over the adapter's connection when `bytes` is read — so read it before the command closes. */
|
|
8
8
|
export interface RemoteFile {
|
|
9
9
|
kind: string;
|
|
@@ -502,7 +502,7 @@ export interface AccountTools {
|
|
|
502
502
|
/** The person with this phone number, where their privacy lets the owner find them; `not_found` otherwise. */
|
|
503
503
|
lookup(phone: string): Promise<Member>;
|
|
504
504
|
/** The owner's contact list as the messenger keeps it — the address book, not the chats. */
|
|
505
|
-
addressBook(): Promise<
|
|
505
|
+
addressBook(): Promise<AddressBookEntry[]>;
|
|
506
506
|
}
|
|
507
507
|
/** Everything the messenger says about one person — what `contacts show` shows. */
|
|
508
508
|
export interface PersonProfiles {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pickPerson } from "../resolve.js";
|
|
2
|
-
import { storeIfOpen } from "./deps.js";
|
|
2
|
+
import { storeIfOpen, unlessBusy } from "./deps.js";
|
|
3
3
|
/**
|
|
4
4
|
* The person as the messenger describes them, and their stored activity in every chat shared with
|
|
5
5
|
* them. Online, one profile read; `--offline`, or a messenger that cannot describe a person, what
|
|
@@ -10,13 +10,18 @@ import { storeIfOpen } from "./deps.js";
|
|
|
10
10
|
export const personProfile = async (deps, person, known) => {
|
|
11
11
|
const online = deps.offline ? undefined : await deps.connection();
|
|
12
12
|
const held = await storeIfOpen(deps);
|
|
13
|
-
const
|
|
13
|
+
const read = online?.profile
|
|
14
14
|
? await online.profile(person)
|
|
15
15
|
: known
|
|
16
16
|
? factsOfCard(known)
|
|
17
17
|
: await storedFacts(held ?? { store: await deps.store(), account: await deps.account() }, person);
|
|
18
18
|
if (!held)
|
|
19
|
-
return { ...
|
|
19
|
+
return { ...read, chats: quiet(read), aliases: [] };
|
|
20
|
+
if (online?.profile && read.phone) {
|
|
21
|
+
await unlessBusy(() => held.store.people.savePeople(held.account, [{ id: read.id, name: read.name, phone: read.phone }]));
|
|
22
|
+
}
|
|
23
|
+
const phone = read.phone ?? (await held.store.people.phoneOf(held.account, read.id));
|
|
24
|
+
const facts = phone ? { ...read, phone } : read;
|
|
20
25
|
return {
|
|
21
26
|
...facts,
|
|
22
27
|
chats: await activityIn(held.store, held.account, facts),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Id } from "../domain/models.js";
|
|
2
2
|
import type { ServiceDeps } from "./deps.js";
|
|
3
3
|
import type { MessagesService } from "./messages.js";
|
|
4
4
|
/** A bigger file is no photo a chat sends: skipping it costs a vector, reading it costs memory. */
|
|
@@ -20,6 +20,7 @@ export interface PictureHit {
|
|
|
20
20
|
messageId: Id;
|
|
21
21
|
timestamp: string;
|
|
22
22
|
senderName: string | null;
|
|
23
|
+
senderIsMe: boolean;
|
|
23
24
|
score: number;
|
|
24
25
|
}
|
|
25
26
|
export interface PictureLane {
|
|
@@ -3,6 +3,7 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
4
|
import { CliError } from "@wirecat/cli-base";
|
|
5
5
|
import { formatLocator } from "../domain/locator.js";
|
|
6
|
+
import { senderIsMe } from "../domain/models.js";
|
|
6
7
|
import { DEFAULT_IMAGE_MODEL, imageModel, imageModelsDirectory, isImageModelInstalled, } from "../images/picture/models.js";
|
|
7
8
|
import { capability } from "../ports/messenger.js";
|
|
8
9
|
import { refreshedFiles } from "./file-download.js";
|
|
@@ -218,6 +219,7 @@ export const picturesService = (deps) => {
|
|
|
218
219
|
messageId,
|
|
219
220
|
timestamp: message.timestamp,
|
|
220
221
|
senderName: message.senderName,
|
|
222
|
+
senderIsMe: senderIsMe(message, account.account),
|
|
221
223
|
score: Number(score.toFixed(4)),
|
|
222
224
|
},
|
|
223
225
|
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ErrorCode } from "@wirecat/cli-base";
|
|
2
|
-
import type
|
|
2
|
+
import { type MailFields } from "../domain/models.js";
|
|
3
3
|
import type { MessengerTraits } from "../ports/traits.js";
|
|
4
4
|
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
5
5
|
import type { SearchFound, SearchQuery } from "./messages.js";
|
|
@@ -25,6 +25,8 @@ export interface SearchAllItem {
|
|
|
25
25
|
name: string | null;
|
|
26
26
|
ref: string | null;
|
|
27
27
|
};
|
|
28
|
+
/** A message's or a mail's: whether the owner wrote it. */
|
|
29
|
+
senderIsMe?: boolean;
|
|
28
30
|
/** A mail's people, labels and thread, as `search mail` gives them. */
|
|
29
31
|
email?: MailFields;
|
|
30
32
|
/** Which search found it: words, meaning or both. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { parseLucene } from "@wirecat/cli-base/query";
|
|
3
3
|
import { parseLocator } from "../domain/locator.js";
|
|
4
|
+
import { senderIsMe } from "../domain/models.js";
|
|
4
5
|
import { INTERNAL_NOTES, kindIn } from "../domain/providers.js";
|
|
5
6
|
import { runEmbedders } from "../embeddings/embed.js";
|
|
6
7
|
import { DEFAULT_TEXT_MODEL } from "../embeddings/models.js";
|
|
@@ -96,6 +97,7 @@ export const searchAll = async (store, account, request, messenger = {}, searchM
|
|
|
96
97
|
from: hit.email?.from
|
|
97
98
|
? { name: hit.email.from.name ?? hit.email.from.address, ref: hit.email.from.ref }
|
|
98
99
|
: { name: hit.senderName, ref: null },
|
|
100
|
+
senderIsMe: senderIsMe(hit, at.account),
|
|
99
101
|
...(hit.email ? { email: hit.email } : {}),
|
|
100
102
|
foundBy,
|
|
101
103
|
};
|
package/dist/store/messages.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CounterObservations } from "../domain/counters.js";
|
|
2
|
-
import type { Delivery, Id, Message, Page, PersonReaction, PollVote, Reactions, WindowedMessage } from "../domain/models.js";
|
|
2
|
+
import type { Delivery, Id, Message, Page, PersonReaction, PollVote, Reactions, Reactor, WindowedMessage } from "../domain/models.js";
|
|
3
3
|
import { type DocumentLinkMessage } from "./sqlite/document-links.js";
|
|
4
4
|
import type { StoreContext } from "./sqlite/open.js";
|
|
5
5
|
import type { AccountKey, DeletionScope, IngestedVia, MessageFilter, SenderChatStats, StoredHit } from "./types.js";
|
|
@@ -118,6 +118,8 @@ export interface AccountMessages {
|
|
|
118
118
|
saveVote(key: AccountKey, chatId: Id, messageId: Id, vote: PollVote): Promise<void>;
|
|
119
119
|
/** Each voter's newest vote in the poll a message carries, newest first. */
|
|
120
120
|
pollVotes(key: AccountKey, chatId: Id, messageId: Id): Promise<PollVote[]>;
|
|
121
|
+
/** Each person's newest reaction to a message, newest first; a person who took theirs back is not listed. */
|
|
122
|
+
reactors(key: AccountKey, chatId: Id, messageId: Id): Promise<Reactor[]>;
|
|
121
123
|
/** One person's stored messages per chat of this account, newest first. */
|
|
122
124
|
senderStats(key: AccountKey, senderId: Id): Promise<SenderChatStats[]>;
|
|
123
125
|
/** What a voice message said, when it was heard before, and by what. */
|
package/dist/store/messages.js
CHANGED
|
@@ -110,6 +110,10 @@ export const accountMessagesOver = (context) => {
|
|
|
110
110
|
const chatKey = chatKeyOf(key, chatId);
|
|
111
111
|
return chatKey === undefined ? [] : messageWrites.pollVotes(context, chatKey, messageId);
|
|
112
112
|
},
|
|
113
|
+
reactors: async (key, chatId, messageId) => {
|
|
114
|
+
const chatKey = chatKeyOf(key, chatId);
|
|
115
|
+
return chatKey === undefined ? [] : messageWrites.reactors(context, chatKey, messageId);
|
|
116
|
+
},
|
|
113
117
|
senderStats: async (key, senderId) => {
|
|
114
118
|
const accountKey = findAccountPk(key);
|
|
115
119
|
return accountKey === undefined ? [] : reads.senderStats(context, accountKey, key.provider, senderId);
|
package/dist/store/people.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface AccountPeople {
|
|
|
21
21
|
refreshRecency(key: AccountKey): Promise<void>;
|
|
22
22
|
/** Every name and username this account's store recorded for one person, the current ones included. */
|
|
23
23
|
personNames(key: AccountKey, personId: Id): Promise<PersonAlias[]>;
|
|
24
|
+
/** The phone saved for them, `null` when none was ever shown to the owner. */
|
|
25
|
+
phoneOf(key: AccountKey, personId: Id): Promise<string | null>;
|
|
24
26
|
privateContact(key: AccountKey, personId: Id): Promise<PrivateContact>;
|
|
25
27
|
setContactAlias(key: AccountKey, personId: Id, alias: string | null): Promise<{
|
|
26
28
|
personId: Id;
|
package/dist/store/people.js
CHANGED
|
@@ -39,6 +39,7 @@ export const accountPeopleOver = (context) => {
|
|
|
39
39
|
const accountKey = findAccountPk(key);
|
|
40
40
|
return accountKey === undefined ? [] : identities.namesOf(context, accountKey, key.provider, personId);
|
|
41
41
|
},
|
|
42
|
+
phoneOf: async (key, personId) => identities.phoneOf(context, key.provider, personId),
|
|
42
43
|
privateContact: async (key, person) => privatePeople.privateContact(context, key, person),
|
|
43
44
|
setContactAlias: async (key, person, alias) => {
|
|
44
45
|
return write(database, () => privatePeople.setAlias(context, key, person, alias));
|
|
@@ -45,4 +45,5 @@ export declare const refreshRecency: ({ orm }: StoreContext, accountKey: number)
|
|
|
45
45
|
export declare const namesOf: ({ orm }: StoreContext, accountKey: number, provider: Provider, nativeId: Id) => PersonAlias[];
|
|
46
46
|
/** Stable provider identity only; participant labels never resolve or merge other people. */
|
|
47
47
|
export declare const meetingIdentityPk: (context: StoreContext, accountId: number, identity: IdentityInput) => number;
|
|
48
|
+
export declare const phoneOf: ({ orm }: StoreContext, provider: Provider, nativeId: Id) => string | null;
|
|
48
49
|
export {};
|
|
@@ -13,6 +13,7 @@ const prepare = (orm) => ({
|
|
|
13
13
|
username: identities.username,
|
|
14
14
|
isBot: identities.bot,
|
|
15
15
|
description: identities.description,
|
|
16
|
+
phone: identities.phone,
|
|
16
17
|
updatedAt: identities.updatedAt,
|
|
17
18
|
personId: identityLinks.personId,
|
|
18
19
|
})
|
|
@@ -118,12 +119,14 @@ const saveIdentity = ({ orm, now, database }, provider, nativeId, name, facts, r
|
|
|
118
119
|
username: facts.username ?? found.username,
|
|
119
120
|
bot: flag(facts.isBot) ?? found.isBot,
|
|
120
121
|
description: facts.description ?? found.description,
|
|
122
|
+
phone: facts.phone ?? found.phone,
|
|
121
123
|
};
|
|
122
124
|
// Only on a real change: the search trigger rewrites the index row on every update of `name`.
|
|
123
125
|
if (changed.name !== found.name ||
|
|
124
126
|
changed.username !== found.username ||
|
|
125
127
|
changed.bot !== found.isBot ||
|
|
126
|
-
changed.description !== found.description
|
|
128
|
+
changed.description !== found.description ||
|
|
129
|
+
changed.phone !== found.phone) {
|
|
127
130
|
orm
|
|
128
131
|
.update(identities)
|
|
129
132
|
.set({ ...changed, updatedAt: now() })
|
|
@@ -150,6 +153,7 @@ const saveIdentity = ({ orm, now, database }, provider, nativeId, name, facts, r
|
|
|
150
153
|
username: facts.username ?? null,
|
|
151
154
|
bot: flag(facts.isBot),
|
|
152
155
|
description: facts.description ?? null,
|
|
156
|
+
phone: facts.phone ?? null,
|
|
153
157
|
createdAt: at,
|
|
154
158
|
updatedAt: at,
|
|
155
159
|
})
|
|
@@ -325,3 +329,8 @@ export const meetingIdentityPk = (context, accountId, identity) => {
|
|
|
325
329
|
statementsOf(context.orm).seen.run({ accountId, identityId: saved.pk, createdAt: context.now() });
|
|
326
330
|
return saved.pk;
|
|
327
331
|
};
|
|
332
|
+
export const phoneOf = ({ orm }, provider, nativeId) => orm
|
|
333
|
+
.select({ phone: identities.phone })
|
|
334
|
+
.from(identities)
|
|
335
|
+
.where(and(eq(identities.provider, provider), eq(identities.externalId, nativeId)))
|
|
336
|
+
.get()?.phone ?? null;
|
|
@@ -37,6 +37,13 @@ export interface Corpus {
|
|
|
37
37
|
*/
|
|
38
38
|
export declare const speech: (alias: string) => string;
|
|
39
39
|
export declare const SPEECH: string;
|
|
40
|
+
/**
|
|
41
|
+
* What mentions are read from in an attachment: its text, or for a shared contact card its phone behind a phone
|
|
42
|
+
* word, which makes the number count as a phone whatever its country code. One expression for finding and reading
|
|
43
|
+
* back, so the offsets agree.
|
|
44
|
+
*/
|
|
45
|
+
export declare const mentionText: (alias: string) => string;
|
|
46
|
+
export declare const mentionField: (alias: string) => string;
|
|
40
47
|
export declare const quoted: (value: string) => string;
|
|
41
48
|
/** A leaf's words as an FTS5 match: a phrase, or with `~N` its words at most N apart, in any order. */
|
|
42
49
|
export declare const ftsMatch: (node: ResolvedPredicate) => string;
|
|
@@ -20,6 +20,13 @@ import { canonicalTagNamed } from "./tags.js";
|
|
|
20
20
|
*/
|
|
21
21
|
export const speech = (alias) => `(coalesce(${alias}.extractor,'') LIKE 'speech:%' OR ${alias}.kind IN ('voice','video_note') OR coalesce(${alias}.mime,'')='audio/ogg')`;
|
|
22
22
|
export const SPEECH = speech("att");
|
|
23
|
+
/**
|
|
24
|
+
* What mentions are read from in an attachment: its text, or for a shared contact card its phone behind a phone
|
|
25
|
+
* word, which makes the number count as a phone whatever its country code. One expression for finding and reading
|
|
26
|
+
* back, so the offsets agree.
|
|
27
|
+
*/
|
|
28
|
+
export const mentionText = (alias) => `CASE WHEN coalesce(json_extract(${alias}.contact, '$.phone'), '') <> '' THEN 'phone: ' || json_extract(${alias}.contact, '$.phone') ELSE ${alias}.text END`;
|
|
29
|
+
export const mentionField = (alias) => `CASE WHEN coalesce(json_extract(${alias}.contact, '$.phone'), '') <> '' THEN 'contact' WHEN ${speech(alias)} THEN 'transcript' ELSE 'file_text' END`;
|
|
23
30
|
export const quoted = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
24
31
|
/** A leaf's words as an FTS5 match: a phrase, or with `~N` its words at most N apart, in any order. */
|
|
25
32
|
export const ftsMatch = (node) => {
|
|
@@ -48,6 +48,8 @@ export const MANIFEST = [
|
|
|
48
48
|
{ name: "20261027102259_version-35-calendar-neutral", version: 35, minCompatible: 34 },
|
|
49
49
|
{ name: "20261027102302_version-35-calendar-neutral-data", version: 35, minCompatible: 34 },
|
|
50
50
|
{ name: "20261029162435_version-36-attachment-contact", version: 36, minCompatible: 34 },
|
|
51
|
+
{ name: "20261029180959_version-37-identity-phone", version: 37, minCompatible: 34 },
|
|
52
|
+
{ name: "20261029181000_version-37-contact-mention-triggers", version: 37, minCompatible: 34 },
|
|
51
53
|
];
|
|
52
54
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
53
55
|
const migrations = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { findMentions, MENTION_KINDS, MENTION_VERSIONS, oneTrackedLinkPerSite, telegramTarget, } from "../../domain/mentions.js";
|
|
2
2
|
import { findNames, findUnknownNames, nameIndex } from "../../domain/person-names.js";
|
|
3
3
|
import { createStemmer } from "../../search/stem.js";
|
|
4
|
-
import {
|
|
4
|
+
import { mentionField, mentionText } from "./lucene-fields.js";
|
|
5
5
|
import { storePhoneCountries } from "./phone-countries.js";
|
|
6
6
|
import { placeOf } from "./place-key.js";
|
|
7
7
|
/** Mentions a store write finds before it commits: an older binary may have queued thousands. */
|
|
@@ -98,13 +98,13 @@ export const SOURCES = {
|
|
|
98
98
|
},
|
|
99
99
|
attachment: {
|
|
100
100
|
count: "SELECT count(*) AS n FROM attachments a JOIN messages m ON m.id = a.attachable_id " +
|
|
101
|
-
|
|
102
|
-
page: `SELECT a.id, a
|
|
101
|
+
`WHERE a.attachable_type = 'message' AND coalesce(${mentionText("a")}, '') <> '' AND m.deleted_at IS NULL`,
|
|
102
|
+
page: `SELECT a.id, ${mentionText("a")} AS text, ${mentionField("a")} AS field, ` +
|
|
103
103
|
"m.chat_id FROM attachments a JOIN messages m ON m.id = a.attachable_id WHERE a.id > ? AND a.attachable_type = 'message' " +
|
|
104
|
-
|
|
105
|
-
one: `SELECT a
|
|
104
|
+
`AND coalesce(${mentionText("a")}, '') <> '' AND m.deleted_at IS NULL ORDER BY a.id LIMIT ?`,
|
|
105
|
+
one: `SELECT ${mentionText("a")} AS text, ${mentionField("a")} AS field, ` +
|
|
106
106
|
"m.chat_id FROM attachments a JOIN messages m ON m.id = a.attachable_id WHERE a.id = ? AND a.attachable_type = 'message' " +
|
|
107
|
-
|
|
107
|
+
`AND ${mentionText("a")} IS NOT NULL AND m.deleted_at IS NULL`,
|
|
108
108
|
},
|
|
109
109
|
// Texts outside a chat: no one wrote them in a chat, so a name in them is never linked by rule.
|
|
110
110
|
document: {
|
|
@@ -11,7 +11,7 @@ export interface StoredMention {
|
|
|
11
11
|
domain: string | null;
|
|
12
12
|
confidence: number;
|
|
13
13
|
status: MentionStatus;
|
|
14
|
-
/** Which text of the message: `text`, a file's `file_text`,
|
|
14
|
+
/** Which text of the message: `text`, a file's `file_text`, a voice note's `transcript`, or a shared card's `contact`. */
|
|
15
15
|
field: string;
|
|
16
16
|
/** Who linked or dismissed it: `owner`, an agent by name, `rules` for a value that already had an owner. */
|
|
17
17
|
author: string | null;
|
|
@@ -3,6 +3,7 @@ import { defineQuery } from "@wirecat/cli-base/query";
|
|
|
3
3
|
import { MENTION_KINDS, siteOf } from "../../domain/mentions.js";
|
|
4
4
|
import { findNames, nameIndex } from "../../domain/person-names.js";
|
|
5
5
|
import { createStemmer } from "../../search/stem.js";
|
|
6
|
+
import { mentionText } from "./lucene-fields.js";
|
|
6
7
|
import { knownPeople, participants, SOURCE_TYPES, SOURCES, sourceOf, telegramLink, writer, } from "./mention-drain.js";
|
|
7
8
|
import { linkIdentityToPerson, personByUid } from "./person-links.js";
|
|
8
9
|
import { valueMatch } from "./phone-countries.js";
|
|
@@ -94,7 +95,9 @@ export const mentionsState = (database) => {
|
|
|
94
95
|
// A mention in a file or a voice note belongs to that attachment's message, and is listed with it.
|
|
95
96
|
const SELECT = "SELECT mn.id, mn.kind, mn.value, mn.domain, mn.confidence, mn.status, mn.author, mn.source_field, " +
|
|
96
97
|
"mn.target_type, mn.target_id, mn.created_at, " +
|
|
97
|
-
"mn.start_offset, mn.end_offset, CASE WHEN mn.source_type = 'attachment' THEN
|
|
98
|
+
"mn.start_offset, mn.end_offset, CASE WHEN mn.source_type = 'attachment' THEN " +
|
|
99
|
+
mentionText("att") +
|
|
100
|
+
" ELSE m.text END AS text, " +
|
|
98
101
|
"m.external_id AS message_id, m.sent_at, m.chat_id AS chat_pk, c.external_id AS chat_id FROM mentions mn " +
|
|
99
102
|
"LEFT JOIN attachments att ON mn.source_type = 'attachment' AND att.id = mn.source_id AND att.attachable_type = 'message' " +
|
|
100
103
|
"JOIN messages m ON mn.source_type IN ('message', 'attachment') " +
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Delivery, Id, Message, PersonReaction, PollVote, Provider, Reactions } from "../../domain/models.js";
|
|
1
|
+
import type { Delivery, Id, Message, PersonReaction, PollVote, Provider, Reactions, Reactor } from "../../domain/models.js";
|
|
2
2
|
import type { AccountKey, DeletionScope } from "../types.js";
|
|
3
3
|
import type { StoreContext } from "./open.js";
|
|
4
4
|
export declare const upsertMessage: (context: StoreContext, key: AccountKey, accountKey: number, chatKey: number, message: Message, via: string, seenAt?: number) => boolean;
|
|
@@ -27,6 +27,7 @@ export declare const pollVotes: ({ database }: StoreContext, chatKey: number, me
|
|
|
27
27
|
* messenger's own totals stand. A taken-back reaction still counts as held, so all of them taken back is zero.
|
|
28
28
|
*/
|
|
29
29
|
export declare const derivedReactions: ({ database }: StoreContext, chatKey: number, messageId: Id) => Reactions | undefined;
|
|
30
|
+
export declare const reactors: ({ database }: StoreContext, chatKey: number, messageId: Id) => Reactor[];
|
|
30
31
|
/** A reaction older than the one held for that person is dropped, and so is any reaction to a deleted message. */
|
|
31
32
|
export declare const saveReaction: (context: StoreContext, accountKey: number, provider: Provider, chatKey: number, messageId: Id, { person, own, reaction, reactedAt }: PersonReaction) => void;
|
|
32
33
|
export declare const writeDerivedReactions: (context: StoreContext, chatKey: number, messageId: Id) => void;
|
|
@@ -412,6 +412,16 @@ export const derivedReactions = ({ database }, chatKey, messageId) => {
|
|
|
412
412
|
total: sorted.reduce((sum, [, { count }]) => sum + count, 0),
|
|
413
413
|
};
|
|
414
414
|
};
|
|
415
|
+
export const reactors = ({ database }, chatKey, messageId) => database
|
|
416
|
+
.prepare(`SELECT i.external_id AS id, i.name, i.username, r.reaction, r.reacted_at AS reactedAt
|
|
417
|
+
FROM message_reactions r JOIN identities i ON i.id = r.identity_id
|
|
418
|
+
WHERE r.chat_id = ? AND r.message_external_id = ? AND r.reaction IS NOT NULL
|
|
419
|
+
ORDER BY r.reacted_at DESC, r.id DESC`)
|
|
420
|
+
.all(chatKey, messageId).map(({ id, name, username, reaction, reactedAt }) => ({
|
|
421
|
+
person: { id, name, username },
|
|
422
|
+
reaction,
|
|
423
|
+
reactedAt: new Date(reactedAt).toISOString(),
|
|
424
|
+
}));
|
|
415
425
|
/** A reaction older than the one held for that person is dropped, and so is any reaction to a deleted message. */
|
|
416
426
|
export const saveReaction = (context, accountKey, provider, chatKey, messageId, { person, own, reaction, reactedAt }) => {
|
|
417
427
|
const { database, now } = context;
|
|
@@ -784,5 +784,18 @@ export const GENERATED = [
|
|
|
784
784
|
"statements": [
|
|
785
785
|
"ALTER TABLE `attachments` ADD `contact` text;"
|
|
786
786
|
]
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
"name": "20261029180959_version-37-identity-phone",
|
|
790
|
+
"statements": [
|
|
791
|
+
"ALTER TABLE `identities` ADD `phone` text;"
|
|
792
|
+
]
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
"name": "20261029181000_version-37-contact-mention-triggers",
|
|
796
|
+
"statements": [
|
|
797
|
+
"-- A shared contact card has no text, only a phone: queue it for mentions like an attachment's text.\n-- Cards stored before version 37 wait for `mentions rebuild`.\nCREATE TRIGGER mention_pending_attachment_contact_ai AFTER INSERT ON attachments\n WHEN new.attachable_type = 'message' AND new.contact IS NOT NULL BEGIN\n INSERT OR IGNORE INTO mention_pending (source_type, source_id) VALUES ('attachment', new.id);\nEND;",
|
|
798
|
+
"CREATE TRIGGER mention_pending_attachment_contact_au AFTER UPDATE OF contact ON attachments\n WHEN new.attachable_type = 'message' AND old.contact IS NOT new.contact BEGIN\n INSERT OR IGNORE INTO mention_pending (source_type, source_id) VALUES ('attachment', new.id);\nEND;"
|
|
799
|
+
]
|
|
787
800
|
}
|
|
788
801
|
];
|
|
@@ -1042,6 +1042,22 @@ export declare const identities: import("drizzle-orm/sqlite-core").SQLiteTableWi
|
|
|
1042
1042
|
identity: undefined;
|
|
1043
1043
|
generated: undefined;
|
|
1044
1044
|
}, {}>;
|
|
1045
|
+
phone: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
1046
|
+
name: string;
|
|
1047
|
+
tableName: "identities";
|
|
1048
|
+
dataType: "string";
|
|
1049
|
+
data: string;
|
|
1050
|
+
driverParam: string;
|
|
1051
|
+
notNull: false;
|
|
1052
|
+
hasDefault: false;
|
|
1053
|
+
isPrimaryKey: false;
|
|
1054
|
+
isAutoincrement: false;
|
|
1055
|
+
hasRuntimeDefault: false;
|
|
1056
|
+
enumValues: [string, ...string[]];
|
|
1057
|
+
baseColumn: never;
|
|
1058
|
+
identity: undefined;
|
|
1059
|
+
generated: undefined;
|
|
1060
|
+
}, {}>;
|
|
1045
1061
|
};
|
|
1046
1062
|
dialect: 'sqlite';
|
|
1047
1063
|
isAlias: false;
|
|
@@ -100,6 +100,7 @@ export const identities = sqliteTable("identities", {
|
|
|
100
100
|
updatedAt: integer("updated_at").notNull(),
|
|
101
101
|
description: text("description"),
|
|
102
102
|
email: text("email"),
|
|
103
|
+
phone: text("phone"),
|
|
103
104
|
}, (table) => [
|
|
104
105
|
unique().on(table.provider, table.externalId),
|
|
105
106
|
index("identities_by_email").on(table.email).where(sql `email IS NOT NULL`),
|
package/dist/store/types.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface PersonFacts {
|
|
|
34
34
|
description?: string | null;
|
|
35
35
|
/** An address the provider shows for them: they become one person with the mail contact of that exact address. */
|
|
36
36
|
email?: string | null;
|
|
37
|
+
/** As the provider showed it, whole; only where it shows it to the owner. */
|
|
38
|
+
phone?: string | null;
|
|
37
39
|
}
|
|
38
40
|
/** One identity in one messenger, by the messenger's own id. `provider` is any string: `email` is one. */
|
|
39
41
|
export interface IdentityRef {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.479.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",
|