Skip to content

feat: add Threads provider with auto token refresh (1.4.0)#5

Merged
rockfordlhotka merged 1 commit intomainfrom
feat/threads-provider
May 4, 2026
Merged

feat: add Threads provider with auto token refresh (1.4.0)#5
rockfordlhotka merged 1 commit intomainfrom
feat/threads-provider

Conversation

@rockfordlhotka
Copy link
Copy Markdown
Member

@rockfordlhotka rockfordlhotka commented May 4, 2026

⚠️ Not live-tested — Meta dashboard blocker

The Threads provider has not been tested against the live Threads API. Token acquisition is blocked upstream of any code in this PR: Meta's developer dashboard returns a persistent 404 from the Threads use-case redirect-URI save endpoint (/apps/{id}/async/threads-login/setting/save/), which prevents registering the OAuth Redirect Callback URL, which prevents completing the OAuth flow, which prevents minting a token. Reproduced with both an existing app and a brand-new app from scratch, in normal and incognito browser sessions, so this is a Meta-side dashboard issue, not an account- or app-state issue. Filed/observable separately; not in scope for this PR.

Threads remains disabled by default (SocialAgent:Providers:Threads:Enabled=false in appsettings.json), so merging has zero effect on the existing Mastodon and Bluesky behavior. When Meta's dashboard cooperates and a long-lived token can be obtained, the provider activates with config changes only — no code changes required:

dotnet user-secrets set "SocialAgent:Providers:Threads:Enabled" "true"
dotnet user-secrets set "SocialAgent:Providers:Threads:AccessToken" "<long-lived-token>"

Live integration tests are gated on THREADS_ACCESS_TOKEN and ready to run once a token exists.


Summary

  • Adds SocialAgent.Providers.Threads implementing ISocialMediaProvider against Meta's Threads Graph API v1.0. Posts via /me/threads, notifications synthesized from /me/mentions + /me/replies, optional likes/follower count via insights gated behind IncludePostInsights.
  • Adds automatic OAuth token refresh for Threads' 60-day long-lived tokens. New ThreadsTokenRefreshService (registered only when Threads is enabled) seeds the in-memory ThreadsTokenStore from a new ProviderTokens database row on startup, calls GET /refresh_access_token when fewer than RefreshThresholdDays (default 7) remain, and persists the rotated value — so refreshes survive pod restarts.
  • Bumps the agent version 1.3.4 → 1.4.0 (csproj <Version> + k8s deployment image tag); the /.well-known/agent-card.json now reports 1.4.0.
  • DatabaseMigrationService now performs an idempotent CREATE TABLE IF NOT EXISTS "ProviderTokens" (dialect-aware: PG text/timestamptz, SQLite TEXT) after EnsureCreatedAsync, so existing live PostgreSQL deployments pick up the new table on rollout with zero manual DDL.

Notable implementation choices

  • Layering: the Threads provider stays free of SocialAgent.Data. Token persistence lives in the host (ThreadsTokenRefreshService); the provider just exposes RefreshTokenAsync and a singleton ThreadsTokenStore.
  • Lazy fallback: ThreadsTokenStore.Current falls back to ThreadsOptions.AccessToken with an assumed 60-day expiry if the refresh service hasn't seeded yet, so the provider can operate during the brief startup gap before DB seed completes.
  • Concurrency: the provider builds per-request HttpRequestMessage objects (rather than mutating HttpClient.DefaultRequestHeaders) so a token rotation during an in-flight A2A request is safe.
  • Read state: Threads has no native notification read marker, so all returned SocialNotification items have IsRead = false. Documented as a known limitation; local read-state tracking is a candidate for a follow-up.
  • App Review degradation: threads_manage_replies and threads_manage_insights require Meta App Review for production tokens. When scopes are missing, affected calls log a warning and return empty rather than failing the poll cycle.

Files of note

  • src/SocialAgent.Providers.Threads/ThreadsProvider.cs — provider impl + RefreshTokenAsync
  • src/SocialAgent.Providers.Threads/ThreadsTokenStore.cs — singleton current-token holder
  • src/SocialAgent.Host/Services/ThreadsTokenRefreshService.cs — DB seed + scheduled refresh
  • src/SocialAgent.Host/Services/DatabaseMigrationService.cs — adds idempotent table patch path
  • docs/providers/threads.md — full provider doc (scopes, refresh, limits, schema)
  • CHANGELOG.md — new file with 1.4.0 entry

Test plan

  • dotnet build SocialAgent.slnx — clean (TreatWarningsAsErrors)
  • dotnet test SocialAgent.slnx --filter "TestCategory!=Integration" — 11/11 pass (4 new in SocialAgent.Providers.Threads.Tests covering options defaults and ThreadsTokenStore lazy fallback / set / unconfigured-throws)
  • Blocked: live OAuth flow + integration tests against graph.threads.net — pending Meta dashboard fix per banner above

🤖 Generated with Claude Code

Adds SocialAgent.Providers.Threads implementing ISocialMediaProvider
against Meta's Threads Graph API v1.0. Notifications are synthesized
from /me/mentions and /me/replies; per-post likes and follower count
are gated behind an IncludePostInsights flag. A new
ThreadsTokenRefreshService persists the long-lived OAuth token to a
ProviderTokens table and refreshes ahead of expiry, surviving pod
restarts. DatabaseMigrationService now adds new tables idempotently
on existing live databases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rockfordlhotka rockfordlhotka merged commit 3b77023 into main May 4, 2026
@rockfordlhotka rockfordlhotka deleted the feat/threads-provider branch May 4, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant