108 lines
3.7 KiB
TypeScript
108 lines
3.7 KiB
TypeScript
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<Ctx>) => {
|
|
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<Ctx, "message:text">) => {
|
|
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<Ctx, "message:text">): Promise<boolean> => {
|
|
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, ":new_chat_members">) => {
|
|
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 }
|