# main.py import os import json from datetime import datetime import httpx from fastapi import FastAPI, Request from google.oauth2 import service_account from googleapiclient.discovery import build app = FastAPI() # ===== Notion Config ===== NOTION_SECRET = os.getenv("NOTION_SECRET", "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") NOTION_VERSION = "2025-09-03" NOTION_DRIVE_PROP_ID = os.getenv("NOTION_DRIVE_PROP_ID", "H=Fs") # ID بتاع حقل Google Drive Link # ===== Google Drive Config ===== GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS") # JSON string للـ service account PARENT_FOLDER_ID = os.getenv( "PARENT_FOLDER_ID", "1ddMDm2SFnNfmt6pGIMB5-HI54ywd52Jt" # اللي انت بعتهالي ) # ========== Notion Template Blocks ========== def build_content_template_children(): """نفس التمبلت بالملّي لكن كـ children فقط.""" return [ # ===== Assets link ===== { "object": "block", "type": "callout", "callout": { "rich_text": [ { "type": "text", "text": {"content": "Assets link - روابط اللقطات الخام"}, "annotations": {"bold": True} }, { "type": "text", "text": { "content": "\nهنا هنتم وضع روابط لقطات خام يمكن استخدامها في التصميم أو الفيديو" } } ], "icon": {"emoji": "📁"}, "color": "brown_background" } }, # سطر فاضي { "object": "block", "type": "paragraph", "paragraph": {"rich_text": []} }, # ===== Media Buyer ===== { "object": "block", "type": "callout", "callout": { "rich_text": [ { "type": "text", "text": {"content": "Media Buyer"}, "annotations": {"bold": True} }, { "type": "text", "text": { "content": "\nهنا الجزء الخاص بالميديا باير (يتضمن طريقة تسمية الإعلان - الكانڤسر الخاصة بكل منصة - نسخ الكوبي المختلفة للإعلان)" } } ], "icon": {"emoji": "🎯"}, "color": "brown_background" } }, # Net Name callout منفصل { "object": "block", "type": "callout", "callout": { "rich_text": [ { "type": "text", "text": { "content": "Net Name = Name-(CreatorName/Vo)-(Offer)" }, "annotations": { "code": True, "bold": True } } ], "icon": {"emoji": "🔤"}, "color": "yellow_background" } }, # سطر فاضي { "object": "block", "type": "paragraph", "paragraph": {"rich_text": []} }, # ===== Ad Captions ===== { "object": "block", "type": "callout", "callout": { "rich_text": [ { "type": "text", "text": {"content": "Ad Captions"}, "annotations": {"bold": True} }, { "type": "text", "text": { "content": "\nهنا كابشنز المنصات اللي هيستخدمها الميديا باير أثناء إطلاق الإعلان" } } ], "icon": {"emoji": "✍️"}, "color": "purple_background" } }, # سطر فاضي { "object": "block", "type": "paragraph", "paragraph": {"rich_text": []} }, # ===== منصات الكابشنز كـ H3 toggle ===== { "object": "block", "type": "heading_3", "heading_3": { "rich_text": [ {"type": "text", "text": {"content": "SNAPCHAT"}} ], "is_toggleable": True } }, { "object": "block", "type": "heading_3", "heading_3": { "rich_text": [ {"type": "text", "text": {"content": "INSTAGRAM"}} ], "is_toggleable": True } }, { "object": "block", "type": "heading_3", "heading_3": { "rich_text": [ {"type": "text", "text": {"content": "TIKTOK"}} ], "is_toggleable": True } }, { "object": "block", "type": "heading_3", "heading_3": { "rich_text": [ {"type": "text", "text": {"content": "Google"}} ], "is_toggleable": True } }, # سطر فاضي { "object": "block", "type": "paragraph", "paragraph": {"rich_text": []} }, # ===== Copy Variations ===== { "object": "block", "type": "callout", "callout": { "rich_text": [ { "type": "text", "text": {"content": "Copy Variations"}, "annotations": {"bold": True} }, { "type": "text", "text": { "content": "\nهنا نسخ الكوبي اللي هنتم وضعها أثناء إطلاق الإعلان" } } ], "icon": {"emoji": "📄"}, "color": "gray_background" } }, # سطر فاضي { "object": "block", "type": "paragraph", "paragraph": {"rich_text": []} }, # ===== جدول الكوبيز a / b ===== { "object": "block", "type": "table", "table": { "table_width": 2, "has_column_header": True, "has_row_header": False, "children": [ { "object": "block", "type": "table_row", "table_row": { "cells": [ [ { "type": "text", "text": {"content": "Version"} } ], [ { "type": "text", "text": {"content": "Copy"} } ] ] } }, { "object": "block", "type": "table_row", "table_row": { "cells": [ [ { "type": "text", "text": {"content": "a"} } ], [ { "type": "text", "text": {"content": ""} } ] ] } }, { "object": "block", "type": "table_row", "table_row": { "cells": [ [ { "type": "text", "text": {"content": "b"} } ], [ { "type": "text", "text": {"content": ""} } ] ] } } ] } }, # سطر فاضي { "object": "block", "type": "paragraph", "paragraph": {"rich_text": []} }, # ===== NOTES ===== { "object": "block", "type": "callout", "callout": { "rich_text": [ { "type": "text", "text": {"content": "NOTES (References if needed)"}, "annotations": {"bold": True} }, { "type": "text", "text": { "content": "\nهنا لو في نوتس مهم نشتغل عليها جميعًا سواء كاتب المحتوى، المصمم أو الميديا باير" } } ], "icon": {"emoji": "📌"}, "color": "brown_background" } } ] # ========== Notion Helpers ========== async def get_page_title(page_id: str) -> str: """Fetch page title from Notion.""" async with httpx.AsyncClient() as client: res = await client.get( f"https://api.notion.com/v1/pages/{page_id}", headers={ "Authorization": f"Bearer {NOTION_SECRET}", "Notion-Version": NOTION_VERSION, }, ) data = res.json() props = data.get("properties", {}) for prop in props.values(): if prop.get("type") == "title": title_items = prop.get("title", []) if len(title_items) > 0: return title_items[0].get("plain_text", "") return "(No Title)" async def is_block_archived(page_id: str) -> bool: """يرجع True لو البلوك/الصفحة archived.""" async with httpx.AsyncClient() as client: res = await client.get( f"https://api.notion.com/v1/blocks/{page_id}", headers={ "Authorization": f"Bearer {NOTION_SECRET}", "Notion-Version": NOTION_VERSION, }, ) if res.status_code != 200: print(f"[ARCHIVE CHECK] Failed to fetch block {page_id}, status: {res.status_code}") try: print("[ARCHIVE CHECK] Response:", res.json()) except Exception: print("[ARCHIVE CHECK] Raw response:", res.text) return False # ما نكسرش الفلو data = res.json() archived = data.get("archived", False) print(f"[ARCHIVE CHECK] Block {page_id} archived = {archived}") return archived async def apply_template_to_page(page_id: str): """Append التمبلت دي لجسم الصفحة (الـ body) باستخدام blocks API.""" children = build_content_template_children() print(f"[TEMPLATE] Applying template to page: {page_id}") async with httpx.AsyncClient() as client: res = await client.patch( f"https://api.notion.com/v1/blocks/{page_id}/children", headers={ "Authorization": f"Bearer {NOTION_SECRET}", "Notion-Version": NOTION_VERSION, "Content-Type": "application/json", }, json={"children": children}, ) print(f"[TEMPLATE] Status: {res.status_code}") try: print("[TEMPLATE] Response:", res.json()) except Exception: print("[TEMPLATE] Raw Response:", res.text) async def update_page_drive_link(page_id: str, drive_link: str): """يحدث حقل URL في الصفحة برابط Google Drive.""" print(f"[NOTION] Updating drive link for page {page_id} -> {drive_link}") async with httpx.AsyncClient() as client: res = await client.patch( f"https://api.notion.com/v1/pages/{page_id}", headers={ "Authorization": f"Bearer {NOTION_SECRET}", "Notion-Version": NOTION_VERSION, "Content-Type": "application/json", }, json={ "properties": { NOTION_DRIVE_PROP_ID: { "url": drive_link } } }, ) print(f"[NOTION] Drive link update status: {res.status_code}") try: print("[NOTION] Drive link response:", res.json()) except Exception: print("[NOTION] Drive link raw response:", res.text) # ========== Google Drive Helpers ========== def get_drive_service(): if not GOOGLE_CREDENTIALS: raise RuntimeError("GOOGLE_CREDENTIALS env var is missing") creds_info = json.loads(GOOGLE_CREDENTIALS) creds = service_account.Credentials.from_service_account_info( creds_info, scopes=["https://www.googleapis.com/auth/drive"] ) service = build("drive", "v3", credentials=creds) return service def search_folder(name: str, parent_id: str, drive_service): query = ( f"name = '{name}' and " f"'{parent_id}' in parents and " "mimeType = 'application/vnd.google-apps.folder' and trashed = false" ) results = drive_service.files().list( q=query, fields="files(id, name)" ).execute() files = results.get("files", []) return files[0] if files else None def create_folder(name: str, parent_id: str, drive_service): metadata = { "name": name, "mimeType": "application/vnd.google-apps.folder", "parents": [parent_id], } folder = drive_service.files().create( body=metadata, fields="id, name" ).execute() return folder def ensure_drive_folder_for_page(page_title: str) -> str: """ - جوه PARENT_FOLDER_ID - فولدر شهر (Dec 2025 مثلاً) - جوه فولدر باسم الـ page_title وترجع Google Drive folder link. """ drive_service = get_drive_service() # لو العنوان فاضي، استخدم page_id أو اسم fallback من برّه safe_title = page_title.strip() if page_title and page_title.strip() else "Untitled Task" # من الأفضل نشيل Slash عشان ما يكسرش الاسم safe_title = safe_title.replace("/", "-") month_name = datetime.now().strftime("%b %Y") # مثال: "Dec 2025" print(f"[DRIVE] Ensuring month folder '{month_name}' under parent {PARENT_FOLDER_ID}") month_folder = search_folder(month_name, PARENT_FOLDER_ID, drive_service) if not month_folder: month_folder = create_folder(month_name, PARENT_FOLDER_ID, drive_service) print(f"[DRIVE] Created month folder: {month_folder['id']} - {month_folder['name']}") else: print(f"[DRIVE] Found month folder: {month_folder['id']} - {month_folder['name']}") month_folder_id = month_folder["id"] print(f"[DRIVE] Ensuring page folder '{safe_title}' under month folder {month_folder_id}") page_folder = search_folder(safe_title, month_folder_id, drive_service) if not page_folder: page_folder = create_folder(safe_title, month_folder_id, drive_service) print(f"[DRIVE] Created page folder: {page_folder['id']} - {page_folder['name']}") else: print(f"[DRIVE] Found page folder: {page_folder['id']} - {page_folder['name']}") folder_id = page_folder["id"] drive_link = f"https://drive.google.com/drive/folders/{folder_id}" print(f"[DRIVE] Final folder link: {drive_link}") return drive_link # ========== Webhook ========== @app.post("/webhook") async def webhook(request: Request): body = await request.json() event_type = body.get("type") page_id = body.get("entity", {}).get("id") timestamp = body.get("timestamp") print("\n===== NEW WEBHOOK EVENT =====") print("Raw body:", body) if event_type == "page.created": title = await get_page_title(page_id) print(f"Event Type : {event_type}") print(f"Page Title : {title}") print(f"Page ID : {page_id}") print(f"Timestamp : {timestamp}") # تشيك الأول: هل الصفحة دي Archived (زي لما تدوس Escape على placeholder)؟ archived = await is_block_archived(page_id) if archived: print(f"[TEMPLATE] Page {page_id} is archived right after creation. Skipping template & drive.") else: # 1) نطبّق التمبلت try: await apply_template_to_page(page_id) except Exception as e: print("[ERROR] While applying template:", e) # 2) ننشئ فولدر على الدرايف باسم الصفحة ونحط اللينك في النوشن try: drive_link = ensure_drive_folder_for_page(title or page_id) await update_page_drive_link(page_id, drive_link) except Exception as e: print("[ERROR] While creating drive folder or updating Notion URL:", e) elif event_type == "page.deleted": print(f"Page deleted. ID: {page_id}, Timestamp: {timestamp}") else: print("Event received but ignored:", event_type) print("================================\n") return {"status": "ok"}