@hunterowner/kick-js
Version:
A typescript bot interface for kick.com
744 lines (737 loc) • 23.6 kB
JavaScript
// 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
};