@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.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,59 @@ var TikTokLive = class extends TypedEmitter {
773
825
  maxReconnectAttempts;
774
826
  heartbeatInterval;
775
827
  debug;
776
- webSocketImpl;
777
- WS;
828
+ _sessionId;
829
+ _ttTargetIdc;
778
830
  constructor(options) {
779
831
  super();
780
832
  this.uniqueId = options.uniqueId.replace(/^@/, "");
781
- this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
833
+ this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
782
834
  if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
783
835
  this.apiKey = options.apiKey;
784
836
  this.autoReconnect = options.autoReconnect ?? true;
785
837
  this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
786
838
  this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
787
839
  this.debug = options.debug ?? false;
788
- this.webSocketImpl = options.webSocketImpl;
840
+ this._sessionId = options.sessionId;
841
+ this._ttTargetIdc = options.ttTargetIdc;
789
842
  }
790
843
  async connect() {
791
844
  this.intentionalClose = false;
792
- if (!this.WS) {
793
- this.WS = await resolveWebSocket(this.webSocketImpl);
845
+ const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
846
+ "User-Agent": DEFAULT_UA,
847
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
848
+ "Accept-Encoding": "gzip, deflate, br",
849
+ "Accept-Language": "en-US,en;q=0.9"
850
+ });
851
+ let ttwid = "";
852
+ for (const sc of [resp.headers["set-cookie"] || []].flat()) {
853
+ if (typeof sc === "string" && sc.startsWith("ttwid=")) {
854
+ ttwid = sc.split(";")[0].split("=").slice(1).join("=");
855
+ break;
856
+ }
794
857
  }
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;
858
+ if (!ttwid) throw new Error("Failed to obtain session cookie");
859
+ const html = resp.body.toString();
860
+ let roomId = "";
861
+ let ownerUserId = "";
862
+ const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
863
+ if (sigiMatch) {
864
+ try {
865
+ const json = JSON.parse(sigiMatch[1]);
866
+ const jsonStr = JSON.stringify(json);
867
+ const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
868
+ if (m) roomId = m[1];
869
+ const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
870
+ if (ownerUser?.id) {
871
+ ownerUserId = String(ownerUser.id);
872
+ }
873
+ } catch {
874
+ }
875
+ }
876
+ if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
799
877
  this._roomId = roomId;
878
+ this._ownerUserId = ownerUserId;
879
+ const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
880
+ const clusterRegion = crMatch ? crMatch[1] : "";
800
881
  const wsHost = getWsHost(clusterRegion);
801
882
  const wsParams = new URLSearchParams({
802
883
  version_code: "270000",
@@ -807,9 +888,9 @@ var TikTokLive = class extends TypedEmitter {
807
888
  browser_language: "en-US",
808
889
  browser_platform: "Win32",
809
890
  browser_name: "Mozilla",
810
- browser_version: DEFAULT_UA2.split("Mozilla/")[1] || "5.0",
891
+ browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
811
892
  browser_online: "true",
812
- tz_name: "Etc/UTC",
893
+ tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
813
894
  app_name: "tiktok_web",
814
895
  sup_ws_ds_opt: "1",
815
896
  update_version_code: "2.0.0",
@@ -850,71 +931,55 @@ var TikTokLive = class extends TypedEmitter {
850
931
  wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
851
932
  }
852
933
  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);
934
+ let cookieHeader = `ttwid=${ttwid}`;
935
+ if (this._sessionId) {
936
+ cookieHeader += `; sessionid=${this._sessionId}; sessionid_ss=${this._sessionId}; sid_tt=${this._sessionId}`;
937
+ if (this._ttTargetIdc) {
938
+ cookieHeader += `; tt-target-idc=${this._ttTargetIdc}`;
939
+ }
864
940
  }
865
- const ws = this.ws;
866
- let settled = false;
867
- ws.onopen = () => {
941
+ this.ws = new WebSocket(wsUrl, {
942
+ headers: {
943
+ "User-Agent": DEFAULT_UA,
944
+ "Cookie": cookieHeader,
945
+ "Origin": "https://www.tiktok.com"
946
+ }
947
+ });
948
+ this.ws.on("open", () => {
868
949
  this._connected = true;
869
950
  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));
951
+ this.ws.send(buildHeartbeat(roomId));
952
+ this.ws.send(buildImEnterRoom(roomId));
874
953
  this.startHeartbeat(roomId);
875
954
  const roomInfo = {
876
955
  roomId,
877
956
  wsHost,
878
957
  clusterRegion,
879
- connectedAt: (/* @__PURE__ */ new Date()).toISOString()
958
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
959
+ ownerUserId: ownerUserId || void 0
880
960
  };
881
961
  this.emit("connected");
882
962
  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) => {
963
+ resolve();
964
+ });
965
+ this.ws.on("message", (rawData) => {
966
+ this.handleFrame(Buffer.from(rawData));
967
+ });
968
+ this.ws.on("close", (code, reason) => {
893
969
  this._connected = false;
894
970
  this.stopHeartbeat();
895
- const code = event?.code ?? 1006;
896
- const reason = event?.reason ?? "";
897
- this.emit("disconnected", code, reason?.toString?.() || "");
971
+ const reasonStr = reason?.toString() || "";
972
+ this.emit("disconnected", code, reasonStr);
898
973
  if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
899
974
  this.reconnectAttempts++;
900
975
  const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
901
976
  setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
902
977
  }
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);
978
+ });
979
+ this.ws.on("error", (err) => {
980
+ this.emit("error", err);
981
+ if (!this._connected) reject(err);
982
+ });
918
983
  });
919
984
  }
920
985
  disconnect() {
@@ -935,6 +1000,29 @@ var TikTokLive = class extends TypedEmitter {
935
1000
  get roomId() {
936
1001
  return this._roomId;
937
1002
  }
1003
+ /** Get the stored session ID (if any) */
1004
+ get sessionId() {
1005
+ return this._sessionId;
1006
+ }
1007
+ /** Update the session ID at runtime (e.g. after TikTok login) */
1008
+ setSession(sessionId, ttTargetIdc) {
1009
+ this._sessionId = sessionId;
1010
+ if (ttTargetIdc) this._ttTargetIdc = ttTargetIdc;
1011
+ }
1012
+ /**
1013
+ * Build a cookie header string for authenticated API requests (e.g. ranklist).
1014
+ * Returns undefined if no session is set.
1015
+ */
1016
+ buildSessionCookieHeader() {
1017
+ if (!this._sessionId) return void 0;
1018
+ const parts = [
1019
+ `sessionid=${this._sessionId}`,
1020
+ `sessionid_ss=${this._sessionId}`,
1021
+ `sid_tt=${this._sessionId}`
1022
+ ];
1023
+ if (this._ttTargetIdc) parts.push(`tt-target-idc=${this._ttTargetIdc}`);
1024
+ return parts.join("; ");
1025
+ }
938
1026
  on(event, listener) {
939
1027
  return super.on(event, listener);
940
1028
  }
@@ -947,69 +1035,27 @@ var TikTokLive = class extends TypedEmitter {
947
1035
  emit(event, ...args) {
948
1036
  return super.emit(event, ...args);
949
1037
  }
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) {
1038
+ handleFrame(buf) {
969
1039
  try {
970
1040
  const fields = decodeProto(buf);
971
1041
  const idField = fields.find((f) => f.fn === 2 && f.wt === 0);
972
1042
  const id = idField ? idField.value : 0n;
973
1043
  const type = getStr(fields, 7);
974
1044
  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));
1045
+ if (id > 0n && this.ws?.readyState === WebSocket.OPEN) {
1046
+ this.ws.send(buildAck(id));
978
1047
  }
979
1048
  if (type === "msg" && binary && binary.length > 0) {
980
1049
  let inner = binary;
981
1050
  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);
1051
+ try {
1052
+ inner = zlib.gunzipSync(inner);
1053
+ } catch {
987
1054
  }
988
1055
  }
989
1056
  const events = parseWebcastResponse(inner);
990
1057
  for (const evt of events) {
991
1058
  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
1059
  this.emit("event", evt);
1014
1060
  this.emit(evt.type, evt);
1015
1061
  }
@@ -1020,9 +1066,8 @@ var TikTokLive = class extends TypedEmitter {
1020
1066
  startHeartbeat(roomId) {
1021
1067
  this.stopHeartbeat();
1022
1068
  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));
1069
+ if (this.ws?.readyState === WebSocket.OPEN) {
1070
+ this.ws.send(buildHeartbeat(roomId));
1026
1071
  }
1027
1072
  }, this.heartbeatInterval);
1028
1073
  }
@@ -1033,14 +1078,63 @@ var TikTokLive = class extends TypedEmitter {
1033
1078
  }
1034
1079
  }
1035
1080
  };
1081
+
1082
+ // src/api.ts
1083
+ var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
1084
+ var PAGE_CACHE_TTL = 5 * 60 * 1e3;
1085
+ async function getRanklist(opts) {
1086
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1087
+ const ak = encodeURIComponent(opts.apiKey);
1088
+ const body = {};
1089
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1090
+ if (opts.roomId) body.room_id = opts.roomId;
1091
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1092
+ if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
1093
+ if (opts.type) body.type = opts.type;
1094
+ if (opts.rankType) body.rank_type = opts.rankType;
1095
+ const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
1096
+ method: "POST",
1097
+ headers: { "Content-Type": "application/json" },
1098
+ body: JSON.stringify(body)
1099
+ });
1100
+ const data = await resp.json();
1101
+ if (data.status_code === 20003) {
1102
+ throw new Error(
1103
+ data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
1104
+ );
1105
+ }
1106
+ if (data.status_code !== 0) {
1107
+ throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
1108
+ }
1109
+ if (data.sign_and_return) {
1110
+ return data;
1111
+ }
1112
+ return data.data;
1113
+ }
1114
+ async function getRegionalRanklist(opts) {
1115
+ const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
1116
+ const ak = encodeURIComponent(opts.apiKey);
1117
+ const body = {};
1118
+ if (opts.uniqueId) body.unique_id = opts.uniqueId;
1119
+ if (opts.roomId) body.room_id = opts.roomId;
1120
+ if (opts.anchorId) body.anchor_id = opts.anchorId;
1121
+ if (opts.rankType) body.rank_type = opts.rankType;
1122
+ if (opts.type) body.type = opts.type;
1123
+ if (opts.gapInterval) body.gap_interval = opts.gapInterval;
1124
+ const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
1125
+ method: "POST",
1126
+ headers: { "Content-Type": "application/json" },
1127
+ body: JSON.stringify(body)
1128
+ });
1129
+ const data = await resp.json();
1130
+ if (data.status_code !== 0) {
1131
+ throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
1132
+ }
1133
+ return data;
1134
+ }
1036
1135
  export {
1037
1136
  TikTokLive,
1038
- callApi,
1039
- fetchSignedUrl,
1040
- resolveLivePage,
1041
- resolveRoomId,
1042
- solvePuzzle,
1043
- solveRotate,
1044
- solveShapes
1137
+ getRanklist,
1138
+ getRegionalRanklist
1045
1139
  };
1046
1140
  //# sourceMappingURL=index.mjs.map