Add Django site, Docker packaging, and beta/prod Gitea deploys.
Deploy Beta / unit-tests (push) Successful in 9s
Deploy Beta / docker (push) Successful in 17s
Deploy Beta / deploy-beta (push) Successful in 2m31s

Unignore site/ (was blocked by mkdocs /site rule), add compose/Docker/uv tooling, and split deploys so push to main goes to beta while prod stays manual.
This commit is contained in:
2026-08-08 07:32:55 -05:00
parent 7dca98bbf6
commit 1f7d78de64
204 changed files with 21662 additions and 70 deletions
View File
+10
View File
@@ -0,0 +1,10 @@
from typing import Protocol
class SocialConnector(Protocol):
platform: str
def publish(self, post, target) -> str:
"""Publish and return remote post id."""
def refresh_token(self, account) -> None: ...
+48
View File
@@ -0,0 +1,48 @@
"""LinkedIn API connector."""
import json
import logging
import requests
from social.crypto import decrypt_tokens
logger = logging.getLogger(__name__)
class LinkedInConnector:
platform = "linkedin"
def publish(self, post, target) -> str:
tokens = json.loads(decrypt_tokens(target.account.encrypted_tokens) or "{}")
access_token = tokens.get("access_token")
author_urn = target.account.external_id or tokens.get("author_urn")
if not access_token or not author_urn:
raise RuntimeError("LinkedIn account missing access_token/author_urn")
payload = {
"author": author_urn,
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {"text": post.body},
"shareMediaCategory": "NONE",
}
},
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
}
response = requests.post(
"https://api.linkedin.com/v2/ugcPosts",
json=payload,
headers={
"Authorization": f"Bearer {access_token}",
"X-Restli-Protocol-Version": "2.0.0",
"Content-Type": "application/json",
},
timeout=30,
)
response.raise_for_status()
return str(response.headers.get("x-restli-id") or response.json().get("id") or "")
def refresh_token(self, account) -> None:
logger.info("LinkedIn token refresh stub for %s", account.pk)
+43
View File
@@ -0,0 +1,43 @@
"""Meta Graph API connector (Facebook Page + Instagram Business)."""
import logging
import requests
from django.conf import settings
from social.crypto import decrypt_tokens
import json
logger = logging.getLogger(__name__)
class MetaConnector:
platform = "meta"
GRAPH = "https://graph.facebook.com/v21.0"
def publish(self, post, target) -> str:
tokens = json.loads(decrypt_tokens(target.account.encrypted_tokens) or "{}")
access_token = tokens.get("access_token")
page_id = target.account.external_id or tokens.get("page_id")
if not access_token or not page_id:
raise RuntimeError("Meta account missing access_token/page_id")
if target.platform == "instagram":
# IG content publishing is a multi-step Graph flow; stub container create.
raise NotImplementedError(
"Instagram publish requires IG business account wiring — complete OAuth first"
)
response = requests.post(
f"{self.GRAPH}/{page_id}/feed",
data={"message": post.body, "access_token": access_token},
timeout=30,
)
response.raise_for_status()
return str(response.json().get("id") or "")
def refresh_token(self, account) -> None:
# Long-lived token exchange when META_APP_ID/SECRET are set.
if not settings.META_APP_ID or not settings.META_APP_SECRET:
return
logger.info("Meta token refresh not yet implemented for account %s", account.pk)