Files
rose-ash/.claude/plans/unified-inventing-kay.md
giles 094b6c55cd
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m10s
Fix AP blueprint cross-DB queries + harden Ghost sync init
AP blueprints (activitypub.py, ap_social.py) were querying federation
tables (ap_actor_profiles etc.) on g.s which points to the app's own DB
after the per-app split. Now uses g._ap_s backed by get_federation_session()
for non-federation apps.

Also hardens Ghost sync before_app_serving to catch/rollback on failure
instead of crashing the Hypercorn worker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 14:06:42 +00:00

7.5 KiB

Social Network Sharing Integration

Context

Rose Ash already has ActivityPub for federated social sharing. This plan adds OAuth-based sharing to mainstream social networks — Facebook, Instagram, Threads, Twitter/X, LinkedIn, and Mastodon. Users connect their social accounts via the account dashboard, then manually share content (blog posts, events, products) via a share button on content pages.

All social logic lives in the account microservice. Content apps get a share button that opens the account share page.


Phase 1: Data Model + Encryption

1a. shared/models/social_connection.py (NEW)

  • SQLAlchemy 2.0 model following oauth_grant.py pattern
  • Table social_connections in db_account
  • Columns: id, user_id (FK to users.id with CASCADE), platform (facebook/instagram/threads/twitter/linkedin/mastodon), platform_user_id, platform_username, display_name, access_token_enc, refresh_token_enc, token_expires_at, scopes, extra_data (JSONB — mastodon instance URL, facebook page ID, etc.), created_at, updated_at, revoked_at
  • Indexes: (user_id, platform), unique (platform, platform_user_id)

1b. shared/models/__init__.py (MODIFY)

  • Add from .social_connection import SocialConnection

1c. shared/infrastructure/social_crypto.py (NEW)

  • Fernet encrypt/decrypt using SOCIAL_ENCRYPTION_KEY env var
  • encrypt_token(plaintext) -> str, decrypt_token(ciphertext) -> str

1d. Alembic migration (NEW)

  • Creates social_connections table

1e. docker-compose.yml (MODIFY)

  • Add to x-app-env: SOCIAL_ENCRYPTION_KEY, plus per-platform credentials (SOCIAL_FACEBOOK_APP_ID, SOCIAL_FACEBOOK_APP_SECRET, SOCIAL_TWITTER_CLIENT_ID, SOCIAL_TWITTER_CLIENT_SECRET, SOCIAL_LINKEDIN_CLIENT_ID, SOCIAL_LINKEDIN_CLIENT_SECRET)

Phase 2: Platform OAuth Clients

All in account/services/social_platforms/:

2a. base.py (NEW)

  • OAuthResult dataclass (platform_user_id, tokens, expiry, extra_data)
  • ShareResult dataclass (success, platform_post_id, platform_post_url, error)
  • SocialPlatform abstract base class: get_authorize_url(), exchange_code(), refresh_access_token(), share_link(), verify_token()

2b. meta.py (NEW) — Facebook + Instagram + Threads

  • Facebook: OAuth2 via Graph API, pages_manage_posts scope, exchange user token → long-lived → page token, post via /{page_id}/feed
  • Instagram: Same Meta OAuth, instagram_basic + instagram_content_publish scopes, business/creator accounts only, container → publish workflow
  • Threads: Separate OAuth at threads.net, threads_basic + threads_content_publish scopes, container → publish

2c. twitter.py (NEW) — Twitter/X

  • OAuth 2.0 with PKCE, tweet.write + offline.access scopes
  • Post via POST https://api.twitter.com/2/tweets

2d. linkedin.py (NEW) — LinkedIn

  • OAuth 2.0, w_member_social + openid scopes
  • Post via LinkedIn Posts API

2e. mastodon.py (NEW) — Mastodon

  • Dynamic app registration per instance (POST /api/v1/apps)
  • OAuth 2.0, write:statuses scope
  • Post via POST /api/v1/statuses
  • Instance URL stored in extra_data["instance_url"]

2f. __init__.py (NEW) — Platform registry

  • PLATFORMS dict, lazy-initialized from env vars
  • Mastodon always available (no pre-configured credentials)
  • get_platform(name), available_platforms()

Phase 3: Account Social Blueprint

3a. account/bp/social/__init__.py (NEW)

3b. account/bp/social/routes.py (NEW)

Routes (all require login):

  • GET /social/ — list connected accounts + available platforms
  • GET /social/connect/<platform>/ — start OAuth redirect (Mastodon: accept instance URL param)
  • GET /social/callback/<platform>/ — OAuth callback, exchange code, encrypt & store tokens
  • POST /social/disconnect/<int:id>/ — soft-delete (set revoked_at)
  • GET /social/share/ — share page (params: url, title, description, image)
  • POST /social/share/ — execute share to selected accounts, return results

OAuth state stored in session (nonce + platform + redirect params).

3c. account/bp/__init__.py (MODIFY)

  • Add from .social.routes import register as register_social_bp

3d. account/app.py (MODIFY)

  • Register social blueprint before account blueprint (account has catch-all /<slug>/)
app.register_blueprint(register_auth_bp())
app.register_blueprint(register_social_bp())   # <-- NEW, before account
app.register_blueprint(register_account_bp())
app.register_blueprint(register_fragments())

3e. account/templates/_types/auth/_nav.html (MODIFY)

  • Add "social" link between newsletters and account_nav_html

Phase 4: Templates

4a. account/templates/_types/auth/_social_panel.html (NEW)

  • Platform cards with icons (Font Awesome: fa-facebook, fa-instagram, fa-threads, fa-x-twitter, fa-linkedin, fa-mastodon)
  • Connected accounts per platform: display name, username, disconnect button
  • "Connect" button per platform
  • Mastodon: instance URL input before connecting

4b. account/templates/_types/auth/_share_panel.html (NEW)

  • Content preview card (title, image, URL)
  • Connected accounts as checkboxes grouped by platform
  • Optional message textarea
  • Share button → HTMX POST to /social/share/

4c. account/templates/_types/auth/_share_result.html (NEW)

  • Per-platform success/failure with links to created posts

4d. account/templates/_types/auth/_mastodon_connect.html (NEW)

  • Instance URL input form

Phase 5: Share Button in Content Apps

5a. account/bp/fragments/routes.py (MODIFY)

  • Add share-button handler: accepts url, title, description, image params
  • Returns a share icon/link pointing to account.rose-ash.com/social/share/?url=...&title=...

5b. account/templates/fragments/share_button.html (NEW)

  • Small button: <a href="..." target="_blank"><i class="fa-solid fa-share-nodes"></i> Share</a>

5c. Content app integration

  • Blog post detail: fetch share-button fragment from account, render in post template
  • Events detail: same pattern
  • Market product detail: same pattern
  • Each passes its own public URL, title, description, image to the fragment

Phase 6: Token Refresh + Share History

6a. Token refresh in share flow

  • Before posting, check token_expires_at; if expired, call refresh_access_token()
  • Update encrypted tokens in DB
  • If refresh fails, mark connection with error and prompt reconnect

6b. shared/models/social_share.py (NEW, optional)

  • Table social_shares: connection_id, shared_url, shared_title, platform_post_id, platform_post_url, status, error_message, created_at
  • Prevents duplicate shares, enables "shared" indicator on content pages

Key Patterns to Follow

Pattern Reference File
ORM model (mapped_column, FK, indexes) shared/models/oauth_grant.py
Blueprint registration + OOB template account/bp/account/routes.py
Fragment handler dict account/bp/fragments/routes.py
Account nav link account/templates/_types/auth/_nav.html
httpx async client shared/infrastructure/actions.py

Verification

  1. Generate SOCIAL_ENCRYPTION_KEY, add to .env
  2. Run Alembic migration
  3. Start account app, navigate to /social/
  4. Connect a test Mastodon account (easiest — no app review needed)
  5. Navigate to a blog post, click Share, select Mastodon account, verify post appears
  6. Disconnect account, verify soft-delete
  7. Test token refresh by connecting Facebook with short-lived token