# 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//` — start OAuth redirect (Mastodon: accept instance URL param) - `GET /social/callback//` — OAuth callback, exchange code, encrypt & store tokens - `POST /social/disconnect//` — 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 `//`) ```python 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: ` Share` ### 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