136 lines
4.4 KiB
TypeScript
136 lines
4.4 KiB
TypeScript
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<Database>, 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<Ctx, "message:text">, db: Kysely<Database>) => {
|
|
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.setMinutes(timeoutUntil.getMinutes() + TIMEOUT_AFTER_FAIL_MINS)
|
|
|
|
await db.updateTable('users').set({
|
|
timeout_until: timeoutUntil,
|
|
is_timeout: true,
|
|
}).where('tg_id', '=', ctx.from.id).execute()
|
|
|
|
await ctx.reply(ctx.t("captcha-failed",
|
|
{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<Database>, 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<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
|
|
|
|
const user = users[0]
|
|
const userRestrictions = await checkUserRestrictions(db, user)
|
|
if (userRestrictions.isBlocked) return
|
|
if (userRestrictions.isTimeout) {
|
|
await ctx.reply("You were timed out")
|
|
return
|
|
}
|
|
console.log("User Info: ", user)
|
|
|
|
if (isSolutionCorrect(ctx.session.captcha_data!.solution, ctx.message.text)) {
|
|
await captchaPassed(ctx, db, cfg)
|
|
} else {
|
|
await captchaFailed(ctx, db)
|
|
}
|
|
}
|
|
|