feat(wip): update note attachments

This commit is contained in:
Nya Candy 2024-07-31 19:16:05 +08:00 committed by fly_mc
parent 21e1598584
commit 3d09a7cbb6
5 changed files with 88 additions and 24 deletions

View file

@ -123,6 +123,8 @@ export interface NoteEventTypes {
updatedAt: string;
tags?: string[];
emojis?: Record<string, string>;
fileIds?: string[];
files?: Packed<'DriveFile'>[];
};
reacted: {
reaction: string;

View file

@ -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,
});

View file

@ -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,

View file

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

View file

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