-
-
Save poeti8/d84dfc4538510366a2d89294ff52b4ae to your computer and use it in GitHub Desktop.
import { getUser, verifyCaptcha } from '../stores/user'; | |
import { Composer } from 'telegraf'; | |
import type { ExtendedContext } from '../typings/context'; | |
import { logError } from '../utils/log'; | |
import { lrm } from '../utils/html'; | |
import { telegram } from '../bot'; | |
// Time to answer the math question, in seconds | |
const TIME_TO_ANSWER = 60; | |
// How long should the user stay banned, in seconds | |
const BAN_DURATION = 60; | |
// How long after join the bot is allowed to show captcha, in seconds | |
// e.g. when set to 30, if bot gets the join message after 1 minute, it would ignore and not show captcha | |
// useful for when the bot is down and you don't want to take old joins into affect | |
const AFFECTIVE_JOIN_TIME = Infinity; | |
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]; | |
const calc = { | |
'+': (a: number, b: number) => a + b, | |
'-': (a: number, b: number) => a - b, | |
'*': (a: number, b: number) => a * b, | |
}; | |
const pick = <T>(list: T[]) => list[Math.floor(Math.random() * list.length)]; | |
const catchError = (err) => logError('[captcha] ' + err.message); | |
type Challenge = { | |
userId: number; | |
mathQuestion: string; | |
mathAnswer: number; | |
timeout: number; | |
groups: { id?: number; messageId: number }[]; | |
}; | |
const kickOutMember = ( | |
challenges: Challenge[], | |
curerntChallenge: Challenge | |
): number => { | |
return (setTimeout(() => { | |
// Delete from active challenges | |
const foundChallengeIndex = challenges.indexOf(curerntChallenge); | |
if (foundChallengeIndex >= 0) { | |
challenges.splice(foundChallengeIndex, 1); | |
} | |
// Check if user is banned already | |
const user = getUser({ id: curerntChallenge.userId }); | |
// For each group: | |
curerntChallenge.groups.forEach((group) => { | |
// Kick user | |
if (group.id && !user.banned) { | |
telegram | |
.kickChatMember( | |
group.id, | |
curerntChallenge.userId, | |
Date.now() / 1000 + BAN_DURATION | |
) | |
.catch(catchError); | |
} | |
// Delete message | |
if (group.id && group.messageId) { | |
telegram.deleteMessage(group.id, group.messageId).catch(catchError); | |
} | |
}); | |
}, TIME_TO_ANSWER * 1000) as unknown) as number; | |
}; | |
const createMath = (): { answer: number; question: string } => { | |
let a: number; | |
let b: number; | |
let op: keyof typeof calc; | |
let result: number; | |
do { | |
a = pick(numbers); | |
b = pick(numbers); | |
op = pick(Object.keys(calc)) as keyof typeof calc; | |
result = calc[op](a, b); | |
} while (result === 0); | |
return { | |
answer: result, | |
question: `${a} ${op} ${b}`, | |
}; | |
}; | |
const activeChallenges: Challenge[] = []; | |
export = Composer.mount('message', async (ctx: ExtendedContext, next) => { | |
// If chat is private ignore | |
if (ctx.chat?.type === 'private') { | |
return next(); | |
} | |
const members = ctx.message?.new_chat_members?.filter( | |
(user) => user.username !== ctx.me | |
); | |
// If a text message | |
if (!members || members.length === 0) { | |
// Check if there's a challenge for this user | |
const foundChallenge = activeChallenges.find( | |
(activeChallenge) => activeChallenge.userId === ctx.from?.id | |
); | |
if (!foundChallenge) { | |
return next(); | |
} | |
// If answer is right | |
const isAnswerRight = | |
Number(ctx.message?.text) === foundChallenge.mathAnswer; | |
if (isAnswerRight) { | |
// For each challange | |
// 1. Clear timeout | |
clearTimeout(foundChallenge.timeout); | |
// 2. Delete question | |
foundChallenge.groups.forEach((group) => { | |
if (group.id && group.messageId) { | |
telegram.deleteMessage(group.id, group.messageId).catch(catchError); | |
} | |
}); | |
// 3. Remove user and challange from active challenges | |
activeChallenges.splice(activeChallenges.indexOf(foundChallenge), 1); | |
// 4. Verify user | |
await verifyCaptcha({ id: ctx.from?.id }).catch(catchError); | |
} | |
// Delete answer | |
if (ctx.message?.message_id) { | |
ctx.deleteMessage(ctx.message?.message_id).catch(catchError); | |
} | |
return next(); | |
} | |
// If new member | |
await Promise.all( | |
members.map(async (member) => { | |
// If join time is old, ignore | |
const shouldIgnoreJoin = | |
Math.abs((ctx.message?.date || 0) - Date.now() / 1000) > | |
AFFECTIVE_JOIN_TIME; | |
if (shouldIgnoreJoin) return; | |
// Get user and check if already is verified | |
const user = getUser({ id: member.id }); | |
if (user.captcha) return; | |
// Check if already has shown | |
const challenge = activeChallenges.find( | |
(activeChallenge) => activeChallenge.userId === member.id | |
) as Challenge; | |
// Create or use already created math | |
const math = createMath(); | |
const mathAnswer = challenge ? challenge.mathAnswer : math.answer; | |
const mathQuestion = challenge ? challenge.mathQuestion : math.question; | |
// Send message | |
const message = await ctx.replyWithHTML( | |
`${lrm}<a href="tg://user?id=${member.id}">${member.first_name}</a> [<code>${member.id}</code>] ` + | |
'please solve the following arithmetic operation in ' + | |
`${TIME_TO_ANSWER} seconds and read our rules(!) right after:\n` + | |
`<b>${mathQuestion} = ?</b>`, | |
{ disable_notification: true } | |
); | |
const challengeGroup = { | |
id: ctx.chat?.id, | |
messageId: message.message_id, | |
}; | |
// Create a channel to use when there's none | |
const newChallenge: Challenge = { | |
groups: [challengeGroup], | |
mathAnswer, | |
mathQuestion, | |
userId: member.id, | |
timeout: 0, | |
}; | |
// Clear previous timeouts | |
if (challenge) { | |
clearTimeout(challenge.timeout); | |
} | |
// Create kick out timeout | |
const timeout = kickOutMember( | |
activeChallenges, | |
challenge || newChallenge | |
); | |
if (challenge) { | |
challenge.groups.push(challengeGroup); | |
challenge.timeout = timeout; | |
} else { | |
newChallenge.timeout = timeout; | |
activeChallenges.push(newChallenge); | |
} | |
}) | |
).catch(catchError); | |
return next(); | |
}); |
Is there a way to send the captcha without a notification? It's a nuisance, especially in the "Devs Share" chat where notifications are welcome.
I'm familiar with the Telegram API but not the Javascipt library used here, so I'm unable to make a patch.
https://core.telegram.org/bots/api#sendmessage "disable_notification"
@torresjrjr Thanks for the suggestion, added.
this seems to be the only working plugin at the moment after the bot was updated and the plugin page doesn't seem to be updated either as it still shows the old way and the api gives errors. i'm not 100% familiar with typescript and i realize this would be a big ask, but would it be possible for you to upload a plugin script that simply responds to a phrase? it would show basic API functionality and would help a lot in getting started setting up my own plugins. just something that sees a certain specific message and replies to it. that way i could figure out how to do different types of stuff from other non-bot related scripts.. in responce to events, messages, or various commands. it would be extremely helpful in learning.. and i feel like something like that might help other people than just myself
This is now part of https://github.com/thedevs-network/the-guard-bot-plugins
This plugin adds a simple captcha to The Guard bot.
It is synced across groups and kicks the user if they don't answer within the specified time.
Preview: