@tiktool/live 2.4.4 → 2.4.6

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,59 @@ var TikTokLive = class extends TypedEmitter {
816
863
  maxReconnectAttempts;
817
864
  heartbeatInterval;
818
865
  debug;
819
- webSocketImpl;
820
- WS;
866
+ _sessionId;
867
+ _ttTargetIdc;
821
868
  constructor(options) {
822
869
  super();
823
870
  this.uniqueId = options.uniqueId.replace(/^@/, "");
824
- this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
871
+ this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
825
872
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
826
873
  this.apiKey = options.apiKey;
827
874
  this.autoReconnect = options.autoReconnect ?? true;
828
875
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
829
876
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
830
877
  this.debug = options.debug ?? false;
831
- this.webSocketImpl = options.webSocketImpl;
878
+ this._sessionId = options.sessionId;
879
+ this._ttTargetIdc = options.ttTargetIdc;
832
880
  }
833
881
  async connect() {
834
882
  this.intentionalClose = false;
835
- if (!this.WS) {
836
- this.WS = await resolveWebSocket(this.webSocketImpl);
883
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
884
+ "User-Agent": DEFAULT_UA,
885
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
886
+ "Accept-Encoding": "gzip, deflate, br",
887
+ "Accept-Language": "en-US,en;q=0.9"
888
+ });
889
+ let ttwid = "";
890
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
891
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
892
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
893
+ break;
894
+ }
837
895
  }
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;
896
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
897
+ const html = resp.body.toString();
898
+ let roomId = "";
899
+ let ownerUserId = "";
900
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
901
+ if (sigiMatch) {
902
+ try {
903
+ const json = JSON.parse(sigiMatch[1]);
904
+ const jsonStr = JSON.stringify(json);
905
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
906
+ if (m) roomId = m[1];
907
+ const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
908
+ if (ownerUser?.id) {
909
+ ownerUserId = String(ownerUser.id);
910
+ }
911
+ } catch {
912
+ }
913
+ }
914
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
842
915
  this._roomId = roomId;
916
+ this._ownerUserId = ownerUserId;
917
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
918
+ const clusterRegion = crMatch ? crMatch[1] : "";
843
919
  const wsHost = getWsHost(clusterRegion);
844
920
  const wsParams = new URLSearchParams({
845
921
  version_code: "270000",
@@ -850,9 +926,9 @@ var TikTokLive = class extends TypedEmitter {
850
926
  browser_language: "en-US",
851
927
  browser_platform: "Win32",
852
928
  browser_name: "Mozilla",
853
- browser_version: DEFAULT_UA2.split("Mozilla/")[1] || "5.0",
929
+ browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
854
930
  browser_online: "true",
855
- tz_name: "Etc/UTC",
931
+ tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
856
932
  app_name: "tiktok_web",
857
933
  sup_ws_ds_opt: "1",
858
934
  update_version_code: "2.0.0",
@@ -893,71 +969,55 @@ var TikTokLive = class extends TypedEmitter {
893
969
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
894
970
  }
895
971
  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);
972
+ let cookieHeader = `ttwid=${ttwid}`;
973
+ if (this._sessionId) {
974
+ cookieHeader += `; sessionid=${this._sessionId}; sessionid_ss=${this._sessionId}; sid_tt=${this._sessionId}`;
975
+ if (this._ttTargetIdc) {
976
+ cookieHeader += `; tt-target-idc=${this._ttTargetIdc}`;
977
+ }
907
978
  }
908
- const ws = this.ws;
909
- let settled = false;
910
- ws.onopen = () => {
979
+ this.ws = new import_ws.default(wsUrl, {
980
+ headers: {
981
+ "User-Agent": DEFAULT_UA,
982
+ "Cookie": cookieHeader,
983
+ "Origin": "https://www.tiktok.com"
984
+ }
985
+ });
986
+ this.ws.on("open", () => {
911
987
  this._connected = true;
912
988
  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));
989
+ this.ws.send(buildHeartbeat(roomId));
990
+ this.ws.send(buildImEnterRoom(roomId));
917
991
  this.startHeartbeat(roomId);
918
992
  const roomInfo = {
919
993
  roomId,
920
994
  wsHost,
921
995
  clusterRegion,
922
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
996
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
997
+ ownerUserId: ownerUserId || void 0
923
998
  };
924
999
  this.emit("connected");
925
1000
  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) => {
1001
+ resolve();
1002
+ });
1003
+ this.ws.on("message", (rawData) => {
1004
+ this.handleFrame(Buffer.from(rawData));
1005
+ });
1006
+ this.ws.on("close", (code, reason) => {
936
1007
  this._connected = false;
937
1008
  this.stopHeartbeat();
938
- const code = event?.code ?? 1006;
939
- const reason = event?.reason ?? "";
940
- this.emit("disconnected", code, reason?.toString?.() || "");
1009
+ const reasonStr = reason?.toString() || "";
1010
+ this.emit("disconnected", code, reasonStr);
941
1011
  if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
942
1012
  this.reconnectAttempts++;
943
1013
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
944
1014
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
945
1015
  }
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);
1016
+ });
1017
+ this.ws.on("error", (err) => {
1018
+ this.emit("error", err);
1019
+ if (!this._connected) reject(err);
1020
+ });
961
1021
  });
962
1022
  }
963
1023
  disconnect() {
@@ -978,6 +1038,29 @@ var TikTokLive = class extends TypedEmitter {
978
1038
  get roomId() {
979
1039
  return this._roomId;
980
1040
  }
1041
+ /** Get the stored session ID (if any) */
1042
+ get sessionId() {
1043
+ return this._sessionId;
1044
+ }
1045
+ /** Update the session ID at runtime (e.g. after TikTok login) */
1046
+ setSession(sessionId, ttTargetIdc) {
1047
+ this._sessionId = sessionId;
1048
+ if (ttTargetIdc) this._ttTargetIdc = ttTargetIdc;
1049
+ }
1050
+ /**
1051
+ * Build a cookie header string for authenticated API requests (e.g. ranklist).
1052
+ * Returns undefined if no session is set.
1053
+ */
1054
+ buildSessionCookieHeader() {
1055
+ if (!this._sessionId) return void 0;
1056
+ const parts = [
1057
+ `sessionid=${this._sessionId}`,
1058
+ `sessionid_ss=${this._sessionId}`,
1059
+ `sid_tt=${this._sessionId}`
1060
+ ];
1061
+ if (this._ttTargetIdc) parts.push(`tt-target-idc=${this._ttTargetIdc}`);
1062
+ return parts.join("; ");
1063
+ }
981
1064
  on(event, listener) {
982
1065
  return super.on(event, listener);
983
1066
  }
@@ -990,69 +1073,27 @@ var TikTokLive = class extends TypedEmitter {
990
1073
  emit(event, ...args) {
991
1074
  return super.emit(event, ...args);
992
1075
  }
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) {
1076
+ handleFrame(buf) {
1012
1077
  try {
1013
1078
  const fields = decodeProto(buf);
1014
1079
  const idField = fields.find((f) => f.fn === 2 && f.wt === 0);
1015
1080
  const id = idField ? idField.value : 0n;
1016
1081
  const type = getStr(fields, 7);
1017
1082
  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));
1083
+ if (id > 0n && this.ws?.readyState === import_ws.default.OPEN) {
1084
+ this.ws.send(buildAck(id));
1021
1085
  }
1022
1086
  if (type === "msg" && binary && binary.length > 0) {
1023
1087
  let inner = binary;
1024
1088
  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);
1089
+ try {
1090
+ inner = zlib.gunzipSync(inner);
1091
+ } catch {
1030
1092
  }
1031
1093
  }
1032
1094
  const events = parseWebcastResponse(inner);
1033
1095
  for (const evt of events) {
1034
1096
  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
1097
  this.emit("event", evt);
1057
1098
  this.emit(evt.type, evt);
1058
1099
  }
@@ -1063,9 +1104,8 @@ var TikTokLive = class extends TypedEmitter {
1063
1104
  startHeartbeat(roomId) {
1064
1105
  this.stopHeartbeat();
1065
1106
  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));
1107
+ if (this.ws?.readyState === import_ws.default.OPEN) {
1108
+ this.ws.send(buildHeartbeat(roomId));
1069
1109
  }
1070
1110
  }, this.heartbeatInterval);
1071
1111
  }
@@ -1076,15 +1116,64 @@ var TikTokLive = class extends TypedEmitter {
1076
1116
  }
1077
1117
  }
1078
1118
  };
1119
+
1120
+ // src/api.ts
1121
+ var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
1122
+ var PAGE_CACHE_TTL = 5 * 60 * 1e3;
1123
+ async function getRanklist(opts) {
1124
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1125
+ const ak = encodeURIComponent(opts.apiKey);
1126
+ const body = {};
1127
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1128
+ if (opts.roomId) body.room_id = opts.roomId;
1129
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1130
+ if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
1131
+ if (opts.type) body.type = opts.type;
1132
+ if (opts.rankType) body.rank_type = opts.rankType;
1133
+ const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
1134
+ method: "POST",
1135
+ headers: { "Content-Type": "application/json" },
1136
+ body: JSON.stringify(body)
1137
+ });
1138
+ const data = await resp.json();
1139
+ if (data.status_code === 20003) {
1140
+ throw new Error(
1141
+ data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
1142
+ );
1143
+ }
1144
+ if (data.status_code !== 0) {
1145
+ throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
1146
+ }
1147
+ if (data.sign_and_return) {
1148
+ return data;
1149
+ }
1150
+ return data.data;
1151
+ }
1152
+ async function getRegionalRanklist(opts) {
1153
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1154
+ const ak = encodeURIComponent(opts.apiKey);
1155
+ const body = {};
1156
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1157
+ if (opts.roomId) body.room_id = opts.roomId;
1158
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1159
+ if (opts.rankType) body.rank_type = opts.rankType;
1160
+ if (opts.type) body.type = opts.type;
1161
+ if (opts.gapInterval) body.gap_interval = opts.gapInterval;
1162
+ const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
1163
+ method: "POST",
1164
+ headers: { "Content-Type": "application/json" },
1165
+ body: JSON.stringify(body)
1166
+ });
1167
+ const data = await resp.json();
1168
+ if (data.status_code !== 0) {
1169
+ throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
1170
+ }
1171
+ return data;
1172
+ }
1079
1173
  // Annotate the CommonJS export names for ESM import in node:
1080
1174
  0 && (module.exports = {
1081
1175
  TikTokLive,
1082
- callApi,
1083
- fetchSignedUrl,
1084
- resolveLivePage,
1085
- resolveRoomId,
1086
- solvePuzzle,
1087
- solveRotate,
1088
- solveShapes
1176
+ getRanklist,
1177
+ getRegionalRanklist
1089
1178
  });
1090
1179
  //# sourceMappingURL=index.js.map