feat: Telegram Bot API integration

- New client: src/clients/telegram.ts
- Tools: telegram_get_me, telegram_send_message, telegram_send_photo, telegram_get_updates, telegram_get_chat
- REST endpoints: GET /api/telegram/me, POST /api/telegram/message, POST /api/telegram/photo, GET /api/telegram/updates, GET /api/telegram/chat
- Multi-account env var pattern: TELEGRAM_{ACCOUNT}_BOT_TOKEN
- Uses Telegram Bot API (https://api.telegram.org)

Total tools: 24 (email 6, obsidian 5, whatsapp 3, linkedin 4, telegram 5)
This commit is contained in:
Garfield
2026-05-05 16:54:07 -04:00
parent 73f83c0d86
commit 385f91de4d
5 changed files with 439 additions and 0 deletions

View File

@@ -54,3 +54,9 @@ LINKEDIN_DEFAULT_ACCESS_TOKEN=your-linkedin-access-token
LINKEDIN_DEFAULT_CLIENT_ID=your-linkedin-client-id LINKEDIN_DEFAULT_CLIENT_ID=your-linkedin-client-id
LINKEDIN_DEFAULT_CLIENT_SECRET=your-linkedin-client-secret LINKEDIN_DEFAULT_CLIENT_SECRET=your-linkedin-client-secret
# For additional accounts, duplicate with LINKEDIN_{ACCOUNT}_* # For additional accounts, duplicate with LINKEDIN_{ACCOUNT}_*
# ── Telegram Bot API ─────────────────────────────────────────────────────────
# Create a bot via @BotFather on Telegram and copy the token
# For default account:
TELEGRAM_DEFAULT_BOT_TOKEN=your-telegram-bot-token
# For additional accounts, duplicate with TELEGRAM_{ACCOUNT}_*

129
src/clients/telegram.ts Normal file
View File

@@ -0,0 +1,129 @@
const TELEGRAM_API_BASE = 'https://api.telegram.org';
function getToken(account: string): string {
const envKey = `TELEGRAM_${account.toUpperCase()}_BOT_TOKEN`;
return process.env[envKey] ?? '';
}
async function telegramRequest(
token: string,
method: string,
params?: Record<string, unknown>
) {
const url = `${TELEGRAM_API_BASE}/bot${token}/${method}`;
const res = await fetch(url, {
method: params ? 'POST' : 'GET',
headers: params ? { 'Content-Type': 'application/json' } : undefined,
body: params ? JSON.stringify(params) : undefined,
signal: AbortSignal.timeout(15000),
});
const data = await res.json();
if (!data.ok) {
throw new Error(`Telegram API error: ${data.description ?? 'Unknown error'}`);
}
return data.result;
}
export async function getMe(args: { account?: string }): Promise<{
id: number;
first_name: string;
username: string;
is_bot: boolean;
}> {
const token = getToken(args.account ?? 'default');
if (!token) {
throw new Error('Missing Telegram credentials. Set TELEGRAM_{ACCOUNT}_BOT_TOKEN');
}
return telegramRequest(token, 'getMe');
}
export async function sendMessage(args: {
chat_id: string | number;
text: string;
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
account?: string;
}): Promise<{ message_id: number; chat_id: string | number }> {
const token = getToken(args.account ?? 'default');
if (!token) {
throw new Error('Missing Telegram credentials. Set TELEGRAM_{ACCOUNT}_BOT_TOKEN');
}
const result = await telegramRequest(token, 'sendMessage', {
chat_id: args.chat_id,
text: args.text,
parse_mode: args.parse_mode,
});
return {
message_id: result.message_id,
chat_id: result.chat.id,
};
}
export async function sendPhoto(args: {
chat_id: string | number;
photo: string;
caption?: string;
account?: string;
}): Promise<{ message_id: number; chat_id: string | number }> {
const token = getToken(args.account ?? 'default');
if (!token) {
throw new Error('Missing Telegram credentials. Set TELEGRAM_{ACCOUNT}_BOT_TOKEN');
}
const result = await telegramRequest(token, 'sendPhoto', {
chat_id: args.chat_id,
photo: args.photo,
caption: args.caption,
});
return {
message_id: result.message_id,
chat_id: result.chat.id,
};
}
export async function getUpdates(args: {
limit?: number;
account?: string;
}): Promise<Array<{
update_id: number;
message?: {
message_id: number;
from?: { id: number; first_name: string; username?: string };
chat: { id: number; type: string; title?: string };
date: number;
text?: string;
};
}>> {
const token = getToken(args.account ?? 'default');
if (!token) {
throw new Error('Missing Telegram credentials. Set TELEGRAM_{ACCOUNT}_BOT_TOKEN');
}
return telegramRequest(token, 'getUpdates', {
limit: args.limit ?? 10,
});
}
export async function getChat(args: {
chat_id: string | number;
account?: string;
}): Promise<{
id: number;
type: string;
title?: string;
username?: string;
description?: string;
member_count?: number;
}> {
const token = getToken(args.account ?? 'default');
if (!token) {
throw new Error('Missing Telegram credentials. Set TELEGRAM_{ACCOUNT}_BOT_TOKEN');
}
return telegramRequest(token, 'getChat', {
chat_id: args.chat_id,
});
}

View File

@@ -623,6 +623,62 @@ app.post('/api/linkedin/message', requireAuth, async (req, res) => {
} }
}); });
// ── Telegram REST endpoints ─────────────────────────────────────
app.get('/api/telegram/me', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('telegram_get_me', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/telegram/message', requireAuth, async (req, res) => {
const { chat_id, text, parse_mode, account } = req.body as Record<string, unknown>;
if (!chat_id || !text) { res.status(400).json({ error: 'chat_id and text are required' }); return; }
try {
const result = await handleToolCall('telegram_send_message', { chat_id, text, parse_mode, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/telegram/photo', requireAuth, async (req, res) => {
const { chat_id, photo, caption, account } = req.body as Record<string, unknown>;
if (!chat_id || !photo) { res.status(400).json({ error: 'chat_id and photo are required' }); return; }
try {
const result = await handleToolCall('telegram_send_photo', { chat_id, photo, caption, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/telegram/updates', requireAuth, async (req, res) => {
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('telegram_get_updates', { limit, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/telegram/chat', requireAuth, async (req, res) => {
const chat_id = req.query.chat_id as string | undefined;
const account = req.query.account as string | undefined;
if (!chat_id) { res.status(400).json({ error: 'chat_id is required' }); return; }
try {
const result = await handleToolCall('telegram_get_chat', { chat_id, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/pilot-request', async (req, res) => { app.post('/api/pilot-request', async (req, res) => {
const origin = req.get('origin'); const origin = req.get('origin');
if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) { if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) {

View File

@@ -567,6 +567,142 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
examples: [{ recipient_id: 'urn:li:person:abc123', message: 'Hi, thanks for connecting!', account: 'default' }], examples: [{ recipient_id: 'urn:li:person:abc123', message: 'Hi, thanks for connecting!', account: 'default' }],
}, },
// ── Telegram tools ──────────────────────────────────────────────────────
{
name: 'telegram_get_me',
category: 'telegram',
description: 'Get information about the connected Telegram bot',
when_to_use:
'User asks about the Telegram bot, wants to verify it is connected, or needs the bot username.',
input_schema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
id: { type: 'number', description: 'Bot user ID' },
first_name: { type: 'string' },
username: { type: 'string' },
is_bot: { type: 'boolean' },
},
},
examples: [{ account: 'default' }],
},
{
name: 'telegram_send_message',
category: 'telegram',
description: 'Send a text message via Telegram bot',
when_to_use:
'User wants to send a Telegram message, DM someone, or notify a group or channel.',
input_schema: {
type: 'object',
required: ['chat_id', 'text'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
text: { type: 'string', description: 'Message text' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown', 'MarkdownV2'], description: 'Message formatting mode' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
message_id: { type: 'number' },
chat_id: { type: 'string', description: 'Chat ID where the message was sent' },
},
},
examples: [{ chat_id: '@mychannel', text: 'Hello from Hermes!', account: 'default' }],
},
{
name: 'telegram_send_photo',
category: 'telegram',
description: 'Send a photo via Telegram bot',
when_to_use:
'User wants to share an image through Telegram.',
input_schema: {
type: 'object',
required: ['chat_id', 'photo'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
photo: { type: 'string', description: 'Photo URL or Telegram file_id' },
caption: { type: 'string', description: 'Optional caption text' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
message_id: { type: 'number' },
chat_id: { type: 'string' },
},
},
examples: [{ chat_id: '@mychannel', photo: 'https://example.com/image.jpg', caption: 'Check this out', account: 'default' }],
},
{
name: 'telegram_get_updates',
category: 'telegram',
description: 'Get recent incoming messages for the Telegram bot',
when_to_use:
'User asks to check Telegram messages, read DMs, or see recent bot activity.',
input_schema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max updates to return (default: 10)' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
returns: {
type: 'array',
items: {
type: 'object',
properties: {
update_id: { type: 'number' },
message: {
type: 'object',
properties: {
message_id: { type: 'number' },
from: { type: 'object', properties: { id: { type: 'number' }, first_name: { type: 'string' }, username: { type: 'string' } } },
chat: { type: 'object', properties: { id: { type: 'number' }, type: { type: 'string' }, title: { type: 'string' } } },
date: { type: 'number' },
text: { type: 'string' },
},
},
},
},
},
examples: [{ limit: 5, account: 'default' }],
},
{
name: 'telegram_get_chat',
category: 'telegram',
description: 'Get information about a Telegram chat or channel',
when_to_use:
'User wants to verify a Telegram chat exists or get its details before sending a message.',
input_schema: {
type: 'object',
required: ['chat_id'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
id: { type: 'number' },
type: { type: 'string' },
title: { type: 'string' },
username: { type: 'string' },
description: { type: 'string' },
member_count: { type: 'number' },
},
},
examples: [{ chat_id: '@mychannel', account: 'default' }],
},
// ── Obsidian tools ────────────────────────────────────────────────────── // ── Obsidian tools ──────────────────────────────────────────────────────
{ {
name: 'obsidian_search_notes', name: 'obsidian_search_notes',
@@ -734,6 +870,10 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
description: 'LinkedIn profile and posting via LinkedIn API', description: 'LinkedIn profile and posting via LinkedIn API',
icon: '🔗', icon: '🔗',
}, },
telegram: {
description: 'Telegram messaging via Bot API',
icon: '✈️',
},
}, },
}; };
} }

View File

@@ -4,6 +4,7 @@ import { sendEmail, createDraft } from './smtp.js';
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js'; import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js';
import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './clients/whatsapp.js'; import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './clients/whatsapp.js';
import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js'; import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js';
import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto as sendTelegramPhoto, getUpdates as getTelegramUpdates, getChat as getTelegramChat } from './clients/telegram.js';
const ACCOUNT_PARAM = { const ACCOUNT_PARAM = {
account: { account: {
@@ -284,6 +285,74 @@ export const tools: Tool[] = [
required: ['recipient_id', 'message'], required: ['recipient_id', 'message'],
}, },
}, },
// ── Telegram tools ─────────────────────────────────────────────
{
name: 'telegram_get_me',
description:
'Get information about the Telegram bot. Use to verify the bot is connected and get its username.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_send_message',
description:
'Send a text message via Telegram bot. Use when the user asks to send a Telegram message, DM someone, or notify a group/channel.',
inputSchema: {
type: 'object',
required: ['chat_id', 'text'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
text: { type: 'string', description: 'Message text to send' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown', 'MarkdownV2'], description: 'Formatting mode for the message' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_send_photo',
description:
'Send a photo via Telegram bot. Use when the user wants to share an image through Telegram.',
inputSchema: {
type: 'object',
required: ['chat_id', 'photo'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
photo: { type: 'string', description: 'Photo URL or file_id to send' },
caption: { type: 'string', description: 'Optional caption text' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_get_updates',
description:
'Get recent incoming messages and updates for the Telegram bot. Use when the user asks to check messages or read Telegram DMs.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max number of updates to return (default: 10)' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_get_chat',
description:
'Get information about a Telegram chat or channel. Use to verify a chat exists and get its details.',
inputSchema: {
type: 'object',
required: ['chat_id'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
]; ];
function acct(args: Record<string, unknown>): Account { function acct(args: Record<string, unknown>): Account {
@@ -417,6 +486,45 @@ export async function handleToolCall(
}); });
break; break;
// ── Telegram ───────────────────────────────────────────────
case 'telegram_get_me':
result = await getTelegramMe({
account: args.account as string | undefined,
});
break;
case 'telegram_send_message':
result = await sendTelegramMessage({
chat_id: args.chat_id as string | number,
text: args.text as string,
parse_mode: (args.parse_mode as 'HTML' | 'Markdown' | 'MarkdownV2') ?? undefined,
account: args.account as string | undefined,
});
break;
case 'telegram_send_photo':
result = await sendTelegramPhoto({
chat_id: args.chat_id as string | number,
photo: args.photo as string,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
});
break;
case 'telegram_get_updates':
result = await getTelegramUpdates({
limit: (args.limit as number) ?? 10,
account: args.account as string | undefined,
});
break;
case 'telegram_get_chat':
result = await getTelegramChat({
chat_id: args.chat_id as string | number,
account: args.account as string | undefined,
});
break;
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions // Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
case 'yahoo_get_profile': case 'yahoo_get_profile':
result = await getProfile('yahoo'); result = await getProfile('yahoo');