Mobile wallpaper
1556 字
8 分钟

Docker搭建Node Foward Bot(NFD) TG机器人

2025-03-15
浏览量 加载中...
本文主要内容

使用Docker本地搭建NFD Telegram机器人,替代原始的Cloudflare方法。

原始NFD项目是通过Cloudflare Worker进行搭建的,使用了Cloudflare kv作为数据存储方案,而kv有每天1000条写入的配额限制。此本地搭建方式使用sqlite代替kv数据库,以避免每日限额问题。

为什么不使用Livegram Bot?因为这个平台最近鬼迷心窍,开始用Livegram Bot给用户群发广告,几乎所有使用者受到影响,取消广告需要购买付费套餐,不买的话,你的机器人就变成tg的发广告工具了,着实抽象。

部署这个项目请确保你的VPS服务器可以正常访问Telegram服务器。

组件说明#

  • NFD:主要的机器人组件
  • Nginx Proxy Manager:反代机器人组件的TG Webhook

部署流程#

配置NFD机器人#

基本配置#

  1. 创建对应文件夹,拉取项目
    Terminal window
    mkdir -p /docker_data/nfd_bot && cd docker_data/nfd_bot && git clone https://github.com/LloydAsp/nfd.git src
  2. 添加pachage.json文件
    Terminal window
    cat << EOF > package.json
    {
    "name": "nfd-bot",
    "version": "1.0.0",
    "description": "Telegram message forwarding bot with anti-fraud features",
    "main": "src/index.js",
    "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
    },
    "dependencies": {
    "axios": "^1.4.0",
    "body-parser": "^1.20.2",
    "express": "^4.18.2",
    "sqlite3": "^5.1.6",
    "sqlite": "^4.2.1"
    },
    "devDependencies": {
    "nodemon": "^2.0.22"
    }
    }
    EOF
  3. 创建Dockerfile
    Terminal window
    cat << EOF > Dockerfile
    FROM node:16-alpine
    WORKDIR /app
    COPY package.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]
    EOF
  4. 创建docker-compose.yaml.env文件
    Terminal window
    cat << EOF > Dockerfile
    services:
    nfd-bot:
    build: .
    container_name: nfd-bot
    restart: always
    ports:
    - "${PORT:-3000}:3000"
    environment:
    - BOT_TOKEN=${BOT_TOKEN}
    - BOT_SECRET=${BOT_SECRET}
    - ADMIN_UID=${ADMIN_UID}
    - PORT=3000
    - BASE_URL=${BASE_URL}
    volumes:
    - ./src/data:/app/src/data
    network_mode: bridge
    EOF
    Terminal window
    BOT_TOKEN='替换为你自己的Bot Token (Bot Father获取)'
    BOT_SECRET='UUID生成或替换为你自己的强密码'
    ADMIN_UID=访问https://t.me/myidbot获取你自己账户的id
    BASE_URL=https://example.com/tg # 注意最后不要加'/'
    PORT=65035 # 替换你自己的端口
    UUID生成
  5. 创建src/index.jsnano src/index.js
    const express = require('express');
    const bodyParser = require('body-parser');
    const axios = require('axios');
    const fs = require('fs');
    const path = require('path');
    const sqlite3 = require('sqlite3');
    const { open } = require('sqlite');
    const app = express();
    app.use(bodyParser.json());
    // 从环境变量获取配置
    const TOKEN = process.env.BOT_TOKEN;
    const SECRET = process.env.BOT_SECRET;
    const ADMIN_UID = process.env.ADMIN_UID;
    const PORT = process.env.PORT || 3000;
    const WEBHOOK_PATH = process.env.WEBHOOK_PATH || '/endpoint';
    const NOTIFY_INTERVAL = 3600 * 1000;
    const enable_notification = true;
    // 数据文件路径
    const FRAUD_DB_PATH = path.join(__dirname, 'data/fraud.db');
    const NOTIFICATION_PATH = path.join(__dirname, 'data/notification.txt');
    const START_MSG_PATH = path.join(__dirname, 'data/startMessage.md');
    const DB_PATH = path.join(__dirname, 'data/nfd.sqlite');
    // SQLite 数据库连接
    let db;
    // 初始化数据库
    async function initDatabase() {
    db = await open({
    filename: DB_PATH,
    driver: sqlite3.Database
    });
    // 创建表
    await db.exec(`
    CREATE TABLE IF NOT EXISTS kv_store (
    key TEXT PRIMARY KEY,
    value TEXT,
    timestamp INTEGER
    )
    `);
    console.log('Database initialized');
    }
    // 封装 KV 操作的函数
    async function kvGet(key) {
    const row = await db.get('SELECT value FROM kv_store WHERE key = ?', [key]);
    return row ? JSON.parse(row.value) : null;
    }
    async function kvPut(key, value) {
    const jsonValue = JSON.stringify(value);
    await db.run(
    'INSERT OR REPLACE INTO kv_store (key, value, timestamp) VALUES (?, ?, ?)',
    [key, jsonValue, Date.now()]
    );
    }
    /**
    * 返回 Telegram API URL
    */
    function apiUrl(methodName, params = null) {
    let query = '';
    if (params) {
    const searchParams = new URLSearchParams();
    Object.entries(params).forEach(([key, value]) => {
    searchParams.append(key, value);
    });
    query = '?' + searchParams.toString();
    }
    return `https://api.telegram.org/bot${TOKEN}/${methodName}${query}`;
    }
    /**
    * 向 Telegram API 发送请求
    */
    async function requestTelegram(methodName, body, params = null) {
    try {
    const response = await axios.post(apiUrl(methodName, params), body);
    return response.data;
    } catch (error) {
    console.error(`Error calling Telegram API ${methodName}:`, error.message);
    return { ok: false, error: error.message };
    }
    }
    function makeReqBody(body) {
    return body;
    }
    function sendMessage(msg = {}) {
    return requestTelegram('sendMessage', makeReqBody(msg));
    }
    function copyMessage(msg = {}) {
    return requestTelegram('copyMessage', makeReqBody(msg));
    }
    function forwardMessage(msg) {
    return requestTelegram('forwardMessage', makeReqBody(msg));
    }
    /**
    * 处理 webhook 请求
    */
    app.post(WEBHOOK_PATH, async (req, res) => {
    // 验证 secret token
    if (req.headers['x-telegram-bot-api-secret-token'] !== SECRET) {
    return res.status(403).send('Unauthorized');
    }
    const update = req.body;
    // 异步处理 update
    onUpdate(update).catch(err => {
    console.error('Error processing update:', err);
    });
    return res.status(200).send('Ok');
    });
    /**
    * 处理传入的 Update
    */
    async function onUpdate(update) {
    if ('message' in update) {
    await onMessage(update.message);
    }
    }
    /**
    * 处理传入的 Message
    */
    async function onMessage(message) {
    if (message.text === '/start') {
    let startMsg = await readFile(START_MSG_PATH);
    return sendMessage({
    chat_id: message.chat.id,
    text: startMsg,
    });
    }
    if (message.chat.id.toString() === ADMIN_UID) {
    if (!message?.reply_to_message?.chat) {
    return sendMessage({
    chat_id: ADMIN_UID,
    text: '使用方法,回复转发的消息,并发送回复消息,或者`/block`、`/unblock`、`/checkblock`等指令'
    });
    }
    if (/^\/block$/.exec(message.text)) {
    return handleBlock(message);
    }
    if (/^\/unblock$/.exec(message.text)) {
    return handleUnBlock(message);
    }
    if (/^\/checkblock$/.exec(message.text)) {
    return checkBlock(message);
    }
    let guestChantId = await kvGet(`msg-map-${message?.reply_to_message.message_id}`);
    return copyMessage({
    chat_id: guestChantId,
    from_chat_id: message.chat.id,
    message_id: message.message_id,
    });
    }
    return handleGuestMessage(message);
    }
    async function handleGuestMessage(message) {
    let chatId = message.chat.id;
    let isblocked = await kvGet(`isblocked-${chatId}`);
    if (isblocked) {
    return sendMessage({
    chat_id: chatId,
    text: 'Your are blocked'
    });
    }
    let forwardReq = await forwardMessage({
    chat_id: ADMIN_UID,
    from_chat_id: message.chat.id,
    message_id: message.message_id
    });
    console.log(JSON.stringify(forwardReq));
    if (forwardReq.ok) {
    await kvPut(`msg-map-${forwardReq.result.message_id}`, chatId);
    }
    return handleNotify(message);
    }
    async function handleNotify(message) {
    // 先判断是否是诈骗人员,如果是,则直接提醒
    // 如果不是,则根据时间间隔提醒:用户id,交易注意点等
    let chatId = message.chat.id;
    if (await isFraud(chatId)) {
    return sendMessage({
    chat_id: ADMIN_UID,
    text: `检测到骗子,UID${chatId}`
    });
    }
    if (enable_notification) {
    let lastMsgTime = await kvGet(`lastmsg-${chatId}`);
    if (!lastMsgTime || Date.now() - lastMsgTime > NOTIFY_INTERVAL) {
    await kvPut(`lastmsg-${chatId}`, Date.now());
    return sendMessage({
    chat_id: ADMIN_UID,
    text: await readFile(NOTIFICATION_PATH)
    });
    }
    }
    }
    async function handleBlock(message) {
    let guestChantId = await kvGet(`msg-map-${message.reply_to_message.message_id}`);
    if (guestChantId === ADMIN_UID) {
    return sendMessage({
    chat_id: ADMIN_UID,
    text: '不能屏蔽自己'
    });
    }
    await kvPut(`isblocked-${guestChantId}`, true);
    return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}屏蔽成功`,
    });
    }
    async function handleUnBlock(message) {
    let guestChantId = await kvGet(`msg-map-${message.reply_to_message.message_id}`);
    await kvPut(`isblocked-${guestChantId}`, false);
    return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}解除屏蔽成功`,
    });
    }
    async function checkBlock(message) {
    let guestChantId = await kvGet(`msg-map-${message.reply_to_message.message_id}`);
    let blocked = await kvGet(`isblocked-${guestChantId}`);
    return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}` + (blocked ? '被屏蔽' : '没有被屏蔽')
    });
    }
    /**
    * 发送纯文本消息
    */
    async function sendPlainText(chatId, text) {
    return sendMessage({
    chat_id: chatId,
    text
    });
    }
    /**
    * 设置 webhook
    */
    app.get('/registerWebhook', async (req, res) => {
    const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
    const webhookUrl = `${BASE_URL}${WEBHOOK_PATH}`;
    try {
    const r = await axios.get(apiUrl('setWebhook', {
    url: webhookUrl,
    secret_token: SECRET
    }));
    if (r.data.ok) {
    return res.send('Webhook registered successfully');
    } else {
    return res.status(400).send(JSON.stringify(r.data, null, 2));
    }
    } catch (error) {
    return res.status(500).send(`Error: ${error.message}`);
    }
    });
    /**
    * 删除 webhook
    */
    app.get('/unRegisterWebhook', async (req, res) => {
    try {
    const r = await axios.get(apiUrl('setWebhook', { url: '' }));
    if (r.data.ok) {
    return res.send('Webhook unregistered successfully');
    } else {
    return res.status(400).send(JSON.stringify(r.data, null, 2));
    }
    } catch (error) {
    return res.status(500).send(`Error: ${error.message}`);
    }
    });
    /**
    * 检查是否是骗子
    */
    async function isFraud(id) {
    id = id.toString();
    const fraudDb = await readFile(FRAUD_DB_PATH);
    let arr = fraudDb.split('\n').filter(v => v);
    console.log(JSON.stringify(arr));
    let flag = arr.filter(v => v === id).length !== 0;
    console.log(flag);
    return flag;
    }
    /**
    * 读取文件辅助函数
    */
    function readFile(filePath) {
    return new Promise((resolve, reject) => {
    fs.readFile(filePath, 'utf8', (err, data) => {
    if (err) {
    console.error(`Error reading file ${filePath}:`, err);
    reject(err);
    } else {
    resolve(data);
    }
    });
    });
    }
    // 启动服务器
    async function startServer() {
    // 初始化数据库
    await initDatabase();
    // 启动 Express 服务器
    app.listen(PORT, () => {
    console.log(`NFD Bot server running on port ${PORT}`);
    console.log(`Webhook path: ${WEBHOOK_PATH}`);
    });
    }
    // 启动应用
    startServer().catch(err => {
    console.error('Failed to start server:', err);
    process.exit(1);
    });
  6. 启动服务
    Terminal window
    docker compose up -d

 

配置Nginx Proxy Manager#

  1. 随便找一个已经反代了的服务(这里假设是https://example.com服务),前往Custom locations,如图添加一个location image.png
  2. 访问'https://example.com/tg/registerWebhook

其他#

  1. 验证是否成功
    1. 如果Bot发送消息给你了,那就代表成功了。
    2. 或者运行curl "https://api.telegram.org/bot替换为你的BotToken/getWebhookInfo",返回如下结果就算成功。
      {"ok":true,"result":{"url":"https://example/tg/endpoint","has_custom_certificate":false,"pending_update_count":0,"max_connections":40,"ip_address":"xx.xx.xx.xx"}}
Docker搭建Node Foward Bot(NFD) TG机器人
https://blog.useforall.com/posts/12/
最后更新于 2025-03-15,距今已过 246 天

部分内容可能已过时

评论区

目录