@retconned/kick-js
Version:
A typescript bot interface for kick.com
836 lines (827 loc) • 27 kB
JavaScript
;
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 src_exports = {};
__export(src_exports, {
createClient: () => createClient
});
module.exports = __toCommonJS(src_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 setupPuppeteer = async () => {
const puppeteerExtra = import_puppeteer_extra.default.use((0, import_puppeteer_extra_plugin_stealth.default)());
const browser = await puppeteerExtra.launch({
headless: true,
defaultViewport: null,
args: ["--start-maximized"]
});
const page = await browser.newPage();
return { browser, page };
};
var getChannelData = async (channel) => {
const { browser, page } = await setupPuppeteer();
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);
});
return jsonContent;
} catch (error) {
console.error("Error getting channel data:", error);
return null;
} finally {
await browser.close();
}
};
var getVideoData = async (video_id) => {
const { browser, page } = await setupPuppeteer();
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);
});
return jsonContent;
} catch (error) {
console.error("Error getting video data:", error);
return null;
} finally {
await browser.close();
}
};
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();
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();
});
const selectorTimeout = 6e3;
try {
await page.goto("https://kick.com/");
await page.waitForSelector("nav > div:nth-child(3) > button:first-child", {
visible: true,
timeout: selectorTimeout
});
await page.click("nav > div:nth-child(3) > button:first-child");
await page.waitForSelector('input[name="emailOrUsername"]', {
visible: true,
timeout: selectorTimeout
});
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: selectorTimeout }
);
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;
}
if (!cookieString || cookieString === "") {
throw new Error("Failed to capture cookies");
}
if (!bearerToken || bearerToken === "") {
throw new Error("Failed to capture bearer token");
}
if (!xsrfToken || xsrfToken === "") {
throw new Error("Failed to capture xsrf token");
}
isAuthenticated = true;
return {
bearerToken,
xsrfToken,
cookies: cookieString,
isAuthenticated
};
} catch (error) {
throw error;
} finally {
await browser.close();
}
};
// 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/utils.ts
var parseJSON = (json) => JSON.parse(json);
var validateCredentials = (options) => {
const { type, credentials } = options;
switch (type) {
case "login":
if (!credentials.username || typeof credentials.username !== "string") {
throw new Error("Username is required and must be a string");
}
if (!credentials.password || typeof credentials.password !== "string") {
throw new Error("Password is required and must be a string");
}
if (!credentials.otp_secret || typeof credentials.otp_secret !== "string") {
throw new Error("OTP secret is required and must be a string");
}
break;
case "tokens":
if (!credentials.bearerToken || typeof credentials.bearerToken !== "string") {
throw new Error("bearerToken is required and must be a string");
}
if (!credentials.xsrfToken || typeof credentials.xsrfToken !== "string") {
throw new Error("xsrfToken is required and must be a string");
}
if (!credentials.cookies || typeof credentials.cookies !== "string") {
throw new Error("cookies are required and must be a string");
}
break;
default:
throw new Error("Invalid login type");
}
};
// src/core/messageHandling.ts
var parseMessage = (message) => {
try {
const messageEventJSON = parseJSON(message);
switch (messageEventJSON.event) {
case "App\\Events\\ChatMessageEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "ChatMessage", data };
}
case "App\\Events\\SubscriptionEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "Subscription", data };
}
case "App\\Events\\GiftedSubscriptionsEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "GiftedSubscriptions", data };
}
case "App\\Events\\StreamHostEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "StreamHost", data };
}
case "App\\Events\\MessageDeletedEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "MessageDeleted", data };
}
case "App\\Events\\UserBannedEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "UserBanned", data };
}
case "App\\Events\\UserUnbannedEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "UserUnbanned", data };
}
case "App\\Events\\PinnedMessageCreatedEvent": {
const data = parseJSON(
messageEventJSON.data
);
return { type: "PinnedMessageCreated", data };
}
case "App\\Events\\PinnedMessageDeletedEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "PinnedMessageDeleted", data };
}
case "App\\Events\\PollUpdateEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "PollUpdate", data };
}
case "App\\Events\\PollDeleteEvent": {
const data = parseJSON(messageEventJSON.data);
return { type: "PollDelete", data };
}
default: {
console.log("Unknown event type:", messageEventJSON.event);
return null;
}
}
return null;
} catch (error) {
console.error("Error parsing message:", error);
return null;
}
};
// src/core/requestHelper.ts
var import_axios = __toESM(require("axios"), 1);
var import_axios2 = require("axios");
var createHeaders = ({
bearerToken,
cookies,
channelSlug
}) => {
const headers = new import_axios2.AxiosHeaders();
headers.set("accept", "application/json");
headers.set("accept-language", "en-US,en;q=0.9");
headers.set("authorization", `Bearer ${bearerToken}`);
headers.set("cache-control", "max-age=0");
headers.set("cluster", "v2");
headers.set("content-type", "application/json");
headers.set("priority", "u=1, i");
headers.set("cookie", cookies);
headers.set("Referer", `https://kick.com/${channelSlug}`);
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
return headers;
};
var makeRequest = async (method, url, headers, data) => {
try {
const response = await (0, import_axios.default)({
method,
url,
headers,
data
});
if (response.status === 200) {
return response.data;
}
console.error(`Request failed with status: ${response.status}`);
return null;
} catch (error) {
console.error(`Request error for ${url}:`, error);
return null;
}
};
// 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) {
throw new Error("Missing bearer token");
}
if (!clientCookies) {
throw new Error("Missing cookies");
}
};
const login = async (options2) => {
const { type, credentials } = options2;
try {
switch (type) {
case "login":
if (!credentials) {
throw new Error("Credentials are required for login");
}
validateCredentials(options2);
if (mergedOptions.logger) {
console.log("Starting authentication process with login ...");
}
const { bearerToken, xsrfToken, cookies, isAuthenticated } = await authentication({
username: credentials.username,
password: credentials.password,
otp_secret: credentials.otp_secret
});
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();
break;
case "tokens":
if (!credentials) {
throw new Error("Tokens are required for login");
}
if (mergedOptions.logger) {
console.log("Starting authentication process with tokens ...");
}
clientBearerToken = credentials.bearerToken;
clientToken = credentials.xsrfToken;
clientCookies = credentials.cookies;
isLoggedIn = true;
await initialize();
break;
default:
throw new Error("Invalid authentication type");
}
return true;
} catch (error) {
console.error("Login failed:", error);
throw error;
}
};
const initialize = async () => {
try {
if (mergedOptions.readOnly === false && !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) {
switch (parsedMessage.type) {
case "ChatMessage":
if (mergedOptions.plainEmote) {
const messageData = parsedMessage.data;
messageData.content = messageData.content.replace(
/\[emote:(\d+):(\w+)\]/g,
(_, __, emoteName) => emoteName
);
}
break;
case "Subscription":
break;
case "GiftedSubscriptions":
break;
case "StreamHostEvent":
break;
case "UserBannedEvent":
break;
case "UserUnbannedEvent":
break;
case "PinnedMessageCreatedEvent":
break;
}
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 === true) {
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");
}
if (messageContent.length > 500) {
throw new Error("Message content must be less than 500 characters");
}
if (!clientCookies) {
throw new Error("Cookies missing");
}
if (!clientBearerToken) {
throw new Error("Bearer token missing");
}
if (!clientToken) {
throw new Error("XSRF token missing");
}
const res = fetch(
`https://kick.com/api/v2/messages/send/${channelInfo.chatroom.id}`,
{
headers: {
accept: "application/json",
"accept-language": "en-US,en;q=0.9",
authorization: `Bearer ${clientBearerToken}`,
"x-CSRF-token": clientToken,
"cache-control": "max-age=0",
cluster: "v2",
"content-type": "application/json",
priority: "u=1, i",
"sec-ch-ua": '"Not A(Brand";v="8", "Chromium";v="132"',
"sec-ch-ua-arch": '"arm"',
"sec-ch-ua-bitness": '"64"',
"sec-ch-ua-full-version": '"132.0.6834.111"',
"sec-ch-ua-full-version-list": '"Not A(Brand";v="8.0.0.0", "Chromium";v="132.0.6834.111"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-model": '""',
"sec-ch-ua-platform": '"macOS"',
"sec-ch-ua-platform-version": '"15.0.1"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
cookie: clientCookies,
Referer: `https://kick.com/${channelInfo.slug}`,
"Referrer-Policy": "strict-origin-when-cross-origin"
},
body: `{"content":"${messageContent}","type":"message"}`,
method: "POST"
}
);
};
const banUser = async (targetUser, durationInMinutes, permanent = false) => {
if (!channelInfo) {
throw new Error("Channel info not available");
}
checkAuth();
if (!targetUser) {
throw new Error("Specify a user to ban");
}
if (!permanent) {
if (!durationInMinutes) {
throw new Error("Specify a duration in minutes");
}
if (durationInMinutes < 1) {
throw new Error("Duration must be more than 0 minutes");
}
}
const headers = createHeaders({
bearerToken: clientBearerToken,
xsrfToken: clientToken,
cookies: clientCookies,
channelSlug: channelInfo.slug
});
try {
const data = permanent ? { banned_username: targetUser, permanent: true } : {
banned_username: targetUser,
duration: durationInMinutes,
permanent: false
};
const result = await makeRequest(
"post",
`https://kick.com/api/v2/channels/${channelInfo.id}/bans`,
headers,
data
);
if (result) {
console.log(
`User ${targetUser} ${permanent ? "banned" : "timed out"} successfully`
);
} else {
console.error(`Failed to ${permanent ? "ban" : "time out"} user.`);
}
} catch (error) {
console.error(
`Error ${permanent ? "banning" : "timing out"} user:`,
error
);
}
};
const unbanUser = async (targetUser) => {
if (!channelInfo) {
throw new Error("Channel info not available");
}
checkAuth();
if (!targetUser) {
throw new Error("Specify a user to unban");
}
const headers = createHeaders({
bearerToken: clientBearerToken,
xsrfToken: clientToken,
cookies: clientCookies,
channelSlug: channelInfo.slug
});
try {
const result = await makeRequest(
"delete",
`https://kick.com/api/v2/channels/${channelInfo.id}/bans/${targetUser}`,
headers
);
if (result) {
console.log(`User ${targetUser} unbanned successfully`);
} else {
console.error(`Failed to unban user.`);
}
} catch (error) {
console.error("Error unbanning user:", 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");
}
const headers = createHeaders({
bearerToken: clientBearerToken,
xsrfToken: clientToken,
cookies: clientCookies,
channelSlug: channelInfo.slug
});
try {
const result = await makeRequest(
"delete",
`https://kick.com/api/v2/channels/${channelInfo.id}/messages/${messageId}`,
headers
);
if (result) {
console.log(`Message ${messageId} deleted successfully`);
} else {
console.error(`Failed to delete message.`);
}
} catch (error) {
console.error("Error deleting 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 either 'on' or 'off'");
}
if (mode === "on" && (!durationInSeconds || durationInSeconds < 1)) {
throw new Error(
"Invalid duration, must be greater than 0 if mode is 'on'"
);
}
const headers = createHeaders({
bearerToken: clientBearerToken,
xsrfToken: clientToken,
cookies: clientCookies,
channelSlug: channelInfo.slug
});
try {
const data = mode === "off" ? { slow_mode: false } : { slow_mode: true, message_interval: durationInSeconds };
const result = await makeRequest(
"put",
`https://kick.com/api/v2/channels/${channelInfo.slug}/chatroom`,
headers,
data
);
if (result?.success) {
console.log(
mode === "off" ? "Slow mode disabled successfully" : `Slow mode enabled with ${durationInSeconds} second interval`
);
} else {
console.error(
`Failed to ${mode === "off" ? "disable" : "enable"} slow mode.`
);
}
} catch (error) {
console.error(
`Error ${mode === "off" ? "disabling" : "enabling"} slow mode:`,
error
);
}
};
const getPoll = async (targetChannel) => {
const channel = targetChannel || channelName;
if (!targetChannel && !channelInfo) {
throw new Error("Channel info not available");
}
const headers = createHeaders({
bearerToken: clientBearerToken,
xsrfToken: clientToken,
cookies: clientCookies,
channelSlug: channel
});
try {
const result = await makeRequest(
"get",
`https://kick.com/api/v2/channels/${channel}/polls`,
headers
);
if (result) {
console.log(`Poll retrieved successfully for channel: ${channel}`);
return result;
}
} catch (error) {
console.error(`Error retrieving poll for channel ${channel}:`, error);
}
return null;
};
const getLeaderboards = async (targetChannel) => {
const channel = targetChannel || channelName;
if (!targetChannel && !channelInfo) {
throw new Error("Channel info not available");
}
const headers = createHeaders({
bearerToken: clientBearerToken,
xsrfToken: clientToken,
cookies: clientCookies,
channelSlug: channel
});
try {
const result = await makeRequest(
"get",
`https://kick.com/api/v2/channels/${channel}/leaderboards`,
headers
);
if (result) {
console.log(
`Leaderboards retrieved successfully for channel: ${channel}`
);
return result;
}
} catch (error) {
console.error(
`Error retrieving leaderboards for channel ${channel}:`,
error
);
}
return null;
};
return {
login,
on,
get user() {
return getUser();
},
vod,
sendMessage,
banUser,
unbanUser,
deleteMessage,
slowMode,
getPoll,
getLeaderboards
};
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createClient
});