import json from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect, render from django.http import JsonResponse from django.utils import timezone from django.views.decorators.http import require_http_methods, require_POST from messaging.services import parse_scheduled_for from social.crypto import encrypt_tokens from social.models import Platform, SocialAccount, SocialPost, SocialPostTarget from social.ollama import OllamaError, generate_social_post from social.tasks import publish_social_post CONNECT_INSTRUCTIONS = { Platform.FACEBOOK: { "title": "Connect Facebook Page", "steps": [ "Open Meta for Developers → your app → Tools → Graph API Explorer.", "Select your app, then Get Page Access Token for the Page you manage.", "Grant pages_manage_posts and pages_read_engagement (and pages_show_list).", "Copy the Page access token and the numeric Page ID.", "Paste both below. Tokens are encrypted at rest.", ], "docs_url": "https://developers.facebook.com/docs/pages/access-tokens/", "fields": [ ("label", "Display name", "Monica Dhillon · EXIT"), ("external_id", "Page ID", "1029384756"), ("access_token", "Page access token", ""), ], }, Platform.INSTAGRAM: { "title": "Connect Instagram Business", "steps": [ "Instagram publishing uses a Facebook Page linked to an IG professional account.", "In Meta Business Suite, confirm the IG account is connected to your Page.", "From Graph API Explorer, get a Page token that can manage the linked IG account.", "Use the Instagram Business Account ID (not the username) as External ID.", "Paste Page access token + IG business account ID below.", ], "docs_url": "https://developers.facebook.com/docs/instagram-api/getting-started/", "fields": [ ("label", "Display name", "@mkdrealtor"), ("external_id", "IG business account ID", ""), ("access_token", "Page access token", ""), ("page_id", "Facebook Page ID (optional)", ""), ], }, Platform.LINKEDIN: { "title": "Connect LinkedIn", "steps": [ "Create a LinkedIn Developer app and add the Share on LinkedIn / Marketing products.", "Generate a member or organization access token with w_member_social " "(or w_organization_social for company pages).", "Find your author URN: person URN looks like urn:li:person:XXXX; " "organization URN like urn:li:organization:XXXX.", "Paste the access token and author URN below. LinkedIn tokens expire — " "reconnect when publishing fails with auth errors.", ], "docs_url": "https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/ugc-post-api", "fields": [ ("label", "Display name", "Monica Dhillon"), ("external_id", "Author URN", "urn:li:person:…"), ("access_token", "Access token", ""), ], }, } @login_required def post_list(request): posts = SocialPost.objects.all()[:100] return render(request, "social/post_list.html", {"posts": posts}) @login_required def post_detail(request, pk): post = get_object_or_404(SocialPost.objects.prefetch_related("targets"), pk=pk) return render(request, "social/post_detail.html", {"post": post}) @login_required @require_http_methods(["GET", "POST"]) def account_list(request): accounts = SocialAccount.objects.all() connect_platform = (request.GET.get("connect") or "").strip() if connect_platform not in Platform.values: connect_platform = "" if request.method == "POST": action = (request.POST.get("action") or "connect").strip() if action == "disconnect": pk = request.POST.get("account_id") account = get_object_or_404(SocialAccount, pk=pk) account.is_active = False account.encrypted_tokens = "" account.save(update_fields=["is_active", "encrypted_tokens", "updated_at"]) messages.success(request, f"Disconnected {account.label}.") return redirect("social:account_list") if action == "reactivate": pk = request.POST.get("account_id") account = get_object_or_404(SocialAccount, pk=pk) account.is_active = True account.save(update_fields=["is_active", "updated_at"]) messages.success(request, f"Reactivated {account.label}.") return redirect("social:account_list") platform = (request.POST.get("platform") or "").strip() if platform not in Platform.values: messages.error(request, "Choose a platform.") return redirect("social:account_list") label = (request.POST.get("label") or "").strip() or platform.title() external_id = (request.POST.get("external_id") or "").strip() access_token = (request.POST.get("access_token") or "").strip() page_id = (request.POST.get("page_id") or "").strip() if not access_token or not external_id: messages.error(request, "Access token and external ID are required.") return redirect(f"{request.path}?connect={platform}") token_blob = {"access_token": access_token} if platform == Platform.FACEBOOK: token_blob["page_id"] = external_id elif platform == Platform.INSTAGRAM: if page_id: token_blob["page_id"] = page_id elif platform == Platform.LINKEDIN: token_blob["author_urn"] = external_id account, created = SocialAccount.objects.update_or_create( platform=platform, external_id=external_id, defaults={ "label": label, "encrypted_tokens": encrypt_tokens(json.dumps(token_blob)), "is_active": True, "owner": request.user, }, ) verb = "Connected" if created else "Updated" messages.success(request, f"{verb} {account.get_platform_display()} · {account.label}.") return redirect("social:account_list") return render( request, "social/account_list.html", { "accounts": accounts, "connect_platform": connect_platform, "connect_meta": CONNECT_INSTRUCTIONS.get(connect_platform), "platforms": Platform, }, ) @login_required @require_http_methods(["GET", "POST"]) def composer(request): """Compose, schedule, or publish to connected social accounts.""" accounts = list(SocialAccount.objects.filter(is_active=True)) form = { "body": "", "prompt": "", "publish_mode": "schedule", "scheduled_for": "", "account_ids": [], } error = "" draft = "" if request.method == "POST": form["body"] = (request.POST.get("body") or "").strip() form["prompt"] = (request.POST.get("prompt") or "").strip() form["publish_mode"] = (request.POST.get("publish_mode") or "schedule").strip() form["scheduled_for"] = request.POST.get("scheduled_for") or "" form["account_ids"] = request.POST.getlist("account_ids") action = (request.POST.get("action") or "save").strip() if action == "generate" and form["prompt"]: try: draft = generate_social_post(form["prompt"]) form["body"] = draft except OllamaError as exc: error = str(exc) elif action in {"save", "publish"}: if not form["body"]: error = "Caption / body is required." elif form["publish_mode"] != "draft" and not form["account_ids"]: error = "Select at least one connected account." else: scheduled_for = None status = SocialPost.Status.DRAFT if form["publish_mode"] == "draft": status = SocialPost.Status.DRAFT elif form["publish_mode"] == "schedule": try: scheduled_for = parse_scheduled_for(form["scheduled_for"]) except ValueError as exc: error = str(exc) else: if not scheduled_for: error = "Pick a schedule date/time, or choose Publish now." else: status = SocialPost.Status.SCHEDULED elif form["publish_mode"] == "now": status = SocialPost.Status.QUEUED scheduled_for = timezone.now() else: error = "Unknown publish mode." if not error: selected = SocialAccount.objects.filter( pk__in=form["account_ids"], is_active=True ) if form["publish_mode"] != "draft" and not selected.exists(): error = "No valid accounts selected." else: post = SocialPost.objects.create( body=form["body"], ollama_prompt=form["prompt"], scheduled_for=scheduled_for, status=status, created_by=request.user, ) for account in selected: SocialPostTarget.objects.create( post=post, account=account, platform=account.platform, ) if status == SocialPost.Status.QUEUED: publish_social_post.enqueue(post_id=str(post.pk)) messages.success( request, "Post queued for publishing to selected accounts.", ) elif status == SocialPost.Status.SCHEDULED: messages.success( request, f"Post scheduled for {scheduled_for:%b %d, %I:%M %p}.", ) else: messages.success(request, "Draft saved.") return redirect("social:post_detail", pk=post.pk) return render( request, "social/composer.html", { "accounts": accounts, "form": form, "error": error, "draft": draft or form["body"], }, ) @login_required @require_POST def api_generate(request): """ JSON endpoint for the portal to draft posts via Ollama. POST JSON: {"prompt": "...", "platform": "facebook"|...} """ try: payload = json.loads(request.body.decode() or "{}") except json.JSONDecodeError: payload = request.POST.dict() prompt = (payload.get("prompt") or "").strip() if not prompt: return JsonResponse({"error": "prompt required"}, status=400) try: text = generate_social_post( prompt, platform=(payload.get("platform") or "").strip() ) except OllamaError as exc: return JsonResponse({"error": str(exc)}, status=502) return JsonResponse({"text": text})