enhance: Proxy custom emojis to reduce image size and accelerate the frontend (#9431)
* fix(server): /emoji to accept `@.` host expression
* fix(client): use MkEmoji for custom emoji in MkEmojiPicker
* change convertToWebp
* nanka iroiro
* remove
* fix
* nearLosslessは労多くして益少なしなのでやめる
* do not cleanup tmp for development
* update sharp.js to 0.31.3
* mixed: true
* fix MkAutocomplete of 912791b3ab
* clean up
* https://github.com/misskey-dev/misskey/pull/9431#discussion_r1059215943
			
			
This commit is contained in:
		
							parent
							
								
									f227091826
								
							
						
					
					
						commit
						8b46edeccf
					
				
					 20 changed files with 140 additions and 92 deletions
				
			
		|  | @ -105,7 +105,7 @@ | ||||||
| 		"sanitize-html": "2.8.1", | 		"sanitize-html": "2.8.1", | ||||||
| 		"seedrandom": "^3.0.5", | 		"seedrandom": "^3.0.5", | ||||||
| 		"semver": "7.3.8", | 		"semver": "7.3.8", | ||||||
| 		"sharp": "0.29.3", | 		"sharp": "0.31.3", | ||||||
| 		"speakeasy": "2.0.0", | 		"speakeasy": "2.0.0", | ||||||
| 		"strict-event-emitter-types": "2.0.0", | 		"strict-event-emitter-types": "2.0.0", | ||||||
| 		"stringz": "2.1.0", | 		"stringz": "2.1.0", | ||||||
|  |  | ||||||
|  | @ -2,12 +2,10 @@ import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { DataSource, In, IsNull } from 'typeorm'; | import { DataSource, In, IsNull } from 'typeorm'; | ||||||
| import { GlobalEventService } from '@/core/GlobalEventService.js'; | import { GlobalEventService } from '@/core/GlobalEventService.js'; | ||||||
| import { DI } from '@/di-symbols.js'; | import { DI } from '@/di-symbols.js'; | ||||||
| import type { Config } from '@/config.js'; |  | ||||||
| import { IdService } from '@/core/IdService.js'; | import { IdService } from '@/core/IdService.js'; | ||||||
| import type { DriveFile } from '@/models/entities/DriveFile.js'; | import type { DriveFile } from '@/models/entities/DriveFile.js'; | ||||||
| import type { Emoji } from '@/models/entities/Emoji.js'; | import type { Emoji } from '@/models/entities/Emoji.js'; | ||||||
| import { Cache } from '@/misc/cache.js'; | import { Cache } from '@/misc/cache.js'; | ||||||
| import { query } from '@/misc/prelude/url.js'; |  | ||||||
| import type { Note } from '@/models/entities/Note.js'; | import type { Note } from '@/models/entities/Note.js'; | ||||||
| import type { EmojisRepository } from '@/models/index.js'; | import type { EmojisRepository } from '@/models/index.js'; | ||||||
| import { UtilityService } from '@/core/UtilityService.js'; | import { UtilityService } from '@/core/UtilityService.js'; | ||||||
|  | @ -27,9 +25,6 @@ export class CustomEmojiService { | ||||||
| 	private cache: Cache<Emoji | null>; | 	private cache: Cache<Emoji | null>; | ||||||
| 
 | 
 | ||||||
| 	constructor( | 	constructor( | ||||||
| 		@Inject(DI.config) |  | ||||||
| 		private config: Config, |  | ||||||
| 
 |  | ||||||
| 		@Inject(DI.db) | 		@Inject(DI.db) | ||||||
| 		private db: DataSource, | 		private db: DataSource, | ||||||
| 
 | 
 | ||||||
|  | @ -117,7 +112,7 @@ export class CustomEmojiService { | ||||||
| 
 | 
 | ||||||
| 		const isLocal = emoji.host == null; | 		const isLocal = emoji.host == null; | ||||||
| 		const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
 | 		const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
 | ||||||
| 		const url = isLocal ? emojiUrl : `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`; | 		const url = emojiUrl; | ||||||
| 
 | 
 | ||||||
| 		return { | 		return { | ||||||
| 			name: emojiName, | 			name: emojiName, | ||||||
|  |  | ||||||
|  | @ -33,7 +33,7 @@ export class DownloadService { | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public async downloadUrl(url: string, path: string): Promise<void> { | 	public async downloadUrl(url: string, path: string): Promise<void> { | ||||||
| 		this.logger.info(`Downloading ${chalk.cyan(url)} ...`); | 		this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); | ||||||
| 	 | 	 | ||||||
| 		const timeout = 30 * 1000; | 		const timeout = 30 * 1000; | ||||||
| 		const operationTimeout = 60 * 1000; | 		const operationTimeout = 60 * 1000; | ||||||
|  |  | ||||||
|  | @ -8,6 +8,16 @@ export type IImage = { | ||||||
| 	ext: string | null; | 	ext: string | null; | ||||||
| 	type: string; | 	type: string; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const webpDefault: sharp.WebpOptions = { | ||||||
|  | 	quality: 85, | ||||||
|  | 	alphaQuality: 95, | ||||||
|  | 	lossless: false, | ||||||
|  | 	nearLossless: false, | ||||||
|  | 	smartSubsample: true, | ||||||
|  | 	mixed: true, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| import { bindThis } from '@/decorators.js'; | import { bindThis } from '@/decorators.js'; | ||||||
| 
 | 
 | ||||||
| @Injectable() | @Injectable() | ||||||
|  | @ -53,21 +63,19 @@ export class ImageProcessingService { | ||||||
| 	 *   with resize, remove metadata, resolve orientation, stop animation | 	 *   with resize, remove metadata, resolve orientation, stop animation | ||||||
| 	 */ | 	 */ | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public async convertToWebp(path: string, width: number, height: number, quality = 85): Promise<IImage> { | 	public async convertToWebp(path: string, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { | ||||||
| 		return this.convertSharpToWebp(await sharp(path), width, height, quality); | 		return this.convertSharpToWebp(await sharp(path), width, height, options); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@bindThis | 	@bindThis | ||||||
| 	public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, quality = 85): Promise<IImage> { | 	public async convertSharpToWebp(sharp: sharp.Sharp, width: number, height: number, options: sharp.WebpOptions = webpDefault): Promise<IImage> { | ||||||
| 		const data = await sharp | 		const data = await sharp | ||||||
| 			.resize(width, height, { | 			.resize(width, height, { | ||||||
| 				fit: 'inside', | 				fit: 'inside', | ||||||
| 				withoutEnlargement: true, | 				withoutEnlargement: true, | ||||||
| 			}) | 			}) | ||||||
| 			.rotate() | 			.rotate() | ||||||
| 			.webp({ | 			.webp(options) | ||||||
| 				quality, |  | ||||||
| 			}) |  | ||||||
| 			.toBuffer(); | 			.toBuffer(); | ||||||
| 
 | 
 | ||||||
| 		return { | 		return { | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ export function createTemp(): Promise<[string, () => void]> { | ||||||
| 	return new Promise<[string, () => void]>((res, rej) => { | 	return new Promise<[string, () => void]>((res, rej) => { | ||||||
| 		tmp.file((e, path, fd, cleanup) => { | 		tmp.file((e, path, fd, cleanup) => { | ||||||
| 			if (e) return rej(e); | 			if (e) return rej(e); | ||||||
| 			res([path, cleanup]); | 			res([path, process.env.NODE_ENV === 'production' ? cleanup : () => {}]); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  | @ -17,7 +17,7 @@ export function createTempDir(): Promise<[string, () => void]> { | ||||||
| 			}, | 			}, | ||||||
| 			(e, path, cleanup) => { | 			(e, path, cleanup) => { | ||||||
| 				if (e) return rej(e); | 				if (e) return rej(e); | ||||||
| 				res([path, cleanup]); | 				res([path, process.env.NODE_ENV === 'production' ? cleanup : () => {}]); | ||||||
| 			}, | 			}, | ||||||
| 		); | 		); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | /* objを検査して | ||||||
|  |  * 1. 配列に何も入っていない時はクエリを付けない | ||||||
|  |  * 2. プロパティがundefinedの時はクエリを付けない | ||||||
|  |  * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) | ||||||
|  |  */  | ||||||
| export function query(obj: Record<string, unknown>): string { | export function query(obj: Record<string, unknown>): string { | ||||||
| 	const params = Object.entries(obj) | 	const params = Object.entries(obj) | ||||||
| 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ import type { Config } from '@/config.js'; | ||||||
| import { isMimeImage } from '@/misc/is-mime-image.js'; | import { isMimeImage } from '@/misc/is-mime-image.js'; | ||||||
| import { createTemp } from '@/misc/create-temp.js'; | import { createTemp } from '@/misc/create-temp.js'; | ||||||
| import { DownloadService } from '@/core/DownloadService.js'; | import { DownloadService } from '@/core/DownloadService.js'; | ||||||
| import { ImageProcessingService } from '@/core/ImageProcessingService.js'; | import { ImageProcessingService, webpDefault } from '@/core/ImageProcessingService.js'; | ||||||
| import type { IImage } from '@/core/ImageProcessingService.js'; | import type { IImage } from '@/core/ImageProcessingService.js'; | ||||||
| import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; | import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; | ||||||
| import { StatusError } from '@/misc/status-error.js'; | import { StatusError } from '@/misc/status-error.js'; | ||||||
|  | @ -81,8 +81,21 @@ export class MediaProxyServerService { | ||||||
| 			const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); | 			const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image'); | ||||||
| 	 | 	 | ||||||
| 			let image: IImage; | 			let image: IImage; | ||||||
|  | 			if ('emoji' in request.query && isConvertibleImage) { | ||||||
|  | 				const data = await sharp(path, { animated: !('static' in request.query) }) | ||||||
|  | 					.resize({ | ||||||
|  | 						height: 128, | ||||||
|  | 						withoutEnlargement: true, | ||||||
|  | 					}) | ||||||
|  | 					.webp(webpDefault) | ||||||
|  | 					.toBuffer(); | ||||||
| 
 | 
 | ||||||
| 			if ('static' in request.query && isConvertibleImage) { | 				image = { | ||||||
|  | 					data, | ||||||
|  | 					ext: 'webp', | ||||||
|  | 					type: 'image/webp', | ||||||
|  | 				}; | ||||||
|  | 			} else if ('static' in request.query && isConvertibleImage) { | ||||||
| 				image = await this.imageProcessingService.convertToWebp(path, 498, 280); | 				image = await this.imageProcessingService.convertToWebp(path, 498, 280); | ||||||
| 			} else if ('preview' in request.query && isConvertibleImage) { | 			} else if ('preview' in request.query && isConvertibleImage) { | ||||||
| 				image = await this.imageProcessingService.convertToWebp(path, 200, 200); | 				image = await this.imageProcessingService.convertToWebp(path, 200, 200); | ||||||
|  | @ -121,8 +134,8 @@ export class MediaProxyServerService { | ||||||
| 					ext: 'png', | 					ext: 'png', | ||||||
| 					type: 'image/png', | 					type: 'image/png', | ||||||
| 				}; | 				}; | ||||||
| 			}	else if (mime === 'image/svg+xml') { | 			} else if (mime === 'image/svg+xml') { | ||||||
| 				image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, 1); | 				image = await this.imageProcessingService.convertToWebp(path, 2048, 2048, webpDefault); | ||||||
| 			} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { | 			} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) { | ||||||
| 				throw new StatusError('Rejected type', 403, 'Rejected type'); | 				throw new StatusError('Rejected type', 403, 'Rejected type'); | ||||||
| 			} else { | 			} else { | ||||||
|  |  | ||||||
|  | @ -220,7 +220,7 @@ export class ClientServerService { | ||||||
| 			return reply.sendFile('/apple-touch-icon.png', staticAssets); | 			return reply.sendFile('/apple-touch-icon.png', staticAssets); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		fastify.get<{ Params: { path: string } }>('/emoji/:path(.*)', async (request, reply) => { | 		fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { | ||||||
| 			const path = request.params.path; | 			const path = request.params.path; | ||||||
| 
 | 
 | ||||||
| 			if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { | 			if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) { | ||||||
|  | @ -244,8 +244,15 @@ export class ClientServerService { | ||||||
| 
 | 
 | ||||||
| 			reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); | 			reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); | ||||||
| 
 | 
 | ||||||
| 			// ?? emoji.originalUrl してるのは後方互換性のため
 | 			const url = new URL("/proxy/emoji.webp", this.config.url); | ||||||
| 			return await reply.redirect(301, emoji.publicUrl ?? emoji.originalUrl); | 			url.searchParams.set('url', emoji.publicUrl ?? emoji.originalUrl); // ?? emoji.originalUrl してるのは後方互換性のため
 | ||||||
|  | 			url.searchParams.set('emoji', '1'); | ||||||
|  | 			if ('static' in request.query) url.searchParams.set('static', '1'); | ||||||
|  | 
 | ||||||
|  | 			return await reply.redirect( | ||||||
|  | 				301, | ||||||
|  | 				url.toString(), | ||||||
|  | 			); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { | 		fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { | ||||||
|  |  | ||||||
|  | @ -16,12 +16,12 @@ | ||||||
| 		</li> | 		</li> | ||||||
| 	</ol> | 	</ol> | ||||||
| 	<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> | 	<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> | ||||||
| 		<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> | 		<li v-for="emoji in emojis" tabindex="-1" :key="emoji.emoji" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> | ||||||
| 			<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="`/emoji/${emoji.name}.webp`" :alt="emoji.emoji"/></span> | 			<div class="emoji"> | ||||||
| 			<span v-else-if="defaultStore.state.emojiStyle != 'native'" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span> | 				<MkEmoji :emoji="emoji.emoji" /> | ||||||
| 			<span v-else class="emoji">{{ emoji.emoji }}</span> | 			</div> | ||||||
| 			<!-- eslint-disable-next-line vue/no-v-html --> | 			<!-- eslint-disable-next-line vue/no-v-html --> | ||||||
| 			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> | 			<span class="name" v-html="emoji.name.replace(q ?? '', `<b>${q}</b>`)"></span> | ||||||
| 			<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> | 			<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> | ||||||
| 		</li> | 		</li> | ||||||
| 	</ol> | 	</ol> | ||||||
|  | @ -37,7 +37,6 @@ | ||||||
| import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; | import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; | ||||||
| import contains from '@/scripts/contains'; | import contains from '@/scripts/contains'; | ||||||
| import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; | import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; |  | ||||||
| import { acct } from '@/filters/user'; | import { acct } from '@/filters/user'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { MFM_TAGS } from '@/scripts/mfm-tags'; | import { MFM_TAGS } from '@/scripts/mfm-tags'; | ||||||
|  | @ -49,9 +48,13 @@ import { i18n } from '@/i18n'; | ||||||
| type EmojiDef = { | type EmojiDef = { | ||||||
| 	emoji: string; | 	emoji: string; | ||||||
| 	name: string; | 	name: string; | ||||||
|  | 	url: string; | ||||||
| 	aliasOf?: string; | 	aliasOf?: string; | ||||||
| 	url?: string; | } | { | ||||||
| 	isCustomEmoji?: boolean; | 	emoji: string; | ||||||
|  | 	name: string; | ||||||
|  | 	aliasOf?: string; | ||||||
|  | 	isCustomEmoji?: true; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const lib = emojilist.filter(x => x.category !== 'flags'); | const lib = emojilist.filter(x => x.category !== 'flags'); | ||||||
|  | @ -87,7 +90,6 @@ for (const x of customEmojis) { | ||||||
| 	emojiDefinitions.push({ | 	emojiDefinitions.push({ | ||||||
| 		name: x.name, | 		name: x.name, | ||||||
| 		emoji: `:${x.name}:`, | 		emoji: `:${x.name}:`, | ||||||
| 		url: x.url, |  | ||||||
| 		isCustomEmoji: true, | 		isCustomEmoji: true, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | @ -97,7 +99,6 @@ for (const x of customEmojis) { | ||||||
| 				name: alias, | 				name: alias, | ||||||
| 				aliasOf: x.name, | 				aliasOf: x.name, | ||||||
| 				emoji: `:${x.name}:`, | 				emoji: `:${x.name}:`, | ||||||
| 				url: x.url, |  | ||||||
| 				isCustomEmoji: true, | 				isCustomEmoji: true, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  | @ -452,14 +453,20 @@ onBeforeUnmount(() => { | ||||||
| 	> .emojis > li { | 	> .emojis > li { | ||||||
| 
 | 
 | ||||||
| 		.emoji { | 		.emoji { | ||||||
| 			display: inline-block; | 			display: flex; | ||||||
| 			margin: 0 4px 0 0; | 			margin: 0 4px 0 0; | ||||||
|  | 			height: 24px; | ||||||
| 			width: 24px; | 			width: 24px; | ||||||
|  | 			justify-content: center; | ||||||
|  | 			align-items: center; | ||||||
|  | 			font-size: 20px; | ||||||
| 
 | 
 | ||||||
| 			> img { | 			> img { | ||||||
|  | 				height: 24px; | ||||||
| 				width: 24px; | 				width: 24px; | ||||||
| 				vertical-align: bottom; | 				object-fit: scale-down; | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		.alias { | 		.alias { | ||||||
|  |  | ||||||
|  | @ -81,7 +81,6 @@ import { ref, computed, watch, onMounted } from 'vue'; | ||||||
| import * as Misskey from 'misskey-js'; | import * as Misskey from 'misskey-js'; | ||||||
| import XSection from '@/components/MkEmojiPicker.section.vue'; | import XSection from '@/components/MkEmojiPicker.section.vue'; | ||||||
| import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; | import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; |  | ||||||
| import Ripple from '@/components/MkRipple.vue'; | import Ripple from '@/components/MkRipple.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { isTouchUsing } from '@/scripts/touch'; | import { isTouchUsing } from '@/scripts/touch'; | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { watch } from 'vue'; | import { watch } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/media-proxy'; | ||||||
| import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted, watch } from 'vue'; | import { onMounted, watch } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/media-proxy'; | ||||||
| import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; | import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash'; | ||||||
| import { acct, userPage } from '@/filters/user'; | import { acct, userPage } from '@/filters/user'; | ||||||
| import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; | import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/media-proxy'; | ||||||
| import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; | import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
| import { getEmojiName } from '@/scripts/emojilist'; | import { getEmojiName } from '@/scripts/emojilist'; | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { onMounted } from 'vue'; | import { onMounted } from 'vue'; | ||||||
| import * as misskey from 'misskey-js'; | import * as misskey from 'misskey-js'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/media-proxy'; | ||||||
| import { notePage } from '@/filters/note'; | import { notePage } from '@/filters/note'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import MkContainer from '@/components/MkContainer.vue'; | import MkContainer from '@/components/MkContainer.vue'; | ||||||
|  |  | ||||||
|  | @ -1,19 +0,0 @@ | ||||||
| import { url as instanceUrl } from '@/config'; |  | ||||||
| import * as url from '@/scripts/url'; |  | ||||||
| 
 |  | ||||||
| export function getStaticImageUrl(baseUrl: string): string { |  | ||||||
| 	const u = new URL(baseUrl); |  | ||||||
| 	if (u.href.startsWith(`${instanceUrl}/proxy/`)) { |  | ||||||
| 		// もう既にproxyっぽそうだったらsearchParams付けるだけ
 |  | ||||||
| 		u.searchParams.set('static', '1'); |  | ||||||
| 		return u.href; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する
 |  | ||||||
| 	const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; |  | ||||||
| 
 |  | ||||||
| 	return `${instanceUrl}/proxy/${dummy}?${url.query({ |  | ||||||
| 		url: u.href, |  | ||||||
| 		static: '1', |  | ||||||
| 	})}`;
 |  | ||||||
| } |  | ||||||
|  | @ -1,7 +1,15 @@ | ||||||
| import { query } from '@/scripts/url'; | import { query, appendQuery } from '@/scripts/url'; | ||||||
| import { url } from '@/config'; | import { url } from '@/config'; | ||||||
| 
 | 
 | ||||||
| export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { | export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { | ||||||
|  | 	if (imageUrl.startsWith(`${url}/proxy/`) || imageUrl.startsWith('/proxy/')) { | ||||||
|  | 		// もう既にproxyっぽそうだったらsearchParams付けるだけ
 | ||||||
|  | 		return appendQuery(imageUrl, query({ | ||||||
|  | 			fallback: '1', | ||||||
|  | 			...(type ? { [type]: '1' } : {}), | ||||||
|  | 		})); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return `${url}/proxy/image.webp?${query({ | 	return `${url}/proxy/image.webp?${query({ | ||||||
| 		url: imageUrl, | 		url: imageUrl, | ||||||
| 		fallback: '1', | 		fallback: '1', | ||||||
|  | @ -13,3 +21,27 @@ export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, | ||||||
| 	if (imageUrl == null) return null; | 	if (imageUrl == null) return null; | ||||||
| 	return getProxiedImageUrl(imageUrl, type); | 	return getProxiedImageUrl(imageUrl, type); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export function getStaticImageUrl(baseUrl: string): string { | ||||||
|  | 	const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); | ||||||
|  | 
 | ||||||
|  | 	if (u.href.startsWith(`${url}/proxy/`)) { | ||||||
|  | 		// もう既にproxyっぽそうだったらsearchParams付けるだけ
 | ||||||
|  | 		u.searchParams.set('static', '1'); | ||||||
|  | 		return u.href; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (u.href.startsWith(`${url}/emoji/`)) { | ||||||
|  | 		// もう既にemojiっぽそうだったらsearchParams付けるだけ
 | ||||||
|  | 		u.searchParams.set('static', '1'); | ||||||
|  | 		return u.href; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する
 | ||||||
|  | 	const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; | ||||||
|  | 
 | ||||||
|  | 	return `${url}/proxy/${dummy}?${query({ | ||||||
|  | 		url: u.href, | ||||||
|  | 		static: '1', | ||||||
|  | 	})}`;
 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | /* objを検査して | ||||||
|  |  * 1. 配列に何も入っていない時はクエリを付けない | ||||||
|  |  * 2. プロパティがundefinedの時はクエリを付けない | ||||||
|  |  * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) | ||||||
|  |  */  | ||||||
| export function query(obj: Record<string, any>): string { | export function query(obj: Record<string, any>): string { | ||||||
| 	const params = Object.entries(obj) | 	const params = Object.entries(obj) | ||||||
| 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue'; | ||||||
| import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; | ||||||
| import { GetFormResultType } from '@/scripts/form'; | import { GetFormResultType } from '@/scripts/form'; | ||||||
| import { stream } from '@/stream'; | import { stream } from '@/stream'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/media-proxy'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import MkContainer from '@/components/MkContainer.vue'; | import MkContainer from '@/components/MkContainer.vue'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
|  |  | ||||||
|  | @ -1,3 +1,8 @@ | ||||||
|  | /* objを検査して | ||||||
|  |  * 1. 配列に何も入っていない時はクエリを付けない | ||||||
|  |  * 2. プロパティがundefinedの時はクエリを付けない | ||||||
|  |  * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) | ||||||
|  |  */  | ||||||
| export function query(obj: object): string { | export function query(obj: object): string { | ||||||
| 	const params = Object.entries(obj) | 	const params = Object.entries(obj) | ||||||
| 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | 		.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) | ||||||
|  |  | ||||||
							
								
								
									
										49
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										49
									
								
								yarn.lock
									
										
									
									
									
								
							|  | @ -4257,7 +4257,7 @@ __metadata: | ||||||
|     sanitize-html: 2.8.1 |     sanitize-html: 2.8.1 | ||||||
|     seedrandom: ^3.0.5 |     seedrandom: ^3.0.5 | ||||||
|     semver: 7.3.8 |     semver: 7.3.8 | ||||||
|     sharp: 0.29.3 |     sharp: 0.31.3 | ||||||
|     speakeasy: 2.0.0 |     speakeasy: 2.0.0 | ||||||
|     strict-event-emitter-types: 2.0.0 |     strict-event-emitter-types: 2.0.0 | ||||||
|     stringz: 2.1.0 |     stringz: 2.1.0 | ||||||
|  | @ -5367,7 +5367,7 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "color@npm:^4.0.1": | "color@npm:^4.2.3": | ||||||
|   version: 4.2.3 |   version: 4.2.3 | ||||||
|   resolution: "color@npm:4.2.3" |   resolution: "color@npm:4.2.3" | ||||||
|   dependencies: |   dependencies: | ||||||
|  | @ -6161,16 +6161,7 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "detect-libc@npm:^1.0.3": | "detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": | ||||||
|   version: 1.0.3 |  | ||||||
|   resolution: "detect-libc@npm:1.0.3" |  | ||||||
|   bin: |  | ||||||
|     detect-libc: ./bin/detect-libc.js |  | ||||||
|   checksum: daaaed925ffa7889bd91d56e9624e6c8033911bb60f3a50a74a87500680652969dbaab9526d1e200a4c94acf80fc862a22131841145a0a8482d60a99c24f4a3e |  | ||||||
|   languageName: node |  | ||||||
|   linkType: hard |  | ||||||
| 
 |  | ||||||
| "detect-libc@npm:^2.0.0": |  | ||||||
|   version: 2.0.1 |   version: 2.0.1 | ||||||
|   resolution: "detect-libc@npm:2.0.1" |   resolution: "detect-libc@npm:2.0.1" | ||||||
|   checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 |   checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 | ||||||
|  | @ -12167,12 +12158,12 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "node-addon-api@npm:^4.2.0": | "node-addon-api@npm:^5.0.0": | ||||||
|   version: 4.3.0 |   version: 5.0.0 | ||||||
|   resolution: "node-addon-api@npm:4.3.0" |   resolution: "node-addon-api@npm:5.0.0" | ||||||
|   dependencies: |   dependencies: | ||||||
|     node-gyp: latest |     node-gyp: latest | ||||||
|   checksum: 3de396e23cc209f539c704583e8e99c148850226f6e389a641b92e8967953713228109f919765abc1f4355e801e8f41842f96210b8d61c7dcc10a477002dcf00 |   checksum: 7c5e2043ac37f6108784d94ed73a44ae6d3e68eb968de60680922fc6bc3d17fa69448c0feb4e0c9d3f4c74a0324822e566a8340a56916d9d6f23cb3e85620334 | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
|  | @ -13672,7 +13663,7 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "prebuild-install@npm:^7.0.0": | "prebuild-install@npm:^7.1.1": | ||||||
|   version: 7.1.1 |   version: 7.1.1 | ||||||
|   resolution: "prebuild-install@npm:7.1.1" |   resolution: "prebuild-install@npm:7.1.1" | ||||||
|   dependencies: |   dependencies: | ||||||
|  | @ -15053,7 +15044,7 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "semver@npm:7.3.8, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7": | "semver@npm:7.3.8, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7, semver@npm:^7.3.8": | ||||||
|   version: 7.3.8 |   version: 7.3.8 | ||||||
|   resolution: "semver@npm:7.3.8" |   resolution: "semver@npm:7.3.8" | ||||||
|   dependencies: |   dependencies: | ||||||
|  | @ -15146,20 +15137,20 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "sharp@npm:0.29.3": | "sharp@npm:0.31.3": | ||||||
|   version: 0.29.3 |   version: 0.31.3 | ||||||
|   resolution: "sharp@npm:0.29.3" |   resolution: "sharp@npm:0.31.3" | ||||||
|   dependencies: |   dependencies: | ||||||
|     color: ^4.0.1 |     color: ^4.2.3 | ||||||
|     detect-libc: ^1.0.3 |     detect-libc: ^2.0.1 | ||||||
|     node-addon-api: ^4.2.0 |     node-addon-api: ^5.0.0 | ||||||
|     node-gyp: latest |     node-gyp: latest | ||||||
|     prebuild-install: ^7.0.0 |     prebuild-install: ^7.1.1 | ||||||
|     semver: ^7.3.5 |     semver: ^7.3.8 | ||||||
|     simple-get: ^4.0.0 |     simple-get: ^4.0.1 | ||||||
|     tar-fs: ^2.1.1 |     tar-fs: ^2.1.1 | ||||||
|     tunnel-agent: ^0.6.0 |     tunnel-agent: ^0.6.0 | ||||||
|   checksum: d496cdd546c9abe743aebcee013731295f735687819a18c2bdcbba6f31a6b259f3da95af5c11260a8fedc9d4ab95697f5f8c4f3cd65232792b5cfb876bea7c9a |   checksum: 29fd1dfbc616c6389f53f366cec342b4353d9f2a37e98952ca273db38dca57dfa0f336322d6d763f0fae876042ead22fd86ffe26d70c32ade2458d421db60d04 | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
|  | @ -15204,7 +15195,7 @@ __metadata: | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
| 
 | 
 | ||||||
| "simple-get@npm:^4.0.0": | "simple-get@npm:^4.0.0, simple-get@npm:^4.0.1": | ||||||
|   version: 4.0.1 |   version: 4.0.1 | ||||||
|   resolution: "simple-get@npm:4.0.1" |   resolution: "simple-get@npm:4.0.1" | ||||||
|   dependencies: |   dependencies: | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue