Send market closed notifications every 5 days
This commit is contained in:
parent
d507c4092e
commit
274f7fa849
|
@ -10,6 +10,7 @@ import {
|
||||||
MANIFOLD_AVATAR_URL,
|
MANIFOLD_AVATAR_URL,
|
||||||
MANIFOLD_USER_NAME,
|
MANIFOLD_USER_NAME,
|
||||||
MANIFOLD_USER_USERNAME,
|
MANIFOLD_USER_USERNAME,
|
||||||
|
PrivateUser,
|
||||||
User,
|
User,
|
||||||
} from '../../common/user'
|
} from '../../common/user'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
|
@ -42,21 +43,19 @@ type recipients_to_reason_texts = {
|
||||||
[userId: string]: { reason: notification_reason_types }
|
[userId: string]: { reason: notification_reason_types }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNotification = async (
|
export const createFollowOrMarketSubsidizedNotification = async (
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
sourceType: 'contract' | 'liquidity' | 'follow',
|
sourceType: 'liquidity' | 'follow',
|
||||||
sourceUpdateType: 'closed' | 'created',
|
sourceUpdateType: 'created',
|
||||||
sourceUser: User,
|
sourceUser: User,
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
sourceText: string,
|
sourceText: string,
|
||||||
miscData?: {
|
miscData?: {
|
||||||
contract?: Contract
|
contract?: Contract
|
||||||
recipients?: string[]
|
recipients?: string[]
|
||||||
slug?: string
|
|
||||||
title?: string
|
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { contract: sourceContract, recipients, slug, title } = miscData ?? {}
|
const { contract: sourceContract, recipients } = miscData ?? {}
|
||||||
|
|
||||||
const shouldReceiveNotification = (
|
const shouldReceiveNotification = (
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -100,23 +99,15 @@ export const createNotification = async (
|
||||||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||||
sourceContractTitle: sourceContract?.question,
|
sourceContractTitle: sourceContract?.question,
|
||||||
sourceContractSlug: sourceContract?.slug,
|
sourceContractSlug: sourceContract?.slug,
|
||||||
sourceSlug: slug ? slug : sourceContract?.slug,
|
sourceSlug: sourceContract?.slug,
|
||||||
sourceTitle: title ? title : sourceContract?.question,
|
sourceTitle: sourceContract?.question,
|
||||||
}
|
}
|
||||||
await notificationRef.set(removeUndefinedProps(notification))
|
await notificationRef.set(removeUndefinedProps(notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sendToEmail) continue
|
if (!sendToEmail) continue
|
||||||
|
|
||||||
if (reason === 'your_contract_closed' && privateUser && sourceContract) {
|
if (reason === 'subsidized_your_market') {
|
||||||
// TODO: include number and names of bettors waiting for creator to resolve their market
|
|
||||||
await sendMarketCloseEmail(
|
|
||||||
reason,
|
|
||||||
sourceUser,
|
|
||||||
privateUser,
|
|
||||||
sourceContract
|
|
||||||
)
|
|
||||||
} else if (reason === 'subsidized_your_market') {
|
|
||||||
// TODO: send email to creator of market that was subsidized
|
// TODO: send email to creator of market that was subsidized
|
||||||
} else if (reason === 'on_new_follow') {
|
} else if (reason === 'on_new_follow') {
|
||||||
// TODO: send email to user who was followed
|
// TODO: send email to user who was followed
|
||||||
|
@ -133,20 +124,7 @@ export const createNotification = async (
|
||||||
reason: 'on_new_follow',
|
reason: 'on_new_follow',
|
||||||
}
|
}
|
||||||
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
|
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
|
||||||
} else if (
|
} else if (sourceType === 'liquidity' && sourceContract) {
|
||||||
sourceType === 'contract' &&
|
|
||||||
sourceUpdateType === 'closed' &&
|
|
||||||
sourceContract
|
|
||||||
) {
|
|
||||||
userToReasonTexts[sourceContract.creatorId] = {
|
|
||||||
reason: 'your_contract_closed',
|
|
||||||
}
|
|
||||||
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
|
|
||||||
} else if (
|
|
||||||
sourceType === 'liquidity' &&
|
|
||||||
sourceUpdateType === 'created' &&
|
|
||||||
sourceContract
|
|
||||||
) {
|
|
||||||
if (shouldReceiveNotification(sourceContract.creatorId, userToReasonTexts))
|
if (shouldReceiveNotification(sourceContract.creatorId, userToReasonTexts))
|
||||||
userToReasonTexts[sourceContract.creatorId] = {
|
userToReasonTexts[sourceContract.creatorId] = {
|
||||||
reason: 'subsidized_your_market',
|
reason: 'subsidized_your_market',
|
||||||
|
@ -1133,3 +1111,41 @@ export const createBadgeAwardedNotification = async (
|
||||||
|
|
||||||
// TODO send email notification
|
// TODO send email notification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createMarketClosedNotification = async (
|
||||||
|
contract: Contract,
|
||||||
|
creator: User,
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
idempotencyKey: string
|
||||||
|
) => {
|
||||||
|
const notificationRef = firestore
|
||||||
|
.collection(`/users/${creator.id}/notifications`)
|
||||||
|
.doc(idempotencyKey)
|
||||||
|
const notification: Notification = {
|
||||||
|
id: idempotencyKey,
|
||||||
|
userId: creator.id,
|
||||||
|
reason: 'your_contract_closed',
|
||||||
|
createdTime: Date.now(),
|
||||||
|
isSeen: false,
|
||||||
|
sourceId: contract.id,
|
||||||
|
sourceType: 'contract',
|
||||||
|
sourceUpdateType: 'closed',
|
||||||
|
sourceContractId: contract?.id,
|
||||||
|
sourceUserName: creator.name,
|
||||||
|
sourceUserUsername: creator.username,
|
||||||
|
sourceUserAvatarUrl: creator.avatarUrl,
|
||||||
|
sourceText: contract.closeTime?.toString() ?? new Date().toString(),
|
||||||
|
sourceContractCreatorUsername: creator.username,
|
||||||
|
sourceContractTitle: contract.question,
|
||||||
|
sourceContractSlug: contract.slug,
|
||||||
|
sourceSlug: contract.slug,
|
||||||
|
sourceTitle: contract.question,
|
||||||
|
}
|
||||||
|
await notificationRef.set(removeUndefinedProps(notification))
|
||||||
|
await sendMarketCloseEmail(
|
||||||
|
'your_contract_closed',
|
||||||
|
creator,
|
||||||
|
privateUser,
|
||||||
|
contract
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -301,12 +301,7 @@ export const sendMarketCloseEmail = async (
|
||||||
privateUser: PrivateUser,
|
privateUser: PrivateUser,
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
if (!privateUser.email) return
|
||||||
privateUser,
|
|
||||||
reason
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!privateUser.email || !sendToEmail) return
|
|
||||||
|
|
||||||
const { username, name, id: userId } = user
|
const { username, name, id: userId } = user
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
@ -315,6 +310,7 @@ export const sendMarketCloseEmail = async (
|
||||||
|
|
||||||
const url = `https://${DOMAIN}/${username}/${slug}`
|
const url = `https://${DOMAIN}/${username}/${slug}`
|
||||||
|
|
||||||
|
// We ignore if they were able to unsubscribe from market close emails, this is a necessary email
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
'Your market has closed',
|
'Your market has closed',
|
||||||
|
@ -322,7 +318,7 @@ export const sendMarketCloseEmail = async (
|
||||||
{
|
{
|
||||||
question,
|
question,
|
||||||
url,
|
url,
|
||||||
unsubscribeUrl,
|
unsubscribeUrl: '',
|
||||||
userId,
|
userId,
|
||||||
name: firstName,
|
name: firstName,
|
||||||
volume: formatMoney(volume),
|
volume: formatMoney(volume),
|
||||||
|
|
|
@ -3,8 +3,10 @@ import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { getPrivateUser, getUserByUsername } from './utils'
|
import { getPrivateUser, getUserByUsername } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createMarketClosedNotification } from './create-notification'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
|
|
||||||
|
const SEND_NOTIFICATIONS_EVERY_DAYS = 5
|
||||||
export const marketCloseNotifications = functions
|
export const marketCloseNotifications = functions
|
||||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
.pubsub.schedule('every 1 hours')
|
.pubsub.schedule('every 1 hours')
|
||||||
|
@ -14,31 +16,31 @@ export const marketCloseNotifications = functions
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
async function sendMarketCloseEmails() {
|
export async function sendMarketCloseEmails() {
|
||||||
const contracts = await firestore.runTransaction(async (transaction) => {
|
const contracts = await firestore.runTransaction(async (transaction) => {
|
||||||
const snap = await transaction.get(
|
const snap = await transaction.get(
|
||||||
firestore.collection('contracts').where('isResolved', '!=', true)
|
firestore.collection('contracts').where('isResolved', '!=', true)
|
||||||
)
|
)
|
||||||
|
const contracts = snap.docs.map((doc) => doc.data() as Contract)
|
||||||
|
const now = Date.now()
|
||||||
|
const closeContracts = contracts.filter(
|
||||||
|
(contract) =>
|
||||||
|
contract.closeTime &&
|
||||||
|
contract.closeTime < now &&
|
||||||
|
shouldSendFirstOrFollowUpCloseNotification(contract)
|
||||||
|
)
|
||||||
|
|
||||||
return snap.docs
|
await Promise.all(
|
||||||
.map((doc) => {
|
closeContracts.map(async (contract) => {
|
||||||
const contract = doc.data() as Contract
|
await transaction.update(
|
||||||
|
firestore.collection('contracts').doc(contract.id),
|
||||||
if (
|
{
|
||||||
contract.resolution ||
|
closeEmailsSent: admin.firestore.FieldValue.increment(1),
|
||||||
(contract.closeEmailsSent ?? 0) >= 1 ||
|
}
|
||||||
contract.closeTime === undefined ||
|
|
||||||
(contract.closeTime ?? 0) > Date.now()
|
|
||||||
)
|
)
|
||||||
return undefined
|
|
||||||
|
|
||||||
transaction.update(doc.ref, {
|
|
||||||
closeEmailsSent: (contract.closeEmailsSent ?? 0) + 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
return contract
|
|
||||||
})
|
})
|
||||||
.filter((x) => !!x) as Contract[]
|
)
|
||||||
|
return closeContracts
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const contract of contracts) {
|
for (const contract of contracts) {
|
||||||
|
@ -55,14 +57,38 @@ async function sendMarketCloseEmails() {
|
||||||
const privateUser = await getPrivateUser(user.id)
|
const privateUser = await getPrivateUser(user.id)
|
||||||
if (!privateUser) continue
|
if (!privateUser) continue
|
||||||
|
|
||||||
await createNotification(
|
await createMarketClosedNotification(
|
||||||
contract.id,
|
contract,
|
||||||
'contract',
|
|
||||||
'closed',
|
|
||||||
user,
|
user,
|
||||||
contract.id + '-closed-at-' + contract.closeTime,
|
privateUser,
|
||||||
contract.closeTime?.toString() ?? new Date().toString(),
|
contract.id + '-closed-at-' + contract.closeTime
|
||||||
{ contract }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function shouldSendFirstOrFollowUpCloseNotification(contract: Contract) {
|
||||||
|
if (!contract.closeEmailsSent || contract.closeEmailsSent === 0) return true
|
||||||
|
const { closedMultipleOfNDaysAgo, fullTimePeriodsSinceClose } =
|
||||||
|
marketClosedMultipleOfNDaysAgo(contract)
|
||||||
|
// Sends another notification if it's been a multiple of N days since the market closed AND
|
||||||
|
// the number of close notifications we've sent is equal to the number of time periods since the market closed
|
||||||
|
return (
|
||||||
|
contract.closeEmailsSent > 0 &&
|
||||||
|
closedMultipleOfNDaysAgo &&
|
||||||
|
contract.closeEmailsSent === fullTimePeriodsSinceClose
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function marketClosedMultipleOfNDaysAgo(contract: Contract) {
|
||||||
|
const now = Date.now()
|
||||||
|
const closeTime = contract.closeTime
|
||||||
|
if (!closeTime)
|
||||||
|
return { closedMultipleOfNDaysAgo: false, fullTimePeriodsSinceClose: 0 }
|
||||||
|
const daysSinceClose = Math.floor((now - closeTime) / DAY_MS)
|
||||||
|
return {
|
||||||
|
closedMultipleOfNDaysAgo:
|
||||||
|
daysSinceClose % SEND_NOTIFICATIONS_EVERY_DAYS == 0,
|
||||||
|
fullTimePeriodsSinceClose: Math.floor(
|
||||||
|
daysSinceClose / SEND_NOTIFICATIONS_EVERY_DAYS
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import { getContract, getUser, log } from './utils'
|
import { getContract, getUser, log } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createFollowOrMarketSubsidizedNotification } from './create-notification'
|
||||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||||
import { addUserToContractFollowers } from './follow-market'
|
import { addUserToContractFollowers } from './follow-market'
|
||||||
import { FIXED_ANTE } from '../../common/economy'
|
import { FIXED_ANTE } from '../../common/economy'
|
||||||
|
@ -36,7 +36,7 @@ export const onCreateLiquidityProvision = functions.firestore
|
||||||
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
||||||
await addUserToContractFollowers(contract.id, liquidityProvider.id)
|
await addUserToContractFollowers(contract.id, liquidityProvider.id)
|
||||||
|
|
||||||
await createNotification(
|
await createFollowOrMarketSubsidizedNotification(
|
||||||
contract.id,
|
contract.id,
|
||||||
'liquidity',
|
'liquidity',
|
||||||
'created',
|
'created',
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { createNotification } from './create-notification'
|
import { createFollowOrMarketSubsidizedNotification } from './create-notification'
|
||||||
import { FieldValue } from 'firebase-admin/firestore'
|
import { FieldValue } from 'firebase-admin/firestore'
|
||||||
|
|
||||||
export const onFollowUser = functions.firestore
|
export const onFollowUser = functions.firestore
|
||||||
|
@ -23,7 +23,7 @@ export const onFollowUser = functions.firestore
|
||||||
followerCountCached: FieldValue.increment(1),
|
followerCountCached: FieldValue.increment(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
await createNotification(
|
await createFollowOrMarketSubsidizedNotification(
|
||||||
followingUser.id,
|
followingUser.id,
|
||||||
'follow',
|
'follow',
|
||||||
'created',
|
'created',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { APIError, newEndpoint } from './api'
|
import { APIError, newEndpoint } from './api'
|
||||||
import { isProd } from './utils'
|
import { isProd } from './utils'
|
||||||
import { sendTrendingMarketsEmailsToAllUsers } from 'functions/src/weekly-markets-emails'
|
import { sendMarketCloseEmails } from 'functions/src/market-close-notifications'
|
||||||
|
|
||||||
// Function for testing scheduled functions locally
|
// Function for testing scheduled functions locally
|
||||||
export const testscheduledfunction = newEndpoint(
|
export const testscheduledfunction = newEndpoint(
|
||||||
|
@ -10,7 +10,7 @@ export const testscheduledfunction = newEndpoint(
|
||||||
throw new APIError(400, 'This function is only available in dev mode')
|
throw new APIError(400, 'This function is only available in dev mode')
|
||||||
|
|
||||||
// Replace your function here
|
// Replace your function here
|
||||||
await sendTrendingMarketsEmailsToAllUsers()
|
await sendMarketCloseEmails()
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1002,6 +1002,7 @@ function MarketClosedNotification(props: {
|
||||||
isChildOfGroup={isChildOfGroup}
|
isChildOfGroup={isChildOfGroup}
|
||||||
highlighted={highlighted}
|
highlighted={highlighted}
|
||||||
subtitle={'Please resolve'}
|
subtitle={'Please resolve'}
|
||||||
|
hideUserName={true}
|
||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<span>
|
<span>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user