Files
blog/bot/src/tg-handlers.js
T
2026-06-01 17:57:22 +08:00

414 lines
18 KiB
JavaScript

// 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 <b>Write \\u5DF2\\u542F\\u52A8</b>\\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 <b>WriteBot \\u547D\\u4EE4</b>\\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<b>" + title + "</b>\\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<b>" + s.title + "</b>\\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 <slug>\\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 <slug>\\nFind slugs with /list");
const ip = getLanIP();
const url = "http://" + ip + ":1313/" + slug + ".html";
return sendMessage(chatId, "\\uD83D\\uDCC9 <b>Preview</b>\\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<b>" + newTitle + "</b>");
};
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 <b>" + s.title + "</b>\\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<b>" + s.title + "</b>");
} 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 <b>" + s.title + "</b>\\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 <b>\\u6587\\u7AE0\\u5217\\u8868</b>\\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 <b>\\u53CB\\u94FE</b> \\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 <url> <name>\\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 <url>\\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 <url>\\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 <b>\\u8BA2\\u9605\\u6E90</b> \\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 <url>\\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 <url>\\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 <b>\\u7F51\\u7AD9\\u6570\\u636E</b>\\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;