编辑
2025-03-15
VPS
00

目录

组件说明
部署流程
配置NFD机器人
基本配置
配置Nginx Proxy Manager
其他

本文主要内容

使用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. 创建对应文件夹,拉取项目
    bash
    mkdir -p /docker_data/nfd_bot && cd docker_data/nfd_bot && git clone https://github.com/LloydAsp/nfd.git src
  2. 添加pachage.json文件
    bash
    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
    bash
    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文件
    bash
    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
    bash
    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
    javascript
    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. 启动服务
    bash
    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"}}

本文作者:Lim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!