@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,1452 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constants as fsConstants,
|
|
3
|
+
closeSync,
|
|
4
|
+
fstatSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
openSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeSync,
|
|
9
|
+
type BigIntStats,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import { Database, constants as sqliteConstants } from "bun:sqlite";
|
|
14
|
+
import { canonicalJson } from "../canonical-json";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
canonicalWhatsAppParticipantJid,
|
|
18
|
+
} from "./whatsapp-account-identity";
|
|
19
|
+
import {
|
|
20
|
+
WHATSAPP_CONTACT_PROJECTION_PROTOCOL_VERSION,
|
|
21
|
+
isExactWhatsAppContactProjectionMode,
|
|
22
|
+
} from "./whatsapp-contact-projection-protocol";
|
|
23
|
+
import {
|
|
24
|
+
WHATSAPP_MATCHED_OWNER_IDENTITY,
|
|
25
|
+
projectWhatsAppContactsFromBoundCwd,
|
|
26
|
+
type WhatsAppMatchedOwnerIdentity,
|
|
27
|
+
} from "./whatsapp-contact-projection-helper";
|
|
28
|
+
import {
|
|
29
|
+
WHATSAPP_INTERACTION_PROJECTION_MAX_STDIN_BYTES,
|
|
30
|
+
WHATSAPP_INTERACTION_PROJECTION_MAX_STDOUT_BYTES,
|
|
31
|
+
WHATSAPP_INTERACTION_PROJECTION_PROTOCOL_VERSION,
|
|
32
|
+
WHATSAPP_INTERACTION_PROJECTION_SCHEMA_FINGERPRINT,
|
|
33
|
+
createWhatsAppInteractionProjectionFailure,
|
|
34
|
+
parseWhatsAppInteractionProjectionRequest,
|
|
35
|
+
type WhatsAppInteractionProjectionErrorCode,
|
|
36
|
+
type WhatsAppInteractionProjectionItem,
|
|
37
|
+
type WhatsAppInteractionProjectionRequest,
|
|
38
|
+
type WhatsAppInteractionProjectionSuccess,
|
|
39
|
+
} from "./whatsapp-interaction-projection-protocol";
|
|
40
|
+
import {
|
|
41
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDIN_BYTES,
|
|
42
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
|
|
43
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
|
|
44
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_SCHEMA_FINGERPRINT,
|
|
45
|
+
createWhatsAppMessageExportProjectionFailure,
|
|
46
|
+
parseWhatsAppMessageExportProjectionRequest,
|
|
47
|
+
type WhatsAppMessageExportProjectionItem,
|
|
48
|
+
type WhatsAppMessageExportProjectionRequest,
|
|
49
|
+
type WhatsAppMessageExportProjectionSuccess,
|
|
50
|
+
} from "./whatsapp-message-export-projection-protocol";
|
|
51
|
+
|
|
52
|
+
const MESSAGE_DATABASE_NAME = "wacli.db";
|
|
53
|
+
const SESSION_DATABASE_NAME = "session.db";
|
|
54
|
+
const IMMUTABLE_MESSAGE_DATABASE_URI = `file:${MESSAGE_DATABASE_NAME}?mode=ro&immutable=1`;
|
|
55
|
+
const IMMUTABLE_MESSAGE_DATABASE_FLAGS = sqliteConstants.SQLITE_OPEN_READONLY
|
|
56
|
+
| sqliteConstants.SQLITE_OPEN_URI
|
|
57
|
+
| sqliteConstants.SQLITE_OPEN_NOFOLLOW
|
|
58
|
+
| sqliteConstants.SQLITE_OPEN_PRIVATECACHE
|
|
59
|
+
| sqliteConstants.SQLITE_OPEN_EXRESCODE;
|
|
60
|
+
const MAX_MESSAGE_DATABASE_BYTES = 2n * 1024n * 1024n * 1024n;
|
|
61
|
+
const SQLITE_CACHE_KIB = 4_096;
|
|
62
|
+
const MAX_UNIX_SECONDS = 253_402_300_799n;
|
|
63
|
+
const SYSTEM_SENTINEL_JID = "0@s.whatsapp.net";
|
|
64
|
+
const MESSAGE_EXPORT_SESSION_MAX_MESSAGES = 500_000;
|
|
65
|
+
type MessageExportSessionSelfChatsExcluded = "none-detected" | "present-excluded";
|
|
66
|
+
|
|
67
|
+
type BoundMessageStoreRequest = Pick<
|
|
68
|
+
WhatsAppInteractionProjectionRequest,
|
|
69
|
+
"accountSubject" | "storeIdentity" | "sessionIdentity" | "messageStoreIdentity"
|
|
70
|
+
>;
|
|
71
|
+
|
|
72
|
+
class HelperFailure extends Error {
|
|
73
|
+
readonly code: WhatsAppInteractionProjectionErrorCode;
|
|
74
|
+
|
|
75
|
+
constructor(code: WhatsAppInteractionProjectionErrorCode) {
|
|
76
|
+
super("WhatsApp interaction projection helper failed");
|
|
77
|
+
this.name = "WhatsAppInteractionProjectionHelperFailure";
|
|
78
|
+
this.code = code;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fail(code: WhatsAppInteractionProjectionErrorCode): never {
|
|
83
|
+
throw new HelperFailure(code);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function errorCode(error: unknown): WhatsAppInteractionProjectionErrorCode {
|
|
87
|
+
return error instanceof HelperFailure ? error.code : "database-invalid";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isNoEntry(error: unknown): boolean {
|
|
91
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function currentUid(): bigint | null {
|
|
95
|
+
return typeof process.getuid === "function" ? BigInt(process.getuid()) : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function sameSnapshot(left: BigIntStats, right: BigIntStats): boolean {
|
|
99
|
+
return left.dev === right.dev
|
|
100
|
+
&& left.ino === right.ino
|
|
101
|
+
&& left.mode === right.mode
|
|
102
|
+
&& left.uid === right.uid
|
|
103
|
+
&& left.gid === right.gid
|
|
104
|
+
&& left.nlink === right.nlink
|
|
105
|
+
&& left.size === right.size
|
|
106
|
+
&& left.mtimeNs === right.mtimeNs
|
|
107
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function matchesIdentity(
|
|
111
|
+
stats: BigIntStats,
|
|
112
|
+
identity: BoundMessageStoreRequest["messageStoreIdentity"],
|
|
113
|
+
): boolean {
|
|
114
|
+
return stats.dev.toString() === identity.dev && stats.ino.toString() === identity.ino;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertBoundCwd(request: BoundMessageStoreRequest, initial?: BigIntStats): BigIntStats {
|
|
118
|
+
let cwd: string;
|
|
119
|
+
let pathStats: BigIntStats;
|
|
120
|
+
let dotStats: BigIntStats;
|
|
121
|
+
try {
|
|
122
|
+
cwd = process.cwd();
|
|
123
|
+
pathStats = lstatSync(cwd, { bigint: true });
|
|
124
|
+
dotStats = lstatSync(".", { bigint: true });
|
|
125
|
+
if (realpathSync(".") !== cwd) fail("store-binding-invalid");
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof HelperFailure) throw error;
|
|
128
|
+
return fail("store-binding-invalid");
|
|
129
|
+
}
|
|
130
|
+
const uid = currentUid();
|
|
131
|
+
if (
|
|
132
|
+
!pathStats.isDirectory()
|
|
133
|
+
|| !dotStats.isDirectory()
|
|
134
|
+
|| pathStats.isSymbolicLink()
|
|
135
|
+
|| (uid !== null && (pathStats.uid !== uid || dotStats.uid !== uid))
|
|
136
|
+
|| !isExactWhatsAppContactProjectionMode(pathStats.mode, 0o700)
|
|
137
|
+
|| !sameSnapshot(pathStats, dotStats)
|
|
138
|
+
|| !matchesIdentity(pathStats, request.storeIdentity)
|
|
139
|
+
|| (initial !== undefined && !sameSnapshot(initial, pathStats))
|
|
140
|
+
) fail("store-binding-invalid");
|
|
141
|
+
return pathStats;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertNoMessageSidecars(): void {
|
|
145
|
+
for (const suffix of ["-journal", "-wal", "-shm"] as const) {
|
|
146
|
+
try {
|
|
147
|
+
lstatSync(`${MESSAGE_DATABASE_NAME}${suffix}`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (isNoEntry(error)) continue;
|
|
150
|
+
return fail("message-store-sidecar-state-unverified");
|
|
151
|
+
}
|
|
152
|
+
fail("message-store-sidecar-present");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertMessageFile(
|
|
157
|
+
stats: BigIntStats,
|
|
158
|
+
request: BoundMessageStoreRequest,
|
|
159
|
+
): void {
|
|
160
|
+
const uid = currentUid();
|
|
161
|
+
if (
|
|
162
|
+
!stats.isFile()
|
|
163
|
+
|| stats.isSymbolicLink()
|
|
164
|
+
|| stats.nlink !== 1n
|
|
165
|
+
|| (uid !== null && stats.uid !== uid)
|
|
166
|
+
|| !isExactWhatsAppContactProjectionMode(stats.mode, 0o600)
|
|
167
|
+
|| !matchesIdentity(stats, request.messageStoreIdentity)
|
|
168
|
+
) fail("message-store-file-invalid");
|
|
169
|
+
if (stats.size < 1n || stats.size > MAX_MESSAGE_DATABASE_BYTES) {
|
|
170
|
+
fail("message-store-file-too-large");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function assertSessionOwnerFile(
|
|
175
|
+
stats: BigIntStats,
|
|
176
|
+
request: BoundMessageStoreRequest,
|
|
177
|
+
): void {
|
|
178
|
+
const uid = currentUid();
|
|
179
|
+
if (
|
|
180
|
+
!stats.isFile()
|
|
181
|
+
|| stats.isSymbolicLink()
|
|
182
|
+
|| stats.nlink !== 1n
|
|
183
|
+
|| (uid !== null && stats.uid !== uid)
|
|
184
|
+
|| !isExactWhatsAppContactProjectionMode(stats.mode, 0o600)
|
|
185
|
+
|| stats.dev.toString() !== request.sessionIdentity.dev
|
|
186
|
+
|| stats.ino.toString() !== request.sessionIdentity.ino
|
|
187
|
+
) fail("session-binding-invalid");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function exactSessionOwnerPathSnapshot(
|
|
191
|
+
initial: BigIntStats,
|
|
192
|
+
request: BoundMessageStoreRequest,
|
|
193
|
+
): void {
|
|
194
|
+
let pathStats: BigIntStats;
|
|
195
|
+
try {
|
|
196
|
+
pathStats = lstatSync(SESSION_DATABASE_NAME, { bigint: true });
|
|
197
|
+
} catch {
|
|
198
|
+
return fail("session-binding-invalid");
|
|
199
|
+
}
|
|
200
|
+
assertSessionOwnerFile(pathStats, request);
|
|
201
|
+
if (!sameSnapshot(initial, pathStats)) fail("session-binding-invalid");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function exactMessagePathSnapshot(
|
|
205
|
+
initial: BigIntStats,
|
|
206
|
+
request: BoundMessageStoreRequest,
|
|
207
|
+
): void {
|
|
208
|
+
let pathStats: BigIntStats;
|
|
209
|
+
try {
|
|
210
|
+
pathStats = lstatSync(MESSAGE_DATABASE_NAME, { bigint: true });
|
|
211
|
+
} catch {
|
|
212
|
+
return fail("message-store-file-invalid");
|
|
213
|
+
}
|
|
214
|
+
assertMessageFile(pathStats, request);
|
|
215
|
+
if (!sameSnapshot(initial, pathStats)) fail("message-store-file-invalid");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type SqliteRow = Readonly<Record<string, unknown>>;
|
|
219
|
+
|
|
220
|
+
function rows(
|
|
221
|
+
value: Iterable<unknown>,
|
|
222
|
+
maximum: number,
|
|
223
|
+
failureCode: WhatsAppInteractionProjectionErrorCode = "schema-mismatch",
|
|
224
|
+
): readonly SqliteRow[] {
|
|
225
|
+
const result: SqliteRow[] = [];
|
|
226
|
+
for (const item of value) {
|
|
227
|
+
if (result.length >= maximum) fail(failureCode);
|
|
228
|
+
if (
|
|
229
|
+
typeof item !== "object"
|
|
230
|
+
|| item === null
|
|
231
|
+
|| Array.isArray(item)
|
|
232
|
+
|| Object.getPrototypeOf(item) !== Object.prototype
|
|
233
|
+
) fail(failureCode);
|
|
234
|
+
result.push(item as SqliteRow);
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function exactRowKeys(row: SqliteRow, expected: readonly string[]): void {
|
|
240
|
+
const actual = Object.keys(row).sort();
|
|
241
|
+
const wanted = [...expected].sort();
|
|
242
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
243
|
+
fail("schema-mismatch");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function pragmaInteger(value: unknown, expected: bigint): boolean {
|
|
248
|
+
return value === expected || value === Number(expected);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function singlePragmaInteger(database: Database, sql: string): bigint {
|
|
252
|
+
const result = rows(database.query(sql).iterate(), 1, "database-invalid");
|
|
253
|
+
if (result.length !== 1 || Object.values(result[0]!).length !== 1) fail("database-invalid");
|
|
254
|
+
const value = Object.values(result[0]!)[0];
|
|
255
|
+
if (typeof value === "bigint") return value;
|
|
256
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
|
|
257
|
+
return fail("database-invalid");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function configureDatabase(database: Database): void {
|
|
261
|
+
try {
|
|
262
|
+
database.exec(`
|
|
263
|
+
PRAGMA query_only = ON;
|
|
264
|
+
PRAGMA trusted_schema = OFF;
|
|
265
|
+
PRAGMA foreign_keys = ON;
|
|
266
|
+
PRAGMA temp_store = MEMORY;
|
|
267
|
+
PRAGMA cell_size_check = ON;
|
|
268
|
+
PRAGMA recursive_triggers = OFF;
|
|
269
|
+
PRAGMA writable_schema = OFF;
|
|
270
|
+
PRAGMA ignore_check_constraints = OFF;
|
|
271
|
+
PRAGMA automatic_index = OFF;
|
|
272
|
+
PRAGMA mmap_size = 0;
|
|
273
|
+
PRAGMA cache_size = -${SQLITE_CACHE_KIB};
|
|
274
|
+
`);
|
|
275
|
+
for (const [sql, expected] of [
|
|
276
|
+
["PRAGMA query_only", 1n],
|
|
277
|
+
["PRAGMA trusted_schema", 0n],
|
|
278
|
+
["PRAGMA foreign_keys", 1n],
|
|
279
|
+
["PRAGMA temp_store", 2n],
|
|
280
|
+
["PRAGMA cell_size_check", 1n],
|
|
281
|
+
["PRAGMA recursive_triggers", 0n],
|
|
282
|
+
["PRAGMA writable_schema", 0n],
|
|
283
|
+
["PRAGMA ignore_check_constraints", 0n],
|
|
284
|
+
["PRAGMA automatic_index", 0n],
|
|
285
|
+
["PRAGMA cache_size", BigInt(-SQLITE_CACHE_KIB)],
|
|
286
|
+
] as const) {
|
|
287
|
+
if (singlePragmaInteger(database, sql) !== expected) fail("database-invalid");
|
|
288
|
+
}
|
|
289
|
+
const mmap = rows(database.query("PRAGMA mmap_size").iterate(), 1, "database-invalid");
|
|
290
|
+
if (mmap.length === 1 && !pragmaInteger(Object.values(mmap[0]!)[0], 0n)) {
|
|
291
|
+
fail("database-invalid");
|
|
292
|
+
}
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error instanceof HelperFailure) throw error;
|
|
295
|
+
fail("database-invalid");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function assertIntegrity(database: Database): void {
|
|
300
|
+
try {
|
|
301
|
+
const integrity = rows(
|
|
302
|
+
database.query("PRAGMA integrity_check(1)").iterate(),
|
|
303
|
+
1,
|
|
304
|
+
"database-integrity-failed",
|
|
305
|
+
);
|
|
306
|
+
if (
|
|
307
|
+
integrity.length !== 1
|
|
308
|
+
|| Object.values(integrity[0]!).length !== 1
|
|
309
|
+
|| Object.values(integrity[0]!)[0] !== "ok"
|
|
310
|
+
|| rows(
|
|
311
|
+
database.query("PRAGMA foreign_key_check").iterate(),
|
|
312
|
+
1,
|
|
313
|
+
"database-integrity-failed",
|
|
314
|
+
).length !== 0
|
|
315
|
+
) fail("database-integrity-failed");
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof HelperFailure) throw error;
|
|
318
|
+
fail("database-integrity-failed");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const MESSAGE_COLUMNS = Object.freeze([
|
|
323
|
+
["rowid", "INTEGER", 0n, null, 1n], ["chat_jid", "TEXT", 1n, null, 0n],
|
|
324
|
+
["chat_name", "TEXT", 0n, null, 0n], ["msg_id", "TEXT", 1n, null, 0n],
|
|
325
|
+
["sender_jid", "TEXT", 0n, null, 0n], ["sender_name", "TEXT", 0n, null, 0n],
|
|
326
|
+
["ts", "INTEGER", 1n, null, 0n], ["from_me", "INTEGER", 1n, null, 0n],
|
|
327
|
+
["text", "TEXT", 0n, null, 0n], ["display_text", "TEXT", 0n, null, 0n],
|
|
328
|
+
["quoted_msg_id", "TEXT", 0n, null, 0n], ["quoted_sender_jid", "TEXT", 0n, null, 0n],
|
|
329
|
+
["is_forwarded", "INTEGER", 1n, "0", 0n], ["forwarding_score", "INTEGER", 1n, "0", 0n],
|
|
330
|
+
["reaction_to_id", "TEXT", 0n, null, 0n], ["reaction_emoji", "TEXT", 0n, null, 0n],
|
|
331
|
+
["media_type", "TEXT", 0n, null, 0n], ["media_caption", "TEXT", 0n, null, 0n],
|
|
332
|
+
["filename", "TEXT", 0n, null, 0n], ["mime_type", "TEXT", 0n, null, 0n],
|
|
333
|
+
["direct_path", "TEXT", 0n, null, 0n], ["media_key", "BLOB", 0n, null, 0n],
|
|
334
|
+
["file_sha256", "BLOB", 0n, null, 0n], ["file_enc_sha256", "BLOB", 0n, null, 0n],
|
|
335
|
+
["file_length", "INTEGER", 0n, null, 0n], ["local_path", "TEXT", 0n, null, 0n],
|
|
336
|
+
["downloaded_at", "INTEGER", 0n, null, 0n], ["media_unavailable_at", "INTEGER", 0n, null, 0n],
|
|
337
|
+
["revoked", "INTEGER", 1n, "0", 0n], ["deleted_for_me", "INTEGER", 1n, "0", 0n],
|
|
338
|
+
["deleted_at", "INTEGER", 0n, null, 0n], ["deletion_reason", "TEXT", 0n, null, 0n],
|
|
339
|
+
["payload_purged_at", "INTEGER", 0n, null, 0n],
|
|
340
|
+
["edited", "INTEGER", 1n, "0", 0n], ["edited_ts", "INTEGER", 1n, "0", 0n],
|
|
341
|
+
["buttons", "TEXT", 0n, null, 0n],
|
|
342
|
+
] as const);
|
|
343
|
+
|
|
344
|
+
const CHAT_COLUMNS = Object.freeze([
|
|
345
|
+
["jid", "TEXT", 0n, null, 1n], ["kind", "TEXT", 1n, null, 0n],
|
|
346
|
+
["name", "TEXT", 0n, null, 0n], ["last_message_ts", "INTEGER", 0n, null, 0n],
|
|
347
|
+
["archived", "INTEGER", 1n, "0", 0n], ["pinned", "INTEGER", 1n, "0", 0n],
|
|
348
|
+
["muted_until", "INTEGER", 1n, "0", 0n], ["unread", "INTEGER", 1n, "0", 0n],
|
|
349
|
+
["unread_count", "INTEGER", 1n, "0", 0n],
|
|
350
|
+
] as const);
|
|
351
|
+
|
|
352
|
+
function assertColumns(database: Database, table: "messages" | "chats", expected: typeof MESSAGE_COLUMNS | typeof CHAT_COLUMNS): void {
|
|
353
|
+
const actual = rows(database.query(`PRAGMA table_xinfo('${table}')`).iterate(), expected.length);
|
|
354
|
+
if (actual.length !== expected.length) fail("schema-mismatch");
|
|
355
|
+
for (const [index, tuple] of expected.entries()) {
|
|
356
|
+
const row = actual[index]!;
|
|
357
|
+
exactRowKeys(row, ["cid", "name", "type", "notnull", "dflt_value", "pk", "hidden"]);
|
|
358
|
+
const [name, type, notnull, defaultValue, pk] = tuple;
|
|
359
|
+
if (
|
|
360
|
+
!pragmaInteger(row.cid, BigInt(index))
|
|
361
|
+
|| row.name !== name
|
|
362
|
+
|| row.type !== type
|
|
363
|
+
|| !pragmaInteger(row.notnull, notnull)
|
|
364
|
+
|| row.dflt_value !== defaultValue
|
|
365
|
+
|| !pragmaInteger(row.pk, pk)
|
|
366
|
+
|| !pragmaInteger(row.hidden, 0n)
|
|
367
|
+
) fail("schema-mismatch");
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function assertIndexes(database: Database): void {
|
|
372
|
+
const indexes = rows(database.query("PRAGMA index_list('messages')").iterate(), 3);
|
|
373
|
+
if (indexes.length !== 3) fail("schema-mismatch");
|
|
374
|
+
const expected = new Map<string, readonly [bigint, string]>([
|
|
375
|
+
["idx_messages_ts", [0n, "c"]],
|
|
376
|
+
["idx_messages_chat_ts", [0n, "c"]],
|
|
377
|
+
["sqlite_autoindex_messages_1", [1n, "u"]],
|
|
378
|
+
] as const);
|
|
379
|
+
for (const row of indexes) {
|
|
380
|
+
exactRowKeys(row, ["seq", "name", "unique", "origin", "partial"]);
|
|
381
|
+
const specification = typeof row.name === "string" ? expected.get(row.name) : undefined;
|
|
382
|
+
if (
|
|
383
|
+
specification === undefined
|
|
384
|
+
|| !pragmaInteger(row.unique, specification[0])
|
|
385
|
+
|| row.origin !== specification[1]
|
|
386
|
+
|| !pragmaInteger(row.partial, 0n)
|
|
387
|
+
) fail("schema-mismatch");
|
|
388
|
+
}
|
|
389
|
+
for (const [name, columns] of [
|
|
390
|
+
["idx_messages_ts", ["ts"]],
|
|
391
|
+
["idx_messages_chat_ts", ["chat_jid", "ts"]],
|
|
392
|
+
] as const) {
|
|
393
|
+
const indexColumns = rows(
|
|
394
|
+
database.query(`PRAGMA index_xinfo('${name}')`).iterate(),
|
|
395
|
+
columns.length + 1,
|
|
396
|
+
);
|
|
397
|
+
if (indexColumns.length !== columns.length + 1) fail("schema-mismatch");
|
|
398
|
+
for (const [index, column] of columns.entries()) {
|
|
399
|
+
const row = indexColumns[index]!;
|
|
400
|
+
exactRowKeys(row, ["seqno", "cid", "name", "desc", "coll", "key"]);
|
|
401
|
+
if (
|
|
402
|
+
!pragmaInteger(row.seqno, BigInt(index))
|
|
403
|
+
|| row.name !== column
|
|
404
|
+
|| !pragmaInteger(row.desc, 0n)
|
|
405
|
+
|| row.coll !== "BINARY"
|
|
406
|
+
|| !pragmaInteger(row.key, 1n)
|
|
407
|
+
) fail("schema-mismatch");
|
|
408
|
+
}
|
|
409
|
+
const rowid = indexColumns.at(-1)!;
|
|
410
|
+
exactRowKeys(rowid, ["seqno", "cid", "name", "desc", "coll", "key"]);
|
|
411
|
+
if (
|
|
412
|
+
rowid.name !== null
|
|
413
|
+
|| !pragmaInteger(rowid.cid, -1n)
|
|
414
|
+
|| !pragmaInteger(rowid.key, 0n)
|
|
415
|
+
) fail("schema-mismatch");
|
|
416
|
+
}
|
|
417
|
+
const uniqueColumns = rows(
|
|
418
|
+
database.query("PRAGMA index_xinfo('sqlite_autoindex_messages_1')").iterate(),
|
|
419
|
+
3,
|
|
420
|
+
);
|
|
421
|
+
if (
|
|
422
|
+
uniqueColumns.length !== 3
|
|
423
|
+
|| uniqueColumns[0]?.name !== "chat_jid"
|
|
424
|
+
|| uniqueColumns[1]?.name !== "msg_id"
|
|
425
|
+
|| !pragmaInteger(uniqueColumns[0]?.key, 1n)
|
|
426
|
+
|| !pragmaInteger(uniqueColumns[1]?.key, 1n)
|
|
427
|
+
|| !pragmaInteger(uniqueColumns[2]?.key, 0n)
|
|
428
|
+
) fail("schema-mismatch");
|
|
429
|
+
const chatIndexes = rows(database.query("PRAGMA index_list('chats')").iterate(), 1);
|
|
430
|
+
if (
|
|
431
|
+
chatIndexes.length !== 1
|
|
432
|
+
|| chatIndexes[0]?.name !== "sqlite_autoindex_chats_1"
|
|
433
|
+
|| chatIndexes[0]?.origin !== "pk"
|
|
434
|
+
|| !pragmaInteger(chatIndexes[0]?.unique, 1n)
|
|
435
|
+
|| !pragmaInteger(chatIndexes[0]?.partial, 0n)
|
|
436
|
+
) fail("schema-mismatch");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function assertForeignKey(database: Database): void {
|
|
440
|
+
const foreignKeys = rows(database.query("PRAGMA foreign_key_list('messages')").iterate(), 1);
|
|
441
|
+
const row = foreignKeys[0];
|
|
442
|
+
if (foreignKeys.length !== 1 || row === undefined) fail("schema-mismatch");
|
|
443
|
+
exactRowKeys(row, ["id", "seq", "table", "from", "to", "on_update", "on_delete", "match"]);
|
|
444
|
+
if (
|
|
445
|
+
!pragmaInteger(row.id, 0n)
|
|
446
|
+
|| !pragmaInteger(row.seq, 0n)
|
|
447
|
+
|| row.table !== "chats"
|
|
448
|
+
|| row.from !== "chat_jid"
|
|
449
|
+
|| row.to !== "jid"
|
|
450
|
+
|| row.on_update !== "NO ACTION"
|
|
451
|
+
|| row.on_delete !== "CASCADE"
|
|
452
|
+
|| row.match !== "NONE"
|
|
453
|
+
) fail("schema-mismatch");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function assertPinnedSchema(database: Database): void {
|
|
457
|
+
try {
|
|
458
|
+
const tables = rows(database.query(`
|
|
459
|
+
SELECT schema, name, type, ncol, wr, strict
|
|
460
|
+
FROM pragma_table_list
|
|
461
|
+
WHERE schema = 'main' AND name IN ('messages', 'chats')
|
|
462
|
+
LIMIT 3
|
|
463
|
+
`).iterate(), 3);
|
|
464
|
+
for (const [name, count] of [["messages", 36n], ["chats", 9n]] as const) {
|
|
465
|
+
const matches = tables.filter((row) => row.name === name);
|
|
466
|
+
if (matches.length === 1) {
|
|
467
|
+
exactRowKeys(matches[0]!, ["schema", "name", "type", "ncol", "wr", "strict"]);
|
|
468
|
+
}
|
|
469
|
+
if (
|
|
470
|
+
matches.length !== 1
|
|
471
|
+
|| matches[0]?.type !== "table"
|
|
472
|
+
|| !pragmaInteger(matches[0]?.ncol, count)
|
|
473
|
+
|| !pragmaInteger(matches[0]?.wr, 0n)
|
|
474
|
+
|| !pragmaInteger(matches[0]?.strict, 0n)
|
|
475
|
+
) fail("schema-mismatch");
|
|
476
|
+
}
|
|
477
|
+
assertColumns(database, "messages", MESSAGE_COLUMNS);
|
|
478
|
+
assertColumns(database, "chats", CHAT_COLUMNS);
|
|
479
|
+
assertIndexes(database);
|
|
480
|
+
assertForeignKey(database);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof HelperFailure) throw error;
|
|
483
|
+
fail("schema-mismatch");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function sqliteBigInt(value: unknown): bigint {
|
|
488
|
+
if (typeof value === "bigint") return value;
|
|
489
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
|
|
490
|
+
if (typeof value === "string" && /^(?:0|[1-9][0-9]{0,18})$/u.test(value)) {
|
|
491
|
+
return BigInt(value);
|
|
492
|
+
}
|
|
493
|
+
return fail("projection-invalid");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function projectedJid(value: unknown, nullable = false): string | null {
|
|
497
|
+
if (nullable && (value === null || value === "")) return null;
|
|
498
|
+
if (
|
|
499
|
+
typeof value !== "string"
|
|
500
|
+
|| value.length < 9
|
|
501
|
+
|| value.length > 96
|
|
502
|
+
|| !(
|
|
503
|
+
value === SYSTEM_SENTINEL_JID
|
|
504
|
+
|| /^[0-9]{5,20}(?::[0-9]{1,5})?@s\.whatsapp\.net$/u.test(value)
|
|
505
|
+
|| /^[0-9]{5,32}(?::[0-9]{1,5})?@lid$/u.test(value)
|
|
506
|
+
|| /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u.test(value)
|
|
507
|
+
|| /^[0-9]{5,32}@newsletter$/u.test(value)
|
|
508
|
+
|| /^(?:status|[0-9]{5,32})@broadcast$/u.test(value)
|
|
509
|
+
)
|
|
510
|
+
) fail("projection-invalid");
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function legacyInteractionParticipantJid(value: string): string {
|
|
515
|
+
const match = /^([0-9]{5,20})(?::[0-9]{1,5})?@s\.whatsapp\.net$/u.exec(value)
|
|
516
|
+
?? /^([0-9]{5,32})(?::[0-9]{1,5})?@lid$/u.exec(value);
|
|
517
|
+
if (match?.[1] === undefined) return fail("projection-invalid");
|
|
518
|
+
return value.endsWith("@lid")
|
|
519
|
+
? `${match[1]}@lid`
|
|
520
|
+
: `${match[1]}@s.whatsapp.net`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function assertMessageDirection(
|
|
524
|
+
item: Pick<WhatsAppInteractionProjectionItem, "chatJid" | "chatKind" | "senderJid" | "fromMe">,
|
|
525
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
526
|
+
grammar: "canonical" | "legacy-interaction",
|
|
527
|
+
): void {
|
|
528
|
+
if (item.chatKind !== "dm" && item.chatKind !== "group") return;
|
|
529
|
+
const directJid = grammar === "canonical"
|
|
530
|
+
? /^(?:[1-9][0-9]{4,14}@s\.whatsapp\.net|[1-9][0-9]{4,19}@lid)$/u
|
|
531
|
+
: /^(?:[0-9]{5,20}@s\.whatsapp\.net|[0-9]{5,32}@lid)$/u;
|
|
532
|
+
const groupJid = grammar === "canonical"
|
|
533
|
+
? /^[1-9][0-9]{4,19}(?:-[1-9][0-9]{0,19})?@g\.us$/u
|
|
534
|
+
: /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u;
|
|
535
|
+
if ((item.chatKind === "dm" && !directJid.test(item.chatJid))
|
|
536
|
+
|| (item.chatKind === "group" && !groupJid.test(item.chatJid))) {
|
|
537
|
+
fail("projection-invalid");
|
|
538
|
+
}
|
|
539
|
+
const selfJids = new Set(owner.selfJids);
|
|
540
|
+
const senderJid = item.senderJid === null
|
|
541
|
+
? null
|
|
542
|
+
: grammar === "canonical"
|
|
543
|
+
? canonicalWhatsAppParticipantJid(item.senderJid)
|
|
544
|
+
: legacyInteractionParticipantJid(item.senderJid);
|
|
545
|
+
const valid = item.chatKind === "dm"
|
|
546
|
+
? senderJid === null || (item.fromMe ? selfJids.has(senderJid) : senderJid === item.chatJid)
|
|
547
|
+
: senderJid === null || (item.fromMe ? selfJids.has(senderJid) : !selfJids.has(senderJid));
|
|
548
|
+
if (!valid) fail("projection-invalid");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function projectItem(
|
|
552
|
+
row: SqliteRow,
|
|
553
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
554
|
+
): WhatsAppInteractionProjectionItem {
|
|
555
|
+
exactRowKeys(row, ["rowid", "chat_jid", "msg_id", "sender_jid", "ts", "from_me", "chat_kind"]);
|
|
556
|
+
const rowid = sqliteBigInt(row.rowid);
|
|
557
|
+
const seconds = sqliteBigInt(row.ts);
|
|
558
|
+
const fromMe = sqliteBigInt(row.from_me);
|
|
559
|
+
if (
|
|
560
|
+
rowid < 1n
|
|
561
|
+
|| seconds < 0n
|
|
562
|
+
|| seconds > MAX_UNIX_SECONDS
|
|
563
|
+
|| (fromMe !== 0n && fromMe !== 1n)
|
|
564
|
+
|| typeof row.msg_id !== "string"
|
|
565
|
+
|| !/^[A-Za-z0-9._~:-]{1,256}$/u.test(row.msg_id)
|
|
566
|
+
|| (row.chat_kind !== "dm"
|
|
567
|
+
&& row.chat_kind !== "group"
|
|
568
|
+
&& row.chat_kind !== "broadcast"
|
|
569
|
+
&& row.chat_kind !== "newsletter"
|
|
570
|
+
&& row.chat_kind !== "unknown")
|
|
571
|
+
) fail("projection-invalid");
|
|
572
|
+
const chatJid = projectedJid(row.chat_jid)! as string;
|
|
573
|
+
const projected = Object.freeze({
|
|
574
|
+
rowid: rowid.toString(),
|
|
575
|
+
chatJid,
|
|
576
|
+
messageId: row.msg_id,
|
|
577
|
+
senderJid: projectedJid(row.sender_jid, true),
|
|
578
|
+
timestamp: new Date(Number(seconds) * 1_000).toISOString(),
|
|
579
|
+
fromMe: fromMe === 1n,
|
|
580
|
+
chatKind: chatJid === SYSTEM_SENTINEL_JID ? "unknown" : row.chat_kind,
|
|
581
|
+
});
|
|
582
|
+
assertMessageDirection(projected, owner, "legacy-interaction");
|
|
583
|
+
return projected;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function interactionAnchor(item: WhatsAppInteractionProjectionItem): string {
|
|
587
|
+
return createHash("sha256")
|
|
588
|
+
.update(item.rowid)
|
|
589
|
+
.update("\0")
|
|
590
|
+
.update(item.chatJid)
|
|
591
|
+
.update("\0")
|
|
592
|
+
.update(item.messageId)
|
|
593
|
+
.digest("hex");
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function assertCursorAnchor(
|
|
597
|
+
database: Database,
|
|
598
|
+
request: WhatsAppInteractionProjectionRequest,
|
|
599
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
600
|
+
): void {
|
|
601
|
+
if (request.cursor === "0") return;
|
|
602
|
+
let projected: readonly SqliteRow[];
|
|
603
|
+
try {
|
|
604
|
+
projected = rows(database.query(`
|
|
605
|
+
SELECT
|
|
606
|
+
CAST(m.rowid AS TEXT) AS rowid, m.chat_jid, m.msg_id, m.sender_jid,
|
|
607
|
+
CAST(m.ts AS TEXT) AS ts, CAST(m.from_me AS TEXT) AS from_me,
|
|
608
|
+
c.kind AS chat_kind
|
|
609
|
+
FROM messages m
|
|
610
|
+
JOIN chats c ON c.jid = m.chat_jid
|
|
611
|
+
WHERE m.rowid = ?1
|
|
612
|
+
LIMIT 2
|
|
613
|
+
`).iterate(BigInt(request.cursor)), 2, "projection-invalid");
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (error instanceof HelperFailure) throw error;
|
|
616
|
+
return fail("projection-invalid");
|
|
617
|
+
}
|
|
618
|
+
if (
|
|
619
|
+
projected.length !== 1
|
|
620
|
+
|| interactionAnchor(projectItem(projected[0]!, owner)) !== request.cursorAnchor
|
|
621
|
+
) fail("projection-invalid");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function projectInteractions(
|
|
625
|
+
database: Database,
|
|
626
|
+
request: WhatsAppInteractionProjectionRequest,
|
|
627
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
628
|
+
): WhatsAppInteractionProjectionSuccess {
|
|
629
|
+
let projectedRows: readonly SqliteRow[];
|
|
630
|
+
try {
|
|
631
|
+
projectedRows = rows(database.query(`
|
|
632
|
+
SELECT
|
|
633
|
+
CAST(m.rowid AS TEXT) AS rowid,
|
|
634
|
+
m.chat_jid,
|
|
635
|
+
m.msg_id,
|
|
636
|
+
m.sender_jid,
|
|
637
|
+
CAST(m.ts AS TEXT) AS ts,
|
|
638
|
+
CAST(m.from_me AS TEXT) AS from_me,
|
|
639
|
+
c.kind AS chat_kind
|
|
640
|
+
FROM messages m
|
|
641
|
+
JOIN chats c ON c.jid = m.chat_jid
|
|
642
|
+
WHERE m.rowid > ?1
|
|
643
|
+
ORDER BY m.rowid ASC
|
|
644
|
+
LIMIT ?2
|
|
645
|
+
`).iterate(BigInt(request.cursor), request.limit + 1), request.limit + 1, "projection-invalid");
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (error instanceof HelperFailure) throw error;
|
|
648
|
+
return fail("projection-invalid");
|
|
649
|
+
}
|
|
650
|
+
const projected = projectedRows.map((row) => projectItem(row, owner));
|
|
651
|
+
for (let index = 0; index < projected.length; index += 1) {
|
|
652
|
+
const previous = index === 0 ? request.cursor : projected[index - 1]?.rowid;
|
|
653
|
+
if (previous === undefined || BigInt(projected[index]!.rowid) <= BigInt(previous)) {
|
|
654
|
+
fail("projection-invalid");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const hasMore = projected.length > request.limit;
|
|
658
|
+
const interactions = Object.freeze(projected.slice(0, request.limit));
|
|
659
|
+
const last = interactions.at(-1);
|
|
660
|
+
const checkpoint = last === undefined
|
|
661
|
+
? Object.freeze({ cursor: request.cursor, anchor: request.cursorAnchor })
|
|
662
|
+
: Object.freeze({ cursor: last.rowid, anchor: interactionAnchor(last) });
|
|
663
|
+
return Object.freeze({
|
|
664
|
+
schemaVersion: WHATSAPP_INTERACTION_PROJECTION_PROTOCOL_VERSION,
|
|
665
|
+
status: "succeeded",
|
|
666
|
+
projectionGeneration: Object.freeze({
|
|
667
|
+
messageStoreIdentity: request.messageStoreIdentity,
|
|
668
|
+
schemaFingerprint: WHATSAPP_INTERACTION_PROJECTION_SCHEMA_FINGERPRINT,
|
|
669
|
+
}),
|
|
670
|
+
accountJidAliases: owner.accountJidAliases,
|
|
671
|
+
interactions,
|
|
672
|
+
nextCursor: hasMore ? interactions.at(-1)?.rowid ?? null : null,
|
|
673
|
+
localInsertPageComplete: !hasMore,
|
|
674
|
+
checkpoint,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function messageExportGeneration(
|
|
679
|
+
stats: BigIntStats,
|
|
680
|
+
): WhatsAppMessageExportProjectionSuccess["projectionGeneration"] {
|
|
681
|
+
return Object.freeze({
|
|
682
|
+
messageStoreIdentity: Object.freeze({
|
|
683
|
+
dev: stats.dev.toString(),
|
|
684
|
+
ino: stats.ino.toString(),
|
|
685
|
+
}),
|
|
686
|
+
size: stats.size.toString(),
|
|
687
|
+
mtimeNs: stats.mtimeNs.toString(),
|
|
688
|
+
ctimeNs: stats.ctimeNs.toString(),
|
|
689
|
+
schemaFingerprint: WHATSAPP_MESSAGE_EXPORT_PROJECTION_SCHEMA_FINGERPRINT,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function sameMessageExportGeneration(
|
|
694
|
+
left: WhatsAppMessageExportProjectionSuccess["projectionGeneration"],
|
|
695
|
+
right: WhatsAppMessageExportProjectionSuccess["projectionGeneration"],
|
|
696
|
+
): boolean {
|
|
697
|
+
return left.messageStoreIdentity.dev === right.messageStoreIdentity.dev
|
|
698
|
+
&& left.messageStoreIdentity.ino === right.messageStoreIdentity.ino
|
|
699
|
+
&& left.size === right.size
|
|
700
|
+
&& left.mtimeNs === right.mtimeNs
|
|
701
|
+
&& left.ctimeNs === right.ctimeNs
|
|
702
|
+
&& left.schemaFingerprint === right.schemaFingerprint;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function projectedText(
|
|
706
|
+
value: unknown,
|
|
707
|
+
maximumBytes: number,
|
|
708
|
+
): string | null {
|
|
709
|
+
if (value === null || value === "") return null;
|
|
710
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > maximumBytes) {
|
|
711
|
+
return fail("projection-invalid");
|
|
712
|
+
}
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function projectedBoolean(value: unknown): boolean {
|
|
717
|
+
const parsed = sqliteBigInt(value);
|
|
718
|
+
if (parsed !== 0n && parsed !== 1n) return fail("projection-invalid");
|
|
719
|
+
return parsed === 1n;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function projectedUnixTimestamp(value: unknown, nullable: boolean): string | null {
|
|
723
|
+
if (nullable && (value === null || value === "" || value === 0 || value === 0n || value === "0")) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const seconds = sqliteBigInt(value);
|
|
727
|
+
if (seconds < 0n || seconds > MAX_UNIX_SECONDS) return fail("projection-invalid");
|
|
728
|
+
return new Date(Number(seconds) * 1_000).toISOString();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function projectedFileLength(value: unknown): number | null {
|
|
732
|
+
if (value === null || value === "") return null;
|
|
733
|
+
const parsed = sqliteBigInt(value);
|
|
734
|
+
if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
735
|
+
return fail("projection-invalid");
|
|
736
|
+
}
|
|
737
|
+
return Number(parsed);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function projectMessageExportItem(
|
|
741
|
+
row: SqliteRow,
|
|
742
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
743
|
+
): WhatsAppMessageExportProjectionItem {
|
|
744
|
+
exactRowKeys(row, [
|
|
745
|
+
"rowid", "chat_jid", "chat_kind", "chat_name", "msg_id", "sender_jid", "sender_name",
|
|
746
|
+
"ts", "from_me", "text", "display_text", "quoted_msg_id", "quoted_sender_jid",
|
|
747
|
+
"reaction_to_id", "reaction_emoji", "media_type", "media_caption", "filename",
|
|
748
|
+
"mime_type", "file_length", "revoked", "deleted_for_me", "deleted_at",
|
|
749
|
+
"payload_purged_at", "edited", "edited_ts",
|
|
750
|
+
]);
|
|
751
|
+
const rowid = sqliteBigInt(row.rowid);
|
|
752
|
+
if (
|
|
753
|
+
rowid < 1n
|
|
754
|
+
|| (row.chat_kind !== "dm" && row.chat_kind !== "group")
|
|
755
|
+
|| typeof row.msg_id !== "string"
|
|
756
|
+
|| !/^[A-Za-z0-9._~:-]{1,256}$/u.test(row.msg_id)
|
|
757
|
+
) return fail("projection-invalid");
|
|
758
|
+
const timestamp = projectedUnixTimestamp(row.ts, false)! as string;
|
|
759
|
+
const editedFlag = projectedBoolean(row.edited);
|
|
760
|
+
const rawEditedAt = projectedUnixTimestamp(row.edited_ts, true);
|
|
761
|
+
const edited = editedFlag && rawEditedAt !== null && rawEditedAt >= timestamp;
|
|
762
|
+
const revoked = projectedBoolean(row.revoked);
|
|
763
|
+
const deletedForMe = projectedBoolean(row.deleted_for_me);
|
|
764
|
+
const deletedAt = projectedUnixTimestamp(row.deleted_at, true);
|
|
765
|
+
const payloadPurgedAt = projectedUnixTimestamp(row.payload_purged_at, true);
|
|
766
|
+
const reactionToMessageId = projectedText(row.reaction_to_id, 256);
|
|
767
|
+
const reactionEmoji = projectedText(row.reaction_emoji, 8 * 1024);
|
|
768
|
+
if (
|
|
769
|
+
(reactionToMessageId !== null && !/^[A-Za-z0-9._~:-]{1,256}$/u.test(reactionToMessageId))
|
|
770
|
+
|| (reactionEmoji !== null && reactionToMessageId === null)
|
|
771
|
+
|| (deletedAt !== null && !revoked && !deletedForMe)
|
|
772
|
+
|| (deletedAt !== null && deletedAt < timestamp)
|
|
773
|
+
|| (payloadPurgedAt !== null && payloadPurgedAt < timestamp)
|
|
774
|
+
) return fail("projection-invalid");
|
|
775
|
+
const fileNameCandidate = projectedText(row.filename, 8 * 1024);
|
|
776
|
+
const fileName = fileNameCandidate !== null && (
|
|
777
|
+
fileNameCandidate === "."
|
|
778
|
+
|| fileNameCandidate === ".."
|
|
779
|
+
|| fileNameCandidate.includes("/")
|
|
780
|
+
|| fileNameCandidate.includes("\\")
|
|
781
|
+
) ? null : fileNameCandidate;
|
|
782
|
+
const senderJid = row.sender_jid === SYSTEM_SENTINEL_JID
|
|
783
|
+
? null
|
|
784
|
+
: projectedJid(row.sender_jid, true);
|
|
785
|
+
const quotedSenderJid = row.quoted_sender_jid === SYSTEM_SENTINEL_JID
|
|
786
|
+
? null
|
|
787
|
+
: projectedJid(row.quoted_sender_jid, true);
|
|
788
|
+
const projected = Object.freeze({
|
|
789
|
+
rowid: rowid.toString(),
|
|
790
|
+
chatJid: projectedJid(row.chat_jid)! as string,
|
|
791
|
+
chatKind: row.chat_kind,
|
|
792
|
+
chatName: projectedText(row.chat_name, 8 * 1024),
|
|
793
|
+
messageId: row.msg_id,
|
|
794
|
+
senderJid,
|
|
795
|
+
senderName: projectedText(row.sender_name, 8 * 1024),
|
|
796
|
+
timestamp,
|
|
797
|
+
fromMe: projectedBoolean(row.from_me),
|
|
798
|
+
text: projectedText(row.text, 1024 * 1024),
|
|
799
|
+
displayText: projectedText(row.display_text, 1024 * 1024),
|
|
800
|
+
quotedMessageId: projectedText(row.quoted_msg_id, 256),
|
|
801
|
+
quotedSenderJid,
|
|
802
|
+
reactionToMessageId,
|
|
803
|
+
reactionEmoji,
|
|
804
|
+
mediaType: projectedText(row.media_type, 256),
|
|
805
|
+
mediaCaption: projectedText(row.media_caption, 1024 * 1024),
|
|
806
|
+
fileName,
|
|
807
|
+
mimeType: projectedText(row.mime_type, 256),
|
|
808
|
+
fileLength: projectedFileLength(row.file_length),
|
|
809
|
+
revoked,
|
|
810
|
+
deletedForMe,
|
|
811
|
+
deletedAt,
|
|
812
|
+
payloadPurgedAt,
|
|
813
|
+
edited,
|
|
814
|
+
editedAt: edited ? rawEditedAt : null,
|
|
815
|
+
});
|
|
816
|
+
assertMessageDirection(projected, owner, "canonical");
|
|
817
|
+
return projected;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function messageExportAnchor(item: WhatsAppMessageExportProjectionItem): string {
|
|
821
|
+
return createHash("sha256").update(JSON.stringify(item)).digest("hex");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function numericSqlExpression(
|
|
825
|
+
value: string,
|
|
826
|
+
minimumLength: number,
|
|
827
|
+
maximumLength: number,
|
|
828
|
+
): string {
|
|
829
|
+
return `(
|
|
830
|
+
length(${value}) BETWEEN ${String(minimumLength)} AND ${String(maximumLength)}
|
|
831
|
+
AND substr(${value}, 1, 1) GLOB '[1-9]'
|
|
832
|
+
AND ${value} NOT GLOB '*[^0-9]*'
|
|
833
|
+
)`;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const MESSAGE_EXPORT_PN_BODY = "substr(m.chat_jid, 1, length(m.chat_jid) - 15)";
|
|
837
|
+
const MESSAGE_EXPORT_LID_BODY = "substr(m.chat_jid, 1, length(m.chat_jid) - 4)";
|
|
838
|
+
const MESSAGE_EXPORT_GROUP_BODY = "substr(m.chat_jid, 1, length(m.chat_jid) - 5)";
|
|
839
|
+
const MESSAGE_EXPORT_GROUP_SEPARATOR = `instr(${MESSAGE_EXPORT_GROUP_BODY}, '-')`;
|
|
840
|
+
const MESSAGE_EXPORT_GROUP_LEFT =
|
|
841
|
+
`substr(${MESSAGE_EXPORT_GROUP_BODY}, 1, ${MESSAGE_EXPORT_GROUP_SEPARATOR} - 1)`;
|
|
842
|
+
const MESSAGE_EXPORT_GROUP_RIGHT =
|
|
843
|
+
`substr(${MESSAGE_EXPORT_GROUP_BODY}, ${MESSAGE_EXPORT_GROUP_SEPARATOR} + 1)`;
|
|
844
|
+
|
|
845
|
+
const MESSAGE_EXPORT_FILTER = `
|
|
846
|
+
(
|
|
847
|
+
(
|
|
848
|
+
c.kind = 'dm'
|
|
849
|
+
AND (
|
|
850
|
+
(
|
|
851
|
+
substr(m.chat_jid, -15) = '@s.whatsapp.net'
|
|
852
|
+
AND ${numericSqlExpression(MESSAGE_EXPORT_PN_BODY, 5, 15)}
|
|
853
|
+
)
|
|
854
|
+
OR (
|
|
855
|
+
substr(m.chat_jid, -4) = '@lid'
|
|
856
|
+
AND ${numericSqlExpression(MESSAGE_EXPORT_LID_BODY, 5, 20)}
|
|
857
|
+
)
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
OR (
|
|
861
|
+
c.kind = 'group'
|
|
862
|
+
AND substr(m.chat_jid, -5) = '@g.us'
|
|
863
|
+
AND (
|
|
864
|
+
(
|
|
865
|
+
${MESSAGE_EXPORT_GROUP_SEPARATOR} = 0
|
|
866
|
+
AND ${numericSqlExpression(MESSAGE_EXPORT_GROUP_BODY, 5, 20)}
|
|
867
|
+
)
|
|
868
|
+
OR (
|
|
869
|
+
${MESSAGE_EXPORT_GROUP_SEPARATOR} BETWEEN 6 AND 21
|
|
870
|
+
AND ${numericSqlExpression(MESSAGE_EXPORT_GROUP_LEFT, 5, 20)}
|
|
871
|
+
AND ${numericSqlExpression(MESSAGE_EXPORT_GROUP_RIGHT, 1, 20)}
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
`;
|
|
877
|
+
|
|
878
|
+
const MESSAGE_EXPORT_SELF_CHAT_FILTER = `
|
|
879
|
+
NOT (
|
|
880
|
+
c.kind = 'dm'
|
|
881
|
+
AND (m.chat_jid = ?3 OR (?4 IS NOT NULL AND m.chat_jid = ?4))
|
|
882
|
+
)
|
|
883
|
+
`;
|
|
884
|
+
|
|
885
|
+
function hasExcludedNonConversationMessages(database: Database): boolean {
|
|
886
|
+
let projected: readonly SqliteRow[];
|
|
887
|
+
try {
|
|
888
|
+
projected = rows(database.query(`
|
|
889
|
+
SELECT EXISTS(
|
|
890
|
+
SELECT 1
|
|
891
|
+
FROM messages m
|
|
892
|
+
JOIN chats c ON c.jid = m.chat_jid
|
|
893
|
+
WHERE NOT (${MESSAGE_EXPORT_FILTER})
|
|
894
|
+
LIMIT 1
|
|
895
|
+
) AS excluded
|
|
896
|
+
`).iterate(), 1, "projection-invalid");
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error instanceof HelperFailure) throw error;
|
|
899
|
+
return fail("projection-invalid");
|
|
900
|
+
}
|
|
901
|
+
if (projected.length !== 1) return fail("projection-invalid");
|
|
902
|
+
exactRowKeys(projected[0]!, ["excluded"]);
|
|
903
|
+
return projectedBoolean(projected[0]!.excluded);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function selfChatsExcluded(
|
|
907
|
+
database: Database,
|
|
908
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
909
|
+
): MessageExportSessionSelfChatsExcluded {
|
|
910
|
+
try {
|
|
911
|
+
const result = rows(database.query(`
|
|
912
|
+
SELECT EXISTS(
|
|
913
|
+
SELECT 1
|
|
914
|
+
FROM messages m
|
|
915
|
+
JOIN chats c ON c.jid = m.chat_jid
|
|
916
|
+
WHERE c.kind = 'dm'
|
|
917
|
+
AND (m.chat_jid = ?1 OR (?2 IS NOT NULL AND m.chat_jid = ?2))
|
|
918
|
+
LIMIT 1
|
|
919
|
+
) AS excluded
|
|
920
|
+
`).iterate(
|
|
921
|
+
owner.accountJidAliases.pnJid,
|
|
922
|
+
owner.accountJidAliases.lidJid,
|
|
923
|
+
), 1, "projection-invalid");
|
|
924
|
+
if (result.length !== 1) return fail("projection-invalid");
|
|
925
|
+
exactRowKeys(result[0]!, ["excluded"]);
|
|
926
|
+
return projectedBoolean(result[0]!.excluded) ? "present-excluded" : "none-detected";
|
|
927
|
+
} catch (error) {
|
|
928
|
+
if (error instanceof HelperFailure) throw error;
|
|
929
|
+
return fail("projection-invalid");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const MESSAGE_EXPORT_COLUMNS = `
|
|
934
|
+
CAST(m.rowid AS TEXT) AS rowid,
|
|
935
|
+
m.chat_jid,
|
|
936
|
+
c.kind AS chat_kind,
|
|
937
|
+
COALESCE(m.chat_name, c.name) AS chat_name,
|
|
938
|
+
m.msg_id,
|
|
939
|
+
m.sender_jid,
|
|
940
|
+
m.sender_name,
|
|
941
|
+
CAST(m.ts AS TEXT) AS ts,
|
|
942
|
+
CAST(m.from_me AS TEXT) AS from_me,
|
|
943
|
+
m.text,
|
|
944
|
+
m.display_text,
|
|
945
|
+
m.quoted_msg_id,
|
|
946
|
+
m.quoted_sender_jid,
|
|
947
|
+
m.reaction_to_id,
|
|
948
|
+
m.reaction_emoji,
|
|
949
|
+
m.media_type,
|
|
950
|
+
m.media_caption,
|
|
951
|
+
m.filename,
|
|
952
|
+
m.mime_type,
|
|
953
|
+
CAST(m.file_length AS TEXT) AS file_length,
|
|
954
|
+
CAST(m.revoked AS TEXT) AS revoked,
|
|
955
|
+
CAST(m.deleted_for_me AS TEXT) AS deleted_for_me,
|
|
956
|
+
CAST(m.deleted_at AS TEXT) AS deleted_at,
|
|
957
|
+
CAST(m.payload_purged_at AS TEXT) AS payload_purged_at,
|
|
958
|
+
CAST(m.edited AS TEXT) AS edited,
|
|
959
|
+
CAST(m.edited_ts AS TEXT) AS edited_ts
|
|
960
|
+
`;
|
|
961
|
+
|
|
962
|
+
function assertMessageExportCursorAnchor(
|
|
963
|
+
database: Database,
|
|
964
|
+
request: WhatsAppMessageExportProjectionRequest,
|
|
965
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
966
|
+
): void {
|
|
967
|
+
if (request.cursor === "0") return;
|
|
968
|
+
let projected: readonly SqliteRow[];
|
|
969
|
+
try {
|
|
970
|
+
projected = rows(database.query(`
|
|
971
|
+
SELECT ${MESSAGE_EXPORT_COLUMNS}
|
|
972
|
+
FROM messages m
|
|
973
|
+
JOIN chats c ON c.jid = m.chat_jid
|
|
974
|
+
WHERE m.rowid = ?1
|
|
975
|
+
AND ${MESSAGE_EXPORT_FILTER}
|
|
976
|
+
AND ${MESSAGE_EXPORT_SELF_CHAT_FILTER}
|
|
977
|
+
LIMIT 2
|
|
978
|
+
`).iterate(
|
|
979
|
+
BigInt(request.cursor),
|
|
980
|
+
request.limit + 1,
|
|
981
|
+
owner.accountJidAliases.pnJid,
|
|
982
|
+
owner.accountJidAliases.lidJid,
|
|
983
|
+
), 2, "projection-invalid");
|
|
984
|
+
} catch (error) {
|
|
985
|
+
if (error instanceof HelperFailure) throw error;
|
|
986
|
+
return fail("projection-invalid");
|
|
987
|
+
}
|
|
988
|
+
if (
|
|
989
|
+
projected.length !== 1
|
|
990
|
+
|| messageExportAnchor(projectMessageExportItem(projected[0]!, owner)) !== request.cursorAnchor
|
|
991
|
+
) return fail("projection-invalid");
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function projectMessageExport(
|
|
995
|
+
database: Database,
|
|
996
|
+
request: WhatsAppMessageExportProjectionRequest,
|
|
997
|
+
fileStats: BigIntStats,
|
|
998
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
999
|
+
precomputedNonConversationChatsExcluded?: boolean,
|
|
1000
|
+
): WhatsAppMessageExportProjectionSuccess {
|
|
1001
|
+
const projectionGeneration = messageExportGeneration(fileStats);
|
|
1002
|
+
if (
|
|
1003
|
+
request.expectedGeneration !== null
|
|
1004
|
+
&& !sameMessageExportGeneration(projectionGeneration, request.expectedGeneration)
|
|
1005
|
+
) return fail("generation-mismatch");
|
|
1006
|
+
const nonConversationChatsExcluded = precomputedNonConversationChatsExcluded
|
|
1007
|
+
?? hasExcludedNonConversationMessages(database);
|
|
1008
|
+
assertMessageExportCursorAnchor(database, request, owner);
|
|
1009
|
+
let projectedRows: readonly SqliteRow[];
|
|
1010
|
+
try {
|
|
1011
|
+
projectedRows = rows(database.query(`
|
|
1012
|
+
SELECT ${MESSAGE_EXPORT_COLUMNS}
|
|
1013
|
+
FROM messages m
|
|
1014
|
+
JOIN chats c ON c.jid = m.chat_jid
|
|
1015
|
+
WHERE m.rowid > ?1
|
|
1016
|
+
AND ${MESSAGE_EXPORT_FILTER}
|
|
1017
|
+
AND ${MESSAGE_EXPORT_SELF_CHAT_FILTER}
|
|
1018
|
+
ORDER BY m.rowid ASC
|
|
1019
|
+
LIMIT ?2
|
|
1020
|
+
`).iterate(
|
|
1021
|
+
BigInt(request.cursor),
|
|
1022
|
+
request.limit + 1,
|
|
1023
|
+
owner.accountJidAliases.pnJid,
|
|
1024
|
+
owner.accountJidAliases.lidJid,
|
|
1025
|
+
), request.limit + 1, "projection-invalid");
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (error instanceof HelperFailure) throw error;
|
|
1028
|
+
return fail("projection-invalid");
|
|
1029
|
+
}
|
|
1030
|
+
const projected = projectedRows.map((row) =>
|
|
1031
|
+
projectMessageExportItem(row, owner));
|
|
1032
|
+
for (let index = 0; index < projected.length; index += 1) {
|
|
1033
|
+
const previous = index === 0 ? request.cursor : projected[index - 1]!.rowid;
|
|
1034
|
+
if (BigInt(projected[index]!.rowid) <= BigInt(previous)) return fail("projection-invalid");
|
|
1035
|
+
}
|
|
1036
|
+
const hasMore = projected.length > request.limit;
|
|
1037
|
+
const messages = Object.freeze(projected.slice(0, request.limit));
|
|
1038
|
+
const last = messages.at(-1);
|
|
1039
|
+
const checkpoint = last === undefined
|
|
1040
|
+
? Object.freeze({ cursor: request.cursor, anchor: request.cursorAnchor })
|
|
1041
|
+
: Object.freeze({ cursor: last.rowid, anchor: messageExportAnchor(last) });
|
|
1042
|
+
return Object.freeze({
|
|
1043
|
+
schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
|
|
1044
|
+
status: "succeeded",
|
|
1045
|
+
projectionGeneration,
|
|
1046
|
+
accountJidAliases: owner.accountJidAliases,
|
|
1047
|
+
nonConversationChatsExcluded,
|
|
1048
|
+
messages,
|
|
1049
|
+
nextCursor: hasMore ? last?.rowid ?? null : null,
|
|
1050
|
+
localInsertPageComplete: !hasMore,
|
|
1051
|
+
checkpoint,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function projectBoundWhatsAppMessageStore<Result>(
|
|
1056
|
+
request: BoundMessageStoreRequest,
|
|
1057
|
+
projection: (
|
|
1058
|
+
database: Database,
|
|
1059
|
+
fileStats: BigIntStats,
|
|
1060
|
+
owner: WhatsAppMatchedOwnerIdentity,
|
|
1061
|
+
revalidate: () => void,
|
|
1062
|
+
) => Result,
|
|
1063
|
+
): Result {
|
|
1064
|
+
const initialCwd = assertBoundCwd(request);
|
|
1065
|
+
const noFollow = fsConstants.O_NOFOLLOW;
|
|
1066
|
+
if (typeof noFollow !== "number" || noFollow === 0) return fail("message-store-file-invalid");
|
|
1067
|
+
const nonBlock = typeof fsConstants.O_NONBLOCK === "number" ? fsConstants.O_NONBLOCK : 0;
|
|
1068
|
+
let sessionDescriptor: number;
|
|
1069
|
+
try {
|
|
1070
|
+
sessionDescriptor = openSync(SESSION_DATABASE_NAME, fsConstants.O_RDONLY | noFollow | nonBlock);
|
|
1071
|
+
} catch {
|
|
1072
|
+
return fail("session-binding-invalid");
|
|
1073
|
+
}
|
|
1074
|
+
let initialSession: BigIntStats;
|
|
1075
|
+
let owner: WhatsAppMatchedOwnerIdentity;
|
|
1076
|
+
try {
|
|
1077
|
+
initialSession = fstatSync(sessionDescriptor, { bigint: true });
|
|
1078
|
+
assertSessionOwnerFile(initialSession, request);
|
|
1079
|
+
exactSessionOwnerPathSnapshot(initialSession, request);
|
|
1080
|
+
const contacts = projectWhatsAppContactsFromBoundCwd({
|
|
1081
|
+
schemaVersion: WHATSAPP_CONTACT_PROJECTION_PROTOCOL_VERSION,
|
|
1082
|
+
operation: "contacts.list",
|
|
1083
|
+
accountSubject: request.accountSubject,
|
|
1084
|
+
cursor: null,
|
|
1085
|
+
limit: 1,
|
|
1086
|
+
storeIdentity: request.storeIdentity,
|
|
1087
|
+
sessionIdentity: request.sessionIdentity,
|
|
1088
|
+
});
|
|
1089
|
+
owner = contacts[WHATSAPP_MATCHED_OWNER_IDENTITY];
|
|
1090
|
+
const afterOwner = fstatSync(sessionDescriptor, { bigint: true });
|
|
1091
|
+
if (!sameSnapshot(initialSession, afterOwner)) fail("session-binding-invalid");
|
|
1092
|
+
exactSessionOwnerPathSnapshot(initialSession, request);
|
|
1093
|
+
} catch {
|
|
1094
|
+
try { closeSync(sessionDescriptor); } catch { /* owner mismatch remains categorical */ }
|
|
1095
|
+
return fail("owner-mismatch");
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
assertBoundCwd(request, initialCwd);
|
|
1099
|
+
assertNoMessageSidecars();
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
try {
|
|
1102
|
+
closeSync(sessionDescriptor);
|
|
1103
|
+
} catch {
|
|
1104
|
+
return fail("session-binding-invalid");
|
|
1105
|
+
}
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
1108
|
+
let descriptor: number;
|
|
1109
|
+
try {
|
|
1110
|
+
descriptor = openSync(MESSAGE_DATABASE_NAME, fsConstants.O_RDONLY | noFollow | nonBlock);
|
|
1111
|
+
} catch {
|
|
1112
|
+
try { closeSync(sessionDescriptor); } catch { /* message-store failure remains categorical */ }
|
|
1113
|
+
return fail("message-store-file-invalid");
|
|
1114
|
+
}
|
|
1115
|
+
let initialFile: BigIntStats | undefined;
|
|
1116
|
+
let database: Database | undefined;
|
|
1117
|
+
let result: Result | undefined;
|
|
1118
|
+
let failure: unknown;
|
|
1119
|
+
try {
|
|
1120
|
+
initialFile = fstatSync(descriptor, { bigint: true });
|
|
1121
|
+
assertMessageFile(initialFile, request);
|
|
1122
|
+
exactMessagePathSnapshot(initialFile, request);
|
|
1123
|
+
// wacli persists WAL journal mode in the database header. Once the linked
|
|
1124
|
+
// device is quiescent it may leave no -wal/-shm sidecars, and an ordinary
|
|
1125
|
+
// read-only SQLite open then tries (and fails) to recreate shared-memory
|
|
1126
|
+
// state. The path is fixed, the parent directory and inode are bound above,
|
|
1127
|
+
// and both are revalidated after close, so immutable mode is the correct
|
|
1128
|
+
// side-effect-free way to read that already-proven quiescent snapshot.
|
|
1129
|
+
// SQLITE_OPEN_URI is explicit because Bun's Linux SQLite build does not
|
|
1130
|
+
// inherit macOS SQLite's process-wide URI-filename configuration.
|
|
1131
|
+
database = new Database(
|
|
1132
|
+
IMMUTABLE_MESSAGE_DATABASE_URI,
|
|
1133
|
+
IMMUTABLE_MESSAGE_DATABASE_FLAGS,
|
|
1134
|
+
);
|
|
1135
|
+
configureDatabase(database);
|
|
1136
|
+
assertIntegrity(database);
|
|
1137
|
+
assertPinnedSchema(database);
|
|
1138
|
+
const revalidate = (): void => {
|
|
1139
|
+
if (initialFile === undefined) fail("message-store-file-invalid");
|
|
1140
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
1141
|
+
if (!sameSnapshot(initialFile, after)) fail("message-store-file-invalid");
|
|
1142
|
+
exactMessagePathSnapshot(initialFile, request);
|
|
1143
|
+
const sessionAfter = fstatSync(sessionDescriptor, { bigint: true });
|
|
1144
|
+
if (!sameSnapshot(initialSession, sessionAfter)) fail("session-binding-invalid");
|
|
1145
|
+
exactSessionOwnerPathSnapshot(initialSession, request);
|
|
1146
|
+
assertBoundCwd(request, initialCwd);
|
|
1147
|
+
assertNoMessageSidecars();
|
|
1148
|
+
};
|
|
1149
|
+
result = projection(database, initialFile, owner, revalidate);
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
failure = error;
|
|
1152
|
+
} finally {
|
|
1153
|
+
try {
|
|
1154
|
+
database?.close();
|
|
1155
|
+
} catch {
|
|
1156
|
+
failure = new HelperFailure("database-invalid");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
try {
|
|
1160
|
+
if (initialFile !== undefined) {
|
|
1161
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
1162
|
+
if (!sameSnapshot(initialFile, after)) fail("message-store-file-invalid");
|
|
1163
|
+
exactMessagePathSnapshot(initialFile, request);
|
|
1164
|
+
}
|
|
1165
|
+
const sessionAfter = fstatSync(sessionDescriptor, { bigint: true });
|
|
1166
|
+
if (!sameSnapshot(initialSession, sessionAfter)) fail("session-binding-invalid");
|
|
1167
|
+
exactSessionOwnerPathSnapshot(initialSession, request);
|
|
1168
|
+
assertBoundCwd(request, initialCwd);
|
|
1169
|
+
assertNoMessageSidecars();
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
failure = error;
|
|
1172
|
+
} finally {
|
|
1173
|
+
try {
|
|
1174
|
+
closeSync(descriptor);
|
|
1175
|
+
} catch {
|
|
1176
|
+
failure = new HelperFailure("message-store-file-invalid");
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
closeSync(sessionDescriptor);
|
|
1180
|
+
} catch {
|
|
1181
|
+
failure = new HelperFailure("session-binding-invalid");
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
if (failure !== undefined) {
|
|
1185
|
+
throw failure instanceof Error ? failure : new HelperFailure("database-invalid");
|
|
1186
|
+
}
|
|
1187
|
+
if (result === undefined) return fail("projection-invalid");
|
|
1188
|
+
return result;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export function projectWhatsAppInteractionsFromBoundCwd(
|
|
1192
|
+
requestValue: unknown,
|
|
1193
|
+
): WhatsAppInteractionProjectionSuccess {
|
|
1194
|
+
const request = parseWhatsAppInteractionProjectionRequest(requestValue);
|
|
1195
|
+
return projectBoundWhatsAppMessageStore(request, (database, _fileStats, owner) => {
|
|
1196
|
+
assertCursorAnchor(database, request, owner);
|
|
1197
|
+
return projectInteractions(database, request, owner);
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
export function projectWhatsAppMessageExportFromBoundCwd(
|
|
1202
|
+
requestValue: unknown,
|
|
1203
|
+
): WhatsAppMessageExportProjectionSuccess {
|
|
1204
|
+
const request = parseWhatsAppMessageExportProjectionRequest(requestValue);
|
|
1205
|
+
return projectBoundWhatsAppMessageStore(
|
|
1206
|
+
request,
|
|
1207
|
+
(database, fileStats, owner) => projectMessageExport(database, request, fileStats, owner),
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
export function projectWhatsAppMessageExportSessionFromBoundCwd(
|
|
1212
|
+
requestValue: unknown,
|
|
1213
|
+
writeFrame: (frame: unknown) => void,
|
|
1214
|
+
options: Readonly<{
|
|
1215
|
+
/** Test-only seam proving the whole-store exclusion scan is session-scoped. */
|
|
1216
|
+
hasExcludedNonConversationMessagesForTest?: (database: Database) => boolean;
|
|
1217
|
+
}> = {},
|
|
1218
|
+
): Readonly<{
|
|
1219
|
+
pages: number;
|
|
1220
|
+
messages: number;
|
|
1221
|
+
checkpoint: WhatsAppMessageExportProjectionSuccess["checkpoint"];
|
|
1222
|
+
projectionGeneration: WhatsAppMessageExportProjectionSuccess["projectionGeneration"];
|
|
1223
|
+
selfJids: readonly string[];
|
|
1224
|
+
selfChatsExcluded: MessageExportSessionSelfChatsExcluded;
|
|
1225
|
+
integrityChecks: 1;
|
|
1226
|
+
framesSha256: string;
|
|
1227
|
+
}> {
|
|
1228
|
+
const initial = parseWhatsAppMessageExportProjectionRequest(requestValue);
|
|
1229
|
+
if (
|
|
1230
|
+
initial.cursor !== "0"
|
|
1231
|
+
|| initial.cursorAnchor !== null
|
|
1232
|
+
|| initial.expectedGeneration !== null
|
|
1233
|
+
) return fail("request-invalid");
|
|
1234
|
+
if (
|
|
1235
|
+
options.hasExcludedNonConversationMessagesForTest !== undefined
|
|
1236
|
+
&& process.env.NODE_ENV !== "test"
|
|
1237
|
+
) return fail("projection-invalid");
|
|
1238
|
+
return projectBoundWhatsAppMessageStore(
|
|
1239
|
+
initial,
|
|
1240
|
+
(database, fileStats, owner, revalidate) => {
|
|
1241
|
+
let request = initial;
|
|
1242
|
+
let pages = 0;
|
|
1243
|
+
let messages = 0;
|
|
1244
|
+
const framesHash = createHash("sha256");
|
|
1245
|
+
let finalResponse: WhatsAppMessageExportProjectionSuccess | undefined;
|
|
1246
|
+
const selfJids = Object.freeze([...owner.selfJids].sort());
|
|
1247
|
+
const expectedSelfJids = Object.freeze([
|
|
1248
|
+
owner.accountJidAliases.pnJid,
|
|
1249
|
+
...(owner.accountJidAliases.lidJid === null
|
|
1250
|
+
? []
|
|
1251
|
+
: [owner.accountJidAliases.lidJid]),
|
|
1252
|
+
].sort());
|
|
1253
|
+
const excludedSelfChats = selfChatsExcluded(database, owner);
|
|
1254
|
+
const nonConversationChatsExcluded = (
|
|
1255
|
+
options.hasExcludedNonConversationMessagesForTest
|
|
1256
|
+
?? hasExcludedNonConversationMessages
|
|
1257
|
+
)(database);
|
|
1258
|
+
if (
|
|
1259
|
+
selfJids.length !== expectedSelfJids.length
|
|
1260
|
+
|| selfJids.some((jid, index) => jid !== expectedSelfJids[index])
|
|
1261
|
+
) return fail("owner-mismatch");
|
|
1262
|
+
for (;;) {
|
|
1263
|
+
revalidate();
|
|
1264
|
+
pages += 1;
|
|
1265
|
+
const response = projectMessageExport(
|
|
1266
|
+
database,
|
|
1267
|
+
request,
|
|
1268
|
+
fileStats,
|
|
1269
|
+
owner,
|
|
1270
|
+
nonConversationChatsExcluded,
|
|
1271
|
+
);
|
|
1272
|
+
messages += response.messages.length;
|
|
1273
|
+
if (messages > MESSAGE_EXPORT_SESSION_MAX_MESSAGES) {
|
|
1274
|
+
return fail("projection-invalid");
|
|
1275
|
+
}
|
|
1276
|
+
finalResponse = response;
|
|
1277
|
+
const frame = Object.freeze({
|
|
1278
|
+
kind: "page" as const,
|
|
1279
|
+
index: pages,
|
|
1280
|
+
projectionGeneration: response.projectionGeneration,
|
|
1281
|
+
selfJids,
|
|
1282
|
+
selfChatsExcluded: excludedSelfChats,
|
|
1283
|
+
nonConversationChatsExcluded: response.nonConversationChatsExcluded,
|
|
1284
|
+
messages: response.messages,
|
|
1285
|
+
checkpoint: response.checkpoint,
|
|
1286
|
+
terminal: response.localInsertPageComplete,
|
|
1287
|
+
});
|
|
1288
|
+
const canonicalFrame = canonicalJson(frame);
|
|
1289
|
+
framesHash.update(canonicalFrame).update("\n");
|
|
1290
|
+
writeFrame(frame);
|
|
1291
|
+
revalidate();
|
|
1292
|
+
if (response.localInsertPageComplete) break;
|
|
1293
|
+
if (response.nextCursor === null || response.nextCursor !== response.checkpoint.cursor) {
|
|
1294
|
+
return fail("projection-invalid");
|
|
1295
|
+
}
|
|
1296
|
+
request = parseWhatsAppMessageExportProjectionRequest({
|
|
1297
|
+
...request,
|
|
1298
|
+
cursor: response.checkpoint.cursor,
|
|
1299
|
+
cursorAnchor: response.checkpoint.anchor,
|
|
1300
|
+
expectedGeneration: response.projectionGeneration,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (finalResponse === undefined) return fail("projection-invalid");
|
|
1304
|
+
return Object.freeze({
|
|
1305
|
+
pages,
|
|
1306
|
+
messages,
|
|
1307
|
+
checkpoint: finalResponse.checkpoint,
|
|
1308
|
+
projectionGeneration: finalResponse.projectionGeneration,
|
|
1309
|
+
selfJids,
|
|
1310
|
+
selfChatsExcluded: excludedSelfChats,
|
|
1311
|
+
integrityChecks: 1 as const,
|
|
1312
|
+
framesSha256: framesHash.digest("hex"),
|
|
1313
|
+
});
|
|
1314
|
+
},
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function writeSessionFrame(frame: unknown): void {
|
|
1319
|
+
const bytes = Buffer.from(`${canonicalJson(frame)}\n`, "utf8");
|
|
1320
|
+
if (bytes.byteLength > WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES) {
|
|
1321
|
+
return fail("output-too-large");
|
|
1322
|
+
}
|
|
1323
|
+
let offset = 0;
|
|
1324
|
+
while (offset < bytes.length) {
|
|
1325
|
+
const written = writeSync(1, bytes, offset, bytes.length - offset);
|
|
1326
|
+
if (written < 1) return fail("projection-invalid");
|
|
1327
|
+
offset += written;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function readBoundedStdin(): Promise<unknown> {
|
|
1332
|
+
const reader = Bun.stdin.stream().getReader();
|
|
1333
|
+
const chunks: Uint8Array[] = [];
|
|
1334
|
+
let length = 0;
|
|
1335
|
+
try {
|
|
1336
|
+
for (;;) {
|
|
1337
|
+
const item = await reader.read();
|
|
1338
|
+
if (item.done) break;
|
|
1339
|
+
length += item.value.byteLength;
|
|
1340
|
+
if (length > Math.max(
|
|
1341
|
+
WHATSAPP_INTERACTION_PROJECTION_MAX_STDIN_BYTES,
|
|
1342
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDIN_BYTES,
|
|
1343
|
+
)) {
|
|
1344
|
+
return fail("request-invalid");
|
|
1345
|
+
}
|
|
1346
|
+
chunks.push(item.value);
|
|
1347
|
+
}
|
|
1348
|
+
} catch {
|
|
1349
|
+
return fail("request-invalid");
|
|
1350
|
+
} finally {
|
|
1351
|
+
reader.releaseLock();
|
|
1352
|
+
}
|
|
1353
|
+
if (length < 2) return fail("request-invalid");
|
|
1354
|
+
try {
|
|
1355
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
|
|
1356
|
+
Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), length),
|
|
1357
|
+
)) as unknown;
|
|
1358
|
+
} catch {
|
|
1359
|
+
return fail("request-invalid");
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export async function runWhatsAppInteractionProjectionHelper(): Promise<
|
|
1364
|
+
unknown
|
|
1365
|
+
> {
|
|
1366
|
+
let requestValue: unknown;
|
|
1367
|
+
try {
|
|
1368
|
+
if (process.argv.length !== 2) fail("request-invalid");
|
|
1369
|
+
requestValue = await readBoundedStdin();
|
|
1370
|
+
} catch {
|
|
1371
|
+
return createWhatsAppInteractionProjectionFailure("request-invalid");
|
|
1372
|
+
}
|
|
1373
|
+
const session = typeof requestValue === "object"
|
|
1374
|
+
&& requestValue !== null
|
|
1375
|
+
&& !Array.isArray(requestValue)
|
|
1376
|
+
&& "operation" in requestValue
|
|
1377
|
+
&& requestValue.operation === "message-like-me.export-session";
|
|
1378
|
+
if (session) {
|
|
1379
|
+
const record = requestValue as Readonly<Record<string, unknown>>;
|
|
1380
|
+
if (
|
|
1381
|
+
Object.keys(record).sort().join("\0") !== "operation\0request"
|
|
1382
|
+
) {
|
|
1383
|
+
writeSessionFrame(Object.freeze({
|
|
1384
|
+
kind: "failed" as const,
|
|
1385
|
+
errorCode: "request-invalid" as const,
|
|
1386
|
+
}));
|
|
1387
|
+
return undefined;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const summary = projectWhatsAppMessageExportSessionFromBoundCwd(
|
|
1391
|
+
record.request,
|
|
1392
|
+
writeSessionFrame,
|
|
1393
|
+
);
|
|
1394
|
+
// projectBoundWhatsAppMessageStore closes and revalidates the database
|
|
1395
|
+
// before returning. The seal therefore proves that every page preceded
|
|
1396
|
+
// a clean close of the one identity-bound snapshot.
|
|
1397
|
+
writeSessionFrame(Object.freeze({ kind: "seal" as const, ...summary }));
|
|
1398
|
+
return undefined;
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
writeSessionFrame(Object.freeze({
|
|
1401
|
+
kind: "failed" as const,
|
|
1402
|
+
errorCode: errorCode(error),
|
|
1403
|
+
}));
|
|
1404
|
+
return undefined;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const messageExport = typeof requestValue === "object"
|
|
1408
|
+
&& requestValue !== null
|
|
1409
|
+
&& !Array.isArray(requestValue)
|
|
1410
|
+
&& "operation" in requestValue
|
|
1411
|
+
&& requestValue.operation === "message-like-me.export";
|
|
1412
|
+
if (messageExport) {
|
|
1413
|
+
let request: WhatsAppMessageExportProjectionRequest;
|
|
1414
|
+
try {
|
|
1415
|
+
request = parseWhatsAppMessageExportProjectionRequest(requestValue);
|
|
1416
|
+
} catch {
|
|
1417
|
+
return createWhatsAppMessageExportProjectionFailure("request-invalid");
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
return projectWhatsAppMessageExportFromBoundCwd(request);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
return createWhatsAppMessageExportProjectionFailure(errorCode(error));
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
let request: WhatsAppInteractionProjectionRequest;
|
|
1426
|
+
try {
|
|
1427
|
+
request = parseWhatsAppInteractionProjectionRequest(requestValue);
|
|
1428
|
+
} catch {
|
|
1429
|
+
return createWhatsAppInteractionProjectionFailure("request-invalid");
|
|
1430
|
+
}
|
|
1431
|
+
try {
|
|
1432
|
+
return projectWhatsAppInteractionsFromBoundCwd(request);
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
return createWhatsAppInteractionProjectionFailure(errorCode(error));
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
async function main(): Promise<void> {
|
|
1439
|
+
let response = await runWhatsAppInteractionProjectionHelper();
|
|
1440
|
+
if (response === undefined) return;
|
|
1441
|
+
let output = `${JSON.stringify(response)}\n`;
|
|
1442
|
+
if (Buffer.byteLength(output, "utf8") > Math.max(
|
|
1443
|
+
WHATSAPP_INTERACTION_PROJECTION_MAX_STDOUT_BYTES,
|
|
1444
|
+
WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
|
|
1445
|
+
)) {
|
|
1446
|
+
response = createWhatsAppMessageExportProjectionFailure("output-too-large");
|
|
1447
|
+
output = `${JSON.stringify(response)}\n`;
|
|
1448
|
+
}
|
|
1449
|
+
await Bun.write(Bun.stdout, output);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (import.meta.main) await main();
|