Skip to content
Back to skills

Add Telegram

CSecurity

Add Telegram as a channel. Can replace WhatsApp entirely or run alongside it. Also configurable as a control-only channel (triggers actions) or passive channel (receives notifications only).

  • 30,863 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added February 9, 2026
ai-agentstypescriptbashtestingapidatabase

Works with

  • api

Security analysis

C71/100
  • criticalModifies startup scripts or system services for persistence
  • criticalModifies startup scripts or system services for persistence
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned February 12, 2026

npx -y skills add gavrielc/nanoclaw --skill add-telegram --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Add Telegram?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Add Telegram
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/gavrielc-add-telegram/badge)](https://www.skillsdirectory.com/skills/gavrielc-add-telegram)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: add-telegram
description: Add Telegram as a channel. Can replace WhatsApp entirely or run alongside it. Also configurable as a control-only channel (triggers actions) or passive channel (receives notifications only).
---

# Add Telegram Channel

This skill adds Telegram support to NanoClaw. Users can choose to:

1. **Replace WhatsApp** - Use Telegram as the only messaging channel
2. **Add alongside WhatsApp** - Both channels active
3. **Control channel** - Telegram triggers agent but doesn't receive all outputs
4. **Notification channel** - Receives outputs but limited triggering

## Prerequisites

### 1. Install Grammy

```bash
npm install grammy
```

Grammy is a modern, TypeScript-first Telegram bot framework.

### 2. Create Telegram Bot

Tell the user:

> I need you to create a Telegram bot:
>
> 1. Open Telegram and search for `@BotFather`
> 2. Send `/newbot` and follow prompts:
>    - Bot name: Something friendly (e.g., "Andy Assistant")
>    - Bot username: Must end with "bot" (e.g., "andy_ai_bot")
> 3. Copy the bot token (looks like `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`)

Wait for user to provide the token.

### 3. Get Chat ID

Tell the user:

> To register a chat, you need its Chat ID. Here's how:
>
> **For Private Chat (DM with bot):**
> 1. Search for your bot in Telegram
> 2. Start a chat and send any message
> 3. I'll add a `/chatid` command to help you get the ID
>
> **For Group Chat:**
> 1. Add your bot to the group
> 2. Send any message
> 3. Use the `/chatid` command in the group

## Questions to Ask

Before making changes, ask:

1. **Mode**: Replace WhatsApp or add alongside it?
   - If replace: Set `TELEGRAM_ONLY=true`
   - If alongside: Both will run

2. **Chat behavior**: Should this chat respond to all messages or only when @mentioned?
   - Main chat: Responds to all
   - Other chats: Can configure `respondToAll: true` in registered_groups.json

## Implementation

### Step 1: Update Configuration

Read `src/config.ts` and add Telegram config exports:

```typescript
export const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
export const TELEGRAM_ONLY = process.env.TELEGRAM_ONLY === "true";
```

These should be added near the top with other configuration exports.

### Step 2: Add storeMessageDirect to Database

Read `src/db.ts` and add this function (place it near the `storeMessage` function):

```typescript
/**
 * Store a message directly (for non-WhatsApp channels that don't use Baileys proto).
 */
export function storeMessageDirect(msg: {
  id: string;
  chat_jid: string;
  sender: string;
  sender_name: string;
  content: string;
  timestamp: string;
  is_from_me: boolean;
}): void {
  db.prepare(
    `INSERT OR REPLACE INTO messages (id, chat_jid, sender, sender_name, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?, ?)`,
  ).run(
    msg.id,
    msg.chat_jid,
    msg.sender,
    msg.sender_name,
    msg.content,
    msg.timestamp,
    msg.is_from_me ? 1 : 0,
  );
}
```

Also update the db.ts exports to include `storeMessageDirect`.

### Step 3: Create Telegram Module

Create `src/telegram.ts` with this content:

```typescript
import { Bot } from "grammy";
import pino from "pino";
import {
  ASSISTANT_NAME,
  TRIGGER_PATTERN,
  MAIN_GROUP_FOLDER,
} from "./config.js";
import { RegisteredGroup, NewMessage } from "./types.js";
import { storeChatMetadata, storeMessageDirect } from "./db.js";

const logger = pino({
  level: process.env.LOG_LEVEL || "info",
  transport: { target: "pino-pretty", options: { colorize: true } },
});

export interface TelegramCallbacks {
  onMessage: (
    msg: NewMessage,
    group: RegisteredGroup,
  ) => Promise<string | null>;
  getRegisteredGroups: () => Record<string, RegisteredGroup>;
}

let bot: Bot | null = null;
let callbacks: TelegramCallbacks | null = null;

export async function connectTelegram(
  botToken: string,
  cbs: TelegramCallbacks,
): Promise<void> {
  callbacks = cbs;
  bot = new Bot(botToken);

  // Command to get chat ID (useful for registration)
  bot.command("chatid", (ctx) => {
    const chatId = ctx.chat.id;
    const chatType = ctx.chat.type;
    const chatName =
      chatType === "private"
        ? ctx.from?.first_name || "Private"
        : (ctx.chat as any).title || "Unknown";

    ctx.reply(
      `Chat ID: \`tg:${chatId}\`\nName: ${chatName}\nType: ${chatType}`,
      { parse_mode: "Markdown" },
    );
  });

  // Command to check bot status
  bot.command("ping", (ctx) => {
    ctx.reply(`${ASSISTANT_NAME} is online.`);
  });

  bot.on("message:text", async (ctx) => {
    // Skip commands
    if (ctx.message.text.startsWith("/")) return;

    const chatId = `tg:${ctx.chat.id}`;
    const content = ctx.message.text;
    const timestamp = new Date(ctx.message.date * 1000).toISOString();
    const senderName =
      ctx.from?.first_name ||
      ctx.from?.username ||
      ctx.from?.id.toString() ||
      "Unknown";
    const sender = ctx.from?.id.toString() || "";
    const msgId = ctx.message.message_id.toString();

    // Determine chat name
    const chatName =
      ctx.chat.type === "private"
        ? senderName
        : (ctx.chat as any).title || chatId;

    // Store chat metadata for discovery
    storeChatMetadata(chatId, timestamp, chatName);

    // Check if this chat is registered
    const registeredGroups = callbacks!.getRegisteredGroups();
    const group = registeredGroups[chatId];

    if (!group) {
      logger.debug(
        { chatId, chatName },
        "Message from unregistered Telegram chat",
      );
      return;
    }

    // Store message for registered chats
    storeMessageDirect({
      id: msgId,
      chat_jid: chatId,
      sender,
      sender_name: senderName,
      content,
      timestamp,
      is_from_me: false,
    });

    const isMain = group.folder === MAIN_GROUP_FOLDER;
    const respondToAll = (group as any).respondToAll === true;

    // Check if bot is @mentioned in the message (Telegram native mention)
    const botUsername = ctx.me?.username?.toLowerCase();
    const entities = ctx.message.entities || [];
    const isBotMentioned = entities.some((entity) => {
      if (entity.type === "mention") {
        const mentionText = content
          .substring(entity.offset, entity.offset + entity.length)
          .toLowerCase();
        return mentionText === `@${botUsername}`;
      }
      return false;
    });

    // Respond if: main group, respondToAll group, bot is @mentioned, or trigger pattern matches
    if (
      !isMain &&
      !respondToAll &&
      !isBotMentioned &&
      !TRIGGER_PATTERN.test(content)
    ) {
      return;
    }

    logger.info(
      { chatId, chatName, sender: senderName },
      "Processing Telegram message",
    );

    // Send typing indicator
    await ctx.replyWithChatAction("typing");

    const msg: NewMessage = {
      id: msgId,
      chat_jid: chatId,
      sender,
      sender_name: senderName,
      content,
      timestamp,
    };

    try {
      const response = await callbacks!.onMessage(msg, group);
      if (response) {
        await ctx.reply(`${ASSISTANT_NAME}: ${response}`);
      }
    } catch (err) {
      logger.error({ err, chatId }, "Error processing Telegram message");
    }
  });

  // Handle errors gracefully
  bot.catch((err) => {
    logger.error({ err: err.message }, "Telegram bot error");
  });

  // Start polling
  bot.start({
    onStart: (botInfo) => {
      logger.info(
        { username: botInfo.username, id: botInfo.id },
        "Telegram bot connected",
      );
      console.log(`\n  Telegram bot: @${botInfo.username}`);
      console.log(
        `  Send /chatid to the bot to get a chat's registration ID\n`,
      );
    },
  });
}

export async function sendTelegramMessage(
  chatId: string,
  text: string,
): Promise<void> {
  if (!bot) {
    logger.warn("Telegram bot not initialized");
    return;
  }

  try {
    // Remove tg: prefix if present
    const numericId = chatId.replace(/^tg:/, "");
    await bot.api.sendMessage(numericId, text);
    logger.info({ chatId, length: text.length }, "Telegram message sent");
  } catch (err) {
    logger.error({ chatId, err }, "Failed to send Telegram message");
  }
}

export function isTelegramConnected(): boolean {
  return bot !== null;
}

export function stopTelegram(): void {
  if (bot) {
    bot.stop();
    bot = null;
    callbacks = null;
    logger.info("Telegram bot stopped");
  }
}
```

### Step 4: Update Main Application

Modify `src/index.ts`:

1. Add imports at the top:

```typescript
import {
  connectTelegram,
  sendTelegramMessage,
  isTelegramConnected,
} from "./telegram.js";
import { TELEGRAM_BOT_TOKEN, TELEGRAM_ONLY } from "./config.js";
```

2. Update `sendMessage` function to route by channel. Find the `sendMessage` function and replace it with:

```typescript
async function sendMessage(jid: string, text: string): Promise<void> {
  if (jid.startsWith("tg:")) {
    await sendTelegramMessage(jid, text);
  } else {
    try {
      await sock.sendMessage(jid, { text });
      logger.info({ jid, length: text.length }, "Message sent");
    } catch (err) {
      logger.error({ jid, err }, "Failed to send message");
    }
  }
}
```

3. Update `main()` function. Find the `main()` function and update it to support Telegram. Add this before the `connectWhatsApp()` call:

```typescript
const hasTelegram = !!TELEGRAM_BOT_TOKEN;

if (hasTelegram) {
  await connectTelegram(TELEGRAM_BOT_TOKEN, {
    onMessage: async (msg, group) => {
      // Get messages since last agent interaction for context
      const sinceTimestamp = lastAgentTimestamp[msg.chat_jid] || "";
      const missedMessages = getMessagesSince(
        msg.chat_jid,
        sinceTimestamp,
        ASSISTANT_NAME,
      );

      const lines = missedMessages.map((m) => {
        const escapeXml = (s: string) =>
          s
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;");
        return `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`;
      });
      const prompt = `<messages>\n${lines.join("\n")}\n</messages>`;

      const group = registeredGroups[msg.chat_jid];
      const isMain = group.folder === MAIN_GROUP_FOLDER;

      const output = await runContainerAgent(group, {
        prompt,
        sessionId: sessions[group.folder],
        groupFolder: group.folder,
        chatJid: msg.chat_jid,
        isMain,
        isScheduledTask: false,
      });

      if (output.newSessionId) {
        sessions[group.folder] = output.newSessionId;
        saveJson(path.join(DATA_DIR, "sessions.json"), sessions);
      }

      lastAgentTimestamp[msg.chat_jid] = msg.timestamp;
      saveState();

      return output.status === "success" ? output.result : null;
    },
    getRegisteredGroups: () => registeredGroups,
  });
}
```

4. Wrap the `connectWhatsApp()` call to support Telegram-only mode. Replace:

```typescript
await connectWhatsApp();
```

With:

```typescript
if (!TELEGRAM_ONLY) {
  await connectWhatsApp();
} else {
  // Telegram-only mode: start scheduler and IPC without WhatsApp
  startSchedulerLoop({
    sendMessage,
    registeredGroups: () => registeredGroups,
    getSessions: () => sessions,
  });
  startIpcWatcher();
  logger.info(
    `NanoClaw running (Telegram-only, trigger: @${ASSISTANT_NAME})`,
  );
}
```

### Step 5: Update Environment

Add to `.env`:

```bash
TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE

# Optional: Set to "true" to disable WhatsApp entirely
# TELEGRAM_ONLY=true
```

### Step 6: Register a Telegram Chat

After installing and starting the bot, tell the user:

> 1. Send `/chatid` to your bot (in private chat or in a group)
> 2. Copy the chat ID (e.g., `tg:123456789` or `tg:-1001234567890`)
> 3. I'll add it to registered_groups.json

Then update `data/registered_groups.json`:

For private chat:

```json
{
  "tg:123456789": {
    "name": "Personal",
    "folder": "main",
    "trigger": "@Andy",
    "added_at": "2026-02-05T12:00:00.000Z"
  }
}
```

For group chat (note the negative ID for groups):

```json
{
  "tg:-1001234567890": {
    "name": "My Telegram Group",
    "folder": "telegram-group",
    "trigger": "@Andy",
    "added_at": "2026-02-05T12:00:00.000Z",
    "respondToAll": false
  }
}
```

Set `respondToAll: true` if you want the bot to respond to all messages in that chat (not just when @mentioned or triggered).

### Step 7: Build and Restart

```bash
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```

Or for systemd:

```bash
npm run build
systemctl --user restart nanoclaw
```

### Step 8: Test

Tell the user:

> Send a message to your registered Telegram chat:
> - For main chat: Any message works
> - For non-main: `@Andy hello` or @mention the bot
>
> Check logs: `tail -f logs/nanoclaw.log`

## Replace WhatsApp Entirely

If user wants Telegram-only:

1. Set `TELEGRAM_ONLY=true` in `.env`
2. The WhatsApp connection code is automatically skipped
3. Optionally remove `@whiskeysockets/baileys` dependency (but it's harmless to keep)

## Features

### Chat ID Formats

- **WhatsApp**: `120363336345536173@g.us` (groups) or `1234567890@s.whatsapp.net` (DM)
- **Telegram**: `tg:123456789` (positive for private) or `tg:-1001234567890` (negative for groups)

### Trigger Options

The bot responds when:
1. Message is in the main chat (folder: "main")
2. Chat has `respondToAll: true` in registered_groups.json
3. Bot is @mentioned using native Telegram mention (e.g., @your_bot_username)
4. Message matches TRIGGER_PATTERN (e.g., starts with @Andy)

### Commands

- `/chatid` - Get chat ID for registration
- `/ping` - Check if bot is online

## Troubleshooting

### Bot not responding

Check:
1. `TELEGRAM_BOT_TOKEN` is set in `.env`
2. Chat is registered in `data/registered_groups.json` with `tg:` prefix
3. For non-main chats: message includes trigger or @mention
4. Service is running: `launchctl list | grep nanoclaw`

### Getting chat ID

If `/chatid` doesn't work:
- Verify bot token is valid: `curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe"`
- Check bot is started: `tail -f logs/nanoclaw.log`

### Service conflicts

If running `npm run dev` while launchd service is active:
```bash
launchctl unload ~/Library/LaunchAgents/com.nanoclaw.plist
npm run dev
# When done testing:
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
```

## Removal

To remove Telegram integration:

1. Delete `src/telegram.ts`
2. Remove Telegram imports from `src/index.ts`
3. Remove `sendTelegramMessage` logic from `sendMessage()` function
4. Remove `connectTelegram()` call from `main()`
5. Remove `storeMessageDirect` from `src/db.ts`
6. Remove Telegram config from `src/config.ts`
7. Uninstall: `npm uninstall grammy`
8. Rebuild: `npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.