『NFD3.0』 Telegram私聊机器人 拦截全部垃圾广告

特性

  • Serverless 部署:直接部署在 Cloudflare Workers,零成本。
  • 双向沟通:用户发消息给机器人 -> 机器人转发给管理员 -> 管理员回复消息 -> 机器人发回给用户。
  • 🛡️ 暴躁验证:首次聊天的用户必须完成 CF人机验证
  • 🚫 铁血黑名单:支持永久拉黑恶意用户,被拉黑用户无法再发送消息。
  • KV 持久化:使用 Cloudflare KV 存储验证状态和黑名单,数据不丢失。
  • 便捷指令:支持直接回复消息进行拉黑、解封、重置验证等操作。

🛠️ 部署教程

1. 准备工作

  • Telegram Bot Token:在 TG 上找 @BotFather 创建机器人获取。
  • Admin UID:在 TG 上找 @userinfobot 获取你自己的 User ID。
  • Secret:生成一个随机字符串(用于 Webhook 验证)。

2. 配置 Cloudflare Workers

  1. 登录 Cloudflare Dashboard
  2. 进入 Workers & Pages -> Create Application -> Create Worker
  3. 命名为 fuck-spam-bot (或其他你喜欢的名字),点击 Deploy

3. 配置 KV 数据库

  1. 在 Workers 页面,点击左侧菜单的 KV
  2. 点击 Create a Namespace,命名为 TG_BOT_KV (或者其他名字)。
  3. 回到你刚才创建的 Worker,进入 Settings -> Variables
  4. 向下滚动到 KV Namespace Bindings
  5. 点击 Add binding
    • Variable name: 必须填 nfd (代码中写死了这个名字)。
    • KV Namespace: 选择刚才创建的 TG_BOT_KV
  6. 点击 Save and deploy

4. 设置环境变量

在 Worker 的 Settings -> Variables -> Environment Variables 中添加以下变量:

变量名 说明 示例
ENV_BOT_TOKEN 你的 Bot Token 123456:ABC-DEF...
ENV_BOT_SECRET Webhook 密钥 (随机字符串) random_string_123
ENV_ADMIN_UID 管理员的 User ID 123456789

5. 部署代码

  1. 点击 Edit code 进入在线编辑器。
  2. 将本项目 worker.js 的内容完整复制粘贴进去。
  3. 点击右上角的 Deploy

6. 绑定 Webhook

部署完成后,在浏览器访问以下 URL 来激活机器人:
https://你的worker域名.workers.dev/registerWebhook

如果看到 Ok,说明部署成功!

🤖 指令说明 (管理员专用)

所有指令建议直接回复 (Reply) 用户转发过来的消息使用,机器人会自动提取目标用户 ID。

指令 作用 示例
回复消息 直接回复内容给用户 (直接打字发送)
/block 拉黑该用户 (永久) 回复某条消息发送 /block
/unblock 解封该用户 回复某条消息发送 /unblock
/clear_ver 重置验证 (强制重新验证) 回复某条消息发送 /clear_ver

也可以手动指定 ID,例如 /unblock 123456789,但回复消息更方便且不易出错。

📝 验证机制说明

  • 验证有效期:默认 30 天。用户通过验证后,30 天内无需再次验证。
  • 黑名单永久有效,除非管理员手动解封。

️ 注意事项

  • 请确保 KV Namespace 的变量名绑定为 nfd,否则机器人无法记忆状态。
  • Cloudflare KV 存在短暂的最终一致性延迟(约 1 分钟)。如果你刚解封用户,可能需要等几十秒才会生效。

License: MIT 全部代码由AI完成,我是抄的隔壁的

// ================================================================= 
//                     自行修改14-15行
// =================================================================

const TOKEN = ENV_BOT_TOKEN;      // 不需要修改,在KV中配置
const WEBHOOK = '/endpoint';
const SECRET = ENV_BOT_SECRET;    // 在KV中配置
const ADMIN_UID = ENV_ADMIN_UID;  // 在KV中配置

// 验证通过后的有效期 (秒),默认 30 天
const VERIFICATION_TTL = 60 * 60 * 24 * 30;

// Cloudflare Turnstile Keys
const CF_TURNSTILE_SITE_KEY = '0x4AAAAAasdasd0H5ADQjY';
const CF_TURNSTILE_SECRET_KEY = '0x4AAAAAACG6XsdfsdfsdfsdfZbm2cph_mxgV0';

// =================================================================
//                      核心功能
// =================================================================

function apiUrl(methodName, params = null) {
  let query = '';
  if (params) {
    query = '?'   new URLSearchParams(params).toString();
  }
  return `https://api.telegram.org/bot${TOKEN}/${methodName}${query}`;
}

function requestTelegram(methodName, body, params = null) {
  return fetch(apiUrl(methodName, params), {
    method: 'POST',
    headers: {
      'content-type': 'application/json'
    },
    body: JSON.stringify(body)
  }).then(r => r.json());
}

function sendMessage(msg = {}) {
  return requestTelegram('sendMessage', msg);
}

function copyMessage(msg = {}) {
  return requestTelegram('copyMessage', msg);
}

function forwardMessage(msg) {
  return requestTelegram('forwardMessage', msg);
}

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  if (url.pathname === WEBHOOK) {
    event.respondWith(handleWebhook(event, url));
  } else if (url.pathname === '/registerWebhook') {
    event.respondWith(registerWebhook(event, url, WEBHOOK, SECRET));
  } else if (url.pathname === '/unRegisterWebhook') {
    event.respondWith(unRegisterWebhook(event));
  } else if (url.pathname === '/verify') {
    event.respondWith(handleVerifyPage(event.request));
  } else if (url.pathname === '/verify-callback') {
    event.respondWith(handleVerifyCallback(event.request));
  } else {
    event.respondWith(new Response('No handler for this request'));
  }
});

async function handleWebhook(event, url) {
  if (event.request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) {
    return new Response('Unauthorized', { status: 403 });
  }

  const update = await event.request.json();
  event.waitUntil(onUpdate(update, url.origin));
  return new Response('Ok');
}

async function onUpdate(update, origin) {
  if ('message' in update) {
    await onMessage(update.message, origin);
  }
}

async function onMessage(message, origin) {
  const chatId = message.chat.id.toString();

  // 1. 如果是管理员发消息
  if (chatId === ADMIN_UID) {
    return handleAdminMessage(message);
  }

  // 2. 如果是访客 (普通用户)
  else {
    // 0. 检查黑名单
    const isBlocked = await nfd.get('blocked-'   chatId);
    if (isBlocked) {
      // 被拉黑了,回复提示
      return sendMessage({
        chat_id: chatId,
        text: '🚫 您已被管理员拉黑,无法发送消息。'
      });
    }

    // 1. 检查是否已通过验证
    const isVerified = await nfd.get('verified-'   chatId);

    if (isVerified) {
      // 已验证,正常转发给管理员
      return handleGuestMessage(message);
    } else {
      // 未验证,进入验证流程
      return handleVerification(message, chatId, origin);
    }
  }
}

// 辅助函数:尝试从回复或参数中获取目标 ID
async function getTargetId(message, commandName) {
  const text = (message.text || '').trim();
  const args = text.split(/\s /);
  const reply = message.reply_to_message;

  // 优先 1:从回复的消息中提取
  if (reply && (reply.forward_from || reply.forward_sender_name)) {
    const guestChatId = await nfd.get('msg-map-'   reply.message_id);
    if (guestChatId) return guestChatId;
  }

  // 优先 2:从指令参数中提取 (例如 /unblock 123456)
  if (args.length > 1) {
    const potentialId = args[1];
    // 简单的数字校验
    if (/^\d $/.test(potentialId)) {
      return potentialId;
    }
  }

  return null;
}

// 处理管理员消息
async function handleAdminMessage(message) {
  const text = (message.text || '').trim();
  const reply = message.reply_to_message;

  // --- 管理指令区域 ---

  // 指令:/block (需回复用户消息)
  if (text === '/block') {
    if (reply && (reply.forward_from || reply.forward_sender_name)) {
      const guestChatId = await nfd.get('msg-map-'   reply.message_id);
      if (guestChatId) {
        await nfd.put('blocked-'   guestChatId, 'true'); // 永久拉黑
        return sendMessage({ chat_id: ADMIN_UID, text: `🚫 用户 ${guestChatId} 已被拉黑。` });
      } else {
        return sendMessage({ chat_id: ADMIN_UID, text: '⚠️ 无法获取用户ID,可能是旧消息。' });
      }
    } else {
      return sendMessage({ chat_id: ADMIN_UID, text: '⚠️ 请回复一条用户转发的消息来拉黑。' });
    }
  }

  // 指令:/unblock [ID] (支持回复或手输)
  if (text.startsWith('/unblock')) {
    const targetId = await getTargetId(message, '/unblock');
    if (targetId) {
      await nfd.delete('blocked-'   targetId);
      return sendMessage({ chat_id: ADMIN_UID, text: `✅ 用户 ${targetId} 已解封。` });
    } else {
      return sendMessage({ chat_id: ADMIN_UID, text: '⚠️ 格式错误。
请回复用户消息发送 /unblock
或发送 /unblock 123456 (必须是数字 ID)' });
    }
  }

  // 指令:/clear_ver [ID] (支持回复或手输)
  if (text.startsWith('/clear_ver')) {
    const targetId = await getTargetId(message, '/clear_ver');
    if (targetId) {
      await nfd.delete('verified-'   targetId);
      return sendMessage({ chat_id: ADMIN_UID, text: `🔄 用户 ${targetId} 验证状态已重置。` });
    } else {
      return sendMessage({ chat_id: ADMIN_UID, text: '⚠️ 格式错误。
请回复用户消息发送 /clear_ver
或发送 /clear_ver 123456 (必须是数字 ID)' });
    }
  }

  // --- 普通回复逻辑 ---

  // 检查是否在回复转发消息
  if (reply && (reply.forward_from || reply.forward_sender_name)) {
    const guestChatId = await nfd.get('msg-map-'   reply.message_id);
    if (guestChatId) {
      return copyMessage({
        chat_id: guestChatId,
        from_chat_id: message.chat.id,
        message_id: message.message_id,
      });
    } else {
      return sendMessage({
        chat_id: ADMIN_UID,
        text: '⚠️ 未找到原用户映射,可能消息太旧或被清理了缓存。'
      });
    }
  } else {
    // 既不是指令也不是回复
    return sendMessage({
      chat_id: ADMIN_UID,
      text: '🤖 管理面板

回复消息 = 发送给用户
回复并发送 /block = 拉黑
回复并发送 /unblock = 解封
回复并发送 /clear_ver = 重置验证'
    });
  }
}

// 处理验证流程
async function handleVerification(message, chatId, origin) {
  // 生成验证链接
  const verifyUrl = `${origin}/verify?uid=${chatId}`;

  return sendMessage({
    chat_id: chatId,
    text: '🛡 为了防止垃圾消息,请点击下方按钮完成人机验证:',
    reply_markup: {
      inline_keyboard: [[
        { text: '🤖 点击进行人机验证', web_app: { url: verifyUrl } }
      ]]
    }
  });
}

// 渲染验证页面
function handleVerifyPage(request) {
  const html = `



    
    
    人机验证
    
    
    
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
            margin: 0;
            background-color: #f0f2f5;
        }
        .container {
            background: white;
            padding: 2rem;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            text-align: center;
        }
        h1 { margin-bottom: 1.5rem; color: #1a1a1a; }
        .success-msg { color: #10b981; display: none; }
        .error-msg { color: #ef4444; display: none; margin-top: 1rem; }
    


    

请完成验证

✅ 验证成功!
请返回 Telegram 继续聊天。

验证失败,请刷新重试。

// 初始化 Telegram Web App const tg = window.Telegram.WebApp; tg.ready(); tg.expand(); // 尝试展开到最大高度 function onVerify(token) { const urlParams = new URLSearchParams(window.location.search); const uid = urlParams.get('uid'); if (!uid) { document.getElementById('error-msg').innerText = "错误:缺少用户 ID"; document.getElementById('error-msg').style.display = 'block'; return; } fetch('/verify-callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, uid }) }) .then(response => { if (response.ok) { document.getElementById('turnstile-widget').style.display = 'none'; document.getElementById('success-msg').style.display = 'block'; // 验证成功 1.5 秒后自动关闭窗口 setTimeout(() => { tg.close(); }, 1500); } else { throw new Error('Verification failed'); } }) .catch(err => { document.getElementById('error-msg').style.display = 'block'; }); } `; return new Response(html, { headers: { 'content-type': 'text/html;charset=UTF-8' } }); } // 处理验证回调 async function handleVerifyCallback(request) { if (request.method !== 'POST') { return new Response('Method Not Allowed', { status: 405 }); } try { const { token, uid } = await request.json(); if (!token || !uid) { return new Response('Missing token or uid', { status: 400 }); } // 向 Cloudflare 验证 Token const formData = new FormData(); formData.append('secret', CF_TURNSTILE_SECRET_KEY); formData.append('response', token); // formData.append('remoteip', request.headers.get('CF-Connecting-IP')); // 可选 const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', body: formData }).then(r => r.json()); if (result.success) { // 验证通过!写入 KV await nfd.put('verified-' uid, 'true', { expirationTtl: VERIFICATION_TTL }); // 主动通知用户验证成功 await sendMessage({ chat_id: uid, text: '✅ 验证通过!您可以直接发送消息给管理员了。' }); return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'content-type': 'application/json' } }); } else { return new Response(JSON.stringify({ success: false, error: result['error-codes'] }), { status: 400, headers: { 'content-type': 'application/json' } }); } } catch (e) { return new Response(e.message, { status: 500 }); } } // 处理访客消息 (已验证) async function handleGuestMessage(message) { const forwardReq = await forwardMessage({ chat_id: ADMIN_UID, from_chat_id: message.chat.id, message_id: message.message_id }); if (forwardReq.ok && forwardReq.result && forwardReq.result.message_id) { // 存储消息映射关系,用于管理员回复 // 这里也可以设置一个过期时间,比如 48 小时,避免 KV 爆炸 await nfd.put('msg-map-' forwardReq.result.message_id, message.chat.id.toString(), { expirationTtl: 172800 }); } else { await sendMessage({ chat_id: ADMIN_UID, text: `❌ 转发消息失败:${JSON.stringify(forwardReq)}` }); } } // ================================================================= // Webhook 设置工具 // ================================================================= async function registerWebhook(event, requestUrl, suffix, secret) { const webhookUrl = `${requestUrl.protocol}//${requestUrl.hostname}${suffix}`; const r = await (await fetch(apiUrl('setWebhook', { url: webhookUrl, secret_token: secret }))).json(); return new Response('ok' in r && r.ok ? 'Ok' : JSON.stringify(r, null, 2)); } async function unRegisterWebhook(event) { const r = await (await fetch(apiUrl('setWebhook', { url: '' }))).json(); return new Response('ok' in r && r.ok ? 'Ok' : JSON.stringify(r, null, 2)); }
点赞
  1. 阿卡迪亚说道:

    先赞后看

  2. beyond说道:

    收藏

  3. Femtocell说道:

    能不能别复制粘贴原来的教程 cf 这 ui 面板完全改版了根本没法按你的教程操作

    比如到这个操作

    3.配置 KV 数据库

    1.在 Workers 页面,点击左侧菜单的 KV

    只有在 Storage & databases 的下栏才能找到 worker KV这写的在左侧菜单,也不知道面板已经把这个设置改动到 worker 内了

    还有到这里

    向下滚动到 KV Namespace Bindings。

    点击 Add binding:
    Variable name: 必须填 nfd (代码中写死了这个名字)。
    KV Namespace: 选择刚才创建的 TG_BOT_KV。

    这里截图也上传不了,只有 bindings 的"add binding" 里才有"kv namespace"而且没有填入框只能添加代码,你这能不能自己亲手操作弄正确流程给大家分享下

    我本来想跟你 tg 机器人私聊跟进这些小问题的,但我想你大概率不回复,所以我只能论坛说了 •••

发表回复

电子邮件地址不会被公开。必填项已用 * 标注

×
订阅图标按钮