Skip to content

Commit 8777ff4

Browse files
authored
feat: official Follow Intent and audience insights (#4)
* chore(manifest): define audience API contracts * feat(core): add audience domain contracts * feat(cli): add official follow intent * fix(auth): harden audience reauthorization * feat(provider): add official audience endpoints * feat(store): persist audience snapshots * feat(store): rank observed engagement * feat(ingest): refresh audience data * feat(cli): render audience reports * feat(cli): add audience commands * test(cli): cover audience workflows end to end * docs: document audience and follow support * refactor(core): split model into domain modules and add demographic insight * refactor(store): split query module and harden private file I/O * refactor(provider): split client and provider modules; correct DTOs, paths, and retry * fix(auth): harden token store, config, and local secret handling * refactor(ingest): split orchestrator and correct audience refresh * fix(cli): correct audience output and recovery; update tests and docs * fix(store): exclude quote posts from engagement ranking * fix(auth): add oauth endpoint seams and guide insights permission denial * test(ingest): assert repeated audience refresh is idempotent * fix(provider): forward client-selected mentions page size and correct docs * fix(auth): gate oauth endpoint injection behind test-support * fix(core): map permission denials to typed permission requirements
1 parent 0f0c7eb commit 8777ff4

103 files changed

Lines changed: 10941 additions & 2886 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 75 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ csv = "1.3"
4747

4848
# cli
4949
clap = { version = "4.5", features = ["derive", "env", "wrap_help"] }
50+
open = "5"
5051

5152
# storage
5253
rusqlite = { version = "0.33", features = ["bundled", "chrono", "serde_json"] }
@@ -64,6 +65,7 @@ keyring = { version = "3", default-features = false, features = [
6465
"sync-secret-service",
6566
] }
6667
dirs = "5"
68+
libc = "0.2"
6769

6870
# test helpers
6971
tempfile = "3"

README.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ A Rust CLI for ingesting, modeling, searching, and exporting
1212
1313
## Status
1414

15-
Phase 0 foundation scaffolding. See
15+
Audience snapshots, observed-engagement queries, and the official Follow
16+
Intent command are implemented. Private follower enumeration and automated
17+
follow remain unsupported. See
1618
[`docs/architecture.md`](docs/architecture.md) and
1719
[`threads_api_cli_prd_correction.md`](threads_api_cli_prd_correction.md).
1820

@@ -37,9 +39,17 @@ cargo build --workspace
3739
cargo test --workspace
3840
```
3941

42+
## Token persistence
43+
44+
`auth login` always atomically mirrors the access token and its metadata to a
45+
private token file, then saves the same value to Keychain on a best-effort
46+
basis. Reads are file-first; Keychain is consulted only when the private file
47+
is absent. On Unix, the file is accepted only when it is an owner-only regular
48+
file in an owner-controlled directory that is not group- or world-writable.
49+
4050
## Commands
4151

42-
Read-only ingest + query (always safe):
52+
Local ingest/query and user-mediated intent commands:
4353

4454
```
4555
threads-cli init
@@ -48,8 +58,43 @@ threads-cli ingest me | thread <post_id> | engagement [--depth N]
4858
threads-cli show <post_id> [--thread]
4959
threads-cli search "<query>"
5060
threads-cli export --format json|jsonl|csv
61+
threads-cli follow <username> [--no-open]
62+
threads-cli audience refresh
63+
threads-cli audience show [--history N]
64+
threads-cli audience engaged [--limit N] [--sort total|replies|mentions]
65+
threads-cli audience purge --before <date> [--apply]
5166
```
5267

68+
`follow` prints and optionally opens Meta's official Follow Intent. It does
69+
not perform or confirm a follow. `audience refresh` fetches aggregate
70+
`followers_count`, eligible country/city/age/gender demographics, and
71+
official public mentions. `show`, `engaged`, and `purge` are local and use the
72+
token-bound account. Engagement is based on observed direct replies and
73+
mentions, not a follower list; engaged accounts must never be interpreted as
74+
followers. Audience snapshots are private local data, excluded from post
75+
export, and explicitly removable with `audience purge`.
76+
77+
Audience defaults: `show --history 10`, `engaged --limit 20 --sort total`.
78+
`purge` is a dry run unless `--apply` is supplied. `refresh` requires the
79+
recorded `threads_manage_insights` and `threads_manage_mentions` scopes.
80+
The broad login requests exactly these six scopes:
81+
`threads_basic`, `threads_read_replies`, `threads_delete`,
82+
`threads_content_publish`, `threads_manage_insights`, and
83+
`threads_manage_mentions`. Requested scopes are not proof that Meta granted
84+
them; App Review and the user grant control access. The Mentions live gate is
85+
EXTERNALLY UNVERIFIED.
86+
87+
Mentions uses Meta's cursor pagination. `audience refresh` selects 100 items
88+
per page as this client's implementation choice; it is not an asserted Meta
89+
maximum.
90+
91+
After the Insights snapshot is committed, only a Mentions permission denial is
92+
downgraded to a warning. That warning names the required
93+
`threads_manage_mentions` scope and recommends `threads-cli auth login` to
94+
request it again. Other Mentions-phase errors (authentication, network, parse,
95+
rate-limit, or store failures) fail `audience refresh`; the already committed
96+
Insights snapshot remains available locally.
97+
5398
Destructive remote ops (dry-run by default; `--apply` actually performs the
5499
delete via Meta's `DELETE /v1.0/{id}` endpoint):
55100

@@ -65,9 +110,11 @@ accept either RFC 3339 (`2025-01-15T00:00:00Z`) or bare ISO date
65110
quota will reset. See [`docs/plans/delete.md`](docs/plans/delete.md) for the
66111
full design.
67112

68-
Publishing (`threads_publish`), `archive` (Meta does not expose a remote
69-
archive endpoint for root posts), multi-account, and the private
70-
`threads.net/api/graphql` adapter are deferred past v1.
113+
Publishing is supported. `archive` (Meta does not expose a remote archive
114+
endpoint for root posts), multi-account, and the private
115+
`threads.net/api/graphql` adapter remain outside the supported v1 surface.
116+
The web adapter is disabled by default and read-only; it must not be used for
117+
follower enumeration or automated follow actions.
71118

72119
## License
73120

crates/threads-cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ chrono.workspace = true
2222
clap.workspace = true
2323
csv.workspace = true
2424
dirs.workspace = true
25+
libc = "0.2"
26+
open.workspace = true
27+
rand = { version = "0.10", default-features = false, features = ["std", "std_rng", "sys_rng"] }
2528
serde.workspace = true
2629
serde_json.workspace = true
2730
toml.workspace = true
@@ -33,3 +36,4 @@ url.workspace = true
3336
[dev-dependencies]
3437
async-trait.workspace = true
3538
tempfile.workspace = true
39+
threads-provider-official = { workspace = true, features = ["test-support"] }

0 commit comments

Comments
 (0)