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