Skip to content

Commit

Permalink
refactor(create-emoji): compress source image (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranet authored Feb 14, 2024
1 parent dd845e1 commit b7c427a
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 12 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@discordjs/builders": "^1.7.0",
"@discordjs/collection": "^2.0.0",
"@napi-rs/canvas": "^0.1.45",
"@napi-rs/image": "^1.8.0",
"@prisma/client": "^5.9.1",
"@sapphire/async-queue": "^1.5.2",
"@sapphire/duration": "^1.1.2",
Expand Down
35 changes: 24 additions & 11 deletions src/commands/create-emoji.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type DiscordEmoji
} from '#lib/utilities/emoji';
import { DiscordAPIError, HTTPError } from '@discordjs/rest';
import { Transformer } from '@napi-rs/image';
import { Result, err, ok } from '@sapphire/result';
import { isNullish, isNullishOrEmpty, isNullishOrZero, tryParseURL, type Nullish } from '@sapphire/utilities';
import { Command, RegisterCommand } from '@skyra/http-framework';
Expand Down Expand Up @@ -156,21 +157,25 @@ export class UserCommand extends Command {
}

const response = downloadResult.unwrap();
const validationResult = this.parseContentLength(response.headers.get('Content-Length')) //
const contentTypeResult = this.parseContentLength(response.headers.get('Content-Length')) //
.andThen(() => this.parseContentType(response.headers.get('Content-Type')));
if (contentTypeResult.isErr()) {
const content = resolveUserKey(interaction, contentTypeResult.unwrapErr());
return deferred.update({ content });
}

const content = await validationResult.match({
ok: (contentType) => this.performUpload(interaction, response, name, contentType),
const imageResult = await this.maybeCompressImage(Buffer.from(await response.arrayBuffer()), contentTypeResult.unwrap());
const content = await imageResult.match({
ok: (data) => this.performUpload(interaction, data.image, name, data.contentType),
err: (error) => resolveUserKey(interaction, error)
});
return deferred.update({ content });
}

private async performUpload(interaction: Command.ChatInputInteraction, response: Response, name: string, contentType: string) {
const buffer = await response.arrayBuffer();
private async performUpload(interaction: Command.ChatInputInteraction, buffer: Buffer, name: string, contentType: string) {
const body: RESTPostAPIGuildEmojiJSONBody = {
name,
image: `data:${contentType};base64,${Buffer.from(buffer).toString('base64')}`
image: `data:${contentType};base64,${buffer.toString('base64')}`
};
const reason = resolveKey(interaction, Root.UploadedBy, { user: interaction.member!.user });
const uploadResult = await Result.fromAsync(
Expand Down Expand Up @@ -246,10 +251,7 @@ export class UserCommand extends Command {
// Edge case (should never happen): error on invalid (NaN, non-integer, negative) Content-Length:
if (!Number.isSafeInteger(parsed) || parsed < 0) return err(Root.ContentLengthInvalid);

// `parsed` is in bytes, maximum upload size is 256 kilobytes. Error if Content-Length exceeds the limit:
if (parsed > 256000) return err(Root.ContentLengthTooBig);

return ok();
return ok(parsed);
}

private parseContentType(type: string | Nullish) {
Expand All @@ -262,13 +264,22 @@ export class UserCommand extends Command {
case 'image/jpeg':
case 'image/gif':
case 'image/webp':
return ok(type);
return ok(type as ContentType);
}

return err(Root.ContentTypeUnsupported);
}

private async maybeCompressImage(original: Buffer, contentType: ContentType) {
if (original.byteLength <= UserCommand.MaximumUploadSize) return ok({ image: original, contentType });
if (contentType === 'image/gif') return err(Root.ContentLengthTooBig);

const image = await new Transformer(original).fastResize({ width: 128, height: 128, fit: 2 }).webp(100);
return image.byteLength > UserCommand.MaximumUploadSize ? err(Root.ContentLengthTooBig) : ok({ image, contentType: 'image/webp' });
}

private static readonly NameValidatorRegExp = /^[0-9a-zA-Z_]{2,32}$/;
private static readonly MaximumUploadSize = 256 * 1024;
}

interface Options {
Expand All @@ -277,3 +288,5 @@ interface Options {
name?: string;
variant?: EmojiSource;
}

type ContentType = 'image/png' | 'image/jpg' | 'image/jpeg' | 'image/gif' | 'image/webp';
172 changes: 171 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,24 @@ __metadata:
languageName: node
linkType: hard

"@emnapi/core@npm:^0.45.0":
version: 0.45.0
resolution: "@emnapi/core@npm:0.45.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/7736e53b6af05fe6e3bb49a7f59f93e7951a546704154e3758a408f8e1b2af9a0c88bab4508c02a023eb2baeefaab7fa9f666c4acd549be95684307886a5fadf
languageName: node
linkType: hard

"@emnapi/runtime@npm:^0.45.0":
version: 0.45.0
resolution: "@emnapi/runtime@npm:0.45.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/be9f794e7c52bff178975c7287e48c84bdab63ed7d4f21f9239f8101fcc04f059bff9b1c5d730cf0c7a6d812231a46749208c315bc085bb5170882c1c9163676
languageName: node
linkType: hard

"@esbuild/aix-ppc64@npm:0.19.10":
version: 0.19.10
resolution: "@esbuild/aix-ppc64@npm:0.19.10"
Expand Down Expand Up @@ -761,6 +779,148 @@ __metadata:
languageName: node
linkType: hard

"@napi-rs/image-android-arm64@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-android-arm64@npm:1.8.0"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard

"@napi-rs/image-darwin-arm64@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-darwin-arm64@npm:1.8.0"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard

"@napi-rs/image-darwin-x64@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-darwin-x64@npm:1.8.0"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard

"@napi-rs/image-freebsd-x64@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-freebsd-x64@npm:1.8.0"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard

"@napi-rs/image-linux-arm-gnueabihf@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-linux-arm-gnueabihf@npm:1.8.0"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard

"@napi-rs/image-linux-arm64-gnu@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-linux-arm64-gnu@npm:1.8.0"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard

"@napi-rs/image-linux-arm64-musl@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-linux-arm64-musl@npm:1.8.0"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard

"@napi-rs/image-linux-x64-gnu@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-linux-x64-gnu@npm:1.8.0"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard

"@napi-rs/image-linux-x64-musl@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-linux-x64-musl@npm:1.8.0"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard

"@napi-rs/image-wasm32-wasi@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-wasm32-wasi@npm:1.8.0"
dependencies:
"@napi-rs/wasm-runtime": "npm:^0.1.1"
conditions: cpu=wasm32
languageName: node
linkType: hard

"@napi-rs/image-win32-ia32-msvc@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-win32-ia32-msvc@npm:1.8.0"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard

"@napi-rs/image-win32-x64-msvc@npm:1.8.0":
version: 1.8.0
resolution: "@napi-rs/image-win32-x64-msvc@npm:1.8.0"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard

"@napi-rs/image@npm:^1.8.0":
version: 1.8.0
resolution: "@napi-rs/image@npm:1.8.0"
dependencies:
"@napi-rs/image-android-arm64": "npm:1.8.0"
"@napi-rs/image-darwin-arm64": "npm:1.8.0"
"@napi-rs/image-darwin-x64": "npm:1.8.0"
"@napi-rs/image-freebsd-x64": "npm:1.8.0"
"@napi-rs/image-linux-arm-gnueabihf": "npm:1.8.0"
"@napi-rs/image-linux-arm64-gnu": "npm:1.8.0"
"@napi-rs/image-linux-arm64-musl": "npm:1.8.0"
"@napi-rs/image-linux-x64-gnu": "npm:1.8.0"
"@napi-rs/image-linux-x64-musl": "npm:1.8.0"
"@napi-rs/image-wasm32-wasi": "npm:1.8.0"
"@napi-rs/image-win32-ia32-msvc": "npm:1.8.0"
"@napi-rs/image-win32-x64-msvc": "npm:1.8.0"
dependenciesMeta:
"@napi-rs/image-android-arm64":
optional: true
"@napi-rs/image-darwin-arm64":
optional: true
"@napi-rs/image-darwin-x64":
optional: true
"@napi-rs/image-freebsd-x64":
optional: true
"@napi-rs/image-linux-arm-gnueabihf":
optional: true
"@napi-rs/image-linux-arm64-gnu":
optional: true
"@napi-rs/image-linux-arm64-musl":
optional: true
"@napi-rs/image-linux-x64-gnu":
optional: true
"@napi-rs/image-linux-x64-musl":
optional: true
"@napi-rs/image-wasm32-wasi":
optional: true
"@napi-rs/image-win32-ia32-msvc":
optional: true
"@napi-rs/image-win32-x64-msvc":
optional: true
checksum: 10/3caa6eab66255a328fade140ac92d6d61b538bad14a92691f089430bf72941f794f56044d4432a391f4375238529450f3a38e2dbf862a51dd51c265308b3db33
languageName: node
linkType: hard

"@napi-rs/wasm-runtime@npm:^0.1.1":
version: 0.1.1
resolution: "@napi-rs/wasm-runtime@npm:0.1.1"
dependencies:
"@emnapi/core": "npm:^0.45.0"
"@emnapi/runtime": "npm:^0.45.0"
"@tybys/wasm-util": "npm:^0.8.1"
checksum: 10/243b07483553ad73265aa8bd591047d28864672a22f3c61848509d1d3fb360b10eb1a012429fdca0eb7d88ef453d2fdfa34ead020e3a2317fbbd4ff35ae41917
languageName: node
linkType: hard

"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
Expand Down Expand Up @@ -1247,6 +1407,7 @@ __metadata:
"@discordjs/builders": "npm:^1.7.0"
"@discordjs/collection": "npm:^2.0.0"
"@napi-rs/canvas": "npm:^0.1.45"
"@napi-rs/image": "npm:^1.8.0"
"@prisma/client": "npm:^5.9.1"
"@sapphire/async-queue": "npm:^1.5.2"
"@sapphire/duration": "npm:^1.1.2"
Expand Down Expand Up @@ -1319,6 +1480,15 @@ __metadata:
languageName: node
linkType: hard

"@tybys/wasm-util@npm:^0.8.1":
version: 0.8.1
resolution: "@tybys/wasm-util@npm:0.8.1"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10/4e1bb353313225e6c6901b49e969f7963e7802ddad0f79148f759c898d4fcb6761225c9dd622343810b6108a95ae3dfc9a716628dc5c1e40acb776d36d885bc4
languageName: node
linkType: hard

"@types/estree@npm:^1.0.0":
version: 1.0.5
resolution: "@types/estree@npm:1.0.5"
Expand Down Expand Up @@ -5608,7 +5778,7 @@ __metadata:
languageName: node
linkType: hard

"tslib@npm:^2.1.0, tslib@npm:^2.5.2, tslib@npm:^2.6.2":
"tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.5.2, tslib@npm:^2.6.2":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca
Expand Down

0 comments on commit b7c427a

Please sign in to comment.