import crypto from 'crypto'; import * as textToImage from 'text-to-image'; const GEN_POOL = 'aceoxyABCEHKMOPTX3'; const RUS_POOL = 'асеохуАВСЕНКМОРТХЗ'; const ALL_RUS_POOL = 'абвгдежзийклмнопрстуфхцчшщьыэюяАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЫЬЭЮЯ'; const FONTS = ['calico_cyrillic.ttf', 'sawesome.ttf'] type Captcha = { key: string; value: string; img: string; instructionImg: string; createdAt: number; } const captchasPool = new Set(); const chooseLetter = (rusPool: boolean = false) => { const pool = rusPool ? RUS_POOL : ALL_RUS_POOL; return pool[Math.floor(Math.random() * pool.length)]; }; const chooseFont = () => FONTS[Math.floor(Math.random() * FONTS.length)]; const encodeLetter = (letter: string) => { const idx = RUS_POOL.indexOf(letter); if (idx < 0) return letter; return GEN_POOL[idx]; } const encodeString = (string: string) => Array.from(string).map(encodeLetter).join(''); const generate = async () => { const captcha: Captcha = { createdAt: Date.now(), key: crypto.randomUUID(), value: '', img: '', instructionImg: '', } for (let i = 0; i < 6; i++) { captcha.value += chooseLetter(); } captcha.img = await textToImage.generate(captcha.value, { fontSize: 32, bgColor: '#222', textColor: '#fff', fontPath: chooseFont(), margin: 0, maxWidth: 155, }); const hasAddon = Math.random() < 0.5; const addon = Math.random() < 0.5 ? 'ь' : 'ъ'; const toEnd = Math.random() < 0.5; const isLower = Math.random() < 0.5; const reverse = Math.random() < 0.5; if (reverse) { captcha.value = Array.from(captcha.value).reverse().join(''); } if (hasAddon) { if (toEnd) { captcha.value += addon; } else { captcha.value = addon + captcha.value; } } captcha.value = isLower ? captcha.value.toLocaleLowerCase() : captcha.value.toLocaleUpperCase(); captcha.instructionImg = await textToImage.generate( `Напиши этот текст ${isLower ? 'маленькими' : 'большими' } буквами ${reverse ? 'наоборот ' : '' }и ${hasAddon ? 'добавь' : 'не добавляй' } в ${toEnd ? 'конец' : 'начало' } ${addon === 'ъ' ? 'твёрдый' : 'мягкий' } знак:`, { fontSize: 16, bgColor: '#222', textColor: '#fff', margin: 0, fontPath: chooseFont(), maxWidth: 720, }); captchasPool.add(captcha); return captcha; }; const expire = () => { const expired = []; const nowMinusTime = Date.now() - 20000; for (const captcha of captchasPool) { if (captcha.createdAt < nowMinusTime) { expired.push(captcha); } } expired.forEach(c => captchasPool.delete(c)); } const check = (key: string | undefined, value: string | undefined) => { if (!key || !value) return false; expire(); for (const captcha of captchasPool) { if (captcha.key === key && captcha.value === value) { captchasPool.delete(captcha); return true; } } return false; } export const captchas = { generate, check, encodeString, };