UNPKG

@hunterowner/kick-js

Version:

A typescript bot interface for kick.com

781 lines (772 loc) 25.7 kB
"use strict"; 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 __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); // src/index.ts var index_exports = {}; __export(index_exports, { createClient: () => createClient }); module.exports = __toCommonJS(index_exports); // src/client/client.ts var import_ws2 = require("ws"); var import_events = __toESM(require("events"), 1); // src/core/kickApi.ts var import_puppeteer_extra = __toESM(require("puppeteer-extra"), 1); var import_puppeteer_extra_plugin_stealth = __toESM(require("puppeteer-extra-plugin-stealth"), 1); var import_otplib = require("otplib"); var getChannelData = async (channel) => { const puppeteerExtra = import_puppeteer_extra.default.use((0, import_puppeteer_extra_plugin_stealth.default)()); const browser = await puppeteerExtra.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] }); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); try { const response = await page.goto( `https://kick.com/api/v2/channels/${channel}` ); if (response?.status() === 403) { throw new Error( "Request blocked by Cloudflare protection. Please try again later." ); } await page.waitForSelector("body"); const jsonContent = await page.evaluate(() => { const bodyElement = document.querySelector("body"); if (!bodyElement || !bodyElement.textContent) { throw new Error("Unable to fetch channel data"); } return JSON.parse(bodyElement.textContent); }); await browser.close(); return jsonContent; } catch (error) { await browser.close(); if (error instanceof Error && error.message.includes("Cloudflare")) { throw error; } console.error("Error getting channel data:", error); return null; } }; var getVideoData = async (video_id) => { const puppeteerExtra = import_puppeteer_extra.default.use((0, import_puppeteer_extra_plugin_stealth.default)()); const browser = await puppeteerExtra.launch({ headless: true }); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); try { const response = await page.goto( `https://kick.com/api/v1/video/${video_id}` ); if (response?.status() === 403) { throw new Error( "Request blocked by Cloudflare protection. Please try again later." ); } await page.waitForSelector("body"); const jsonContent = await page.evaluate(() => { const bodyElement = document.querySelector("body"); if (!bodyElement || !bodyElement.textContent) { throw new Error("Unable to fetch video data"); } return JSON.parse(bodyElement.textContent); }); await browser.close(); return jsonContent; } catch (error) { await browser.close(); if (error instanceof Error && error.message.includes("Cloudflare")) { throw error; } console.error("Error getting video data:", error); return null; } }; var authentication = async ({ username, password, otp_secret }) => { let bearerToken = ""; let xsrfToken = ""; let cookieString = ""; let isAuthenticated = false; const puppeteerExtra = import_puppeteer_extra.default.use((0, import_puppeteer_extra_plugin_stealth.default)()); const browser = await puppeteerExtra.launch({ headless: true, defaultViewport: null }); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); let requestData = []; await page.setRequestInterception(true); page.on("request", (request) => { const url = request.url(); const headers = request.headers(); if (url.includes("/api/v2/channels/followed")) { const reqBearerToken = headers["authorization"] || ""; cookieString = headers["cookie"] || ""; if (!bearerToken && reqBearerToken.includes("Bearer ")) { const splitToken = reqBearerToken.split("Bearer ")[1]; if (splitToken) { bearerToken = splitToken; } } } requestData.push({ url, headers, method: request.method(), resourceType: request.resourceType() }); request.continue(); }); try { await page.goto("https://kick.com/"); await page.waitForSelector("nav > div:nth-child(3) > button:first-child", { visible: true, timeout: 5e3 }); await page.click("nav > div:nth-child(3) > button:first-child"); await page.waitForSelector('input[name="emailOrUsername"]', { visible: true, timeout: 5e3 }); await page.type('input[name="emailOrUsername"]', username, { delay: 100 }); await page.type('input[name="password"]', password, { delay: 100 }); await page.click('button[data-test="login-submit"]'); try { await page.waitForFunction( () => { const element = document.querySelector( 'input[data-input-otp="true"]' ); const verifyText = document.body.textContent?.includes("Verify 2FA Code"); return element || !verifyText; }, { timeout: 5e3 } ); const requires2FA = await page.evaluate(() => { return !!document.querySelector('input[data-input-otp="true"]'); }); if (requires2FA) { if (!otp_secret) { throw new Error("2FA authentication required"); } const token = import_otplib.authenticator.generate(otp_secret); await page.waitForSelector('input[data-input-otp="true"]'); await page.type('input[data-input-otp="true"]', token, { delay: 100 }); await page.click('button[type="submit"]'); await page.waitForNavigation({ waitUntil: "networkidle0" }); } } catch (error) { if (error.message.includes("2FA authentication required")) throw error; } await page.goto("https://kick.com/api/v2/channels/followed"); const cookies = await page.cookies(); cookieString = cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; "); const xsrfTokenCookie = cookies.find( (cookie) => cookie.name === "XSRF-TOKEN" )?.value; if (xsrfTokenCookie) { xsrfToken = xsrfTokenCookie; } isAuthenticated = true; await browser.close(); return { bearerToken, xsrfToken, cookies: cookieString, isAuthenticated }; } catch (error) { await browser.close(); throw error; } }; // src/core/websocket.ts var import_ws = __toESM(require("ws"), 1); var import_url = require("url"); var BASE_URL = "wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679"; var createWebSocket = (chatroomId) => { const urlParams = new import_url.URLSearchParams({ protocol: "7", client: "js", version: "7.4.0", flash: "false" }); const url = `${BASE_URL}?${urlParams.toString()}`; const socket = new import_ws.default(url); socket.on("open", () => { const connect = JSON.stringify({ event: "pusher:subscribe", data: { auth: "", channel: `chatrooms.${chatroomId}.v2` } }); socket.send(connect); }); return socket; }; // src/utils/messageHandling.ts var parseMessage = (message) => { try { const messageEventJSON = JSON.parse(message); if (messageEventJSON.event === "App\\Events\\ChatMessageEvent") { const data = JSON.parse(messageEventJSON.data); return { type: "ChatMessage", data }; } else if (messageEventJSON.event === "App\\Events\\SubscriptionEvent") { } else if (messageEventJSON.event === "App\\Events\\RaidEvent") { } return null; } catch (error) { console.error("Error parsing message:", error); return null; } }; // src/client/client.ts var import_axios = __toESM(require("axios"), 1); // src/utils/utils.ts var validateAuthSettings = (credentials) => { const { username, password, otp_secret } = credentials; if (!username || typeof username !== "string") { throw new Error("Username is required and must be a string"); } if (!password || typeof password !== "string") { throw new Error("Password is required and must be a string"); } if (!otp_secret || typeof otp_secret !== "string") { throw new Error("OTP secret is required and must be a string"); } }; // src/client/client.ts var createClient = (channelName, options = {}) => { const emitter = new import_events.default(); let socket = null; let channelInfo = null; let videoInfo = null; let clientToken = null; let clientCookies = null; let clientBearerToken = null; let isLoggedIn = false; const defaultOptions = { plainEmote: true, logger: false, readOnly: false }; const mergedOptions = { ...defaultOptions, ...options }; const checkAuth = () => { if (!isLoggedIn) { throw new Error("Authentication required. Please login first."); } if (!clientBearerToken || !clientToken || !clientCookies) { throw new Error("Missing authentication tokens"); } }; const login = async (credentials) => { try { validateAuthSettings(credentials); if (mergedOptions.logger) { console.log("Starting authentication process..."); } const { bearerToken, xsrfToken, cookies, isAuthenticated } = await authentication(credentials); if (mergedOptions.logger) { console.log("Authentication tokens received, validating..."); } clientBearerToken = bearerToken; clientToken = xsrfToken; clientCookies = cookies; isLoggedIn = isAuthenticated; if (!isAuthenticated) { throw new Error("Authentication failed"); } if (mergedOptions.logger) { console.log("Authentication successful, initializing client..."); } await initialize(); return true; } catch (error) { console.error("Login failed:", error); throw error; } }; const initialize = async () => { try { if (!mergedOptions.readOnly && !isLoggedIn) { throw new Error("Authentication required. Please login first."); } if (mergedOptions.logger) { console.log(`Fetching channel data for: ${channelName}`); } channelInfo = await getChannelData(channelName); if (!channelInfo) { throw new Error("Unable to fetch channel data"); } if (mergedOptions.logger) { console.log( "Channel data received, establishing WebSocket connection..." ); } socket = createWebSocket(channelInfo.chatroom.id); socket.on("open", () => { if (mergedOptions.logger) { console.log(`Connected to channel: ${channelName}`); } emitter.emit("ready", getUser()); }); socket.on("message", (data) => { const parsedMessage = parseMessage(data.toString()); if (parsedMessage) { if (mergedOptions.plainEmote && parsedMessage.type === "ChatMessage") { const messageData = parsedMessage.data; messageData.content = messageData.content.replace( /\[emote:(\d+):(\w+)\]/g, (_, __, emoteName) => emoteName ); } emitter.emit(parsedMessage.type, parsedMessage.data); } }); socket.on("close", () => { if (mergedOptions.logger) { console.log(`Disconnected from channel: ${channelName}`); } emitter.emit("disconnect"); }); socket.on("error", (error) => { console.error("WebSocket error:", error); emitter.emit("error", error); }); } catch (error) { console.error("Error during initialization:", error); throw error; } }; if (mergedOptions.readOnly) { void initialize(); } const on = (event, listener) => { emitter.on(event, listener); }; const getUser = () => channelInfo ? { id: channelInfo.id, username: channelInfo.slug, tag: channelInfo.user.username } : null; const vod = async (video_id) => { videoInfo = await getVideoData(video_id); if (!videoInfo) { throw new Error("Unable to fetch video data"); } return { id: videoInfo.id, title: videoInfo.livestream.session_title, thumbnail: videoInfo.livestream.thumbnail, duration: videoInfo.livestream.duration, live_stream_id: videoInfo.live_stream_id, start_time: videoInfo.livestream.start_time, created_at: videoInfo.created_at, updated_at: videoInfo.updated_at, uuid: videoInfo.uuid, views: videoInfo.views, stream: videoInfo.source, language: videoInfo.livestream.language, livestream: videoInfo.livestream, channel: videoInfo.livestream.channel }; }; const sendMessage = async (messageContent) => { if (!channelInfo) { throw new Error("Channel info not available"); } checkAuth(); try { const response = await import_axios.default.post( `https://kick.com/api/v2/messages/send/${channelInfo.id}`, { content: messageContent, type: "message" }, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log(`Message sent successfully: ${messageContent}`); } else { console.error(`Failed to send message. Status: ${response.status}`); } } catch (error) { console.error("Error sending message:", error); } }; const timeOut = async (targetUser, durationInMinutes) => { if (!channelInfo) { throw new Error("Channel info not available"); } if (!durationInMinutes) { throw new Error("Specify a duration in minutes"); } if (durationInMinutes < 1) { throw new Error("Duration must be more than 0 minutes"); } checkAuth(); if (!targetUser) { throw new Error("Specify a user to ban"); } try { const response = await import_axios.default.post( `https://kick.com/api/v2/channels/${channelInfo.id}/bans`, { banned_username: targetUser, duration: durationInMinutes, permanent: false }, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log(`User ${targetUser} timed out successfully`); } else { console.error(`Failed to time out user. Status: ${response.status}`); } } catch (error) { console.error("Error sending message:", error); } }; const permanentBan = async (targetUser) => { if (!channelInfo) { throw new Error("Channel info not available"); } checkAuth(); if (!targetUser) { throw new Error("Specify a user to ban"); } try { const response = await import_axios.default.post( `https://kick.com/api/v2/channels/${channelInfo.id}/bans`, { banned_username: targetUser, permanent: true }, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log(`User ${targetUser} banned successfully`); } else { console.error(`Failed to ban user. Status: ${response.status}`); } } catch (error) { console.error("Error sending message:", error); } }; const unban = async (targetUser) => { if (!channelInfo) { throw new Error("Channel info not available"); } checkAuth(); if (!targetUser) { throw new Error("Specify a user to unban"); } try { const response = await import_axios.default.delete( `https://kick.com/api/v2/channels/${channelInfo.id}/bans/${targetUser}`, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log(`User ${targetUser} unbanned successfully`); } else { console.error(`Failed to unban user. Status: ${response.status}`); } } catch (error) { console.error("Error sending message:", error); } }; const deleteMessage = async (messageId) => { if (!channelInfo) { throw new Error("Channel info not available"); } checkAuth(); if (!messageId) { throw new Error("Specify a messageId to delete"); } try { const response = await import_axios.default.delete( `https://kick.com/api/v2/channels/${channelInfo.id}/messages/${messageId}`, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log(`Message ${messageId} deleted successfully`); } else { console.error(`Failed to delete message. Status: ${response.status}`); } } catch (error) { console.error("Error sending message:", error); } }; const slowMode = async (mode, durationInSeconds) => { if (!channelInfo) { throw new Error("Channel info not available"); } checkAuth(); if (mode !== "on" && mode !== "off") { throw new Error("Invalid mode, must be 'on' or 'off'"); } if (mode === "on" && durationInSeconds && durationInSeconds < 1) { throw new Error( "Invalid duration, must be greater than 0 if mode is 'on'" ); } try { if (mode === "off") { const response = await await import_axios.default.put( `https://kick.com/api/v2/channels/${channelInfo.slug}/chatroom`, { slow_mode: false }, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log("Slow mode disabled successfully"); } else { console.error( `Failed to disable slow mode. Status: ${response.status}` ); } } else { const response = await await import_axios.default.put( `https://kick.com/api/v2/channels/${channelInfo.slug}/chatroom`, { slow_mode: true, message_interval: durationInSeconds }, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelInfo.slug}` } } ); if (response.status === 200) { console.log( `Slow mode enabled with ${durationInSeconds} second interval` ); } else { console.error( `Failed to enable slow mode. Status: ${response.status}` ); } } } catch (error) { console.error("Error sending message:", error); } }; const getPoll = async (targetChannel) => { if (targetChannel) { try { const response = await import_axios.default.get( `https://kick.com/api/v2/channels/${targetChannel}/polls`, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${targetChannel}` } } ); if (response.status === 200) { console.log( `Poll retrieved successfully for channel: ${targetChannel}` ); return response.data; } } catch (error) { console.error( `Error retrieving poll for channel ${targetChannel}:`, error ); return null; } } if (!channelInfo) { throw new Error("Channel info not available"); } try { const response = await import_axios.default.get( `https://kick.com/api/v2/channels/${channelName}/polls`, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelName}` } } ); if (response.status === 200) { console.log(`Poll retrieved successfully for current channel`); return response.data; } } catch (error) { console.error("Error retrieving poll for current channel:", error); return null; } return null; }; const getLeaderboards = async (targetChannel) => { if (targetChannel) { try { const response = await import_axios.default.get( `https://kick.com/api/v2/channels/${targetChannel}/leaderboards`, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${targetChannel}` } } ); if (response.status === 200) { console.log( `Leaderboards retrieved successfully for channel: ${targetChannel}` ); return response.data; } } catch (error) { console.error( `Error retrieving leaderboards for channel ${targetChannel}:`, error ); return null; } } if (!channelInfo) { throw new Error("Channel info not available"); } try { const response = await import_axios.default.get( `https://kick.com/api/v2/channels/${channelName}/leaderboards`, { headers: { accept: "application/json, text/plain, */*", authorization: `Bearer ${clientBearerToken}`, "content-type": "application/json", "x-xsrf-token": clientToken, cookie: clientCookies, Referer: `https://kick.com/${channelName}` } } ); if (response.status === 200) { console.log(`Leaderboards retrieved successfully for current channel`); return response.data; } } catch (error) { console.error( "Error retrieving leaderboards for current channel:", error ); return null; } return null; }; return { login, on, get user() { return getUser(); }, vod, sendMessage, timeOut, permanentBan, unban, deleteMessage, slowMode, getPoll, getLeaderboards }; }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createClient });