mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-29 12:01:32 +02:00
[web] Add a new "Overleaf Labs" program, for Galileo GitOrigin-RevId: 8f6c79c37c1719a59bd8405998cc3de2fd29960d
261 lines
7.0 KiB
JavaScript
261 lines
7.0 KiB
JavaScript
const logger = require('@overleaf/logger')
|
|
const Settings = require('@overleaf/settings')
|
|
const crypto = require('crypto')
|
|
const Mailchimp = require('mailchimp-api-v3')
|
|
const OError = require('@overleaf/o-error')
|
|
const { callbackify } = require('util')
|
|
|
|
function mailchimpIsConfigured() {
|
|
return Settings.mailchimp != null && Settings.mailchimp.api_key != null
|
|
}
|
|
|
|
function make(listName, listId) {
|
|
let provider
|
|
if (!mailchimpIsConfigured() || !listId) {
|
|
logger.debug({ listName }, 'Using newsletter provider: none')
|
|
provider = makeNullProvider(listName)
|
|
} else {
|
|
logger.debug({ listName }, 'Using newsletter provider: mailchimp')
|
|
provider = makeMailchimpProvider(listName, listId)
|
|
}
|
|
return {
|
|
subscribed: callbackify(provider.subscribed),
|
|
subscribe: callbackify(provider.subscribe),
|
|
unsubscribe: callbackify(provider.unsubscribe),
|
|
changeEmail: callbackify(provider.changeEmail),
|
|
promises: provider,
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
make,
|
|
}
|
|
|
|
class NonFatalEmailUpdateError extends OError {
|
|
constructor(message, oldEmail, newEmail) {
|
|
super(message, { oldEmail, newEmail })
|
|
}
|
|
}
|
|
|
|
function makeMailchimpProvider(listName, listId) {
|
|
const mailchimp = new Mailchimp(Settings.mailchimp.api_key)
|
|
const MAILCHIMP_LIST_ID = listId
|
|
|
|
return {
|
|
subscribed,
|
|
subscribe,
|
|
unsubscribe,
|
|
changeEmail,
|
|
}
|
|
|
|
async function subscribed(user) {
|
|
try {
|
|
const path = getSubscriberPath(user.email)
|
|
const result = await mailchimp.get(path)
|
|
return result?.status === 'subscribed'
|
|
} catch (err) {
|
|
if (err.status === 404) {
|
|
return false
|
|
}
|
|
throw OError.tag(err, 'error getting newsletter subscriptions status', {
|
|
userId: user._id,
|
|
listName,
|
|
})
|
|
}
|
|
}
|
|
|
|
async function subscribe(user) {
|
|
try {
|
|
const path = getSubscriberPath(user.email)
|
|
await mailchimp.put(path, {
|
|
email_address: user.email,
|
|
status: 'subscribed',
|
|
status_if_new: 'subscribed',
|
|
merge_fields: getMergeFields(user),
|
|
})
|
|
logger.debug(
|
|
{ user, listName },
|
|
'finished subscribing user to newsletter'
|
|
)
|
|
} catch (err) {
|
|
throw OError.tag(err, 'error subscribing user to newsletter', {
|
|
userId: user._id,
|
|
listName,
|
|
})
|
|
}
|
|
}
|
|
|
|
async function unsubscribe(user, options = {}) {
|
|
try {
|
|
const path = getSubscriberPath(user.email)
|
|
if (options.delete) {
|
|
await mailchimp.delete(path)
|
|
} else {
|
|
await mailchimp.patch(path, {
|
|
status: 'unsubscribed',
|
|
merge_fields: getMergeFields(user),
|
|
})
|
|
}
|
|
logger.debug(
|
|
{ user, options, listName },
|
|
'finished unsubscribing user from newsletter'
|
|
)
|
|
} catch (err) {
|
|
if (err.status === 404 || err.status === 405) {
|
|
// silently ignore users who were never subscribed (404) or previously deleted (405)
|
|
return
|
|
}
|
|
|
|
if (err.message.includes('looks fake or invalid')) {
|
|
logger.debug(
|
|
{ err, user, options, listName },
|
|
'Mailchimp declined to unsubscribe user because it finds the email looks fake'
|
|
)
|
|
return
|
|
}
|
|
|
|
throw OError.tag(err, 'error unsubscribing user from newsletter', {
|
|
userId: user._id,
|
|
listName,
|
|
})
|
|
}
|
|
}
|
|
|
|
async function changeEmail(user, newEmail) {
|
|
const oldEmail = user.email
|
|
|
|
try {
|
|
await updateEmailInMailchimp(user, newEmail)
|
|
} catch (updateError) {
|
|
// if we failed to update the user, delete their old email address so that
|
|
// we don't leave it stuck in mailchimp
|
|
logger.debug(
|
|
{ oldEmail, newEmail, updateError, listName },
|
|
'unable to change email in newsletter, removing old mail'
|
|
)
|
|
|
|
try {
|
|
await unsubscribe(user, { delete: true })
|
|
} catch (unsubscribeError) {
|
|
// something went wrong removing the user's address
|
|
throw OError.tag(
|
|
unsubscribeError,
|
|
'error unsubscribing old email in response to email change failure',
|
|
{ oldEmail, newEmail, updateError, listName }
|
|
)
|
|
}
|
|
|
|
if (!(updateError instanceof NonFatalEmailUpdateError)) {
|
|
throw updateError
|
|
}
|
|
}
|
|
}
|
|
|
|
async function updateEmailInMailchimp(user, newEmail) {
|
|
const oldEmail = user.email
|
|
|
|
// mailchimp doesn't give us error codes, so we have to parse the message :'(
|
|
const errors = {
|
|
'merge fields were invalid': 'user has never subscribed',
|
|
'could not be validated':
|
|
'user has previously unsubscribed or new email already exist on list',
|
|
'is already a list member': 'new email is already on mailing list',
|
|
'looks fake or invalid': 'mail looks fake to mailchimp',
|
|
}
|
|
|
|
try {
|
|
const path = getSubscriberPath(oldEmail)
|
|
await mailchimp.patch(path, {
|
|
email_address: newEmail,
|
|
merge_fields: getMergeFields(user),
|
|
})
|
|
logger.debug(
|
|
{ newEmail, listName },
|
|
'finished changing email in the newsletter'
|
|
)
|
|
} catch (err) {
|
|
// silently ignore users who were never subscribed
|
|
if (err.status === 404) {
|
|
return
|
|
}
|
|
|
|
// look through expected mailchimp errors and log if we find one
|
|
Object.keys(errors).forEach(key => {
|
|
if (err.message.includes(key)) {
|
|
const message = `unable to change email in newsletter, ${errors[key]}`
|
|
|
|
logger.debug({ oldEmail, newEmail, listName }, message)
|
|
|
|
throw new NonFatalEmailUpdateError(
|
|
message,
|
|
oldEmail,
|
|
newEmail
|
|
).withCause(err)
|
|
}
|
|
})
|
|
|
|
// if we didn't find an expected error, generate something to throw
|
|
throw OError.tag(err, 'error changing email in newsletter', {
|
|
oldEmail,
|
|
newEmail,
|
|
listName,
|
|
})
|
|
}
|
|
}
|
|
|
|
function getSubscriberPath(email) {
|
|
const emailHash = hashEmail(email)
|
|
return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}`
|
|
}
|
|
|
|
function hashEmail(email) {
|
|
return crypto.createHash('md5').update(email.toLowerCase()).digest('hex')
|
|
}
|
|
|
|
function getMergeFields(user) {
|
|
return {
|
|
FNAME: user.first_name,
|
|
LNAME: user.last_name,
|
|
MONGO_ID: user._id.toString(),
|
|
}
|
|
}
|
|
}
|
|
|
|
function makeNullProvider(listName) {
|
|
return {
|
|
subscribed,
|
|
subscribe,
|
|
unsubscribe,
|
|
changeEmail,
|
|
}
|
|
|
|
async function subscribed(user) {
|
|
logger.debug(
|
|
{ user, listName },
|
|
'Not checking user because no newsletter provider is configured'
|
|
)
|
|
return false
|
|
}
|
|
|
|
async function subscribe(user) {
|
|
logger.debug(
|
|
{ user, listName },
|
|
'Not subscribing user to newsletter because no newsletter provider is configured'
|
|
)
|
|
}
|
|
|
|
async function unsubscribe(user) {
|
|
logger.debug(
|
|
{ user, listName },
|
|
'Not unsubscribing user from newsletter because no newsletter provider is configured'
|
|
)
|
|
}
|
|
|
|
async function changeEmail(user, newEmail) {
|
|
logger.debug(
|
|
{ userId: user._id, newEmail, listName },
|
|
'Not changing email in newsletter for user because no newsletter provider is configured'
|
|
)
|
|
}
|
|
}
|