UNPKG

kichat.js

Version:

A JavaScript library to connect to Kick.com chat.

515 lines (508 loc) 19.1 kB
"use strict"; var kichat = (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // node_modules/ws/browser.js var require_browser = __commonJS({ "node_modules/ws/browser.js"(exports, module) { "use strict"; module.exports = function() { throw new Error( "ws does not work in the browser. Browser clients must use the native WebSocket object" ); }; } }); // src/index.ts var src_exports = {}; __export(src_exports, { KiChannel: () => KiChannel, KiChatjs: () => KiChatjs, default: () => src_default }); // src/lib/KiChannel.ts var KiChannel = class { info; chatroom; connectionNotified = false; constructor(info, chatroom) { this.info = info; this.chatroom = chatroom; } get id() { return this.info.id; } get name() { return this.info.user.username; } get slug() { return this.info.slug; } get chatroomId() { return this.info.chatroom.id; } get notified() { return this.connectionNotified; } set notified(value) { this.connectionNotified = value; } static toLogin(channelName) { const name = channelName.trim().toLowerCase(); return name.startsWith("#") ? name.slice(1) : name; } toString() { return this.name; } }; // src/lib/EventEmitter.ts var EventEmitter = class { listeners = /* @__PURE__ */ new Map(); on(event, listener) { if (!this.listeners.has(event)) { this.listeners.set(event, /* @__PURE__ */ new Set()); } this.listeners.get(event).add(listener); return this; } off(event, listener) { this.listeners.get(event)?.delete(listener); return this; } emit(event, ...args) { if (!this.listeners.has(event)) { if (event === "error") { if (args[0] instanceof Error) { throw args[0]; } else { const uncaughtError = new Error("Uncaught error emitted", { cause: args[0] }); throw uncaughtError; } } return false; } for (const listener of this.listeners.get(event)) { listener(...args); } return true; } }; // src/KiChatjs.ts var parseJSON = (json) => { try { return JSON.parse(json); } catch { return null; } }; var BASE_URL = "wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679"; var KiChatjs = class extends EventEmitter { socket; wasCloseCalled = false; reconnectAttempts = 0; pingInterval; socketSession = { activity_timeout: 120, socket_id: "" }; // Reconnect options reconnectEnabled; reconnectMaxAttempts; reconnectInitialTimeout; reconnectMaxTimeout; channels = /* @__PURE__ */ new Map(); channelsByChatroomId = /* @__PURE__ */ new Map(); subscribePusher; constructor(options = {}) { super(); this.reconnectEnabled = options.reconnect ?? true; this.reconnectMaxAttempts = options?.reconnectMaxAttempts ?? Infinity; this.reconnectInitialTimeout = options?.reconnectInitialTimeout ?? 1e3; this.reconnectMaxTimeout = options?.reconnectMaxTimeout ?? 6e4; this.subscribePusher = options?.subscribePusher ?? { channel: true, chatRoom: true, predictions: true }; if (options.channels) { options.channels.forEach((channel) => this.join(channel)); } } isConnected() { return this.socket?.readyState === 1; } connect() { if (this.isConnected()) { throw new Error("Client is already connected."); } this.wasCloseCalled = false; this.createWebSocket().catch((err) => this.emit("socketError", err)); } async createWebSocket() { const urlParams = new URLSearchParams({ protocol: "7", client: "js", version: "7.4.0", flash: "false" }); const url = `${BASE_URL}?${urlParams.toString()}`; if (typeof window !== "undefined" && typeof window.WebSocket !== "undefined") { this.socket = new window.WebSocket(url); this.socket.onerror = (_err) => { }; this.socket.onopen = () => this.onSocketOpen(); this.socket.onmessage = (event) => this.onSocketMessage(event.data); this.socket.onclose = (event) => this.onSocketClose(event.code, event.reason); this.socket.onerror = () => this.onSocketError(new Error("WebSocket error")); } else { const { default: NodeWebSocket } = await Promise.resolve().then(() => __toESM(require_browser(), 1)); this.socket = new NodeWebSocket(url); this.socket.onerror = (_err) => { }; this.socket.on("open", () => this.onSocketOpen()); this.socket.on("message", (data) => this.onSocketMessage(data)); this.socket.on("close", (code, reason) => this.onSocketClose(code, reason.toString())); this.socket.on("socketError", (error) => this.onSocketError(error)); } } close() { if (this.socket) { this.wasCloseCalled = true; this.socket.close(); } } async reconnect() { if (this.isConnected()) { this.socket.close(); } if (this.reconnectAttempts >= this.reconnectMaxAttempts) { this.emit("socketError", new Error("Maximum reconnect attempts reached.")); return; } this.reconnectAttempts++; const waitTime = Math.min(this.reconnectInitialTimeout * 1.23 ** this.reconnectAttempts, this.reconnectMaxTimeout); this.emit("reconnecting"); await new Promise((resolve) => setTimeout(resolve, waitTime)); this.connect(); } onSocketOpen() { this.reconnectAttempts = 0; this.channels.forEach((channel) => this.subscribeToChannel(channel)); } onSocketClose(code, reason) { clearInterval(this.pingInterval); if (!this.wasCloseCalled && this.reconnectEnabled) { this.reconnect(); } else { this.emit("disconnected", reason || `Socket closed with code ${code}`); } } onSocketError(error) { this.emit("socketError", error); } onSocketMessage(rawData) { const messageStr = rawData.toString(); this.emit("raw", messageStr); const messageEvent = parseJSON(messageStr); if (!messageEvent) return; let channel; if (messageEvent.channel) { const match = messageEvent.channel.match(/(\d{5,})/); if (match) { const roomId = parseInt(match[1], 10); Array.from(this.channelsByChatroomId.entries()).forEach(([_, ch]) => { if (ch.chatroomId == roomId || ch.id == roomId) channel = this.channelsByChatroomId.get(ch.chatroomId); }); } } switch (messageEvent.event) { case "pusher:connection_established": { const data = parseJSON(messageEvent.data); if (data) { this.socketSession = data; this.startPing(); this.emit("connected"); } break; } case "pusher_internal:subscription_succeeded": { if (channel?.notified == false) { this.channels.get(channel.slug).notified = true; this.emit("join", channel); } break; } case "pusher:pong": break; case "pusher:error": { const data = parseJSON(messageEvent.data); if (data?.code === 4200) { this.reconnect(); } break; } case "App\\Events\\ChatMessageEvent": { const data = parseJSON(messageEvent.data); if (data && channel) { this.emit("message", data, channel); } break; } case "App\\Events\\SubscriptionEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("subscription", data, channel); break; } case "GiftedSubscriptionsEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("giftedSubscriptions", data, channel); break; } case "App\\Events\\StreamHostEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("streamHost", data, channel); break; } case "App\\Events\\UserBannedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("userBanned", data, channel); break; } case "App\\Events\\UserUnbannedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("userUnbanned", data, channel); break; } case "App\\Events\\MessageDeletedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("messageDeleted", data, channel); break; } case "App\\Events\\PinnedMessageCreatedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("pinnedMessageCreated", data, channel); break; } case "App\\Events\\PinnedMessageDeletedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("pinnedMessageDeleted", data, channel); break; } case "App\\Events\\ChatroomUpdatedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("chatroomUpdated", data, channel); break; } case "App\\Events\\PollUpdateEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("pollUpdate", data, channel); break; } case "App\\Events\\PollDeleteEvent": { if (channel) this.emit("pollDelete", channel); break; } case "App\\Events\\StreamerIsLive": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("streamerIsLive", data, channel); break; } case "App\\Events\\StopStreamBroadcast": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("stopStreamBroadcast", data, channel); break; } // From myold.ts case "GoalCreatedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("goalCreated", data, channel); break; } case "GoalCanceledEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("goalCanceled", data, channel); break; } case "GoalProgressUpdateEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("goalProgressUpdate", data, channel); break; } case "App\\Events\\LivestreamUpdated": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("livestreamUpdated", data, channel); break; } case "PredictionCreated": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("predictionCreated", data, channel); break; } case "PredictionUpdated": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("predictionUpdated", data, channel); break; } case "RewardRedeemedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("rewardRedeemed", data, channel); break; } case "App\\Events\\ChannelSubscriptionEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("channelSubscription", data, channel); break; } case "App\\Events\\LuckyUsersWhoGotGiftSubscriptionsEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("luckyUsersWhoGotGiftSubscriptions", data, channel); break; } case "App\\Events\\VideoPrivatedEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("videoPrivated", data, channel); break; } case "GiftsLeaderboardUpdated": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("giftsLeaderboardUpdated", data, channel); break; } case "App\\Events\\ChatMoveToSupportedChannelEvent": { const data = parseJSON(messageEvent.data); if (data && channel) this.emit("chatMoveToSupportedChannel", data, channel); break; } } } sendPusher(channel, type = "subscribe") { if (this.isConnected()) { this.socket.send(JSON.stringify({ event: `pusher:${type}`, data: { auth: "", channel } })); } } startPing() { clearInterval(this.pingInterval); this.pingInterval = setInterval(() => { if (this.isConnected()) { this.socket.send(JSON.stringify({ event: "pusher:ping", data: {} })); } }, this.socketSession.activity_timeout * 1e3); } subscribeToChannel(channel) { if (this.subscribePusher?.chatRoom == true) this.sendPusher(`chatrooms.${channel.chatroomId}.v2`); if (this.subscribePusher?.chatRoom == true) this.sendPusher(`chatroom_${channel.chatroomId}`); if (this.subscribePusher?.channel == true) this.sendPusher(`channel_${channel.id}`); if (this.subscribePusher?.channel == true) this.sendPusher(`channel.${channel.id}`); if (this.subscribePusher?.predictions == true) this.sendPusher(`predictions-channel-${channel.id}`); } async fetchUserInfo(channelName) { const normalizedName = KiChannel.toLogin(channelName); const infoRes = await fetch(`https://kick.com/api/v2/channels/${normalizedName}/info`, { cache: "no-cache" }); if (!infoRes.ok) return; return await infoRes.json(); } async fetchChatRoom(channelName) { const normalizedName = KiChannel.toLogin(channelName); const infoRes = await fetch(`https://kick.com/api/v2/channels/${normalizedName}/info`, { cache: "no-cache" }); if (!infoRes.ok) return; return await infoRes.json(); } async join(channelName) { const normalizedName = KiChannel.toLogin(channelName); if (this.channels.has(normalizedName)) { return this.channels.get(normalizedName); } try { const infoData = await this.fetchUserInfo(normalizedName); if (!infoData) throw new Error(`Failed to fetch channel info for ${normalizedName}`); const chatroomData = await this.fetchChatRoom(normalizedName); if (!chatroomData) throw new Error(`Failed to fetch chatroom info for ${normalizedName}`); const channel = new KiChannel(infoData, chatroomData); this.channels.set(normalizedName, channel); this.channelsByChatroomId.set(channel.chatroomId, channel); if (this.isConnected()) { this.subscribeToChannel(channel); } const joinedChannel = await this.waitForEvent("join", (ch) => { return ch.slug === normalizedName; }); return joinedChannel; } catch (error) { this.channels.delete(normalizedName); const ch = Array.from(this.channelsByChatroomId.values()).find((c) => c.slug === normalizedName); if (ch) { this.channelsByChatroomId.delete(ch.chatroomId); } this.emit("socketError", error); throw error; } } leave(channelName) { const normalizedName = KiChannel.toLogin(channelName); const channel = this.channels.get(normalizedName); if (channel) { if (this.isConnected()) { if (this.subscribePusher?.chatRoom == true) this.sendPusher(`chatrooms.${channel.chatroomId}.v2`, "unsubscribe"); if (this.subscribePusher?.chatRoom == true) this.sendPusher(`chatroom_${channel.chatroomId}`, "unsubscribe"); if (this.subscribePusher?.channel == true) this.sendPusher(`channel_${channel.id}`, "unsubscribe"); if (this.subscribePusher?.channel == true) this.sendPusher(`channel.${channel.id}`, "unsubscribe"); if (this.subscribePusher?.predictions == true) this.sendPusher(`predictions-channel-${channel.id}`, "unsubscribe"); } this.channels.delete(normalizedName); this.channelsByChatroomId.delete(channel.chatroomId); this.emit("leave", channel, "Disconnected by user"); } } waitForEvent(event, filter, timeoutMs = 1e4) { return new Promise((resolve, reject) => { const listener = (...args) => { if (filter(...args)) { this.off(event, listener); clearTimeout(timeout); resolve(args[0]); } }; const timeout = setTimeout(() => { this.off(event, listener); reject(new Error(`Timed out waiting for event: ${event}`)); }, timeoutMs); this.on(event, listener); }); } }; // src/index.ts var src_default = { KiChatjs }; return __toCommonJS(src_exports); })(); //# sourceMappingURL=kichat.js.browser-global.js.map