feat(status): warn on Discord message content intent
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
- Daemon runtime: remove Bun from selection options.
|
- Daemon runtime: remove Bun from selection options.
|
||||||
- CLI: restore hidden `gateway-daemon` alias for legacy launchd configs.
|
- CLI: restore hidden `gateway-daemon` alias for legacy launchd configs.
|
||||||
- Control UI: show skill install progress + per-skill results, hide install once binaries present. (#445) — thanks @pkrmf
|
- 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.
|
||||||
|
|
||||||
## 2026.1.8
|
## 2026.1.8
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ import {
|
|||||||
printWizardHeader,
|
printWizardHeader,
|
||||||
} from "./onboard-helpers.js";
|
} from "./onboard-helpers.js";
|
||||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||||
|
import { callGateway } from "../gateway/call.js";
|
||||||
|
import { collectProvidersStatusIssues } from "../infra/providers-status-issues.js";
|
||||||
|
|
||||||
function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
|
||||||
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
return cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||||
@@ -237,6 +239,30 @@ 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) {
|
||||||
|
note(
|
||||||
|
issues
|
||||||
|
.map(
|
||||||
|
(issue) =>
|
||||||
|
`- ${issue.provider} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
"Provider warnings",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore: doctor already reported gateway health
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!healthOk) {
|
if (!healthOk) {
|
||||||
const service = resolveGatewayService();
|
const service = resolveGatewayService();
|
||||||
const loaded = await service.isLoaded({ env: process.env });
|
const loaded = await service.isLoaded({ env: process.env });
|
||||||
|
|||||||
@@ -323,4 +323,20 @@ describe("providers command", () => {
|
|||||||
expect(whatsappIndex).toBeGreaterThan(-1);
|
expect(whatsappIndex).toBeGreaterThan(-1);
|
||||||
expect(telegramIndex).toBeLessThan(whatsappIndex);
|
expect(telegramIndex).toBeLessThan(whatsappIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("surfaces Discord privileged intent issues in providers status output", () => {
|
||||||
|
const lines = formatGatewayProvidersStatusLines({
|
||||||
|
discordAccounts: [
|
||||||
|
{
|
||||||
|
accountId: "default",
|
||||||
|
enabled: true,
|
||||||
|
configured: true,
|
||||||
|
application: { intents: { messageContent: "limited" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(lines.join("\n")).toMatch(/Warnings:/);
|
||||||
|
expect(lines.join("\n")).toMatch(/Message Content Intent is limited/i);
|
||||||
|
expect(lines.join("\n")).toMatch(/Run: clawdbot doctor/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
} from "../../imessage/accounts.js";
|
} from "../../imessage/accounts.js";
|
||||||
import { formatAge } from "../../infra/provider-summary.js";
|
import { formatAge } from "../../infra/provider-summary.js";
|
||||||
|
import { collectProvidersStatusIssues } from "../../infra/providers-status-issues.js";
|
||||||
import { listChatProviders } from "../../providers/registry.js";
|
import { listChatProviders } from "../../providers/registry.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
import {
|
import {
|
||||||
@@ -98,6 +99,17 @@ export function formatGatewayProvidersStatusLines(
|
|||||||
) {
|
) {
|
||||||
bits.push(`app:${account.appTokenSource}`);
|
bits.push(`app:${account.appTokenSource}`);
|
||||||
}
|
}
|
||||||
|
const application = account.application as
|
||||||
|
| { intents?: { messageContent?: string } }
|
||||||
|
| undefined;
|
||||||
|
const messageContent = application?.intents?.messageContent;
|
||||||
|
if (
|
||||||
|
typeof messageContent === "string" &&
|
||||||
|
messageContent.length > 0 &&
|
||||||
|
messageContent !== "enabled"
|
||||||
|
) {
|
||||||
|
bits.push(`intents:content=${messageContent}`);
|
||||||
|
}
|
||||||
if (typeof account.baseUrl === "string" && account.baseUrl) {
|
if (typeof account.baseUrl === "string" && account.baseUrl) {
|
||||||
bits.push(`url:${account.baseUrl}`);
|
bits.push(`url:${account.baseUrl}`);
|
||||||
}
|
}
|
||||||
@@ -150,6 +162,17 @@ export function formatGatewayProvidersStatusLines(
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
const issues = collectProvidersStatusIssues(payload);
|
||||||
|
if (issues.length > 0) {
|
||||||
|
lines.push(theme.warn("Warnings:"));
|
||||||
|
for (const issue of issues) {
|
||||||
|
lines.push(
|
||||||
|
`- ${issue.provider} ${issue.accountId}: ${issue.message}${issue.fix ? ` (${issue.fix})` : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(`- Run: clawdbot doctor`);
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
lines.push(
|
lines.push(
|
||||||
`Tip: ${formatDocsLink("/cli#status", "status --deep")} runs local probes without a gateway.`,
|
`Tip: ${formatDocsLink("/cli#status", "status --deep")} runs local probes without a gateway.`,
|
||||||
);
|
);
|
||||||
|
|||||||
42
src/discord/probe.intents.test.ts
Normal file
42
src/discord/probe.intents.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { resolveDiscordPrivilegedIntentsFromFlags } from "./probe.js";
|
||||||
|
|
||||||
|
describe("resolveDiscordPrivilegedIntentsFromFlags", () => {
|
||||||
|
it("reports disabled when no bits set", () => {
|
||||||
|
expect(resolveDiscordPrivilegedIntentsFromFlags(0)).toEqual({
|
||||||
|
presence: "disabled",
|
||||||
|
guildMembers: "disabled",
|
||||||
|
messageContent: "disabled",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports enabled when full intent bits set", () => {
|
||||||
|
const flags = (1 << 12) | (1 << 14) | (1 << 18);
|
||||||
|
expect(resolveDiscordPrivilegedIntentsFromFlags(flags)).toEqual({
|
||||||
|
presence: "enabled",
|
||||||
|
guildMembers: "enabled",
|
||||||
|
messageContent: "enabled",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports limited when limited intent bits set", () => {
|
||||||
|
const flags = (1 << 13) | (1 << 15) | (1 << 19);
|
||||||
|
expect(resolveDiscordPrivilegedIntentsFromFlags(flags)).toEqual({
|
||||||
|
presence: "limited",
|
||||||
|
guildMembers: "limited",
|
||||||
|
messageContent: "limited",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers enabled over limited when both set", () => {
|
||||||
|
const flags =
|
||||||
|
(1 << 12) | (1 << 13) | (1 << 14) | (1 << 15) | (1 << 18) | (1 << 19);
|
||||||
|
expect(resolveDiscordPrivilegedIntentsFromFlags(flags)).toEqual({
|
||||||
|
presence: "enabled",
|
||||||
|
guildMembers: "enabled",
|
||||||
|
messageContent: "enabled",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -8,8 +8,89 @@ export type DiscordProbe = {
|
|||||||
error?: string | null;
|
error?: string | null;
|
||||||
elapsedMs: number;
|
elapsedMs: number;
|
||||||
bot?: { id?: string | null; username?: string | null };
|
bot?: { id?: string | null; username?: string | null };
|
||||||
|
application?: DiscordApplicationSummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DiscordPrivilegedIntentStatus = "enabled" | "limited" | "disabled";
|
||||||
|
|
||||||
|
export type DiscordPrivilegedIntentsSummary = {
|
||||||
|
messageContent: DiscordPrivilegedIntentStatus;
|
||||||
|
guildMembers: DiscordPrivilegedIntentStatus;
|
||||||
|
presence: DiscordPrivilegedIntentStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordApplicationSummary = {
|
||||||
|
id?: string | null;
|
||||||
|
flags?: number | null;
|
||||||
|
intents?: DiscordPrivilegedIntentsSummary;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DISCORD_APP_FLAG_GATEWAY_PRESENCE = 1 << 12;
|
||||||
|
const DISCORD_APP_FLAG_GATEWAY_PRESENCE_LIMITED = 1 << 13;
|
||||||
|
const DISCORD_APP_FLAG_GATEWAY_GUILD_MEMBERS = 1 << 14;
|
||||||
|
const DISCORD_APP_FLAG_GATEWAY_GUILD_MEMBERS_LIMITED = 1 << 15;
|
||||||
|
const DISCORD_APP_FLAG_GATEWAY_MESSAGE_CONTENT = 1 << 18;
|
||||||
|
const DISCORD_APP_FLAG_GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19;
|
||||||
|
|
||||||
|
export function resolveDiscordPrivilegedIntentsFromFlags(
|
||||||
|
flags: number,
|
||||||
|
): DiscordPrivilegedIntentsSummary {
|
||||||
|
const resolve = (enabledBit: number, limitedBit: number) => {
|
||||||
|
if ((flags & enabledBit) !== 0) return "enabled";
|
||||||
|
if ((flags & limitedBit) !== 0) return "limited";
|
||||||
|
return "disabled";
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
presence: resolve(
|
||||||
|
DISCORD_APP_FLAG_GATEWAY_PRESENCE,
|
||||||
|
DISCORD_APP_FLAG_GATEWAY_PRESENCE_LIMITED,
|
||||||
|
),
|
||||||
|
guildMembers: resolve(
|
||||||
|
DISCORD_APP_FLAG_GATEWAY_GUILD_MEMBERS,
|
||||||
|
DISCORD_APP_FLAG_GATEWAY_GUILD_MEMBERS_LIMITED,
|
||||||
|
),
|
||||||
|
messageContent: resolve(
|
||||||
|
DISCORD_APP_FLAG_GATEWAY_MESSAGE_CONTENT,
|
||||||
|
DISCORD_APP_FLAG_GATEWAY_MESSAGE_CONTENT_LIMITED,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDiscordApplicationSummary(
|
||||||
|
token: string,
|
||||||
|
timeoutMs: number,
|
||||||
|
fetcher: typeof fetch = fetch,
|
||||||
|
): Promise<DiscordApplicationSummary | undefined> {
|
||||||
|
const normalized = normalizeDiscordToken(token);
|
||||||
|
if (!normalized) return undefined;
|
||||||
|
try {
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||||
|
timeoutMs,
|
||||||
|
fetcher,
|
||||||
|
{
|
||||||
|
Authorization: `Bot ${normalized}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!res.ok) return undefined;
|
||||||
|
const json = (await res.json()) as { id?: string; flags?: number };
|
||||||
|
const flags =
|
||||||
|
typeof json.flags === "number" && Number.isFinite(json.flags)
|
||||||
|
? json.flags
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
id: json.id ?? null,
|
||||||
|
flags: flags ?? null,
|
||||||
|
intents:
|
||||||
|
typeof flags === "number"
|
||||||
|
? resolveDiscordPrivilegedIntentsFromFlags(flags)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchWithTimeout(
|
async function fetchWithTimeout(
|
||||||
url: string,
|
url: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
@@ -28,8 +109,11 @@ async function fetchWithTimeout(
|
|||||||
export async function probeDiscord(
|
export async function probeDiscord(
|
||||||
token: string,
|
token: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
|
opts?: { fetcher?: typeof fetch; includeApplication?: boolean },
|
||||||
): Promise<DiscordProbe> {
|
): Promise<DiscordProbe> {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
|
const fetcher = opts?.fetcher ?? fetch;
|
||||||
|
const includeApplication = opts?.includeApplication === true;
|
||||||
const normalized = normalizeDiscordToken(token);
|
const normalized = normalizeDiscordToken(token);
|
||||||
const result: DiscordProbe = {
|
const result: DiscordProbe = {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -48,7 +132,7 @@ export async function probeDiscord(
|
|||||||
const res = await fetchWithTimeout(
|
const res = await fetchWithTimeout(
|
||||||
`${DISCORD_API_BASE}/users/@me`,
|
`${DISCORD_API_BASE}/users/@me`,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
fetch,
|
fetcher,
|
||||||
{
|
{
|
||||||
Authorization: `Bot ${normalized}`,
|
Authorization: `Bot ${normalized}`,
|
||||||
},
|
},
|
||||||
@@ -64,6 +148,11 @@ export async function probeDiscord(
|
|||||||
id: json.id ?? null,
|
id: json.id ?? null,
|
||||||
username: json.username ?? null,
|
username: json.username ?? null,
|
||||||
};
|
};
|
||||||
|
if (includeApplication) {
|
||||||
|
result.application =
|
||||||
|
(await fetchDiscordApplicationSummary(normalized, timeoutMs, fetcher)) ??
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
return { ...result, elapsedMs: Date.now() - started };
|
return { ...result, elapsedMs: Date.now() - started };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -130,7 +130,9 @@ export const providersHandlers: GatewayRequestHandlers = {
|
|||||||
let discordProbe: DiscordProbe | undefined;
|
let discordProbe: DiscordProbe | undefined;
|
||||||
let lastProbeAt: number | null = null;
|
let lastProbeAt: number | null = null;
|
||||||
if (probe && configured && account.enabled) {
|
if (probe && configured && account.enabled) {
|
||||||
discordProbe = await probeDiscord(account.token, timeoutMs);
|
discordProbe = await probeDiscord(account.token, timeoutMs, {
|
||||||
|
includeApplication: true,
|
||||||
|
});
|
||||||
lastProbeAt = Date.now();
|
lastProbeAt = Date.now();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -139,6 +141,8 @@ export const providersHandlers: GatewayRequestHandlers = {
|
|||||||
enabled: account.enabled,
|
enabled: account.enabled,
|
||||||
configured,
|
configured,
|
||||||
tokenSource: account.tokenSource,
|
tokenSource: account.tokenSource,
|
||||||
|
bot: rt?.bot ?? null,
|
||||||
|
application: rt?.application ?? null,
|
||||||
running: rt?.running ?? false,
|
running: rt?.running ?? false,
|
||||||
lastStartAt: rt?.lastStartAt ?? null,
|
lastStartAt: rt?.lastStartAt ?? null,
|
||||||
lastStopAt: rt?.lastStopAt ?? null,
|
lastStopAt: rt?.lastStopAt ?? null,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
resolveDiscordAccount,
|
resolveDiscordAccount,
|
||||||
} from "../discord/accounts.js";
|
} from "../discord/accounts.js";
|
||||||
import { monitorDiscordProvider } from "../discord/index.js";
|
import { monitorDiscordProvider } from "../discord/index.js";
|
||||||
|
import type { DiscordApplicationSummary, DiscordProbe } from "../discord/probe.js";
|
||||||
import { probeDiscord } from "../discord/probe.js";
|
import { probeDiscord } from "../discord/probe.js";
|
||||||
import { shouldLogVerbose } from "../globals.js";
|
import { shouldLogVerbose } from "../globals.js";
|
||||||
import {
|
import {
|
||||||
@@ -56,6 +57,8 @@ export type DiscordRuntimeStatus = {
|
|||||||
lastStartAt?: number | null;
|
lastStartAt?: number | null;
|
||||||
lastStopAt?: number | null;
|
lastStopAt?: number | null;
|
||||||
lastError?: string | null;
|
lastError?: string | null;
|
||||||
|
bot?: DiscordProbe["bot"];
|
||||||
|
application?: DiscordApplicationSummary;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SlackRuntimeStatus = {
|
export type SlackRuntimeStatus = {
|
||||||
@@ -194,6 +197,8 @@ export function createProviderManager(
|
|||||||
lastStartAt: null,
|
lastStartAt: null,
|
||||||
lastStopAt: null,
|
lastStopAt: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
|
bot: undefined,
|
||||||
|
application: undefined,
|
||||||
});
|
});
|
||||||
const defaultSlackStatus = (): SlackRuntimeStatus => ({
|
const defaultSlackStatus = (): SlackRuntimeStatus => ({
|
||||||
running: false,
|
running: false,
|
||||||
@@ -544,9 +549,24 @@ export function createProviderManager(
|
|||||||
}
|
}
|
||||||
let discordBotLabel = "";
|
let discordBotLabel = "";
|
||||||
try {
|
try {
|
||||||
const probe = await probeDiscord(token, 2500);
|
const probe = await probeDiscord(token, 2500, {
|
||||||
|
includeApplication: true,
|
||||||
|
});
|
||||||
const username = probe.ok ? probe.bot?.username?.trim() : null;
|
const username = probe.ok ? probe.bot?.username?.trim() : null;
|
||||||
if (username) discordBotLabel = ` (@${username})`;
|
if (username) discordBotLabel = ` (@${username})`;
|
||||||
|
const latest =
|
||||||
|
discordRuntimes.get(account.accountId) ?? defaultDiscordStatus();
|
||||||
|
discordRuntimes.set(account.accountId, {
|
||||||
|
...latest,
|
||||||
|
bot: probe.bot,
|
||||||
|
application: probe.application,
|
||||||
|
});
|
||||||
|
const messageContent = probe.application?.intents?.messageContent;
|
||||||
|
if (messageContent && messageContent !== "enabled") {
|
||||||
|
logDiscord.warn(
|
||||||
|
`[${account.accountId}] Discord Message Content Intent is ${messageContent}; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (shouldLogVerbose()) {
|
if (shouldLogVerbose()) {
|
||||||
logDiscord.debug(
|
logDiscord.debug(
|
||||||
|
|||||||
90
src/infra/providers-status-issues.ts
Normal file
90
src/infra/providers-status-issues.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export type ProviderStatusIssue = {
|
||||||
|
provider: "discord";
|
||||||
|
accountId: string;
|
||||||
|
kind: "intent" | "permissions" | "config";
|
||||||
|
message: string;
|
||||||
|
fix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscordIntentSummary = {
|
||||||
|
messageContent?: "enabled" | "limited" | "disabled";
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscordApplicationSummary = {
|
||||||
|
intents?: DiscordIntentSummary;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscordAccountStatus = {
|
||||||
|
accountId?: unknown;
|
||||||
|
enabled?: unknown;
|
||||||
|
configured?: unknown;
|
||||||
|
application?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
function asString(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" && value.trim().length > 0
|
||||||
|
? value.trim()
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDiscordAccountStatus(value: unknown): DiscordAccountStatus | null {
|
||||||
|
if (!isRecord(value)) return null;
|
||||||
|
return {
|
||||||
|
accountId: value.accountId,
|
||||||
|
enabled: value.enabled,
|
||||||
|
configured: value.configured,
|
||||||
|
application: value.application,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDiscordApplicationSummary(value: unknown): DiscordApplicationSummary {
|
||||||
|
if (!isRecord(value)) return {};
|
||||||
|
const intentsRaw = value.intents;
|
||||||
|
if (!isRecord(intentsRaw)) return {};
|
||||||
|
return {
|
||||||
|
intents: {
|
||||||
|
messageContent:
|
||||||
|
intentsRaw.messageContent === "enabled" ||
|
||||||
|
intentsRaw.messageContent === "limited" ||
|
||||||
|
intentsRaw.messageContent === "disabled"
|
||||||
|
? intentsRaw.messageContent
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectProvidersStatusIssues(
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
): ProviderStatusIssue[] {
|
||||||
|
const issues: ProviderStatusIssue[] = [];
|
||||||
|
const discordAccountsRaw = payload.discordAccounts;
|
||||||
|
if (!Array.isArray(discordAccountsRaw)) return issues;
|
||||||
|
|
||||||
|
for (const entry of discordAccountsRaw) {
|
||||||
|
const account = readDiscordAccountStatus(entry);
|
||||||
|
if (!account) continue;
|
||||||
|
const accountId = asString(account.accountId) ?? "default";
|
||||||
|
const enabled = account.enabled !== false;
|
||||||
|
const configured = account.configured === true;
|
||||||
|
if (!enabled || !configured) continue;
|
||||||
|
|
||||||
|
const app = readDiscordApplicationSummary(account.application);
|
||||||
|
const messageContent = app.intents?.messageContent;
|
||||||
|
if (messageContent && messageContent !== "enabled") {
|
||||||
|
issues.push({
|
||||||
|
provider: "discord",
|
||||||
|
accountId,
|
||||||
|
kind: "intent",
|
||||||
|
message: `Message Content Intent is ${messageContent}. Bot may not see normal channel messages.`,
|
||||||
|
fix: "Enable Message Content Intent in Discord Dev Portal → Bot → Privileged Gateway Intents, or require mention-only operation.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user