From af343b974f9d6d69d4ba926919ef27595a3a3eb4 Mon Sep 17 00:00:00 2001 From: "A.M. Hasani" Date: Tue, 16 Jun 2026 10:35:26 +0330 Subject: [PATCH 1/5] Update worker.js --- worker.js | 2143 ++++++++++++++++++++--------------------------------- 1 file changed, 809 insertions(+), 1334 deletions(-) diff --git a/worker.js b/worker.js index 1bb5c54..c9854a6 100644 --- a/worker.js +++ b/worker.js @@ -1,1386 +1,861 @@ /** - * Telegram Rich Markdown Bot — Cloudflare Worker - * ÐΛɌ₭ᑎΞ𐒡𐒡 https://github.com/DarknessShade - * Bot API 10.1 + * Telegram Rich Markdown & Channel Formatter Bot — Cloudflare Worker + * + * Features: + * - Direct Messages: Renders any Markdown/HTML sent to the bot. + * - Channels: Automatically formats posts containing Markdown/HTML tags on publish. + * - Smart Edit Detection: Ignores posts without formatting to avoid unnecessary (edited) marks. + * - Per-channel disconnect & channel-only tag guide. + * - Admin Panel: /stats and /broadcast (Restricted to ADMIN_ID). + * - Persistent KV Cache for users, channels, and stats tracking. */ -const BOT_TOKEN = "Enter your Telegram bot token"; +const BOT_TOKEN = "BOT TOKEN ADD KON INJA"; +const ADMIN_ID = 12345677899; #adminuserid const TELEGRAM_API = `https://api.telegram.org/bot${BOT_TOKEN}`; +const TTL_PROCESSED_SEC = 30 * 24 * 60 * 60; // 30 days +const TTL_CHANNEL_SEC = 30 * 24 * 60 * 60; // 30 days export default { - async fetch(request) { - if (request.method === "GET") return new Response("✅ Bot is running!", { status: 200 }); - if (request.method !== "POST") return new Response("OK"); - - let update; - try { update = await request.json(); } catch { return new Response("Bad JSON", { status: 400 }); } - - try { - const message = update.message; - const callbackQuery = update.callback_query; - if (callbackQuery) await handleCallback(callbackQuery); - else if (message?.text) await handleMessage(message); - } catch (err) { - console.error("Handler error:", err && err.stack ? err.stack : err); - // Try to notify the chat if possible, but never fail the response - try { - const chatId = update.message?.chat?.id || update.callback_query?.message?.chat?.id; - if (chatId) { - await fetch(`${TELEGRAM_API}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: chatId, text: `⚠️ Internal error: ${err?.message || err}` }), - }); - } - } catch {} - } - - // ALWAYS return 200 — Telegram disables webhooks after repeated non-200 responses - return new Response("OK", { status: 200 }); - }, + async fetch(request, env) { + if (request.method === "GET") return new Response("✅ Bot is running!", { status: 200 }); + if (request.method !== "POST") return new Response("OK"); + if (!env.DB) { + console.error("KV Namespace 'DB' is not bound!"); + return new Response("KV Setup Error", { status: 500 }); + } + let update; + try { + update = await request.json(); + } catch { + return new Response("Bad JSON", { status: 400 }); + } + try { + if (update.message) { + await handleMessage(update.message, env); + } else if (update.callback_query) { + await handleCallback(update.callback_query, env); + } else if (update.channel_post) { + await handleChannelPost(update.channel_post, env); + } + } catch (err) { + console.error("Handler error:", err && err.stack ? err.stack : err); + } + return new Response("OK", { status: 200 }); + }, }; -// ─── Keyboards ──────────────────────────────────────────────────────────────── -function mainKeyboard(lang) { - if (lang === "fa") return { - inline_keyboard: [ - [ - { text: "📖 راهنمای Markdown", callback_data: "fa_help_md" }, - { text: "🌐 راهنمای HTML", callback_data: "fa_help_html" }, - ], - [ - { text: "🖼 راهنمای مدیا", callback_data: "fa_help_media" }, - ], - [ - { text: "🎨 دمو کامل", callback_data: "fa_demo" }, - { text: "ℹ️ درباره بات", callback_data: "fa_about" }, - ], - [ - { text: "🇬🇧 Switch to English", callback_data: "en_start" }, - ], - ], - }; - return { - inline_keyboard: [ - [ - { text: "📖 Markdown Guide", callback_data: "en_help_md" }, - { text: "🌐 HTML Guide", callback_data: "en_help_html" }, - ], - [ - { text: "🖼 Media Guide", callback_data: "en_help_media" }, - ], - [ - { text: "🎨 Full Demo", callback_data: "en_demo" }, - { text: "ℹ️ About", callback_data: "en_about" }, - ], - [ - { text: "🇮🇷 تغییر به فارسی", callback_data: "fa_start" }, - ], - ], - }; +// ─── KV Storage Helpers & Stats ─────────────────────────────────────────────── +async function registerUser(env, userId) { + const key = `user:${userId}`; + const exists = await env.DB.get(key); + if (!exists) { + await env.DB.put(key, "1"); + await incrementStat(env, "stats:users"); + } +} +async function kvAuthorizeChannel(env, channelId) { + await env.DB.put(`channel:${channelId}`, "authorized", { expirationTtl: TTL_CHANNEL_SEC }); +} +async function kvDeauthorizeChannel(env, channelId) { + await env.DB.delete(`channel:${channelId}`); +} +async function kvIsChannelAuthorized(env, channelId) { + return (await env.DB.get(`channel:${channelId}`)) !== null; +} +async function kvHasProcessedMessage(env, channelId, messageId) { + return (await env.DB.get(`processed:${channelId}:${messageId}`)) !== null; +} +async function kvMarkMessageProcessed(env, channelId, messageId) { + await env.DB.put(`processed:${channelId}:${messageId}`, "1", { expirationTtl: TTL_PROCESSED_SEC }); +} +async function incrementStat(env, key) { + let count = parseInt(await env.DB.get(key)) || 0; + await env.DB.put(key, (count + 1).toString()); +} +async function getCachedBot(env) { + let botStr = await env.DB.get("cache:bot"); + if (botStr) return JSON.parse(botStr); + const me = await callApi("getMe", {}); + const bot = { id: me?.result?.id, username: me?.result?.username }; + if (!bot.id) throw new Error("getMe failed"); + await env.DB.put("cache:bot", JSON.stringify(bot), { expirationTtl: 86400 }); + return bot; +} +async function kvLinkUserToChannel(env, userId, channelId, title) { + const key = `user_channels:${userId}`; + let channels = await env.DB.get(key, "json"); + if (!channels || !Array.isArray(channels)) channels = []; + if (!channels.find(c => String(c.id) === String(channelId))) { + channels.push({ id: channelId, title: title }); + await env.DB.put(key, JSON.stringify(channels)); + } +} +async function kvUnlinkUserFromChannel(env, userId, channelId) { + const key = `user_channels:${userId}`; + let channels = await env.DB.get(key, "json"); + if (!channels || !Array.isArray(channels)) return; + channels = channels.filter(c => String(c.id) !== String(channelId)); + await env.DB.put(key, JSON.stringify(channels)); +} +async function kvGetUserChannels(env, userId) { + const key = `user_channels:${userId}`; + const channels = await env.DB.get(key, "json"); + return channels || []; } +// ─── Keyboards ──────────────────────────────────────────────────────────────── +function mainKeyboard(lang, botUsername) { + const isFa = lang === "fa"; + return { + inline_keyboard: [ + [ + { text: isFa ? "📖 راهنمای Markdown" : "📖 Markdown Guide", callback_data: `${lang}_help_md` }, + { text: isFa ? "🌐 راهنمای HTML" : "🌐 HTML Guide", callback_data: `${lang}_help_html` }, + ], + [ + { text: isFa ? "🖼 راهنمای مدیا" : "🖼 Media Guide", callback_data: `${lang}_help_media` }, + { text: isFa ? "📢 راهنمای کانال" : "📢 Channel Guide", callback_data: `${lang}_help_channel` }, + ], + [ + { text: isFa ? "🎨 دمو کامل" : "🎨 Full Demo", callback_data: `${lang}_demo` }, + { text: isFa ? "ℹ️ درباره بات" : "ℹ️ About", callback_data: `${lang}_about` }, + ], + [ + { + text: isFa ? "➕ افزودن به کانال" : "➕ Add to Channel", + url: `https://t.me/${botUsername}?startchannel&admin=post_messages+edit_messages`, + }, + ], + [ + { text: isFa ? "🇬🇧 Switch to English" : "🇮🇷 تغییر به فارسی", callback_data: isFa ? "en_start" : "fa_start" }, + ], + ], + }; +} function backKeyboard(lang) { - return { - inline_keyboard: [ - [ - lang === "fa" - ? { text: "⬅️ بازگشت به منو", callback_data: "fa_back" } - : { text: "⬅️ Back to Menu", callback_data: "en_back" }, - lang === "fa" - ? { text: "🇬🇧 English", callback_data: "en_start" } - : { text: "🇮🇷 فارسی", callback_data: "fa_start" }, - ], - ], - }; + const isFa = lang === "fa"; + return { + inline_keyboard: [ + [ + { text: isFa ? "⬅️ بازگشت به منو" : "⬅️ Back to Menu", callback_data: `${lang}_back` }, + { text: isFa ? "🇬🇧 English" : "🇮🇷 فارسی", callback_data: isFa ? "en_start" : "fa_start" }, + ], + ], + }; +} +// Dynamic keyboard for the channel guide: tag-guide button + a disconnect button per channel. +function channelGuideKeyboard(lang, userChannels) { + const isFa = lang === "fa"; + const rows = []; + rows.push([ + { text: isFa ? "🏷 تگ‌های مخصوص کانال" : "🏷 Channel-only Tags", callback_data: `${lang}_chtags` }, + ]); + for (const c of userChannels || []) { + rows.push([ + { + text: (isFa ? "🔌 قطع اتصال: " : "🔌 Disconnect: ") + (c.title || c.id), + callback_data: `${lang}_off_${c.id}`, + }, + ]); + } + rows.push([ + { text: isFa ? "⬅️ بازگشت به منو" : "⬅️ Back to Menu", callback_data: `${lang}_back` }, + { text: isFa ? "🇬🇧 English" : "🇮🇷 فارسی", callback_data: isFa ? "en_start" : "fa_start" }, + ]); + return { inline_keyboard: rows }; +} +function backToChannelKeyboard(lang) { + const isFa = lang === "fa"; + return { + inline_keyboard: [[ + { text: isFa ? "⬅️ بازگشت به راهنمای کانال" : "⬅️ Back to Channel Guide", callback_data: `${lang}_help_channel` }, + ]], + }; } +const LANG_SELECT_MESSAGE = "🌐 Please choose your language / زبان خود را انتخاب کنید:"; +const LANG_SELECT_KEYBOARD = { + inline_keyboard: [[ + { text: "🇮🇷 فارسی", callback_data: "fa_start" }, + { text: "🇬🇧 English", callback_data: "en_start" }, + ]], +}; // ─── Handlers ───────────────────────────────────────────────────────────────── -async function handleMessage(message) { - const chatId = message.chat.id; - const rawText = message.text; - const trimmed = rawText.trim(); - - if (trimmed === "/start" || trimmed === "/help") { - // Show language selection first - await sendPlain(chatId, LANG_SELECT_MESSAGE, LANG_SELECT_KEYBOARD); - return; - } - - // Telegram clients auto-format `**bold**`, ```code```, etc. typed by the user - // into formatting entities and STRIP the raw markdown syntax from message.text. - // Reconstruct the original Markdown/HTML from text + entities before echoing it. - let text = entitiesToMarkdown(rawText, message.entities).trim(); - if (!text) text = trimmed; - - if (text.startsWith("<") || /<\/?\w/.test(text)) { - await sendRichHtml(chatId, text); - } else { - await sendRichMarkdown(chatId, text); - } +async function handleMessage(message, env) { + const chatId = message.chat.id; + const userId = message.from?.id; + if (!userId) return; + const rawText = message.text || ""; + const trimmed = rawText.trim(); + await registerUser(env, userId); + + // --- ADMIN COMMANDS --- + if (userId === ADMIN_ID) { + if (trimmed === "/stats") { + const users = (await env.DB.get("stats:users")) || "0"; + const processed = (await env.DB.get("stats:processed")) || "0"; + await sendPlain(chatId, `📊 **Bot Statistics:**\n\n👥 Total Users: ${users}\n🔄 Rendered Posts: ${processed}`); + return; + } + if (trimmed.startsWith("/broadcast ")) { + const bText = trimmed.replace("/broadcast ", ""); + await sendPlain(chatId, "⏳ Broadcasting message..."); + let cursor = null; + let count = 0; + do { + const list = await env.DB.list({ prefix: "user:", cursor }); + for (const key of list.keys) { + const uid = key.name.split(":")[1]; + await callApi("sendMessage", { chat_id: uid, text: bText }); + count++; + } + cursor = list.list_complete ? null : list.cursor; + } while (cursor); + await sendPlain(chatId, `✅ Broadcast sent to ${count} users.`); + return; + } + } + + // --- CHANNEL AUTHORIZATION VIA FORWARD --- + const fwdChat = message.forward_from_chat || message.forward_origin?.chat; + if (fwdChat && fwdChat.type === "channel") { + const channelId = fwdChat.id; + const botInfo = await getCachedBot(env); + const member = await callApi("getChatMember", { chat_id: channelId, user_id: botInfo.id }); + const status = member?.result?.status; + const canEdit = member?.result?.can_edit_messages; + + if ((status === "administrator" || status === "creator") && canEdit) { + await kvAuthorizeChannel(env, channelId); + await kvLinkUserToChannel(env, userId, channelId, fwdChat.title || channelId.toString()); + + const guideText = `✅ **کانال با موفقیت ثبت شد:** ${fwdChat.title || channelId}\n\nربات از این پس پست‌های دارای مارک‌داون یا HTML را هنگام انتشار، به‌صورت خودکار در این کانال رندر می‌کند.\n\n---\n\n` + HELP_CHANNEL["fa"]; + const userChannels = await kvGetUserChannels(env, userId); + await sendRichMarkdown(chatId, guideText, channelGuideKeyboard("fa", userChannels)); + } else { + await sendPlain(chatId, `❌ **احراز هویت شکست خورد.**\nربات باید در کانال ادمین باشد و دسترسی ویرایش پیام (Edit Messages) داشته باشد.`); + } + return; + } + + // --- NORMAL COMMANDS & MESSAGES --- + if (trimmed === "/start" || trimmed === "/help") { + await sendPlain(chatId, LANG_SELECT_MESSAGE, LANG_SELECT_KEYBOARD); + return; + } + + let text = entitiesToMarkdown(rawText, message.entities).trim(); + if (!text) text = trimmed; + if (!text) return; + + // Convert custom ///.../// to standard markdown code blocks + text = text.replace(/\/\/\/([\s\S]+?)\/\/\//g, "```\n$1\n```"); + + if (text.startsWith("<") || /<\/?\w/.test(text)) { + await sendRichHtml(chatId, text); + } else { + await sendRichMarkdown(chatId, text); + } } -// ─── Convert Telegram message entities back into Markdown/HTML source ───────── -function entitiesToMarkdown(text, entities) { - if (!entities || !entities.length) return text; - - const items = entities.map((e, idx) => ({ e, idx, start: e.offset, end: e.offset + e.length })); - - function isTopLevel(item, pool) { - return !pool.some(other => { - if (other.idx === item.idx) return false; - const strictlyLarger = - other.start <= item.start && other.end >= item.end && - (other.start < item.start || other.end > item.end); - const sameSpanOuter = - other.start === item.start && other.end === item.end && other.idx < item.idx; - return strictlyLarger || sameSpanOuter; - }); - } - - function render(start, end, pool) { - const inRange = pool.filter(it => it.start >= start && it.end <= end); - const top = inRange.filter(it => isTopLevel(it, inRange)).sort((a, b) => a.start - b.start); - - let out = ""; - let pos = start; - for (const item of top) { - out += text.slice(pos, item.start); - // Exclude this item itself to avoid infinite recursion on its own range - const innerPool = pool.filter(p => p.idx !== item.idx); - const inner = render(item.start, item.end, innerPool); - out += wrapEntity(item.e, inner); - pos = item.end; - } - out += text.slice(pos, end); - return out; - } - - return render(0, text.length, items); +async function handleCallback(cb, env) { + const chatId = cb.message.chat.id; + const msgId = cb.message.message_id; + const data = cb.data; + await callApi("answerCallbackQuery", { callback_query_id: cb.id }); + const lang = data.startsWith("fa_") ? "fa" : "en"; + const action = data.slice(3); + const kb = backKeyboard(lang); + const botInfo = await getCachedBot(env); + const main = mainKeyboard(lang, botInfo.username); + + if (action === "start" || action === "back") { + await editRichMarkdown(chatId, msgId, WELCOME[lang], main); + } else if (action === "help_md") { + await editRichMarkdown(chatId, msgId, HELP_MD[lang], kb); + } else if (action === "help_html") { + await editRichMarkdown(chatId, msgId, HELP_HTML[lang], kb); + } else if (action === "help_media") { + await editRichMarkdown(chatId, msgId, HELP_MEDIA[lang], kb); + } else if (action === "help_channel") { + const userChannels = await kvGetUserChannels(env, cb.from.id); + let prefix = ""; + if (userChannels.length > 0) { + const list = userChannels.map(c => `• ${c.title}`).join("\n"); + prefix = lang === "fa" + ? `✅ **کانال‌های ثبت‌شده‌ی شما:**\n${list}\n\nبرای افزودن کانال جدید، من را در کانال ادمین کنید و یک پیام از آنجا به اینجا فوروارد کنید.\n\n---\n` + : `✅ **Your Authorized Channels:**\n${list}\n\nTo add a new channel, make me an admin there and forward a message here.\n\n---\n`; + } else { + prefix = lang === "fa" + ? `❌ **شما هنوز هیچ کانالی ثبت نکرده‌اید.**\n\nبرای افزودن کانال، من را در کانال ادمین کنید و یک پیام از آنجا به اینجا فوروارد کنید.\n\n---\n` + : `❌ **You haven't authorized any channels yet.**\n\nTo add a channel, make me an admin there and forward a message here.\n\n---\n`; + } + await editRichMarkdown(chatId, msgId, prefix + HELP_CHANNEL[lang], channelGuideKeyboard(lang, userChannels)); + } else if (action === "chtags") { + // Guide: which tags/styles work in the bot but NOT in the channel. + await editRichMarkdown(chatId, msgId, HELP_CHANNEL_TAGS[lang], backToChannelKeyboard(lang)); + } else if (action.startsWith("off_")) { + // Disconnect a channel. + const channelId = action.slice(4); + await kvDeauthorizeChannel(env, channelId); + await kvUnlinkUserFromChannel(env, cb.from.id, channelId); + const userChannels = await kvGetUserChannels(env, cb.from.id); + const confirm = lang === "fa" + ? `🔌 **اتصال قطع شد.** ربات دیگر پست‌های آن کانال را قالب‌بندی نمی‌کند.\nهر زمان خواستی دوباره وصل کنی، کافیست یک پیام از کانال را به اینجا فوروارد کنی.\n\n---\n` + : `🔌 **Disconnected.** The bot will no longer format posts in that channel.\nTo reconnect anytime, just forward a message from the channel here.\n\n---\n`; + await editRichMarkdown(chatId, msgId, confirm + HELP_CHANNEL[lang], channelGuideKeyboard(lang, userChannels)); + } else if (action === "demo") { + await editRichMarkdown(chatId, msgId, DEMO[lang], kb); + } else if (action === "about") { + await editRichMarkdown(chatId, msgId, ABOUT[lang], kb); + } } -function wrapEntity(e, content) { - switch (e.type) { - case "bold": return `**${content}**`; - case "italic": return `*${content}*`; - case "underline": return `${content}`; - case "strikethrough": return `~~${content}~~`; - case "spoiler": return `||${content}||`; - case "code": return `\`${content}\``; - case "pre": { - const lang = e.language || ""; - return "```" + lang + "\n" + content + "\n```"; - } - case "text_link": - return `[${content}](${e.url})`; - case "text_mention": - return e.user ? `[${content}](tg://user?id=${e.user.id})` : content; - case "blockquote": - case "expandable_blockquote": - return content.split("\n").map(l => `>${l}`).join("\n"); - default: - // mention, hashtag, cashtag, bot_command, url, email, phone_number, custom_emoji, etc. - return content; - } +// ─── Channel Post Formatter ─────────────────────────────────────────────────── +async function handleChannelPost(post, env) { + const channelId = post.chat?.id; + const messageId = post.message_id; + if (!channelId || !messageId) return; + if (!(await kvIsChannelAuthorized(env, channelId))) return; + if (await kvHasProcessedMessage(env, channelId, messageId)) return; + + const isCaption = post.caption !== undefined; + const rawText = isCaption ? post.caption : post.text; + const entities = isCaption ? post.caption_entities : post.entities; + if (!rawText || !rawText.trim()) return; + + // Check the RAW text for markdown tags to prevent the bot from editing normal posts + // or posts where formatting was natively applied by the user (like standard URLs). + if (!hasRealFormatting(rawText)) return; + + let text = entitiesToMarkdown(rawText, entities).trim(); + if (!text) text = rawText.trim(); + + // Convert custom ///.../// to standard markdown code blocks + text = text.replace(/\/\/\/([\s\S]+?)\/\/\//g, "```\n$1\n```"); + + await kvMarkMessageProcessed(env, channelId, messageId); + + let res; + if (isCaption) { + // Captions have NO rich field — only classic inline formatting via parse_mode. + res = await callApi("editMessageCaption", { + chat_id: channelId, + message_id: messageId, + caption: mdInlineToHtml(text), + parse_mode: "HTML", + }); + } else { + // FIX: text posts now use Telegram's NATIVE Rich renderer (same as DMs), + // so to-do lists / tables / headings render exactly like inside the bot. + const isHtml = text.startsWith("<") || /<\/?\w/.test(text); + res = await callApi("editMessageText", { + chat_id: channelId, + message_id: messageId, + rich_message: isHtml ? { html: text } : { markdown: text }, + }); + } + + if (res?.ok) { + await kvAuthorizeChannel(env, channelId); + await incrementStat(env, "stats:processed"); + } } -async function handleCallback(cb) { - const chatId = cb.message.chat.id; - const msgId = cb.message.message_id; - const data = cb.data; - - await fetch(`${TELEGRAM_API}/answerCallbackQuery`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ callback_query_id: cb.id }), - }); - - // Parse lang prefix: "fa_help_md" → lang="fa", action="help_md" - const lang = data.startsWith("fa_") ? "fa" : "en"; - const action = data.slice(3); // remove "fa_" or "en_" +// ─── Format Detectors & Helpers ─────────────────────────────────────────────── +function hasRealFormatting(t) { + return ( + /\*\*[^\n]+?\*\*/.test(t) || // **bold** + /__[^\n]+?__/.test(t) || // __bold__ + /~~[^\n]+?~~/.test(t) || // ~~strike~~ + /\|\|[^\n]+?\|\|/.test(t) || // ||spoiler|| + /`[^`\n]+?`/.test(t) || // `code` + /```[\s\S]+?```/.test(t) || // code block + /\/\/\/[\s\S]+?\/\/\//.test(t) || // /// code block /// + /(^|\n)\s{0,3}#{1,6}\s+\S/.test(t) || // # heading (needs a space) + /(^|\n)\s*[-*+]\s*\[[ xX]\]\s+\S/.test(t) || // - [ ] / - [x] task + /(^|\n)\s*[-*+]\s+\S/.test(t) || // - bullet + /(^|\n)\s*\d+\.\s+\S/.test(t) || // 1. ordered + /(^|\n)\s*>\s+\S/.test(t) || // > quote + /\[[^\]\n]+\]\([^)\n]+\)/.test(t) || // [link](url) + /(^|\n)\s*\|.+\|\s*(\n|$)/.test(t) || // | table | + /\$\$[\s\S]+?\$\$/.test(t) || // $$ block math $$ + /\$[^$\n]+\$/.test(t) || // $ inline math $ + /<\/?\w+[^>]*>/.test(t) // any real HTML tag + ); +} - const kb = backKeyboard(lang); - const main = mainKeyboard(lang); +function escapeHtml(s) { + return String(s).replace(/&/g, "&").replace(//g, ">"); +} - if (action === "start" || action === "back") { - await editRichMarkdown(chatId, msgId, WELCOME[lang], main); - } else if (action === "help_md") { - await editRichMarkdown(chatId, msgId, HELP_MD[lang], kb); - } else if (action === "help_html") { - await editRichMarkdown(chatId, msgId, HELP_HTML[lang], kb); - } else if (action === "help_media") { - await editRichMarkdown(chatId, msgId, HELP_MEDIA[lang], kb); - } else if (action === "demo") { - await editRichMarkdown(chatId, msgId, DEMO[lang], kb); - } else if (action === "about") { - await editRichMarkdown(chatId, msgId, ABOUT[lang], kb); - } +// Inline-only converter for media CAPTIONS (block elements aren't supported on captions). +function mdInlineToHtml(md) { + let s = escapeHtml(String(md)); + s = s.replace(/\*\*(.+?)\*\*/g, "$1"); + s = s.replace(/~~(.+?)~~/g, "$1"); + s = s.replace(/\|\|(.+?)\|\|/g, "$1"); + s = s.replace(/(^|[^*])\*([^*\n]+?)\*(?!\*)/g, "$1$2"); + s = s.replace(/`([^`\n]+?)`/g, "$1"); + s = s.replace(/\[([^\]\n]+?)\]\((https?:\/\/[^)\n]+)\)/g, '$1'); + return s; } -// ─── Language select (plain sendMessage so it's always fresh) ───────────────── -const LANG_SELECT_MESSAGE = "🌐 Please choose your language / زبان خود را انتخاب کنید:"; -const LANG_SELECT_KEYBOARD = { - inline_keyboard: [[ - { text: "🇮🇷 فارسی", callback_data: "fa_start" }, - { text: "🇬🇧 English", callback_data: "en_start" }, - ]], -}; +// ─── Convert Telegram message entities back into Markdown source ────────────── +function entitiesToMarkdown(text, entities) { + if (!entities || !entities.length) return text; + const items = entities.map((e, idx) => ({ e, idx, start: e.offset, end: e.offset + e.length })); + function isTopLevel(item, pool) { + return !pool.some(other => { + if (other.idx === item.idx) return false; + const strictlyLarger = other.start <= item.start && other.end >= item.end && + (other.start < item.start || other.end > item.end); + const sameSpanOuter = other.start === item.start && other.end === item.end && other.idx < item.idx; + return strictlyLarger || sameSpanOuter; + }); + } + function render(start, end, pool) { + const inRange = pool.filter(it => it.start >= start && it.end <= end); + const top = inRange.filter(it => isTopLevel(it, inRange)).sort((a, b) => a.start - b.start); + let out = ""; + let pos = start; + for (const item of top) { + out += text.slice(pos, item.start); + const innerPool = pool.filter(p => p.idx !== item.idx); + const inner = render(item.start, item.end, innerPool); + out += wrapEntity(item.e, inner); + pos = item.end; + } + out += text.slice(pos, end); + return out; + } + return render(0, text.length, items); +} +function wrapEntity(e, content) { + switch (e.type) { + case "bold": return `**${content}**`; + case "italic": return `*${content}*`; + case "underline": return `${content}`; + case "strikethrough": return `~~${content}~~`; + case "spoiler": return `||${content}||`; + case "code": return `\`${content}\``; + case "pre": return "```" + (e.language || "") + "\n" + content + "\n```"; + case "text_link": return `[${content}](${e.url})`; + case "text_mention": return e.user ? `[${content}](tg://user?id=${e.user.id})` : content; + case "blockquote": + case "expandable_blockquote": + return content.split("\n").map(l => `>${l}`).join("\n"); + default: return content; + } +} // ─── API helpers ────────────────────────────────────────────────────────────── async function sendPlain(chatId, text, replyMarkup) { - const body = { chat_id: chatId, text }; - if (replyMarkup) body.reply_markup = replyMarkup; - await callApi("sendMessage", body); + const body = { chat_id: chatId, text, parse_mode: "Markdown" }; + if (replyMarkup) body.reply_markup = replyMarkup; + await callApi("sendMessage", body); } - async function sendRichMarkdown(chatId, markdown, replyMarkup) { - const body = { chat_id: chatId, rich_message: { markdown } }; - if (replyMarkup) body.reply_markup = replyMarkup; - await callApi("sendRichMessage", body); + const body = { chat_id: chatId, rich_message: { markdown } }; + if (replyMarkup) body.reply_markup = replyMarkup; + const res = await callApi("sendRichMessage", body); + // FIX: if Rich sending fails for any reason, fall back to a plain message so the user always gets it. + if (!res?.ok) { + const fb = { chat_id: chatId, text: markdown }; + if (replyMarkup) fb.reply_markup = replyMarkup; + await callApi("sendMessage", fb); + } + return res; } - async function sendRichHtml(chatId, html, replyMarkup) { - const body = { chat_id: chatId, rich_message: { html } }; - if (replyMarkup) body.reply_markup = replyMarkup; - await callApi("sendRichMessage", body); + const body = { chat_id: chatId, rich_message: { html } }; + if (replyMarkup) body.reply_markup = replyMarkup; + const res = await callApi("sendRichMessage", body); + if (!res?.ok) { + const fb = { chat_id: chatId, text: html }; + if (replyMarkup) fb.reply_markup = replyMarkup; + await callApi("sendMessage", fb); + } + return res; } - async function editRichMarkdown(chatId, messageId, markdown, replyMarkup) { - const body = { chat_id: chatId, message_id: messageId, rich_message: { markdown } }; - if (replyMarkup) body.reply_markup = replyMarkup; - await callApi("editMessageText", body); + const body = { chat_id: chatId, message_id: messageId, rich_message: { markdown } }; + if (replyMarkup) body.reply_markup = replyMarkup; + await callApi("editMessageText", body); } - async function callApi(method, body) { - const res = await fetch(`${TELEGRAM_API}/${method}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - - if (!res.ok) { - const err = await res.text(); - console.error(`[${method}] ${res.status}: ${err}`); - await fetch(`${TELEGRAM_API}/sendMessage`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ chat_id: body.chat_id, text: `⚠️ Error (${res.status}): ${err}` }), - }); - } + const res = await fetch(`${TELEGRAM_API}/${method}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + let json = null; + try { json = await res.json(); } catch { json = null; } + if (!res.ok) { + console.error(`[${method}] failed`, res.status, json || await res.text().catch(() => "")); + } + return json; } // ═══════════════════════════════════════════════════════════════════════════════ // CONTENT // ═══════════════════════════════════════════════════════════════════════════════ - const WELCOME = { - fa: `# 🤖 Rich Markdown Bot - -هر متن **Markdown** یا **HTML** بفرستید، به صورت Rich Message رندر میشه. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - -از دکمه‌های زیر برای دیدن راهنما و دمو استفاده کنید 👇`, - - en: `# 🤖 Rich Markdown Bot - -Send any **Markdown** or **HTML** text and it will be echoed back as a rendered Rich Message. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - -Use the buttons below to explore 👇`, + fa: [ + "# 🤖 Rich Markdown Bot", + "", + "هر متن **Markdown** یا **HTML** بفرستید، به صورت Rich Message رندر میشه.", + "همچنین می‌توانید بات را به کانال خود اضافه کنید تا پست‌ها به صورت خودکار قالب‌بندی شوند.", + "", + "از دکمه‌های زیر برای دیدن راهنما و دمو استفاده کنید 👇", + ].join("\n"), + en: [ + "# 🤖 Rich Markdown Bot", + "", + "Send any **Markdown** or **HTML** text and it will be echoed back as a rendered Rich Message.", + "You can also add this bot to your channel to automatically format posts.", + "", + "Use the buttons below to explore 👇", + ].join("\n"), }; - -// ─── About ──────────────────────────────────────────────────────────────────── const ABOUT = { - fa: `# ÐΛɌ₭ᑎΞ𐒡𐒡|𓄂𓆃 - -[ÐΛɌ₭ᑎΞ𐒡𐒡](https://darkness-web.pages.dev/) - ---- - -🗽 [GitHub](https://github.com/DarknessShade) - -🗽 [Paradise Of Freedom](https://t.me/Paradise_Of_Freedom) - -🗽 [ConfigWireguard](https://t.me/ConfigWireguard)`, - - en: `# ÐΛɌ₭ᑎΞ𐒡𐒡|𓄂𓆃 - -[ÐΛɌ₭ᑎΞ𐒡𐒡](https://darkness-web.pages.dev/) - ---- - -🗽 [GitHub](https://github.com/DarknessShade) - -🗽 [Paradise Of Freedom](https://t.me/Paradise_Of_Freedom) - -🗽 [ConfigWireguard](https://t.me/ConfigWireguard)`, + fa: [ + "# ℹ️ درباره بات", + "", + "این بات برای رندر کردن پیام‌های Markdown و HTML شما طراحی شده است. از ساختار مدرن Rich Message تلگرام برای نمایش عناصر پیشرفته استفاده می‌کند. می‌توانید آن را به کانال‌های خود اضافه کنید تا پست‌ها را به صورت خودکار، در لحظه انتشار زیباتر کند.", + ].join("\n"), + en: [ + "# ℹ️ About", + "", + "This bot is designed to render your Markdown and HTML messages natively using Telegram's Rich Message format. You can also add it to your channels to automatically format your posts in real-time as you publish them.", + ].join("\n"), +}; +const HELP_CHANNEL = { + fa: [ + "### 🛠 آموزش اتصال کانال:", + "1. ربات را به کانال خود اضافه کنید.", + "2. دسترسی **Edit Messages** (ویرایش پیام‌ها) را به آن بدهید.", + "3. یک پیام از کانال خود به همینجا (داخل بات) **Forward** کنید.", + "", + "پس از تایید، هر زمان که در کانال پست بگذارید (دارای تگ‌های مارک‌داون یا HTML)، بات در لحظه آن را به نسخه رندر شده تغییر می‌دهد!", + "", + "*(پست‌هایی که هیچ تگی ندارند نادیده گرفته می‌شوند تا برچسب \"Edited\" نخورند)* ✨", + "", + "", + "### 🏷 تگ‌های مخصوص کانال", + "بعضی استایل‌ها فقط روی پیام متنی کانال رندر می‌شوند و روی کپشن مدیا (عکس/ویدیو) کار نمی‌کنند. این محدودیتِ خودِ تلگرام است، نه باگ بات.", + "", + "**✅ روی پیام متنی کانال کار می‌کند:**", + "- هدینگ‌ها و برجسته‌سازی متن (# عنوان)", + "- لیست کارها (- [ ] و - [x])", + "- جدول‌ها", + "- بلوک کد و فرمول ریاضی داخل تگ‌های (///....///) یا ($$...$$)", + "- نقل‌قول (> ...)", + "- همه‌ی استایل‌های inline (بولد، ایتالیک، خط‌خورده، کد، لینک، اسپویلر)", + "", + "**⚠️ روی کپشن مدیا کار نمی‌کند:**", + "فقط استایل‌های inline رندر می‌شوند:", + "بولد · ایتالیک · خط‌خورده · کد · لینک · ||اسپویلر||", + "هدینگ، لیست کارها، جدول، بلوک کد و فرمول روی کپشن نمایش داده نمی‌شوند.", + "", + "💡 اگر می‌خواهی todo / جدول / هدینگ داشته باشی، پست را به‌صورت متنی (بدون عکس) بفرست.", + ].join("\n"), + + en: [ + "### 🛠 How to setup:", + "1. Add the bot to your channel as an administrator.", + "2. Grant it the **Edit Messages** permission.", + "3. **Forward** any message from your channel to this bot.", + "", + "Once authorized, whenever you post text or media captions containing Markdown/HTML tags, the bot will instantly render it!", + "", + "*(Posts without any markdown tags are safely ignored to avoid the \"Edited\" label)* ✨", + ].join("\n"), +}; +const HELP_CHANNEL_TAGS = { + fa: [ + "# 🏷 تگ‌های مخصوص کانال", + "", + "بعضی استایل‌ها فقط روی **پیام متنی** کانال رندر می‌شوند و روی **کپشن مدیا** (عکس/ویدیو) کار نمی‌کنند. این محدودیتِ خودِ تلگرام است، نه باگ بات.", + "", + "---", + "## ✅ روی پیام متنی کانال کار می‌کند", + "- هدینگ‌ها و برجسته‌سازی متن (`# عنوان`)", + "- لیست کارها (`- [ ]` و `- [x]`)", + "- جدول‌ها", + "- بلوک کد و فرمول ریاضی داخل تگ‌های (`///...///`) یا (`$$...$$`)", + "- نقل‌قول (`> ...`)", + "- همه‌ی استایل‌های inline (بولد، ایتالیک، خط‌خورده، کد، لینک، اسپویلر)", + "", + "---", + "## ⚠️ روی کپشن مدیا کار نمی‌کند", + "فقط استایل‌های inline رندر می‌شوند:", + "**بولد** · *ایتالیک* · ~~خط‌خورده~~ · `کد` · [لینک](http://url) · ||اسپویلر||", + "", + "هدینگ، لیست کارها، جدول، بلوک کد و فرمول روی کپشن نمایش داده **نمی‌شوند**.", + "", + "> 💡 اگر می‌خواهی todo / جدول / هدینگ داشته باشی، پست را به‌صورت **متنی** (بدون عکس) بفرست.", + ].join("\n"), + en: [ + "# 🏷 Channel-only Tags", + "", + "Some styles only render on a **text post** in a channel and do NOT work on a **media caption** (photo/video). This is a Telegram limitation, not a bot bug.", + "", + "---", + "## ✅ Works on channel text posts", + "- Headings (`# Title`)", + "- Task lists (`- [ ]` and `- [x]`)", + "- Tables", + "- Code blocks & math inside (`///...///`) or (`$$...$$`)", + "- Quotes (`> ...`)", + "- All inline styles (bold, italic, strike, code, links, spoiler)", + "", + "---", + "## ⚠️ Does NOT work on media captions", + "Only inline styles render:", + "**bold** · *italic* · ~~strike~~ · `code` · [link](http://url) · ||spoiler||", + "", + "Headings, task lists, tables, code blocks and math are **not** shown on captions.", + "", + "> 💡 If you need todos / tables / headings, send the post as **text** (no photo).", + ].join("\n"), }; - -// ─── Markdown Help ──────────────────────────────────────────────────────────── const HELP_MD = { - fa: `# 📖 راهنمای Markdown - -متن Markdown بفرستید، رندر شده برمیگرده. -کادر خاکستری = چیزی که تایپ میکنید ↓ نتیجه بعدشه. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## Text Styles - -\`\`\` -**bold** *italic* ~~strike~~ \`code\` ==marked== ||spoiler|| -\`\`\` - -**bold** *italic* ~~strike~~ \`code\` ==marked== ||spoiler|| - ---- - -## Headings - -\`\`\` -# Heading 1 -## Heading 2 -### Heading 3 -#### Heading 4 -##### Heading 5 -###### Heading 6 -\`\`\` - -# Heading 1 -## Heading 2 -### Heading 3 -#### Heading 4 -##### Heading 5 -###### Heading 6 - ---- - -## Lists - -\`\`\` -- milk -- eggs -- [ ] todo -- [x] done - -1. wake up -2. ship it -\`\`\` - -- milk -- eggs -- [ ] todo -- [x] done - -1. wake up -2. ship it - ---- - -## Links & Quotes - -\`\`\` -[Telegram](https://telegram.org) - ->To be, or not to be. -\`\`\` - -[Telegram](https://telegram.org) - ->To be, or not to be. - ---- - -## Block Quote (چند خط) - -\`\`\` ->Block quotation started -> ->Block quotation continued on the next line ->Block quotation continued on the same line - ->The last line of the block quotation -\`\`\` - ->Block quotation started -> ->Block quotation continued on the next line ->Block quotation continued on the same line - ->The last line of the block quotation - ---- - -## Unordered List (علامت‌های مختلف) - -\`\`\` -- unordered list item -* unordered list item -+ unordered list item -\`\`\` - -- unordered list item -* unordered list item -+ unordered list item - ---- - -## Divider - -\`\`\` ---- -\`\`\` - ---- - -## Code Blocks - -\`\`\`\` -\`\`\`python -print("hello") -\`\`\` -\`\`\`\` - -\`\`\`python -print("hello") -\`\`\` - ---- - -## Tables - -\`\`\`\` -| Lang | Speed | -|:-----|------:| -| Rust | fast | -| Py | comfy | -\`\`\`\` - -| Lang | Speed | -|:-----|------:| -| Rust | fast | -| Py | comfy | - ---- - -## Math - -\`\`\`\` -Inline $E = mc^2$ and a block: -$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$ -\`\`\`\` - -Inline $E = mc^2$ and a block: - -$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$ - ---- - -## Details - -\`\`\`\` -
**کلیک کن** -محتوای مخفی! -
-\`\`\`\` - -
**کلیک کن** -محتوای مخفی! -
- ---- - -*محدودیت: تا 32,768 کاراکتر در هر پیام* ✨`, - - en: `# 📖 Markdown Guide - -Send Markdown text and get it echoed back rendered. -Grey box = what you type ↓ result comes right after. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) ---- - -## Text Styles - -\`\`\` -**bold** *italic* ~~strike~~ \`code\` ==marked== ||spoiler|| -\`\`\` - -**bold** *italic* ~~strike~~ \`code\` ==marked== ||spoiler|| - ---- - -## Headings - -\`\`\` -# Heading 1 -## Heading 2 -### Heading 3 -#### Heading 4 -##### Heading 5 -###### Heading 6 -\`\`\` - -# Heading 1 -## Heading 2 -### Heading 3 -#### Heading 4 -##### Heading 5 -###### Heading 6 - ---- - -## Lists - -\`\`\` -- milk -- eggs -- [ ] todo -- [x] done - -1. wake up -2. ship it -\`\`\` - -- milk -- eggs -- [ ] todo -- [x] done - -1. wake up -2. ship it - ---- - -## Unordered List (all markers) - -\`\`\` -- unordered list item -* unordered list item -+ unordered list item -\`\`\` - -- unordered list item -* unordered list item -+ unordered list item - ---- - -## Links & Quotes - -\`\`\` -[Telegram](https://telegram.org) - ->To be, or not to be. -\`\`\` - -[Telegram](https://telegram.org) - ->To be, or not to be. - ---- - -## Block Quote (multi-line) - -\`\`\` ->Block quotation started -> ->Block quotation continued on the next line ->Block quotation continued on the same line - ->The last line of the block quotation -\`\`\` - ->Block quotation started -> ->Block quotation continued on the next line ->Block quotation continued on the same line - ->The last line of the block quotation - ---- - -## Divider - -\`\`\` ---- -\`\`\` - ---- - -## Code Blocks - -\`\`\`\` -\`\`\`python -print("hello") -\`\`\` -\`\`\`\` - -\`\`\`python -print("hello") -\`\`\` - ---- - -## Tables - -\`\`\` -| Lang | Speed | -|:-----|------:| -| Rust | fast | -| Py | comfy | -\`\`\` - -| Lang | Speed | -|:-----|------:| -| Rust | fast | -| Py | comfy | - ---- - -## Math - -\`\`\` -Inline $E = mc^2$ and a block: -$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$ -\`\`\` - -Inline $E = mc^2$ and a block: - -$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$ - ---- - -## Details (Collapsible) - -\`\`\` -
**Click me** -Hidden content! -
-\`\`\` - -
**Click me** -Hidden content! -
- ---- - -*Limit: up to 32,768 characters per message* ✨`, + fa: [ + "# 📖 راهنمای Markdown", + "", + "متن Markdown بفرستید، رندر شده برمیگرده. کادر خاکستری = چیزی که تایپ میکنید ↓ نتیجه بعدشه.", + "", + "---", + "## Text Styles", + "```", + "**bold** *italic* ~~strike~~", + "`code` ==marked== ||spoiler||", + "```", + "**bold** *italic* ~~strike~~ `code` ==marked== ||spoiler||", + "", + "---", + "## Headings", + "```", + "# Heading 1", + "## Heading 2", + "### Heading 3", + "```", + "# Heading 1", + "## Heading 2", + "### Heading 3", + "", + "---", + "## Lists", + "```", + "- milk", + "- eggs", + "- [ ] todo", + "- [x] done", + "", + "1. wake up", + "2. ship it", + "```", + "- milk", + "- eggs", + "- [ ] todo", + "- [x] done", + "", + "1. wake up", + "2. ship it", + "", + "---", + "## Code Blocks", + "```", + " \\`\\`\\`python", + " print(\"hello\")", + " \\`\\`\\`", + "```", + "```python", + "print(\"hello\")", + "```", + ].join("\n"), + en: [ + "# 📖 Markdown Guide", + "", + "Send Markdown text and get it echoed back rendered. Grey box = what you type ↓ result comes right after.", + "", + "---", + "## Text Styles", + "```", + "**bold** *italic* ~~strike~~", + "`code` ==marked== ||spoiler||", + "```", + "**bold** *italic* ~~strike~~ `code` ==marked== ||spoiler||", + "", + "---", + "## Headings", + "```", + "# Heading 1", + "## Heading 2", + "### Heading 3", + "```", + "# Heading 1", + "## Heading 2", + "### Heading 3", + "", + "---", + "## Lists", + "```", + "- milk", + "- eggs", + "- [ ] todo", + "- [x] done", + "```", + "- milk", + "- eggs", + "- [ ] todo", + "- [x] done", + "", + "---", + "## Code Blocks", + "```", + " \\`\\`\\`python", + " print(\"hello\")", + " \\`\\`\\`", + "```", + "```python", + "print(\"hello\")", + "```", + ].join("\n"), }; - -// ─── HTML Help ──────────────────────────────────────────────────────────────── const HELP_HTML = { - fa: `# 🌐 راهنمای HTML - -اگه پیامت با \`<\` شروع بشه، بات به عنوان HTML رندر میکنه. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## Text Styles - -\`\`\` -bold italic underline -strike code marked -spoiler -superscript subscript -\`\`\` - -bold italic underline strike code marked spoiler sup sub - ---- - -## Headings - -\`\`\` -

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

-
Heading 3
-\`\`\` - -

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

-
Heading 3
- ---- - -## Lists - -\`\`\` - -
  1. wake up
  2. ship it
- -\`\`\` - - -
  1. wake up
  2. ship it
- - ---- - -## Links & Quotes - -\`\`\` -Telegram -
متن نقل‌قولنویسنده
- -\`\`\` - -Telegram -
متن نقل‌قولنویسنده
- - ---- - -## Superscript & Subscript - -\`\`\` -subscript text -superscript text -\`\`\` - -متن نرمال با subscript text و superscript text - ---- - -## Footnotes - -\`\`\` -Text with a reference[^id1] and another one[^id2]. - -[^id1]: Definition of the first footnote. -[^id2]: Definition of the second footnote. -\`\`\` - -Text with a reference[^id1] and another one[^id2]. - -[^id1]: Definition of the first footnote. -[^id2]: Definition of the second footnote. - ---- - -## Code - -\`\`\` -
print("hello")
-\`\`\` - -
print("hello")
- ---- - -## Table - -\`\`\` - - - - -
LangSpeed
Rustfast
Pycomfy
-\`\`\` - -
LangSpeed
Rustfast
Pycomfy
- ---- - -## Math - -\`\`\` -x^2 + y^2 -E = mc^2 -\`\`\` - -x^2 + y^2 - -E = mc^2 - ---- - -## Details - -\`\`\` -
عنوانمحتوا
-\`\`\` - -
عنوانمحتوا
- ---- - -*یه HTML بفرست و ببین چطور رندر میشه* ✨`, - - en: `# 🌐 HTML Guide - -If your message starts with \`<\`, the bot renders it as HTML. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## Text Styles - -\`\`\` -bold italic underline -strike code marked -spoiler -superscript subscript -\`\`\` - -bold italic underline strike code marked spoiler sup sub - ---- - -## Headings - -\`\`\` -

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

-
Heading 3
-\`\`\` - -

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

-
Heading 3
- ---- - -## Lists - -\`\`\` - -
  1. wake up
  2. ship it
- -\`\`\` - - -
  1. wake up
  2. ship it
- - ---- - -## Links & Quotes - -\`\`\` -Telegram -
Quote textAuthor
- -\`\`\` - -Telegram -
Quote textAuthor
- - ---- - -## Superscript & Subscript - -\`\`\` -subscript text -superscript text -\`\`\` - -Normal text with subscript text and superscript text - ---- - -## Footnotes - -\`\`\` -Text with a reference[^id1] and another one[^id2]. - -[^id1]: Definition of the first footnote. -[^id2]: Definition of the second footnote. -\`\`\` - -Text with a reference[^id1] and another one[^id2]. - -[^id1]: Definition of the first footnote. -[^id2]: Definition of the second footnote. - ---- - -## Code - -\`\`\` -
print("hello")
-\`\`\` - -
print("hello")
- ---- - -## Table - -\`\`\` - - - - -
LangSpeed
Rustfast
Pycomfy
-\`\`\` - -
LangSpeed
Rustfast
Pycomfy
- ---- - -## Math - -\`\`\` -x^2 + y^2 -E = mc^2 -\`\`\` - -x^2 + y^2 - -E = mc^2 - ---- - -## Details (Collapsible) - -\`\`\` -
TitleContent here
-\`\`\` - -
TitleContent here
- ---- - -*Send some HTML and watch it render* ✨`, + fa: [ + "# 🌐 راهنمای HTML", + "", + "اگه پیامت با `<` شروع بشه، بات به عنوان HTML رندر میکنه.", + "", + "---", + "## Text Styles", + "```", + "bold italic underline", + "strike code marked", + "spoiler", + "```", + "bold italic underline strike code marked spoiler", + "", + "---", + "## Lists", + "```", + "", + "
  1. wake up
", + "```", + "
  1. wake up
", + ].join("\n"), + en: [ + "# 🌐 HTML Guide", + "", + "If your message starts with `<`, the bot renders it as HTML.", + "", + "---", + "## Text Styles", + "```", + "bold italic underline", + "strike code marked", + "spoiler", + "```", + "bold italic underline strike code marked spoiler", + "", + "---", + "## Lists", + "```", + "", + "
  1. wake up
", + "```", + "
  1. wake up
", + ].join("\n"), }; - -// ─── Media Help ─────────────────────────────────────────────────────────────── const HELP_MEDIA = { - fa: `# 🖼 راهنمای مدیا - -برای ارسال مدیا در Rich Message از سینتکس تصویر Markdown استفاده کنید. -URL پسوند فایل تعیین می‌کنه چه نوع مدیایی نمایش داده بشه. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## نقشه - -\`\`\` - -\`\`\` - - - ---- - -## عکس - -\`\`\` -![](https://telegram.org/example/photo.jpg) -\`\`\` - -![](https://telegram.org/example/photo.jpg) - ---- - -## ویدیو - -\`\`\` -![](https://telegram.org/example/video.mp4) -\`\`\` - -![](https://telegram.org/example/video.mp4) - ---- - -## فایل صوتی - -\`\`\` -![](https://telegram.org/example/audio.mp3) -\`\`\` - -![](https://telegram.org/example/audio.mp3) - ---- - -## ویس نوت (ogg) - -\`\`\` -![](https://telegram.org/example/audio.ogg) -\`\`\` - -![](https://telegram.org/example/audio.ogg) - ---- - -## انیمیشن (gif) - -\`\`\` -![](https://telegram.org/example/animation.gif) -\`\`\` - -![](https://telegram.org/example/animation.gif) - ---- - -## مدیا با کپشن - -\`\`\` -![](https://telegram.org/example/photo.jpg "Photo caption") -![](https://telegram.org/example/video.mp4 "Video caption") -![](https://telegram.org/example/audio.mp3 "Audio caption") -![](https://telegram.org/example/audio.ogg "Voice note caption") -![](https://telegram.org/example/animation.gif "Animation caption") -\`\`\` - -![](https://telegram.org/example/photo.jpg "Photo caption") -![](https://telegram.org/example/video.mp4 "Video caption") -![](https://telegram.org/example/audio.mp3 "Audio caption") -![](https://telegram.org/example/audio.ogg "Voice note caption") -![](https://telegram.org/example/animation.gif "Animation caption") - ---- - -## اسلایدشو (ترکیبی) - -\`\`\` - - - - -\`\`\` - - - - - - ---- - -*پسوند URL = نوع مدیا: jpg/png=عکس · mp4=ویدیو · mp3=صوت · ogg=ویس · gif=انیمیشن* ✨`, - - en: `# 🖼 Media Guide - -Use Markdown image syntax to embed media in Rich Messages. -The URL file extension determines the media type rendered. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## Map - -\`\`\` - -\`\`\` - - - ---- - -## Photo - -\`\`\` -![](https://telegram.org/example/photo.jpg) -\`\`\` - -![](https://telegram.org/example/photo.jpg) - ---- - -## Video - -\`\`\` -![](https://telegram.org/example/video.mp4) -\`\`\` - -![](https://telegram.org/example/video.mp4) - ---- - -## Audio - -\`\`\` -![](https://telegram.org/example/audio.mp3) -\`\`\` - -![](https://telegram.org/example/audio.mp3) - ---- - -## Voice Note (ogg) - -\`\`\` -![](https://telegram.org/example/audio.ogg) -\`\`\` - -![](https://telegram.org/example/audio.ogg) - ---- - -## Animation (gif) - -\`\`\` -![](https://telegram.org/example/animation.gif) -\`\`\` - -![](https://telegram.org/example/animation.gif) - ---- - -## Media with Captions - -\`\`\` -![](https://telegram.org/example/photo.jpg "Photo caption") -![](https://telegram.org/example/video.mp4 "Video caption") -![](https://telegram.org/example/audio.mp3 "Audio caption") -![](https://telegram.org/example/audio.ogg "Voice note caption") -![](https://telegram.org/example/animation.gif "Animation caption") -\`\`\` - -![](https://telegram.org/example/photo.jpg "Photo caption") -![](https://telegram.org/example/video.mp4 "Video caption") -![](https://telegram.org/example/audio.mp3 "Audio caption") -![](https://telegram.org/example/audio.ogg "Voice note caption") -![](https://telegram.org/example/animation.gif "Animation caption") - ---- - -## Slideshow (Combined) - -\`\`\` - - - - -\`\`\` - - - - - - ---- - -*URL extension = media type: jpg/png=photo · mp4=video · mp3=audio · ogg=voice · gif=animation* ✨`, + fa: [ + "# 🖼 راهنمای مدیا", + "", + "برای ارسال مدیا در Rich Message از سینتکس تصویر Markdown استفاده کنید.", + "", + "---", + "## عکس / ویدیو", + "```", + "![]([https://telegram.org/example/photo.jpg](https://telegram.org/example/photo.jpg))", + "![]([https://telegram.org/example/video.mp4](https://telegram.org/example/video.mp4))", + "```", + "", + "---", + "## اسلایدشو", + "```", + "", + " ", + " ", + "```", + ].join("\n"), + en: [ + "# 🖼 Media Guide", + "", + "Use Markdown image syntax to embed media in Rich Messages.", + "", + "---", + "## Photo / Video", + "```", + "![]([https://telegram.org/example/photo.jpg](https://telegram.org/example/photo.jpg))", + "![]([https://telegram.org/example/video.mp4](https://telegram.org/example/video.mp4))", + "```", + "", + "---", + "## Slideshow", + "```", + "", + " ", + " ", + "```", + ].join("\n"), }; - -// ─── Demo ───────────────────────────────────────────────────────────────────── const DEMO = { - fa: `# 🎨 دمو کامل — نمونه خروجی - -این پیام نمونه خروجی واقعی همه قابلیت‌هاست. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## Text Styles - -**bold** *italic* ~~strike~~ \`code\` ==marked== ||spoiler|| -underline super sub - ---- - -## Nested Formatting - -**Bold _italic underlined italic bold italic_ bold** - ->نقل‌قول با **bold**، ~~strikethrough~~، و ||spoiler||، و [لینک](https://t.me/). - ---- - -## Lists - -- آیتم با \`inline code\` و **bold** -- آیتم با ~~strikethrough~~ و ==highlight== -- [ ] کار انجام نشده -- [x] کار انجام شده - -1. اول -2. دوم -3. سوم - ---- - -## Code Block - -\`\`\`python -def greet(name: str) -> str: - return f"سلام، {name}!" - -print(greet("تلگرام")) -\`\`\` - ---- - -## Table - -| متریک | مقدار | وضعیت | -|:--------|:---------:|---------:| -| سرعت | **42** ms | ==fast== | -| حافظه | 128 MB | ==ok== | -| آپتایم | 99.9% | ~~down~~ | - ---- - -## Math - -Inline: $E = mc^2$ و $x^2 + y^2 = r^2$ - -$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$ - ---- - -## Details - -
**جزئیات بیشتر — کلیک کن** - -### داخل Details - -- **Markdown** داخل details کار میکنه -- جدول، کد، لیست همه سازگارن - -| Key | Value | -|:----|------:| -| A | 1 | -| B | 2 | - -\`\`\`js -console.log("inside details!"); -\`\`\` - -
- ---- - -## Media — اسلایدشو ترکیبی - - - - - - ---- - -🗽 [GitHub](https://github.com/DarknessShade) •|• 🗽 [Paradise Of Freedom](https://t.me/Paradise_Of_Freedom) •|• 🗽 [ConfigWireguard](https://t.me/ConfigWireguard)`, - - en: `# 🎨 Full Demo — Live Output Sample - -This message demonstrates every supported feature rendered live. - -[Rich Markdown Telegram](https://core.telegram.org/bots/api#rich-message-formatting-options) - ---- - -## Text Styles - -**bold** *italic* ~~strike~~ \`code\` ==marked== ||spoiler|| -underline super sub - ---- - -## Nested Formatting - -**Bold _italic underlined italic bold italic_ bold** - ->Quote with **bold**, ~~strikethrough~~, and ||spoiler||, plus [a link](https://t.me/). - ---- - -## Lists - -- Item with \`inline code\` and **bold** -- Item with ~~strikethrough~~ and ==highlight== -- [ ] Task todo -- [x] Task done - -1. First -2. Second -3. Third - ---- - -## Code Block - -\`\`\`python -def greet(name: str) -> str: - return f"Hello, {name}!" - -print(greet("Telegram")) -\`\`\` - ---- - -## Table - -| Metric | Value | Status | -|:--------|:----------:|---------:| -| Speed | **42** ms | ==fast== | -| Memory | 128 MB | ==ok== | -| Uptime | 99.9% | ~~down~~ | - ---- - -## Math - -Inline: $E = mc^2$ and $x^2 + y^2 = r^2$ - -$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$ - ---- - -## Details (Collapsible) - -
**More details — click me** - -### Inside Details - -- **Markdown** works inside details -- Tables, code, lists all supported - -| Key | Value | -|:----|------:| -| A | 1 | -| B | 2 | - -\`\`\`js -console.log("inside details!"); -\`\`\` - -
- ---- - -## Media — Combined Slideshow - - - - - - ---- - -🗽 [GitHub](https://github.com/DarknessShade) •|• 🗽 [Paradise Of Freedom](https://t.me/Paradise_Of_Freedom) •|• 🗽 [ConfigWireguard](https://t.me/ConfigWireguard)`, + fa: [ + "# 🎨 دمو کامل", + "", + "**Bold** _italic_ ~~strike~~ `code` ==highlight== ||spoiler|| underline", + "", + ">نقل‌قول با **bold**", + "", + "- [ ] Task 1", + "- [x] Task 2", + "", + "```python", + "print(\"Hello Telegram\")", + "```", + "", + "| Lang | Speed |", + "|:-----|------:|", + "| Rust | fast |", + "", + "Inline $E = mc^2$", + "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", + ].join("\n"), + en: [ + "# 🎨 Full Demo", + "", + "**Bold** _italic_ ~~strike~~ `code` ==highlight== ||spoiler|| underline", + "", + ">Quote with **bold**", + "", + "- [ ] Task 1", + "- [x] Task 2", + "", + "```python", + "print(\"Hello Telegram\")", + "```", + "", + "| Lang | Speed |", + "|:-----|------:|", + "| Rust | fast |", + "", + "Inline $E = mc^2$", + "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", + ].join("\n"), }; From 6359b3b3a3b0f0bc0def543bf7d4fdfe0f7f4584 Mon Sep 17 00:00:00 2001 From: "A.M. Hasani" Date: Wed, 17 Jun 2026 10:26:26 +0330 Subject: [PATCH 2/5] Update README.md --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 6356532..87af68e 100644 --- a/README.md +++ b/README.md @@ -157,10 +157,6 @@ https://api.telegram.org/bot/getWebhookInfo Released under the **MIT License** — free to use, modify, and distribute, with attribution appreciated. -Created by **ÐΛɌ₭ᑎΞ𐒡𐒡** — [GitHub](https://github.com/DarknessShade) • [Paradise Of Freedom](https://t.me/Paradise_Of_Freedom) • [ConfigWireguard](https://t.me/ConfigWireguard) - -⭐ Star the project: **[github.com/DarknessShade/TeleRich](https://github.com/DarknessShade/TeleRich)** - --- --- @@ -301,6 +297,3 @@ https://api.telegram.org/bot/getWebhookInfo این پروژه تحت **لایسنس MIT** منتشر شده — استفاده، تغییر و توزیع آن آزاد است، ولی ذکر منبع باعث خوشحالی‌مون می‌شه. 🙏 -**ÐΛɌ₭ᑎΞ𐒡𐒡** — [GitHub](https://github.com/DarknessShade) • [Paradise Of Freedom](https://t.me/Paradise_Of_Freedom) • [ConfigWireguard](https://t.me/ConfigWireguard) - -⭐ اگه پروژه رو دوست داشتید، ستاره بدید: **[github.com/DarknessShade/TeleRich](https://github.com/DarknessShade/TeleRich)** From 965ecb5a54805dfb8b00609c332d1b0c3bba16cf Mon Sep 17 00:00:00 2001 From: "A.M. Hasani" Date: Thu, 18 Jun 2026 07:41:03 +0330 Subject: [PATCH 3/5] =?UTF-8?q?=D9=81=DB=8C=DA=86=D8=B1=D9=87=D8=A7=DB=8C?= =?UTF-8?q?=20=D8=AC=D8=AF=DB=8C=D8=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new tag new setting essy tag bug fix --- worker.js | 935 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 713 insertions(+), 222 deletions(-) diff --git a/worker.js b/worker.js index c9854a6..359ac42 100644 --- a/worker.js +++ b/worker.js @@ -1,20 +1,32 @@ /** * Telegram Rich Markdown & Channel Formatter Bot — Cloudflare Worker * - * Features: - * - Direct Messages: Renders any Markdown/HTML sent to the bot. - * - Channels: Automatically formats posts containing Markdown/HTML tags on publish. - * - Smart Edit Detection: Ignores posts without formatting to avoid unnecessary (edited) marks. - * - Per-channel disconnect & channel-only tag guide. - * - Admin Panel: /stats and /broadcast (Restricted to ADMIN_ID). - * - Persistent KV Cache for users, channels, and stats tracking. + * Core: + * - DMs: render Markdown (with inline HTML) as Rich Messages. + * - Channels: auto-format posts on publish (smart edit detection). + * - Per-channel disconnect, channel-only tag guide. + * - Admin Panel, KV cache. + * - Manual spacing preserved (NBSP), auto-slideshow, pull-quote, map shortcut. + * + * NEW: + * 1) Live preview: every DM render shows "📤 Publish to channel" buttons. + * 2) Time shortcut: [زمان: ...] / [time: ...] -> . + * 3) Auto channel footer (multi-link), set & remove anytime. (FIXED) + * 4) Better /stats dashboard (channels, footers, today, 7-day chart). + * 5) Undo: /undo in PM -> bot asks to revert the channel's last render to raw. + * 6) Tag Render Settings: Per-channel toggle for specific tags via glass buttons. */ -const BOT_TOKEN = "BOT TOKEN ADD KON INJA"; -const ADMIN_ID = 12345677899; #adminuserid +const BOT_TOKEN = "TOKEN BOT IN PLACE"; // ⚠️ revoke the leaked one +const ADMIN_ID = 12345678987; // admin user id const TELEGRAM_API = `https://api.telegram.org/bot${BOT_TOKEN}`; const TTL_PROCESSED_SEC = 30 * 24 * 60 * 60; // 30 days -const TTL_CHANNEL_SEC = 30 * 24 * 60 * 60; // 30 days +const TTL_CHANNEL_SEC = 30 * 24 * 60 * 60; // 30 days +const TTL_RENDER_SEC = 7 * 24 * 60 * 60; // 7 days (undo window) +const TTL_PREVIEW_SEC = 60 * 60; // 1 hour (pending preview) +const TTL_AWAIT_SEC = 10 * 60; // 10 min (footer input) + +const DEFAULT_CONFIG = { quote: true, time: true, map: true, code: true, expand: true, media: true, spoiler: true }; export default { async fetch(request, env) { @@ -103,6 +115,34 @@ async function kvGetUserChannels(env, userId) { const channels = await env.DB.get(key, "json"); return channels || []; } +async function getChannelConfig(env, channelId) { + const conf = await env.DB.get(`config:${channelId}`, "json"); + return { ...DEFAULT_CONFIG, ...(conf || {}) }; +} +async function setChannelConfig(env, channelId, config) { + await env.DB.put(`config:${channelId}`, JSON.stringify(config)); +} + +// Stats / dashboard helpers +function dateKey(offsetDays = 0) { + const ms = Date.now() + 3.5 * 3600 * 1000 - offsetDays * 24 * 3600 * 1000; + const d = new Date(ms); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`; +} +async function incrementDailyPosts(env) { + const key = `stats:posts:${dateKey(0)}`; + let c = parseInt(await env.DB.get(key)) || 0; + await env.DB.put(key, (c + 1).toString(), { expirationTtl: 9 * 24 * 3600 }); +} +async function countPrefix(env, prefix) { + let n = 0, cursor = null; + do { + const list = await env.DB.list({ prefix, cursor }); + n += list.keys.length; + cursor = list.list_complete ? null : list.cursor; + } while (cursor); + return n; +} // ─── Keyboards ──────────────────────────────────────────────────────────────── function mainKeyboard(lang, botUsername) { @@ -144,13 +184,17 @@ function backKeyboard(lang) { ], }; } -// Dynamic keyboard for the channel guide: tag-guide button + a disconnect button per channel. +// Channel guide keyboard function channelGuideKeyboard(lang, userChannels) { const isFa = lang === "fa"; const rows = []; rows.push([ { text: isFa ? "🏷 تگ‌های مخصوص کانال" : "🏷 Channel-only Tags", callback_data: `${lang}_chtags` }, ]); + rows.push([ + { text: isFa ? "⚙️ تنظیمات رندر (تگ‌ها)" : "⚙️ Render Settings", callback_data: `${lang}_rendermenu` }, + { text: isFa ? "✍️ امضای کانال" : "✍️ Channel Footer", callback_data: `${lang}_footermenu` }, + ]); for (const c of userChannels || []) { rows.push([ { @@ -173,6 +217,64 @@ function backToChannelKeyboard(lang) { ]], }; } +// Config menus +function renderMenuKeyboard(lang, userChannels) { + const isFa = lang === "fa"; + const rows = []; + for (const c of userChannels || []) { + rows.push([{ text: `⚙️ ${c.title || c.id}`, callback_data: `${lang}_rcfg_${c.id}` }]); + } + rows.push([{ text: isFa ? "⬅️ بازگشت" : "⬅️ Back", callback_data: `${lang}_help_channel` }]); + return { inline_keyboard: rows }; +} +function renderTogglesKeyboard(lang, channelId, config) { + const isFa = lang === "fa"; + const rows = []; + const mkBtn = (key, labelFa, labelEn) => { + const status = config[key] ? "✅" : "❌"; + return { text: `${status} ${isFa ? labelFa : labelEn}`, callback_data: `${lang}_rtgl_${channelId}_${key}` }; + }; + rows.push([mkBtn("quote", "نقل‌قول (\"\")", "Quote (\"\")"), mkBtn("time", "زمان", "Time")]); + rows.push([mkBtn("map", "نقشه", "Map"), mkBtn("code", "کد (///)", "Code (///)")]); + rows.push([mkBtn("expand", "کشویی (???)", "Expand (???)"), mkBtn("media", "مدیا (اسلایدشو)", "Media")]); + rows.push([mkBtn("spoiler", "اسپویلر (🙈)", "Spoiler (🙈)")]); + rows.push([{ text: isFa ? "⬅️ بازگشت به لیست" : "⬅️ Back to list", callback_data: `${lang}_rendermenu` }]); + return { inline_keyboard: rows }; +} +// Footer set/remove menu. +function footerMenuKeyboard(lang, userChannels) { + const isFa = lang === "fa"; + const rows = []; + for (const c of userChannels || []) { + rows.push([ + { text: (isFa ? "✍️ تنظیم: " : "✍️ Set: ") + (c.title || c.id), callback_data: `${lang}_setfooter_${c.id}` }, + { text: "🗑", callback_data: `${lang}_delfooter_${c.id}` }, + ]); + } + rows.push([ + { text: isFa ? "⬅️ بازگشت به راهنمای کانال" : "⬅️ Back to Channel Guide", callback_data: `${lang}_help_channel` }, + ]); + return { inline_keyboard: rows }; +} +// Live preview +function previewKeyboard(userChannels) { + const rows = []; + for (const c of userChannels || []) { + rows.push([{ text: `📤 ارسال به / Publish: ${c.title || c.id}`, callback_data: `pub_${c.id}` }]); + } + rows.push([{ text: "❌ بستن پیش‌نمایش / Close", callback_data: "preview_close" }]); + return { inline_keyboard: rows }; +} +// Undo confirmation. +function undoConfirmKeyboard(lang, channelId) { + const isFa = lang === "fa"; + return { + inline_keyboard: [[ + { text: isFa ? "✅ بله، خام کن" : "✅ Yes, revert", callback_data: `${lang}_undoyes_${channelId}` }, + { text: isFa ? "❌ خیر" : "❌ No", callback_data: `${lang}_undono` }, + ]], + }; +} const LANG_SELECT_MESSAGE = "🌐 Please choose your language / زبان خود را انتخاب کنید:"; const LANG_SELECT_KEYBOARD = { inline_keyboard: [[ @@ -190,12 +292,48 @@ async function handleMessage(message, env) { const trimmed = rawText.trim(); await registerUser(env, userId); + // --- FOOTER INPUT STATE --- + const awaitFooter = await env.DB.get(`await_footer:${userId}`); + if (awaitFooter) { + if (trimmed.startsWith("/")) { + await env.DB.delete(`await_footer:${userId}`); + } else { + const footerText = renderUserText(rawText, message.entities, DEFAULT_CONFIG) || trimmed; + await env.DB.put(`footer:${awaitFooter}`, footerText); + await env.DB.delete(`await_footer:${userId}`); + await sendPlain(chatId, "✅ امضای کانال ذخیره شد. از این پس ته هر پستِ همان کانال اضافه می‌شود.\nبرای حذف، از منوی «✍️ امضای کانال» دکمه‌ی 🗑 را بزن."); + return; + } + } + // --- ADMIN COMMANDS --- if (userId === ADMIN_ID) { if (trimmed === "/stats") { const users = (await env.DB.get("stats:users")) || "0"; const processed = (await env.DB.get("stats:processed")) || "0"; - await sendPlain(chatId, `📊 **Bot Statistics:**\n\n👥 Total Users: ${users}\n🔄 Rendered Posts: ${processed}`); + const channels = await countPrefix(env, "channel:"); + const footers = await countPrefix(env, "footer:"); + const today = parseInt(await env.DB.get(`stats:posts:${dateKey(0)}`)) || 0; + let chart = ""; + for (let i = 6; i >= 0; i--) { + const k = dateKey(i); + const c = parseInt(await env.DB.get(`stats:posts:${k}`)) || 0; + const bar = c > 0 ? "▇".repeat(Math.min(c, 20)) : "·"; + chart += `${k.slice(5)} ${bar} ${c}\n`; + } + const msg = +`📊 *داشبورد بات* + +👥 کاربران: ${users} +🔄 کل پست‌های رندرشده: ${processed} +📢 کانال‌های فعال: ${channels} +✍️ امضاهای تنظیم‌شده: ${footers} +🗓 پست‌های امروز: ${today} + +📈 ۷ روز اخیر: +\`\`\` +${chart}\`\`\``; + await sendPlain(chatId, msg); return; } if (trimmed.startsWith("/broadcast ")) { @@ -217,6 +355,37 @@ async function handleMessage(message, env) { } } + // --- UNDO --- + if (trimmed === "/undo") { + const chs = await kvGetUserChannels(env, userId); + if (!chs.length) { + await sendPlain(chatId, "❌ هنوز کانالی ثبت نکرده‌ای."); + return; + } + const rows = chs.map(c => [{ text: `↩️ ${c.title || c.id}`, callback_data: `fa_undo_${c.id}` }]); + await callApi("sendMessage", { + chat_id: chatId, + text: "↩️ آخرین رندرِ کدام کانال را به متن خام برگردانم؟", + reply_markup: { inline_keyboard: rows }, + }); + return; + } + + // --- FOOTER command --- + if (trimmed === "/footer") { + const chs = await kvGetUserChannels(env, userId); + if (!chs.length) { + await sendPlain(chatId, "❌ اول یک کانال ثبت کن (یک پیام از کانال فوروارد کن)."); + return; + } + await callApi("sendMessage", { + chat_id: chatId, + text: "✍️ امضای کدام کانال را تنظیم/حذف کنم؟", + reply_markup: footerMenuKeyboard("fa", chs), + }); + return; + } + // --- CHANNEL AUTHORIZATION VIA FORWARD --- const fwdChat = message.forward_from_chat || message.forward_origin?.chat; if (fwdChat && fwdChat.type === "channel") { @@ -230,7 +399,7 @@ async function handleMessage(message, env) { await kvAuthorizeChannel(env, channelId); await kvLinkUserToChannel(env, userId, channelId, fwdChat.title || channelId.toString()); - const guideText = `✅ **کانال با موفقیت ثبت شد:** ${fwdChat.title || channelId}\n\nربات از این پس پست‌های دارای مارک‌داون یا HTML را هنگام انتشار، به‌صورت خودکار در این کانال رندر می‌کند.\n\n---\n\n` + HELP_CHANNEL["fa"]; + const guideText = `✅ **کانال با موفقیت ثبت شد:** ${fwdChat.title || channelId}\n\nربات از این پس پست‌های دارای فرمت (مارک‌داون/HTML/فرمول/تگ‌های فارسی) را هنگام انتشار، به‌صورت خودکار در این کانال رندر می‌کند.\n\n---\n\n` + HELP_CHANNEL["fa"]; const userChannels = await kvGetUserChannels(env, userId); await sendRichMarkdown(chatId, guideText, channelGuideKeyboard("fa", userChannels)); } else { @@ -245,15 +414,14 @@ async function handleMessage(message, env) { return; } - let text = entitiesToMarkdown(rawText, message.entities).trim(); + let text = renderUserText(rawText, message.entities, DEFAULT_CONFIG); if (!text) text = trimmed; if (!text) return; - // Convert custom ///.../// to standard markdown code blocks - text = text.replace(/\/\/\/([\s\S]+?)\/\/\//g, "```\n$1\n```"); - - if (text.startsWith("<") || /<\/?\w/.test(text)) { - await sendRichHtml(chatId, text); + const userChannels = await kvGetUserChannels(env, userId); + if (userChannels.length > 0) { + await env.DB.put(`preview:${userId}`, text, { expirationTtl: TTL_PREVIEW_SEC }); + await sendRichMarkdown(chatId, text, previewKeyboard(userChannels)); } else { await sendRichMarkdown(chatId, text); } @@ -262,8 +430,41 @@ async function handleMessage(message, env) { async function handleCallback(cb, env) { const chatId = cb.message.chat.id; const msgId = cb.message.message_id; + const userId = cb.from.id; const data = cb.data; await callApi("answerCallbackQuery", { callback_query_id: cb.id }); + + // --- Non-language-prefixed actions --- + if (data.startsWith("pub_")) { + const channelId = data.slice(4); + const text = await env.DB.get(`preview:${userId}`); + if (!text) { + await sendPlain(chatId, "⛔️ پیش‌نمایش منقضی شده. دوباره متن را بفرست."); + return; + } + const res = await sendRichMarkdown(channelId, text); + if (res?.ok && res.result?.message_id) { + await kvMarkMessageProcessed(env, channelId, res.result.message_id); + await env.DB.put( + `last_render:${channelId}`, + JSON.stringify({ messageId: res.result.message_id, isCaption: false, raw: text }), + { expirationTtl: TTL_RENDER_SEC } + ); + await incrementStat(env, "stats:processed"); + await incrementDailyPosts(env); + await callApi("editMessageReplyMarkup", { chat_id: chatId, message_id: msgId, reply_markup: { inline_keyboard: [] } }); + await sendPlain(chatId, "✅ به کانال ارسال شد."); + } else { + await sendPlain(chatId, "⚠️ ارسال ناموفق بود. مطمئن شو ربات در کانال ادمین با دسترسی ارسال پیام است."); + } + return; + } + if (data === "preview_close") { + await callApi("editMessageReplyMarkup", { chat_id: chatId, message_id: msgId, reply_markup: { inline_keyboard: [] } }); + return; + } + + // --- Language-prefixed actions --- const lang = data.startsWith("fa_") ? "fa" : "en"; const action = data.slice(3); const kb = backKeyboard(lang); @@ -279,7 +480,7 @@ async function handleCallback(cb, env) { } else if (action === "help_media") { await editRichMarkdown(chatId, msgId, HELP_MEDIA[lang], kb); } else if (action === "help_channel") { - const userChannels = await kvGetUserChannels(env, cb.from.id); + const userChannels = await kvGetUserChannels(env, userId); let prefix = ""; if (userChannels.length > 0) { const list = userChannels.map(c => `• ${c.title}`).join("\n"); @@ -293,18 +494,105 @@ async function handleCallback(cb, env) { } await editRichMarkdown(chatId, msgId, prefix + HELP_CHANNEL[lang], channelGuideKeyboard(lang, userChannels)); } else if (action === "chtags") { - // Guide: which tags/styles work in the bot but NOT in the channel. await editRichMarkdown(chatId, msgId, HELP_CHANNEL_TAGS[lang], backToChannelKeyboard(lang)); + } else if (action === "rendermenu") { + const chs = await kvGetUserChannels(env, userId); + await editRichMarkdown( + chatId, msgId, + lang === "fa" ? "⚙️ برای کدام کانال تگ‌ها را تنظیم کنم؟" : "⚙️ Choose a channel to configure tags:", + renderMenuKeyboard(lang, chs) + ); + } else if (action.startsWith("rcfg_")) { + const channelId = action.slice(5); + const conf = await getChannelConfig(env, channelId); + await editRichMarkdown( + chatId, msgId, + lang === "fa" ? "⚙️ فعال/غیرفعال‌سازی تگ‌ها:" : "⚙️ Enable/Disable tags:", + renderTogglesKeyboard(lang, channelId, conf) + ); + } else if (action.startsWith("rtgl_")) { + const actParts = action.match(/^rtgl_(-?\d+)_([a-z]+)$/); + if (actParts) { + const channelId = actParts[1]; + const key = actParts[2]; + const conf = await getChannelConfig(env, channelId); + conf[key] = !conf[key]; + await setChannelConfig(env, channelId, conf); + await editRichMarkdown( + chatId, msgId, + lang === "fa" ? "⚙️ فعال/غیرفعال‌سازی تگ‌ها:" : "⚙️ Enable/Disable tags:", + renderTogglesKeyboard(lang, channelId, conf) + ); + } + } else if (action === "footermenu") { + const chs = await kvGetUserChannels(env, userId); + await editRichMarkdown( + chatId, msgId, + lang === "fa" ? "✍️ امضای کدام کانال را تنظیم/حذف کنم؟" : "✍️ Set/remove footer for which channel?", + footerMenuKeyboard(lang, chs) + ); + } else if (action.startsWith("setfooter_")) { + const channelId = action.slice("setfooter_".length); + await env.DB.put(`await_footer:${userId}`, channelId, { expirationTtl: TTL_AWAIT_SEC }); + await editRichMarkdown( + chatId, msgId, + lang === "fa" + ? "✍️ حالا **متن امضا** را بفرست. می‌تونی چند لینک و استایل بذاری. مثال:\n\n`📌 کانال ما` — [عضویت](https://t.me/yourchannel) | [سایت](https://example.com)" + : "✍️ Now send the **footer text**. You can include multiple links/styles. Example:\n\n`📌 Our channel` — [Join](https://t.me/yourchannel) | [Site](https://example.com)", + null + ); + } else if (action.startsWith("delfooter_")) { + const channelId = action.slice("delfooter_".length); + await env.DB.delete(`footer:${channelId}`); + const chs = await kvGetUserChannels(env, userId); + await editRichMarkdown( + chatId, msgId, + lang === "fa" ? "🗑 امضای این کانال حذف شد." : "🗑 Footer removed for this channel.", + footerMenuKeyboard(lang, chs) + ); } else if (action.startsWith("off_")) { - // Disconnect a channel. const channelId = action.slice(4); await kvDeauthorizeChannel(env, channelId); - await kvUnlinkUserFromChannel(env, cb.from.id, channelId); - const userChannels = await kvGetUserChannels(env, cb.from.id); + await kvUnlinkUserFromChannel(env, userId, channelId); + const userChannels = await kvGetUserChannels(env, userId); const confirm = lang === "fa" ? `🔌 **اتصال قطع شد.** ربات دیگر پست‌های آن کانال را قالب‌بندی نمی‌کند.\nهر زمان خواستی دوباره وصل کنی، کافیست یک پیام از کانال را به اینجا فوروارد کنی.\n\n---\n` : `🔌 **Disconnected.** The bot will no longer format posts in that channel.\nTo reconnect anytime, just forward a message from the channel here.\n\n---\n`; await editRichMarkdown(chatId, msgId, confirm + HELP_CHANNEL[lang], channelGuideKeyboard(lang, userChannels)); + } else if (action.startsWith("undoyes_")) { + const channelId = action.slice("undoyes_".length); + const last = await env.DB.get(`last_render:${channelId}`, "json"); + if (!last) { + await editRichMarkdown(chatId, msgId, "❌ چیزی برای بازگردانی پیدا نشد.", null); + return; + } + let r; + if (last.isCaption) { + r = await callApi("editMessageCaption", { chat_id: channelId, message_id: last.messageId, caption: last.raw }); + } else { + r = await callApi("editMessageText", { chat_id: channelId, message_id: last.messageId, text: last.raw }); + } + if (r?.ok) { + await env.DB.delete(`last_render:${channelId}`); + await env.DB.delete(`processed:${channelId}:${last.messageId}`); + await editRichMarkdown(chatId, msgId, "✅ آخرین رندر به متن خام برگردانده شد.", null); + } else { + await editRichMarkdown(chatId, msgId, "⚠️ بازگردانی ناموفق بود (شاید پیام خیلی قدیمی است یا حذف شده).", null); + } + } else if (action === "undono") { + await editRichMarkdown(chatId, msgId, "باشه، تغییری اعمال نشد.", null); + } else if (action.startsWith("undo_")) { + const channelId = action.slice("undo_".length); + const last = await env.DB.get(`last_render:${channelId}`, "json"); + if (!last) { + await editRichMarkdown(chatId, msgId, "❌ برای این کانال رندری ثبت نشده.", null); + return; + } + await editRichMarkdown( + chatId, msgId, + "⚠️ آخرین رندرِ این کانال را به **متن خام** برگردانم؟", + undoConfirmKeyboard(lang, channelId) + ); } else if (action === "demo") { await editRichMarkdown(chatId, msgId, DEMO[lang], kb); } else if (action === "about") { @@ -325,21 +613,28 @@ async function handleChannelPost(post, env) { const entities = isCaption ? post.caption_entities : post.entities; if (!rawText || !rawText.trim()) return; - // Check the RAW text for markdown tags to prevent the bot from editing normal posts - // or posts where formatting was natively applied by the user (like standard URLs). - if (!hasRealFormatting(rawText)) return; + const footer = await env.DB.get(`footer:${channelId}`); + const config = await getChannelConfig(env, channelId); + + // Skip posts with no real formatting, UNLESS a footer is set (so we still append it). + if (!hasRealFormatting(rawText) && !isAllMediaUrls(rawText) && !footer) return; - let text = entitiesToMarkdown(rawText, entities).trim(); + let text = renderUserText(rawText, entities, config); if (!text) text = rawText.trim(); - // Convert custom ///.../// to standard markdown code blocks - text = text.replace(/\/\/\/([\s\S]+?)\/\/\//g, "```\n$1\n```"); + // Append auto channel footer + if (footer) text = text + "\n\n" + footer; + + await env.DB.put( + `last_render:${channelId}`, + JSON.stringify({ messageId, isCaption, raw: rawText }), + { expirationTtl: TTL_RENDER_SEC } + ); await kvMarkMessageProcessed(env, channelId, messageId); let res; if (isCaption) { - // Captions have NO rich field — only classic inline formatting via parse_mode. res = await callApi("editMessageCaption", { chat_id: channelId, message_id: messageId, @@ -347,50 +642,141 @@ async function handleChannelPost(post, env) { parse_mode: "HTML", }); } else { - // FIX: text posts now use Telegram's NATIVE Rich renderer (same as DMs), - // so to-do lists / tables / headings render exactly like inside the bot. - const isHtml = text.startsWith("<") || /<\/?\w/.test(text); res = await callApi("editMessageText", { chat_id: channelId, message_id: messageId, - rich_message: isHtml ? { html: text } : { markdown: text }, + rich_message: { markdown: text, is_rtl: isRtl(text) }, }); } if (res?.ok) { await kvAuthorizeChannel(env, channelId); await incrementStat(env, "stats:processed"); + await incrementDailyPosts(env); + } +} + +// ─── Render pipeline ────────────────────────────────────────────────────────── +function renderUserText(rawText, entities, config) { + let text = entitiesToMarkdown(rawText, entities); + if (config.media) text = autoGroupMedia(text); + text = applyCustomSyntax(text, config); + text = text.trim(); + text = preserveManualSpacing(text); + return text; +} + +function applyCustomSyntax(text, config) { + if (config.quote) { + text = applyPullQuote(text); + } + if (config.time) { + text = text.replace(/\[(?:زمان|تایمر|time|timer)\s*:\s*([^\]]+)\]/g, (_, v) => { + const t = Date.parse(v.trim()); + return isNaN(t) ? `${v.trim()}` : ``; + }); + } + if (config.map) { + text = text.replace( + /\[(?:نقشه|map)\s*:\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\]/g, + '' + ); + } + if (config.code) { + text = text.replace(/\/\/\/([\s\S]+?)\/\/\//g, "```\n$1\n```"); + } + if (config.expand) { + text = text.replace(/\?\?\?([\s\S]+?)\?\?\?/g, "
\n🔽\n\n$1\n\n
"); + text = text.replace(/\[(?:کشویی|expand)\]([\s\S]+?)\[\/(?:کشویی|expand)\]/g, "
\n🔽\n\n$1\n\n
"); + } + if (config.media) { + text = text.replace(/\[(?:اسلایدشو|slideshow)\]([\s\S]+?)\[\/(?:اسلایدشو|slideshow)\]/g, "\n$1\n"); + text = text.replace(/\[(?:کلاژ|collage)\]([\s\S]+?)\[\/(?:کلاژ|collage)\]/g, "\n$1\n"); + } + if (config.spoiler) { + text = text.replace(/^🙈[ \t]*(.+)$/gm, "$1"); } + return text; +} + +function applyPullQuote(text) { + return text.replace(/"""([\s\S]+?)"""/g, (_, body) => { + const lines = body.trim().split("\n"); + let credit = ""; + if (lines.length > 1 && /^\s*[—\-]\s*/.test(lines[lines.length - 1])) { + credit = lines.pop().replace(/^\s*[—\-]\s*/, "").trim(); + } + const quote = lines.join("\n").trim(); + return credit ? `` : ``; + }); +} + +function autoGroupMedia(text) { + if (!isAllMediaUrls(text)) return text; + const media = text.split("\n").map(l => l.trim()).filter(Boolean).map(u => `![](${u})`).join("\n"); + return `\n${media}\n`; +} + +// ─── Preserve manual spacing ────────────────────────────────────────────────── +function preserveManualSpacing(text) { + const NBSP = "\u00A0"; + const fences = []; + text = text.replace(/```[\s\S]*?```/g, m => { + fences.push(m); + return `\u0000F${fences.length - 1}\u0000`; + }); + const out = text.split("\n").map(line => { + if (/\u0000F\d+\u0000/.test(line)) return line; + if (/^\s*\|.*\|\s*$/.test(line)) return line; + const codes = []; + let l = line.replace(/`[^`\n]*`/g, m => { + codes.push(m); + return `\u0000C${codes.length - 1}\u0000`; + }); + l = l.replace(/ {2,}/g, m => NBSP.repeat(m.length)); + l = l.replace(/\u0000C(\d+)\u0000/g, (_, i) => codes[Number(i)]); + return l; + }).join("\n"); + return out.replace(/\u0000F(\d+)\u0000/g, (_, i) => fences[Number(i)]); } // ─── Format Detectors & Helpers ─────────────────────────────────────────────── function hasRealFormatting(t) { return ( - /\*\*[^\n]+?\*\*/.test(t) || // **bold** - /__[^\n]+?__/.test(t) || // __bold__ - /~~[^\n]+?~~/.test(t) || // ~~strike~~ - /\|\|[^\n]+?\|\|/.test(t) || // ||spoiler|| - /`[^`\n]+?`/.test(t) || // `code` - /```[\s\S]+?```/.test(t) || // code block - /\/\/\/[\s\S]+?\/\/\//.test(t) || // /// code block /// - /(^|\n)\s{0,3}#{1,6}\s+\S/.test(t) || // # heading (needs a space) - /(^|\n)\s*[-*+]\s*\[[ xX]\]\s+\S/.test(t) || // - [ ] / - [x] task - /(^|\n)\s*[-*+]\s+\S/.test(t) || // - bullet - /(^|\n)\s*\d+\.\s+\S/.test(t) || // 1. ordered - /(^|\n)\s*>\s+\S/.test(t) || // > quote - /\[[^\]\n]+\]\([^)\n]+\)/.test(t) || // [link](url) - /(^|\n)\s*\|.+\|\s*(\n|$)/.test(t) || // | table | - /\$\$[\s\S]+?\$\$/.test(t) || // $$ block math $$ - /\$[^$\n]+\$/.test(t) || // $ inline math $ - /<\/?\w+[^>]*>/.test(t) // any real HTML tag + /\*\*[^\n]+?\*\*/.test(t) || + /__[^\n]+?__/.test(t) || + /~~[^\n]+?~~/.test(t) || + /`[^`\n]+?`/.test(t) || + /```[\s\S]+?```/.test(t) || + /\/\/\/[\s\S]+?\/\/\//.test(t) || + /\?\?\?[\s\S]+?\?\?\?/.test(t) || + /"""[\s\S]+?"""/.test(t) || + /\[(?:زمان|تایمر|time|timer)\s*:/.test(t) || + /\[(?:نقشه|map)\s*:/.test(t) || + /\[(?:اسلایدشو|slideshow|کلاژ|collage|کشویی|expand)\]/.test(t) || + /[🙈🔽]/.test(t) || + /(^|\n)\s{0,3}#{1,6}\s+\S/.test(t) || + /(^|\n)\s*[-*+]\s*\[[ xX]\]\s+\S/.test(t) || + /(^|\n)\s*[-*+]\s+\S/.test(t) || + /(^|\n)\s*\d+\.\s+\S/.test(t) || + /(^|\n)\s*>\s+\S/.test(t) || + /\[[^\]\n]+\]\([^)\n]+\)/.test(t) || + /(^|\n)\s*\|.+\|\s*(\n|$)/.test(t) || + /\$\$[\s\S]+?\$\$/.test(t) || + /\$[^$\n]+\$/.test(t) || + /<\/?\w+[^>]*>/.test(t) ); } - +function isAllMediaUrls(t) { + const lines = (t || "").split("\n").map(l => l.trim()).filter(Boolean); + return lines.length >= 2 && lines.every(l => /^https?:\/\/\S+$/.test(l)); +} +function isRtl(s) { + return /[\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF]/.test(s || ""); +} function escapeHtml(s) { return String(s).replace(/&/g, "&").replace(//g, ">"); } - -// Inline-only converter for media CAPTIONS (block elements aren't supported on captions). function mdInlineToHtml(md) { let s = escapeHtml(String(md)); s = s.replace(/\*\*(.+?)\*\*/g, "$1"); @@ -438,14 +824,14 @@ function wrapEntity(e, content) { case "italic": return `*${content}*`; case "underline": return `${content}`; case "strikethrough": return `~~${content}~~`; - case "spoiler": return `||${content}||`; - case "code": return `\`${content}\``; + case "spoiler": return `${content}`; + case "code": return "`" + content + "`"; case "pre": return "```" + (e.language || "") + "\n" + content + "\n```"; case "text_link": return `[${content}](${e.url})`; case "text_mention": return e.user ? `[${content}](tg://user?id=${e.user.id})` : content; case "blockquote": case "expandable_blockquote": - return content.split("\n").map(l => `>${l}`).join("\n"); + return content.split("\n").map(l => `> ${l}`).join("\n"); default: return content; } } @@ -457,30 +843,19 @@ async function sendPlain(chatId, text, replyMarkup) { await callApi("sendMessage", body); } async function sendRichMarkdown(chatId, markdown, replyMarkup) { - const body = { chat_id: chatId, rich_message: { markdown } }; + const body = { chat_id: chatId, rich_message: { markdown, is_rtl: isRtl(markdown) } }; if (replyMarkup) body.reply_markup = replyMarkup; const res = await callApi("sendRichMessage", body); - // FIX: if Rich sending fails for any reason, fall back to a plain message so the user always gets it. if (!res?.ok) { const fb = { chat_id: chatId, text: markdown }; if (replyMarkup) fb.reply_markup = replyMarkup; - await callApi("sendMessage", fb); - } - return res; -} -async function sendRichHtml(chatId, html, replyMarkup) { - const body = { chat_id: chatId, rich_message: { html } }; - if (replyMarkup) body.reply_markup = replyMarkup; - const res = await callApi("sendRichMessage", body); - if (!res?.ok) { - const fb = { chat_id: chatId, text: html }; - if (replyMarkup) fb.reply_markup = replyMarkup; - await callApi("sendMessage", fb); + const fbRes = await callApi("sendMessage", fb); + return fbRes; } return res; } async function editRichMarkdown(chatId, messageId, markdown, replyMarkup) { - const body = { chat_id: chatId, message_id: messageId, rich_message: { markdown } }; + const body = { chat_id: chatId, message_id: messageId, rich_message: { markdown, is_rtl: isRtl(markdown) } }; if (replyMarkup) body.reply_markup = replyMarkup; await callApi("editMessageText", body); } @@ -505,17 +880,19 @@ const WELCOME = { fa: [ "# 🤖 Rich Markdown Bot", "", - "هر متن **Markdown** یا **HTML** بفرستید، به صورت Rich Message رندر میشه.", - "همچنین می‌توانید بات را به کانال خود اضافه کنید تا پست‌ها به صورت خودکار قالب‌بندی شوند.", + "هر متن **Markdown** بفرستید، به‌صورت Rich Message رندر می‌شود. می‌توانید وسط همان متن، تگ‌های HTML تلگرام (مثل `` یا `
`) یا فرمول (`$x^2$`) هم بگذارید؛ بقیه‌ی فرمت‌های دستی‌تان سالم می‌ماند.", + "همچنین می‌توانید بات را به کانال خود اضافه کنید تا پست‌ها خودکار قالب‌بندی شوند.", "", + "🧰 دستورها: `/footer` (امضای کانال) · `/undo` (بازگردانی آخرین پست)", "از دکمه‌های زیر برای دیدن راهنما و دمو استفاده کنید 👇", ].join("\n"), en: [ "# 🤖 Rich Markdown Bot", "", - "Send any **Markdown** or **HTML** text and it will be echoed back as a rendered Rich Message.", - "You can also add this bot to your channel to automatically format posts.", + "Send any **Markdown** text and it is echoed back as a rendered Rich Message. You can mix in Telegram HTML tags (e.g. ``, `
`) or formulas (`$x^2$`) right inside it — the rest of your manual formatting stays intact.", + "You can also add this bot to your channel to auto-format posts.", "", + "🧰 Commands: `/footer` (channel footer) · `/undo` (revert last post)", "Use the buttons below to explore 👇", ].join("\n"), }; @@ -523,100 +900,112 @@ const ABOUT = { fa: [ "# ℹ️ درباره بات", "", - "این بات برای رندر کردن پیام‌های Markdown و HTML شما طراحی شده است. از ساختار مدرن Rich Message تلگرام برای نمایش عناصر پیشرفته استفاده می‌کند. می‌توانید آن را به کانال‌های خود اضافه کنید تا پست‌ها را به صورت خودکار، در لحظه انتشار زیباتر کند.", + "این بات پیام‌های شما را با فرمت مدرن **Rich Message** تلگرام رندر می‌کند. حالت پایه «مارک‌داون» است و چون مارک‌داونِ تلگرام می‌تواند HTML را هم در خودش جا بدهد، برای استفاده از یک تگ یا فرمول، دیگر لازم نیست کل متن را HTML کنید؛ همین یعنی فاصله‌ها و بولدهای دستی‌تان هیچ‌وقت نمی‌پرند.", ].join("\n"), en: [ "# ℹ️ About", "", - "This bot is designed to render your Markdown and HTML messages natively using Telegram's Rich Message format. You can also add it to your channels to automatically format your posts in real-time as you publish them.", + "This bot renders your messages using Telegram's modern **Rich Message** format. The base mode is Markdown, and since Telegram Markdown can embed HTML, you can drop in a single tag or formula without forcing the whole message into HTML — so your manual spacing and bold never get wiped.", ].join("\n"), }; const HELP_CHANNEL = { fa: [ "### 🛠 آموزش اتصال کانال:", - "1. ربات را به کانال خود اضافه کنید.", + "1. ربات را به کانال خود به‌عنوان **ادمین** اضافه کنید.", "2. دسترسی **Edit Messages** (ویرایش پیام‌ها) را به آن بدهید.", - "3. یک پیام از کانال خود به همینجا (داخل بات) **Forward** کنید.", - "", - "پس از تایید، هر زمان که در کانال پست بگذارید (دارای تگ‌های مارک‌داون یا HTML)، بات در لحظه آن را به نسخه رندر شده تغییر می‌دهد!", + "3. یک پیام از کانال خود به همین‌جا (داخل بات) **Forward** کنید.", "", - "*(پست‌هایی که هیچ تگی ندارند نادیده گرفته می‌شوند تا برچسب \"Edited\" نخورند)* ✨", + "پس از تأیید، هر پستی که فرمت داشته باشد (مارک‌داون، HTML، فرمول، یا تگ‌های فارسی) در لحظه رندر می‌شود!", "", + "*(پست‌های بدون هیچ فرمتی نادیده گرفته می‌شوند تا برچسب «Edited» نخورند)* ✨", "", - "### 🏷 تگ‌های مخصوص کانال", - "بعضی استایل‌ها فقط روی پیام متنی کانال رندر می‌شوند و روی کپشن مدیا (عکس/ویدیو) کار نمی‌کنند. این محدودیتِ خودِ تلگرام است، نه باگ بات.", + "🧰 **امکانات مدیریتی:**", + "- ✍️ **امضای کانال**: یک فوتر ثابت (با چند لینک) ته هر پست. از دکمه‌ی زیر یا `/footer`.", + "- ⚙️ **تنظیمات تگ‌ها**: فعال/غیرفعال کردن قابلیت‌های رندر (مثل اسلایدشو، نقشه و...) برای هر کانال.", + "- ↩️ **بازگردانی**: با `/undo` آخرین رندر کانال را به متن خام برگردان.", "", - "**✅ روی پیام متنی کانال کار می‌کند:**", - "- هدینگ‌ها و برجسته‌سازی متن (# عنوان)", - "- لیست کارها (- [ ] و - [x])", - "- جدول‌ها", - "- بلوک کد و فرمول ریاضی داخل تگ‌های (///....///) یا ($$...$$)", - "- نقل‌قول (> ...)", - "- همه‌ی استایل‌های inline (بولد، ایتالیک، خط‌خورده، کد، لینک، اسپویلر)", - "", - "**⚠️ روی کپشن مدیا کار نمی‌کند:**", - "فقط استایل‌های inline رندر می‌شوند:", - "بولد · ایتالیک · خط‌خورده · کد · لینک · ||اسپویلر||", - "هدینگ، لیست کارها، جدول، بلوک کد و فرمول روی کپشن نمایش داده نمی‌شوند.", - "", - "💡 اگر می‌خواهی todo / جدول / هدینگ داشته باشی، پست را به‌صورت متنی (بدون عکس) بفرست.", + "برای دیدن تگ‌های مخصوص کانال و میان‌بُرهای فارسی، دکمه‌ی زیر را بزنید 👇", ].join("\n"), - en: [ "### 🛠 How to setup:", - "1. Add the bot to your channel as an administrator.", + "1. Add the bot to your channel as an **administrator**.", "2. Grant it the **Edit Messages** permission.", "3. **Forward** any message from your channel to this bot.", "", - "Once authorized, whenever you post text or media captions containing Markdown/HTML tags, the bot will instantly render it!", + "Once authorized, any post containing formatting (Markdown, HTML, formulas, or the shorthand tags) is rendered instantly!", + "", + "*(Posts without any formatting are safely ignored to avoid the \"Edited\" label)* ✨", "", - "*(Posts without any markdown tags are safely ignored to avoid the \"Edited\" label)* ✨", + "🧰 **Management:**", + "- ✍️ **Channel footer**: a fixed footer (with multiple links) appended to every post. Use the button below or `/footer`.", + "- ⚙️ **Render Settings**: Enable/Disable specific formatting tags (like map, slideshow) per channel.", + "- ↩️ **Undo**: use `/undo` to revert a channel's last render back to raw text.", + "", + "Tap below to see channel-only tags and shortcuts 👇", ].join("\n"), }; const HELP_CHANNEL_TAGS = { fa: [ - "# 🏷 تگ‌های مخصوص کانال", + "# 🏷 تگ‌های مخصوص کانال و میان‌بُرها", + "", + "چون حالت پایه مارک‌داون است، می‌توانی هر تگ یا فرمول را وسط متن عادی بگذاری و **بقیه‌ی متن دست‌نخورده** می‌ماند.", "", - "بعضی استایل‌ها فقط روی **پیام متنی** کانال رندر می‌شوند و روی **کپشن مدیا** (عکس/ویدیو) کار نمی‌کنند. این محدودیتِ خودِ تلگرام است، نه باگ بات.", + "---", + "## ✍️ میان‌بُرهای ساده (بدون تگ خام)", + "- `///کد///` → بلوک کد", + "- `???متن???` → بلوک **کشویی/بازشو**", + "- `\"\"\"نقل‌قول — نویسنده\"\"\"` → **نقل‌قول وسط‌چین** با امضا", + "- `[زمان: 2026-07-01 20:00]` → **زمان زنده** (در تایم‌زون هر کاربر)", + "- `[نقشه: 35.7, 51.4]` → **نقشه**", + "- `[اسلایدشو] ... [/اسلایدشو]` / `[کلاژ] ... [/کلاژ]` / `[کشویی] ... [/کشویی]`", + "- خط شروع‌شده با 🙈 → آن خط **اسپویلر** می‌شود", + "- فقط چند **لینک مدیا** پشت‌سرهم بفرست → خودکار **اسلایدشو** می‌شود", "", "---", "## ✅ روی پیام متنی کانال کار می‌کند", - "- هدینگ‌ها و برجسته‌سازی متن (`# عنوان`)", - "- لیست کارها (`- [ ]` و `- [x]`)", - "- جدول‌ها", - "- بلوک کد و فرمول ریاضی داخل تگ‌های (`///...///`) یا (`$$...$$`)", - "- نقل‌قول (`> ...`)", - "- همه‌ی استایل‌های inline (بولد، ایتالیک، خط‌خورده، کد، لینک، اسپویلر)", + "- هدینگ (`# عنوان`)، نقل‌قول (`> ...`)", + "- لیست و تسک (`- [ ]` و `- [x]`)", + "- جدول، بلوک کد، و فرمول (`$...$` و `$$...$$`)", + "- بلوک کشویی `
` و اسپویلر ``", + "- همه‌ی استایل‌های inline (بولد، ایتالیک، خط‌خورده، کد، لینک)", "", "---", "## ⚠️ روی کپشن مدیا کار نمی‌کند", "فقط استایل‌های inline رندر می‌شوند:", - "**بولد** · *ایتالیک* · ~~خط‌خورده~~ · `کد` · [لینک](http://url) · ||اسپویلر||", - "", - "هدینگ، لیست کارها، جدول، بلوک کد و فرمول روی کپشن نمایش داده **نمی‌شوند**.", + "**بولد** · *ایتالیک* · ~~خط‌خورده~~ · `کد` · [لینک](https://t.me/) · ||اسپویلر||", + "هدینگ، لیست، جدول، بلوک کد و فرمول روی کپشن نمایش داده **نمی‌شوند**.", "", - "> 💡 اگر می‌خواهی todo / جدول / هدینگ داشته باشی، پست را به‌صورت **متنی** (بدون عکس) بفرست.", + "> 💡 اگر به todo / جدول / هدینگ نیاز داری، پست را به‌صورت **متنی** (بدون عکس) بفرست.", ].join("\n"), en: [ - "# 🏷 Channel-only Tags", + "# 🏷 Channel-only Tags & Shortcuts", "", - "Some styles only render on a **text post** in a channel and do NOT work on a **media caption** (photo/video). This is a Telegram limitation, not a bot bug.", + "Since the base mode is Markdown, you can drop any tag or formula into normal text and **the rest stays untouched**.", + "", + "---", + "## ✍️ Simple shortcuts (no raw tags)", + "- `///code///` → code block", + "- `???text???` → **expandable/collapsible** block", + "- `\"\"\"quote — author\"\"\"` → **centered pull-quote** with credit", + "- `[time: 2026-07-01 20:00]` → **live time** (in each user's timezone)", + "- `[map: 35.7, 51.4]` → **map** embed", + "- `[slideshow] ... [/slideshow]` / `[collage] ... [/collage]` / `[expand] ... [/expand]`", + "- a line starting with 🙈 → that line becomes a **spoiler**", + "- send just a few **media URLs** in a row → auto **slideshow**", "", "---", "## ✅ Works on channel text posts", - "- Headings (`# Title`)", - "- Task lists (`- [ ]` and `- [x]`)", - "- Tables", - "- Code blocks & math inside (`///...///`) or (`$$...$$`)", - "- Quotes (`> ...`)", - "- All inline styles (bold, italic, strike, code, links, spoiler)", + "- Headings (`# Title`), quotes (`> ...`)", + "- Lists & tasks (`- [ ]`, `- [x]`)", + "- Tables, code blocks, formulas (`$...$`, `$$...$$`)", + "- Expandable `
` and ``", + "- All inline styles (bold, italic, strike, code, links)", "", "---", "## ⚠️ Does NOT work on media captions", "Only inline styles render:", - "**bold** · *italic* · ~~strike~~ · `code` · [link](http://url) · ||spoiler||", - "", - "Headings, task lists, tables, code blocks and math are **not** shown on captions.", + "**bold** · *italic* · ~~strike~~ · `code` · [link](https://t.me/) · ||spoiler||", + "Headings, lists, tables, code blocks and formulas are **not** shown on captions.", "", "> 💡 If you need todos / tables / headings, send the post as **text** (no photo).", ].join("\n"), @@ -625,56 +1014,52 @@ const HELP_MD = { fa: [ "# 📖 راهنمای Markdown", "", - "متن Markdown بفرستید، رندر شده برمیگرده. کادر خاکستری = چیزی که تایپ میکنید ↓ نتیجه بعدشه.", + "متن Markdown بفرست، رندرشده برمی‌گردد. کادر خاکستری = چیزی که تایپ می‌کنی ↓ نتیجه بعدش می‌آید.", "", "---", - "## Text Styles", + "## استایل متن", "```", - "**bold** *italic* ~~strike~~", - "`code` ==marked== ||spoiler||", + "**بولد** *ایتالیک* ~~خط‌خورده~~", + "`کد` ==های‌لایت== آندرلاین اسپویلر", "```", - "**bold** *italic* ~~strike~~ `code` ==marked== ||spoiler||", + "**بولد** *ایتالیک* ~~خط‌خورده~~ `کد` ==های‌لایت== آندرلاین اسپویلر", "", "---", - "## Headings", + "## هدینگ", "```", - "# Heading 1", - "## Heading 2", - "### Heading 3", + "# هدینگ ۱", + "## هدینگ ۲", + "### هدینگ ۳", "```", - "# Heading 1", - "## Heading 2", - "### Heading 3", "", "---", - "## Lists", + "## لیست‌ها", "```", - "- milk", - "- eggs", - "- [ ] todo", - "- [x] done", + "- شیر", + "- تخم‌مرغ", + "- [ ] کار انجام‌نشده", + "- [x] کار انجام‌شده", "", - "1. wake up", - "2. ship it", + "1. بیدار شو", + "2. منتشر کن", "```", - "- milk", - "- eggs", - "- [ ] todo", - "- [x] done", - "", - "1. wake up", - "2. ship it", "", "---", - "## Code Blocks", + "## نقل‌قول و فرمول", "```", - " \\`\\`\\`python", - " print(\"hello\")", - " \\`\\`\\`", + "> نقل‌قول با **بولد**", + "فرمول داخل‌خطی $E = mc^2$", + "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", "```", - "```python", + "", + "---", + "## بلوک کد", + "```", + "///", "print(\"hello\")", + "///", "```", + "💡 می‌توانی هر تگ HTML تلگرام را هم وسط همین مارک‌داون بگذاری؛ لازم نیست کل پیام HTML شود.", ].join("\n"), en: [ "# 📖 Markdown Guide", @@ -682,12 +1067,12 @@ const HELP_MD = { "Send Markdown text and get it echoed back rendered. Grey box = what you type ↓ result comes right after.", "", "---", - "## Text Styles", + "## Text styles", "```", "**bold** *italic* ~~strike~~", - "`code` ==marked== ||spoiler||", + "`code` ==marked== underline spoiler", "```", - "**bold** *italic* ~~strike~~ `code` ==marked== ||spoiler||", + "**bold** *italic* ~~strike~~ `code` ==marked== underline spoiler", "", "---", "## Headings", @@ -696,9 +1081,6 @@ const HELP_MD = { "## Heading 2", "### Heading 3", "```", - "# Heading 1", - "## Heading 2", - "### Heading 3", "", "---", "## Lists", @@ -707,54 +1089,74 @@ const HELP_MD = { "- eggs", "- [ ] todo", "- [x] done", + "", + "1. wake up", + "2. ship it", "```", - "- milk", - "- eggs", - "- [ ] todo", - "- [x] done", "", "---", - "## Code Blocks", + "## Quotes & formulas", "```", - " \\`\\`\\`python", - " print(\"hello\")", - " \\`\\`\\`", + "> Quote with **bold**", + "Inline formula $E = mc^2$", + "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", "```", - "```python", + "", + "---", + "## Code block", + "```", + "///", "print(\"hello\")", + "///", "```", + "💡 You can also drop any Telegram HTML tag right inside this Markdown — no need to make the whole message HTML.", ].join("\n"), }; const HELP_HTML = { fa: [ "# 🌐 راهنمای HTML", "", - "اگه پیامت با `<` شروع بشه، بات به عنوان HTML رندر میکنه.", + "تگ‌های HTML تلگرام را می‌توانی **هرجای متن** بگذاری (لازم نیست پیام با `<` شروع شود).", "", "---", - "## Text Styles", + "## استایل متن", "```", - "bold italic underline", - "strike code marked", - "spoiler", + "بولد ایتالیک آندرلاین", + "خط‌خورده کد های‌لایت", + "اسپویلر", "```", - "bold italic underline strike code marked spoiler", + "بولد ایتالیک آندرلاین خط‌خورده کد های‌لایت اسپویلر", "", "---", - "## Lists", + "## بلوک کشویی و نقل‌قول", "```", - "
  • milk
  • eggs
", - "
  1. wake up
", + "
عنوانمحتوای بازشو
", + "
نقل‌قول
", + "", + "```", + "", + "---", + "## لیست و جدول", + "```", + "
  • شیر
  • تخم‌مرغ
", + "
  1. اول
  2. دوم
", + "
سرستون
۱۲
", + "```", + "", + "---", + "## فرمول", + "```", + "x^2 + y^2", + "E = mc^2", "```", - "
  • milk
  • eggs
  1. wake up
", ].join("\n"), en: [ "# 🌐 HTML Guide", "", - "If your message starts with `<`, the bot renders it as HTML.", + "You can put Telegram HTML tags **anywhere** in your text (the message doesn't need to start with `<`).", "", "---", - "## Text Styles", + "## Text styles", "```", "bold italic underline", "strike code marked", @@ -763,99 +1165,188 @@ const HELP_HTML = { "bold italic underline strike code marked spoiler", "", "---", - "## Lists", + "## Expandable & quotes", + "```", + "
TitleCollapsible content
", + "
Quote
", + "", + "```", + "", + "---", + "## Lists & tables", "```", "
  • milk
  • eggs
", - "
  1. wake up
", + "
  1. first
  2. second
", + "
HeadCol
12
", + "```", + "", + "---", + "## Formulas", + "```", + "x^2 + y^2", + "E = mc^2", "```", - "
  • milk
  • eggs
  1. wake up
", ].join("\n"), }; const HELP_MEDIA = { fa: [ "# 🖼 راهنمای مدیا", "", - "برای ارسال مدیا در Rich Message از سینتکس تصویر Markdown استفاده کنید.", + "می‌توانی عکس/ویدیو/فایل را داخل پیام Rich جاسازی کنی یا چند مدیا را گروهی نمایش بدهی.", "", "---", - "## عکس / ویدیو", + "## عکس و ویدیو تکی", "```", - "![]([https://telegram.org/example/photo.jpg](https://telegram.org/example/photo.jpg))", - "![]([https://telegram.org/example/video.mp4](https://telegram.org/example/video.mp4))", + "![کپشن](https://example.com/photo.jpg)", + "", "```", + "💡 لینک باید مستقیم و http/https باشد.", "", "---", - "## اسلایدشو", + "## اسلایدشو و کلاژ", + "```", + "[اسلایدشو]", + "![](https://example.com/1.jpg)", + "![](https://example.com/2.jpg)", + "[/اسلایدشو]", + "```", + "یا با تگ خام:", "```", "", - " ", - " ", "```", + "💡 میان‌بُر: اگر فقط چند **لینک مدیا** را پشت‌سرهم (هر خط یکی) بفرستی، خودکار به اسلایدشو تبدیل می‌شود.", + "", + "---", + "## نقشه", + "```", + "[نقشه: 35.6892, 51.3890]", + "```", ].join("\n"), en: [ "# 🖼 Media Guide", "", - "Use Markdown image syntax to embed media in Rich Messages.", + "You can embed photos/videos/files in a Rich message or display several media as a group.", "", "---", - "## Photo / Video", + "## Single photo & video", "```", - "![]([https://telegram.org/example/photo.jpg](https://telegram.org/example/photo.jpg))", - "![]([https://telegram.org/example/video.mp4](https://telegram.org/example/video.mp4))", + "![caption](https://example.com/photo.jpg)", + "", "```", + "💡 The link must be a direct http/https URL.", "", "---", - "## Slideshow", + "## Slideshow & collage", + "```", + "[slideshow]", + "![](https://example.com/1.jpg)", + "![](https://example.com/2.jpg)", + "[/slideshow]", + "```", + "Or with the raw tag:", "```", "", - " ", - " ", "```", + "💡 Shortcut: send just a few **media URLs** in a row (one per line) and they auto-group into a slideshow.", + "", + "---", + "## Map", + "```", + "[map: 35.6892, 51.3890]", + "```", ].join("\n"), }; const DEMO = { fa: [ "# 🎨 دمو کامل", "", - "**Bold** _italic_ ~~strike~~ `code` ==highlight== ||spoiler|| underline", + "این یک نمونه از همه‌ی امکانات با هم است:", + "", + "## ۱) استایل‌ها", + "**بولد** · *ایتالیک* · ~~خط‌خورده~~ · `کد` · آندرلاین · اسپویلر", + "", + "## ۲) لیست و تسک", + "- مورد اول", + "- مورد دوم", + "- [x] انجام شد", + "- [ ] در انتظار", "", - ">نقل‌قول با **bold**", + "## ۳) نقل‌قول", + "> دانش، قدرت است. **همیشه**.", "", - "- [ ] Task 1", - "- [x] Task 2", + "## ۴) نقل‌قول وسط‌چین", + "", "", + "## ۵) فرمول", + "داخل‌خطی $a^2 + b^2 = c^2$ و بلوکی:", + "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$", + "", + "## ۶) بلوک کد", "```python", - "print(\"Hello Telegram\")", + "def hi():", + " print(\"سلام دنیا\")", "```", "", - "| Lang | Speed |", - "|:-----|------:|", - "| Rust | fast |", + "## ۷) جدول", + "| نام | امتیاز |", + "|-----|--------|", + "| علی | ۹۵ |", + "| رضا | ۸۸ |", "", - "Inline $E = mc^2$", - "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", + "## ۸) بلوک کشویی", + "
برای دیدن بزنمحتوای مخفی اینجاست 🎉
", + "", + "## ۹) زمان زنده و نقشه", + "[زمان: 2026-07-01 20:00]", + "[نقشه: 35.6892, 51.3890]", ].join("\n"), en: [ "# 🎨 Full Demo", "", - "**Bold** _italic_ ~~strike~~ `code` ==highlight== ||spoiler|| underline", + "A sample showing every feature together:", "", - ">Quote with **bold**", + "## 1) Styles", + "**bold** · *italic* · ~~strike~~ · `code` · underline · spoiler", "", - "- [ ] Task 1", - "- [x] Task 2", + "## 2) Lists & tasks", + "- first item", + "- second item", + "- [x] done", + "- [ ] pending", + "", + "## 3) Quote", + "> Knowledge is power. **Always**.", + "", + "## 4) Pull-quote", + "", "", + "## 5) Formulas", + "Inline $a^2 + b^2 = c^2$ and block:", + "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$", + "", + "## 6) Code block", "```python", - "print(\"Hello Telegram\")", + "def hi():", + " print(\"hello world\")", "```", "", - "| Lang | Speed |", - "|:-----|------:|", - "| Rust | fast |", + "## 7) Table", + "| Name | Score |", + "|------|-------|", + "| Ali | 95 |", + "| Reza | 88 |", "", - "Inline $E = mc^2$", - "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", + "## 8) Expandable", + "
Tap to revealHidden content here 🎉
", + "", + "## 9) Live time & map", + "[time: 2026-07-01 20:00]", + "[map: 35.6892, 51.3890]", ].join("\n"), }; From e84aeb9564af3a9cc4a6590783ed83cdc4338dd0 Mon Sep 17 00:00:00 2001 From: "A.M. Hasani" Date: Fri, 19 Jun 2026 01:47:46 +0330 Subject: [PATCH 4/5] bug fixed --- worker.js | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/worker.js b/worker.js index 359ac42..97a821b 100644 --- a/worker.js +++ b/worker.js @@ -17,8 +17,8 @@ * 6) Tag Render Settings: Per-channel toggle for specific tags via glass buttons. */ -const BOT_TOKEN = "TOKEN BOT IN PLACE"; // ⚠️ revoke the leaked one -const ADMIN_ID = 12345678987; // admin user id +const BOT_TOKEN = "TOKEN IN PLACE"; +const ADMIN_ID = 112345678895; // admin user id const TELEGRAM_API = `https://api.telegram.org/bot${BOT_TOKEN}`; const TTL_PROCESSED_SEC = 30 * 24 * 60 * 60; // 30 days const TTL_CHANNEL_SEC = 30 * 24 * 60 * 60; // 30 days @@ -667,6 +667,22 @@ function renderUserText(rawText, entities, config) { } function applyCustomSyntax(text, config) { + // Mask code blocks to protect them from regex replacements + const codeBlocks = []; + text = text.replace(/(```[\s\S]*?```|\/\/\/[\s\S]*?\/\/\/)/g, match => { + codeBlocks.push(match); + return `\u0000CB${codeBlocks.length - 1}\u0000`; + }); + + // Disable standard Markdown headings (inserts Zero-Width Space so parser ignores them) + text = text.replace(/(^|\n)([ \t>]*)(#{1,6})([ \t]+)/g, "$1$2$3\u200B$4"); + + // Enable custom `#_ ` heading syntax + text = text.replace(/(^|\n)([ \t>]*)(#{1,6})_([ \t]+)/g, "$1$2$3$4"); + + // Restore code blocks + text = text.replace(/\u0000CB(\d+)\u0000/g, (_, i) => codeBlocks[Number(i)]); + if (config.quote) { text = applyPullQuote(text); } @@ -755,7 +771,7 @@ function hasRealFormatting(t) { /\[(?:نقشه|map)\s*:/.test(t) || /\[(?:اسلایدشو|slideshow|کلاژ|collage|کشویی|expand)\]/.test(t) || /[🙈🔽]/.test(t) || - /(^|\n)\s{0,3}#{1,6}\s+\S/.test(t) || + /(^|\n)[ \t>]*#{1,6}_\s+\S/.test(t) || /(^|\n)\s*[-*+]\s*\[[ xX]\]\s+\S/.test(t) || /(^|\n)\s*[-*+]\s+\S/.test(t) || /(^|\n)\s*\d+\.\s+\S/.test(t) || @@ -963,7 +979,7 @@ const HELP_CHANNEL_TAGS = { "", "---", "## ✅ روی پیام متنی کانال کار می‌کند", - "- هدینگ (`# عنوان`)، نقل‌قول (`> ...`)", + "- هدینگ (`#_ عنوان`)، نقل‌قول (`> ...`)", "- لیست و تسک (`- [ ]` و `- [x]`)", "- جدول، بلوک کد، و فرمول (`$...$` و `$$...$$`)", "- بلوک کشویی `
` و اسپویلر ``", @@ -995,7 +1011,7 @@ const HELP_CHANNEL_TAGS = { "", "---", "## ✅ Works on channel text posts", - "- Headings (`# Title`), quotes (`> ...`)", + "- Headings (`#_ Title`), quotes (`> ...`)", "- Lists & tasks (`- [ ]`, `- [x]`)", "- Tables, code blocks, formulas (`$...$`, `$$...$$`)", "- Expandable `
` and ``", @@ -1027,9 +1043,9 @@ const HELP_MD = { "---", "## هدینگ", "```", - "# هدینگ ۱", - "## هدینگ ۲", - "### هدینگ ۳", + "#_ هدینگ ۱", + "##_ هدینگ ۲", + "###_ هدینگ ۳", "```", "", "---", @@ -1077,9 +1093,9 @@ const HELP_MD = { "---", "## Headings", "```", - "# Heading 1", - "## Heading 2", - "### Heading 3", + "#_ Heading 1", + "##_ Heading 2", + "###_ Heading 3", "```", "", "---", From 5e6f2029dfe3649eb75dbf175d7233691c589c15 Mon Sep 17 00:00:00 2001 From: "A.M. Hasani" Date: Sat, 4 Jul 2026 00:23:00 +0330 Subject: [PATCH 5/5] Update worker.js --- worker.js | 435 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 249 insertions(+), 186 deletions(-) diff --git a/worker.js b/worker.js index 97a821b..7b1ed6c 100644 --- a/worker.js +++ b/worker.js @@ -17,6 +17,7 @@ * 6) Tag Render Settings: Per-channel toggle for specific tags via glass buttons. */ + const BOT_TOKEN = "TOKEN IN PLACE"; const ADMIN_ID = 112345678895; // admin user id const TELEGRAM_API = `https://api.telegram.org/bot${BOT_TOKEN}`; @@ -27,7 +28,7 @@ const TTL_PREVIEW_SEC = 60 * 60; // 1 hour (pending preview) const TTL_AWAIT_SEC = 10 * 60; // 10 min (footer input) const DEFAULT_CONFIG = { quote: true, time: true, map: true, code: true, expand: true, media: true, spoiler: true }; - + export default { async fetch(request, env) { if (request.method === "GET") return new Response("✅ Bot is running!", { status: 200 }); @@ -49,6 +50,8 @@ export default { await handleCallback(update.callback_query, env); } else if (update.channel_post) { await handleChannelPost(update.channel_post, env); + } else if (update.edited_channel_post) { + await handleChannelPost(update.edited_channel_post, env); } } catch (err) { console.error("Handler error:", err && err.stack ? err.stack : err); @@ -125,6 +128,7 @@ async function setChannelConfig(env, channelId, config) { // Stats / dashboard helpers function dateKey(offsetDays = 0) { + // Tehran local date (UTC+3:30) const ms = Date.now() + 3.5 * 3600 * 1000 - offsetDays * 24 * 3600 * 1000; const d = new Date(ms); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`; @@ -184,7 +188,6 @@ function backKeyboard(lang) { ], }; } -// Channel guide keyboard function channelGuideKeyboard(lang, userChannels) { const isFa = lang === "fa"; const rows = []; @@ -217,7 +220,6 @@ function backToChannelKeyboard(lang) { ]], }; } -// Config menus function renderMenuKeyboard(lang, userChannels) { const isFa = lang === "fa"; const rows = []; @@ -234,14 +236,13 @@ function renderTogglesKeyboard(lang, channelId, config) { const status = config[key] ? "✅" : "❌"; return { text: `${status} ${isFa ? labelFa : labelEn}`, callback_data: `${lang}_rtgl_${channelId}_${key}` }; }; - rows.push([mkBtn("quote", "نقل‌قول (\"\")", "Quote (\"\")"), mkBtn("time", "زمان", "Time")]); + rows.push([mkBtn("quote", 'نقل‌قول ("")', 'Quote ("")'), mkBtn("time", "زمان", "Time")]); rows.push([mkBtn("map", "نقشه", "Map"), mkBtn("code", "کد (///)", "Code (///)")]); rows.push([mkBtn("expand", "کشویی (???)", "Expand (???)"), mkBtn("media", "مدیا (اسلایدشو)", "Media")]); rows.push([mkBtn("spoiler", "اسپویلر (🙈)", "Spoiler (🙈)")]); rows.push([{ text: isFa ? "⬅️ بازگشت به لیست" : "⬅️ Back to list", callback_data: `${lang}_rendermenu` }]); return { inline_keyboard: rows }; } -// Footer set/remove menu. function footerMenuKeyboard(lang, userChannels) { const isFa = lang === "fa"; const rows = []; @@ -256,7 +257,6 @@ function footerMenuKeyboard(lang, userChannels) { ]); return { inline_keyboard: rows }; } -// Live preview function previewKeyboard(userChannels) { const rows = []; for (const c of userChannels || []) { @@ -265,7 +265,6 @@ function previewKeyboard(userChannels) { rows.push([{ text: "❌ بستن پیش‌نمایش / Close", callback_data: "preview_close" }]); return { inline_keyboard: rows }; } -// Undo confirmation. function undoConfirmKeyboard(lang, channelId) { const isFa = lang === "fa"; return { @@ -292,16 +291,16 @@ async function handleMessage(message, env) { const trimmed = rawText.trim(); await registerUser(env, userId); - // --- FOOTER INPUT STATE --- + // --- FOOTER INPUT STATE (capture next message as footer) --- const awaitFooter = await env.DB.get(`await_footer:${userId}`); if (awaitFooter) { if (trimmed.startsWith("/")) { - await env.DB.delete(`await_footer:${userId}`); + await env.DB.delete(`await_footer:${userId}`); // a command cancels footer input } else { const footerText = renderUserText(rawText, message.entities, DEFAULT_CONFIG) || trimmed; await env.DB.put(`footer:${awaitFooter}`, footerText); await env.DB.delete(`await_footer:${userId}`); - await sendPlain(chatId, "✅ امضای کانال ذخیره شد. از این پس ته هر پستِ همان کانال اضافه می‌شود.\nبرای حذف، از منوی «✍️ امضای کانال» دکمه‌ی 🗑 را بزن."); + await sendPlain(chatId, `✅ امضای کانال ذخیره شد. از این پس ته هر پستِ همان کانال اضافه می‌شود.\n\nمتنِ ذخیره‌شده:\n${footerText}\n\nبرای حذف، از منوی «✍️ امضای کانال» دکمه‌ی 🗑 را بزن.`); return; } } @@ -322,17 +321,13 @@ async function handleMessage(message, env) { chart += `${k.slice(5)} ${bar} ${c}\n`; } const msg = -`📊 *داشبورد بات* - -👥 کاربران: ${users} -🔄 کل پست‌های رندرشده: ${processed} -📢 کانال‌های فعال: ${channels} -✍️ امضاهای تنظیم‌شده: ${footers} -🗓 پست‌های امروز: ${today} - -📈 ۷ روز اخیر: -\`\`\` -${chart}\`\`\``; + `📊 *داشبورد بات*\n\n` + + `👥 کاربران: ${users}\n` + + `🔄 کل پست‌های رندرشده: ${processed}\n` + + `📢 کانال‌های فعال: ${channels}\n` + + `✍️ امضاهای تنظیم‌شده: ${footers}\n` + + `🗓 پست‌های امروز: ${today}\n\n` + + `📈 ۷ روز اخیر:\n\`\`\`\n${chart}\`\`\``; await sendPlain(chatId, msg); return; } @@ -355,7 +350,7 @@ ${chart}\`\`\``; } } - // --- UNDO --- + // --- UNDO (any user, in PM) --- if (trimmed === "/undo") { const chs = await kvGetUserChannels(env, userId); if (!chs.length) { @@ -434,7 +429,7 @@ async function handleCallback(cb, env) { const data = cb.data; await callApi("answerCallbackQuery", { callback_query_id: cb.id }); - // --- Non-language-prefixed actions --- + // --- Non-language-prefixed actions (from DM live preview) --- if (data.startsWith("pub_")) { const channelId = data.slice(4); const text = await env.DB.get(`preview:${userId}`); @@ -442,7 +437,10 @@ async function handleCallback(cb, env) { await sendPlain(chatId, "⛔️ پیش‌نمایش منقضی شده. دوباره متن را بفرست."); return; } - const res = await sendRichMarkdown(channelId, text); + let outText = text; + const footer = await env.DB.get(`footer:${channelId}`); + if (footer) outText = outText + "\n\n" + footer; + const res = await sendRichMarkdown(channelId, outText); if (res?.ok && res.result?.message_id) { await kvMarkMessageProcessed(env, channelId, res.result.message_id); await env.DB.put( @@ -574,7 +572,7 @@ async function handleCallback(cb, env) { } if (r?.ok) { await env.DB.delete(`last_render:${channelId}`); - await env.DB.delete(`processed:${channelId}:${last.messageId}`); + await env.DB.delete(`processed:${channelId}:${last.messageId}`); // allow re-render later await editRichMarkdown(chatId, msgId, "✅ آخرین رندر به متن خام برگردانده شد.", null); } else { await editRichMarkdown(chatId, msgId, "⚠️ بازگردانی ناموفق بود (شاید پیام خیلی قدیمی است یا حذف شده).", null); @@ -622,9 +620,10 @@ async function handleChannelPost(post, env) { let text = renderUserText(rawText, entities, config); if (!text) text = rawText.trim(); - // Append auto channel footer + // Append auto channel footer. if (footer) text = text + "\n\n" + footer; + // Save the original raw text so /undo can revert this render. await env.DB.put( `last_render:${channelId}`, JSON.stringify({ messageId, isCaption, raw: rawText }), @@ -635,6 +634,7 @@ async function handleChannelPost(post, env) { let res; if (isCaption) { + // Captions have NO rich field — only classic inline formatting via parse_mode. res = await callApi("editMessageCaption", { chat_id: channelId, message_id: messageId, @@ -642,6 +642,7 @@ async function handleChannelPost(post, env) { parse_mode: "HTML", }); } else { + // Text posts use Telegram's NATIVE Rich renderer (markdown mode, may embed HTML). res = await callApi("editMessageText", { chat_id: channelId, message_id: messageId, @@ -658,29 +659,29 @@ async function handleChannelPost(post, env) { // ─── Render pipeline ────────────────────────────────────────────────────────── function renderUserText(rawText, entities, config) { + config = config || DEFAULT_CONFIG; let text = entitiesToMarkdown(rawText, entities); - if (config.media) text = autoGroupMedia(text); - text = applyCustomSyntax(text, config); + if (config.media) text = autoGroupMedia(text); // pure media URLs -> slideshow + text = applyCustomSyntax(text, config); // headings, quote, time, map, ///, ???, tags, 🙈 text = text.trim(); - text = preserveManualSpacing(text); + text = preserveManualSpacing(text); // keep manual spacing (after trim!) return text; } function applyCustomSyntax(text, config) { - // Mask code blocks to protect them from regex replacements + // 1) Mask code (fenced ```...```, ///...///, and inline `...`) so replacements skip them. const codeBlocks = []; - text = text.replace(/(```[\s\S]*?```|\/\/\/[\s\S]*?\/\/\/)/g, match => { + text = text.replace(/(```[\s\S]*?```|\/\/\/[\s\S]*?\/\/\/|`[^`\n]+`)/g, match => { codeBlocks.push(match); return `\u0000CB${codeBlocks.length - 1}\u0000`; }); - // Disable standard Markdown headings (inserts Zero-Width Space so parser ignores them) - text = text.replace(/(^|\n)([ \t>]*)(#{1,6})([ \t]+)/g, "$1$2$3\u200B$4"); - - // Enable custom `#_ ` heading syntax - text = text.replace(/(^|\n)([ \t>]*)(#{1,6})_([ \t]+)/g, "$1$2$3$4"); + // 2) FIX A — hashtags & headings. + // Only the explicit `#_ ` syntax becomes a real heading; every other + // line-start `#` is escaped so Telegram never renders it as a heading/bold. + text = neutralizeHashesAndHeadings(text); - // Restore code blocks + // 3) Restore code blocks. text = text.replace(/\u0000CB(\d+)\u0000/g, (_, i) => codeBlocks[Number(i)]); if (config.quote) { @@ -715,6 +716,23 @@ function applyCustomSyntax(text, config) { return text; } +// FIX A: neutralize accidental headings / hashtag-bold. +// Runs while code is masked out by the caller. +function neutralizeHashesAndHeadings(text) { + // a) Custom heading `#_ ` / `##_ ` ... -> unique sentinel (protected from step b). + text = text.replace(/(^|\n)([ \t>]*)(#{1,6})_[ \t]+/g, + (_, nl, pre, hashes) => `${nl}${pre}\u0000H${hashes.length}\u0000`); + // b) Escape any remaining line-start `#` run so it renders literally. + // Telegram still auto-detects `#hashtags` as clickable entities. + text = text.replace(/(^|\n)([ \t>]*)(#{1,6})/g, + (_, nl, pre, hashes) => `${nl}${pre}${hashes.replace(/#/g, "\\#")}`); + // c) Restore custom headings as real ATX headings. + text = text.replace(/\u0000H([1-6])\u0000/g, (_, n) => "#".repeat(Number(n)) + " "); + return text; +} + +// """ quote +// — author """ -> function applyPullQuote(text) { return text.replace(/"""([\s\S]+?)"""/g, (_, body) => { const lines = body.trim().split("\n"); @@ -727,33 +745,60 @@ function applyPullQuote(text) { }); } +// A message that is ONLY media URLs -> slideshow. function autoGroupMedia(text) { if (!isAllMediaUrls(text)) return text; const media = text.split("\n").map(l => l.trim()).filter(Boolean).map(u => `![](${u})`).join("\n"); return `\n${media}\n`; } -// ─── Preserve manual spacing ────────────────────────────────────────────────── +// ─── FIX B: Preserve manual spacing ─────────────────────────────────────────── function preserveManualSpacing(text) { const NBSP = "\u00A0"; + + // Protect fenced code blocks entirely. const fences = []; text = text.replace(/```[\s\S]*?```/g, m => { fences.push(m); return `\u0000F${fences.length - 1}\u0000`; }); - const out = text.split("\n").map(line => { - if (/\u0000F\d+\u0000/.test(line)) return line; - if (/^\s*\|.*\|\s*$/.test(line)) return line; + + const lines = text.split("\n"); + const out = lines.map(line => { + if (/\u0000F\d+\u0000/.test(line)) return line; // fenced code placeholder + if (/^\s*\|.*\|\s*$/.test(line)) return line; // table row -> keep alignment + + // Protect inline code so we don't touch its spaces. const codes = []; let l = line.replace(/`[^`\n]*`/g, m => { codes.push(m); return `\u0000C${codes.length - 1}\u0000`; }); + + // Preserve LEADING indentation — but only for plain text lines. + // Skip block starters (lists/quotes/headings/tables) so their nesting/markup stays valid. + const rest = l.replace(/^[ \t]+/, ""); + const isBlockStarter = /^(?:[-*+]\s|\d+[.)]\s|>|#|\\#|\||<)/.test(rest); + if (!isBlockStarter) { + l = l.replace(/^[ \t]+/, m => NBSP.repeat(m.length)); + } + + // Preserve runs of 2+ internal spaces. l = l.replace(/ {2,}/g, m => NBSP.repeat(m.length)); + + // Restore inline code. l = l.replace(/\u0000C(\d+)\u0000/g, (_, i) => codes[Number(i)]); return l; - }).join("\n"); - return out.replace(/\u0000F(\d+)\u0000/g, (_, i) => fences[Number(i)]); + }); + + let joined = out.join("\n"); + + // Preserve multiple consecutive blank lines (markdown normally collapses them). + // 3+ newlines = 2+ blank lines -> keep the extra gaps with NBSP-only lines. + joined = joined.replace(/\n{3,}/g, m => "\n\n" + (NBSP + "\n").repeat(m.length - 2)); + + // Restore fenced code blocks. + return joined.replace(/\u0000F(\d+)\u0000/g, (_, i) => fences[Number(i)]); } // ─── Format Detectors & Helpers ─────────────────────────────────────────────── @@ -771,7 +816,7 @@ function hasRealFormatting(t) { /\[(?:نقشه|map)\s*:/.test(t) || /\[(?:اسلایدشو|slideshow|کلاژ|collage|کشویی|expand)\]/.test(t) || /[🙈🔽]/.test(t) || - /(^|\n)[ \t>]*#{1,6}_\s+\S/.test(t) || + /(^|\n)[ \t>]*#{1,6}_\s+\S/.test(t) || // custom `#_ ` heading only /(^|\n)\s*[-*+]\s*\[[ xX]\]\s+\S/.test(t) || /(^|\n)\s*[-*+]\s+\S/.test(t) || /(^|\n)\s*\d+\.\s+\S/.test(t) || @@ -848,7 +893,7 @@ function wrapEntity(e, content) { case "blockquote": case "expandable_blockquote": return content.split("\n").map(l => `> ${l}`).join("\n"); - default: return content; + default: return content; // hashtag / mention / url / etc. -> left as-is } } @@ -856,7 +901,14 @@ function wrapEntity(e, content) { async function sendPlain(chatId, text, replyMarkup) { const body = { chat_id: chatId, text, parse_mode: "Markdown" }; if (replyMarkup) body.reply_markup = replyMarkup; - await callApi("sendMessage", body); + const res = await callApi("sendMessage", body); + if (!res?.ok) { + // Fallback: send without parse_mode if Markdown parsing failed. + const fb = { chat_id: chatId, text }; + if (replyMarkup) fb.reply_markup = replyMarkup; + return await callApi("sendMessage", fb); + } + return res; } async function sendRichMarkdown(chatId, markdown, replyMarkup) { const body = { chat_id: chatId, rich_message: { markdown, is_rtl: isRtl(markdown) } }; @@ -865,15 +917,32 @@ async function sendRichMarkdown(chatId, markdown, replyMarkup) { if (!res?.ok) { const fb = { chat_id: chatId, text: markdown }; if (replyMarkup) fb.reply_markup = replyMarkup; - const fbRes = await callApi("sendMessage", fb); - return fbRes; + return await callApi("sendMessage", fb); + } + return res; +} +async function sendRichHtml(chatId, html, replyMarkup) { + const body = { chat_id: chatId, rich_message: { html, is_rtl: isRtl(html) } }; + if (replyMarkup) body.reply_markup = replyMarkup; + const res = await callApi("sendRichMessage", body); + if (!res?.ok) { + const fb = { chat_id: chatId, text: html, parse_mode: "HTML" }; + if (replyMarkup) fb.reply_markup = replyMarkup; + return await callApi("sendMessage", fb); } return res; } async function editRichMarkdown(chatId, messageId, markdown, replyMarkup) { const body = { chat_id: chatId, message_id: messageId, rich_message: { markdown, is_rtl: isRtl(markdown) } }; if (replyMarkup) body.reply_markup = replyMarkup; - await callApi("editMessageText", body); + const res = await callApi("editMessageText", body); + if (!res?.ok) { + // Fallback to plain edit if rich edit fails. + const fb = { chat_id: chatId, message_id: messageId, text: markdown }; + if (replyMarkup) fb.reply_markup = replyMarkup; + await callApi("editMessageText", fb); + } + return res; } async function callApi(method, body) { const res = await fetch(`${TELEGRAM_API}/${method}`, { @@ -888,13 +957,12 @@ async function callApi(method, body) { } return json; } - // ═══════════════════════════════════════════════════════════════════════════════ // CONTENT // ═══════════════════════════════════════════════════════════════════════════════ const WELCOME = { fa: [ - "# 🤖 Rich Markdown Bot", + "#_ 🤖 Rich Markdown Bot", "", "هر متن **Markdown** بفرستید، به‌صورت Rich Message رندر می‌شود. می‌توانید وسط همان متن، تگ‌های HTML تلگرام (مثل `` یا `
`) یا فرمول (`$x^2$`) هم بگذارید؛ بقیه‌ی فرمت‌های دستی‌تان سالم می‌ماند.", "همچنین می‌توانید بات را به کانال خود اضافه کنید تا پست‌ها خودکار قالب‌بندی شوند.", @@ -903,7 +971,7 @@ const WELCOME = { "از دکمه‌های زیر برای دیدن راهنما و دمو استفاده کنید 👇", ].join("\n"), en: [ - "# 🤖 Rich Markdown Bot", + "#_ 🤖 Rich Markdown Bot", "", "Send any **Markdown** text and it is echoed back as a rendered Rich Message. You can mix in Telegram HTML tags (e.g. ``, `
`) or formulas (`$x^2$`) right inside it — the rest of your manual formatting stays intact.", "You can also add this bot to your channel to auto-format posts.", @@ -912,21 +980,27 @@ const WELCOME = { "Use the buttons below to explore 👇", ].join("\n"), }; + const ABOUT = { fa: [ - "# ℹ️ درباره بات", + "#_ ℹ️ درباره بات", "", "این بات پیام‌های شما را با فرمت مدرن **Rich Message** تلگرام رندر می‌کند. حالت پایه «مارک‌داون» است و چون مارک‌داونِ تلگرام می‌تواند HTML را هم در خودش جا بدهد، برای استفاده از یک تگ یا فرمول، دیگر لازم نیست کل متن را HTML کنید؛ همین یعنی فاصله‌ها و بولدهای دستی‌تان هیچ‌وقت نمی‌پرند.", + "", + "نکته: هشتگ‌ها (مثل `#تخفیف`) دیگر بولد نمی‌شوند و مثل هشتگ عادی می‌مانند.", ].join("\n"), en: [ - "# ℹ️ About", + "#_ ℹ️ About", "", "This bot renders your messages using Telegram's modern **Rich Message** format. The base mode is Markdown, and since Telegram Markdown can embed HTML, you can drop in a single tag or formula without forcing the whole message into HTML — so your manual spacing and bold never get wiped.", + "", + "Note: hashtags (e.g. `#sale`) are no longer turned into bold headings — they stay normal hashtags.", ].join("\n"), }; + const HELP_CHANNEL = { fa: [ - "### 🛠 آموزش اتصال کانال:", + "###_ 🛠 آموزش اتصال کانال:", "1. ربات را به کانال خود به‌عنوان **ادمین** اضافه کنید.", "2. دسترسی **Edit Messages** (ویرایش پیام‌ها) را به آن بدهید.", "3. یک پیام از کانال خود به همین‌جا (داخل بات) **Forward** کنید.", @@ -943,7 +1017,7 @@ const HELP_CHANNEL = { "برای دیدن تگ‌های مخصوص کانال و میان‌بُرهای فارسی، دکمه‌ی زیر را بزنید 👇", ].join("\n"), en: [ - "### 🛠 How to setup:", + "###_ 🛠 How to setup:", "1. Add the bot to your channel as an **administrator**.", "2. Grant it the **Edit Messages** permission.", "3. **Forward** any message from your channel to this bot.", @@ -960,14 +1034,15 @@ const HELP_CHANNEL = { "Tap below to see channel-only tags and shortcuts 👇", ].join("\n"), }; + const HELP_CHANNEL_TAGS = { fa: [ - "# 🏷 تگ‌های مخصوص کانال و میان‌بُرها", + "#_ 🏷 تگ‌های مخصوص کانال و میان‌بُرها", "", "چون حالت پایه مارک‌داون است، می‌توانی هر تگ یا فرمول را وسط متن عادی بگذاری و **بقیه‌ی متن دست‌نخورده** می‌ماند.", "", "---", - "## ✍️ میان‌بُرهای ساده (بدون تگ خام)", + "##_ ✍️ میان‌بُرهای ساده (بدون تگ خام)", "- `///کد///` → بلوک کد", "- `???متن???` → بلوک **کشویی/بازشو**", "- `\"\"\"نقل‌قول — نویسنده\"\"\"` → **نقل‌قول وسط‌چین** با امضا", @@ -978,7 +1053,12 @@ const HELP_CHANNEL_TAGS = { "- فقط چند **لینک مدیا** پشت‌سرهم بفرست → خودکار **اسلایدشو** می‌شود", "", "---", - "## ✅ روی پیام متنی کانال کار می‌کند", + "##_ #️⃣ هدینگ و هشتگ", + "- برای **هدینگ** از `#_ عنوان` استفاده کن (`##_` و `###_` هم داریم).", + "- هشتگ عادی مثل `#تخفیف` دیگر بولد/هدینگ نمی‌شود و سالم می‌ماند.", + "", + "---", + "##_ ✅ روی پیام متنی کانال کار می‌کند", "- هدینگ (`#_ عنوان`)، نقل‌قول (`> ...`)", "- لیست و تسک (`- [ ]` و `- [x]`)", "- جدول، بلوک کد، و فرمول (`$...$` و `$$...$$`)", @@ -986,7 +1066,7 @@ const HELP_CHANNEL_TAGS = { "- همه‌ی استایل‌های inline (بولد، ایتالیک، خط‌خورده، کد، لینک)", "", "---", - "## ⚠️ روی کپشن مدیا کار نمی‌کند", + "##_ ⚠️ روی کپشن مدیا کار نمی‌کند", "فقط استایل‌های inline رندر می‌شوند:", "**بولد** · *ایتالیک* · ~~خط‌خورده~~ · `کد` · [لینک](https://t.me/) · ||اسپویلر||", "هدینگ، لیست، جدول، بلوک کد و فرمول روی کپشن نمایش داده **نمی‌شوند**.", @@ -994,12 +1074,12 @@ const HELP_CHANNEL_TAGS = { "> 💡 اگر به todo / جدول / هدینگ نیاز داری، پست را به‌صورت **متنی** (بدون عکس) بفرست.", ].join("\n"), en: [ - "# 🏷 Channel-only Tags & Shortcuts", + "#_ 🏷 Channel-only Tags & Shortcuts", "", "Since the base mode is Markdown, you can drop any tag or formula into normal text and **the rest stays untouched**.", "", "---", - "## ✍️ Simple shortcuts (no raw tags)", + "##_ ✍️ Simple shortcuts (no raw tags)", "- `///code///` → code block", "- `???text???` → **expandable/collapsible** block", "- `\"\"\"quote — author\"\"\"` → **centered pull-quote** with credit", @@ -1010,7 +1090,12 @@ const HELP_CHANNEL_TAGS = { "- send just a few **media URLs** in a row → auto **slideshow**", "", "---", - "## ✅ Works on channel text posts", + "##_ #️⃣ Headings vs hashtags", + "- For a **heading** use `#_ Title` (`##_` and `###_` too).", + "- A normal hashtag like `#sale` is no longer bolded/turned into a heading — it stays intact.", + "", + "---", + "##_ ✅ Works on channel text posts", "- Headings (`#_ Title`), quotes (`> ...`)", "- Lists & tasks (`- [ ]`, `- [x]`)", "- Tables, code blocks, formulas (`$...$`, `$$...$$`)", @@ -1018,7 +1103,7 @@ const HELP_CHANNEL_TAGS = { "- All inline styles (bold, italic, strike, code, links)", "", "---", - "## ⚠️ Does NOT work on media captions", + "##_ ⚠️ Does NOT work on media captions", "Only inline styles render:", "**bold** · *italic* · ~~strike~~ · `code` · [link](https://t.me/) · ||spoiler||", "Headings, lists, tables, code blocks and formulas are **not** shown on captions.", @@ -1026,31 +1111,34 @@ const HELP_CHANNEL_TAGS = { "> 💡 If you need todos / tables / headings, send the post as **text** (no photo).", ].join("\n"), }; + const HELP_MD = { fa: [ - "# 📖 راهنمای Markdown", + "#_ 📖 راهنمای Markdown", "", - "متن Markdown بفرست، رندرشده برمی‌گردد. کادر خاکستری = چیزی که تایپ می‌کنی ↓ نتیجه بعدش می‌آید.", + "متن Markdown بفرست، رندرشده برمی‌گردد.", "", "---", - "## استایل متن", - "```", + "##_ استایل متن", + "///", "**بولد** *ایتالیک* ~~خط‌خورده~~", "`کد` ==های‌لایت== آندرلاین اسپویلر", - "```", + "///", + "↓", "**بولد** *ایتالیک* ~~خط‌خورده~~ `کد` ==های‌لایت== آندرلاین اسپویلر", "", "---", - "## هدینگ", - "```", + "##_ هدینگ", + "///", "#_ هدینگ ۱", "##_ هدینگ ۲", "###_ هدینگ ۳", - "```", + "///", + "💡 هشتگ عادی مثل `#خبر` بولد نمی‌شود؛ فقط `#_` هدینگ می‌سازد.", "", "---", - "## لیست‌ها", - "```", + "##_ لیست‌ها", + "///", "- شیر", "- تخم‌مرغ", "- [ ] کار انجام‌نشده", @@ -1058,49 +1146,49 @@ const HELP_MD = { "", "1. بیدار شو", "2. منتشر کن", - "```", + "///", "", "---", - "## نقل‌قول و فرمول", - "```", + "##_ نقل‌قول و فرمول", + "///", "> نقل‌قول با **بولد**", "فرمول داخل‌خطی $E = mc^2$", "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", - "```", + "///", "", "---", - "## بلوک کد", - "```", + "##_ بلوک کد", "///", "print(\"hello\")", "///", - "```", "💡 می‌توانی هر تگ HTML تلگرام را هم وسط همین مارک‌داون بگذاری؛ لازم نیست کل پیام HTML شود.", ].join("\n"), en: [ - "# 📖 Markdown Guide", + "#_ 📖 Markdown Guide", "", - "Send Markdown text and get it echoed back rendered. Grey box = what you type ↓ result comes right after.", + "Send Markdown text and get it echoed back rendered.", "", "---", - "## Text styles", - "```", + "##_ Text styles", + "///", "**bold** *italic* ~~strike~~", "`code` ==marked== underline spoiler", - "```", + "///", + "↓", "**bold** *italic* ~~strike~~ `code` ==marked== underline spoiler", "", "---", - "## Headings", - "```", + "##_ Headings", + "///", "#_ Heading 1", "##_ Heading 2", "###_ Heading 3", - "```", + "///", + "💡 A normal hashtag like `#news` is not bolded; only `#_` makes a heading.", "", "---", - "## Lists", - "```", + "##_ Lists", + "///", "- milk", "- eggs", "- [ ] todo", @@ -1108,260 +1196,235 @@ const HELP_MD = { "", "1. wake up", "2. ship it", - "```", + "///", "", "---", - "## Quotes & formulas", - "```", + "##_ Quotes & formulas", + "///", "> Quote with **bold**", "Inline formula $E = mc^2$", "$$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$", - "```", + "///", "", "---", - "## Code block", - "```", + "##_ Code block", "///", "print(\"hello\")", "///", - "```", "💡 You can also drop any Telegram HTML tag right inside this Markdown — no need to make the whole message HTML.", ].join("\n"), }; + const HELP_HTML = { fa: [ - "# 🌐 راهنمای HTML", + "#_ 🌐 راهنمای HTML", "", "تگ‌های HTML تلگرام را می‌توانی **هرجای متن** بگذاری (لازم نیست پیام با `<` شروع شود).", "", "---", - "## استایل متن", - "```", - "بولد ایتالیک آندرلاین", - "خط‌خورده کد های‌لایت", - "اسپویلر", - "```", + "##_ استایل متن", "بولد ایتالیک آندرلاین خط‌خورده کد های‌لایت اسپویلر", "", "---", - "## بلوک کشویی و نقل‌قول", - "```", + "##_ بلوک کشویی و نقل‌قول", "
عنوانمحتوای بازشو
", "
نقل‌قول
", "", - "```", "", "---", - "## لیست و جدول", - "```", + "##_ لیست و جدول", "
  • شیر
  • تخم‌مرغ
", "
  1. اول
  2. دوم
", "
سرستون
۱۲
", - "```", "", "---", - "## فرمول", - "```", + "##_ فرمول", "x^2 + y^2", "E = mc^2", - "```", ].join("\n"), en: [ - "# 🌐 HTML Guide", + "#_ 🌐 HTML Guide", "", "You can put Telegram HTML tags **anywhere** in your text (the message doesn't need to start with `<`).", "", "---", - "## Text styles", - "```", - "bold italic underline", - "strike code marked", - "spoiler", - "```", + "##_ Text styles", "bold italic underline strike code marked spoiler", "", "---", - "## Expandable & quotes", - "```", + "##_ Expandable & quotes", "
TitleCollapsible content
", "
Quote
", "", - "```", "", "---", - "## Lists & tables", - "```", + "##_ Lists & tables", "
  • milk
  • eggs
", "
  1. first
  2. second
", "
HeadCol
12
", - "```", "", "---", - "## Formulas", - "```", + "##_ Formulas", "x^2 + y^2", "E = mc^2", - "```", ].join("\n"), }; + const HELP_MEDIA = { fa: [ - "# 🖼 راهنمای مدیا", + "#_ 🖼 راهنمای مدیا", "", "می‌توانی عکس/ویدیو/فایل را داخل پیام Rich جاسازی کنی یا چند مدیا را گروهی نمایش بدهی.", "", "---", - "## عکس و ویدیو تکی", - "```", + "##_ عکس و ویدیو تکی", + "///", "![کپشن](https://example.com/photo.jpg)", - "", - "```", + "", + "///", "💡 لینک باید مستقیم و http/https باشد.", "", "---", - "## اسلایدشو و کلاژ", - "```", + "##_ اسلایدشو و کلاژ", + "///", "[اسلایدشو]", "![](https://example.com/1.jpg)", "![](https://example.com/2.jpg)", "[/اسلایدشو]", - "```", + "///", "یا با تگ خام:", - "```", + "///", "", - "![]([https://example.com/1.jpg](https://example.com/1.jpg))", - "![]([https://example.com/2.jpg](https://example.com/2.jpg))", + "![](https://example.com/1.jpg)", + "![](https://example.com/2.jpg)", "", - "```", + "///", "💡 میان‌بُر: اگر فقط چند **لینک مدیا** را پشت‌سرهم (هر خط یکی) بفرستی، خودکار به اسلایدشو تبدیل می‌شود.", "", "---", - "## نقشه", - "```", - "[نقشه: 35.6892, 51.3890]", - "```", + "##_ نقشه", + "`[نقشه: 35.6892, 51.3890]`", ].join("\n"), en: [ - "# 🖼 Media Guide", + "#_ 🖼 Media Guide", "", "You can embed photos/videos/files in a Rich message or display several media as a group.", "", "---", - "## Single photo & video", - "```", + "##_ Single photo & video", + "///", "![caption](https://example.com/photo.jpg)", - "", - "```", + "", + "///", "💡 The link must be a direct http/https URL.", "", "---", - "## Slideshow & collage", - "```", + "##_ Slideshow & collage", + "///", "[slideshow]", "![](https://example.com/1.jpg)", "![](https://example.com/2.jpg)", "[/slideshow]", - "```", + "///", "Or with the raw tag:", - "```", + "///", "", - "![]([https://example.com/1.jpg](https://example.com/1.jpg))", - "![]([https://example.com/2.jpg](https://example.com/2.jpg))", + "![](https://example.com/1.jpg)", + "![](https://example.com/2.jpg)", "", - "```", + "///", "💡 Shortcut: send just a few **media URLs** in a row (one per line) and they auto-group into a slideshow.", "", "---", - "## Map", - "```", - "[map: 35.6892, 51.3890]", - "```", + "##_ Map", + "`[map: 35.6892, 51.3890]`", ].join("\n"), }; + const DEMO = { fa: [ - "# 🎨 دمو کامل", + "#_ 🎨 دمو کامل", "", "این یک نمونه از همه‌ی امکانات با هم است:", "", - "## ۱) استایل‌ها", + "##_ ۱) استایل‌ها", "**بولد** · *ایتالیک* · ~~خط‌خورده~~ · `کد` · آندرلاین · اسپویلر", "", - "## ۲) لیست و تسک", + "##_ ۲) لیست و تسک", "- مورد اول", "- مورد دوم", "- [x] انجام شد", "- [ ] در انتظار", "", - "## ۳) نقل‌قول", + "##_ ۳) نقل‌قول", "> دانش، قدرت است. **همیشه**.", "", - "## ۴) نقل‌قول وسط‌چین", + "##_ ۴) نقل‌قول وسط‌چین", "", "", - "## ۵) فرمول", + "##_ ۵) فرمول", "داخل‌خطی $a^2 + b^2 = c^2$ و بلوکی:", "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$", "", - "## ۶) بلوک کد", - "```python", + "##_ ۶) بلوک کد", + "///", "def hi():", " print(\"سلام دنیا\")", - "```", + "///", "", - "## ۷) جدول", + "##_ ۷) جدول", "| نام | امتیاز |", "|-----|--------|", "| علی | ۹۵ |", "| رضا | ۸۸ |", "", - "## ۸) بلوک کشویی", + "##_ ۸) بلوک کشویی", "
برای دیدن بزنمحتوای مخفی اینجاست 🎉
", "", - "## ۹) زمان زنده و نقشه", + "##_ ۹) زمان زنده و نقشه", "[زمان: 2026-07-01 20:00]", "[نقشه: 35.6892, 51.3890]", ].join("\n"), en: [ - "# 🎨 Full Demo", + "#_ 🎨 Full Demo", "", "A sample showing every feature together:", "", - "## 1) Styles", + "##_ 1) Styles", "**bold** · *italic* · ~~strike~~ · `code` · underline · spoiler", "", - "## 2) Lists & tasks", + "##_ 2) Lists & tasks", "- first item", "- second item", "- [x] done", "- [ ] pending", "", - "## 3) Quote", + "##_ 3) Quote", "> Knowledge is power. **Always**.", "", - "## 4) Pull-quote", + "##_ 4) Pull-quote", "", "", - "## 5) Formulas", + "##_ 5) Formulas", "Inline $a^2 + b^2 = c^2$ and block:", "$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$", "", - "## 6) Code block", - "```python", + "##_ 6) Code block", + "///", "def hi():", " print(\"hello world\")", - "```", + "///", "", - "## 7) Table", + "##_ 7) Table", "| Name | Score |", "|------|-------|", "| Ali | 95 |", "| Reza | 88 |", "", - "## 8) Expandable", + "##_ 8) Expandable", "
Tap to revealHidden content here 🎉
", "", - "## 9) Live time & map", + "##_ 9) Live time & map", "[time: 2026-07-01 20:00]", "[map: 35.6892, 51.3890]", ].join("\n"),