@tiktool/live 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -363
- package/dist/index.d.mts +288 -135
- package/dist/index.d.ts +288 -135
- package/dist/index.js +455 -403
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +453 -396
- 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,55 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
816
863
|
maxReconnectAttempts;
|
|
817
864
|
heartbeatInterval;
|
|
818
865
|
debug;
|
|
819
|
-
webSocketImpl;
|
|
820
|
-
WS;
|
|
821
866
|
constructor(options) {
|
|
822
867
|
super();
|
|
823
868
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
824
|
-
this.signServerUrl = (options.signServerUrl ||
|
|
869
|
+
this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
|
|
825
870
|
if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
|
|
826
871
|
this.apiKey = options.apiKey;
|
|
827
872
|
this.autoReconnect = options.autoReconnect ?? true;
|
|
828
873
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
829
874
|
this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
|
|
830
875
|
this.debug = options.debug ?? false;
|
|
831
|
-
this.webSocketImpl = options.webSocketImpl;
|
|
832
876
|
}
|
|
833
877
|
async connect() {
|
|
834
878
|
this.intentionalClose = false;
|
|
835
|
-
|
|
836
|
-
|
|
879
|
+
const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
|
|
880
|
+
"User-Agent": DEFAULT_UA,
|
|
881
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
882
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
883
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
884
|
+
});
|
|
885
|
+
let ttwid = "";
|
|
886
|
+
for (const sc of [resp.headers["set-cookie"] || []].flat()) {
|
|
887
|
+
if (typeof sc === "string" && sc.startsWith("ttwid=")) {
|
|
888
|
+
ttwid = sc.split(";")[0].split("=").slice(1).join("=");
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (!ttwid) throw new Error("Failed to obtain session cookie");
|
|
893
|
+
const html = resp.body.toString();
|
|
894
|
+
let roomId = "";
|
|
895
|
+
let ownerUserId = "";
|
|
896
|
+
const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
|
|
897
|
+
if (sigiMatch) {
|
|
898
|
+
try {
|
|
899
|
+
const json = JSON.parse(sigiMatch[1]);
|
|
900
|
+
const jsonStr = JSON.stringify(json);
|
|
901
|
+
const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
|
|
902
|
+
if (m) roomId = m[1];
|
|
903
|
+
const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
|
|
904
|
+
if (ownerUser?.id) {
|
|
905
|
+
ownerUserId = String(ownerUser.id);
|
|
906
|
+
}
|
|
907
|
+
} catch {
|
|
908
|
+
}
|
|
837
909
|
}
|
|
838
|
-
|
|
839
|
-
if (!pageInfo) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
840
|
-
if (!pageInfo.ttwid) throw new Error("Failed to obtain session cookie");
|
|
841
|
-
const { roomId, ttwid, clusterRegion } = pageInfo;
|
|
910
|
+
if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
842
911
|
this._roomId = roomId;
|
|
912
|
+
this._ownerUserId = ownerUserId;
|
|
913
|
+
const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
|
|
914
|
+
const clusterRegion = crMatch ? crMatch[1] : "";
|
|
843
915
|
const wsHost = getWsHost(clusterRegion);
|
|
844
916
|
const wsParams = new URLSearchParams({
|
|
845
917
|
version_code: "270000",
|
|
@@ -850,9 +922,9 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
850
922
|
browser_language: "en-US",
|
|
851
923
|
browser_platform: "Win32",
|
|
852
924
|
browser_name: "Mozilla",
|
|
853
|
-
browser_version:
|
|
925
|
+
browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
|
|
854
926
|
browser_online: "true",
|
|
855
|
-
tz_name:
|
|
927
|
+
tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
856
928
|
app_name: "tiktok_web",
|
|
857
929
|
sup_ws_ds_opt: "1",
|
|
858
930
|
update_version_code: "2.0.0",
|
|
@@ -893,71 +965,48 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
893
965
|
wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
|
|
894
966
|
}
|
|
895
967
|
return new Promise((resolve, reject) => {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
});
|
|
905
|
-
} catch {
|
|
906
|
-
this.ws = new this.WS(connUrl);
|
|
907
|
-
}
|
|
908
|
-
const ws = this.ws;
|
|
909
|
-
let settled = false;
|
|
910
|
-
ws.onopen = () => {
|
|
968
|
+
this.ws = new import_ws.default(wsUrl, {
|
|
969
|
+
headers: {
|
|
970
|
+
"User-Agent": DEFAULT_UA,
|
|
971
|
+
"Cookie": `ttwid=${ttwid}`,
|
|
972
|
+
"Origin": "https://www.tiktok.com"
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
this.ws.on("open", () => {
|
|
911
976
|
this._connected = true;
|
|
912
977
|
this.reconnectAttempts = 0;
|
|
913
|
-
|
|
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));
|
|
978
|
+
this.ws.send(buildHeartbeat(roomId));
|
|
979
|
+
this.ws.send(buildImEnterRoom(roomId));
|
|
917
980
|
this.startHeartbeat(roomId);
|
|
918
981
|
const roomInfo = {
|
|
919
982
|
roomId,
|
|
920
983
|
wsHost,
|
|
921
984
|
clusterRegion,
|
|
922
|
-
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
985
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
986
|
+
ownerUserId: ownerUserId || void 0
|
|
923
987
|
};
|
|
924
988
|
this.emit("connected");
|
|
925
989
|
this.emit("roomInfo", roomInfo);
|
|
926
|
-
|
|
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) => {
|
|
990
|
+
resolve();
|
|
991
|
+
});
|
|
992
|
+
this.ws.on("message", (rawData) => {
|
|
993
|
+
this.handleFrame(Buffer.from(rawData));
|
|
994
|
+
});
|
|
995
|
+
this.ws.on("close", (code, reason) => {
|
|
936
996
|
this._connected = false;
|
|
937
997
|
this.stopHeartbeat();
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
this.emit("disconnected", code, reason?.toString?.() || "");
|
|
998
|
+
const reasonStr = reason?.toString() || "";
|
|
999
|
+
this.emit("disconnected", code, reasonStr);
|
|
941
1000
|
if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
942
1001
|
this.reconnectAttempts++;
|
|
943
1002
|
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
944
1003
|
setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
|
|
945
1004
|
}
|
|
946
|
-
};
|
|
947
|
-
ws.
|
|
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);
|
|
1005
|
+
});
|
|
1006
|
+
this.ws.on("error", (err) => {
|
|
1007
|
+
this.emit("error", err);
|
|
1008
|
+
if (!this._connected) reject(err);
|
|
1009
|
+
});
|
|
961
1010
|
});
|
|
962
1011
|
}
|
|
963
1012
|
disconnect() {
|
|
@@ -990,69 +1039,27 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
990
1039
|
emit(event, ...args) {
|
|
991
1040
|
return super.emit(event, ...args);
|
|
992
1041
|
}
|
|
993
|
-
|
|
994
|
-
try {
|
|
995
|
-
let bytes;
|
|
996
|
-
if (raw instanceof ArrayBuffer) {
|
|
997
|
-
bytes = new Uint8Array(raw);
|
|
998
|
-
} else if (raw instanceof Uint8Array) {
|
|
999
|
-
bytes = raw;
|
|
1000
|
-
} else if (typeof Blob !== "undefined" && raw instanceof Blob) {
|
|
1001
|
-
bytes = new Uint8Array(await raw.arrayBuffer());
|
|
1002
|
-
} else if (raw?.buffer instanceof ArrayBuffer) {
|
|
1003
|
-
bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
1004
|
-
} else {
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
this.handleFrame(bytes);
|
|
1008
|
-
} catch {
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
async handleFrame(buf) {
|
|
1042
|
+
handleFrame(buf) {
|
|
1012
1043
|
try {
|
|
1013
1044
|
const fields = decodeProto(buf);
|
|
1014
1045
|
const idField = fields.find((f) => f.fn === 2 && f.wt === 0);
|
|
1015
1046
|
const id = idField ? idField.value : 0n;
|
|
1016
1047
|
const type = getStr(fields, 7);
|
|
1017
1048
|
const binary = getBytes(fields, 8);
|
|
1018
|
-
if (id > 0n && this.ws
|
|
1019
|
-
|
|
1020
|
-
this.ws.send(ack.buffer.byteLength === ack.length ? ack.buffer : ack.buffer.slice(ack.byteOffset, ack.byteOffset + ack.byteLength));
|
|
1049
|
+
if (id > 0n && this.ws?.readyState === import_ws.default.OPEN) {
|
|
1050
|
+
this.ws.send(buildAck(id));
|
|
1021
1051
|
}
|
|
1022
1052
|
if (type === "msg" && binary && binary.length > 0) {
|
|
1023
1053
|
let inner = binary;
|
|
1024
1054
|
if (inner.length > 2 && inner[0] === 31 && inner[1] === 139) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
} else {
|
|
1029
|
-
inner = await gunzip(inner);
|
|
1055
|
+
try {
|
|
1056
|
+
inner = zlib.gunzipSync(inner);
|
|
1057
|
+
} catch {
|
|
1030
1058
|
}
|
|
1031
1059
|
}
|
|
1032
1060
|
const events = parseWebcastResponse(inner);
|
|
1033
1061
|
for (const evt of events) {
|
|
1034
1062
|
this._eventCount++;
|
|
1035
|
-
if (evt.type === "battle") {
|
|
1036
|
-
for (const team of evt.teams) {
|
|
1037
|
-
const host = team.users.find((u) => u.user.id === team.hostUserId);
|
|
1038
|
-
if (host) {
|
|
1039
|
-
this._battleHosts.set(team.hostUserId, host.user);
|
|
1040
|
-
team.hostUser = host.user;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
if (evt.type === "battleArmies") {
|
|
1045
|
-
for (const team of evt.teams) {
|
|
1046
|
-
const host = team.users.find((u) => u.user.id === team.hostUserId);
|
|
1047
|
-
if (host) {
|
|
1048
|
-
team.hostUser = host.user;
|
|
1049
|
-
this._battleHosts.set(team.hostUserId, host.user);
|
|
1050
|
-
} else {
|
|
1051
|
-
const cached = this._battleHosts.get(team.hostUserId);
|
|
1052
|
-
if (cached) team.hostUser = cached;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
1063
|
this.emit("event", evt);
|
|
1057
1064
|
this.emit(evt.type, evt);
|
|
1058
1065
|
}
|
|
@@ -1063,9 +1070,8 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
1063
1070
|
startHeartbeat(roomId) {
|
|
1064
1071
|
this.stopHeartbeat();
|
|
1065
1072
|
this.heartbeatTimer = setInterval(() => {
|
|
1066
|
-
if (this.ws
|
|
1067
|
-
|
|
1068
|
-
this.ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
|
|
1073
|
+
if (this.ws?.readyState === import_ws.default.OPEN) {
|
|
1074
|
+
this.ws.send(buildHeartbeat(roomId));
|
|
1069
1075
|
}
|
|
1070
1076
|
}, this.heartbeatInterval);
|
|
1071
1077
|
}
|
|
@@ -1076,15 +1082,61 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
1076
1082
|
}
|
|
1077
1083
|
}
|
|
1078
1084
|
};
|
|
1085
|
+
|
|
1086
|
+
// src/api.ts
|
|
1087
|
+
var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
|
|
1088
|
+
var PAGE_CACHE_TTL = 5 * 60 * 1e3;
|
|
1089
|
+
async function getRanklist(opts) {
|
|
1090
|
+
const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
|
|
1091
|
+
const ak = encodeURIComponent(opts.apiKey);
|
|
1092
|
+
const body = {};
|
|
1093
|
+
if (opts.uniqueId) body.unique_id = opts.uniqueId;
|
|
1094
|
+
if (opts.roomId) body.room_id = opts.roomId;
|
|
1095
|
+
if (opts.anchorId) body.anchor_id = opts.anchorId;
|
|
1096
|
+
if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
|
|
1097
|
+
if (opts.type) body.type = opts.type;
|
|
1098
|
+
if (opts.rankType) body.rank_type = opts.rankType;
|
|
1099
|
+
const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
|
|
1100
|
+
method: "POST",
|
|
1101
|
+
headers: { "Content-Type": "application/json" },
|
|
1102
|
+
body: JSON.stringify(body)
|
|
1103
|
+
});
|
|
1104
|
+
const data = await resp.json();
|
|
1105
|
+
if (data.status_code === 20003) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
if (data.status_code !== 0) {
|
|
1111
|
+
throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
|
|
1112
|
+
}
|
|
1113
|
+
return data.data;
|
|
1114
|
+
}
|
|
1115
|
+
async function getRegionalRanklist(opts) {
|
|
1116
|
+
const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
|
|
1117
|
+
const ak = encodeURIComponent(opts.apiKey);
|
|
1118
|
+
const body = {};
|
|
1119
|
+
if (opts.uniqueId) body.unique_id = opts.uniqueId;
|
|
1120
|
+
if (opts.roomId) body.room_id = opts.roomId;
|
|
1121
|
+
if (opts.anchorId) body.anchor_id = opts.anchorId;
|
|
1122
|
+
if (opts.rankType) body.rank_type = opts.rankType;
|
|
1123
|
+
if (opts.type) body.type = opts.type;
|
|
1124
|
+
if (opts.gapInterval) body.gap_interval = opts.gapInterval;
|
|
1125
|
+
const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
|
|
1126
|
+
method: "POST",
|
|
1127
|
+
headers: { "Content-Type": "application/json" },
|
|
1128
|
+
body: JSON.stringify(body)
|
|
1129
|
+
});
|
|
1130
|
+
const data = await resp.json();
|
|
1131
|
+
if (data.status_code !== 0) {
|
|
1132
|
+
throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
|
|
1133
|
+
}
|
|
1134
|
+
return data;
|
|
1135
|
+
}
|
|
1079
1136
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1080
1137
|
0 && (module.exports = {
|
|
1081
1138
|
TikTokLive,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
resolveLivePage,
|
|
1085
|
-
resolveRoomId,
|
|
1086
|
-
solvePuzzle,
|
|
1087
|
-
solveRotate,
|
|
1088
|
-
solveShapes
|
|
1139
|
+
getRanklist,
|
|
1140
|
+
getRegionalRanklist
|
|
1089
1141
|
});
|
|
1090
1142
|
//# sourceMappingURL=index.js.map
|