From 95fbb27a286cc7863218788e755562893b5447ea Mon Sep 17 00:00:00 2001 From: taichanne30 Date: Sun, 1 Oct 2023 19:59:10 +0900 Subject: [PATCH] =?UTF-8?q?enhance:=20=E7=B7=A8=E9=9B=86=E3=82=92=E9=80=A3?= =?UTF-8?q?=E5=90=88=E3=81=95=E3=81=9B=E3=82=8B=EF=BC=88=E9=80=81=E4=BF=A1?= =?UTF-8?q?=E3=81=AE=E3=81=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/CoreModule.ts | 6 + .../backend/src/core/GlobalEventService.ts | 2 +- .../backend/src/core/NoteUpdateService.ts | 239 ++++++++++++++++++ .../src/core/activitypub/ApInboxService.ts | 2 + .../src/server/api/endpoints/notes/update.ts | 23 +- 5 files changed, 259 insertions(+), 13 deletions(-) create mode 100644 packages/backend/src/core/NoteUpdateService.ts diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 78333e70a5..2e64c362a1 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -31,6 +31,7 @@ import { MetaService } from './MetaService.js'; import { MfmService } from './MfmService.js'; import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; +import { NoteUpdateService } from './NoteUpdateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; import { NoteReadService } from './NoteReadService.js'; @@ -157,6 +158,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService }; const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; +const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; @@ -287,6 +289,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting MfmService, ModerationLogService, NoteCreateService, + NoteUpdateService, NoteDeleteService, NotePiningService, NoteReadService, @@ -410,6 +413,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $MfmService, $ModerationLogService, $NoteCreateService, + $NoteUpdateService, $NoteDeleteService, $NotePiningService, $NoteReadService, @@ -534,6 +538,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting MfmService, ModerationLogService, NoteCreateService, + NoteUpdateService, NoteDeleteService, NotePiningService, NoteReadService, @@ -656,6 +661,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $MfmService, $ModerationLogService, $NoteCreateService, + $NoteUpdateService, $NoteDeleteService, $NotePiningService, $NoteReadService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index b74fbbe584..6d64b2b1ee 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -110,7 +110,7 @@ export interface NoteEventTypes { }; updated: { cw: string | null; - text: string; + text: string | null; }; reacted: { reaction: string; diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts new file mode 100644 index 0000000000..e0c0376526 --- /dev/null +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -0,0 +1,239 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setImmediate } from 'node:timers/promises'; +import { In, DataSource } from 'typeorm'; +import * as Redis from 'ioredis'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import type { IMentionedRemoteUsers } from '@/models/Note.js'; +import { MiNote } from '@/models/Note.js'; +import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { RelayService } from '@/core/RelayService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { NoteReadService } from '@/core/NoteReadService.js'; +import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; +import { bindThis } from '@/decorators.js'; +import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { RoleService } from '@/core/RoleService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { SearchService } from '@/core/SearchService.js'; + +type Option = { + updatedAt?: Date | null; + text: string | null; + cw: string | null; +}; + +@Injectable() +export class NoteUpdateService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.mutedNotesRepository) + private mutedNotesRepository: MutedNotesRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private noteEntityService: NoteEntityService, + private idService: IdService, + private globalEventService: GlobalEventService, + private queueService: QueueService, + private noteReadService: NoteReadService, + private notificationService: NotificationService, + private relayService: RelayService, + private federatedInstanceService: FederatedInstanceService, + private remoteUserResolveService: RemoteUserResolveService, + private apDeliverManagerService: ApDeliverManagerService, + private apRendererService: ApRendererService, + private roleService: RoleService, + private metaService: MetaService, + private searchService: SearchService, + private activeUsersChart: ActiveUsersChart, + ) { } + + @bindThis + public async update(user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + createdAt: MiUser['createdAt']; + isBot: MiUser['isBot']; + }, data: Option, note: MiNote, silent = false): Promise { + if (data.updatedAt == null) data.updatedAt = new Date(); + + if (data.text) { + if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { + data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + } + data.text = data.text.trim(); + } else { + data.text = null; + } + + const updatedNote = await this.updateNote(user, note, data); + + if (updatedNote) { + setImmediate('post updated', { signal: this.#shutdownController.signal }).then( + () => this.postNoteUpdated(updatedNote, user, silent), + () => { /* aborted, ignore this */ }, + ); + } + + return note; + } + + @bindThis + private async updateNote(user: { id: MiUser['id']; host: MiUser['host']; }, note: MiNote, data: Option) { + const values = { + updatedAt: data.updatedAt!, + text: data.text, + cw: data.cw ?? null, + }; + + // 投稿を更新 + try { + await this.notesRepository.update({ id: note.id }, values); + + return await this.notesRepository.findOneBy({ id: note.id }); + } catch (e) { + console.error(e); + + throw e; + } + } + + @bindThis + private async postNoteUpdated(note: MiNote, user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + createdAt: MiUser['createdAt']; + isBot: MiUser['isBot']; + }, silent: boolean) { + if (!silent) { + if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); + + this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, text: note.text }); + + //#region AP deliver + if (this.userEntityService.isLocalUser(user)) { + (async () => { + const noteActivity = await this.renderNoteActivity(note); + + this.deliverToConcerned(user, note, noteActivity); + })(); + } + //#endregion + } + + // Register to search database + this.reIndex(note); + } + + @bindThis + private async renderNoteActivity(note: MiNote) { + const content = this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), note); + + return this.apRendererService.addContext(content); + } + + @bindThis + private async getMentionedRemoteUsers(note: MiNote) { + const where = [] as any[]; + + // mention / reply / dm + const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); + if (uris.length > 0) { + where.push( + { uri: In(uris) }, + ); + } + + // renote / quote + if (note.renoteUserId) { + where.push({ + id: note.renoteUserId, + }); + } + + if (where.length === 0) return []; + + return await this.usersRepository.find({ + where, + }) as MiRemoteUser[]; + } + + @bindThis + private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { + this.apDeliverManagerService.deliverToFollowers(user, content); + this.relayService.deliverToRelays(user, content); + const remoteUsers = await this.getMentionedRemoteUsers(note); + for (const remoteUser of remoteUsers) { + this.apDeliverManagerService.deliverToUser(user, content, remoteUser); + } + } + + @bindThis + private reIndex(note: MiNote) { + if (note.text == null && note.cw == null) return; + + this.searchService.unindexNote(note); + this.searchService.indexNote(note); + } + + @bindThis + public dispose(): void { + this.#shutdownController.abort(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index b921ee7454..066b5eeb6f 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; @@ -73,6 +74,7 @@ export class ApInboxService { private notePiningService: NotePiningService, private userBlockingService: UserBlockingService, private noteCreateService: NoteCreateService, + private noteUpdateService: NoteUpdateService, private noteDeleteService: NoteDeleteService, private appLockService: AppLockService, private apResolverService: ApResolverService, diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index cdf7f085e0..6be2aa7c0e 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -7,7 +7,8 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -58,11 +59,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private getterService: GetterService, - private globalEventService: GlobalEventService, + private noteEntityService: NoteEntityService, + private noteUpdateService: NoteUpdateService, ) { super(meta, paramDef, async (ps, me) => { const note = await this.getterService.getNote(ps.noteId).catch(err => { @@ -74,16 +73,16 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchNote); } - await this.notesRepository.update({ id: note.id }, { - updatedAt: new Date(), + const data = { cw: ps.cw, text: ps.text, - }); + }; - this.globalEventService.publishNoteStream(note.id, 'updated', { - cw: ps.cw, - text: ps.text, - }); + const updatedNote = await this.noteUpdateService.update(await this.usersRepository.findOneByOrFail({ id: note.userId }), data, note, false); + + return { + updatedNote: await this.noteEntityService.pack(updatedNote, me), + }; }); } }