1
0
Fork 0
mirror of https://github.com/paricafe/misskey.git synced 2025-03-01 11:54:26 -06:00

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
packages
backend/src
core
server/api/endpoints/notes
frontend/src/scripts

View file

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

View file

@ -7,7 +7,7 @@ import { Injectable, Inject } from '@nestjs/common';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { MiNote } from '@/models/Note.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 { RelayService } from '@/core/RelayService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { DI } from '@/di-symbols.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 { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js";
import { UtilityService } from "@/core/UtilityService.js"; import { UtilityService } from "@/core/UtilityService.js";
import { CustomEmojiService } from "@/core/CustomEmojiService.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; apHashtags?: string[] | null;
apEmojis?: string[] | null; apEmojis?: string[] | null;
} }
@ -54,6 +60,7 @@ export class NoteUpdateService {
private instancesRepository: InstancesRepository, private instancesRepository: InstancesRepository,
private customEmojiService: CustomEmojiService, private customEmojiService: CustomEmojiService,
private driveFileEntityService: DriveFileEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
@ -76,21 +83,19 @@ export class NoteUpdateService {
* Update note * Update note
* @param user Note creator * @param user Note creator
* @param note Note to update * @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) { async update(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, data: Option, quiet = false, updater?: MiUser) {
if (!ps.updatedAt) { if (!data.updatedAt) {
throw new Error('update time is required'); 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 // Same history already exists, skip this
return; return;
} }
// Parse tags & emojis // Parse tags & emojis
const data = ps;
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
let tags = data.apHashtags; let tags = data.apHashtags;
@ -114,25 +119,28 @@ export class NoteUpdateService {
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
const newNote = { const newNote: MiNote = {
...note, ...note,
// Overwrite updated fields // Overwrite updated fields
text: ps.text, text: data.text,
cw: ps.cw, cw: data.cw,
updatedAt: ps.updatedAt, updatedAt: data.updatedAt,
tags, tags,
emojis, emojis,
fileIds: data.files ? data.files.map(file => file.id) : [],
}; };
if (!quiet) { if (!quiet) {
this.globalEventService.publishNoteStream(note.id, 'updated', { this.globalEventService.publishNoteStream(note.id, 'updated', await awaitAll({
cw: ps.cw, fileIds: newNote.fileIds,
text: ps.text ?? '', // prevent null files: this.driveFileEntityService.packManyByIds(newNote.fileIds),
updatedAt: ps.updatedAt.toISOString(), cw: data.cw,
text: data.text ?? '', // prevent null
updatedAt: data.updatedAt.toISOString(),
tags: tags.length > 0 ? tags : undefined, 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) { if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
const content = this.apRendererService.addContext( const content = this.apRendererService.addContext(
@ -151,7 +159,7 @@ export class NoteUpdateService {
cw: note.cw, cw: note.cw,
text: note.text, text: note.text,
}]; }];
if (note.updatedAt && note.updatedAt >= ps.updatedAt) { if (note.updatedAt && note.updatedAt >= data.updatedAt) {
// Previous version, just update history // Previous version, just update history
history.sort((h1, h2) => new Date(h1.createdAt).getTime() - new Date(h2.createdAt).getTime()); // earliest -> latest 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 // Update note info
await this.notesRepository.update({ id: note.id }, { await this.notesRepository.update({ id: note.id }, {
updatedAt: ps.updatedAt, updatedAt: data.updatedAt,
fileIds: newNote.fileIds,
history, history,
cw: ps.cw, cw: data.cw,
text: ps.text, text: data.text,
tags, tags,
emojis, emojis,
}); });

View file

@ -370,6 +370,15 @@ export class ApNoteService {
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; 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 => { const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
this.logger.info(`extractEmojis: ${e}`); this.logger.info(`extractEmojis: ${e}`);
return []; return [];
@ -378,6 +387,7 @@ export class ApNoteService {
const apEmojis = emojis.map(emoji => emoji.name); const apEmojis = emojis.map(emoji => emoji.name);
await this.noteUpdateService.update(actor, originNote, { await this.noteUpdateService.update(actor, originNote, {
files,
cw, cw,
text, text,
apHashtags, apHashtags,

View file

@ -5,7 +5,7 @@
import ms from 'ms'; import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common'; 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 { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js'; import { GetterService } from '@/server/api/GetterService.js';
@ -33,6 +33,12 @@ export const meta = {
code: 'NO_SUCH_NOTE', code: 'NO_SUCH_NOTE',
id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', 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; } as const;
@ -46,6 +52,20 @@ export const paramDef = {
maxLength: MAX_NOTE_TEXT_LENGTH, maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false, 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 }, cw: { type: 'string', nullable: true, maxLength: 100 },
}, },
required: ['noteId', 'text', 'cw'], required: ['noteId', 'text', 'cw'],
@ -57,6 +77,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private getterService: GetterService, private getterService: GetterService,
private noteUpdateService: NoteUpdateService, private noteUpdateService: NoteUpdateService,
) { ) {
@ -70,7 +93,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchNote); 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 // The same as old note, nothing to do
return; return;
} }
@ -79,6 +119,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
text: ps.text, text: ps.text,
cw: ps.cw, cw: ps.cw,
updatedAt: new Date(), updatedAt: new Date(),
files,
}, false, me); }, false, me);
}); });
} }

View file

@ -89,6 +89,8 @@ export function useNoteCapture(props: {
note.value.text = body.text; note.value.text = body.text;
note.value.tags = body.tags; note.value.tags = body.tags;
note.value.emojis = body.emojis; note.value.emojis = body.emojis;
note.value.fileIds = body.fileIds;
note.value.files = body.files;
break; break;
} }