Handle Telegram poll vote updates for agent context

This commit is contained in:
Krish
2026-02-16 21:19:54 +05:30
committed by Peter Steinberger
parent 5cbfaf5cc7
commit 0a02b91638
6 changed files with 139 additions and 1 deletions

View File

@@ -70,6 +70,7 @@ 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:
@@ -89,6 +90,25 @@ 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)
@@ -133,6 +153,7 @@ Mattermost supports multiple accounts under `channels.mattermost.accounts`:
## Troubleshooting
- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
- 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"`).
- 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.

View File

@@ -0,0 +1,9 @@
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");
});
});

View File

@@ -4,6 +4,9 @@ type TelegramUpdateType = (typeof API_CONSTANTS.ALL_UPDATE_TYPES)[number];
export function resolveTelegramAllowedUpdates(): ReadonlyArray<TelegramUpdateType> {
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");
}

View File

@@ -50,6 +50,7 @@ import {
parseModelCallbackData,
type ProviderInfo,
} from "./model-buttons.js";
import { getSentPoll } from "./poll-vote-cache.js";
import { buildInlineKeyboard } from "./send.js";
export const registerTelegramHandlers = ({
@@ -749,6 +750,65 @@ 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 {

View File

@@ -0,0 +1,35 @@
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<string, TelegramSentPoll>();
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<TelegramSentPoll, "createdAt">) {
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();
}

View File

@@ -27,6 +27,7 @@ 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";
@@ -1055,6 +1056,15 @@ 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",