const { callbackify } = require('util') const request = require('request-promise-native') const settings = require('@overleaf/settings') const OError = require('@overleaf/o-error') const UserGetter = require('../User/UserGetter') module.exports = { initializeProject: callbackify(initializeProject), flushProject: callbackify(flushProject), resyncProject: callbackify(resyncProject), deleteProject: callbackify(deleteProject), injectUserDetails: callbackify(injectUserDetails), promises: { initializeProject, flushProject, resyncProject, deleteProject, injectUserDetails, }, } async function initializeProject() { if ( !( settings.apis.project_history && settings.apis.project_history.initializeHistoryForNewProjects ) ) { return } try { const body = await request.post({ url: `${settings.apis.project_history.url}/project`, }) const project = JSON.parse(body) const overleafId = project && project.project && project.project.id if (!overleafId) { throw new Error('project-history did not provide an id', project) } return { overleaf_id: overleafId } } catch (err) { throw OError.tag(err, 'failed to initialize project history') } } async function flushProject(projectId) { try { await request.post({ url: `${settings.apis.project_history.url}/project/${projectId}/flush`, }) } catch (err) { throw OError.tag(err, 'failed to flush project to project history', { projectId, }) } } async function resyncProject(projectId) { try { await request.post({ url: `${settings.apis.project_history.url}/project/${projectId}/resync`, }) } catch (err) { throw OError.tag(err, 'failed to resync project history', { projectId }) } } async function deleteProject(projectId, historyId) { try { const tasks = [ request.delete( `${settings.apis.project_history.url}/project/${projectId}` ), ] if (historyId != null) { tasks.push( request.delete({ url: `${settings.apis.v1_history.url}/projects/${historyId}`, auth: { user: settings.apis.v1_history.user, pass: settings.apis.v1_history.pass, }, }) ) } await Promise.all(tasks) } catch (err) { throw OError.tag(err, 'failed to clear project history', { projectId, historyId, }) } } async function injectUserDetails(data) { // data can be either: // { // diff: [{ // i: "foo", // meta: { // users: ["user_id", v1_user_id, ...] // ... // } // }, ...] // } // or // { // updates: [{ // pathnames: ["main.tex"] // meta: { // users: ["user_id", v1_user_id, ...] // ... // }, // ... // }, ...] // } // Either way, the top level key points to an array of objects with a meta.users property // that we need to replace user_ids with populated user objects. // Note that some entries in the users arrays may be v1 ids returned by the v1 history // service. v1 ids will be `numbers` let userIds = new Set() let v1UserIds = new Set() const entries = Array.isArray(data.diff) ? data.diff : Array.isArray(data.updates) ? data.updates : [] for (const entry of entries) { for (const user of (entry.meta && entry.meta.users) || []) { if (typeof user === 'string') { userIds.add(user) } else if (typeof user === 'number') { v1UserIds.add(user) } } } userIds = Array.from(userIds) v1UserIds = Array.from(v1UserIds) const projection = { first_name: 1, last_name: 1, email: 1 } const usersArray = await UserGetter.promises.getUsers(userIds, projection) const users = {} for (const user of usersArray) { users[user._id.toString()] = _userView(user) } projection.overleaf = 1 const v1IdentifiedUsersArray = await UserGetter.promises.getUsersByV1Ids( v1UserIds, projection ) for (const user of v1IdentifiedUsersArray) { users[user.overleaf.id] = _userView(user) } for (const entry of entries) { if (entry.meta != null) { entry.meta.users = ((entry.meta && entry.meta.users) || []).map(user => { if (typeof user === 'string' || typeof user === 'number') { return users[user] } else { return user } }) } } return data } function _userView(user) { const { _id, first_name: firstName, last_name: lastName, email } = user return { first_name: firstName, last_name: lastName, email, id: _id } }