Skip to main content

Telegram Integration

The telegram-channel plugin connects a Telegram bot to Codebolt using grammy. It supports text messages, photos, documents, voice, and video — with optional group filtering and config persistence for auto-reconnect.

Prerequisites

  1. Message @BotFather on Telegram
  2. Send /newbot and follow the prompts
  3. Copy the Bot Token (123456:ABCdef...)

Installation

codebolt plugin install telegram-channel

Configuration

FieldRequiredDescription
botTokenYesToken from @BotFather
agentIdNoDefault agent. Uses project default if omitted.
threadStrategyNoper-conversation (default) · per-user · per-message · single

The plugin persists config to the KV store and auto-reconnects on server restart if a valid config was previously saved.

Message routing

Groups and supergroups: Only messages that @mention the bot by username are forwarded. Other messages are ignored.

Private chats (DMs): All messages are forwarded.

User in group: @MyBot what's the weather?
→ forwarded (mention present)

User in group: what's the weather?
→ ignored (no mention)

User in private chat: what's the weather?
→ forwarded (DM, no mention required)

Media support

The plugin forwards media messages as attachment metadata:

Media typeForwarded as
Photo{ type: 'image', url: fileUrl }
Document{ type: 'file', url: fileUrl, name: fileName }
Voice{ type: 'file', url: fileUrl }
Video{ type: 'file', url: fileUrl }

Agent replies with image attachments are sent via bot.api.sendPhoto(). File attachments are sent via bot.api.sendDocument().

Reply format

Agent text replies are sent with HTML parse mode where possible, falling back to plain text if the HTML parse fails:

try {
await bot.api.sendMessage(chatId, reply.text, { parse_mode: 'HTML' });
} catch {
await bot.api.sendMessage(chatId, reply.text);
}

Minimal custom implementation

import { Bot } from 'grammy';

export async function connect(plugin, config) {
const bot = new Bot(config.botToken);
const me = await bot.api.getMe();

await plugin.gateway.registerChannel({
channelId: 'my-telegram',
name: 'Telegram',
platform: 'telegram',
agentId: config.agentId,
threadStrategy: config.threadStrategy ?? 'per-conversation',
});

bot.on('message:text', async (ctx) => {
const isGroup = ctx.chat.type !== 'private';
const mentionedBot = ctx.message.text.includes(`@${me.username}`);

if (isGroup && !mentionedBot) return;

const text = ctx.message.text.replace(`@${me.username}`, '').trim();

await plugin.gateway.routeMessage({
source: 'channel',
sourceId: plugin.pluginId,
threadStrategy: config.threadStrategy ?? 'per-conversation',
agentId: config.agentId,
userId: String(ctx.from.id),
externalThreadId: String(ctx.chat.id),
text,
metadata: {
senderName: ctx.from.first_name,
externalMessageId: String(ctx.message.message_id),
},
replyTo: {
channelId: 'my-telegram',
externalThreadId: String(ctx.chat.id),
userId: String(ctx.from.id),
},
});
});

plugin.gateway.onReply(async (reply) => {
const chatId = Number(reply.replyTo.externalThreadId);
try {
await bot.api.sendMessage(chatId, reply.text, { parse_mode: 'HTML' });
} catch {
await bot.api.sendMessage(chatId, reply.text);
}
});

bot.start();
}

Auto-reconnect

The first-party plugin saves its config to the KV store on successful connect:

await plugin.kvStore.set('config', config);

On plugin startup, it reads this config and reconnects automatically — no manual intervention needed after a server restart.

See also