@tiktool/live 2.4.4 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,6 +1,76 @@
1
+ // src/client.ts
2
+ import { EventEmitter } from "events";
3
+ import * as http from "http";
4
+ import * as https from "https";
5
+ import * as zlib from "zlib";
6
+ import WebSocket from "ws";
7
+
1
8
  // src/proto.ts
2
9
  var encoder = new TextEncoder();
3
10
  var decoder = new TextDecoder();
11
+ var TIKTOK_EMOTE_MAP = {
12
+ // Standard emojis
13
+ "happy": "\u{1F60A}",
14
+ "angry": "\u{1F621}",
15
+ "cry": "\u{1F622}",
16
+ "embarrassed": "\u{1F633}",
17
+ "surprised": "\u{1F62E}",
18
+ "wronged": "\u{1F61E}",
19
+ "shout": "\u{1F624}",
20
+ "flushed": "\u{1F633}",
21
+ "yummy": "\u{1F60B}",
22
+ "complacent": "\u{1F60C}",
23
+ "drool": "\u{1F924}",
24
+ "scream": "\u{1F631}",
25
+ "weep": "\u{1F62D}",
26
+ "speechless": "\u{1F636}",
27
+ "funnyface": "\u{1F92A}",
28
+ "laughwithtears": "\u{1F602}",
29
+ "wicked": "\u{1F608}",
30
+ "facewithrollingeyes": "\u{1F644}",
31
+ "sulk": "\u{1F612}",
32
+ "thinking": "\u{1F914}",
33
+ "lovely": "\u{1F970}",
34
+ "greedy": "\u{1F911}",
35
+ "wow": "\u{1F62F}",
36
+ "joyful": "\u{1F603}",
37
+ "hehe": "\u{1F601}",
38
+ "slap": "\u{1F44B}",
39
+ "tears": "\u{1F63F}",
40
+ "stun": "\u{1F635}",
41
+ "cute": "\u{1F97A}",
42
+ "blink": "\u{1F609}",
43
+ "disdain": "\u{1F60F}",
44
+ "astonish": "\u{1F632}",
45
+ "cool": "\u{1F60E}",
46
+ "excited": "\u{1F929}",
47
+ "proud": "\u{1F624}",
48
+ "smileface": "\u{1F60A}",
49
+ "evil": "\u{1F47F}",
50
+ "angel": "\u{1F607}",
51
+ "laugh": "\u{1F606}",
52
+ "pride": "\u{1F981}",
53
+ "nap": "\u{1F634}",
54
+ "loveface": "\u{1F60D}",
55
+ "awkward": "\u{1F62C}",
56
+ "shock": "\u{1F628}",
57
+ "funny": "\u{1F604}",
58
+ "rage": "\u{1F92C}",
59
+ // Common aliases used in TikTok LIVE
60
+ "laughcry": "\u{1F602}",
61
+ "heart": "\u2764\uFE0F",
62
+ "like": "\u{1F44D}",
63
+ "love": "\u{1F495}",
64
+ "shy": "\u{1F648}",
65
+ "smile": "\u{1F60A}"
66
+ };
67
+ function replaceEmotes(text, imageMap) {
68
+ return text.replace(/\[([a-zA-Z_]+)\]/g, (match, name) => {
69
+ const lower = name.toLowerCase();
70
+ const emoji = TIKTOK_EMOTE_MAP[lower];
71
+ return emoji || match;
72
+ });
73
+ }
4
74
  function concatBytes(...arrays) {
5
75
  let totalLength = 0;
6
76
  for (const arr of arrays) totalLength += arr.length;
@@ -101,6 +171,10 @@ function getInt(fields, fn) {
101
171
  const f = fields.find((x) => x.fn === fn && x.wt === 0);
102
172
  return f ? Number(f.value) : 0;
103
173
  }
174
+ function getIntStr(fields, fn) {
175
+ const f = fields.find((x) => x.fn === fn && x.wt === 0);
176
+ return f ? String(f.value) : "";
177
+ }
104
178
  function getAllBytes(fields, fn) {
105
179
  return fields.filter((x) => x.fn === fn && x.wt === 2).map((x) => x.value);
106
180
  }
@@ -139,7 +213,7 @@ function looksLikeUsername(s) {
139
213
  }
140
214
  function parseUser(data) {
141
215
  const f = decodeProto(data);
142
- const id = String(getInt(f, 1) || getStr(f, 1));
216
+ const id = getIntStr(f, 1) || getStr(f, 1);
143
217
  const nickname = getStr(f, 3) || getStr(f, 2);
144
218
  let uniqueId = "";
145
219
  const uid4 = getStr(f, 4);
@@ -194,9 +268,10 @@ function parseUser(data) {
194
268
  }
195
269
  function parseBattleTeamFromArmies(itemBuf) {
196
270
  const f = decodeProto(itemBuf);
197
- const hostUserId = String(getInt(f, 1));
271
+ const hostUserId = getIntStr(f, 1) || "0";
198
272
  let teamScore = 0;
199
273
  const users = [];
274
+ let hostUser;
200
275
  const groups = getAllBytes(f, 2);
201
276
  for (const gb of groups) {
202
277
  try {
@@ -214,7 +289,19 @@ function parseBattleTeamFromArmies(itemBuf) {
214
289
  } catch {
215
290
  }
216
291
  }
217
- return { hostUserId, score: teamScore, users };
292
+ for (const fieldNum of [3, 4, 5, 6, 7, 8]) {
293
+ if (hostUser) break;
294
+ const buf = getBytes(f, fieldNum);
295
+ if (!buf) continue;
296
+ try {
297
+ const parsed = parseUser(buf);
298
+ if (parsed && (parsed.nickname || parsed.uniqueId) && parsed.uniqueId !== parsed.id && looksLikeUsername(parsed.uniqueId || "")) {
299
+ hostUser = parsed;
300
+ }
301
+ } catch {
302
+ }
303
+ }
304
+ return { hostUserId, score: teamScore, users, hostUser };
218
305
  }
219
306
  function parseWebcastMessage(method, payload) {
220
307
  const f = decodeProto(payload);
@@ -232,7 +319,25 @@ function parseWebcastMessage(method, payload) {
232
319
  case "WebcastChatMessage": {
233
320
  const userBuf = getBytes(f, 2);
234
321
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
235
- return { ...base, type: "chat", user, comment: getStr(f, 3) };
322
+ const rawComment = getStr(f, 3);
323
+ const emoteImages = {};
324
+ for (const field of f) {
325
+ if (field.fn === 22 && field.wt === 2) {
326
+ try {
327
+ const ewi = decodeProto(field.value);
328
+ const emoteKey = getStr(ewi, 1);
329
+ const imgBuf = getBytes(ewi, 2);
330
+ if (imgBuf && emoteKey) {
331
+ const imgFields = decodeProto(imgBuf);
332
+ const url = getStr(imgFields, 1);
333
+ if (url) emoteImages[emoteKey] = url;
334
+ }
335
+ } catch {
336
+ }
337
+ }
338
+ }
339
+ const comment = replaceEmotes(rawComment, emoteImages);
340
+ return { ...base, type: "chat", user, comment };
236
341
  }
237
342
  case "WebcastMemberMessage": {
238
343
  const userBuf = getBytes(f, 2);
@@ -286,7 +391,7 @@ function parseWebcastMessage(method, payload) {
286
391
  const extraBuf = getBytes(f, 23);
287
392
  if (extraBuf) {
288
393
  const ef = decodeProto(extraBuf);
289
- toUserId = String(getInt(ef, 8));
394
+ toUserId = getIntStr(ef, 8) || "";
290
395
  }
291
396
  const groupId = toUserId || getStr(f, 11);
292
397
  return {
@@ -329,10 +434,36 @@ function parseWebcastMessage(method, payload) {
329
434
  return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
330
435
  }
331
436
  case "WebcastLinkMicBattle": {
332
- const battleId = String(getInt(f, 1) || "");
333
- const status = getInt(f, 2) || 1;
334
- const battleDuration = getInt(f, 3);
437
+ const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
438
+ const overallStatus = getInt(f, 4);
335
439
  const teams = [];
440
+ let battleDuration = 0;
441
+ let battleSettings;
442
+ const settingsBuf = getBytes(f, 3);
443
+ if (settingsBuf) {
444
+ try {
445
+ const sf = decodeProto(settingsBuf);
446
+ const startTimeMs = getInt(sf, 2);
447
+ const duration = getInt(sf, 3);
448
+ const phase = getInt(sf, 5);
449
+ const endTimeMs = getInt(sf, 10);
450
+ battleDuration = duration;
451
+ battleSettings = {
452
+ startTimeMs: startTimeMs || void 0,
453
+ duration: duration || void 0,
454
+ endTimeMs: endTimeMs || void 0
455
+ };
456
+ } catch {
457
+ }
458
+ }
459
+ const settingsPhase = settingsBuf ? (() => {
460
+ try {
461
+ return getInt(decodeProto(settingsBuf), 5);
462
+ } catch {
463
+ return 0;
464
+ }
465
+ })() : 0;
466
+ const status = settingsPhase || (overallStatus > 0 && overallStatus <= 10 ? overallStatus : 0) || 1;
336
467
  const battleUserBufs = getAllBytes(f, 10);
337
468
  for (const bub of battleUserBufs) {
338
469
  try {
@@ -353,6 +484,23 @@ function parseWebcastMessage(method, payload) {
353
484
  } catch {
354
485
  }
355
486
  }
487
+ if (teams.length === 0) {
488
+ const teamBufs5 = getAllBytes(f, 5);
489
+ for (const tb of teamBufs5) {
490
+ try {
491
+ const tf = decodeProto(tb);
492
+ const userId = getIntStr(tf, 1);
493
+ if (userId && userId !== "0") {
494
+ teams.push({
495
+ hostUserId: userId,
496
+ score: 0,
497
+ users: []
498
+ });
499
+ }
500
+ } catch {
501
+ }
502
+ }
503
+ }
356
504
  if (teams.length === 0) {
357
505
  const teamBufs7 = getAllBytes(f, 7);
358
506
  for (const tb of teamBufs7) {
@@ -362,10 +510,10 @@ function parseWebcastMessage(method, payload) {
362
510
  }
363
511
  }
364
512
  }
365
- return { ...base, type: "battle", battleId, status, battleDuration, teams };
513
+ return { ...base, type: "battle", battleId, status, battleDuration, teams, battleSettings };
366
514
  }
367
515
  case "WebcastLinkMicArmies": {
368
- const battleId = String(getInt(f, 1) || "");
516
+ const battleId = getIntStr(f, 1) || "";
369
517
  const battleStatus = getInt(f, 7);
370
518
  const teams = [];
371
519
  const itemBufs = getAllBytes(f, 3);
@@ -447,8 +595,7 @@ function parseWebcastMessage(method, payload) {
447
595
  const title = getStr(f, 4) || getStr(f, 2);
448
596
  return { ...base, type: "liveIntro", roomId, title };
449
597
  }
450
- case "WebcastLinkMicMethod":
451
- case "WebcastLinkmicBattleTaskMessage": {
598
+ case "WebcastLinkMicMethod": {
452
599
  const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
453
600
  const users = [];
454
601
  const userBufs = getAllBytes(f, 2);
@@ -460,6 +607,155 @@ function parseWebcastMessage(method, payload) {
460
607
  }
461
608
  return { ...base, type: "linkMic", action, users };
462
609
  }
610
+ case "WebcastLinkmicBattleTaskMessage": {
611
+ const taskAction = getInt(f, 2);
612
+ const battleRefId = getIntStr(f, 20) || "";
613
+ let timerType = 0;
614
+ let remainingSeconds = 0;
615
+ let endTimestampS = 0;
616
+ const timerBuf = getBytes(f, 5);
617
+ if (timerBuf) {
618
+ try {
619
+ const tf = decodeProto(timerBuf);
620
+ timerType = getInt(tf, 1);
621
+ remainingSeconds = getInt(tf, 2);
622
+ endTimestampS = getInt(tf, 3);
623
+ } catch {
624
+ }
625
+ }
626
+ let multiplier = 0;
627
+ let missionDuration = 0;
628
+ let missionTarget = 0;
629
+ let missionType = "";
630
+ const bonusBuf = getBytes(f, 3);
631
+ if (bonusBuf) {
632
+ try {
633
+ const bf = decodeProto(bonusBuf);
634
+ const missionBuf = getBytes(bf, 1);
635
+ if (missionBuf) {
636
+ const mf = decodeProto(missionBuf);
637
+ missionTarget = getInt(mf, 1);
638
+ const items = getAllBytes(mf, 2);
639
+ for (const item of items) {
640
+ try {
641
+ const itemFields = decodeProto(item);
642
+ const descBuf = getBytes(itemFields, 2);
643
+ if (descBuf) {
644
+ const descFields = decodeProto(descBuf);
645
+ const paramBufs = getAllBytes(descFields, 2);
646
+ for (const pb of paramBufs) {
647
+ try {
648
+ const pf = decodeProto(pb);
649
+ const key = getStr(pf, 1);
650
+ const val = getStr(pf, 2);
651
+ if (key === "multi" && val) multiplier = parseInt(val) || 0;
652
+ if (key === "dur" && val) missionDuration = parseInt(val) || 0;
653
+ if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
654
+ } catch {
655
+ }
656
+ }
657
+ const typeStr = getStr(descFields, 1);
658
+ if (typeStr) missionType = typeStr;
659
+ }
660
+ } catch {
661
+ }
662
+ }
663
+ const countdownBuf = getBytes(mf, 3);
664
+ if (countdownBuf) {
665
+ try {
666
+ const cf = decodeProto(countdownBuf);
667
+ if (!missionDuration) missionDuration = getInt(cf, 2);
668
+ } catch {
669
+ }
670
+ }
671
+ }
672
+ } catch {
673
+ }
674
+ }
675
+ if (!missionType) {
676
+ const descBuf6 = getBytes(f, 6);
677
+ if (descBuf6) {
678
+ try {
679
+ const d6 = decodeProto(descBuf6);
680
+ const innerBuf = getBytes(d6, 1);
681
+ if (innerBuf) {
682
+ const innerF = decodeProto(innerBuf);
683
+ const typeStr = getStr(innerF, 1);
684
+ if (typeStr) missionType = typeStr;
685
+ const paramBufs = getAllBytes(innerF, 2);
686
+ for (const pb of paramBufs) {
687
+ try {
688
+ const pf = decodeProto(pb);
689
+ const key = getStr(pf, 1);
690
+ const val = getStr(pf, 2);
691
+ if (key === "multi" && val) multiplier = parseInt(val) || 0;
692
+ if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
693
+ } catch {
694
+ }
695
+ }
696
+ }
697
+ } catch {
698
+ }
699
+ }
700
+ }
701
+ return {
702
+ ...base,
703
+ type: "battleTask",
704
+ taskAction,
705
+ battleRefId,
706
+ missionType,
707
+ multiplier,
708
+ missionDuration,
709
+ missionTarget,
710
+ remainingSeconds,
711
+ endTimestampS,
712
+ timerType
713
+ };
714
+ }
715
+ case "WebcastBarrageMessage": {
716
+ const fieldSummary = f.map((x) => `fn=${x.fn} wt=${x.wt} val=${x.wt === 0 ? x.value : x.wt === 2 ? `[bytes:${x.value.length}]` : x.value}`).join(" | ");
717
+ console.log(`[proto] WebcastBarrageMessage fields: ${fieldSummary}`);
718
+ const msgType = getInt(f, 3);
719
+ const duration = getInt(f, 4);
720
+ const displayType = getInt(f, 5);
721
+ const subType = getInt(f, 6);
722
+ let defaultPattern = "";
723
+ let content = "";
724
+ const contentBuf = getBytes(f, 2);
725
+ if (contentBuf) {
726
+ try {
727
+ const cf = decodeProto(contentBuf);
728
+ defaultPattern = getStr(cf, 1) || "";
729
+ const paramBufs = getAllBytes(cf, 2);
730
+ const params = [];
731
+ for (const pb of paramBufs) {
732
+ try {
733
+ const pf = decodeProto(pb);
734
+ const key = getStr(pf, 1);
735
+ const val = getStr(pf, 2);
736
+ if (key && val) params.push(`${key}=${val}`);
737
+ } catch {
738
+ }
739
+ }
740
+ if (params.length > 0) content = params.join(", ");
741
+ } catch {
742
+ }
743
+ }
744
+ if (!defaultPattern) {
745
+ defaultPattern = getStr(f, 1) || "";
746
+ }
747
+ console.log(`[proto] Barrage parsed: msgType=${msgType} subType=${subType} displayType=${displayType} duration=${duration} pattern="${defaultPattern}" content="${content}"`);
748
+ return {
749
+ ...base,
750
+ type: "barrage",
751
+ msgType,
752
+ subType,
753
+ displayType,
754
+ duration,
755
+ defaultPattern,
756
+ content
757
+ };
758
+ }
463
759
  default:
464
760
  return { ...base, type: "unknown", method };
465
761
  }
@@ -481,260 +777,30 @@ function parseWebcastResponse(payload) {
481
777
  return events;
482
778
  }
483
779
 
484
- // src/api.ts
780
+ // src/client.ts
485
781
  var DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
486
782
  var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
487
- var pageCache = /* @__PURE__ */ new Map();
488
- var PAGE_CACHE_TTL = 5 * 60 * 1e3;
489
- async function resolveLivePage(uniqueId) {
490
- const clean = uniqueId.replace(/^@/, "");
491
- const cached = pageCache.get(clean);
492
- if (cached && Date.now() - cached.ts < PAGE_CACHE_TTL) {
493
- return cached.info;
494
- }
495
- try {
496
- const resp = await fetch(`https://www.tiktok.com/@${clean}/live`, {
497
- headers: {
498
- "User-Agent": DEFAULT_UA,
499
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
500
- "Accept-Language": "en-US,en;q=0.9"
501
- },
502
- redirect: "follow"
783
+ function httpGet(url, headers) {
784
+ return new Promise((resolve, reject) => {
785
+ const mod = url.startsWith("https") ? https : http;
786
+ const req = mod.get(url, { headers }, (res) => {
787
+ const chunks = [];
788
+ const enc = res.headers["content-encoding"];
789
+ const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
790
+ stream.on("data", (c) => chunks.push(c));
791
+ stream.on("end", () => resolve({
792
+ status: res.statusCode || 0,
793
+ headers: res.headers,
794
+ body: Buffer.concat(chunks)
795
+ }));
796
+ stream.on("error", reject);
503
797
  });
504
- if (!resp.ok) return null;
505
- let ttwid = "";
506
- const setCookies = resp.headers.get("set-cookie") || "";
507
- for (const part of setCookies.split(",")) {
508
- const trimmed = part.trim();
509
- if (trimmed.startsWith("ttwid=")) {
510
- ttwid = trimmed.split(";")[0].split("=").slice(1).join("=");
511
- break;
512
- }
513
- }
514
- if (!ttwid && typeof resp.headers.getSetCookie === "function") {
515
- for (const sc of resp.headers.getSetCookie()) {
516
- if (typeof sc === "string" && sc.startsWith("ttwid=")) {
517
- ttwid = sc.split(";")[0].split("=").slice(1).join("=");
518
- break;
519
- }
520
- }
521
- }
522
- const html = await resp.text();
523
- let roomId = "";
524
- const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
525
- if (sigiMatch) {
526
- try {
527
- const json = JSON.parse(sigiMatch[1]);
528
- const jsonStr = JSON.stringify(json);
529
- const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
530
- if (m) roomId = m[1];
531
- } catch {
532
- }
533
- }
534
- if (!roomId) {
535
- const patterns = [
536
- /"roomId"\s*:\s*"(\d+)"/,
537
- /room_id[=/](\d{10,})/,
538
- /"idStr"\s*:\s*"(\d{10,})"/
539
- ];
540
- for (const p of patterns) {
541
- const m = html.match(p);
542
- if (m) {
543
- roomId = m[1];
544
- break;
545
- }
546
- }
547
- }
548
- if (!roomId) return null;
549
- const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
550
- const clusterRegion = crMatch ? crMatch[1] : "";
551
- const info = { roomId, ttwid, clusterRegion };
552
- pageCache.set(clean, { info, ts: Date.now() });
553
- return info;
554
- } catch {
555
- }
556
- return null;
557
- }
558
- async function resolveRoomId(uniqueId) {
559
- const info = await resolveLivePage(uniqueId);
560
- return info?.roomId ?? null;
561
- }
562
- async function fetchSignedUrl(response) {
563
- if (!response.signed_url) {
564
- return null;
565
- }
566
- const headers = { ...response.headers || {} };
567
- if (response.cookies) {
568
- headers["Cookie"] = response.cookies;
569
- }
570
- const resp = await fetch(response.signed_url, { headers, redirect: "follow" });
571
- const text = await resp.text();
572
- try {
573
- return JSON.parse(text);
574
- } catch {
575
- return null;
576
- }
577
- }
578
- async function callApi(opts) {
579
- const serverUrl = (opts.serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
580
- const isGet = opts.method === "GET";
581
- const ak = encodeURIComponent(opts.apiKey);
582
- const url1 = isGet ? `${serverUrl}${opts.endpoint}?apiKey=${ak}&unique_id=${encodeURIComponent(opts.uniqueId)}` : `${serverUrl}${opts.endpoint}?apiKey=${ak}`;
583
- const fetchOpts1 = isGet ? {} : {
584
- method: "POST",
585
- headers: { "Content-Type": "application/json" },
586
- body: JSON.stringify({ unique_id: opts.uniqueId, ...opts.extraBody })
587
- };
588
- const resp1 = await fetch(url1, fetchOpts1);
589
- const data1 = await resp1.json();
590
- if (data1.signed_url || data1.action === "fetch_signed_url") {
591
- return fetchSignedUrl(data1);
592
- }
593
- if (data1.status_code === 0 && data1.action !== "resolve_required") {
594
- return data1;
595
- }
596
- if (data1.action === "resolve_required") {
597
- const roomId = await resolveRoomId(opts.uniqueId);
598
- if (!roomId) return null;
599
- const url2 = isGet ? `${serverUrl}${opts.endpoint}?apiKey=${ak}&room_id=${encodeURIComponent(roomId)}` : `${serverUrl}${opts.endpoint}?apiKey=${ak}`;
600
- const fetchOpts2 = isGet ? {} : {
601
- method: "POST",
602
- headers: { "Content-Type": "application/json" },
603
- body: JSON.stringify({ room_id: roomId, ...opts.extraBody })
604
- };
605
- const resp2 = await fetch(url2, fetchOpts2);
606
- const data2 = await resp2.json();
607
- if (data2.signed_url || data2.action === "fetch_signed_url") {
608
- return fetchSignedUrl(data2);
609
- }
610
- return data2;
611
- }
612
- return data1;
613
- }
614
- async function solvePuzzle(apiKey, puzzleB64, pieceB64, serverUrl) {
615
- const base = (serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
616
- const resp = await fetch(`${base}/captcha/solve/puzzle?apiKey=${encodeURIComponent(apiKey)}`, {
617
- method: "POST",
618
- headers: { "Content-Type": "application/json" },
619
- body: JSON.stringify({ puzzle: puzzleB64, piece: pieceB64 })
620
- });
621
- const data = await resp.json();
622
- if (data.status_code !== 0) {
623
- throw new Error(data.error || `Solver failed (status ${data.status_code})`);
624
- }
625
- return data.data;
626
- }
627
- async function solveRotate(apiKey, outerB64, innerB64, serverUrl) {
628
- const base = (serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
629
- const resp = await fetch(`${base}/captcha/solve/rotate?apiKey=${encodeURIComponent(apiKey)}`, {
630
- method: "POST",
631
- headers: { "Content-Type": "application/json" },
632
- body: JSON.stringify({ outer: outerB64, inner: innerB64 })
633
- });
634
- const data = await resp.json();
635
- if (data.status_code !== 0) {
636
- throw new Error(data.error || `Solver failed (status ${data.status_code})`);
637
- }
638
- return data.data;
639
- }
640
- async function solveShapes(apiKey, imageB64, serverUrl) {
641
- const base = (serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
642
- const resp = await fetch(`${base}/captcha/solve/shapes?apiKey=${encodeURIComponent(apiKey)}`, {
643
- method: "POST",
644
- headers: { "Content-Type": "application/json" },
645
- body: JSON.stringify({ image: imageB64 })
646
- });
647
- const data = await resp.json();
648
- if (data.status_code !== 0) {
649
- throw new Error(data.error || `Solver failed (status ${data.status_code})`);
650
- }
651
- return data.data;
652
- }
653
-
654
- // src/client.ts
655
- var DEFAULT_UA2 = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
656
- var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
657
- var TypedEmitter = class {
658
- _listeners = /* @__PURE__ */ new Map();
659
- on(event, fn) {
660
- const arr = this._listeners.get(event) || [];
661
- arr.push(fn);
662
- this._listeners.set(event, arr);
663
- return this;
664
- }
665
- once(event, fn) {
666
- const wrapper = (...args) => {
667
- this.off(event, wrapper);
668
- fn(...args);
669
- };
670
- return this.on(event, wrapper);
671
- }
672
- off(event, fn) {
673
- const arr = this._listeners.get(event);
674
- if (arr) {
675
- this._listeners.set(event, arr.filter((l) => l !== fn));
676
- }
677
- return this;
678
- }
679
- emit(event, ...args) {
680
- const arr = this._listeners.get(event);
681
- if (!arr || arr.length === 0) return false;
682
- for (const fn of [...arr]) fn(...args);
683
- return true;
684
- }
685
- removeAllListeners(event) {
686
- if (event) this._listeners.delete(event);
687
- else this._listeners.clear();
688
- return this;
689
- }
690
- };
691
- async function gunzip(data) {
692
- if (typeof DecompressionStream !== "undefined") {
693
- const ds = new DecompressionStream("gzip");
694
- const writer = ds.writable.getWriter();
695
- const reader = ds.readable.getReader();
696
- writer.write(data);
697
- writer.close();
698
- const chunks = [];
699
- while (true) {
700
- const { done, value } = await reader.read();
701
- if (done) break;
702
- chunks.push(value);
703
- }
704
- return concatBytes(...chunks);
705
- }
706
- try {
707
- const zlib = await import("zlib");
708
- return new Promise((resolve, reject) => {
709
- zlib.gunzip(data, (err, result) => {
710
- if (err) reject(err);
711
- else resolve(new Uint8Array(result));
712
- });
798
+ req.on("error", reject);
799
+ req.setTimeout(15e3, () => {
800
+ req.destroy();
801
+ reject(new Error("Request timeout"));
713
802
  });
714
- } catch {
715
- return data;
716
- }
717
- }
718
- var _zlib = null;
719
- var _zlibLoadAttempted = false;
720
- async function ensureZlib() {
721
- if (_zlib) return _zlib;
722
- if (_zlibLoadAttempted) return null;
723
- _zlibLoadAttempted = true;
724
- try {
725
- _zlib = await import("zlib");
726
- } catch {
727
- }
728
- return _zlib;
729
- }
730
- ensureZlib();
731
- function gunzipSync(data) {
732
- if (!_zlib) return null;
733
- try {
734
- return new Uint8Array(_zlib.gunzipSync(data));
735
- } catch {
736
- return null;
737
- }
803
+ });
738
804
  }
739
805
  function getWsHost(clusterRegion) {
740
806
  if (!clusterRegion) return "webcast-ws.tiktok.com";
@@ -743,21 +809,7 @@ function getWsHost(clusterRegion) {
743
809
  if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
744
810
  return "webcast-ws.tiktok.com";
745
811
  }
746
- async function resolveWebSocket(userImpl) {
747
- if (userImpl) return userImpl;
748
- if (typeof globalThis.WebSocket !== "undefined") {
749
- return globalThis.WebSocket;
750
- }
751
- try {
752
- const ws = await import("ws");
753
- return ws.default || ws;
754
- } catch {
755
- throw new Error(
756
- 'No WebSocket implementation found. Either use Node.js 22+ (native WebSocket), Cloudflare Workers, or install the "ws" package: npm i ws'
757
- );
758
- }
759
- }
760
- var TikTokLive = class extends TypedEmitter {
812
+ var TikTokLive = class extends EventEmitter {
761
813
  ws = null;
762
814
  heartbeatTimer = null;
763
815
  reconnectAttempts = 0;
@@ -765,7 +817,7 @@ var TikTokLive = class extends TypedEmitter {
765
817
  _connected = false;
766
818
  _eventCount = 0;
767
819
  _roomId = "";
768
- _battleHosts = /* @__PURE__ */ new Map();
820
+ _ownerUserId = "";
769
821
  uniqueId;
770
822
  signServerUrl;
771
823
  apiKey;
@@ -773,30 +825,55 @@ var TikTokLive = class extends TypedEmitter {
773
825
  maxReconnectAttempts;
774
826
  heartbeatInterval;
775
827
  debug;
776
- webSocketImpl;
777
- WS;
778
828
  constructor(options) {
779
829
  super();
780
830
  this.uniqueId = options.uniqueId.replace(/^@/, "");
781
- this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
831
+ this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
782
832
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
783
833
  this.apiKey = options.apiKey;
784
834
  this.autoReconnect = options.autoReconnect ?? true;
785
835
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
786
836
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
787
837
  this.debug = options.debug ?? false;
788
- this.webSocketImpl = options.webSocketImpl;
789
838
  }
790
839
  async connect() {
791
840
  this.intentionalClose = false;
792
- if (!this.WS) {
793
- this.WS = await resolveWebSocket(this.webSocketImpl);
841
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
842
+ "User-Agent": DEFAULT_UA,
843
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
844
+ "Accept-Encoding": "gzip, deflate, br",
845
+ "Accept-Language": "en-US,en;q=0.9"
846
+ });
847
+ let ttwid = "";
848
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
849
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
850
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
851
+ break;
852
+ }
853
+ }
854
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
855
+ const html = resp.body.toString();
856
+ let roomId = "";
857
+ let ownerUserId = "";
858
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
859
+ if (sigiMatch) {
860
+ try {
861
+ const json = JSON.parse(sigiMatch[1]);
862
+ const jsonStr = JSON.stringify(json);
863
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
864
+ if (m) roomId = m[1];
865
+ const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
866
+ if (ownerUser?.id) {
867
+ ownerUserId = String(ownerUser.id);
868
+ }
869
+ } catch {
870
+ }
794
871
  }
795
- const pageInfo = await resolveLivePage(this.uniqueId);
796
- if (!pageInfo) throw new Error(`User @${this.uniqueId} is not currently live`);
797
- if (!pageInfo.ttwid) throw new Error("Failed to obtain session cookie");
798
- const { roomId, ttwid, clusterRegion } = pageInfo;
872
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
799
873
  this._roomId = roomId;
874
+ this._ownerUserId = ownerUserId;
875
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
876
+ const clusterRegion = crMatch ? crMatch[1] : "";
800
877
  const wsHost = getWsHost(clusterRegion);
801
878
  const wsParams = new URLSearchParams({
802
879
  version_code: "270000",
@@ -807,9 +884,9 @@ var TikTokLive = class extends TypedEmitter {
807
884
  browser_language: "en-US",
808
885
  browser_platform: "Win32",
809
886
  browser_name: "Mozilla",
810
- browser_version: DEFAULT_UA2.split("Mozilla/")[1] || "5.0",
887
+ browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
811
888
  browser_online: "true",
812
- tz_name: "Etc/UTC",
889
+ tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
813
890
  app_name: "tiktok_web",
814
891
  sup_ws_ds_opt: "1",
815
892
  update_version_code: "2.0.0",
@@ -850,71 +927,48 @@ var TikTokLive = class extends TypedEmitter {
850
927
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
851
928
  }
852
929
  return new Promise((resolve, reject) => {
853
- const connUrl = wsUrl + (wsUrl.includes("?") ? "&" : "?") + `ttwid=${ttwid}`;
854
- try {
855
- this.ws = new this.WS(connUrl, {
856
- headers: {
857
- "User-Agent": DEFAULT_UA2,
858
- "Cookie": `ttwid=${ttwid}`,
859
- "Origin": "https://www.tiktok.com"
860
- }
861
- });
862
- } catch {
863
- this.ws = new this.WS(connUrl);
864
- }
865
- const ws = this.ws;
866
- let settled = false;
867
- ws.onopen = () => {
930
+ this.ws = new WebSocket(wsUrl, {
931
+ headers: {
932
+ "User-Agent": DEFAULT_UA,
933
+ "Cookie": `ttwid=${ttwid}`,
934
+ "Origin": "https://www.tiktok.com"
935
+ }
936
+ });
937
+ this.ws.on("open", () => {
868
938
  this._connected = true;
869
939
  this.reconnectAttempts = 0;
870
- const hb = buildHeartbeat(roomId);
871
- const enter = buildImEnterRoom(roomId);
872
- ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
873
- ws.send(enter.buffer.byteLength === enter.length ? enter.buffer : enter.buffer.slice(enter.byteOffset, enter.byteOffset + enter.byteLength));
940
+ this.ws.send(buildHeartbeat(roomId));
941
+ this.ws.send(buildImEnterRoom(roomId));
874
942
  this.startHeartbeat(roomId);
875
943
  const roomInfo = {
876
944
  roomId,
877
945
  wsHost,
878
946
  clusterRegion,
879
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
947
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
948
+ ownerUserId: ownerUserId || void 0
880
949
  };
881
950
  this.emit("connected");
882
951
  this.emit("roomInfo", roomInfo);
883
- if (!settled) {
884
- settled = true;
885
- resolve();
886
- }
887
- };
888
- ws.onmessage = (event) => {
889
- const raw = event.data !== void 0 ? event.data : event;
890
- this.handleMessage(raw);
891
- };
892
- ws.onclose = (event) => {
952
+ resolve();
953
+ });
954
+ this.ws.on("message", (rawData) => {
955
+ this.handleFrame(Buffer.from(rawData));
956
+ });
957
+ this.ws.on("close", (code, reason) => {
893
958
  this._connected = false;
894
959
  this.stopHeartbeat();
895
- const code = event?.code ?? 1006;
896
- const reason = event?.reason ?? "";
897
- this.emit("disconnected", code, reason?.toString?.() || "");
960
+ const reasonStr = reason?.toString() || "";
961
+ this.emit("disconnected", code, reasonStr);
898
962
  if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
899
963
  this.reconnectAttempts++;
900
964
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
901
965
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
902
966
  }
903
- };
904
- ws.onerror = (err) => {
905
- this.emit("error", err instanceof Error ? err : new Error(String(err?.message || err)));
906
- if (!settled) {
907
- settled = true;
908
- reject(err);
909
- }
910
- };
911
- setTimeout(() => {
912
- if (!settled) {
913
- settled = true;
914
- ws.close();
915
- reject(new Error("Connection timeout"));
916
- }
917
- }, 15e3);
967
+ });
968
+ this.ws.on("error", (err) => {
969
+ this.emit("error", err);
970
+ if (!this._connected) reject(err);
971
+ });
918
972
  });
919
973
  }
920
974
  disconnect() {
@@ -947,69 +1001,27 @@ var TikTokLive = class extends TypedEmitter {
947
1001
  emit(event, ...args) {
948
1002
  return super.emit(event, ...args);
949
1003
  }
950
- async handleMessage(raw) {
951
- try {
952
- let bytes;
953
- if (raw instanceof ArrayBuffer) {
954
- bytes = new Uint8Array(raw);
955
- } else if (raw instanceof Uint8Array) {
956
- bytes = raw;
957
- } else if (typeof Blob !== "undefined" && raw instanceof Blob) {
958
- bytes = new Uint8Array(await raw.arrayBuffer());
959
- } else if (raw?.buffer instanceof ArrayBuffer) {
960
- bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
961
- } else {
962
- return;
963
- }
964
- this.handleFrame(bytes);
965
- } catch {
966
- }
967
- }
968
- async handleFrame(buf) {
1004
+ handleFrame(buf) {
969
1005
  try {
970
1006
  const fields = decodeProto(buf);
971
1007
  const idField = fields.find((f) => f.fn === 2 && f.wt === 0);
972
1008
  const id = idField ? idField.value : 0n;
973
1009
  const type = getStr(fields, 7);
974
1010
  const binary = getBytes(fields, 8);
975
- if (id > 0n && this.ws && this.ws.readyState === 1) {
976
- const ack = buildAck(id);
977
- this.ws.send(ack.buffer.byteLength === ack.length ? ack.buffer : ack.buffer.slice(ack.byteOffset, ack.byteOffset + ack.byteLength));
1011
+ if (id > 0n && this.ws?.readyState === WebSocket.OPEN) {
1012
+ this.ws.send(buildAck(id));
978
1013
  }
979
1014
  if (type === "msg" && binary && binary.length > 0) {
980
1015
  let inner = binary;
981
1016
  if (inner.length > 2 && inner[0] === 31 && inner[1] === 139) {
982
- const syncResult = gunzipSync(inner);
983
- if (syncResult) {
984
- inner = syncResult;
985
- } else {
986
- inner = await gunzip(inner);
1017
+ try {
1018
+ inner = zlib.gunzipSync(inner);
1019
+ } catch {
987
1020
  }
988
1021
  }
989
1022
  const events = parseWebcastResponse(inner);
990
1023
  for (const evt of events) {
991
1024
  this._eventCount++;
992
- if (evt.type === "battle") {
993
- for (const team of evt.teams) {
994
- const host = team.users.find((u) => u.user.id === team.hostUserId);
995
- if (host) {
996
- this._battleHosts.set(team.hostUserId, host.user);
997
- team.hostUser = host.user;
998
- }
999
- }
1000
- }
1001
- if (evt.type === "battleArmies") {
1002
- for (const team of evt.teams) {
1003
- const host = team.users.find((u) => u.user.id === team.hostUserId);
1004
- if (host) {
1005
- team.hostUser = host.user;
1006
- this._battleHosts.set(team.hostUserId, host.user);
1007
- } else {
1008
- const cached = this._battleHosts.get(team.hostUserId);
1009
- if (cached) team.hostUser = cached;
1010
- }
1011
- }
1012
- }
1013
1025
  this.emit("event", evt);
1014
1026
  this.emit(evt.type, evt);
1015
1027
  }
@@ -1020,9 +1032,8 @@ var TikTokLive = class extends TypedEmitter {
1020
1032
  startHeartbeat(roomId) {
1021
1033
  this.stopHeartbeat();
1022
1034
  this.heartbeatTimer = setInterval(() => {
1023
- if (this.ws && this.ws.readyState === 1) {
1024
- const hb = buildHeartbeat(roomId);
1025
- this.ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
1035
+ if (this.ws?.readyState === WebSocket.OPEN) {
1036
+ this.ws.send(buildHeartbeat(roomId));
1026
1037
  }
1027
1038
  }, this.heartbeatInterval);
1028
1039
  }
@@ -1033,14 +1044,60 @@ var TikTokLive = class extends TypedEmitter {
1033
1044
  }
1034
1045
  }
1035
1046
  };
1047
+
1048
+ // src/api.ts
1049
+ var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
1050
+ var PAGE_CACHE_TTL = 5 * 60 * 1e3;
1051
+ async function getRanklist(opts) {
1052
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1053
+ const ak = encodeURIComponent(opts.apiKey);
1054
+ const body = {};
1055
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1056
+ if (opts.roomId) body.room_id = opts.roomId;
1057
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1058
+ if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
1059
+ if (opts.type) body.type = opts.type;
1060
+ if (opts.rankType) body.rank_type = opts.rankType;
1061
+ const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
1062
+ method: "POST",
1063
+ headers: { "Content-Type": "application/json" },
1064
+ body: JSON.stringify(body)
1065
+ });
1066
+ const data = await resp.json();
1067
+ if (data.status_code === 20003) {
1068
+ throw new Error(
1069
+ data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
1070
+ );
1071
+ }
1072
+ if (data.status_code !== 0) {
1073
+ throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
1074
+ }
1075
+ return data.data;
1076
+ }
1077
+ async function getRegionalRanklist(opts) {
1078
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1079
+ const ak = encodeURIComponent(opts.apiKey);
1080
+ const body = {};
1081
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1082
+ if (opts.roomId) body.room_id = opts.roomId;
1083
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1084
+ if (opts.rankType) body.rank_type = opts.rankType;
1085
+ if (opts.type) body.type = opts.type;
1086
+ if (opts.gapInterval) body.gap_interval = opts.gapInterval;
1087
+ const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
1088
+ method: "POST",
1089
+ headers: { "Content-Type": "application/json" },
1090
+ body: JSON.stringify(body)
1091
+ });
1092
+ const data = await resp.json();
1093
+ if (data.status_code !== 0) {
1094
+ throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
1095
+ }
1096
+ return data;
1097
+ }
1036
1098
  export {
1037
1099
  TikTokLive,
1038
- callApi,
1039
- fetchSignedUrl,
1040
- resolveLivePage,
1041
- resolveRoomId,
1042
- solvePuzzle,
1043
- solveRotate,
1044
- solveShapes
1100
+ getRanklist,
1101
+ getRegionalRanklist
1045
1102
  };
1046
1103
  //# sourceMappingURL=index.mjs.map