diff --git a/keyserver/src/deleters/link-deleters.js b/keyserver/src/deleters/link-deleters.js index b575ffb86..ecea51fcb 100644 --- a/keyserver/src/deleters/link-deleters.js +++ b/keyserver/src/deleters/link-deleters.js @@ -1,87 +1,107 @@ // @flow import { inviteLinkBlobHash } from 'lib/shared/invite-links.js'; import type { DisableInviteLinkRequest } from 'lib/types/link-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { dbQuery, SQL } from '../database/database.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { deleteBlob } from '../services/blob.js'; import { Viewer } from '../session/viewer.js'; type InviteLinksToDelete = { +id: string, +name: string, +blobHolder: string, }; async function deleteInviteLinks( links: $ReadOnlyArray, ): Promise { if (links.length === 0) { return; } await Promise.all( links.map(({ name, blobHolder }) => deleteBlob( { hash: inviteLinkBlobHash(name), holder: blobHolder, }, true, ), ), ); const ids = links.map(({ id }) => id); await dbQuery( SQL` START TRANSACTION; DELETE FROM invite_links WHERE id IN (${ids}); DELETE FROM ids WHERE id IN (${ids}); COMMIT; `, { multipleStatements: true }, ); } async function deleteInviteLink( viewer: Viewer, request: DisableInviteLinkRequest, ): Promise { const hasPermission = await checkThreadPermission( viewer, request.communityID, threadPermissions.MANAGE_INVITE_LINKS, ); if (!hasPermission) { throw new ServerError('invalid_credentials'); } const [[result]] = await dbQuery(SQL` SELECT id, name, blob_holder AS blobHolder FROM invite_links WHERE name = ${request.name} AND community = ${request.communityID} `); if (!result) { return; } await deleteInviteLinks([result]); } async function deleteOrphanedInviteLinks(): Promise { const [selectResults] = await dbQuery(SQL` SELECT i.id, i.name, i.blob_holder AS blobHolder FROM invite_links i LEFT JOIN threads tc ON tc.id = i.community LEFT JOIN threads tt ON tt.id = i.thread WHERE tc.id IS NULL OR (i.thread IS NOT NULL AND tt.id IS NULL) `); const inviteLinksToDelete = selectResults.map(({ id, name, blobHolder }) => ({ id, name, blobHolder, })); await deleteInviteLinks(inviteLinksToDelete); } -export { deleteInviteLink, deleteOrphanedInviteLinks }; +async function deleteInviteLinksForThreadIDs( + threadIDs: $ReadOnlyArray, +): Promise { + const [selectResults] = await dbQuery(SQL` + SELECT id, name, blob_holder AS blobHolder + FROM invite_links + WHERE community IN (${threadIDs}) OR thread IN (${threadIDs}) + `); + const inviteLinksToDelete = selectResults.map(({ id, name, blobHolder }) => ({ + id, + name, + blobHolder, + })); + await deleteInviteLinks(inviteLinksToDelete); +} + +export { + deleteInviteLink, + deleteOrphanedInviteLinks, + deleteInviteLinksForThreadIDs, +}; diff --git a/keyserver/src/deleters/thread-deleters.js b/keyserver/src/deleters/thread-deleters.js index 5d5a5f3a5..4efacad3e 100644 --- a/keyserver/src/deleters/thread-deleters.js +++ b/keyserver/src/deleters/thread-deleters.js @@ -1,169 +1,171 @@ // @flow import { permissionLookup } from 'lib/permissions/thread-permissions.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { type ThreadDeletionRequest, type LeaveThreadResult, } from 'lib/types/thread-types.js'; import { updateTypes } from 'lib/types/update-types-enum.js'; import { ServerError } from 'lib/utils/errors.js'; +import { deleteInviteLinksForThreadIDs } from './link-deleters.js'; import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { fetchServerThreadInfos, fetchContainedThreadIDs, } from '../fetchers/thread-fetchers.js'; import { fetchThreadPermissionsBlob } from '../fetchers/thread-permission-fetchers.js'; import { fetchUpdateInfoForThreadDeletion } from '../fetchers/update-fetchers.js'; import { rescindPushNotifs } from '../push/rescind.js'; import { removeBlobHolders } from '../services/blob.js'; import type { Viewer } from '../session/viewer.js'; import { blobHoldersFromUploadRows } from '../uploads/media-utils.js'; type DeleteThreadOptions = Partial<{ +ignorePermissions: boolean, }>; async function deleteThread( viewer: Viewer, threadDeletionRequest: ThreadDeletionRequest, options?: DeleteThreadOptions, ): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const ignorePermissions = (options?.ignorePermissions && viewer.isScriptViewer) ?? false; const { threadID } = threadDeletionRequest; const permissionsBlob = await fetchThreadPermissionsBlob(viewer, threadID); if (!permissionsBlob && !ignorePermissions) { // This should only occur if the first request goes through but the client // never receives the response const { updateInfos } = await fetchUpdateInfoForThreadDeletion( viewer, threadID, ); return { updatesResult: { newUpdates: updateInfos } }; } const hasPermission = permissionLookup( permissionsBlob, threadPermissions.DELETE_THREAD, ); if (!hasPermission && !ignorePermissions) { throw new ServerError('invalid_credentials'); } // TODO: handle descendant thread permission update correctly. // thread-permission-updaters should be used for descendant threads. const threadIDs = await fetchContainedThreadIDs(threadID); await fetchAndDeleteThreadBlobHolders(threadIDs); const [{ threadInfos: serverThreadInfos }] = await Promise.all([ fetchServerThreadInfos({ threadIDs: new Set(threadIDs) }), rescindPushNotifs( SQL`n.thread IN (${threadIDs})`, SQL`IF(m.thread IN (${threadIDs}), NULL, m.thread)`, ), ]); const time = Date.now(); const updateDatas = []; for (const containedThreadID of threadIDs) { for (const memberInfo of serverThreadInfos[containedThreadID].members) { updateDatas.push({ type: updateTypes.DELETE_THREAD, userID: memberInfo.id, time, threadID: containedThreadID, }); } } const [{ viewerUpdates }] = await Promise.all([ createUpdates(updateDatas, { viewer, updatesForCurrentSession: 'return' }), - deleteThreadsFromDB(threadIDs), + deleteThreads(threadIDs), ]); return { updatesResult: { newUpdates: viewerUpdates } }; } async function fetchAndDeleteThreadBlobHolders( threadIDs: $ReadOnlyArray, ): Promise { const query = SQL` SELECT extra FROM uploads WHERE container IN (${threadIDs}) `; const [rows] = await dbQuery(query); const blobHolders = blobHoldersFromUploadRows(rows); await removeBlobHolders(blobHolders); } -function deleteThreadsFromDB( - threadIDs: $ReadOnlyArray, -): Promise { +async function deleteThreads(threadIDs: $ReadOnlyArray): Promise { const deletionQuery = SQL` START TRANSACTION; DELETE FROM threads WHERE id IN (${threadIDs}); DELETE FROM communities WHERE id IN (${threadIDs}); DELETE FROM ids WHERE id IN (${threadIDs}); DELETE d, id, e, ie, r, ir FROM days d LEFT JOIN ids id ON id.id = d.id LEFT JOIN entries e ON e.day = d.id LEFT JOIN ids ie ON ie.id = e.id LEFT JOIN revisions r ON r.entry = e.id LEFT JOIN ids ir ON ir.id = r.id WHERE d.thread IN (${threadIDs}); DELETE FROM memberships WHERE thread IN (${threadIDs}); DELETE r, i FROM roles r LEFT JOIN ids i ON i.id = r.id WHERE r.thread IN (${threadIDs}); DELETE m, im, u, iu FROM messages m LEFT JOIN ids im ON im.id = m.id LEFT JOIN uploads u ON u.container = m.id LEFT JOIN ids iu ON iu.id = u.id WHERE m.thread IN (${threadIDs}); DELETE FROM uploads WHERE container IN (${threadIDs}); DELETE FROM focused WHERE thread IN (${threadIDs}); DELETE n, i FROM notifications n LEFT JOIN ids i ON i.id = n.id WHERE n.thread IN (${threadIDs}); COMMIT; `; - return dbQuery(deletionQuery, { multipleStatements: true }); + await Promise.all([ + dbQuery(deletionQuery, { multipleStatements: true }), + deleteInviteLinksForThreadIDs(threadIDs), + ]); } async function deleteInaccessibleThreads(): Promise { // A thread is considered "inaccessible" if it has no membership rows. Note // that membership rows exist whenever a user can see a thread, even if they // are not technically a member (in which case role=0). For now, we're also // excluding threads with children, since to properly delete those we would // need to update their parent_thread_id, and possibly change their type. const [fetchResult] = await dbQuery(SQL` SELECT t.id FROM threads t LEFT JOIN memberships m ON m.thread = t.id AND m.role > -1 LEFT JOIN threads c ON c.parent_thread_id = t.id WHERE m.thread IS NULL AND c.id IS NULL `); const threadIDs = new Set(fetchResult.map(({ id }) => id)); if (threadIDs.size === 0) { return; } const containerIDs = [...threadIDs]; await fetchAndDeleteThreadBlobHolders(containerIDs); - await deleteThreadsFromDB(containerIDs); + await deleteThreads(containerIDs); } export { deleteThread, deleteInaccessibleThreads };