mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(discord): harden partial thread channels
This commit is contained in:
@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord: harden inbound thread metadata handling against partial Carbon channel getters, so non-command thread messages and queued jobs no longer crash when `name`, `parentId`, `parent`, or `ownerId` requires fetched raw data.
|
||||
- Discord: let `message` tool reactions resolve `user:<id>` DM targets and preserve `channels.discord.guilds.<guild>.channels.<channel>.requireMention: false` during reply-stage activation fallback. Fixes #70165 and #69441.
|
||||
- Telegram/webhooks: lower the grammY webhook callback timeout to 5s so Telegram gets an early 200 response instead of retrying long-running updates as read timeouts. (#70146) Thanks @friday-james.
|
||||
- Telegram/polling: rebuild the polling HTTP transport after `getUpdates` 409 conflicts, so retries use a fresh TCP connection instead of looping on a Telegram-terminated keep-alive socket. (#69873) Thanks @hclsys.
|
||||
|
||||
@@ -53,8 +53,12 @@ export function resolveDiscordChannelParentIdSafe(channel: unknown): string | un
|
||||
return resolveDiscordChannelStringPropertySafe(channel, "parentId");
|
||||
}
|
||||
|
||||
export function resolveDiscordChannelParentSafe(channel: unknown): unknown {
|
||||
return readDiscordChannelPropertySafe(channel, "parent");
|
||||
}
|
||||
|
||||
export function resolveDiscordChannelInfoSafe(channel: unknown): DiscordChannelInfoSafe {
|
||||
const parent = readDiscordChannelPropertySafe(channel, "parent");
|
||||
const parent = resolveDiscordChannelParentSafe(channel);
|
||||
return {
|
||||
name: resolveDiscordChannelNameSafe(channel),
|
||||
topic: resolveDiscordChannelTopicSafe(channel),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Message } from "@buape/carbon";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js";
|
||||
import {
|
||||
buildDiscordInboundJob,
|
||||
materializeDiscordInboundJob,
|
||||
@@ -90,6 +91,33 @@ describe("buildDiscordInboundJob", () => {
|
||||
expect(() => JSON.stringify(job.payload)).not.toThrow();
|
||||
});
|
||||
|
||||
it("normalizes partial thread channels without reading throwing getters", async () => {
|
||||
const threadChannel = createPartialDiscordChannelWithThrowingGetters(
|
||||
{
|
||||
id: "thread-1",
|
||||
name: "codex",
|
||||
parentId: "forum-1",
|
||||
parent: { id: "forum-1", name: "Forum" },
|
||||
ownerId: "user-1",
|
||||
},
|
||||
["name", "parentId", "parent", "ownerId"],
|
||||
);
|
||||
const ctx = await createBaseDiscordMessageContext({
|
||||
threadChannel,
|
||||
});
|
||||
|
||||
const job = buildDiscordInboundJob(ctx);
|
||||
|
||||
expect(job.payload.threadChannel).toEqual({
|
||||
id: "thread-1",
|
||||
name: undefined,
|
||||
parentId: undefined,
|
||||
parent: undefined,
|
||||
ownerId: undefined,
|
||||
});
|
||||
expect(() => JSON.stringify(job.payload)).not.toThrow();
|
||||
});
|
||||
|
||||
it("re-materializes the process context with an overridden abort signal", async () => {
|
||||
const ctx = await createBaseDiscordMessageContext();
|
||||
const job = buildDiscordInboundJob(ctx, { replayKeys: ["default:ch-1:m-1"] });
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { resolveDiscordChannelNameSafe } from "./channel-access.js";
|
||||
import {
|
||||
resolveDiscordChannelIdSafe,
|
||||
resolveDiscordChannelInfoSafe,
|
||||
resolveDiscordChannelNameSafe,
|
||||
resolveDiscordChannelParentSafe,
|
||||
} from "./channel-access.js";
|
||||
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.types.js";
|
||||
|
||||
type DiscordInboundJobRuntimeField =
|
||||
@@ -102,16 +107,18 @@ function normalizeDiscordThreadChannel(
|
||||
if (!threadChannel) {
|
||||
return null;
|
||||
}
|
||||
const channelInfo = resolveDiscordChannelInfoSafe(threadChannel);
|
||||
const parent = resolveDiscordChannelParentSafe(threadChannel);
|
||||
return {
|
||||
id: threadChannel.id,
|
||||
name: threadChannel.name,
|
||||
parentId: threadChannel.parentId,
|
||||
parent: threadChannel.parent
|
||||
name: channelInfo.name,
|
||||
parentId: channelInfo.parentId,
|
||||
parent: parent
|
||||
? {
|
||||
id: threadChannel.parent.id,
|
||||
name: resolveDiscordChannelNameSafe(threadChannel.parent),
|
||||
id: resolveDiscordChannelIdSafe(parent),
|
||||
name: resolveDiscordChannelNameSafe(parent),
|
||||
}
|
||||
: undefined,
|
||||
ownerId: threadChannel.ownerId,
|
||||
ownerId: channelInfo.ownerId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChannelType } from "@buape/carbon";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js";
|
||||
|
||||
const transcribeFirstAudioMock = vi.hoisted(() => vi.fn());
|
||||
const resolveDiscordDmCommandAccessMock = vi.hoisted(() => vi.fn());
|
||||
@@ -757,6 +758,67 @@ describe("preflightDiscordMessage", () => {
|
||||
expect(result?.shouldRequireMention).toBe(false);
|
||||
});
|
||||
|
||||
it("handles partial thread channel owner getters during mention preflight", async () => {
|
||||
const threadId = "thread-partial-owner";
|
||||
const parentId = "parent-partial-owner";
|
||||
const message = createDiscordMessage({
|
||||
id: "m-thread-partial-owner",
|
||||
channelId: threadId,
|
||||
content: "thread hello",
|
||||
author: {
|
||||
id: "user-1",
|
||||
bot: false,
|
||||
username: "Peter",
|
||||
},
|
||||
});
|
||||
Object.defineProperty(message, "channel", {
|
||||
value: createPartialDiscordChannelWithThrowingGetters(
|
||||
{
|
||||
id: threadId,
|
||||
isThread: () => true,
|
||||
ownerId: "owner-1",
|
||||
parentId,
|
||||
parent: { id: parentId, name: "general" },
|
||||
},
|
||||
["ownerId", "parentId", "parent"],
|
||||
),
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
const result = await preflightDiscordMessage({
|
||||
...createPreflightArgs({
|
||||
cfg: DEFAULT_PREFLIGHT_CFG,
|
||||
discordConfig: {} as DiscordConfig,
|
||||
data: createGuildEvent({
|
||||
channelId: threadId,
|
||||
guildId: "guild-1",
|
||||
author: message.author,
|
||||
message,
|
||||
includeGuildObject: false,
|
||||
}),
|
||||
client: createThreadClient({
|
||||
threadId,
|
||||
parentId,
|
||||
}),
|
||||
}),
|
||||
guildEntries: {
|
||||
"guild-1": {
|
||||
channels: {
|
||||
[parentId]: {
|
||||
enabled: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.threadParentId).toBe(parentId);
|
||||
expect(result?.shouldRequireMention).toBe(false);
|
||||
});
|
||||
|
||||
it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => {
|
||||
const channelId = "channel-other-mention-1";
|
||||
const guildId = "guild-other-mention-1";
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
resolveDiscordShouldRequireMention,
|
||||
resolveGroupDmAllow,
|
||||
} from "./allow-list.js";
|
||||
import { resolveDiscordChannelNameSafe } from "./channel-access.js";
|
||||
import { resolveDiscordChannelInfoSafe, resolveDiscordChannelNameSafe } from "./channel-access.js";
|
||||
import { resolveDiscordDmCommandAccess } from "./dm-command-auth.js";
|
||||
import { handleDiscordDmCommandDecision } from "./dm-command-decision.js";
|
||||
import {
|
||||
@@ -849,7 +849,9 @@ export async function preflightDiscordMessage(
|
||||
} satisfies HistoryEntry)
|
||||
: undefined;
|
||||
|
||||
const threadOwnerId = threadChannel ? (threadChannel.ownerId ?? channelInfo?.ownerId) : undefined;
|
||||
const threadOwnerId = threadChannel
|
||||
? (resolveDiscordChannelInfoSafe(threadChannel).ownerId ?? channelInfo?.ownerId)
|
||||
: undefined;
|
||||
const shouldRequireMentionByConfig = resolveDiscordShouldRequireMention({
|
||||
isGuildMessage,
|
||||
isThread: Boolean(threadChannel),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChannelType } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js";
|
||||
import { __resetDiscordChannelInfoCacheForTest } from "./message-utils.js";
|
||||
import { resolveDiscordThreadParentInfo } from "./threading.js";
|
||||
|
||||
@@ -50,6 +51,50 @@ describe("resolveDiscordThreadParentInfo", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to fetched thread parentId when partial channel getters throw", async () => {
|
||||
const fetchChannel = vi.fn(async (channelId: string) => {
|
||||
if (channelId === "thread-1") {
|
||||
return {
|
||||
id: "thread-1",
|
||||
type: ChannelType.PublicThread,
|
||||
name: "thread-name",
|
||||
parentId: "parent-1",
|
||||
};
|
||||
}
|
||||
if (channelId === "parent-1") {
|
||||
return {
|
||||
id: "parent-1",
|
||||
type: ChannelType.GuildText,
|
||||
name: "parent-name",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const client = { fetchChannel } as unknown as import("@buape/carbon").Client;
|
||||
const threadChannel = createPartialDiscordChannelWithThrowingGetters(
|
||||
{
|
||||
id: "thread-1",
|
||||
parent: { id: "stale-parent", name: "stale-parent-name" },
|
||||
},
|
||||
["parentId", "parent"],
|
||||
);
|
||||
|
||||
const result = await resolveDiscordThreadParentInfo({
|
||||
client,
|
||||
threadChannel,
|
||||
channelInfo: null,
|
||||
});
|
||||
|
||||
expect(fetchChannel).toHaveBeenCalledWith("thread-1");
|
||||
expect(fetchChannel).toHaveBeenCalledWith("parent-1");
|
||||
expect(result).toEqual({
|
||||
id: "parent-1",
|
||||
name: "parent-name",
|
||||
type: ChannelType.GuildText,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fetch thread info when parentId is already present", async () => {
|
||||
const fetchChannel = vi.fn(async (channelId: string) => {
|
||||
if (channelId === "parent-1") {
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
truncateUtf16Safe,
|
||||
} from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { DiscordChannelConfigResolved } from "./allow-list.js";
|
||||
import { resolveDiscordChannelNameSafe } from "./channel-access.js";
|
||||
import {
|
||||
resolveDiscordChannelIdSafe,
|
||||
resolveDiscordChannelNameSafe,
|
||||
resolveDiscordChannelParentIdSafe,
|
||||
resolveDiscordChannelParentSafe,
|
||||
} from "./channel-access.js";
|
||||
import {
|
||||
resolveDiscordChannelInfo,
|
||||
resolveDiscordEmbedText,
|
||||
@@ -199,8 +204,12 @@ export async function resolveDiscordThreadParentInfo(params: {
|
||||
channelInfo: import("./message-utils.js").DiscordChannelInfo | null;
|
||||
}): Promise<DiscordThreadParentInfo> {
|
||||
const { threadChannel, channelInfo, client } = params;
|
||||
const parent = resolveDiscordChannelParentSafe(threadChannel);
|
||||
let parentId =
|
||||
threadChannel.parentId ?? threadChannel.parent?.id ?? channelInfo?.parentId ?? undefined;
|
||||
resolveDiscordChannelParentIdSafe(threadChannel) ??
|
||||
resolveDiscordChannelIdSafe(parent) ??
|
||||
channelInfo?.parentId ??
|
||||
undefined;
|
||||
if (!parentId && threadChannel.id) {
|
||||
const threadInfo = await resolveDiscordChannelInfo(client, threadChannel.id);
|
||||
parentId = threadInfo?.parentId ?? undefined;
|
||||
@@ -208,7 +217,7 @@ export async function resolveDiscordThreadParentInfo(params: {
|
||||
if (!parentId) {
|
||||
return {};
|
||||
}
|
||||
let parentName = resolveDiscordChannelNameSafe(threadChannel.parent);
|
||||
let parentName = resolveDiscordChannelNameSafe(parent);
|
||||
const parentInfo = await resolveDiscordChannelInfo(client, parentId);
|
||||
parentName = parentName ?? parentInfo?.name;
|
||||
const parentType = parentInfo?.type;
|
||||
|
||||
Reference in New Issue
Block a user