1556 字
8 分钟
Docker搭建Node Foward Bot(NFD) TG机器人
本文主要内容使用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机器人
基本配置
- 创建对应文件夹,拉取项目
Terminal window mkdir -p /docker_data/nfd_bot && cd docker_data/nfd_bot && git clone https://github.com/LloydAsp/nfd.git src - 添加
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 - 创建
DockerfileTerminal window cat << EOF > DockerfileFROM node:16-alpineWORKDIR /appCOPY package.json ./RUN npm installCOPY . .EXPOSE 3000CMD ["npm", "start"]EOF - 创建
docker-compose.yaml和.env文件Terminal window cat << EOF > Dockerfileservices:nfd-bot:build: .container_name: nfd-botrestart: alwaysports:- "${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/datanetwork_mode: bridgeEOFTerminal window BOT_TOKEN='替换为你自己的Bot Token (Bot Father获取)'BOT_SECRET='UUID生成或替换为你自己的强密码'ADMIN_UID=访问https://t.me/myidbot获取你自己账户的idBASE_URL=https://example.com/tg # 注意最后不要加'/'PORT=65035 # 替换你自己的端口UUID生成 - 创建
src/index.js,nano src/index.jsconst 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 tokenif (req.headers['x-telegram-bot-api-secret-token'] !== SECRET) {return res.status(403).send('Unauthorized');}const update = req.body;// 异步处理 updateonUpdate(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);}); - 启动服务
Terminal window docker compose up -d
配置Nginx Proxy Manager
- 随便找一个已经反代了的服务(这里假设是
https://example.com服务),前往Custom locations,如图添加一个location
- 访问
'https://example.com/tg/registerWebhook。
其他
- 验证是否成功
- 如果Bot发送消息给你了,那就代表成功了。
- 或者运行
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 天
部分内容可能已过时
Lim's Blog