@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/README.md +119 -363
- package/dist/index.d.mts +324 -135
- package/dist/index.d.ts +324 -135
- package/dist/index.js +491 -402
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +489 -395
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -28
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
|
-
|
|
35
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
376
|
-
const
|
|
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 =
|
|
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/
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
820
|
-
|
|
866
|
+
_sessionId;
|
|
867
|
+
_ttTargetIdc;
|
|
821
868
|
constructor(options) {
|
|
822
869
|
super();
|
|
823
870
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
824
|
-
this.signServerUrl = (options.signServerUrl ||
|
|
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.
|
|
878
|
+
this._sessionId = options.sessionId;
|
|
879
|
+
this._ttTargetIdc = options.ttTargetIdc;
|
|
832
880
|
}
|
|
833
881
|
async connect() {
|
|
834
882
|
this.intentionalClose = false;
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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:
|
|
929
|
+
browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
|
|
854
930
|
browser_online: "true",
|
|
855
|
-
tz_name:
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
914
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
};
|
|
931
|
-
ws.
|
|
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
|
|
939
|
-
|
|
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.
|
|
948
|
-
this.emit("error", err
|
|
949
|
-
if (!
|
|
950
|
-
|
|
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
|
-
|
|
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
|
|
1019
|
-
|
|
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
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
|
1067
|
-
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
resolveLivePage,
|
|
1085
|
-
resolveRoomId,
|
|
1086
|
-
solvePuzzle,
|
|
1087
|
-
solveRotate,
|
|
1088
|
-
solveShapes
|
|
1176
|
+
getRanklist,
|
|
1177
|
+
getRegionalRanklist
|
|
1089
1178
|
});
|
|
1090
1179
|
//# sourceMappingURL=index.js.map
|