mic-bot/bot/normal_mode/captcha.ts

123 lines
4.2 KiB
TypeScript
Raw Normal View History

2024-10-25 17:35:12 +00:00
import { Filter } from "https://deno.land/x/grammy/mod.ts";
2024-10-24 08:35:13 +00:00
import { BotConfig } from "../../cfg/config.ts";
2024-10-19 10:00:35 +00:00
import { Ctx } from "../ctx.ts";
import { LangManager } from "../lang/export.ts";
2024-10-24 08:35:13 +00:00
import { Kysely } from 'npm:kysely';
import { Database } from "../../repo/scheme.ts";
import { checkUserRestrictions } from "../../core/users.ts";
import type { CheckUserOnStartOut } from "./user_managment.ts";
2024-10-19 10:00:35 +00:00
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
]
2024-10-19 10:00:35 +00:00
}
2024-10-23 16:09:10 +00:00
2024-10-19 10:00:35 +00:00
// 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
}
2024-10-23 16:09:10 +00:00
const captchaPassed = async (ctx: Ctx, db: Kysely<Database>, cfg: BotConfig) => {
if (ctx.chatId && ctx.session.captcha_data) {
2024-10-25 17:35:12 +00:00
await ctx.api.deleteMessage(
2024-10-23 16:09:10 +00:00
ctx.chatId, ctx.session.captcha_data.message_id
2024-10-25 17:35:12 +00:00
)
2024-10-23 16:09:10 +00:00
}
ctx.session.captcha_data = undefined
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').where('tg_id', '=', ctx.from!.id)
.set({ is_captcha_passed: true }).execute()
await trx.insertInto('invite_links').values({
link: link.invite_link,
expect_user_tg_id: ctx.from!.id,
valid_until: linkValidUntil
}).execute()
})
2024-10-25 17:35:12 +00:00
ctx.reply(LangManager.getLang(ctx.from!.language_code)
2024-10-23 16:09:10 +00:00
.replies.captcha.passed(link.invite_link))
ctx.session.captcha_data = undefined
}
2024-10-25 17:35:12 +00:00
const initUserCaptcha = async (ctx: Ctx, db: Kysely<Database>, user: CheckUserOnStartOut, cfg: BotConfig) => {
2024-10-23 16:09:10 +00:00
if (user.isChatParticipant) {
2024-10-25 17:35:12 +00:00
ctx.reply(LangManager.getLang(ctx.from!.language_code)
2024-10-19 10:00:35 +00:00
.replies.captcha.already_in_chat)
2024-10-25 17:35:12 +00:00
} else if (user.activeInviteLink) {
await ctx.reply(user.activeInviteLink)
} else if (user.isCaptchaSolved) {
2024-10-23 16:09:10 +00:00
await captchaPassed(ctx, db, cfg)
2024-10-25 17:35:12 +00:00
} else {
const [captchaText, captchaSolution] = genCaptcha()
const msg = await ctx.reply(captchaText)
2024-10-25 17:35:12 +00:00
ctx.session.captcha_data = {
message_id: msg.message_id,
tries_failed: 0,
generated_at: new Date().getTime(),
solution: captchaSolution.toString(),
2024-10-25 17:35:12 +00:00
}
2024-10-19 10:00:35 +00:00
}
}
// returns true if captcha is response to a captcha; else -- returns false
2024-10-23 16:09:10 +00:00
const checkCaptchaSolution = async (ctx: Filter<Ctx, "message:text">, db: Kysely<Database>, 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
2024-10-25 17:35:12 +00:00
const user = users[0]
const userRestrictions = await checkUserRestrictions(db, user)
2024-10-23 16:09:10 +00:00
if (userRestrictions.isBlocked || userRestrictions.isTimeout) return
if (isSolutionCorrect(ctx.session.captcha_data!.solution, ctx.message.text)) {
await captchaPassed(ctx, db, cfg)
2024-10-19 10:00:35 +00:00
} else {
2024-10-23 16:09:10 +00:00
ctx.session.captcha_data!.tries_failed++
2024-10-25 17:35:12 +00:00
await ctx.api.deleteMessage(ctx.chatId, ctx.msg.message_id)
2024-10-23 16:09:10 +00:00
if (ctx.session.captcha_data!.tries_failed > CAPTCHA_FAILS_LIMIT) {
2024-10-19 10:00:35 +00:00
ctx.reply(LangManager.getLang(ctx.msg.from.language_code)
.replies.captcha.failed(TIMEOUT_AFTER_FAIL_MINS))
2024-10-25 17:35:12 +00:00
await ctx.api.deleteMessage(ctx.chatId, ctx.session.captcha_data!.message_id)
2024-10-24 08:35:13 +00:00
// TODO: Add timeout
2024-10-19 10:00:35 +00:00
}
}
2024-10-23 16:09:10 +00:00
return
2024-10-19 10:00:35 +00:00
}
2024-10-23 16:09:10 +00:00
export { checkCaptchaSolution, initUserCaptcha }