@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,1130 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { types as nodeTypes } from "node:util";
|
|
3
|
+
|
|
4
|
+
import type { OperationInput } from "../model";
|
|
5
|
+
import type { ProviderActionContext } from "../provider-context";
|
|
6
|
+
import {
|
|
7
|
+
parseContactDirectionStats,
|
|
8
|
+
projectContactDirectionStats,
|
|
9
|
+
type ContactDirectionStats,
|
|
10
|
+
} from "./contact-projection";
|
|
11
|
+
import {
|
|
12
|
+
buildGmailThreadUrl,
|
|
13
|
+
createGmailApiClient,
|
|
14
|
+
extractGmailEmailAddresses,
|
|
15
|
+
fetchGmailContacts,
|
|
16
|
+
fetchGmailMessageInteractionMetadata,
|
|
17
|
+
fetchGmailMessageList,
|
|
18
|
+
fetchGmailMessageMetadata,
|
|
19
|
+
fetchGmailThread,
|
|
20
|
+
listGmailSendAsAliases,
|
|
21
|
+
fetchGmailThreadList,
|
|
22
|
+
fetchGmailThreadMetadata,
|
|
23
|
+
getAuthenticatedGmailProfile,
|
|
24
|
+
parseGmailId,
|
|
25
|
+
parseGmailThread,
|
|
26
|
+
resolveGmailThreadBodies,
|
|
27
|
+
type GmailApiClient,
|
|
28
|
+
type GmailContact,
|
|
29
|
+
type GmailContactCollection,
|
|
30
|
+
type GmailProfile,
|
|
31
|
+
type GmailThread,
|
|
32
|
+
} from "./gmail-api";
|
|
33
|
+
|
|
34
|
+
const CONTACTS_DEFAULT_LIMIT = 20;
|
|
35
|
+
const CONTACTS_MAX_LIMIT = 100;
|
|
36
|
+
const CONTACTS_DEFAULT_STATS_SCAN_LIMIT = 100;
|
|
37
|
+
const CONTACTS_MAX_STATS_SCAN_LIMIT = 2_000;
|
|
38
|
+
const CONTACT_STATS_MAX_DIRECTION_PRODUCT = 2_000;
|
|
39
|
+
const INTERACTIONS_DEFAULT_LIMIT = 100;
|
|
40
|
+
const INTERACTIONS_MAX_LIMIT = 100;
|
|
41
|
+
const INTERACTIONS_MAX_ADDRESSES = 10_000;
|
|
42
|
+
const MESSAGING_DEFAULT_LIMIT = 50;
|
|
43
|
+
const MESSAGING_MAX_LIMIT = 100;
|
|
44
|
+
const GMAIL_LIST_PAGE_MAXIMUM = 500;
|
|
45
|
+
const CONTACT_STATS_QUERY_MAX_BYTES = 4_096;
|
|
46
|
+
const MAX_CONCURRENT_GMAIL_READS = 4;
|
|
47
|
+
const GMAIL_MESSAGING_READ_BODY_BYTES = 7 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
50
|
+
type GmailListView = "inbox" | "search";
|
|
51
|
+
type ContactDirection = "sent" | "received";
|
|
52
|
+
|
|
53
|
+
type ParsedContactsInput = Readonly<{
|
|
54
|
+
collection: GmailContactCollection;
|
|
55
|
+
cursor: string | null;
|
|
56
|
+
projection: "core" | "dates";
|
|
57
|
+
includeStats: boolean;
|
|
58
|
+
limit: number;
|
|
59
|
+
statsScanLimit: number;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
type ParsedInteractionsInput = Readonly<{
|
|
63
|
+
after: string | null;
|
|
64
|
+
afterMilliseconds: number | null;
|
|
65
|
+
before: string;
|
|
66
|
+
beforeMilliseconds: number;
|
|
67
|
+
cursor: string | null;
|
|
68
|
+
limit: number;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
type ParsedMessagingListInput = Readonly<{
|
|
72
|
+
view: GmailListView;
|
|
73
|
+
query: string | null;
|
|
74
|
+
cursor: string | null;
|
|
75
|
+
limit: number;
|
|
76
|
+
includeSpamTrash: boolean;
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
type GmailDirectionStatsIncompleteReason =
|
|
80
|
+
| "message-internal-date-unavailable"
|
|
81
|
+
| "no-contact-addresses"
|
|
82
|
+
| "scan-limit-reached"
|
|
83
|
+
| "unsupported-contact-addresses";
|
|
84
|
+
|
|
85
|
+
type ContactEmailCoverage = Readonly<{
|
|
86
|
+
emails: readonly string[];
|
|
87
|
+
kind: "complete" | "partial" | "unavailable" | "unsupported";
|
|
88
|
+
unsupportedAddressCount: number;
|
|
89
|
+
}>;
|
|
90
|
+
|
|
91
|
+
type ConcurrentAdmission = <T>(action: () => Promise<T>) => Promise<T>;
|
|
92
|
+
|
|
93
|
+
export type GmailThreadSummary = Readonly<{
|
|
94
|
+
id: string;
|
|
95
|
+
historyId: string | null;
|
|
96
|
+
snippet: string | null;
|
|
97
|
+
subject: string | null;
|
|
98
|
+
orderedAt: string | null;
|
|
99
|
+
messageCount: number;
|
|
100
|
+
participants: readonly Readonly<{
|
|
101
|
+
email: string;
|
|
102
|
+
displayName: null;
|
|
103
|
+
}>[];
|
|
104
|
+
unread: boolean;
|
|
105
|
+
archived: boolean | null;
|
|
106
|
+
threadUrl: string;
|
|
107
|
+
readInput: Readonly<{ thread_id: string }>;
|
|
108
|
+
}>;
|
|
109
|
+
|
|
110
|
+
export type GmailMessagingListOutput = Readonly<{
|
|
111
|
+
provider: "gmail";
|
|
112
|
+
operation: "messaging.list";
|
|
113
|
+
accountSubject: string;
|
|
114
|
+
view: GmailListView;
|
|
115
|
+
query: string | null;
|
|
116
|
+
includeSpamTrash: boolean;
|
|
117
|
+
threads: readonly GmailThreadSummary[];
|
|
118
|
+
nextCursor: string | null;
|
|
119
|
+
resultSizeEstimate: number;
|
|
120
|
+
}>;
|
|
121
|
+
|
|
122
|
+
export type GmailMessagingReadOutput = Readonly<{
|
|
123
|
+
provider: "gmail";
|
|
124
|
+
operation: "messaging.read";
|
|
125
|
+
accountSubject: string;
|
|
126
|
+
thread: GmailThread;
|
|
127
|
+
threadUrl: string;
|
|
128
|
+
}>;
|
|
129
|
+
|
|
130
|
+
export type GmailContactInteraction = Readonly<{
|
|
131
|
+
email: string;
|
|
132
|
+
sentCount: number;
|
|
133
|
+
receivedCount: number;
|
|
134
|
+
sentCountComplete: boolean;
|
|
135
|
+
receivedCountComplete: boolean;
|
|
136
|
+
firstSentAt: string | null;
|
|
137
|
+
lastSentAt: string | null;
|
|
138
|
+
firstReceivedAt: string | null;
|
|
139
|
+
lastReceivedAt: string | null;
|
|
140
|
+
sent30d: number;
|
|
141
|
+
sent90d: number;
|
|
142
|
+
sent365d: number;
|
|
143
|
+
received30d: number;
|
|
144
|
+
received90d: number;
|
|
145
|
+
received365d: number;
|
|
146
|
+
}>;
|
|
147
|
+
|
|
148
|
+
export type GmailContactInteractionsListOutput = Readonly<{
|
|
149
|
+
provider: "gmail";
|
|
150
|
+
operation: "contacts.list";
|
|
151
|
+
contactCollection: "interactions";
|
|
152
|
+
accountSubject: string;
|
|
153
|
+
accountAddresses: readonly string[];
|
|
154
|
+
after: string | null;
|
|
155
|
+
before: string;
|
|
156
|
+
interactions: readonly GmailContactInteraction[];
|
|
157
|
+
messageKeys: readonly string[];
|
|
158
|
+
threadKeys: readonly string[];
|
|
159
|
+
messagesScanned: number;
|
|
160
|
+
messagesIncluded: number;
|
|
161
|
+
messagesSkipped: Readonly<{
|
|
162
|
+
draftOrChat: number;
|
|
163
|
+
missingInternalDate: number;
|
|
164
|
+
noExternalAddress: number;
|
|
165
|
+
outsideWindow: number;
|
|
166
|
+
}>;
|
|
167
|
+
nextCursor: string | null;
|
|
168
|
+
resultSizeEstimate: number;
|
|
169
|
+
scanScope: "messages-in-half-open-window-excluding-spam-trash-drafts-chats";
|
|
170
|
+
}>;
|
|
171
|
+
|
|
172
|
+
function fail(label: string, message: string): never {
|
|
173
|
+
throw new Error(`official Gmail ${label} ${message}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function record(value: unknown, label: string): JsonRecord {
|
|
177
|
+
if (
|
|
178
|
+
nodeTypes.isProxy(value)
|
|
179
|
+
|| typeof value !== "object"
|
|
180
|
+
|| value === null
|
|
181
|
+
|| Array.isArray(value)
|
|
182
|
+
|| (
|
|
183
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
184
|
+
&& Object.getPrototypeOf(value) !== null
|
|
185
|
+
)
|
|
186
|
+
) return fail(label, "must be a plain object");
|
|
187
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
188
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
189
|
+
if (typeof key !== "string") return fail(label, "must not contain symbol properties");
|
|
190
|
+
const descriptor = descriptors[key];
|
|
191
|
+
if (
|
|
192
|
+
descriptor === undefined
|
|
193
|
+
|| !("value" in descriptor)
|
|
194
|
+
|| !descriptor.enumerable
|
|
195
|
+
) return fail(`${label}.${key}`, "must be an enumerable data property");
|
|
196
|
+
}
|
|
197
|
+
return value as JsonRecord;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function exactKeys(
|
|
201
|
+
value: JsonRecord,
|
|
202
|
+
required: readonly string[],
|
|
203
|
+
optional: readonly string[],
|
|
204
|
+
label: string,
|
|
205
|
+
): void {
|
|
206
|
+
const allowed = new Set([...required, ...optional]);
|
|
207
|
+
for (const key of Object.keys(value)) {
|
|
208
|
+
if (!allowed.has(key)) return fail(label, `contains unreviewed property ${key}`);
|
|
209
|
+
}
|
|
210
|
+
for (const key of required) {
|
|
211
|
+
if (!Object.hasOwn(value, key)) return fail(`${label}.${key}`, "is required");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasUnsafeControl(value: string): boolean {
|
|
216
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
217
|
+
const code = value.charCodeAt(index);
|
|
218
|
+
if (
|
|
219
|
+
code <= 0x1f
|
|
220
|
+
|| code === 0x7f
|
|
221
|
+
|| (code >= 0x80 && code <= 0x9f)
|
|
222
|
+
|| code === 0x061c
|
|
223
|
+
|| code === 0x200e
|
|
224
|
+
|| code === 0x200f
|
|
225
|
+
|| (code >= 0x2028 && code <= 0x202e)
|
|
226
|
+
|| (code >= 0x2066 && code <= 0x2069)
|
|
227
|
+
) return true;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isWellFormedText(value: string): boolean {
|
|
233
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
234
|
+
const code = value.charCodeAt(index);
|
|
235
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
236
|
+
const next = value.charCodeAt(index + 1);
|
|
237
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
238
|
+
index += 1;
|
|
239
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) return false;
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function boundedInputText(value: unknown, label: string, maximum: number): string {
|
|
245
|
+
if (
|
|
246
|
+
typeof value !== "string"
|
|
247
|
+
|| value.length === 0
|
|
248
|
+
|| value.length > maximum
|
|
249
|
+
|| !isWellFormedText(value)
|
|
250
|
+
|| hasUnsafeControl(value)
|
|
251
|
+
) return fail(label, `must be non-empty text of at most ${maximum} code units without unsafe controls`);
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function optionalInputText(value: unknown, label: string, maximum: number): string | null {
|
|
256
|
+
return value === undefined ? null : boundedInputText(value, label, maximum);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function inputInteger(
|
|
260
|
+
value: unknown,
|
|
261
|
+
label: string,
|
|
262
|
+
fallback: number,
|
|
263
|
+
minimum: number,
|
|
264
|
+
maximum: number,
|
|
265
|
+
): number {
|
|
266
|
+
const candidate = value === undefined ? fallback : value;
|
|
267
|
+
if (
|
|
268
|
+
typeof candidate !== "number"
|
|
269
|
+
|| !Number.isSafeInteger(candidate)
|
|
270
|
+
|| candidate < minimum
|
|
271
|
+
|| candidate > maximum
|
|
272
|
+
) return fail(label, `must be an integer from ${minimum} through ${maximum}`);
|
|
273
|
+
return candidate;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseContactsInput(
|
|
277
|
+
input: OperationInput,
|
|
278
|
+
contractVersion: number,
|
|
279
|
+
): ParsedContactsInput {
|
|
280
|
+
const source = record(input, "contacts.list input");
|
|
281
|
+
if (contractVersion !== 4 && contractVersion !== 5) {
|
|
282
|
+
return fail("contacts.list contract", "must be version 4 or 5");
|
|
283
|
+
}
|
|
284
|
+
if (contractVersion === 4 && source.include_dates !== undefined) {
|
|
285
|
+
return fail(
|
|
286
|
+
"contacts.list input.include_dates",
|
|
287
|
+
"is available only in contract v5",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
exactKeys(source, [], [
|
|
291
|
+
"collection",
|
|
292
|
+
"cursor",
|
|
293
|
+
...(contractVersion === 5 ? ["include_dates"] : []),
|
|
294
|
+
"include_stats",
|
|
295
|
+
"limit",
|
|
296
|
+
"stats_scan_limit",
|
|
297
|
+
], "contacts.list input");
|
|
298
|
+
const collection = source.collection === undefined ? "contacts" : source.collection;
|
|
299
|
+
if (collection !== "contacts" && collection !== "other-contacts") {
|
|
300
|
+
return fail(
|
|
301
|
+
"contacts.list input.collection",
|
|
302
|
+
"must be contacts or other-contacts",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const includeDates = contractVersion === 5 && source.include_dates !== undefined
|
|
306
|
+
? source.include_dates
|
|
307
|
+
: false;
|
|
308
|
+
if (typeof includeDates !== "boolean") {
|
|
309
|
+
return fail("contacts.list input.include_dates", "must be boolean");
|
|
310
|
+
}
|
|
311
|
+
if (includeDates && collection !== "contacts") {
|
|
312
|
+
return fail(
|
|
313
|
+
"contacts.list input.include_dates",
|
|
314
|
+
"is supported only for saved contacts",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const includeStats = source.include_stats === undefined
|
|
318
|
+
? true
|
|
319
|
+
: source.include_stats;
|
|
320
|
+
if (typeof includeStats !== "boolean") {
|
|
321
|
+
return fail("contacts.list input.include_stats", "must be boolean");
|
|
322
|
+
}
|
|
323
|
+
if (!includeStats && source.stats_scan_limit !== undefined) {
|
|
324
|
+
return fail(
|
|
325
|
+
"contacts.list input.stats_scan_limit",
|
|
326
|
+
"is accepted only when include_stats is true",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const limit = inputInteger(
|
|
330
|
+
source.limit,
|
|
331
|
+
"contacts.list input.limit",
|
|
332
|
+
CONTACTS_DEFAULT_LIMIT,
|
|
333
|
+
1,
|
|
334
|
+
CONTACTS_MAX_LIMIT,
|
|
335
|
+
);
|
|
336
|
+
const statsScanLimit = inputInteger(
|
|
337
|
+
source.stats_scan_limit,
|
|
338
|
+
"contacts.list input.stats_scan_limit",
|
|
339
|
+
CONTACTS_DEFAULT_STATS_SCAN_LIMIT,
|
|
340
|
+
1,
|
|
341
|
+
CONTACTS_MAX_STATS_SCAN_LIMIT,
|
|
342
|
+
);
|
|
343
|
+
if (
|
|
344
|
+
includeStats
|
|
345
|
+
&& limit * statsScanLimit > CONTACT_STATS_MAX_DIRECTION_PRODUCT
|
|
346
|
+
) {
|
|
347
|
+
return fail(
|
|
348
|
+
"contacts.list input",
|
|
349
|
+
`limit multiplied by stats_scan_limit must not exceed the ${CONTACT_STATS_MAX_DIRECTION_PRODUCT}-entry per-direction work budget`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return Object.freeze({
|
|
353
|
+
collection,
|
|
354
|
+
cursor: optionalInputText(source.cursor, "contacts.list input.cursor", 4_096),
|
|
355
|
+
projection: includeDates ? "dates" : "core",
|
|
356
|
+
includeStats,
|
|
357
|
+
limit,
|
|
358
|
+
statsScanLimit,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function parseInteractionsInput(input: OperationInput): ParsedInteractionsInput {
|
|
363
|
+
const source = record(input, "contacts.list interactions input");
|
|
364
|
+
exactKeys(
|
|
365
|
+
source,
|
|
366
|
+
["collection", "before"],
|
|
367
|
+
["after", "cursor", "limit"],
|
|
368
|
+
"contacts.list interactions input",
|
|
369
|
+
);
|
|
370
|
+
if (source.collection !== "interactions") {
|
|
371
|
+
return fail("contacts.list interactions input.collection", "must be interactions");
|
|
372
|
+
}
|
|
373
|
+
const before = boundedInputText(
|
|
374
|
+
source.before,
|
|
375
|
+
"contacts.list interactions input.before",
|
|
376
|
+
24,
|
|
377
|
+
);
|
|
378
|
+
const parsed = new Date(before);
|
|
379
|
+
if (
|
|
380
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/u.test(before)
|
|
381
|
+
|| !Number.isFinite(parsed.getTime())
|
|
382
|
+
|| parsed.toISOString() !== before
|
|
383
|
+
) {
|
|
384
|
+
return fail(
|
|
385
|
+
"contacts.list interactions input.before",
|
|
386
|
+
"must be a canonical whole-second UTC timestamp",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const after = source.after === undefined
|
|
390
|
+
? null
|
|
391
|
+
: boundedInputText(source.after, "contacts.list interactions input.after", 24);
|
|
392
|
+
const parsedAfter = after === null ? null : new Date(after);
|
|
393
|
+
if (
|
|
394
|
+
after !== null
|
|
395
|
+
&& (
|
|
396
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/u.test(after)
|
|
397
|
+
|| parsedAfter === null
|
|
398
|
+
|| !Number.isFinite(parsedAfter.getTime())
|
|
399
|
+
|| parsedAfter.toISOString() !== after
|
|
400
|
+
|| parsedAfter.getTime() >= parsed.getTime()
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
return fail(
|
|
404
|
+
"contacts.list interactions input.after",
|
|
405
|
+
"must be a canonical whole-second UTC timestamp before input.before",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
after,
|
|
410
|
+
afterMilliseconds: parsedAfter?.getTime() ?? null,
|
|
411
|
+
before,
|
|
412
|
+
beforeMilliseconds: parsed.getTime(),
|
|
413
|
+
cursor: optionalInputText(
|
|
414
|
+
source.cursor,
|
|
415
|
+
"contacts.list interactions input.cursor",
|
|
416
|
+
4_096,
|
|
417
|
+
),
|
|
418
|
+
limit: inputInteger(
|
|
419
|
+
source.limit,
|
|
420
|
+
"contacts.list interactions input.limit",
|
|
421
|
+
INTERACTIONS_DEFAULT_LIMIT,
|
|
422
|
+
1,
|
|
423
|
+
INTERACTIONS_MAX_LIMIT,
|
|
424
|
+
),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function parseMessagingListInput(input: OperationInput): ParsedMessagingListInput {
|
|
429
|
+
const source = record(input, "messaging.list input");
|
|
430
|
+
exactKeys(source, ["view"], [
|
|
431
|
+
"query",
|
|
432
|
+
"cursor",
|
|
433
|
+
"limit",
|
|
434
|
+
"include_spam_trash",
|
|
435
|
+
], "messaging.list input");
|
|
436
|
+
if (source.view !== "inbox" && source.view !== "search") {
|
|
437
|
+
return fail("messaging.list input.view", "must be inbox or search");
|
|
438
|
+
}
|
|
439
|
+
const query = optionalInputText(source.query, "messaging.list input.query", 512);
|
|
440
|
+
if (source.view === "search" && query === null) {
|
|
441
|
+
return fail("messaging.list input.query", "is required when view is search");
|
|
442
|
+
}
|
|
443
|
+
if (source.view === "inbox" && query !== null) {
|
|
444
|
+
return fail("messaging.list input.query", "is not accepted when view is inbox");
|
|
445
|
+
}
|
|
446
|
+
if (query !== null && query.trim().length === 0) {
|
|
447
|
+
return fail("messaging.list input.query", "must contain a non-whitespace expression");
|
|
448
|
+
}
|
|
449
|
+
const includeSpamTrash = source.include_spam_trash === undefined
|
|
450
|
+
? false
|
|
451
|
+
: source.include_spam_trash;
|
|
452
|
+
if (typeof includeSpamTrash !== "boolean") {
|
|
453
|
+
return fail("messaging.list input.include_spam_trash", "must be boolean");
|
|
454
|
+
}
|
|
455
|
+
if (source.view === "inbox" && source.include_spam_trash !== undefined) {
|
|
456
|
+
return fail(
|
|
457
|
+
"messaging.list input.include_spam_trash",
|
|
458
|
+
"is accepted only when view is search",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return Object.freeze({
|
|
462
|
+
view: source.view,
|
|
463
|
+
query,
|
|
464
|
+
cursor: optionalInputText(source.cursor, "messaging.list input.cursor", 4_096),
|
|
465
|
+
limit: inputInteger(
|
|
466
|
+
source.limit,
|
|
467
|
+
"messaging.list input.limit",
|
|
468
|
+
MESSAGING_DEFAULT_LIMIT,
|
|
469
|
+
1,
|
|
470
|
+
MESSAGING_MAX_LIMIT,
|
|
471
|
+
),
|
|
472
|
+
includeSpamTrash,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function parseMessagingReadInput(input: OperationInput): string {
|
|
477
|
+
const source = record(input, "messaging.read input");
|
|
478
|
+
exactKeys(source, ["thread_id"], [], "messaging.read input");
|
|
479
|
+
return parseGmailId(source.thread_id, "messaging.read input.thread_id");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function authenticatedClient(context: ProviderActionContext): Promise<{
|
|
483
|
+
readonly client: GmailApiClient;
|
|
484
|
+
readonly profile: GmailProfile;
|
|
485
|
+
}> {
|
|
486
|
+
if (context.auth.subject === undefined) {
|
|
487
|
+
throw new Error("official Gmail reads require an OAuth locator with an exact email subject");
|
|
488
|
+
}
|
|
489
|
+
const client = createGmailApiClient({
|
|
490
|
+
http: context.http,
|
|
491
|
+
accessToken: context.token.accessToken,
|
|
492
|
+
subject: context.auth.subject,
|
|
493
|
+
});
|
|
494
|
+
const profile = await getAuthenticatedGmailProfile(client);
|
|
495
|
+
return Object.freeze({ client, profile });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function mapConcurrent<T, R>(
|
|
499
|
+
values: readonly T[],
|
|
500
|
+
maximum: number,
|
|
501
|
+
worker: (
|
|
502
|
+
value: T,
|
|
503
|
+
index: number,
|
|
504
|
+
admit: ConcurrentAdmission,
|
|
505
|
+
) => Promise<R>,
|
|
506
|
+
): Promise<readonly R[]> {
|
|
507
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1) {
|
|
508
|
+
throw new Error("Gmail concurrent mapping requires a positive worker bound");
|
|
509
|
+
}
|
|
510
|
+
const results = new Array<R>(values.length);
|
|
511
|
+
let nextIndex = 0;
|
|
512
|
+
const noFailure = Symbol("no Gmail concurrent failure");
|
|
513
|
+
let firstFailure: unknown = noFailure;
|
|
514
|
+
const rememberFailure = (error: unknown): void => {
|
|
515
|
+
if (firstFailure === noFailure) firstFailure = error;
|
|
516
|
+
};
|
|
517
|
+
const admit: ConcurrentAdmission = async <V>(action: () => Promise<V>): Promise<V> => {
|
|
518
|
+
if (firstFailure !== noFailure) throw firstFailure;
|
|
519
|
+
try {
|
|
520
|
+
return await action();
|
|
521
|
+
} catch (error) {
|
|
522
|
+
rememberFailure(error);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const run = async (): Promise<void> => {
|
|
527
|
+
while (firstFailure === noFailure && nextIndex < values.length) {
|
|
528
|
+
const index = nextIndex;
|
|
529
|
+
nextIndex += 1;
|
|
530
|
+
const value = values[index];
|
|
531
|
+
if (value === undefined) {
|
|
532
|
+
rememberFailure(new Error("Gmail concurrent mapping lost a dense input item"));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
results[index] = await worker(value, index, admit);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
rememberFailure(error);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const settlements = await Promise.allSettled(Array.from(
|
|
544
|
+
{ length: Math.min(maximum, values.length) },
|
|
545
|
+
() => run(),
|
|
546
|
+
));
|
|
547
|
+
for (const settlement of settlements) {
|
|
548
|
+
if (settlement.status === "rejected") rememberFailure(settlement.reason);
|
|
549
|
+
}
|
|
550
|
+
if (firstFailure !== noFailure) throw firstFailure;
|
|
551
|
+
return Object.freeze(results);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function contactEmailCoverage(contact: GmailContact): ContactEmailCoverage {
|
|
555
|
+
const emails = Object.freeze([...new Set(contact.emailAddresses.flatMap((email) =>
|
|
556
|
+
email.canonicalValue === null ? [] : [email.canonicalValue]))]);
|
|
557
|
+
const unsupportedAddressCount = contact.emailAddresses.reduce(
|
|
558
|
+
(count, email) => count + (email.canonicalValue === null ? 1 : 0),
|
|
559
|
+
0,
|
|
560
|
+
);
|
|
561
|
+
return Object.freeze({
|
|
562
|
+
emails,
|
|
563
|
+
kind: contact.emailAddresses.length === 0
|
|
564
|
+
? "unavailable"
|
|
565
|
+
: emails.length === 0
|
|
566
|
+
? "unsupported"
|
|
567
|
+
: unsupportedAddressCount === 0
|
|
568
|
+
? "complete"
|
|
569
|
+
: "partial",
|
|
570
|
+
unsupportedAddressCount,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function contactQuery(direction: ContactDirection, emails: readonly string[]): string {
|
|
575
|
+
const terms = direction === "received"
|
|
576
|
+
? emails.map((email) => `from:"${email}"`)
|
|
577
|
+
: emails.flatMap((email) => [`to:"${email}"`, `cc:"${email}"`, `bcc:"${email}"`]);
|
|
578
|
+
const prefix = direction === "received" ? "-in:sent " : "in:sent ";
|
|
579
|
+
return `${prefix}{${terms.join(" ")}}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function contactQueryChunks(
|
|
583
|
+
direction: ContactDirection,
|
|
584
|
+
emails: readonly string[],
|
|
585
|
+
): readonly string[] {
|
|
586
|
+
const chunks: string[][] = [];
|
|
587
|
+
let current: string[] = [];
|
|
588
|
+
for (const email of emails) {
|
|
589
|
+
const candidate = [...current, email];
|
|
590
|
+
if (
|
|
591
|
+
current.length > 0
|
|
592
|
+
&& Buffer.byteLength(contactQuery(direction, candidate), "utf8") > CONTACT_STATS_QUERY_MAX_BYTES
|
|
593
|
+
) {
|
|
594
|
+
chunks.push(current);
|
|
595
|
+
current = [email];
|
|
596
|
+
} else current = candidate;
|
|
597
|
+
if (Buffer.byteLength(contactQuery(direction, current), "utf8") > CONTACT_STATS_QUERY_MAX_BYTES) {
|
|
598
|
+
return fail("contacts.list stats query", "cannot represent one validated contact address within the query bound");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (current.length > 0) chunks.push(current);
|
|
602
|
+
return Object.freeze(chunks.map((chunk) => contactQuery(direction, chunk)));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function scanContactDirection(
|
|
606
|
+
client: GmailApiClient,
|
|
607
|
+
addressCoverage: ContactEmailCoverage,
|
|
608
|
+
direction: ContactDirection,
|
|
609
|
+
scanLimit: number,
|
|
610
|
+
admit: ConcurrentAdmission,
|
|
611
|
+
): Promise<ContactDirectionStats> {
|
|
612
|
+
const queries = contactQueryChunks(direction, addressCoverage.emails);
|
|
613
|
+
if (queries.length === 0) {
|
|
614
|
+
const addressCoverageComplete = addressCoverage.kind === "complete";
|
|
615
|
+
const incompleteReasons: readonly GmailDirectionStatsIncompleteReason[] = addressCoverageComplete
|
|
616
|
+
? Object.freeze([])
|
|
617
|
+
: addressCoverage.kind === "unavailable"
|
|
618
|
+
? Object.freeze(["no-contact-addresses"] as const)
|
|
619
|
+
: Object.freeze(["unsupported-contact-addresses"] as const);
|
|
620
|
+
return parseContactDirectionStats({
|
|
621
|
+
count: 0,
|
|
622
|
+
complete: addressCoverageComplete,
|
|
623
|
+
lowerBound: !addressCoverageComplete,
|
|
624
|
+
truncated: false,
|
|
625
|
+
lastAt: null,
|
|
626
|
+
lastAtComplete: addressCoverageComplete,
|
|
627
|
+
lastAtBasis: "unavailable",
|
|
628
|
+
incompleteReasons,
|
|
629
|
+
}, "official Gmail contacts.list direction stats");
|
|
630
|
+
}
|
|
631
|
+
const messageIds = new Set<string>();
|
|
632
|
+
let scannedEntries = 0;
|
|
633
|
+
let complete = true;
|
|
634
|
+
for (const [queryIndex, query] of queries.entries()) {
|
|
635
|
+
let cursor: string | null = null;
|
|
636
|
+
const seenCursors = new Set<string>();
|
|
637
|
+
while (scannedEntries < scanLimit) {
|
|
638
|
+
const pageLimit = Math.min(GMAIL_LIST_PAGE_MAXIMUM, scanLimit - scannedEntries);
|
|
639
|
+
const page = await admit(() => fetchGmailMessageList(client, {
|
|
640
|
+
limit: pageLimit,
|
|
641
|
+
pageToken: cursor,
|
|
642
|
+
query,
|
|
643
|
+
includeSpamTrash: false,
|
|
644
|
+
}));
|
|
645
|
+
if (page.messages.length === 0 && page.nextPageToken !== null) {
|
|
646
|
+
return fail("contacts.list stats pagination", "returned an empty non-terminal page");
|
|
647
|
+
}
|
|
648
|
+
scannedEntries += page.messages.length;
|
|
649
|
+
for (const message of page.messages) messageIds.add(message.id);
|
|
650
|
+
if (page.nextPageToken === null) break;
|
|
651
|
+
if (seenCursors.has(page.nextPageToken)) {
|
|
652
|
+
return fail("contacts.list stats pagination", "repeated a page token");
|
|
653
|
+
}
|
|
654
|
+
seenCursors.add(page.nextPageToken);
|
|
655
|
+
cursor = page.nextPageToken;
|
|
656
|
+
if (scannedEntries >= scanLimit) {
|
|
657
|
+
complete = false;
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (scannedEntries >= scanLimit && queryIndex < queries.length - 1) complete = false;
|
|
662
|
+
if (!complete) break;
|
|
663
|
+
}
|
|
664
|
+
let lastAt: string | null = null;
|
|
665
|
+
let metadataDatesComplete = true;
|
|
666
|
+
for (const messageId of messageIds) {
|
|
667
|
+
const message = await admit(() => fetchGmailMessageMetadata(client, messageId));
|
|
668
|
+
if (message.id !== messageId) {
|
|
669
|
+
return fail("contacts.list stats metadata", "returned a message other than the requested ID");
|
|
670
|
+
}
|
|
671
|
+
if (message.internalDate === null) metadataDatesComplete = false;
|
|
672
|
+
else if (lastAt === null || message.internalDate > lastAt) {
|
|
673
|
+
lastAt = message.internalDate;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const addressCoverageComplete = addressCoverage.kind === "complete";
|
|
677
|
+
const countComplete = complete && addressCoverageComplete;
|
|
678
|
+
const lastAtComplete = countComplete && metadataDatesComplete;
|
|
679
|
+
const incompleteReasons: readonly GmailDirectionStatsIncompleteReason[] = Object.freeze([
|
|
680
|
+
...(addressCoverageComplete ? [] : ["unsupported-contact-addresses" as const]),
|
|
681
|
+
...(complete ? [] : ["scan-limit-reached" as const]),
|
|
682
|
+
...(metadataDatesComplete ? [] : ["message-internal-date-unavailable" as const]),
|
|
683
|
+
]);
|
|
684
|
+
return parseContactDirectionStats({
|
|
685
|
+
count: messageIds.size,
|
|
686
|
+
complete: countComplete,
|
|
687
|
+
lowerBound: !countComplete,
|
|
688
|
+
truncated: !complete,
|
|
689
|
+
lastAt,
|
|
690
|
+
lastAtComplete,
|
|
691
|
+
lastAtBasis: lastAt === null ? "unavailable" : "bounded-matched-message-internal-date",
|
|
692
|
+
incompleteReasons,
|
|
693
|
+
}, "official Gmail contacts.list direction stats");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function executeContactsList(context: ProviderActionContext): Promise<void> {
|
|
697
|
+
const input = parseContactsInput(
|
|
698
|
+
context.input,
|
|
699
|
+
context.recipe.contractVersion,
|
|
700
|
+
);
|
|
701
|
+
const { client, profile } = await authenticatedClient(context);
|
|
702
|
+
const page = await fetchGmailContacts(client, {
|
|
703
|
+
collection: input.collection,
|
|
704
|
+
projection: input.projection,
|
|
705
|
+
limit: input.limit,
|
|
706
|
+
pageToken: input.cursor,
|
|
707
|
+
});
|
|
708
|
+
const tasks = input.includeStats ? page.contacts.flatMap((contact) => {
|
|
709
|
+
const addressCoverage = contactEmailCoverage(contact);
|
|
710
|
+
return (["sent", "received"] as const).map((direction) => Object.freeze({
|
|
711
|
+
addressCoverage,
|
|
712
|
+
resourceName: contact.resourceName,
|
|
713
|
+
direction,
|
|
714
|
+
}));
|
|
715
|
+
}) : [];
|
|
716
|
+
const stats = await mapConcurrent(
|
|
717
|
+
tasks,
|
|
718
|
+
MAX_CONCURRENT_GMAIL_READS,
|
|
719
|
+
async (task, _index, admit) => Object.freeze({
|
|
720
|
+
resourceName: task.resourceName,
|
|
721
|
+
direction: task.direction,
|
|
722
|
+
stats: await scanContactDirection(
|
|
723
|
+
client,
|
|
724
|
+
task.addressCoverage,
|
|
725
|
+
task.direction,
|
|
726
|
+
input.statsScanLimit,
|
|
727
|
+
admit,
|
|
728
|
+
),
|
|
729
|
+
}),
|
|
730
|
+
);
|
|
731
|
+
const byContact = new Map<string, Partial<Record<ContactDirection, ContactDirectionStats>>>();
|
|
732
|
+
for (const entry of stats) {
|
|
733
|
+
const current = byContact.get(entry.resourceName) ?? {};
|
|
734
|
+
current[entry.direction] = entry.stats;
|
|
735
|
+
byContact.set(entry.resourceName, current);
|
|
736
|
+
}
|
|
737
|
+
const contacts = page.contacts.map((contact) => {
|
|
738
|
+
if (!input.includeStats) return contact;
|
|
739
|
+
const values = byContact.get(contact.resourceName);
|
|
740
|
+
if (values?.sent === undefined || values.received === undefined) {
|
|
741
|
+
return fail("contacts.list stats", "did not settle both directions for one contact");
|
|
742
|
+
}
|
|
743
|
+
const addressCoverage = contactEmailCoverage(contact);
|
|
744
|
+
return Object.freeze({
|
|
745
|
+
...contact,
|
|
746
|
+
...projectContactDirectionStats(values.sent, values.received),
|
|
747
|
+
statsAddressCoverage: addressCoverage.kind,
|
|
748
|
+
statsSupportedAddressCount: addressCoverage.emails.length,
|
|
749
|
+
statsUnsupportedAddressCount: addressCoverage.unsupportedAddressCount,
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
context.setOutput(Object.freeze({
|
|
753
|
+
provider: "gmail",
|
|
754
|
+
operation: "contacts.list",
|
|
755
|
+
accountSubject: profile.emailAddress,
|
|
756
|
+
contactCollection: input.collection,
|
|
757
|
+
statsIncluded: input.includeStats,
|
|
758
|
+
contacts: Object.freeze(contacts),
|
|
759
|
+
nextCursor: page.nextPageToken,
|
|
760
|
+
totalItems: page.totalItems,
|
|
761
|
+
statsScanLimit: input.includeStats ? input.statsScanLimit : null,
|
|
762
|
+
statsScope: input.includeStats
|
|
763
|
+
? "per-contact-gmail-search-excluding-spam-trash"
|
|
764
|
+
: "not-requested",
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
type MutableInteractionDirection = {
|
|
769
|
+
count: number;
|
|
770
|
+
complete: boolean;
|
|
771
|
+
firstAt: string | null;
|
|
772
|
+
lastAt: string | null;
|
|
773
|
+
count30d: number;
|
|
774
|
+
count90d: number;
|
|
775
|
+
count365d: number;
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
type MutableContactInteraction = {
|
|
779
|
+
sent: MutableInteractionDirection;
|
|
780
|
+
received: MutableInteractionDirection;
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
function newInteractionDirection(): MutableInteractionDirection {
|
|
784
|
+
return {
|
|
785
|
+
count: 0,
|
|
786
|
+
complete: true,
|
|
787
|
+
firstAt: null,
|
|
788
|
+
lastAt: null,
|
|
789
|
+
count30d: 0,
|
|
790
|
+
count90d: 0,
|
|
791
|
+
count365d: 0,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function interactionFor(
|
|
796
|
+
interactions: Map<string, MutableContactInteraction>,
|
|
797
|
+
email: string,
|
|
798
|
+
): MutableContactInteraction {
|
|
799
|
+
const present = interactions.get(email);
|
|
800
|
+
if (present !== undefined) return present;
|
|
801
|
+
if (interactions.size >= INTERACTIONS_MAX_ADDRESSES) {
|
|
802
|
+
return fail(
|
|
803
|
+
"contacts.list interactions response",
|
|
804
|
+
`contains more than ${INTERACTIONS_MAX_ADDRESSES} external addresses in one page`,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const created = {
|
|
808
|
+
sent: newInteractionDirection(),
|
|
809
|
+
received: newInteractionDirection(),
|
|
810
|
+
};
|
|
811
|
+
interactions.set(email, created);
|
|
812
|
+
return created;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function messageInteractionAddresses(
|
|
816
|
+
message: GmailThread["messages"][number],
|
|
817
|
+
accountAddresses: ReadonlySet<string>,
|
|
818
|
+
direction: ContactDirection,
|
|
819
|
+
): readonly string[] {
|
|
820
|
+
const headers = direction === "sent"
|
|
821
|
+
? [message.to, message.cc, message.bcc]
|
|
822
|
+
: [message.from];
|
|
823
|
+
return Object.freeze([...new Set(headers.flatMap((header) =>
|
|
824
|
+
extractGmailEmailAddresses(header)))].filter((email) => !accountAddresses.has(email)));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function recordDatedInteraction(
|
|
828
|
+
direction: MutableInteractionDirection,
|
|
829
|
+
timestamp: string,
|
|
830
|
+
beforeMilliseconds: number,
|
|
831
|
+
): void {
|
|
832
|
+
const milliseconds = new Date(timestamp).getTime();
|
|
833
|
+
direction.count += 1;
|
|
834
|
+
if (direction.firstAt === null || timestamp < direction.firstAt) direction.firstAt = timestamp;
|
|
835
|
+
if (direction.lastAt === null || timestamp > direction.lastAt) direction.lastAt = timestamp;
|
|
836
|
+
const age = beforeMilliseconds - milliseconds;
|
|
837
|
+
if (age <= 30 * 86_400_000) direction.count30d += 1;
|
|
838
|
+
if (age <= 90 * 86_400_000) direction.count90d += 1;
|
|
839
|
+
if (age <= 365 * 86_400_000) direction.count365d += 1;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function opaqueGmailKey(kind: "message" | "thread", id: string): string {
|
|
843
|
+
return createHash("sha256").update(`gmail-${kind}\0${id}`, "utf8").digest("hex");
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function projectInteraction(
|
|
847
|
+
email: string,
|
|
848
|
+
interaction: MutableContactInteraction,
|
|
849
|
+
): GmailContactInteraction {
|
|
850
|
+
return Object.freeze({
|
|
851
|
+
email,
|
|
852
|
+
sentCount: interaction.sent.count,
|
|
853
|
+
receivedCount: interaction.received.count,
|
|
854
|
+
sentCountComplete: interaction.sent.complete,
|
|
855
|
+
receivedCountComplete: interaction.received.complete,
|
|
856
|
+
firstSentAt: interaction.sent.firstAt,
|
|
857
|
+
lastSentAt: interaction.sent.lastAt,
|
|
858
|
+
firstReceivedAt: interaction.received.firstAt,
|
|
859
|
+
lastReceivedAt: interaction.received.lastAt,
|
|
860
|
+
sent30d: interaction.sent.count30d,
|
|
861
|
+
sent90d: interaction.sent.count90d,
|
|
862
|
+
sent365d: interaction.sent.count365d,
|
|
863
|
+
received30d: interaction.received.count30d,
|
|
864
|
+
received90d: interaction.received.count90d,
|
|
865
|
+
received365d: interaction.received.count365d,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async function executeContactInteractionsList(
|
|
870
|
+
context: ProviderActionContext,
|
|
871
|
+
): Promise<void> {
|
|
872
|
+
const input = parseInteractionsInput(context.input);
|
|
873
|
+
const { client, profile } = await authenticatedClient(context);
|
|
874
|
+
const accountAddresses = input.cursor === null
|
|
875
|
+
? await listGmailSendAsAliases(client)
|
|
876
|
+
: Object.freeze([] as string[]);
|
|
877
|
+
const page = await fetchGmailMessageList(client, {
|
|
878
|
+
limit: input.limit,
|
|
879
|
+
pageToken: input.cursor,
|
|
880
|
+
query: input.afterMilliseconds === null
|
|
881
|
+
? `before:${String(input.beforeMilliseconds / 1_000)}`
|
|
882
|
+
: `after:${String(input.afterMilliseconds / 1_000 - 1)} before:${String(input.beforeMilliseconds / 1_000)}`,
|
|
883
|
+
includeSpamTrash: false,
|
|
884
|
+
});
|
|
885
|
+
const messages = await mapConcurrent(
|
|
886
|
+
page.messages,
|
|
887
|
+
MAX_CONCURRENT_GMAIL_READS,
|
|
888
|
+
async (stub, _index, admit) => {
|
|
889
|
+
const message = await admit(() =>
|
|
890
|
+
fetchGmailMessageInteractionMetadata(client, stub.id));
|
|
891
|
+
if (message.id !== stub.id) {
|
|
892
|
+
return fail(
|
|
893
|
+
"contacts.list interactions metadata",
|
|
894
|
+
"returned a message other than the requested ID",
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
stub.threadId !== null
|
|
899
|
+
&& message.threadId !== null
|
|
900
|
+
&& stub.threadId !== message.threadId
|
|
901
|
+
) {
|
|
902
|
+
return fail(
|
|
903
|
+
"contacts.list interactions metadata",
|
|
904
|
+
"returned a message bound to a different thread",
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
return message;
|
|
908
|
+
},
|
|
909
|
+
);
|
|
910
|
+
const accountSubject = profile.emailAddress.toLowerCase();
|
|
911
|
+
const selfAddresses = new Set([accountSubject, ...accountAddresses]);
|
|
912
|
+
const interactions = new Map<string, MutableContactInteraction>();
|
|
913
|
+
const threadKeys = new Set<string>();
|
|
914
|
+
let messagesIncluded = 0;
|
|
915
|
+
let draftOrChat = 0;
|
|
916
|
+
let missingInternalDate = 0;
|
|
917
|
+
let noExternalAddress = 0;
|
|
918
|
+
let outsideWindow = 0;
|
|
919
|
+
for (const message of messages) {
|
|
920
|
+
if (message.threadId !== null) {
|
|
921
|
+
threadKeys.add(opaqueGmailKey("thread", message.threadId));
|
|
922
|
+
}
|
|
923
|
+
if (message.labelIds.includes("DRAFT") || message.labelIds.includes("CHAT")) {
|
|
924
|
+
draftOrChat += 1;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const direction: ContactDirection = message.labelIds.includes("SENT")
|
|
928
|
+
? "sent"
|
|
929
|
+
: "received";
|
|
930
|
+
const addresses = messageInteractionAddresses(message, selfAddresses, direction);
|
|
931
|
+
if (addresses.length === 0) {
|
|
932
|
+
noExternalAddress += 1;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if (message.internalDate === null) {
|
|
936
|
+
missingInternalDate += 1;
|
|
937
|
+
for (const email of addresses) {
|
|
938
|
+
interactionFor(interactions, email)[direction].complete = false;
|
|
939
|
+
}
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
if (
|
|
943
|
+
input.afterMilliseconds !== null
|
|
944
|
+
&& new Date(message.internalDate).getTime() < input.afterMilliseconds
|
|
945
|
+
) {
|
|
946
|
+
outsideWindow += 1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (new Date(message.internalDate).getTime() >= input.beforeMilliseconds) {
|
|
950
|
+
return fail(
|
|
951
|
+
"contacts.list interactions metadata",
|
|
952
|
+
"returned a message outside the requested before cutoff",
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
messagesIncluded += 1;
|
|
956
|
+
for (const email of addresses) {
|
|
957
|
+
recordDatedInteraction(
|
|
958
|
+
interactionFor(interactions, email)[direction],
|
|
959
|
+
message.internalDate,
|
|
960
|
+
input.beforeMilliseconds,
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const output: GmailContactInteractionsListOutput = Object.freeze({
|
|
965
|
+
provider: "gmail",
|
|
966
|
+
operation: "contacts.list",
|
|
967
|
+
contactCollection: "interactions",
|
|
968
|
+
accountSubject,
|
|
969
|
+
accountAddresses,
|
|
970
|
+
after: input.after,
|
|
971
|
+
before: input.before,
|
|
972
|
+
interactions: Object.freeze([...interactions.entries()]
|
|
973
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
974
|
+
.map(([email, interaction]) => projectInteraction(email, interaction))),
|
|
975
|
+
messageKeys: Object.freeze(page.messages.map((message) =>
|
|
976
|
+
opaqueGmailKey("message", message.id))),
|
|
977
|
+
threadKeys: Object.freeze([...threadKeys].sort()),
|
|
978
|
+
messagesScanned: messages.length,
|
|
979
|
+
messagesIncluded,
|
|
980
|
+
messagesSkipped: Object.freeze({
|
|
981
|
+
draftOrChat,
|
|
982
|
+
missingInternalDate,
|
|
983
|
+
noExternalAddress,
|
|
984
|
+
outsideWindow,
|
|
985
|
+
}),
|
|
986
|
+
nextCursor: page.nextPageToken,
|
|
987
|
+
resultSizeEstimate: page.resultSizeEstimate,
|
|
988
|
+
scanScope: "messages-in-half-open-window-excluding-spam-trash-drafts-chats",
|
|
989
|
+
});
|
|
990
|
+
context.setOutput(output);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function threadOrderedAt(thread: GmailThread): string | null {
|
|
994
|
+
let result: string | null = null;
|
|
995
|
+
for (const message of thread.messages) {
|
|
996
|
+
if (message.internalDate !== null && (result === null || message.internalDate > result)) {
|
|
997
|
+
result = message.internalDate;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function threadParticipants(
|
|
1004
|
+
thread: GmailThread,
|
|
1005
|
+
accountSubject: string,
|
|
1006
|
+
): GmailThreadSummary["participants"] {
|
|
1007
|
+
const account = accountSubject.toLowerCase();
|
|
1008
|
+
const emails = new Set<string>();
|
|
1009
|
+
for (const message of thread.messages) {
|
|
1010
|
+
for (const header of [message.from, message.to, message.cc, message.bcc]) {
|
|
1011
|
+
for (const email of extractGmailEmailAddresses(header)) {
|
|
1012
|
+
if (email !== account) emails.add(email);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return Object.freeze([...emails].sort().map((email) => Object.freeze({
|
|
1017
|
+
email,
|
|
1018
|
+
displayName: null,
|
|
1019
|
+
})));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function summarizeThread(
|
|
1023
|
+
thread: GmailThread,
|
|
1024
|
+
accountSubject: string,
|
|
1025
|
+
view: GmailListView,
|
|
1026
|
+
): GmailThreadSummary {
|
|
1027
|
+
const labels = new Set(thread.messages.flatMap((message) => message.labelIds));
|
|
1028
|
+
const archived = labels.has("INBOX")
|
|
1029
|
+
? false
|
|
1030
|
+
: labels.has("SPAM") || labels.has("TRASH")
|
|
1031
|
+
? null
|
|
1032
|
+
: true;
|
|
1033
|
+
return Object.freeze({
|
|
1034
|
+
id: thread.id,
|
|
1035
|
+
historyId: thread.historyId,
|
|
1036
|
+
snippet: thread.snippet,
|
|
1037
|
+
subject: thread.messages.find((message) => message.subject !== null)?.subject ?? null,
|
|
1038
|
+
orderedAt: threadOrderedAt(thread),
|
|
1039
|
+
messageCount: thread.messages.length,
|
|
1040
|
+
participants: threadParticipants(thread, accountSubject),
|
|
1041
|
+
unread: labels.has("UNREAD"),
|
|
1042
|
+
archived,
|
|
1043
|
+
threadUrl: buildGmailThreadUrl(
|
|
1044
|
+
accountSubject,
|
|
1045
|
+
thread.id,
|
|
1046
|
+
view === "inbox" ? "inbox" : "all",
|
|
1047
|
+
),
|
|
1048
|
+
readInput: Object.freeze({ thread_id: thread.id }),
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
async function executeMessagingList(context: ProviderActionContext): Promise<void> {
|
|
1053
|
+
const input = parseMessagingListInput(context.input);
|
|
1054
|
+
const { client, profile } = await authenticatedClient(context);
|
|
1055
|
+
const page = await fetchGmailThreadList(client, {
|
|
1056
|
+
limit: input.limit,
|
|
1057
|
+
pageToken: input.cursor,
|
|
1058
|
+
query: input.query,
|
|
1059
|
+
labelIds: input.view === "inbox" ? ["INBOX"] : [],
|
|
1060
|
+
includeSpamTrash: input.includeSpamTrash,
|
|
1061
|
+
});
|
|
1062
|
+
const threads = await mapConcurrent(
|
|
1063
|
+
page.threads,
|
|
1064
|
+
MAX_CONCURRENT_GMAIL_READS,
|
|
1065
|
+
async (stub, _index, admit) => {
|
|
1066
|
+
const thread = parseGmailThread(
|
|
1067
|
+
await admit(() => fetchGmailThreadMetadata(client, stub.id)),
|
|
1068
|
+
{ deadlineCheckpoint: () => client.http.throwIfUnavailable() },
|
|
1069
|
+
);
|
|
1070
|
+
if (thread.id !== stub.id) {
|
|
1071
|
+
return fail("messaging.list metadata", "returned a thread other than the requested ID");
|
|
1072
|
+
}
|
|
1073
|
+
return summarizeThread(thread, profile.emailAddress, input.view);
|
|
1074
|
+
},
|
|
1075
|
+
);
|
|
1076
|
+
const output: GmailMessagingListOutput = Object.freeze({
|
|
1077
|
+
provider: "gmail",
|
|
1078
|
+
operation: "messaging.list",
|
|
1079
|
+
accountSubject: profile.emailAddress,
|
|
1080
|
+
view: input.view,
|
|
1081
|
+
query: input.query,
|
|
1082
|
+
includeSpamTrash: input.includeSpamTrash,
|
|
1083
|
+
threads,
|
|
1084
|
+
nextCursor: page.nextPageToken,
|
|
1085
|
+
resultSizeEstimate: page.resultSizeEstimate,
|
|
1086
|
+
});
|
|
1087
|
+
context.setOutput(output);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function executeMessagingRead(context: ProviderActionContext): Promise<void> {
|
|
1091
|
+
const threadId = parseMessagingReadInput(context.input);
|
|
1092
|
+
const { client, profile } = await authenticatedClient(context);
|
|
1093
|
+
const parsedThread = parseGmailThread(
|
|
1094
|
+
await fetchGmailThread(client, threadId),
|
|
1095
|
+
{
|
|
1096
|
+
maxBodyBytes: GMAIL_MESSAGING_READ_BODY_BYTES,
|
|
1097
|
+
deadlineCheckpoint: () => client.http.throwIfUnavailable(),
|
|
1098
|
+
},
|
|
1099
|
+
);
|
|
1100
|
+
const thread = await resolveGmailThreadBodies(client, parsedThread);
|
|
1101
|
+
if (thread.id !== threadId) {
|
|
1102
|
+
return fail("messaging.read response", "returned a thread other than the requested ID");
|
|
1103
|
+
}
|
|
1104
|
+
if (thread.messages.length === 0) {
|
|
1105
|
+
return fail("messaging.read response.messages", "must contain at least one message");
|
|
1106
|
+
}
|
|
1107
|
+
const threadUrl = buildGmailThreadUrl(profile.emailAddress, thread.id);
|
|
1108
|
+
const output: GmailMessagingReadOutput = Object.freeze({
|
|
1109
|
+
provider: "gmail",
|
|
1110
|
+
operation: "messaging.read",
|
|
1111
|
+
accountSubject: profile.emailAddress,
|
|
1112
|
+
thread,
|
|
1113
|
+
threadUrl,
|
|
1114
|
+
});
|
|
1115
|
+
context.setOutput(output);
|
|
1116
|
+
context.setFinalUrl(threadUrl);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Execute one reviewed official Gmail or People read without acknowledgement mutation. */
|
|
1120
|
+
export async function executeGmailProvider(context: ProviderActionContext): Promise<void> {
|
|
1121
|
+
const action = context.recipe.action;
|
|
1122
|
+
if (action === "contacts.list") {
|
|
1123
|
+
const source = record(context.input, "contacts.list input");
|
|
1124
|
+
if (source.collection === "interactions") await executeContactInteractionsList(context);
|
|
1125
|
+
else await executeContactsList(context);
|
|
1126
|
+
}
|
|
1127
|
+
else if (action === "messaging.list") await executeMessagingList(context);
|
|
1128
|
+
else if (action === "messaging.read") await executeMessagingRead(context);
|
|
1129
|
+
else throw new Error(`official Gmail provider does not implement ${action}`);
|
|
1130
|
+
}
|