Spaces:
Sleeping
Sleeping
| # 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 ========== | |
| 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"} | |