feat(telegram): add silent message option (#2382)
* feat(telegram): add silent message option (disable_notification) Add support for sending Telegram messages silently without notification sound via the `silent` parameter on the message tool. Changes: - Add `silent` boolean to message tool schema - Extract and pass `silent` through telegram plugin - Add `disable_notification: true` to Telegram API calls - Add `--silent` flag to CLI `message send` command - Add unit test for silent flag Closes #2249 AI-assisted (Claude) - fully tested with unit tests + manual Telegram testing * feat(telegram): add silent send option (#2382) (thanks @Suksham-sharma) --------- Co-authored-by: Pocket Clawd <pocket@Pockets-Mac-mini.local>
This commit is contained in:
@@ -41,6 +41,7 @@ Status: unreleased.
|
|||||||
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
- Routing: precompile session key regexes. (#1697) Thanks @Ray0907.
|
||||||
- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.
|
- TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein.
|
||||||
- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc.
|
- Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc.
|
||||||
|
- Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma.
|
||||||
- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
|
- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
|
||||||
- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
|
- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
|
||||||
- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
|
- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole
|
|||||||
replyTo: Type.Optional(Type.String()),
|
replyTo: Type.Optional(Type.String()),
|
||||||
threadId: Type.Optional(Type.String()),
|
threadId: Type.Optional(Type.String()),
|
||||||
asVoice: Type.Optional(Type.Boolean()),
|
asVoice: Type.Optional(Type.Boolean()),
|
||||||
|
silent: Type.Optional(Type.Boolean()),
|
||||||
bestEffort: Type.Optional(Type.Boolean()),
|
bestEffort: Type.Optional(Type.Boolean()),
|
||||||
gifPlayback: Type.Optional(Type.Boolean()),
|
gifPlayback: Type.Optional(Type.Boolean()),
|
||||||
buttons: Type.Optional(
|
buttons: Type.Optional(
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export async function handleTelegramAction(
|
|||||||
replyToMessageId: replyToMessageId ?? undefined,
|
replyToMessageId: replyToMessageId ?? undefined,
|
||||||
messageThreadId: messageThreadId ?? undefined,
|
messageThreadId: messageThreadId ?? undefined,
|
||||||
asVoice: typeof params.asVoice === "boolean" ? params.asVoice : undefined,
|
asVoice: typeof params.asVoice === "boolean" ? params.asVoice : undefined,
|
||||||
|
silent: typeof params.silent === "boolean" ? params.silent : undefined,
|
||||||
});
|
});
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -36,4 +36,30 @@ describe("telegramMessageActions", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes silent flag for silent sends", async () => {
|
||||||
|
handleTelegramAction.mockClear();
|
||||||
|
const cfg = { channels: { telegram: { botToken: "tok" } } } as ClawdbotConfig;
|
||||||
|
|
||||||
|
await telegramMessageActions.handleAction({
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
to: "456",
|
||||||
|
message: "Silent notification test",
|
||||||
|
silent: true,
|
||||||
|
},
|
||||||
|
cfg,
|
||||||
|
accountId: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleTelegramAction).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
action: "sendMessage",
|
||||||
|
to: "456",
|
||||||
|
content: "Silent notification test",
|
||||||
|
silent: true,
|
||||||
|
}),
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function readTelegramSendParams(params: Record<string, unknown>) {
|
|||||||
const threadId = readStringParam(params, "threadId");
|
const threadId = readStringParam(params, "threadId");
|
||||||
const buttons = params.buttons;
|
const buttons = params.buttons;
|
||||||
const asVoice = typeof params.asVoice === "boolean" ? params.asVoice : undefined;
|
const asVoice = typeof params.asVoice === "boolean" ? params.asVoice : undefined;
|
||||||
|
const silent = typeof params.silent === "boolean" ? params.silent : undefined;
|
||||||
return {
|
return {
|
||||||
to,
|
to,
|
||||||
content,
|
content,
|
||||||
@@ -28,6 +29,7 @@ function readTelegramSendParams(params: Record<string, unknown>) {
|
|||||||
messageThreadId: threadId ?? undefined,
|
messageThreadId: threadId ?? undefined,
|
||||||
buttons,
|
buttons,
|
||||||
asVoice,
|
asVoice,
|
||||||
|
silent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export function registerMessageSendCommand(message: Command, helpers: MessageCli
|
|||||||
.option("--card <json>", "Adaptive Card JSON object (when supported by the channel)")
|
.option("--card <json>", "Adaptive Card JSON object (when supported by the channel)")
|
||||||
.option("--reply-to <id>", "Reply-to message id")
|
.option("--reply-to <id>", "Reply-to message id")
|
||||||
.option("--thread-id <id>", "Thread id (Telegram forum thread)")
|
.option("--thread-id <id>", "Thread id (Telegram forum thread)")
|
||||||
.option("--gif-playback", "Treat video media as GIF playback (WhatsApp only).", false),
|
.option("--gif-playback", "Treat video media as GIF playback (WhatsApp only).", false)
|
||||||
|
.option("--silent", "Send message silently without notification (Telegram only)", false),
|
||||||
)
|
)
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
await helpers.runMessageAction("send", opts);
|
await helpers.runMessageAction("send", opts);
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ export const ToolPolicySchema = ToolPolicyBaseSchema.superRefine((value, ctx) =>
|
|||||||
if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) {
|
if (value.allow && value.allow.length > 0 && value.alsoAllow && value.alsoAllow.length > 0) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "tools policy cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)",
|
message:
|
||||||
|
"tools policy cannot set both allow and alsoAllow in the same scope (merge alsoAllow into allow, or remove allow and use profile + alsoAllow)",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|||||||
@@ -476,6 +476,28 @@ describe("sendMessageTelegram", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets disable_notification when silent is true", async () => {
|
||||||
|
const chatId = "123";
|
||||||
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
message_id: 1,
|
||||||
|
chat: { id: chatId },
|
||||||
|
});
|
||||||
|
const api = { sendMessage } as unknown as {
|
||||||
|
sendMessage: typeof sendMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendMessageTelegram(chatId, "hi", {
|
||||||
|
token: "tok",
|
||||||
|
api,
|
||||||
|
silent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMessage).toHaveBeenCalledWith(chatId, "hi", {
|
||||||
|
parse_mode: "HTML",
|
||||||
|
disable_notification: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("parses message_thread_id from recipient string (telegram:group:...:topic:...)", async () => {
|
it("parses message_thread_id from recipient string (telegram:group:...:topic:...)", async () => {
|
||||||
const chatId = "-1001234567890";
|
const chatId = "-1001234567890";
|
||||||
const sendMessage = vi.fn().mockResolvedValue({
|
const sendMessage = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type TelegramSendOpts = {
|
|||||||
plainText?: string;
|
plainText?: string;
|
||||||
/** Send audio as voice message (voice bubble) instead of audio file. Defaults to false. */
|
/** Send audio as voice message (voice bubble) instead of audio file. Defaults to false. */
|
||||||
asVoice?: boolean;
|
asVoice?: boolean;
|
||||||
|
/** Send message silently (no notification). Defaults to false. */
|
||||||
|
silent?: boolean;
|
||||||
/** Message ID to reply to (for threading) */
|
/** Message ID to reply to (for threading) */
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
/** Forum topic thread ID (for forum supergroups) */
|
/** Forum topic thread ID (for forum supergroups) */
|
||||||
@@ -245,6 +247,7 @@ export async function sendMessageTelegram(
|
|||||||
const sendParams = {
|
const sendParams = {
|
||||||
parse_mode: "HTML" as const,
|
parse_mode: "HTML" as const,
|
||||||
...baseParams,
|
...baseParams,
|
||||||
|
...(opts.silent === true ? { disable_notification: true } : {}),
|
||||||
};
|
};
|
||||||
const res = await requestWithDiag(
|
const res = await requestWithDiag(
|
||||||
() => api.sendMessage(chatId, htmlText, sendParams),
|
() => api.sendMessage(chatId, htmlText, sendParams),
|
||||||
@@ -298,6 +301,7 @@ export async function sendMessageTelegram(
|
|||||||
caption: htmlCaption,
|
caption: htmlCaption,
|
||||||
...(htmlCaption ? { parse_mode: "HTML" as const } : {}),
|
...(htmlCaption ? { parse_mode: "HTML" as const } : {}),
|
||||||
...baseMediaParams,
|
...baseMediaParams,
|
||||||
|
...(opts.silent === true ? { disable_notification: true } : {}),
|
||||||
};
|
};
|
||||||
let result:
|
let result:
|
||||||
| Awaited<ReturnType<typeof api.sendPhoto>>
|
| Awaited<ReturnType<typeof api.sendPhoto>>
|
||||||
|
|||||||
Reference in New Issue
Block a user