diff --git a/keyserver/src/deleters/account-deleters.js b/keyserver/src/deleters/account-deleters.js index 6a869dd3f..a37276d13 100644 --- a/keyserver/src/deleters/account-deleters.js +++ b/keyserver/src/deleters/account-deleters.js @@ -1,152 +1,179 @@ // @flow import { getRustAPI } from 'rust-node-addon'; import type { LogOutResponse } from 'lib/types/account-types.js'; import type { ReservedUsernameMessage } from 'lib/types/crypto-types.js'; import { updateTypes } from 'lib/types/update-types-enum.js'; import type { UserInfo } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { values } from 'lib/utils/objects.js'; import { promiseAll, ignorePromiseRejections } from 'lib/utils/promises.js'; import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; import { fetchKnownUserInfos, fetchUsername, } from '../fetchers/user-fetchers.js'; import { rescindPushNotifs } from '../push/rescind.js'; +import { removeBlobHolders } from '../services/blob.js'; import { createNewAnonymousCookie } from '../session/cookies.js'; import type { Viewer, AnonymousViewerData } from '../session/viewer.js'; import { fetchOlmAccount } from '../updaters/olm-account-updater.js'; +import { blobHoldersFromUploadRows } from '../uploads/media-utils.js'; + +async function deleteUploadsForUser(deletedUserID: string): Promise { + try { + const [holderRows] = await dbQuery(SQL` + SELECT extra + FROM uploads + WHERE user_container = ${deletedUserID} + `); + const blobHolders = blobHoldersFromUploadRows(holderRows); + await removeBlobHolders(blobHolders); + await dbQuery(SQL` + DELETE u, i + FROM uploads u + LEFT JOIN ids i on i.id = u.id + WHERE u.user_container = ${deletedUserID}; + `); + } catch (err) { + // unassign uploads so the deletion will be retried + // by the `deleteUnassignedUploads()` + await dbQuery(SQL` + UPDATE uploads + SET user_container = NULL + WHERE user_container = ${deletedUserID}; + `); + throw err; + } +} async function deleteAccount(viewer: Viewer): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const deletedUserID = viewer.userID; await rescindPushNotifs(SQL`n.user = ${deletedUserID}`, SQL`NULL`); const knownUserInfos = await fetchKnownUserInfos(viewer); const usersToUpdate: $ReadOnlyArray = values(knownUserInfos).filter( (user: UserInfo): boolean => user.id !== deletedUserID, ); + ignorePromiseRejections(deleteUploadsForUser(deletedUserID)); + // TODO: if this results in any orphaned orgs, convert them to chats const deletionQuery = SQL` START TRANSACTION; DELETE FROM users WHERE id = ${deletedUserID}; DELETE FROM ids WHERE id = ${deletedUserID}; DELETE c, i FROM cookies c LEFT JOIN ids i ON i.id = c.id WHERE c.user = ${deletedUserID}; DELETE FROM memberships WHERE user = ${deletedUserID}; DELETE FROM focused WHERE user = ${deletedUserID}; DELETE n, i FROM notifications n LEFT JOIN ids i ON i.id = n.id WHERE n.user = ${deletedUserID}; DELETE u, i FROM updates u LEFT JOIN ids i ON i.id = u.id WHERE u.user = ${deletedUserID}; DELETE s, i FROM sessions s LEFT JOIN ids i ON i.id = s.id WHERE s.user = ${deletedUserID}; DELETE r, i FROM reports r LEFT JOIN ids i ON i.id = r.id WHERE r.user = ${deletedUserID}; - DELETE u, i - FROM uploads u - LEFT JOIN ids i on i.id = u.id - WHERE u.user_container = ${deletedUserID}; DELETE FROM relationships_undirected WHERE user1 = ${deletedUserID}; DELETE FROM relationships_undirected WHERE user2 = ${deletedUserID}; DELETE FROM relationships_directed WHERE user1 = ${deletedUserID}; DELETE FROM relationships_directed WHERE user2 = ${deletedUserID}; COMMIT; `; const deletionPromise = dbQuery(deletionQuery, { multipleStatements: true }); const anonymousViewerDataPromise: Promise = (async () => { if (viewer.isScriptViewer) { return undefined; } return await createNewAnonymousCookie({ platformDetails: viewer.platformDetails, deviceToken: viewer.deviceToken, }); })(); const usernamePromise = fetchUsername(deletedUserID); const { anonymousViewerData, username } = await promiseAll({ anonymousViewerData: anonymousViewerDataPromise, username: usernamePromise, deletion: deletionPromise, }); if (username) { const issuedAt = new Date().toISOString(); const reservedUsernameMessage: ReservedUsernameMessage = { statement: 'Remove the following username from reserved list', payload: username, issuedAt, }; const message = JSON.stringify(reservedUsernameMessage); ignorePromiseRejections( (async () => { const rustAPI = await getRustAPI(); const accountInfo = await fetchOlmAccount('content'); const signature = accountInfo.account.sign(message); await rustAPI.removeReservedUsername(message, signature); })(), ); } if (anonymousViewerData) { viewer.setNewCookie(anonymousViewerData); } const deletionUpdatesPromise = createAccountDeletionUpdates( usersToUpdate, deletedUserID, ); if (viewer.isScriptViewer) { await deletionUpdatesPromise; } else { ignorePromiseRejections(deletionUpdatesPromise); } if (viewer.isScriptViewer) { return null; } return { currentUserInfo: { anonymous: true, }, }; } async function createAccountDeletionUpdates( knownUserInfos: $ReadOnlyArray, deletedUserID: string, ): Promise { const time = Date.now(); const updateDatas = []; for (const userInfo of knownUserInfos) { const { id: userID } = userInfo; updateDatas.push({ type: updateTypes.DELETE_ACCOUNT, userID, time, deletedUserID, }); } await createUpdates(updateDatas); } export { deleteAccount };