Reooiq96 commited on
Commit
f8d328a
·
verified ·
1 Parent(s): 6a91d9f

remove browser eye (operator request); relay-v2.42-guestwatch-pro

Browse files
Files changed (7) hide show
  1. Dockerfile +2 -4
  2. app.py +1 -28
  3. browser_eye.py +0 -302
  4. guest_probe.py +1 -68
  5. ingest.py +1 -1
  6. test_browser_eye.py +0 -86
  7. verify.sh +0 -1
Dockerfile CHANGED
@@ -5,10 +5,8 @@ RUN ffmpeg -hide_banner -h protocol=rtmps 2>&1 | grep -q rtmp_enhanced_codecs
5
  WORKDIR /app
6
  COPY requirements.txt .
7
  RUN pip install --no-cache-dir -U -r requirements.txt
8
- # متصفح حقيقي للعين الاحترافية (guestwatch) — يُفعَّل فقط بـ GUEST_BROWSER=1
9
- RUN pip install --no-cache-dir playwright && playwright install --with-deps chromium-headless-shell
10
- COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py stream_monitor.py telemetry.py tiktok_auth.py ingest.py guest_probe.py tt_sign.py browser_eye.py index.html ./
11
- COPY verify.sh verify_tokens.py probe_live.py test_watchdog.py test_lifecycle.py test_relay.py test_source_reconnect.py test_ingest_routes.py test_transition_ingest.py test_concurrency.py test_flv_bridge.py test_adaptive_format.py test_regressions.py test_completion.py test_channel_switch.py test_dashboard.js test_telemetry.py test_smart_mode.py test_guest_probe.py test_tt_sign.py test_browser_eye.py ./
12
 
13
  # TikTok Live Monitor → Direct Relay → YouTube. No local video files.
14
  ENV TZ=Asia/Baghdad \
 
5
  WORKDIR /app
6
  COPY requirements.txt .
7
  RUN pip install --no-cache-dir -U -r requirements.txt
8
+ COPY app.py relay.py youtube_api.py youtube_manager.py flv_bridge.py stream_monitor.py telemetry.py tiktok_auth.py ingest.py guest_probe.py tt_sign.py index.html ./
9
+ COPY verify.sh verify_tokens.py probe_live.py test_watchdog.py test_lifecycle.py test_relay.py test_source_reconnect.py test_ingest_routes.py test_transition_ingest.py test_concurrency.py test_flv_bridge.py test_adaptive_format.py test_regressions.py test_completion.py test_channel_switch.py test_dashboard.js test_telemetry.py test_smart_mode.py test_guest_probe.py test_tt_sign.py ./
 
 
10
 
11
  # TikTok Live Monitor → Direct Relay → YouTube. No local video files.
12
  ENV TZ=Asia/Baghdad \
app.py CHANGED
@@ -490,33 +490,6 @@ def _guest_pro_eye():
490
  except Exception:
491
  _guest_pro=None
492
  return _guest_pro
493
- _guest_browser=False
494
- def _guest_browser_eye():
495
- global _guest_browser
496
- if _guest_browser not in (False,'building'):return _guest_browser
497
- if os.getenv('GUEST_BROWSER','').strip().lower() not in ('1','true','yes','on'):
498
- _guest_browser=None;return None
499
- if _guest_browser=='building':return None
500
- _guest_browser='building'
501
- def _build():
502
- global _guest_browser
503
- try:
504
- import browser_eye
505
- if not browser_eye.ensure_installed(lambda m:log('system',m)):
506
- log('system','[GUEST] browser eye unavailable (playwright install failed)')
507
- _guest_browser=None;return
508
- cookie=''
509
- try:
510
- us=_guest_users()
511
- if us:cookie=tiktok_auth.headers(us[0]).get('Cookie','') or ''
512
- except Exception:cookie=''
513
- _guest_browser=browser_eye.BrowserEye(cookie_header=cookie)
514
- log('system','[GUEST] browser eye ready (cookies=%s)'%('yes' if cookie else 'no'))
515
- except Exception as e:
516
- log('system','[GUEST] browser eye build failed: %s %s'%(type(e).__name__,str(e)[:120]))
517
- _guest_browser=None
518
- import threading;threading.Thread(target=_build,name='browser-eye-build',daemon=True).start()
519
- return None
520
  _guest_diag={'n':0}
521
  def _guest_cycle():
522
  global _guest_watcher
@@ -528,7 +501,7 @@ def _guest_cycle():
528
  import guest_probe as _gp
529
  _hosts=_guest_hosts()
530
  _scan=(lambda uids:_gp.scan_host_rooms(uids,_hosts)) if _hosts else None
531
- written=_guest_watcher.cycle(users,probe=lambda u:_gp.probe_user(u,browser=_guest_browser_eye(),pro=_guest_pro_eye(),**_guest_fetchers(u)),host_scan=_scan)
532
  except Exception as e:
533
  _guest_diag['n']+=1
534
  if _guest_diag['n']<=5 or _guest_diag['n']%30==1:
 
490
  except Exception:
491
  _guest_pro=None
492
  return _guest_pro
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  _guest_diag={'n':0}
494
  def _guest_cycle():
495
  global _guest_watcher
 
501
  import guest_probe as _gp
502
  _hosts=_guest_hosts()
503
  _scan=(lambda uids:_gp.scan_host_rooms(uids,_hosts)) if _hosts else None
504
+ written=_guest_watcher.cycle(users,probe=lambda u:_gp.probe_user(u,pro=_guest_pro_eye(),**_guest_fetchers(u)),host_scan=_scan)
505
  except Exception as e:
506
  _guest_diag['n']+=1
507
  if _guest_diag['n']<=5 or _guest_diag['n']%30==1:
browser_eye.py DELETED
@@ -1,302 +0,0 @@
1
- """العين المتصفحية: متصفح Chromium حقيقي (Playwright) ينفّذ طلبات تيك توك من
2
- داخل صفحة tiktok.com الأصلية، فيوقّعها webmssdk تلقائيًا (X-Bogus/X-Gnarly/
3
- msToken حقيقية) وتُنفَّذ باتصال متصفح كامل — نفس نمط خوادم التوقيع المجانية
4
- المنشورة (carcabot/tiktok-signature وأمثالها).
5
-
6
- لماذا؟ قياس 2026-10-17: طلبات /api/* تُفرَّغ (200 بطول 0) لكل عميل غير
7
- متصفحي مهما كان التوقيع سليمًا، و webcast/room/info_by_user المنفّذة داخل
8
- متصفح حقيقي هي أقوى سياق مجاني متاح. مع كوكيز جلسة المستخدم نفسه (اختياري)
9
- تصبح العين «مُصادقة + موقّعة + متصفحية» معًا.
10
-
11
- البنية: خيط خلفي واحد يملك حلقة asyncio ومتصفحًا كسول الإقلاع يُعاد تدويره
12
- كل recycle_seconds. كل resolve ≤ ~40 ثانية؛ أي فشل يرفع استثناء والمستدعي
13
- (guest_probe) يسجّله ويكمل بالقنوات الأخرى. بلا أي حذف/تدوير لأي سجل.
14
- """
15
- from __future__ import annotations
16
-
17
- import asyncio
18
- import json
19
- import re
20
- import threading
21
- import time
22
-
23
- UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
24
- '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36')
25
- HOME_URL = 'https://www.tiktok.com/'
26
- LIVE_URL = 'https://www.tiktok.com/@%s/live'
27
- BY_USER_URL = ('https://webcast.tiktok.com/webcast/room/info_by_user/'
28
- '?aid=1988&unique_id=%s')
29
- DETAIL_URL = 'https://www.tiktok.com/api/user/detail/?uniqueId=%s'
30
- _ROOM_RE = re.compile(r'"roomId"\s*:\s*"(\d{10,})"')
31
-
32
-
33
- def parse_cookie_header(header):
34
- """'a=1; b=2' -> قائمة كوكيز Playwright لنطاق تيك توك."""
35
- out = []
36
- for part in str(header or '').split(';'):
37
- part = part.strip()
38
- if not part or '=' not in part:
39
- continue
40
- name, value = part.split('=', 1)
41
- name = name.strip()
42
- if not name:
43
- continue
44
- out.append({'name': name, 'value': value.strip(),
45
- 'domain': '.tiktok.com', 'path': '/'})
46
- return out
47
-
48
-
49
- def _json_obj_at(text, start):
50
- depth = 0
51
- in_str = False
52
- esc = False
53
- for i in range(start, len(text)):
54
- ch = text[i]
55
- if in_str:
56
- if esc:
57
- esc = False
58
- elif ch == '\\':
59
- esc = True
60
- elif ch == '"':
61
- in_str = False
62
- elif ch == '"':
63
- in_str = True
64
- elif ch == '{':
65
- depth += 1
66
- elif ch == '}':
67
- depth -= 1
68
- if depth == 0:
69
- try:
70
- return json.loads(text[start:i + 1])
71
- except ValueError:
72
- return None
73
- return None
74
-
75
-
76
- def parse_by_user_body(text):
77
- """رد info_by_user -> {owner, room, status, message}."""
78
- out = {}
79
- try:
80
- j = json.loads(text or '')
81
- except ValueError:
82
- return out
83
- data = (j or {}).get('data') or {}
84
- if not isinstance(data, dict):
85
- return out
86
- if data.get('message'):
87
- out['message'] = str(data.get('message'))[:80]
88
- owner = data.get('owner') or {}
89
- handle = str(owner.get('display_id') or owner.get('uniqueId') or owner.get('unique_id') or '')
90
- if handle:
91
- out['owner'] = handle
92
- for k in ('id_str', 'room_id', 'roomId', 'id'):
93
- v = data.get(k)
94
- if isinstance(v, int) and v > 10 ** 15:
95
- out['room'] = str(v)
96
- break
97
- if isinstance(v, str) and v.isdigit() and len(v) >= 15:
98
- out['room'] = v
99
- break
100
- if data.get('status') is not None:
101
- out['status'] = data.get('status')
102
- return out
103
-
104
-
105
- def parse_detail_body(text):
106
- """رد user/detail -> أدلة الحقول الحيّة فقط (بلا روابط)."""
107
- out = {'bytes': len(text or '')}
108
- if not text:
109
- return out
110
- try:
111
- j = json.loads(text)
112
- except ValueError:
113
- out['unparsed'] = True
114
- return out
115
- if isinstance(j, dict):
116
- usr = ((j.get('userInfo') or {}).get('user')) or {}
117
- fields = {}
118
- for k, v in usr.items():
119
- if 'live' in str(k).lower() or 'room' in str(k).lower():
120
- fields[k] = v if not isinstance(v, (dict, list)) else '<obj>'
121
- if fields:
122
- out['live_fields'] = fields
123
- ids = sorted(set(_ROOM_RE.findall(text)))
124
- if ids:
125
- out['room_ids'] = ids
126
- m = re.search(r'"liveRoom"\s*:\s*\{', text)
127
- if m:
128
- node = _json_obj_at(text, m.end() - 1)
129
- if isinstance(node, dict) and str(node.get('status')) == '2':
130
- for k in ('id_str', 'room_id', 'roomId', 'id'):
131
- v = node.get(k)
132
- if isinstance(v, int) and v > 10 ** 15:
133
- out['guest_room'] = str(v)
134
- break
135
- if isinstance(v, str) and v.isdigit() and len(v) >= 15:
136
- out['guest_room'] = v
137
- break
138
- ow = node.get('owner') or {}
139
- if isinstance(ow, dict):
140
- handle = str(ow.get('display_id') or ow.get('uniqueId') or '')
141
- if handle:
142
- out['guest_owner'] = handle
143
- return out
144
-
145
-
146
- def ensure_installed(log=lambda *a: None):
147
- """ثبّت Playwright + Chromium عند غيابهما (يُستدعى من خيط الإقلاع فقط)."""
148
- try:
149
- import playwright # noqa: F401
150
- from playwright.sync_api import sync_playwright
151
- with sync_playwright() as pw:
152
- exe = pw.chromium.executable_path
153
- import os
154
- if exe and os.path.exists(exe):
155
- return True
156
- except Exception:
157
- pass
158
- import subprocess
159
- import sys
160
- log('[BROWSER] installing playwright chromium (one-time per container)')
161
- subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'playwright'],
162
- check=False, timeout=600)
163
- subprocess.run([sys.executable, '-m', 'playwright', 'install', 'chromium-headless-shell'],
164
- check=False, timeout=900)
165
- try:
166
- import os
167
- from playwright.sync_api import sync_playwright
168
- with sync_playwright() as pw:
169
- return bool(pw.chromium.executable_path and os.path.exists(pw.chromium.executable_path))
170
- except Exception:
171
- return False
172
-
173
-
174
- class BrowserEye:
175
- """واجهة متزامنة فوق متصفح خلفي. resolve(user) يعيد dict بالأدلة."""
176
-
177
- def __init__(self, cookie_header='', recycle_seconds=1500.0):
178
- self._cookies = parse_cookie_header(cookie_header)
179
- self._recycle = float(recycle_seconds)
180
- self._loop = None
181
- self._thread = None
182
- self._browser = None
183
- self._context = None
184
- self._born = 0.0
185
- self._started = False
186
- self._lock = threading.Lock()
187
-
188
- # ---------- البنية الخلفية ----------
189
- def _ensure_thread(self):
190
- if self._thread and self._thread.is_alive():
191
- return
192
- ready = threading.Event()
193
-
194
- def run():
195
- self._loop = asyncio.new_event_loop()
196
- asyncio.set_event_loop(self._loop)
197
- ready.set()
198
- self._loop.run_forever()
199
-
200
- self._thread = threading.Thread(target=run, name='browser-eye', daemon=True)
201
- self._thread.start()
202
- ready.wait(10)
203
-
204
- def _run(self, coro, timeout):
205
- self._ensure_thread()
206
- fut = asyncio.run_coroutine_threadsafe(coro, self._loop)
207
- return fut.result(timeout=timeout)
208
-
209
- async def _get_context(self):
210
- from playwright.async_api import async_playwright
211
- now = time.time()
212
- if (self._context is not None and self._browser is not None
213
- and now - self._born < self._recycle):
214
- return self._context
215
- await self._close_async()
216
- self._pw = await async_playwright().start()
217
- self._browser = await self._pw.chromium.launch(args=['--no-sandbox'])
218
- self._context = await self._browser.new_context(user_agent=UA, locale='en-US')
219
- if self._cookies:
220
- try:
221
- await self._context.add_cookies(self._cookies)
222
- except Exception:
223
- pass
224
- page = await self._context.new_page()
225
- try:
226
- await page.goto(HOME_URL, wait_until='domcontentloaded', timeout=60000)
227
- await page.wait_for_timeout(5000)
228
- finally:
229
- await page.close()
230
- self._born = now
231
- return self._context
232
-
233
- async def _close_async(self):
234
- for attr in ('_context', '_browser'):
235
- obj = getattr(self, attr, None)
236
- if obj is not None:
237
- try:
238
- await obj.close()
239
- except Exception:
240
- pass
241
- setattr(self, attr, None)
242
- pw = getattr(self, '_pw', None)
243
- if pw is not None:
244
- try:
245
- await pw.stop()
246
- except Exception:
247
- pass
248
- self._pw = None
249
-
250
- # ---------- القياس ----------
251
- async def _resolve_async(self, user):
252
- import guest_probe
253
- ctx = await self._get_context()
254
- page = await ctx.new_page()
255
- captured = {}
256
-
257
- def on_req(req):
258
- u = req.url
259
- if 'user/detail' in u:
260
- captured['detail'] = u
261
- elif 'info_by_user' in u:
262
- captured['by_user'] = u
263
-
264
- page.on('request', on_req)
265
- out = {}
266
- try:
267
- await page.goto(LIVE_URL % user, wait_until='domcontentloaded', timeout=60000)
268
- await page.wait_for_timeout(7000)
269
- html = await page.content()
270
- out['ssr'] = guest_probe.parse_live_page(html)
271
- out['final_url'] = page.url[:120]
272
- await page.evaluate(
273
- """(u) => {
274
- fetch('/api/user/detail/?uniqueId=' + u).catch(() => {});
275
- fetch('https://webcast.tiktok.com/webcast/room/info_by_user/?aid=1988&unique_id=' + u,
276
- {credentials: 'include'}).catch(() => {});
277
- return 1;
278
- }""", user)
279
- await page.wait_for_timeout(4000)
280
- finally:
281
- page.remove_listener('request', on_req)
282
- await page.close()
283
- for name, parser in (('by_user', parse_by_user_body), ('detail', parse_detail_body)):
284
- url = captured.get(name)
285
- if not url:
286
- continue
287
- p2 = await ctx.new_page()
288
- try:
289
- resp = await p2.goto(url, wait_until='domcontentloaded', timeout=45000)
290
- body = await p2.inner_text('body')
291
- parsed = parser(body)
292
- parsed['http'] = resp.status if resp else 0
293
- out[name] = parsed
294
- except Exception as e:
295
- out[name] = {'err': type(e).__name__}
296
- finally:
297
- await p2.close()
298
- return out
299
-
300
- def resolve(self, user, timeout=120.0):
301
- with self._lock:
302
- return self._run(self._resolve_async(user), timeout)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
guest_probe.py CHANGED
@@ -546,70 +546,8 @@ def _euler_view(user, rec, pro, get_json, req):
546
  return None
547
 
548
 
549
- def _browser_view(user, rec, browser, get_json, req):
550
- """قرار العين المتصفحية: Chromium حقيقي + (اختياريًا) جلسة المستخدم نفسه.
551
- نفس قواعد _pro_view لكن بمصادر browser_*، والأدلة تحت rec['browser']."""
552
- ev = browser.resolve(user) or {}
553
- bv = {}
554
- promoted = None
555
-
556
- def _decide(owner, owner_id, room, status, source):
557
- nonlocal promoted
558
- if promoted is not None or not owner or not room or str(status) != '2':
559
- return
560
- same_person = owner.lower() == str(user).lower() or (
561
- owner_id and rec.get('uid') and str(owner_id) == str(rec.get('uid')))
562
- if same_person:
563
- promoted = ('hosting', user, room, source)
564
- else:
565
- promoted = ('guest', owner, room, source)
566
-
567
- ssr = ev.get('ssr') or {}
568
- keep = {k: ssr.get(k) for k in ('owner', 'owner_id', 'room', 'status')
569
- if ssr.get(k) not in (None, '')}
570
- if keep:
571
- bv['ssr'] = keep
572
- _decide(str(ssr.get('owner') or ''), str(ssr.get('owner_id') or ''),
573
- str(ssr.get('room') or ''), ssr.get('status'), 'browser_ssr')
574
- bu = ev.get('by_user') or {}
575
- keep = {k: bu.get(k) for k in ('owner', 'room', 'status', 'message', 'http')
576
- if bu.get(k) not in (None, '')}
577
- if keep:
578
- bv['by_user'] = keep
579
- _decide(str(bu.get('owner') or ''), '', str(bu.get('room') or ''),
580
- bu.get('status'), 'browser_by_user')
581
- dt = ev.get('detail') or {}
582
- if dt:
583
- bv['detail'] = dt
584
- if promoted is None:
585
- groom = str(dt.get('guest_room') or '')
586
- if groom and groom != str(rec.get('own_room') or ''):
587
- promoted = ('guest', str(dt.get('guest_owner') or ''), groom, 'browser_detail')
588
- rec['browser'] = bv
589
- if promoted:
590
- kind, host_user, host_room, src = promoted
591
- rec.update(state=kind, host_user=host_user, host_room=host_room,
592
- host_status=2, evidence_source=src)
593
- if kind == 'guest':
594
- try:
595
- if get_json is not None:
596
- info = (get_json(INFO_API, {'aid': 1988, 'room_id': host_room}) or {}).get('data') or {}
597
- else:
598
- rr = req().get(INFO_API, params={'aid': 1988, 'room_id': host_room},
599
- impersonate='chrome',
600
- headers={'Referer': 'https://www.tiktok.com/'}, timeout=20)
601
- rr.raise_for_status()
602
- info = (rr.json() or {}).get('data') or {}
603
- rec['watched_present'] = _watched_present(info, rec.get('uid', ''), user)
604
- rec['evidence'] = _participants_view(info)
605
- rec['host_title'] = str(info.get('title') or '')[:80]
606
- except Exception as e:
607
- rec['err'] = (rec['err'] + ' ' if rec['err'] else '') + 'browser_confirm:' + type(e).__name__
608
- return promoted
609
-
610
-
611
  def probe_user(user, client=None, get_json=None, get_page=None,
612
- get_json_authed=None, get_page_authed=None, pro=None, browser=None):
613
  """قياس كامل لمستخدم واحد الآن. يعيد سجلًّا جاهزًا للإلحاق (لا يكتب بنفسه)."""
614
  req = client
615
  def _req():
@@ -687,11 +625,6 @@ def probe_user(user, client=None, get_json=None, get_page=None,
687
  rec['state'] = classify(watched=user, own_status=rec['own_status'],
688
  page_user=rec['page_user'], page_room=rec['page_room'],
689
  host_status=rec['host_status'])
690
- if rec['state'] in ('offline', 'unknown') and browser is not None:
691
- try:
692
- _browser_view(user, rec, browser, get_json, _req)
693
- except Exception as e:
694
- rec['browser'] = {'err': type(e).__name__}
695
  if rec['state'] in ('offline', 'unknown') and pro is not None:
696
  try:
697
  _pro_view(user, rec, pro, get_json, _req)
 
546
  return None
547
 
548
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  def probe_user(user, client=None, get_json=None, get_page=None,
550
+ get_json_authed=None, get_page_authed=None, pro=None):
551
  """قياس كامل لمستخدم واحد الآن. يعيد سجلًّا جاهزًا للإلحاق (لا يكتب بنفسه)."""
552
  req = client
553
  def _req():
 
625
  rec['state'] = classify(watched=user, own_status=rec['own_status'],
626
  page_user=rec['page_user'], page_room=rec['page_room'],
627
  host_status=rec['host_status'])
 
 
 
 
 
628
  if rec['state'] in ('offline', 'unknown') and pro is not None:
629
  try:
630
  _pro_view(user, rec, pro, get_json, _req)
ingest.py CHANGED
@@ -6,7 +6,7 @@ import os, threading, time
6
  from datetime import datetime
7
  from zoneinfo import ZoneInfo
8
 
9
- APP_VERSION = 'relay-v2.41-guestwatch-browser'
10
 
11
 
12
  def env_rtmp_destination():
 
6
  from datetime import datetime
7
  from zoneinfo import ZoneInfo
8
 
9
+ APP_VERSION = 'relay-v2.42-guestwatch-pro'
10
 
11
 
12
  def env_rtmp_destination():
test_browser_eye.py DELETED
@@ -1,86 +0,0 @@
1
- """اختبارات نقية للعين المتصفحية: تحليل الكوكيز والردود دون متصفح فعلي."""
2
- import json
3
- import unittest
4
-
5
- import browser_eye as be
6
- import guest_probe as gp
7
-
8
-
9
- class CookieParseTests(unittest.TestCase):
10
- def test_pairs(self):
11
- out = be.parse_cookie_header('sessionid=abc; ttwid=xyz; ; msToken = tok ')
12
- names = [c['name'] for c in out]
13
- self.assertEqual(names, ['sessionid', 'ttwid', 'msToken'])
14
- self.assertTrue(all(c['domain'] == '.tiktok.com' for c in out))
15
-
16
- def test_empty(self):
17
- self.assertEqual(be.parse_cookie_header(''), [])
18
- self.assertEqual(be.parse_cookie_header(None), [])
19
-
20
-
21
- class BodyParseTests(unittest.TestCase):
22
- def test_by_user_finished(self):
23
- out = be.parse_by_user_body('{"data":{"message":"room has finished"},"status_code":30003}')
24
- self.assertEqual(out, {'message': 'room has finished'})
25
-
26
- def test_by_user_live_host(self):
27
- body = json.dumps({'data': {'id_str': '777666555444333222', 'status': 2,
28
- 'owner': {'display_id': 'bob'}}})
29
- out = be.parse_by_user_body(body)
30
- self.assertEqual(out['owner'], 'bob')
31
- self.assertEqual(out['room'], '777666555444333222')
32
- self.assertEqual(out['status'], 2)
33
-
34
- def test_detail_empty(self):
35
- self.assertEqual(be.parse_detail_body(''), {'bytes': 0})
36
-
37
- def test_detail_guest_room(self):
38
- body = json.dumps({'userInfo': {'user': {'uniqueId': 'alice', 'roomId': ''}},
39
- 'liveRoom': {'status': 2, 'id_str': '777666555444333222',
40
- 'owner': {'display_id': 'bob'}}})
41
- out = be.parse_detail_body(body)
42
- self.assertEqual(out['guest_room'], '777666555444333222')
43
- self.assertEqual(out['guest_owner'], 'bob')
44
-
45
-
46
- class _FakeBrowser:
47
- def __init__(self, result):
48
- self._result = result
49
-
50
- def resolve(self, user):
51
- return dict(self._result)
52
-
53
-
54
- class BrowserViewTests(unittest.TestCase):
55
- def _offline_json(self, url, params):
56
- if 'user/room' in url:
57
- return {'data': {'liveRoom': {'status': 4, 'roomId': '555'},
58
- 'user': {'id': '4242', 'roomId': '555', 'nickname': 'Alice'}}}
59
- return {'data': {'status': 2, 'title': 'Host LIVE', 'owner': {'display_id': 'bob'},
60
- 'link_mic': {'linked_user_list': []},
61
- 'linker_map': {'4242': {'user_id': '4242'}}}}
62
-
63
- def _page_self(self, url):
64
- return 'https://www.tiktok.com/@alice/live', '"roomId":"555"'
65
-
66
- def test_browser_by_user_guest(self):
67
- br = _FakeBrowser({'by_user': {'owner': 'bob', 'room': '777666555444333222', 'status': 2, 'http': 200}})
68
- rec = gp.probe_user('alice', get_json=self._offline_json,
69
- get_page=self._page_self, browser=br)
70
- self.assertEqual(rec['state'], 'guest')
71
- self.assertEqual(rec['host_user'], 'bob')
72
- self.assertEqual(rec['evidence_source'], 'browser_by_user')
73
- self.assertIs(rec['watched_present'], True)
74
-
75
- def test_browser_ssr_offline_stays_offline(self):
76
- br = _FakeBrowser({'ssr': {'owner': 'alice', 'owner_id': '4242', 'room': '555', 'status': 4},
77
- 'by_user': {'message': 'room has finished', 'http': 200}})
78
- rec = gp.probe_user('alice', get_json=self._offline_json,
79
- get_page=self._page_self, browser=br)
80
- self.assertEqual(rec['state'], 'offline')
81
- self.assertEqual(rec['browser']['by_user']['message'], 'room has finished')
82
-
83
-
84
- if __name__ == '__main__':
85
- import sys
86
- unittest.main(argv=[sys.argv[0]])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
verify.sh CHANGED
@@ -36,7 +36,6 @@ python3 test_telemetry.py "$ROOT"
36
  python3 test_smart_mode.py "$ROOT"
37
  python3 test_guest_probe.py "$ROOT"
38
  python3 test_tt_sign.py "$ROOT"
39
- python3 test_browser_eye.py "$ROOT"
40
  # Optional heavy tests when ffmpeg exists
41
  if command -v ffmpeg >/dev/null 2>&1; then
42
  python3 test_watchdog.py "$ROOT"
 
36
  python3 test_smart_mode.py "$ROOT"
37
  python3 test_guest_probe.py "$ROOT"
38
  python3 test_tt_sign.py "$ROOT"
 
39
  # Optional heavy tests when ffmpeg exists
40
  if command -v ffmpeg >/dev/null 2>&1; then
41
  python3 test_watchdog.py "$ROOT"