@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.js CHANGED
@@ -31,19 +31,84 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  TikTokLive: () => TikTokLive,
34
- callApi: () => callApi,
35
- fetchSignedUrl: () => fetchSignedUrl,
36
- resolveLivePage: () => resolveLivePage,
37
- resolveRoomId: () => resolveRoomId,
38
- solvePuzzle: () => solvePuzzle,
39
- solveRotate: () => solveRotate,
40
- solveShapes: () => solveShapes
34
+ getRanklist: () => getRanklist,
35
+ getRegionalRanklist: () => getRegionalRanklist
41
36
  });
42
37
  module.exports = __toCommonJS(index_exports);
43
38
 
39
+ // src/client.ts
40
+ var import_events = require("events");
41
+ var http = __toESM(require("http"));
42
+ var https = __toESM(require("https"));
43
+ var zlib = __toESM(require("zlib"));
44
+ var import_ws = __toESM(require("ws"));
45
+
44
46
  // src/proto.ts
45
47
  var encoder = new TextEncoder();
46
48
  var decoder = new TextDecoder();
49
+ var TIKTOK_EMOTE_MAP = {
50
+ // Standard emojis
51
+ "happy": "\u{1F60A}",
52
+ "angry": "\u{1F621}",
53
+ "cry": "\u{1F622}",
54
+ "embarrassed": "\u{1F633}",
55
+ "surprised": "\u{1F62E}",
56
+ "wronged": "\u{1F61E}",
57
+ "shout": "\u{1F624}",
58
+ "flushed": "\u{1F633}",
59
+ "yummy": "\u{1F60B}",
60
+ "complacent": "\u{1F60C}",
61
+ "drool": "\u{1F924}",
62
+ "scream": "\u{1F631}",
63
+ "weep": "\u{1F62D}",
64
+ "speechless": "\u{1F636}",
65
+ "funnyface": "\u{1F92A}",
66
+ "laughwithtears": "\u{1F602}",
67
+ "wicked": "\u{1F608}",
68
+ "facewithrollingeyes": "\u{1F644}",
69
+ "sulk": "\u{1F612}",
70
+ "thinking": "\u{1F914}",
71
+ "lovely": "\u{1F970}",
72
+ "greedy": "\u{1F911}",
73
+ "wow": "\u{1F62F}",
74
+ "joyful": "\u{1F603}",
75
+ "hehe": "\u{1F601}",
76
+ "slap": "\u{1F44B}",
77
+ "tears": "\u{1F63F}",
78
+ "stun": "\u{1F635}",
79
+ "cute": "\u{1F97A}",
80
+ "blink": "\u{1F609}",
81
+ "disdain": "\u{1F60F}",
82
+ "astonish": "\u{1F632}",
83
+ "cool": "\u{1F60E}",
84
+ "excited": "\u{1F929}",
85
+ "proud": "\u{1F624}",
86
+ "smileface": "\u{1F60A}",
87
+ "evil": "\u{1F47F}",
88
+ "angel": "\u{1F607}",
89
+ "laugh": "\u{1F606}",
90
+ "pride": "\u{1F981}",
91
+ "nap": "\u{1F634}",
92
+ "loveface": "\u{1F60D}",
93
+ "awkward": "\u{1F62C}",
94
+ "shock": "\u{1F628}",
95
+ "funny": "\u{1F604}",
96
+ "rage": "\u{1F92C}",
97
+ // Common aliases used in TikTok LIVE
98
+ "laughcry": "\u{1F602}",
99
+ "heart": "\u2764\uFE0F",
100
+ "like": "\u{1F44D}",
101
+ "love": "\u{1F495}",
102
+ "shy": "\u{1F648}",
103
+ "smile": "\u{1F60A}"
104
+ };
105
+ function replaceEmotes(text, imageMap) {
106
+ return text.replace(/\[([a-zA-Z_]+)\]/g, (match, name) => {
107
+ const lower = name.toLowerCase();
108
+ const emoji = TIKTOK_EMOTE_MAP[lower];
109
+ return emoji || match;
110
+ });
111
+ }
47
112
  function concatBytes(...arrays) {
48
113
  let totalLength = 0;
49
114
  for (const arr of arrays) totalLength += arr.length;
@@ -144,6 +209,10 @@ function getInt(fields, fn) {
144
209
  const f = fields.find((x) => x.fn === fn && x.wt === 0);
145
210
  return f ? Number(f.value) : 0;
146
211
  }
212
+ function getIntStr(fields, fn) {
213
+ const f = fields.find((x) => x.fn === fn && x.wt === 0);
214
+ return f ? String(f.value) : "";
215
+ }
147
216
  function getAllBytes(fields, fn) {
148
217
  return fields.filter((x) => x.fn === fn && x.wt === 2).map((x) => x.value);
149
218
  }
@@ -182,7 +251,7 @@ function looksLikeUsername(s) {
182
251
  }
183
252
  function parseUser(data) {
184
253
  const f = decodeProto(data);
185
- const id = String(getInt(f, 1) || getStr(f, 1));
254
+ const id = getIntStr(f, 1) || getStr(f, 1);
186
255
  const nickname = getStr(f, 3) || getStr(f, 2);
187
256
  let uniqueId = "";
188
257
  const uid4 = getStr(f, 4);
@@ -237,9 +306,10 @@ function parseUser(data) {
237
306
  }
238
307
  function parseBattleTeamFromArmies(itemBuf) {
239
308
  const f = decodeProto(itemBuf);
240
- const hostUserId = String(getInt(f, 1));
309
+ const hostUserId = getIntStr(f, 1) || "0";
241
310
  let teamScore = 0;
242
311
  const users = [];
312
+ let hostUser;
243
313
  const groups = getAllBytes(f, 2);
244
314
  for (const gb of groups) {
245
315
  try {
@@ -257,7 +327,19 @@ function parseBattleTeamFromArmies(itemBuf) {
257
327
  } catch {
258
328
  }
259
329
  }
260
- return { hostUserId, score: teamScore, users };
330
+ for (const fieldNum of [3, 4, 5, 6, 7, 8]) {
331
+ if (hostUser) break;
332
+ const buf = getBytes(f, fieldNum);
333
+ if (!buf) continue;
334
+ try {
335
+ const parsed = parseUser(buf);
336
+ if (parsed && (parsed.nickname || parsed.uniqueId) && parsed.uniqueId !== parsed.id && looksLikeUsername(parsed.uniqueId || "")) {
337
+ hostUser = parsed;
338
+ }
339
+ } catch {
340
+ }
341
+ }
342
+ return { hostUserId, score: teamScore, users, hostUser };
261
343
  }
262
344
  function parseWebcastMessage(method, payload) {
263
345
  const f = decodeProto(payload);
@@ -275,7 +357,25 @@ function parseWebcastMessage(method, payload) {
275
357
  case "WebcastChatMessage": {
276
358
  const userBuf = getBytes(f, 2);
277
359
  const user = userBuf ? parseUser(userBuf) : { id: "0", nickname: "", uniqueId: "" };
278
- return { ...base, type: "chat", user, comment: getStr(f, 3) };
360
+ const rawComment = getStr(f, 3);
361
+ const emoteImages = {};
362
+ for (const field of f) {
363
+ if (field.fn === 22 && field.wt === 2) {
364
+ try {
365
+ const ewi = decodeProto(field.value);
366
+ const emoteKey = getStr(ewi, 1);
367
+ const imgBuf = getBytes(ewi, 2);
368
+ if (imgBuf && emoteKey) {
369
+ const imgFields = decodeProto(imgBuf);
370
+ const url = getStr(imgFields, 1);
371
+ if (url) emoteImages[emoteKey] = url;
372
+ }
373
+ } catch {
374
+ }
375
+ }
376
+ }
377
+ const comment = replaceEmotes(rawComment, emoteImages);
378
+ return { ...base, type: "chat", user, comment };
279
379
  }
280
380
  case "WebcastMemberMessage": {
281
381
  const userBuf = getBytes(f, 2);
@@ -329,7 +429,7 @@ function parseWebcastMessage(method, payload) {
329
429
  const extraBuf = getBytes(f, 23);
330
430
  if (extraBuf) {
331
431
  const ef = decodeProto(extraBuf);
332
- toUserId = String(getInt(ef, 8));
432
+ toUserId = getIntStr(ef, 8) || "";
333
433
  }
334
434
  const groupId = toUserId || getStr(f, 11);
335
435
  return {
@@ -372,10 +472,36 @@ function parseWebcastMessage(method, payload) {
372
472
  return { ...base, type: "roomUserSeq", totalViewers, viewerCount };
373
473
  }
374
474
  case "WebcastLinkMicBattle": {
375
- const battleId = String(getInt(f, 1) || "");
376
- const status = getInt(f, 2) || 1;
377
- const battleDuration = getInt(f, 3);
475
+ const battleId = getIntStr(f, 2) || getIntStr(f, 1) || "";
476
+ const overallStatus = getInt(f, 4);
378
477
  const teams = [];
478
+ let battleDuration = 0;
479
+ let battleSettings;
480
+ const settingsBuf = getBytes(f, 3);
481
+ if (settingsBuf) {
482
+ try {
483
+ const sf = decodeProto(settingsBuf);
484
+ const startTimeMs = getInt(sf, 2);
485
+ const duration = getInt(sf, 3);
486
+ const phase = getInt(sf, 5);
487
+ const endTimeMs = getInt(sf, 10);
488
+ battleDuration = duration;
489
+ battleSettings = {
490
+ startTimeMs: startTimeMs || void 0,
491
+ duration: duration || void 0,
492
+ endTimeMs: endTimeMs || void 0
493
+ };
494
+ } catch {
495
+ }
496
+ }
497
+ const settingsPhase = settingsBuf ? (() => {
498
+ try {
499
+ return getInt(decodeProto(settingsBuf), 5);
500
+ } catch {
501
+ return 0;
502
+ }
503
+ })() : 0;
504
+ const status = settingsPhase || (overallStatus > 0 && overallStatus <= 10 ? overallStatus : 0) || 1;
379
505
  const battleUserBufs = getAllBytes(f, 10);
380
506
  for (const bub of battleUserBufs) {
381
507
  try {
@@ -396,6 +522,23 @@ function parseWebcastMessage(method, payload) {
396
522
  } catch {
397
523
  }
398
524
  }
525
+ if (teams.length === 0) {
526
+ const teamBufs5 = getAllBytes(f, 5);
527
+ for (const tb of teamBufs5) {
528
+ try {
529
+ const tf = decodeProto(tb);
530
+ const userId = getIntStr(tf, 1);
531
+ if (userId && userId !== "0") {
532
+ teams.push({
533
+ hostUserId: userId,
534
+ score: 0,
535
+ users: []
536
+ });
537
+ }
538
+ } catch {
539
+ }
540
+ }
541
+ }
399
542
  if (teams.length === 0) {
400
543
  const teamBufs7 = getAllBytes(f, 7);
401
544
  for (const tb of teamBufs7) {
@@ -405,10 +548,10 @@ function parseWebcastMessage(method, payload) {
405
548
  }
406
549
  }
407
550
  }
408
- return { ...base, type: "battle", battleId, status, battleDuration, teams };
551
+ return { ...base, type: "battle", battleId, status, battleDuration, teams, battleSettings };
409
552
  }
410
553
  case "WebcastLinkMicArmies": {
411
- const battleId = String(getInt(f, 1) || "");
554
+ const battleId = getIntStr(f, 1) || "";
412
555
  const battleStatus = getInt(f, 7);
413
556
  const teams = [];
414
557
  const itemBufs = getAllBytes(f, 3);
@@ -490,8 +633,7 @@ function parseWebcastMessage(method, payload) {
490
633
  const title = getStr(f, 4) || getStr(f, 2);
491
634
  return { ...base, type: "liveIntro", roomId, title };
492
635
  }
493
- case "WebcastLinkMicMethod":
494
- case "WebcastLinkmicBattleTaskMessage": {
636
+ case "WebcastLinkMicMethod": {
495
637
  const action = getStr(f, 1) || `action_${getInt(f, 1)}`;
496
638
  const users = [];
497
639
  const userBufs = getAllBytes(f, 2);
@@ -503,6 +645,155 @@ function parseWebcastMessage(method, payload) {
503
645
  }
504
646
  return { ...base, type: "linkMic", action, users };
505
647
  }
648
+ case "WebcastLinkmicBattleTaskMessage": {
649
+ const taskAction = getInt(f, 2);
650
+ const battleRefId = getIntStr(f, 20) || "";
651
+ let timerType = 0;
652
+ let remainingSeconds = 0;
653
+ let endTimestampS = 0;
654
+ const timerBuf = getBytes(f, 5);
655
+ if (timerBuf) {
656
+ try {
657
+ const tf = decodeProto(timerBuf);
658
+ timerType = getInt(tf, 1);
659
+ remainingSeconds = getInt(tf, 2);
660
+ endTimestampS = getInt(tf, 3);
661
+ } catch {
662
+ }
663
+ }
664
+ let multiplier = 0;
665
+ let missionDuration = 0;
666
+ let missionTarget = 0;
667
+ let missionType = "";
668
+ const bonusBuf = getBytes(f, 3);
669
+ if (bonusBuf) {
670
+ try {
671
+ const bf = decodeProto(bonusBuf);
672
+ const missionBuf = getBytes(bf, 1);
673
+ if (missionBuf) {
674
+ const mf = decodeProto(missionBuf);
675
+ missionTarget = getInt(mf, 1);
676
+ const items = getAllBytes(mf, 2);
677
+ for (const item of items) {
678
+ try {
679
+ const itemFields = decodeProto(item);
680
+ const descBuf = getBytes(itemFields, 2);
681
+ if (descBuf) {
682
+ const descFields = decodeProto(descBuf);
683
+ const paramBufs = getAllBytes(descFields, 2);
684
+ for (const pb of paramBufs) {
685
+ try {
686
+ const pf = decodeProto(pb);
687
+ const key = getStr(pf, 1);
688
+ const val = getStr(pf, 2);
689
+ if (key === "multi" && val) multiplier = parseInt(val) || 0;
690
+ if (key === "dur" && val) missionDuration = parseInt(val) || 0;
691
+ if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
692
+ } catch {
693
+ }
694
+ }
695
+ const typeStr = getStr(descFields, 1);
696
+ if (typeStr) missionType = typeStr;
697
+ }
698
+ } catch {
699
+ }
700
+ }
701
+ const countdownBuf = getBytes(mf, 3);
702
+ if (countdownBuf) {
703
+ try {
704
+ const cf = decodeProto(countdownBuf);
705
+ if (!missionDuration) missionDuration = getInt(cf, 2);
706
+ } catch {
707
+ }
708
+ }
709
+ }
710
+ } catch {
711
+ }
712
+ }
713
+ if (!missionType) {
714
+ const descBuf6 = getBytes(f, 6);
715
+ if (descBuf6) {
716
+ try {
717
+ const d6 = decodeProto(descBuf6);
718
+ const innerBuf = getBytes(d6, 1);
719
+ if (innerBuf) {
720
+ const innerF = decodeProto(innerBuf);
721
+ const typeStr = getStr(innerF, 1);
722
+ if (typeStr) missionType = typeStr;
723
+ const paramBufs = getAllBytes(innerF, 2);
724
+ for (const pb of paramBufs) {
725
+ try {
726
+ const pf = decodeProto(pb);
727
+ const key = getStr(pf, 1);
728
+ const val = getStr(pf, 2);
729
+ if (key === "multi" && val) multiplier = parseInt(val) || 0;
730
+ if (key === "sum" && val) missionTarget = parseInt(val) || missionTarget;
731
+ } catch {
732
+ }
733
+ }
734
+ }
735
+ } catch {
736
+ }
737
+ }
738
+ }
739
+ return {
740
+ ...base,
741
+ type: "battleTask",
742
+ taskAction,
743
+ battleRefId,
744
+ missionType,
745
+ multiplier,
746
+ missionDuration,
747
+ missionTarget,
748
+ remainingSeconds,
749
+ endTimestampS,
750
+ timerType
751
+ };
752
+ }
753
+ case "WebcastBarrageMessage": {
754
+ 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(" | ");
755
+ console.log(`[proto] WebcastBarrageMessage fields: ${fieldSummary}`);
756
+ const msgType = getInt(f, 3);
757
+ const duration = getInt(f, 4);
758
+ const displayType = getInt(f, 5);
759
+ const subType = getInt(f, 6);
760
+ let defaultPattern = "";
761
+ let content = "";
762
+ const contentBuf = getBytes(f, 2);
763
+ if (contentBuf) {
764
+ try {
765
+ const cf = decodeProto(contentBuf);
766
+ defaultPattern = getStr(cf, 1) || "";
767
+ const paramBufs = getAllBytes(cf, 2);
768
+ const params = [];
769
+ for (const pb of paramBufs) {
770
+ try {
771
+ const pf = decodeProto(pb);
772
+ const key = getStr(pf, 1);
773
+ const val = getStr(pf, 2);
774
+ if (key && val) params.push(`${key}=${val}`);
775
+ } catch {
776
+ }
777
+ }
778
+ if (params.length > 0) content = params.join(", ");
779
+ } catch {
780
+ }
781
+ }
782
+ if (!defaultPattern) {
783
+ defaultPattern = getStr(f, 1) || "";
784
+ }
785
+ console.log(`[proto] Barrage parsed: msgType=${msgType} subType=${subType} displayType=${displayType} duration=${duration} pattern="${defaultPattern}" content="${content}"`);
786
+ return {
787
+ ...base,
788
+ type: "barrage",
789
+ msgType,
790
+ subType,
791
+ displayType,
792
+ duration,
793
+ defaultPattern,
794
+ content
795
+ };
796
+ }
506
797
  default:
507
798
  return { ...base, type: "unknown", method };
508
799
  }
@@ -524,260 +815,30 @@ function parseWebcastResponse(payload) {
524
815
  return events;
525
816
  }
526
817
 
527
- // src/api.ts
818
+ // src/client.ts
528
819
  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";
529
820
  var DEFAULT_SIGN_SERVER = "https://api.tik.tools";
530
- var pageCache = /* @__PURE__ */ new Map();
531
- var PAGE_CACHE_TTL = 5 * 60 * 1e3;
532
- async function resolveLivePage(uniqueId) {
533
- const clean = uniqueId.replace(/^@/, "");
534
- const cached = pageCache.get(clean);
535
- if (cached && Date.now() - cached.ts < PAGE_CACHE_TTL) {
536
- return cached.info;
537
- }
538
- try {
539
- const resp = await fetch(`https://www.tiktok.com/@${clean}/live`, {
540
- headers: {
541
- "User-Agent": DEFAULT_UA,
542
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
543
- "Accept-Language": "en-US,en;q=0.9"
544
- },
545
- redirect: "follow"
821
+ function httpGet(url, headers) {
822
+ return new Promise((resolve, reject) => {
823
+ const mod = url.startsWith("https") ? https : http;
824
+ const req = mod.get(url, { headers }, (res) => {
825
+ const chunks = [];
826
+ const enc = res.headers["content-encoding"];
827
+ const stream = enc === "gzip" || enc === "br" ? res.pipe(enc === "br" ? zlib.createBrotliDecompress() : zlib.createGunzip()) : res;
828
+ stream.on("data", (c) => chunks.push(c));
829
+ stream.on("end", () => resolve({
830
+ status: res.statusCode || 0,
831
+ headers: res.headers,
832
+ body: Buffer.concat(chunks)
833
+ }));
834
+ stream.on("error", reject);
546
835
  });
547
- if (!resp.ok) return null;
548
- let ttwid = "";
549
- const setCookies = resp.headers.get("set-cookie") || "";
550
- for (const part of setCookies.split(",")) {
551
- const trimmed = part.trim();
552
- if (trimmed.startsWith("ttwid=")) {
553
- ttwid = trimmed.split(";")[0].split("=").slice(1).join("=");
554
- break;
555
- }
556
- }
557
- if (!ttwid && typeof resp.headers.getSetCookie === "function") {
558
- for (const sc of resp.headers.getSetCookie()) {
559
- if (typeof sc === "string" && sc.startsWith("ttwid=")) {
560
- ttwid = sc.split(";")[0].split("=").slice(1).join("=");
561
- break;
562
- }
563
- }
564
- }
565
- const html = await resp.text();
566
- let roomId = "";
567
- const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
568
- if (sigiMatch) {
569
- try {
570
- const json = JSON.parse(sigiMatch[1]);
571
- const jsonStr = JSON.stringify(json);
572
- const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
573
- if (m) roomId = m[1];
574
- } catch {
575
- }
576
- }
577
- if (!roomId) {
578
- const patterns = [
579
- /"roomId"\s*:\s*"(\d+)"/,
580
- /room_id[=/](\d{10,})/,
581
- /"idStr"\s*:\s*"(\d{10,})"/
582
- ];
583
- for (const p of patterns) {
584
- const m = html.match(p);
585
- if (m) {
586
- roomId = m[1];
587
- break;
588
- }
589
- }
590
- }
591
- if (!roomId) return null;
592
- const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
593
- const clusterRegion = crMatch ? crMatch[1] : "";
594
- const info = { roomId, ttwid, clusterRegion };
595
- pageCache.set(clean, { info, ts: Date.now() });
596
- return info;
597
- } catch {
598
- }
599
- return null;
600
- }
601
- async function resolveRoomId(uniqueId) {
602
- const info = await resolveLivePage(uniqueId);
603
- return info?.roomId ?? null;
604
- }
605
- async function fetchSignedUrl(response) {
606
- if (!response.signed_url) {
607
- return null;
608
- }
609
- const headers = { ...response.headers || {} };
610
- if (response.cookies) {
611
- headers["Cookie"] = response.cookies;
612
- }
613
- const resp = await fetch(response.signed_url, { headers, redirect: "follow" });
614
- const text = await resp.text();
615
- try {
616
- return JSON.parse(text);
617
- } catch {
618
- return null;
619
- }
620
- }
621
- async function callApi(opts) {
622
- const serverUrl = (opts.serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
623
- const isGet = opts.method === "GET";
624
- const ak = encodeURIComponent(opts.apiKey);
625
- const url1 = isGet ? `${serverUrl}${opts.endpoint}?apiKey=${ak}&unique_id=${encodeURIComponent(opts.uniqueId)}` : `${serverUrl}${opts.endpoint}?apiKey=${ak}`;
626
- const fetchOpts1 = isGet ? {} : {
627
- method: "POST",
628
- headers: { "Content-Type": "application/json" },
629
- body: JSON.stringify({ unique_id: opts.uniqueId, ...opts.extraBody })
630
- };
631
- const resp1 = await fetch(url1, fetchOpts1);
632
- const data1 = await resp1.json();
633
- if (data1.signed_url || data1.action === "fetch_signed_url") {
634
- return fetchSignedUrl(data1);
635
- }
636
- if (data1.status_code === 0 && data1.action !== "resolve_required") {
637
- return data1;
638
- }
639
- if (data1.action === "resolve_required") {
640
- const roomId = await resolveRoomId(opts.uniqueId);
641
- if (!roomId) return null;
642
- const url2 = isGet ? `${serverUrl}${opts.endpoint}?apiKey=${ak}&room_id=${encodeURIComponent(roomId)}` : `${serverUrl}${opts.endpoint}?apiKey=${ak}`;
643
- const fetchOpts2 = isGet ? {} : {
644
- method: "POST",
645
- headers: { "Content-Type": "application/json" },
646
- body: JSON.stringify({ room_id: roomId, ...opts.extraBody })
647
- };
648
- const resp2 = await fetch(url2, fetchOpts2);
649
- const data2 = await resp2.json();
650
- if (data2.signed_url || data2.action === "fetch_signed_url") {
651
- return fetchSignedUrl(data2);
652
- }
653
- return data2;
654
- }
655
- return data1;
656
- }
657
- async function solvePuzzle(apiKey, puzzleB64, pieceB64, serverUrl) {
658
- const base = (serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
659
- const resp = await fetch(`${base}/captcha/solve/puzzle?apiKey=${encodeURIComponent(apiKey)}`, {
660
- method: "POST",
661
- headers: { "Content-Type": "application/json" },
662
- body: JSON.stringify({ puzzle: puzzleB64, piece: pieceB64 })
663
- });
664
- const data = await resp.json();
665
- if (data.status_code !== 0) {
666
- throw new Error(data.error || `Solver failed (status ${data.status_code})`);
667
- }
668
- return data.data;
669
- }
670
- async function solveRotate(apiKey, outerB64, innerB64, serverUrl) {
671
- const base = (serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
672
- const resp = await fetch(`${base}/captcha/solve/rotate?apiKey=${encodeURIComponent(apiKey)}`, {
673
- method: "POST",
674
- headers: { "Content-Type": "application/json" },
675
- body: JSON.stringify({ outer: outerB64, inner: innerB64 })
676
- });
677
- const data = await resp.json();
678
- if (data.status_code !== 0) {
679
- throw new Error(data.error || `Solver failed (status ${data.status_code})`);
680
- }
681
- return data.data;
682
- }
683
- async function solveShapes(apiKey, imageB64, serverUrl) {
684
- const base = (serverUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
685
- const resp = await fetch(`${base}/captcha/solve/shapes?apiKey=${encodeURIComponent(apiKey)}`, {
686
- method: "POST",
687
- headers: { "Content-Type": "application/json" },
688
- body: JSON.stringify({ image: imageB64 })
689
- });
690
- const data = await resp.json();
691
- if (data.status_code !== 0) {
692
- throw new Error(data.error || `Solver failed (status ${data.status_code})`);
693
- }
694
- return data.data;
695
- }
696
-
697
- // src/client.ts
698
- 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";
699
- var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
700
- var TypedEmitter = class {
701
- _listeners = /* @__PURE__ */ new Map();
702
- on(event, fn) {
703
- const arr = this._listeners.get(event) || [];
704
- arr.push(fn);
705
- this._listeners.set(event, arr);
706
- return this;
707
- }
708
- once(event, fn) {
709
- const wrapper = (...args) => {
710
- this.off(event, wrapper);
711
- fn(...args);
712
- };
713
- return this.on(event, wrapper);
714
- }
715
- off(event, fn) {
716
- const arr = this._listeners.get(event);
717
- if (arr) {
718
- this._listeners.set(event, arr.filter((l) => l !== fn));
719
- }
720
- return this;
721
- }
722
- emit(event, ...args) {
723
- const arr = this._listeners.get(event);
724
- if (!arr || arr.length === 0) return false;
725
- for (const fn of [...arr]) fn(...args);
726
- return true;
727
- }
728
- removeAllListeners(event) {
729
- if (event) this._listeners.delete(event);
730
- else this._listeners.clear();
731
- return this;
732
- }
733
- };
734
- async function gunzip(data) {
735
- if (typeof DecompressionStream !== "undefined") {
736
- const ds = new DecompressionStream("gzip");
737
- const writer = ds.writable.getWriter();
738
- const reader = ds.readable.getReader();
739
- writer.write(data);
740
- writer.close();
741
- const chunks = [];
742
- while (true) {
743
- const { done, value } = await reader.read();
744
- if (done) break;
745
- chunks.push(value);
746
- }
747
- return concatBytes(...chunks);
748
- }
749
- try {
750
- const zlib = await import("zlib");
751
- return new Promise((resolve, reject) => {
752
- zlib.gunzip(data, (err, result) => {
753
- if (err) reject(err);
754
- else resolve(new Uint8Array(result));
755
- });
836
+ req.on("error", reject);
837
+ req.setTimeout(15e3, () => {
838
+ req.destroy();
839
+ reject(new Error("Request timeout"));
756
840
  });
757
- } catch {
758
- return data;
759
- }
760
- }
761
- var _zlib = null;
762
- var _zlibLoadAttempted = false;
763
- async function ensureZlib() {
764
- if (_zlib) return _zlib;
765
- if (_zlibLoadAttempted) return null;
766
- _zlibLoadAttempted = true;
767
- try {
768
- _zlib = await import("zlib");
769
- } catch {
770
- }
771
- return _zlib;
772
- }
773
- ensureZlib();
774
- function gunzipSync(data) {
775
- if (!_zlib) return null;
776
- try {
777
- return new Uint8Array(_zlib.gunzipSync(data));
778
- } catch {
779
- return null;
780
- }
841
+ });
781
842
  }
782
843
  function getWsHost(clusterRegion) {
783
844
  if (!clusterRegion) return "webcast-ws.tiktok.com";
@@ -786,21 +847,7 @@ function getWsHost(clusterRegion) {
786
847
  if (r.startsWith("us") || r.includes("us")) return "webcast-ws.us.tiktok.com";
787
848
  return "webcast-ws.tiktok.com";
788
849
  }
789
- async function resolveWebSocket(userImpl) {
790
- if (userImpl) return userImpl;
791
- if (typeof globalThis.WebSocket !== "undefined") {
792
- return globalThis.WebSocket;
793
- }
794
- try {
795
- const ws = await import("ws");
796
- return ws.default || ws;
797
- } catch {
798
- throw new Error(
799
- 'No WebSocket implementation found. Either use Node.js 22+ (native WebSocket), Cloudflare Workers, or install the "ws" package: npm i ws'
800
- );
801
- }
802
- }
803
- var TikTokLive = class extends TypedEmitter {
850
+ var TikTokLive = class extends import_events.EventEmitter {
804
851
  ws = null;
805
852
  heartbeatTimer = null;
806
853
  reconnectAttempts = 0;
@@ -808,7 +855,7 @@ var TikTokLive = class extends TypedEmitter {
808
855
  _connected = false;
809
856
  _eventCount = 0;
810
857
  _roomId = "";
811
- _battleHosts = /* @__PURE__ */ new Map();
858
+ _ownerUserId = "";
812
859
  uniqueId;
813
860
  signServerUrl;
814
861
  apiKey;
@@ -816,30 +863,55 @@ var TikTokLive = class extends TypedEmitter {
816
863
  maxReconnectAttempts;
817
864
  heartbeatInterval;
818
865
  debug;
819
- webSocketImpl;
820
- WS;
821
866
  constructor(options) {
822
867
  super();
823
868
  this.uniqueId = options.uniqueId.replace(/^@/, "");
824
- this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
869
+ this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
825
870
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
826
871
  this.apiKey = options.apiKey;
827
872
  this.autoReconnect = options.autoReconnect ?? true;
828
873
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
829
874
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
830
875
  this.debug = options.debug ?? false;
831
- this.webSocketImpl = options.webSocketImpl;
832
876
  }
833
877
  async connect() {
834
878
  this.intentionalClose = false;
835
- if (!this.WS) {
836
- this.WS = await resolveWebSocket(this.webSocketImpl);
879
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
880
+ "User-Agent": DEFAULT_UA,
881
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
882
+ "Accept-Encoding": "gzip, deflate, br",
883
+ "Accept-Language": "en-US,en;q=0.9"
884
+ });
885
+ let ttwid = "";
886
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
887
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
888
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
889
+ break;
890
+ }
891
+ }
892
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
893
+ const html = resp.body.toString();
894
+ let roomId = "";
895
+ let ownerUserId = "";
896
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
897
+ if (sigiMatch) {
898
+ try {
899
+ const json = JSON.parse(sigiMatch[1]);
900
+ const jsonStr = JSON.stringify(json);
901
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
902
+ if (m) roomId = m[1];
903
+ const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
904
+ if (ownerUser?.id) {
905
+ ownerUserId = String(ownerUser.id);
906
+ }
907
+ } catch {
908
+ }
837
909
  }
838
- const pageInfo = await resolveLivePage(this.uniqueId);
839
- if (!pageInfo) throw new Error(`User @${this.uniqueId} is not currently live`);
840
- if (!pageInfo.ttwid) throw new Error("Failed to obtain session cookie");
841
- const { roomId, ttwid, clusterRegion } = pageInfo;
910
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
842
911
  this._roomId = roomId;
912
+ this._ownerUserId = ownerUserId;
913
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
914
+ const clusterRegion = crMatch ? crMatch[1] : "";
843
915
  const wsHost = getWsHost(clusterRegion);
844
916
  const wsParams = new URLSearchParams({
845
917
  version_code: "270000",
@@ -850,9 +922,9 @@ var TikTokLive = class extends TypedEmitter {
850
922
  browser_language: "en-US",
851
923
  browser_platform: "Win32",
852
924
  browser_name: "Mozilla",
853
- browser_version: DEFAULT_UA2.split("Mozilla/")[1] || "5.0",
925
+ browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
854
926
  browser_online: "true",
855
- tz_name: "Etc/UTC",
927
+ tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
856
928
  app_name: "tiktok_web",
857
929
  sup_ws_ds_opt: "1",
858
930
  update_version_code: "2.0.0",
@@ -893,71 +965,48 @@ var TikTokLive = class extends TypedEmitter {
893
965
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
894
966
  }
895
967
  return new Promise((resolve, reject) => {
896
- const connUrl = wsUrl + (wsUrl.includes("?") ? "&" : "?") + `ttwid=${ttwid}`;
897
- try {
898
- this.ws = new this.WS(connUrl, {
899
- headers: {
900
- "User-Agent": DEFAULT_UA2,
901
- "Cookie": `ttwid=${ttwid}`,
902
- "Origin": "https://www.tiktok.com"
903
- }
904
- });
905
- } catch {
906
- this.ws = new this.WS(connUrl);
907
- }
908
- const ws = this.ws;
909
- let settled = false;
910
- ws.onopen = () => {
968
+ this.ws = new import_ws.default(wsUrl, {
969
+ headers: {
970
+ "User-Agent": DEFAULT_UA,
971
+ "Cookie": `ttwid=${ttwid}`,
972
+ "Origin": "https://www.tiktok.com"
973
+ }
974
+ });
975
+ this.ws.on("open", () => {
911
976
  this._connected = true;
912
977
  this.reconnectAttempts = 0;
913
- const hb = buildHeartbeat(roomId);
914
- const enter = buildImEnterRoom(roomId);
915
- ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
916
- ws.send(enter.buffer.byteLength === enter.length ? enter.buffer : enter.buffer.slice(enter.byteOffset, enter.byteOffset + enter.byteLength));
978
+ this.ws.send(buildHeartbeat(roomId));
979
+ this.ws.send(buildImEnterRoom(roomId));
917
980
  this.startHeartbeat(roomId);
918
981
  const roomInfo = {
919
982
  roomId,
920
983
  wsHost,
921
984
  clusterRegion,
922
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
985
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
986
+ ownerUserId: ownerUserId || void 0
923
987
  };
924
988
  this.emit("connected");
925
989
  this.emit("roomInfo", roomInfo);
926
- if (!settled) {
927
- settled = true;
928
- resolve();
929
- }
930
- };
931
- ws.onmessage = (event) => {
932
- const raw = event.data !== void 0 ? event.data : event;
933
- this.handleMessage(raw);
934
- };
935
- ws.onclose = (event) => {
990
+ resolve();
991
+ });
992
+ this.ws.on("message", (rawData) => {
993
+ this.handleFrame(Buffer.from(rawData));
994
+ });
995
+ this.ws.on("close", (code, reason) => {
936
996
  this._connected = false;
937
997
  this.stopHeartbeat();
938
- const code = event?.code ?? 1006;
939
- const reason = event?.reason ?? "";
940
- this.emit("disconnected", code, reason?.toString?.() || "");
998
+ const reasonStr = reason?.toString() || "";
999
+ this.emit("disconnected", code, reasonStr);
941
1000
  if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
942
1001
  this.reconnectAttempts++;
943
1002
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
944
1003
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
945
1004
  }
946
- };
947
- ws.onerror = (err) => {
948
- this.emit("error", err instanceof Error ? err : new Error(String(err?.message || err)));
949
- if (!settled) {
950
- settled = true;
951
- reject(err);
952
- }
953
- };
954
- setTimeout(() => {
955
- if (!settled) {
956
- settled = true;
957
- ws.close();
958
- reject(new Error("Connection timeout"));
959
- }
960
- }, 15e3);
1005
+ });
1006
+ this.ws.on("error", (err) => {
1007
+ this.emit("error", err);
1008
+ if (!this._connected) reject(err);
1009
+ });
961
1010
  });
962
1011
  }
963
1012
  disconnect() {
@@ -990,69 +1039,27 @@ var TikTokLive = class extends TypedEmitter {
990
1039
  emit(event, ...args) {
991
1040
  return super.emit(event, ...args);
992
1041
  }
993
- async handleMessage(raw) {
994
- try {
995
- let bytes;
996
- if (raw instanceof ArrayBuffer) {
997
- bytes = new Uint8Array(raw);
998
- } else if (raw instanceof Uint8Array) {
999
- bytes = raw;
1000
- } else if (typeof Blob !== "undefined" && raw instanceof Blob) {
1001
- bytes = new Uint8Array(await raw.arrayBuffer());
1002
- } else if (raw?.buffer instanceof ArrayBuffer) {
1003
- bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
1004
- } else {
1005
- return;
1006
- }
1007
- this.handleFrame(bytes);
1008
- } catch {
1009
- }
1010
- }
1011
- async handleFrame(buf) {
1042
+ handleFrame(buf) {
1012
1043
  try {
1013
1044
  const fields = decodeProto(buf);
1014
1045
  const idField = fields.find((f) => f.fn === 2 && f.wt === 0);
1015
1046
  const id = idField ? idField.value : 0n;
1016
1047
  const type = getStr(fields, 7);
1017
1048
  const binary = getBytes(fields, 8);
1018
- if (id > 0n && this.ws && this.ws.readyState === 1) {
1019
- const ack = buildAck(id);
1020
- this.ws.send(ack.buffer.byteLength === ack.length ? ack.buffer : ack.buffer.slice(ack.byteOffset, ack.byteOffset + ack.byteLength));
1049
+ if (id > 0n && this.ws?.readyState === import_ws.default.OPEN) {
1050
+ this.ws.send(buildAck(id));
1021
1051
  }
1022
1052
  if (type === "msg" && binary && binary.length > 0) {
1023
1053
  let inner = binary;
1024
1054
  if (inner.length > 2 && inner[0] === 31 && inner[1] === 139) {
1025
- const syncResult = gunzipSync(inner);
1026
- if (syncResult) {
1027
- inner = syncResult;
1028
- } else {
1029
- inner = await gunzip(inner);
1055
+ try {
1056
+ inner = zlib.gunzipSync(inner);
1057
+ } catch {
1030
1058
  }
1031
1059
  }
1032
1060
  const events = parseWebcastResponse(inner);
1033
1061
  for (const evt of events) {
1034
1062
  this._eventCount++;
1035
- if (evt.type === "battle") {
1036
- for (const team of evt.teams) {
1037
- const host = team.users.find((u) => u.user.id === team.hostUserId);
1038
- if (host) {
1039
- this._battleHosts.set(team.hostUserId, host.user);
1040
- team.hostUser = host.user;
1041
- }
1042
- }
1043
- }
1044
- if (evt.type === "battleArmies") {
1045
- for (const team of evt.teams) {
1046
- const host = team.users.find((u) => u.user.id === team.hostUserId);
1047
- if (host) {
1048
- team.hostUser = host.user;
1049
- this._battleHosts.set(team.hostUserId, host.user);
1050
- } else {
1051
- const cached = this._battleHosts.get(team.hostUserId);
1052
- if (cached) team.hostUser = cached;
1053
- }
1054
- }
1055
- }
1056
1063
  this.emit("event", evt);
1057
1064
  this.emit(evt.type, evt);
1058
1065
  }
@@ -1063,9 +1070,8 @@ var TikTokLive = class extends TypedEmitter {
1063
1070
  startHeartbeat(roomId) {
1064
1071
  this.stopHeartbeat();
1065
1072
  this.heartbeatTimer = setInterval(() => {
1066
- if (this.ws && this.ws.readyState === 1) {
1067
- const hb = buildHeartbeat(roomId);
1068
- this.ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
1073
+ if (this.ws?.readyState === import_ws.default.OPEN) {
1074
+ this.ws.send(buildHeartbeat(roomId));
1069
1075
  }
1070
1076
  }, this.heartbeatInterval);
1071
1077
  }
@@ -1076,15 +1082,61 @@ var TikTokLive = class extends TypedEmitter {
1076
1082
  }
1077
1083
  }
1078
1084
  };
1085
+
1086
+ // src/api.ts
1087
+ var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
1088
+ var PAGE_CACHE_TTL = 5 * 60 * 1e3;
1089
+ async function getRanklist(opts) {
1090
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1091
+ const ak = encodeURIComponent(opts.apiKey);
1092
+ const body = {};
1093
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1094
+ if (opts.roomId) body.room_id = opts.roomId;
1095
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1096
+ if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
1097
+ if (opts.type) body.type = opts.type;
1098
+ if (opts.rankType) body.rank_type = opts.rankType;
1099
+ const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
1100
+ method: "POST",
1101
+ headers: { "Content-Type": "application/json" },
1102
+ body: JSON.stringify(body)
1103
+ });
1104
+ const data = await resp.json();
1105
+ if (data.status_code === 20003) {
1106
+ throw new Error(
1107
+ data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
1108
+ );
1109
+ }
1110
+ if (data.status_code !== 0) {
1111
+ throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
1112
+ }
1113
+ return data.data;
1114
+ }
1115
+ async function getRegionalRanklist(opts) {
1116
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1117
+ const ak = encodeURIComponent(opts.apiKey);
1118
+ const body = {};
1119
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1120
+ if (opts.roomId) body.room_id = opts.roomId;
1121
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1122
+ if (opts.rankType) body.rank_type = opts.rankType;
1123
+ if (opts.type) body.type = opts.type;
1124
+ if (opts.gapInterval) body.gap_interval = opts.gapInterval;
1125
+ const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
1126
+ method: "POST",
1127
+ headers: { "Content-Type": "application/json" },
1128
+ body: JSON.stringify(body)
1129
+ });
1130
+ const data = await resp.json();
1131
+ if (data.status_code !== 0) {
1132
+ throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
1133
+ }
1134
+ return data;
1135
+ }
1079
1136
  // Annotate the CommonJS export names for ESM import in node:
1080
1137
  0 && (module.exports = {
1081
1138
  TikTokLive,
1082
- callApi,
1083
- fetchSignedUrl,
1084
- resolveLivePage,
1085
- resolveRoomId,
1086
- solvePuzzle,
1087
- solveRotate,
1088
- solveShapes
1139
+ getRanklist,
1140
+ getRegionalRanklist
1089
1141
  });
1090
1142
  //# sourceMappingURL=index.js.map