diff --git a/src/frontend/utils/conflicts.js b/src/frontend/utils/conflicts.js deleted file mode 100644 index 560d62d9..00000000 --- a/src/frontend/utils/conflicts.js +++ /dev/null @@ -1,133 +0,0 @@ -import flatten from 'lodash/flatten'; -import pick from 'lodash/pick'; -import uniq from 'lodash/uniq'; -import { INDEXED_PREPRINT_PROPS } from '../constants'; -import { getId, cleanup, arrayify } from '../utils/jsonld'; -import { getUniqueModerationActions } from '../utils/actions'; - -export function mergePreprintConflicts(docs) { - return docs.reduce((merged, doc) => { - // score: latest wins - if ( - new Date(merged.dateScoreLastUpdated).getTime() < - new Date(doc.dateScoreLastUpdated).getTime() - ) { - merged.dateScoreLastUpdated = doc.dateScoreLastUpdated; - merged.score = doc.score; - } - - // indexed preprint props: higher number of `sdRetrievedFields` wins and - // if equal, latest `sdDateRetrieved` wins - if ( - merged.sdRetrievedFields.length < doc.sdRetrievedFields.length || - (merged.sdRetrievedFields.length === doc.sdRetrievedFields.length && - new Date(merged.sdDateRetrieved).getTime() < - new Date(doc.sdDateRetrieved).getTime()) - ) { - INDEXED_PREPRINT_PROPS.forEach(p => { - merged[p] = doc[p]; - }); - merged.sdRetrievedFields = doc.sdRetrievedFields; - merged.sdDateRetrieved = doc.sdDateRetrieved; - } - - // `potentialAction`: we merge all distincts taking care to always merge - // the `moderationAction` - if (doc.potentialAction) { - merged.potentialAction = arrayify(merged.potentialAction) - .map(potentialAction => { - const _potentialAction = arrayify(doc.potentialAction).find( - _potentialAction => - getId(_potentialAction) === getId(potentialAction), - ); - - if (_potentialAction) { - return cleanup( - Object.assign({}, potentialAction, { - moderationAction: getUniqueModerationActions( - arrayify(potentialAction.moderationAction).concat( - arrayify(_potentialAction.moderationAction), - ), - ), - }), - { removeEmptyArray: true }, - ); - } else { - return potentialAction; - } - }) - .concat( - arrayify(doc.potentialAction).filter(_potentialAction => { - return !arrayify(merged.potentialAction).some( - potentialAction => - getId(potentialAction) === getId(_potentialAction), - ); - }), - ); - } - - return merged; - }, Object.assign({}, docs[0])); -} - -export function mergeReviewActionConflicts(docs) { - // Note that the reviewAction are all identical aside from the - // `moderationAction` list so we take the first one and just focus on merging - // `moderationAction` - return cleanup( - Object.assign({}, docs[0], { - moderationAction: getUniqueModerationActions( - flatten(docs.map(doc => arrayify(doc.moderationAction))), - ), - }), - { removeEmptyArray: true }, - ); -} - -export function mergeUserConflicts(docs) { - const specialProps = ['token', 'apiKey', 'hasRole', '_rev']; // those need special logic to be merged - - return docs.reduce((merged, doc) => { - // all props but `specialProps` (latest wins) - if ( - new Date(merged.dateModified).getTime() < - new Date(doc.dateModified).getTime() - ) { - merged = Object.assign( - pick(merged, specialProps), - pick(doc, Object.keys(doc).filter(p => !specialProps.includes(p))), - ); - } - - // `token`: latest wins - if ( - (!merged.token && doc.token) || - (merged.token && - doc.token && - new Date(merged.token.dateCreated).getTime() < - new Date(doc.token.dateCreated).getTime()) - ) { - merged.token = doc.token; - } - - // `apiKey`: latest wins - if ( - (!merged.apiKey && doc.apiKey) || - (merged.apiKey && - doc.apiKey && - new Date(merged.apiKey.dateCreated).getTime() < - new Date(doc.apiKey.dateCreated).getTime()) - ) { - merged.apiKey = doc.apiKey; - } - - // `hasRole` - // We can NOT lose role @ids => we ALWAYS merge - // Later we can sameAs in case the user reconciles - merged.hasRole = uniq( - arrayify(merged.hasRole).concat(arrayify(doc.hasRole)), - ); - - return merged; - }, Object.assign({}, docs[0])); -} diff --git a/src/frontend/utils/email.js b/src/frontend/utils/email.js deleted file mode 100644 index 49dd5967..00000000 --- a/src/frontend/utils/email.js +++ /dev/null @@ -1,216 +0,0 @@ -import email from '@sendgrid/mail'; -import uniq from 'lodash/uniq'; -import { getId, unprefix } from '../utils/jsonld'; -import { - ORG, - SENDER_EMAIL_HREF, - CONTACT_EMAIL_HREF, - PRODUCTION_DOMAIN, -} from '../constants'; - -export function createSendgridEmailClient(config = {}) { - if (config.sendgridApiKey === null) { - return null; - } - const apiKey = config.sendgridApiKey || process.env.SENDGRID_API_KEY; - - if (apiKey) { - email.setApiKey(apiKey); - return email; - } - - return null; -} - -export async function createEmailMessages( - { db }, - action, // result from db.post (so `action.result` is defined) -) { - const messages = []; - - switch (action['@type']) { - case 'RegisterAction': - if (action.isFirstTimeLogin) { - const roles = action.resultRole; - const publicRole = roles.find( - role => role['@type'] === 'PublicReviewerRole', - ); - const anonRole = roles.find( - role => role['@type'] === 'AnonymousReviewerRole', - ); - - messages.push({ - from: unprefix(SENDER_EMAIL_HREF), - to: unprefix(CONTACT_EMAIL_HREF), - subject: 'New user registration', - text: `Hello, - -A new user just registered. - -- public profile: ${PRODUCTION_DOMAIN}/about/${unprefix(getId(publicRole))} -- anonymous profile: ${PRODUCTION_DOMAIN}/about/${unprefix(getId(anonRole))} - -Have a good day! - `, - }); - } - break; - - case 'ReportRapidPREreviewAction': { - const reviewAction = action.result; - - messages.push({ - from: unprefix(SENDER_EMAIL_HREF), - to: unprefix(CONTACT_EMAIL_HREF), - subject: 'New moderation report', - text: `Hello, - -A new moderation report was just posted. - -- reported by: ${PRODUCTION_DOMAIN}/about/${unprefix(getId(action.agent))} -- about: - - title: ${reviewAction.object.name} - - url: ${PRODUCTION_DOMAIN}/${unprefix( - getId(reviewAction.object.doi || reviewAction.object.arXivId), - )}?role=${unprefix(getId(reviewAction.agent))} - -You can act on it from your moderation panel: ${PRODUCTION_DOMAIN}/moderate - -Have a good day! - `, - }); - break; - } - - case 'RapidPREreviewAction': { - // admin message - messages.push({ - from: unprefix(SENDER_EMAIL_HREF), - to: unprefix(CONTACT_EMAIL_HREF), - subject: `New Rapid PREreview for "${action.object.name}"`, - text: `Hello, - -A new Rapid PREreview was just posted. - -- posted by: ${PRODUCTION_DOMAIN}/about/${unprefix(getId(action.agent))} -- about: - - title: ${action.object.name} - - url: ${PRODUCTION_DOMAIN}/${unprefix( - getId(action.object.doi || action.object.arXivId), - )} - -Have a good day! - `, - }); - - // requesters message - const requests = await db.getRequestsByPreprintId(getId(action.object)); - const roleIds = requests - .map(request => getId(request.agent)) - .filter(roleId => roleId && roleId !== getId(action.agent)); - - const users = await db.getUsersByRoleIds(roleIds); - - const to = uniq( - users - .filter( - user => - user.contactPoint && - user.contactPoint.email && - user.contactPoint.dateVerified && - user.contactPoint.active, - ) - .map(user => unprefix(user.contactPoint.email)), - ); - - if (to.length) { - messages.push({ - from: unprefix(SENDER_EMAIL_HREF), - to, - subject: `New Rapid PREreview for "${action.object.name}"`, - text: `Hello, - -A new Rapid PREreview was just posted for a preprint for which you requested reviews (${ - action.object.name - }). - -You can view: -- the review by visiting ${PRODUCTION_DOMAIN}/${unprefix( - getId(action.object.doi || action.object.arXivId), - )}?role=${unprefix(getId(action.agent))}. -- the reviewer profile by visiting ${PRODUCTION_DOMAIN}/about/${unprefix( - getId(action.agent), - )} - -Have a good day! - -PS: You can turn off those notifications from the settings page: ${PRODUCTION_DOMAIN}/settings. - `, - isMultiple: !!(to.length > 1), - }); - } - - break; - } - - case 'RequestForRapidPREreviewAction': { - messages.push({ - from: unprefix(SENDER_EMAIL_HREF), - to: unprefix(CONTACT_EMAIL_HREF), - subject: `New Request for Rapid PREreview for "${action.object.name}"`, - text: `Hello, - -A new Request for Rapid PREreview was just posted. - -- posted by: ${PRODUCTION_DOMAIN}/about/${unprefix(getId(action.agent))} -- about: - - title: ${action.object.name} - - url: ${PRODUCTION_DOMAIN}/${unprefix( - getId(action.object.doi || action.object.arXivId), - )} - -Have a good day! - `, - }); - break; - } - - case 'UpdateContactPointAction': { - // if email address was updated - if ( - action.result.contactPoint && - action.result.contactPoint.email && - action.result.contactPoint.token && - action.result.contactPoint.token['@type'] === - 'ContactPointVerificationToken' && - action.result.contactPoint.token.value && - action.result.contactPoint.token.dateCreated === - action.result.dateModified - ) { - messages.push({ - from: unprefix(SENDER_EMAIL_HREF), - to: unprefix(action.result.contactPoint.email), - subject: `Verify your email address for ${ORG}`, - text: `Hello, - -We need to verify that this email address belongs to you and that you want to use it to receive notifications from ${ORG}. - -If that is the case, please click on the following link: ${PRODUCTION_DOMAIN}/api/verify?token=${ - action.result.contactPoint.token.value - }. - -Otherwise, you can ignore this email. - -Have a good day! - `, - }); - } - break; - } - - default: - break; - } - - return messages; -} diff --git a/src/frontend/utils/get-bundle-paths.js b/src/frontend/utils/get-bundle-paths.js deleted file mode 100644 index 31b84e2e..00000000 --- a/src/frontend/utils/get-bundle-paths.js +++ /dev/null @@ -1,42 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -const manifestPath = path.resolve( - __dirname, - '../../public/assets/bundle-manifest.json', -); - -let lastSeen = 0; -let cache = {}; -let retries = 10; - -export default function getBundlePaths(cb) { - fs.stat(manifestPath, (err, info) => { - if (err) { - if (err.code === 'ENOENT') { - if (!retries) return cb(err); - retries--; - return setTimeout(() => getBundlePaths(cb), 5000); - } - return cb(err); - } - - // !! info.mtime.getTime() can be 0 on AWS - if ( - info.mtime.getTime() > lastSeen || - info.birthtime.getTime() > lastSeen - ) { - return fs.readFile(manifestPath, 'utf8', (err, data) => { - if (err) return cb(err); - try { - lastSeen = info.mtime.getTime(); - cache = JSON.parse(data); - } catch (err) { - return cb(err); - } - cb(null, cache); - }); - } - cb(null, cache); - }); -} diff --git a/src/frontend/utils/redis.js b/src/frontend/utils/redis.js deleted file mode 100644 index 3b8b70a7..00000000 --- a/src/frontend/utils/redis.js +++ /dev/null @@ -1,28 +0,0 @@ -import redis from 'redis'; - -export function createRedisClient(config = {}) { - const opts = { - host: config.redisHost || process.env['REDIS_HOST'] || '127.0.0.1', - port: config.redisPort || process.env['REDIS_PORT'] || 6379, - }; - - const redisPrefix = config.redisPrefix || 'rpos:'; - - opts.prefix = redisPrefix; - - const redisPassword = config.redisPassword || process.env['REDIS_PASSWORD']; - if (redisPassword) { - opts.pass = redisPassword; // for compatibility with RedisStore (used in the session middleware) - opts.auth_pass = redisPassword; // for compatibility with Azure - opts.password = redisPassword; - } - - // See https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-nodejs-get-started - if (opts.port.toString() === '6380') { - opts.tls = { - servername: opts.host, - }; - } - - return redis.createClient(opts); -} diff --git a/src/frontend/utils/score.js b/src/frontend/utils/score.js deleted file mode 100644 index 53ebb6aa..00000000 --- a/src/frontend/utils/score.js +++ /dev/null @@ -1,71 +0,0 @@ -import { arrayify } from './jsonld'; - -export const SCORE_THRESHOLD = 1e-5; - -/** - * See https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d - */ -export function getScore( - actions, // list of `RapidPREreviewAction` or `RequestForRapidPREreviewAction` - { now = new Date().toISOString() } = {}, - { - g = 1.8, // gravity factor - threshold = SCORE_THRESHOLD, // if a score is below `threshold` with set it to 0 - } = {}, -) { - // sort by date posted (`startTime`) - actions = arrayify(actions).sort((a, b) => { - return new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); - }); - - if (!actions.length) { - return 0; - } - - const dateTimeFirstActivity = actions[0].startTime; - const timeSinceFirstActivityHours = - Math.max( - new Date(now).getTime() - new Date(dateTimeFirstActivity).getTime(), - 0, - ) / - (1000 * 60 * 60); - - const nReviews = actions.reduce((n, action) => { - if (action['@type'] === 'RapidPREreviewAction') { - n++; - } - return n; - }, 0); - - const nRequests = actions.reduce((n, action) => { - if (action['@type'] === 'RequestForRapidPREreviewAction') { - n++; - } - return n; - }, 0); - - const score = - (nReviews + nRequests) / Math.pow(timeSinceFirstActivityHours + 1, g); - - return score <= threshold ? 0 : score; -} - -/** - * Used to display the filling of the hourglass visualization in the score badge - * Returned value must be between 0 and 1 - */ -export function getTimeScore( - dateTimeFirstActivity, - now = new Date().toISOString(), -) { - const timeSinceFirstActivityHours = - Math.max( - new Date(now).getTime() - new Date(dateTimeFirstActivity).getTime(), - 0, - ) / - (1000 * 60 * 60); - - const timeScore = 1 / Math.pow(timeSinceFirstActivityHours + 1, 0.5); - - return timeScore; -} diff --git a/src/frontend/utils/set-interval-async.js b/src/frontend/utils/set-interval-async.js deleted file mode 100644 index 90dbd0bb..00000000 --- a/src/frontend/utils/set-interval-async.js +++ /dev/null @@ -1,19 +0,0 @@ -import noop from 'lodash/noop'; - -export function setIntervalAsync(f, delay, onError = noop, intervalId = {}) { - f() - .catch(err => { - onError(err); - }) - .then(() => { - intervalId.timeoutId = setTimeout(() => { - setIntervalAsync(f, delay, onError, intervalId); - }, delay); - }); - - return intervalId; -} - -export function clearIntervalAsync(intervalId) { - clearTimeout(intervalId.timeoutId); -} diff --git a/src/frontend/utils/striptags.js b/src/frontend/utils/striptags.js deleted file mode 100644 index 7f400512..00000000 --- a/src/frontend/utils/striptags.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Note: this must be written is ES5 to be used in Cloudant search - */ -export default function striptags(html) { - var STATE_OUTPUT = 0, - STATE_HTML = 1, - STATE_PRE_COMMENT = 2, - STATE_COMMENT = 3; - if (!html) html = ''; - var state = STATE_OUTPUT, - depth = 0, - output = '', - inQuote = false, - i, - length, - c; - for (i = 0, length = html.length; i < length; i++) { - c = html[i]; - switch (c) { - case '<': { - // ignore '<' if inside a quote - if (inQuote) { - break; - } - // '<' followed by a space is not a valid tag, continue - if (html[i + 1] == ' ') { - consumeCharacter(c); - break; - } - // change to STATE_HTML - if (state == STATE_OUTPUT) { - state = STATE_HTML; - consumeCharacter(c); - break; - } - // ignore additional '<' characters when inside a tag - if (state == STATE_HTML) { - depth++; - break; - } - consumeCharacter(c); - break; - } - case '>': { - // something like this is happening: '<<>>' - if (depth) { - depth--; - break; - } - // ignore '>' if inside a quote - if (inQuote) { - break; - } - // an HTML tag was closed - if (state == STATE_HTML) { - inQuote = state = 0; - break; - } - // '' - if (state == STATE_PRE_COMMENT) { - inQuote = state = 0; - break; - } - // if last two characters were '--', then end comment - if ( - state == STATE_COMMENT && - html[i - 1] == '-' && - html[i - 2] == '-' - ) { - inQuote = state = 0; - break; - } - consumeCharacter(c); - break; - } - // catch both single and double quotes - case '"': - case "'": { - if (state == STATE_HTML) { - if (inQuote == c) { - // end quote found - inQuote = false; - } else if (!inQuote) { - // start quote only if not already in one - inQuote = c; - } - } - consumeCharacter(c); - break; - } - case '!': { - if (state == STATE_HTML && html[i - 1] == '<') { - // looks like we might be starting a comment - state = STATE_PRE_COMMENT; - break; - } - consumeCharacter(c); - break; - } - case '-': { - // if the previous two characters were '!-', this is a comment - if ( - state == STATE_PRE_COMMENT && - html[i - 1] == '-' && - html[i - 2] == '!' - ) { - state = STATE_COMMENT; - break; - } - consumeCharacter(c); - break; - } - case 'E': - case 'e': { - // check for DOCTYPE, because it looks like a comment and isn't - if ( - state == STATE_PRE_COMMENT && - html.substr(i - 6, 7).toLowerCase() == 'doctype' - ) { - state = STATE_HTML; - break; - } - consumeCharacter(c); - break; - } - default: { - consumeCharacter(c); - } - } - } - function consumeCharacter(c) { - if (state == STATE_OUTPUT) { - output += c; - } - } - return output; -}