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; }