Переглянути джерело

telegram: gate /start, /help, /status behind dmPolicy (#894)

The bot command handlers bypassed access control — they responded to
any DM user regardless of dmPolicy, leaking bot presence and
contradicting ACCESS.md's "Drop silently. No reply." contract for
allowlist mode.

Add dmCommandGate() that applies the same disabled/allowlist checks
as gate() without the pairing side effects, and route all three
handlers through it. Also prune expired pending codes before /status
iterates them.

Fixes #854

Co-authored-by: Claude <noreply@anthropic.com>
Noah Zweben 4 місяців тому
батько
коміт
5a71459c03
1 змінених файлів з 19 додано та 13 видалено
  1. 19 13
      external_plugins/telegram/server.ts

+ 19 - 13
external_plugins/telegram/server.ts

@@ -284,6 +284,19 @@ function gate(ctx: Context): GateResult {
   return { action: 'drop' }
 }
 
+// Like gate() but for bot commands: no pairing side effects, just allow/drop.
+function dmCommandGate(ctx: Context): { access: Access; senderId: string } | null {
+  if (ctx.chat?.type !== 'private') return null
+  if (!ctx.from) return null
+  const senderId = String(ctx.from.id)
+  const access = loadAccess()
+  const pruned = pruneExpired(access)
+  if (pruned) saveAccess(access)
+  if (access.dmPolicy === 'disabled') return null
+  if (access.dmPolicy === 'allowlist' && !access.allowFrom.includes(senderId)) return null
+  return { access, senderId }
+}
+
 function isMentioned(ctx: Context, extraPatterns?: string[]): boolean {
   const entities = ctx.message?.entities ?? ctx.message?.caption_entities ?? []
   const text = ctx.message?.text ?? ctx.message?.caption ?? ''
@@ -669,12 +682,7 @@ setInterval(() => {
 // the gate's behavior for unrecognized groups.
 
 bot.command('start', async ctx => {
-  if (ctx.chat?.type !== 'private') return
-  const access = loadAccess()
-  if (access.dmPolicy === 'disabled') {
-    await ctx.reply(`This bot isn't accepting new connections.`)
-    return
-  }
+  if (!dmCommandGate(ctx)) return
   await ctx.reply(
     `This bot bridges Telegram to a Claude Code session.\n\n` +
     `To pair:\n` +
@@ -685,7 +693,7 @@ bot.command('start', async ctx => {
 })
 
 bot.command('help', async ctx => {
-  if (ctx.chat?.type !== 'private') return
+  if (!dmCommandGate(ctx)) return
   await ctx.reply(
     `Messages you send here route to a paired Claude Code session. ` +
     `Text and photos are forwarded; replies and reactions come back.\n\n` +
@@ -695,14 +703,12 @@ bot.command('help', async ctx => {
 })
 
 bot.command('status', async ctx => {
-  if (ctx.chat?.type !== 'private') return
-  const from = ctx.from
-  if (!from) return
-  const senderId = String(from.id)
-  const access = loadAccess()
+  const gated = dmCommandGate(ctx)
+  if (!gated) return
+  const { access, senderId } = gated
 
   if (access.allowFrom.includes(senderId)) {
-    const name = from.username ? `@${from.username}` : senderId
+    const name = ctx.from!.username ? `@${ctx.from!.username}` : senderId
     await ctx.reply(`Paired as ${name}.`)
     return
   }