本文主要内容
使用Docker本地搭建NFD Telegram机器人,替代原始的Cloudflare方法。
原始NFD项目是通过Cloudflare Worker进行搭建的,使用了Cloudflare kv作为数据存储方案,而kv有每天1000条写入的配额限制。此本地搭建方式使用sqlite代替kv数据库,以避免每日限额问题。
为什么不使用Livegram Bot?因为这个平台最近鬼迷心窍,开始用Livegram Bot给用户群发广告,几乎所有使用者受到影响,取消广告需要购买付费套餐,不买的话,你的机器人就变成tg的发广告工具了,着实抽象。
部署这个项目请确保你的VPS服务器可以正常访问Telegram服务器。
bashmkdir -p /docker_data/nfd_bot && cd docker_data/nfd_bot && git clone https://github.com/LloydAsp/nfd.git src
pachage.json
文件
bashcat << 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
Dockerfile
bashcat << EOF > Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
EOF
docker-compose.yaml
和.env
文件
bashcat << 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
bashBOT_TOKEN='替换为你自己的Bot Token (Bot Father获取)'
BOT_SECRET='UUID生成或替换为你自己的强密码'
ADMIN_UID=访问https://t.me/myidbot获取你自己账户的id
BASE_URL=https://example.com/tg # 注意最后不要加'/'
PORT=65035 # 替换你自己的端口
UUID生成
src/index.js
,nano src/index.js
javascriptconst 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);
});
bashdocker compose up -d
https://example.com
服务),前往Custom locations
,如图添加一个location
'https://example.com/tg/registerWebhook
。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 许可协议。转载请注明出处!