mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-03 03:03:24 -04:00
Handle Telegram poll vote updates for agent context
This commit is contained in:
@@ -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.
|
||||
|
||||
9
src/telegram/allowed-updates.test.ts
Normal file
9
src/telegram/allowed-updates.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
35
src/telegram/poll-vote-cache.ts
Normal file
35
src/telegram/poll-vote-cache.ts
Normal 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();
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user