@hraness/ghostget 0.17.2
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 +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import type { GhostgetAuth } from "./auth";
|
|
7
|
+
import {
|
|
8
|
+
beginBeeperMessageLikeMeHelperLaunch,
|
|
9
|
+
bindBeeperMessageLikeMeHelperOwner,
|
|
10
|
+
markBeeperMessageLikeMeHelperCleanupUnsafe,
|
|
11
|
+
settleBeeperMessageLikeMeHelper,
|
|
12
|
+
type BeeperMessageLikeMeExportAdmission,
|
|
13
|
+
} from "./beeper-message-like-me-recovery";
|
|
14
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
15
|
+
import {
|
|
16
|
+
canonicalWhatsAppAccountSubjectJid,
|
|
17
|
+
canonicalWhatsAppParticipantJid,
|
|
18
|
+
isCanonicalWhatsAppAccountSubject,
|
|
19
|
+
} from "./providers/whatsapp-account-identity";
|
|
20
|
+
import {
|
|
21
|
+
containsWhatsAppContactProjectionCleanupUnverified,
|
|
22
|
+
runWhatsAppMessageExportSessionHelperChild,
|
|
23
|
+
validateWhatsAppStoreDirectory,
|
|
24
|
+
type WhatsAppContactProjectionHelperInvocation,
|
|
25
|
+
type WhatsAppContactProjectionHelperResult,
|
|
26
|
+
type WhatsAppMessageExportSessionCanonicalFrame,
|
|
27
|
+
type WhatsAppMessageExportSessionHelperResult,
|
|
28
|
+
} from "./providers/whatsapp-web-runtime";
|
|
29
|
+
import {
|
|
30
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
|
|
31
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
|
|
32
|
+
parseWhatsAppMessageExportProjectionRequest,
|
|
33
|
+
parseWhatsAppMessageExportProjectionResponse,
|
|
34
|
+
type WhatsAppMessageExportProjectionGeneration,
|
|
35
|
+
type WhatsAppMessageExportProjectionItem,
|
|
36
|
+
type WhatsAppMessageExportProjectionRequest,
|
|
37
|
+
} from "./providers/whatsapp-message-export-projection-protocol";
|
|
38
|
+
import {
|
|
39
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
|
|
40
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_PROVIDER,
|
|
41
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
42
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_SOURCE,
|
|
43
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS,
|
|
44
|
+
type WhatsAppMessageBundleV2Attachment,
|
|
45
|
+
type WhatsAppMessageBundleV2Completion,
|
|
46
|
+
type WhatsAppMessageBundleV2Conversation,
|
|
47
|
+
type WhatsAppMessageBundleV2Record,
|
|
48
|
+
} from "./whatsapp-message-bundle-v2";
|
|
49
|
+
|
|
50
|
+
const PAGE_LIMIT = 500;
|
|
51
|
+
const PAGE_TIMEOUT_MS = 5 * 60_000;
|
|
52
|
+
const EXPORT_TIMEOUT_MS = 6 * 60 * 60_000;
|
|
53
|
+
const MAX_STDERR_BYTES = 16 * 1024;
|
|
54
|
+
|
|
55
|
+
type WhatsAppAuth = Extract<GhostgetAuth, { readonly kind: "linked-device-store" }>;
|
|
56
|
+
|
|
57
|
+
export type WhatsAppMessageLikeMeProgress =
|
|
58
|
+
| Readonly<{ phase: "preparing" }>
|
|
59
|
+
| Readonly<{ phase: "page-started"; page: number; messages: number }>
|
|
60
|
+
| Readonly<{ phase: "page-completed"; page: number; messages: number }>
|
|
61
|
+
| Readonly<{
|
|
62
|
+
phase: "conversion-completed";
|
|
63
|
+
messages: number;
|
|
64
|
+
conversations: number;
|
|
65
|
+
participants: number;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
export type WhatsAppMessageLikeMeSourceDependencies = Readonly<{
|
|
69
|
+
helperPath?: string;
|
|
70
|
+
configPath?: string;
|
|
71
|
+
runHelper?: (
|
|
72
|
+
invocation: WhatsAppContactProjectionHelperInvocation,
|
|
73
|
+
) => Promise<WhatsAppContactProjectionHelperResult>;
|
|
74
|
+
/** Test-only persistent-session process seam. */
|
|
75
|
+
runSessionHelper?: (
|
|
76
|
+
invocation: WhatsAppContactProjectionHelperInvocation,
|
|
77
|
+
) => Promise<WhatsAppMessageExportSessionHelperResult>;
|
|
78
|
+
/** Test-only total-operation deadline override. */
|
|
79
|
+
totalTimeoutMs?: number;
|
|
80
|
+
/** Test-only monotonic clock seam. */
|
|
81
|
+
monotonicNow?: () => number;
|
|
82
|
+
/** Test-only smaller v2 aggregate-record bound. */
|
|
83
|
+
recordLimit?: number;
|
|
84
|
+
}>;
|
|
85
|
+
|
|
86
|
+
export type WhatsAppMessageLikeMeSourceRequest = Readonly<{
|
|
87
|
+
auth: GhostgetAuth;
|
|
88
|
+
stateEnvironment?: Readonly<Record<string, string | undefined>>;
|
|
89
|
+
/** Internal durable admission held by the native CLI. */
|
|
90
|
+
admission?: BeeperMessageLikeMeExportAdmission;
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
onProgress?: (progress: WhatsAppMessageLikeMeProgress) => void;
|
|
93
|
+
dependencies?: WhatsAppMessageLikeMeSourceDependencies;
|
|
94
|
+
}>;
|
|
95
|
+
|
|
96
|
+
export type WhatsAppMessageLikeMeExportSource = Readonly<{
|
|
97
|
+
descriptor: unknown;
|
|
98
|
+
records: AsyncIterable<unknown>;
|
|
99
|
+
completion: () => Promise<unknown>;
|
|
100
|
+
}>;
|
|
101
|
+
|
|
102
|
+
type ConversationFact = {
|
|
103
|
+
readonly jid: string;
|
|
104
|
+
readonly type: "direct" | "group";
|
|
105
|
+
title: string | null;
|
|
106
|
+
readonly participantJids: Set<string>;
|
|
107
|
+
startedAt: string;
|
|
108
|
+
lastMessageAt: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type ParticipantFact = {
|
|
112
|
+
readonly jid: string;
|
|
113
|
+
displayName: string | null;
|
|
114
|
+
readonly isSelf: boolean;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function fail(message: string): never {
|
|
118
|
+
throw new Error(`WhatsApp Message Like Me source: ${message}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function requireAuth(value: GhostgetAuth): WhatsAppAuth & Readonly<{ subject: string }> {
|
|
122
|
+
if (
|
|
123
|
+
value.kind !== "linked-device-store"
|
|
124
|
+
|| value.provider !== "whatsapp"
|
|
125
|
+
|| value.subject === undefined
|
|
126
|
+
|| !isCanonicalWhatsAppAccountSubject(value.subject)
|
|
127
|
+
) return fail(
|
|
128
|
+
"a bound WhatsApp linked-device-store auth locator is required; its account subject must be canonical, so re-pair a legacy noncanonical account under a new auth id",
|
|
129
|
+
);
|
|
130
|
+
return value as WhatsAppAuth & Readonly<{ subject: string }>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function senderJid(
|
|
134
|
+
item: WhatsAppMessageExportProjectionItem,
|
|
135
|
+
accountJid: string,
|
|
136
|
+
accountJidAliases: ReadonlySet<string>,
|
|
137
|
+
directPeer: string | null,
|
|
138
|
+
): string | null {
|
|
139
|
+
const explicit = item.senderJid === null
|
|
140
|
+
? null
|
|
141
|
+
: canonicalWhatsAppParticipantJid(item.senderJid);
|
|
142
|
+
if (item.chatKind === "dm") {
|
|
143
|
+
if (directPeer === null) return fail("direct conversation peer is missing");
|
|
144
|
+
if (item.fromMe) {
|
|
145
|
+
if (explicit !== null && !accountJidAliases.has(explicit)) {
|
|
146
|
+
return fail("outgoing direct sender does not match the bound account");
|
|
147
|
+
}
|
|
148
|
+
return accountJid;
|
|
149
|
+
}
|
|
150
|
+
if (explicit !== null && explicit !== directPeer) {
|
|
151
|
+
return fail("incoming direct sender does not match the exact peer");
|
|
152
|
+
}
|
|
153
|
+
return explicit ?? directPeer;
|
|
154
|
+
}
|
|
155
|
+
if (item.fromMe) {
|
|
156
|
+
if (explicit !== null && !accountJidAliases.has(explicit)) {
|
|
157
|
+
return fail("outgoing group sender does not match the bound account");
|
|
158
|
+
}
|
|
159
|
+
return accountJid;
|
|
160
|
+
}
|
|
161
|
+
if (explicit !== null && accountJidAliases.has(explicit)) {
|
|
162
|
+
return fail("incoming group sender cannot be the bound account");
|
|
163
|
+
}
|
|
164
|
+
return explicit;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function localId(kind: string, ...coordinates: readonly string[]): string {
|
|
168
|
+
return `whatsapp:${kind}:${sha256(coordinates.join("\u0000"))}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function messageProviderId(item: Pick<WhatsAppMessageExportProjectionItem, "chatJid" | "messageId">): string {
|
|
172
|
+
return `${item.chatJid}/${item.messageId}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function participantId(accountJid: string, participantJid: string): string {
|
|
176
|
+
return localId("participant", accountJid, participantJid);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function userHandle(jid: string): string | null {
|
|
180
|
+
const phone = /^([1-9][0-9]{4,14})@s\.whatsapp\.net$/u.exec(jid);
|
|
181
|
+
return phone?.[1] === undefined ? null : `+${phone[1]}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function provenance(providerId: string, accountJid: string, observedAt: string, revision: string | null) {
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
providerId,
|
|
187
|
+
providerRevision: revision,
|
|
188
|
+
observedAt,
|
|
189
|
+
connectedAccountProviderId: accountJid,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function attachment(item: WhatsAppMessageExportProjectionItem): readonly WhatsAppMessageBundleV2Attachment[] {
|
|
194
|
+
if (
|
|
195
|
+
item.mediaType === null
|
|
196
|
+
&& item.mimeType === null
|
|
197
|
+
&& item.fileName === null
|
|
198
|
+
&& item.fileLength === null
|
|
199
|
+
) return Object.freeze([]);
|
|
200
|
+
const source = item.mediaType?.toLowerCase() ?? "";
|
|
201
|
+
const kind: WhatsAppMessageBundleV2Attachment["kind"] =
|
|
202
|
+
source === "audio" || source === "ptt"
|
|
203
|
+
? "audio"
|
|
204
|
+
: source === "document"
|
|
205
|
+
? "document"
|
|
206
|
+
: source === "image"
|
|
207
|
+
? "image"
|
|
208
|
+
: source === "sticker"
|
|
209
|
+
? "sticker"
|
|
210
|
+
: source === "video"
|
|
211
|
+
? "video"
|
|
212
|
+
: "unknown";
|
|
213
|
+
return Object.freeze([Object.freeze({
|
|
214
|
+
kind,
|
|
215
|
+
mimeType: item.mimeType,
|
|
216
|
+
name: item.fileName,
|
|
217
|
+
sizeBytes: item.fileLength,
|
|
218
|
+
})]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function body(item: WhatsAppMessageExportProjectionItem): string | null {
|
|
222
|
+
if (item.revoked || item.deletedForMe || item.payloadPurgedAt !== null) return null;
|
|
223
|
+
|
|
224
|
+
const candidates = [item.text, item.mediaCaption].filter(
|
|
225
|
+
(candidate): candidate is string => candidate !== null && candidate.trim().length > 0,
|
|
226
|
+
);
|
|
227
|
+
const mediaType = item.mediaType?.toLowerCase();
|
|
228
|
+
if (mediaType === "audio" || mediaType === "ptt") {
|
|
229
|
+
const authoredCandidates = candidates.filter((candidate) => candidate !== "[Audio]");
|
|
230
|
+
if (authoredCandidates.length > 0) return authoredCandidates[0] ?? null;
|
|
231
|
+
if (candidates.length > 0) return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return candidates[0] ?? null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function messageDeletion(item: WhatsAppMessageExportProjectionItem, observedAt: string) {
|
|
238
|
+
if (!item.revoked && !item.deletedForMe) return null;
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
state: item.revoked && item.deletedForMe
|
|
241
|
+
? "revoked-and-deleted-for-me" as const
|
|
242
|
+
: item.revoked
|
|
243
|
+
? "revoked" as const
|
|
244
|
+
: "deleted-for-me" as const,
|
|
245
|
+
observedAt: item.deletedAt ?? observedAt,
|
|
246
|
+
providerRevision: item.deletedAt,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function fixedHelperFiles(
|
|
251
|
+
dependencies: WhatsAppMessageLikeMeSourceDependencies | undefined,
|
|
252
|
+
): Readonly<{ helper: string; config: string }> {
|
|
253
|
+
if ((dependencies?.helperPath === undefined) !== (dependencies?.configPath === undefined)) {
|
|
254
|
+
return fail("test helper and config paths must be supplied together");
|
|
255
|
+
}
|
|
256
|
+
if (dependencies?.helperPath !== undefined && dependencies.configPath !== undefined) {
|
|
257
|
+
return Object.freeze({ helper: dependencies.helperPath, config: dependencies.configPath });
|
|
258
|
+
}
|
|
259
|
+
const sourceDirectory = dirname(fileURLToPath(import.meta.url));
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
helper: resolve(sourceDirectory, "providers/whatsapp-interaction-projection-helper.ts"),
|
|
262
|
+
config: resolve(sourceDirectory, "state-helper.bunfig.toml"),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function boundIdentities(store: string) {
|
|
267
|
+
const [storeStats, sessionStats, messageStats] = await Promise.all([
|
|
268
|
+
lstat(store, { bigint: true }),
|
|
269
|
+
lstat(resolve(store, "session.db"), { bigint: true }),
|
|
270
|
+
lstat(resolve(store, "wacli.db"), { bigint: true }),
|
|
271
|
+
]);
|
|
272
|
+
return Object.freeze({
|
|
273
|
+
store: Object.freeze({ dev: storeStats.dev.toString(), ino: storeStats.ino.toString() }),
|
|
274
|
+
session: Object.freeze({ dev: sessionStats.dev.toString(), ino: sessionStats.ino.toString() }),
|
|
275
|
+
message: Object.freeze({ dev: messageStats.dev.toString(), ino: messageStats.ino.toString() }),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
280
|
+
if (signal?.aborted === true) return fail("export was cancelled");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function remainingExportTimeout(
|
|
284
|
+
startedAt: number,
|
|
285
|
+
dependencies: WhatsAppMessageLikeMeSourceDependencies | undefined,
|
|
286
|
+
pageBound: boolean,
|
|
287
|
+
): number {
|
|
288
|
+
const now = dependencies?.monotonicNow ?? (() => performance.now());
|
|
289
|
+
const timeout = dependencies?.totalTimeoutMs ?? EXPORT_TIMEOUT_MS;
|
|
290
|
+
if (!Number.isFinite(timeout) || timeout < 1) {
|
|
291
|
+
return fail("the total export timeout is invalid");
|
|
292
|
+
}
|
|
293
|
+
const elapsed = now() - startedAt;
|
|
294
|
+
if (!Number.isFinite(elapsed) || elapsed < 0 || elapsed >= timeout) {
|
|
295
|
+
return fail("export exceeded its total deadline");
|
|
296
|
+
}
|
|
297
|
+
const remaining = Math.ceil(timeout - elapsed);
|
|
298
|
+
return Math.max(1, pageBound ? Math.min(PAGE_TIMEOUT_MS, remaining) : remaining);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function earliest(current: string | null, candidate: string): string {
|
|
302
|
+
return current === null || candidate < current ? candidate : current;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function latest(current: string | null, candidate: string): string {
|
|
306
|
+
return current === null || candidate > current ? candidate : current;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const SESSION_PN_JID_PATTERN = /^[1-9][0-9]{4,14}@s\.whatsapp\.net$/u;
|
|
310
|
+
const SESSION_LID_JID_PATTERN = /^[1-9][0-9]{4,19}@lid$/u;
|
|
311
|
+
|
|
312
|
+
function sessionAccountJidAliases(value: unknown): Readonly<{
|
|
313
|
+
selfJids: readonly string[];
|
|
314
|
+
pnJid: string;
|
|
315
|
+
lidJid: string | null;
|
|
316
|
+
}> {
|
|
317
|
+
if (
|
|
318
|
+
!Array.isArray(value)
|
|
319
|
+
|| value.length < 1
|
|
320
|
+
|| value.length > 2
|
|
321
|
+
|| !value.every((jid) => typeof jid === "string")
|
|
322
|
+
|| new Set(value).size !== value.length
|
|
323
|
+
) return fail("one fixed projection page had unsupported owner aliases");
|
|
324
|
+
const selfJids = [...value].sort();
|
|
325
|
+
if (!value.every((jid, index) => jid === selfJids[index])) {
|
|
326
|
+
return fail("one fixed projection page had unsorted owner aliases");
|
|
327
|
+
}
|
|
328
|
+
const pnJids = selfJids.filter((jid) => SESSION_PN_JID_PATTERN.test(jid));
|
|
329
|
+
const lidJids = selfJids.filter((jid) => SESSION_LID_JID_PATTERN.test(jid));
|
|
330
|
+
if (
|
|
331
|
+
pnJids.length !== 1
|
|
332
|
+
|| lidJids.length > 1
|
|
333
|
+
|| pnJids.length + lidJids.length !== selfJids.length
|
|
334
|
+
) return fail("one fixed projection page had unsupported owner aliases");
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
selfJids: Object.freeze(selfJids),
|
|
337
|
+
pnJid: pnJids[0]!,
|
|
338
|
+
lidJid: lidJids[0] ?? null,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function* runMessageExportSession(
|
|
343
|
+
initialRequest: WhatsAppMessageExportProjectionRequest,
|
|
344
|
+
invocationBase: Omit<WhatsAppContactProjectionHelperInvocation, "stdin" | "maxOutputBytes">,
|
|
345
|
+
runHelper: (
|
|
346
|
+
invocation: WhatsAppContactProjectionHelperInvocation,
|
|
347
|
+
) => Promise<WhatsAppMessageExportSessionHelperResult>,
|
|
348
|
+
admission: BeeperMessageLikeMeExportAdmission | undefined,
|
|
349
|
+
checkDeadline: () => void,
|
|
350
|
+
): AsyncGenerator<unknown> {
|
|
351
|
+
type SessionPage = Readonly<{
|
|
352
|
+
response: Extract<ReturnType<typeof parseWhatsAppMessageExportProjectionResponse>, { status: "succeeded" }>;
|
|
353
|
+
selfChatsExcluded: "none-detected" | "present-excluded";
|
|
354
|
+
}>;
|
|
355
|
+
|
|
356
|
+
class SessionValidator {
|
|
357
|
+
readonly #framesHash = createHash("sha256");
|
|
358
|
+
#request = initialRequest;
|
|
359
|
+
#pages = 0;
|
|
360
|
+
#messages = 0;
|
|
361
|
+
#finalCheckpoint: unknown;
|
|
362
|
+
#finalGeneration: unknown;
|
|
363
|
+
#finalSelfJids: readonly string[] | undefined;
|
|
364
|
+
#finalSelfChatsExcluded: "none-detected" | "present-excluded" | undefined;
|
|
365
|
+
#finalNonConversationChatsExcluded: boolean | undefined;
|
|
366
|
+
#failed = false;
|
|
367
|
+
#sealed = false;
|
|
368
|
+
#terminal = false;
|
|
369
|
+
|
|
370
|
+
accept(canonicalFrame: WhatsAppMessageExportSessionCanonicalFrame): SessionPage | null {
|
|
371
|
+
checkDeadline();
|
|
372
|
+
if (this.#failed || this.#sealed) {
|
|
373
|
+
return fail("the fixed projection session emitted an extra frame");
|
|
374
|
+
}
|
|
375
|
+
const frame = canonicalFrame.value;
|
|
376
|
+
if (
|
|
377
|
+
typeof frame !== "object"
|
|
378
|
+
|| frame === null
|
|
379
|
+
|| Array.isArray(frame)
|
|
380
|
+
|| !("kind" in frame)
|
|
381
|
+
) return fail("one fixed projection frame was malformed");
|
|
382
|
+
if (frame.kind === "failed") {
|
|
383
|
+
if (
|
|
384
|
+
Object.keys(frame).sort().join("\0") !== "errorCode\0kind"
|
|
385
|
+
|| !("errorCode" in frame)
|
|
386
|
+
|| typeof frame.errorCode !== "string"
|
|
387
|
+
) return fail("the fixed projection failure frame was malformed");
|
|
388
|
+
const failure = parseWhatsAppMessageExportProjectionResponse({
|
|
389
|
+
schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
|
|
390
|
+
status: "failed",
|
|
391
|
+
errorCode: frame.errorCode,
|
|
392
|
+
}, this.#request);
|
|
393
|
+
if (failure.status !== "failed") {
|
|
394
|
+
return fail("the fixed projection failure frame was invalid");
|
|
395
|
+
}
|
|
396
|
+
this.#failed = true;
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
if (frame.kind === "page") {
|
|
400
|
+
if (
|
|
401
|
+
this.#terminal
|
|
402
|
+
|| Object.keys(frame).sort().join("\0")
|
|
403
|
+
!== "checkpoint\0index\0kind\0messages\0nonConversationChatsExcluded\0projectionGeneration\0selfChatsExcluded\0selfJids\0terminal"
|
|
404
|
+
|| !("index" in frame)
|
|
405
|
+
|| frame.index !== this.#pages + 1
|
|
406
|
+
|| !("selfChatsExcluded" in frame)
|
|
407
|
+
|| (frame.selfChatsExcluded !== "none-detected"
|
|
408
|
+
&& frame.selfChatsExcluded !== "present-excluded")
|
|
409
|
+
|| !("selfJids" in frame)
|
|
410
|
+
|| this.#pages >= 1_000
|
|
411
|
+
|| !("terminal" in frame)
|
|
412
|
+
|| typeof frame.terminal !== "boolean"
|
|
413
|
+
|| !("messages" in frame)
|
|
414
|
+
|| !Array.isArray(frame.messages)
|
|
415
|
+
|| !("checkpoint" in frame)
|
|
416
|
+
|| !("projectionGeneration" in frame)
|
|
417
|
+
|| !("nonConversationChatsExcluded" in frame)
|
|
418
|
+
|| typeof frame.nonConversationChatsExcluded !== "boolean"
|
|
419
|
+
) return fail("one fixed projection page frame was unsupported");
|
|
420
|
+
const aliases = sessionAccountJidAliases(frame.selfJids);
|
|
421
|
+
const response = parseWhatsAppMessageExportProjectionResponse({
|
|
422
|
+
schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
|
|
423
|
+
status: "succeeded",
|
|
424
|
+
projectionGeneration: frame.projectionGeneration,
|
|
425
|
+
accountJidAliases: { pnJid: aliases.pnJid, lidJid: aliases.lidJid },
|
|
426
|
+
nonConversationChatsExcluded: frame.nonConversationChatsExcluded,
|
|
427
|
+
messages: frame.messages,
|
|
428
|
+
nextCursor: frame.terminal ? null : (frame.checkpoint as { cursor?: unknown }).cursor,
|
|
429
|
+
localInsertPageComplete: frame.terminal,
|
|
430
|
+
checkpoint: frame.checkpoint,
|
|
431
|
+
}, this.#request);
|
|
432
|
+
if (response.status !== "succeeded") {
|
|
433
|
+
return fail("one fixed projection page failed");
|
|
434
|
+
}
|
|
435
|
+
const provedSelfJids = new Set(aliases.selfJids);
|
|
436
|
+
if (response.messages.some((message) =>
|
|
437
|
+
message.chatKind === "dm"
|
|
438
|
+
&& provedSelfJids.has(canonicalWhatsAppParticipantJid(message.chatJid)))) {
|
|
439
|
+
return fail("the fixed projection helper leaked a proved self chat");
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
response.localInsertPageComplete
|
|
443
|
+
&& response.messages.length === 0
|
|
444
|
+
&& this.#request.cursor !== "0"
|
|
445
|
+
) return fail("the fixed projection session contradicted its prior lookahead");
|
|
446
|
+
if (
|
|
447
|
+
this.#finalGeneration !== undefined
|
|
448
|
+
&& canonicalJson(this.#finalGeneration) !== canonicalJson(response.projectionGeneration)
|
|
449
|
+
) return fail("the fixed projection generation changed inside its session");
|
|
450
|
+
if (
|
|
451
|
+
this.#finalSelfJids !== undefined
|
|
452
|
+
&& canonicalJson(this.#finalSelfJids) !== canonicalJson(aliases.selfJids)
|
|
453
|
+
) return fail("the fixed projection owner aliases changed inside its session");
|
|
454
|
+
if (
|
|
455
|
+
this.#finalSelfChatsExcluded !== undefined
|
|
456
|
+
&& this.#finalSelfChatsExcluded !== frame.selfChatsExcluded
|
|
457
|
+
) return fail("the fixed projection self-chat exclusion changed inside its session");
|
|
458
|
+
if (
|
|
459
|
+
this.#finalNonConversationChatsExcluded !== undefined
|
|
460
|
+
&& this.#finalNonConversationChatsExcluded !== frame.nonConversationChatsExcluded
|
|
461
|
+
) return fail("the fixed projection non-conversation exclusion changed inside its session");
|
|
462
|
+
this.#framesHash.update(canonicalFrame.canonical).update("\n");
|
|
463
|
+
this.#messages += response.messages.length;
|
|
464
|
+
if (this.#messages > 500_000) {
|
|
465
|
+
return fail("the fixed projection session exceeded its message bound");
|
|
466
|
+
}
|
|
467
|
+
this.#pages += 1;
|
|
468
|
+
this.#finalCheckpoint = response.checkpoint;
|
|
469
|
+
this.#finalGeneration = response.projectionGeneration;
|
|
470
|
+
this.#finalSelfJids = aliases.selfJids;
|
|
471
|
+
this.#finalSelfChatsExcluded = frame.selfChatsExcluded;
|
|
472
|
+
this.#finalNonConversationChatsExcluded = frame.nonConversationChatsExcluded;
|
|
473
|
+
this.#terminal = response.localInsertPageComplete;
|
|
474
|
+
if (!response.localInsertPageComplete) {
|
|
475
|
+
this.#request = parseWhatsAppMessageExportProjectionRequest({
|
|
476
|
+
...this.#request,
|
|
477
|
+
cursor: response.checkpoint.cursor,
|
|
478
|
+
cursorAnchor: response.checkpoint.anchor,
|
|
479
|
+
expectedGeneration: response.projectionGeneration,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
checkDeadline();
|
|
483
|
+
return Object.freeze({ response, selfChatsExcluded: frame.selfChatsExcluded });
|
|
484
|
+
}
|
|
485
|
+
if (
|
|
486
|
+
frame.kind !== "seal"
|
|
487
|
+
|| Object.keys(frame).sort().join("\0")
|
|
488
|
+
!== "checkpoint\0framesSha256\0integrityChecks\0kind\0messages\0pages\0projectionGeneration\0selfChatsExcluded\0selfJids"
|
|
489
|
+
|| !("pages" in frame)
|
|
490
|
+
|| frame.pages !== this.#pages
|
|
491
|
+
|| !("messages" in frame)
|
|
492
|
+
|| frame.messages !== this.#messages
|
|
493
|
+
|| !this.#terminal
|
|
494
|
+
|| !("integrityChecks" in frame)
|
|
495
|
+
|| frame.integrityChecks !== 1
|
|
496
|
+
|| !("framesSha256" in frame)
|
|
497
|
+
|| frame.framesSha256 !== this.#framesHash.digest("hex")
|
|
498
|
+
|| !("checkpoint" in frame)
|
|
499
|
+
|| canonicalJson(frame.checkpoint) !== canonicalJson(this.#finalCheckpoint)
|
|
500
|
+
|| !("projectionGeneration" in frame)
|
|
501
|
+
|| canonicalJson(frame.projectionGeneration) !== canonicalJson(this.#finalGeneration)
|
|
502
|
+
|| !("selfJids" in frame)
|
|
503
|
+
|| canonicalJson(frame.selfJids) !== canonicalJson(this.#finalSelfJids)
|
|
504
|
+
|| !("selfChatsExcluded" in frame)
|
|
505
|
+
|| frame.selfChatsExcluded !== this.#finalSelfChatsExcluded
|
|
506
|
+
) return fail("the fixed projection session seal was invalid");
|
|
507
|
+
this.#sealed = true;
|
|
508
|
+
checkDeadline();
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
finish(): void {
|
|
513
|
+
checkDeadline();
|
|
514
|
+
if (this.#failed) return fail("fixed read-only projection rejected the private store");
|
|
515
|
+
if (!this.#sealed || this.#pages < 1) {
|
|
516
|
+
return fail("the fixed projection session omitted its seal");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const captureValidator = new SessionValidator();
|
|
522
|
+
if (admission !== undefined) beginBeeperMessageLikeMeHelperLaunch(admission);
|
|
523
|
+
let result: WhatsAppMessageExportSessionHelperResult | undefined;
|
|
524
|
+
let operationFailure: unknown;
|
|
525
|
+
try {
|
|
526
|
+
checkDeadline();
|
|
527
|
+
result = await runHelper(Object.freeze({
|
|
528
|
+
...invocationBase,
|
|
529
|
+
stdin: `${canonicalJson({
|
|
530
|
+
operation: "message-like-me.export-session",
|
|
531
|
+
request: initialRequest,
|
|
532
|
+
})}\n`,
|
|
533
|
+
maxOutputBytes: WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
|
|
534
|
+
onCanonicalFrame: (frame) => { captureValidator.accept(frame); },
|
|
535
|
+
...(admission === undefined
|
|
536
|
+
? {}
|
|
537
|
+
: { onSpawned: (pid: number) => bindBeeperMessageLikeMeHelperOwner(admission, pid) }),
|
|
538
|
+
}));
|
|
539
|
+
checkDeadline();
|
|
540
|
+
if (result.exitCode !== 0 || result.stderr.length !== 0) {
|
|
541
|
+
return fail("fixed read-only projection session failed before reviewed output");
|
|
542
|
+
}
|
|
543
|
+
captureValidator.finish();
|
|
544
|
+
const replayValidator = new SessionValidator();
|
|
545
|
+
for await (const page of result.spool.replay(
|
|
546
|
+
(frame) => replayValidator.accept(frame),
|
|
547
|
+
)) {
|
|
548
|
+
if (page !== null) yield page;
|
|
549
|
+
checkDeadline();
|
|
550
|
+
}
|
|
551
|
+
replayValidator.finish();
|
|
552
|
+
} catch (error) {
|
|
553
|
+
operationFailure = error;
|
|
554
|
+
} finally {
|
|
555
|
+
let cleanupFailure: unknown;
|
|
556
|
+
try {
|
|
557
|
+
await result?.spool.close();
|
|
558
|
+
} catch (error) {
|
|
559
|
+
cleanupFailure = error;
|
|
560
|
+
}
|
|
561
|
+
const combinedFailure = operationFailure === undefined
|
|
562
|
+
? cleanupFailure
|
|
563
|
+
: cleanupFailure === undefined
|
|
564
|
+
? operationFailure
|
|
565
|
+
: new AggregateError(
|
|
566
|
+
[operationFailure, cleanupFailure],
|
|
567
|
+
"WhatsApp projection session operation and spool cleanup both failed",
|
|
568
|
+
);
|
|
569
|
+
if (admission !== undefined) {
|
|
570
|
+
try {
|
|
571
|
+
if (
|
|
572
|
+
combinedFailure !== undefined
|
|
573
|
+
&& containsWhatsAppContactProjectionCleanupUnverified(combinedFailure)
|
|
574
|
+
) {
|
|
575
|
+
markBeeperMessageLikeMeHelperCleanupUnsafe(admission);
|
|
576
|
+
} else {
|
|
577
|
+
settleBeeperMessageLikeMeHelper(admission);
|
|
578
|
+
}
|
|
579
|
+
} catch (lifecycleError) {
|
|
580
|
+
if (combinedFailure !== undefined) {
|
|
581
|
+
throw new AggregateError(
|
|
582
|
+
[combinedFailure, lifecycleError],
|
|
583
|
+
"WhatsApp helper lifecycle failed closed",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
throw lifecycleError;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (cleanupFailure !== undefined) {
|
|
590
|
+
if (operationFailure !== undefined) {
|
|
591
|
+
throw new AggregateError(
|
|
592
|
+
[operationFailure, cleanupFailure],
|
|
593
|
+
"WhatsApp projection session operation and spool cleanup both failed",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
throw cleanupFailure;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (operationFailure !== undefined) throw operationFailure;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function createWhatsAppMessageLikeMeSource(
|
|
603
|
+
request: WhatsAppMessageLikeMeSourceRequest,
|
|
604
|
+
): WhatsAppMessageLikeMeExportSource {
|
|
605
|
+
const auth = requireAuth(request.auth);
|
|
606
|
+
const accountJid = canonicalWhatsAppAccountSubjectJid(auth.subject);
|
|
607
|
+
const accountId = localId("account", accountJid);
|
|
608
|
+
const selfParticipantId = participantId(accountJid, accountJid);
|
|
609
|
+
let consumed = false;
|
|
610
|
+
let completion: WhatsAppMessageBundleV2Completion | undefined;
|
|
611
|
+
|
|
612
|
+
const records = (async function* (): AsyncGenerator<WhatsAppMessageBundleV2Record> {
|
|
613
|
+
if (consumed) return fail("record stream is single-use");
|
|
614
|
+
consumed = true;
|
|
615
|
+
throwIfAborted(request.signal);
|
|
616
|
+
request.onProgress?.(Object.freeze({ phase: "preparing" }));
|
|
617
|
+
const observedAt = new Date().toISOString();
|
|
618
|
+
const monotonicNow = request.dependencies?.monotonicNow
|
|
619
|
+
?? (() => performance.now());
|
|
620
|
+
const exportStartedAt = monotonicNow();
|
|
621
|
+
if (!Number.isFinite(exportStartedAt) || exportStartedAt < 0) {
|
|
622
|
+
return fail("the total export clock is unavailable");
|
|
623
|
+
}
|
|
624
|
+
const checkExportDeadline = (): void => {
|
|
625
|
+
throwIfAborted(request.signal);
|
|
626
|
+
remainingExportTimeout(exportStartedAt, request.dependencies, false);
|
|
627
|
+
};
|
|
628
|
+
const recordLimit = request.dependencies?.recordLimit
|
|
629
|
+
?? WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.records;
|
|
630
|
+
if (
|
|
631
|
+
!Number.isSafeInteger(recordLimit)
|
|
632
|
+
|| recordLimit < 1
|
|
633
|
+
|| recordLimit > WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.records
|
|
634
|
+
) return fail("the v2 record bound is invalid");
|
|
635
|
+
const store = await validateWhatsAppStoreDirectory(auth.path, "projection");
|
|
636
|
+
checkExportDeadline();
|
|
637
|
+
const identities = await boundIdentities(store);
|
|
638
|
+
checkExportDeadline();
|
|
639
|
+
const fixed = fixedHelperFiles(request.dependencies);
|
|
640
|
+
const run = request.dependencies?.runHelper;
|
|
641
|
+
const participants = new Map<string, ParticipantFact>();
|
|
642
|
+
participants.set(accountJid, { jid: accountJid, displayName: null, isSelf: true });
|
|
643
|
+
const conversations = new Map<string, ConversationFact>();
|
|
644
|
+
let cursor = "0";
|
|
645
|
+
let cursorAnchor: string | null = null;
|
|
646
|
+
let expectedGeneration: WhatsAppMessageExportProjectionGeneration | null = null;
|
|
647
|
+
let page = 0;
|
|
648
|
+
let messageRows = 0;
|
|
649
|
+
let messageRecords = 0;
|
|
650
|
+
let observedFrom: string | null = null;
|
|
651
|
+
let observedThrough: string | null = null;
|
|
652
|
+
let unprovenReactionStateRows = 0;
|
|
653
|
+
let excludedSelfChatRows = 0;
|
|
654
|
+
let payloadPurgedRows = 0;
|
|
655
|
+
let nonConversationChatsExcluded: boolean | undefined;
|
|
656
|
+
let accountJidAliases: ReadonlySet<string> | null = null;
|
|
657
|
+
|
|
658
|
+
yield Object.freeze({
|
|
659
|
+
schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
660
|
+
kind: "account",
|
|
661
|
+
id: accountId,
|
|
662
|
+
accountId,
|
|
663
|
+
network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
|
|
664
|
+
provenance: provenance(accountJid, accountJid, observedAt, null),
|
|
665
|
+
displayName: null,
|
|
666
|
+
handle: userHandle(accountJid),
|
|
667
|
+
selfParticipantId,
|
|
668
|
+
});
|
|
669
|
+
checkExportDeadline();
|
|
670
|
+
|
|
671
|
+
let sessionIterator: AsyncIterator<unknown> | undefined;
|
|
672
|
+
try {
|
|
673
|
+
for (;;) {
|
|
674
|
+
checkExportDeadline();
|
|
675
|
+
page += 1;
|
|
676
|
+
if (page > 1_000) return fail("the fixed projection session exceeded its page bound");
|
|
677
|
+
request.onProgress?.(Object.freeze({ phase: "page-started", page, messages: messageRows }));
|
|
678
|
+
const projectionRequest = parseWhatsAppMessageExportProjectionRequest({
|
|
679
|
+
schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
|
|
680
|
+
operation: "message-like-me.export",
|
|
681
|
+
accountSubject: auth.subject,
|
|
682
|
+
cursor,
|
|
683
|
+
cursorAnchor,
|
|
684
|
+
limit: PAGE_LIMIT,
|
|
685
|
+
expectedGeneration,
|
|
686
|
+
storeIdentity: identities.store,
|
|
687
|
+
sessionIdentity: identities.session,
|
|
688
|
+
messageStoreIdentity: identities.message,
|
|
689
|
+
}) satisfies WhatsAppMessageExportProjectionRequest;
|
|
690
|
+
const invocation = Object.freeze({
|
|
691
|
+
command: Object.freeze([
|
|
692
|
+
process.execPath,
|
|
693
|
+
"--no-env-file",
|
|
694
|
+
"--no-install",
|
|
695
|
+
"--no-macros",
|
|
696
|
+
"--no-addons",
|
|
697
|
+
`--config=${fixed.config}`,
|
|
698
|
+
fixed.helper,
|
|
699
|
+
]),
|
|
700
|
+
cwd: store,
|
|
701
|
+
environment: Object.freeze({
|
|
702
|
+
PATH: "/usr/bin:/bin",
|
|
703
|
+
LANG: "C.UTF-8",
|
|
704
|
+
LC_ALL: "C.UTF-8",
|
|
705
|
+
TZ: "UTC",
|
|
706
|
+
}),
|
|
707
|
+
stdin: `${JSON.stringify(projectionRequest)}\n`,
|
|
708
|
+
timeoutMs: remainingExportTimeout(
|
|
709
|
+
exportStartedAt,
|
|
710
|
+
request.dependencies,
|
|
711
|
+
run !== undefined,
|
|
712
|
+
),
|
|
713
|
+
maxOutputBytes: WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
|
|
714
|
+
maxStderrBytes: MAX_STDERR_BYTES,
|
|
715
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
716
|
+
}) satisfies WhatsAppContactProjectionHelperInvocation;
|
|
717
|
+
let response: ReturnType<typeof parseWhatsAppMessageExportProjectionResponse>;
|
|
718
|
+
if (run !== undefined) {
|
|
719
|
+
const result = await run(invocation);
|
|
720
|
+
throwIfAborted(request.signal);
|
|
721
|
+
remainingExportTimeout(exportStartedAt, request.dependencies, true);
|
|
722
|
+
if (result.exitCode !== 0 || result.stderr.length !== 0) {
|
|
723
|
+
return fail("fixed read-only projection helper failed before reviewed output");
|
|
724
|
+
}
|
|
725
|
+
let decoded: unknown;
|
|
726
|
+
try {
|
|
727
|
+
decoded = JSON.parse(result.stdout.trim()) as unknown;
|
|
728
|
+
} catch {
|
|
729
|
+
return fail("fixed read-only projection helper returned malformed output");
|
|
730
|
+
}
|
|
731
|
+
response = parseWhatsAppMessageExportProjectionResponse(decoded, projectionRequest);
|
|
732
|
+
} else {
|
|
733
|
+
sessionIterator ??= runMessageExportSession(
|
|
734
|
+
projectionRequest,
|
|
735
|
+
Object.freeze({
|
|
736
|
+
command: invocation.command,
|
|
737
|
+
cwd: invocation.cwd,
|
|
738
|
+
environment: invocation.environment,
|
|
739
|
+
timeoutMs: invocation.timeoutMs,
|
|
740
|
+
maxStderrBytes: invocation.maxStderrBytes,
|
|
741
|
+
...(invocation.signal === undefined ? {} : { signal: invocation.signal }),
|
|
742
|
+
}),
|
|
743
|
+
request.dependencies?.runSessionHelper
|
|
744
|
+
?? runWhatsAppMessageExportSessionHelperChild,
|
|
745
|
+
request.admission,
|
|
746
|
+
checkExportDeadline,
|
|
747
|
+
)[Symbol.asyncIterator]();
|
|
748
|
+
const pageResult = await sessionIterator.next();
|
|
749
|
+
if (pageResult.done) {
|
|
750
|
+
return fail("fixed read-only projection session omitted a page");
|
|
751
|
+
}
|
|
752
|
+
if (
|
|
753
|
+
typeof pageResult.value !== "object"
|
|
754
|
+
|| pageResult.value === null
|
|
755
|
+
|| Array.isArray(pageResult.value)
|
|
756
|
+
|| Object.keys(pageResult.value).sort().join("\0") !== "response\0selfChatsExcluded"
|
|
757
|
+
|| !("response" in pageResult.value)
|
|
758
|
+
|| !("selfChatsExcluded" in pageResult.value)
|
|
759
|
+
|| (pageResult.value.selfChatsExcluded !== "none-detected"
|
|
760
|
+
&& pageResult.value.selfChatsExcluded !== "present-excluded")
|
|
761
|
+
) return fail("fixed read-only projection session returned an unsupported page");
|
|
762
|
+
if (pageResult.value.selfChatsExcluded === "present-excluded") {
|
|
763
|
+
excludedSelfChatRows = Math.max(1, excludedSelfChatRows);
|
|
764
|
+
}
|
|
765
|
+
// SessionValidator already produced the one final parsed page graph.
|
|
766
|
+
// Re-parsing it here would retain a second full page during conversion.
|
|
767
|
+
response = pageResult.value.response as ReturnType<
|
|
768
|
+
typeof parseWhatsAppMessageExportProjectionResponse
|
|
769
|
+
>;
|
|
770
|
+
}
|
|
771
|
+
if (response.status === "failed") {
|
|
772
|
+
return fail(`fixed read-only projection rejected the store (${response.errorCode})`);
|
|
773
|
+
}
|
|
774
|
+
if (
|
|
775
|
+
response.localInsertPageComplete
|
|
776
|
+
&& response.messages.length === 0
|
|
777
|
+
&& projectionRequest.cursor !== "0"
|
|
778
|
+
) return fail("the fixed projection contradicted its prior lookahead");
|
|
779
|
+
expectedGeneration = response.projectionGeneration;
|
|
780
|
+
if (nonConversationChatsExcluded === undefined) {
|
|
781
|
+
nonConversationChatsExcluded = response.nonConversationChatsExcluded;
|
|
782
|
+
} else if (nonConversationChatsExcluded !== response.nonConversationChatsExcluded) {
|
|
783
|
+
return fail("fixed projection non-conversation exclusion changed inside its snapshot");
|
|
784
|
+
}
|
|
785
|
+
const pageAliases = new Set([
|
|
786
|
+
response.accountJidAliases.pnJid,
|
|
787
|
+
response.accountJidAliases.lidJid,
|
|
788
|
+
].filter((jid): jid is string => jid !== null));
|
|
789
|
+
if (accountJidAliases === null) {
|
|
790
|
+
accountJidAliases = pageAliases;
|
|
791
|
+
} else if (
|
|
792
|
+
accountJidAliases.size !== pageAliases.size
|
|
793
|
+
|| [...accountJidAliases].some((jid) => !pageAliases.has(jid))
|
|
794
|
+
) {
|
|
795
|
+
return fail("bound account aliases changed inside the fixed snapshot");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
for (const item of response.messages) {
|
|
799
|
+
checkExportDeadline();
|
|
800
|
+
messageRows += 1;
|
|
801
|
+
if (messageRows > 500_000) {
|
|
802
|
+
return fail("the fixed projection session exceeded its message bound");
|
|
803
|
+
}
|
|
804
|
+
if (
|
|
805
|
+
item.chatKind === "dm"
|
|
806
|
+
&& accountJidAliases.has(canonicalWhatsAppParticipantJid(item.chatJid))
|
|
807
|
+
) {
|
|
808
|
+
excludedSelfChatRows += 1;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (item.reactionToMessageId !== null) {
|
|
812
|
+
// Wacli 0.15 may retain the prior emoji when the same reaction row is
|
|
813
|
+
// removed, so no projected reaction row proves current active state.
|
|
814
|
+
unprovenReactionStateRows += 1;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
observedFrom = earliest(observedFrom, item.timestamp);
|
|
818
|
+
observedThrough = latest(observedThrough, item.timestamp);
|
|
819
|
+
let conversation = conversations.get(item.chatJid);
|
|
820
|
+
if (conversation === undefined) {
|
|
821
|
+
conversation = {
|
|
822
|
+
jid: item.chatJid,
|
|
823
|
+
type: item.chatKind === "dm" ? "direct" : "group",
|
|
824
|
+
title: item.chatName,
|
|
825
|
+
participantJids: new Set([accountJid]),
|
|
826
|
+
startedAt: item.timestamp,
|
|
827
|
+
lastMessageAt: item.timestamp,
|
|
828
|
+
};
|
|
829
|
+
conversations.set(item.chatJid, conversation);
|
|
830
|
+
} else {
|
|
831
|
+
if (conversation.type !== (item.chatKind === "dm" ? "direct" : "group")) {
|
|
832
|
+
return fail("one chat changed kind inside the fixed snapshot");
|
|
833
|
+
}
|
|
834
|
+
conversation.title ??= item.chatName;
|
|
835
|
+
if (item.timestamp < conversation.startedAt) conversation.startedAt = item.timestamp;
|
|
836
|
+
if (item.timestamp > conversation.lastMessageAt) conversation.lastMessageAt = item.timestamp;
|
|
837
|
+
}
|
|
838
|
+
const directPeer = item.chatKind === "dm"
|
|
839
|
+
? canonicalWhatsAppParticipantJid(item.chatJid)
|
|
840
|
+
: null;
|
|
841
|
+
if (directPeer !== null) {
|
|
842
|
+
conversation.participantJids.add(directPeer);
|
|
843
|
+
if (!participants.has(directPeer)) {
|
|
844
|
+
participants.set(directPeer, { jid: directPeer, displayName: item.chatName, isSelf: false });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const resolvedSenderJid = senderJid(item, accountJid, accountJidAliases, directPeer);
|
|
848
|
+
if (resolvedSenderJid !== null) {
|
|
849
|
+
conversation.participantJids.add(resolvedSenderJid);
|
|
850
|
+
const current = participants.get(resolvedSenderJid);
|
|
851
|
+
if (current === undefined) {
|
|
852
|
+
participants.set(resolvedSenderJid, {
|
|
853
|
+
jid: resolvedSenderJid,
|
|
854
|
+
displayName: item.senderName,
|
|
855
|
+
isSelf: resolvedSenderJid === accountJid,
|
|
856
|
+
});
|
|
857
|
+
} else current.displayName ??= item.senderName;
|
|
858
|
+
}
|
|
859
|
+
if (
|
|
860
|
+
conversation.participantJids.size
|
|
861
|
+
> WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.participantsPerConversation
|
|
862
|
+
) return fail("one conversation exceeded its participant bound");
|
|
863
|
+
|
|
864
|
+
const providerId = messageProviderId(item);
|
|
865
|
+
const deletion = messageDeletion(item, observedAt);
|
|
866
|
+
const payloadPurged = item.payloadPurgedAt !== null;
|
|
867
|
+
const bodyTruncated = payloadPurged && deletion === null;
|
|
868
|
+
if (bodyTruncated) payloadPurgedRows += 1;
|
|
869
|
+
const edit = item.edited && item.editedAt !== null
|
|
870
|
+
? Object.freeze({
|
|
871
|
+
kind: "in-place" as const,
|
|
872
|
+
editedAt: item.editedAt,
|
|
873
|
+
providerRevision: item.editedAt,
|
|
874
|
+
})
|
|
875
|
+
: null;
|
|
876
|
+
messageRecords += 1;
|
|
877
|
+
if (
|
|
878
|
+
1 + messageRecords + participants.size + conversations.size
|
|
879
|
+
> recordLimit
|
|
880
|
+
) return fail("the fixed projection exceeded the v2 record bound");
|
|
881
|
+
const messageRecord = Object.freeze({
|
|
882
|
+
schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
883
|
+
kind: "message",
|
|
884
|
+
id: localId("message", accountJid, item.chatJid, item.messageId),
|
|
885
|
+
accountId,
|
|
886
|
+
network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
|
|
887
|
+
provenance: provenance(
|
|
888
|
+
providerId,
|
|
889
|
+
accountJid,
|
|
890
|
+
observedAt,
|
|
891
|
+
item.editedAt ?? item.deletedAt ?? item.payloadPurgedAt,
|
|
892
|
+
),
|
|
893
|
+
conversationId: localId("conversation", accountJid, item.chatJid),
|
|
894
|
+
senderParticipantId: resolvedSenderJid === null
|
|
895
|
+
? null
|
|
896
|
+
: participantId(accountJid, resolvedSenderJid),
|
|
897
|
+
direction: item.fromMe ? "outgoing" : "incoming",
|
|
898
|
+
sentAt: item.timestamp,
|
|
899
|
+
sortKey: item.rowid.padStart(19, "0"),
|
|
900
|
+
body: payloadPurged ? null : body(item),
|
|
901
|
+
bodyTruncated,
|
|
902
|
+
replyTo: item.quotedMessageId === null
|
|
903
|
+
? null
|
|
904
|
+
: Object.freeze({
|
|
905
|
+
messageId: null,
|
|
906
|
+
providerId: `${item.chatJid}/${item.quotedMessageId}`,
|
|
907
|
+
}),
|
|
908
|
+
edit,
|
|
909
|
+
deletion,
|
|
910
|
+
attachments: attachment(item),
|
|
911
|
+
});
|
|
912
|
+
checkExportDeadline();
|
|
913
|
+
yield messageRecord;
|
|
914
|
+
checkExportDeadline();
|
|
915
|
+
}
|
|
916
|
+
request.onProgress?.(Object.freeze({ phase: "page-completed", page, messages: messageRows }));
|
|
917
|
+
cursor = response.checkpoint.cursor;
|
|
918
|
+
cursorAnchor = response.checkpoint.anchor;
|
|
919
|
+
if (response.localInsertPageComplete) {
|
|
920
|
+
if (sessionIterator !== undefined) {
|
|
921
|
+
const sealed = await sessionIterator.next();
|
|
922
|
+
if (!sealed.done) return fail("fixed projection session emitted data after its terminal page");
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
if (response.nextCursor === null || response.nextCursor !== cursor) {
|
|
927
|
+
return fail("paged projection omitted its next cursor");
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} finally {
|
|
931
|
+
await sessionIterator?.return?.();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
for (const participant of [...participants.values()].sort((left, right) => left.jid.localeCompare(right.jid))) {
|
|
935
|
+
checkExportDeadline();
|
|
936
|
+
const participantRecord = Object.freeze({
|
|
937
|
+
schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
938
|
+
kind: "participant",
|
|
939
|
+
id: participantId(accountJid, participant.jid),
|
|
940
|
+
accountId,
|
|
941
|
+
network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
|
|
942
|
+
provenance: provenance(participant.jid, accountJid, observedAt, null),
|
|
943
|
+
displayName: participant.displayName,
|
|
944
|
+
handle: userHandle(participant.jid),
|
|
945
|
+
isSelf: participant.isSelf,
|
|
946
|
+
});
|
|
947
|
+
checkExportDeadline();
|
|
948
|
+
yield participantRecord;
|
|
949
|
+
checkExportDeadline();
|
|
950
|
+
}
|
|
951
|
+
for (const conversation of [...conversations.values()].sort((left, right) => left.jid.localeCompare(right.jid))) {
|
|
952
|
+
checkExportDeadline();
|
|
953
|
+
const participantIds = [...conversation.participantJids]
|
|
954
|
+
.sort()
|
|
955
|
+
.map((jid) => participantId(accountJid, jid));
|
|
956
|
+
const record: WhatsAppMessageBundleV2Conversation = Object.freeze({
|
|
957
|
+
schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
958
|
+
kind: "conversation",
|
|
959
|
+
id: localId("conversation", accountJid, conversation.jid),
|
|
960
|
+
accountId,
|
|
961
|
+
network: WHATSAPP_MESSAGE_BUNDLE_V2_NETWORK,
|
|
962
|
+
provenance: provenance(conversation.jid, accountJid, observedAt, null),
|
|
963
|
+
type: conversation.type,
|
|
964
|
+
title: conversation.title,
|
|
965
|
+
participantIds: Object.freeze(participantIds),
|
|
966
|
+
participantsComplete: conversation.type === "direct",
|
|
967
|
+
startedAt: conversation.startedAt,
|
|
968
|
+
lastMessageAt: conversation.lastMessageAt,
|
|
969
|
+
});
|
|
970
|
+
checkExportDeadline();
|
|
971
|
+
yield record;
|
|
972
|
+
checkExportDeadline();
|
|
973
|
+
}
|
|
974
|
+
if (
|
|
975
|
+
1 + messageRecords + participants.size + conversations.size
|
|
976
|
+
> recordLimit
|
|
977
|
+
) return fail("the fixed projection exceeded the v2 record bound");
|
|
978
|
+
const warnings = Object.freeze([
|
|
979
|
+
"remote-history-incomplete",
|
|
980
|
+
...(unprovenReactionStateRows > 0 ? ["reaction-state-unproven"] : []),
|
|
981
|
+
...(excludedSelfChatRows > 0 ? ["self-chat-excluded"] : []),
|
|
982
|
+
...(payloadPurgedRows > 0 ? ["message-payload-purged"] : []),
|
|
983
|
+
...(nonConversationChatsExcluded === true ? ["non-conversation-chats-excluded"] : []),
|
|
984
|
+
]);
|
|
985
|
+
const completed = Object.freeze({
|
|
986
|
+
completeness: Object.freeze({
|
|
987
|
+
kind: "bounded-local",
|
|
988
|
+
reason: "local-store-coverage-unknown",
|
|
989
|
+
observedFrom,
|
|
990
|
+
observedThrough,
|
|
991
|
+
}),
|
|
992
|
+
warnings,
|
|
993
|
+
});
|
|
994
|
+
request.onProgress?.(Object.freeze({
|
|
995
|
+
phase: "conversion-completed",
|
|
996
|
+
messages: messageRows,
|
|
997
|
+
conversations: conversations.size,
|
|
998
|
+
participants: participants.size,
|
|
999
|
+
}));
|
|
1000
|
+
checkExportDeadline();
|
|
1001
|
+
completion = completed;
|
|
1002
|
+
})();
|
|
1003
|
+
|
|
1004
|
+
return Object.freeze({
|
|
1005
|
+
descriptor: Object.freeze({
|
|
1006
|
+
source: WHATSAPP_MESSAGE_BUNDLE_V2_SOURCE,
|
|
1007
|
+
provider: WHATSAPP_MESSAGE_BUNDLE_V2_PROVIDER,
|
|
1008
|
+
}),
|
|
1009
|
+
records,
|
|
1010
|
+
completion: async () => completion ?? fail("record stream did not complete"),
|
|
1011
|
+
});
|
|
1012
|
+
}
|