From 2e051c5871802e29d377443524ebd85417485cc6 Mon Sep 17 00:00:00 2001 From: Nanashia Date: Sun, 19 Mar 2023 20:26:38 +0900 Subject: [PATCH 01/48] test(backend): Add tests for clips (#10358) --- packages/backend/test/e2e/clips.ts | 962 +++++++++++++++++++++++++++++ packages/backend/test/utils.ts | 47 ++ 2 files changed, 1009 insertions(+) create mode 100644 packages/backend/test/e2e/clips.ts diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts new file mode 100644 index 0000000000..f35aae9dc6 --- /dev/null +++ b/packages/backend/test/e2e/clips.ts @@ -0,0 +1,962 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { JTDDataType } from 'ajv/dist/jtd'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { paramDef as CreateParamDef } from '@/server/api/endpoints/clips/create.js'; +import { paramDef as UpdateParamDef } from '@/server/api/endpoints/clips/update.js'; +import { paramDef as DeleteParamDef } from '@/server/api/endpoints/clips/delete.js'; +import { paramDef as ShowParamDef } from '@/server/api/endpoints/clips/show.js'; +import { paramDef as FavoriteParamDef } from '@/server/api/endpoints/clips/favorite.js'; +import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unfavorite.js'; +import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js'; +import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js'; +import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js'; +import { + signup, + post, + startServer, + api, + successfulApiCall, + failedApiCall, + ApiRequest, + hiddenNote, +} from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('クリップ', () => { + type User = Packed<'User'>; + type Note = Packed<'Note'>; + type Clip = Packed<'Clip'>; + + let app: INestApplicationContext; + + let alice: User; + let bob: User; + let aliceNote: Note; + let aliceHomeNote: Note; + let aliceFollowersNote: Note; + let aliceSpecifiedNote: Note; + let bobNote: Note; + let bobHomeNote: Note; + let bobFollowersNote: Note; + let bobSpecifiedNote: Note; + + const compareBy = (selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { + return selector(a).localeCompare(selector(b)); + }; + + type CreateParam = JTDDataType; + const defaultCreate = (): Partial => ({ + name: 'test', + }); + const create = async (parameters: Partial = {}, request: Partial = {}): Promise => { + const clip = await successfulApiCall({ + endpoint: '/clips/create', + parameters: { + ...defaultCreate(), + ...parameters, + }, + user: alice, + ...request, + }); + + // 入力が結果として入っていること + assert.deepStrictEqual(clip, { + ...clip, + ...defaultCreate(), + ...parameters, + }); + return clip; + }; + + const createMany = async (parameters: Partial, count = 10, user = alice): Promise => { + return await Promise.all([...Array(count)].map((_, i) => create({ + name: `test${i}`, + ...parameters, + }, { user }))); + }; + + type UpdateParam = JTDDataType; + const update = async (parameters: Partial, request: Partial = {}): Promise => { + const clip = await successfulApiCall({ + endpoint: '/clips/update', + parameters: { + name: 'updated', + ...parameters, + }, + user: alice, + ...request, + }); + + // 入力が結果として入っていること。clipIdはidになるので消しておく + delete (parameters as { clipId?: string }).clipId; + assert.deepStrictEqual(clip, { + ...clip, + ...parameters, + }); + return clip; + }; + + type DeleteParam = JTDDataType; + const deleteClip = async (parameters: DeleteParam, request: Partial = {}): Promise => { + return await successfulApiCall({ + endpoint: '/clips/delete', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type ShowParam = JTDDataType; + const show = async (parameters: ShowParam, request: Partial = {}): Promise => { + return await successfulApiCall({ + endpoint: '/clips/show', + parameters, + user: alice, + ...request, + }); + }; + + const list = async (request: Partial): Promise => { + return successfulApiCall({ + endpoint: '/clips/list', + parameters: {}, + user: alice, + ...request, + }); + }; + + const usersClips = async (request: Partial): Promise => { + return await successfulApiCall({ + endpoint: '/users/clips', + parameters: {}, + user: alice, + ...request, + }); + }; + + beforeAll(async () => { + app = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + + // FIXME: misskey-jsのNoteはoutdatedなので直接変換できない + aliceNote = await post(alice, { text: 'test' }) as any; + aliceHomeNote = await post(alice, { text: 'home only', visibility: 'home' }) as any; + aliceFollowersNote = await post(alice, { text: 'followers only', visibility: 'followers' }) as any; + aliceSpecifiedNote = await post(alice, { text: 'specified only', visibility: 'specified' }) as any; + bobNote = await post(bob, { text: 'test' }) as any; + bobHomeNote = await post(bob, { text: 'home only', visibility: 'home' }) as any; + bobFollowersNote = await post(bob, { text: 'followers only', visibility: 'followers' }) as any; + bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any; + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { + // テスト間で影響し合わないように毎回全部消す。 + for (const user of [alice, bob]) { + const list = await api('/clips/list', { limit: 11 }, user); + for (const clip of list.body) { + await api('/clips/delete', { clipId: clip.id }, user); + } + } + }); + + test('の作成ができる', async () => { + const res = await create(); + // ISO 8601で日付が返ってくること + assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.lastClippedAt, null); + assert.strictEqual(res.name, 'test'); + assert.strictEqual(res.description, null); + assert.strictEqual(res.isPublic, false); + assert.strictEqual(res.favoritedCount, 0); + assert.strictEqual(res.isFavorited, false); + }); + + test('の作成はポリシーで定められた数以上はできない。', async () => { + // ポリシー + 1まで作れるという所がミソ + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + for (let i = 0; i < clipLimit; i++) { + await create(); + } + + await failedApiCall({ + endpoint: '/clips/create', + parameters: defaultCreate(), + user: alice, + }, { + status: 400, + code: 'TOO_MANY_CLIPS', + id: '920f7c2d-6208-4b76-8082-e632020f5883', + }); + }); + + const createClipAllowedPattern = [ + { label: 'nameが最大長', parameters: { name: 'x'.repeat(100) } }, + { label: 'private', parameters: { isPublic: false } }, + { label: 'public', parameters: { isPublic: true } }, + { label: 'descriptionがnull', parameters: { description: null } }, + { label: 'descriptionが最大長', parameters: { description: 'a'.repeat(2048) } }, + ]; + test.each(createClipAllowedPattern)('の作成は$labelでもできる', async ({ parameters }) => await create(parameters)); + + const createClipDenyPattern = [ + { label: 'nameがnull', parameters: { name: null } }, + { label: 'nameが最大長+1', parameters: { name: 'x'.repeat(101) } }, + { label: 'isPublicがboolじゃない', parameters: { isPublic: 'true' } }, + { label: 'descriptionがゼロ長', parameters: { description: '' } }, + { label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } }, + ]; + test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({ + endpoint: '/clips/create', + parameters: { + ...defaultCreate(), + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + })); + + test('の更新ができる', async () => { + const res = await update({ + clipId: (await create()).id, + name: 'updated', + description: 'new description', + isPublic: true, + }); + + // ISO 8601で日付が返ってくること + assert.strictEqual(res.createdAt, new Date(res.createdAt).toISOString()); + assert.strictEqual(res.lastClippedAt, null); + assert.strictEqual(res.name, 'updated'); + assert.strictEqual(res.description, 'new description'); + assert.strictEqual(res.isPublic, true); + assert.strictEqual(res.favoritedCount, 0); + assert.strictEqual(res.isFavorited, false); + }); + + test.each(createClipAllowedPattern)('の更新は$labelでもできる', async ({ parameters }) => await update({ + clipId: (await create()).id, + name: 'updated', + ...parameters, + })); + + test.each([ + { label: 'clipIdがnull', parameters: { clipId: null } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', + } }, + { label: '他人のクリップ', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: 'b4d92d70-b216-46fa-9a3f-a8c811699257', + } }, + ...createClipDenyPattern as any, + ])('の更新は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/update', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + name: 'updated', + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('の削除ができる', async () => { + await deleteClip({ + clipId: (await create()).id, + }); + assert.deepStrictEqual(await list({}), []); + }); + + test.each([ + { label: 'clipIdがnull', parameters: { clipId: null } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754', + } }, + { label: '他人のクリップ', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '70ca08ba-6865-4630-b6fb-8494759aa754', + } }, + ])('の削除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/delete', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('のID指定取得ができる', async () => { + const clip = await create(); + const res = await show({ clipId: clip.id }); + assert.deepStrictEqual(res, clip); + }); + + test('のID指定取得は他人のPrivateなクリップは取得できない', async () => { + const clip = await create({ isPublic: false }, { user: bob } ); + failedApiCall({ + endpoint: '/clips/show', + parameters: { clipId: clip.id }, + user: alice, + }, { + status: 400, + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', + }); + }); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', + } }, + ])('のID指定取得は$labelならできない', async ({ parameters, assetion }) => failedApiCall({ + endpoint: '/clips/show', + parameters: { + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('の一覧(clips/list)が取得できる(空)', async () => { + const res = await list({}); + assert.deepStrictEqual(res, []); + }); + + test('の一覧(clips/list)が取得できる(上限いっぱい)', async () => { + const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + const clips = await createMany({}, clipLimit); + const res = await list({ + parameters: { limit: 1 }, // FIXME: 無視されて11全部返ってくる + }); + + // 返ってくる配列には順序保障がないのでidでソートして厳密比較 + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + clips.sort(compareBy(s => s.id)), + ); + }); + + test('の一覧が取得できる(空)', async () => { + const res = await usersClips({ + parameters: { + userId: alice.id, + }, + }); + assert.deepStrictEqual(res, []); + }); + + test.each([ + { label: '' }, + { label: '他人アカウントから', user: (): User => bob }, + ])('の一覧が$label取得できる', async () => { + const clips = await createMany({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + }, + }); + + // 返ってくる配列には順序保障がないのでidでソートして厳密比較 + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + clips.sort(compareBy(s => s.id))); + + // 認証状態で見たときだけisFavoritedが入っている + for (const clip of res) { + assert.strictEqual(clip.isFavorited, false); + } + }); + + test.each([ + { label: '未認証', user: (): undefined => undefined }, + { label: '存在しないユーザーのもの', parameters: { userId: 'xxxxxxx' } }, + ])('の一覧は$labelでも取得できる', async ({ parameters, user }) => { + const clips = await createMany({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + limit: clips.length, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }); + + // 未認証で見たときはisFavoritedは入らない + for (const clip of res) { + assert.strictEqual('isFavorited' in clip, false); + } + }); + + test('の一覧はPrivateなクリップを含まない(自分のものであっても。)', async () => { + await create({ isPublic: false }); + const aliceClip = await create({ isPublic: true }); + const res = await usersClips({ + parameters: { + userId: alice.id, + limit: 2, + }, + }); + assert.deepStrictEqual(res, [aliceClip]); + }); + + test('の一覧はID指定で範囲選択ができる', async () => { + const clips = await createMany({ isPublic: true }, 7); + clips.sort(compareBy(s => s.id)); + const res = await usersClips({ + parameters: { + userId: alice.id, + sinceId: clips[1].id, + untilId: clips[5].id, + limit: 4, + }, + }); + + // Promise.allで返ってくる配列には順序保障がないのでidでソートして厳密比較 + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + [clips[2], clips[3], clips[4]], // sinceIdとuntilId自体は結果に含まれない + clips[1].id + ' ... ' + clips[3].id + ' with ' + clips.map(s => s.id) + ' vs. ' + res.map(s => s.id)); + }); + + test.each([ + { label: 'userId未指定', parameters: { userId: undefined } }, + { label: 'limitゼロ', parameters: { limit: 0 } }, + { label: 'limit最大+1', parameters: { limit: 101 } }, + ])('の一覧は$labelだと取得できない', async ({ parameters }) => failedApiCall({ + endpoint: '/users/clips', + parameters: { + userId: alice.id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + })); + + test.each([ + { label: '作成', endpoint: '/clips/create' }, + { label: '更新', endpoint: '/clips/update' }, + { label: '削除', endpoint: '/clips/delete' }, + { label: '取得', endpoint: '/clips/list' }, + { label: 'お気に入り設定', endpoint: '/clips/favorite' }, + { label: 'お気に入り解除', endpoint: '/clips/unfavorite' }, + { label: 'お気に入り取得', endpoint: '/clips/my-favorites' }, + { label: 'ノート追加', endpoint: '/clips/add-note' }, + { label: 'ノート削除', endpoint: '/clips/remove-note' }, + ])('の$labelは未認証ではできない', async ({ endpoint }) => await failedApiCall({ + endpoint: endpoint, + parameters: {}, + user: undefined, + }, { + status: 401, + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + })); + + describe('のお気に入り', () => { + let aliceClip: Clip; + + type FavoriteParam = JTDDataType; + const favorite = async (parameters: FavoriteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/favorite', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type UnfavoriteParam = JTDDataType; + const unfavorite = async (parameters: UnfavoriteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/unfavorite', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + const myFavorites = async (request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/my-favorites', + parameters: {}, + user: alice, + ...request, + }); + }; + + beforeEach(async () => { + aliceClip = await create(); + }); + + test('を設定できる。', async () => { + await favorite({ clipId: aliceClip.id }); + const clip = await show({ clipId: aliceClip.id }); + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + }); + + test('はPublicな他人のクリップに設定できる。', async () => { + const publicClip = await create({ isPublic: true }); + await favorite({ clipId: publicClip.id }, { user: bob }); + const clip = await show({ clipId: publicClip.id }, { user: bob }); + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + + // isFavoritedは見る人によって切り替わる。 + const clip2 = await show({ clipId: publicClip.id }); + assert.strictEqual(clip2.favoritedCount, 1); + assert.strictEqual(clip2.isFavorited, false); + }); + + test('は1つのクリップに対して複数人が設定できる。', async () => { + const publicClip = await create({ isPublic: true }); + await favorite({ clipId: publicClip.id }, { user: bob }); + await favorite({ clipId: publicClip.id }); + const clip = await show({ clipId: publicClip.id }, { user: bob }); + assert.strictEqual(clip.favoritedCount, 2); + assert.strictEqual(clip.isFavorited, true); + + const clip2 = await show({ clipId: publicClip.id }); + assert.strictEqual(clip2.favoritedCount, 2); + assert.strictEqual(clip2.isFavorited, true); + }); + + test('は11を超えて設定できる。', async () => { + const clips = [ + aliceClip, + ...await createMany({}, 10, alice), + ...await createMany({ isPublic: true }, 10, bob), + ]; + for (const clip of clips) { + await favorite({ clipId: clip.id }); + } + + // pagenationはない。全部一気にとれる。 + const favorited = await myFavorites(); + assert.strictEqual(favorited.length, clips.length); + for (const clip of favorited) { + assert.strictEqual(clip.favoritedCount, 1); + assert.strictEqual(clip.isFavorited, true); + } + }); + + test('は同じクリップに対して二回設定できない。', async () => { + await favorite({ clipId: aliceClip.id }); + await failedApiCall({ + endpoint: '/clips/favorite', + parameters: { + clipId: aliceClip.id, + }, + user: alice, + }, { + status: 400, + code: 'ALREADY_FAVORITED', + id: '92658936-c625-4273-8326-2d790129256e', + }); + }); + + test.each([ + { label: 'clipIdがnull', parameters: { clipId: null } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + } }, + { label: '他人のクリップ', user: (): User => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5', + } }, + ])('の設定は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/favorite', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('を設定解除できる。', async () => { + await favorite({ clipId: aliceClip.id }); + await unfavorite({ clipId: aliceClip.id }); + const clip = await show({ clipId: aliceClip.id }); + assert.strictEqual(clip.favoritedCount, 0); + assert.strictEqual(clip.isFavorited, false); + assert.deepStrictEqual(await myFavorites(), []); + }); + + test.each([ + { label: 'clipIdがnull', parameters: { clipId: null } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '2603966e-b865-426c-94a7-af4a01241dc1', + } }, + { label: '他人のクリップ', user: (): User => bob, assertion: { + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + } }, + { label: 'お気に入りしていないクリップ', assertion: { + code: 'NOT_FAVORITED', + id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187', + } }, + ])('の設定解除は$labelならできない', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/unfavorite', + parameters: { + clipId: (await create({}, { user: (user ?? ((): User => alice))() })).id, + ...parameters, + }, + user: alice, + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + + test('を取得できる。', async () => { + await favorite({ clipId: aliceClip.id }); + const favorited = await myFavorites(); + assert.deepStrictEqual(favorited, [await show({ clipId: aliceClip.id })]); + }); + + test('を取得したとき他人のお気に入りは含まない。', async () => { + await favorite({ clipId: aliceClip.id }); + const favorited = await myFavorites({ user: bob }); + assert.deepStrictEqual(favorited, []); + }); + }); + + describe('に紐づくノート', () => { + let aliceClip: Clip; + + const sampleNotes = (): Note[] => [ + aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, + bobNote, bobHomeNote, bobFollowersNote, bobSpecifiedNote, + ]; + + type AddNoteParam = JTDDataType; + const addNote = async (parameters: AddNoteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/add-note', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type RemoveNoteParam = JTDDataType; + const removeNote = async (parameters: RemoveNoteParam, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/remove-note', + parameters, + user: alice, + ...request, + }, { + status: 204, + }); + }; + + type NotesParam = JTDDataType; + const notes = async (parameters: Partial, request: Partial = {}): Promise => { + return successfulApiCall({ + endpoint: '/clips/notes', + parameters, + user: alice, + ...request, + }); + }; + + beforeEach(async () => { + aliceClip = await create(); + }); + + test('を追加できる。', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + const res = await show({ clipId: aliceClip.id }); + assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString()); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]); + + // 他人の非公開ノートも突っ込める + await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id }); + await addNote({ clipId: aliceClip.id, noteId: bobFollowersNote.id }); + await addNote({ clipId: aliceClip.id, noteId: bobSpecifiedNote.id }); + }); + + test('として同じノートを二回紐づけることはできない', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: alice, + }, { + status: 400, + code: 'ALREADY_CLIPPED', + id: '734806c4-542c-463a-9311-15c512803965', + }); + }); + + // TODO: 17000msくらいかかる... + test('をポリシーで定められた上限いっぱい(200)を超えて追加はできない。', async () => { + const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; + const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { + text: `test ${i}`, + }) as unknown)) as Note[]; + await Promise.all(noteList.map(s => addNote({ clipId: aliceClip.id, noteId: s.id }))); + + await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: alice, + }, { + status: 400, + code: 'TOO_MANY_CLIP_NOTES', + id: 'f0dba960-ff73-4615-8df4-d6ac5d9dc118', + }); + }); + + test('は他人のクリップへ追加できない。', async () => await failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + }, + user: bob, + }, { + status: 400, + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + })); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + } }, + { label: '存在しないノート', parameters: { noteId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_NOTE', + id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b', + } }, + { label: '他人のクリップ', user: (): object => bob, assetion: { + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf', + } }, + ])('の追加は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ + endpoint: '/clips/add-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('を削除できる。', async () => { + await addNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + await removeNote({ clipId: aliceClip.id, noteId: aliceNote.id }); + assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), []); + }); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'noteId未指定', parameters: { noteId: undefined } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる + } }, + { label: '存在しないノート', parameters: { noteId: 'xxxxxx' }, assetion: { + code: 'NO_SUCH_NOTE', + id: 'aff017de-190e-434b-893e-33a9ff5049d8', // add-noteと異なる + } }, + { label: '他人のクリップ', user: (): object => bob, assetion: { + code: 'NO_SUCH_CLIP', + id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52', // add-noteと異なる + } }, + ])('の削除は$labelだとできない', async ({ parameters, user, assetion }) => failedApiCall({ + endpoint: '/clips/remove-note', + parameters: { + clipId: aliceClip.id, + noteId: aliceNote.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assetion, + })); + + test('を取得できる。', async () => { + const noteList = sampleNotes(); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ clipId: aliceClip.id }); + + // 自分のノートは非公開でも入れられるし、見える + // 他人の非公開ノートは入れられるけど、除外される + const expects = [ + aliceNote, aliceHomeNote, aliceFollowersNote, aliceSpecifiedNote, + bobNote, bobHomeNote, + ]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test('を始端IDとlimitで取得できる。', async () => { + const noteList = sampleNotes(); + noteList.sort(compareBy(s => s.id)); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ + clipId: aliceClip.id, + sinceId: noteList[2].id, + limit: 3, + }); + + // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 + const expects = [noteList[3], noteList[4], noteList[5]]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test('をID範囲指定で取得できる。', async () => { + const noteList = sampleNotes(); + noteList.sort(compareBy(s => s.id)); + for (const note of noteList) { + await addNote({ clipId: aliceClip.id, noteId: note.id }); + } + + const res = await notes({ + clipId: aliceClip.id, + sinceId: noteList[1].id, + untilId: noteList[4].id, + }); + + // Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較 + const expects = [noteList[2], noteList[3]]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test.todo('Remoteのノートもクリップできる。どうテストしよう?'); + + test('は他人のPublicなクリップからも取得できる。', async () => { + const bobClip = await create({ isPublic: true }, { user: bob } ); + await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob }); + const res = await notes({ clipId: bobClip.id }); + assert.deepStrictEqual(res, [aliceNote]); + }); + + test('はPublicなクリップなら認証なしでも取得できる。(非公開ノートはhideされて返ってくる)', async () => { + const publicClip = await create({ isPublic: true }); + await addNote({ clipId: publicClip.id, noteId: aliceNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceHomeNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceFollowersNote.id }); + await addNote({ clipId: publicClip.id, noteId: aliceSpecifiedNote.id }); + + const res = await notes({ clipId: publicClip.id }, { user: undefined }); + const expects = [ + aliceNote, aliceHomeNote, + // 認証なしだと非公開ノートは結果には含むけどhideされる。 + hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote), + ]; + assert.deepStrictEqual( + res.sort(compareBy(s => s.id)), + expects.sort(compareBy(s => s.id))); + }); + + test.todo('ブロック、ミュートされたユーザーからの設定&取得etc.'); + + test.each([ + { label: 'clipId未指定', parameters: { clipId: undefined } }, + { label: 'limitゼロ', parameters: { limit: 0 } }, + { label: 'limit最大+1', parameters: { limit: 101 } }, + { label: '存在しないクリップ', parameters: { clipId: 'xxxxxx' }, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + { label: '他人のPrivateなクリップから', user: (): object => bob, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + { label: '未認証でPrivateなクリップから', user: (): undefined => undefined, assertion: { + code: 'NO_SUCH_CLIP', + id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', + } }, + ])('は$labelだと取得できない', async ({ parameters, user, assertion }) => failedApiCall({ + endpoint: '/clips/notes', + parameters: { + clipId: aliceClip.id, + ...parameters, + }, + user: (user ?? ((): User => alice))(), + }, { + status: 400, + code: 'INVALID_PARAM', + id: '3d81ceae-475f-4600-b2a8-2bc116157532', + ...assertion, + })); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 4d52c2f062..879d5ec79a 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,5 +1,7 @@ +import * as assert from 'assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; +import { inspect } from 'node:util'; import WebSocket from 'ws'; import fetch, { Blob, File, RequestInit } from 'node-fetch'; import { DataSource } from 'typeorm'; @@ -22,6 +24,36 @@ export const api = async (endpoint: string, params: any, me?: any) => { return await request(`api/${normalized}`, params, me); }; +export type ApiRequest = { + endpoint: string, + parameters: object, + user: object | undefined, +}; + +export const successfulApiCall = async (request: ApiRequest, assertion: { + status: number, +} = { status: 200 }): Promise => { + const { endpoint, parameters, user } = request; + const { status } = assertion; + const res = await api(endpoint, parameters, user); + assert.strictEqual(res.status, status, inspect(res.body)); + return res.body; +}; + +export const failedApiCall = async (request: ApiRequest, assertion: { + status: number, + code: string, + id: string +}): Promise => { + const { endpoint, parameters, user } = request; + const { status, code, id } = assertion; + const res = await api(endpoint, parameters, user); + assert.strictEqual(res.status, status, inspect(res.body)); + assert.strictEqual(res.body.error.code, code, inspect(res.body)); + assert.strictEqual(res.body.error.id, id, inspect(res.body)); + return res.body; +}; + const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token, @@ -69,6 +101,21 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create'] return res.body ? res.body.createdNote : null; }; +// 非公開ノートをAPI越しに見たときのノート NoteEntityService.ts +export const hiddenNote = (note: any): any => { + const temp = { + ...note, + fileIds: [], + files: [], + text: null, + cw: null, + isHidden: true, + }; + delete temp.visibleUserIds; + delete temp.poll; + return temp; +}; + export const react = async (user: any, note: any, reaction: string): Promise => { await api('notes/reactions/create', { noteId: note.id, From e542a030e4fc43e9842d0a006bdcf206441f62be Mon Sep 17 00:00:00 2001 From: Kagami Sascha Rosylight Date: Sun, 19 Mar 2023 12:27:17 +0100 Subject: [PATCH 02/48] =?UTF-8?q?fix(backend/URLPreviewService):=20?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=A7HTTP=20422=E3=82=92?= =?UTF-8?q?=E5=87=BA=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=20(#10339)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend/URLPreviewService): エラーでHTTP 402を出すように * fix import --- .../src/server/web/UrlPreviewService.ts | 18 ++++++++++++------ packages/backend/test/e2e/endpoints.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 21cf414087..b3e193cd34 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from 'summaly'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; @@ -9,6 +8,7 @@ import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; +import { ApiError } from '@/server/api/error.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -40,9 +40,9 @@ export class UrlPreviewService { @bindThis public async handle( - request: FastifyRequest<{ Querystring: { url: string; lang: string; } }>, + request: FastifyRequest<{ Querystring: { url: string; lang?: string; } }>, reply: FastifyReply, - ) { + ): Promise { const url = request.query.url; if (typeof url !== 'string') { reply.code(400); @@ -78,7 +78,7 @@ export class UrlPreviewService { this.logger.succ(`Got preview of ${url}: ${summary.title}`); - if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { + if (!(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) { throw new Error('unsupported schema included'); } @@ -95,9 +95,15 @@ export class UrlPreviewService { return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); - reply.code(200); + reply.code(422); reply.header('Cache-Control', 'max-age=86400, immutable'); - return {}; + return { + error: new ApiError({ + message: 'Failed to get preview', + code: 'URL_PREVIEW_FAILED', + id: '09d01cb5-53b9-4856-82e5-38a50c290a3b', + }), + }; } } } diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index cbe7b894f4..0a45a320f9 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -841,4 +841,12 @@ describe('Endpoints', () => { assert.strictEqual(res.body[0].id, carolPost.id); }); }); + + describe('URL preview', () => { + test('Error from summaly becomes HTTP 422', async () => { + const res = await simpleGet('/url?url=https://e:xample.com'); + assert.strictEqual(res.status, 422); + assert.strictEqual(res.body.error.code, 'URL_PREVIEW_FAILED'); + }); + }); }); From 866aded6bcdd44e19f55bc4c9bf4aa0a8a67f530 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 19 Mar 2023 20:28:19 +0900 Subject: [PATCH 03/48] =?UTF-8?q?fix:=20PC=E7=89=88=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A7=E3=82=B9=E3=83=9E=E3=83=9B?= =?UTF-8?q?=E7=94=A8UI=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E3=81=AE=E3=82=92=E9=98=B2=E3=81=90=E3=83=AA=E3=83=80?= =?UTF-8?q?=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20(#10326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (Fix misskey-dev#10324)設定画面:PC版UIへのリダイレクトをルート遷移時にも実行 * Update changelog * (fix) missing semicolon * (fix) strict equals --- CHANGELOG.md | 1 + packages/frontend/src/pages/admin/index.vue | 8 +++++++- packages/frontend/src/pages/settings/index.vue | 8 +++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b2d50dc0..d7a6814049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ - リテンション分析が上手く機能しないことがあるのを修正 - 空のアンテナが作成できないように修正 - 特定の条件で通報が見れない問題を修正 +- PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正 ## 13.9.2 (2023/03/06) diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 550de24bb2..8aae39cba1 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -12,7 +12,7 @@ {{ i18n.ts.noBotProtectionWarning }} {{ i18n.ts.configure }} {{ i18n.ts.noEmailServerWarning }} {{ i18n.ts.configure }} - + @@ -220,6 +220,12 @@ onUnmounted(() => { ro.disconnect(); }); +watch(router.currentRef, (to) => { + if (to.route.path === "/admin" && to.child?.route.name == null && !narrow) { + router.replace('/admin/overview'); + } +}); + provideMetadataReceiver((info) => { if (info == null) { childInfo = null; diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index f1a450e18e..ae36466eec 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -7,7 +7,7 @@
@@ -230,6 +230,12 @@ onUnmounted(() => { ro.disconnect(); }); +watch(router.currentRef, (to) => { + if (to.route.name === "settings" && to.child?.route.name == null && !narrow) { + router.replace('/settings/profile'); + } +}); + const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified)); provideMetadataReceiver((info) => { From 4a989f7ebbd96ba4c90133935fd32af296474785 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 19 Mar 2023 20:28:50 +0900 Subject: [PATCH 04/48] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a6814049..6fb090f267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - Safariでプラグインが複数ある場合に正常に読み込まれない問題を修正 - Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正 - 非ログイン時の「Misskeyについて」の表示を修正 +- PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正 ### Server - OpenAPIエンドポイントを復旧 @@ -59,7 +60,6 @@ - リテンション分析が上手く機能しないことがあるのを修正 - 空のアンテナが作成できないように修正 - 特定の条件で通報が見れない問題を修正 -- PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正 ## 13.9.2 (2023/03/06) From 1d6f43aa30a932b1b7539f417f19d0b239cde511 Mon Sep 17 00:00:00 2001 From: CyberRex Date: Mon, 20 Mar 2023 12:58:06 +0900 Subject: [PATCH 05/48] feat: drive cleaner (#10366) * feat: drive-cleaner * Update CHANGELOG.md --- CHANGELOG.md | 1 + locales/ja-JP.yml | 5 + .../src/server/api/endpoints/drive/files.ts | 10 + .../src/pages/settings/drive-cleaner.vue | 193 ++++++++++++++++++ .../frontend/src/pages/settings/drive.vue | 3 + packages/frontend/src/router.ts | 4 + 6 files changed, 216 insertions(+) create mode 100644 packages/frontend/src/pages/settings/drive-cleaner.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb090f267..30b0ac018c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ - Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正 - 非ログイン時の「Misskeyについて」の表示を修正 - PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正 +- ドライブクリーナーを追加 ### Server - OpenAPIエンドポイントを復旧 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c4e86fc64a..54742cef96 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -977,6 +977,7 @@ notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" +drivecleaner: "ドライブクリーナー" _achievements: earnedAt: "獲得日時" @@ -1922,3 +1923,7 @@ _dialog: _disabledTimeline: title: "無効化されたタイムライン" description: "現在のロールでは、このタイムラインを使用することはできません。" + +_drivecleaner: + orderBySizeDesc: "サイズが大きい順" + orderByCreatedAtAsc: "追加日が古い順" \ No newline at end of file diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index f6fad50fd9..4609307774 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -31,6 +31,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, }, required: [], } as const; @@ -63,6 +64,15 @@ export default class extends Endpoint { } } + switch (ps.sort) { + case '+createdAt': query.orderBy('file.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('file.createdAt', 'ASC'); break; + case '+name': query.orderBy('file.name', 'DESC'); break; + case '-name': query.orderBy('file.name', 'ASC'); break; + case '+size': query.orderBy('file.size', 'DESC'); break; + case '-size': query.orderBy('file.size', 'ASC'); break; + } + const files = await query.take(ps.limit).getMany(); return await this.driveFileEntityService.packMany(files, { detail: false, self: true }); diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue new file mode 100644 index 0000000000..ce8ab214e4 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index a23bdfe69e..d3fb422e01 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -32,6 +32,9 @@ + + {{ i18n.ts.drivecleaner }} + diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 590c5765fd..c8077edd28 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -65,6 +65,10 @@ export const routes = [{ path: '/drive', name: 'drive', component: page(() => import('./pages/settings/drive.vue')), + }, { + path: '/drive/cleaner', + name: 'drive', + component: page(() => import('./pages/settings/drive-cleaner.vue')), }, { path: '/notifications', name: 'notifications', From bf5706ef6e47c90a112fdbb9ce3ad405828d1151 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 12:58:55 +0900 Subject: [PATCH 06/48] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b0ac018c..9403763aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ ### Client - 設定から自分のロールを確認できるように - 広告一覧ページを追加 +- ドライブクリーナーを追加 - DM作成時にメンションも含むように - フォロー申請のボタンのデザインを改善 - 付箋ウィジェットの高さを設定可能に @@ -43,7 +44,6 @@ - Bookwyrmのユーザーのプロフィールページで「リモートで表示」をタップしても反応がない問題を修正 - 非ログイン時の「Misskeyについて」の表示を修正 - PC版にて「設定」「コントロールパネル」のリンクを2度以上続けてクリックした際に空白のページが表示される問題を修正 -- ドライブクリーナーを追加 ### Server - OpenAPIエンドポイントを復旧 From 32c60c774c87f67f3308d01be12bc9cff89fab12 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 13:00:21 +0900 Subject: [PATCH 07/48] fix indentation --- packages/backend/src/misc/correct-filename.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/misc/correct-filename.ts b/packages/backend/src/misc/correct-filename.ts index 3357d8c1bd..23a0699f39 100644 --- a/packages/backend/src/misc/correct-filename.ts +++ b/packages/backend/src/misc/correct-filename.ts @@ -1,15 +1,15 @@ // 与えられた拡張子とファイル名が一致しているかどうかを確認し、 // 一致していない場合は拡張子を付与して返す export function correctFilename(filename: string, ext: string | null) { - const dotExt = ext ? ext.startsWith('.') ? 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}`; + const dotExt = ext ? ext.startsWith('.') ? 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}`; } From 3d6aaa7aaab988bce402ee3c6bac4c4bc0fc93fa Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 13:20:21 +0900 Subject: [PATCH 08/48] tweak drive-cleaner --- .../frontend/src/components/MkDrive.file.vue | 96 +--------- .../src/pages/settings/drive-cleaner.vue | 171 +++++++----------- .../src/scripts/get-drive-file-menu.ts | 93 ++++++++++ 3 files changed, 164 insertions(+), 196 deletions(-) create mode 100644 packages/frontend/src/scripts/get-drive-file-menu.ts diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 8c17c0530a..ab408b5008 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -32,14 +32,14 @@ diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts new file mode 100644 index 0000000000..56ab516038 --- /dev/null +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -0,0 +1,93 @@ +import * as Misskey from 'misskey-js'; +import { defineAsyncComponent } from 'vue'; +import { i18n } from '@/i18n'; +import copyToClipboard from '@/scripts/copy-to-clipboard'; +import * as os from '@/os'; + +function rename(file: Misskey.entities.DriveFile) { + os.inputText({ + title: i18n.ts.renameFile, + placeholder: i18n.ts.inputNewFileName, + default: file.name, + }).then(({ canceled, result: name }) => { + if (canceled) return; + os.api('drive/files/update', { + fileId: file.id, + name: name, + }); + }); +} + +function describe(file: Misskey.entities.DriveFile) { + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + default: file.comment != null ? file.comment : '', + file: file, + }, { + done: caption => { + os.api('drive/files/update', { + fileId: file.id, + comment: caption.length === 0 ? null : caption, + }); + }, + }, 'closed'); +} + +function toggleSensitive(file: Misskey.entities.DriveFile) { + os.api('drive/files/update', { + fileId: file.id, + isSensitive: !file.isSensitive, + }); +} + +function copyUrl(file: Misskey.entities.DriveFile) { + copyToClipboard(file.url); + os.success(); +} +/* +function addApp() { + alert('not implemented yet'); +} +*/ +async function deleteFile(file: Misskey.entities.DriveFile) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + }); + + if (canceled) return; + os.api('drive/files/delete', { + fileId: file.id, + }); +} + +export function getDriveFileMenu(file: Misskey.entities.DriveFile) { + return [{ + text: i18n.ts.rename, + icon: 'ti ti-forms', + action: rename, + }, { + text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, + icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-off', + action: toggleSensitive, + }, { + text: i18n.ts.describeFile, + icon: 'ti ti-text-caption', + action: describe, + }, null, { + text: i18n.ts.copyUrl, + icon: 'ti ti-link', + action: copyUrl, + }, { + type: 'a', + href: file.url, + target: '_blank', + text: i18n.ts.download, + icon: 'ti ti-download', + download: file.name, + }, null, { + text: i18n.ts.delete, + icon: 'ti ti-trash', + danger: true, + action: deleteFile, + }]; +} From e1520479126a5ca79012a49e4b1a6872f1dc481a Mon Sep 17 00:00:00 2001 From: nenohi Date: Mon, 20 Mar 2023 14:24:18 +0900 Subject: [PATCH 09/48] =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=81=AE?= =?UTF-8?q?=E5=90=8D=E5=89=8D=E3=81=AB@=E3=82=84:=E3=81=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=A7=E3=81=8D=E3=82=8B=20(#9964)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#9918)名前の一致でもエラーとするように * 判定を逆に * )の位置間違えてる * カテゴリ分けとかしたときにエラーになる * エラー消し * こういうこと・・・? --- .../src/server/api/endpoints/admin/emoji/update.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index dad0e3ef86..1c649db93e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -19,6 +19,11 @@ export const meta = { code: 'NO_SUCH_EMOJI', id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8', }, + alreadyexistsemoji: { + message: 'Emoji already exists', + code: 'EMOJI_ALREADY_EXISTS', + id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8', + }, }, } as const; @@ -26,7 +31,7 @@ export const paramDef = { type: 'object', properties: { id: { type: 'string', format: 'misskey:id' }, - name: { type: 'string' }, + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, category: { type: 'string', nullable: true, @@ -57,9 +62,9 @@ export default class extends Endpoint { ) { super(meta, paramDef, async (ps, me) => { const emoji = await this.emojisRepository.findOneBy({ id: ps.id }); - + const emojiname = await this.emojisRepository.findOneBy({ name: ps.name }); if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - + if (emojiname != null && emojiname.id !== ps.id) throw new ApiError(meta.errors.alreadyexistsemoji); await this.emojisRepository.update(emoji.id, { updatedAt: new Date(), name: ps.name, From 3014e3e5f88ec18ef741ec343794af7057194009 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 14:25:21 +0900 Subject: [PATCH 10/48] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9403763aa8..a4a3e2a927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ - リテンション分析が上手く機能しないことがあるのを修正 - 空のアンテナが作成できないように修正 - 特定の条件で通報が見れない問題を修正 +- 絵文字の名前に任意の文字が使用できる問題を修正 ## 13.9.2 (2023/03/06) From eb5781465b9a810a1f26e3be4d24776a227e39ec Mon Sep 17 00:00:00 2001 From: choco <8362391+choco14t@users.noreply.github.com> Date: Mon, 20 Mar 2023 14:26:11 +0900 Subject: [PATCH 11/48] =?UTF-8?q?fix(users/show):=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=8C=E8=A6=8B=E3=81=A4=E3=81=8B=E3=82=89?= =?UTF-8?q?=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E5=A0=B4=E5=90=88=E3=81=AB?= =?UTF-8?q?404=E3=82=B9=E3=83=86=E3=83=BC=E3=82=BF=E3=82=B9=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E8=BF=94=E3=81=99=20(#10344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(users/show): ユーザーが見つからなかった場合に404ステータスコードを返す * test(users/show): ステータスコードの期待値を修正 --- packages/backend/src/server/api/endpoints/users/show.ts | 1 + packages/backend/test/e2e/endpoints.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 29f24b045a..ba432c273b 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -48,6 +48,7 @@ export const meta = { message: 'No such user.', code: 'NO_SUCH_USER', id: '4362f8dc-731f-4ad8-a694-be5a88922a24', + httpStatusCode: 404, }, }, } as const; diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index 0a45a320f9..afb72c84d4 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -162,14 +162,14 @@ describe('Endpoints', () => { const res = await api('/users/show', { userId: '000000000000000000000000', }); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); }); test('間違ったIDで怒られる', async () => { const res = await api('/users/show', { userId: 'kyoppie', }); - assert.strictEqual(res.status, 400); + assert.strictEqual(res.status, 404); }); }); From 54630edb0f8cf91480e19f4e8e56c05158bc3a8f Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 20:12:38 +0900 Subject: [PATCH 12/48] =?UTF-8?q?enhance:=20=E4=BD=BF=E3=82=8F=E3=82=8C?= =?UTF-8?q?=E3=81=A6=E3=81=AA=E3=81=84=E3=82=A2=E3=83=B3=E3=83=86=E3=83=8A?= =?UTF-8?q?=E3=81=AF=E8=87=AA=E5=8B=95=E5=81=9C=E6=AD=A2=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #9373 --- CHANGELOG.md | 1 + .../migration/1679309757174-antenna-active.js | 17 +++++++++++++++++ packages/backend/src/core/AntennaService.ts | 6 +++++- .../src/core/entities/AntennaEntityService.ts | 1 + packages/backend/src/models/entities/Antenna.ts | 10 ++++++++++ .../backend/src/models/json-schema/antenna.ts | 4 ++++ .../queue/processors/CleanProcessorService.ts | 15 +++++++++++++-- .../src/server/api/endpoints/antennas/create.ts | 5 ++++- .../src/server/api/endpoints/antennas/notes.ts | 4 ++++ 9 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 packages/backend/migration/1679309757174-antenna-active.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a3e2a927..4e2ea11028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - ロールの並び順を設定可能に - カスタム絵文字にライセンス情報を付与できるように - 指定した文字列を含む投稿の公開範囲をホームにできるように +- 使われてないアンテナは自動停止されるように ### Client - 設定から自分のロールを確認できるように diff --git a/packages/backend/migration/1679309757174-antenna-active.js b/packages/backend/migration/1679309757174-antenna-active.js new file mode 100644 index 0000000000..69e845c142 --- /dev/null +++ b/packages/backend/migration/1679309757174-antenna-active.js @@ -0,0 +1,17 @@ +export class antennaActive1679309757174 { + name = 'antennaActive1679309757174' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT 'now'`); + await queryRunner.query(`ALTER TABLE "antenna" ADD "isActive" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`CREATE INDEX "IDX_084c2abb8948ef59a37dce6ac1" ON "antenna" ("lastUsedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_36ef5192a1ce55ed0e40aa4db5" ON "antenna" ("isActive") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_36ef5192a1ce55ed0e40aa4db5"`); + await queryRunner.query(`DROP INDEX "public"."IDX_084c2abb8948ef59a37dce6ac1"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "isActive"`); + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "lastUsedAt"`); + } +} diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 35fbb53e81..aaa26a8321 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -71,12 +71,14 @@ export class AntennaService implements OnApplicationShutdown { this.antennas.push({ ...body, createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), }); break; case 'antennaUpdated': this.antennas[this.antennas.findIndex(a => a.id === body.id)] = { ...body, createdAt: new Date(body.createdAt), + lastUsedAt: new Date(body.lastUsedAt), }; break; case 'antennaDeleted': @@ -217,7 +219,9 @@ export class AntennaService implements OnApplicationShutdown { @bindThis public async getAntennas() { if (!this.antennasFetched) { - this.antennas = await this.antennasRepository.find(); + this.antennas = await this.antennasRepository.findBy({ + isActive: true, + }); this.antennasFetched = true; } diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 89137c0ec0..e02daefd64 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -37,6 +37,7 @@ export class AntennaEntityService { notify: antenna.notify, withReplies: antenna.withReplies, withFile: antenna.withFile, + isActive: antenna.isActive, hasUnreadNote, }; } diff --git a/packages/backend/src/models/entities/Antenna.ts b/packages/backend/src/models/entities/Antenna.ts index 5b2164ef17..e63e7f2c72 100644 --- a/packages/backend/src/models/entities/Antenna.ts +++ b/packages/backend/src/models/entities/Antenna.ts @@ -13,6 +13,10 @@ export class Antenna { }) public createdAt: Date; + @Index() + @Column('timestamp with time zone') + public lastUsedAt: Date; + @Index() @Column({ ...id(), @@ -83,4 +87,10 @@ export class Antenna { @Column('boolean') public notify: boolean; + + @Index() + @Column('boolean', { + default: true, + }) + public isActive: boolean; } diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index f0994e48f7..4483510610 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -75,6 +75,10 @@ export const packedAntennaSchema = { type: 'boolean', optional: false, nullable: false, }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, hasUnreadNote: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts index 7fd2cde9c0..9534454fd7 100644 --- a/packages/backend/src/queue/processors/CleanProcessorService.ts +++ b/packages/backend/src/queue/processors/CleanProcessorService.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In, LessThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; +import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -26,6 +26,9 @@ export class CleanProcessorService { @Inject(DI.mutedNotesRepository) private mutedNotesRepository: MutedNotesRepository, + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + @Inject(DI.antennaNotesRepository) private antennaNotesRepository: AntennaNotesRepository, @@ -55,8 +58,16 @@ export class CleanProcessorService { reason: 'word', }); - this.antennaNotesRepository.delete({ + this.mutedNotesRepository.delete({ id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))), + reason: 'word', + }); + + // 7日以上使われてないアンテナを停止 + this.antennasRepository.update({ + lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))), + }, { + isActive: false, }); const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign') diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index b57906a688..d147ddb7f1 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -103,9 +103,12 @@ export default class extends Endpoint { } } + const now = new Date(); + const antenna = await this.antennasRepository.insert({ id: this.idService.genId(), - createdAt: new Date(), + createdAt: now, + lastUsedAt: now, userId: me.id, name: ps.name, src: ps.src, diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts index fbb5acf617..039ba1115a 100644 --- a/packages/backend/src/server/api/endpoints/antennas/notes.ts +++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts @@ -101,6 +101,10 @@ export default class extends Endpoint { this.noteReadService.read(me.id, notes); } + this.antennasRepository.update(antenna.id, { + lastUsedAt: new Date(), + }); + return await this.noteEntityService.packMany(notes, me); }); } From 21b10603fea5a709535ec1450a05f68314701c45 Mon Sep 17 00:00:00 2001 From: Ekke Date: Mon, 20 Mar 2023 20:21:54 +0900 Subject: [PATCH 13/48] =?UTF-8?q?feat(frontend):=20=E3=83=8A=E3=83=93?= =?UTF-8?q?=E3=82=B2=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=9E=E3=82=A4=E3=82=BA?= =?UTF-8?q?=E3=82=92=E3=83=89=E3=83=A9=E3=83=83=E3=82=B0&=E3=83=89?= =?UTF-8?q?=E3=83=AD=E3=83=83=E3=83=97=E3=81=A7=E8=A1=8C=E3=81=88=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20(#10356)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(frontend): ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるようにする * eslintのエラーを修正 * ハンドルをつかんでドラッグするように変更 * eslintのエラーを修正 * デザインの軽微な変更 * 修正 * Update CHANGELOG.md * Update CHANGELOG.md * ドラッグハンドルを3本線から2本線に --------- Co-authored-by: root --- CHANGELOG.md | 3 + .../frontend/src/pages/settings/navbar.vue | 131 +++++++++++++++--- 2 files changed, 117 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2ea11028..fc016c4646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ - APオブジェクトを入力してフェッチする機能とユーザーやノートの検索機能を分離 - ナビゲーションバーの項目に「プロフィール」を追加できるように - AiScriptを0.13.1に更新 +- ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるように + +### Bugfixes - oEmbedをサポートしているウェブサイトのプレビューができるように - YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように - ([FirefoxでSpotifyのプレビューを開けるとフルサイズじゃなくプレビューサイズだけ再生できる問題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)があります) diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index ead551e7c4..dc13477268 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -1,9 +1,34 @@ + From 5e1014c0725684f85bb3c8d73fb59b9d047c3ce3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 20:22:24 +0900 Subject: [PATCH 14/48] Update CHANGELOG.md --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc016c4646..557379a663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,10 +33,8 @@ - 付箋ウィジェットの高さを設定可能に - APオブジェクトを入力してフェッチする機能とユーザーやノートの検索機能を分離 - ナビゲーションバーの項目に「プロフィール」を追加できるように -- AiScriptを0.13.1に更新 - ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるように - -### Bugfixes +- AiScriptを0.13.1に更新 - oEmbedをサポートしているウェブサイトのプレビューができるように - YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように - ([FirefoxでSpotifyのプレビューを開けるとフルサイズじゃなくプレビューサイズだけ再生できる問題](https://bugzilla.mozilla.org/show_bug.cgi?id=1792395)があります) From dac4fbcb1eb5be0ac8d67a642b5aeefd5ea46041 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 20:35:49 +0900 Subject: [PATCH 15/48] tweak settings/navbar.vue --- .../frontend/src/pages/settings/navbar.vue | 136 ++++++++---------- 1 file changed, 57 insertions(+), 79 deletions(-) diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index dc13477268..b3b33b8026 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -5,27 +5,27 @@
- {{ i18n.ts.addItem }} + {{ i18n.ts.addItem }} {{ i18n.ts.default }} {{ i18n.ts.save }}
@@ -56,7 +56,10 @@ import { deepClone } from '@/scripts/clone'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); -const items = ref(deepClone(defaultStore.state.menu)); +const items = ref(defaultStore.state.menu.map(x => ({ + id: Math.random().toString(), + type: x, +}))); const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); @@ -81,7 +84,10 @@ async function addItem() { }], }); if (canceled) return; - items.value = [...items.value, item]; + items.value = [...items.value, { + id: Math.random().toString(), + type: item, + }]; } function removeItem(index: number) { @@ -89,12 +95,15 @@ function removeItem(index: number) { } async function save() { - defaultStore.set('menu', items.value); + defaultStore.set('menu', items.value.map(x => x.type)); await reloadAsk(); } function reset() { - items.value = defaultStore.def.menu.default; + items.value = defaultStore.def.menu.default.map(x => ({ + id: Math.random().toString(), + type: x, + })); } watch(menuDisplay, async () => { @@ -110,75 +119,44 @@ definePageMetadata({ icon: 'ti ti-list', }); - From 75888a55c35eb8bb81186fc44abcf7d476db6d74 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 20 Mar 2023 20:49:10 +0900 Subject: [PATCH 16/48] New Crowdin updates (#10369) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) --- locales/de-DE.yml | 4 ++++ locales/en-US.yml | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 08808ea6a4..0716bcc4ad 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -977,6 +977,7 @@ notesSearchNotAvailable: "Die Notizsuche ist nicht verfügbar." license: "Lizenz" unfavoriteConfirm: "Wirklich aus Favoriten entfernen?" myClips: "Meine Clips" +drivecleaner: "Drive-Reiniger" _achievements: earnedAt: "Freigeschaltet am" _types: @@ -1868,3 +1869,6 @@ _dialog: _disabledTimeline: title: "Chronik deaktiviert" description: "Mit deinen jetzigen Rollen ist diese Chronik nicht verfügbar." +_drivecleaner: + orderBySizeDesc: "Absteigende Dateigrößen" + orderByCreatedAtAsc: "Aufsteigendes Erstelldatum" diff --git a/locales/en-US.yml b/locales/en-US.yml index 9e018ce2ac..5efb7b7d1e 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -530,7 +530,7 @@ nothing: "There's nothing to see here" installedDate: "Authorized at" lastUsedDate: "Last used at" state: "State" -sort: "Sort" +sort: "Sorting order" ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" @@ -977,6 +977,7 @@ notesSearchNotAvailable: "Note search is unavailable." license: "License" unfavoriteConfirm: "Really remove from favorites?" myClips: "My clips" +drivecleaner: "Drive Cleaner" _achievements: earnedAt: "Unlocked at" _types: @@ -1868,3 +1869,6 @@ _dialog: _disabledTimeline: title: "Timeline disabled" description: "You cannot use this timeline under your current roles." +_drivecleaner: + orderBySizeDesc: "Descending Filesizes" + orderByCreatedAtAsc: "Ascending Dates" From 7331de0bcef74bde0789259830f98be2b063de57 Mon Sep 17 00:00:00 2001 From: CyberRex Date: Wed, 22 Mar 2023 08:58:23 +0900 Subject: [PATCH 17/48] feat: queue force promote (#10370) * feat: queue force promote * Update CHANGELOG.md * small fix --- CHANGELOG.md | 1 + locales/ja-JP.yml | 3 ++ .../backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/admin/queue/promote.ts | 52 +++++++++++++++++++ packages/frontend/src/pages/admin/queue.vue | 15 ++++++ 6 files changed, 77 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/admin/queue/promote.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 557379a663..c5fe52bc80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - APオブジェクトを入力してフェッチする機能とユーザーやノートの検索機能を分離 - ナビゲーションバーの項目に「プロフィール」を追加できるように - ナビゲーションバーのカスタマイズをドラッグ&ドロップで行えるように +- ジョブキューの再試行をワンクリックでできるように - AiScriptを0.13.1に更新 - oEmbedをサポートしているウェブサイトのプレビューができるように - YouTubeをoEmbedでロードし、プレビューで共有ボタンを押すとOSの共有画面がでるように diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 54742cef96..2011ca3636 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -978,6 +978,9 @@ license: "ライセンス" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" drivecleaner: "ドライブクリーナー" +retryAllQueuesNow: "すべてのキューを今すぐ再試行" +retryAllQueuesConfirmTitle: "今すぐ再試行しますか?" +retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。" _achievements: earnedAt: "獲得日時" diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 516e90dcb3..835e884193 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -42,6 +42,7 @@ import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; @@ -370,6 +371,7 @@ const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useCla const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default }; const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default }; const $admin_queue_inboxDelayed: Provider = { provide: 'ep:admin/queue/inbox-delayed', useClass: ep___admin_queue_inboxDelayed.default }; +const $admin_queue_promote: Provider = { provide: 'ep:admin/queue/promote', useClass: ep___admin_queue_promote.default }; const $admin_queue_stats: Provider = { provide: 'ep:admin/queue/stats', useClass: ep___admin_queue_stats.default }; const $admin_relays_add: Provider = { provide: 'ep:admin/relays/add', useClass: ep___admin_relays_add.default }; const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass: ep___admin_relays_list.default }; @@ -702,6 +704,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_queue_clear, $admin_queue_deliverDelayed, $admin_queue_inboxDelayed, + $admin_queue_promote, $admin_queue_stats, $admin_relays_add, $admin_relays_list, @@ -1028,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_queue_clear, $admin_queue_deliverDelayed, $admin_queue_inboxDelayed, + $admin_queue_promote, $admin_queue_stats, $admin_relays_add, $admin_relays_list, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 2930468a22..f6fc79fc70 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -42,6 +42,7 @@ import * as ep___admin_promo_create from './endpoints/admin/promo/create.js'; import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js'; import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js'; import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js'; +import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js'; import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js'; import * as ep___admin_relays_add from './endpoints/admin/relays/add.js'; import * as ep___admin_relays_list from './endpoints/admin/relays/list.js'; @@ -368,6 +369,7 @@ const eps = [ ['admin/queue/clear', ep___admin_queue_clear], ['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed], ['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed], + ['admin/queue/promote', ep___admin_queue_promote], ['admin/queue/stats', ep___admin_queue_stats], ['admin/relays/add', ep___admin_relays_add], ['admin/relays/list', ep___admin_relays_list], diff --git a/packages/backend/src/server/api/endpoints/admin/queue/promote.ts b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts new file mode 100644 index 0000000000..4e57e6613e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/queue/promote.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + type: { type: 'string', enum: ['deliver', 'inbox'] }, + }, + required: ['type'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + private moderationLogService: ModerationLogService, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + let delayedQueues; + + switch (ps.type) { + case 'deliver': + delayedQueues = await this.queueService.deliverQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + + case 'inbox': + delayedQueues = await this.queueService.inboxQueue.getDelayed(); + for (let queueIndex = 0; queueIndex < delayedQueues.length; queueIndex++) { + const queue = delayedQueues[queueIndex]; + await queue.promote(); + } + break; + } + + this.moderationLogService.insertModerationLog(me, 'promoteQueue'); + }); + } +} diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 80e97fed93..509d329eb1 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -4,6 +4,8 @@ +
+ {{ i18n.ts.retryAllQueuesNow }}
@@ -15,6 +17,7 @@ import * as os from '@/os'; import * as config from '@/config'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import MkButton from '@/components/MkButton.vue'; let tab = $ref('deliver'); @@ -30,6 +33,18 @@ function clear() { }); } +function promoteAllQueues() { + os.confirm({ + type: 'warning', + title: i18n.ts.retryAllQueuesConfirmTitle, + text: i18n.ts.retryAllQueuesConfirmText, + }).then(({ canceled }) => { + if (canceled) return; + + os.apiWithDialog('admin/queue/promote', { type: tab }); + }); +} + const headerActions = $computed(() => [{ asFullButton: true, icon: 'ti ti-external-link', From 9a40a4e315bb4a33ec21ef62397e9db98695cc59 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 22 Mar 2023 08:59:50 +0900 Subject: [PATCH 18/48] Update packages/backend/test/utils.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Acid Chicken (硫酸鶏) --- packages/backend/test/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 879d5ec79a..4f501a8726 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -1,4 +1,4 @@ -import * as assert from 'assert'; +import * as assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { isAbsolute, basename } from 'node:path'; import { inspect } from 'node:util'; From 78a3d78a7f6c5bc423b5e2eec84fe9d831edb4a5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 22 Mar 2023 09:09:43 +0900 Subject: [PATCH 19/48] fix drive-cleaner --- packages/frontend/src/pages/settings/drive-cleaner.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 2342280ca7..8178343bbb 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -43,7 +43,7 @@