2022-06-01 13:11:25 +00:00
|
|
|
import * as admin from 'firebase-admin'
|
|
|
|
import {
|
|
|
|
Notification,
|
|
|
|
notification_reason_types,
|
2022-06-06 17:36:59 +00:00
|
|
|
notification_source_update_types,
|
2022-06-01 13:11:25 +00:00
|
|
|
notification_source_types,
|
|
|
|
} from '../../common/notification'
|
|
|
|
import { User } from '../../common/user'
|
|
|
|
import { Contract } from '../../common/contract'
|
2022-06-06 17:36:59 +00:00
|
|
|
import { getUserByUsername, getValues } from './utils'
|
2022-06-01 13:11:25 +00:00
|
|
|
import { Comment } from '../../common/comment'
|
|
|
|
import { uniq } from 'lodash'
|
|
|
|
import { Bet } from '../../common/bet'
|
|
|
|
import { Answer } from '../../common/answer'
|
2022-06-06 17:36:59 +00:00
|
|
|
import { getContractBetMetrics } from '../../common/calculate'
|
|
|
|
import { removeUndefinedProps } from '../../common/util/object'
|
2022-06-01 13:11:25 +00:00
|
|
|
const firestore = admin.firestore()
|
|
|
|
|
|
|
|
type user_to_reason_texts = {
|
2022-06-06 17:36:59 +00:00
|
|
|
[userId: string]: { reason: notification_reason_types }
|
2022-06-01 13:11:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const createNotification = async (
|
|
|
|
sourceId: string,
|
|
|
|
sourceType: notification_source_types,
|
2022-06-06 17:36:59 +00:00
|
|
|
sourceUpdateType: notification_source_update_types,
|
2022-06-01 13:11:25 +00:00
|
|
|
sourceUser: User,
|
2022-06-06 17:36:59 +00:00
|
|
|
idempotencyKey: string,
|
2022-06-08 14:43:24 +00:00
|
|
|
sourceText: string,
|
2022-06-06 17:36:59 +00:00
|
|
|
sourceContract?: Contract,
|
|
|
|
relatedSourceType?: notification_source_types,
|
2022-06-22 16:35:50 +00:00
|
|
|
relatedUserId?: string,
|
|
|
|
sourceSlug?: string,
|
|
|
|
sourceTitle?: string
|
2022-06-01 13:11:25 +00:00
|
|
|
) => {
|
|
|
|
const shouldGetNotification = (
|
|
|
|
userId: string,
|
|
|
|
userToReasonTexts: user_to_reason_texts
|
|
|
|
) => {
|
|
|
|
return (
|
|
|
|
sourceUser.id != userId &&
|
|
|
|
!Object.keys(userToReasonTexts).includes(userId)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const createUsersNotifications = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts
|
|
|
|
) => {
|
|
|
|
await Promise.all(
|
|
|
|
Object.keys(userToReasonTexts).map(async (userId) => {
|
|
|
|
const notificationRef = firestore
|
|
|
|
.collection(`/users/${userId}/notifications`)
|
|
|
|
.doc(idempotencyKey)
|
|
|
|
const notification: Notification = {
|
|
|
|
id: idempotencyKey,
|
|
|
|
userId,
|
|
|
|
reason: userToReasonTexts[userId].reason,
|
|
|
|
createdTime: Date.now(),
|
|
|
|
isSeen: false,
|
|
|
|
sourceId,
|
|
|
|
sourceType,
|
2022-06-06 17:36:59 +00:00
|
|
|
sourceUpdateType,
|
|
|
|
sourceContractId: sourceContract?.id,
|
2022-06-01 13:11:25 +00:00
|
|
|
sourceUserName: sourceUser.name,
|
|
|
|
sourceUserUsername: sourceUser.username,
|
|
|
|
sourceUserAvatarUrl: sourceUser.avatarUrl,
|
2022-06-08 14:43:24 +00:00
|
|
|
sourceText,
|
|
|
|
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
2022-06-22 16:35:50 +00:00
|
|
|
// TODO: move away from sourceContractTitle to sourceTitle
|
|
|
|
sourceContractTitle: sourceContract?.question,
|
2022-07-01 13:47:19 +00:00
|
|
|
// TODO: move away from sourceContractSlug to sourceSlug
|
2022-06-08 14:43:24 +00:00
|
|
|
sourceContractSlug: sourceContract?.slug,
|
2022-06-22 16:35:50 +00:00
|
|
|
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
|
|
|
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
2022-06-01 13:11:25 +00:00
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
await notificationRef.set(removeUndefinedProps(notification))
|
2022-06-01 13:11:25 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-10 22:48:28 +00:00
|
|
|
const notifyLiquidityProviders = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
contract: Contract
|
|
|
|
) => {
|
|
|
|
const liquidityProviders = await firestore
|
|
|
|
.collection(`contracts/${contract.id}/liquidity`)
|
|
|
|
.get()
|
|
|
|
const liquidityProvidersIds = uniq(
|
|
|
|
liquidityProviders.docs.map((doc) => doc.data().userId)
|
|
|
|
)
|
|
|
|
liquidityProvidersIds.forEach((userId) => {
|
|
|
|
if (!shouldGetNotification(userId, userToReasonTexts)) return
|
|
|
|
userToReasonTexts[userId] = {
|
|
|
|
reason: 'on_contract_with_users_shares_in',
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-08 14:43:24 +00:00
|
|
|
const notifyUsersFollowers = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts
|
|
|
|
) => {
|
|
|
|
const followers = await firestore
|
|
|
|
.collectionGroup('follows')
|
|
|
|
.where('userId', '==', sourceUser.id)
|
|
|
|
.get()
|
|
|
|
|
|
|
|
followers.docs.forEach((doc) => {
|
|
|
|
const followerUserId = doc.ref.parent.parent?.id
|
|
|
|
if (
|
|
|
|
followerUserId &&
|
|
|
|
shouldGetNotification(followerUserId, userToReasonTexts)
|
|
|
|
) {
|
|
|
|
userToReasonTexts[followerUserId] = {
|
|
|
|
reason: 'you_follow_user',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const notifyRepliedUsers = async (
|
2022-06-10 22:48:28 +00:00
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
relatedUserId: string,
|
|
|
|
relatedSourceType: notification_source_types
|
2022-06-06 17:36:59 +00:00
|
|
|
) => {
|
2022-06-10 22:48:28 +00:00
|
|
|
if (!shouldGetNotification(relatedUserId, userToReasonTexts)) return
|
2022-06-06 17:36:59 +00:00
|
|
|
if (relatedSourceType === 'comment') {
|
|
|
|
userToReasonTexts[relatedUserId] = {
|
|
|
|
reason: 'reply_to_users_comment',
|
|
|
|
}
|
|
|
|
} else if (relatedSourceType === 'answer') {
|
|
|
|
userToReasonTexts[relatedUserId] = {
|
|
|
|
reason: 'reply_to_users_answer',
|
|
|
|
}
|
2022-06-06 16:54:25 +00:00
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
}
|
2022-06-01 13:11:25 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const notifyFollowedUser = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
followedUserId: string
|
|
|
|
) => {
|
|
|
|
if (shouldGetNotification(followedUserId, userToReasonTexts))
|
|
|
|
userToReasonTexts[followedUserId] = {
|
|
|
|
reason: 'on_new_follow',
|
|
|
|
}
|
|
|
|
}
|
2022-06-01 13:11:25 +00:00
|
|
|
|
2022-06-10 22:48:28 +00:00
|
|
|
const notifyTaggedUsers = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
sourceText: string
|
|
|
|
) => {
|
2022-06-06 17:36:59 +00:00
|
|
|
const taggedUsers = sourceText.match(/@\w+/g)
|
|
|
|
if (!taggedUsers) return
|
|
|
|
// await all get tagged users:
|
|
|
|
const users = await Promise.all(
|
|
|
|
taggedUsers.map(async (username) => {
|
|
|
|
return await getUserByUsername(username.slice(1))
|
2022-06-06 16:54:25 +00:00
|
|
|
})
|
2022-06-06 17:36:59 +00:00
|
|
|
)
|
|
|
|
users.forEach((taggedUser) => {
|
|
|
|
if (taggedUser && shouldGetNotification(taggedUser.id, userToReasonTexts))
|
|
|
|
userToReasonTexts[taggedUser.id] = {
|
|
|
|
reason: 'tagged_user',
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2022-06-01 13:11:25 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const notifyContractCreator = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
2022-06-10 22:48:28 +00:00
|
|
|
sourceContract: Contract,
|
|
|
|
options?: { force: boolean }
|
2022-06-06 17:36:59 +00:00
|
|
|
) => {
|
2022-06-10 22:48:28 +00:00
|
|
|
if (
|
|
|
|
options?.force ||
|
|
|
|
shouldGetNotification(sourceContract.creatorId, userToReasonTexts)
|
|
|
|
)
|
2022-06-06 17:36:59 +00:00
|
|
|
userToReasonTexts[sourceContract.creatorId] = {
|
|
|
|
reason: 'on_users_contract',
|
|
|
|
}
|
|
|
|
}
|
2022-06-01 13:11:25 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const notifyOtherAnswerersOnContract = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
sourceContract: Contract
|
|
|
|
) => {
|
|
|
|
const answers = await getValues<Answer>(
|
|
|
|
firestore
|
|
|
|
.collection('contracts')
|
|
|
|
.doc(sourceContract.id)
|
|
|
|
.collection('answers')
|
|
|
|
)
|
|
|
|
const recipientUserIds = uniq(answers.map((answer) => answer.userId))
|
|
|
|
recipientUserIds.forEach((userId) => {
|
|
|
|
if (shouldGetNotification(userId, userToReasonTexts))
|
|
|
|
userToReasonTexts[userId] = {
|
|
|
|
reason: 'on_contract_with_users_answer',
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2022-06-01 13:11:25 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const notifyOtherCommentersOnContract = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
sourceContract: Contract
|
|
|
|
) => {
|
|
|
|
const comments = await getValues<Comment>(
|
|
|
|
firestore
|
|
|
|
.collection('contracts')
|
|
|
|
.doc(sourceContract.id)
|
|
|
|
.collection('comments')
|
|
|
|
)
|
|
|
|
const recipientUserIds = uniq(comments.map((comment) => comment.userId))
|
|
|
|
recipientUserIds.forEach((userId) => {
|
|
|
|
if (shouldGetNotification(userId, userToReasonTexts))
|
|
|
|
userToReasonTexts[userId] = {
|
|
|
|
reason: 'on_contract_with_users_comment',
|
|
|
|
}
|
|
|
|
})
|
2022-06-01 13:11:25 +00:00
|
|
|
}
|
2022-06-06 16:52:11 +00:00
|
|
|
|
2022-06-10 22:48:28 +00:00
|
|
|
const notifyBettorsOnContract = async (
|
2022-06-06 17:36:59 +00:00
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
sourceContract: Contract
|
|
|
|
) => {
|
|
|
|
const betsSnap = await firestore
|
|
|
|
.collection(`contracts/${sourceContract.id}/bets`)
|
|
|
|
.get()
|
|
|
|
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
|
|
|
// filter bets for only users that have an amount invested still
|
|
|
|
const recipientUserIds = uniq(bets.map((bet) => bet.userId)).filter(
|
|
|
|
(userId) => {
|
|
|
|
return (
|
|
|
|
getContractBetMetrics(
|
|
|
|
sourceContract,
|
|
|
|
bets.filter((bet) => bet.userId === userId)
|
|
|
|
).invested > 0
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
recipientUserIds.forEach((userId) => {
|
|
|
|
if (shouldGetNotification(userId, userToReasonTexts))
|
|
|
|
userToReasonTexts[userId] = {
|
|
|
|
reason: 'on_contract_with_users_shares_in',
|
|
|
|
}
|
|
|
|
})
|
2022-06-06 16:52:11 +00:00
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
|
2022-06-22 16:35:50 +00:00
|
|
|
const notifyUserAddedToGroup = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
relatedUserId: string
|
|
|
|
) => {
|
|
|
|
if (shouldGetNotification(relatedUserId, userToReasonTexts))
|
|
|
|
userToReasonTexts[relatedUserId] = {
|
|
|
|
reason: 'added_you_to_group',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-01 13:47:19 +00:00
|
|
|
const notifyUserReceivedReferralBonus = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
relatedUserId: string
|
|
|
|
) => {
|
|
|
|
if (shouldGetNotification(relatedUserId, userToReasonTexts))
|
|
|
|
userToReasonTexts[relatedUserId] = {
|
|
|
|
// If the referrer is the market creator, just tell them they joined to bet on their market
|
|
|
|
reason:
|
|
|
|
sourceContract?.creatorId === relatedUserId
|
|
|
|
? 'user_joined_to_bet_on_your_market'
|
|
|
|
: 'you_referred_user',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-05 17:29:26 +00:00
|
|
|
const notifyContractCreatorOfUniqueBettorsBonus = async (
|
|
|
|
userToReasonTexts: user_to_reason_texts,
|
|
|
|
userId: string
|
|
|
|
) => {
|
|
|
|
userToReasonTexts[userId] = {
|
|
|
|
reason: 'unique_bettors_on_your_contract',
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const getUsersToNotify = async () => {
|
|
|
|
const userToReasonTexts: user_to_reason_texts = {}
|
|
|
|
// The following functions modify the userToReasonTexts object in place.
|
2022-07-01 13:47:19 +00:00
|
|
|
if (sourceType === 'follow' && relatedUserId) {
|
2022-06-06 17:36:59 +00:00
|
|
|
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
2022-06-22 16:35:50 +00:00
|
|
|
} else if (sourceType === 'group' && relatedUserId) {
|
|
|
|
if (sourceUpdateType === 'created')
|
|
|
|
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
|
2022-07-01 13:47:19 +00:00
|
|
|
} else if (sourceType === 'user' && relatedUserId) {
|
|
|
|
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
|
|
|
|
}
|
|
|
|
|
|
|
|
// The following functions need sourceContract to be defined.
|
|
|
|
if (!sourceContract) return userToReasonTexts
|
|
|
|
if (
|
|
|
|
sourceType === 'comment' ||
|
|
|
|
sourceType === 'answer' ||
|
|
|
|
(sourceType === 'contract' &&
|
|
|
|
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
|
|
|
) {
|
|
|
|
if (sourceType === 'comment') {
|
|
|
|
if (relatedUserId && relatedSourceType)
|
|
|
|
await notifyRepliedUsers(
|
|
|
|
userToReasonTexts,
|
|
|
|
relatedUserId,
|
|
|
|
relatedSourceType
|
|
|
|
)
|
|
|
|
if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText)
|
|
|
|
}
|
|
|
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
|
|
|
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
|
|
|
await notifyLiquidityProviders(userToReasonTexts, sourceContract)
|
|
|
|
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
|
|
|
|
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
|
|
|
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
|
|
|
await notifyUsersFollowers(userToReasonTexts)
|
|
|
|
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
|
|
|
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
|
|
|
force: true,
|
|
|
|
})
|
|
|
|
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
|
|
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
2022-07-05 17:29:26 +00:00
|
|
|
} else if (sourceType === 'bonus' && sourceUpdateType === 'created') {
|
|
|
|
// Note: the daily bonus won't have a contract attached to it
|
|
|
|
await notifyContractCreatorOfUniqueBettorsBonus(
|
|
|
|
userToReasonTexts,
|
|
|
|
sourceContract.creatorId
|
|
|
|
)
|
2022-06-06 17:36:59 +00:00
|
|
|
}
|
|
|
|
return userToReasonTexts
|
|
|
|
}
|
|
|
|
|
|
|
|
const userToReasonTexts = await getUsersToNotify()
|
|
|
|
await createUsersNotifications(userToReasonTexts)
|
2022-06-01 13:11:25 +00:00
|
|
|
}
|