@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.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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
333
|
-
const
|
|
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 =
|
|
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/
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
820
|
+
_ownerUserId = "";
|
|
769
821
|
uniqueId;
|
|
770
822
|
signServerUrl;
|
|
771
823
|
apiKey;
|
|
@@ -773,30 +825,55 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
773
825
|
maxReconnectAttempts;
|
|
774
826
|
heartbeatInterval;
|
|
775
827
|
debug;
|
|
776
|
-
webSocketImpl;
|
|
777
|
-
WS;
|
|
778
828
|
constructor(options) {
|
|
779
829
|
super();
|
|
780
830
|
this.uniqueId = options.uniqueId.replace(/^@/, "");
|
|
781
|
-
this.signServerUrl = (options.signServerUrl ||
|
|
831
|
+
this.signServerUrl = (options.signServerUrl || DEFAULT_SIGN_SERVER).replace(/\/$/, "");
|
|
782
832
|
if (!options.apiKey) throw new Error("apiKey is required. Get a free key at https://tik.tools");
|
|
783
833
|
this.apiKey = options.apiKey;
|
|
784
834
|
this.autoReconnect = options.autoReconnect ?? true;
|
|
785
835
|
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
786
836
|
this.heartbeatInterval = options.heartbeatInterval ?? 1e4;
|
|
787
837
|
this.debug = options.debug ?? false;
|
|
788
|
-
this.webSocketImpl = options.webSocketImpl;
|
|
789
838
|
}
|
|
790
839
|
async connect() {
|
|
791
840
|
this.intentionalClose = false;
|
|
792
|
-
|
|
793
|
-
|
|
841
|
+
const resp = await httpGet(`https://www.tiktok.com/@${this.uniqueId}/live`, {
|
|
842
|
+
"User-Agent": DEFAULT_UA,
|
|
843
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
844
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
845
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
846
|
+
});
|
|
847
|
+
let ttwid = "";
|
|
848
|
+
for (const sc of [resp.headers["set-cookie"] || []].flat()) {
|
|
849
|
+
if (typeof sc === "string" && sc.startsWith("ttwid=")) {
|
|
850
|
+
ttwid = sc.split(";")[0].split("=").slice(1).join("=");
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (!ttwid) throw new Error("Failed to obtain session cookie");
|
|
855
|
+
const html = resp.body.toString();
|
|
856
|
+
let roomId = "";
|
|
857
|
+
let ownerUserId = "";
|
|
858
|
+
const sigiMatch = html.match(/id="SIGI_STATE"[^>]*>([^<]+)/);
|
|
859
|
+
if (sigiMatch) {
|
|
860
|
+
try {
|
|
861
|
+
const json = JSON.parse(sigiMatch[1]);
|
|
862
|
+
const jsonStr = JSON.stringify(json);
|
|
863
|
+
const m = jsonStr.match(/"roomId"\s*:\s*"(\d+)"/);
|
|
864
|
+
if (m) roomId = m[1];
|
|
865
|
+
const ownerUser = json?.LiveRoom?.liveRoomUserInfo?.user;
|
|
866
|
+
if (ownerUser?.id) {
|
|
867
|
+
ownerUserId = String(ownerUser.id);
|
|
868
|
+
}
|
|
869
|
+
} catch {
|
|
870
|
+
}
|
|
794
871
|
}
|
|
795
|
-
|
|
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;
|
|
872
|
+
if (!roomId) throw new Error(`User @${this.uniqueId} is not currently live`);
|
|
799
873
|
this._roomId = roomId;
|
|
874
|
+
this._ownerUserId = ownerUserId;
|
|
875
|
+
const crMatch = html.match(/"clusterRegion"\s*:\s*"([^"]+)"/);
|
|
876
|
+
const clusterRegion = crMatch ? crMatch[1] : "";
|
|
800
877
|
const wsHost = getWsHost(clusterRegion);
|
|
801
878
|
const wsParams = new URLSearchParams({
|
|
802
879
|
version_code: "270000",
|
|
@@ -807,9 +884,9 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
807
884
|
browser_language: "en-US",
|
|
808
885
|
browser_platform: "Win32",
|
|
809
886
|
browser_name: "Mozilla",
|
|
810
|
-
browser_version:
|
|
887
|
+
browser_version: DEFAULT_UA.split("Mozilla/")[1] || "5.0",
|
|
811
888
|
browser_online: "true",
|
|
812
|
-
tz_name:
|
|
889
|
+
tz_name: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
813
890
|
app_name: "tiktok_web",
|
|
814
891
|
sup_ws_ds_opt: "1",
|
|
815
892
|
update_version_code: "2.0.0",
|
|
@@ -850,71 +927,48 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
850
927
|
wsUrl = rawWsUrl.replace(/^https:\/\//, "wss://");
|
|
851
928
|
}
|
|
852
929
|
return new Promise((resolve, reject) => {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
});
|
|
862
|
-
} catch {
|
|
863
|
-
this.ws = new this.WS(connUrl);
|
|
864
|
-
}
|
|
865
|
-
const ws = this.ws;
|
|
866
|
-
let settled = false;
|
|
867
|
-
ws.onopen = () => {
|
|
930
|
+
this.ws = new WebSocket(wsUrl, {
|
|
931
|
+
headers: {
|
|
932
|
+
"User-Agent": DEFAULT_UA,
|
|
933
|
+
"Cookie": `ttwid=${ttwid}`,
|
|
934
|
+
"Origin": "https://www.tiktok.com"
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
this.ws.on("open", () => {
|
|
868
938
|
this._connected = true;
|
|
869
939
|
this.reconnectAttempts = 0;
|
|
870
|
-
|
|
871
|
-
|
|
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));
|
|
940
|
+
this.ws.send(buildHeartbeat(roomId));
|
|
941
|
+
this.ws.send(buildImEnterRoom(roomId));
|
|
874
942
|
this.startHeartbeat(roomId);
|
|
875
943
|
const roomInfo = {
|
|
876
944
|
roomId,
|
|
877
945
|
wsHost,
|
|
878
946
|
clusterRegion,
|
|
879
|
-
connectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
947
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
948
|
+
ownerUserId: ownerUserId || void 0
|
|
880
949
|
};
|
|
881
950
|
this.emit("connected");
|
|
882
951
|
this.emit("roomInfo", roomInfo);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
};
|
|
888
|
-
ws.
|
|
889
|
-
const raw = event.data !== void 0 ? event.data : event;
|
|
890
|
-
this.handleMessage(raw);
|
|
891
|
-
};
|
|
892
|
-
ws.onclose = (event) => {
|
|
952
|
+
resolve();
|
|
953
|
+
});
|
|
954
|
+
this.ws.on("message", (rawData) => {
|
|
955
|
+
this.handleFrame(Buffer.from(rawData));
|
|
956
|
+
});
|
|
957
|
+
this.ws.on("close", (code, reason) => {
|
|
893
958
|
this._connected = false;
|
|
894
959
|
this.stopHeartbeat();
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
this.emit("disconnected", code, reason?.toString?.() || "");
|
|
960
|
+
const reasonStr = reason?.toString() || "";
|
|
961
|
+
this.emit("disconnected", code, reasonStr);
|
|
898
962
|
if (!this.intentionalClose && this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
899
963
|
this.reconnectAttempts++;
|
|
900
964
|
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 3e4);
|
|
901
965
|
setTimeout(() => this.connect().catch((e) => this.emit("error", e)), delay);
|
|
902
966
|
}
|
|
903
|
-
};
|
|
904
|
-
ws.
|
|
905
|
-
this.emit("error", err
|
|
906
|
-
if (!
|
|
907
|
-
|
|
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);
|
|
967
|
+
});
|
|
968
|
+
this.ws.on("error", (err) => {
|
|
969
|
+
this.emit("error", err);
|
|
970
|
+
if (!this._connected) reject(err);
|
|
971
|
+
});
|
|
918
972
|
});
|
|
919
973
|
}
|
|
920
974
|
disconnect() {
|
|
@@ -947,69 +1001,27 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
947
1001
|
emit(event, ...args) {
|
|
948
1002
|
return super.emit(event, ...args);
|
|
949
1003
|
}
|
|
950
|
-
|
|
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) {
|
|
1004
|
+
handleFrame(buf) {
|
|
969
1005
|
try {
|
|
970
1006
|
const fields = decodeProto(buf);
|
|
971
1007
|
const idField = fields.find((f) => f.fn === 2 && f.wt === 0);
|
|
972
1008
|
const id = idField ? idField.value : 0n;
|
|
973
1009
|
const type = getStr(fields, 7);
|
|
974
1010
|
const binary = getBytes(fields, 8);
|
|
975
|
-
if (id > 0n && this.ws
|
|
976
|
-
|
|
977
|
-
this.ws.send(ack.buffer.byteLength === ack.length ? ack.buffer : ack.buffer.slice(ack.byteOffset, ack.byteOffset + ack.byteLength));
|
|
1011
|
+
if (id > 0n && this.ws?.readyState === WebSocket.OPEN) {
|
|
1012
|
+
this.ws.send(buildAck(id));
|
|
978
1013
|
}
|
|
979
1014
|
if (type === "msg" && binary && binary.length > 0) {
|
|
980
1015
|
let inner = binary;
|
|
981
1016
|
if (inner.length > 2 && inner[0] === 31 && inner[1] === 139) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
} else {
|
|
986
|
-
inner = await gunzip(inner);
|
|
1017
|
+
try {
|
|
1018
|
+
inner = zlib.gunzipSync(inner);
|
|
1019
|
+
} catch {
|
|
987
1020
|
}
|
|
988
1021
|
}
|
|
989
1022
|
const events = parseWebcastResponse(inner);
|
|
990
1023
|
for (const evt of events) {
|
|
991
1024
|
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
1025
|
this.emit("event", evt);
|
|
1014
1026
|
this.emit(evt.type, evt);
|
|
1015
1027
|
}
|
|
@@ -1020,9 +1032,8 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
1020
1032
|
startHeartbeat(roomId) {
|
|
1021
1033
|
this.stopHeartbeat();
|
|
1022
1034
|
this.heartbeatTimer = setInterval(() => {
|
|
1023
|
-
if (this.ws
|
|
1024
|
-
|
|
1025
|
-
this.ws.send(hb.buffer.byteLength === hb.length ? hb.buffer : hb.buffer.slice(hb.byteOffset, hb.byteOffset + hb.byteLength));
|
|
1035
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
1036
|
+
this.ws.send(buildHeartbeat(roomId));
|
|
1026
1037
|
}
|
|
1027
1038
|
}, this.heartbeatInterval);
|
|
1028
1039
|
}
|
|
@@ -1033,14 +1044,60 @@ var TikTokLive = class extends TypedEmitter {
|
|
|
1033
1044
|
}
|
|
1034
1045
|
}
|
|
1035
1046
|
};
|
|
1047
|
+
|
|
1048
|
+
// src/api.ts
|
|
1049
|
+
var DEFAULT_SIGN_SERVER2 = "https://api.tik.tools";
|
|
1050
|
+
var PAGE_CACHE_TTL = 5 * 60 * 1e3;
|
|
1051
|
+
async function getRanklist(opts) {
|
|
1052
|
+
const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
|
|
1053
|
+
const ak = encodeURIComponent(opts.apiKey);
|
|
1054
|
+
const body = {};
|
|
1055
|
+
if (opts.uniqueId) body.unique_id = opts.uniqueId;
|
|
1056
|
+
if (opts.roomId) body.room_id = opts.roomId;
|
|
1057
|
+
if (opts.anchorId) body.anchor_id = opts.anchorId;
|
|
1058
|
+
if (opts.sessionCookie) body.session_cookie = opts.sessionCookie;
|
|
1059
|
+
if (opts.type) body.type = opts.type;
|
|
1060
|
+
if (opts.rankType) body.rank_type = opts.rankType;
|
|
1061
|
+
const resp = await fetch(`${base}/webcast/ranklist?apiKey=${ak}`, {
|
|
1062
|
+
method: "POST",
|
|
1063
|
+
headers: { "Content-Type": "application/json" },
|
|
1064
|
+
body: JSON.stringify(body)
|
|
1065
|
+
});
|
|
1066
|
+
const data = await resp.json();
|
|
1067
|
+
if (data.status_code === 20003) {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
data.message || "TikTok requires login. Provide sessionCookie with your TikTok session."
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
if (data.status_code !== 0) {
|
|
1073
|
+
throw new Error(data.error || `Ranklist failed (status ${data.status_code})`);
|
|
1074
|
+
}
|
|
1075
|
+
return data.data;
|
|
1076
|
+
}
|
|
1077
|
+
async function getRegionalRanklist(opts) {
|
|
1078
|
+
const base = (opts.serverUrl || DEFAULT_SIGN_SERVER2).replace(/\/$/, "");
|
|
1079
|
+
const ak = encodeURIComponent(opts.apiKey);
|
|
1080
|
+
const body = {};
|
|
1081
|
+
if (opts.uniqueId) body.unique_id = opts.uniqueId;
|
|
1082
|
+
if (opts.roomId) body.room_id = opts.roomId;
|
|
1083
|
+
if (opts.anchorId) body.anchor_id = opts.anchorId;
|
|
1084
|
+
if (opts.rankType) body.rank_type = opts.rankType;
|
|
1085
|
+
if (opts.type) body.type = opts.type;
|
|
1086
|
+
if (opts.gapInterval) body.gap_interval = opts.gapInterval;
|
|
1087
|
+
const resp = await fetch(`${base}/webcast/ranklist/regional?apiKey=${ak}`, {
|
|
1088
|
+
method: "POST",
|
|
1089
|
+
headers: { "Content-Type": "application/json" },
|
|
1090
|
+
body: JSON.stringify(body)
|
|
1091
|
+
});
|
|
1092
|
+
const data = await resp.json();
|
|
1093
|
+
if (data.status_code !== 0) {
|
|
1094
|
+
throw new Error(data.error || `Regional ranklist failed (status ${data.status_code})`);
|
|
1095
|
+
}
|
|
1096
|
+
return data;
|
|
1097
|
+
}
|
|
1036
1098
|
export {
|
|
1037
1099
|
TikTokLive,
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
resolveLivePage,
|
|
1041
|
-
resolveRoomId,
|
|
1042
|
-
solvePuzzle,
|
|
1043
|
-
solveRotate,
|
|
1044
|
-
solveShapes
|
|
1100
|
+
getRanklist,
|
|
1101
|
+
getRegionalRanklist
|
|
1045
1102
|
};
|
|
1046
1103
|
//# sourceMappingURL=index.mjs.map
|