import { CommandContext, Filter } from "https://deno.land/x/grammy@v1.30.0/mod.ts"; import { BotConfig } from "../../cfg/bot.ts" import { Ctx } from "../ctx.ts"; import { LangManager } from "../lang/export.ts"; const CAPTCHA_FAILS_LIMIT = 3 const TIMEOUT_AFTER_FAIL_MINS = 5 export interface CaptchaSessionData { message_id: number, tries_failed: number, generated_at: number, // Captcha can be changed from a simple matematical ones // to a more complicated images or even science-related questions solution: string, } const randInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min) class Captcha { // ! READ ONLY ! Initialized by the constructor public _generated_at = Date.now() // ! READ ONLY ! Initialized by the constructor public _solution: number // ! READ ONLY ! Initialized by the constructor public _text: string constructor() { const number1 = randInt(-100, 100) const number2 = randInt(-100, 100) this._solution = number1 + number2 this._text = `Captcha: ${number1}+${number2}=?` } } // User captcha response sanitizer/parser/validator const isSolutionCorrect = (expectSolution: string, solution: string): boolean => { solution = solution.replace(' ','').replace('\t','') // Sanitizing return parseInt(solution, 10) == parseInt(expectSolution, 10) ? true : false } const initUserCaptcha = async (ctx: CommandContext) => { if (ctx.chat.type != "private" || ctx.session.captcha_data) return if (ctx.session.chat_participant) { if (!ctx.msg?.from) return ctx.reply(LangManager.getLang(ctx.msg.from.language_code) .replies.captcha.already_in_chat) } const captcha = new Captcha() const msg = await ctx.reply(captcha._text) ctx.session.captcha_data = { message_id: msg.message_id, tries_failed: 0, generated_at: captcha._generated_at, solution: captcha._solution.toString(), } } const captchaPassed = async (botCfg: BotConfig, ctx: Filter) => { if (ctx.session.captcha_data) { ctx.api.deleteMessage( ctx.chatId, ctx.session.captcha_data.message_id ).catch() } ctx.session.captcha_data = undefined ctx.session.captcha_solved = true const now = Math.floor(Date.now() / 1000); const linkExpireTimestamp = now + (12 * 60 * 60); const link = await ctx.api.createChatInviteLink(botCfg.chat_id, { member_limit: 1, expire_date: linkExpireTimestamp, }) ctx.reply(LangManager.getLang(ctx.msg.from.language_code) .replies.captcha.passed(link.invite_link)) ctx.session.captcha_data = undefined } // returns true if captcha is response to a captcha; else -- returns false const handleNewMessage = async (botCfg: BotConfig, ctx: Filter): Promise => { if (ctx.message.chat.type != "private" || !ctx.session.captcha_data) return false if (isSolutionCorrect(ctx.session.captcha_data.solution, ctx.message.text)) { await captchaPassed(botCfg, ctx) } else { ctx.session.captcha_data.tries_failed++ ctx.api.deleteMessage(ctx.chatId, ctx.msg.message_id).catch() if (ctx.session.captcha_data.tries_failed > CAPTCHA_FAILS_LIMIT) { ctx.reply(LangManager.getLang(ctx.msg.from.language_code) .replies.captcha.failed(TIMEOUT_AFTER_FAIL_MINS)) ctx.api.deleteMessage(ctx.chatId, ctx.session.captcha_data.message_id).catch() } } return true } const handleUserJoinChat = async (botCfg: BotConfig, ctx: Filter) => { ctx.session.chat_participant = true const inviteLink = ctx.session.invite_link if (!inviteLink) return await ctx.api.revokeChatInviteLink(botCfg.chat_id, inviteLink) } export { handleNewMessage, handleUserJoinChat, initUserCaptcha }