/** * Pattern Matching Engine * * DSL syntax: * - lowercase letters a-z: variable placeholder. Same letter = same character. * Different letters = independent variables (may coincidentally be equal). * Matches [a-z0-9]. * - digits 0-9: variable placeholder (same rules as letters). Matches [a-z0-9]. * - `#`: identical-run marker. A maximal run of `#` matches a run of identical * characters. The first `#` starts a new group, subsequent `#`s backreference it. * Matches [a-z0-9] (valid Telegram username chars excluding literal `_`). * - `_`: literal underscore. * - `\X`: literal character X (escape for when you need a literal a-z / 0-9 / #). * - `[literal]`: a literal block — everything between `[` and `]` is treated as * literal text (no variable interpretation). Useful for fixed words like * `[vip]ab` which matches "vip" + two variables (vipdd, vipx1, vipab). * - any other character: treated as a literal (regex-escaped). * * Examples (all match the user's listed usernames): * - "a_b_0" -> ^([a-z0-9])_([a-z0-9])_([a-z0-9])$ matches e_y_3, e_e_h, a_k_v * - "ab###" -> ^([a-z0-9])([a-z0-9])([a-z0-9])\3\3$ matches av000 * - "###ab" -> ^([a-z0-9])\1\1([a-z0-9])([a-z0-9])$ matches tttxy * - "a###a" -> ^([a-z0-9])([a-z0-9])\2\2\1$ matches u000u * - "a###b" -> ^([a-z0-9])([a-z0-9])\2\2([a-z0-9])$ matches u000q, kjjjf * - "aa###" -> ^([a-z0-9])\1([a-z0-9])\3\3$ matches aaccc * - "###bb" -> ^([a-z0-9])\1\1([a-z0-9])\4$ matches gggbb * - "[vip]ab" -> ^vip([a-z0-9])([a-z0-9])$ matches vipdd, vipx1 */ export interface CompiledPattern { regex: string; flags: string; variableCount: number; length: number; } export interface PatternCompileError { error: string; } const USERNAME_CHAR = "[a-z0-9]"; function escapeRegex(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Compile a DSL expression into an anchored regex source string. */ export function compilePattern(expr: string): CompiledPattern { if (!expr || expr.length === 0) { throw new Error("Pattern expression cannot be empty"); } if (expr.length < 3) { throw new Error("Pattern is too short (Telegram usernames are 5-32 chars, patterns should be at least 3)"); } if (expr.length > 32) { throw new Error("Pattern is too long (Telegram usernames are max 32 chars)"); } let regex = ""; const varGroups = new Map(); let groupCount = 0; let i = 0; while (i < expr.length) { const ch = expr[i]; // Escape sequence -> literal if (ch === "\\" && i + 1 < expr.length) { regex += escapeRegex(expr[i + 1]); i += 2; continue; } // Literal block [....] -> everything inside is literal text if (ch === "[") { const end = expr.indexOf("]", i + 1); if (end === -1) { throw new Error("Unclosed [ literal block — missing ]"); } const literalContent = expr.slice(i + 1, end); if (literalContent.length === 0) { throw new Error("Empty [ ] literal block"); } regex += escapeRegex(literalContent); i = end + 1; continue; } // Identical run of # if (ch === "#") { let runLen = 0; while (i < expr.length && expr[i] === "#") { runLen++; i++; } groupCount++; const groupNum = groupCount; regex += `(${USERNAME_CHAR})`; for (let r = 1; r < runLen; r++) { regex += `\\${groupNum}`; } continue; } // Variable (letter or digit) if (/[a-z0-9]/.test(ch)) { if (varGroups.has(ch)) { regex += `\\${varGroups.get(ch)}`; } else { groupCount++; varGroups.set(ch, groupCount); regex += `(${USERNAME_CHAR})`; } i++; continue; } // Literal regex += escapeRegex(ch); i++; } return { regex: `^${regex}$`, flags: "i", variableCount: groupCount, length: expr.length, }; } /** * Build a RegExp from a compiled pattern. */ export function buildRegExp(compiled: CompiledPattern): RegExp { return new RegExp(compiled.regex, compiled.flags); } /** * Test whether a username (without @) matches a compiled pattern. */ export function matchesPattern(username: string, compiled: CompiledPattern): boolean { const re = buildRegExp(compiled); return re.test(username); } /** * Validate a Telegram username (basic rules). * - 5-32 chars * - a-z, 0-9, _ only * - must start with a letter * - no leading/trailing underscore * - no consecutive underscores */ export function isValidTelegramUsername(username: string): boolean { if (!username) return false; if (username.length < 5 || username.length > 32) return false; if (!/^[a-z]/.test(username)) return false; if (!/^[a-z0-9_]+$/.test(username)) return false; if (username.startsWith("_") || username.endsWith("_")) return false; if (username.includes("__")) return false; return true; } /** * Generate example usernames that match a pattern (for preview in the dashboard). * Returns up to `count` examples. */ export function generateExamples(expr: string, count = 5): string[] { let compiled: CompiledPattern; try { compiled = compilePattern(expr); } catch { return []; } const re = buildRegExp(compiled); const examples: string[] = []; const seen = new Set(); // Deterministic generation: try a set of known-matching seeds + random attempts. // We brute-force over the pattern structure by building candidates from the DSL. const candidates = generateCandidates(expr); for (const c of candidates) { if (seen.has(c)) continue; seen.add(c); if (re.test(c) && isValidTelegramUsername(c)) { examples.push(c); if (examples.length >= count) break; } } // Random fill if not enough. let attempts = 0; while (examples.length < count && attempts < 2000) { attempts++; const candidate = randomUsernameForPattern(expr); if (candidate && !seen.has(candidate) && re.test(candidate) && isValidTelegramUsername(candidate)) { seen.add(candidate); examples.push(candidate); } } return examples; } /** * Build a small set of deterministic candidate usernames from the DSL expression * by substituting concrete values for each variable / run. */ function generateCandidates(expr: string): string[] { // Identify variable slots and run slots. const varNames = new Set(); const runs: { start: number; len: number; varName: string }[] = []; let i = 0; const structure: { type: "var" | "run" | "literal"; value: string; runLen?: number; varName?: string }[] = []; while (i < expr.length) { const ch = expr[i]; if (ch === "\\" && i + 1 < expr.length) { structure.push({ type: "literal", value: expr[i + 1] }); i += 2; continue; } if (ch === "[") { const end = expr.indexOf("]", i + 1); if (end === -1) break; structure.push({ type: "literal", value: expr.slice(i + 1, end) }); i = end + 1; continue; } if (ch === "#") { let runLen = 0; let j = i; while (j < expr.length && expr[j] === "#") { runLen++; j++; } const varName = `__run${runs.length}`; runs.push({ start: i, len: runLen, varName }); varNames.add(varName); structure.push({ type: "run", value: "", runLen, varName }); i = j; continue; } if (/[a-z0-9]/.test(ch)) { varNames.add(ch); structure.push({ type: "var", value: "", varName: ch }); i++; continue; } structure.push({ type: "literal", value: ch }); i++; } const vars = Array.from(varNames); // Letter pool for variables (avoid _ for vars since _ is literal). const letterPool = "abcdefghijklmnopqrstuvwxyz".split(""); // For runs we allow letters and digits. const runPool = "abcdefghijklmnopqrstuvwxyz0123456789".split(""); const results: string[] = []; // Generate a few combinations. const combos: Record[] = []; const maxCombos = 8; const pick = (arr: T[], n: number): T => arr[n % arr.length]; for (let n = 0; n < maxCombos; n++) { const combo: Record = {}; vars.forEach((v, idx) => { const isRun = v.startsWith("__run"); const pool = isRun ? runPool : letterPool; combo[v] = pick(pool, n + idx * 3); }); combos.push(combo); } for (const combo of combos) { let s = ""; for (const part of structure) { if (part.type === "literal") { s += part.value; } else if (part.type === "var") { s += combo[part.varName!]; } else if (part.type === "run") { s += combo[part.varName!].repeat(part.runLen!); } } results.push(s); } return results; } function randomUsernameForPattern(expr: string): string | null { const structure: { type: "var" | "run" | "literal"; value: string; runLen?: number; varName?: string }[] = []; const varNames = new Set(); let i = 0; let runIdx = 0; while (i < expr.length) { const ch = expr[i]; if (ch === "\\" && i + 1 < expr.length) { structure.push({ type: "literal", value: expr[i + 1] }); i += 2; continue; } if (ch === "[") { const end = expr.indexOf("]", i + 1); if (end === -1) break; structure.push({ type: "literal", value: expr.slice(i + 1, end) }); i = end + 1; continue; } if (ch === "#") { let runLen = 0; let j = i; while (j < expr.length && expr[j] === "#") { runLen++; j++; } const varName = `__run${runIdx++}`; varNames.add(varName); structure.push({ type: "run", value: "", runLen, varName }); i = j; continue; } if (/[a-z0-9]/.test(ch)) { varNames.add(ch); structure.push({ type: "var", value: "", varName: ch }); i++; continue; } structure.push({ type: "literal", value: ch }); i++; } const letters = "abcdefghijklmnopqrstuvwxyz"; const alnum = "abcdefghijklmnopqrstuvwxyz0123456789"; const rand = (arr: T[]) => arr[Math.floor(Math.random() * arr.length)]; const combo: Record = {}; for (const v of varNames) { const pool = v.startsWith("__run") ? alnum : letters; combo[v] = rand(pool.split("")); } let s = ""; for (const part of structure) { if (part.type === "literal") s += part.value; else if (part.type === "var") s += combo[part.varName!]; else if (part.type === "run") s += combo[part.varName!].repeat(part.runLen!); } return s; }