UNPKG

@hunterowner/kick-js

Version:

A typescript bot interface for kick.com

744 lines (737 loc) 23.6 kB
// src/client/client.ts import "ws"; import EventEmitter from "events"; // src/core/kickApi.ts import puppeteer from "puppeteer-extra"; import StealthPlugin from "puppeteer-extra-plugin-stealth"; import { authenticator } from "otplib"; var getChannelData = async (channel) => { const puppeteerExtra = puppeteer.use(StealthPlugin()); 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 = puppeteer.use(StealthPlugin()); 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 = puppeteer.use(StealthPlugin()); 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 = 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 import WebSocket from "ws"; import { URLSearchParams } from "url"; var BASE_URL = "wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679"; var createWebSocket = (chatroomId) => { const urlParams = new URLSearchParams({ protocol: "7", client: "js", version: "7.4.0", flash: "false" }); const url = `${BASE_URL}?${urlParams.toString()}`; const socket = new WebSocket(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 import axios from "axios"; // 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 EventEmitter(); 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 axios.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 axios.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 axios.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 axios.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 axios.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 axios.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 axios.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 axios.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 axios.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 axios.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 axios.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 }; }; export { createClient };