// tg-handlers.js - All Telegram /command handlers const config = require("./config"); const { getLanIP, makeSlug, sessionSummary } = require("./helpers"); const { getSession, clearSession, sessions, saveViaAPI } = require("./sessions"); const { getWriteStatus, ensureWriteReady } = require("./write-server"); const { sendMessage } = require("./tg-core"); const CMD_HANDLERS = {}; // ---------- System ---------- CMD_HANDLERS.start = async function (chatId) { const ip = getLanIP(); const ready = await ensureWriteReady(chatId, sendMessage); if (ready) { return sendMessage(chatId, "\\u2705 Write \\u5DF2\\u542F\\u52A8\\n\\n" + "\\uD83D\\uDCF1 \\u624B\\u673A: http://" + ip + ":8016/\\n" + "\\uD83D\\uDCDD \\u6587\\u7AE0: http://" + ip + ":1313/posts/\\n" + "\\uD83D\\uDCBB \\u7535\\u8111: http://localhost:8016/\\n\\n" + "/help \\u67E5\\u770B\\u5168\\u90E8\\u547D\\u4EE4" ); } return sendMessage(chatId, "\\u274C \\u542F\\u52A8\\u8D85\\u65F6\\uFF0C\\u67E5\\u770B\\u65E5\\u5FD7 http://127.0.0.1:" + config.CONTROL_PORT + "/log"); }; CMD_HANDLERS.stop = async function (chatId) { const { stopWriteServer } = require("./write-server"); if (stopWriteServer()) { return sendMessage(chatId, "\\u26A1 Write stopped."); } return sendMessage(chatId, "\\u23F8 Write is not running."); }; CMD_HANDLERS.status = async function (chatId) { const s = getWriteStatus(); const status = s.running ? "\\u2705 Running" : s.starting ? "\\u23F3 Starting..." : "\\u23F8 Stopped"; const lines = ["Status: " + status]; if (s.running) { const ip = getLanIP(); lines.push("Local: http://localhost:" + config.WRITE_PORT); lines.push("Phone: http://" + ip + ":" + config.WRITE_PORT); } return sendMessage(chatId, lines.join("\\n")); }; CMD_HANDLERS.ip = async function (chatId) { const ip = getLanIP(); return sendMessage(chatId, "Phone: http://" + ip + ":" + config.WRITE_PORT + " (" + config.PREFER_IFACE + ")"); }; CMD_HANDLERS.help = async function (chatId) { if (sessions.has(chatId)) { return sendMessage(chatId, "\\u270D \\u6B63\\u5728\\u5199\\u4F5C\\u4E2D\\n\\n" + "\\u26A1 /publish \\u53D1\\u5E03 \\uD83D\\uDCC4 /draft \\u8349\\u7A3F \\uD83D\\uDD0D /preview \\u9884\\u89C8\\n" + "\\u270F /title /categories /tags /author\\n" + "\\u274C /cancel \\u4E22\\u5F03" ); } return sendMessage(chatId, "\\uD83E\\uDD16 WriteBot \\u547D\\u4EE4\\n\\n" + "\\uD83D\\uDCDD \\u5199\\u4F5C:\\n" + " /new \\u6807\\u9898 - \\u5F00\\u59CB\\u5199\\u4E00\\u7BC7\\n" + " /edit slug - \\u7F16\\u8F91\\u5DF2\\u6709\\u6587\\u7AE0\\n" + " /delete slug - \\u5220\\u9664\\u6587\\u7AE0\\n" + " /list - \\u6587\\u7AE0\\u5217\\u8868\\n\\n" + "\\uD83D\\uDD17 \\u53CB\\u94FE:\\n" + " /links - \\u67E5\\u770B\\u53CB\\u94FE\\n" + " /link_add url name - \\u6DFB\\u52A0\\u53CB\\u94FE\\n" + " /link_del url - \\u5220\\u9664\\u53CB\\u94FE\\n" + " /link_toggle url - \\u663E\\u793A/\\u9690\\u85CF\\n\\n" + "\\uD83D\\uDCE1 \\u8BA2\\u9605:\\n" + " /feeds - \\u67E5\\u770B\\u8BA2\\u9605\\u6E90\\n" + " /feed_add url - \\u6DFB\\u52A0\\u8BA2\\u9605\\u6E90\\n" + " /feed_del url - \\u5220\\u9664\\u8BA2\\u9605\\u6E90\\n\\n" + "\\uD83D\\uDCCA /stats \\u7F51\\u7AD9\\u6570\\u636E" ); }; // ---------- Articles ---------- CMD_HANDLERS.new = async function (chatId, title) { if (!title) return sendMessage(chatId, "\\u683C\\u5F0F\\uFF1A\\u5199\\u4E00\\u7BC7 \\u6807\\u9898"); const s = getSession(chatId); s.title = title; s.slug = makeSlug(title); s.content = ""; s.categories = []; s.tags = []; s.draft = true; s.author = "qunlin"; return sendMessage(chatId, "\\uD83D\\uDCDD \\u5F00\\u59CB\\u5199\\uFF1A" + title + "\\n\\n" + "\\u76F4\\u63A5\\u53D1\\u6D88\\u606F\\u5199\\u4F5C\\uFF0C\\u5B8C\\u6210\\u540E /publish\\n" + "\\u968F\\u65F6 /preview \\u67E5\\u770B\\uFF0C/help \\u5168\\u90E8\\u547D\\u4EE4" ); }; CMD_HANDLERS.edit = async function (chatId, slug) { if (!slug) return sendMessage(chatId, "\\u683C\\u5F0F\\uFF1A/edit slug\\n\\u5148\\u7528 /list \\u67E5\\u770B\\u6587\\u7AE0 slug\\u3002"); try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts/" + slug); if (!res.ok) throw new Error("\\u672A\\u627E\\u5230\\u8BE5\\u6587\\u7AE0"); const post = await res.json(); const s = getSession(chatId); s.title = post.frontMatter.title; s.slug = post.frontMatter.slug; s.content = post.content || ""; s.categories = post.frontMatter.categories || []; s.tags = post.frontMatter.tags || []; s.author = post.frontMatter.author || "qunlin"; s.draft = post.frontMatter.draft; return sendMessage(chatId, "\\u270F \\u7F16\\u8F91\\uFF1A" + s.title + "\\n\\n" + sessionSummary(s) + "\\n\\n\\u76F4\\u63A5\\u53D1\\u6D88\\u606F\\u8FFD\\u52A0\\u5185\\u5BB9\\uFF0C\\u5199\\u5B8C\\u53D1\\u300C\\u53D1\\u5E03\\u300D\\u3002"); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; CMD_HANDLERS.delete = async function (chatId, slug) { if (!slug) return sendMessage(chatId, "Usage: /delete \\nFind slugs with /list\\n\\nArticles are moved to recycle bin (30-day retention)."); try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts/" + slug, { method: "DELETE" }); if (!res.ok) throw new Error((await res.json()).error || "Delete failed"); return sendMessage(chatId, "\\u2702 Moved to recycle bin: " + slug + "\\nRecoverable within 30 days."); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; CMD_HANDLERS.view = async function (chatId, slug) { if (!slug) return sendMessage(chatId, "Usage: /view \\nFind slugs with /list"); const ip = getLanIP(); const url = "http://" + ip + ":1313/" + slug + ".html"; return sendMessage(chatId, "\\uD83D\\uDCC9 Preview\\n\\n" + url + "\\n\\n\\uD83D\\uDCBB http://localhost:1313/" + slug + ".html"); }; CMD_HANDLERS.title = async function (chatId, newTitle) { const s = getSession(chatId); if (!newTitle) return sendMessage(chatId, "\\u683C\\u5F0F\\uFF1A\\u6807\\u9898 \\u65B0\\u6807\\u9898"); s.title = newTitle; s.slug = makeSlug(newTitle); return sendMessage(chatId, "\\u270F \\u6807\\u9898\\u5DF2\\u66F4\\u65B0\\uFF1A" + newTitle + ""); }; CMD_HANDLERS.categories = async function (chatId, cats) { const s = getSession(chatId); s.categories = cats ? cats.split(/[,;\"\\",\\"\\uFF0C\\u3001]+/).map(function(c){return c.trim();}).filter(Boolean) : []; return sendMessage(chatId, "\\uD83D\\uDCC2 \\u5206\\u7C7B\\uFF1A" + (s.categories.join("\\u3001") || "\\u5DF2\\u6E05\\u7A7A")); }; CMD_HANDLERS.tags = async function (chatId, tags) { const s = getSession(chatId); s.tags = tags ? tags.split(/[,;\"\\",\\"\\uFF0C\\u3001]+/).map(function(t){return t.trim();}).filter(Boolean) : []; return sendMessage(chatId, "\\uD83C\\uDFF7 \\u6807\\u7B7E\\uFF1A" + (s.tags.join("\\u3001") || "\\u5DF2\\u6E05\\u7A7A")); }; CMD_HANDLERS.author = async function (chatId, author) { const s = getSession(chatId); s.author = author || "qunlin"; return sendMessage(chatId, "\\u270D \\u4F5C\\u8005\\uFF1A" + s.author); }; CMD_HANDLERS.preview = async function (chatId) { const s = sessions.get(chatId); if (!s || !s.title) return sendMessage(chatId, "\\u8FD8\\u6CA1\\u5F00\\u59CB\\u5199\\u3002\\u53D1\\u300C\\u5199\\u4E00\\u7BC7\\u300D\\u5F00\\u59CB\\u3002"); if (!s.content) return sendMessage(chatId, "\\u6B63\\u6587\\u4E3A\\u7A7A\\u3002"); const maxLen = 3800; const preview = s.content.slice(0, maxLen); return sendMessage(chatId, "\\uD83D\\uDCDD " + s.title + "\\n\\n" + preview + (s.content.length > maxLen ? "\\n\\n... \\u524D " + maxLen + " \\u5B57" : "")); }; CMD_HANDLERS.draft = async function (chatId) { const s = sessions.get(chatId); if (!s || !s.title) return sendMessage(chatId, "\\u8FD8\\u6CA1\\u5F00\\u59CB\\u5199\\u3002\\u53D1\\u300C\\u5199\\u4E00\\u7BC7\\u300D\\u5F00\\u59CB\\u3002"); if (!s.content) return sendMessage(chatId, "\\u6B63\\u6587\\u4E3A\\u7A7A\\uFF0C\\u5148\\u5199\\u70B9\\u5185\\u5BB9\\u518D\\u4FDD\\u5B58\\u3002"); try { await saveViaAPI(s, true); clearSession(chatId); return sendMessage(chatId, "\\uD83D\\uDCC4 \\u8349\\u7A3F\\u5DF2\\u4FDD\\u5B58\\uFF1A" + s.title + ""); } catch (e) { return sendMessage(chatId, "\\u274C \\u4FDD\\u5B58\\u5931\\u8D25\\uFF1A" + e.message + "\\n\\n\\u8BF7\\u5148 /start \\u542F\\u52A8\\u540E\\u53F0\\u670D\\u52A1\\u3002"); } }; CMD_HANDLERS.publish = async function (chatId) { const s = sessions.get(chatId); if (!s || !s.title) return sendMessage(chatId, "\\u8FD8\\u6CA1\\u5F00\\u59CB\\u5199\\u3002\\u53D1\\u300C\\u5199\\u4E00\\u7BC7\\u300D\\u5F00\\u59CB\\u3002"); if (!s.content) return sendMessage(chatId, "\\u6B63\\u6587\\u4E3A\\u7A7A\\uFF0C\\u5148\\u5199\\u70B9\\u5185\\u5BB9\\u518D\\u53D1\\u5E03\\u3002"); try { await saveViaAPI(s, false); const slug = s.slug; clearSession(chatId); const ip = getLanIP(); return sendMessage(chatId, "\\u2705 \\u5DF2\\u53D1\\u5E03\\uFF01\\n\\n\\uD83D\\uDCDD " + s.title + "\\n\\u9884\\u89C8\\uFF1Ahttp://localhost:1313/" + slug + ".html\\n\\u7EBF\\u4E0A\\uFF1Ahttps://usj.cc/" + slug + ".html"); } catch (e) { return sendMessage(chatId, "\\u274C \\u53D1\\u5E03\\u5931\\u8D25\\uFF1A" + e.message + "\\n\\n\\u8BF7\\u5148 /start \\u542F\\u52A8\\u540E\\u53F0\\u670D\\u52A1\\u3002"); } }; CMD_HANDLERS.cancel = async function (chatId) { const s = sessions.get(chatId); if (!s || !s.title) return sendMessage(chatId, "\\u6CA1\\u6709\\u6D3B\\u52A8\\u7684\\u8349\\u7A3F\\u3002"); clearSession(chatId); return sendMessage(chatId, "\\u2702 \\u8349\\u7A3F\\u5DF2\\u4E22\\u5F03\\u3002"); }; CMD_HANDLERS.list = async function (chatId, pageStr) { const ip = getLanIP(); return sendMessage(chatId, "\\uD83D\\uDCD1 \\u6587\\u7AE0\\u5217\\u8868\\n\\n" + "\\uD83D\\uDCF1 \\u624B\\u673A\\u6253\\u5F00: http://" + ip + ":1313/posts/\\n" + "\\uD83D\\uDCBB \\u7535\\u8111\\u6253\\u5F00: http://localhost:1313/posts/\\n\\n" + "\\u2705 \\u65B0\\u5EFA: /new \\u300A\\u6807\\u9898\\u300B\\n" + "\\uD83D\\uDCD1 \\u7F16\\u8F91: /edit slug\\n" + "\\uD83D\\uDCC4 \\u8349\\u7A3F: /draft\\n" + "\\uD83D\\uDE80 \\u53D1\\u5E03: /publish" ); }; // ---------- Links (友链) ---------- CMD_HANDLERS.links = async function (chatId, pageStr) { const page = parseInt(pageStr) || 1; const perPage = 10; try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?all=1"); const data = await res.json(); const allLinks = data.links || []; if (allLinks.length === 0) return sendMessage(chatId, "No links found."); const totalPages = Math.ceil(allLinks.length / perPage); const p = page > totalPages ? totalPages : page; const start = (p - 1) * perPage; const pageLinks = allLinks.slice(start, start + perPage); const lines = ["\\uD83D\\uDD17 \\u53CB\\u94FE \\u5171" + allLinks.length + " \\u4E2A \\u00B7 \\u7B2C" + p + "/" + totalPages + " \\u9875"]; pageLinks.forEach(function(l) { const icon = l.hidden ? "\\uD83D\\uDD12" : "\\uD83C\\uDF10"; const name = (l.name || l.url).slice(0, 40); lines.push(icon + " " + name + " " + l.url); }); const keyboard = { inline_keyboard: [] }; const navRow = []; if (p > 1) navRow.push({ text: "\\u25C0 \\u4E0A\\u4E00\\u9875", callback_data: "links_" + (p - 1) }); if (p < totalPages) navRow.push({ text: "\\u4E0B\\u4E00\\u9875 \\u25B6", callback_data: "links_" + (p + 1) }); if (navRow.length) keyboard.inline_keyboard.push(navRow); const payload = { chat_id: chatId, text: lines.join("\\n"), parse_mode: "HTML", reply_markup: navRow.length ? keyboard : undefined, disable_web_page_preview: true, }; const { tg } = require("./tg-core"); return tg("sendMessage", payload); } catch (e) { return sendMessage(chatId, "Please /start Write first."); } }; CMD_HANDLERS.link_add = async function (chatId, arg) { if (!arg) return sendMessage(chatId, "Usage: /link_add \\nExample: /link_add https://example.com My Blog"); const parts = arg.split(/\\s+/); let url = parts[0]; const name = parts.slice(1).join(" ") || url; if (!url.startsWith("http")) url = "https://" + url; try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, name }), }); if (!res.ok) throw new Error((await res.json()).error || "Add failed"); return sendMessage(chatId, "\\u2705 Link added: " + name + "\\n" + url); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; CMD_HANDLERS.link_del = async function (chatId, url) { if (!url) return sendMessage(chatId, "Usage: /link_del \\nUse /links to find the URL."); try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?url=" + encodeURIComponent(url), { method: "DELETE", }); if (!res.ok) throw new Error((await res.json()).error || "Delete failed"); return sendMessage(chatId, "\\u2705 Link deleted: " + url); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; CMD_HANDLERS.link_toggle = async function (chatId, url) { if (!url) return sendMessage(chatId, "Usage: /link_toggle \\nToggle visibility of a link."); try { const listRes = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?all=1"); const listData = await listRes.json(); const link = (listData.links || []).find(function(l) { return l.url === url; }); if (!link) return sendMessage(chatId, "\\u274C Link not found: " + url); const newHidden = !link.hidden; const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ oldUrl: url, hidden: newHidden }), }); if (!res.ok) throw new Error((await res.json()).error || "Toggle failed"); const label = newHidden ? "\\uD83D\\uDD12 Hidden" : "\\uD83C\\uDF10 Visible"; return sendMessage(chatId, label + ": " + url); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; // ---------- Feeds (订阅) ---------- CMD_HANDLERS.feeds = async function (chatId, pageStr) { const page = parseInt(pageStr) || 1; const perPage = 10; try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds"); const data = await res.json(); const allFeeds = data.feeds || []; if (allFeeds.length === 0) return sendMessage(chatId, "No feeds configured."); const totalPages = Math.ceil(allFeeds.length / perPage); const p = page > totalPages ? totalPages : page; const start = (p - 1) * perPage; const pageFeeds = allFeeds.slice(start, start + perPage); const lines = ["\\uD83D\\uDCE1 \\u8BA2\\u9605\\u6E90 \\u5171" + allFeeds.length + " \\u4E2A \\u00B7 \\u7B2C" + p + "/" + totalPages + " \\u9875"]; pageFeeds.forEach(function(f) { const title = (f.feedTitle || f.url).slice(0, 50); lines.push("\\uD83D\\uDCDC " + title + "\\n " + f.url); }); const keyboard = { inline_keyboard: [] }; const navRow = []; if (p > 1) navRow.push({ text: "\\u25C0 \\u4E0A\\u4E00\\u9875", callback_data: "feeds_" + (p - 1) }); if (p < totalPages) navRow.push({ text: "\\u4E0B\\u4E00\\u9875 \\u25B6", callback_data: "feeds_" + (p + 1) }); if (navRow.length) keyboard.inline_keyboard.push(navRow); const { tg } = require("./tg-core"); return tg("sendMessage", { chat_id: chatId, text: lines.join("\\n"), parse_mode: "HTML", reply_markup: navRow.length ? keyboard : undefined, disable_web_page_preview: true, }); } catch (e) { return sendMessage(chatId, "Please /start Write first."); } }; CMD_HANDLERS.feed_add = async function (chatId, arg) { if (!arg) return sendMessage(chatId, "Usage: /feed_add \\nExample: /feed_add https://example.com/rss.xml"); const parts = arg.split(/\\s+/); const url = parts[0]; const feedTitle = parts.slice(1).join(" ") || ""; try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, feedTitle }), }); if (!res.ok) throw new Error((await res.json()).error || "Add failed"); return sendMessage(chatId, "\\u2705 Feed added: " + url); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; CMD_HANDLERS.feed_del = async function (chatId, url) { if (!url) return sendMessage(chatId, "Usage: /feed_del \\nUse /feeds to find the URL."); try { const res = await fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds?url=" + encodeURIComponent(url), { method: "DELETE", }); if (!res.ok) throw new Error((await res.json()).error || "Delete failed"); return sendMessage(chatId, "\\u2705 Feed deleted: " + url); } catch (e) { return sendMessage(chatId, "\\u274C " + e.message); } }; // ---------- Stats ---------- CMD_HANDLERS.stats = async function (chatId) { try { const [resPosts, resLinks, resFeeds] = await Promise.all([ fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/posts?perPage=1"), fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/links?all=1"), fetch("http://127.0.0.1:" + config.WRITE_PORT + "/api/rss/feeds"), ]); const postsData = await resPosts.json(); const linksData = await resLinks.json(); const feedsData = await resFeeds.json(); const totalPosts = postsData.totalCount || postsData.total || 0; const totalLinks = (linksData.links || []).filter(function(l) { return !l.hidden; }).length; const totalFeeds = (feedsData.feeds || []).length; return sendMessage(chatId, "\\uD83D\\uDCCA \\u7F51\\u7AD9\\u6570\\u636E\\n\\n" + "\\uD83D\\uDCDD \\u6587\\u7AE0\\uFF1A" + totalPosts + " \\u7BC7\\n" + "\\uD83D\\uDD17 \\u53CB\\u94FE\\uFF1A" + totalLinks + " \\u4E2A\\n" + "\\uD83D\\uDCE1 \\u8BA2\\u9605\\u6E90\\uFF1A" + totalFeeds + " \\u4E2A\\n\\n" + "\\uD83C\\uDF10 usj.cc" ); } catch (e) { return sendMessage(chatId, "\\u8BF7\\u5148 /start \\u542F\\u52A8\\u540E\\u53F0\\u670D\\u52A1\\u3002"); } }; module.exports = CMD_HANDLERS;