Auto-Delete Telegram Bot Messages with Cloudflare Workers

Preface

Set up a keyword-reply bot in a group. There are tons of projects on Github. Skipping that.
But there was never a feature to auto-delete reply messages after a delay.
In my understanding, a worker only runs when an HTTP request comes in, meaning it only runs when someone sends a message in the group. So naturally, it couldn't handle delayed message deletion.

Inspiration

Today I had a sudden idea and asked the AI, "How to implement scheduled tasks with cloudflare workers?" 

Then came the boring GPT-oriented development
The pasted code is a telegram bot based on cloudflare worker.  
I want to implement a feature to delete the bot's reply messages after a delay of 5~10 minutes.

I bound a KV to this worker:  BOT_MSG, used to save the data of sent messages.

I set a cron trigger for this worker to run every 5 minutes.

Please improve this code

Let me paste the code here.
const TOKEN = 'your_bot_token'
const WEBHOOK = '/endpoint'
const SECRET = 'you_should_generate_random_string'

const DELETE_AFTER_MS = 5 * 60 * 1000 // 5 minutes

/**
 * Store sent messages into KV
 * key: msg:{delete_at}:{chat_id}:{message_id}
 */
async function saveMessage(env, chatId, messageId) {
  const deleteAt = String(Date.now() + DELETE_AFTER_MS).padStart(16, '0')
  const key = `msg:${deleteAt}:${chatId}:${messageId}`
  await env.BOT_MSG.put(key, '1', {
    expirationTtl: 20 * 60 // 20 minutes fallback cleanup
  })
}

/**
 * Cron trigger: delete all expired messages
 */
async function handleScheduled(env) {
  const now = Date.now()
  const { keys } = await env.BOT_MSG.list({ prefix: 'msg:' })

  for (const { name } of keys) {
    // key format: msg:{delete_at}:{chat_id}:{message_id}
    const [, deleteAt, chatId, messageId] = name.split(':')
    if (parseInt(deleteAt) <= now) {
      await deleteMessage(chatId, messageId)
      await env.BOT_MSG.delete(name)
    }
  }
}

/**
 * Call Telegram API to delete a message
 */
async function deleteMessage(chatId, messageId) {
  return (await fetch(apiUrl('deleteMessage', {
    chat_id: chatId,
    message_id: messageId
  }))).json()
}

// ─── Export ────

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url)
    if (url.pathname === WEBHOOK) {
      return handleWebhook(request, env)
    } else if (url.pathname === '/registerWebhook') {
      return registerWebhook(url, WEBHOOK, SECRET)
    } else if (url.pathname === '/unRegisterWebhook') {
      return unRegisterWebhook()
    } else {
      return new Response('No handler for this request')
    }
  },

  async scheduled(event, env, ctx) {
    ctx.waitUntil(handleScheduled(env))
  }
}

// ─── Webhook ────

async function handleWebhook(request, env) {
  if (request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) {
    return new Response('Unauthorized', { status: 403 })
  }
  const update = await request.json()
  await onUpdate(update, env)
  return new Response('Ok')
}

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

// ─── Message Handler ────
async function onMessage(message, env) {
  const text = message.text.toLowerCase();
  const originalText = message.text;

  // Detect English keywords
  const hasHappy = text.includes('happy');
  const hasNew = text.includes('new');
  const hasYear = text.includes('year');

  if (hasHappy && hasNew && hasYear) {
    const result = await sendPlainText(message.chat.id, 'Happy New Year to you, too! 🎁', message.message_id);
    if (result?.ok && result?.result?.message_id) {
      await saveMessage(env, message.chat.id, result.result.message_id)
    }
    return;
  }

  // Detect Chinese keywords
  const has新 = originalText.includes('新');
  const has年 = originalText.includes('年');
  const has快 = originalText.includes('快');
  const has乐 = originalText.includes('乐');

  if (has新 && has年 && has快 && has乐) {
    const result = await sendPlainText(message.chat.id, '也祝你新年快乐! 🧧', message.message_id);
    if (result?.ok && result?.result?.message_id) {
      await saveMessage(env, message.chat.id, result.result.message_id)
    }
    return;
  }

  // 233 group's script keyword
  const has脚本 = originalText.includes('脚本');
  if (has脚本) {
    const replyText = `
安装 sing-box 脚本 (第一次运行默认就安装 Reality 协议):
bash <(wget -qO- -o- https://github.com/233boy/sing-box/raw/main/install.sh)

安装 Xray 脚本 (第一次运行默认就安装 Reality 协议):
bash <(wget -qO- -o- https://github.com/233boy/Xray/raw/main/install.sh)
安装 V2Ray 脚本 (不要用 第一次运行默认装出来的 VMESS+TCP):
bash <(wget -qO- -o- https://github.com/233boy/v2ray/raw/master/install.sh)
`;
    const result = await sendPlainText(message.chat.id, replyText, message.message_id);
    if (result?.ok && result?.result?.message_id) {
      await saveMessage(env, message.chat.id, result.result.message_id)
    }
    return;
  }
}

// ─── Telegram API Helpers ────

async function sendPlainText(chatId, text, replyToMessageId = null) {
  return (await fetch(apiUrl('sendMessage', {
    chat_id: chatId,
    text,
    reply_to_message_id: replyToMessageId,
    parse_mode: 'HTML'
  }))).json()
}

async function registerWebhook(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() {
  const r = await (await fetch(apiUrl('setWebhook', { url: '' }))).json()
  return new Response('ok' in r && r.ok ? 'Ok' : JSON.stringify(r, null, 2))
}

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

========

Postscript

The GPT used this time is

========

Related Recommendations


Comments