@gakr-gakr/whatsapp 0.1.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.
Files changed (159) hide show
  1. package/action-runtime-api.ts +1 -0
  2. package/action-runtime.runtime.ts +1 -0
  3. package/api.ts +67 -0
  4. package/auth-presence.ts +80 -0
  5. package/autobot.plugin.json +23 -0
  6. package/channel-config-api.ts +1 -0
  7. package/channel-plugin-api.ts +3 -0
  8. package/config-api.ts +4 -0
  9. package/constants.ts +1 -0
  10. package/contract-api.ts +29 -0
  11. package/directory-contract-api.ts +4 -0
  12. package/doctor-contract-api.ts +8 -0
  13. package/index.ts +16 -0
  14. package/legacy-session-surface-api.ts +6 -0
  15. package/legacy-state-migrations-api.ts +1 -0
  16. package/light-runtime-api.ts +12 -0
  17. package/login-qr-api.ts +1 -0
  18. package/login-qr-runtime.ts +23 -0
  19. package/outbound-payload-test-api.ts +1 -0
  20. package/package.json +76 -0
  21. package/runtime-api.ts +84 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-contract-api.ts +4 -0
  24. package/setup-entry.ts +21 -0
  25. package/setup-plugin-api.ts +3 -0
  26. package/src/account-config.ts +77 -0
  27. package/src/account-ids.ts +17 -0
  28. package/src/account-types.ts +5 -0
  29. package/src/accounts.ts +176 -0
  30. package/src/action-runtime-target-auth.ts +27 -0
  31. package/src/action-runtime.ts +76 -0
  32. package/src/active-listener.ts +17 -0
  33. package/src/agent-tools-login.ts +113 -0
  34. package/src/approval-auth.ts +27 -0
  35. package/src/auth-store.runtime.ts +1 -0
  36. package/src/auth-store.ts +494 -0
  37. package/src/auto-reply/config.runtime.ts +16 -0
  38. package/src/auto-reply/constants.ts +1 -0
  39. package/src/auto-reply/deliver-reply.ts +332 -0
  40. package/src/auto-reply/loggers.ts +6 -0
  41. package/src/auto-reply/mentions.ts +131 -0
  42. package/src/auto-reply/monitor/ack-reaction.ts +99 -0
  43. package/src/auto-reply/monitor/audio-preflight.runtime.ts +9 -0
  44. package/src/auto-reply/monitor/broadcast.ts +153 -0
  45. package/src/auto-reply/monitor/commands.ts +19 -0
  46. package/src/auto-reply/monitor/echo.ts +64 -0
  47. package/src/auto-reply/monitor/group-activation.runtime.ts +1 -0
  48. package/src/auto-reply/monitor/group-activation.ts +73 -0
  49. package/src/auto-reply/monitor/group-gating.runtime.ts +8 -0
  50. package/src/auto-reply/monitor/group-gating.ts +218 -0
  51. package/src/auto-reply/monitor/group-members.ts +65 -0
  52. package/src/auto-reply/monitor/inbound-context.ts +92 -0
  53. package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +22 -0
  54. package/src/auto-reply/monitor/inbound-dispatch.ts +749 -0
  55. package/src/auto-reply/monitor/last-route.ts +61 -0
  56. package/src/auto-reply/monitor/listener-log.ts +28 -0
  57. package/src/auto-reply/monitor/message-line.runtime.ts +38 -0
  58. package/src/auto-reply/monitor/message-line.ts +54 -0
  59. package/src/auto-reply/monitor/on-message.ts +333 -0
  60. package/src/auto-reply/monitor/peer.ts +17 -0
  61. package/src/auto-reply/monitor/process-message.ts +584 -0
  62. package/src/auto-reply/monitor/runtime-api.ts +36 -0
  63. package/src/auto-reply/monitor/status-reaction.ts +108 -0
  64. package/src/auto-reply/monitor-state.ts +114 -0
  65. package/src/auto-reply/monitor.ts +720 -0
  66. package/src/auto-reply/reply-resolver.runtime.ts +1 -0
  67. package/src/auto-reply/types.ts +48 -0
  68. package/src/auto-reply/util.ts +62 -0
  69. package/src/auto-reply.impl.ts +6 -0
  70. package/src/auto-reply.ts +1 -0
  71. package/src/channel-actions.runtime.ts +7 -0
  72. package/src/channel-actions.ts +85 -0
  73. package/src/channel-outbound.ts +87 -0
  74. package/src/channel-react-action.runtime.ts +10 -0
  75. package/src/channel-react-action.ts +247 -0
  76. package/src/channel.runtime.ts +117 -0
  77. package/src/channel.setup.ts +32 -0
  78. package/src/channel.ts +356 -0
  79. package/src/command-policy.ts +7 -0
  80. package/src/config-accessors.ts +22 -0
  81. package/src/config-schema.ts +6 -0
  82. package/src/config-ui-hints.ts +24 -0
  83. package/src/connection-controller-registry.ts +49 -0
  84. package/src/connection-controller.ts +680 -0
  85. package/src/creds-files.ts +19 -0
  86. package/src/creds-persistence.ts +71 -0
  87. package/src/directory-config.ts +40 -0
  88. package/src/doctor-contract.ts +11 -0
  89. package/src/doctor.ts +56 -0
  90. package/src/document-filename.ts +17 -0
  91. package/src/group-intro.ts +15 -0
  92. package/src/group-policy.ts +40 -0
  93. package/src/group-session-contract.ts +20 -0
  94. package/src/group-session-key.ts +42 -0
  95. package/src/heartbeat.ts +34 -0
  96. package/src/identity.ts +164 -0
  97. package/src/inbound/access-control.ts +187 -0
  98. package/src/inbound/dedupe.ts +132 -0
  99. package/src/inbound/extract.ts +484 -0
  100. package/src/inbound/lifecycle.ts +39 -0
  101. package/src/inbound/media.ts +128 -0
  102. package/src/inbound/monitor.ts +1042 -0
  103. package/src/inbound/outbound-mentions.ts +260 -0
  104. package/src/inbound/runtime-api.ts +7 -0
  105. package/src/inbound/save-media.runtime.ts +1 -0
  106. package/src/inbound/send-api.ts +203 -0
  107. package/src/inbound/send-result.ts +109 -0
  108. package/src/inbound/types.ts +107 -0
  109. package/src/inbound-policy.ts +215 -0
  110. package/src/inbound.ts +9 -0
  111. package/src/login-qr.ts +542 -0
  112. package/src/login.ts +83 -0
  113. package/src/media.ts +10 -0
  114. package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +417 -0
  115. package/src/monitor-inbox.append-upsert.test-support.ts +133 -0
  116. package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +418 -0
  117. package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +308 -0
  118. package/src/monitor-inbox.streams-inbound-messages.test-support.ts +824 -0
  119. package/src/normalize-target.ts +148 -0
  120. package/src/normalize.ts +8 -0
  121. package/src/outbound-adapter.ts +36 -0
  122. package/src/outbound-base.ts +256 -0
  123. package/src/outbound-media-contract.ts +307 -0
  124. package/src/outbound-media.runtime.ts +41 -0
  125. package/src/outbound-send-deps.ts +1 -0
  126. package/src/outbound-test-support.ts +16 -0
  127. package/src/qa-driver.runtime.ts +189 -0
  128. package/src/qr-image.ts +1 -0
  129. package/src/qr-terminal.ts +1 -0
  130. package/src/quoted-message.ts +184 -0
  131. package/src/reaction-level.ts +24 -0
  132. package/src/reconnect.ts +55 -0
  133. package/src/resolve-outbound-target.ts +58 -0
  134. package/src/runtime-api.ts +59 -0
  135. package/src/runtime-group-policy.ts +16 -0
  136. package/src/runtime.ts +9 -0
  137. package/src/security-contract.ts +47 -0
  138. package/src/security-fix.ts +71 -0
  139. package/src/send.ts +342 -0
  140. package/src/session-contract.ts +43 -0
  141. package/src/session-errors.ts +125 -0
  142. package/src/session-route.ts +32 -0
  143. package/src/session.runtime.ts +8 -0
  144. package/src/session.ts +327 -0
  145. package/src/setup-core.ts +52 -0
  146. package/src/setup-finalize.ts +450 -0
  147. package/src/setup-surface.ts +71 -0
  148. package/src/setup-test-helpers.ts +217 -0
  149. package/src/shared.ts +291 -0
  150. package/src/socket-timing.ts +38 -0
  151. package/src/state-migrations.ts +55 -0
  152. package/src/status-issues.ts +185 -0
  153. package/src/system-prompt.ts +31 -0
  154. package/src/targets-runtime.ts +221 -0
  155. package/src/text-runtime.ts +18 -0
  156. package/src/vcard.ts +84 -0
  157. package/targets.ts +5 -0
  158. package/test-api.ts +2 -0
  159. package/tsconfig.json +16 -0
@@ -0,0 +1,71 @@
1
+ import { replaceFileAtomic } from "autobot/plugin-sdk/security-runtime";
2
+ import { resolveWebCredsPath } from "./creds-files.js";
3
+
4
+ const CREDS_FILE_MODE = 0o600;
5
+ const CREDS_SAVE_FLUSH_TIMEOUT_MS = 15_000;
6
+
7
+ const credsSaveQueues = new Map<string, Promise<void>>();
8
+
9
+ export type CredsQueueWaitResult = "drained" | "timed_out";
10
+
11
+ async function stringifyCreds(creds: unknown): Promise<string> {
12
+ const { BufferJSON } = await import("./session.runtime.js");
13
+ return JSON.stringify(creds, BufferJSON.replacer);
14
+ }
15
+
16
+ export async function writeCredsJsonAtomically(authDir: string, creds: unknown): Promise<void> {
17
+ const credsPath = resolveWebCredsPath(authDir);
18
+ const json = await stringifyCreds(creds);
19
+ await replaceFileAtomic({
20
+ filePath: credsPath,
21
+ content: json,
22
+ dirMode: 0o700,
23
+ mode: CREDS_FILE_MODE,
24
+ tempPrefix: ".creds",
25
+ syncTempFile: true,
26
+ syncParentDir: true,
27
+ });
28
+ }
29
+
30
+ export function enqueueCredsSave(
31
+ authDir: string,
32
+ saveCreds: () => Promise<void> | void,
33
+ onError: (error: unknown) => void,
34
+ ): void {
35
+ const previous = credsSaveQueues.get(authDir) ?? Promise.resolve();
36
+ const next = previous
37
+ .then(() => saveCreds())
38
+ .catch((error) => {
39
+ onError(error);
40
+ })
41
+ .finally(() => {
42
+ if (credsSaveQueues.get(authDir) === next) {
43
+ credsSaveQueues.delete(authDir);
44
+ }
45
+ });
46
+ credsSaveQueues.set(authDir, next);
47
+ }
48
+
49
+ export function waitForCredsSaveQueue(authDir?: string): Promise<void> {
50
+ if (authDir) {
51
+ return credsSaveQueues.get(authDir) ?? Promise.resolve();
52
+ }
53
+ return Promise.all(credsSaveQueues.values()).then(() => {});
54
+ }
55
+
56
+ export async function waitForCredsSaveQueueWithTimeout(
57
+ authDir: string,
58
+ timeoutMs = CREDS_SAVE_FLUSH_TIMEOUT_MS,
59
+ ): Promise<CredsQueueWaitResult> {
60
+ let flushTimeout: ReturnType<typeof setTimeout> | undefined;
61
+ return await Promise.race([
62
+ waitForCredsSaveQueue(authDir).then(() => "drained" as const),
63
+ new Promise<CredsQueueWaitResult>((resolve) => {
64
+ flushTimeout = setTimeout(() => resolve("timed_out"), timeoutMs);
65
+ }),
66
+ ]).finally(() => {
67
+ if (flushTimeout) {
68
+ clearTimeout(flushTimeout);
69
+ }
70
+ });
71
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ listResolvedDirectoryGroupEntriesFromMapKeys,
3
+ listResolvedDirectoryUserEntriesFromAllowFrom,
4
+ type DirectoryConfigParams,
5
+ } from "autobot/plugin-sdk/directory-config-runtime";
6
+ import { resolveMergedWhatsAppAccountConfig } from "./account-config.js";
7
+ import type { WhatsAppAccountConfig } from "./account-types.js";
8
+ import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "./normalize.js";
9
+
10
+ type WhatsAppDirectoryAccount = WhatsAppAccountConfig & { accountId: string };
11
+
12
+ function resolveWhatsAppDirectoryAccount(
13
+ cfg: DirectoryConfigParams["cfg"],
14
+ accountId?: string | null,
15
+ ): WhatsAppDirectoryAccount {
16
+ return resolveMergedWhatsAppAccountConfig({ cfg, accountId });
17
+ }
18
+
19
+ export async function listWhatsAppDirectoryPeersFromConfig(params: DirectoryConfigParams) {
20
+ return listResolvedDirectoryUserEntriesFromAllowFrom<WhatsAppDirectoryAccount>({
21
+ ...params,
22
+ resolveAccount: resolveWhatsAppDirectoryAccount,
23
+ resolveAllowFrom: (account) => account.allowFrom,
24
+ normalizeId: (entry) => {
25
+ const normalized = normalizeWhatsAppTarget(entry);
26
+ if (!normalized || isWhatsAppGroupJid(normalized)) {
27
+ return null;
28
+ }
29
+ return normalized;
30
+ },
31
+ });
32
+ }
33
+
34
+ export async function listWhatsAppDirectoryGroupsFromConfig(params: DirectoryConfigParams) {
35
+ return listResolvedDirectoryGroupEntriesFromMapKeys<WhatsAppDirectoryAccount>({
36
+ ...params,
37
+ resolveAccount: resolveWhatsAppDirectoryAccount,
38
+ resolveGroups: (account) => account.groups,
39
+ });
40
+ }
@@ -0,0 +1,11 @@
1
+ import type { ChannelDoctorConfigMutation } from "autobot/plugin-sdk/channel-contract";
2
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
3
+ import { normalizeCompatibilityConfig as normalizeCompatibilityConfigImpl } from "./doctor.js";
4
+
5
+ export function normalizeCompatibilityConfig({
6
+ cfg,
7
+ }: {
8
+ cfg: AutoBotConfig;
9
+ }): ChannelDoctorConfigMutation {
10
+ return normalizeCompatibilityConfigImpl({ cfg });
11
+ }
package/src/doctor.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type {
2
+ ChannelDoctorAdapter,
3
+ ChannelDoctorConfigMutation,
4
+ } from "autobot/plugin-sdk/channel-contract";
5
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
6
+
7
+ export function normalizeCompatibilityConfig({
8
+ cfg,
9
+ }: {
10
+ cfg: AutoBotConfig;
11
+ }): ChannelDoctorConfigMutation {
12
+ const legacyAckReaction = cfg.messages?.ackReaction?.trim();
13
+ if (!legacyAckReaction || cfg.channels?.whatsapp === undefined) {
14
+ return { config: cfg, changes: [] };
15
+ }
16
+ if (cfg.channels.whatsapp?.ackReaction !== undefined) {
17
+ return { config: cfg, changes: [] };
18
+ }
19
+
20
+ const legacyScope = cfg.messages?.ackReactionScope ?? "group-mentions";
21
+ let direct = true;
22
+ let group: "always" | "mentions" | "never" = "mentions";
23
+ if (legacyScope === "all") {
24
+ direct = true;
25
+ group = "always";
26
+ } else if (legacyScope === "direct") {
27
+ direct = true;
28
+ group = "never";
29
+ } else if (legacyScope === "group-all") {
30
+ direct = false;
31
+ group = "always";
32
+ } else if (legacyScope === "group-mentions") {
33
+ direct = false;
34
+ group = "mentions";
35
+ }
36
+
37
+ return {
38
+ config: {
39
+ ...cfg,
40
+ channels: {
41
+ ...cfg.channels,
42
+ whatsapp: {
43
+ ...cfg.channels?.whatsapp,
44
+ ackReaction: { emoji: legacyAckReaction, direct, group },
45
+ },
46
+ },
47
+ },
48
+ changes: [
49
+ `Copied messages.ackReaction → channels.whatsapp.ackReaction (scope: ${legacyScope}).`,
50
+ ],
51
+ };
52
+ }
53
+
54
+ export const whatsappDoctor: ChannelDoctorAdapter = {
55
+ normalizeCompatibilityConfig,
56
+ };
@@ -0,0 +1,17 @@
1
+ import { extensionForMime } from "autobot/plugin-sdk/media-mime";
2
+
3
+ const WHATSAPP_DEFAULT_DOCUMENT_FILE_NAME = "file";
4
+
5
+ export function resolveWhatsAppDefaultDocumentFileName(mimetype?: string): string {
6
+ const extension = extensionForMime(mimetype);
7
+ return extension
8
+ ? `${WHATSAPP_DEFAULT_DOCUMENT_FILE_NAME}${extension}`
9
+ : WHATSAPP_DEFAULT_DOCUMENT_FILE_NAME;
10
+ }
11
+
12
+ export function resolveWhatsAppDocumentFileName(params: {
13
+ fileName?: string;
14
+ mimetype?: string;
15
+ }): string {
16
+ return params.fileName?.trim() || resolveWhatsAppDefaultDocumentFileName(params.mimetype);
17
+ }
@@ -0,0 +1,15 @@
1
+ const WHATSAPP_GROUP_INTRO_HINT =
2
+ "WhatsApp IDs: SenderId is the participant JID (group participant id).";
3
+
4
+ export function resolveWhatsAppGroupIntroHint(): string {
5
+ return WHATSAPP_GROUP_INTRO_HINT;
6
+ }
7
+
8
+ export function resolveWhatsAppMentionStripRegexes(ctx: { To?: string | null }): RegExp[] {
9
+ const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/i, "");
10
+ if (!selfE164) {
11
+ return [];
12
+ }
13
+ const escaped = selfE164.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
+ return [new RegExp(escaped, "g"), new RegExp(`@${escaped}`, "g")];
15
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ resolveChannelGroupRequireMention,
3
+ resolveChannelGroupToolsPolicy,
4
+ type GroupToolPolicyConfig,
5
+ } from "autobot/plugin-sdk/channel-policy";
6
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
7
+
8
+ type WhatsAppGroupContext = {
9
+ cfg: AutoBotConfig;
10
+ accountId?: string | null;
11
+ groupId?: string | null;
12
+ senderId?: string | null;
13
+ senderName?: string | null;
14
+ senderUsername?: string | null;
15
+ senderE164?: string | null;
16
+ };
17
+
18
+ export function resolveWhatsAppGroupRequireMention(params: WhatsAppGroupContext): boolean {
19
+ return resolveChannelGroupRequireMention({
20
+ cfg: params.cfg,
21
+ channel: "whatsapp",
22
+ groupId: params.groupId,
23
+ accountId: params.accountId,
24
+ });
25
+ }
26
+
27
+ export function resolveWhatsAppGroupToolPolicy(
28
+ params: WhatsAppGroupContext,
29
+ ): GroupToolPolicyConfig | undefined {
30
+ return resolveChannelGroupToolsPolicy({
31
+ cfg: params.cfg,
32
+ channel: "whatsapp",
33
+ groupId: params.groupId,
34
+ accountId: params.accountId,
35
+ senderId: params.senderId,
36
+ senderName: params.senderName,
37
+ senderUsername: params.senderUsername,
38
+ senderE164: params.senderE164,
39
+ });
40
+ }
@@ -0,0 +1,20 @@
1
+ import { normalizeLowercaseStringOrEmpty } from "autobot/plugin-sdk/string-coerce-runtime";
2
+
3
+ export function resolveLegacyGroupSessionKey(ctx: { From?: string }): {
4
+ key: string;
5
+ channel: string;
6
+ id: string;
7
+ chatType: "group";
8
+ } | null {
9
+ const from = typeof ctx.From === "string" ? ctx.From.trim() : "";
10
+ const normalized = normalizeLowercaseStringOrEmpty(from);
11
+ if (!from || from.includes(":") || !normalized.endsWith("@g.us")) {
12
+ return null;
13
+ }
14
+ return {
15
+ key: `whatsapp:group:${normalized}`,
16
+ channel: "whatsapp",
17
+ id: normalized,
18
+ chatType: "group",
19
+ };
20
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ DEFAULT_ACCOUNT_ID,
3
+ normalizeAccountId,
4
+ resolveThreadSessionKeys,
5
+ type ResolvedAgentRoute,
6
+ } from "autobot/plugin-sdk/routing";
7
+
8
+ function resolveWhatsAppGroupAccountThreadId(accountId: string): string {
9
+ return `whatsapp-account-${normalizeAccountId(accountId)}`;
10
+ }
11
+
12
+ export function resolveWhatsAppLegacyGroupSessionKey(params: {
13
+ sessionKey: string;
14
+ accountId?: string | null;
15
+ }): string | null {
16
+ const accountId = normalizeAccountId(params.accountId);
17
+ if (!accountId || accountId === DEFAULT_ACCOUNT_ID || !params.sessionKey.includes(":group:")) {
18
+ return null;
19
+ }
20
+ const suffix = `:thread:${resolveWhatsAppGroupAccountThreadId(accountId)}`;
21
+ return params.sessionKey.endsWith(suffix) ? params.sessionKey.slice(0, -suffix.length) : null;
22
+ }
23
+
24
+ export function resolveWhatsAppGroupSessionRoute(route: ResolvedAgentRoute): ResolvedAgentRoute {
25
+ if (route.accountId === DEFAULT_ACCOUNT_ID || !route.sessionKey.includes(":group:")) {
26
+ return route;
27
+ }
28
+ const scopedSession = resolveThreadSessionKeys({
29
+ baseSessionKey: route.sessionKey,
30
+ threadId: resolveWhatsAppGroupAccountThreadId(route.accountId),
31
+ });
32
+ return {
33
+ ...route,
34
+ sessionKey: scopedSession.sessionKey,
35
+ };
36
+ }
37
+
38
+ export const testing = {
39
+ resolveWhatsAppGroupAccountThreadId,
40
+ resolveWhatsAppLegacyGroupSessionKey,
41
+ };
42
+ export { testing as __testing };
@@ -0,0 +1,34 @@
1
+ import { resolveWhatsAppAccount } from "./accounts.js";
2
+ import { readWebAuthExistsForDecision, WHATSAPP_AUTH_UNSTABLE_CODE } from "./auth-store.js";
3
+ import type { AutoBotConfig } from "./runtime-api.js";
4
+ import { loadWhatsAppChannelRuntime } from "./shared.js";
5
+
6
+ export async function checkWhatsAppHeartbeatReady(params: {
7
+ cfg: AutoBotConfig;
8
+ accountId?: string;
9
+ deps?: {
10
+ readWebAuthExistsForDecision?: typeof readWebAuthExistsForDecision;
11
+ hasActiveWebListener?: (accountId?: string) => boolean;
12
+ };
13
+ }) {
14
+ if (params.cfg.web?.enabled === false) {
15
+ return { ok: false as const, reason: "whatsapp-disabled" as const };
16
+ }
17
+ const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId });
18
+ const authState = await (
19
+ params.deps?.readWebAuthExistsForDecision ?? readWebAuthExistsForDecision
20
+ )(account.authDir);
21
+ if (authState.outcome === "unstable") {
22
+ return { ok: false as const, reason: WHATSAPP_AUTH_UNSTABLE_CODE };
23
+ }
24
+ if (!authState.exists) {
25
+ return { ok: false as const, reason: "whatsapp-not-linked" as const };
26
+ }
27
+ const listenerActive = params.deps?.hasActiveWebListener
28
+ ? params.deps.hasActiveWebListener(account.accountId)
29
+ : Boolean((await loadWhatsAppChannelRuntime()).getActiveWebListener(account.accountId));
30
+ if (!listenerActive) {
31
+ return { ok: false as const, reason: "whatsapp-not-running" as const };
32
+ }
33
+ return { ok: true as const, reason: "ok" as const };
34
+ }
@@ -0,0 +1,164 @@
1
+ import { jidToE164, normalizeE164 } from "./text-runtime.js";
2
+
3
+ const WHATSAPP_LID_RE = /@(lid|hosted\.lid)$/i;
4
+
5
+ export type WhatsAppIdentity = {
6
+ jid?: string | null;
7
+ lid?: string | null;
8
+ e164?: string | null;
9
+ name?: string | null;
10
+ label?: string | null;
11
+ };
12
+
13
+ export type WhatsAppSelfIdentity = {
14
+ jid?: string | null;
15
+ lid?: string | null;
16
+ e164?: string | null;
17
+ };
18
+
19
+ export type WhatsAppReplyContext = {
20
+ id?: string;
21
+ body: string;
22
+ sender?: WhatsAppIdentity | null;
23
+ };
24
+
25
+ type LegacySenderLike = {
26
+ sender?: WhatsAppIdentity;
27
+ senderJid?: string;
28
+ senderE164?: string;
29
+ senderName?: string;
30
+ };
31
+
32
+ type LegacySelfLike = {
33
+ self?: WhatsAppSelfIdentity;
34
+ selfJid?: string | null;
35
+ selfLid?: string | null;
36
+ selfE164?: string | null;
37
+ };
38
+
39
+ type LegacyReplyLike = {
40
+ replyTo?: WhatsAppReplyContext;
41
+ replyToId?: string;
42
+ replyToBody?: string;
43
+ replyToSender?: string;
44
+ replyToSenderJid?: string;
45
+ replyToSenderE164?: string;
46
+ };
47
+
48
+ type LegacyMentionsLike = {
49
+ mentions?: string[];
50
+ mentionedJids?: string[];
51
+ };
52
+
53
+ function normalizeDeviceScopedJid(jid: string | null | undefined): string | null {
54
+ return jid ? jid.replace(/:\d+/, "") : null;
55
+ }
56
+
57
+ function isLidJid(jid: string | null | undefined): boolean {
58
+ return Boolean(jid && WHATSAPP_LID_RE.test(jid));
59
+ }
60
+
61
+ export function resolveComparableIdentity(
62
+ identity: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
63
+ authDir?: string,
64
+ ): WhatsAppIdentity {
65
+ const rawJid = normalizeDeviceScopedJid(identity?.jid);
66
+ const rawLid = normalizeDeviceScopedJid(identity?.lid);
67
+ const lid = rawLid ?? (isLidJid(rawJid) ? rawJid : null);
68
+ const jid = rawJid && !isLidJid(rawJid) ? rawJid : null;
69
+ const e164 =
70
+ identity?.e164 != null
71
+ ? normalizeE164(identity.e164)
72
+ : ((jid ? jidToE164(jid, authDir ? { authDir } : undefined) : null) ??
73
+ (lid ? jidToE164(lid, authDir ? { authDir } : undefined) : null));
74
+ return {
75
+ ...identity,
76
+ jid,
77
+ lid,
78
+ e164,
79
+ };
80
+ }
81
+
82
+ export function getComparableIdentityValues(
83
+ identity: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
84
+ ): string[] {
85
+ const resolved = resolveComparableIdentity(identity);
86
+ return [resolved.e164, resolved.jid, resolved.lid].filter((value): value is string =>
87
+ Boolean(value),
88
+ );
89
+ }
90
+
91
+ export function identitiesOverlap(
92
+ left: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
93
+ right: WhatsAppIdentity | WhatsAppSelfIdentity | null | undefined,
94
+ ): boolean {
95
+ const leftValues = new Set(getComparableIdentityValues(left));
96
+ if (leftValues.size === 0) {
97
+ return false;
98
+ }
99
+ return getComparableIdentityValues(right).some((value) => leftValues.has(value));
100
+ }
101
+
102
+ export function getSenderIdentity(msg: LegacySenderLike, authDir?: string): WhatsAppIdentity {
103
+ return resolveComparableIdentity(
104
+ msg.sender ?? {
105
+ jid: msg.senderJid ?? null,
106
+ e164: msg.senderE164 ?? null,
107
+ name: msg.senderName ?? null,
108
+ },
109
+ authDir,
110
+ );
111
+ }
112
+
113
+ export function getSelfIdentity(msg: LegacySelfLike, authDir?: string): WhatsAppSelfIdentity {
114
+ return resolveComparableIdentity(
115
+ msg.self ?? {
116
+ jid: msg.selfJid ?? null,
117
+ lid: msg.selfLid ?? null,
118
+ e164: msg.selfE164 ?? null,
119
+ },
120
+ authDir,
121
+ );
122
+ }
123
+
124
+ export function getReplyContext(
125
+ msg: LegacyReplyLike,
126
+ authDir?: string,
127
+ ): WhatsAppReplyContext | null {
128
+ if (msg.replyTo) {
129
+ return {
130
+ ...msg.replyTo,
131
+ sender: resolveComparableIdentity(msg.replyTo.sender, authDir),
132
+ };
133
+ }
134
+ if (!msg.replyToBody) {
135
+ return null;
136
+ }
137
+ return {
138
+ id: msg.replyToId,
139
+ body: msg.replyToBody,
140
+ sender: resolveComparableIdentity(
141
+ {
142
+ jid: msg.replyToSenderJid ?? null,
143
+ e164: msg.replyToSenderE164 ?? null,
144
+ label: msg.replyToSender ?? null,
145
+ },
146
+ authDir,
147
+ ),
148
+ };
149
+ }
150
+
151
+ function getMentionJids(msg: LegacyMentionsLike): string[] {
152
+ return msg.mentions ?? msg.mentionedJids ?? [];
153
+ }
154
+
155
+ export function getMentionIdentities(
156
+ msg: LegacyMentionsLike,
157
+ authDir?: string,
158
+ ): WhatsAppIdentity[] {
159
+ return getMentionJids(msg).map((jid) => resolveComparableIdentity({ jid }, authDir));
160
+ }
161
+
162
+ export function getPrimaryIdentityId(identity: WhatsAppIdentity | null | undefined): string | null {
163
+ return identity?.e164 || identity?.jid?.trim() || identity?.lid || null;
164
+ }