feat(telegram-pairing): accept bare 4-digit codes

Require the message to be exactly the 4 digits (optionally prefixed by
@botname). Loose matches like "my pin is 0349" are rejected to avoid false
positives from chat traffic that happens to contain a 4-digit number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Koshkoshinsk
2026-04-13 12:27:06 +00:00
parent 2017589683
commit 2454444f2e
3 changed files with 26 additions and 14 deletions

View File

@@ -45,15 +45,20 @@ describe('extractAddressedText', () => {
});
describe('extractCode', () => {
it('finds 4-digit code after @botname', () => {
it('accepts a bare 4-digit code', () => {
expect(extractCode('0349', 'nanobot')).toBe('0349');
});
it('accepts 4-digit code after @botname', () => {
expect(extractCode('@nanobot 0042', 'nanobot')).toBe('0042');
});
it('rejects non-4-digit numbers', () => {
expect(extractCode('@nanobot 12345', 'nanobot')).toBeNull();
expect(extractCode('@nanobot 12', 'nanobot')).toBeNull();
expect(extractCode('12345', 'nanobot')).toBeNull();
});
it('returns null without addressing', () => {
expect(extractCode('1234', 'nanobot')).toBeNull();
it('rejects loose matches with surrounding text', () => {
expect(extractCode('my pin is 0349', 'nanobot')).toBeNull();
expect(extractCode('0349 thanks', 'nanobot')).toBeNull();
});
});
@@ -103,7 +108,7 @@ describe('tryConsume', () => {
expect(out).toBeNull();
});
it('returns null without @botname addressing', async () => {
it('matches a bare code without @botname addressing', async () => {
const r = await createPairing('main');
const out = await tryConsume({
text: r.code,
@@ -111,7 +116,8 @@ describe('tryConsume', () => {
platformId: 'x',
isGroup: false,
});
expect(out).toBeNull();
expect(out).not.toBeNull();
expect(out!.status).toBe('consumed');
});
it('cannot be consumed twice', async () => {

View File

@@ -3,10 +3,12 @@
*
* BotFather hands out tokens with no user binding, so anyone who guesses the
* bot's username can DM it. Pairing closes that gap: setup creates a one-time
* 4-digit code and the operator echoes it back as `@botname CODE` from the
* chat they want to register. The inbound interceptor in telegram.ts matches
* the code and records the chat (with admin_user_id) before it ever reaches
* the router.
* 4-digit code and the operator echoes it back from the chat they want to
* register. The message must be exactly the 4 digits (optionally prefixed by
* `@botname ` for groups with privacy ON) — arbitrary messages that happen to
* contain a 4-digit number do NOT match. The inbound interceptor in
* telegram.ts matches the code and records the chat (with admin_user_id)
* before it ever reaches the router.
*
* Storage is a JSON file at data/telegram-pairings.json — single-process,
* read-modify-write under an in-process mutex.
@@ -144,11 +146,15 @@ export function extractAddressedText(text: string, botUsername: string): string
return trimmed.slice(m[0].length).trim();
}
/** Find a 4-digit code in `@botname CODE`-style text. Returns null if none. */
/**
* Extract a pairing code from an inbound message. The message must be exactly
* 4 digits (optionally prefixed by `@botname `) — loose matches like
* "my pin is 1234" are rejected to avoid false positives from chatter.
*/
export function extractCode(text: string, botUsername: string): string | null {
const remainder = extractAddressedText(text, botUsername);
if (remainder === null) return null;
const m = remainder.match(/\b(\d{4})\b/);
const addressed = extractAddressedText(text, botUsername);
const candidate = (addressed !== null ? addressed : text).trim();
const m = candidate.match(/^(\d{4})$/);
return m ? m[1] : null;
}