Mr-Help commited on
Commit
995f9ae
·
verified ·
1 Parent(s): 89930e9

Upload 2 files

Browse files
Files changed (2) hide show
  1. main.py +559 -0
  2. requirements.txt +8 -0
main.py ADDED
@@ -0,0 +1,559 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # main.py
2
+ import os
3
+ import json
4
+ from datetime import datetime
5
+
6
+ import httpx
7
+ from fastapi import FastAPI, Request
8
+
9
+ from google.oauth2 import service_account
10
+ from googleapiclient.discovery import build
11
+
12
+ app = FastAPI()
13
+
14
+ # ===== Notion Config =====
15
+ NOTION_SECRET = os.getenv("NOTION_SECRET", "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")
16
+ NOTION_VERSION = "2025-09-03"
17
+ NOTION_DRIVE_PROP_ID = os.getenv("NOTION_DRIVE_PROP_ID", "H=Fs") # ID بتاع حقل Google Drive Link
18
+
19
+ # ===== Google Drive Config =====
20
+ GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS") # JSON string للـ service account
21
+ PARENT_FOLDER_ID = os.getenv(
22
+ "PARENT_FOLDER_ID",
23
+ "1ddMDm2SFnNfmt6pGIMB5-HI54ywd52Jt" # اللي انت بعتهالي
24
+ )
25
+
26
+
27
+ # ========== Notion Template Blocks ==========
28
+ def build_content_template_children():
29
+ """نفس التمبلت بالملّي لكن كـ children فقط."""
30
+ return [
31
+ # ===== Assets link =====
32
+ {
33
+ "object": "block",
34
+ "type": "callout",
35
+ "callout": {
36
+ "rich_text": [
37
+ {
38
+ "type": "text",
39
+ "text": {"content": "Assets link - روابط اللقطات الخام"},
40
+ "annotations": {"bold": True}
41
+ },
42
+ {
43
+ "type": "text",
44
+ "text": {
45
+ "content": "\nهنا هنتم وضع روابط لقطات خام يمكن استخدامها في التصميم أو الفيديو"
46
+ }
47
+ }
48
+ ],
49
+ "icon": {"emoji": "📁"},
50
+ "color": "brown_background"
51
+ }
52
+ },
53
+
54
+ # سطر فاضي
55
+ {
56
+ "object": "block",
57
+ "type": "paragraph",
58
+ "paragraph": {"rich_text": []}
59
+ },
60
+
61
+ # ===== Media Buyer =====
62
+ {
63
+ "object": "block",
64
+ "type": "callout",
65
+ "callout": {
66
+ "rich_text": [
67
+ {
68
+ "type": "text",
69
+ "text": {"content": "Media Buyer"},
70
+ "annotations": {"bold": True}
71
+ },
72
+ {
73
+ "type": "text",
74
+ "text": {
75
+ "content": "\nهنا الجزء الخاص بالميديا باير (يتضمن طريقة تسمية الإعلان - الكانڤسر الخاصة بكل منصة - نسخ الكوبي المختلفة للإعلان)"
76
+ }
77
+ }
78
+ ],
79
+ "icon": {"emoji": "🎯"},
80
+ "color": "brown_background"
81
+ }
82
+ },
83
+
84
+ # Net Name callout منفصل
85
+ {
86
+ "object": "block",
87
+ "type": "callout",
88
+ "callout": {
89
+ "rich_text": [
90
+ {
91
+ "type": "text",
92
+ "text": {
93
+ "content": "Net Name = Name-(CreatorName/Vo)-(Offer)"
94
+ },
95
+ "annotations": {
96
+ "code": True,
97
+ "bold": True
98
+ }
99
+ }
100
+ ],
101
+ "icon": {"emoji": "🔤"},
102
+ "color": "yellow_background"
103
+ }
104
+ },
105
+
106
+ # سطر فاضي
107
+ {
108
+ "object": "block",
109
+ "type": "paragraph",
110
+ "paragraph": {"rich_text": []}
111
+ },
112
+
113
+ # ===== Ad Captions =====
114
+ {
115
+ "object": "block",
116
+ "type": "callout",
117
+ "callout": {
118
+ "rich_text": [
119
+ {
120
+ "type": "text",
121
+ "text": {"content": "Ad Captions"},
122
+ "annotations": {"bold": True}
123
+ },
124
+ {
125
+ "type": "text",
126
+ "text": {
127
+ "content": "\nهنا كابشنز المنصات اللي هيستخدمها الميديا باير أثناء إطلاق الإعلان"
128
+ }
129
+ }
130
+ ],
131
+ "icon": {"emoji": "✍️"},
132
+ "color": "purple_background"
133
+ }
134
+ },
135
+
136
+ # سطر فاضي
137
+ {
138
+ "object": "block",
139
+ "type": "paragraph",
140
+ "paragraph": {"rich_text": []}
141
+ },
142
+
143
+ # ===== منصات الكابشنز كـ H3 toggle =====
144
+ {
145
+ "object": "block",
146
+ "type": "heading_3",
147
+ "heading_3": {
148
+ "rich_text": [
149
+ {"type": "text", "text": {"content": "SNAPCHAT"}}
150
+ ],
151
+ "is_toggleable": True
152
+ }
153
+ },
154
+ {
155
+ "object": "block",
156
+ "type": "heading_3",
157
+ "heading_3": {
158
+ "rich_text": [
159
+ {"type": "text", "text": {"content": "INSTAGRAM"}}
160
+ ],
161
+ "is_toggleable": True
162
+ }
163
+ },
164
+ {
165
+ "object": "block",
166
+ "type": "heading_3",
167
+ "heading_3": {
168
+ "rich_text": [
169
+ {"type": "text", "text": {"content": "TIKTOK"}}
170
+ ],
171
+ "is_toggleable": True
172
+ }
173
+ },
174
+ {
175
+ "object": "block",
176
+ "type": "heading_3",
177
+ "heading_3": {
178
+ "rich_text": [
179
+ {"type": "text", "text": {"content": "Google"}}
180
+ ],
181
+ "is_toggleable": True
182
+ }
183
+ },
184
+
185
+ # سطر فاضي
186
+ {
187
+ "object": "block",
188
+ "type": "paragraph",
189
+ "paragraph": {"rich_text": []}
190
+ },
191
+
192
+ # ===== Copy Variations =====
193
+ {
194
+ "object": "block",
195
+ "type": "callout",
196
+ "callout": {
197
+ "rich_text": [
198
+ {
199
+ "type": "text",
200
+ "text": {"content": "Copy Variations"},
201
+ "annotations": {"bold": True}
202
+ },
203
+ {
204
+ "type": "text",
205
+ "text": {
206
+ "content": "\nهنا نسخ الكوبي اللي هنتم وضعها أثناء إطلاق الإعلان"
207
+ }
208
+ }
209
+ ],
210
+ "icon": {"emoji": "📄"},
211
+ "color": "gray_background"
212
+ }
213
+ },
214
+
215
+ # سطر فاضي
216
+ {
217
+ "object": "block",
218
+ "type": "paragraph",
219
+ "paragraph": {"rich_text": []}
220
+ },
221
+
222
+ # ===== جدول الكوبيز a / b =====
223
+ {
224
+ "object": "block",
225
+ "type": "table",
226
+ "table": {
227
+ "table_width": 2,
228
+ "has_column_header": True,
229
+ "has_row_header": False,
230
+ "children": [
231
+ {
232
+ "object": "block",
233
+ "type": "table_row",
234
+ "table_row": {
235
+ "cells": [
236
+ [
237
+ {
238
+ "type": "text",
239
+ "text": {"content": "Version"}
240
+ }
241
+ ],
242
+ [
243
+ {
244
+ "type": "text",
245
+ "text": {"content": "Copy"}
246
+ }
247
+ ]
248
+ ]
249
+ }
250
+ },
251
+ {
252
+ "object": "block",
253
+ "type": "table_row",
254
+ "table_row": {
255
+ "cells": [
256
+ [
257
+ {
258
+ "type": "text",
259
+ "text": {"content": "a"}
260
+ }
261
+ ],
262
+ [
263
+ {
264
+ "type": "text",
265
+ "text": {"content": ""}
266
+ }
267
+ ]
268
+ ]
269
+ }
270
+ },
271
+ {
272
+ "object": "block",
273
+ "type": "table_row",
274
+ "table_row": {
275
+ "cells": [
276
+ [
277
+ {
278
+ "type": "text",
279
+ "text": {"content": "b"}
280
+ }
281
+ ],
282
+ [
283
+ {
284
+ "type": "text",
285
+ "text": {"content": ""}
286
+ }
287
+ ]
288
+ ]
289
+ }
290
+ }
291
+ ]
292
+ }
293
+ },
294
+
295
+ # سطر فاضي
296
+ {
297
+ "object": "block",
298
+ "type": "paragraph",
299
+ "paragraph": {"rich_text": []}
300
+ },
301
+
302
+ # ===== NOTES =====
303
+ {
304
+ "object": "block",
305
+ "type": "callout",
306
+ "callout": {
307
+ "rich_text": [
308
+ {
309
+ "type": "text",
310
+ "text": {"content": "NOTES (References if needed)"},
311
+ "annotations": {"bold": True}
312
+ },
313
+ {
314
+ "type": "text",
315
+ "text": {
316
+ "content": "\nهنا لو في نوتس مهم نشتغل عليها جميعًا سواء كاتب المحتوى، المصمم أو الميديا باير"
317
+ }
318
+ }
319
+ ],
320
+ "icon": {"emoji": "📌"},
321
+ "color": "brown_background"
322
+ }
323
+ }
324
+ ]
325
+
326
+
327
+ # ========== Notion Helpers ==========
328
+
329
+ async def get_page_title(page_id: str) -> str:
330
+ """Fetch page title from Notion."""
331
+ async with httpx.AsyncClient() as client:
332
+ res = await client.get(
333
+ f"https://api.notion.com/v1/pages/{page_id}",
334
+ headers={
335
+ "Authorization": f"Bearer {NOTION_SECRET}",
336
+ "Notion-Version": NOTION_VERSION,
337
+ },
338
+ )
339
+
340
+ data = res.json()
341
+
342
+ props = data.get("properties", {})
343
+ for prop in props.values():
344
+ if prop.get("type") == "title":
345
+ title_items = prop.get("title", [])
346
+ if len(title_items) > 0:
347
+ return title_items[0].get("plain_text", "")
348
+
349
+ return "(No Title)"
350
+
351
+
352
+ async def is_block_archived(page_id: str) -> bool:
353
+ """يرجع True لو البلوك/الصفحة archived."""
354
+ async with httpx.AsyncClient() as client:
355
+ res = await client.get(
356
+ f"https://api.notion.com/v1/blocks/{page_id}",
357
+ headers={
358
+ "Authorization": f"Bearer {NOTION_SECRET}",
359
+ "Notion-Version": NOTION_VERSION,
360
+ },
361
+ )
362
+
363
+ if res.status_code != 200:
364
+ print(f"[ARCHIVE CHECK] Failed to fetch block {page_id}, status: {res.status_code}")
365
+ try:
366
+ print("[ARCHIVE CHECK] Response:", res.json())
367
+ except Exception:
368
+ print("[ARCHIVE CHECK] Raw response:", res.text)
369
+ return False # ما نكسرش الفلو
370
+
371
+ data = res.json()
372
+ archived = data.get("archived", False)
373
+ print(f"[ARCHIVE CHECK] Block {page_id} archived = {archived}")
374
+ return archived
375
+
376
+
377
+ async def apply_template_to_page(page_id: str):
378
+ """Append التمبلت دي لجسم الصفحة (الـ body) باستخدام blocks API."""
379
+ children = build_content_template_children()
380
+
381
+ print(f"[TEMPLATE] Applying template to page: {page_id}")
382
+
383
+ async with httpx.AsyncClient() as client:
384
+ res = await client.patch(
385
+ f"https://api.notion.com/v1/blocks/{page_id}/children",
386
+ headers={
387
+ "Authorization": f"Bearer {NOTION_SECRET}",
388
+ "Notion-Version": NOTION_VERSION,
389
+ "Content-Type": "application/json",
390
+ },
391
+ json={"children": children},
392
+ )
393
+
394
+ print(f"[TEMPLATE] Status: {res.status_code}")
395
+ try:
396
+ print("[TEMPLATE] Response:", res.json())
397
+ except Exception:
398
+ print("[TEMPLATE] Raw Response:", res.text)
399
+
400
+
401
+ async def update_page_drive_link(page_id: str, drive_link: str):
402
+ """يحدث حقل URL في الصفحة برابط Google Drive."""
403
+ print(f"[NOTION] Updating drive link for page {page_id} -> {drive_link}")
404
+
405
+ async with httpx.AsyncClient() as client:
406
+ res = await client.patch(
407
+ f"https://api.notion.com/v1/pages/{page_id}",
408
+ headers={
409
+ "Authorization": f"Bearer {NOTION_SECRET}",
410
+ "Notion-Version": NOTION_VERSION,
411
+ "Content-Type": "application/json",
412
+ },
413
+ json={
414
+ "properties": {
415
+ NOTION_DRIVE_PROP_ID: {
416
+ "url": drive_link
417
+ }
418
+ }
419
+ },
420
+ )
421
+
422
+ print(f"[NOTION] Drive link update status: {res.status_code}")
423
+ try:
424
+ print("[NOTION] Drive link response:", res.json())
425
+ except Exception:
426
+ print("[NOTION] Drive link raw response:", res.text)
427
+
428
+
429
+ # ========== Google Drive Helpers ==========
430
+
431
+ def get_drive_service():
432
+ if not GOOGLE_CREDENTIALS:
433
+ raise RuntimeError("GOOGLE_CREDENTIALS env var is missing")
434
+
435
+ creds_info = json.loads(GOOGLE_CREDENTIALS)
436
+ creds = service_account.Credentials.from_service_account_info(
437
+ creds_info,
438
+ scopes=["https://www.googleapis.com/auth/drive"]
439
+ )
440
+ service = build("drive", "v3", credentials=creds)
441
+ return service
442
+
443
+
444
+ def search_folder(name: str, parent_id: str, drive_service):
445
+ query = (
446
+ f"name = '{name}' and "
447
+ f"'{parent_id}' in parents and "
448
+ "mimeType = 'application/vnd.google-apps.folder' and trashed = false"
449
+ )
450
+ results = drive_service.files().list(
451
+ q=query,
452
+ fields="files(id, name)"
453
+ ).execute()
454
+ files = results.get("files", [])
455
+ return files[0] if files else None
456
+
457
+
458
+ def create_folder(name: str, parent_id: str, drive_service):
459
+ metadata = {
460
+ "name": name,
461
+ "mimeType": "application/vnd.google-apps.folder",
462
+ "parents": [parent_id],
463
+ }
464
+ folder = drive_service.files().create(
465
+ body=metadata,
466
+ fields="id, name"
467
+ ).execute()
468
+ return folder
469
+
470
+
471
+ def ensure_drive_folder_for_page(page_title: str) -> str:
472
+ """
473
+ - جوه PARENT_FOLDER_ID
474
+ - فولدر شهر (Dec 2025 مثلاً)
475
+ - جوه فولدر باسم الـ page_title
476
+ وترجع Google Drive folder link.
477
+ """
478
+ drive_service = get_drive_service()
479
+
480
+ # لو العنوان فاضي، استخدم page_id أو اسم fallback من برّه
481
+ safe_title = page_title.strip() if page_title and page_title.strip() else "Untitled Task"
482
+
483
+ # من الأفضل نشيل Slash عشان ما يكسرش الاسم
484
+ safe_title = safe_title.replace("/", "-")
485
+
486
+ month_name = datetime.now().strftime("%b %Y") # مثال: "Dec 2025"
487
+
488
+ print(f"[DRIVE] Ensuring month folder '{month_name}' under parent {PARENT_FOLDER_ID}")
489
+ month_folder = search_folder(month_name, PARENT_FOLDER_ID, drive_service)
490
+ if not month_folder:
491
+ month_folder = create_folder(month_name, PARENT_FOLDER_ID, drive_service)
492
+ print(f"[DRIVE] Created month folder: {month_folder['id']} - {month_folder['name']}")
493
+ else:
494
+ print(f"[DRIVE] Found month folder: {month_folder['id']} - {month_folder['name']}")
495
+
496
+ month_folder_id = month_folder["id"]
497
+
498
+ print(f"[DRIVE] Ensuring page folder '{safe_title}' under month folder {month_folder_id}")
499
+ page_folder = search_folder(safe_title, month_folder_id, drive_service)
500
+ if not page_folder:
501
+ page_folder = create_folder(safe_title, month_folder_id, drive_service)
502
+ print(f"[DRIVE] Created page folder: {page_folder['id']} - {page_folder['name']}")
503
+ else:
504
+ print(f"[DRIVE] Found page folder: {page_folder['id']} - {page_folder['name']}")
505
+
506
+ folder_id = page_folder["id"]
507
+ drive_link = f"https://drive.google.com/drive/folders/{folder_id}"
508
+ print(f"[DRIVE] Final folder link: {drive_link}")
509
+ return drive_link
510
+
511
+
512
+ # ========== Webhook ==========
513
+
514
+ @app.post("/webhook")
515
+ async def webhook(request: Request):
516
+ body = await request.json()
517
+
518
+ event_type = body.get("type")
519
+ page_id = body.get("entity", {}).get("id")
520
+ timestamp = body.get("timestamp")
521
+
522
+ print("\n===== NEW WEBHOOK EVENT =====")
523
+ print("Raw body:", body)
524
+
525
+ if event_type == "page.created":
526
+ title = await get_page_title(page_id)
527
+
528
+ print(f"Event Type : {event_type}")
529
+ print(f"Page Title : {title}")
530
+ print(f"Page ID : {page_id}")
531
+ print(f"Timestamp : {timestamp}")
532
+
533
+ # تشيك الأول: هل الصفحة دي Archived (زي لما تدوس Escape على placeholder)؟
534
+ archived = await is_block_archived(page_id)
535
+ if archived:
536
+ print(f"[TEMPLATE] Page {page_id} is archived right after creation. Skipping template & drive.")
537
+ else:
538
+ # 1) نطبّق التمبلت
539
+ try:
540
+ await apply_template_to_page(page_id)
541
+ except Exception as e:
542
+ print("[ERROR] While applying template:", e)
543
+
544
+ # 2) ننشئ فولدر على الدرايف باسم الصفحة ونحط اللينك في النوشن
545
+ try:
546
+ drive_link = ensure_drive_folder_for_page(title or page_id)
547
+ await update_page_drive_link(page_id, drive_link)
548
+ except Exception as e:
549
+ print("[ERROR] While creating drive folder or updating Notion URL:", e)
550
+
551
+ elif event_type == "page.deleted":
552
+ print(f"Page deleted. ID: {page_id}, Timestamp: {timestamp}")
553
+
554
+ else:
555
+ print("Event received but ignored:", event_type)
556
+
557
+ print("================================\n")
558
+
559
+ return {"status": "ok"}
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ httpx
4
+ pydantic
5
+ google-auth
6
+ google-auth-oauthlib
7
+ google-auth-httplib2
8
+ google-api-python-client