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:
parent
21e1598584
commit
3d09a7cbb6
5 changed files with 88 additions and 24 deletions
packages
backend/src
core
server/api/endpoints/notes
frontend/src/scripts
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue