mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-05-19 21:02:40 +00:00
feat(telegram,discord): inline buttons for permission approval (#945)
Replace "Reply 'yes abcde' to allow" text instruction with native
inline buttons (Telegram InlineKeyboard, Discord ButtonBuilder).
One tap to approve/deny instead of typing a 5-char ID.
- Telegram: callback_query handler with allowFrom gate, edits message
to show outcome and remove buttons after decision
- Discord: interactionCreate handler with allowFrom gate, updates
interaction with outcome and clears components
- Text-reply path (PERMISSION_REPLY_RE) kept as fallback
- Bump both plugins to v0.0.3
🏠 Remote-Dev: homespace
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
CallToolRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { z } from 'zod'
|
||||
import { Bot, GrammyError, InputFile, type Context } from 'grammy'
|
||||
import { Bot, GrammyError, InlineKeyboard, InputFile, type Context } from 'grammy'
|
||||
import type { ReactionTypeEmoji } from 'grammy/types'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, renameSync, realpathSync, chmodSync } from 'fs'
|
||||
@@ -399,10 +399,12 @@ mcp.setNotificationHandler(
|
||||
const text =
|
||||
`🔐 Permission request [${request_id}]\n` +
|
||||
`${tool_name}: ${description}\n` +
|
||||
`${input_preview}\n\n` +
|
||||
`Reply "yes ${request_id}" to allow or "no ${request_id}" to deny.`
|
||||
`${input_preview}`
|
||||
const keyboard = new InlineKeyboard()
|
||||
.text('✅ Allow', `perm:allow:${request_id}`)
|
||||
.text('❌ Deny', `perm:deny:${request_id}`)
|
||||
for (const chat_id of access.allowFrom) {
|
||||
void bot.api.sendMessage(chat_id, text).catch(e => {
|
||||
void bot.api.sendMessage(chat_id, text, { reply_markup: keyboard }).catch(e => {
|
||||
process.stderr.write(`permission_request send to ${chat_id} failed: ${e}\n`)
|
||||
})
|
||||
}
|
||||
@@ -683,6 +685,37 @@ bot.command('status', async ctx => {
|
||||
await ctx.reply(`Not paired. Send me a message to get a pairing code.`)
|
||||
})
|
||||
|
||||
// Inline-button handler for permission requests. Callback data is
|
||||
// `perm:allow:<id>` or `perm:deny:<id>` — set when the request was sent.
|
||||
// Security mirrors the text-reply path: allowFrom must contain the sender.
|
||||
bot.on('callback_query:data', async ctx => {
|
||||
const data = ctx.callbackQuery.data
|
||||
const m = /^perm:(allow|deny):([a-km-z]{5})$/.exec(data)
|
||||
if (!m) {
|
||||
await ctx.answerCallbackQuery().catch(() => {})
|
||||
return
|
||||
}
|
||||
const access = loadAccess()
|
||||
const senderId = String(ctx.from.id)
|
||||
if (!access.allowFrom.includes(senderId)) {
|
||||
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
||||
return
|
||||
}
|
||||
const [, behavior, request_id] = m
|
||||
void mcp.notification({
|
||||
method: 'notifications/claude/channel/permission',
|
||||
params: { request_id, behavior },
|
||||
})
|
||||
const label = behavior === 'allow' ? '✅ Allowed' : '❌ Denied'
|
||||
await ctx.answerCallbackQuery({ text: label }).catch(() => {})
|
||||
// Replace buttons with the outcome so the same request can't be answered
|
||||
// twice and the chat history shows what was chosen.
|
||||
const msg = ctx.callbackQuery.message
|
||||
if (msg && 'text' in msg && msg.text) {
|
||||
await ctx.editMessageText(`${msg.text}\n\n${label}`).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
bot.on('message:text', async ctx => {
|
||||
await handleInbound(ctx, ctx.message.text, undefined)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user