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>
172 lines
7.5 KiB
Markdown
172 lines
7.5 KiB
Markdown
# 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>/`)
|
|
```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: `<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
|