From aee7ed992b40c31c0e0c4f2eff3f2403ecaecdd0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 4 Mar 2023 10:17:45 +0900 Subject: [PATCH 01/46] enhance(client): add share button to clip and channel page Resolve #10183 --- CHANGELOG.md | 8 ++++++++ packages/frontend/src/pages/channel.vue | 11 +++++++++++ packages/frontend/src/pages/clip.vue | 13 ++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a5ab3e1..67d1d5070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ You should also include the user name that made the change. --> +## 13.x.x (unreleased) + +### Improvements +- クリップ、チャンネルページに共有ボタンを追加 + +### Bugfixes +- + ## 13.9.1 (2023/03/03) ### Bugfixes diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 6b4fcb32f..7e9cebbd4 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -47,6 +47,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { deviceKind } from '@/scripts/device-kind'; import MkNotes from '@/components/MkNotes.vue'; +import { url } from '@/config'; const router = useRouter(); @@ -77,6 +78,16 @@ function edit() { } const headerActions = $computed(() => channel && channel.userId ? [{ + icon: 'ti ti-share', + text: i18n.ts.share, + handler: async (): Promise => { + navigator.share({ + title: channel.name, + text: channel.description, + url: `${url}/channels/${channel.id}`, + }); + }, +}, { icon: 'ti ti-settings', text: i18n.ts.edit, handler: edit, diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index d4e8f2700..d66088d33 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -26,6 +26,7 @@ import { $i } from '@/account'; import { i18n } from '@/i18n'; import * as os from '@/os'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { url } from '@/config'; const props = defineProps<{ clipId: string, @@ -82,7 +83,17 @@ const headerActions = $computed(() => clip && isOwned ? [{ ...result, }); }, -}, { +}, ...(clip.isPublic ? [{ + icon: 'ti ti-share', + text: i18n.ts.share, + handler: async (): Promise => { + navigator.share({ + title: clip.name, + text: clip.description, + url: `${url}/clips/${clip.id}`, + }); + }, +}] : []), { icon: 'ti ti-trash', text: i18n.ts.delete, danger: true, From dcd4d808690f2ee9b9bea6bdfd164efa43296d52 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 4 Mar 2023 10:34:54 +0900 Subject: [PATCH 02/46] enhance(client): improve channel ui --- locales/ja-JP.yml | 2 ++ packages/frontend/src/pages/channel.vue | 29 ++++++++++++++++++- .../frontend/src/pages/settings/general.vue | 2 ++ .../pages/settings/preferences-backups.vue | 1 + packages/frontend/src/store.ts | 4 +++ packages/frontend/src/style.scss | 6 ++++ .../frontend/src/ui/deck/channel-column.vue | 1 - 7 files changed, 43 insertions(+), 2 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8fee2726e..ef7046993 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -506,6 +506,7 @@ objectStorageSetPublicRead: "アップロード時に'public-read'を設定す serverLogs: "サーバーログ" deleteAll: "全て削除" showFixedPostForm: "タイムライン上部に投稿フォームを表示する" +showFixedPostFormInChannel: "タイムライン上部に投稿フォームを表示する(チャンネル)" newNoteRecived: "新しいノートがあります" sounds: "サウンド" sound: "サウンド" @@ -955,6 +956,7 @@ exploreOtherServers: "他のサーバーを探す" letsLookAtTimeline: "タイムラインを見てみる" disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。" invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。" +postToTheChannel: "チャンネルに投稿" _achievements: earnedAt: "獲得日時" diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 7e9cebbd4..81fc5ec87 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -24,7 +24,7 @@ - + @@ -32,6 +32,15 @@ + @@ -48,6 +57,8 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import { deviceKind } from '@/scripts/device-kind'; import MkNotes from '@/components/MkNotes.vue'; import { url } from '@/config'; +import MkButton from '@/components/MkButton.vue'; +import { defaultStore } from '@/store'; const router = useRouter(); @@ -77,6 +88,14 @@ function edit() { router.push(`/channels/${channel.id}/edit`); } +function openPostForm() { + os.post({ + channel: { + id: channel.channelId, + }, + }); +} + const headerActions = $computed(() => channel && channel.userId ? [{ icon: 'ti ti-share', text: i18n.ts.share, @@ -109,6 +128,14 @@ definePageMetadata(computed(() => channel ? { } : null)); + + - From 49f0837729ab094d2f7646c77ff7ba16a39430c0 Mon Sep 17 00:00:00 2001 From: rinsuki <428rinsuki+git@gmail.com> Date: Sat, 4 Mar 2023 16:48:50 +0900 Subject: [PATCH 05/46] fix(server): DriveFile related N+1 query when call note packMany (again) (#10190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "fix(server): DriveFile related N+1 query when call note packMany (#10133)"" This reverts commit a7c82eeabcc732e76c5c358c98812cac8457d57f. * packManyByIdsMap: 存在チェックをしてなかったものは null を入れるように * Note.packMany で reply とか renote がもうあったらそのファイルも引く * テストを書く * fix test * fix test * fix test * fix test --- .../core/entities/DriveFileEntityService.ts | 28 ++++- .../core/entities/GalleryPostEntityService.ts | 3 +- .../src/core/entities/NoteEntityService.ts | 24 +++- packages/backend/test/e2e/note.ts | 118 +++++++++++++++++- 4 files changed, 168 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 158fafa9d..f769ddd5e 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -1,5 +1,5 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { NotesRepository, DriveFilesRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; @@ -21,6 +21,7 @@ type PackOptions = { }; import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; +import { isNotNull } from '@/misc/is-not-null.js'; @Injectable() export class DriveFileEntityService { @@ -255,10 +256,33 @@ export class DriveFileEntityService { @bindThis public async packMany( - files: (DriveFile['id'] | DriveFile)[], + files: DriveFile[], options?: PackOptions, ): Promise[]> { const items = await Promise.all(files.map(f => this.packNullable(f, options))); return items.filter((x): x is Packed<'DriveFile'> => x != null); } + + @bindThis + public async packManyByIdsMap( + fileIds: DriveFile['id'][], + options?: PackOptions, + ): Promise['id'], Packed<'DriveFile'> | null>> { + const files = await this.driveFilesRepository.findBy({ id: In(fileIds) }); + const packedFiles = await this.packMany(files, options); + const map = new Map['id'], Packed<'DriveFile'> | null>(packedFiles.map(f => [f.id, f])); + for (const id of fileIds) { + if (!map.has(id)) map.set(id, null); + } + return map; + } + + @bindThis + public async packManyByIds( + fileIds: DriveFile['id'][], + options?: PackOptions, + ): Promise[]> { + const filesMap = await this.packManyByIdsMap(fileIds, options); + return fileIds.map(id => filesMap.get(id)).filter(isNotNull); + } } diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts index ab29e7dba..fb147ae18 100644 --- a/packages/backend/src/core/entities/GalleryPostEntityService.ts +++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts @@ -41,7 +41,8 @@ export class GalleryPostEntityService { title: post.title, description: post.description, fileIds: post.fileIds, - files: this.driveFileEntityService.packMany(post.fileIds), + // TODO: packMany causes N+1 queries + files: this.driveFileEntityService.packManyByIds(post.fileIds), tags: post.tags.length > 0 ? post.tags : undefined, isSensitive: post.isSensitive, likedCount: post.likedCount, diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 2ffe5f1c2..4ec10df9a 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,6 +11,7 @@ import type { Note } from '@/models/entities/Note.js'; import type { NoteReaction } from '@/models/entities/NoteReaction.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, DriveFilesRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; +import { isNotNull } from '@/misc/is-not-null.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; @@ -248,6 +249,21 @@ export class NoteEntityService implements OnModuleInit { return true; } + @bindThis + public async packAttachedFiles(fileIds: Note['fileIds'], packedFiles: Map | null>): Promise[]> { + const missingIds = []; + for (const id of fileIds) { + if (!packedFiles.has(id)) missingIds.push(id); + } + if (missingIds.length) { + const additionalMap = await this.driveFileEntityService.packManyByIdsMap(missingIds); + for (const [k, v] of additionalMap) { + packedFiles.set(k, v); + } + } + return fileIds.map(id => packedFiles.get(id)).filter(isNotNull); + } + @bindThis public async pack( src: Note['id'] | Note, @@ -257,6 +273,7 @@ export class NoteEntityService implements OnModuleInit { skipHide?: boolean; _hint_?: { myReactions: Map; + packedFiles: Map | null>; }; }, ): Promise> { @@ -284,6 +301,7 @@ export class NoteEntityService implements OnModuleInit { const reactionEmojiNames = Object.keys(note.reactions) .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); + const packedFiles = options?._hint_?.packedFiles; const packed: Packed<'Note'> = await awaitAll({ id: note.id, @@ -304,7 +322,7 @@ export class NoteEntityService implements OnModuleInit { emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, tags: note.tags.length > 0 ? note.tags : undefined, fileIds: note.fileIds, - files: this.driveFileEntityService.packMany(note.fileIds), + files: packedFiles != null ? this.packAttachedFiles(note.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(note.fileIds), replyId: note.replyId, renoteId: note.renoteId, channelId: note.channelId ?? undefined, @@ -388,11 +406,15 @@ export class NoteEntityService implements OnModuleInit { } await this.customEmojiService.prefetchEmojis(this.customEmojiService.aggregateNoteEmojis(notes)); + // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく + const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); + const packedFiles = await this.driveFileEntityService.packManyByIdsMap(fileIds); return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { myReactions: myReactionsMap, + packedFiles, }, }))); } diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 98ee34d8d..1b5f9580d 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { Note } from '@/models/entities/Note.js'; -import { signup, post, uploadUrl, startServer, initTestDb, api } from '../utils.js'; +import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; describe('Note', () => { @@ -213,6 +213,122 @@ describe('Note', () => { assert.deepStrictEqual(noteDoc.mentions, [bob.id]); }); + describe('添付ファイル情報', () => { + test('ファイルを添付した場合、投稿成功時にファイル情報入りのレスポンスが帰ってくる', async () => { + const file = await uploadFile(alice); + const res = await api('/notes/create', { + fileIds: [file.body.id], + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.files.length, 1); + assert.strictEqual(res.body.createdNote.files[0].id, file.body.id); + }); + + test('ファイルを添付した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { + const file = await uploadFile(alice); + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], + }, alice); + + assert.strictEqual(createdNote.status, 200); + + const res = await api('/notes', { + withFiles: true, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); + assert.notEqual(myNote, null); + assert.strictEqual(myNote.files.length, 1); + assert.strictEqual(myNote.files[0].id, file.body.id); + }); + + test('ファイルが添付されたノートをリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { + const file = await uploadFile(alice); + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], + }, alice); + + assert.strictEqual(createdNote.status, 200); + + const renoted = await api('/notes/create', { + renoteId: createdNote.body.createdNote.id, + }, alice); + assert.strictEqual(renoted.status, 200); + + const res = await api('/notes', { + renote: true, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); + assert.notEqual(myNote, null); + assert.strictEqual(myNote.renote.files.length, 1); + assert.strictEqual(myNote.renote.files[0].id, file.body.id); + }); + + test('ファイルが添付されたノートに返信した場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { + const file = await uploadFile(alice); + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], + }, alice); + + assert.strictEqual(createdNote.status, 200); + + const reply = await api('/notes/create', { + replyId: createdNote.body.createdNote.id, + text: 'this is reply', + }, alice); + assert.strictEqual(reply.status, 200); + + const res = await api('/notes', { + reply: true, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); + assert.notEqual(myNote, null); + assert.strictEqual(myNote.reply.files.length, 1); + assert.strictEqual(myNote.reply.files[0].id, file.body.id); + }); + + test('ファイルが添付されたノートへの返信をリノートした場合、タイムラインでファイル情報入りのレスポンスが帰ってくる', async () => { + const file = await uploadFile(alice); + const createdNote = await api('/notes/create', { + fileIds: [file.body.id], + }, alice); + + assert.strictEqual(createdNote.status, 200); + + const reply = await api('/notes/create', { + replyId: createdNote.body.createdNote.id, + text: 'this is reply', + }, alice); + assert.strictEqual(reply.status, 200); + + const renoted = await api('/notes/create', { + renoteId: reply.body.createdNote.id, + }, alice); + assert.strictEqual(renoted.status, 200); + + const res = await api('/notes', { + renote: true, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); + assert.notEqual(myNote, null); + assert.strictEqual(myNote.renote.reply.files.length, 1); + assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id); + }); + }); + describe('notes/create', () => { test('投票を添付できる', async () => { const res = await api('/notes/create', { From 2d551a8598de12210ddb7f708561e51867ce3f10 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 4 Mar 2023 16:51:07 +0900 Subject: [PATCH 06/46] =?UTF-8?q?enhance(server):=20downloadUrl=E3=81=A7Co?= =?UTF-8?q?ntent-Disposition=E3=81=8B=E3=82=89=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E5=90=8D=E3=82=92=E5=8F=96=E5=BE=97=20(#10150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(server): downloadUrlでContent-Dispositionからファイル名を取得 Resolve #10036 Resolve #4750 * untitled * オブジェクトストレージのContent-Dispositionのファイル名の拡張子をContent-Typeに添ったものにする * :v: * tiff * fix filename * add test * /files/でもContent-Disposition * comment * fix test --- packages/backend/src/core/DownloadService.ts | 20 +++++++- packages/backend/src/core/DriveService.ts | 44 +++++++++-------- packages/backend/src/misc/correct-filename.ts | 15 ++++++ .../backend/src/server/FileServerService.ts | 48 +++++++++++-------- packages/backend/test/e2e/endpoints.ts | 10 +++- packages/backend/test/unit/misc/others.ts | 39 +++++++++++++++ 6 files changed, 134 insertions(+), 42 deletions(-) create mode 100644 packages/backend/src/misc/correct-filename.ts create mode 100644 packages/backend/test/unit/misc/others.ts diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 852c1f32e..bd999c67d 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -6,6 +6,7 @@ import IPCIDR from 'ip-cidr'; import PrivateIp from 'private-ip'; import chalk from 'chalk'; import got, * as Got from 'got'; +import { parse } from 'content-disposition'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -32,13 +33,18 @@ export class DownloadService { } @bindThis - public async downloadUrl(url: string, path: string): Promise { + public async downloadUrl(url: string, path: string): Promise<{ + filename: string; + }> { this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); const timeout = 30 * 1000; const operationTimeout = 60 * 1000; const maxSize = this.config.maxFileSize ?? 262144000; + const urlObj = new URL(url); + let filename = urlObj.pathname.split('/').pop() ?? 'untitled'; + const req = got.stream(url, { headers: { 'User-Agent': this.config.userAgent, @@ -77,6 +83,14 @@ export class DownloadService { req.destroy(); } } + + const contentDisposition = res.headers['content-disposition']; + if (contentDisposition != null) { + const parsed = parse(contentDisposition); + if (parsed.parameters.filename) { + filename = parsed.parameters.filename; + } + } }).on('downloadProgress', (progress: Got.Progress) => { if (progress.transferred > maxSize) { this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`); @@ -95,6 +109,10 @@ export class DownloadService { } this.logger.succ(`Download finished: ${chalk.cyan(url)}`); + + return { + filename, + }; } @bindThis diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index b15c967c8..f4a06faeb 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -34,6 +34,7 @@ import { FileInfoService } from '@/core/FileInfoService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type S3 from 'aws-sdk/clients/s3.js'; +import { correctFilename } from '@/misc/correct-filename.js'; type AddFileArgs = { /** User who wish to add file */ @@ -168,7 +169,7 @@ export class DriveService { //#region Uploads this.registerLogger.info(`uploading original: ${key}`); const uploads = [ - this.upload(key, fs.createReadStream(path), type, name), + this.upload(key, fs.createReadStream(path), type, ext, name), ]; if (alts.webpublic) { @@ -176,7 +177,7 @@ export class DriveService { webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); - uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, name)); + uploads.push(this.upload(webpublicKey, alts.webpublic.data, alts.webpublic.type, alts.webpublic.ext, name)); } if (alts.thumbnail) { @@ -184,7 +185,7 @@ export class DriveService { thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); - uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type)); + uploads.push(this.upload(thumbnailKey, alts.thumbnail.data, alts.thumbnail.type, alts.thumbnail.ext)); } await Promise.all(uploads); @@ -360,7 +361,7 @@ export class DriveService { * Upload to ObjectStorage */ @bindThis - private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { + private async upload(key: string, stream: fs.ReadStream | Buffer, type: string, ext?: string | null, filename?: string) { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; @@ -374,7 +375,12 @@ export class DriveService { CacheControl: 'max-age=31536000, immutable', } as S3.PutObjectRequest; - if (filename) params.ContentDisposition = contentDisposition('inline', filename); + if (filename) params.ContentDisposition = contentDisposition( + 'inline', + // 拡張子からContent-Typeを設定してそうな挙動を示すオブジェクトストレージ (upcloud?) も存在するので、 + // 許可されているファイル形式でしか拡張子をつけない + ext ? correctFilename(filename, ext) : filename, + ); if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; const s3 = this.s3Service.getS3(meta); @@ -466,7 +472,12 @@ export class DriveService { //} // detect name - const detectedName = name ?? (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); + const detectedName = correctFilename( + // DriveFile.nameは256文字, validateFileNameは200文字制限であるため、 + // extを付加してデータベースの文字数制限に当たることはまずない + (name && this.driveFileEntityService.validateFileName(name)) ? name : 'untitled', + info.type.ext + ); if (user && !force) { // Check if there is a file with the same hash @@ -736,24 +747,19 @@ export class DriveService { requestIp = null, requestHeaders = null, }: UploadFromUrlArgs): Promise { - let name = new URL(url).pathname.split('/').pop() ?? null; - if (name == null || !this.driveFileEntityService.validateFileName(name)) { - name = null; - } - - // If the comment is same as the name, skip comment - // (image.name is passed in when receiving attachment) - if (comment !== null && name === comment) { - comment = null; - } - // Create temp file const [path, cleanup] = await createTemp(); try { // write content at URL to temp file - await this.downloadService.downloadUrl(url, path); - + const { filename: name } = await this.downloadService.downloadUrl(url, path); + + // If the comment is same as the name, skip comment + // (image.name is passed in when receiving attachment) + if (comment !== null && name === comment) { + comment = null; + } + const driveFile = await this.addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); this.downloaderLogger.succ(`Got: ${driveFile.id}`); return driveFile!; diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts new file mode 100644 index 000000000..8dcce6df7 --- /dev/null +++ b/packages/backend/src/misc/correct-filename.ts @@ -0,0 +1,15 @@ +// 与えられた拡張子とファイル名が一致しているかどうかを確認し、 +// 一致していない場合は拡張子を付与して返す +export function correctFilename(filename: string, ext: string | null) { + const dotExt = ext ? `.${ext}` : '.unknown'; + if (filename.endsWith(dotExt)) { + return filename; + } + if (ext === 'jpg' && filename.endsWith('.jpeg')) { + return filename; + } + if (ext === 'tif' && filename.endsWith('.tiff')) { + return filename; + } + return `${filename}${dotExt}`; +} diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index e5eefac1f..8eeb76f7c 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -22,6 +22,7 @@ import { bindThis } from '@/decorators.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; import { isMimeImage } from '@/misc/is-mime-image.js'; import sharp from 'sharp'; +import { correctFilename } from '@/misc/correct-filename.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -51,15 +52,6 @@ export class FileServerService { //this.createServer = this.createServer.bind(this); } - @bindThis - public commonReadableHandlerGenerator(reply: FastifyReply) { - return (err: Error): void => { - this.logger.error(err); - reply.code(500); - reply.header('Cache-Control', 'max-age=300'); - }; - } - @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.addHook('onRequest', (request, reply, done) => { @@ -190,13 +182,19 @@ export class FileServerService { } reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream'); + reply.header('Content-Disposition', + contentDisposition( + 'inline', + correctFilename(file.filename, image.ext) + ) + ); return image.data; } if (file.fileRole !== 'original') { - const filename = rename(file.file.name, { + const filename = rename(file.filename, { suffix: file.fileRole === 'thumbnail' ? '-thumb' : '-web', - extname: file.ext ? `.${file.ext}` : undefined, + extname: file.ext ? `.${file.ext}` : '.unknown', }).toString(); reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); @@ -204,12 +202,10 @@ export class FileServerService { reply.header('Content-Disposition', contentDisposition('inline', filename)); return fs.createReadStream(file.path); } else { - const stream = fs.createReadStream(file.path); - stream.on('error', this.commonReadableHandlerGenerator(reply)); reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); - reply.header('Content-Disposition', contentDisposition('inline', file.file.name)); - return stream; + reply.header('Content-Disposition', contentDisposition('inline', file.filename)); + return fs.createReadStream(file.path); } } catch (e) { if ('cleanup' in file) file.cleanup(); @@ -360,6 +356,12 @@ export class FileServerService { reply.header('Content-Type', image.type); reply.header('Cache-Control', 'max-age=31536000, immutable'); + reply.header('Content-Disposition', + contentDisposition( + 'inline', + correctFilename(file.filename, image.ext) + ) + ); return image.data; } catch (e) { if ('cleanup' in file) file.cleanup(); @@ -369,8 +371,8 @@ export class FileServerService { @bindThis private async getStreamAndTypeFromUrl(url: string): Promise< - { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; } + { state: 'remote'; fileRole?: 'thumbnail' | 'webpublic' | 'original'; file?: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } + | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' > { @@ -386,11 +388,11 @@ export class FileServerService { @bindThis private async downloadAndDetectTypeFromUrl(url: string): Promise< - { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; } + { state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; } > { const [path, cleanup] = await createTemp(); try { - await this.downloadService.downloadUrl(url, path); + const { filename } = await this.downloadService.downloadUrl(url, path); const { mime, ext } = await this.fileInfoService.detectType(path); @@ -398,6 +400,7 @@ export class FileServerService { state: 'remote', mime, ext, path, cleanup, + filename, }; } catch (e) { cleanup(); @@ -407,8 +410,8 @@ export class FileServerService { @bindThis private async getFileFromKey(key: string): Promise< - { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } - | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; } + { state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; } + | { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; filename: string; mime: string; ext: string | null; path: string; } | '404' | '204' > { @@ -432,6 +435,7 @@ export class FileServerService { url: file.uri, fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original', file, + filename: file.name, }; } @@ -443,6 +447,7 @@ export class FileServerService { state: 'stored_internal', fileRole: isThumbnail ? 'thumbnail' : 'webpublic', file, + filename: file.name, mime, ext, path, }; @@ -452,6 +457,7 @@ export class FileServerService { state: 'stored_internal', fileRole: 'original', file, + filename: file.name, mime: file.type, ext: null, path, diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index e864eab6c..42bdc5f24 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -410,11 +410,19 @@ describe('Endpoints', () => { }); test('ファイルに名前を付けられる', async () => { + const res = await uploadFile(alice, { name: 'Belmond.jpg' }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Belmond.jpg'); + }); + + test('ファイルに名前を付けられるが、拡張子は正しいものになる', async () => { const res = await uploadFile(alice, { name: 'Belmond.png' }); assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.name, 'Belmond.png'); + assert.strictEqual(res.body.name, 'Belmond.png.jpg'); }); test('ファイル無しで怒られる', async () => { diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts new file mode 100644 index 000000000..8241d3078 --- /dev/null +++ b/packages/backend/test/unit/misc/others.ts @@ -0,0 +1,39 @@ +import { describe, test, expect } from '@jest/globals'; +import { contentDisposition } from '@/misc/content-disposition.js'; +import { correctFilename } from '@/misc/correct-filename.js'; + +describe('misc:content-disposition', () => { + test('inline', () => { + expect(contentDisposition('inline', 'foo bar')).toBe('inline; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); + }); + test('attachment', () => { + expect(contentDisposition('attachment', 'foo bar')).toBe('attachment; filename=\"foo_bar\"; filename*=UTF-8\'\'foo%20bar'); + }); + test('non ascii', () => { + expect(contentDisposition('attachment', 'ファイル名')).toBe('attachment; filename=\"_____\"; filename*=UTF-8\'\'%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D'); + }); +}); + +describe('misc:correct-filename', () => { + test('simple', () => { + expect(correctFilename('filename', 'jpg')).toBe('filename.jpg'); + }); + test('with same ext', () => { + expect(correctFilename('filename.jpg', 'jpg')).toBe('filename.jpg'); + }); + test('with different ext', () => { + expect(correctFilename('filename.webp', 'jpg')).toBe('filename.webp.jpg'); + }); + test('non ascii with space', () => { + expect(correctFilename('ファイル 名前', 'jpg')).toBe('ファイル 名前.jpg'); + }); + test('jpeg', () => { + expect(correctFilename('filename.jpeg', 'jpg')).toBe('filename.jpeg'); + }); + test('tiff', () => { + expect(correctFilename('filename.tiff', 'tif')).toBe('filename.tiff'); + }); + test('null ext', () => { + expect(correctFilename('filename', null)).toBe('filename.unknown'); + }); +}); From 3f507241ca98148f274211cbdeeac7b227f287d8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 4 Mar 2023 17:19:18 +0900 Subject: [PATCH 07/46] chore(client): tweak default value of numberOfPageCache --- packages/frontend/src/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 9378cd573..2766b434f 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -275,7 +275,7 @@ export const defaultStore = markRaw(new Storage('base', { }, numberOfPageCache: { where: 'device', - default: 5, + default: 3, }, showNoteActionsOnlyHover: { where: 'device', From 320c2bf7712f06f0498fefaf770fa2c361f8f5d4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 4 Mar 2023 17:19:21 +0900 Subject: [PATCH 08/46] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d1d5070..0ce1c8aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ You should also include the user name that made the change. ### Improvements - クリップ、チャンネルページに共有ボタンを追加 +- ドライブの「URLからアップロード」で、content-dispositionのfilenameがあればそれをファイル名に +- サーバーのパフォーマンスを改善 ### Bugfixes - From 1711ae7156ed2aa36c5f5e43facc66bfba41fe89 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 4 Mar 2023 17:36:11 +0900 Subject: [PATCH 09/46] :art: --- packages/frontend/src/pages/admin/roles.vue | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index d89f0d2a7..25d8f3ad6 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -4,7 +4,6 @@
- {{ i18n.ts._role.new }}
@@ -132,8 +131,20 @@ {{ i18n.ts.save }}
+ {{ i18n.ts._role.new }}
- + + +
+ +
+
+ + +
+ +
+
@@ -155,6 +166,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { instance } from '@/instance'; import { useRouter } from '@/router'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; const ROLE_POLICIES = [ 'gtlAvailable', From 6dd9374b994e3c0cca75124cd174d328df4fb318 Mon Sep 17 00:00:00 2001 From: Ikumi Nakamura <28798279+johnmanjiro13@users.noreply.github.com> Date: Sat, 4 Mar 2023 17:42:03 +0900 Subject: [PATCH 10/46] chore: Replace tab with space (#10185) --- chart/templates/Deployment.yml | 6 +++--- chart/templates/Service.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml index d16aece91..d5dd14f59 100644 --- a/chart/templates/Deployment.yml +++ b/chart/templates/Deployment.yml @@ -3,16 +3,16 @@ kind: Deployment metadata: name: {{ include "misskey.fullname" . }} labels: - {{- include "misskey.labels" . | nindent 4 }} + {{- include "misskey.labels" . | nindent 4 }} spec: selector: matchLabels: - {{- include "misskey.selectorLabels" . | nindent 6 }} + {{- include "misskey.selectorLabels" . | nindent 6 }} replicas: 1 template: metadata: labels: - {{- include "misskey.selectorLabels" . | nindent 8 }} + {{- include "misskey.selectorLabels" . | nindent 8 }} spec: containers: - name: misskey diff --git a/chart/templates/Service.yml b/chart/templates/Service.yml index 320958129..afd851a9f 100644 --- a/chart/templates/Service.yml +++ b/chart/templates/Service.yml @@ -11,4 +11,4 @@ spec: protocol: TCP name: http selector: - {{- include "misskey.selectorLabels" . | nindent 4 }} + {{- include "misskey.selectorLabels" . | nindent 4 }} From 72b315491bfc000a433f7cbd4cbcc6df7fedd5d4 Mon Sep 17 00:00:00 2001 From: nexryai <61890205+nexryai@users.noreply.github.com> Date: Sat, 4 Mar 2023 19:19:55 +0900 Subject: [PATCH 11/46] Fix: If mail delivery is disabled on the server, make the settings page indicate this (#10191) Co-authored-by: syuilo --- locales/ja-JP.yml | 1 + packages/frontend/src/pages/settings/email.vue | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ef7046993..f94133c19 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -956,6 +956,7 @@ exploreOtherServers: "他のサーバーを探す" letsLookAtTimeline: "タイムラインを見てみる" disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。" invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。" +emailNotSupported: "このサーバーではメール配信はサポートされていません" postToTheChannel: "チャンネルに投稿" _achievements: diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index 1734dcfe4..b1e6f223b 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -1,5 +1,5 @@ - From e844710ef27792f49d2ebdd568d377cd58c5efca Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 5 Mar 2023 20:32:02 +0900 Subject: [PATCH 33/46] 13.9.2 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f79acfb90..128aee124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ You should also include the user name that made the change. --> -## 13.x.x (unreleased) +## 13.9.2 (2023/03/05) ### Improvements - クリップ、チャンネルページに共有ボタンを追加 diff --git a/package.json b/package.json index a2d04030f..d3e34e782 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.9.1", + "version": "13.9.2", "codename": "nasubi", "repository": { "type": "git", From 517a7f96c85e65432424f80abead47a3c1bc6727 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 5 Mar 2023 20:46:18 +0900 Subject: [PATCH 34/46] [ci skip] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 128aee124..0f252eaaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ You should also include the user name that made the change. - クリップ、チャンネルページに共有ボタンを追加 - ドライブの「URLからアップロード」で、content-dispositionのfilenameがあればそれをファイル名に - Identiconがローカルとリモートで同じになるように + - これまでのIdenticonは異なる画像になります - サーバーのパフォーマンスを改善 ### Bugfixes From 9f73c23b38b74c5307736e08978e3fb6531035ee Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 5 Mar 2023 20:48:46 +0900 Subject: [PATCH 35/46] [ci skip] fix(client): fix post button of channel --- packages/frontend/src/pages/channel.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index d757e5b1a..65edb97e8 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -85,7 +85,7 @@ function edit() { function openPostForm() { os.post({ channel: { - id: channel.channelId, + id: channel.id, }, }); } From 353b1cc682547275e8548abfe748e3c2acc89376 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 5 Mar 2023 20:52:26 +0900 Subject: [PATCH 36/46] [ci skip] :art: --- packages/frontend/src/components/MkPagination.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 378d0ac02..a1a61a6fd 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -21,14 +21,14 @@
- + {{ i18n.ts.loadMore }}
- + {{ i18n.ts.loadMore }} From 8c9c89a137ab24dd56f8bf3d51fa3d8e3c80a245 Mon Sep 17 00:00:00 2001 From: arrow2nd <44780846+arrow2nd@users.noreply.github.com> Date: Sun, 5 Mar 2023 21:59:41 +0900 Subject: [PATCH 37/46] =?UTF-8?q?fix(frontend):=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E3=83=90?= =?UTF-8?q?=E3=83=83=E3=82=B8=E8=A1=A8=E7=A4=BA=E3=82=92=E9=81=A9=E5=88=87?= =?UTF-8?q?=E3=81=AB=E6=8A=98=E3=82=8A=E8=BF=94=E3=81=99=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=20(#10222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): ユーザーページのバッジ表示を適切に折り返すように * Update CHANGELOG.md --- CHANGELOG.md | 2 +- packages/frontend/src/pages/user/home.vue | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f252eaaf..57cdd41e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - ### Bugfixes -- +- ユーザーページのバッジ表示を適切に折り返すように @arrow2nd You should also include the user name that made the change. --> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 441b19440..02794175a 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -352,6 +352,9 @@ onUnmounted(() => { > .roles { padding: 24px 24px 0 154px; font-size: 0.95em; + display: flex; + flex-wrap: wrap; + gap: 8px; > .role { border: solid 1px var(--color, var(--divider)); @@ -493,7 +496,7 @@ onUnmounted(() => { > .roles { padding: 16px 16px 0 16px; - text-align: center; + justify-content: center; } > .description { From b1c173ec4f77411f8a97c8680905f6d525d529ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Sun, 5 Mar 2023 22:00:17 +0900 Subject: [PATCH 38/46] fix(docker): cannot build docker image on some environments (#10220) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index eeff38e48..fd0b5e1c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,8 +65,8 @@ RUN apt-get update \ && corepack enable \ && groupadd -g "${GID}" misskey \ && useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \ - && find / -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ - && find / -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ + && find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \ + && find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; \ && apt-get clean \ && rm -rf /var/lib/apt/lists From 824398509d805a62bd656aaf3a5c2114b1d24cb4 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 5 Mar 2023 22:01:06 +0900 Subject: [PATCH 39/46] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57cdd41e4..e0db634aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - ### Bugfixes -- ユーザーページのバッジ表示を適切に折り返すように @arrow2nd +x You should also include the user name that made the change. --> @@ -21,6 +21,7 @@ You should also include the user name that made the change. ### Bugfixes - ロールの権限で「一般ユーザー」のロールがいきなり設定できない問題を修正 +- ユーザーページのバッジ表示を適切に折り返すように @arrow2nd ## 13.9.1 (2023/03/03) From 87d0f56dc75cdc5e29cef7a3cd41f41a998f9649 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 6 Mar 2023 01:17:13 +0000 Subject: [PATCH 40/46] =?UTF-8?q?fix=20correctFilename=20ext=E3=81=8C.?= =?UTF-8?q?=E3=81=8B=E3=82=89=E5=A7=8B=E3=81=BE=E3=82=8B=E5=A0=B4=E5=90=88?= =?UTF-8?q?=E3=82=82=E8=80=83=E6=85=AE=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/misc/correct-filename.ts | 2 +- packages/backend/test/unit/misc/others.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index 8dcce6df7..3357d8c1b 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,7 +1,7 @@ // 与えられた拡張子とファイル名が一致しているかどうかを確認し、 // 一致していない場合は拡張子を付与して返す export function correctFilename(filename: string, ext: string | null) { - const dotExt = ext ? `.${ext}` : '.unknown'; + const dotExt = ext ? ext.startsWith('.') ? ext : `.${ext}` : '.unknown'; if (filename.endsWith(dotExt)) { return filename; } diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index 8241d3078..c476aef33 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -21,6 +21,9 @@ describe('misc:correct-filename', () => { test('with same ext', () => { expect(correctFilename('filename.jpg', 'jpg')).toBe('filename.jpg'); }); + test('.ext', () => { + expect(correctFilename('filename.jpg', '.jpg')).toBe('filename.jpg'); + }); test('with different ext', () => { expect(correctFilename('filename.webp', 'jpg')).toBe('filename.webp.jpg'); }); From 6bb11492fa3c8de2189085541eb36f9268587320 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 6 Mar 2023 01:31:44 +0000 Subject: [PATCH 41/46] update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0db634aa..543d1a8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,11 @@ x You should also include the user name that made the change. --> -## 13.9.2 (2023/03/05) +## 13.9.2 (2023/03/06) ### Improvements - クリップ、チャンネルページに共有ボタンを追加 +- チャンネルでタイムライン上部に投稿フォームを表示するかどうかのオプションを追加 - ドライブの「URLからアップロード」で、content-dispositionのfilenameがあればそれをファイル名に - Identiconがローカルとリモートで同じになるように - これまでのIdenticonは異なる画像になります From b719f6cd1c57dad42ccd13539d1f860f0db0be31 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 6 Mar 2023 10:32:36 +0900 Subject: [PATCH 42/46] New Crowdin updates (#10223) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Italian) * New translations ja-JP.yml (Italian) --- locales/it-IT.yml | 3 +++ locales/th-TH.yml | 1 + locales/zh-CN.yml | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/locales/it-IT.yml b/locales/it-IT.yml index ca1702585..d5638aeb6 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -506,6 +506,7 @@ objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di cari serverLogs: "Log del server" deleteAll: "Cancella cronologia" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" +showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" newNoteRecived: "Vedi le nuove note" sounds: "Impostazioni suoni" sound: "Impostazioni suoni" @@ -955,7 +956,9 @@ exploreOtherServers: "Trova altre istanze" letsLookAtTimeline: "Sbircia la timeline" disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione." invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore." +emailNotSupported: "L'istanza non supporta l'invio di email" postToTheChannel: "Pubblica sul canale" +cannotBeChangedLater: "Non sarà più modificabile" _achievements: earnedAt: "Data di conseguimento" _types: diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 4511f1a40..cf33e6642 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -958,6 +958,7 @@ disableFederationWarn: "การดำเนินการนี้ถ้า invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ" emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ" postToTheChannel: "โพสต์ลงช่อง" +cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" _achievements: earnedAt: "ได้รับเมื่อ" _types: diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c1dd2a5da..7798582db 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -506,6 +506,7 @@ objectStorageSetPublicRead: "上传时设置为public-read" serverLogs: "服务器日志" deleteAll: "全部删除" showFixedPostForm: "在时间线顶部显示发帖框" +showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)" newNoteRecived: "有新的帖子" sounds: "提示音" sound: "提示音" @@ -955,6 +956,9 @@ exploreOtherServers: "探索其他服务器" letsLookAtTimeline: "时间线" disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。" invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。" +emailNotSupported: "此服务器不支持发送邮件" +postToTheChannel: "发布到频道" +cannotBeChangedLater: "之后不能再更改。" _achievements: earnedAt: "达成时间" _types: From 6778111891a2810be42f1b83a1b119ff00795a55 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 6 Mar 2023 02:04:53 +0000 Subject: [PATCH 43/46] update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543d1a8aa..424eeaf00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ You should also include the user name that made the change. ### Improvements - クリップ、チャンネルページに共有ボタンを追加 - チャンネルでタイムライン上部に投稿フォームを表示するかどうかのオプションを追加 +- ブラウザでメディアプロキシ(/proxy)からファイルを保存した際に、なるべくオリジナルのファイル名を継承するように - ドライブの「URLからアップロード」で、content-dispositionのfilenameがあればそれをファイル名に - Identiconがローカルとリモートで同じになるように - これまでのIdenticonは異なる画像になります From 7852815fc3e6f617d947b40fa9ecc18e11f612ec Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 6 Mar 2023 02:08:06 +0000 Subject: [PATCH 44/46] update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 424eeaf00..30264e75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ You should also include the user name that made the change. ### Bugfixes - ロールの権限で「一般ユーザー」のロールがいきなり設定できない問題を修正 - ユーザーページのバッジ表示を適切に折り返すように @arrow2nd +- macOSでDev Containerが動作しない問題を修正 @RyotaK ## 13.9.1 (2023/03/03) From f20abb4ee53b9f809969097918d7275009433251 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 6 Mar 2023 02:12:35 +0000 Subject: [PATCH 45/46] =?UTF-8?q?fix(client):=20=E3=81=BF=E3=81=A4?= =?UTF-8?q?=E3=81=91=E3=82=8B=E3=81=AE=E3=83=AD=E3=83=BC=E3=83=AB=E4=B8=80?= =?UTF-8?q?=E8=A6=A7=E3=81=A7=E3=82=B3=E3=83=B3=E3=83=87=E3=82=A3=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=8A=E3=83=AB=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=8C?= =?UTF-8?q?=E5=90=AB=E3=81=BE=E3=82=8C=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + packages/frontend/src/pages/explore.roles.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0db634aa..ad0ac15a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ You should also include the user name that made the change. ### Bugfixes - ロールの権限で「一般ユーザー」のロールがいきなり設定できない問題を修正 - ユーザーページのバッジ表示を適切に折り返すように @arrow2nd +- fix(client): みつけるのロール一覧でコンディショナルロールが含まれるのを修正 ## 13.9.1 (2023/03/03) diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue index 8be11008c..51177d079 100644 --- a/packages/frontend/src/pages/explore.roles.vue +++ b/packages/frontend/src/pages/explore.roles.vue @@ -16,7 +16,7 @@ let roles = $ref(); os.api('roles/list', { limit: 30, }).then(res => { - roles = res; + roles = res.filter(x => x.target === 'manual'); }); From 31f9ea31b9bd5cf008c8c1d9fde7db5cb9522a01 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 6 Mar 2023 11:53:04 +0900 Subject: [PATCH 46/46] =?UTF-8?q?[ci=20skip]=20chore(client):=20showNoteAc?= =?UTF-8?q?tionsOnlyHover=E5=A4=89=E6=9B=B4=E6=99=82=E3=81=AB=E3=83=AA?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=82=92=E5=87=BA=E3=81=99=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/settings/general.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 8c8c9b9db..2e2c456c0 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -193,6 +193,7 @@ watch([ enableInfiniteScroll, squareAvatars, aiChanMode, + showNoteActionsOnlyHover, showGapBetweenNotesInTimeline, instanceTicker, overridedDeviceKind,