mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
feat: add cover image support to Discord event create (#60883)
* feat: add image param to Discord event create for cover art * fix: pass trusted media roots to event cover image loader * fix: solve lint error * fix: add changelog entry for Discord event cover image support (#60883) (thanks @bittoby) --------- Co-authored-by: Shadow <hi@shadowing.dev>
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/context engine: expose prompt-cache runtime context to context engines and keep current-turn prompt-cache usage aligned with the active attempt instead of stale prior-turn assistant state. (#62179) Thanks @jalehman.
|
||||
- Tools/media: document per-provider music and video generation capabilities, and add shared live video-to-video sweep coverage for providers that support local reference clips.
|
||||
- Compaction: add pluggable compaction provider registry so plugins can replace the built-in summarization pipeline. Configure via `agents.defaults.compaction.provider`; falls back to LLM summarization on provider failure. (#56224) Thanks @DhruvBhatia0.
|
||||
- Discord/events: allow `event-create` to accept a cover image URL or local file path, load and validate PNG/JPG/GIF event cover media, and pass the encoded image payload through Discord admin action/runtime paths. (#60883) Thanks @bittoby.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
type Ctx = Pick<
|
||||
ChannelMessageActionContext,
|
||||
"action" | "params" | "cfg" | "accountId" | "requesterSenderId"
|
||||
"action" | "params" | "cfg" | "accountId" | "requesterSenderId" | "mediaLocalRoots"
|
||||
>;
|
||||
|
||||
export async function tryHandleDiscordMessageActionGuildAdmin(params: {
|
||||
@@ -336,6 +336,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
|
||||
const channelId = readStringParam(actionParams, "channelId");
|
||||
const location = readStringParam(actionParams, "location");
|
||||
const entityType = readStringParam(actionParams, "eventType");
|
||||
const image = readStringParam(actionParams, "image", { trim: false });
|
||||
return await handleDiscordAction(
|
||||
{
|
||||
action: "eventCreate",
|
||||
@@ -348,8 +349,10 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
|
||||
channelId,
|
||||
location,
|
||||
entityType,
|
||||
image,
|
||||
},
|
||||
cfg,
|
||||
{ mediaLocalRoots: ctx.mediaLocalRoots },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
setChannelPermissionDiscord,
|
||||
uploadEmojiDiscord,
|
||||
uploadStickerDiscord,
|
||||
resolveEventCoverImage,
|
||||
} from "../send.js";
|
||||
import { readDiscordParentIdParam } from "./runtime.shared.js";
|
||||
|
||||
@@ -37,6 +38,7 @@ export const discordGuildActionRuntime = {
|
||||
addRoleDiscord,
|
||||
createChannelDiscord,
|
||||
createScheduledEventDiscord,
|
||||
resolveEventCoverImage,
|
||||
deleteChannelDiscord,
|
||||
editChannelDiscord,
|
||||
fetchChannelInfoDiscord,
|
||||
@@ -95,6 +97,7 @@ export async function handleDiscordGuildAction(
|
||||
params: Record<string, unknown>,
|
||||
isActionEnabled: ActionGate<DiscordActionConfig>,
|
||||
cfg?: OpenClawConfig,
|
||||
options?: { mediaLocalRoots?: readonly string[] },
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
const accountId = readStringParam(params, "accountId");
|
||||
switch (action) {
|
||||
@@ -299,8 +302,14 @@ export async function handleDiscordGuildAction(
|
||||
const description = readStringParam(params, "description");
|
||||
const channelId = readStringParam(params, "channelId");
|
||||
const location = readStringParam(params, "location");
|
||||
const imageUrl = readStringParam(params, "image", { trim: false });
|
||||
const entityTypeRaw = readStringParam(params, "entityType");
|
||||
const entityType = entityTypeRaw === "stage" ? 1 : entityTypeRaw === "external" ? 3 : 2;
|
||||
const image = imageUrl
|
||||
? await discordGuildActionRuntime.resolveEventCoverImage(imageUrl, {
|
||||
localRoots: options?.mediaLocalRoots,
|
||||
})
|
||||
: undefined;
|
||||
const payload = {
|
||||
name,
|
||||
description,
|
||||
@@ -309,6 +318,7 @@ export async function handleDiscordGuildAction(
|
||||
entity_type: entityType,
|
||||
channel_id: channelId,
|
||||
entity_metadata: entityType === 3 && location ? { location } : undefined,
|
||||
image,
|
||||
privacy_level: 2,
|
||||
};
|
||||
const event = accountId
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function handleDiscordAction(
|
||||
return await handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg);
|
||||
}
|
||||
if (guildActions.has(action)) {
|
||||
return await handleDiscordGuildAction(action, params, isActionEnabled, cfg);
|
||||
return await handleDiscordGuildAction(action, params, isActionEnabled, cfg, options);
|
||||
}
|
||||
if (moderationActions.has(action)) {
|
||||
return await handleDiscordModerationAction(action, params, isActionEnabled);
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
RESTPostAPIGuildScheduledEventJSONBody,
|
||||
} from "discord-api-types/v10";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import { loadWebMediaRaw } from "openclaw/plugin-sdk/web-media";
|
||||
import { resolveDiscordRest } from "./send.shared.js";
|
||||
import type {
|
||||
DiscordModerationTarget,
|
||||
@@ -14,6 +15,7 @@ import type {
|
||||
DiscordRoleChange,
|
||||
DiscordTimeoutTarget,
|
||||
} from "./send.types.js";
|
||||
import { DISCORD_MAX_EVENT_COVER_BYTES } from "./send.types.js";
|
||||
|
||||
export async function fetchMemberInfoDiscord(
|
||||
guildId: string,
|
||||
@@ -77,6 +79,25 @@ export async function listScheduledEventsDiscord(
|
||||
return (await rest.get(Routes.guildScheduledEvents(guildId))) as APIGuildScheduledEvent[];
|
||||
}
|
||||
|
||||
const ALLOWED_EVENT_COVER_TYPES = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]);
|
||||
|
||||
// Loads an image from a URL or path and returns a data URI suitable for the Discord API.
|
||||
export async function resolveEventCoverImage(
|
||||
imageUrl: string,
|
||||
opts?: { localRoots?: readonly string[] },
|
||||
): Promise<string> {
|
||||
const media = await loadWebMediaRaw(imageUrl, DISCORD_MAX_EVENT_COVER_BYTES, {
|
||||
localRoots: opts?.localRoots,
|
||||
});
|
||||
const contentType = media.contentType?.toLowerCase();
|
||||
if (!contentType || !ALLOWED_EVENT_COVER_TYPES.has(contentType)) {
|
||||
throw new Error(
|
||||
`Discord event cover images must be PNG, JPG, or GIF (got ${contentType ?? "unknown"})`,
|
||||
);
|
||||
}
|
||||
return `data:${contentType};base64,${media.buffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
export async function createScheduledEventDiscord(
|
||||
guildId: string,
|
||||
payload: RESTPostAPIGuildScheduledEventJSONBody,
|
||||
|
||||
@@ -15,6 +15,7 @@ export {
|
||||
addRoleDiscord,
|
||||
banMemberDiscord,
|
||||
createScheduledEventDiscord,
|
||||
resolveEventCoverImage,
|
||||
fetchChannelInfoDiscord,
|
||||
fetchMemberInfoDiscord,
|
||||
fetchRoleInfoDiscord,
|
||||
|
||||
@@ -22,6 +22,7 @@ export class DiscordSendError extends Error {
|
||||
|
||||
export const DISCORD_MAX_EMOJI_BYTES = 256 * 1024;
|
||||
export const DISCORD_MAX_STICKER_BYTES = 512 * 1024;
|
||||
export const DISCORD_MAX_EVENT_COVER_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
export type DiscordSendResult = {
|
||||
messageId: string;
|
||||
|
||||
@@ -283,6 +283,9 @@ function buildEventSchema() {
|
||||
endTime: Type.Optional(Type.String()),
|
||||
desc: Type.Optional(Type.String()),
|
||||
location: Type.Optional(Type.String()),
|
||||
image: Type.Optional(
|
||||
Type.String({ description: "Cover image URL or local file path for the event." }),
|
||||
),
|
||||
durationMin: Type.Optional(Type.Number()),
|
||||
until: Type.Optional(Type.String()),
|
||||
};
|
||||
|
||||
@@ -109,6 +109,7 @@ export function registerMessageDiscordAdminCommands(message: Command, helpers: M
|
||||
.option("--channel-id <id>", "Channel id")
|
||||
.option("--location <text>", "Event location")
|
||||
.option("--event-type <stage|external|voice>", "Event type")
|
||||
.option("--image <url>", "Cover image URL or local file path")
|
||||
.action(async (opts) => {
|
||||
await helpers.runMessageAction("event-create", opts);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user