telegram双向机器人Turnstile 版

源码

const TOKEN = ENV_BOT_TOKEN // Get it from @BotFather
const WEBHOOK = '/endpoint'
const SECRET = ENV_BOT_SECRET // A-Z, a-z, 0-9, _ and -
const ADMIN_UID = ENV_ADMIN_UID // your user id, get it from https://t.me/username_to_id_bot

// Turnstile相关常量
const TURNSTILE_SITE_KEY = ENV_TURNSTILE_SITE_KEY // 从Cloudflare获取的站点密钥
const TURNSTILE_SECRET_KEY = ENV_TURNSTILE_SECRET_KEY // 从Cloudflare获取的密钥

const NOTIFY_INTERVAL = 3600 * 1000;
const fraudDb = 'https://raw.githubusercontent.com/LloydAsp/nfd/main/data/fraud.db';
const notificationUrl = 'https://raw.githubusercontent.com/LloydAsp/nfd/main/data/notification.txt'
const startMsgUrl = 'https://raw.githubusercontent.com/LloydAsp/nfd/main/data/startMessage.md';

const enable_notification = false

/**
 * Return url to telegram api, optionally with parameters added
 */
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), body)
    .then(r => r.json())
}

function makeReqBody(body){
  return {
    method:'POST',
    headers:{
      'content-type':'application/json'
    },
    body:JSON.stringify(body)
  }
}

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

function answerCallbackQuery(callbackQueryId, text = null, showAlert = false) {
  return requestTelegram('answerCallbackQuery', makeReqBody({
    callback_query_id: callbackQueryId,
    text: text,
    show_alert: showAlert
  }))
}

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

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

// 验证Turnstile token的函数
async function verifyTurnstileToken(token) {
  const formData = new FormData();
  formData.append('secret', TURNSTILE_SECRET_KEY);
  formData.append('response', token);
  
  const result = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body: formData
  }).then(response => response.json());
  
  return result.success;
}

/**
 * Wait for requests to the worker
 */
addEventListener('fetch', event => {
  const url = new URL(event.request.url)
  if (url.pathname === WEBHOOK) {
    event.respondWith(handleWebhook(event))
  } 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(handleVerificationPage(event))
  } else {
    event.respondWith(new Response('No handler for this request'))
  }
})

/**
 * Handle requests to WEBHOOK
 * https://core.telegram.org/bots/api#update
 */
async function handleWebhook (event) {
  // Check secret
  if (event.request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) {
    return new Response('Unauthorized', { status: 403 })
  }

  // Read request body synchronously
  const update = await event.request.json()
  // Deal with response asynchronously
  event.waitUntil(onUpdate(update))

  return new Response('Ok')
}

/**
 * Handle incoming Update
 * https://core.telegram.org/bots/api#update
 */
async function onUpdate (update) {
  if ('message' in update) {
    await onMessage(update.message)
  } else if ('callback_query' in update) {
    // 处理回调查询,用于Turnstile验证
    await onCallbackQuery(update.callback_query)
  }
}

/**
 * Handle incoming Callback Query
 * https://core.telegram.org/bots/api#callbackquery
 */
async function onCallbackQuery(callbackQuery) {
  const data = callbackQuery.data;
  const chatId = callbackQuery.message.chat.id;
  const messageId = callbackQuery.message.message_id;
  
  if (data === 'verify') {
    // 生成唯一验证ID
    const verifyId = 'verify_' + Date.now() + '_' + Math.random().toString(36).substring(2, 15);
    
    // 存储验证ID与用户ID的映射
    await nfd.put('verify-' + verifyId, chatId.toString());
    
    // 创建验证页面URL - 替换为您的实际Worker URL
    const verifyUrl = `https://xx.xx.worker.dev/verify?id=${verifyId}`;
    
    // 使用web_app类型的按钮,确保在Telegram内嵌浏览器中打开
    return requestTelegram('editMessageText', makeReqBody({
      chat_id: chatId,
      message_id: messageId,
      text: "为了保护机器人免受滥用,请点击下方按钮完成验证:",
      reply_markup: {
        inline_keyboard: [[{
          text: "🔒 完成验证",
          web_app: { url: verifyUrl }
        }]]
      }
    }));
  }
  
  // 回答回调查询
  return answerCallbackQuery(callbackQuery.id);
}

/**
 * Handle verification page
 */
async function handleVerificationPage(event) {
  const url = new URL(event.request.url);
  const verifyId = url.searchParams.get('id');
  
  // 如果是POST请求,处理验证
  if (event.request.method === 'POST') {
    try {
      const { token, verifyId } = await event.request.json();
      
      // 验证Turnstile token
      const isValid = await verifyTurnstileToken(token);
      
      if (isValid) {
        // 获取用户ID
        const chatId = await nfd.get('verify-' + verifyId);
        
        if (chatId) {
          // 标记用户已验证
          await nfd.put('verified-' + chatId, 'true');
          
          // 通知用户验证成功
          sendMessage({
            chat_id: parseInt(chatId),
            text: "✅ 验证成功!您现在可以使用机器人了。"
          });
          
          return new Response(JSON.stringify({ success: true }), {
            headers: { 'Content-Type': 'application/json' }
          });
        }
      }
      
      return new Response(JSON.stringify({ success: false }), {
        headers: { 'Content-Type': 'application/json' }
      });
    } catch (error) {
      return new Response(JSON.stringify({ success: false }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
  }
  
  if (!verifyId) {
    return new Response('Invalid request', { status: 400 });
  }
  
  // 获取用户ID
  const chatId = await nfd.get('verify-' + verifyId);
  
  if (!chatId) {
    return new Response('Verification link expired', { status: 400 });
  }
  
  // 返回包含Turnstile的HTML页面,完全适配Telegram内嵌浏览器
  const html = `
    <!DOCTYPE html>
    <html>
    <head>
      <title>机器人验证</title>
      <script src="https://telegram.org/js/telegram-web-app.js"></script>
      <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
      <style>
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        
        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
          background-color: var(--tg-theme-bg-color, #f5f5f5);
          color: var(--tg-theme-text-color, #333);
          min-height: 100vh;
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          padding: 20px;
          line-height: 1.5;
        }
        
        .container {
          background-color: var(--tg-theme-secondary-bg-color, white);
          border-radius: 16px;
          padding: 24px;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
          width: 100%;
          max-width: 400px;
          text-align: center;
        }
        
        .icon {
          font-size: 48px;
          margin-bottom: 16px;
        }
        
        h1 {
          font-size: 24px;
          margin-bottom: 16px;
          color: var(--tg-theme-text-color, #333);
        }
        
        p {
          color: var(--tg-theme-hint-color, #666);
          margin-bottom: 24px;
        }
        
        .turnstile-container {
          margin: 20px 0;
          display: flex;
          justify-content: center;
        }
        
        #status {
          margin-top: 20px;
          padding: 12px;
          border-radius: 8px;
          font-weight: 500;
          transition: all 0.3s ease;
        }
        
        .success {
          background-color: rgba(40, 167, 69, 0.1);
          color: #28a745;
        }
        
        .error {
          background-color: rgba(220, 53, 69, 0.1);
          color: #dc3545;
        }
        
        .loading {
          background-color: rgba(0, 123, 255, 0.1);
          color: #007bff;
        }
        
        .progress {
          width: 100%;
          height: 4px;
          background-color: var(--tg-theme-secondary-bg-color, #e9ecef);
          border-radius: 2px;
          overflow: hidden;
          margin-top: 16px;
        }
        
        .progress-bar {
          height: 100%;
          background-color: var(--tg-theme-button-color, #007bff);
          width: 0%;
          transition: width 0.3s ease;
        }
        
        .instructions {
          background-color: var(--tg-theme-secondary-bg-color, #f8f9fa);
          padding: 12px;
          border-radius: 8px;
          margin-bottom: 20px;
          font-size: 14px;
          color: var(--tg-theme-hint-color, #666);
        }
      </style>
    </head>
    <body>
      <div class="container">
        <div class="icon">🤖</div>
        <h1>机器人验证</h1>
        <p>请完成下面的验证以继续使用机器人</p>
        
        <div class="instructions">
          点击下方验证按钮,完成人机验证后即可继续使用
        </div>
        
        <div class="turnstile-container">
          <div class="cf-turnstile" data-sitekey="${TURNSTILE_SITE_KEY}" data-callback="onSuccess" data-error-callback="onError"></div>
        </div>
        
        <div id="status"></div>
        <div class="progress">
          <div class="progress-bar" id="progress"></div>
        </div>
      </div>
      
      <script>
        // 初始化Telegram Web App
        window.Telegram.WebApp.ready();
        window.Telegram.WebApp.expand();
        window.Telegram.WebApp.setHeaderColor('#ffffff');
        
        // 设置主按钮
        window.Telegram.WebApp.MainButton.setText('关闭').onClick(() => {
          window.Telegram.WebApp.close();
        }).show();
        
        // 进度条动画
        function updateProgress(percent) {
          document.getElementById('progress').style.width = percent + '%';
        }
        
        function onSuccess(token) {
          const statusEl = document.getElementById('status');
          statusEl.textContent = "验证中...";
          statusEl.className = "loading";
          updateProgress(30);
          
          fetch('/verify', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              token: token,
              verifyId: '${verifyId}'
            })
          })
          .then(response => {
            updateProgress(60);
            return response.json();
          })
          .then(data => {
            updateProgress(90);
            if (data.success) {
              statusEl.textContent = "✅ 验证成功!正在返回...";
              statusEl.className = "success";
              updateProgress(100);
              
              // 显示成功提示
              window.Telegram.WebApp.showPopup({
                title: "验证成功",
                message: "您现在可以使用机器人了",
                buttons: [{
                  text: "确定"
                }]
              });
              
              // 1.5秒后关闭Web App
              setTimeout(() => {
                window.Telegram.WebApp.close();
              }, 1500);
            } else {
              statusEl.textContent = "❌ 验证失败,请重试。";
              statusEl.className = "error";
              updateProgress(0);
              
              window.Telegram.WebApp.showPopup({
                title: "验证失败",
                message: "请重试验证",
                buttons: [{
                  text: "确定"
                }]
              });
            }
          })
          .catch(error => {
            statusEl.textContent = "❌ 发生错误,请重试。";
            statusEl.className = "error";
            updateProgress(0);
            
            window.Telegram.WebApp.showPopup({
              title: "错误",
              message: "发生错误,请重试",
              buttons: [{
                text: "确定"
              }]
            });
            
            console.error('Error:', error);
          });
        }
        
        function onError() {
          const statusEl = document.getElementById('status');
          statusEl.textContent = "❌ 验证出错,请刷新页面重试";
          statusEl.className = "error";
          updateProgress(0);
        }
        
        // 监听Web App关闭事件
        window.Telegram.WebApp.onEvent('viewportChanged', () => {
          window.Telegram.WebApp.expand();
        });
      </script>
    </body>
    </html>
  `;
  
  return new Response(html, {
    headers: { 
      'Content-Type': 'text/html',
      'Access-Control-Allow-Origin': '*'
    }
  });
}

/**
 * Handle incoming Message
 * https://core.telegram.org/bots/api#message
 */
async function onMessage (message) {
  if(message.text === '/start'){
    // 检查用户是否已验证
    const chatId = message.chat.id;
    const isVerified = await nfd.get('verified-' + chatId);
    
    if (!isVerified) {
      return sendMessage({
        chat_id: chatId,
        text: "👋 欢迎使用龘龘的私聊机器人!\n\n为了保护机器人免受滥用,请先完成验证:",
        reply_markup: {
          inline_keyboard: [[{
            text: "🔒 验证",
            callback_data: "verify"
          }]]
        }
      });
    }
    
    return sendMessage({
      chat_id: chatId,
      text: "👋 欢迎使用龘龘的私聊机器人",
    });
  }
  
  // 对于其他消息,也检查验证状态
  const chatId = message.chat.id;
  const isVerified = await nfd.get('verified-' + chatId);
  
  if (!isVerified && chatId.toString() !== ADMIN_UID) {
    return sendMessage({
      chat_id: chatId,
      text: "⚠️ 请先完成验证才能使用机器人功能。",
      reply_markup: {
        inline_keyboard: [[{
          text: "🔒 验证",
          callback_data: "verify"
        }]]
      }
    });
  }
  
  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 nfd.get('msg-map-' + message?.reply_to_message.message_id,
                                      { type: "json" })
    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 nfd.get('isblocked-' + chatId, { type: "json" })
  
  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 nfd.put('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 nfd.get('lastmsg-' + chatId, { type: "json" })
    if(!lastMsgTime || Date.now() - lastMsgTime > NOTIFY_INTERVAL){
      await nfd.put('lastmsg-' + chatId, Date.now())
      return sendMessage({
        chat_id: ADMIN_UID,
        text:await fetch(notificationUrl).then(r => r.text())
      })
    }
  }
}

async function handleBlock(message){
  let guestChantId = await nfd.get('msg-map-' + message.reply_to_message.message_id,
                                      { type: "json" })
  if(guestChantId === ADMIN_UID){
    return sendMessage({
      chat_id: ADMIN_UID,
      text:'不能屏蔽自己'
    })
  }
  await nfd.put('isblocked-' + guestChantId, true)

  return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}屏蔽成功`,
  })
}

async function handleUnBlock(message){
  let guestChantId = await nfd.get('msg-map-' + message.reply_to_message.message_id,
  { type: "json" })

  await nfd.put('isblocked-' + guestChantId, false)

  return sendMessage({
    chat_id: ADMIN_UID,
    text:`UID:${guestChantId}解除屏蔽成功`,
  })
}

async function checkBlock(message){
  let guestChantId = await nfd.get('msg-map-' + message.reply_to_message.message_id,
  { type: "json" })
  let blocked = await nfd.get('isblocked-' + guestChantId, { type: "json" })

  return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}` + (blocked ? '被屏蔽' : '没有被屏蔽')
  })
}

/**
 * Send plain text message
 * https://core.telegram.org/bots/api#sendmessage
 */
async function sendPlainText (chatId, text) {
  return sendMessage({
    chat_id: chatId,
    text
  })
}

/**
 * Set webhook to this worker's url
 * https://core.telegram.org/bots/api#setwebhook
 */
async function registerWebhook (event, requestUrl, suffix, secret) {
  // https://core.telegram.org/bots/api#setwebhook
  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))
}

/**
 * Remove webhook
 * https://core.telegram.org/bots/api#setwebhook
 */
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))
}

async function isFraud(id){
  id = id.toString()
  let db = await fetch(fraudDb).then(r => r.text())
  let arr = db.split('\n').filter(v => v)
  console.log(JSON.stringify(arr))
  let flag = arr.filter(v => v === id).length !== 0
  console.log(flag)
  return flag
}

环境变量设置

ENV_ADMIN_UID #管理员id 可在@myidbot获取
ENV_BOT_SECRET #随机加密字符串
ENV_BOT_TOKEN # 机器人token
ENV_TURNSTILE_SECRET_KEY  #turnstile 密钥
ENV_TURNSTILE_SITE_KEY #turnstile站点密钥

1
2
3
4

https://xx.xx.worker.dev 代码中要替换成你真实的地址