import { Filter } from "https://deno.land/x/grammy/mod.ts"; import { BotConfig } from "../../cfg/config.ts"; import { Ctx } from "../ctx.ts"; import { Kysely } from 'npm:kysely'; import { Database } from "../../repo/scheme.ts"; import { checkUserRestrictions } from "../../core/users.ts"; import type { CheckUserOnStartOut } from "./user_managment.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) // Returns captcha task (string) and the solution (number) const genCaptcha = (): [string, number] => { const number1 = randInt(-100, 100) const number2 = randInt(-100, 100) return [ `Captcha: ${number1}+${number2}=?`, 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 captchaPassed = async (ctx: Ctx, db: Kysely, cfg: BotConfig) => { if (ctx.chatId && ctx.session.captcha_data) { await ctx.api.deleteMessage( ctx.chatId, ctx.session.captcha_data.message_id ) } const linkValidUntil = new Date() linkValidUntil.setHours(linkValidUntil.getHours() + 12) const link = await ctx.api.createChatInviteLink(cfg.chat_id, { member_limit: 1, expire_date: Math.floor(linkValidUntil.getTime() / 1000), }) await db.transaction().execute(async trx => { await trx.updateTable('users').set({ is_captcha_passed: true }).where('tg_id', '=', ctx.from!.id).execute() await trx.insertInto('invite_links').values({ link: link.invite_link, expect_user_tg_id: ctx.from!.id, valid_until: linkValidUntil }).execute() }) ctx.session.captcha_data = undefined await ctx.reply(ctx.t("captcha-passed", { invite_link: link.invite_link })) } const captchaFailed = async (ctx: Filter, db: Kysely) => { console.log("Captcha solution failed") ctx.session.captcha_data!.tries_failed++ if (ctx.session.captcha_data!.tries_failed > CAPTCHA_FAILS_LIMIT) { const timeoutUntil = new Date() timeoutUntil.setHours(timeoutUntil.getHours() + 12) await db.updateTable('users').set({ timeout_until: timeoutUntil, is_timeout: true, }).where('tg_id', '=', ctx.from.id).execute() await ctx.reply(ctx.t("captcha-already-in-chat", {timeout_mins: TIMEOUT_AFTER_FAIL_MINS})) await ctx.api.deleteMessage(ctx.chatId, ctx.session.captcha_data!.message_id) } await ctx.api.deleteMessage(ctx.chatId, ctx.msg.message_id) } export const initUserCaptcha = async (ctx: Ctx, db: Kysely, user: CheckUserOnStartOut, cfg: BotConfig) => { if (user.isChatParticipant) { await ctx.reply(ctx.t("captcha-already-in-chat")) } else if (user.activeInviteLink) { await ctx.reply(user.activeInviteLink) } else if (user.isCaptchaSolved) { await captchaPassed(ctx, db, cfg) } else { const [captchaText, captchaSolution] = genCaptcha() const msg = await ctx.reply(captchaText) ctx.session.captcha_data = { message_id: msg.message_id, tries_failed: 0, generated_at: new Date().getTime(), solution: captchaSolution.toString(), } } } // returns true if captcha is response to a captcha; else -- returns false export const checkCaptchaSolution = async (ctx: Filter, db: Kysely, cfg: BotConfig) => { if (!ctx.msg) return const users = await db.selectFrom('users').selectAll().where('tg_id', '=', ctx.msg.from.id).execute() if (users.length < 1) return // TODO: Maybe create new user here, idk const user = users[0] const userRestrictions = await checkUserRestrictions(db, user) if (userRestrictions.isBlocked || userRestrictions.isTimeout) return if (isSolutionCorrect(ctx.session.captcha_data!.solution, ctx.message.text)) { await captchaPassed(ctx, db, cfg) } else { await captchaFailed(ctx, db) } }