/* * SPDX-FileCopyrightText: syuilo and other misskey contributors * SPDX-License-Identifier: AGPL-3.0-only */ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.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 NotesChart from '@/core/chart/charts/notes.js'; import PerUserNotesChart from '@/core/chart/charts/per-user-notes.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @Injectable() export class NoteDeleteService { constructor( @Inject(DI.config) private config: Config, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private metaService: MetaService, private searchService: SearchService, private moderationLogService: ModerationLogService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, ) {} /** * 投稿を削除します。 * @param user 投稿者 * @param note 投稿 */ async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { const deletedAt = new Date(); const cascadingNotes = await this.findCascadingNotes(note); // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) { this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1); if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1); } if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); } if (!quiet) { this.globalEventService.publishNoteStream(note.id, 'deleted', { deletedAt: deletedAt, }); //#region ローカルの投稿なら削除アクティビティを配送 if (this.userEntityService.isLocalUser(user) && !note.localOnly) { let renote: MiNote | null = null; // if deletd note is renote if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) { renote = await this.notesRepository.findOneBy({ id: note.renoteId, }); } const content = this.apRendererService.addContext(renote ? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user) : this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user)); this.deliverToConcerned(user, note, content); } // also deliever delete activity to cascaded notes const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes for (const cascadingNote of federatedLocalCascadingNotes) { if (!cascadingNote.user) continue; if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); this.deliverToConcerned(cascadingNote.user, cascadingNote, content); } //#endregion const meta = await this.metaService.fetch(); this.notesChart.update(note, false); if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserNotesChart.update(user, note, false); } if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, false); } }); } } for (const cascadingNote of cascadingNotes) { this.searchService.unindexNote(cascadingNote); } this.searchService.unindexNote(note); await this.notesRepository.delete({ id: note.id, userId: user.id, }); if (deleter && (note.userId !== deleter.id)) { const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); this.moderationLogService.log(deleter, 'deleteNote', { noteId: note.id, noteUserId: note.userId, noteUserUsername: user.username, noteUserHost: user.host, note: note, }); } } @bindThis private async findCascadingNotes(note: MiNote): Promise { const recursive = async (noteId: string): Promise => { const query = this.notesRepository.createQueryBuilder('note') .where('note.replyId = :noteId', { noteId }) .orWhere(new Brackets(q => { q.where('note.renoteId = :noteId', { noteId }) .andWhere('note.text IS NOT NULL'); })) .leftJoinAndSelect('note.user', 'user'); const replies = await query.getMany(); return [ replies, ...await Promise.all(replies.map(reply => recursive(reply.id))), ].flat(); }; const cascadingNotes: MiNote[] = await recursive(note.id); return cascadingNotes; } @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); } } }