added:增加Linux-server
This commit is contained in:
1 parent
6fe4e9d23f
commit
d78f3f811b
90 files changed
+16784
-1
No files matched your search
@@ -0,0 +1,249 @@
|
||||
import { TG_TOKEN } from "./config";
|
||||
import { tg, allowed, sendMessage } from "./tg";
|
||||
import { hasSession, getSession, appendContent } from "./sessions";
|
||||
import handlers from "./handlers";
|
||||
|
||||
const ARG_COMMANDS = new Set([
|
||||
"new", "title", "categories", "tags", "author",
|
||||
"edit", "view", "delete",
|
||||
"link_add", "link_edit", "link_del", "link_toggle",
|
||||
"feed_add", "feed_edit", "feed_del",
|
||||
"list", "links", "feeds", "read",
|
||||
]);
|
||||
|
||||
const CALLBACK_PREFIXES: Record<string, string> = {
|
||||
edit: "edit",
|
||||
confirm_del: "delete",
|
||||
cancel_del: "noop",
|
||||
list: "list",
|
||||
links: "links",
|
||||
feeds: "feeds",
|
||||
skip_cat: "skip_categories",
|
||||
skip_tag: "skip_tags",
|
||||
confirm_shutdown: "confirm_shutdown",
|
||||
cancel_shutdown: "cancel_shutdown",
|
||||
confirm_reboot: "confirm_reboot",
|
||||
cancel_reboot: "cancel_reboot",
|
||||
};
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
export async function startPolling(): Promise<void> {
|
||||
if (!TG_TOKEN) {
|
||||
console.log("[bot] TG_BOT_TOKEN not set, skipping bot startup");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[bot] starting Telegram polling...");
|
||||
|
||||
let lastOffset = 0;
|
||||
let backoff = 3000;
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const data = await tg<Record<string, unknown>[]>("getUpdates", {
|
||||
offset: lastOffset + 1,
|
||||
timeout: 30,
|
||||
allowed_updates: ["message", "callback_query"],
|
||||
});
|
||||
|
||||
if (data.ok && data.result) {
|
||||
for (const update of data.result) {
|
||||
lastOffset = update.update_id as number;
|
||||
|
||||
if (update.callback_query) {
|
||||
const cb = update.callback_query as {
|
||||
message?: { chat?: { id: number } };
|
||||
from?: { id: number };
|
||||
id: string;
|
||||
data?: string;
|
||||
};
|
||||
const cbChatId = cb.message?.chat?.id ?? cb.from?.id ?? 0;
|
||||
if (cb.data && allowed(cbChatId)) {
|
||||
await handleInlineCallback(cbChatId, cb.id, cb.data);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const msg = update.message as {
|
||||
chat?: { id: number };
|
||||
photo?: { file_id: string }[];
|
||||
text?: string;
|
||||
caption?: string;
|
||||
reply_to_message?: { text?: string };
|
||||
} | undefined;
|
||||
if (!msg?.chat?.id) continue;
|
||||
const chatId = msg.chat.id;
|
||||
if (!allowed(chatId)) continue;
|
||||
|
||||
if (msg.photo && hasSession(chatId)) {
|
||||
await handlePhoto(chatId, msg.photo, msg.caption);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!msg.text) continue;
|
||||
const fullText = msg.text.trim();
|
||||
|
||||
// ForceReply routing
|
||||
if (msg.reply_to_message?.text) {
|
||||
const original = msg.reply_to_message.text;
|
||||
if (original.includes("请输入文章标题")) { await handleCommand(chatId, "new", fullText); continue; }
|
||||
if (original.includes("请输入新标题")) { await handleCommand(chatId, "title", fullText); continue; }
|
||||
if (original.includes("请输入分类")) { await handleCommand(chatId, "categories", fullText); continue; }
|
||||
if (original.includes("请输入标签")) { await handleCommand(chatId, "tags", fullText); continue; }
|
||||
if (original.includes("请输入友链信息")) { await handleCommand(chatId, "link_add", fullText); continue; }
|
||||
if (original.includes("请输入新名称")) { await handleCommand(chatId, "link_edit", fullText); continue; }
|
||||
if (original.includes("请输入订阅源 URL")) { await handleCommand(chatId, "feed_add", fullText); continue; }
|
||||
if (original.includes("请输入新的订阅信息")) { await handleCommand(chatId, "feed_edit", fullText); continue; }
|
||||
}
|
||||
|
||||
const parts = fullText.split("@")[0].split(/\s+/);
|
||||
const cmdRaw = parts[0].toLowerCase();
|
||||
const slashCmd = cmdRaw.startsWith("/") ? cmdRaw.slice(1) : "";
|
||||
const slashArg = parts.slice(1).join(" ");
|
||||
|
||||
// In writing session, non-slash text appends to content
|
||||
if (hasSession(chatId) && !cmdRaw.startsWith("/")) {
|
||||
try { appendContent(chatId, fullText); }
|
||||
catch (e) { console.error("[bot:append]", e instanceof Error ? e.message : String(e)); }
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cmdRaw.startsWith("/")) {
|
||||
await handleCommand(chatId, slashCmd, slashArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backoff = 3000;
|
||||
} catch (e) {
|
||||
console.error("[bot:poll]", e instanceof Error ? e.message : String(e));
|
||||
await sleep(backoff);
|
||||
backoff = Math.min(backoff * 2, 60000);
|
||||
}
|
||||
|
||||
await sleep(100);
|
||||
setImmediate(poll);
|
||||
}
|
||||
|
||||
// Register commands with Telegram
|
||||
try {
|
||||
await tg("setMyCommands", {
|
||||
commands: [
|
||||
{ command: "new", description: "写一篇文章" },
|
||||
{ command: "edit", description: "编辑文章" },
|
||||
{ command: "delete", description: "删除文章" },
|
||||
{ command: "list", description: "文章列表" },
|
||||
{ command: "publish", description: "保存发布" },
|
||||
{ command: "draft", description: "保存草稿" },
|
||||
{ command: "deploy", description: "推送到线上" },
|
||||
{ command: "sync", description: "同步远程" },
|
||||
{ command: "undo", description: "撤销最后一段" },
|
||||
{ command: "preview", description: "预览" },
|
||||
{ command: "links", description: "友链管理" },
|
||||
{ command: "link_add", description: "添加友链" },
|
||||
{ command: "link_edit", description: "编辑友链" },
|
||||
{ command: "link_del", description: "删除友链" },
|
||||
{ command: "link_toggle", description: "显示/隐藏友链" },
|
||||
{ command: "feeds", description: "订阅管理" },
|
||||
{ command: "feed_add", description: "添加订阅源" },
|
||||
{ command: "feed_edit", description: "编辑订阅源" },
|
||||
{ command: "feed_del", description: "删除订阅源" },
|
||||
{ command: "feed_health", description: "订阅源健康检查" },
|
||||
{ command: "read", description: "友链最新文章" },
|
||||
{ command: "stats", description: "网站数据" },
|
||||
{ command: "shutdown", description: "关机" },
|
||||
{ command: "reboot", description: "重启" },
|
||||
{ command: "help", description: "帮助" },
|
||||
],
|
||||
});
|
||||
console.log("[bot] commands registered");
|
||||
} catch (e) {
|
||||
console.error("[bot] failed to register commands:", e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
|
||||
poll();
|
||||
}
|
||||
|
||||
async function handlePhoto(chatId: number, photos: { file_id: string }[], caption?: string) {
|
||||
try {
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
const { getSession } = await import("./sessions");
|
||||
|
||||
const s = getSession(chatId);
|
||||
if (!s.dirPath) {
|
||||
await sendMessage(chatId, "❌ 请先发 /new 或 /edit 开始写作");
|
||||
return;
|
||||
}
|
||||
|
||||
const largestPhoto = photos[photos.length - 1];
|
||||
const fileRes = await tg<{ file_path?: string }>("getFile", { file_id: largestPhoto.file_id });
|
||||
if (!fileRes.ok || !fileRes.result?.file_path) return;
|
||||
|
||||
const filePath = fileRes.result.file_path;
|
||||
const fileUrl = "https://api.telegram.org/file/bot" + TG_TOKEN + "/" + filePath;
|
||||
const imgBuffer = await fetch(fileUrl).then((r) => r.arrayBuffer());
|
||||
|
||||
// Save to article's own folder (Hugo page bundle)
|
||||
const articleDir = s.dirPath;
|
||||
if (!fs.existsSync(articleDir)) fs.mkdirSync(articleDir, { recursive: true });
|
||||
|
||||
const ext = filePath.split(".").pop() || "jpg";
|
||||
const filename = Date.now() + "-" + largestPhoto.file_id.slice(0, 8) + "." + ext;
|
||||
fs.writeFileSync(path.join(articleDir, filename), Buffer.from(imgBuffer));
|
||||
|
||||
const imgCaption = caption || "image";
|
||||
appendContent(chatId, `\n\n`);
|
||||
await sendMessage(chatId, `📷 图片已保存:${filename}`);
|
||||
} catch (e) {
|
||||
console.error("[bot:photo]", e instanceof Error ? e.message : String(e));
|
||||
await sendMessage(chatId, "❌ 图片保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCommand(chatId: number, cmd: string, arg?: string) {
|
||||
try {
|
||||
if (cmd === "noop") return;
|
||||
if (ARG_COMMANDS.has(cmd)) {
|
||||
const handler = handlers[cmd];
|
||||
if (handler) await handler(chatId, arg);
|
||||
else await sendMessage(chatId, "❌ 未知命令:/" + cmd);
|
||||
} else {
|
||||
const handler = handlers[cmd];
|
||||
if (handler) await handler(chatId);
|
||||
else if (cmd) await sendMessage(chatId, "❌ 未知命令:/" + cmd);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[bot:" + cmd + "]", e instanceof Error ? e.message : String(e));
|
||||
try {
|
||||
await sendMessage(chatId, "❌ " + (e instanceof Error ? e.message : "error").slice(0, 200));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInlineCallback(chatId: number, queryId: string, data: string) {
|
||||
let prefix = "";
|
||||
let arg = "";
|
||||
// First try prefix match (e.g. "edit_slug", "list_2")
|
||||
for (const p of Object.keys(CALLBACK_PREFIXES)) {
|
||||
if (data.startsWith(p + "_")) {
|
||||
prefix = p;
|
||||
arg = data.slice(p.length + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Then try exact match (e.g. "confirm_shutdown", "skip_cat")
|
||||
if (!prefix && CALLBACK_PREFIXES[data]) {
|
||||
prefix = data;
|
||||
}
|
||||
if (!prefix) {
|
||||
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
|
||||
return;
|
||||
}
|
||||
const cmd = CALLBACK_PREFIXES[prefix];
|
||||
await handleCommand(chatId, cmd, arg);
|
||||
try { await tg("answerCallbackQuery", { callback_query_id: queryId }); } catch (_e) {}
|
||||
}
|
||||
Reference in new issue
Block a user