diff --git a/keyserver/src/cron/cron.js b/keyserver/src/cron/cron.js index e27227595..1ad4bb55d 100644 --- a/keyserver/src/cron/cron.js +++ b/keyserver/src/cron/cron.js @@ -1,166 +1,168 @@ // @flow import type { Account as OlmAccount } from '@commapp/olm'; import cluster from 'cluster'; import schedule from 'node-schedule'; import { backupDB } from './backups.js'; import { createDailyUpdatesThread } from './daily-updates.js'; import { postLeaderboard } from './phab-leaderboard.js'; import { updateAndReloadGeoipDB } from './update-geoip-db.js'; import { updateIdentityReservedUsernames } from './update-identity-reserved-usernames.js'; import { deleteOrphanedActivity } from '../deleters/activity-deleters.js'; import { deleteExpiredCookies } from '../deleters/cookie-deleters.js'; import { deleteOrphanedDays } from '../deleters/day-deleters.js'; import { deleteOrphanedEntries } from '../deleters/entry-deleters.js'; +import { deleteOrphanedInviteLinks } from '../deleters/link-deleters.js'; import { deleteOrphanedMemberships } from '../deleters/membership-deleters.js'; import { deleteOrphanedMessages } from '../deleters/message-deleters.js'; import { deleteOrphanedNotifs } from '../deleters/notif-deleters.js'; import { deleteOrphanedRevisions } from '../deleters/revision-deleters.js'; import { deleteOrphanedRoles } from '../deleters/role-deleters.js'; import { deleteOrphanedSessions, deleteOldWebSessions, } from '../deleters/session-deleters.js'; import { deleteStaleSIWENonceEntries } from '../deleters/siwe-nonce-deleters.js'; import { deleteInaccessibleThreads } from '../deleters/thread-deleters.js'; import { deleteExpiredUpdates } from '../deleters/update-deleters.js'; import { deleteUnassignedUploads } from '../deleters/upload-deleters.js'; import { fetchCallUpdateOlmAccount } from '../updaters/olm-account-updater.js'; import { validateAndUploadAccountPrekeys } from '../utils/olm-utils.js'; import { isPrimaryNode, isAuxiliaryNode, } from '../utils/primary-secondary-utils.js'; import { synchronizeInviteLinksWithBlobs } from '../utils/synchronize-invite-links-with-blobs.js'; const { RUN_COMM_TEAM_DEV_SCRIPTS } = process.env; if (cluster.isMaster) { schedule.scheduleJob( '0 3 ? * 0', // every Sunday at 3:00 AM in the keyserver's timezone async () => { try { await updateAndReloadGeoipDB(); } catch (e) { console.warn( 'encountered error while trying to update GeoIP database', e, ); } }, ); if (isPrimaryNode) { schedule.scheduleJob( '30 3 * * *', // every day at 3:30 AM in the keyserver's timezone async () => { try { // Do everything one at a time to reduce load since we're in no hurry, // and since some queries depend on previous ones. await deleteExpiredCookies(); await deleteInaccessibleThreads(); await deleteOrphanedMemberships(); await deleteOrphanedDays(); await deleteOrphanedEntries(); await deleteOrphanedRevisions(); await deleteOrphanedRoles(); await deleteOrphanedMessages(); await deleteOrphanedActivity(); await deleteOrphanedNotifs(); await deleteOrphanedSessions(); await deleteOldWebSessions(); await deleteExpiredUpdates(); await deleteUnassignedUploads(); await deleteStaleSIWENonceEntries(); + await deleteOrphanedInviteLinks(); } catch (e) { console.warn('encountered error while trying to clean database', e); } }, ); schedule.scheduleJob( '0 5 * * *', // every day at 5:00 AM in the keyserver's timezone async () => { try { await updateIdentityReservedUsernames(); } catch (e) { console.warn( 'encountered error while trying to update reserved usernames on ' + 'identity service', e, ); } }, ); schedule.scheduleJob( '0 0 * * *', // every day at midnight in the keyserver's timezone async () => { try { await fetchCallUpdateOlmAccount( 'content', (contentAccount: OlmAccount) => fetchCallUpdateOlmAccount( 'notifications', (notifAccount: OlmAccount) => validateAndUploadAccountPrekeys(contentAccount, notifAccount), ), ); } catch (e) { console.warn('encountered error while trying to validate prekeys', e); } }, ); schedule.scheduleJob( '0 2 * * *', // every day at 2:00 AM in the keyserver's timezone async () => { try { await synchronizeInviteLinksWithBlobs(); } catch (e) { console.warn( 'encountered an error while trying to synchronize invite links with blobs', e, ); } }, ); } if (isPrimaryNode || isAuxiliaryNode) { schedule.scheduleJob( '0 */4 * * *', // every four hours async () => { try { await backupDB(); } catch (e) { console.warn('encountered error while trying to backup database', e); } }, ); } if (RUN_COMM_TEAM_DEV_SCRIPTS && (isPrimaryNode || isAuxiliaryNode)) { schedule.scheduleJob( '0 0 * * *', // every day at midnight in the keyserver's timezone async () => { try { await createDailyUpdatesThread(); } catch (e) { console.warn( 'encountered error while trying to create daily updates thread', e, ); } }, ); schedule.scheduleJob( '0 0 8 * *', // 8th of every month at midnight in the keyserver's timezone async () => { try { await postLeaderboard(); } catch (e) { console.warn( 'encountered error while trying to post Phabricator leaderboard', e, ); } }, ); } } diff --git a/keyserver/src/deleters/link-deleters.js b/keyserver/src/deleters/link-deleters.js index 0c7b58d6c..b575ffb86 100644 --- a/keyserver/src/deleters/link-deleters.js +++ b/keyserver/src/deleters/link-deleters.js @@ -1,71 +1,87 @@ // @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]); } -export { deleteInviteLink }; +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 };