2025-08-02 04:21:26 +08:00
/ * *
* Telegram Webhook Handler
* Handles incoming Telegram messages and commands
* /
const express = require ( 'express' ) ;
const crypto = require ( 'crypto' ) ;
const axios = require ( 'axios' ) ;
const path = require ( 'path' ) ;
const fs = require ( 'fs' ) ;
const Logger = require ( '../../core/logger' ) ;
const ControllerInjector = require ( '../../utils/controller-injector' ) ;
class TelegramWebhookHandler {
constructor ( config = { } ) {
this . config = config ;
this . logger = new Logger ( 'TelegramWebhook' ) ;
this . sessionsDir = path . join ( _ _dirname , '../../data/sessions' ) ;
this . injector = new ControllerInjector ( ) ;
this . app = express ( ) ;
this . apiBaseUrl = 'https://api.telegram.org' ;
this . botUsername = null ; // Cache for bot username
this . _setupMiddleware ( ) ;
this . _setupRoutes ( ) ;
}
_setupMiddleware ( ) {
// Parse JSON for all requests
this . app . use ( express . json ( ) ) ;
}
_setupRoutes ( ) {
// Telegram webhook endpoint
this . app . post ( '/webhook/telegram' , this . _handleWebhook . bind ( this ) ) ;
2025-08-03 00:57:03 +08:00
2025-08-02 04:21:26 +08:00
// Health check endpoint
this . app . get ( '/health' , ( req , res ) => {
res . json ( { status : 'ok' , service : 'telegram-webhook' } ) ;
} ) ;
}
2025-08-03 00:57:03 +08:00
/ * *
* Generate network options for axios requests
* @ returns { Object } Network options object
* /
_getNetworkOptions ( ) {
const options = { } ;
if ( this . config . forceIPv4 ) {
options . family = 4 ;
}
return options ;
}
2025-08-02 04:21:26 +08:00
async _handleWebhook ( req , res ) {
try {
const update = req . body ;
// Handle different update types
if ( update . message ) {
await this . _handleMessage ( update . message ) ;
} else if ( update . callback _query ) {
await this . _handleCallbackQuery ( update . callback _query ) ;
}
res . status ( 200 ) . send ( 'OK' ) ;
} catch ( error ) {
this . logger . error ( 'Webhook handling error:' , error . message ) ;
res . status ( 500 ) . send ( 'Internal Server Error' ) ;
}
}
async _handleMessage ( message ) {
const chatId = message . chat . id ;
const userId = message . from . id ;
const messageText = message . text ? . trim ( ) ;
if ( ! messageText ) return ;
// Check if user is authorized
if ( ! this . _isAuthorized ( userId , chatId ) ) {
this . logger . warn ( ` Unauthorized user/chat: ${ userId } / ${ chatId } ` ) ;
await this . _sendMessage ( chatId , '⚠️ You are not authorized to use this bot.' ) ;
return ;
}
// Handle /start command
if ( messageText === '/start' ) {
await this . _sendWelcomeMessage ( chatId ) ;
return ;
}
// Handle /help command
if ( messageText === '/help' ) {
await this . _sendHelpMessage ( chatId ) ;
return ;
}
// Parse command
const commandMatch = messageText . match ( /^\/cmd\s+([A-Z0-9]{8})\s+(.+)$/i ) ;
if ( ! commandMatch ) {
// Check if it's a direct command without /cmd prefix
const directMatch = messageText . match ( /^([A-Z0-9]{8})\s+(.+)$/ ) ;
if ( directMatch ) {
await this . _processCommand ( chatId , directMatch [ 1 ] , directMatch [ 2 ] ) ;
} else {
await this . _sendMessage ( chatId ,
'❌ Invalid format. Use:\n`/cmd <TOKEN> <command>`\n\nExample:\n`/cmd ABC12345 analyze this code`' ,
{ parse _mode : 'Markdown' } ) ;
}
return ;
}
const token = commandMatch [ 1 ] . toUpperCase ( ) ;
const command = commandMatch [ 2 ] ;
await this . _processCommand ( chatId , token , command ) ;
}
async _processCommand ( chatId , token , command ) {
// Find session by token
const session = await this . _findSessionByToken ( token ) ;
if ( ! session ) {
await this . _sendMessage ( chatId ,
'❌ Invalid or expired token. Please wait for a new task notification.' ,
{ parse _mode : 'Markdown' } ) ;
return ;
}
// Check if session is expired
if ( session . expiresAt < Math . floor ( Date . now ( ) / 1000 ) ) {
await this . _sendMessage ( chatId ,
'❌ Token has expired. Please wait for a new task notification.' ,
{ parse _mode : 'Markdown' } ) ;
await this . _removeSession ( session . id ) ;
return ;
}
try {
// Inject command into tmux session
const tmuxSession = session . tmuxSession || 'default' ;
await this . injector . injectCommand ( command , tmuxSession ) ;
// Send confirmation
await this . _sendMessage ( chatId ,
` ✅ *Command sent successfully* \n \n 📝 *Command:* ${ command } \n 🖥️ *Session:* ${ tmuxSession } \n \n Claude is now processing your request... ` ,
{ parse _mode : 'Markdown' } ) ;
// Log command execution
this . logger . info ( ` Command injected - User: ${ chatId } , Token: ${ token } , Command: ${ command } ` ) ;
} catch ( error ) {
this . logger . error ( 'Command injection failed:' , error . message ) ;
await this . _sendMessage ( chatId ,
` ❌ *Command execution failed:* ${ error . message } ` ,
{ parse _mode : 'Markdown' } ) ;
}
}
async _handleCallbackQuery ( callbackQuery ) {
const chatId = callbackQuery . message . chat . id ;
const data = callbackQuery . data ;
// Answer callback query to remove loading state
await this . _answerCallbackQuery ( callbackQuery . id ) ;
if ( data . startsWith ( 'personal:' ) ) {
const token = data . split ( ':' ) [ 1 ] ;
// Send personal chat command format
await this . _sendMessage ( chatId ,
` 📝 *Personal Chat Command Format:* \n \n \` /cmd ${ token } <your command> \` \n \n *Example:* \n \` /cmd ${ token } please analyze this code \` \n \n 💡 *Copy and paste the format above, then add your command!* ` ,
{ parse _mode : 'Markdown' } ) ;
} else if ( data . startsWith ( 'group:' ) ) {
const token = data . split ( ':' ) [ 1 ] ;
// Send group chat command format with @bot_name
const botUsername = await this . _getBotUsername ( ) ;
await this . _sendMessage ( chatId ,
` 👥 *Group Chat Command Format:* \n \n \` @ ${ botUsername } /cmd ${ token } <your command> \` \n \n *Example:* \n \` @ ${ botUsername } /cmd ${ token } please analyze this code \` \n \n 💡 *Copy and paste the format above, then add your command!* ` ,
{ parse _mode : 'Markdown' } ) ;
} else if ( data . startsWith ( 'session:' ) ) {
const token = data . split ( ':' ) [ 1 ] ;
// For backward compatibility - send help message for old callback buttons
await this . _sendMessage ( chatId ,
` 📝 *How to send a command:* \n \n Type: \n \` /cmd ${ token } <your command> \` \n \n Example: \n \` /cmd ${ token } please analyze this code \` \n \n 💡 *Tip:* New notifications have a button that auto-fills the command for you! ` ,
{ parse _mode : 'Markdown' } ) ;
}
}
async _sendWelcomeMessage ( chatId ) {
const message = ` 🤖 *Welcome to Claude Code Remote Bot!* \n \n ` +
` I'll notify you when Claude completes tasks or needs input. \n \n ` +
` When you receive a notification with a token, you can send commands back using: \n ` +
` \` /cmd <TOKEN> <your command> \` \n \n ` +
` Type /help for more information. ` ;
await this . _sendMessage ( chatId , message , { parse _mode : 'Markdown' } ) ;
}
async _sendHelpMessage ( chatId ) {
const message = ` 📚 *Claude Code Remote Bot Help* \n \n ` +
` *Commands:* \n ` +
` • \` /start \` - Welcome message \n ` +
` • \` /help \` - Show this help \n ` +
` • \` /cmd <TOKEN> <command> \` - Send command to Claude \n \n ` +
` *Example:* \n ` +
` \` /cmd ABC12345 analyze the performance of this function \` \n \n ` +
` *Tips:* \n ` +
` • Tokens are case-insensitive \n ` +
` • Tokens expire after 24 hours \n ` +
` • You can also just type \` TOKEN command \` without /cmd ` ;
await this . _sendMessage ( chatId , message , { parse _mode : 'Markdown' } ) ;
}
_isAuthorized ( userId , chatId ) {
// Check whitelist
const whitelist = this . config . whitelist || [ ] ;
if ( whitelist . includes ( String ( chatId ) ) || whitelist . includes ( String ( userId ) ) ) {
return true ;
}
// If no whitelist configured, allow configured chat/user
if ( whitelist . length === 0 ) {
const configuredChatId = this . config . chatId || this . config . groupId ;
if ( configuredChatId && String ( chatId ) === String ( configuredChatId ) ) {
return true ;
}
}
return false ;
}
async _getBotUsername ( ) {
if ( this . botUsername ) {
return this . botUsername ;
}
try {
const response = await axios . get (
2025-08-03 00:57:03 +08:00
` ${ this . apiBaseUrl } /bot ${ this . config . botToken } /getMe ` ,
this . _getNetworkOptions ( )
2025-08-02 04:21:26 +08:00
) ;
if ( response . data . ok && response . data . result . username ) {
this . botUsername = response . data . result . username ;
return this . botUsername ;
}
} catch ( error ) {
this . logger . error ( 'Failed to get bot username:' , error . message ) ;
}
// Fallback to configured username or default
return this . config . botUsername || 'claude_remote_bot' ;
}
async _findSessionByToken ( token ) {
const files = fs . readdirSync ( this . sessionsDir ) ;
for ( const file of files ) {
if ( ! file . endsWith ( '.json' ) ) continue ;
const sessionPath = path . join ( this . sessionsDir , file ) ;
try {
const session = JSON . parse ( fs . readFileSync ( sessionPath , 'utf8' ) ) ;
if ( session . token === token ) {
return session ;
}
} catch ( error ) {
this . logger . error ( ` Failed to read session file ${ file } : ` , error . message ) ;
}
}
return null ;
}
async _removeSession ( sessionId ) {
const sessionFile = path . join ( this . sessionsDir , ` ${ sessionId } .json ` ) ;
if ( fs . existsSync ( sessionFile ) ) {
fs . unlinkSync ( sessionFile ) ;
this . logger . debug ( ` Session removed: ${ sessionId } ` ) ;
}
}
async _sendMessage ( chatId , text , options = { } ) {
try {
await axios . post (
` ${ this . apiBaseUrl } /bot ${ this . config . botToken } /sendMessage ` ,
{
chat _id : chatId ,
text : text ,
... options
2025-08-03 00:57:03 +08:00
} ,
this . _getNetworkOptions ( )
2025-08-02 04:21:26 +08:00
) ;
} catch ( error ) {
this . logger . error ( 'Failed to send message:' , error . response ? . data || error . message ) ;
}
}
async _answerCallbackQuery ( callbackQueryId , text = '' ) {
try {
await axios . post (
` ${ this . apiBaseUrl } /bot ${ this . config . botToken } /answerCallbackQuery ` ,
{
callback _query _id : callbackQueryId ,
text : text
2025-08-03 00:57:03 +08:00
} ,
this . _getNetworkOptions ( )
2025-08-02 04:21:26 +08:00
) ;
} catch ( error ) {
this . logger . error ( 'Failed to answer callback query:' , error . response ? . data || error . message ) ;
}
}
async setWebhook ( webhookUrl ) {
try {
const response = await axios . post (
` ${ this . apiBaseUrl } /bot ${ this . config . botToken } /setWebhook ` ,
{
url : webhookUrl ,
allowed _updates : [ 'message' , 'callback_query' ]
2025-08-03 00:57:03 +08:00
} ,
this . _getNetworkOptions ( )
2025-08-02 04:21:26 +08:00
) ;
2025-08-03 00:57:03 +08:00
2025-08-02 04:21:26 +08:00
this . logger . info ( 'Webhook set successfully:' , response . data ) ;
return response . data ;
} catch ( error ) {
this . logger . error ( 'Failed to set webhook:' , error . response ? . data || error . message ) ;
throw error ;
}
}
start ( port = 3000 ) {
this . app . listen ( port , ( ) => {
this . logger . info ( ` Telegram webhook server started on port ${ port } ` ) ;
} ) ;
}
}
2025-08-03 00:57:03 +08:00
module . exports = TelegramWebhookHandler ;