diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md index 052a8cd6b1..f4353180e2 100644 --- a/docs/channels/mattermost.md +++ b/docs/channels/mattermost.md @@ -70,7 +70,6 @@ Mattermost responds to DMs automatically. Channel behavior is controlled by `cha - `oncall` (default): respond only when @mentioned in channels. - `onmessage`: respond to every channel message. -- `always`: respond to every message in channels (same channel behavior as `onmessage`). - `onchar`: respond when a message starts with a trigger prefix. Config example: @@ -90,25 +89,6 @@ Notes: - `onchar` still responds to explicit @mentions. - `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred. -- Current limitation: due to Mattermost plugin event behavior (`#11797`), `chatmode: "onmessage"` and - `chatmode: "always"` may still require explicit group mention override to respond without @mentions. - Use: - -```json5 -{ - channels: { - mattermost: { - groupPolicy: "open", - groups: { - "*": { requireMention: false }, - }, - }, - }, -} -``` - -Reference: [Bug: Mattermost plugin does not receive channel message events via WebSocket #11797](https://github.com/open-webui/open-webui/issues/11797). -Related fix scope: [fix(mattermost): honor chatmode mention fallback in group mention gating #14995](https://github.com/open-webui/open-webui/pull/14995). ## Access control (DMs) @@ -153,7 +133,6 @@ Mattermost supports multiple accounts under `channels.mattermost.accounts`: ## Troubleshooting -- No replies in channels: ensure the bot is in the channel and use the mode behavior correctly: mention it (`oncall`), use a trigger prefix (`onchar`), or use `onmessage`/`always` with: - `channels.mattermost.groups["*"].requireMention = false` (and typically `groupPolicy: "open"`). +- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`. - Auth errors: check the bot token, base URL, and whether the account is enabled. - Multi-account issues: env vars only apply to the `default` account. diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 26a871556a..6dd624f5d7 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -1,7 +1,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { OpenClawConfig } from "../../config/config.js"; -import { createTelegramActionGate } from "../../telegram/accounts.js"; import type { TelegramButtonStyle, TelegramInlineButtons } from "../../telegram/button-types.js"; +import { createTelegramActionGate } from "../../telegram/accounts.js"; import { resolveTelegramInlineButtonsScope, resolveTelegramTargetChatType, diff --git a/src/telegram/allowed-updates.test.ts b/src/telegram/allowed-updates.test.ts deleted file mode 100644 index 86e0b5224a..0000000000 --- a/src/telegram/allowed-updates.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveTelegramAllowedUpdates } from "./allowed-updates.js"; - -describe("resolveTelegramAllowedUpdates", () => { - it("includes poll_answer updates", () => { - const updates = resolveTelegramAllowedUpdates(); - expect(updates).toContain("poll_answer"); - }); -}); diff --git a/src/telegram/allowed-updates.ts b/src/telegram/allowed-updates.ts index 7dfbb7a825..e32fefd096 100644 --- a/src/telegram/allowed-updates.ts +++ b/src/telegram/allowed-updates.ts @@ -4,9 +4,6 @@ type TelegramUpdateType = (typeof API_CONSTANTS.ALL_UPDATE_TYPES)[number]; export function resolveTelegramAllowedUpdates(): ReadonlyArray { const updates = [...API_CONSTANTS.DEFAULT_UPDATE_TYPES] as TelegramUpdateType[]; - if (!updates.includes("poll_answer")) { - updates.push("poll_answer"); - } if (!updates.includes("message_reaction")) { updates.push("message_reaction"); } diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 64a05b5a8c..f21e8504c0 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -1,4 +1,7 @@ import type { Message, ReactionTypeEmoji } from "@grammyjs/types"; +import type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; +import type { TelegramMediaRef } from "./bot-message-context.js"; +import type { TelegramContext } from "./bot/types.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { hasControlCommand } from "../auto-reply/command-detection.js"; import { @@ -14,7 +17,6 @@ import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js import { loadConfig } from "../config/config.js"; import { writeConfigFile } from "../config/io.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; -import type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; import { danger, logVerbose, warn } from "../globals.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; @@ -26,7 +28,6 @@ import { normalizeAllowFromWithStore, type NormalizedAllowFrom, } from "./bot-access.js"; -import type { TelegramMediaRef } from "./bot-message-context.js"; import { RegisterTelegramHandlerParams } from "./bot-native-commands.js"; import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js"; import { resolveMedia } from "./bot/delivery.js"; @@ -36,7 +37,6 @@ import { resolveTelegramForumThreadId, resolveTelegramGroupAllowFromContext, } from "./bot/helpers.js"; -import type { TelegramContext } from "./bot/types.js"; import { evaluateTelegramGroupBaseAccess, evaluateTelegramGroupPolicyAccess, @@ -51,7 +51,6 @@ import { parseModelCallbackData, type ProviderInfo, } from "./model-buttons.js"; -import { getSentPoll } from "./poll-vote-cache.js"; import { buildInlineKeyboard } from "./send.js"; import { wasSentByBot } from "./sent-message-cache.js"; @@ -844,65 +843,6 @@ export const registerTelegramHandlers = ({ } }); - bot.on("poll_answer", async (ctx) => { - try { - if (shouldSkipUpdate(ctx)) { - return; - } - const pollAnswer = (ctx.update as { poll_answer?: unknown })?.poll_answer as - | { - poll_id?: string; - user?: { id?: number; username?: string; first_name?: string }; - option_ids?: number[]; - } - | undefined; - if (!pollAnswer) { - return; - } - const pollId = pollAnswer?.poll_id?.trim(); - if (!pollId) { - return; - } - const pollMeta = getSentPoll(pollId); - if (!pollMeta) { - return; - } - if (pollMeta.accountId && pollMeta.accountId !== accountId) { - return; - } - const userId = pollAnswer.user?.id; - if (typeof userId !== "number") { - return; - } - const optionIds = Array.isArray(pollAnswer.option_ids) ? pollAnswer.option_ids : []; - const selected = optionIds.map((id) => pollMeta.options[id] ?? `option#${id + 1}`); - const selectedText = selected.length > 0 ? selected.join(", ") : "(cleared vote)"; - const syntheticText = `Poll vote update: "${pollMeta.question}" -> ${selectedText}`; - const syntheticMessage = { - message_id: Date.now(), - date: Math.floor(Date.now() / 1000), - chat: { - id: Number(pollMeta.chatId), - type: String(pollMeta.chatId).startsWith("-") ? "supergroup" : "private", - }, - from: { - id: userId, - is_bot: false, - first_name: pollAnswer.user?.first_name ?? "User", - username: pollAnswer.user?.username, - }, - text: syntheticText, - } as unknown as Message; - const storeAllowFrom = await loadStoreAllowFrom(); - await processMessage(buildSyntheticContext(ctx, syntheticMessage), [], storeAllowFrom, { - forceWasMentioned: true, - messageIdOverride: `poll:${pollId}:${userId}:${Date.now()}`, - }); - } catch (err) { - runtime.error?.(danger(`poll_answer handler failed: ${String(err)}`)); - } - }); - // Handle group migration to supergroup (chat ID changes) bot.on("message:migrate_to_chat_id", async (ctx) => { try { diff --git a/src/telegram/poll-vote-cache.ts b/src/telegram/poll-vote-cache.ts deleted file mode 100644 index c65759985f..0000000000 --- a/src/telegram/poll-vote-cache.ts +++ /dev/null @@ -1,35 +0,0 @@ -const TTL_MS = 24 * 60 * 60 * 1000; - -export type TelegramSentPoll = { - pollId: string; - chatId: string; - question: string; - options: string[]; - accountId?: string; - createdAt: number; -}; - -const pollById = new Map(); - -function cleanupExpired() { - const now = Date.now(); - for (const [pollId, poll] of pollById) { - if (now - poll.createdAt > TTL_MS) { - pollById.delete(pollId); - } - } -} - -export function recordSentPoll(poll: Omit) { - cleanupExpired(); - pollById.set(poll.pollId, { ...poll, createdAt: Date.now() }); -} - -export function getSentPoll(pollId: string): TelegramSentPoll | undefined { - cleanupExpired(); - return pollById.get(pollId); -} - -export function clearSentPollCache() { - pollById.clear(); -} diff --git a/src/telegram/send.test.ts b/src/telegram/send.test.ts index f9f191289e..6c710925e1 100644 --- a/src/telegram/send.test.ts +++ b/src/telegram/send.test.ts @@ -1341,38 +1341,6 @@ describe("sendPollTelegram", () => { expect(sendPollMock.mock.calls[0]?.[3]).toMatchObject({ open_period: 60 }); }); - it("defaults polls to public (is_anonymous=false)", async () => { - const api = { - sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })), - }; - - await sendPollTelegram( - "123", - { question: "Q", options: ["A", "B"] }, - { token: "t", api: api as unknown as Bot["api"] }, - ); - - expect(api.sendPoll).toHaveBeenCalledTimes(1); - const sendPollMock = api.sendPoll as ReturnType; - expect(sendPollMock.mock.calls[0]?.[3]).toMatchObject({ is_anonymous: false }); - }); - - it("supports explicit anonymous polls", async () => { - const api = { - sendPoll: vi.fn(async () => ({ message_id: 123, chat: { id: 555 }, poll: { id: "p1" } })), - }; - - await sendPollTelegram( - "123", - { question: "Q", options: ["A", "B"] }, - { token: "t", api: api as unknown as Bot["api"], isAnonymous: true }, - ); - - expect(api.sendPoll).toHaveBeenCalledTimes(1); - const sendPollMock = api.sendPoll as ReturnType; - expect(sendPollMock.mock.calls[0]?.[3]).toMatchObject({ is_anonymous: true }); - }); - it("retries without message_thread_id on thread-not-found", async () => { const api = { sendPoll: vi.fn( diff --git a/src/telegram/send.ts b/src/telegram/send.ts index 4e37a24028..23e841f1a6 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -5,6 +5,8 @@ import type { ReactionTypeEmoji, } from "@grammyjs/types"; import { type ApiClientOptions, Bot, HttpError, InputFile } from "grammy"; +import type { RetryConfig } from "../infra/retry.js"; +import type { TelegramInlineButtons } from "./button-types.js"; import { loadConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { logVerbose } from "../globals.js"; @@ -12,7 +14,6 @@ import { recordChannelActivity } from "../infra/channel-activity.js"; import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js"; import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js"; import { createTelegramRetryRunner } from "../infra/retry-policy.js"; -import type { RetryConfig } from "../infra/retry.js"; import { redactSensitiveText } from "../logging/redact.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { mediaKindFromMime } from "../media/constants.js"; @@ -22,12 +23,10 @@ import { loadWebMedia } from "../web/media.js"; import { type ResolvedTelegramAccount, resolveTelegramAccount } from "./accounts.js"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { buildTelegramThreadParams } from "./bot/helpers.js"; -import type { TelegramInlineButtons } from "./button-types.js"; import { splitTelegramCaption } from "./caption.js"; import { resolveTelegramFetch } from "./fetch.js"; import { renderTelegramHtmlText } from "./format.js"; import { isRecoverableTelegramNetworkError } from "./network-errors.js"; -import { recordSentPoll } from "./poll-vote-cache.js"; import { makeProxyFetch } from "./proxy.js"; import { recordSentMessage } from "./sent-message-cache.js"; import { parseTelegramTarget, stripTelegramInternalPrefixes } from "./targets.js"; @@ -991,7 +990,7 @@ type TelegramPollOpts = { messageThreadId?: number; /** Send message silently (no notification). Defaults to false. */ silent?: boolean; - /** Whether votes are anonymous. Defaults to false (public poll). */ + /** Whether votes are anonymous. Defaults to true (Telegram default). */ isAnonymous?: boolean; }; @@ -1050,7 +1049,7 @@ export async function sendPollTelegram( // sendPoll(chat_id, question, options, other?, signal?) const pollParams = { allows_multiple_answers: normalizedPoll.maxSelections > 1, - is_anonymous: opts.isAnonymous ?? false, + is_anonymous: opts.isAnonymous ?? true, ...(durationSeconds !== undefined ? { open_period: durationSeconds } : {}), ...(Object.keys(threadParams).length > 0 ? threadParams : {}), ...(opts.silent === true ? { disable_notification: true } : {}), @@ -1073,15 +1072,6 @@ export async function sendPollTelegram( if (result?.message_id) { recordSentMessage(chatId, result.message_id); } - if (pollId) { - recordSentPoll({ - pollId, - chatId: resolvedChatId, - question: normalizedPoll.question, - options: normalizedPoll.options, - accountId: account.accountId, - }); - } recordChannelActivity({ channel: "telegram",