UNPKG

tiktok-live-connector

Version:

Node.js library to receive live stream chat events like comments and gifts from TikTok LIVE.

1,155 lines 85.1 kB
import "./chunk-ZRW5x4aP.js"; import { EventEmitter } from "node:events"; import * as tikTokSchema from "tiktok-live-proto/v3"; import { ControlAction, EnvelopeBusinessType, HeartBeatMessage, WebcastImEnterRoomMessage, WebcastPushFrame } from "tiktok-live-proto/v3"; import * as zlib from "node:zlib"; import * as util from "node:util"; import { base64Encode } from "@bufbuild/protobuf/wire"; import { WebSocket } from "ws"; import got from "got"; import EulerStreamApiClient, { WebcastFetchPlatform } from "tiktok-live-api-sdk"; import { isIP } from "node:net"; //#region src/types/errors.ts var ConnectError = class extends Error { constructor(message) { super(message); } }; var InvalidUniqueIdError = class extends Error {}; var InvalidResponseError = class extends Error { constructor(config, ...args) { super(`[${config.routeId}] ${args.join(" ")}`); this.config = config; } }; var InvalidResponseCompositeError = class extends Error { constructor(config, ...args) { super(...args); this.config = config; } }; var InvalidRequestError = class extends Error { constructor(config, ...args) { super(`[${config.routeId}] ${args.join(" ")}`); this.config = config; } }; var AlreadyConnectingError = class extends ConnectError {}; var AlreadyConnectedError = class extends ConnectError {}; var UserOfflineError = class extends ConnectError {}; var ConnectTimeoutError = class extends ConnectError {}; var InvalidSchemaNameError = class extends Error {}; var SchemaDecodeError = class extends Error {}; var TikTokLiveError = class extends Error { constructor(message) { super(message); this.name = this.constructor.name; } }; let ErrorReason = /* @__PURE__ */ function(ErrorReason) { ErrorReason["RATE_LIMIT"] = "Rate Limited"; ErrorReason["CONNECT_ERROR"] = "Connect Error"; ErrorReason["EMPTY_PAYLOAD"] = "Empty Payload"; ErrorReason["SIGN_NOT_200"] = "Sign Error"; ErrorReason["EMPTY_COOKIES"] = "Empty Cookies"; ErrorReason["PREMIUM_FEATURE"] = "Premium Feature"; ErrorReason["AUTHENTICATED_WS"] = "Authenticated WS"; return ErrorReason; }({}); var SignAPIError = class extends TikTokLiveError { reason; requestId; agentId; constructor(reason, requestId, agentId, ...args) { super([`[${reason}]`, ...args.filter((a) => a !== void 0).map((a) => a instanceof Error ? a.message : a)].join(" ")); this.reason = reason; this.requestId = requestId; this.agentId = agentId; } static formatSignServerMessage(message) { message = message.trim(); const msgLen = message.length; const headerLen = Math.floor((msgLen - 19) / 2); const paddingLen = (msgLen - 19) % 2; const footer = "+" + "-".repeat(msgLen + 2) + "+"; const header = "+" + "-".repeat(headerLen) + " SIGN SERVER MESSAGE " + "-".repeat(headerLen + paddingLen) + "+"; return `\n\t${"|" + " ".repeat(header.length - 2) + "|"}\n\t${header}\n\t${"| " + message + " |"}\n\t${footer}\n`; } }; var SignatureRateLimitError = class SignatureRateLimitError extends SignAPIError { retryAfter; resetTime; constructor(apiMessage, formatStr, response) { const retryAfter = SignatureRateLimitError.calculateRetryAfter(response); const resetTime = SignatureRateLimitError.calculateResetTime(response); const logId = response.headers["x-log-id"]; const agentId = response.headers["x-agent-id"]; const args = [formatStr.replace("%s", retryAfter.toString())]; if (apiMessage) { const serverMsg = SignAPIError.formatSignServerMessage(apiMessage); args.push(serverMsg); } super("Rate Limited", logId, agentId, ...args); this.retryAfter = retryAfter; this.resetTime = resetTime; } static parseHeaderNumber(value) { return value ? parseInt(value) : void 0; } static calculateRetryAfter(response) { return parseInt(response.headers["retry-after"] || "0") * 1e3; } static calculateResetTime(response) { const value = response.headers["x-ratelimit-reset"]; return value ? parseInt(value) * 1e3 : void 0; } }; var SignatureMissingTokensError = class extends SignAPIError { constructor(...args) { super("Empty Payload", void 0, void 0, ...args); } }; var PremiumFeatureError = class extends SignAPIError { constructor(apiMessage, ...args) { args.push(SignAPIError.formatSignServerMessage(apiMessage)); super("Premium Feature", void 0, void 0, ...args); } }; var AuthenticatedWebSocketConnectionError = class extends SignAPIError { constructor(...args) { super("Authenticated WS", void 0, void 0, ...args); } }; //#endregion //#region src/lib/ws/lib/proto-utils.ts const unzip = util.promisify(zlib.unzip); function hasProtoName(protoName) { return !!tikTokSchema[protoName]; } const WebcastDeserializeConfig = { skipMessageTypes: [], includeMessageTypes: null, showBase64OnDecodeError: true }; function deserializeMessage(protoName, binaryMessage) { const messageFn = tikTokSchema[protoName]; if (!messageFn) throw new InvalidSchemaNameError(`Invalid schema name: ${protoName}`); let deserializedMessage; try { deserializedMessage = messageFn.decode(binaryMessage); } catch (ex) { let errStr = `Failed to decode message type: ${protoName}: ` + ex.stack; if (WebcastDeserializeConfig.showBase64OnDecodeError) errStr += "Base64 - Use this to create an issue: " + base64Encode(new Uint8Array(binaryMessage)); throw new SchemaDecodeError(errStr); } if (protoName === "ProtoMessageFetchResult") for (const message of deserializedMessage.messages || []) { if (WebcastDeserializeConfig.includeMessageTypes === null) { if (WebcastDeserializeConfig.skipMessageTypes?.includes(message.method)) continue; } else if (!WebcastDeserializeConfig.includeMessageTypes.includes(message.method)) continue; if (!hasProtoName(message.method)) { if (process.env.DEBUG_DESERIALIZE_XD) { console.log("---------------"); console.log(message.method, base64Encode(Buffer.from(message.payload))); console.log("---------------"); } continue; } try { message.decodedData = { type: message.method, data: deserializeMessage(message.method, Buffer.from(message.payload)) }; } catch (ex) { message.decodeError = ex; } } return deserializedMessage; } async function deserializeWebSocketMessage(binaryMessage) { const rawWebcastWebSocketMessage = WebcastPushFrame.decode(binaryMessage); let protoMessageFetchResult = void 0; if (rawWebcastWebSocketMessage.payloadEncoding === "pb" && rawWebcastWebSocketMessage.payload) { let binary = rawWebcastWebSocketMessage.payload; if (binary && binary.length > 2 && binary[0] === 31 && binary[1] === 139 && binary[2] === 8) rawWebcastWebSocketMessage.payload = await unzip(binary); protoMessageFetchResult = deserializeMessage("ProtoMessageFetchResult", Buffer.from(rawWebcastWebSocketMessage.payload)); } const decodedContainer = rawWebcastWebSocketMessage; decodedContainer.protoMessageFetchResult = protoMessageFetchResult; return decodedContainer; } /** * Validates and normalizes the uniqueId (username) input for TikTok live room information retrieval. * It ensures that the input is a string and extracts the username from various possible formats, such as full URLs or with/without '@' symbol. If the input is invalid, it throws an error with a descriptive message. * * @param uniqueId Input value representing the unique identifier (username) of a TikTok user. This can be in various formats, such as a plain username, a full TikTok URL, or with an '@' symbol. */ function validateAndNormalizeUniqueId(uniqueId) { if (typeof uniqueId !== "string") throw new InvalidUniqueIdError("Missing or invalid value for 'uniqueId'. Please provide the username from TikTok URL."); return uniqueId.replace("https://www.tiktok.com/", "").replace("/live", "").replace("@", "").trim(); } /** * Create a base WebcastPushFrame with default values, allowing overrides for specific fields. This is useful for testing or constructing messages without needing to specify every field. * * @param overrides Overrides to apply to the default WebcastPushFrame. Fields not included in the overrides will be set to default values that typically indicate "not set" (e.g., "0" for numeric fields, empty buffer for payload). */ function createBaseWebcastPushFrame(overrides) { const undefinedNum = "0"; overrides = Object.fromEntries(Object.entries(overrides).filter(([_, value]) => value !== void 0)); return WebcastPushFrame.encode({ seqId: undefinedNum, logId: undefinedNum, payloadEncoding: "pb", payloadType: "msg", payload: Buffer.from([]), service: undefinedNum, method: undefinedNum, headers: [], ...overrides }); } //#endregion //#region src/lib/web/defaults.ts /** * Hint the order of the map by putting a templated value for an item that must be dynamically generated. */ const TEMPLATED_VALUE$1 = "[UNRESOLVED]"; const WebcastWebConfigDefaults = { TIKTOK_HOST_WEB: "www.tiktok.com", TIKTOK_HOST_WEBCAST: "webcast.tiktok.com", TIKTOK_HTTP_ORIGIN: "https://www.tiktok.com", DEFAULT_HTTP_CLIENT_OPTIONS: {}, DEFAULT_HTTP_CLIENT_HEADERS: { "Cookie": "tt-target-idc=useast1a", "Connection": "keep-alive", "Cache-Control": "max-age=0", "User-Agent": TEMPLATED_VALUE$1, "Accept": "text/html,application/json,application/protobuf", "Referer": "https://www.tiktok.com/", "Origin": "https://www.tiktok.com", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "Sec-Fetch-Site": "same-site", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Ua-Mobile": "?0" }, DEFAULT_HTTP_CLIENT_PARAMS: { "aid": 1988 .toString(), "app_language": TEMPLATED_VALUE$1, "app_name": "tiktok_web", "browser_language": TEMPLATED_VALUE$1, "browser_name": TEMPLATED_VALUE$1, "browser_online": "true", "browser_platform": TEMPLATED_VALUE$1, "browser_version": TEMPLATED_VALUE$1, "cookie_enabled": "true", "device_platform": "web_pc", "focus_state": "true", "from_page": "user", "history_len": "10", "is_fullscreen": "false", "is_page_visible": "true", "screen_height": TEMPLATED_VALUE$1, "screen_width": TEMPLATED_VALUE$1, "tz_name": TEMPLATED_VALUE$1, "referer": "https://www.tiktok.com/", "root_referer": "https://www.tiktok.com/", "channel": "tiktok_web", "data_collection_enabled": "true", "os": TEMPLATED_VALUE$1, "priority_region": TEMPLATED_VALUE$1, "region": TEMPLATED_VALUE$1, "user_is_login": "true", "webcast_language": TEMPLATED_VALUE$1, "device_id": TEMPLATED_VALUE$1 }, SESSION_COOKIE_NAMES: [ "sessionid", "sessionid_ss", "sid_tt", "sid_guard" ], TARGET_IDC_COOKIE_NAME: "tt-target-idc" }; //#endregion //#region src/lib/ws/defaults.ts const TEMPLATED_VALUE = "[UNRESOLVED]"; const WebSocketConfigDefaults = { TIKTOK_HOST_WS: WebcastWebConfigDefaults.TIKTOK_HOST_WEB, DEFAULT_WS_CLIENT_PARAMS: { "version_code": "180800", "aid": 1988 .toString(), "app_language": TEMPLATED_VALUE, "app_name": "tiktok_web", "browser_platform": TEMPLATED_VALUE, "browser_language": TEMPLATED_VALUE, "browser_name": TEMPLATED_VALUE, "browser_version": TEMPLATED_VALUE, "browser_online": "true", "cookie_enabled": "true", "tz_name": TEMPLATED_VALUE, "device_platform": "web", "identity": "audience", "live_id": "12", "webcast_language": TEMPLATED_VALUE, "ws_direct": "0", "sup_ws_ds_opt": "1", "update_version_code": "2.0.0", "did_rule": "3", "screen_height": TEMPLATED_VALUE, "screen_width": TEMPLATED_VALUE, "heartbeat_duration": "0", "resp_content_type": "protobuf", "history_comment_count": "6", "client_enter": "1", "last_rtt": (Math.random() * 100 + 100).toString() }, DEFAULT_WS_CLIENT_PARAMS_APPEND_PARAMETER: "&version_code=270000", DEFAULT_WS_CLIENT_HEADERS: { "User-Agent": TEMPLATED_VALUE }, DEFAULT_WS_PING_INTERVAL: 1e4 }; //#endregion //#region src/lib/ws/config.ts function getWebSocketConfigDefaults({ device, screen, location }) { const baseConfig = structuredClone(WebSocketConfigDefaults); baseConfig.DEFAULT_WS_CLIENT_PARAMS.app_language = location.lang; baseConfig.DEFAULT_WS_CLIENT_PARAMS.browser_platform = device.browser_platform; baseConfig.DEFAULT_WS_CLIENT_PARAMS.browser_language = location.lang_country; baseConfig.DEFAULT_WS_CLIENT_PARAMS.browser_name = device.browser_name; baseConfig.DEFAULT_WS_CLIENT_PARAMS.browser_version = device.browser_version; baseConfig.DEFAULT_WS_CLIENT_PARAMS.tz_name = location.tz_name; baseConfig.DEFAULT_WS_CLIENT_PARAMS.webcast_language = location.lang; baseConfig.DEFAULT_WS_CLIENT_PARAMS.screen_height = screen.screen_height.toString(); baseConfig.DEFAULT_WS_CLIENT_PARAMS.screen_width = screen.screen_width.toString(); baseConfig.DEFAULT_WS_CLIENT_PARAMS["User-Agent"] = device.user_agent; baseConfig.DEFAULT_WS_CLIENT_HEADERS["User-Agent"] = device.user_agent; return baseConfig; } //#endregion //#region src/lib/ws/lib/ws-utils.ts /** * Generates a random java.Long for the uniqId string. */ function generateUniqId() { const bytes = crypto.getRandomValues(new Uint8Array(8)); bytes[0] &= 127; let val = 0n; for (const b of bytes) val = val << 8n | BigInt(b); if (val === 0n) val = 1n; return val.toString(); } //#endregion //#region src/lib/ws/lib/ws-client.ts const textEncoder = new TextEncoder(); /** * Creates a WebSocket provider function that can be used to create WebcastWebSocketClient instances with dynamic parameters. * * @param webcastWebSocketConfig The default WebSocket configuration to use for all clients created by this provider. Dynamic parameters will override these defaults. * @param wsClientOptions The WebSocket client options to use for all clients created by this provider. These options will be merged with any dynamic parameters provided when creating a client. * */ function createWebSocketProvider(webcastWebSocketConfig, wsClientOptions) { return (dynamicParams) => { return new WebcastWebSocketClient({ ...webcastWebSocketConfig, ...dynamicParams }, wsClientOptions); }; } var WebcastWebSocketClient = class extends WebSocket { pingInterval; seqId = 1; enterUniqueId = null; roomId; webcastWsConfig; constructor(webcastWebSocketConfig, wsClientOptions) { const { headers: wsHeaders, ...reducedWsClientOptions } = wsClientOptions || {}; const cleanWebcastConfig = structuredClone(webcastWebSocketConfig); cleanWebcastConfig.DEFAULT_WS_CLIENT_PARAMS = { ...cleanWebcastConfig.DEFAULT_WS_CLIENT_PARAMS, ...webcastWebSocketConfig.wsParams }; cleanWebcastConfig.DEFAULT_WS_CLIENT_HEADERS = { ...cleanWebcastConfig.DEFAULT_WS_CLIENT_HEADERS, ...wsHeaders, ...webcastWebSocketConfig.wsHeaders }; const webSocketUrl = webcastWebSocketConfig.baseUrl + `?${new URLSearchParams(cleanWebcastConfig.DEFAULT_WS_CLIENT_PARAMS)}${cleanWebcastConfig.DEFAULT_WS_CLIENT_PARAMS_APPEND_PARAMETER}`; super(webSocketUrl, { headers: cleanWebcastConfig.DEFAULT_WS_CLIENT_HEADERS, host: cleanWebcastConfig.TIKTOK_HOST_WS, ...reducedWsClientOptions, autoPong: false }); this.webcastWsConfig = cleanWebcastConfig; this.pingInterval = null; this.on("message", this.onMessage.bind(this)); this.on("close", this.onDisconnect.bind(this)); } get open() { return this.readyState === WebSocket.OPEN; } /** * Send a message to the WebSocket server * @param data The message to send * @returns True if the message was sent, false otherwise */ sendBytes(data) { if (this.open) { super.send(Buffer.from(data)); return true; } return false; } onDisconnect() { if (this.pingInterval) clearInterval(this.pingInterval); this.pingInterval = null; this.seqId = 1; } /** * Handle incoming messages * @param message The incoming WebSocket message (type => Buffer) * @protected */ async onMessage(message) { this.emit("webSocketData", message); try { const decodedContainer = await deserializeWebSocketMessage(message); if (decodedContainer.protoMessageFetchResult) { if (decodedContainer.protoMessageFetchResult.needAck) this.sendAck(decodedContainer); this.emit("protoMessageFetchResult", decodedContainer.protoMessageFetchResult); } if (decodedContainer.payloadType === "im_enter_room_resp") this.emit("imEnteredRoom", decodedContainer); } catch (err) { this.emit("messageDecodingFailed", err); } } /** * Static Keep-Alive ping */ sendHeartbeat() { const hb = HeartBeatMessage.encode({ roomId: this.webcastWsConfig.roomId, sendPacketSeqId: String(this.seqId) }); const webcastPushFrame = createBaseWebcastPushFrame({ payloadEncoding: "pb", payloadType: "hb", payload: Buffer.from(hb.finish()), service: void 0, method: void 0, headers: [] }); this.sendBytes(Buffer.from(webcastPushFrame.finish())); this.seqId++; } /** * Switch to a different TikTok LIVE room while connected to the WebSocket * * @param roomId The room ID to switch to */ switchRooms(roomId) { this.seqId = 1; this.enterUniqueId = generateUniqId(); const imEnterRoomMessage = WebcastImEnterRoomMessage.encode({ roomId, roomTag: "", liveRegion: "", liveId: "12", identity: "audience", cursor: "", accountType: "0", enterUniqueId: this.enterUniqueId, filterWelcomeMsg: "0", isAnchorContinueKeepMsg: false }); const webcastPushFrame = createBaseWebcastPushFrame({ payloadEncoding: "pb", payloadType: "im_enter_room", payload: Buffer.from(imEnterRoomMessage.finish()) }); this.sendBytes(Buffer.from(webcastPushFrame.finish())); if (this.pingInterval) clearInterval(this.pingInterval); this.pingInterval = setInterval(() => this.sendHeartbeat(), this.webcastWsConfig.DEFAULT_WS_PING_INTERVAL); } /** * Acknowledge the message was received */ sendAck({ logId, protoMessageFetchResult }) { if (!logId) return; if (!protoMessageFetchResult?.internalExt) { if (!process.env.DISABLE_ACK_LOG_WARNING) console.error("No internalExt found for message that needs ACK, skipping ACK"); return; } const webcastPushFrame = createBaseWebcastPushFrame({ logId, payloadEncoding: "pb", payloadType: "ack", payload: Buffer.from(textEncoder.encode(protoMessageFetchResult.internalExt)) }); this.sendBytes(Buffer.from(webcastPushFrame.finish())); } }; //#endregion //#region src/types/events.ts let ControlEvent = /* @__PURE__ */ function(ControlEvent) { ControlEvent["CONNECTED"] = "connected"; ControlEvent["DISCONNECTED"] = "disconnected"; ControlEvent["ERROR"] = "error"; ControlEvent["RAW_DATA"] = "rawData"; ControlEvent["DECODED_DATA"] = "decodedData"; ControlEvent["WEBSOCKET_CONNECTED"] = "websocketConnected"; ControlEvent["WEBSOCKET_DATA"] = "websocketData"; ControlEvent["ENTER_ROOM"] = "enterRoom"; return ControlEvent; }({}); let WebcastEvent = /* @__PURE__ */ function(WebcastEvent) { WebcastEvent["CHAT"] = "chat"; WebcastEvent["MEMBER"] = "member"; WebcastEvent["GIFT"] = "gift"; WebcastEvent["ROOM_USER"] = "roomUser"; WebcastEvent["SOCIAL"] = "social"; WebcastEvent["LIKE"] = "like"; WebcastEvent["QUESTION_NEW"] = "questionNew"; WebcastEvent["LINK_MIC_BATTLE"] = "linkMicBattle"; WebcastEvent["LINK_MIC_ARMIES"] = "linkMicArmies"; WebcastEvent["LIVE_INTRO"] = "liveIntro"; WebcastEvent["EMOTE"] = "emote"; WebcastEvent["ENVELOPE"] = "envelope"; WebcastEvent["FOLLOW"] = "follow"; WebcastEvent["SHARE"] = "share"; WebcastEvent["STREAM_END"] = "streamEnd"; WebcastEvent["CONTROL_MESSAGE"] = "controlMessage"; WebcastEvent["BARRAGE"] = "barrage"; WebcastEvent["SYSTEM"] = "system"; WebcastEvent["HOURLY_RANK"] = "hourlyRank"; WebcastEvent["GOAL_UPDATE"] = "goalUpdate"; WebcastEvent["ROOM_MESSAGE"] = "roomMessage"; WebcastEvent["CAPTION_MESSAGE"] = "captionMessage"; WebcastEvent["IM_DELETE"] = "imDelete"; WebcastEvent["IN_ROOM_BANNER"] = "inRoomBanner"; WebcastEvent["RANK_UPDATE"] = "rankUpdate"; WebcastEvent["POLL_MESSAGE"] = "pollMessage"; WebcastEvent["RANK_TEXT"] = "rankText"; WebcastEvent["LINK_MIC_BATTLE_PUNISH_FINISH"] = "linkMicBattlePunishFinish"; WebcastEvent["LINK_MIC_BATTLE_TASK"] = "linkMicBattleTask"; WebcastEvent["LINK_MIC_FAN_TICKET_METHOD"] = "linkMicFanTicketMethod"; WebcastEvent["LINK_MIC_METHOD"] = "linkMicMethod"; WebcastEvent["UNAUTHORIZED_MEMBER"] = "unauthorizedMember"; WebcastEvent["OEC_LIVE_SHOPPING"] = "oecLiveShopping"; WebcastEvent["MSG_DETECT"] = "msgDetect"; WebcastEvent["LINK_MESSAGE"] = "linkMessage"; WebcastEvent["ROOM_VERIFY"] = "roomVerify"; WebcastEvent["LINK_LAYER"] = "linkLayer"; WebcastEvent["ROOM_PIN"] = "roomPin"; WebcastEvent["SUPER_FAN"] = "superFan"; WebcastEvent["SUPER_FAN_JOIN"] = "superFanJoin"; WebcastEvent["SUPER_FAN_BOX"] = "superFanBox"; WebcastEvent["ACCESS_CONTROL"] = "accessControl"; WebcastEvent["ACCESS_RECALL"] = "accessRecall"; WebcastEvent["BOOST_CARD"] = "boostCard"; WebcastEvent["BOTTOM_MESSAGE"] = "bottomMessage"; WebcastEvent["CAPSULE"] = "capsule"; WebcastEvent["GAME_RANK_NOTIFY"] = "gameRankNotify"; WebcastEvent["GIFT_BROADCAST"] = "giftBroadcast"; WebcastEvent["GIFT_DYNAMIC_RESTRICTION"] = "giftDynamicRestriction"; WebcastEvent["GIFT_PANEL_UPDATE"] = "giftPanelUpdate"; WebcastEvent["GIFT_PROMPT"] = "giftPrompt"; WebcastEvent["GUIDE"] = "guide"; WebcastEvent["LINK_MIC_LAYOUT_STATE"] = "linkMicLayoutState"; WebcastEvent["LINK_STATE"] = "linkState"; WebcastEvent["LIVE_GAME_INTRO"] = "liveGameIntro"; WebcastEvent["MARQUEE_ANNOUNCEMENT"] = "marqueeAnnouncement"; WebcastEvent["NOTICE"] = "notice"; WebcastEvent["PARTNERSHIP_DROPS_UPDATE"] = "partnershipDropsUpdate"; WebcastEvent["PARTNERSHIP_GAME_OFFLINE"] = "partnershipGameOffline"; WebcastEvent["PARTNERSHIP_PUNISH"] = "partnershipPunish"; WebcastEvent["PERCEPTION"] = "perception"; WebcastEvent["ROOM_NOTIFY"] = "roomNotify"; WebcastEvent["SPEAKER"] = "speaker"; WebcastEvent["SUB_NOTIFY"] = "subNotify"; WebcastEvent["SUB_PIN_EVENT"] = "subPinEvent"; WebcastEvent["TOAST"] = "toast"; WebcastEvent["VIEWER_PICKS_UPDATE"] = "viewerPicksUpdate"; return WebcastEvent; }({}); let ConnectState = /* @__PURE__ */ function(ConnectState) { ConnectState["DISCONNECTED"] = "DISCONNECTED"; ConnectState["CONNECTING"] = "CONNECTING"; ConnectState["CONNECTED"] = "CONNECTED"; return ConnectState; }({}); const WebcastEventMap = { "WebcastChatMessage": "chat", "WebcastMemberMessage": "member", "WebcastRoomUserSeqMessage": "roomUser", "WebcastSocialMessage": "social", "WebcastLikeMessage": "like", "WebcastQuestionNewMessage": "questionNew", "WebcastLinkMicBattle": "linkMicBattle", "WebcastLinkMicArmies": "linkMicArmies", "WebcastLiveIntroMessage": "liveIntro", "WebcastEmoteChatMessage": "emote", "WebcastEnvelopeMessage": "envelope", "WebcastBarrageMessage": "barrage", "WebcastSystemMessage": "system", "WebcastGoalUpdateMessage": "goalUpdate", "WebcastRoomMessage": "roomMessage", "WebcastCaptionMessage": "captionMessage", "WebcastControlMessage": "controlMessage", "WebcastImDeleteMessage": "imDelete", "WebcastInRoomBannerMessage": "inRoomBanner", "WebcastRankUpdateMessage": "rankUpdate", "WebcastPollMessage": "pollMessage", "WebcastRankTextMessage": "rankText", "WebcastLinkMicBattlePunishFinish": "linkMicBattlePunishFinish", "WebcastLinkmicBattleTaskMessage": "linkMicBattleTask", "WebcastLinkMicFanTicketMethod": "linkMicFanTicketMethod", "WebcastLinkMicMethod": "linkMicMethod", "WebcastUnauthorizedMemberMessage": "unauthorizedMember", "WebcastOecLiveShoppingMessage": "oecLiveShopping", "WebcastMsgDetectMessage": "msgDetect", "WebcastLinkMessage": "linkMessage", "WebcastLinkLayerMessage": "linkLayer", "WebcastRoomPinMessage": "roomPin", "WebcastAccessControlMessage": "accessControl", "WebcastAccessRecallMessage": "accessRecall", "WebcastBoostCardMessage": "boostCard", "WebcastBottomMessage": "bottomMessage", "WebcastCapsuleMessage": "capsule", "WebcastGameRankNotifyMessage": "gameRankNotify", "WebcastGiftBroadcastMessage": "giftBroadcast", "WebcastGiftDynamicRestrictionMessage": "giftDynamicRestriction", "WebcastGiftPanelUpdateMessage": "giftPanelUpdate", "WebcastGiftPromptMessage": "giftPrompt", "WebcastGuideMessage": "guide", "WebcastHourlyRankRewardMessage": "hourlyRank", "WebcastLinkMicLayoutStateMessage": "linkMicLayoutState", "WebcastLinkStateMessage": "linkState", "WebcastLiveGameIntroMessage": "liveGameIntro", "WebcastMarqueeAnnouncementMessage": "marqueeAnnouncement", "WebcastNoticeMessage": "notice", "WebcastPartnershipDropsUpdateMessage": "partnershipDropsUpdate", "WebcastPartnershipGameOfflineMessage": "partnershipGameOffline", "WebcastPartnershipPunishMessage": "partnershipPunish", "WebcastPerceptionMessage": "perception", "WebcastRoomNotifyMessage": "roomNotify", "WebcastRoomVerifyMessage": "roomVerify", "WebcastSpeakerMessage": "speaker", "WebcastSubNotifyMessage": "subNotify", "WebcastSubPinEventMessage": "subPinEvent", "WebcastToastMessage": "toast", "WebcastViewerPicksUpdateMessage": "viewerPicksUpdate" }; //#endregion //#region src/lib/web/lib/cookie-jar.ts /** * Custom cookie jar for got * * Exposes `beforeRequest` / `afterResponse` hooks that the HTTP client wires into `got.extend(...)`, * mirroring the axios interceptor model from before the migration. * * https://github.com/zerodytrash/TikTok-Livestream-Chat-Connector/issues/18 */ var WebcastCookieJar = class WebcastCookieJar { /** * The internal cookie store, a simple key-value object. The keys are cookie names, the values are cookie values. */ store; /** * Constructor * * @param webConfig The current web config for the HTTP client */ constructor(webConfig) { this.webConfig = webConfig; this.store = WebcastCookieJar.parseCookies(webConfig.DEFAULT_HTTP_CLIENT_HEADERS.Cookie); } /** * Set the session ID and tt-target-idc * * @param session The session bundle containing the session ID and tt-target-idc */ async setSessionBundle(session) { if (session.type !== "cookie") throw new TypeError("Invalid session bundle type. Expected 'cookie'."); this.webConfig.SESSION_COOKIE_NAMES.forEach((cookie) => this.store[cookie] = session.value.sessionId); this.store[this.webConfig.TARGET_IDC_COOKIE_NAME] = session.value.ttTargetIdc; } /** * Get the session bundle containing the session ID and tt-target-idc * */ async getSessionBundle() { let ttTargetIdc = this.store[this.webConfig.TARGET_IDC_COOKIE_NAME] || null; let sessionId = null; for (const cookieName of this.webConfig.SESSION_COOKIE_NAMES) if (this.store[cookieName]) { sessionId = this.store[cookieName]; break; } if (ttTargetIdc && sessionId) return { type: "cookie", value: { ttTargetIdc, sessionId } }; return null; } /** * Process a single set-cookie header * * @param setCookieHeader The set-cookie header */ async processSetCookieHeader(setCookieHeader) { if (typeof setCookieHeader === "string") { const parts = setCookieHeader.split(";")[0].split("="); const cookieName = parts.shift(); const cookieValue = parts.join("="); if (typeof cookieName === "string" && cookieName !== "") this.store[decodeURIComponent(cookieName)] = decodeURIComponent(cookieValue); } } /** * We ignore the URL parameter because TikTok's cookies are not scoped by path or domain - any cookie we set should be sent with every request to TikTok regardless of URL * * @param __url Ignored */ async getCookieString(__url = "") { return WebcastCookieJar.serializeCookieObject(this.store); } /** * Set cookie string - we ignore the URL parameter for the same reason as in getCookieString * @param rawCookie The raw cookie string from the set-cookie header * @param __url The URL the cookie is associated with (ignored) */ async setCookie(rawCookie, __url = "") { if (!rawCookie) return; await this.processSetCookieHeader(rawCookie); } /** * Serialize a cookie object into a cookie header string. For example, { sessionid: 'abc', tt-target-idc: 'def' } becomes 'sessionid=abc; tt-target-idc=def' * * @param cookies The cookie object to serialize */ static serializeCookieObject(cookies) { return Object.entries(cookies).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("; "); } /** * Serialize a CookieSessionBundle into a Record of cookie name to cookie value, based on the provided web config. This is used to set the initial cookies in the cookie jar when creating a new session. * * @param config The web config containing the cookie names to use for the session ID and tt-target-idc * @param session The session bundle containing the session ID and tt-target-idc to serialize * */ static serializeCookieSessionBundle(config, session) { const cookies = {}; config.SESSION_COOKIE_NAMES.forEach((cookieName) => { cookies[cookieName] = session.value.sessionId; }); cookies[config.TARGET_IDC_COOKIE_NAME] = session.value.ttTargetIdc; return WebcastCookieJar.serializeCookieObject(cookies); } /** * Parse a cookie header string into a Record of cookie name to cookie value. For example, 'sessionid=abc; tt-target-idc=def' becomes { sessionid: 'abc', tt-target-idc: 'def' } * * @param str */ static parseCookies(str) { const cookies = {}; if (!str) return cookies; str.split("; ").forEach((v) => { if (!v) return; const parts = String(v).split("="); const cookieName = decodeURIComponent(parts.shift() || ""); cookies[cookieName] = parts.join("="); }); return cookies; } }; //#endregion //#region src/version.ts const VERSION = "2.4.0"; //#endregion //#region src/lib/web/routes/euler/config.ts /** * Should never be changed, used to identify the library in requests to TikTok's APIs */ const LIBRARY_IDENTITY = "ttlive-node"; const SignConfig = { basePath: process.env.SIGN_API_URL || "https://api.eulerstream.com", apiKey: process.env.SIGN_API_KEY, baseOptions: { headers: { "User-Agent": `tiktok-live-connector/${VERSION} ${process.platform}` }, validateStatus: () => true }, cachedInstance: void 0 }; /** * Creates (or retrieves from cache) an instance of the Euler Stream API client configured with SignConfig. * * Should be a global singleton to avoid unnecessary re-instantiations and to allow for dynamic config changes (e.g. API key rotation). * */ function createEulerClient() { const { cachedInstance, ...clientConfig } = { ...SignConfig }; if (cachedInstance) return cachedInstance; SignConfig.cachedInstance = new EulerStreamApiClient(clientConfig); return SignConfig.cachedInstance; } //#endregion //#region src/lib/web/lib/route-wrapper.ts function createRoute(routeId, routeHandler) { return (args) => routeHandler({ ...args, routeId }); } //#endregion //#region src/lib/web/routes/routes.ts let BaseFetchRoute = /* @__PURE__ */ function(BaseFetchRoute) { BaseFetchRoute["FETCH_ROOM_GIFTS"] = "fetchRoomGiftsRoute"; BaseFetchRoute["FETCH_ROOM_INFO"] = "fetchRoomInfoRoute"; BaseFetchRoute["FETCH_ROOM_INFO_API_LIVE"] = "fetchRoomInfoApiLiveRoute"; BaseFetchRoute["FETCH_ROOM_INFO_HTML"] = "fetchRoomInfoHtmlRoute"; return BaseFetchRoute; }({}); let CompositeFetchRoute = /* @__PURE__ */ function(CompositeFetchRoute) { CompositeFetchRoute["FETCH_IS_LIVE"] = "fetchIsLiveRoute"; CompositeFetchRoute["FETCH_ROOM_ID"] = "fetchRoomIdRoute"; return CompositeFetchRoute; }({}); let EulerFetchRoute = /* @__PURE__ */ function(EulerFetchRoute) { EulerFetchRoute["FETCH_ROOM_GIFTS"] = "fetchRoomGiftsFromEulerRoute"; EulerFetchRoute["FETCH_ROOM_GIFT_GALLERY"] = "fetchRoomGiftGalleryFromEulerRoute"; EulerFetchRoute["FETCH_ROOM_ID"] = "fetchRoomIdFromEulerRoute"; EulerFetchRoute["FETCH_ROOM_INFO"] = "fetchRoomInfoFromEulerRoute"; EulerFetchRoute["FETCH_SIGNED_WEBSOCKET"] = "fetchSignedWebSocketFromEulerRoute"; EulerFetchRoute["FETCH_WEBCAST_SIGNATURE"] = "fetchWebcastSignatureFromEulerRoute"; EulerFetchRoute["SEND_ROOM_CHAT"] = "sendRoomChatFromEulerRoute"; return EulerFetchRoute; }({}); //#endregion //#region src/lib/web/routes/euler/fetch-room-gifts-euler.ts /** * Fetches the list of gifts available in a TikTok LIVE room from Euler Stream's premium endpoint. If * `roomId` is not provided, the `roomId` currently set on the `webClient` is used. * * @param apiClient The Euler Stream API client to use for the request. * @param webClient The HTTP client instance, used as the source of `roomId` when one is not provided. * @param roomId The ID of the room whose gift list to fetch. Optional if the webClient has a roomId context. * @param webcastLanguage Optional language used to localize the returned gift names and descriptions. */ const fetchRoomGiftsFromEulerRoute = createRoute("fetchRoomGiftsFromEulerRoute", async ({ routeId, apiClient, webClient, webcastLanguage, roomId = webClient.roomId, options }) => { if (!roomId) throw new InvalidRequestError({ routeId }, "Missing roomId. Please provide a roomId to the HTTP client."); return (await apiClient.rooms.retrieveRoomGifts(roomId, webcastLanguage, options)).data; }); //#endregion //#region src/lib/web/routes/euler/fetch-room-gift-gallery-euler.ts /** * Fetches the gift gallery for a TikTok LIVE creator from Euler Stream's premium endpoint. The gallery * describes the creator's gift-collection progress, sponsor info, and anchor ranking league. The request * is authenticated with the `webClient` session — cookie-based when a cookie string is available, * otherwise the OAuth token from the session bundle. * * @param apiClient The Euler Stream API client to use for the request. * @param webClient The HTTP client instance, used as the source of the session cookie / OAuth token. * @param uniqueId The unique identifier (username) of the TikTok creator whose gift gallery is being fetched. */ const fetchRoomGiftGalleryFromEulerRoute = createRoute("fetchRoomGiftGalleryFromEulerRoute", async ({ uniqueId, apiClient, webClient, options }) => { const xCookieHeader = await webClient.cookieJar.getCookieString() || void 0; const xOauthToken = xCookieHeader ? void 0 : webClient.oAuthSessionBundle?.value || void 0; return (await apiClient.anchors.retrieveWebcastGiftGallery(uniqueId, xOauthToken, xCookieHeader, options)).data; }); //#endregion //#region src/lib/web/routes/euler/fetch-room-id-euler.ts /** * Fetches a room ID from Euler Stream using the provided uniqueId. * * @param apiClient The Euler Stream API client to use for the request. * @param uniqueId The unique identifier (username) of the TikTok user whose room ID is being fetched. */ const fetchRoomIdFromEulerRoute = createRoute("fetchRoomIdFromEulerRoute", async ({ apiClient, uniqueId, options }) => { return (await apiClient.anchors.retrieveRoomId(uniqueId, options)).data; }); //#endregion //#region src/lib/web/routes/euler/fetch-room-info-euler.ts /** * Fetches full room information (streamer info, stats, status) from Euler Stream's premium endpoint. * * @param apiClient The Euler Stream API client to use for the request. * @param uniqueId The unique identifier (username) of the TikTok user whose room info is being fetched. */ const fetchRoomInfoFromEulerRoute = createRoute("fetchRoomInfoFromEulerRoute", async ({ apiClient, uniqueId, options }) => { return (await apiClient.anchors.retrieveRoomInfo(uniqueId, options)).data; }); //#endregion //#region src/lib/web/routes/euler/fetch-signed-websocket-euler.ts /** * Fetches a signed TikTok WebSocket URL from Euler Stream along with the initial `ProtoMessageFetchResult` * and any cookies the sign server set. * * Enforces whitelist validation (via `WHITELIST_AUTHENTICATED_SESSION_ID_HOST`) when authenticated WebSocket * connections are requested. Translates sign-server HTTP responses into typed errors * (`SignAPIError`, `SignatureRateLimitError`, `PremiumFeatureError`). */ const fetchSignedWebSocketFromEulerRoute = createRoute("fetchSignedWebSocketFromEulerRoute", async ({ routeId, roomId, apiClient, webClient, useMobile, session, authenticateWs, country, cursor, options }) => { if (session && authenticateWs) { const envHost = process.env.WHITELIST_AUTHENTICATED_SESSION_ID_HOST; const expectedHost = apiClient.configuration.basePath ? new URL(apiClient.configuration.basePath).host : void 0; if (!envHost) throw new AuthenticatedWebSocketConnectionError(`authenticate_websocket is true, but no whitelist host defined. Set the env var WHITELIST_AUTHENTICATED_SESSION_ID_HOST to proceed.`); if (envHost !== expectedHost) throw new AuthenticatedWebSocketConnectionError(`The env var WHITELIST_AUTHENTICATED_SESSION_ID_HOST "${envHost}" does not match sign server host "${expectedHost}".`); } const xCookieHeader = await webClient.cookieJar.getCookieString() || void 0; if (authenticateWs && !xCookieHeader) throw new AuthenticatedWebSocketConnectionError(`authenticate_websocket is true, but no session cookies found.`); let response; try { response = await apiClient.rooms.fetchWebcastURL(roomId, LIBRARY_IDENTITY, void 0, cursor, webClient.clientHeaders["User-Agent"], true, country, useMobile ? WebcastFetchPlatform.Mobile : WebcastFetchPlatform.Web, void 0, authenticateWs ? xCookieHeader : void 0, void 0, void 0, { ...options, responseType: "arraybuffer" }); } catch (err) { throw new SignAPIError("Connect Error", void 0, void 0, "Failed to connect to sign server.", err); } if (response.status === 429) { const data = JSON.parse(Buffer.from(response.data).toString("utf-8")); throw new SignatureRateLimitError(process.env.SIGN_SERVER_MESSAGE_DISABLED ? null : data?.message, `${data?.limit_label ? `(${data.limit_label}) ` : ""}Too many connections started, try again later.`, response.data); } if (response.status === 402) { const data = JSON.parse(Buffer.from(response.data).toString("utf-8")); throw new PremiumFeatureError(process.env.SIGN_SERVER_MESSAGE_DISABLED ? null : data?.message, "Error fetching the signed TikTok WebSocket"); } const logId = response.headers["x-request-id"]; const agentId = response.headers["x-agent-id"]; if (response.status !== 200) { let payload; try { payload = Buffer.from(response.data).toString("utf-8"); } catch { payload = `"${response.statusText}"`; } throw new SignAPIError("Sign Error", logId, agentId, `[${routeId}] Unexpected sign server status ${response.status}. Payload:\n${payload}`); } if (!response.headers["x-set-tt-cookie"]) throw new SignAPIError("Empty Cookies", logId, agentId, `[${routeId}] No cookies received from sign server.`); return { fetchResult: deserializeMessage("ProtoMessageFetchResult", Buffer.from(response.data)), fetchResultCookieHeader: response.headers["x-set-tt-cookie"], fetchResultRoomId: response.headers["x-room-id"] }; }); //#endregion //#region src/lib/web/routes/euler/fetch-webcast-signature-euler.ts /** * Signs an arbitrary TikTok webcast URL through the Euler Stream sign server, returning the * signed URL and token bundle the caller needs to make the request. * * Strips known signature parameters (X-Bogus, X-Gnarly, msToken) from the input URL before signing. * If `webClient.cookieJar` already holds a `sessionid` and `tt-target-idc` pair, both are forwarded * to the sign server so the resulting signature is bound to that session. * * @param apiClient The Euler Stream API client used to issue the sign request. * @param webClient The HTTP client whose cookie jar provides any optional session bundle. * @param url The original TikTok URL (string or `URL`) to sign. * @param method HTTP method the signed URL will be used with. * @param userAgent User-Agent string the signed request will use. * @param options Optional axios request overrides forwarded to the SDK call. */ const fetchWebcastSignatureFromEulerRoute = createRoute("fetchWebcastSignatureFromEulerRoute", async ({ routeId, url, method, userAgent, apiClient, webClient, options }) => { const session = await webClient.cookieJar.getSessionBundle(); const sessionId = session?.value.sessionId; const ttTargetIdc = session?.value.ttTargetIdc; const mustRemoveParams = [ "X-Bogus", "X-Gnarly", "msToken" ]; let cleanUrl = typeof url === "string" ? url : url.toString(); for (const param of mustRemoveParams) { cleanUrl = cleanUrl.replace(new RegExp(`([&?])${param}=[^&]*`, "g"), "$1"); cleanUrl = cleanUrl.replace(/[&?]$/, ""); } if (sessionId && !ttTargetIdc) throw new InvalidRequestError({ routeId }, "ttTargetIdc must be set when sessionId is provided."); const response = await apiClient.general.signTikTokUrl({ url: cleanUrl, method, userAgent, sessionId, ttTargetIdc }, LIBRARY_IDENTITY, options); if (response.status === 403) throw new PremiumFeatureError("You do not have permission from the signature provider to sign this URL.", response.data.message || "Forbidden", JSON.stringify(response.data)); if (response.status !== 200) throw new SignatureMissingTokensError(`[${routeId}] Failed to sign a request: ${response?.data?.message || "Unknown error"}`); if (!response.data || Object.keys(response.data?.response?.tokens || {}).length < 1) throw new SignatureMissingTokensError(`[${routeId}] Failed to sign a request due to missing tokens in response!`); return response.data; }); //#endregion //#region src/lib/web/routes/euler/send-room-chat-euler.ts /** * Sends a chat message into a TikTok LIVE room via Euler Stream's premium endpoint. Supports either * cookie-based (`sessionId` + `ttTargetIdc`) or OAuth-token session bundles; OAuth takes precedence when present. * * Throws `PremiumFeatureError` when the sign server returns 401/403, and `InvalidResponseError` for * any other non-200 response. */ const sendRoomChatFromEulerRoute = createRoute("sendRoomChatFromEulerRoute", async ({ routeId, webClient, apiClient, roomId, content, options }) => { roomId ||= webClient.roomId; if (!roomId) throw new InvalidRequestError({ routeId }, "Room ID must be specified in all cases to send a chat to a room"); const xCookieHeader = await webClient.cookieJar.getCookieString() || void 0; const xOauthToken = xCookieHeader ? void 0 : webClient.oAuthSessionBundle?.value || void 0; const fetchResponse = await apiClient.rooms.sendRoomChat(roomId, { content }, xOauthToken, xCookieHeader, options); switch (fetchResponse.status) { case 401: case 403: throw new PremiumFeatureError("Sending chats requires an API key & a paid plan, as it uses cloud managed services.", fetchResponse.data.message || "Unauthorized", JSON.stringify(fetchResponse.data)); case 200: return fetchResponse.data; default: throw new InvalidResponseError({ routeId }, `Failed to send chat: ${fetchResponse?.data?.message || "Unknown error"}`); } }); //#endregion //#region src/lib/web/routes/base/fetch-room-gifts.ts /** * Fetches the list of gifts available in a TikTok LIVE room directly from TikTok's webcast `gift/list/` * endpoint. The request must be signed for TikTok to return data. If `roomId` is not provided, the * `roomId` currently set on the `webClient` is used. * * @param webClient The HTTP client instance to use for the request. * @param roomId The ID of the room whose gift list to fetch. Optional if the webClient has a roomId context. */ const fetchRoomGiftsRoute = createRoute("fetchRoomGiftsRoute", async ({ routeId, webClient, roomId = webClient.roomId }) => { if (!roomId) throw new InvalidRequestError({ routeId }, "Missing roomId. Please provide a roomId to the HTTP client."); return (await webClient.getJsonObjectFromWebcastApi("gift/list/", { ...webClient.clientParams, room_id: roomId }, true)).data.gifts; }); //#endregion //#region src/lib/web/routes/base/fetch-room-info.ts /** * Fetches room information for a given roomId. If roomId is not provided, it will attempt to use the roomId from the webClient context. * * @param webClient The HTTP client instance to use for the request. * @param roomId The ID of the room to fetch information for. Optional if the webClient has a roomId context. */ const fetchRoomInfoRoute = createRoute("fetchRoomInfoRoute", async ({ routeId, webClient, roomId = webClient.roomId }) => { if (!roomId) throw new InvalidRequestError({ routeId }, "Missing roomId. Please provide a roomId to the HTTP client."); return await webClient.getJsonObjectFromWebcastApi("room/info/", { ...webClient.clientParams, room_id: roomId }, false); }); //#endregion //#region src/lib/web/routes/base/fetch-room-info-api-live.ts /** * Fetches room information from the TikTok API Live endpoint using the provided uniqueId. This route is used as part of the process to determine if a user is currently live streaming and to retrieve associated room information. * * @param webClient The HTTP client instance to use for making the API request. * @param uniqueId The unique identifier (username) of the TikTok user whose room information is being fetched. */ const fetchRoomInfoFromApiLiveRoute = createRoute("fetchRoomInfoApiLiveRoute", async ({ webClient, uniqueId, routeId }) => { const roomData = await webClient.getJsonObjectFromTikTokApi("api-live/user/room/", { ...webClient.clientParams, uniqueId, sourceType: "54" }); if (roomData.statusCode) throw new InvalidResponseError({ routeId }, `API Error ${roomData.statusCode} (${roomData.message || "Unknown Error"})`); if (!roomData?.data?.user?.roomId) throw new InvalidResponseError({ routeId }, `Invalid response from API: ${JSON.stringify(roomData)}`); return roomData; }); //#endregion //#region src/lib/web/routes/base/fetch-room-info-html.ts /** * Mutable configuration for `fetchRoomInfoFromHtmlRoute`. Override `extractionPattern` * if TikTok ever changes the markup wrapping the SIGI_STATE JSON blob. */ const RoomInfoFromHtmlRouteConfig = { extractionPattern: /<script id="SIGI_STATE" type="application\/json">(.*?)<\/script>/ }; /** * Fetches room information by scraping the HTML content of the TikTok user's live page. * This route is used as a fallback method to retrieve room information when API endpoints are unavailable or blocked. It extracts the SIGI_STATE JSON data embedded in the HTML, which contains various details about the live room and user information. * * @param webClient The HTTP client instance to use for making the request to the TikTok website. * @param uniqueId The unique identifier (username) of the TikTok user whose room information is being fetched. */ const fetchRoomInfoFromHtmlRoute = createRoute("fetchRoomInfoHtmlRoute", async ({ webClient, uniqueId, routeId }) => { const match = (await webClient.getHtmlFromTikTokWebsite(`@${uniqueId}/live`)).match(RoomInfoFromHtmlRouteConfig.extractionPattern); if (!match || match.length < 2) throw new InvalidResponseError({ routeId }, "Failed to extract the SIGI_STATE HTML tag, you might be blocked by TikTok."); let sigiState; try { sigiState = JSON.parse(match[1]); } catch (e) { throw new InvalidResponseError({ routeId }, "Failed to parse SIGI_STATE into JSON. Are you captcha-blocked by TikTok?"); } const liveRoom = sigiState?.LiveRoom?.liveRoomUserInfo; if (!liveRoom) throw new InvalidResponseError({ routeId }, "Failed to extract the LiveRoom object from SIGI_STATE."); return liveRoom; }); //#endregion //#region src/lib/web/routes/composite/fetch-room-id.ts const RoomIdRouteConfig = { skipFetchRoomInfoFromHtmlRoute: false, skipFetchRoomInfoFromApiLiveRoute: false, skipFetchRoomIdFromEulerRoute: false }; /** * Resolves a room ID across multiple sources, falling back progressively: * 1. HTML scrape (SIGI_STATE) * 2. TikTok API Live endpoint * 3. Euler Stream (skipped if `RoomIdRouteConfig.skipFetchRoomIdFromEulerRoute` is true) * * Errors from each source are accumulated and surfaced via `InvalidResponseCompositeError` if every source fails. */ const fetchRoomIdComposite = createRoute("fetchRoomIdRoute", async ({ routeId, uniqueId, webClient, apiClient }) => { const errors = []; if (!RoomIdRouteConfig.skipFetchRoomInfoFromHtmlRoute) try { const roomId = (await fetchRoomInfoFromHtmlRoute({ webClient, uniqueId })).user.roomId; if (!roomId) throw new InvalidResponseError({ routeId }, "Failed to extract Room ID from HTML."); return roomId; } catch (ex) { errors.push(ex); } if (!RoomIdRouteConfig.skipFetchRoomInfoFromApiLiveRoute) try { const roomId = (await fetchRoomInfoFromApiLiveRoute({ webClient, uniqueId }))?.data?.user?.roomId; if (!roomId) throw new InvalidResponseError({ routeId }, "Failed to extract Room ID from API."); return roomId; } catch (ex) { errors.push(ex); } if (!RoomIdRouteConfig.skipFetchRoomIdFromEulerRoute) try { const response = await fetchRoomIdFromEulerRoute({ webClient, apiClient, uniqueId }); if ([ 403, 402, 401 ].includes(response.code)) throw new InvalidResponseError({ routeId }, "Failed to retrieve Room ID from Euler Stream, which was made as a last resort due to the previous methods failing. This happened due to a >>lack of permission<< for you to use Euler Stream's (https://www.eulerstream.com) fallback method. If you do not want to use Euler Stream, disable this fallback method by setting `RoomIdRouteConfig.skipFetchRoomIdFromEulerRoute` to `true`."); if (!response.ok) throw new InvalidResponseError({ routeId }, `Failed to retrieve Room ID from Euler due to an error: ${response.message}`); if (!response.room_id) throw new InvalidResponseError({ routeId }, "Failed to extract Room ID from Euler."); return response.room_id; } catch (err) { errors.push(err); } throw new InvalidResponseCompositeError({ routeId, requestErrs: errors }, "Failed to retrieve Room ID from all sources."); }); //#endregion //#region src/lib/web/routes/composite/fetch-is-live.ts const IsLiveRouteConfig = { skipFetchRoomInfoFromHtmlRoute: false, skipFetchRoomInfoFromApiLiveRoute: false, skipFetchRoomIdFromEulerRoute: false }; /** * Determines whether a user is currently live, falling back progressively: * 1. HTML scrape (SIGI_STATE status field) * 2. TikTok API Live endpoint (liveRoom.status) * 3. Euler Stream (is_live flag — skipped if `disableEulerFallback` is true) * * Errors from each source are accumulated and surfaced via `FetchIsLiveError` if every source fails. */ const fetchIsLiveComposit