@wirecat/cli-core 0.478.0 → 0.480.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 +26 -1
- package/dist/cli/messenger/context.d.ts +3 -1
- package/dist/cli/messenger/context.js +4 -0
- package/dist/domain/models.d.ts +4 -0
- package/dist/ports/messenger.d.ts +2 -2
- package/dist/services/person-profile.js +8 -3
- 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/parity.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,32 @@ 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
|
-
##
|
|
8
|
+
## 0.480.0 — 29.10.2026
|
|
9
|
+
|
|
10
|
+
### Changed — may break callers
|
|
11
|
+
|
|
12
|
+
- **`StoredMessages.reactors` is required again.** 0.479.0 made it optional so a messenger that builds its own
|
|
13
|
+
`StoredMessages` kept compiling; whatsapp-cli now implements it, so every messenger must give it (`async () => []` for
|
|
14
|
+
one that stores no reactions). tg-cli and max-cli do not build a `StoredMessages` and are not affected.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`reactions list` is in whatsapp-cli too** (`parity.json`: `tg` and `wa`; max-cli does not list who reacted).
|
|
19
|
+
|
|
20
|
+
## 0.479.0 — 29.10.2026
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`StoredMessages.reactors(chatId, messageId)`** gives an adapter the reactions the store holds for a message, so a
|
|
25
|
+
messenger that sends reactions one person at a time (WhatsApp) can implement `MessageReactions.reactors` from the
|
|
26
|
+
store. One `Reactor` per person (`person`, `reaction`, `reactedAt`), newest first; a person's newest reaction wins
|
|
27
|
+
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.
|
|
28
|
+
- **A person's phone is saved in the store** (store version 37, additive, minCompatible stays 34). `contacts sync`
|
|
29
|
+
saves the phone an address book entry carries (`AddressBookEntry`, a `Member` with an optional `phone`), and an
|
|
30
|
+
online `contacts show` saves the phone the profile shows, so `contacts show --offline` answers it. A read that
|
|
31
|
+
shows no phone never erases a saved one.
|
|
32
|
+
- **A shared contact card's phone is a mention** of field `contact`, beside `file_text` and `transcript`:
|
|
33
|
+
`mentions list kind:phone` and `phone:` search find it, and `mentions rebuild` reads cards stored before.
|
|
9
34
|
|
|
10
35
|
## 0.478.0 — 29.10.2026
|
|
11
36
|
|
|
@@ -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,8 @@ 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
|
+
reactors(chatId: Id, messageId: Id): Promise<Reactor[]>;
|
|
34
36
|
}
|
|
35
37
|
/** `listen` opens a connection that receives updates — only `watch` asks; the rest stay quiet. */
|
|
36
38
|
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
|
/**
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -574,6 +574,10 @@ export interface Contact {
|
|
|
574
574
|
}
|
|
575
575
|
/** Somebody in a chat, as much of them as a chat card shows. */
|
|
576
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
|
+
};
|
|
577
581
|
/** A topic of a forum group; its messages carry this id as `threadId`. */
|
|
578
582
|
export interface Topic {
|
|
579
583
|
id: Id;
|
|
@@ -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),
|
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.480.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",
|
package/parity.json
CHANGED
|
@@ -3671,16 +3671,16 @@
|
|
|
3671
3671
|
"in": "all"
|
|
3672
3672
|
},
|
|
3673
3673
|
"reactions list": {
|
|
3674
|
-
"in": ["tg"],
|
|
3675
|
-
"reason": "max-cli
|
|
3674
|
+
"in": ["tg", "wa"],
|
|
3675
|
+
"reason": "max-cli does not list who reacted to a message",
|
|
3676
3676
|
"options": {
|
|
3677
3677
|
"--cursor": {
|
|
3678
|
-
"in": ["tg"],
|
|
3679
|
-
"reason": "max-cli
|
|
3678
|
+
"in": ["tg", "wa"],
|
|
3679
|
+
"reason": "max-cli does not list who reacted to a message"
|
|
3680
3680
|
},
|
|
3681
3681
|
"--limit": {
|
|
3682
|
-
"in": ["tg"],
|
|
3683
|
-
"reason": "max-cli
|
|
3682
|
+
"in": ["tg", "wa"],
|
|
3683
|
+
"reason": "max-cli does not list who reacted to a message"
|
|
3684
3684
|
}
|
|
3685
3685
|
}
|
|
3686
3686
|
},
|