From 7522b6af3c839e6ce081a7c3183fe67799bedf8f Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 22 Feb 2022 19:35:25 -0600 Subject: [PATCH] Send emails for new comments on markets you commented on or created. --- common/user.ts | 1 + functions/src/emails.ts | 48 +++++++++++++++++++++++++++++- functions/src/index.ts | 1 + functions/src/on-create-comment.ts | 40 +++++++++++++++++++++++++ functions/src/send-email.ts | 5 ++-- functions/src/stripe.ts | 29 +++++++++--------- functions/src/unsubscribe.ts | 28 ++++++++++------- functions/src/utils.ts | 3 ++ 8 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 functions/src/on-create-comment.ts diff --git a/common/user.ts b/common/user.ts index 73186ac8..90ee2070 100644 --- a/common/user.ts +++ b/common/user.ts @@ -28,6 +28,7 @@ export type PrivateUser = { email?: string unsubscribedFromResolutionEmails?: boolean + unsubscribedFromCommentEmails?: boolean initialDeviceToken?: string initialIpAddress?: string } diff --git a/functions/src/emails.ts b/functions/src/emails.ts index 2b5981f5..da6b3c4b 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -1,11 +1,12 @@ import _ = require('lodash') import { getProbability } from '../../common/calculate' +import { Comment } from '../../common/comment' import { Contract } from '../../common/contract' import { CREATOR_FEE } from '../../common/fees' import { PrivateUser, User } from '../../common/user' import { formatMoney, formatPercent } from '../../common/util/format' import { sendTemplateEmail, sendTextEmail } from './send-email' -import { getPrivateUser, getUser } from './utils' +import { getPrivateUser, getUser, isProd } from './utils' type market_resolved_template = { 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} ` + + await sendTemplateEmail( + privateUser.email, + subject, + 'market-comment', + { + commentorName, + commentorAvatarUrl: commentorAvatarUrl ?? '', + comment: text, + marketUrl, + unsubscribeUrl, + }, + { from } + ) +} diff --git a/functions/src/index.ts b/functions/src/index.ts index 50c748af..dc333343 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -12,6 +12,7 @@ export * from './create-contract' export * from './create-user' export * from './create-fold' export * from './create-answer' +export * from './on-create-comment' export * from './on-fold-follow' export * from './on-fold-delete' export * from './unsubscribe' diff --git a/functions/src/on-create-comment.ts b/functions/src/on-create-comment.ts new file mode 100644 index 00000000..0b2788eb --- /dev/null +++ b/functions/src/on-create-comment.ts @@ -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( + 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) + ) + ) + }) diff --git a/functions/src/send-email.ts b/functions/src/send-email.ts index 938e3fc5..b0ac58a6 100644 --- a/functions/src/send-email.ts +++ b/functions/src/send-email.ts @@ -24,10 +24,11 @@ export const sendTemplateEmail = ( to: string, subject: string, templateId: string, - templateData: Record + templateData: Record, + options?: { from: string } ) => { const data = { - from: 'Manifold Markets ', + from: options?.from ?? 'Manifold Markets ', to, subject, template: templateId, diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index 4dffe541..eef9f40f 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import Stripe from 'stripe' -import { payUser } from './utils' +import { isProd, payUser } from './utils' export type StripeTransaction = { userId: string @@ -18,20 +18,19 @@ const stripe = new Stripe(functions.config().stripe.apikey, { }) // manage at https://dashboard.stripe.com/test/products?active=true -const manticDollarStripePrice = - admin.instanceId().app.options.projectId === 'mantic-markets' - ? { - 500: 'price_1KFQXcGdoFKoCJW770gTNBrm', - 1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65', - 2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB', - 10000: 'price_1KFQraGdoFKoCJW77I4XCwM3', - } - : { - 500: 'price_1K8W10GdoFKoCJW7KWORLec1', - 1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk', - 2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e', - 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', - } +const manticDollarStripePrice = isProd + ? { + 500: 'price_1KFQXcGdoFKoCJW770gTNBrm', + 1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65', + 2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB', + 10000: 'price_1KFQraGdoFKoCJW77I4XCwM3', + } + : { + 500: 'price_1K8W10GdoFKoCJW7KWORLec1', + 1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk', + 2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e', + 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', + } export const createCheckoutSession = functions .runWith({ minInstances: 1 }) diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts index d556f4ba..97201ed8 100644 --- a/functions/src/unsubscribe.ts +++ b/functions/src/unsubscribe.ts @@ -1,30 +1,36 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import * as _ from 'lodash' -import { getPrivateUser } from './utils' +import { getUser } from './utils' import { PrivateUser } from '../../common/user' export const unsubscribe = functions .runWith({ minInstances: 1 }) .https.onRequest(async (req, res) => { - let id = req.query.id as string - if (!id) return + const { id, type } = req.query as { id: string; type: string } + if (!id || !type) return - let privateUser = await getPrivateUser(id) + const user = await getUser(id) - if (privateUser) { - let { username } = privateUser + if (user) { + const { name } = user const update: Partial = { - unsubscribedFromResolutionEmails: true, + unsubscribedFromResolutionEmails: type === 'market-resolve', + unsubscribedFromCommentEmails: type === 'market-comment', } await firestore.collection('private-users').doc(id).update(update) - res.send( - username + - ', you have been unsubscribed from market resolution emails on Manifold Markets.' - ) + if (type === 'market-resolve') + res.send( + `${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 { res.send('This user is not currently subscribed or does not exist.') } diff --git a/functions/src/utils.ts b/functions/src/utils.ts index 9f3777e8..0e87538a 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -3,6 +3,9 @@ import * as admin from 'firebase-admin' import { Contract } from '../../common/contract' import { PrivateUser, User } from '../../common/user' +export const isProd = + admin.instanceId().app.options.projectId === 'mantic-markets' + export const getValue = async (collection: string, doc: string) => { const snap = await admin.firestore().collection(collection).doc(doc).get()