All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m10s
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>
7.5 KiB
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.pypattern - Table
social_connectionsin 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_KEYenv var encrypt_token(plaintext) -> str,decrypt_token(ciphertext) -> str
1d. Alembic migration (NEW)
- Creates
social_connectionstable
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)
OAuthResultdataclass (platform_user_id, tokens, expiry, extra_data)ShareResultdataclass (success, platform_post_id, platform_post_url, error)SocialPlatformabstract 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_postsscope, exchange user token → long-lived → page token, post via/{page_id}/feed - Instagram: Same Meta OAuth,
instagram_basic+instagram_content_publishscopes, business/creator accounts only, container → publish workflow - Threads: Separate OAuth at threads.net,
threads_basic+threads_content_publishscopes, container → publish
2c. twitter.py (NEW) — Twitter/X
- OAuth 2.0 with PKCE,
tweet.write+offline.accessscopes - Post via
POST https://api.twitter.com/2/tweets
2d. linkedin.py (NEW) — LinkedIn
- OAuth 2.0,
w_member_social+openidscopes - Post via LinkedIn Posts API
2e. mastodon.py (NEW) — Mastodon
- Dynamic app registration per instance (
POST /api/v1/apps) - OAuth 2.0,
write:statusesscope - Post via
POST /api/v1/statuses - Instance URL stored in
extra_data["instance_url"]
2f. __init__.py (NEW) — Platform registry
PLATFORMSdict, 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 platformsGET /social/connect/<platform>/— start OAuth redirect (Mastodon: accept instance URL param)GET /social/callback/<platform>/— OAuth callback, exchange code, encrypt & store tokensPOST /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-buttonhandler: 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-buttonfragment 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, callrefresh_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
- Generate
SOCIAL_ENCRYPTION_KEY, add to.env - Run Alembic migration
- Start account app, navigate to
/social/ - Connect a test Mastodon account (easiest — no app review needed)
- Navigate to a blog post, click Share, select Mastodon account, verify post appears
- Disconnect account, verify soft-delete
- Test token refresh by connecting Facebook with short-lived token