mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
feat(providers): improve doctor + status probes
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
- Control UI: show skill install progress + per-skill results, hide install once binaries present. (#445) — thanks @pkrmf
|
||||
- Providers/Doctor: surface Discord privileged intent (Message Content) misconfiguration with actionable warnings.
|
||||
- Providers/Doctor: warn when Telegram config expects unmentioned group messages but Bot API privacy mode is likely enabled; surface WhatsApp login/disconnect hints.
|
||||
- Providers/Doctor: add last inbound/outbound activity timestamps in `providers status` and extend `--probe` with Discord channel permission + Telegram group membership audits.
|
||||
- Apps: refresh iOS/Android/macOS app icons for Clawdbot branding. (#521) — thanks @fishfisher
|
||||
- Docs: expand parameter descriptions for agent/wake hooks. (#532) — thanks @mcinteerj
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ Manage chat provider accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage).
|
||||
|
||||
Subcommands:
|
||||
- `providers list`: show configured chat providers and auth profiles (Claude Code + Codex CLI OAuth sync included).
|
||||
- `providers status`: check gateway reachability and provider health (`--probe` to verify credentials; use `status --deep` for local-only probes).
|
||||
- `providers status`: check gateway reachability and provider health (`--probe` to verify credentials and run small provider audits; use `status --deep` for local-only probes).
|
||||
- Tip: `providers status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`).
|
||||
- `providers add`: wizard-style setup when no flags are passed; flags switch to non-interactive mode.
|
||||
- `providers remove`: disable by default; pass `--delete` to remove config entries without prompts.
|
||||
|
||||
@@ -9,6 +9,8 @@ When Clawdbot misbehaves, here's how to fix it.
|
||||
|
||||
Start with the FAQ’s [First 60 seconds](/start/faq#first-60-seconds-if-somethings-broken) if you just want a quick triage recipe. This page goes deeper on runtime failures and diagnostics.
|
||||
|
||||
Provider-specific shortcuts: [/providers/troubleshooting](/providers/troubleshooting)
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Service Installed but Nothing is Running
|
||||
|
||||
@@ -147,12 +147,14 @@ Notes:
|
||||
3. If nothing happens: check **Troubleshooting** below.
|
||||
|
||||
### Troubleshooting
|
||||
- First: run `clawdbot doctor` and `clawdbot providers status --probe` (actionable warnings + quick audits).
|
||||
- **“Used disallowed intents”**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway.
|
||||
- **Bot connects but never replies in a guild channel**:
|
||||
- Missing **Message Content Intent**, or
|
||||
- The bot lacks channel permissions (View/Send/Read History), or
|
||||
- Your config requires mentions and you didn’t mention it, or
|
||||
- Your guild/channel allowlist denies the channel/user.
|
||||
- **Permission audits** (`providers status --probe`) only check numeric channel IDs. If you use slugs/names as `discord.guilds.*.channels` keys, the audit can’t verify permissions.
|
||||
- **DMs don’t work**: `discord.dm.enabled=false`, `discord.dm.policy="disabled"`, or you haven’t been approved yet (`discord.dm.policy="pairing"`).
|
||||
|
||||
## Capabilities & limits
|
||||
|
||||
@@ -232,6 +232,7 @@ Outbound Telegram API calls retry on transient network/429 errors with exponenti
|
||||
- If you set `telegram.groups.*.requireMention=false`, Telegram’s Bot API **privacy mode** must be disabled.
|
||||
- BotFather: `/setprivacy` → **Disable** (then remove + re-add the bot to the group)
|
||||
- `clawdbot providers status` shows a warning when config expects unmentioned group messages.
|
||||
- `clawdbot providers status --probe` can additionally check membership for explicit numeric group IDs (it can’t audit wildcard `"*"` rules).
|
||||
- Quick test: `/activation always` (session-only; use config for persistence)
|
||||
|
||||
**Bot not seeing group messages at all:**
|
||||
|
||||
22
docs/providers/troubleshooting.md
Normal file
22
docs/providers/troubleshooting.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "Provider-specific troubleshooting shortcuts (Discord/Telegram/WhatsApp)"
|
||||
read_when:
|
||||
- A provider connects but messages don’t flow
|
||||
- Investigating provider misconfiguration (intents, permissions, privacy mode)
|
||||
---
|
||||
# Provider troubleshooting
|
||||
|
||||
Start with:
|
||||
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot providers status --probe
|
||||
```
|
||||
|
||||
`providers status --probe` prints warnings when it can detect common provider misconfigurations, and includes small live checks (credentials, some permissions/membership).
|
||||
|
||||
## Providers
|
||||
- Discord: [/providers/discord#troubleshooting](/providers/discord#troubleshooting)
|
||||
- Telegram: [/providers/telegram#troubleshooting](/providers/telegram#troubleshooting)
|
||||
- WhatsApp: [/providers/whatsapp#troubleshooting-quick](/providers/whatsapp#troubleshooting-quick)
|
||||
|
||||
@@ -240,15 +240,15 @@ export async function doctorCommand(
|
||||
}
|
||||
}
|
||||
|
||||
if (healthOk) {
|
||||
try {
|
||||
const status = await callGateway<Record<string, unknown>>({
|
||||
method: "providers.status",
|
||||
params: { probe: false, timeoutMs: 5000 },
|
||||
timeoutMs: 6000,
|
||||
});
|
||||
const issues = collectProvidersStatusIssues(status);
|
||||
if (issues.length > 0) {
|
||||
if (healthOk) {
|
||||
try {
|
||||
const status = await callGateway<Record<string, unknown>>({
|
||||
method: "providers.status",
|
||||
params: { probe: true, timeoutMs: 5000 },
|
||||
timeoutMs: 6000,
|
||||
});
|
||||
const issues = collectProvidersStatusIssues(status);
|
||||
if (issues.length > 0) {
|
||||
note(
|
||||
issues
|
||||
.map(
|
||||
|
||||
@@ -340,6 +340,31 @@ describe("providers command", () => {
|
||||
expect(lines.join("\n")).toMatch(/Run: clawdbot doctor/);
|
||||
});
|
||||
|
||||
it("surfaces Discord permission audit issues in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
discordAccounts: [
|
||||
{
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
audit: {
|
||||
unresolvedChannels: 1,
|
||||
channels: [
|
||||
{
|
||||
channelId: "111",
|
||||
ok: false,
|
||||
missing: ["ViewChannel", "SendMessages"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(lines.join("\n")).toMatch(/Warnings:/);
|
||||
expect(lines.join("\n")).toMatch(/permission audit/i);
|
||||
expect(lines.join("\n")).toMatch(/Channel 111/i);
|
||||
});
|
||||
|
||||
it("surfaces Telegram privacy-mode hints when allowUnmentionedGroups is enabled", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
telegramAccounts: [
|
||||
@@ -355,6 +380,28 @@ describe("providers command", () => {
|
||||
expect(lines.join("\n")).toMatch(/Telegram Bot API privacy mode/i);
|
||||
});
|
||||
|
||||
it("surfaces Telegram group membership audit issues in providers status output", () => {
|
||||
const lines = formatGatewayProvidersStatusLines({
|
||||
telegramAccounts: [
|
||||
{
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
audit: {
|
||||
hasWildcardUnmentionedGroups: true,
|
||||
unresolvedGroups: 1,
|
||||
groups: [
|
||||
{ chatId: "-1001", ok: false, status: "left", error: "not in group" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(lines.join("\n")).toMatch(/Warnings:/);
|
||||
expect(lines.join("\n")).toMatch(/membership probing is not possible/i);
|
||||
expect(lines.join("\n")).toMatch(/Group -1001/i);
|
||||
});
|
||||
|
||||
it("surfaces WhatsApp auth/runtime hints when unlinked or disconnected", () => {
|
||||
const unlinked = formatGatewayProvidersStatusLines({
|
||||
whatsappAccounts: [
|
||||
|
||||
@@ -78,6 +78,16 @@ export function formatGatewayProvidersStatusLines(
|
||||
if (typeof account.connected === "boolean") {
|
||||
bits.push(account.connected ? "connected" : "disconnected");
|
||||
}
|
||||
const inboundAt =
|
||||
typeof account.lastInboundAt === "number" && Number.isFinite(account.lastInboundAt)
|
||||
? account.lastInboundAt
|
||||
: null;
|
||||
const outboundAt =
|
||||
typeof account.lastOutboundAt === "number" && Number.isFinite(account.lastOutboundAt)
|
||||
? account.lastOutboundAt
|
||||
: null;
|
||||
if (inboundAt) bits.push(`in:${formatAge(Date.now() - inboundAt)}`);
|
||||
if (outboundAt) bits.push(`out:${formatAge(Date.now() - outboundAt)}`);
|
||||
if (typeof account.mode === "string" && account.mode.length > 0) {
|
||||
bits.push(`mode:${account.mode}`);
|
||||
}
|
||||
@@ -123,6 +133,10 @@ export function formatGatewayProvidersStatusLines(
|
||||
if (probe && typeof probe.ok === "boolean") {
|
||||
bits.push(probe.ok ? "works" : "probe failed");
|
||||
}
|
||||
const audit = account.audit as { ok?: boolean } | undefined;
|
||||
if (audit && typeof audit.ok === "boolean") {
|
||||
bits.push(audit.ok ? "audit ok" : "audit failed");
|
||||
}
|
||||
if (typeof account.lastError === "string" && account.lastError) {
|
||||
bits.push(`error:${account.lastError}`);
|
||||
}
|
||||
|
||||
52
src/discord/audit.test.ts
Normal file
52
src/discord/audit.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
fetchChannelPermissionsDiscord: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("discord audit", () => {
|
||||
it("collects numeric channel ids and counts unresolved keys", async () => {
|
||||
const { collectDiscordAuditChannelIds, auditDiscordChannelPermissions } =
|
||||
await import("./audit.js");
|
||||
const { fetchChannelPermissionsDiscord } = await import("./send.js");
|
||||
|
||||
const cfg = {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: "t",
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
"111": { allow: true },
|
||||
general: { allow: true },
|
||||
"222": { allow: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as import("../config/config.js").ClawdbotConfig;
|
||||
|
||||
const collected = collectDiscordAuditChannelIds({ cfg, accountId: "default" });
|
||||
expect(collected.channelIds).toEqual(["111"]);
|
||||
expect(collected.unresolvedChannels).toBe(1);
|
||||
|
||||
(fetchChannelPermissionsDiscord as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
channelId: "111",
|
||||
permissions: ["ViewChannel"],
|
||||
raw: "0",
|
||||
isDm: false,
|
||||
});
|
||||
|
||||
const audit = await auditDiscordChannelPermissions({
|
||||
token: "t",
|
||||
accountId: "default",
|
||||
channelIds: collected.channelIds,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
expect(audit.ok).toBe(false);
|
||||
expect(audit.channels[0]?.channelId).toBe("111");
|
||||
expect(audit.channels[0]?.missing).toContain("SendMessages");
|
||||
});
|
||||
});
|
||||
|
||||
122
src/discord/audit.ts
Normal file
122
src/discord/audit.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type {
|
||||
DiscordGuildChannelConfig,
|
||||
DiscordGuildEntry,
|
||||
} from "../config/types.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { fetchChannelPermissionsDiscord } from "./send.js";
|
||||
|
||||
export type DiscordChannelPermissionsAuditEntry = {
|
||||
channelId: string;
|
||||
ok: boolean;
|
||||
missing?: string[];
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export type DiscordChannelPermissionsAudit = {
|
||||
ok: boolean;
|
||||
checkedChannels: number;
|
||||
unresolvedChannels: number;
|
||||
channels: DiscordChannelPermissionsAuditEntry[];
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
const REQUIRED_CHANNEL_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) {
|
||||
if (!config) return true;
|
||||
if (config.allow === false) return false;
|
||||
if (config.enabled === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function listConfiguredGuildChannelKeys(
|
||||
guilds: Record<string, DiscordGuildEntry> | undefined,
|
||||
): string[] {
|
||||
if (!guilds) return [];
|
||||
const ids = new Set<string>();
|
||||
for (const entry of Object.values(guilds)) {
|
||||
if (!entry || typeof entry !== "object") continue;
|
||||
const channelsRaw = (entry as { channels?: unknown }).channels;
|
||||
if (!isRecord(channelsRaw)) continue;
|
||||
for (const [key, value] of Object.entries(channelsRaw)) {
|
||||
const channelId = String(key).trim();
|
||||
if (!channelId) continue;
|
||||
if (!shouldAuditChannelConfig(value as DiscordGuildChannelConfig | undefined))
|
||||
continue;
|
||||
ids.add(channelId);
|
||||
}
|
||||
}
|
||||
return [...ids].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function collectDiscordAuditChannelIds(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId?: string | null;
|
||||
}) {
|
||||
const account = resolveDiscordAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
const keys = listConfiguredGuildChannelKeys(account.config.guilds);
|
||||
const channelIds = keys.filter((key) => /^\d+$/.test(key));
|
||||
const unresolvedChannels = keys.length - channelIds.length;
|
||||
return { channelIds, unresolvedChannels };
|
||||
}
|
||||
|
||||
export async function auditDiscordChannelPermissions(params: {
|
||||
token: string;
|
||||
accountId?: string | null;
|
||||
channelIds: string[];
|
||||
timeoutMs: number;
|
||||
}): Promise<DiscordChannelPermissionsAudit> {
|
||||
const started = Date.now();
|
||||
const token = params.token?.trim() ?? "";
|
||||
if (!token || params.channelIds.length === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
checkedChannels: 0,
|
||||
unresolvedChannels: 0,
|
||||
channels: [],
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
}
|
||||
|
||||
const required = [...REQUIRED_CHANNEL_PERMISSIONS];
|
||||
const channels: DiscordChannelPermissionsAuditEntry[] = [];
|
||||
|
||||
for (const channelId of params.channelIds) {
|
||||
try {
|
||||
const perms = await fetchChannelPermissionsDiscord(channelId, {
|
||||
token,
|
||||
accountId: params.accountId ?? undefined,
|
||||
});
|
||||
const missing = required.filter((p) => !perms.permissions.includes(p));
|
||||
channels.push({
|
||||
channelId,
|
||||
ok: missing.length === 0,
|
||||
missing: missing.length ? missing : undefined,
|
||||
error: null,
|
||||
});
|
||||
} catch (err) {
|
||||
channels.push({
|
||||
channelId,
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: channels.every((c) => c.ok),
|
||||
checkedChannels: channels.length,
|
||||
unresolvedChannels: 0,
|
||||
channels,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { formatDurationSeconds } from "../infra/format-duration.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
import { recordProviderActivity } from "../infra/provider-activity.js";
|
||||
import { getChildLogger } from "../logging.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
import { saveMediaBuffer } from "../media/store.js";
|
||||
@@ -575,6 +576,11 @@ export function createDiscordMessageHandler(params: {
|
||||
}
|
||||
const botId = botUserId;
|
||||
const baseText = resolveDiscordMessageText(message);
|
||||
recordProviderActivity({
|
||||
provider: "discord",
|
||||
accountId,
|
||||
direction: "inbound",
|
||||
});
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
provider: "discord",
|
||||
|
||||
@@ -32,6 +32,7 @@ import { loadWebMedia, loadWebMediaRaw } from "../web/media.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { chunkDiscordText } from "./chunk.js";
|
||||
import { normalizeDiscordToken } from "./token.js";
|
||||
import { recordProviderActivity } from "../infra/provider-activity.js";
|
||||
|
||||
const DISCORD_TEXT_LIMIT = 2000;
|
||||
const DISCORD_MAX_STICKERS = 3;
|
||||
@@ -589,6 +590,11 @@ export async function sendMessageDiscord(
|
||||
});
|
||||
}
|
||||
|
||||
recordProviderActivity({
|
||||
provider: "discord",
|
||||
accountId: accountInfo.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
return {
|
||||
messageId: result.id ? String(result.id) : "unknown",
|
||||
channelId: String(result.channel_id ?? channelId),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import type { TelegramGroupConfig } from "../../config/types.js";
|
||||
import {
|
||||
loadConfig,
|
||||
readConfigFileSnapshot,
|
||||
@@ -51,6 +52,15 @@ import {
|
||||
} from "../protocol/index.js";
|
||||
import { formatForLog } from "../ws-log.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
import { getProviderActivity } from "../../infra/provider-activity.js";
|
||||
import {
|
||||
auditDiscordChannelPermissions,
|
||||
collectDiscordAuditChannelIds,
|
||||
} from "../../discord/audit.js";
|
||||
import {
|
||||
auditTelegramGroupMembership,
|
||||
collectTelegramUnmentionedGroupIds,
|
||||
} from "../../telegram/audit.js";
|
||||
|
||||
export const providersHandlers: GatewayRequestHandlers = {
|
||||
"providers.status": async ({ params, respond, context }) => {
|
||||
@@ -89,6 +99,16 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
const configured = Boolean(account.token);
|
||||
let telegramProbe: TelegramProbe | undefined;
|
||||
let lastProbeAt: number | null = null;
|
||||
const groups =
|
||||
cfg.telegram?.accounts?.[account.accountId]?.groups ??
|
||||
cfg.telegram?.groups;
|
||||
const { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups } =
|
||||
collectTelegramUnmentionedGroupIds(
|
||||
groups as Record<string, TelegramGroupConfig> | undefined,
|
||||
);
|
||||
let audit:
|
||||
| Awaited<ReturnType<typeof auditTelegramGroupMembership>>
|
||||
| undefined;
|
||||
if (probe && configured && account.enabled) {
|
||||
telegramProbe = await probeTelegram(
|
||||
account.token,
|
||||
@@ -96,10 +116,34 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
account.config.proxy,
|
||||
);
|
||||
lastProbeAt = Date.now();
|
||||
const botId =
|
||||
telegramProbe.ok && telegramProbe.bot?.id != null
|
||||
? telegramProbe.bot.id
|
||||
: null;
|
||||
if (botId && (groupIds.length > 0 || unresolvedGroups > 0)) {
|
||||
const auditRes = await auditTelegramGroupMembership({
|
||||
token: account.token,
|
||||
botId,
|
||||
groupIds,
|
||||
proxyUrl: account.config.proxy,
|
||||
timeoutMs,
|
||||
});
|
||||
audit = {
|
||||
...auditRes,
|
||||
unresolvedGroups,
|
||||
hasWildcardUnmentionedGroups,
|
||||
};
|
||||
} else if (unresolvedGroups > 0 || hasWildcardUnmentionedGroups) {
|
||||
audit = {
|
||||
ok: unresolvedGroups === 0 && !hasWildcardUnmentionedGroups,
|
||||
checkedGroups: 0,
|
||||
unresolvedGroups,
|
||||
hasWildcardUnmentionedGroups,
|
||||
groups: [],
|
||||
elapsedMs: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
const groups =
|
||||
cfg.telegram?.accounts?.[account.accountId]?.groups ??
|
||||
cfg.telegram?.groups;
|
||||
const allowUnmentionedGroups =
|
||||
Boolean(
|
||||
groups?.["*"] &&
|
||||
@@ -126,7 +170,16 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
lastError: rt?.lastError ?? null,
|
||||
probe: telegramProbe,
|
||||
lastProbeAt,
|
||||
audit,
|
||||
allowUnmentionedGroups,
|
||||
lastInboundAt: getProviderActivity({
|
||||
provider: "telegram",
|
||||
accountId: account.accountId,
|
||||
}).inboundAt,
|
||||
lastOutboundAt: getProviderActivity({
|
||||
provider: "telegram",
|
||||
accountId: account.accountId,
|
||||
}).outboundAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -146,11 +199,25 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
const configured = Boolean(account.token);
|
||||
let discordProbe: DiscordProbe | undefined;
|
||||
let lastProbeAt: number | null = null;
|
||||
const { channelIds: auditChannelIds, unresolvedChannels } =
|
||||
collectDiscordAuditChannelIds({ cfg, accountId: account.accountId });
|
||||
let audit:
|
||||
| Awaited<ReturnType<typeof auditDiscordChannelPermissions>>
|
||||
| undefined;
|
||||
if (probe && configured && account.enabled) {
|
||||
discordProbe = await probeDiscord(account.token, timeoutMs, {
|
||||
includeApplication: true,
|
||||
});
|
||||
lastProbeAt = Date.now();
|
||||
if (auditChannelIds.length > 0 || unresolvedChannels > 0) {
|
||||
const auditRes = await auditDiscordChannelPermissions({
|
||||
token: account.token,
|
||||
accountId: account.accountId,
|
||||
channelIds: auditChannelIds,
|
||||
timeoutMs,
|
||||
});
|
||||
audit = { ...auditRes, unresolvedChannels };
|
||||
}
|
||||
}
|
||||
return {
|
||||
accountId: account.accountId,
|
||||
@@ -166,6 +233,15 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
lastError: rt?.lastError ?? null,
|
||||
probe: discordProbe,
|
||||
lastProbeAt,
|
||||
audit,
|
||||
lastInboundAt: getProviderActivity({
|
||||
provider: "discord",
|
||||
accountId: account.accountId,
|
||||
}).inboundAt,
|
||||
lastOutboundAt: getProviderActivity({
|
||||
provider: "discord",
|
||||
accountId: account.accountId,
|
||||
}).outboundAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -323,6 +399,14 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
lastMessageAt: rt.lastMessageAt ?? null,
|
||||
lastEventAt: rt.lastEventAt ?? null,
|
||||
lastError: rt.lastError ?? null,
|
||||
lastInboundAt: getProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: account.accountId,
|
||||
}).inboundAt,
|
||||
lastOutboundAt: getProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: account.accountId,
|
||||
}).outboundAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
47
src/infra/provider-activity.test.ts
Normal file
47
src/infra/provider-activity.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
getProviderActivity,
|
||||
recordProviderActivity,
|
||||
resetProviderActivityForTest,
|
||||
} from "./provider-activity.js";
|
||||
|
||||
describe("provider activity", () => {
|
||||
beforeEach(() => {
|
||||
resetProviderActivityForTest();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-08T00:00:00Z"));
|
||||
});
|
||||
|
||||
it("records inbound/outbound separately", () => {
|
||||
recordProviderActivity({ provider: "telegram", direction: "inbound" });
|
||||
vi.advanceTimersByTime(1000);
|
||||
recordProviderActivity({ provider: "telegram", direction: "outbound" });
|
||||
const res = getProviderActivity({ provider: "telegram" });
|
||||
expect(res.inboundAt).toBe(1767830400000);
|
||||
expect(res.outboundAt).toBe(1767830401000);
|
||||
});
|
||||
|
||||
it("isolates accounts", () => {
|
||||
recordProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: "a",
|
||||
direction: "inbound",
|
||||
at: 1,
|
||||
});
|
||||
recordProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: "b",
|
||||
direction: "inbound",
|
||||
at: 2,
|
||||
});
|
||||
expect(getProviderActivity({ provider: "whatsapp", accountId: "a" })).toEqual({
|
||||
inboundAt: 1,
|
||||
outboundAt: null,
|
||||
});
|
||||
expect(getProviderActivity({ provider: "whatsapp", accountId: "b" })).toEqual({
|
||||
inboundAt: 2,
|
||||
outboundAt: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
53
src/infra/provider-activity.ts
Normal file
53
src/infra/provider-activity.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type ProviderId = "discord" | "telegram" | "whatsapp";
|
||||
export type ProviderDirection = "inbound" | "outbound";
|
||||
|
||||
type ActivityEntry = {
|
||||
inboundAt: number | null;
|
||||
outboundAt: number | null;
|
||||
};
|
||||
|
||||
const activity = new Map<string, ActivityEntry>();
|
||||
|
||||
function keyFor(provider: ProviderId, accountId: string) {
|
||||
return `${provider}:${accountId || "default"}`;
|
||||
}
|
||||
|
||||
function ensureEntry(provider: ProviderId, accountId: string): ActivityEntry {
|
||||
const key = keyFor(provider, accountId);
|
||||
const existing = activity.get(key);
|
||||
if (existing) return existing;
|
||||
const created: ActivityEntry = { inboundAt: null, outboundAt: null };
|
||||
activity.set(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
export function recordProviderActivity(params: {
|
||||
provider: ProviderId;
|
||||
accountId?: string | null;
|
||||
direction: ProviderDirection;
|
||||
at?: number;
|
||||
}) {
|
||||
const at = typeof params.at === "number" ? params.at : Date.now();
|
||||
const accountId = params.accountId?.trim() || "default";
|
||||
const entry = ensureEntry(params.provider, accountId);
|
||||
if (params.direction === "inbound") entry.inboundAt = at;
|
||||
if (params.direction === "outbound") entry.outboundAt = at;
|
||||
}
|
||||
|
||||
export function getProviderActivity(params: {
|
||||
provider: ProviderId;
|
||||
accountId?: string | null;
|
||||
}): ActivityEntry {
|
||||
const accountId = params.accountId?.trim() || "default";
|
||||
return (
|
||||
activity.get(keyFor(params.provider, accountId)) ?? {
|
||||
inboundAt: null,
|
||||
outboundAt: null,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function resetProviderActivityForTest() {
|
||||
activity.clear();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type DiscordAccountStatus = {
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
application?: unknown;
|
||||
audit?: unknown;
|
||||
};
|
||||
|
||||
type TelegramAccountStatus = {
|
||||
@@ -26,6 +27,7 @@ type TelegramAccountStatus = {
|
||||
enabled?: unknown;
|
||||
configured?: unknown;
|
||||
allowUnmentionedGroups?: unknown;
|
||||
audit?: unknown;
|
||||
};
|
||||
|
||||
type WhatsAppAccountStatus = {
|
||||
@@ -55,6 +57,7 @@ function readDiscordAccountStatus(value: unknown): DiscordAccountStatus | null {
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
application: value.application,
|
||||
audit: value.audit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +79,49 @@ function readDiscordApplicationSummary(
|
||||
};
|
||||
}
|
||||
|
||||
type DiscordPermissionsAuditSummary = {
|
||||
unresolvedChannels?: number;
|
||||
channels?: Array<{
|
||||
channelId: string;
|
||||
ok?: boolean;
|
||||
missing?: string[];
|
||||
error?: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
function readDiscordPermissionsAuditSummary(
|
||||
value: unknown,
|
||||
): DiscordPermissionsAuditSummary {
|
||||
if (!isRecord(value)) return {};
|
||||
const unresolvedChannels =
|
||||
typeof value.unresolvedChannels === "number" &&
|
||||
Number.isFinite(value.unresolvedChannels)
|
||||
? value.unresolvedChannels
|
||||
: undefined;
|
||||
const channelsRaw = value.channels;
|
||||
const channels = Array.isArray(channelsRaw)
|
||||
? (channelsRaw
|
||||
.map((entry) => {
|
||||
if (!isRecord(entry)) return null;
|
||||
const channelId = asString(entry.channelId);
|
||||
if (!channelId) return null;
|
||||
const ok = typeof entry.ok === "boolean" ? entry.ok : undefined;
|
||||
const missing = Array.isArray(entry.missing)
|
||||
? entry.missing.map((v) => asString(v)).filter(Boolean)
|
||||
: undefined;
|
||||
const error = asString(entry.error) ?? null;
|
||||
return {
|
||||
channelId,
|
||||
ok,
|
||||
missing: missing?.length ? missing : undefined,
|
||||
error,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as DiscordPermissionsAuditSummary["channels"])
|
||||
: undefined;
|
||||
return { unresolvedChannels, channels };
|
||||
}
|
||||
|
||||
function readTelegramAccountStatus(
|
||||
value: unknown,
|
||||
): TelegramAccountStatus | null {
|
||||
@@ -85,9 +131,51 @@ function readTelegramAccountStatus(
|
||||
enabled: value.enabled,
|
||||
configured: value.configured,
|
||||
allowUnmentionedGroups: value.allowUnmentionedGroups,
|
||||
audit: value.audit,
|
||||
};
|
||||
}
|
||||
|
||||
type TelegramGroupMembershipAuditSummary = {
|
||||
unresolvedGroups?: number;
|
||||
hasWildcardUnmentionedGroups?: boolean;
|
||||
groups?: Array<{
|
||||
chatId: string;
|
||||
ok?: boolean;
|
||||
status?: string | null;
|
||||
error?: string | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
function readTelegramGroupMembershipAuditSummary(
|
||||
value: unknown,
|
||||
): TelegramGroupMembershipAuditSummary {
|
||||
if (!isRecord(value)) return {};
|
||||
const unresolvedGroups =
|
||||
typeof value.unresolvedGroups === "number" &&
|
||||
Number.isFinite(value.unresolvedGroups)
|
||||
? value.unresolvedGroups
|
||||
: undefined;
|
||||
const hasWildcardUnmentionedGroups =
|
||||
typeof value.hasWildcardUnmentionedGroups === "boolean"
|
||||
? value.hasWildcardUnmentionedGroups
|
||||
: undefined;
|
||||
const groupsRaw = value.groups;
|
||||
const groups = Array.isArray(groupsRaw)
|
||||
? (groupsRaw
|
||||
.map((entry) => {
|
||||
if (!isRecord(entry)) return null;
|
||||
const chatId = asString(entry.chatId);
|
||||
if (!chatId) return null;
|
||||
const ok = typeof entry.ok === "boolean" ? entry.ok : undefined;
|
||||
const status = asString(entry.status) ?? null;
|
||||
const error = asString(entry.error) ?? null;
|
||||
return { chatId, ok, status, error };
|
||||
})
|
||||
.filter(Boolean) as TelegramGroupMembershipAuditSummary["groups"])
|
||||
: undefined;
|
||||
return { unresolvedGroups, hasWildcardUnmentionedGroups, groups };
|
||||
}
|
||||
|
||||
function readWhatsAppAccountStatus(
|
||||
value: unknown,
|
||||
): WhatsAppAccountStatus | null {
|
||||
@@ -107,6 +195,7 @@ export function collectProvidersStatusIssues(
|
||||
payload: Record<string, unknown>,
|
||||
): ProviderStatusIssue[] {
|
||||
const issues: ProviderStatusIssue[] = [];
|
||||
|
||||
const discordAccountsRaw = payload.discordAccounts;
|
||||
if (Array.isArray(discordAccountsRaw)) {
|
||||
for (const entry of discordAccountsRaw) {
|
||||
@@ -128,6 +217,31 @@ export function collectProvidersStatusIssues(
|
||||
fix: "Enable Message Content Intent in Discord Dev Portal → Bot → Privileged Gateway Intents, or require mention-only operation.",
|
||||
});
|
||||
}
|
||||
|
||||
const audit = readDiscordPermissionsAuditSummary(account.audit);
|
||||
if (audit.unresolvedChannels && audit.unresolvedChannels > 0) {
|
||||
issues.push({
|
||||
provider: "discord",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: `Some configured guild channels are not numeric IDs (unresolvedChannels=${audit.unresolvedChannels}). Permission audit can only check numeric channel IDs.`,
|
||||
fix: "Use numeric channel IDs as keys in discord.guilds.*.channels (then rerun providers status --probe).",
|
||||
});
|
||||
}
|
||||
for (const channel of audit.channels ?? []) {
|
||||
if (channel.ok === true) continue;
|
||||
const missing = channel.missing?.length
|
||||
? ` missing ${channel.missing.join(", ")}`
|
||||
: "";
|
||||
const error = channel.error ? `: ${channel.error}` : "";
|
||||
issues.push({
|
||||
provider: "discord",
|
||||
accountId,
|
||||
kind: "permissions",
|
||||
message: `Channel ${channel.channelId} permission check failed.${missing}${error}`,
|
||||
fix: "Ensure the bot role can view + send in this channel (and that channel overrides don't deny it).",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +254,7 @@ export function collectProvidersStatusIssues(
|
||||
const enabled = account.enabled !== false;
|
||||
const configured = account.configured === true;
|
||||
if (!enabled || !configured) continue;
|
||||
|
||||
if (account.allowUnmentionedGroups === true) {
|
||||
issues.push({
|
||||
provider: "telegram",
|
||||
@@ -150,6 +265,39 @@ export function collectProvidersStatusIssues(
|
||||
fix: "In BotFather run /setprivacy → Disable for this bot (then restart the gateway).",
|
||||
});
|
||||
}
|
||||
|
||||
const audit = readTelegramGroupMembershipAuditSummary(account.audit);
|
||||
if (audit.hasWildcardUnmentionedGroups === true) {
|
||||
issues.push({
|
||||
provider: "telegram",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message:
|
||||
'Telegram groups config uses "*" with requireMention=false; membership probing is not possible without explicit group IDs.',
|
||||
fix: "Add explicit numeric group ids under telegram.groups (or per-account groups) to enable probing.",
|
||||
});
|
||||
}
|
||||
if (audit.unresolvedGroups && audit.unresolvedGroups > 0) {
|
||||
issues.push({
|
||||
provider: "telegram",
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: `Some configured Telegram groups are not numeric IDs (unresolvedGroups=${audit.unresolvedGroups}). Membership probe can only check numeric group IDs.`,
|
||||
fix: "Use numeric chat IDs (e.g. -100...) as keys in telegram.groups for requireMention=false groups.",
|
||||
});
|
||||
}
|
||||
for (const group of audit.groups ?? []) {
|
||||
if (group.ok === true) continue;
|
||||
const status = group.status ? ` status=${group.status}` : "";
|
||||
const err = group.error ? `: ${group.error}` : "";
|
||||
issues.push({
|
||||
provider: "telegram",
|
||||
accountId,
|
||||
kind: "runtime",
|
||||
message: `Group ${group.chatId} not reachable by bot.${status}${err}`,
|
||||
fix: "Invite the bot to the group, then DM the bot once (/start) and restart the gateway.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,3 +343,4 @@ export function collectProvidersStatusIssues(
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
|
||||
66
src/telegram/audit.test.ts
Normal file
66
src/telegram/audit.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("telegram audit", () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("collects unmentioned numeric group ids and flags wildcard", async () => {
|
||||
const { collectTelegramUnmentionedGroupIds } = await import("./audit.js");
|
||||
const res = collectTelegramUnmentionedGroupIds({
|
||||
"*": { requireMention: false },
|
||||
"-1001": { requireMention: false },
|
||||
"@group": { requireMention: false },
|
||||
"-1002": { requireMention: true },
|
||||
"-1003": { requireMention: false, enabled: false },
|
||||
});
|
||||
expect(res.hasWildcardUnmentionedGroups).toBe(true);
|
||||
expect(res.groupIds).toEqual(["-1001"]);
|
||||
expect(res.unresolvedGroups).toBe(1);
|
||||
});
|
||||
|
||||
it("audits membership via getChatMember", async () => {
|
||||
const { auditTelegramGroupMembership } = await import("./audit.js");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ ok: true, result: { status: "member" } }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
const res = await auditTelegramGroupMembership({
|
||||
token: "t",
|
||||
botId: 123,
|
||||
groupIds: ["-1001"],
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.groups[0]?.chatId).toBe("-1001");
|
||||
expect(res.groups[0]?.status).toBe("member");
|
||||
});
|
||||
|
||||
it("reports bot not in group when status is left", async () => {
|
||||
const { auditTelegramGroupMembership } = await import("./audit.js");
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ ok: true, result: { status: "left" } }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
const res = await auditTelegramGroupMembership({
|
||||
token: "t",
|
||||
botId: 123,
|
||||
groupIds: ["-1001"],
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.groups[0]?.ok).toBe(false);
|
||||
expect(res.groups[0]?.status).toBe("left");
|
||||
});
|
||||
});
|
||||
|
||||
140
src/telegram/audit.ts
Normal file
140
src/telegram/audit.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { TelegramGroupConfig } from "../config/types.js";
|
||||
import { makeProxyFetch } from "./proxy.js";
|
||||
|
||||
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
||||
|
||||
export type TelegramGroupMembershipAuditEntry = {
|
||||
chatId: string;
|
||||
ok: boolean;
|
||||
status?: string | null;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export type TelegramGroupMembershipAudit = {
|
||||
ok: boolean;
|
||||
checkedGroups: number;
|
||||
unresolvedGroups: number;
|
||||
hasWildcardUnmentionedGroups: boolean;
|
||||
groups: TelegramGroupMembershipAuditEntry[];
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
type TelegramApiOk<T> = { ok: true; result: T };
|
||||
type TelegramApiErr = { ok: false; description?: string };
|
||||
|
||||
async function fetchWithTimeout(
|
||||
url: string,
|
||||
timeoutMs: number,
|
||||
fetcher: typeof fetch,
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetcher(url, { signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function collectTelegramUnmentionedGroupIds(
|
||||
groups: Record<string, TelegramGroupConfig> | undefined,
|
||||
) {
|
||||
if (!groups || typeof groups !== "object") {
|
||||
return {
|
||||
groupIds: [] as string[],
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
};
|
||||
}
|
||||
const hasWildcardUnmentionedGroups =
|
||||
Boolean(groups["*"]?.requireMention === false) &&
|
||||
groups["*"]?.enabled !== false;
|
||||
const groupIds: string[] = [];
|
||||
let unresolvedGroups = 0;
|
||||
for (const [key, value] of Object.entries(groups)) {
|
||||
if (key === "*") continue;
|
||||
if (!value || typeof value !== "object") continue;
|
||||
if ((value as TelegramGroupConfig).enabled === false) continue;
|
||||
if ((value as TelegramGroupConfig).requireMention !== false) continue;
|
||||
const id = String(key).trim();
|
||||
if (!id) continue;
|
||||
if (/^-?\d+$/.test(id)) {
|
||||
groupIds.push(id);
|
||||
} else {
|
||||
unresolvedGroups += 1;
|
||||
}
|
||||
}
|
||||
groupIds.sort((a, b) => a.localeCompare(b));
|
||||
return { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups };
|
||||
}
|
||||
|
||||
export async function auditTelegramGroupMembership(params: {
|
||||
token: string;
|
||||
botId: number;
|
||||
groupIds: string[];
|
||||
proxyUrl?: string;
|
||||
timeoutMs: number;
|
||||
}): Promise<TelegramGroupMembershipAudit> {
|
||||
const started = Date.now();
|
||||
const token = params.token?.trim() ?? "";
|
||||
if (!token || params.groupIds.length === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
checkedGroups: 0,
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
groups: [],
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
}
|
||||
|
||||
const fetcher = params.proxyUrl ? makeProxyFetch(params.proxyUrl) : fetch;
|
||||
const base = `${TELEGRAM_API_BASE}/bot${token}`;
|
||||
const groups: TelegramGroupMembershipAuditEntry[] = [];
|
||||
|
||||
for (const chatId of params.groupIds) {
|
||||
try {
|
||||
const url = `${base}/getChatMember?chat_id=${encodeURIComponent(chatId)}&user_id=${encodeURIComponent(String(params.botId))}`;
|
||||
const res = await fetchWithTimeout(url, params.timeoutMs, fetcher);
|
||||
const json = (await res.json()) as
|
||||
| TelegramApiOk<{ status?: string }>
|
||||
| TelegramApiErr
|
||||
| unknown;
|
||||
if (!res.ok || !isRecord(json) || json.ok !== true) {
|
||||
const desc =
|
||||
isRecord(json) && json.ok === false && typeof json.description === "string"
|
||||
? json.description
|
||||
: `getChatMember failed (${res.status})`;
|
||||
groups.push({ chatId, ok: false, status: null, error: desc });
|
||||
continue;
|
||||
}
|
||||
const status = isRecord((json as TelegramApiOk<unknown>).result)
|
||||
? (json as TelegramApiOk<{ status?: string }>).result.status ?? null
|
||||
: null;
|
||||
const ok =
|
||||
status === "creator" || status === "administrator" || status === "member";
|
||||
groups.push({ chatId, ok, status, error: ok ? null : "bot not in group" });
|
||||
} catch (err) {
|
||||
groups.push({
|
||||
chatId,
|
||||
ok: false,
|
||||
status: null,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: groups.every((g) => g.ok),
|
||||
checkedGroups: groups.length,
|
||||
unresolvedGroups: 0,
|
||||
hasWildcardUnmentionedGroups: false,
|
||||
groups,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import { recordProviderActivity } from "../infra/provider-activity.js";
|
||||
import { resolveTelegramAccount } from "./accounts.js";
|
||||
import { createTelegramDraftStream } from "./draft-stream.js";
|
||||
import { resolveTelegramFetch } from "./fetch.js";
|
||||
@@ -300,6 +301,11 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
storeAllowFrom: string[],
|
||||
) => {
|
||||
const msg = primaryCtx.message;
|
||||
recordProviderActivity({
|
||||
provider: "telegram",
|
||||
accountId: account.accountId,
|
||||
direction: "inbound",
|
||||
});
|
||||
const chatId = msg.chat.id;
|
||||
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
||||
const messageThreadId = (msg as { message_thread_id?: number })
|
||||
|
||||
@@ -5,6 +5,7 @@ import { loadConfig } from "../config/config.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import type { RetryConfig } from "../infra/retry.js";
|
||||
import { createTelegramRetryRunner } from "../infra/retry-policy.js";
|
||||
import { recordProviderActivity } from "../infra/provider-activity.js";
|
||||
import { mediaKindFromMime } from "../media/constants.js";
|
||||
import { isGifMedia } from "../media/mime.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
@@ -227,6 +228,11 @@ export async function sendMessageTelegram(
|
||||
});
|
||||
}
|
||||
const messageId = String(result?.message_id ?? "unknown");
|
||||
recordProviderActivity({
|
||||
provider: "telegram",
|
||||
accountId: account.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
return { messageId, chatId: String(result?.chat?.id ?? chatId) };
|
||||
}
|
||||
|
||||
@@ -263,6 +269,11 @@ export async function sendMessageTelegram(
|
||||
throw wrapChatNotFound(err);
|
||||
});
|
||||
const messageId = String(res?.message_id ?? "unknown");
|
||||
recordProviderActivity({
|
||||
provider: "telegram",
|
||||
accountId: account.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
return { messageId, chatId: String(res?.chat?.id ?? chatId) };
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { recordProviderActivity } from "../infra/provider-activity.js";
|
||||
import { createSubsystemLogger, getChildLogger } from "../logging.js";
|
||||
import { saveMediaBuffer } from "../media/store.js";
|
||||
import {
|
||||
@@ -171,6 +172,11 @@ export async function monitorWebInbox(options: {
|
||||
}) => {
|
||||
if (upsert.type !== "notify" && upsert.type !== "append") return;
|
||||
for (const msg of upsert.messages ?? []) {
|
||||
recordProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: options.accountId,
|
||||
direction: "inbound",
|
||||
});
|
||||
const id = msg.key?.id ?? undefined;
|
||||
// De-dupe on message id; Baileys can emit retries.
|
||||
if (id && seen.has(id)) continue;
|
||||
@@ -573,6 +579,11 @@ export async function monitorWebInbox(options: {
|
||||
payload = { text };
|
||||
}
|
||||
const result = await sock.sendMessage(jid, payload);
|
||||
recordProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: options.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
return { messageId: result?.key?.id ?? "unknown" };
|
||||
},
|
||||
/**
|
||||
@@ -591,6 +602,11 @@ export async function monitorWebInbox(options: {
|
||||
selectableCount: poll.maxSelections ?? 1,
|
||||
},
|
||||
});
|
||||
recordProviderActivity({
|
||||
provider: "whatsapp",
|
||||
accountId: options.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
return { messageId: result?.key?.id ?? "unknown" };
|
||||
},
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user