Alialdlfy1 commited on
Commit
f9ccc73
·
verified ·
1 Parent(s): cb01086

Delete database.py

Browse files
Files changed (1) hide show
  1. database.py +0 -2606
database.py DELETED
@@ -1,2606 +0,0 @@
1
- import json
2
- import os
3
- import time
4
- import shutil
5
- from threading import Lock
6
-
7
- DB_FILE = "/data/data.json"
8
- BACKUP_DIR = "/data/backups"
9
- SCHEMA_VERSION = 2
10
- lock = Lock()
11
-
12
- DEFAULT_BLOCKED_WORDS = [
13
- "إعلان", "اعلان", "تبادل", "ممول", "تمويل", "للتواصل",
14
- "اشتراك", "تابعونا", "اشترك", "قناتنا", "بوت", "عبر البوت",
15
- "عرض خاص", "لفترة محدودة", "اضغط هنا", "لمشاهدة المزيد",
16
- "حصرياً", "تم النشر بواسطة"
17
- ]
18
-
19
- DEFAULT_DATA = {
20
- "schema_version": SCHEMA_VERSION,
21
- "channels": {},
22
- "public_sources": [],
23
- "source_meta": {},
24
- "blocked_words": DEFAULT_BLOCKED_WORDS,
25
- "allowed_users": [],
26
- "last_source_messages": {},
27
- "settings": {},
28
- "global_remove_terms": [],
29
- "recent_fingerprints": [],
30
- "source_stats": {},
31
- "source_logs": {},
32
- "system": {},
33
- "last_errors": [],
34
- "named_backups": [],
35
- "channel_failures": {},
36
- "published_messages": {},
37
- "sessions": {},
38
- "ai_keys": {},
39
- "publishing_bots": {},
40
- "websites": {},
41
- "trash": [],
42
- "naming_counters": {"session": 0, "ai_key": 0, "bot": 0, "website": 0},
43
- "channel_configs": {},
44
- "settings_clipboard": None,
45
- "_migrated_channel_word_lists": False,
46
- "notification_settings": {
47
- "session_stopped": True,
48
- "ai_key_error": True,
49
- "source_stopped": True,
50
- "channel_idle_hour": True,
51
- "backup_failed": True,
52
- "db_issue": True,
53
- "last_alert_ts": {},
54
- },
55
- }
56
-
57
-
58
- def _now():
59
- return int(time.time())
60
-
61
-
62
- def _dedupe_int_list(values):
63
- out = []
64
- seen = set()
65
- for value in values or []:
66
- try:
67
- item = int(value)
68
- except Exception:
69
- item = value
70
- key = str(item)
71
- if key not in seen:
72
- seen.add(key)
73
- out.append(item)
74
- return out
75
-
76
-
77
- def _normalize_channel(cid, value):
78
- if not isinstance(value, dict):
79
- value = {"name": str(value)}
80
- value.setdefault("name", value.get("title") or str(cid))
81
- value.setdefault("title", value.get("name") or str(cid))
82
- value.setdefault("username", "")
83
- value.setdefault("link", "")
84
- value.setdefault("chat_type", "")
85
- value.setdefault("tail", "")
86
- value.setdefault("special_sources", [])
87
- value.setdefault("paused", False)
88
- value.setdefault("posts_count", 0)
89
- value.setdefault("publish_delay", None)
90
- value.setdefault("bold_publish", True)
91
- value.setdefault("quote_publish", False)
92
- # إعدادات الاقتباس حسب نوع المنشور.
93
- # توافق رجعي: إذا كان quote_publish القديم مفعلاً، نفعّل كل الأنواع افتراضياً.
94
- old_quote = bool(value.get("quote_publish", False))
95
- quote_types = value.get("quote_types")
96
- if not isinstance(quote_types, dict):
97
- quote_types = {}
98
- value["quote_types"] = {
99
- "text": bool(quote_types.get("text", old_quote)),
100
- "photo": bool(quote_types.get("photo", old_quote)),
101
- "video": bool(quote_types.get("video", old_quote)),
102
- "album": bool(quote_types.get("album", old_quote)),
103
- }
104
- # يبقى المفتاح القديم موجود للتوافق مع النسخ الاحتياطية القديمة.
105
- value["quote_publish"] = any(value["quote_types"].values())
106
- value.setdefault("hashtags", [])
107
- if not isinstance(value.get("hashtags"), list):
108
- value["hashtags"] = []
109
- value.setdefault("fail_count", 0)
110
- value.setdefault("disable_web_page_preview", False)
111
- value.setdefault("added_at", _now())
112
- value.setdefault("tail_enabled", True)
113
- value.setdefault("tail_min_words", 20)
114
- value.setdefault("tail_position", "bottom")
115
- value["special_sources"] = _dedupe_int_list(value.get("special_sources", []))
116
- # الكلمات المحظورة وقائمة الحذف الآن مستقلة لكل قناة (وليست عامة).
117
- # None = لم تُهاجَر/تُحدَّد بعد؛ تتم معالجتها لاحقاً في _normalize_data.
118
- if not isinstance(value.get("blocked_words"), list):
119
- value["blocked_words"] = None
120
- if not isinstance(value.get("delete_terms"), list):
121
- value["delete_terms"] = None
122
- return value
123
-
124
-
125
- def _normalize_source_meta(source_id, value=None):
126
- if not isinstance(value, dict):
127
- value = {}
128
- sid = str(source_id)
129
- value.setdefault("id", int(source_id) if str(source_id).lstrip('-').isdigit() else source_id)
130
- value.setdefault("name", value.get("title") or sid)
131
- value.setdefault("title", value.get("name") or sid)
132
- value.setdefault("username", "")
133
- value.setdefault("link", "")
134
- value.setdefault("chat_type", "")
135
- value.setdefault("added_at", _now())
136
- value.setdefault("paused", False)
137
- value.setdefault("remove_terms", [])
138
- if not isinstance(value.get("remove_terms"), list):
139
- value["remove_terms"] = []
140
- value.setdefault("remove_emoji", False)
141
- default_types = {"text": True, "photo": True, "video": True, "album": True, "voice": False, "audio": False, "document": False}
142
- types = value.get("content_types")
143
- if not isinstance(types, dict):
144
- types = {}
145
- value["content_types"] = {k: bool(types.get(k, v)) for k, v in default_types.items()}
146
- return value
147
-
148
-
149
- def _normalize_data(data):
150
- if not isinstance(data, dict):
151
- data = {}
152
-
153
- for key, value in DEFAULT_DATA.items():
154
- if key not in data:
155
- data[key] = value.copy() if isinstance(value, (list, dict)) else value
156
-
157
- if not isinstance(data.get("channels"), dict):
158
- data["channels"] = {}
159
- if not isinstance(data.get("public_sources"), list):
160
- data["public_sources"] = []
161
- if not isinstance(data.get("source_meta"), dict):
162
- data["source_meta"] = {}
163
- if not isinstance(data.get("blocked_words"), list):
164
- data["blocked_words"] = DEFAULT_BLOCKED_WORDS.copy()
165
- if not isinstance(data.get("allowed_users"), list):
166
- data["allowed_users"] = []
167
- if not isinstance(data.get("last_source_messages"), dict):
168
- data["last_source_messages"] = {}
169
- if not isinstance(data.get("settings"), dict):
170
- data["settings"] = {}
171
- if not isinstance(data.get("global_remove_terms"), list):
172
- data["global_remove_terms"] = []
173
- if not isinstance(data.get("recent_fingerprints"), list):
174
- data["recent_fingerprints"] = []
175
- if not isinstance(data.get("source_stats"), dict):
176
- data["source_stats"] = {}
177
- if not isinstance(data.get("source_logs"), dict):
178
- data["source_logs"] = {}
179
- if not isinstance(data.get("system"), dict):
180
- data["system"] = {}
181
- if not isinstance(data.get("last_errors"), list):
182
- data["last_errors"] = []
183
- if not isinstance(data.get("named_backups"), list):
184
- data["named_backups"] = []
185
- if not isinstance(data.get("channel_failures"), dict):
186
- data["channel_failures"] = {}
187
- if not isinstance(data.get("published_messages"), dict):
188
- data["published_messages"] = {}
189
- if not isinstance(data.get("sessions"), dict):
190
- data["sessions"] = {}
191
- if not isinstance(data.get("ai_keys"), dict):
192
- data["ai_keys"] = {}
193
- if not isinstance(data.get("publishing_bots"), dict):
194
- data["publishing_bots"] = {}
195
- if not isinstance(data.get("websites"), dict):
196
- data["websites"] = {}
197
- if not isinstance(data.get("trash"), list):
198
- data["trash"] = []
199
- if not isinstance(data.get("naming_counters"), dict):
200
- data["naming_counters"] = {"session": 0, "ai_key": 0, "bot": 0, "website": 0}
201
- if not isinstance(data.get("channel_configs"), dict):
202
- data["channel_configs"] = {}
203
- if not isinstance(data.get("notification_settings"), dict):
204
- data["notification_settings"] = DEFAULT_DATA["notification_settings"].copy()
205
- ns = data["notification_settings"]
206
- for k, v in DEFAULT_DATA["notification_settings"].items():
207
- ns.setdefault(k, v.copy() if isinstance(v, dict) else v)
208
-
209
- data.setdefault("settings_clipboard", None)
210
-
211
- data["schema_version"] = SCHEMA_VERSION
212
- data["public_sources"] = _dedupe_int_list(data.get("public_sources", []))
213
-
214
- channels = {}
215
- for cid, ch in data.get("channels", {}).items():
216
- channels[str(cid)] = _normalize_channel(cid, ch)
217
- data["channels"] = channels
218
-
219
- # هجرة لمرة واحدة: نسخ القوائم العامة القديمة (الكلمات المحظورة / قائمة
220
- # الحذف العامة) داخل كل قناة موجودة حتى تصبح كل قناة مستقلة بقوائمها،
221
- # بدون فقدان البيانات القديمة. تعمل مرة وحدة فقط بفضل العلم أدناه، حتى
222
- # القنوات التي تُفرّغ قوائمها لاحقاً ما تنرجع تنعبي من جديد.
223
- if not data.get("_migrated_channel_word_lists"):
224
- legacy_blocked = data.get("blocked_words") or DEFAULT_BLOCKED_WORDS.copy()
225
- legacy_terms = data.get("global_remove_terms") or []
226
- for ch in data["channels"].values():
227
- if ch.get("blocked_words") is None:
228
- ch["blocked_words"] = list(legacy_blocked)
229
- if ch.get("delete_terms") is None:
230
- ch["delete_terms"] = list(legacy_terms)
231
- data["_migrated_channel_word_lists"] = True
232
-
233
- # أي قناة بلا قيمة بعد (مثلاً أُضيفت حديثاً بعد الهجرة) تاخذ افتراضي فارغ/عام.
234
- for ch in data["channels"].values():
235
- if ch.get("blocked_words") is None:
236
- ch["blocked_words"] = DEFAULT_BLOCKED_WORDS.copy()
237
- if ch.get("delete_terms") is None:
238
- ch["delete_terms"] = []
239
-
240
- meta = {}
241
- for source in data["public_sources"]:
242
- sid = str(source)
243
- meta[sid] = _normalize_source_meta(source, data.get("source_meta", {}).get(sid, {}))
244
- for sid, value in data.get("source_meta", {}).items():
245
- if sid not in meta:
246
- meta[sid] = _normalize_source_meta(sid, value)
247
- data["source_meta"] = meta
248
-
249
- # normalize last source ids as strings
250
- data["last_source_messages"] = {str(k): v for k, v in data.get("last_source_messages", {}).items()}
251
- return data
252
-
253
-
254
-
255
- def _is_valid_data(data, allow_empty=True):
256
- """يتأكد أن ملف البيانات صالح وليس فارغاً/تالفاً."""
257
- if not isinstance(data, dict):
258
- return False
259
- if "channels" not in data or "public_sources" not in data:
260
- return False
261
- if not isinstance(data.get("channels"), dict):
262
- return False
263
- if not isinstance(data.get("public_sources"), list):
264
- return False
265
- if not allow_empty:
266
- if not data.get("channels") and not data.get("public_sources") and not data.get("source_meta"):
267
- return False
268
- return True
269
-
270
-
271
- def _load_json_file(path):
272
- with open(path, "r", encoding="utf-8") as f:
273
- data = json.load(f)
274
- if not _is_valid_data(data, allow_empty=True):
275
- raise ValueError("ملف بيانات غير صالح")
276
- return _normalize_data(data)
277
-
278
-
279
- def _backup_candidates():
280
- files = []
281
- if os.path.exists(DB_FILE + ".bak"):
282
- files.append(DB_FILE + ".bak")
283
- if os.path.isdir(BACKUP_DIR):
284
- files.extend(
285
- os.path.join(BACKUP_DIR, x)
286
- for x in os.listdir(BACKUP_DIR)
287
- if x.endswith(".json")
288
- )
289
- return sorted(files, key=lambda p: os.path.getmtime(p), reverse=True)
290
-
291
-
292
- def _restore_latest_backup():
293
- """يحاول استعادة أحدث نسخة احتياطية صالحة."""
294
- os.makedirs("/data", exist_ok=True)
295
- for path in _backup_candidates():
296
- try:
297
- data = _load_json_file(path)
298
- tmp = DB_FILE + ".restore.tmp"
299
- with open(tmp, "w", encoding="utf-8") as f:
300
- json.dump(data, f, ensure_ascii=False, indent=2)
301
- os.replace(tmp, DB_FILE)
302
- return data
303
- except Exception:
304
- continue
305
- return None
306
-
307
-
308
- def verify_or_recover_storage():
309
- """فحص تخزين آمن عند تشغيل التطبيق/البوت."""
310
- os.makedirs("/data", exist_ok=True)
311
- if os.path.exists(DB_FILE):
312
- try:
313
- data = _load_json_file(DB_FILE)
314
- return {"ok": True, "recovered": False, "message": "data.json صالح", "data": data}
315
- except Exception as e:
316
- recovered = _restore_latest_backup()
317
- if recovered is not None:
318
- return {"ok": True, "recovered": True, "message": f"تمت الاستعادة من Backup بعد تلف data.json: {e}", "data": recovered}
319
- return {"ok": False, "recovered": False, "message": f"data.json تالف ولا توجد نسخة احتياطية صالحة: {e}", "data": None}
320
-
321
- recovered = _restore_latest_backup()
322
- if recovered is not None:
323
- return {"ok": True, "recovered": True, "message": "تمت الاستعادة من Backup لأن data.json غير موجود", "data": recovered}
324
-
325
- # أول تشغيل فقط: إنشاء ملف جديد فارغ
326
- data = _normalize_data(DEFAULT_DATA.copy())
327
- with open(DB_FILE, "w", encoding="utf-8") as f:
328
- json.dump(data, f, ensure_ascii=False, indent=2)
329
- return {"ok": True, "recovered": False, "message": "تم إنشاء قاعدة بيانات جديدة", "data": data}
330
-
331
-
332
- def _read():
333
- os.makedirs("/data", exist_ok=True)
334
- status = verify_or_recover_storage()
335
- if not status.get("ok"):
336
- raise RuntimeError(status.get("message", "فشل قراءة قاعدة البيانات"))
337
- return _normalize_data(status.get("data") or DEFAULT_DATA.copy())
338
-
339
-
340
- def _make_prewrite_backup():
341
- """نسخة احتياطية فورية قبل أي كتابة حتى لا تضيع البيانات عند توقف الاستضافة."""
342
- try:
343
- if os.path.exists(DB_FILE) and os.path.getsize(DB_FILE) > 5:
344
- shutil.copy2(DB_FILE, DB_FILE + ".bak")
345
- except Exception:
346
- pass
347
-
348
-
349
- def _write(data):
350
- os.makedirs("/data", exist_ok=True)
351
- data = _normalize_data(data)
352
-
353
- # حماية من الكتابة بملف فارغ فوق بيانات موجودة
354
- if not _is_valid_data(data, allow_empty=True):
355
- raise ValueError("رفض كتابة بيانات غير صالحة")
356
-
357
- _make_prewrite_backup()
358
- tmp_file = DB_FILE + ".tmp"
359
- with open(tmp_file, "w", encoding="utf-8") as f:
360
- json.dump(data, f, ensure_ascii=False, indent=2)
361
- f.flush()
362
- try:
363
- os.fsync(f.fileno())
364
- except Exception:
365
- pass
366
- os.replace(tmp_file, DB_FILE)
367
-
368
-
369
- def _write_backup(data):
370
- try:
371
- os.makedirs(BACKUP_DIR, exist_ok=True)
372
- data = _normalize_data(data)
373
- if not _is_valid_data(data, allow_empty=True):
374
- return
375
- backup_file = os.path.join(BACKUP_DIR, f"backup_{int(time.time())}.json")
376
- with open(backup_file, "w", encoding="utf-8") as f:
377
- json.dump(data, f, ensure_ascii=False, indent=2)
378
- # keep last 20 backups
379
- files = sorted([os.path.join(BACKUP_DIR, x) for x in os.listdir(BACKUP_DIR) if x.endswith('.json')])
380
- for old in files[:-20]:
381
- try:
382
- os.remove(old)
383
- except Exception:
384
- pass
385
- except Exception:
386
- pass
387
-
388
- def get_all_channels():
389
- with lock:
390
- data = _read()
391
- return [{"id": k, **v} for k, v in data["channels"].items()]
392
-
393
-
394
- def add_channel(channel_id, name="", title="", username="", link="", chat_type=""):
395
- cid = str(channel_id)
396
- with lock:
397
- data = _read()
398
- created = cid not in data["channels"]
399
- if created:
400
- data["channels"][cid] = _normalize_channel(cid, {
401
- "name": title or name or cid,
402
- "title": title or name or cid,
403
- "username": username or "",
404
- "link": link or "",
405
- "chat_type": chat_type or "",
406
- "tail": "",
407
- "special_sources": [],
408
- "paused": False,
409
- "posts_count": 0,
410
- "added_at": _now(),
411
- })
412
- else:
413
- ch = data["channels"][cid]
414
- if title or name:
415
- ch["name"] = title or name
416
- ch["title"] = title or name
417
- if username is not None:
418
- ch["username"] = username or ch.get("username", "")
419
- if link is not None:
420
- ch["link"] = link or ch.get("link", "")
421
- if chat_type is not None:
422
- ch["chat_type"] = chat_type or ch.get("chat_type", "")
423
- _write(data)
424
- if created:
425
- _write_backup(data)
426
- return created
427
-
428
-
429
- def delete_channel(channel_id):
430
- cid = str(channel_id)
431
- with lock:
432
- data = _read()
433
- if cid in data["channels"]:
434
- del data["channels"][cid]
435
- _write(data)
436
- _write_backup(data)
437
- return True
438
- return False
439
-
440
-
441
- def update_channel(channel_id, key, value):
442
- cid = str(channel_id)
443
- with lock:
444
- data = _read()
445
- if cid in data["channels"]:
446
- data["channels"][cid][key] = value
447
- _write(data)
448
- _write_backup(data)
449
- return True
450
- return False
451
-
452
-
453
- def increment_post_count(channel_id):
454
- cid = str(channel_id)
455
- with lock:
456
- data = _read()
457
- if cid in data["channels"]:
458
- data["channels"][cid].setdefault("posts_count", 0)
459
- data["channels"][cid]["posts_count"] += 1
460
- _write(data)
461
- return True
462
- return False
463
-
464
-
465
- def get_channel(channel_id):
466
- cid = str(channel_id)
467
- with lock:
468
- data = _read()
469
- return data["channels"].get(cid)
470
-
471
-
472
- def get_public_sources():
473
- with lock:
474
- data = _read()
475
- return data["public_sources"]
476
-
477
-
478
- def get_source_meta(source_id):
479
- sid = str(source_id)
480
- with lock:
481
- data = _read()
482
- return data.get("source_meta", {}).get(sid, _normalize_source_meta(source_id))
483
-
484
-
485
- def get_all_public_sources_with_meta():
486
- with lock:
487
- data = _read()
488
- return [{"id": s, **data.get("source_meta", {}).get(str(s), _normalize_source_meta(s))} for s in data["public_sources"]]
489
-
490
-
491
- def add_public_source(source_id, name="", title="", username="", link="", chat_type=""):
492
- try:
493
- source_id = int(source_id)
494
- except Exception:
495
- pass
496
- sid = str(source_id)
497
-
498
- with lock:
499
- data = _read()
500
- created = source_id not in data["public_sources"]
501
- if created:
502
- data["public_sources"].append(source_id)
503
- old = data.setdefault("source_meta", {}).get(sid, {})
504
- meta = _normalize_source_meta(source_id, old)
505
- if title or name:
506
- meta["name"] = title or name
507
- meta["title"] = title or name
508
- if username is not None:
509
- meta["username"] = username or meta.get("username", "")
510
- if link is not None:
511
- meta["link"] = link or meta.get("link", "")
512
- if chat_type is not None:
513
- meta["chat_type"] = chat_type or meta.get("chat_type", "")
514
- data["source_meta"][sid] = meta
515
- _write(data)
516
- if created:
517
- _write_backup(data)
518
- return created
519
-
520
-
521
- def update_source_meta(source_id, **kwargs):
522
- sid = str(source_id)
523
- with lock:
524
- data = _read()
525
- meta = _normalize_source_meta(source_id, data.setdefault("source_meta", {}).get(sid, {}))
526
- for key, value in kwargs.items():
527
- if value is not None:
528
- meta[key] = value
529
- data["source_meta"][sid] = meta
530
- _write(data)
531
- return True
532
-
533
-
534
- def remove_public_source(source_id):
535
- try:
536
- source_id = int(source_id)
537
- except Exception:
538
- pass
539
- sid = str(source_id)
540
-
541
- with lock:
542
- data = _read()
543
- if source_id in data["public_sources"]:
544
- data["public_sources"].remove(source_id)
545
- data["last_source_messages"].pop(sid, None)
546
- data.get("source_meta", {}).pop(sid, None)
547
- _write(data)
548
- _write_backup(data)
549
- return True
550
- return False
551
-
552
-
553
- def get_blocked_words():
554
- with lock:
555
- data = _read()
556
- return data["blocked_words"]
557
-
558
-
559
- def add_blocked_word(word):
560
- word = str(word).strip()
561
- if not word:
562
- return False
563
- with lock:
564
- data = _read()
565
- if word not in data["blocked_words"]:
566
- data["blocked_words"].append(word)
567
- _write(data)
568
- _write_backup(data)
569
- return True
570
- return False
571
-
572
-
573
- def remove_blocked_word(word):
574
- word = str(word).strip()
575
- with lock:
576
- data = _read()
577
- if word in data["blocked_words"]:
578
- data["blocked_words"].remove(word)
579
- _write(data)
580
- _write_backup(data)
581
- return True
582
- return False
583
-
584
-
585
- # ============================================================
586
- # الكلمات المحظورة / قائمة الحذف — مستقلة لكل قناة
587
- # ============================================================
588
-
589
- def get_channel_blocked_words(channel_id):
590
- cid = str(channel_id)
591
- with lock:
592
- data = _read()
593
- ch = data["channels"].get(cid)
594
- return list(ch.get("blocked_words", [])) if ch else []
595
-
596
-
597
- def add_channel_blocked_word(channel_id, word):
598
- word = str(word).strip()
599
- if not word:
600
- return False
601
- cid = str(channel_id)
602
- with lock:
603
- data = _read()
604
- ch = data["channels"].get(cid)
605
- if not ch:
606
- return False
607
- ch.setdefault("blocked_words", [])
608
- if word not in ch["blocked_words"]:
609
- ch["blocked_words"].append(word)
610
- _write(data)
611
- _write_backup(data)
612
- return True
613
- return False
614
-
615
-
616
- def remove_channel_blocked_word(channel_id, word):
617
- word = str(word).strip()
618
- cid = str(channel_id)
619
- with lock:
620
- data = _read()
621
- ch = data["channels"].get(cid)
622
- if not ch:
623
- return False
624
- if word in ch.get("blocked_words", []):
625
- ch["blocked_words"].remove(word)
626
- _write(data)
627
- _write_backup(data)
628
- return True
629
- return False
630
-
631
-
632
- def get_channel_delete_terms(channel_id):
633
- cid = str(channel_id)
634
- with lock:
635
- data = _read()
636
- ch = data["channels"].get(cid)
637
- return list(ch.get("delete_terms", [])) if ch else []
638
-
639
-
640
- def add_channel_delete_terms(channel_id, terms):
641
- cid = str(channel_id)
642
- with lock:
643
- data = _read()
644
- ch = data["channels"].get(cid)
645
- if not ch:
646
- return {"added": 0, "exists": 0}
647
- existing = [str(x).strip() for x in ch.get("delete_terms", []) if str(x).strip()]
648
- added = 0
649
- exists = 0
650
- for term in terms:
651
- term = str(term).strip()
652
- if not term:
653
- continue
654
- if term in existing:
655
- exists += 1
656
- else:
657
- existing.append(term)
658
- added += 1
659
- ch["delete_terms"] = existing
660
- _write(data)
661
- _write_backup(data)
662
- return {"added": added, "exists": exists}
663
-
664
-
665
- def remove_channel_delete_terms(channel_id, terms):
666
- cid = str(channel_id)
667
- with lock:
668
- data = _read()
669
- ch = data["channels"].get(cid)
670
- if not ch:
671
- return {"removed": 0, "missing": 0}
672
- existing = [str(x).strip() for x in ch.get("delete_terms", []) if str(x).strip()]
673
- removed = 0
674
- missing = 0
675
- for term in terms:
676
- term = str(term).strip()
677
- if term in existing:
678
- existing.remove(term)
679
- removed += 1
680
- else:
681
- missing += 1
682
- ch["delete_terms"] = existing
683
- _write(data)
684
- _write_backup(data)
685
- return {"removed": removed, "missing": missing}
686
-
687
-
688
- def get_last_source_message(source_id):
689
- with lock:
690
- data = _read()
691
- value = data.get("last_source_messages", {}).get(str(source_id))
692
- try:
693
- if isinstance(value, dict):
694
- return int(value.get("id")) if value.get("id") is not None else None
695
- return int(value) if value is not None else None
696
- except Exception:
697
- return None
698
-
699
- def set_last_source_message(source_id, message_id):
700
- with lock:
701
- data = _read()
702
- data.setdefault("last_source_messages", {})[str(source_id)] = {"id": int(message_id), "ts": _now()}
703
- _write(data)
704
- return True
705
-
706
- def reset_last_source_message(source_id):
707
- with lock:
708
- data = _read()
709
- if str(source_id) in data.get("last_source_messages", {}):
710
- del data["last_source_messages"][str(source_id)]
711
- _write(data)
712
- _write_backup(data)
713
- return True
714
- return False
715
-
716
-
717
-
718
- def export_data():
719
- with lock:
720
- data = _read()
721
- data = cleanup_old_runtime_data(data, days=3)
722
- return json.dumps(_normalize_data(data), ensure_ascii=False, indent=2)
723
-
724
-
725
- def import_data(json_str):
726
- with lock:
727
- new_data = json.loads(json_str)
728
- if not _is_valid_data(new_data, allow_empty=False):
729
- raise ValueError("ملف الاستيراد فارغ أو غير صالح. تم رفضه لحماية بياناتك.")
730
- new_data = _normalize_data(new_data)
731
-
732
- # لا نقبل ملف بلا قنوات وبلا مصادر إلا إذا كان مقصوداً، وهذا غير مسموح هنا
733
- if not new_data.get("channels") and not new_data.get("public_sources") and not new_data.get("source_meta"):
734
- raise ValueError("ملف الاستيراد لا يحتوي قنوات أو مصادر.")
735
-
736
- _make_prewrite_backup()
737
- _write_backup(_read())
738
- new_data = cleanup_old_runtime_data(new_data, days=3)
739
- _write(new_data)
740
- _write_backup(new_data)
741
- return {
742
- "channels": len(new_data.get("channels", {})),
743
- "public_sources": len(new_data.get("public_sources", [])),
744
- "source_meta": len(new_data.get("source_meta", {})),
745
- "blocked_words": len(new_data.get("blocked_words", [])),
746
- "global_remove_terms": len(new_data.get("global_remove_terms", [])),
747
- "last_source_messages": len(new_data.get("last_source_messages", {})),
748
- "recent_fingerprints": len(new_data.get("recent_fingerprints", [])),
749
- }
750
-
751
-
752
- def cleanup_old_runtime_data(data=None, days=3):
753
- """حذف بيانات التكرار والسجلات و IDs المؤقتة الأقدم من 3 أيام حتى لا يكبر الملف."""
754
- cutoff = _now() - int(days) * 86400
755
- own_write = data is None
756
- if data is None:
757
- data = _read()
758
-
759
- items = data.get("recent_fingerprints", [])
760
- if isinstance(items, list):
761
- data["recent_fingerprints"] = [
762
- x for x in items
763
- if isinstance(x, dict) and int(x.get("ts", _now())) >= cutoff
764
- ][-500:]
765
-
766
- logs = data.get("source_logs", {})
767
- if isinstance(logs, dict):
768
- new_logs = {}
769
- for sid, entries in logs.items():
770
- if isinstance(entries, list):
771
- new_logs[sid] = [
772
- x for x in entries
773
- if isinstance(x, dict) and int(x.get("ts", _now())) >= cutoff
774
- ][-20:]
775
- data["source_logs"] = new_logs
776
-
777
- # تنظيف IDs آخر الأخبار للمصادر إذا خزنّت بصيغة حديثة مع تاريخ.
778
- # القيم القديمة الرقمية تُترك كما هي للتوافق حتى يتم تحديثها تلقائياً عند أول فحص.
779
- last_ids = data.get("last_source_messages", {})
780
- if isinstance(last_ids, dict):
781
- cleaned_last = {}
782
- for sid, value in last_ids.items():
783
- if isinstance(value, dict):
784
- try:
785
- if int(value.get("ts", _now())) >= cutoff:
786
- cleaned_last[str(sid)] = value
787
- except Exception:
788
- pass
789
- else:
790
- cleaned_last[str(sid)] = value
791
- data["last_source_messages"] = cleaned_last
792
-
793
- last_errors = data.get("last_errors", [])
794
- if isinstance(last_errors, list):
795
- data["last_errors"] = [
796
- x for x in last_errors
797
- if isinstance(x, dict) and int(x.get("ts", _now())) >= cutoff
798
- ][-50:]
799
-
800
-
801
- # تنظيف سجل الرسائل المنشورة القديمة حتى لا يكبر الملف.
802
- published = data.get("published_messages", {})
803
- if isinstance(published, dict):
804
- new_published = {}
805
- for cid, by_type in published.items():
806
- if not isinstance(by_type, dict):
807
- continue
808
- new_by_type = {}
809
- for kind, entries in by_type.items():
810
- if isinstance(entries, list):
811
- new_by_type[kind] = [
812
- x for x in entries
813
- if isinstance(x, dict) and int(x.get("ts", _now())) >= cutoff
814
- ][-1000:]
815
- new_published[str(cid)] = new_by_type
816
- data["published_messages"] = new_published
817
-
818
- if own_write:
819
- _write(data)
820
- return _normalize_data(data)
821
-
822
- def get_source_remove_terms(source_id):
823
- sid = str(source_id)
824
- with lock:
825
- data = _read()
826
- meta = data.get("source_meta", {}).get(sid, _normalize_source_meta(source_id))
827
- terms = meta.get("remove_terms", [])
828
- if not isinstance(terms, list):
829
- return []
830
- return [str(x).strip() for x in terms if str(x).strip()]
831
-
832
-
833
- def add_source_remove_terms(source_id, terms):
834
- sid = str(source_id)
835
- clean_terms = []
836
- for term in terms or []:
837
- term = str(term).strip()
838
- if term:
839
- clean_terms.append(term)
840
-
841
- if not clean_terms:
842
- return {"added": 0, "exists": 0}
843
-
844
- with lock:
845
- data = _read()
846
- meta = _normalize_source_meta(source_id, data.setdefault("source_meta", {}).get(sid, {}))
847
- existing = [str(x).strip() for x in meta.get("remove_terms", []) if str(x).strip()]
848
- existing_keys = {x.lower() for x in existing}
849
-
850
- added = 0
851
- exists = 0
852
- for term in clean_terms:
853
- key = term.lower()
854
- if key in existing_keys:
855
- exists += 1
856
- continue
857
- existing.append(term)
858
- existing_keys.add(key)
859
- added += 1
860
-
861
- meta["remove_terms"] = existing
862
- data["source_meta"][sid] = meta
863
- _write(data)
864
- if added:
865
- _write_backup(data)
866
- return {"added": added, "exists": exists}
867
-
868
-
869
- def remove_source_remove_terms(source_id, terms):
870
- sid = str(source_id)
871
- remove_keys = {str(x).strip().lower() for x in terms or [] if str(x).strip()}
872
- if not remove_keys:
873
- return {"removed": 0, "missing": 0}
874
-
875
- with lock:
876
- data = _read()
877
- meta = _normalize_source_meta(source_id, data.setdefault("source_meta", {}).get(sid, {}))
878
- existing = [str(x).strip() for x in meta.get("remove_terms", []) if str(x).strip()]
879
- before = len(existing)
880
- existing = [x for x in existing if x.lower() not in remove_keys]
881
- removed = before - len(existing)
882
- meta["remove_terms"] = existing
883
- data["source_meta"][sid] = meta
884
- _write(data)
885
- if removed:
886
- _write_backup(data)
887
- return {"removed": removed, "missing": max(0, len(remove_keys) - removed)}
888
-
889
-
890
- def remove_source_remove_term(source_id, term_or_index):
891
- terms = get_source_remove_terms(source_id)
892
- try:
893
- idx = int(term_or_index)
894
- if 0 <= idx < len(terms):
895
- return bool(remove_source_remove_terms(source_id, [terms[idx]]).get("removed"))
896
- except Exception:
897
- pass
898
- return bool(remove_source_remove_terms(source_id, [term_or_index]).get("removed"))
899
-
900
-
901
- # ===== Phase 3 Part 1: source filters, global cleanup, smart dedupe =====
902
- def get_global_remove_terms():
903
- with lock:
904
- data = _read()
905
- terms = data.get("global_remove_terms", [])
906
- if not isinstance(terms, list):
907
- return []
908
- return [str(x).strip() for x in terms if str(x).strip()]
909
-
910
-
911
- def add_global_remove_terms(terms):
912
- clean_terms = []
913
- for term in terms or []:
914
- term = str(term).strip()
915
- if term:
916
- clean_terms.append(term)
917
-
918
- if not clean_terms:
919
- return {"added": 0, "exists": 0}
920
-
921
- with lock:
922
- data = _read()
923
- existing = [str(x).strip() for x in data.get("global_remove_terms", []) if str(x).strip()]
924
- existing_keys = {x.lower() for x in existing}
925
- added = 0
926
- exists = 0
927
- for term in clean_terms:
928
- key = term.lower()
929
- if key in existing_keys:
930
- exists += 1
931
- continue
932
- existing.append(term)
933
- existing_keys.add(key)
934
- added += 1
935
- data["global_remove_terms"] = existing
936
- _write(data)
937
- if added:
938
- _write_backup(data)
939
- return {"added": added, "exists": exists}
940
-
941
-
942
- def remove_global_remove_terms(terms):
943
- remove_keys = {str(x).strip().lower() for x in terms or [] if str(x).strip()}
944
- if not remove_keys:
945
- return {"removed": 0, "missing": 0}
946
-
947
- with lock:
948
- data = _read()
949
- existing = [str(x).strip() for x in data.get("global_remove_terms", []) if str(x).strip()]
950
- before = len(existing)
951
- existing = [x for x in existing if x.lower() not in remove_keys]
952
- removed = before - len(existing)
953
- data["global_remove_terms"] = existing
954
- _write(data)
955
- if removed:
956
- _write_backup(data)
957
- return {"removed": removed, "missing": max(0, len(remove_keys) - removed)}
958
-
959
-
960
- def set_source_paused(source_id, paused=True):
961
- return update_source_meta(source_id, paused=bool(paused))
962
-
963
-
964
- def is_source_paused(source_id):
965
- sid = str(source_id)
966
- with lock:
967
- data = _read()
968
- meta = data.get("source_meta", {}).get(sid, _normalize_source_meta(source_id))
969
- return bool(meta.get("paused", False))
970
-
971
-
972
- def set_source_remove_emoji(source_id, enabled=True):
973
- return update_source_meta(source_id, remove_emoji=bool(enabled))
974
-
975
-
976
- def get_source_remove_emoji(source_id):
977
- sid = str(source_id)
978
- with lock:
979
- data = _read()
980
- meta = data.get("source_meta", {}).get(sid, _normalize_source_meta(source_id))
981
- return bool(meta.get("remove_emoji", False))
982
-
983
-
984
- def get_source_content_types(source_id):
985
- sid = str(source_id)
986
- with lock:
987
- data = _read()
988
- meta = data.get("source_meta", {}).get(sid, _normalize_source_meta(source_id))
989
- default_types = {"text": True, "photo": True, "video": True, "album": True, "voice": False, "audio": False, "document": False}
990
- types = meta.get("content_types", {})
991
- if not isinstance(types, dict):
992
- types = {}
993
- return {k: bool(types.get(k, v)) for k, v in default_types.items()}
994
-
995
-
996
- def set_source_content_type(source_id, content_type, enabled=True):
997
- if content_type not in {"text", "photo", "video", "album", "voice", "audio", "document"}:
998
- return False
999
- sid = str(source_id)
1000
- default_types = {"text": True, "photo": True, "video": True, "album": True, "voice": False, "audio": False, "document": False}
1001
- with lock:
1002
- data = _read()
1003
- meta = _normalize_source_meta(source_id, data.setdefault("source_meta", {}).get(sid, {}))
1004
- current = meta.get("content_types", {})
1005
- if not isinstance(current, dict):
1006
- current = {}
1007
- types = {k: bool(current.get(k, v)) for k, v in default_types.items()}
1008
- types[content_type] = bool(enabled)
1009
- # منع إطفاء كل الأنواع حتى لا يصبح المصدر شغال بلا أي محتوى
1010
- if not any(types.values()):
1011
- return False
1012
- meta["content_types"] = types
1013
- data["source_meta"][sid] = meta
1014
- _write(data)
1015
- _write_backup(data)
1016
- return True
1017
-
1018
-
1019
- def set_source_content_types(source_id, content_types):
1020
- allowed = {"text", "photo", "video", "album", "voice", "audio", "document"}
1021
- values = {k: bool(content_types.get(k, False)) for k in allowed}
1022
- if not any(values.values()):
1023
- return False
1024
- return update_source_meta(source_id, content_types=values)
1025
-
1026
-
1027
- def get_recent_fingerprints():
1028
- cutoff = _now() - 3 * 86400
1029
- with lock:
1030
- data = _read()
1031
- items = data.get("recent_fingerprints", [])
1032
- if not isinstance(items, list):
1033
- return []
1034
- fresh = [x for x in items if isinstance(x, dict) and x.get("fp") and int(x.get("ts", _now())) >= cutoff]
1035
- if len(fresh) != len(items):
1036
- data["recent_fingerprints"] = fresh[-500:]
1037
- _write(data)
1038
- return fresh[-500:]
1039
-
1040
- def add_recent_fingerprint(fp, sample="", limit=500):
1041
- fp = str(fp or "").strip()
1042
- if not fp:
1043
- return False
1044
- cutoff = _now() - 3 * 86400
1045
- with lock:
1046
- data = _read()
1047
- items = data.get("recent_fingerprints", [])
1048
- if not isinstance(items, list):
1049
- items = []
1050
- items = [x for x in items if isinstance(x, dict) and x.get("fp") != fp and int(x.get("ts", _now())) >= cutoff]
1051
- items.append({"fp": fp, "sample": str(sample or "")[:500], "ts": _now()})
1052
- if len(items) > int(limit):
1053
- items = items[-int(limit):]
1054
- data["recent_fingerprints"] = items
1055
- _write(data)
1056
- return True
1057
-
1058
- def set_channel_publish_delay(channel_id, delay_seconds):
1059
- try:
1060
- if delay_seconds is None or str(delay_seconds).strip() == "":
1061
- value = None
1062
- else:
1063
- value = max(0.0, float(delay_seconds))
1064
- except Exception:
1065
- return False
1066
- return update_channel(channel_id, "publish_delay", value)
1067
-
1068
-
1069
- def get_channel_publish_delay(channel_id, default=0.5):
1070
- ch = get_channel(channel_id)
1071
- if not ch:
1072
- return float(default)
1073
- value = ch.get("publish_delay", None)
1074
- if value is None or value == "":
1075
- return float(default)
1076
- try:
1077
- return max(0.0, float(value))
1078
- except Exception:
1079
- return float(default)
1080
-
1081
-
1082
-
1083
- def set_channel_bold_publish(channel_id, enabled=True):
1084
- return update_channel(channel_id, "bold_publish", bool(enabled))
1085
-
1086
-
1087
- def get_channel_bold_publish(channel_id, default=True):
1088
- ch = get_channel(channel_id)
1089
- if not ch:
1090
- return bool(default)
1091
- return bool(ch.get("bold_publish", default))
1092
-
1093
- def record_source_event(source_id, event_type, reason="", message_id=None, target_count=0):
1094
- sid = str(source_id)
1095
- with lock:
1096
- data = _read()
1097
- stats = data.setdefault("source_stats", {}).setdefault(sid, {
1098
- "received": 0, "published": 0, "rejected": 0, "ignored": 0,
1099
- "duplicates": 0, "errors": 0, "last_message_id": None,
1100
- "last_event": "", "last_reason": "", "last_ts": None
1101
- })
1102
- event_type = str(event_type or "").lower()
1103
- if event_type == "received":
1104
- stats["received"] = int(stats.get("received", 0)) + 1
1105
- elif event_type == "published":
1106
- stats["published"] = int(stats.get("published", 0)) + 1
1107
- elif event_type == "rejected":
1108
- stats["rejected"] = int(stats.get("rejected", 0)) + 1
1109
- elif event_type == "duplicate":
1110
- stats["duplicates"] = int(stats.get("duplicates", 0)) + 1
1111
- stats["ignored"] = int(stats.get("ignored", 0)) + 1
1112
- elif event_type == "error":
1113
- stats["errors"] = int(stats.get("errors", 0)) + 1
1114
- else:
1115
- stats["ignored"] = int(stats.get("ignored", 0)) + 1
1116
- stats["last_message_id"] = message_id
1117
- stats["last_event"] = event_type
1118
- stats["last_reason"] = str(reason or "")[:300]
1119
- stats["last_ts"] = _now()
1120
- data["source_stats"][sid] = stats
1121
-
1122
- logs = data.setdefault("source_logs", {}).setdefault(sid, [])
1123
- logs.append({
1124
- "ts": _now(), "event": event_type, "reason": str(reason or "")[:300],
1125
- "message_id": message_id, "target_count": int(target_count or 0)
1126
- })
1127
- data["source_logs"][sid] = logs[-20:]
1128
- _write(data)
1129
- return True
1130
-
1131
-
1132
- def get_source_stats(source_id):
1133
- sid = str(source_id)
1134
- with lock:
1135
- data = _read()
1136
- return data.get("source_stats", {}).get(sid, {
1137
- "received": 0, "published": 0, "rejected": 0, "ignored": 0,
1138
- "duplicates": 0, "errors": 0, "last_message_id": None,
1139
- "last_event": "", "last_reason": "", "last_ts": None
1140
- })
1141
-
1142
-
1143
- def get_source_logs(source_id, limit=20):
1144
- sid = str(source_id)
1145
- with lock:
1146
- data = _read()
1147
- logs = data.get("source_logs", {}).get(sid, [])
1148
- if not isinstance(logs, list):
1149
- return []
1150
- return logs[-int(limit):]
1151
-
1152
-
1153
- def copy_source_settings(from_source_id, to_source_ids):
1154
- from_sid = str(from_source_id)
1155
- copied = 0
1156
- with lock:
1157
- data = _read()
1158
- source_meta = _normalize_source_meta(from_source_id, data.setdefault("source_meta", {}).get(from_sid, {}))
1159
- keys = ["remove_terms", "remove_emoji", "content_types", "paused"]
1160
- for dst in to_source_ids or []:
1161
- sid = str(dst)
1162
- meta = _normalize_source_meta(dst, data.setdefault("source_meta", {}).get(sid, {}))
1163
- for key in keys:
1164
- if key in source_meta:
1165
- # copy JSON-compatible value
1166
- meta[key] = json.loads(json.dumps(source_meta[key], ensure_ascii=False))
1167
- data["source_meta"][sid] = meta
1168
- copied += 1
1169
- _write(data)
1170
- if copied:
1171
- _write_backup(data)
1172
- return copied
1173
-
1174
-
1175
- def set_system_value(key, value):
1176
- with lock:
1177
- data = _read()
1178
- data.setdefault("system", {})[str(key)] = value
1179
- _write(data)
1180
- return True
1181
-
1182
-
1183
- def get_system_value(key, default=None):
1184
- with lock:
1185
- data = _read()
1186
- return data.get("system", {}).get(str(key), default)
1187
-
1188
-
1189
- # ===== Professional safety/ops additions =====
1190
- def is_maintenance_mode():
1191
- with lock:
1192
- data = _read()
1193
- return bool(data.get("system", {}).get("maintenance_mode", False))
1194
-
1195
-
1196
- def set_maintenance_mode(enabled):
1197
- with lock:
1198
- data = _read()
1199
- data.setdefault("system", {})["maintenance_mode"] = bool(enabled)
1200
- data["system"]["maintenance_updated_at"] = _now()
1201
- _write(data)
1202
- return True
1203
-
1204
-
1205
- def add_last_error(context, error):
1206
- with lock:
1207
- data = _read()
1208
- errors = data.setdefault("last_errors", [])
1209
- if not isinstance(errors, list):
1210
- errors = []
1211
- errors.append({
1212
- "ts": _now(),
1213
- "context": str(context or "")[:120],
1214
- "error": str(error or "")[:500],
1215
- })
1216
- data["last_errors"] = errors[-20:]
1217
- data.setdefault("system", {})["last_error"] = f"{context}: {error}"[:600]
1218
- data["system"]["last_error_ts"] = _now()
1219
- _write(data)
1220
- return True
1221
-
1222
-
1223
- def get_last_errors(limit=20):
1224
- with lock:
1225
- data = _read()
1226
- errors = data.get("last_errors", [])
1227
- if not isinstance(errors, list):
1228
- return []
1229
- return errors[-int(limit):]
1230
-
1231
-
1232
- def clear_last_errors():
1233
- with lock:
1234
- data = _read()
1235
- data["last_errors"] = []
1236
- data.setdefault("system", {})["last_error"] = ""
1237
- _write(data)
1238
- return True
1239
-
1240
-
1241
- def _normalize_quote_types(value=None, fallback=False):
1242
- if not isinstance(value, dict):
1243
- value = {}
1244
- return {
1245
- "text": bool(value.get("text", fallback)),
1246
- "photo": bool(value.get("photo", fallback)),
1247
- "video": bool(value.get("video", fallback)),
1248
- "album": bool(value.get("album", fallback)),
1249
- }
1250
-
1251
-
1252
- def set_channel_quote_publish(channel_id, enabled=True):
1253
- # المفتاح القديم: يبدّل كل الأنواع مرة واحدة للتوافق فقط.
1254
- cid = str(channel_id)
1255
- enabled = bool(enabled)
1256
- with lock:
1257
- data = _read()
1258
- if cid not in data.get("channels", {}):
1259
- return False
1260
- data["channels"][cid]["quote_types"] = _normalize_quote_types({}, enabled)
1261
- data["channels"][cid]["quote_publish"] = enabled
1262
- _write(data)
1263
- _write_backup(data)
1264
- return True
1265
-
1266
-
1267
- def get_channel_quote_publish(channel_id, default=False):
1268
- ch = get_channel(channel_id)
1269
- if not ch:
1270
- return bool(default)
1271
- qt = _normalize_quote_types(ch.get("quote_types"), ch.get("quote_publish", default))
1272
- return any(qt.values())
1273
-
1274
-
1275
- def get_channel_quote_types(channel_id):
1276
- ch = get_channel(channel_id)
1277
- if not ch:
1278
- return _normalize_quote_types({}, False)
1279
- return _normalize_quote_types(ch.get("quote_types"), ch.get("quote_publish", False))
1280
-
1281
-
1282
- def get_channel_quote_type(channel_id, content_type="text", default=False):
1283
- ctype = str(content_type or "text")
1284
- if ctype not in {"text", "photo", "video", "album"}:
1285
- ctype = "text"
1286
- return bool(get_channel_quote_types(channel_id).get(ctype, default))
1287
-
1288
-
1289
- def set_channel_quote_type(channel_id, content_type, enabled=True):
1290
- cid = str(channel_id)
1291
- ctype = str(content_type or "text")
1292
- if ctype not in {"text", "photo", "video", "album"}:
1293
- return False
1294
- with lock:
1295
- data = _read()
1296
- if cid not in data.get("channels", {}):
1297
- return False
1298
- ch = data["channels"][cid]
1299
- qt = _normalize_quote_types(ch.get("quote_types"), ch.get("quote_publish", False))
1300
- qt[ctype] = bool(enabled)
1301
- ch["quote_types"] = qt
1302
- ch["quote_publish"] = any(qt.values())
1303
- _write(data)
1304
- _write_backup(data)
1305
- return True
1306
-
1307
-
1308
- def get_channel_hashtags(channel_id):
1309
- ch = get_channel(channel_id)
1310
- if not ch:
1311
- return []
1312
- tags = ch.get("hashtags", [])
1313
- if not isinstance(tags, list):
1314
- return []
1315
- out = []
1316
- seen = set()
1317
- for tag in tags:
1318
- tag = str(tag).strip()
1319
- if not tag:
1320
- continue
1321
- if not tag.startswith("#"):
1322
- tag = "#" + tag.lstrip("#")
1323
- key = tag.lower()
1324
- if key not in seen:
1325
- seen.add(key)
1326
- out.append(tag)
1327
- return out
1328
-
1329
-
1330
- def add_channel_hashtags(channel_id, tags):
1331
- clean = []
1332
- for tag in tags or []:
1333
- tag = str(tag).strip()
1334
- if not tag:
1335
- continue
1336
- if not tag.startswith("#"):
1337
- tag = "#" + tag.lstrip("#")
1338
- clean.append(tag)
1339
- if not clean:
1340
- return {"added": 0, "exists": 0}
1341
- cid = str(channel_id)
1342
- with lock:
1343
- data = _read()
1344
- if cid not in data["channels"]:
1345
- return {"added": 0, "exists": 0}
1346
- existing = []
1347
- for tag in data["channels"][cid].get("hashtags", []):
1348
- tag = str(tag).strip()
1349
- if tag:
1350
- existing.append(tag if tag.startswith("#") else "#" + tag.lstrip("#"))
1351
- keys = {x.lower() for x in existing}
1352
- added = 0
1353
- exists = 0
1354
- for tag in clean:
1355
- key = tag.lower()
1356
- if key in keys:
1357
- exists += 1
1358
- continue
1359
- existing.append(tag)
1360
- keys.add(key)
1361
- added += 1
1362
- data["channels"][cid]["hashtags"] = existing
1363
- _write(data)
1364
- if added:
1365
- _write_backup(data)
1366
- return {"added": added, "exists": exists}
1367
-
1368
-
1369
- def remove_channel_hashtags(channel_id, tags):
1370
- remove_keys = set()
1371
- for tag in tags or []:
1372
- tag = str(tag).strip()
1373
- if not tag:
1374
- continue
1375
- if not tag.startswith("#"):
1376
- tag = "#" + tag.lstrip("#")
1377
- remove_keys.add(tag.lower())
1378
- if not remove_keys:
1379
- return {"removed": 0, "missing": 0}
1380
- cid = str(channel_id)
1381
- with lock:
1382
- data = _read()
1383
- if cid not in data["channels"]:
1384
- return {"removed": 0, "missing": len(remove_keys)}
1385
- existing = []
1386
- for tag in data["channels"][cid].get("hashtags", []):
1387
- tag = str(tag).strip()
1388
- if tag:
1389
- existing.append(tag if tag.startswith("#") else "#" + tag.lstrip("#"))
1390
- before = len(existing)
1391
- existing = [x for x in existing if x.lower() not in remove_keys]
1392
- removed = before - len(existing)
1393
- data["channels"][cid]["hashtags"] = existing
1394
- _write(data)
1395
- if removed:
1396
- _write_backup(data)
1397
- return {"removed": removed, "missing": max(0, len(remove_keys)-removed)}
1398
-
1399
-
1400
- def record_channel_publish_success(channel_id):
1401
- cid = str(channel_id)
1402
- with lock:
1403
- data = _read()
1404
- if cid in data.get("channels", {}):
1405
- data["channels"][cid]["fail_count"] = 0
1406
- data.setdefault("channel_failures", {})[cid] = 0
1407
- _write(data)
1408
- return True
1409
-
1410
-
1411
- def record_channel_publish_failure(channel_id, error="", limit=5):
1412
- cid = str(channel_id)
1413
- with lock:
1414
- data = _read()
1415
- failures = data.setdefault("channel_failures", {})
1416
- count = int(failures.get(cid, 0) or 0) + 1
1417
- failures[cid] = count
1418
- if cid in data.get("channels", {}):
1419
- data["channels"][cid]["fail_count"] = count
1420
- if count >= int(limit):
1421
- data["channels"][cid]["paused"] = True
1422
- data["channels"][cid]["auto_paused_reason"] = str(error or "")[:300]
1423
- data["channels"][cid]["auto_paused_at"] = _now()
1424
- errors = data.setdefault("last_errors", [])
1425
- errors.append({"ts": _now(), "context": f"publish channel {cid}", "error": str(error or "")[:500]})
1426
- data["last_errors"] = errors[-20:]
1427
- _write(data)
1428
- return count
1429
-
1430
-
1431
- def create_named_backup(name):
1432
- name = str(name or "").strip()
1433
- if not name:
1434
- name = f"backup_{_now()}"
1435
- safe = "".join(ch for ch in name if ch.isalnum() or ch in ("_", "-", ".", " ")).strip().replace(" ", "_")
1436
- if not safe:
1437
- safe = f"backup_{_now()}"
1438
- with lock:
1439
- data = _read()
1440
- os.makedirs(BACKUP_DIR, exist_ok=True)
1441
- path = os.path.join(BACKUP_DIR, f"manual_{safe}_{_now()}.json")
1442
- with open(path, "w", encoding="utf-8") as f:
1443
- json.dump(_normalize_data(data), f, ensure_ascii=False, indent=2)
1444
- named = data.setdefault("named_backups", [])
1445
- named.append({"name": name, "file": path, "ts": _now()})
1446
- data["named_backups"] = named[-30:]
1447
- _write(data)
1448
- return path
1449
-
1450
-
1451
- def get_named_backups():
1452
- with lock:
1453
- data = _read()
1454
- items = data.get("named_backups", [])
1455
- return items if isinstance(items, list) else []
1456
-
1457
-
1458
- # ===== Published message tracking for channel cleanup =====
1459
- def record_published_message(channel_id, message_id, content_type):
1460
- """يحفظ ID الرسالة التي نشرها البوت حتى يمكن حذفها لاحقاً حسب النوع."""
1461
- if not message_id:
1462
- return False
1463
- cid = str(channel_id)
1464
- ctype = str(content_type or "text")
1465
- if ctype not in {"text", "photo", "video", "album"}:
1466
- ctype = "photo"
1467
- with lock:
1468
- data = _read()
1469
- box = data.setdefault("published_messages", {}).setdefault(cid, {}).setdefault(ctype, [])
1470
- mid = int(message_id)
1471
- if not any(isinstance(x, dict) and int(x.get("id", -1)) == mid for x in box):
1472
- box.append({"id": mid, "ts": _now()})
1473
- data["published_messages"][cid][ctype] = box[-1000:]
1474
- _write(data)
1475
- return True
1476
-
1477
-
1478
- def record_published_messages(channel_id, message_ids, content_type):
1479
- count = 0
1480
- for mid in message_ids or []:
1481
- if record_published_message(channel_id, mid, content_type):
1482
- count += 1
1483
- return count
1484
-
1485
-
1486
- def get_published_message_ids(channel_id, content_type):
1487
- cid = str(channel_id)
1488
- ctype = str(content_type or "")
1489
- with lock:
1490
- data = _read()
1491
- entries = data.get("published_messages", {}).get(cid, {}).get(ctype, [])
1492
- ids = []
1493
- for item in entries:
1494
- try:
1495
- ids.append(int(item.get("id") if isinstance(item, dict) else item))
1496
- except Exception:
1497
- pass
1498
- return ids
1499
-
1500
-
1501
- def clear_published_message_ids(channel_id, content_type, message_ids=None):
1502
- cid = str(channel_id)
1503
- ctype = str(content_type or "")
1504
- remove_set = None
1505
- if message_ids is not None:
1506
- remove_set = {int(x) for x in message_ids if str(x).lstrip("-").isdigit()}
1507
- with lock:
1508
- data = _read()
1509
- entries = data.setdefault("published_messages", {}).setdefault(cid, {}).setdefault(ctype, [])
1510
- if remove_set is None:
1511
- removed = len(entries)
1512
- data["published_messages"][cid][ctype] = []
1513
- else:
1514
- before = len(entries)
1515
- data["published_messages"][cid][ctype] = [
1516
- x for x in entries
1517
- if int(x.get("id") if isinstance(x, dict) else x) not in remove_set
1518
- ]
1519
- removed = before - len(data["published_messages"][cid][ctype])
1520
- _write(data)
1521
- return removed
1522
-
1523
-
1524
- def get_ignore_short_posts():
1525
- with lock:
1526
- data = _read()
1527
- return bool(data.get("settings", {}).get("ignore_short_posts", False))
1528
-
1529
-
1530
- def set_ignore_short_posts(enabled):
1531
- with lock:
1532
- data = _read()
1533
- data.setdefault("settings", {})["ignore_short_posts"] = bool(enabled)
1534
- _write(data)
1535
- _write_backup(data)
1536
- return True
1537
-
1538
-
1539
- # ============================================================
1540
- # Link Preview: per-channel disable
1541
- # ============================================================
1542
-
1543
- def get_channel_disable_preview(channel_id, default=False):
1544
- ch = get_channel(channel_id)
1545
- if not ch:
1546
- return bool(default)
1547
- return bool(ch.get("disable_web_page_preview", default))
1548
-
1549
-
1550
- def set_channel_disable_preview(channel_id, enabled):
1551
- return update_channel(channel_id, "disable_web_page_preview", bool(enabled))
1552
-
1553
-
1554
- def get_channel_tail_enabled(channel_id):
1555
- ch = get_channel(str(channel_id))
1556
- return bool(ch.get("tail_enabled", True)) if ch else True
1557
-
1558
-
1559
- def set_channel_tail_enabled(channel_id, enabled):
1560
- return update_channel(str(channel_id), "tail_enabled", bool(enabled))
1561
-
1562
-
1563
- def get_channel_tail_min_words(channel_id):
1564
- ch = get_channel(str(channel_id))
1565
- return int(ch.get("tail_min_words", 20)) if ch else 20
1566
-
1567
-
1568
- def set_channel_tail_min_words(channel_id, count):
1569
- return update_channel(str(channel_id), "tail_min_words", max(1, int(count)))
1570
-
1571
-
1572
- def get_channel_tail_position(channel_id):
1573
- ch = get_channel(str(channel_id))
1574
- val = ch.get("tail_position", "bottom") if ch else "bottom"
1575
- return val if val in ("top", "bottom") else "bottom"
1576
-
1577
-
1578
- def set_channel_tail_position(channel_id, position):
1579
- position = position if position in ("top", "bottom") else "bottom"
1580
- return update_channel(str(channel_id), "tail_position", position)
1581
-
1582
-
1583
- def get_settings_clipboard():
1584
- with lock:
1585
- data = _read()
1586
- return data.get("settings_clipboard")
1587
-
1588
-
1589
- def set_settings_clipboard(clipboard_data):
1590
- with lock:
1591
- data = _read()
1592
- data["settings_clipboard"] = clipboard_data
1593
- _write(data)
1594
-
1595
-
1596
- # ============================================================
1597
- # Notification Center Settings
1598
- # ============================================================
1599
-
1600
- NOTIFICATION_TYPES = {
1601
- "session_stopped": "توقف Session",
1602
- "ai_key_error": "خطأ في مفتاح AI",
1603
- "source_stopped": "توقف مصدر",
1604
- "channel_idle_hour": "قناة لم تنشر منذ ساعة",
1605
- "backup_failed": "فشل Backup",
1606
- "db_issue": "مشكلة بقاعدة البيانات",
1607
- }
1608
-
1609
-
1610
- def get_notification_settings():
1611
- with lock:
1612
- data = _read()
1613
- ns = data.get("notification_settings", {})
1614
- return {k: bool(ns.get(k, True)) for k in NOTIFICATION_TYPES}
1615
-
1616
-
1617
- def set_notification_setting(key, enabled):
1618
- if key not in NOTIFICATION_TYPES:
1619
- return False
1620
- with lock:
1621
- data = _read()
1622
- ns = data.setdefault("notification_settings", {})
1623
- ns[str(key)] = bool(enabled)
1624
- _write(data)
1625
- return True
1626
-
1627
-
1628
- def get_notification_last_alert(key):
1629
- with lock:
1630
- data = _read()
1631
- ns = data.get("notification_settings", {})
1632
- return ns.get("last_alert_ts", {}).get(str(key), 0)
1633
-
1634
-
1635
- def set_notification_last_alert(key, ts=None):
1636
- if ts is None:
1637
- ts = _now()
1638
- with lock:
1639
- data = _read()
1640
- ns = data.setdefault("notification_settings", {})
1641
- alerts = ns.setdefault("last_alert_ts", {})
1642
- alerts[str(key)] = int(ts)
1643
- _write(data)
1644
- return True
1645
-
1646
-
1647
- # ============================================================
1648
- # Multi-Bot Expansion: Sessions
1649
- # ============================================================
1650
-
1651
- def _normalize_session(sid, value):
1652
- if not isinstance(value, dict):
1653
- value = {}
1654
- sid = str(sid)
1655
- value.setdefault("id", sid)
1656
- value.setdefault("name", value.get("name") or f"Session {sid.split('_')[-1]}")
1657
- value.setdefault("api_id", 0)
1658
- value.setdefault("api_hash", "")
1659
- value.setdefault("session_string", "")
1660
- value.setdefault("enabled", True)
1661
- value.setdefault("created_at", _now())
1662
- value.setdefault("last_used", None)
1663
- value.setdefault("usage_count", 0)
1664
- value.setdefault("error_count", 0)
1665
- value.setdefault("status", "idle")
1666
- value.setdefault("channels_using", [])
1667
- return value
1668
-
1669
-
1670
- def get_all_sessions():
1671
- with lock:
1672
- data = _read()
1673
- return [{"id": k, **v} for k, v in data.get("sessions", {}).items()]
1674
-
1675
-
1676
- def get_session(session_id):
1677
- sid = str(session_id)
1678
- with lock:
1679
- data = _read()
1680
- raw = data.get("sessions", {}).get(sid)
1681
- if raw is None:
1682
- return None
1683
- return {"id": sid, **raw}
1684
-
1685
-
1686
- def add_session(api_id, api_hash, session_string, name=None):
1687
- items = get_all_sessions()
1688
- n = _find_available_number([s.get("name", "") for s in items], "Session")
1689
- final_name = name or f"Session {n}"
1690
- sid = f"session_{n}"
1691
- with lock:
1692
- data = _read()
1693
- if sid in data.get("sessions", {}):
1694
- return False
1695
- entry = _normalize_session(sid, {
1696
- "id": sid, "name": final_name,
1697
- "api_id": int(api_id), "api_hash": str(api_hash),
1698
- "session_string": str(session_string),
1699
- "enabled": True, "created_at": _now(),
1700
- })
1701
- data.setdefault("sessions", {})[sid] = entry
1702
- _write(data)
1703
- _write_backup(data)
1704
- return True
1705
-
1706
-
1707
- def update_session(session_id, **kwargs):
1708
- sid = str(session_id)
1709
- allowed = {"name", "api_id", "api_hash", "session_string", "enabled", "status"}
1710
- with lock:
1711
- data = _read()
1712
- entry = data.get("sessions", {}).get(sid)
1713
- if not entry:
1714
- return False
1715
- for k, v in kwargs.items():
1716
- if k in allowed and v is not None:
1717
- entry[k] = v
1718
- _write(data)
1719
- return True
1720
-
1721
-
1722
- def delete_session(session_id):
1723
- sid = str(session_id)
1724
- with lock:
1725
- data = _read()
1726
- entry = data.get("sessions", {}).pop(sid, None)
1727
- if entry is None:
1728
- return False
1729
- item = {"id": f"trash_session_{_now()}", "type": "session", "original_id": sid, "data": entry, "deleted_at": _now()}
1730
- data.setdefault("trash", []).append(item)
1731
- _write(data)
1732
- _write_backup(data)
1733
- return True
1734
-
1735
-
1736
- def set_session_enabled(session_id, enabled):
1737
- return update_session(session_id, enabled=bool(enabled))
1738
-
1739
-
1740
- def record_session_usage(session_id):
1741
- sid = str(session_id)
1742
- with lock:
1743
- data = _read()
1744
- entry = data.get("sessions", {}).get(sid)
1745
- if not entry:
1746
- return False
1747
- entry["last_used"] = _now()
1748
- entry["usage_count"] = int(entry.get("usage_count", 0)) + 1
1749
- _write(data)
1750
- return True
1751
-
1752
-
1753
- def get_session_stats(session_id):
1754
- s = get_session(session_id)
1755
- if not s:
1756
- return {}
1757
- return {
1758
- "usage_count": s.get("usage_count", 0),
1759
- "error_count": s.get("error_count", 0),
1760
- "last_used": s.get("last_used"),
1761
- "status": s.get("status", "idle"),
1762
- "enabled": s.get("enabled", True),
1763
- "name": s.get("name", ""),
1764
- }
1765
-
1766
-
1767
- # ============================================================
1768
- # Multi-Bot Expansion: AI Keys
1769
- # ============================================================
1770
-
1771
- def _normalize_ai_key(kid, value):
1772
- if not isinstance(value, dict):
1773
- value = {}
1774
- kid = str(kid)
1775
- value.setdefault("id", kid)
1776
- value.setdefault("name", value.get("name") or f"AI {kid.split('_')[-1]}")
1777
- value.setdefault("provider", "gemini")
1778
- value.setdefault("api_key", "")
1779
- value.setdefault("enabled", True)
1780
- value.setdefault("created_at", _now())
1781
- value.setdefault("last_used", None)
1782
- value.setdefault("usage_count", 0)
1783
- value.setdefault("error_count", 0)
1784
- value.setdefault("channels_using", [])
1785
- value.setdefault("models", ["gemini-pro"])
1786
- return value
1787
-
1788
-
1789
- def get_all_ai_keys():
1790
- with lock:
1791
- data = _read()
1792
- return [{"id": k, **v} for k, v in data.get("ai_keys", {}).items()]
1793
-
1794
-
1795
- def get_ai_key(key_id):
1796
- kid = str(key_id)
1797
- with lock:
1798
- data = _read()
1799
- raw = data.get("ai_keys", {}).get(kid)
1800
- return {"id": kid, **raw} if raw else None
1801
-
1802
-
1803
- def add_ai_key(provider, api_key, name=None):
1804
- items = get_all_ai_keys()
1805
- n = _find_available_number([k.get("name", "") for k in items], "AI")
1806
- final_name = name or f"AI {n}"
1807
- kid = f"ai_{n}"
1808
- with lock:
1809
- data = _read()
1810
- if kid in data.get("ai_keys", {}):
1811
- return False
1812
- entry = _normalize_ai_key(kid, {
1813
- "id": kid, "name": final_name,
1814
- "provider": str(provider), "api_key": str(api_key),
1815
- "enabled": True, "created_at": _now(),
1816
- })
1817
- data.setdefault("ai_keys", {})[kid] = entry
1818
- _write(data)
1819
- _write_backup(data)
1820
- return True
1821
-
1822
-
1823
- def update_ai_key(key_id, **kwargs):
1824
- kid = str(key_id)
1825
- allowed = {"name", "provider", "api_key", "enabled", "models"}
1826
- with lock:
1827
- data = _read()
1828
- entry = data.get("ai_keys", {}).get(kid)
1829
- if not entry:
1830
- return False
1831
- for k, v in kwargs.items():
1832
- if k in allowed and v is not None:
1833
- entry[k] = v
1834
- _write(data)
1835
- return True
1836
-
1837
-
1838
- def delete_ai_key(key_id):
1839
- kid = str(key_id)
1840
- with lock:
1841
- data = _read()
1842
- entry = data.get("ai_keys", {}).pop(kid, None)
1843
- if entry is None:
1844
- return False
1845
- item = {"id": f"trash_ai_{_now()}", "type": "ai_key", "original_id": kid, "data": entry, "deleted_at": _now()}
1846
- data.setdefault("trash", []).append(item)
1847
- _write(data)
1848
- _write_backup(data)
1849
- return True
1850
-
1851
-
1852
- def set_ai_key_enabled(key_id, enabled):
1853
- return update_ai_key(key_id, enabled=bool(enabled))
1854
-
1855
-
1856
- def record_ai_key_usage(key_id):
1857
- kid = str(key_id)
1858
- with lock:
1859
- data = _read()
1860
- entry = data.get("ai_keys", {}).get(kid)
1861
- if not entry:
1862
- return False
1863
- entry["last_used"] = _now()
1864
- entry["usage_count"] = int(entry.get("usage_count", 0)) + 1
1865
- _write(data)
1866
- return True
1867
-
1868
-
1869
- def get_ai_key_stats(key_id):
1870
- k = get_ai_key(key_id)
1871
- if not k:
1872
- return {}
1873
- return {
1874
- "provider": k.get("provider", ""),
1875
- "usage_count": k.get("usage_count", 0),
1876
- "error_count": k.get("error_count", 0),
1877
- "last_used": k.get("last_used"),
1878
- "enabled": k.get("enabled", True),
1879
- "name": k.get("name", ""),
1880
- }
1881
-
1882
-
1883
- # ============================================================
1884
- # Multi-Bot Expansion: Publishing Bots
1885
- # ============================================================
1886
-
1887
- def _normalize_publishing_bot(bid, value):
1888
- if not isinstance(value, dict):
1889
- value = {}
1890
- bid = str(bid)
1891
- value.setdefault("id", bid)
1892
- value.setdefault("name", value.get("name") or f"Bot {bid.split('_')[-1]}")
1893
- value.setdefault("token", "")
1894
- value.setdefault("username", "")
1895
- value.setdefault("enabled", True)
1896
- value.setdefault("created_at", _now())
1897
- value.setdefault("last_publish", None)
1898
- value.setdefault("publish_count", 0)
1899
- value.setdefault("error_count", 0)
1900
- value.setdefault("channels_using", [])
1901
- return value
1902
-
1903
-
1904
- def get_all_publishing_bots():
1905
- with lock:
1906
- data = _read()
1907
- return [{"id": k, **v} for k, v in data.get("publishing_bots", {}).items()]
1908
-
1909
-
1910
- def get_publishing_bot(bot_id):
1911
- bid = str(bot_id)
1912
- with lock:
1913
- data = _read()
1914
- raw = data.get("publishing_bots", {}).get(bid)
1915
- return {"id": bid, **raw} if raw else None
1916
-
1917
-
1918
- def add_publishing_bot(token, username="", name=None):
1919
- items = get_all_publishing_bots()
1920
- n = _find_available_number([b.get("name", "") for b in items], "Bot")
1921
- final_name = name or f"Bot {n}"
1922
- bid = f"bot_{n}"
1923
- with lock:
1924
- data = _read()
1925
- if bid in data.get("publishing_bots", {}):
1926
- return False
1927
- entry = _normalize_publishing_bot(bid, {
1928
- "id": bid, "name": final_name,
1929
- "token": str(token), "username": str(username),
1930
- "enabled": True, "created_at": _now(),
1931
- })
1932
- data.setdefault("publishing_bots", {})[bid] = entry
1933
- _write(data)
1934
- _write_backup(data)
1935
- return True
1936
-
1937
-
1938
- def update_publishing_bot(bot_id, **kwargs):
1939
- bid = str(bot_id)
1940
- allowed = {"name", "token", "username", "enabled"}
1941
- with lock:
1942
- data = _read()
1943
- entry = data.get("publishing_bots", {}).get(bid)
1944
- if not entry:
1945
- return False
1946
- for k, v in kwargs.items():
1947
- if k in allowed and v is not None:
1948
- entry[k] = v
1949
- _write(data)
1950
- return True
1951
-
1952
-
1953
- def delete_publishing_bot(bot_id):
1954
- bid = str(bot_id)
1955
- with lock:
1956
- data = _read()
1957
- entry = data.get("publishing_bots", {}).pop(bid, None)
1958
- if entry is None:
1959
- return False
1960
- item = {"id": f"trash_bot_{_now()}", "type": "publishing_bot", "original_id": bid, "data": entry, "deleted_at": _now()}
1961
- data.setdefault("trash", []).append(item)
1962
- _write(data)
1963
- _write_backup(data)
1964
- return True
1965
-
1966
-
1967
- def set_publishing_bot_enabled(bot_id, enabled):
1968
- return update_publishing_bot(bot_id, enabled=bool(enabled))
1969
-
1970
-
1971
- def record_bot_publish(bot_id, content_type="text"):
1972
- bid = str(bot_id)
1973
- ctype = str(content_type or "text")
1974
- if ctype not in ("text", "photo", "video", "album"):
1975
- ctype = "text"
1976
- with lock:
1977
- data = _read()
1978
- entry = data.get("publishing_bots", {}).get(bid)
1979
- if not entry:
1980
- return False
1981
- entry["last_publish"] = _now()
1982
- entry["publish_count"] = int(entry.get("publish_count", 0)) + 1
1983
- stats = entry.setdefault("stats", {})
1984
- stats[ctype] = int(stats.get(ctype, 0)) + 1
1985
- _write(data)
1986
- return True
1987
-
1988
-
1989
- # ============================================================
1990
- # Bot-Channel Verification
1991
- # ============================================================
1992
-
1993
- def _normalize_verification(key, value):
1994
- if not isinstance(value, dict):
1995
- value = {}
1996
- value.setdefault("verified", False)
1997
- value.setdefault("can_post", False)
1998
- value.setdefault("status", "")
1999
- value.setdefault("permissions", {})
2000
- value.setdefault("last_check", 0)
2001
- value.setdefault("bot_name", "")
2002
- value.setdefault("channel_name", "")
2003
- return value
2004
-
2005
-
2006
- def set_bot_channel_verification(bot_id, channel_id, verified, can_post=False, status="", permissions=None, bot_name="", channel_name=""):
2007
- """تخزين نتيجة التحقق من وجود بوت في قناة."""
2008
- key = f"{bot_id}|{channel_id}"
2009
- with lock:
2010
- data = _read()
2011
- mapping = data.setdefault("bot_channel_verified", {})
2012
- entry = _normalize_verification(key, mapping.get(key, {}))
2013
- entry["verified"] = bool(verified)
2014
- entry["can_post"] = bool(can_post)
2015
- entry["status"] = str(status)
2016
- entry["permissions"] = dict(permissions or {})
2017
- entry["last_check"] = _now()
2018
- entry["bot_name"] = str(bot_name)
2019
- entry["channel_name"] = str(channel_name)
2020
- mapping[key] = entry
2021
- _write(data)
2022
- return True
2023
-
2024
-
2025
- def get_bot_channel_verification(bot_id, channel_id):
2026
- """إرجاع حالة التحقق المخزنة لبوت في قناة."""
2027
- key = f"{bot_id}|{channel_id}"
2028
- with lock:
2029
- data = _read()
2030
- entry = data.get("bot_channel_verified", {}).get(key)
2031
- return _normalize_verification(key, entry)
2032
-
2033
-
2034
- def get_all_bot_channel_verifications():
2035
- """إرجاع كل حالات التحقق."""
2036
- with lock:
2037
- data = _read()
2038
- return {k: _normalize_verification(k, v) for k, v in data.get("bot_channel_verified", {}).items()}
2039
-
2040
-
2041
- def get_verifications_for_bot(bot_id):
2042
- """إرجاع كل التحققات لبوت معين."""
2043
- result = {}
2044
- prefix = f"{bot_id}|"
2045
- for k, v in get_all_bot_channel_verifications().items():
2046
- if k.startswith(prefix):
2047
- ch_id = k[len(prefix):]
2048
- result[ch_id] = v
2049
- return result
2050
-
2051
-
2052
- def get_verifications_for_channel(channel_id):
2053
- """إرجاع كل التحققات لقناة معينة."""
2054
- result = {}
2055
- suffix = f"|{channel_id}"
2056
- for k, v in get_all_bot_channel_verifications().items():
2057
- if k.endswith(suffix):
2058
- bot_id = k[:-len(suffix)]
2059
- result[bot_id] = v
2060
- return result
2061
-
2062
-
2063
- # ============================================================
2064
- # Multi-Bot Expansion: Website Sources
2065
- # ============================================================
2066
-
2067
- def _normalize_website(wid, value):
2068
- if not isinstance(value, dict):
2069
- value = {}
2070
- wid = str(wid)
2071
- value.setdefault("id", wid)
2072
- value.setdefault("name", value.get("name") or f"Website {wid.split('_')[-1]}")
2073
- value.setdefault("url", "")
2074
- value.setdefault("enabled", True)
2075
- value.setdefault("selector", "body")
2076
- value.setdefault("interval", 300)
2077
- value.setdefault("created_at", _now())
2078
- value.setdefault("last_fetch", None)
2079
- value.setdefault("error_count", 0)
2080
- value.setdefault("channels_using", [])
2081
- return value
2082
-
2083
-
2084
- def get_all_websites():
2085
- with lock:
2086
- data = _read()
2087
- return [{"id": k, **v} for k, v in data.get("websites", {}).items()]
2088
-
2089
-
2090
- def get_website(website_id):
2091
- wid = str(website_id)
2092
- with lock:
2093
- data = _read()
2094
- raw = data.get("websites", {}).get(wid)
2095
- return {"id": wid, **raw} if raw else None
2096
-
2097
-
2098
- def add_website(url, name=None, selector="body"):
2099
- items = get_all_websites()
2100
- n = _find_available_number([w.get("name", "") for w in items], "Website")
2101
- final_name = name or f"Website {n}"
2102
- wid = f"web_{n}"
2103
- with lock:
2104
- data = _read()
2105
- if wid in data.get("websites", {}):
2106
- return False
2107
- entry = _normalize_website(wid, {
2108
- "id": wid, "name": final_name,
2109
- "url": str(url), "selector": str(selector),
2110
- "enabled": True, "created_at": _now(),
2111
- })
2112
- data.setdefault("websites", {})[wid] = entry
2113
- _write(data)
2114
- _write_backup(data)
2115
- return True
2116
-
2117
-
2118
- def update_website(website_id, **kwargs):
2119
- wid = str(website_id)
2120
- allowed = {"name", "url", "enabled", "selector", "interval"}
2121
- with lock:
2122
- data = _read()
2123
- entry = data.get("websites", {}).get(wid)
2124
- if not entry:
2125
- return False
2126
- for k, v in kwargs.items():
2127
- if k in allowed and v is not None:
2128
- entry[k] = v
2129
- _write(data)
2130
- return True
2131
-
2132
-
2133
- def delete_website(website_id):
2134
- wid = str(website_id)
2135
- with lock:
2136
- data = _read()
2137
- entry = data.get("websites", {}).pop(wid, None)
2138
- if entry is None:
2139
- return False
2140
- item = {"id": f"trash_web_{_now()}", "type": "website", "original_id": wid, "data": entry, "deleted_at": _now()}
2141
- data.setdefault("trash", []).append(item)
2142
- _write(data)
2143
- _write_backup(data)
2144
- return True
2145
-
2146
-
2147
- def set_website_enabled(website_id, enabled):
2148
- return update_website(website_id, enabled=bool(enabled))
2149
-
2150
-
2151
- # ============================================================
2152
- # Multi-Bot Expansion: Trash System
2153
- # ============================================================
2154
-
2155
- def get_trash_items():
2156
- with lock:
2157
- data = _read()
2158
- items = data.get("trash", [])
2159
- return items if isinstance(items, list) else []
2160
-
2161
-
2162
- def restore_from_trash(trash_id):
2163
- with lock:
2164
- data = _read()
2165
- items = data.get("trash", [])
2166
- for idx, item in enumerate(items):
2167
- if item.get("id") == trash_id:
2168
- orig_type = item.get("type")
2169
- orig_id = item.get("original_id")
2170
- orig_data = item.get("data", {})
2171
- if orig_type == "session":
2172
- data.setdefault("sessions", {})[orig_id] = orig_data
2173
- elif orig_type == "ai_key":
2174
- data.setdefault("ai_keys", {})[orig_id] = orig_data
2175
- elif orig_type == "publishing_bot":
2176
- data.setdefault("publishing_bots", {})[orig_id] = orig_data
2177
- elif orig_type == "website":
2178
- data.setdefault("websites", {})[orig_id] = orig_data
2179
- else:
2180
- return False
2181
- items.pop(idx)
2182
- data["trash"] = items
2183
- _write(data)
2184
- _write_backup(data)
2185
- return True
2186
- return False
2187
-
2188
-
2189
- def permanent_delete_from_trash(trash_id):
2190
- with lock:
2191
- data = _read()
2192
- items = data.get("trash", [])
2193
- for idx, item in enumerate(items):
2194
- if item.get("id") == trash_id:
2195
- items.pop(idx)
2196
- data["trash"] = items
2197
- _write(data)
2198
- return True
2199
- return False
2200
-
2201
-
2202
- def empty_trash():
2203
- with lock:
2204
- data = _read()
2205
- data["trash"] = []
2206
- _write(data)
2207
- return True
2208
-
2209
-
2210
- # ============================================================
2211
- # Multi-Bot Expansion: Auto Naming
2212
- # ============================================================
2213
-
2214
- def _find_available_number(names, prefix):
2215
- used = set()
2216
- for name in names:
2217
- if name.startswith(prefix):
2218
- try:
2219
- num = int(name[len(prefix):].strip())
2220
- used.add(num)
2221
- except ValueError:
2222
- pass
2223
- n = 1
2224
- while n in used:
2225
- n += 1
2226
- return n
2227
-
2228
-
2229
- # ============================================================
2230
- # Multi-Bot Expansion: Channel Config Extensions
2231
- # ============================================================
2232
-
2233
- def _normalize_channel_config(cid, value):
2234
- if not isinstance(value, dict):
2235
- value = {}
2236
- cid = str(cid)
2237
- value.setdefault("title_quote", False)
2238
- value.setdefault("signature_quote", False)
2239
- value.setdefault("assigned_sessions", [])
2240
- value.setdefault("assigned_bots", [])
2241
- value.setdefault("assigned_ai", [])
2242
- value.setdefault("websites", [])
2243
- value.setdefault("rss_sources", [])
2244
- value.setdefault("prompt", "")
2245
- value.setdefault("first_comment", "")
2246
- value.setdefault("schedule", None)
2247
- return value
2248
-
2249
-
2250
- def get_channel_config(channel_id):
2251
- cid = str(channel_id)
2252
- with lock:
2253
- data = _read()
2254
- raw = data.get("channel_configs", {}).get(cid)
2255
- return _normalize_channel_config(cid, raw)
2256
-
2257
-
2258
- def update_channel_config(channel_id, **kwargs):
2259
- cid = str(channel_id)
2260
- allowed = {"title_quote", "signature_quote", "assigned_sessions", "assigned_bots", "assigned_ai", "websites", "rss_sources", "prompt", "first_comment", "schedule"}
2261
- with lock:
2262
- data = _read()
2263
- cfg = _normalize_channel_config(cid, data.setdefault("channel_configs", {}).get(cid, {}))
2264
- for k, v in kwargs.items():
2265
- if k in allowed and v is not None:
2266
- cfg[k] = v
2267
- data["channel_configs"][cid] = cfg
2268
- _write(data)
2269
- return True
2270
-
2271
-
2272
- def set_channel_title_quote(channel_id, enabled):
2273
- return update_channel_config(channel_id, title_quote=bool(enabled))
2274
-
2275
-
2276
- def get_channel_title_quote(channel_id):
2277
- cfg = get_channel_config(channel_id)
2278
- return bool(cfg.get("title_quote", False))
2279
-
2280
-
2281
- def set_channel_signature_quote(channel_id, enabled):
2282
- return update_channel_config(channel_id, signature_quote=bool(enabled))
2283
-
2284
-
2285
- def get_channel_signature_quote(channel_id):
2286
- cfg = get_channel_config(channel_id)
2287
- return bool(cfg.get("signature_quote", False))
2288
-
2289
-
2290
- # ============================================================
2291
- # Multi-Bot Expansion: Dependency Check
2292
- # ============================================================
2293
-
2294
- def get_dependencies_for(item_type, item_id):
2295
- cid = str(item_id)
2296
- result = []
2297
- for ch in get_all_channels():
2298
- ch_id = str(ch.get("id", ""))
2299
- cfg = get_channel_config(ch_id)
2300
- if item_type == "session":
2301
- if cid in [str(x) for x in cfg.get("assigned_sessions", [])]:
2302
- result.append(ch)
2303
- elif item_type == "ai_key":
2304
- if cid in [str(x) for x in cfg.get("assigned_ai", [])]:
2305
- result.append(ch)
2306
- elif item_type == "publishing_bot":
2307
- if cid in [str(x) for x in cfg.get("assigned_bots", [])]:
2308
- result.append(ch)
2309
- elif item_type == "website":
2310
- if cid in [str(x) for x in cfg.get("websites", [])]:
2311
- result.append(ch)
2312
- return result
2313
-
2314
-
2315
- # ============================================================
2316
- # Smart Cache System
2317
- # ============================================================
2318
-
2319
- CACHE_TTL = {
2320
- "bot_in_channel": 300, # 5 د للعمليات الحساسة
2321
- "channel_accessible": 600, # 10 د للتحقق من وجود القناة
2322
- "bot_token_valid": 3600, # 1 س لصحة التوكن
2323
- "session_valid": 600, # 10 د لصلاحية الجلسة
2324
- "ui_display": 900, # 15 د للعرض في الواجهات
2325
- }
2326
-
2327
- _verification_cache = {}
2328
- _cache_lock = Lock()
2329
-
2330
-
2331
- def cache_get(key):
2332
- """إرجاع قيمة مخبأة إذا كانت ضمن TTL."""
2333
- with _cache_lock:
2334
- entry = _verification_cache.get(key)
2335
- if entry is None:
2336
- return None
2337
- if time.time() - entry["ts"] < entry["ttl"]:
2338
- return entry["data"]
2339
- del _verification_cache[key]
2340
- return None
2341
-
2342
-
2343
- def cache_set(key, data, ttl=None):
2344
- """تخزين قيمة مع TTL."""
2345
- if ttl is None:
2346
- ttl = CACHE_TTL.get("ui_display", 900)
2347
- with _cache_lock:
2348
- _verification_cache[key] = {"data": data, "ts": time.time(), "ttl": ttl}
2349
-
2350
-
2351
- def cache_invalidate(key):
2352
- """حذف قيمة من الكاش."""
2353
- with _cache_lock:
2354
- _verification_cache.pop(key, None)
2355
-
2356
-
2357
- def cache_invalidate_bot(bot_id):
2358
- """مسح كل الكاش لبوت معين."""
2359
- with _cache_lock:
2360
- to_del = [k for k in _verification_cache if f"|{bot_id}|" in k or f"token_valid|{bot_id}" in k]
2361
- for k in to_del:
2362
- del _verification_cache[k]
2363
-
2364
-
2365
- def cache_invalidate_channel(channel_id):
2366
- """مسح كل الكاش لقناة معينة."""
2367
- with _cache_lock:
2368
- to_del = [k for k in _verification_cache if k.endswith(f"|{channel_id}") or k == f"channel|{channel_id}"]
2369
- for k in to_del:
2370
- del _verification_cache[k]
2371
-
2372
-
2373
- def cache_clear_expired():
2374
- """مسح القيم المنتهية."""
2375
- now = time.time()
2376
- with _cache_lock:
2377
- to_del = [k for k, v in _verification_cache.items() if now - v["ts"] >= v["ttl"]]
2378
- for k in to_del:
2379
- del _verification_cache[k]
2380
-
2381
-
2382
- # ============================================================
2383
- # Real-Time Verification Functions (Database Layer)
2384
- # ============================================================
2385
-
2386
- VERIFY_TTL_SHORT = 60 # 1 دقيقة
2387
- VERIFY_TTL_NORMAL = 300 # 5 دقائق
2388
-
2389
-
2390
- def get_fresh_bot_verification(bot_id, channel_id):
2391
- """إرجاع الت��قق من الكاش فقط إذا كان حديثاً (أقل من TTL قصير)."""
2392
- key = f"verify|{bot_id}|{channel_id}"
2393
- return cache_get(key)
2394
-
2395
-
2396
- def set_fresh_bot_verification(bot_id, channel_id, result):
2397
- """تخزين نتيجة تحقق حديثة في الكاش."""
2398
- key = f"verify|{bot_id}|{channel_id}"
2399
- cache_set(key, result, ttl=VERIFY_TTL_SHORT)
2400
-
2401
-
2402
- def invalidate_bot_verification(bot_id, channel_id):
2403
- """مسح التحقق المخبأ لبوت في قناة."""
2404
- key = f"verify|{bot_id}|{channel_id}"
2405
- cache_invalidate(key)
2406
-
2407
-
2408
- # ============================================================
2409
- # Bot Manager - المصدر الوحيد لجميع Bot Tokens
2410
- # ============================================================
2411
-
2412
- class BotManager:
2413
- """يدير جميع بوتات النشر.
2414
-
2415
- - أي كود يحتاج توكن بوت يستخدم هذا المدير فقط.
2416
- - لا يسمح بالوصول المباشر إلى التوكن من أي مكان آخر.
2417
- - يوفر التوثيق والتخزين والربط مع القنوات.
2418
- """
2419
-
2420
- def __init__(self):
2421
- self._cache = {}
2422
-
2423
- # ---- المصدر الوحيد للتوكن ----
2424
- def get_token(self, bot_id):
2425
- """الطريقة الوحيدة في المشروع بأكمله للحصول على توكن بوت."""
2426
- b = get_publishing_bot(str(bot_id))
2427
- if not b:
2428
- return ""
2429
- return b.get("token", "")
2430
-
2431
- def get_bot(self, bot_id):
2432
- """إرجاع بيانات البوت كاملة (بدون ضمان إخفاء التوكن للاستخدام الداخلي)."""
2433
- return get_publishing_bot(str(bot_id))
2434
-
2435
- def get_all_bots(self):
2436
- """إرجاع جميع البوتات."""
2437
- return get_all_publishing_bots()
2438
-
2439
- def add_bot(self, token, username="", name=""):
2440
- """إضافة بوت جديد."""
2441
- return add_publishing_bot(token, username=username, name=name)
2442
-
2443
- def delete_bot(self, bot_id):
2444
- """حذف بوت ونقل إلى سلة المهملات."""
2445
- return delete_publishing_bot(str(bot_id))
2446
-
2447
- def update_bot(self, bot_id, **kwargs):
2448
- """تحديث بيانات بوت."""
2449
- return update_publishing_bot(str(bot_id), **kwargs)
2450
-
2451
- def set_enabled(self, bot_id, enabled):
2452
- """تشغيل/إيقاف بوت."""
2453
- return set_publishing_bot_enabled(str(bot_id), enabled)
2454
-
2455
- def record_publish(self, bot_id, content_type="text"):
2456
- """تسجيل عملية نشر لبوت."""
2457
- return record_bot_publish(str(bot_id), content_type)
2458
-
2459
- def validate_token_format(self, token):
2460
- """التحقق من صيغة التوكن (بدون اتصال بـ Telegram)."""
2461
- return bool(token and ":" in token and len(token) > 20)
2462
-
2463
-
2464
- # ============================================================
2465
- # Channel Manager - إدارة موحدة للقنوات
2466
- # ============================================================
2467
-
2468
- class ChannelManager:
2469
- """يدير جميع القنوات بشكل موحد.
2470
-
2471
- - يدمج معلومات القناة (channels) مع إعداداتها (channel_configs).
2472
- - كل قناة لها Chat ID كمعرف أساسي.
2473
- - لا يعتمد على الأسماء.
2474
- """
2475
-
2476
- def get(self, channel_id):
2477
- """إرجاع معلومات القناة موحدة مع إعداداتها."""
2478
- cid = str(channel_id)
2479
- ch = get_channel(cid)
2480
- if not ch:
2481
- return None
2482
- cfg = get_channel_config(cid)
2483
- result = dict(ch)
2484
- result["config"] = cfg
2485
- return result
2486
-
2487
- def get_all(self):
2488
- """إرجاع جميع القنوات مع إعداداتها."""
2489
- channels = get_all_channels()
2490
- result = []
2491
- for ch in channels:
2492
- cid = str(ch["id"])
2493
- cfg = get_channel_config(cid)
2494
- item = dict(ch)
2495
- item["config"] = cfg
2496
- result.append(item)
2497
- return result
2498
-
2499
- def get_active(self):
2500
- """إرجاع القنوات غير الموقوفة فقط."""
2501
- return [ch for ch in self.get_all() if not ch.get("paused")]
2502
-
2503
- def add(self, channel_id, **meta):
2504
- """إضافة قناة جديدة."""
2505
- return add_channel(str(channel_id), **meta)
2506
-
2507
- def delete(self, channel_id):
2508
- """حذف قناة."""
2509
- return delete_channel(str(channel_id))
2510
-
2511
- def update(self, channel_id, key, value):
2512
- """تحديث حقل في القناة."""
2513
- return update_channel(str(channel_id), key, value)
2514
-
2515
- def get_config(self, channel_id):
2516
- """إرجاع إعدادات القناة."""
2517
- return get_channel_config(str(channel_id))
2518
-
2519
- def update_config(self, channel_id, **kwargs):
2520
- """تحديث إعدادات القناة."""
2521
- return update_channel_config(str(channel_id), **kwargs)
2522
-
2523
- def exists(self, channel_id):
2524
- """التحقق من وجود القناة في قاعدة البيانات."""
2525
- return get_channel(str(channel_id)) is not None
2526
-
2527
-
2528
- # ============================================================
2529
- # Bot-Channel Mapper - جدول ربط مستقل
2530
- # ============================================================
2531
-
2532
- class BotChannelMapper:
2533
- """يدير العلاقة بين البوتات والقنوات.
2534
-
2535
- - المصدر الوحيد لمعرفة أي بوت مرتبط بأي قناة.
2536
- - لا يعتمد على التخزين في الذاكرة.
2537
- - كل العلاقات مخزنة في قاعدة البيانات.
2538
- """
2539
-
2540
- def assign(self, bot_id, channel_id):
2541
- """ربط بوت بقناة."""
2542
- bid = str(bot_id)
2543
- cid = str(channel_id)
2544
- cfg = get_channel_config(cid)
2545
- assigned = list(cfg.get("assigned_bots", []))
2546
- if bid not in [str(x) for x in assigned]:
2547
- assigned.append(bid)
2548
- update_channel_config(cid, assigned_bots=assigned)
2549
- return True
2550
- return False
2551
-
2552
- def unassign(self, bot_id, channel_id):
2553
- """فك ربط بوت من قناة."""
2554
- bid = str(bot_id)
2555
- cid = str(channel_id)
2556
- cfg = get_channel_config(cid)
2557
- assigned = [str(x) for x in cfg.get("assigned_bots", [])]
2558
- if bid in assigned:
2559
- assigned = [x for x in assigned if x != bid]
2560
- update_channel_config(cid, assigned_bots=assigned)
2561
- return True
2562
- return False
2563
-
2564
- def get_bots_for_channel(self, channel_id):
2565
- """إرجاع IDs البوتات المرتبطة بقناة."""
2566
- cfg = get_channel_config(str(channel_id))
2567
- return [str(x) for x in cfg.get("assigned_bots", [])]
2568
-
2569
- def get_channels_for_bot(self, bot_id):
2570
- """إرجاع IDs القنوات التي يستخدمها بوت."""
2571
- bid = str(bot_id)
2572
- result = []
2573
- for ch in get_all_channels():
2574
- cid = str(ch["id"])
2575
- cfg = get_channel_config(cid)
2576
- if bid in [str(x) for x in cfg.get("assigned_bots", [])]:
2577
- result.append(cid)
2578
- return result
2579
-
2580
- def get_assigned_bot_objects(self, channel_id):
2581
- """إرجاع بيانات البوتات الكاملة المرتبطة بقناة."""
2582
- bots = []
2583
- for bid in self.get_bots_for_channel(channel_id):
2584
- b = get_publishing_bot(bid)
2585
- if b:
2586
- bots.append(b)
2587
- return bots
2588
-
2589
- def is_assigned(self, bot_id, channel_id):
2590
- """التحقق من ارتباط بوت بقناة."""
2591
- return str(bot_id) in self.get_bots_for_channel(channel_id)
2592
-
2593
- def unassign_all_for_bot(self, bot_id):
2594
- """فك ربط بوت من جميع القنوات."""
2595
- bid = str(bot_id)
2596
- count = 0
2597
- for cid in self.get_channels_for_bot(bid):
2598
- if self.unassign(bid, cid):
2599
- count += 1
2600
- return count
2601
-
2602
-
2603
- # إنشاء instance عامة للاستخدام في جميع أنحاء المشروع
2604
- bot_manager = BotManager()
2605
- channel_manager = ChannelManager()
2606
- mapper = BotChannelMapper()