From 3d09a7cbb649143c08756846e86e900b278bfa10 Mon Sep 17 00:00:00 2001
From: Nya Candy <dev@candinya.com>
Date: Wed, 31 Jul 2024 19:16:05 +0800
Subject: [PATCH] feat(wip): update note attachments

---
 .../backend/src/core/GlobalEventService.ts    |  2 +
 .../backend/src/core/NoteUpdateService.ts     | 53 +++++++++++--------
 .../core/activitypub/models/ApNoteService.ts  | 10 ++++
 .../src/server/api/endpoints/notes/update.ts  | 45 +++++++++++++++-
 .../frontend/src/scripts/use-note-capture.ts  |  2 +
 5 files changed, 88 insertions(+), 24 deletions(-)

diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index 3f4084a94..d2cf5346b 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -123,6 +123,8 @@ export interface NoteEventTypes {
 		updatedAt: string;
 		tags?: string[];
 		emojis?: Record<string, string>;
+		fileIds?: string[];
+		files?: Packed<'DriveFile'>[];
 	};
 	reacted: {
 		reaction: string;
diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts
index 72dd620dc..0a61b3ed6 100644
--- a/packages/backend/src/core/NoteUpdateService.ts
+++ b/packages/backend/src/core/NoteUpdateService.ts
@@ -7,7 +7,7 @@ import { Injectable, Inject } from '@nestjs/common';
 import * as mfm from 'mfm-js';
 import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
 import type { MiNote } from '@/models/Note.js';
-import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js';
+import type { InstancesRepository, MiDriveFile, NotesRepository, UsersRepository } from '@/models/_.js';
 import { RelayService } from '@/core/RelayService.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { DI } from '@/di-symbols.js';
@@ -32,8 +32,14 @@ import { extractHashtags } from "@/misc/extract-hashtags.js";
 import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js";
 import { UtilityService } from "@/core/UtilityService.js";
 import { CustomEmojiService } from "@/core/CustomEmojiService.js";
+import { awaitAll } from "@/misc/prelude/await-all.js";
+import type { DriveFileEntityService } from "@/core/entities/DriveFileEntityService.js";
 
-type Option = Pick<MiNote, 'text' | 'cw' | 'updatedAt'> & {
+type Option = {
+	updatedAt?: Date | null;
+	text: string | null;
+	files?: MiDriveFile[] | null;
+	cw: string | null;
 	apHashtags?: string[] | null;
 	apEmojis?: string[] | null;
 }
@@ -54,6 +60,7 @@ export class NoteUpdateService {
 		private instancesRepository: InstancesRepository,
 
 		private customEmojiService: CustomEmojiService,
+		private driveFileEntityService: DriveFileEntityService,
 		private userEntityService: UserEntityService,
 		private noteEntityService: NoteEntityService,
 		private globalEventService: GlobalEventService,
@@ -76,21 +83,19 @@ export class NoteUpdateService {
 	 * Update note
 	 * @param user Note creator
 	 * @param note Note to update
-	 * @param ps New note info
+	 * @param data New note info
 	 */
-	async update(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, ps: Option, quiet = false, updater?: MiUser) {
-		if (!ps.updatedAt) {
+	async update(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, data: Option, quiet = false, updater?: MiUser) {
+		if (!data.updatedAt) {
 			throw new Error('update time is required');
 		}
 
-		if (note.history && note.history.findIndex(h => h.createdAt === ps.updatedAt?.toISOString()) !== -1) {
+		if (note.history && note.history.findIndex(h => h.createdAt === data.updatedAt?.toISOString()) !== -1) {
 			// Same history already exists, skip this
 			return;
 		}
 
 		// Parse tags & emojis
-		const data = ps;
-
 		const meta = await this.metaService.fetch();
 
 		let tags = data.apHashtags;
@@ -114,25 +119,28 @@ export class NoteUpdateService {
 
 		tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
 
-		const newNote = {
+		const newNote: MiNote = {
 			...note,
 
 			// Overwrite updated fields
-			text: ps.text,
-			cw: ps.cw,
-			updatedAt: ps.updatedAt,
+			text: data.text,
+			cw: data.cw,
+			updatedAt: data.updatedAt,
 			tags,
 			emojis,
+			fileIds: data.files ? data.files.map(file => file.id) : [],
 		};
 
 		if (!quiet) {
-			this.globalEventService.publishNoteStream(note.id, 'updated', {
-				cw: ps.cw,
-				text: ps.text ?? '', // prevent null
-				updatedAt: ps.updatedAt.toISOString(),
+			this.globalEventService.publishNoteStream(note.id, 'updated', await awaitAll({
+				fileIds: newNote.fileIds,
+				files: this.driveFileEntityService.packManyByIds(newNote.fileIds),
+				cw: data.cw,
+				text: data.text ?? '', // prevent null
+				updatedAt: data.updatedAt.toISOString(),
 				tags: tags.length > 0 ? tags : undefined,
-				emojis: note.userHost != null ? await this.customEmojiService.populateEmojis(emojis, note.userHost) : undefined,
-			});
+				emojis: note.userHost != null ? this.customEmojiService.populateEmojis(emojis, note.userHost) : undefined,
+			}));
 
 			if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
 				const content = this.apRendererService.addContext(
@@ -151,7 +159,7 @@ export class NoteUpdateService {
 			cw: note.cw,
 			text: note.text,
 		}];
-		if (note.updatedAt && note.updatedAt >= ps.updatedAt) {
+		if (note.updatedAt && note.updatedAt >= data.updatedAt) {
 			// Previous version, just update history
 			history.sort((h1, h2) => new Date(h1.createdAt).getTime() - new Date(h2.createdAt).getTime()); // earliest -> latest
 
@@ -166,10 +174,11 @@ export class NoteUpdateService {
 
 			// Update note info
 			await this.notesRepository.update({ id: note.id }, {
-				updatedAt: ps.updatedAt,
+				updatedAt: data.updatedAt,
+				fileIds: newNote.fileIds,
 				history,
-				cw: ps.cw,
-				text: ps.text,
+				cw: data.cw,
+				text: data.text,
 				tags,
 				emojis,
 			});
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 5b910419c..37ba9f603 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -370,6 +370,15 @@ export class ApNoteService {
 
 		const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
 
+		// 添付ファイル
+		const files: MiDriveFile[] = [];
+
+		for (const attach of toArray(note.attachment)) {
+			attach.sensitive ??= note.sensitive;
+			const file = await this.apImageService.resolveImage(actor, attach);
+			if (file) files.push(file);
+		}
+
 		const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
 			this.logger.info(`extractEmojis: ${e}`);
 			return [];
@@ -378,6 +387,7 @@ export class ApNoteService {
 		const apEmojis = emojis.map(emoji => emoji.name);
 
 		await this.noteUpdateService.update(actor, originNote, {
+			files,
 			cw,
 			text,
 			apHashtags,
diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts
index e78fcb6ce..80a4bbf9b 100644
--- a/packages/backend/src/server/api/endpoints/notes/update.ts
+++ b/packages/backend/src/server/api/endpoints/notes/update.ts
@@ -5,7 +5,7 @@
 
 import ms from 'ms';
 import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiDriveFile, UsersRepository } from '@/models/_.js';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import { DI } from '@/di-symbols.js';
 import { GetterService } from '@/server/api/GetterService.js';
@@ -33,6 +33,12 @@ export const meta = {
 			code: 'NO_SUCH_NOTE',
 			id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
 		},
+
+		noSuchFile: {
+			message: 'Some files are not found.',
+			code: 'NO_SUCH_FILE',
+			id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
+		},
 	},
 } as const;
 
@@ -46,6 +52,20 @@ export const paramDef = {
 			maxLength: MAX_NOTE_TEXT_LENGTH,
 			nullable: false,
 		},
+		fileIds: {
+			type: 'array',
+			uniqueItems: true,
+			minItems: 1,
+			maxItems: 16,
+			items: { type: 'string', format: 'misskey:id' },
+		},
+		mediaIds: {
+			type: 'array',
+			uniqueItems: true,
+			minItems: 1,
+			maxItems: 16,
+			items: { type: 'string', format: 'misskey:id' },
+		},
 		cw: { type: 'string', nullable: true, maxLength: 100 },
 	},
 	required: ['noteId', 'text', 'cw'],
@@ -57,6 +77,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
+		@Inject(DI.driveFilesRepository)
+		private driveFilesRepository: DriveFilesRepository,
+
 		private getterService: GetterService,
 		private noteUpdateService: NoteUpdateService,
 	) {
@@ -70,7 +93,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw new ApiError(meta.errors.noSuchNote);
 			}
 
-			if (note.text === ps.text && note.cw === ps.cw) {
+			let files: MiDriveFile[] = [];
+			const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
+			if (fileIds != null) {
+				files = await this.driveFilesRepository.createQueryBuilder('file')
+					.where('file.userId = :userId AND file.id IN (:...fileIds)', {
+						userId: me.id,
+						fileIds,
+					})
+					.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
+					.setParameters({ fileIds })
+					.getMany();
+
+				if (files.length !== fileIds.length) {
+					throw new ApiError(meta.errors.noSuchFile);
+				}
+			}
+
+			if (note.text === ps.text && note.cw === ps.cw && note.fileIds === fileIds) {
 				// The same as old note, nothing to do
 				return;
 			}
@@ -79,6 +119,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				text: ps.text,
 				cw: ps.cw,
 				updatedAt: new Date(),
+				files,
 			}, false, me);
 		});
 	}
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index 4b0092e59..13c6c1f48 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -89,6 +89,8 @@ export function useNoteCapture(props: {
 				note.value.text = body.text;
 				note.value.tags = body.tags;
 				note.value.emojis = body.emojis;
+				note.value.fileIds = body.fileIds;
+				note.value.files = body.files;
 				break;
 			}