diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index 00252a6f4..5f691bd6b 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -1,11 +1,17 @@ import { DriveFile } from '../../models/entities/drive-file'; import { InternalStorage } from './internal-storage'; -import { DriveFiles, Instances, Notes } from '../../models'; +import { DriveFiles, Instances, Notes, Users } from '../../models'; import { driveChart, perUserDriveChart, instanceChart } from '../chart'; import { createDeleteObjectStorageFileJob } from '../../queue'; import { fetchMeta } from '../../misc/fetch-meta'; import { getS3 } from './s3'; import { v4 as uuid } from 'uuid'; +import { Note } from '../../models/entities/note'; +import { renderActivity } from '../../remote/activitypub/renderer'; +import renderDelete from '../../remote/activitypub/renderer/delete'; +import renderTombstone from '../../remote/activitypub/renderer/tombstone'; +import config from '../../config'; +import { deliverToFollowers } from '../../remote/activitypub/deliver-manager'; export async function deleteFile(file: DriveFile, isExpired = false) { if (file.storedInternal) { @@ -63,7 +69,7 @@ export async function deleteFileSync(file: DriveFile, isExpired = false) { postProcess(file, isExpired); } -function postProcess(file: DriveFile, isExpired = false) { +async function postProcess(file: DriveFile, isExpired = false) { // リモートファイル期限切れ削除後は直リンクにする if (isExpired && file.userHost !== null && file.uri != null) { DriveFiles.update(file.id, { @@ -81,8 +87,23 @@ function postProcess(file: DriveFile, isExpired = false) { DriveFiles.delete(file.id); // TODO: トランザクション + const relatedNotes = await findRelatedNotes(file.id); + for (const relatedNote of relatedNotes) { // for each note with deleted driveFile + const cascadingNotes = (await findCascadingNotes(relatedNote)).filter(note => !note.localOnly); + for (const cascadingNote of cascadingNotes) { // for each notes subject to cascade deletion + if (!cascadingNote.user) continue; + if (!Users.isLocalUser(cascadingNote.user)) continue; + const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); + deliverToFollowers(cascadingNote.user, content); // federate delete msg + } + if (!relatedNote.user) continue; + if (Users.isLocalUser(relatedNote.user)) { + const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${relatedNote.id}`), relatedNote.user)); + deliverToFollowers(relatedNote.user, content); + } + } Notes.createQueryBuilder().delete() - .where(':id = ANY(fileIds)', { id: file.id }) + .where(':id = ANY("fileIds")', { id: file.id }) .execute(); } @@ -106,3 +127,32 @@ export async function deleteObjectStorageFile(key: string) { Key: key }).promise(); } + +async function findRelatedNotes(fileId: string) { + // NOTE: When running raw query, TypeORM converts field name to lowercase. Wrap in quotes to prevent conversion. + const relatedNotes = await Notes.createQueryBuilder('note').where(':id = ANY("fileIds")', { id: fileId }).getMany(); + for (const relatedNote of relatedNotes) { + const user = await Users.findOne({ id: relatedNote.userId }); + if (user) + relatedNote.user = user; + } + return relatedNotes; +} + +async function findCascadingNotes(note: Note) { + const cascadingNotes: Note[] = []; + + const recursive = async (noteId: string) => { + const query = Notes.createQueryBuilder('note') + .where('note.replyId = :noteId', { noteId }) + .leftJoinAndSelect('note.user', 'user'); + const replies = await query.getMany(); + for (const reply of replies) { + cascadingNotes.push(reply); + await recursive(reply.id); + } + }; + await recursive(note.id); + + return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users +} diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index 73cb0f411..29b9c576d 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -20,11 +20,6 @@ import { deliverToFollowers } from '../../remote/activitypub/deliver-manager'; export default async function(user: User, note: Note, quiet = false) { const deletedAt = new Date(); - await Notes.delete({ - id: note.id, - userId: user.id - }); - if (note.renoteId) { Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); Notes.decrement({ id: note.renoteId }, 'score', 1); @@ -39,6 +34,7 @@ export default async function(user: User, note: Note, quiet = false) { if (Users.isLocalUser(user)) { let renote: Note | undefined; + // if deletd note is renote if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) { renote = await Notes.findOne({ id: note.renoteId @@ -51,6 +47,15 @@ export default async function(user: User, note: Note, quiet = false) { deliverToFollowers(user, content); } + + // also deliever delete activity to cascaded notes + const cascadingNotes = (await findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes + for (const cascadingNote of cascadingNotes) { + if (!cascadingNote.user) continue; + if (!Users.isLocalUser(cascadingNote.user)) continue; + const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); + deliverToFollowers(cascadingNote.user, content); + } //#endregion // 統計を更新 @@ -64,4 +69,27 @@ export default async function(user: User, note: Note, quiet = false) { }); } } + + await Notes.delete({ + id: note.id, + userId: user.id + }); +} + +async function findCascadingNotes(note: Note) { + const cascadingNotes: Note[] = []; + + const recursive = async (noteId: string) => { + const query = Notes.createQueryBuilder('note') + .where('note.replyId = :noteId', { noteId }) + .leftJoinAndSelect('note.user', 'user'); + const replies = await query.getMany(); + for (const reply of replies) { + cascadingNotes.push(reply); + await recursive(reply.id); + } + }; + await recursive(note.id); + + return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users }