manifold/functions/src/emails.ts
2022-09-14 17:17:32 -06:00

615 lines
16 KiB
TypeScript

import { DOMAIN } from '../../common/envs/constants'
import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate'
import { Contract } from '../../common/contract'
import { PrivateUser, User } from '../../common/user'
import {
formatLargeNumber,
formatMoney,
formatPercent,
} from '../../common/util/format'
import { getValueFromBucket } from '../../common/calculate-dpm'
import { formatNumericProbability } from '../../common/pseudo-numeric'
import { sendTemplateEmail, sendTextEmail } from './send-email'
import { getUser } from './utils'
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
import { notification_reason_types } from '../../common/notification'
import { Dictionary } from 'lodash'
import {
getNotificationDestinationsForUser,
notification_preference,
} from '../../common/user-notification-preferences'
export const sendMarketResolutionEmail = async (
reason: notification_reason_types,
privateUser: PrivateUser,
investment: number,
payout: number,
creator: User,
creatorPayout: number,
contract: Contract,
resolution: string,
resolutionProbability?: number,
resolutions?: { [outcome: string]: number }
) => {
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
reason
)
if (!privateUser || !privateUser.email || !sendToEmail) return
const user = await getUser(privateUser.id)
if (!user) return
const outcome = toDisplayResolution(
contract,
resolution,
resolutionProbability,
resolutions
)
const subject = `Resolved ${outcome}: ${contract.question}`
const creatorPayoutText =
creatorPayout >= 1 && privateUser.id === creator.id
? ` (plus ${formatMoney(creatorPayout)} in commissions)`
: ''
const correctedInvestment =
Number.isNaN(investment) || investment < 0 ? 0 : investment
const displayedInvestment = formatMoney(correctedInvestment)
const displayedPayout = formatMoney(payout)
const templateData: market_resolved_template = {
userId: user.id,
name: user.name,
creatorName: creator.name,
question: contract.question,
outcome,
investment: displayedInvestment,
payout: displayedPayout + creatorPayoutText,
url: `https://${DOMAIN}/${creator.username}/${contract.slug}`,
unsubscribeUrl,
}
// Modify template here:
// https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
return await sendTemplateEmail(
privateUser.email,
subject,
correctedInvestment === 0 ? 'market-resolved-no-bets' : 'market-resolved',
templateData
)
}
type market_resolved_template = {
userId: string
name: string
creatorName: string
question: string
outcome: string
investment: string
payout: string
url: string
unsubscribeUrl: string
}
const toDisplayResolution = (
contract: Contract,
resolution: string,
resolutionProbability?: number,
resolutions?: { [outcome: string]: number }
) => {
if (contract.outcomeType === 'BINARY') {
const prob = resolutionProbability ?? getProbability(contract)
const display = {
YES: 'YES',
NO: 'NO',
CANCEL: 'N/A',
MKT: formatPercent(prob ?? 0),
}[resolution]
return display || resolution
}
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
const { resolution, resolutionValue } = contract
if (resolution === 'CANCEL') return 'N/A'
return resolutionValue
? formatLargeNumber(resolutionValue)
: formatNumericProbability(
resolutionProbability ?? getProbability(contract),
contract
)
}
if (resolution === 'MKT' && resolutions) return 'MULTI'
if (resolution === 'CANCEL') return 'N/A'
if (contract.outcomeType === 'NUMERIC' && contract.mechanism === 'dpm-2')
return (
contract.resolutionValue?.toString() ??
getValueFromBucket(resolution, contract).toString()
)
const answer = contract.answers.find((a) => a.id === resolution)
if (answer) return answer.text
return `#${resolution}`
}
export const sendWelcomeEmail = async (
user: User,
privateUser: PrivateUser
) => {
if (!privateUser || !privateUser.email) return
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'onboarding_flow' as notification_preference
}`
return await sendTemplateEmail(
privateUser.email,
'Welcome to Manifold Markets!',
'welcome',
{
name: firstName,
unsubscribeUrl,
},
{
from: 'David from Manifold <david@manifold.markets>',
}
)
}
export const sendPersonalFollowupEmail = async (
user: User,
privateUser: PrivateUser,
sendTime: string
) => {
if (!privateUser || !privateUser.email) return
const { name } = user
const firstName = name.split(' ')[0]
const emailBody = `Hi ${firstName},
Thanks for signing up! I'm one of the cofounders of Manifold Markets, and was wondering how you've found your experience on the platform so far?
If you haven't already, I encourage you to try creating your own prediction market (https://manifold.markets/create) and joining our Discord chat (https://discord.com/invite/eHQBNBqXuh).
Feel free to reply to this email with any questions or concerns you have.
Cheers,
James
Cofounder of Manifold Markets
https://manifold.markets
`
await sendTextEmail(
privateUser.email,
'How are you finding Manifold?',
emailBody,
{
from: 'James from Manifold <james@manifold.markets>',
'o:deliverytime': sendTime,
}
)
}
export const sendOneWeekBonusEmail = async (
user: User,
privateUser: PrivateUser
) => {
if (
!privateUser ||
!privateUser.email ||
!privateUser.notificationPreferences.onboarding_flow.includes('email')
)
return
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'onboarding_flow' as notification_preference
}`
return await sendTemplateEmail(
privateUser.email,
'Manifold Markets one week anniversary gift',
'one-week',
{
name: firstName,
unsubscribeUrl,
manalink: 'https://manifold.markets/link/lj4JbBvE',
},
{
from: 'David from Manifold <david@manifold.markets>',
}
)
}
export const sendCreatorGuideEmail = async (
user: User,
privateUser: PrivateUser,
sendTime: string
) => {
if (
!privateUser ||
!privateUser.email ||
!privateUser.notificationPreferences.onboarding_flow.includes('email')
)
return
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'onboarding_flow' as notification_preference
}`
return await sendTemplateEmail(
privateUser.email,
'Create your own prediction market',
'creating-market',
{
name: firstName,
unsubscribeUrl,
},
{
from: 'David from Manifold <david@manifold.markets>',
'o:deliverytime': sendTime,
}
)
}
export const sendThankYouEmail = async (
user: User,
privateUser: PrivateUser
) => {
if (
!privateUser ||
!privateUser.email ||
!privateUser.notificationPreferences.thank_you_for_purchases.includes(
'email'
)
)
return
const { name } = user
const firstName = name.split(' ')[0]
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'thank_you_for_purchases' as notification_preference
}`
return await sendTemplateEmail(
privateUser.email,
'Thanks for your Manifold purchase',
'thank-you',
{
name: firstName,
unsubscribeUrl,
},
{
from: 'David from Manifold <david@manifold.markets>',
}
)
}
export const sendMarketCloseEmail = async (
reason: notification_reason_types,
user: User,
privateUser: PrivateUser,
contract: Contract
) => {
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
reason
)
if (!privateUser.email || !sendToEmail) return
const { username, name, id: userId } = user
const firstName = name.split(' ')[0]
const { question, slug, volume } = contract
const url = `https://${DOMAIN}/${username}/${slug}`
return await sendTemplateEmail(
privateUser.email,
'Your market has closed',
'market-close',
{
question,
url,
unsubscribeUrl,
userId,
name: firstName,
volume: formatMoney(volume),
}
)
}
export const sendNewCommentEmail = async (
reason: notification_reason_types,
privateUser: PrivateUser,
commentCreator: User,
contract: Contract,
commentText: string,
commentId: string,
bet?: Bet,
answerText?: string,
answerId?: string
) => {
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
reason
)
if (!privateUser || !privateUser.email || !sendToEmail) return
const { question } = contract
const marketUrl = `https://${DOMAIN}/${contract.creatorUsername}/${contract.slug}#${commentId}`
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
let betDescription = ''
if (bet) {
const { amount, sale } = bet
betDescription = `${sale || amount < 0 ? 'sold' : 'bought'} ${formatMoney(
Math.abs(amount)
)}`
}
const subject = `Comment on ${question}`
const from = `${commentorName} on Manifold <no-reply@manifold.markets>`
if (contract.outcomeType === 'FREE_RESPONSE' && answerId && answerText) {
const answerNumber = answerId ? `#${answerId}` : ''
return await sendTemplateEmail(
privateUser.email,
subject,
'market-answer-comment',
{
answer: answerText,
answerNumber,
commentorName,
commentorAvatarUrl: commentorAvatarUrl ?? '',
comment: commentText,
marketUrl,
unsubscribeUrl,
betDescription,
},
{ from }
)
} else {
if (bet) {
betDescription = `${betDescription} of ${toDisplayResolution(
contract,
bet.outcome
)}`
}
return await sendTemplateEmail(
privateUser.email,
subject,
'market-comment',
{
commentorName,
commentorAvatarUrl: commentorAvatarUrl ?? '',
comment: commentText,
marketUrl,
unsubscribeUrl,
betDescription,
},
{ from }
)
}
}
export const sendNewAnswerEmail = async (
reason: notification_reason_types,
privateUser: PrivateUser,
name: string,
text: string,
contract: Contract,
avatarUrl?: string
) => {
const { creatorId } = contract
// Don't send the creator's own answers.
if (privateUser.id === creatorId) return
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
reason
)
if (!privateUser.email || !sendToEmail) return
const { question, creatorUsername, slug } = contract
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}`
const subject = `New answer on ${question}`
const from = `${name} <info@manifold.markets>`
return await sendTemplateEmail(
privateUser.email,
subject,
'market-answer',
{
name,
avatarUrl: avatarUrl ?? '',
answer: text,
marketUrl,
unsubscribeUrl,
},
{ from }
)
}
export const sendInterestingMarketsEmail = async (
user: User,
privateUser: PrivateUser,
contractsToSend: Contract[],
deliveryTime?: string
) => {
if (
!privateUser ||
!privateUser.email ||
!privateUser.notificationPreferences.trending_markets.includes('email')
)
return
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings&section=${
'trending_markets' as notification_preference
}`
const { name } = user
const firstName = name.split(' ')[0]
await sendTemplateEmail(
privateUser.email,
`${contractsToSend[0].question} & 5 more interesting markets on Manifold`,
'interesting-markets',
{
name: firstName,
unsubscribeUrl,
question1Title: contractsToSend[0].question,
question1Link: contractUrl(contractsToSend[0]),
question1ImgSrc: imageSourceUrl(contractsToSend[0]),
question2Title: contractsToSend[1].question,
question2Link: contractUrl(contractsToSend[1]),
question2ImgSrc: imageSourceUrl(contractsToSend[1]),
question3Title: contractsToSend[2].question,
question3Link: contractUrl(contractsToSend[2]),
question3ImgSrc: imageSourceUrl(contractsToSend[2]),
question4Title: contractsToSend[3].question,
question4Link: contractUrl(contractsToSend[3]),
question4ImgSrc: imageSourceUrl(contractsToSend[3]),
question5Title: contractsToSend[4].question,
question5Link: contractUrl(contractsToSend[4]),
question5ImgSrc: imageSourceUrl(contractsToSend[4]),
question6Title: contractsToSend[5].question,
question6Link: contractUrl(contractsToSend[5]),
question6ImgSrc: imageSourceUrl(contractsToSend[5]),
},
deliveryTime ? { 'o:deliverytime': deliveryTime } : undefined
)
}
function contractUrl(contract: Contract) {
return `https://manifold.markets/${contract.creatorUsername}/${contract.slug}`
}
function imageSourceUrl(contract: Contract) {
return buildCardUrl(getOpenGraphProps(contract))
}
export const sendNewFollowedMarketEmail = async (
reason: notification_reason_types,
userId: string,
privateUser: PrivateUser,
contract: Contract
) => {
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
reason
)
if (!privateUser.email || !sendToEmail) return
const user = await getUser(privateUser.id)
if (!user) return
const { name } = user
const firstName = name.split(' ')[0]
const creatorName = contract.creatorName
return await sendTemplateEmail(
privateUser.email,
`${creatorName} asked ${contract.question}`,
'new-market-from-followed-user',
{
name: firstName,
creatorName,
unsubscribeUrl,
questionTitle: contract.question,
questionUrl: contractUrl(contract),
questionImgSrc: imageSourceUrl(contract),
},
{
from: `${creatorName} on Manifold <no-reply@manifold.markets>`,
}
)
}
export const sendNewUniqueBettorsEmail = async (
reason: notification_reason_types,
userId: string,
privateUser: PrivateUser,
contract: Contract,
totalPredictors: number,
newPredictors: User[],
userBets: Dictionary<[Bet, ...Bet[]]>,
bonusAmount: number
) => {
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
privateUser,
reason
)
if (!privateUser.email || !sendToEmail) return
const user = await getUser(privateUser.id)
if (!user) return
const { name } = user
const firstName = name.split(' ')[0]
const creatorName = contract.creatorName
// make the emails stack for the same contract
const subject = `You made a popular market! ${
contract.question.length > 50
? contract.question.slice(0, 50) + '...'
: contract.question
} just got ${
newPredictors.length
} new predictions. Check out who's predicting on it inside.`
const templateData: Record<string, string> = {
name: firstName,
creatorName,
totalPredictors: totalPredictors.toString(),
bonusString: formatMoney(bonusAmount),
marketTitle: contract.question,
marketUrl: contractUrl(contract),
unsubscribeUrl,
newPredictors: newPredictors.length.toString(),
}
newPredictors.forEach((p, i) => {
templateData[`bettor${i + 1}Name`] = p.name
if (p.avatarUrl) templateData[`bettor${i + 1}AvatarUrl`] = p.avatarUrl
const bet = userBets[p.id][0]
if (bet) {
const { amount, sale } = bet
templateData[`bet${i + 1}Description`] = `${
sale || amount < 0 ? 'sold' : 'bought'
} ${formatMoney(Math.abs(amount))}`
}
})
return await sendTemplateEmail(
privateUser.email,
subject,
newPredictors.length === 1 ? 'new-unique-bettor' : 'new-unique-bettors',
templateData,
{
from: `Manifold Markets <no-reply@manifold.markets>`,
}
)
}