Merge c617fcbcc9 into 9025da2296
This commit is contained in:
commit
6212c73534
@ -188,6 +188,27 @@ function buildGatewaySchema() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildForumTopicSchema() {
|
||||||
|
return {
|
||||||
|
iconColor: Type.Optional(
|
||||||
|
Type.Number({
|
||||||
|
description:
|
||||||
|
"Topic icon color in RGB (e.g. 0x6FB9F0). Only for Telegram forum topic-create.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
iconCustomEmojiId: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description: "Custom emoji id for the topic icon. For Telegram topic-create/topic-edit.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
messageThreadId: Type.Optional(
|
||||||
|
Type.Number({
|
||||||
|
description: "Forum topic thread id. For Telegram topic-edit/close/reopen/delete.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildChannelManagementSchema() {
|
function buildChannelManagementSchema() {
|
||||||
return {
|
return {
|
||||||
name: Type.Optional(Type.String()),
|
name: Type.Optional(Type.String()),
|
||||||
@ -220,6 +241,7 @@ function buildMessageToolSchemaProps(options: { includeButtons: boolean; include
|
|||||||
...buildModerationSchema(),
|
...buildModerationSchema(),
|
||||||
...buildGatewaySchema(),
|
...buildGatewaySchema(),
|
||||||
...buildChannelManagementSchema(),
|
...buildChannelManagementSchema(),
|
||||||
|
...buildForumTopicSchema(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,14 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
|
||||||
import {
|
import {
|
||||||
|
closeForumTopicTelegram,
|
||||||
|
createForumTopicTelegram,
|
||||||
|
deleteForumTopicTelegram,
|
||||||
deleteMessageTelegram,
|
deleteMessageTelegram,
|
||||||
|
editForumTopicTelegram,
|
||||||
editMessageTelegram,
|
editMessageTelegram,
|
||||||
reactMessageTelegram,
|
reactMessageTelegram,
|
||||||
|
reopenForumTopicTelegram,
|
||||||
sendMessageTelegram,
|
sendMessageTelegram,
|
||||||
sendStickerTelegram,
|
sendStickerTelegram,
|
||||||
} from "../../telegram/send.js";
|
} from "../../telegram/send.js";
|
||||||
@ -318,5 +323,146 @@ export async function handleTelegramAction(
|
|||||||
return jsonResult({ ok: true, ...stats });
|
return jsonResult({ ok: true, ...stats });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === "createForumTopic") {
|
||||||
|
if (!isActionEnabled("forumTopics", false)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name", { required: true });
|
||||||
|
const iconColor = readNumberParam(params, "iconColor", { integer: true });
|
||||||
|
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||||
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const result = await createForumTopicTelegram(chatId ?? "", name, {
|
||||||
|
token,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
iconColor: iconColor ?? undefined,
|
||||||
|
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({
|
||||||
|
ok: true,
|
||||||
|
messageThreadId: result.messageThreadId,
|
||||||
|
name: result.name,
|
||||||
|
iconColor: result.iconColor,
|
||||||
|
iconCustomEmojiId: result.iconCustomEmojiId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "editForumTopic") {
|
||||||
|
if (!isActionEnabled("forumTopics", false)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name");
|
||||||
|
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||||
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await editForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, {
|
||||||
|
token,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
name: name ?? undefined,
|
||||||
|
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, edited: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "closeForumTopic") {
|
||||||
|
if (!isActionEnabled("forumTopics", false)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await closeForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, {
|
||||||
|
token,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, closed: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "reopenForumTopic") {
|
||||||
|
if (!isActionEnabled("forumTopics", false)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await reopenForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, {
|
||||||
|
token,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, reopened: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "deleteForumTopic") {
|
||||||
|
if (!isActionEnabled("forumTopics", false)) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram forum topic actions are disabled. Set channels.telegram.actions.forumTopics to true.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const chatId = readStringOrNumberParam(params, "chatId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const token = resolveTelegramToken(cfg, { accountId }).token;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await deleteForumTopicTelegram(chatId ?? "", messageThreadId ?? 0, {
|
||||||
|
token,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, deleted: true });
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported Telegram action: ${action}`);
|
throw new Error(`Unsupported Telegram action: ${action}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,4 +118,209 @@ describe("telegramMessageActions", () => {
|
|||||||
|
|
||||||
expect(handleTelegramAction).not.toHaveBeenCalled();
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("excludes forum topic actions when not enabled", () => {
|
||||||
|
const cfg = { channels: { telegram: { botToken: "tok" } } } as MoltbotConfig;
|
||||||
|
const actions = telegramMessageActions.listActions({ cfg });
|
||||||
|
expect(actions).not.toContain("topic-create");
|
||||||
|
expect(actions).not.toContain("topic-edit");
|
||||||
|
expect(actions).not.toContain("topic-close");
|
||||||
|
expect(actions).not.toContain("topic-reopen");
|
||||||
|
expect(actions).not.toContain("topic-delete");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes forum topic actions when forumTopics is enabled", () => {
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
const actions = telegramMessageActions.listActions({ cfg });
|
||||||
|
expect(actions).toContain("topic-create");
|
||||||
|
expect(actions).toContain("topic-edit");
|
||||||
|
expect(actions).toContain("topic-close");
|
||||||
|
expect(actions).toContain("topic-reopen");
|
||||||
|
expect(actions).toContain("topic-delete");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps topic-create action to createForumTopic", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "topic-create",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
name: "My New Topic",
|
||||||
|
iconColor: 0x6fb9f0,
|
||||||
|
iconCustomEmojiId: "emoji123",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
action: "createForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
name: "My New Topic",
|
||||||
|
iconColor: 0x6fb9f0,
|
||||||
|
iconCustomEmojiId: "emoji123",
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps topic-edit action to editForumTopic", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "topic-edit",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
name: "Renamed Topic",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
action: "editForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
name: "Renamed Topic",
|
||||||
|
iconCustomEmojiId: undefined,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps topic-close action to closeForumTopic", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "topic-close",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
action: "closeForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps topic-reopen action to reopenForumTopic", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "topic-reopen",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
action: "reopenForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps topic-delete action to deleteForumTopic", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "topic-delete",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId: "-1001234567890",
|
||||||
|
messageThreadId: 42,
|
||||||
|
accountId: undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires messageThreadId for topic-edit", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
telegramMessageActions.handleAction({
|
||||||
|
action: "topic-edit",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
name: "Renamed",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires name for topic-create", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = {
|
||||||
|
channels: { telegram: { botToken: "tok", actions: { forumTopics: true } } },
|
||||||
|
} as MoltbotConfig;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
telegramMessageActions.handleAction({
|
||||||
|
action: "topic-create",
|
||||||
|
params: {
|
||||||
|
to: "-1001234567890",
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(handleTelegramAction).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -52,6 +52,13 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||||||
actions.add("sticker");
|
actions.add("sticker");
|
||||||
actions.add("sticker-search");
|
actions.add("sticker-search");
|
||||||
}
|
}
|
||||||
|
if (gate("forumTopics", false)) {
|
||||||
|
actions.add("topic-create");
|
||||||
|
actions.add("topic-edit");
|
||||||
|
actions.add("topic-close");
|
||||||
|
actions.add("topic-reopen");
|
||||||
|
actions.add("topic-delete");
|
||||||
|
}
|
||||||
return Array.from(actions);
|
return Array.from(actions);
|
||||||
},
|
},
|
||||||
supportsButtons: ({ cfg }) => {
|
supportsButtons: ({ cfg }) => {
|
||||||
@ -183,6 +190,106 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === "topic-create") {
|
||||||
|
const chatId =
|
||||||
|
readStringOrNumberParam(params, "chatId") ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
const name = readStringParam(params, "name", { required: true });
|
||||||
|
const iconColor = readNumberParam(params, "iconColor", { integer: true });
|
||||||
|
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "createForumTopic",
|
||||||
|
chatId,
|
||||||
|
name,
|
||||||
|
iconColor: iconColor ?? undefined,
|
||||||
|
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "topic-edit") {
|
||||||
|
const chatId =
|
||||||
|
readStringOrNumberParam(params, "chatId") ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name");
|
||||||
|
const iconCustomEmojiId = readStringParam(params, "iconCustomEmojiId");
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "editForumTopic",
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
name: name ?? undefined,
|
||||||
|
iconCustomEmojiId: iconCustomEmojiId ?? undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "topic-close") {
|
||||||
|
const chatId =
|
||||||
|
readStringOrNumberParam(params, "chatId") ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "closeForumTopic",
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "topic-reopen") {
|
||||||
|
const chatId =
|
||||||
|
readStringOrNumberParam(params, "chatId") ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "reopenForumTopic",
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "topic-delete") {
|
||||||
|
const chatId =
|
||||||
|
readStringOrNumberParam(params, "chatId") ??
|
||||||
|
readStringParam(params, "to", { required: true });
|
||||||
|
const messageThreadId = readNumberParam(params, "messageThreadId", {
|
||||||
|
required: true,
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
return await handleTelegramAction(
|
||||||
|
{
|
||||||
|
action: "deleteForumTopic",
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -48,6 +48,11 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
|
|||||||
"timeout",
|
"timeout",
|
||||||
"kick",
|
"kick",
|
||||||
"ban",
|
"ban",
|
||||||
|
"topic-create",
|
||||||
|
"topic-edit",
|
||||||
|
"topic-close",
|
||||||
|
"topic-reopen",
|
||||||
|
"topic-delete",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ChannelMessageActionName = (typeof CHANNEL_MESSAGE_ACTION_NAMES)[number];
|
export type ChannelMessageActionName = (typeof CHANNEL_MESSAGE_ACTION_NAMES)[number];
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export type TelegramActionConfig = {
|
|||||||
editMessage?: boolean;
|
editMessage?: boolean;
|
||||||
/** Enable sticker actions (send and search). */
|
/** Enable sticker actions (send and search). */
|
||||||
sticker?: boolean;
|
sticker?: boolean;
|
||||||
|
/** Enable forum topic management actions (create, edit, close, reopen, delete). */
|
||||||
|
forumTopics?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TelegramNetworkConfig = {
|
export type TelegramNetworkConfig = {
|
||||||
|
|||||||
@ -53,6 +53,11 @@ export const MESSAGE_ACTION_TARGET_MODE: Record<ChannelMessageActionName, Messag
|
|||||||
timeout: "none",
|
timeout: "none",
|
||||||
kick: "none",
|
kick: "none",
|
||||||
ban: "none",
|
ban: "none",
|
||||||
|
"topic-create": "to",
|
||||||
|
"topic-edit": "to",
|
||||||
|
"topic-close": "to",
|
||||||
|
"topic-reopen": "to",
|
||||||
|
"topic-delete": "to",
|
||||||
};
|
};
|
||||||
|
|
||||||
const ACTION_TARGET_ALIASES: Partial<Record<ChannelMessageActionName, string[]>> = {
|
const ACTION_TARGET_ALIASES: Partial<Record<ChannelMessageActionName, string[]>> = {
|
||||||
|
|||||||
@ -722,3 +722,236 @@ export async function sendStickerTelegram(
|
|||||||
|
|
||||||
return { messageId, chatId: resolvedChatId };
|
return { messageId, chatId: resolvedChatId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Forum topic management
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type TelegramForumTopicOpts = {
|
||||||
|
token?: string;
|
||||||
|
accountId?: string;
|
||||||
|
verbose?: boolean;
|
||||||
|
api?: Bot["api"];
|
||||||
|
retry?: RetryConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramCreateForumTopicResult = {
|
||||||
|
messageThreadId: number;
|
||||||
|
name: string;
|
||||||
|
iconColor: number;
|
||||||
|
iconCustomEmojiId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createForumTopicTelegram(
|
||||||
|
chatIdInput: string | number,
|
||||||
|
name: string,
|
||||||
|
opts: TelegramForumTopicOpts & {
|
||||||
|
iconColor?: number;
|
||||||
|
iconCustomEmojiId?: string;
|
||||||
|
} = {},
|
||||||
|
): Promise<TelegramCreateForumTopicResult> {
|
||||||
|
if (!name?.trim()) {
|
||||||
|
throw new Error("Forum topic name is required (1-128 characters)");
|
||||||
|
}
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const account = resolveTelegramAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
const token = resolveToken(opts.token, account);
|
||||||
|
const chatId = normalizeChatId(String(chatIdInput));
|
||||||
|
const client = resolveTelegramClientOptions(account);
|
||||||
|
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||||
|
const request = createTelegramRetryRunner({
|
||||||
|
retry: opts.retry,
|
||||||
|
configRetry: account.config.retry,
|
||||||
|
verbose: opts.verbose,
|
||||||
|
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||||
|
});
|
||||||
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
|
logHttpError(label ?? "request", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
const other: Record<string, unknown> = {};
|
||||||
|
if (opts.iconColor != null) other.icon_color = opts.iconColor;
|
||||||
|
if (opts.iconCustomEmojiId) other.icon_custom_emoji_id = opts.iconCustomEmojiId;
|
||||||
|
|
||||||
|
const result = await requestWithDiag(
|
||||||
|
() =>
|
||||||
|
api.createForumTopic(chatId, name.trim(), Object.keys(other).length > 0 ? other : undefined),
|
||||||
|
"createForumTopic",
|
||||||
|
);
|
||||||
|
|
||||||
|
logVerbose(`[telegram] Created forum topic "${name}" in chat ${chatId}`);
|
||||||
|
return {
|
||||||
|
messageThreadId: result.message_thread_id,
|
||||||
|
name: result.name,
|
||||||
|
iconColor: result.icon_color,
|
||||||
|
iconCustomEmojiId: result.icon_custom_emoji_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editForumTopicTelegram(
|
||||||
|
chatIdInput: string | number,
|
||||||
|
messageThreadId: number,
|
||||||
|
opts: TelegramForumTopicOpts & {
|
||||||
|
name?: string;
|
||||||
|
iconCustomEmojiId?: string;
|
||||||
|
} = {},
|
||||||
|
): Promise<{ ok: true }> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const account = resolveTelegramAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
const token = resolveToken(opts.token, account);
|
||||||
|
const chatId = normalizeChatId(String(chatIdInput));
|
||||||
|
const client = resolveTelegramClientOptions(account);
|
||||||
|
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||||
|
const request = createTelegramRetryRunner({
|
||||||
|
retry: opts.retry,
|
||||||
|
configRetry: account.config.retry,
|
||||||
|
verbose: opts.verbose,
|
||||||
|
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||||
|
});
|
||||||
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
|
logHttpError(label ?? "request", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
const other: Record<string, unknown> = {};
|
||||||
|
if (opts.name != null) other.name = opts.name.trim();
|
||||||
|
if (opts.iconCustomEmojiId != null) other.icon_custom_emoji_id = opts.iconCustomEmojiId;
|
||||||
|
|
||||||
|
await requestWithDiag(
|
||||||
|
() =>
|
||||||
|
api.editForumTopic(
|
||||||
|
chatId,
|
||||||
|
messageThreadId,
|
||||||
|
Object.keys(other).length > 0 ? other : undefined,
|
||||||
|
),
|
||||||
|
"editForumTopic",
|
||||||
|
);
|
||||||
|
|
||||||
|
logVerbose(`[telegram] Edited forum topic ${messageThreadId} in chat ${chatId}`);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeForumTopicTelegram(
|
||||||
|
chatIdInput: string | number,
|
||||||
|
messageThreadId: number,
|
||||||
|
opts: TelegramForumTopicOpts = {},
|
||||||
|
): Promise<{ ok: true }> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const account = resolveTelegramAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
const token = resolveToken(opts.token, account);
|
||||||
|
const chatId = normalizeChatId(String(chatIdInput));
|
||||||
|
const client = resolveTelegramClientOptions(account);
|
||||||
|
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||||
|
const request = createTelegramRetryRunner({
|
||||||
|
retry: opts.retry,
|
||||||
|
configRetry: account.config.retry,
|
||||||
|
verbose: opts.verbose,
|
||||||
|
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||||
|
});
|
||||||
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
|
logHttpError(label ?? "request", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestWithDiag(() => api.closeForumTopic(chatId, messageThreadId), "closeForumTopic");
|
||||||
|
|
||||||
|
logVerbose(`[telegram] Closed forum topic ${messageThreadId} in chat ${chatId}`);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reopenForumTopicTelegram(
|
||||||
|
chatIdInput: string | number,
|
||||||
|
messageThreadId: number,
|
||||||
|
opts: TelegramForumTopicOpts = {},
|
||||||
|
): Promise<{ ok: true }> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const account = resolveTelegramAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
const token = resolveToken(opts.token, account);
|
||||||
|
const chatId = normalizeChatId(String(chatIdInput));
|
||||||
|
const client = resolveTelegramClientOptions(account);
|
||||||
|
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||||
|
const request = createTelegramRetryRunner({
|
||||||
|
retry: opts.retry,
|
||||||
|
configRetry: account.config.retry,
|
||||||
|
verbose: opts.verbose,
|
||||||
|
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||||
|
});
|
||||||
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
|
logHttpError(label ?? "request", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestWithDiag(() => api.reopenForumTopic(chatId, messageThreadId), "reopenForumTopic");
|
||||||
|
|
||||||
|
logVerbose(`[telegram] Reopened forum topic ${messageThreadId} in chat ${chatId}`);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteForumTopicTelegram(
|
||||||
|
chatIdInput: string | number,
|
||||||
|
messageThreadId: number,
|
||||||
|
opts: TelegramForumTopicOpts = {},
|
||||||
|
): Promise<{ ok: true }> {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const account = resolveTelegramAccount({
|
||||||
|
cfg,
|
||||||
|
accountId: opts.accountId,
|
||||||
|
});
|
||||||
|
const token = resolveToken(opts.token, account);
|
||||||
|
const chatId = normalizeChatId(String(chatIdInput));
|
||||||
|
const client = resolveTelegramClientOptions(account);
|
||||||
|
const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
|
||||||
|
const request = createTelegramRetryRunner({
|
||||||
|
retry: opts.retry,
|
||||||
|
configRetry: account.config.retry,
|
||||||
|
verbose: opts.verbose,
|
||||||
|
shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" }),
|
||||||
|
});
|
||||||
|
const logHttpError = createTelegramHttpLogger(cfg);
|
||||||
|
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
|
||||||
|
withTelegramApiErrorLogging({
|
||||||
|
operation: label ?? "request",
|
||||||
|
fn: () => request(fn, label),
|
||||||
|
}).catch((err) => {
|
||||||
|
logHttpError(label ?? "request", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
await requestWithDiag(() => api.deleteForumTopic(chatId, messageThreadId), "deleteForumTopic");
|
||||||
|
|
||||||
|
logVerbose(`[telegram] Deleted forum topic ${messageThreadId} in chat ${chatId}`);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user