mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 22:21:23 +00:00
discord: auto-create thread when sending to Forum/Media channels (#12380)
* discord: auto-create thread when sending to Forum/Media channels * Discord: harden forum thread sends (#12380) (thanks @magendary) * fix: clean up discord send exports (#12380) (thanks @magendary) --------- Co-authored-by: Shadow <shadow@clawd.bot>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { RequestClient } from "@buape/carbon";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import type { APIChannel } from "discord-api-types/v10";
|
||||
import { ChannelType, Routes } from "discord-api-types/v10";
|
||||
import type { RetryConfig } from "../infra/retry.js";
|
||||
import type { PollInput } from "../polls.js";
|
||||
import type { DiscordSendResult } from "./send.types.js";
|
||||
@@ -11,6 +12,7 @@ import { convertMarkdownTables } from "../markdown/tables.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import {
|
||||
buildDiscordSendError,
|
||||
buildDiscordTextChunks,
|
||||
createDiscordClient,
|
||||
normalizeDiscordPollInput,
|
||||
normalizeStickerIds,
|
||||
@@ -31,6 +33,24 @@ type DiscordSendOpts = {
|
||||
embeds?: unknown[];
|
||||
};
|
||||
|
||||
/** Discord thread names are capped at 100 characters. */
|
||||
const DISCORD_THREAD_NAME_LIMIT = 100;
|
||||
|
||||
/** Derive a thread title from the first non-empty line of the message text. */
|
||||
function deriveForumThreadName(text: string): string {
|
||||
const firstLine =
|
||||
text
|
||||
.split("\n")
|
||||
.find((l) => l.trim())
|
||||
?.trim() ?? "";
|
||||
return firstLine.slice(0, DISCORD_THREAD_NAME_LIMIT) || new Date().toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
/** Forum/Media channels cannot receive regular messages; detect them here. */
|
||||
function isForumLikeType(channelType?: number): boolean {
|
||||
return channelType === ChannelType.GuildForum || channelType === ChannelType.GuildMedia;
|
||||
}
|
||||
|
||||
export async function sendMessageDiscord(
|
||||
to: string,
|
||||
text: string,
|
||||
@@ -51,6 +71,113 @@ export async function sendMessageDiscord(
|
||||
const { token, rest, request } = createDiscordClient(opts, cfg);
|
||||
const recipient = await parseAndResolveRecipient(to, opts.accountId);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
|
||||
// Forum/Media channels reject POST /messages; auto-create a thread post instead.
|
||||
let channelType: number | undefined;
|
||||
try {
|
||||
const channel = (await rest.get(Routes.channel(channelId))) as APIChannel | undefined;
|
||||
channelType = channel?.type;
|
||||
} catch {
|
||||
// If we can't fetch the channel, fall through to the normal send path.
|
||||
}
|
||||
|
||||
if (isForumLikeType(channelType)) {
|
||||
const threadName = deriveForumThreadName(textWithTables);
|
||||
const chunks = buildDiscordTextChunks(textWithTables, {
|
||||
maxLinesPerMessage: accountInfo.config.maxLinesPerMessage,
|
||||
chunkMode,
|
||||
});
|
||||
const starterContent = chunks[0]?.trim() ? chunks[0] : threadName;
|
||||
const starterEmbeds = opts.embeds?.length ? opts.embeds : undefined;
|
||||
let threadRes: { id: string; message?: { id: string; channel_id: string } };
|
||||
try {
|
||||
threadRes = (await request(
|
||||
() =>
|
||||
rest.post(Routes.threads(channelId), {
|
||||
body: {
|
||||
name: threadName,
|
||||
message: {
|
||||
content: starterContent,
|
||||
...(starterEmbeds ? { embeds: starterEmbeds } : {}),
|
||||
},
|
||||
},
|
||||
}) as Promise<{ id: string; message?: { id: string; channel_id: string } }>,
|
||||
"forum-thread",
|
||||
)) as { id: string; message?: { id: string; channel_id: string } };
|
||||
} catch (err) {
|
||||
throw await buildDiscordSendError(err, {
|
||||
channelId,
|
||||
rest,
|
||||
token,
|
||||
hasMedia: Boolean(opts.mediaUrl),
|
||||
});
|
||||
}
|
||||
|
||||
const threadId = threadRes.id;
|
||||
const messageId = threadRes.message?.id ?? threadId;
|
||||
const resultChannelId = threadRes.message?.channel_id ?? threadId;
|
||||
const remainingChunks = chunks.slice(1);
|
||||
|
||||
try {
|
||||
if (opts.mediaUrl) {
|
||||
const [mediaCaption, ...afterMediaChunks] = remainingChunks;
|
||||
await sendDiscordMedia(
|
||||
rest,
|
||||
threadId,
|
||||
mediaCaption ?? "",
|
||||
opts.mediaUrl,
|
||||
undefined,
|
||||
request,
|
||||
accountInfo.config.maxLinesPerMessage,
|
||||
undefined,
|
||||
chunkMode,
|
||||
);
|
||||
for (const chunk of afterMediaChunks) {
|
||||
await sendDiscordText(
|
||||
rest,
|
||||
threadId,
|
||||
chunk,
|
||||
undefined,
|
||||
request,
|
||||
accountInfo.config.maxLinesPerMessage,
|
||||
undefined,
|
||||
chunkMode,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (const chunk of remainingChunks) {
|
||||
await sendDiscordText(
|
||||
rest,
|
||||
threadId,
|
||||
chunk,
|
||||
undefined,
|
||||
request,
|
||||
accountInfo.config.maxLinesPerMessage,
|
||||
undefined,
|
||||
chunkMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw await buildDiscordSendError(err, {
|
||||
channelId: threadId,
|
||||
rest,
|
||||
token,
|
||||
hasMedia: Boolean(opts.mediaUrl),
|
||||
});
|
||||
}
|
||||
|
||||
recordChannelActivity({
|
||||
channel: "discord",
|
||||
accountId: accountInfo.accountId,
|
||||
direction: "outbound",
|
||||
});
|
||||
return {
|
||||
messageId: messageId ? String(messageId) : "unknown",
|
||||
channelId: String(resultChannelId ?? channelId),
|
||||
};
|
||||
}
|
||||
|
||||
let result: { id: string; channel_id: string } | { id: string | null; channel_id: string };
|
||||
try {
|
||||
if (opts.mediaUrl) {
|
||||
|
||||
Reference in New Issue
Block a user