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:
@@ -54,3 +54,9 @@ LINKEDIN_DEFAULT_ACCESS_TOKEN=your-linkedin-access-token
|
||||
LINKEDIN_DEFAULT_CLIENT_ID=your-linkedin-client-id
|
||||
LINKEDIN_DEFAULT_CLIENT_SECRET=your-linkedin-client-secret
|
||||
# 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
129
src/clients/telegram.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
56
src/index.ts
56
src/index.ts
@@ -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) => {
|
||||
const origin = req.get('origin');
|
||||
if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) {
|
||||
|
||||
140
src/manifest.ts
140
src/manifest.ts
@@ -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' }],
|
||||
},
|
||||
|
||||
// ── 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 ──────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'obsidian_search_notes',
|
||||
@@ -734,6 +870,10 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
|
||||
description: 'LinkedIn profile and posting via LinkedIn API',
|
||||
icon: '🔗',
|
||||
},
|
||||
telegram: {
|
||||
description: 'Telegram messaging via Bot API',
|
||||
icon: '✈️',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
108
src/tools.ts
108
src/tools.ts
@@ -4,6 +4,7 @@ import { sendEmail, createDraft } from './smtp.js';
|
||||
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.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 { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto as sendTelegramPhoto, getUpdates as getTelegramUpdates, getChat as getTelegramChat } from './clients/telegram.js';
|
||||
|
||||
const ACCOUNT_PARAM = {
|
||||
account: {
|
||||
@@ -284,6 +285,74 @@ export const tools: Tool[] = [
|
||||
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 {
|
||||
@@ -417,6 +486,45 @@ export async function handleToolCall(
|
||||
});
|
||||
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
|
||||
case 'yahoo_get_profile':
|
||||
result = await getProfile('yahoo');
|
||||
|
||||
Reference in New Issue
Block a user