Discord: tools for uploading emojis and stickers!
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: discord
|
name: discord
|
||||||
description: Use when you need to control Discord from Clawdis via the discord tool: send messages, react, post stickers, run polls, manage threads/pins/search, fetch permissions or member/role/channel info, or handle moderation actions in Discord DMs or channels.
|
description: Use when you need to control Discord from Clawdis via the discord tool: send messages, react, post or upload stickers, upload emojis, run polls, manage threads/pins/search, fetch permissions or member/role/channel info, or handle moderation actions in Discord DMs or channels.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Discord Actions
|
# Discord Actions
|
||||||
@@ -15,6 +15,8 @@ Use `discord` to manage messages, reactions, threads, polls, and moderation. You
|
|||||||
- For stickers/polls/sendMessage: a `to` target (`channel:<id>` or `user:<id>`). Optional `content` text.
|
- For stickers/polls/sendMessage: a `to` target (`channel:<id>` or `user:<id>`). Optional `content` text.
|
||||||
- Polls also need a `question` plus 2–10 `answers`.
|
- Polls also need a `question` plus 2–10 `answers`.
|
||||||
- For media: `mediaUrl` with `file:///path` for local files or `https://...` for remote.
|
- For media: `mediaUrl` with `file:///path` for local files or `https://...` for remote.
|
||||||
|
- For emoji uploads: `guildId`, `name`, `mediaUrl`, optional `roleIds` (limit 256KB, PNG/JPG/GIF).
|
||||||
|
- For sticker uploads: `guildId`, `name`, `description`, `tags`, `mediaUrl` (limit 512KB, PNG/APNG/Lottie JSON).
|
||||||
|
|
||||||
Message context lines include `discord message id` and `channel` fields you can reuse directly.
|
Message context lines include `discord message id` and `channel` fields you can reuse directly.
|
||||||
|
|
||||||
@@ -58,6 +60,37 @@ Message context lines include `discord message id` and `channel` fields you can
|
|||||||
- Up to 3 sticker IDs per message.
|
- Up to 3 sticker IDs per message.
|
||||||
- `to` can be `user:<id>` for DMs.
|
- `to` can be `user:<id>` for DMs.
|
||||||
|
|
||||||
|
### Upload a custom emoji
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "emojiUpload",
|
||||||
|
"guildId": "999",
|
||||||
|
"name": "party_blob",
|
||||||
|
"mediaUrl": "file:///tmp/party.png",
|
||||||
|
"roleIds": ["222"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Emoji images must be PNG/JPG/GIF and <= 256KB.
|
||||||
|
- `roleIds` is optional; omit to make the emoji available to everyone.
|
||||||
|
|
||||||
|
### Upload a sticker
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "stickerUpload",
|
||||||
|
"guildId": "999",
|
||||||
|
"name": "clawdis_wave",
|
||||||
|
"description": "Clawdis waving hello",
|
||||||
|
"tags": "👋",
|
||||||
|
"mediaUrl": "file:///tmp/wave.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Stickers require `name`, `description`, and `tags`.
|
||||||
|
- Uploads must be PNG/APNG/Lottie JSON and <= 512KB.
|
||||||
|
|
||||||
### Create a poll
|
### Create a poll
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -88,6 +121,7 @@ Message context lines include `discord message id` and `channel` fields you can
|
|||||||
- React with ✅/⚠️ to mark status updates.
|
- React with ✅/⚠️ to mark status updates.
|
||||||
- Post a quick poll for release decisions or meeting times.
|
- Post a quick poll for release decisions or meeting times.
|
||||||
- Send celebratory stickers after successful deploys.
|
- Send celebratory stickers after successful deploys.
|
||||||
|
- Upload new emojis/stickers for release moments.
|
||||||
- Run weekly “priority check” polls in team channels.
|
- Run weekly “priority check” polls in team channels.
|
||||||
- DM stickers as acknowledgements when a user’s request is completed.
|
- DM stickers as acknowledgements when a user’s request is completed.
|
||||||
|
|
||||||
@@ -96,6 +130,7 @@ Message context lines include `discord message id` and `channel` fields you can
|
|||||||
Use `discord.actions.*` to disable action groups:
|
Use `discord.actions.*` to disable action groups:
|
||||||
- `reactions` (react + reactions list + emojiList)
|
- `reactions` (react + reactions list + emojiList)
|
||||||
- `stickers`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
|
- `stickers`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
|
||||||
|
- `emojiUploads`, `stickerUploads`
|
||||||
- `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events`
|
- `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events`
|
||||||
- `roles` (role add/remove, default `false`)
|
- `roles` (role add/remove, default `false`)
|
||||||
- `moderation` (timeout/kick/ban, default `false`)
|
- `moderation` (timeout/kick/ban, default `false`)
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ import {
|
|||||||
sendStickerDiscord,
|
sendStickerDiscord,
|
||||||
timeoutMemberDiscord,
|
timeoutMemberDiscord,
|
||||||
unpinMessageDiscord,
|
unpinMessageDiscord,
|
||||||
|
uploadEmojiDiscord,
|
||||||
|
uploadStickerDiscord,
|
||||||
} from "../discord/send.js";
|
} from "../discord/send.js";
|
||||||
import { callGateway } from "../gateway/call.js";
|
import { callGateway } from "../gateway/call.js";
|
||||||
import { detectMime, imageMimeFromFormat } from "../media/mime.js";
|
import { detectMime, imageMimeFromFormat } from "../media/mime.js";
|
||||||
@@ -1845,6 +1847,21 @@ const DiscordToolSchema = Type.Union([
|
|||||||
action: Type.Literal("emojiList"),
|
action: Type.Literal("emojiList"),
|
||||||
guildId: Type.String(),
|
guildId: Type.String(),
|
||||||
}),
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("emojiUpload"),
|
||||||
|
guildId: Type.String(),
|
||||||
|
name: Type.String(),
|
||||||
|
mediaUrl: Type.String(),
|
||||||
|
roleIds: Type.Optional(Type.Array(Type.String())),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("stickerUpload"),
|
||||||
|
guildId: Type.String(),
|
||||||
|
name: Type.String(),
|
||||||
|
description: Type.String(),
|
||||||
|
tags: Type.String(),
|
||||||
|
mediaUrl: Type.String(),
|
||||||
|
}),
|
||||||
Type.Object({
|
Type.Object({
|
||||||
action: Type.Literal("roleAdd"),
|
action: Type.Literal("roleAdd"),
|
||||||
guildId: Type.String(),
|
guildId: Type.String(),
|
||||||
@@ -2260,6 +2277,50 @@ function createDiscordTool(): AnyAgentTool {
|
|||||||
const emojis = await listGuildEmojisDiscord(guildId);
|
const emojis = await listGuildEmojisDiscord(guildId);
|
||||||
return jsonResult({ ok: true, emojis });
|
return jsonResult({ ok: true, emojis });
|
||||||
}
|
}
|
||||||
|
case "emojiUpload": {
|
||||||
|
if (!isActionEnabled("emojiUploads")) {
|
||||||
|
throw new Error("Discord emoji uploads are disabled.");
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name", { required: true });
|
||||||
|
const mediaUrl = readStringParam(params, "mediaUrl", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const roleIds = readStringArrayParam(params, "roleIds");
|
||||||
|
const emoji = await uploadEmojiDiscord({
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
mediaUrl,
|
||||||
|
roleIds: roleIds?.length ? roleIds : undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, emoji });
|
||||||
|
}
|
||||||
|
case "stickerUpload": {
|
||||||
|
if (!isActionEnabled("stickerUploads")) {
|
||||||
|
throw new Error("Discord sticker uploads are disabled.");
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name", { required: true });
|
||||||
|
const description = readStringParam(params, "description", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const tags = readStringParam(params, "tags", { required: true });
|
||||||
|
const mediaUrl = readStringParam(params, "mediaUrl", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const sticker = await uploadStickerDiscord({
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
mediaUrl,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, sticker });
|
||||||
|
}
|
||||||
case "roleAdd": {
|
case "roleAdd": {
|
||||||
if (!isActionEnabled("roles", false)) {
|
if (!isActionEnabled("roles", false)) {
|
||||||
throw new Error("Discord role changes are disabled.");
|
throw new Error("Discord role changes are disabled.");
|
||||||
|
|||||||
@@ -196,6 +196,8 @@
|
|||||||
"memberInfo": { "label": "member", "detailKeys": ["guildId", "userId"] },
|
"memberInfo": { "label": "member", "detailKeys": ["guildId", "userId"] },
|
||||||
"roleInfo": { "label": "roles", "detailKeys": ["guildId"] },
|
"roleInfo": { "label": "roles", "detailKeys": ["guildId"] },
|
||||||
"emojiList": { "label": "emoji list", "detailKeys": ["guildId"] },
|
"emojiList": { "label": "emoji list", "detailKeys": ["guildId"] },
|
||||||
|
"emojiUpload": { "label": "emoji upload", "detailKeys": ["guildId", "name"] },
|
||||||
|
"stickerUpload": { "label": "sticker upload", "detailKeys": ["guildId", "name"] },
|
||||||
"roleAdd": { "label": "role add", "detailKeys": ["guildId", "userId", "roleId"] },
|
"roleAdd": { "label": "role add", "detailKeys": ["guildId", "userId", "roleId"] },
|
||||||
"roleRemove": { "label": "role remove", "detailKeys": ["guildId", "userId", "roleId"] },
|
"roleRemove": { "label": "role remove", "detailKeys": ["guildId", "userId", "roleId"] },
|
||||||
"channelInfo": { "label": "channel", "detailKeys": ["channelId"] },
|
"channelInfo": { "label": "channel", "detailKeys": ["channelId"] },
|
||||||
|
|||||||
@@ -265,6 +265,8 @@ export type DiscordActionConfig = {
|
|||||||
voiceStatus?: boolean;
|
voiceStatus?: boolean;
|
||||||
events?: boolean;
|
events?: boolean;
|
||||||
moderation?: boolean;
|
moderation?: boolean;
|
||||||
|
emojiUploads?: boolean;
|
||||||
|
stickerUploads?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordConfig = {
|
export type DiscordConfig = {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
sendStickerDiscord,
|
sendStickerDiscord,
|
||||||
timeoutMemberDiscord,
|
timeoutMemberDiscord,
|
||||||
unpinMessageDiscord,
|
unpinMessageDiscord,
|
||||||
|
uploadEmojiDiscord,
|
||||||
|
uploadStickerDiscord,
|
||||||
} from "./send.js";
|
} from "./send.js";
|
||||||
|
|
||||||
vi.mock("../web/media.js", () => ({
|
vi.mock("../web/media.js", () => ({
|
||||||
@@ -30,6 +32,12 @@ vi.mock("../web/media.js", () => ({
|
|||||||
contentType: "image/jpeg",
|
contentType: "image/jpeg",
|
||||||
kind: "image",
|
kind: "image",
|
||||||
}),
|
}),
|
||||||
|
loadWebMediaRaw: vi.fn().mockResolvedValue({
|
||||||
|
buffer: Buffer.from("img"),
|
||||||
|
fileName: "asset.png",
|
||||||
|
contentType: "image/png",
|
||||||
|
kind: "image",
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const makeRest = () => {
|
const makeRest = () => {
|
||||||
@@ -449,6 +457,73 @@ describe("listGuildEmojisDiscord", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("uploadEmojiDiscord", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uploads emoji assets", async () => {
|
||||||
|
const { rest, postMock } = makeRest();
|
||||||
|
postMock.mockResolvedValue({ id: "e1" });
|
||||||
|
await uploadEmojiDiscord(
|
||||||
|
{
|
||||||
|
guildId: "g1",
|
||||||
|
name: "party_blob",
|
||||||
|
mediaUrl: "file:///tmp/party.png",
|
||||||
|
roleIds: ["r1"],
|
||||||
|
},
|
||||||
|
{ rest, token: "t" },
|
||||||
|
);
|
||||||
|
expect(postMock).toHaveBeenCalledWith(
|
||||||
|
Routes.guildEmojis("g1"),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: {
|
||||||
|
name: "party_blob",
|
||||||
|
image: "data:image/png;base64,aW1n",
|
||||||
|
roles: ["r1"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("uploadStickerDiscord", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uploads sticker assets", async () => {
|
||||||
|
const { rest, postMock } = makeRest();
|
||||||
|
postMock.mockResolvedValue({ id: "s1" });
|
||||||
|
await uploadStickerDiscord(
|
||||||
|
{
|
||||||
|
guildId: "g1",
|
||||||
|
name: "clawdis_wave",
|
||||||
|
description: "Clawdis waving",
|
||||||
|
tags: "👋",
|
||||||
|
mediaUrl: "file:///tmp/wave.png",
|
||||||
|
},
|
||||||
|
{ rest, token: "t" },
|
||||||
|
);
|
||||||
|
expect(postMock).toHaveBeenCalledWith(
|
||||||
|
Routes.guildStickers("g1"),
|
||||||
|
expect.objectContaining({
|
||||||
|
body: {
|
||||||
|
name: "clawdis_wave",
|
||||||
|
description: "Clawdis waving",
|
||||||
|
tags: "👋",
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "asset.png",
|
||||||
|
contentType: "image/png",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sendStickerDiscord", () => {
|
describe("sendStickerDiscord", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ import type {
|
|||||||
|
|
||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText } from "../auto-reply/chunk.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { loadWebMedia } from "../web/media.js";
|
import { loadWebMedia, loadWebMediaRaw } from "../web/media.js";
|
||||||
import { normalizeDiscordToken } from "./token.js";
|
import { normalizeDiscordToken } from "./token.js";
|
||||||
|
|
||||||
const DISCORD_TEXT_LIMIT = 2000;
|
const DISCORD_TEXT_LIMIT = 2000;
|
||||||
const DISCORD_MAX_STICKERS = 3;
|
const DISCORD_MAX_STICKERS = 3;
|
||||||
|
const DISCORD_MAX_EMOJI_BYTES = 256 * 1024;
|
||||||
|
const DISCORD_MAX_STICKER_BYTES = 512 * 1024;
|
||||||
const DISCORD_POLL_MIN_ANSWERS = 2;
|
const DISCORD_POLL_MIN_ANSWERS = 2;
|
||||||
const DISCORD_POLL_MAX_ANSWERS = 10;
|
const DISCORD_POLL_MAX_ANSWERS = 10;
|
||||||
const DISCORD_POLL_MAX_DURATION_HOURS = 32 * 24;
|
const DISCORD_POLL_MAX_DURATION_HOURS = 32 * 24;
|
||||||
@@ -128,6 +130,21 @@ export type DiscordTimeoutTarget = DiscordModerationTarget & {
|
|||||||
until?: string;
|
until?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DiscordEmojiUpload = {
|
||||||
|
guildId: string;
|
||||||
|
name: string;
|
||||||
|
mediaUrl: string;
|
||||||
|
roleIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordStickerUpload = {
|
||||||
|
guildId: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tags: string;
|
||||||
|
mediaUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
function resolveToken(explicit?: string) {
|
function resolveToken(explicit?: string) {
|
||||||
const cfgToken = loadConfig().discord?.token;
|
const cfgToken = loadConfig().discord?.token;
|
||||||
const token = normalizeDiscordToken(
|
const token = normalizeDiscordToken(
|
||||||
@@ -194,6 +211,14 @@ function normalizeStickerIds(raw: string[]) {
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeEmojiName(raw: string, label: string) {
|
||||||
|
const name = raw.trim();
|
||||||
|
if (!name) {
|
||||||
|
throw new Error(`${label} is required`);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePollInput(input: DiscordPollInput): RESTAPIPoll {
|
function normalizePollInput(input: DiscordPollInput): RESTAPIPoll {
|
||||||
const question = input.question.trim();
|
const question = input.question.trim();
|
||||||
if (!question) {
|
if (!question) {
|
||||||
@@ -698,6 +723,74 @@ export async function listGuildEmojisDiscord(
|
|||||||
return await rest.get(Routes.guildEmojis(guildId));
|
return await rest.get(Routes.guildEmojis(guildId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadEmojiDiscord(
|
||||||
|
payload: DiscordEmojiUpload,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
) {
|
||||||
|
const token = resolveToken(opts.token);
|
||||||
|
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||||
|
const media = await loadWebMediaRaw(
|
||||||
|
payload.mediaUrl,
|
||||||
|
DISCORD_MAX_EMOJI_BYTES,
|
||||||
|
);
|
||||||
|
const contentType = media.contentType?.toLowerCase();
|
||||||
|
if (
|
||||||
|
!contentType ||
|
||||||
|
!["image/png", "image/jpeg", "image/jpg", "image/gif"].includes(contentType)
|
||||||
|
) {
|
||||||
|
throw new Error("Discord emoji uploads require a PNG, JPG, or GIF image");
|
||||||
|
}
|
||||||
|
const image = `data:${contentType};base64,${media.buffer.toString("base64")}`;
|
||||||
|
const roleIds = (payload.roleIds ?? [])
|
||||||
|
.map((id) => id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return await rest.post(Routes.guildEmojis(payload.guildId), {
|
||||||
|
body: {
|
||||||
|
name: normalizeEmojiName(payload.name, "Emoji name"),
|
||||||
|
image,
|
||||||
|
roles: roleIds.length ? roleIds : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadStickerDiscord(
|
||||||
|
payload: DiscordStickerUpload,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
) {
|
||||||
|
const token = resolveToken(opts.token);
|
||||||
|
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||||
|
const media = await loadWebMediaRaw(
|
||||||
|
payload.mediaUrl,
|
||||||
|
DISCORD_MAX_STICKER_BYTES,
|
||||||
|
);
|
||||||
|
const contentType = media.contentType?.toLowerCase();
|
||||||
|
if (
|
||||||
|
!contentType ||
|
||||||
|
!["image/png", "image/apng", "application/json"].includes(contentType)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Discord sticker uploads require a PNG, APNG, or Lottie JSON file",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return await rest.post(Routes.guildStickers(payload.guildId), {
|
||||||
|
body: {
|
||||||
|
name: normalizeEmojiName(payload.name, "Sticker name"),
|
||||||
|
description: normalizeEmojiName(
|
||||||
|
payload.description,
|
||||||
|
"Sticker description",
|
||||||
|
),
|
||||||
|
tags: normalizeEmojiName(payload.tags, "Sticker tags"),
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
data: media.buffer,
|
||||||
|
name: media.fileName ?? "sticker",
|
||||||
|
contentType,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchMemberInfoDiscord(
|
export async function fetchMemberInfoDiscord(
|
||||||
guildId: string,
|
guildId: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|||||||
@@ -10,15 +10,23 @@ import {
|
|||||||
import { resizeToJpeg } from "../media/image-ops.js";
|
import { resizeToJpeg } from "../media/image-ops.js";
|
||||||
import { detectMime, extensionForMime } from "../media/mime.js";
|
import { detectMime, extensionForMime } from "../media/mime.js";
|
||||||
|
|
||||||
export async function loadWebMedia(
|
type WebMediaResult = {
|
||||||
mediaUrl: string,
|
|
||||||
maxBytes?: number,
|
|
||||||
): Promise<{
|
|
||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
kind: MediaKind;
|
kind: MediaKind;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
}> {
|
};
|
||||||
|
|
||||||
|
type WebMediaOptions = {
|
||||||
|
maxBytes?: number;
|
||||||
|
optimizeImages?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadWebMediaInternal(
|
||||||
|
mediaUrl: string,
|
||||||
|
options: WebMediaOptions = {},
|
||||||
|
): Promise<WebMediaResult> {
|
||||||
|
const { maxBytes, optimizeImages = true } = options;
|
||||||
if (mediaUrl.startsWith("file://")) {
|
if (mediaUrl.startsWith("file://")) {
|
||||||
mediaUrl = mediaUrl.replace("file://", "");
|
mediaUrl = mediaUrl.replace("file://", "");
|
||||||
}
|
}
|
||||||
@@ -74,11 +82,13 @@ export async function loadWebMedia(
|
|||||||
maxBytesForKind(kind),
|
maxBytesForKind(kind),
|
||||||
);
|
);
|
||||||
if (kind === "image") {
|
if (kind === "image") {
|
||||||
// Skip optimization for GIFs to preserve animation
|
// Skip optimization for GIFs to preserve animation.
|
||||||
if (contentType === "image/gif") {
|
if (contentType === "image/gif" || !optimizeImages) {
|
||||||
if (array.length > cap) {
|
if (array.length > cap) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`GIF exceeds ${(cap / (1024 * 1024)).toFixed(0)}MB limit (got ${(
|
`${
|
||||||
|
contentType === "image/gif" ? "GIF" : "Media"
|
||||||
|
} exceeds ${(cap / (1024 * 1024)).toFixed(0)}MB limit (got ${(
|
||||||
array.length / (1024 * 1024)
|
array.length / (1024 * 1024)
|
||||||
).toFixed(2)}MB)`,
|
).toFixed(2)}MB)`,
|
||||||
);
|
);
|
||||||
@@ -116,11 +126,13 @@ export async function loadWebMedia(
|
|||||||
maxBytesForKind(kind),
|
maxBytesForKind(kind),
|
||||||
);
|
);
|
||||||
if (kind === "image") {
|
if (kind === "image") {
|
||||||
// Skip optimization for GIFs to preserve animation
|
// Skip optimization for GIFs to preserve animation.
|
||||||
if (mime === "image/gif") {
|
if (mime === "image/gif" || !optimizeImages) {
|
||||||
if (data.length > cap) {
|
if (data.length > cap) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`GIF exceeds ${(cap / (1024 * 1024)).toFixed(0)}MB limit (got ${(
|
`${
|
||||||
|
mime === "image/gif" ? "GIF" : "Media"
|
||||||
|
} exceeds ${(cap / (1024 * 1024)).toFixed(0)}MB limit (got ${(
|
||||||
data.length / (1024 * 1024)
|
data.length / (1024 * 1024)
|
||||||
).toFixed(2)}MB)`,
|
).toFixed(2)}MB)`,
|
||||||
);
|
);
|
||||||
@@ -139,6 +151,26 @@ export async function loadWebMedia(
|
|||||||
return { buffer: data, contentType: mime, kind, fileName };
|
return { buffer: data, contentType: mime, kind, fileName };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadWebMedia(
|
||||||
|
mediaUrl: string,
|
||||||
|
maxBytes?: number,
|
||||||
|
): Promise<WebMediaResult> {
|
||||||
|
return await loadWebMediaInternal(mediaUrl, {
|
||||||
|
maxBytes,
|
||||||
|
optimizeImages: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadWebMediaRaw(
|
||||||
|
mediaUrl: string,
|
||||||
|
maxBytes?: number,
|
||||||
|
): Promise<WebMediaResult> {
|
||||||
|
return await loadWebMediaInternal(mediaUrl, {
|
||||||
|
maxBytes,
|
||||||
|
optimizeImages: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function optimizeImageToJpeg(
|
export async function optimizeImageToJpeg(
|
||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
maxBytes: number,
|
maxBytes: number,
|
||||||
|
|||||||
Reference in New Issue
Block a user