Skip to content

Instantly share code, notes, and snippets.

@poeti8
Last active September 18, 2023 20:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save poeti8/d84dfc4538510366a2d89294ff52b4ae to your computer and use it in GitHub Desktop.
Save poeti8/d84dfc4538510366a2d89294ff52b4ae to your computer and use it in GitHub Desktop.
the-guard-captcha-plugin
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();
});
@poeti8
Copy link
Author

poeti8 commented Apr 25, 2021

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:

image

@torresjrjr
Copy link

torresjrjr commented May 3, 2021

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"

@poeti8
Copy link
Author

poeti8 commented May 4, 2021

@torresjrjr Thanks for the suggestion, added.

@Mrs-Feathers
Copy link

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

@C0rn3j
Copy link

C0rn3j commented Sep 18, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment