Notion-Webhook / main.py
Mr-Help's picture
Update main.py
f980c60 verified
# 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"}