Send email to market creator on new answer

This commit is contained in:
James Grugett 2022-02-23 00:05:04 -06:00
parent 702755c797
commit cb7c8ba7c2
6 changed files with 570 additions and 12 deletions

View File

@ -29,6 +29,7 @@ export type PrivateUser = {
email?: string email?: string
unsubscribedFromResolutionEmails?: boolean unsubscribedFromResolutionEmails?: boolean
unsubscribedFromCommentEmails?: boolean unsubscribedFromCommentEmails?: boolean
unsubscribedFromAnswerEmails?: boolean
initialDeviceToken?: string initialDeviceToken?: string
initialIpAddress?: string initialIpAddress?: string
} }

View File

@ -5,7 +5,8 @@ import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet' import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer } from '../../common/answer' import { Answer } from '../../common/answer'
import { getValues } from './utils' import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
async ( async (
@ -28,7 +29,7 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
return { status: 'error', message: 'Invalid text' } return { status: 'error', message: 'Invalid text' }
// Run as transaction to prevent race conditions. // Run as transaction to prevent race conditions.
return await firestore.runTransaction(async (transaction) => { const result = await firestore.runTransaction(async (transaction) => {
const userDoc = firestore.doc(`users/${userId}`) const userDoc = firestore.doc(`users/${userId}`)
const userSnap = await transaction.get(userDoc) const userSnap = await transaction.get(userDoc)
if (!userSnap.exists) if (!userSnap.exists)
@ -103,8 +104,15 @@ export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
}) })
transaction.update(userDoc, { balance: newBalance }) transaction.update(userDoc, { balance: newBalance })
return { status: 'success', answerId, betId: newBetDoc.id } return { status: 'success', answerId, betId: newBetDoc.id, answer }
}) })
const { answer } = result
const contract = await getContract(contractId)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return result
} }
) )

View File

@ -0,0 +1,499 @@
<!DOCTYPE html>
<html
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market answer</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body
itemscope
itemtype="http://schema.org/EmailMessage"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
"
bgcolor="#f6f6f6"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
"
valign="top"
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
"
>
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
"
bgcolor="#fff"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
"
align="center"
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 0px 0;
text-align: left;
"
valign="top"
>
<img
src="https://manifold.markets/logo-banner.png"
width="300"
style="height: auto"
alt="Manifold Markets"
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
"
align="center"
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
margin-top: 10px;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 5px 0;
font-weight: bold;
"
valign="top"
>
<div>
<img
src="{{avatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
"
alt=""
/>
{{name}}
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
"
valign="top"
>
<div
style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<span style="white-space: pre-line"
>{{answer}}</span
>
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
"
>
<td style="padding: 20px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
"
>
<span
style="
display: block;
padding: 10px 20px;
line-height: 120%;
"
><span
style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
"
>View answer</span
></span
>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
"
>
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
"
>
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
"
align="center"
valign="top"
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>our Discord</a
>! Or,
<a
href="{{unsubscribeUrl}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
"
valign="top"
></td>
</tr>
</table>
</body>
</html>

View File

@ -333,11 +333,12 @@
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
white-space: pre-line;
margin: 0; margin: 0;
" "
> >
{{comment}} <span style="white-space: pre-line"
>{{comment}}</span
>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,4 +1,5 @@
import _ = require('lodash') import _ = require('lodash')
import { Answer } from '../../common/answer'
import { getProbability } from '../../common/calculate' import { getProbability } from '../../common/calculate'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
@ -149,14 +150,11 @@ export const sendNewCommentEmail = async (
const privateUser = await getPrivateUser(userId) const privateUser = await getPrivateUser(userId)
if ( if (
!privateUser || !privateUser ||
privateUser.unsubscribedFromCommentEmails || !privateUser.email ||
!privateUser.email privateUser.unsubscribedFromCommentEmails
) )
return return
const user = await getUser(userId)
if (!user) return
const { question, creatorUsername, slug } = contract const { question, creatorUsername, slug } = contract
const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}` const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}`
@ -184,3 +182,43 @@ export const sendNewCommentEmail = async (
{ from } { from }
) )
} }
export const sendNewAnswerEmail = async (
answer: Answer,
contract: Contract
) => {
// Send to just the creator for now.
const { creatorId: userId } = contract
const privateUser = await getPrivateUser(userId)
if (
!privateUser ||
!privateUser.email ||
privateUser.unsubscribedFromAnswerEmails
)
return
const { question, creatorUsername, slug } = contract
const { name, avatarUrl, text } = answer
const marketUrl = `https://manifold.markets/${creatorUsername}/${slug}`
const unsubscribeUrl = `https://us-central1-${
isProd ? 'mantic-markets' : 'dev-mantic-markets'
}.cloudfunctions.net/unsubscribe?id=${userId}&type=market-answer`
const subject = `New answer on ${question}`
const from = `${name} <info@manifold.markets>`
await sendTemplateEmail(
privateUser.email,
subject,
'market-answer',
{
name,
avatarUrl: avatarUrl ?? '',
answer: text,
marketUrl,
unsubscribeUrl,
},
{ from }
)
}

View File

@ -16,8 +16,15 @@ export const unsubscribe = functions
const { name } = user const { name } = user
const update: Partial<PrivateUser> = { const update: Partial<PrivateUser> = {
unsubscribedFromResolutionEmails: type === 'market-resolve', ...(type === 'market-resolve' && {
unsubscribedFromCommentEmails: type === 'market-comment', unsubscribedFromResolutionEmails: true,
}),
...(type === 'market-comment' && {
unsubscribedFromCommentEmails: true,
}),
...(type === 'market-answer' && {
unsubscribedFromAnswerEmails: true,
}),
} }
await firestore.collection('private-users').doc(id).update(update) await firestore.collection('private-users').doc(id).update(update)
@ -30,6 +37,10 @@ export const unsubscribe = functions
res.send( res.send(
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.` `${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
) )
else if (type === 'market-answer')
res.send(
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
)
else res.send(`${name}, you have been unsubscribed.`) else res.send(`${name}, you have been unsubscribed.`)
} else { } else {
res.send('This user is not currently subscribed or does not exist.') res.send('This user is not currently subscribed or does not exist.')