diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index c9834c2821..734e0e12dc 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -390,6 +390,7 @@ import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import * as ep___admin_accounts_present_points from './endpoints/admin/accounts/present-points.js'; import type { Provider } from '@nestjs/common'; +import * as ep___emoji_speedtest from './endpoints/admin/emoji/speedtest.js'; const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default }; const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default }; const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; @@ -411,6 +412,7 @@ const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-de const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; +const $emoji_speedtest: Provider = { provide: 'ep:emoji/speedtest', useClass: ep___emoji_speedtest.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -799,6 +801,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_avatarDecorations_update, $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, + $emoji_speedtest, $admin_unsetUserBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, @@ -1183,6 +1186,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, + $emoji_speedtest, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 4c835f25e2..16b230fbb2 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -50,6 +50,7 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js'; +import * as ep___emoji_speedtest from './endpoints/admin/emoji/speedtest.js'; import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; @@ -432,6 +433,7 @@ const eps = [ ['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk], ['admin/emoji/update', ep___admin_emoji_update], ['admin/emoji/update-request', ep___admin_emoji_updateRequest], + ['emoji/speedtest', ep___emoji_speedtest], ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles], ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata], ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/speedtest.ts b/packages/backend/src/server/api/endpoints/admin/emoji/speedtest.ts new file mode 100644 index 0000000000..ca2a4599db --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/speedtest.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import sharp from 'sharp'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; + +export const meta = { + tags: ['admin'], + requireCredential: true, + requireRolePolicy: 'canManageCustomEmojis', + kind: 'write:admin:emoji', +} as const; + +export const paramDef = { + type: 'object', + properties: { + url: { + type: 'string', + }, + }, + required: ['url'], +} as const; + +@Injectable() +export default class extends Endpoint { + constructor( + private customEmojiService: CustomEmojiService, + ) { + super(meta, paramDef, async (ps, me) => { + const response = await fetch(ps.url, { + 'headers': { + 'accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + 'cache-control': 'no-cache', + 'pragma': 'no-cache', + 'priority': 'u=1, i', + }, + 'method': 'GET', + }); + + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); + } + const buffer = await response.arrayBuffer(); + const metadata = await sharp(buffer).metadata(); + + if (!metadata.pages) { + throw new Error('Invalid image format or no animation frames found.'); + } + + const frameRate = metadata.delay && metadata.delay.length > 0 + ? 1000 / metadata.delay[0] + : 30; // Fallback to 30 FPS if no delay information is present + + const colorsPerFrame: number[] = []; + for (let i = 0; i < metadata.pages; i++) { + const { data, info } = await sharp(buffer, { page: i }).raw().toBuffer({ resolveWithObject: true }); + const uniqueColors = new Set(); + for (let y = 0; y < info.height; y++) { + for (let x = 0; x < info.width; x++) { + const offset = (y * info.width + x) * info.channels; + const color = `${data[offset]}-${data[offset + 1]}-${data[offset + 2]}`; + uniqueColors.add(color); + } + } + colorsPerFrame.push(uniqueColors.size); + } + + const colorChanges = colorsPerFrame.map((colorCount, index, arr) => { + if (index === 0) return 0; + return Math.abs(colorCount - arr[index - 1]); + }); + + const averageColorChangePerSecond = colorChanges.reduce((sum, change) => sum + change, 0) / colorsPerFrame.length; + console.log('Average color change per second:', 10 < averageColorChangePerSecond); + return Boolean(10 < averageColorChangePerSecond); + // You can store or use this information as needed + }); + } +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 32b73ec7e2..1cc8602785 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -17,6 +17,7 @@ "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { + "@caed0/webp-conv": "^1.1.0", "@discordapp/twemoji": "15.0.3", "@github/webauthn-json": "2.1.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", @@ -31,6 +32,7 @@ "@vitejs/plugin-vue": "5.0.4", "@vue/compiler-sfc": "3.4.26", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.4", + "apng-js": "^1.1.1", "astring": "1.8.6", "broadcast-channel": "7.0.0", "buraha": "0.0.1", @@ -47,10 +49,14 @@ "escape-regexp": "0.0.1", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", + "gif-frames": "^1.0.1", + "gifshot": "^0.4.5", + "gifuct-js": "^2.1.2", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", + "libwebpjs": "^0.0.1", "matter-js": "0.19.0", "mfm-js": "0.24.0", "misskey-bubble-game": "workspace:*", @@ -75,7 +81,9 @@ "vite": "5.2.11", "vue": "3.4.26", "vuedraggable": "next", - "wavesurfer.js": "^7.7.14" + "wavesurfer.js": "^7.7.14", + "webm-wasm": "^0.4.1", + "webp-hero": "^0.0.2" }, "devDependencies": { "@misskey-dev/eslint-plugin": "1.0.0", diff --git a/packages/frontend/src/components/MkEmojiEditDialog.vue b/packages/frontend/src/components/MkEmojiEditDialog.vue index 1262c59bb5..f0ab622f0e 100644 --- a/packages/frontend/src/components/MkEmojiEditDialog.vue +++ b/packages/frontend/src/components/MkEmojiEditDialog.vue @@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only