Send emails for new comments on markets you commented on or created.

This commit is contained in:
James Grugett 2022-02-22 19:35:25 -06:00
parent a6657a28fd
commit 7522b6af3c
8 changed files with 126 additions and 29 deletions

View File

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

View File

@ -1,11 +1,12 @@
import _ = require('lodash') import _ = require('lodash')
import { getProbability } from '../../common/calculate' import { getProbability } from '../../common/calculate'
import { Comment } from '../../common/comment'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { CREATOR_FEE } from '../../common/fees' import { CREATOR_FEE } from '../../common/fees'
import { PrivateUser, User } from '../../common/user' import { PrivateUser, User } from '../../common/user'
import { formatMoney, formatPercent } from '../../common/util/format' import { formatMoney, formatPercent } from '../../common/util/format'
import { sendTemplateEmail, sendTextEmail } from './send-email' import { sendTemplateEmail, sendTextEmail } from './send-email'
import { getPrivateUser, getUser } from './utils' import { getPrivateUser, getUser, isProd } from './utils'
type market_resolved_template = { type market_resolved_template = {
userId: string userId: string
@ -138,3 +139,48 @@ export const sendMarketCloseEmail = async (
} }
) )
} }
export const sendNewCommentEmail = async (
userId: string,
commentCreator: User,
comment: Comment,
contract: Contract
) => {
const privateUser = await getPrivateUser(userId)
if (
!privateUser ||
privateUser.unsubscribedFromCommentEmails ||
!privateUser.email
)
return
const user = await getUser(userId)
if (!user) return
const { question, creatorUsername, slug } = contract
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-comment`
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
const { text } = comment
const subject = `Comment on ${question}`
const from = `${commentorName} <info@manifold.markets>`
await sendTemplateEmail(
privateUser.email,
subject,
'market-comment',
{
commentorName,
commentorAvatarUrl: commentorAvatarUrl ?? '',
comment: text,
marketUrl,
unsubscribeUrl,
},
{ from }
)
}

View File

@ -12,6 +12,7 @@ export * from './create-contract'
export * from './create-user' export * from './create-user'
export * from './create-fold' export * from './create-fold'
export * from './create-answer' export * from './create-answer'
export * from './on-create-comment'
export * from './on-fold-follow' export * from './on-fold-follow'
export * from './on-fold-delete' export * from './on-fold-delete'
export * from './unsubscribe' export * from './unsubscribe'

View File

@ -0,0 +1,40 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as _ from 'lodash'
import { getContract, getUser, getValues } from './utils'
import { Comment } from '../../common/comment'
import { sendNewCommentEmail } from './emails'
const firestore = admin.firestore()
export const onCreateComment = functions.firestore
.document('contracts/{contractId}/comments/{commentId}')
.onCreate(async (change, context) => {
const { contractId } = context.params as {
contractId: string
}
const contract = await getContract(contractId)
if (!contract) return
const comment = change.data() as Comment
const commentCreator = await getUser(comment.userId)
if (!commentCreator) return
const comments = await getValues<Comment>(
firestore.collection('contracts').doc(contractId).collection('comments')
)
const recipientUserIds = _.uniq([
contract.creatorId,
...comments.map((comment) => comment.userId),
]).filter((id) => id !== comment.userId)
await Promise.all(
recipientUserIds.map((userId) =>
sendNewCommentEmail(userId, commentCreator, comment, contract)
)
)
})

View File

@ -24,10 +24,11 @@ export const sendTemplateEmail = (
to: string, to: string,
subject: string, subject: string,
templateId: string, templateId: string,
templateData: Record<string, string> templateData: Record<string, string>,
options?: { from: string }
) => { ) => {
const data = { const data = {
from: 'Manifold Markets <info@manifold.markets>', from: options?.from ?? 'Manifold Markets <info@manifold.markets>',
to, to,
subject, subject,
template: templateId, template: templateId,

View File

@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import Stripe from 'stripe' import Stripe from 'stripe'
import { payUser } from './utils' import { isProd, payUser } from './utils'
export type StripeTransaction = { export type StripeTransaction = {
userId: string userId: string
@ -18,20 +18,19 @@ const stripe = new Stripe(functions.config().stripe.apikey, {
}) })
// manage at https://dashboard.stripe.com/test/products?active=true // manage at https://dashboard.stripe.com/test/products?active=true
const manticDollarStripePrice = const manticDollarStripePrice = isProd
admin.instanceId().app.options.projectId === 'mantic-markets' ? {
? { 500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
500: 'price_1KFQXcGdoFKoCJW770gTNBrm', 1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65', 2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB',
2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB', 10000: 'price_1KFQraGdoFKoCJW77I4XCwM3',
10000: 'price_1KFQraGdoFKoCJW77I4XCwM3', }
} : {
: { 500: 'price_1K8W10GdoFKoCJW7KWORLec1',
500: 'price_1K8W10GdoFKoCJW7KWORLec1', 1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk',
1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk', 2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e',
2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e', 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', }
}
export const createCheckoutSession = functions export const createCheckoutSession = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })

View File

@ -1,30 +1,36 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import * as _ from 'lodash' import * as _ from 'lodash'
import { getPrivateUser } from './utils' import { getUser } from './utils'
import { PrivateUser } from '../../common/user' import { PrivateUser } from '../../common/user'
export const unsubscribe = functions export const unsubscribe = functions
.runWith({ minInstances: 1 }) .runWith({ minInstances: 1 })
.https.onRequest(async (req, res) => { .https.onRequest(async (req, res) => {
let id = req.query.id as string const { id, type } = req.query as { id: string; type: string }
if (!id) return if (!id || !type) return
let privateUser = await getPrivateUser(id) const user = await getUser(id)
if (privateUser) { if (user) {
let { username } = privateUser const { name } = user
const update: Partial<PrivateUser> = { const update: Partial<PrivateUser> = {
unsubscribedFromResolutionEmails: true, unsubscribedFromResolutionEmails: type === 'market-resolve',
unsubscribedFromCommentEmails: type === 'market-comment',
} }
await firestore.collection('private-users').doc(id).update(update) await firestore.collection('private-users').doc(id).update(update)
res.send( if (type === 'market-resolve')
username + res.send(
', you have been unsubscribed from market resolution emails on Manifold Markets.' `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
) )
else if (type === 'market-comment')
res.send(
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
)
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.')
} }

View File

@ -3,6 +3,9 @@ import * as admin from 'firebase-admin'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { PrivateUser, User } from '../../common/user' import { PrivateUser, User } from '../../common/user'
export const isProd =
admin.instanceId().app.options.projectId === 'mantic-markets'
export const getValue = async <T>(collection: string, doc: string) => { export const getValue = async <T>(collection: string, doc: string) => {
const snap = await admin.firestore().collection(collection).doc(doc).get() const snap = await admin.firestore().collection(collection).doc(doc).get()