Allow to follow/unfollow markets, backfill as well
This commit is contained in:
parent
bea94d58c5
commit
89644502a5
|
@ -57,6 +57,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
|||
uniqueBettorIds?: string[]
|
||||
uniqueBettorCount?: number
|
||||
popularityScore?: number
|
||||
followerCount?: number
|
||||
} & T
|
||||
|
||||
export type BinaryContract = Contract & Binary
|
||||
|
|
|
@ -70,3 +70,4 @@ export type notification_reason_types =
|
|||
| 'challenge_accepted'
|
||||
| 'betting_streak_incremented'
|
||||
| 'loan_income'
|
||||
| 'you_follow_contract'
|
||||
|
|
|
@ -44,6 +44,11 @@ service cloud.firestore {
|
|||
allow read;
|
||||
}
|
||||
|
||||
match /contracts/{contractId}/follows/{userId} {
|
||||
allow read;
|
||||
allow create, delete: if userId == request.auth.uid;
|
||||
}
|
||||
|
||||
match /contracts/{contractId}/challenges/{challengeId}{
|
||||
allow read;
|
||||
allow create: if request.auth.uid == request.resource.data.creatorId;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log",
|
||||
"dev": "nodemon src/serve.ts",
|
||||
"firestore": "firebase emulators:start --only firestore --import=./firestore_export",
|
||||
"firestore": "firebase emulators:start --only firestore,pubsub --import=./firestore_export",
|
||||
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
|
||||
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
|
||||
"db:backup-local": "firebase emulators:export --force ./firestore_export",
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
} from '../../common/notification'
|
||||
import { User } from '../../common/user'
|
||||
import { Contract } from '../../common/contract'
|
||||
import { getValues } from './utils'
|
||||
import { getValues, log } from './utils'
|
||||
import { Comment } from '../../common/comment'
|
||||
import { uniq } from 'lodash'
|
||||
import { Bet, LimitBet } from '../../common/bet'
|
||||
|
@ -33,19 +33,12 @@ export const createNotification = async (
|
|||
sourceText: string,
|
||||
miscData?: {
|
||||
contract?: Contract
|
||||
relatedSourceType?: notification_source_types
|
||||
recipients?: string[]
|
||||
slug?: string
|
||||
title?: string
|
||||
}
|
||||
) => {
|
||||
const {
|
||||
contract: sourceContract,
|
||||
relatedSourceType,
|
||||
recipients,
|
||||
slug,
|
||||
title,
|
||||
} = miscData ?? {}
|
||||
const { contract: sourceContract, recipients, slug, title } = miscData ?? {}
|
||||
|
||||
const shouldGetNotification = (
|
||||
userId: string,
|
||||
|
@ -90,24 +83,6 @@ export const createNotification = async (
|
|||
)
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyUsersFollowers = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
|
@ -129,23 +104,6 @@ export const createNotification = async (
|
|||
})
|
||||
}
|
||||
|
||||
const notifyRepliedUser = (
|
||||
userToReasonTexts: user_to_reason_texts,
|
||||
relatedUserId: string,
|
||||
relatedSourceType: notification_source_types
|
||||
) => {
|
||||
if (!shouldGetNotification(relatedUserId, userToReasonTexts)) return
|
||||
if (relatedSourceType === 'comment') {
|
||||
userToReasonTexts[relatedUserId] = {
|
||||
reason: 'reply_to_users_comment',
|
||||
}
|
||||
} else if (relatedSourceType === 'answer') {
|
||||
userToReasonTexts[relatedUserId] = {
|
||||
reason: 'reply_to_users_answer',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyFollowedUser = (
|
||||
userToReasonTexts: user_to_reason_texts,
|
||||
followedUserId: string
|
||||
|
@ -182,71 +140,6 @@ export const createNotification = async (
|
|||
}
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyBettorsOnContract = async (
|
||||
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',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyUserAddedToGroup = (
|
||||
userToReasonTexts: user_to_reason_texts,
|
||||
relatedUserId: string
|
||||
|
@ -266,58 +159,289 @@ export const createNotification = async (
|
|||
}
|
||||
}
|
||||
|
||||
const getUsersToNotify = async () => {
|
||||
const userToReasonTexts: user_to_reason_texts = {}
|
||||
// The following functions modify the userToReasonTexts object in place.
|
||||
if (sourceType === 'follow' && recipients?.[0]) {
|
||||
notifyFollowedUser(userToReasonTexts, recipients[0])
|
||||
} else if (
|
||||
sourceType === 'group' &&
|
||||
sourceUpdateType === 'created' &&
|
||||
recipients
|
||||
) {
|
||||
recipients.forEach((r) => notifyUserAddedToGroup(userToReasonTexts, r))
|
||||
}
|
||||
const userToReasonTexts: user_to_reason_texts = {}
|
||||
// The following functions modify the userToReasonTexts object in place.
|
||||
|
||||
// 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 (recipients?.[0] && relatedSourceType)
|
||||
notifyRepliedUser(userToReasonTexts, recipients[0], relatedSourceType)
|
||||
if (sourceText) notifyTaggedUsers(userToReasonTexts, recipients ?? [])
|
||||
}
|
||||
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)
|
||||
notifyTaggedUsers(userToReasonTexts, recipients ?? [])
|
||||
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
||||
force: true,
|
||||
})
|
||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||
} else if (sourceType === 'bonus' && sourceUpdateType === 'created') {
|
||||
// Note: the daily bonus won't have a contract attached to it
|
||||
await notifyContractCreatorOfUniqueBettorsBonus(
|
||||
userToReasonTexts,
|
||||
sourceContract.creatorId
|
||||
)
|
||||
}
|
||||
return userToReasonTexts
|
||||
if (sourceType === 'follow' && recipients?.[0]) {
|
||||
notifyFollowedUser(userToReasonTexts, recipients[0])
|
||||
} else if (
|
||||
sourceType === 'group' &&
|
||||
sourceUpdateType === 'created' &&
|
||||
recipients
|
||||
) {
|
||||
recipients.forEach((r) => notifyUserAddedToGroup(userToReasonTexts, r))
|
||||
} else if (
|
||||
sourceType === 'contract' &&
|
||||
sourceUpdateType === 'created' &&
|
||||
sourceContract
|
||||
) {
|
||||
await notifyUsersFollowers(userToReasonTexts)
|
||||
notifyTaggedUsers(userToReasonTexts, recipients ?? [])
|
||||
} else if (
|
||||
sourceType === 'contract' &&
|
||||
sourceUpdateType === 'closed' &&
|
||||
sourceContract
|
||||
) {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
||||
force: true,
|
||||
})
|
||||
} else if (
|
||||
sourceType === 'liquidity' &&
|
||||
sourceUpdateType === 'created' &&
|
||||
sourceContract
|
||||
) {
|
||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||
} else if (
|
||||
sourceType === 'bonus' &&
|
||||
sourceUpdateType === 'created' &&
|
||||
sourceContract
|
||||
) {
|
||||
// Note: the daily bonus won't have a contract attached to it
|
||||
await notifyContractCreatorOfUniqueBettorsBonus(
|
||||
userToReasonTexts,
|
||||
sourceContract.creatorId
|
||||
)
|
||||
}
|
||||
|
||||
const userToReasonTexts = await getUsersToNotify()
|
||||
await createUsersNotifications(userToReasonTexts)
|
||||
}
|
||||
|
||||
export const createCommentOrAnswerOrUpdatedContractNotification = async (
|
||||
sourceId: string,
|
||||
sourceType: notification_source_types,
|
||||
sourceUpdateType: notification_source_update_types,
|
||||
sourceUser: User,
|
||||
idempotencyKey: string,
|
||||
sourceText: string,
|
||||
sourceContract: Contract,
|
||||
miscData?: {
|
||||
relatedSourceType?: notification_source_types
|
||||
repliedUserId?: string
|
||||
taggedUserIds?: string[]
|
||||
}
|
||||
) => {
|
||||
const { relatedSourceType, repliedUserId, taggedUserIds } = miscData ?? {}
|
||||
|
||||
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,
|
||||
sourceUpdateType,
|
||||
sourceContractId: sourceContract.id,
|
||||
sourceUserName: sourceUser.name,
|
||||
sourceUserUsername: sourceUser.username,
|
||||
sourceUserAvatarUrl: sourceUser.avatarUrl,
|
||||
sourceText,
|
||||
sourceContractCreatorUsername: sourceContract.creatorUsername,
|
||||
sourceContractTitle: sourceContract.question,
|
||||
sourceContractSlug: sourceContract.slug,
|
||||
sourceSlug: sourceContract.slug,
|
||||
sourceTitle: sourceContract.question,
|
||||
}
|
||||
await notificationRef.set(removeUndefinedProps(notification))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// get contract follower documents and check here if they're a follower
|
||||
const contractFollowersSnap = await firestore
|
||||
.collection(`contracts/${sourceContract.id}/follows`)
|
||||
.get()
|
||||
const contractFollowersIds = contractFollowersSnap.docs.map(
|
||||
(doc) => doc.data().id
|
||||
)
|
||||
log('contractFollowerIds', contractFollowersIds)
|
||||
|
||||
const stillFollowingContract = (userId: string) => {
|
||||
return contractFollowersIds.includes(userId)
|
||||
}
|
||||
|
||||
const shouldGetNotification = (
|
||||
userId: string,
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
return (
|
||||
sourceUser.id != userId &&
|
||||
!Object.keys(userToReasonTexts).includes(userId)
|
||||
)
|
||||
}
|
||||
|
||||
const notifyContractFollowers = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
for (const userId of contractFollowersIds) {
|
||||
if (shouldGetNotification(userId, userToReasonTexts))
|
||||
userToReasonTexts[userId] = {
|
||||
reason: 'you_follow_contract',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyContractCreator = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
if (
|
||||
shouldGetNotification(sourceContract.creatorId, userToReasonTexts) &&
|
||||
stillFollowingContract(sourceContract.creatorId)
|
||||
)
|
||||
userToReasonTexts[sourceContract.creatorId] = {
|
||||
reason: 'on_users_contract',
|
||||
}
|
||||
}
|
||||
|
||||
const notifyOtherAnswerersOnContract = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
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) &&
|
||||
stillFollowingContract(userId)
|
||||
)
|
||||
userToReasonTexts[userId] = {
|
||||
reason: 'on_contract_with_users_answer',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyOtherCommentersOnContract = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
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) &&
|
||||
stillFollowingContract(userId)
|
||||
)
|
||||
userToReasonTexts[userId] = {
|
||||
reason: 'on_contract_with_users_comment',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyBettorsOnContract = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
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) &&
|
||||
stillFollowingContract(userId)
|
||||
)
|
||||
userToReasonTexts[userId] = {
|
||||
reason: 'on_contract_with_users_shares_in',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyRepliedUser = (
|
||||
userToReasonTexts: user_to_reason_texts,
|
||||
relatedUserId: string,
|
||||
relatedSourceType: notification_source_types
|
||||
) => {
|
||||
if (
|
||||
shouldGetNotification(relatedUserId, userToReasonTexts) &&
|
||||
stillFollowingContract(relatedUserId)
|
||||
) {
|
||||
if (relatedSourceType === 'comment') {
|
||||
userToReasonTexts[relatedUserId] = {
|
||||
reason: 'reply_to_users_comment',
|
||||
}
|
||||
} else if (relatedSourceType === 'answer') {
|
||||
userToReasonTexts[relatedUserId] = {
|
||||
reason: 'reply_to_users_answer',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notifyTaggedUsers = (
|
||||
userToReasonTexts: user_to_reason_texts,
|
||||
userIds: (string | undefined)[]
|
||||
) => {
|
||||
userIds.forEach((id) => {
|
||||
console.log('tagged user: ', id)
|
||||
// Allowing non-following users to get tagged
|
||||
if (id && shouldGetNotification(id, userToReasonTexts))
|
||||
userToReasonTexts[id] = {
|
||||
reason: 'tagged_user',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const notifyLiquidityProviders = async (
|
||||
userToReasonTexts: user_to_reason_texts
|
||||
) => {
|
||||
const liquidityProviders = await firestore
|
||||
.collection(`contracts/${sourceContract.id}/liquidity`)
|
||||
.get()
|
||||
const liquidityProvidersIds = uniq(
|
||||
liquidityProviders.docs.map((doc) => doc.data().userId)
|
||||
)
|
||||
liquidityProvidersIds.forEach((userId) => {
|
||||
if (
|
||||
shouldGetNotification(userId, userToReasonTexts) &&
|
||||
stillFollowingContract(userId)
|
||||
) {
|
||||
userToReasonTexts[userId] = {
|
||||
reason: 'on_contract_with_users_shares_in',
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const userToReasonTexts: user_to_reason_texts = {}
|
||||
|
||||
if (sourceType === 'comment') {
|
||||
if (repliedUserId && relatedSourceType)
|
||||
notifyRepliedUser(userToReasonTexts, repliedUserId, relatedSourceType)
|
||||
if (sourceText) notifyTaggedUsers(userToReasonTexts, taggedUserIds ?? [])
|
||||
}
|
||||
await notifyContractCreator(userToReasonTexts)
|
||||
await notifyOtherAnswerersOnContract(userToReasonTexts)
|
||||
await notifyLiquidityProviders(userToReasonTexts)
|
||||
await notifyBettorsOnContract(userToReasonTexts)
|
||||
await notifyOtherCommentersOnContract(userToReasonTexts)
|
||||
// if they weren't added previously, add them now
|
||||
await notifyContractFollowers(userToReasonTexts)
|
||||
|
||||
await createUsersNotifications(userToReasonTexts)
|
||||
}
|
||||
|
||||
|
|
30
functions/src/follow-market.ts
Normal file
30
functions/src/follow-market.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Contract } from '../../common/lib/contract'
|
||||
import { User } from '../../common/lib/user'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { FieldValue } from 'firebase-admin/firestore'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const addUserToContractFollowers = async (
|
||||
contract: Contract,
|
||||
user: User
|
||||
) => {
|
||||
const followerDoc = await firestore
|
||||
.collection(`contracts/${contract.id}/follows`)
|
||||
.doc(user.id)
|
||||
.get()
|
||||
if (followerDoc.exists) return
|
||||
await firestore
|
||||
.collection(`contracts/${contract.id}/follows`)
|
||||
.doc(user.id)
|
||||
.set({
|
||||
id: user.id,
|
||||
createdTime: Date.now(),
|
||||
})
|
||||
await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contract.id)
|
||||
.update({
|
||||
followerCount: FieldValue.increment(1),
|
||||
})
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import { getContract, getUser } from './utils'
|
||||
import { createNotification } from './create-notification'
|
||||
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
export const onCreateAnswer = functions.firestore
|
||||
.document('contracts/{contractId}/answers/{answerNumber}')
|
||||
|
@ -20,14 +21,14 @@ export const onCreateAnswer = functions.firestore
|
|||
|
||||
const answerCreator = await getUser(answer.userId)
|
||||
if (!answerCreator) throw new Error('Could not find answer creator')
|
||||
|
||||
await createNotification(
|
||||
await addUserToContractFollowers(contract, answerCreator)
|
||||
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||
answer.id,
|
||||
'answer',
|
||||
'created',
|
||||
answerCreator,
|
||||
eventId,
|
||||
answer.text,
|
||||
{ contract }
|
||||
contract
|
||||
)
|
||||
})
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
} from '../../common/antes'
|
||||
import { APIError } from '../../common/api'
|
||||
import { User } from '../../common/user'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime()
|
||||
|
@ -54,14 +55,13 @@ export const onCreateBet = functions.firestore
|
|||
log(`Could not find contract ${contractId}`)
|
||||
return
|
||||
}
|
||||
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
||||
|
||||
const bettor = await getUser(bet.userId)
|
||||
if (!bettor) return
|
||||
|
||||
await addUserToContractFollowers(contract, bettor)
|
||||
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
||||
await notifyFills(bet, contract, eventId, bettor)
|
||||
await updateBettingStreak(bettor, bet, contract, eventId)
|
||||
|
||||
await firestore.collection('users').doc(bettor.id).update({ lastBetTime })
|
||||
})
|
||||
|
||||
|
|
|
@ -6,8 +6,9 @@ import { ContractComment } from '../../common/comment'
|
|||
import { sendNewCommentEmail } from './emails'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { Answer } from '../../common/answer'
|
||||
import { createNotification } from './create-notification'
|
||||
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
|
@ -35,6 +36,8 @@ export const onCreateCommentOnContract = functions
|
|||
const commentCreator = await getUser(comment.userId)
|
||||
if (!commentCreator) throw new Error('Could not find comment creator')
|
||||
|
||||
await addUserToContractFollowers(contract, commentCreator)
|
||||
|
||||
await firestore
|
||||
.collection('contracts')
|
||||
.doc(contract.id)
|
||||
|
@ -77,18 +80,19 @@ export const onCreateCommentOnContract = functions
|
|||
? comments.find((c) => c.id === comment.replyToCommentId)?.userId
|
||||
: answer?.userId
|
||||
|
||||
const recipients = uniq(
|
||||
compact([...parseMentions(comment.content), repliedUserId])
|
||||
)
|
||||
|
||||
await createNotification(
|
||||
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||
comment.id,
|
||||
'comment',
|
||||
'created',
|
||||
commentCreator,
|
||||
eventId,
|
||||
richTextToString(comment.content),
|
||||
{ contract, relatedSourceType, recipients }
|
||||
contract,
|
||||
{
|
||||
relatedSourceType,
|
||||
repliedUserId,
|
||||
taggedUserIds: compact(parseMentions(comment.content)),
|
||||
}
|
||||
)
|
||||
|
||||
const recipientUserIds = uniq([
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createNotification } from './create-notification'
|
|||
import { Contract } from '../../common/contract'
|
||||
import { parseMentions, richTextToString } from '../../common/util/parse'
|
||||
import { JSONContent } from '@tiptap/core'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
export const onCreateContract = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
|
@ -18,6 +19,7 @@ export const onCreateContract = functions
|
|||
|
||||
const desc = contract.description as JSONContent
|
||||
const mentioned = parseMentions(desc)
|
||||
await addUserToContractFollowers(contract, contractCreator)
|
||||
|
||||
await createNotification(
|
||||
contract.id,
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as functions from 'firebase-functions'
|
|||
import { getContract, getUser } from './utils'
|
||||
import { createNotification } from './create-notification'
|
||||
import { LiquidityProvision } from 'common/liquidity-provision'
|
||||
import { addUserToContractFollowers } from './follow-market'
|
||||
|
||||
export const onCreateLiquidityProvision = functions.firestore
|
||||
.document('contracts/{contractId}/liquidity/{liquidityId}')
|
||||
|
@ -18,6 +19,7 @@ export const onCreateLiquidityProvision = functions.firestore
|
|||
|
||||
const liquidityProvider = await getUser(liquidity.userId)
|
||||
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
||||
await addUserToContractFollowers(contract, liquidityProvider)
|
||||
|
||||
await createNotification(
|
||||
contract.id,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import { getUser } from './utils'
|
||||
import { createNotification } from './create-notification'
|
||||
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
|
||||
import { Contract } from '../../common/contract'
|
||||
|
||||
export const onUpdateContract = functions.firestore
|
||||
|
@ -29,14 +29,14 @@ export const onUpdateContract = functions.firestore
|
|||
resolutionText = `${contract.resolutionValue}`
|
||||
}
|
||||
|
||||
await createNotification(
|
||||
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||
contract.id,
|
||||
'contract',
|
||||
'resolved',
|
||||
contractUpdater,
|
||||
eventId,
|
||||
resolutionText,
|
||||
{ contract }
|
||||
contract
|
||||
)
|
||||
} else if (
|
||||
previousValue.closeTime !== contract.closeTime ||
|
||||
|
@ -52,14 +52,14 @@ export const onUpdateContract = functions.firestore
|
|||
sourceText = contract.question
|
||||
}
|
||||
|
||||
await createNotification(
|
||||
await createCommentOrAnswerOrUpdatedContractNotification(
|
||||
contract.id,
|
||||
'contract',
|
||||
'updated',
|
||||
contractUpdater,
|
||||
eventId,
|
||||
sourceText,
|
||||
{ contract }
|
||||
contract
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
74
functions/src/scripts/backfill-contract-followers.ts
Normal file
74
functions/src/scripts/backfill-contract-followers.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { initAdmin } from './script-init'
|
||||
initAdmin()
|
||||
|
||||
import { getValues } from '../utils'
|
||||
import { Contract } from 'common/lib/contract'
|
||||
import { Comment } from 'common/lib/comment'
|
||||
import { uniq } from 'lodash'
|
||||
import { Bet } from 'common/lib/bet'
|
||||
import {
|
||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||
} from 'common/lib/antes'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
async function backfillContractFollowers() {
|
||||
console.log('Backfilling contract followers')
|
||||
const contracts = await getValues<Contract>(
|
||||
firestore.collection('contracts').where('isResolved', '==', false)
|
||||
)
|
||||
let count = 0
|
||||
for (const contract of contracts) {
|
||||
const comments = await getValues<Comment>(
|
||||
firestore.collection('contracts').doc(contract.id).collection('comments')
|
||||
)
|
||||
const commenterIds = uniq(comments.map((comment) => comment.userId))
|
||||
const betsSnap = await firestore
|
||||
.collection(`contracts/${contract.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 bettorIds = uniq(bets.map((bet) => bet.userId))
|
||||
const liquidityProviders = await firestore
|
||||
.collection(`contracts/${contract.id}/liquidity`)
|
||||
.get()
|
||||
const liquidityProvidersIds = uniq(
|
||||
liquidityProviders.docs.map((doc) => doc.data().userId)
|
||||
// exclude free market liquidity provider
|
||||
).filter(
|
||||
(id) =>
|
||||
id !== HOUSE_LIQUIDITY_PROVIDER_ID ||
|
||||
id !== DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
)
|
||||
const followerIds = uniq([
|
||||
...commenterIds,
|
||||
...bettorIds,
|
||||
...liquidityProvidersIds,
|
||||
contract.creatorId,
|
||||
])
|
||||
for (const followerId of followerIds) {
|
||||
await firestore
|
||||
.collection(`contracts/${contract.id}/follows`)
|
||||
.doc(followerId)
|
||||
.set({ id: followerId, createdTime: Date.now() })
|
||||
}
|
||||
const followerCount = followerIds.length
|
||||
await firestore
|
||||
.collection(`contracts`)
|
||||
.doc(contract.id)
|
||||
.update({ followerCount: followerCount })
|
||||
count += 1
|
||||
if (count % 100 === 0) {
|
||||
console.log(`${count} contracts processed`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
backfillContractFollowers()
|
||||
.then(() => process.exit())
|
||||
.catch(console.log)
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import clsx from 'clsx'
|
||||
import { ShareIcon } from '@heroicons/react/outline'
|
||||
import { EyeIcon, EyeOffIcon, ShareIcon } from '@heroicons/react/outline'
|
||||
|
||||
import { Row } from '../layout/row'
|
||||
import { Contract } from 'web/lib/firebase/contracts'
|
||||
import { Contract, contracts } from 'web/lib/firebase/contracts'
|
||||
import { useState } from 'react'
|
||||
import { Button } from 'web/components/button'
|
||||
import { CreateChallengeModal } from '../challenges/create-challenge-modal'
|
||||
|
@ -10,6 +10,9 @@ import { User } from 'common/user'
|
|||
import { CHALLENGES_ENABLED } from 'common/challenge'
|
||||
import { ShareModal } from './share-modal'
|
||||
import { withTracking } from 'web/lib/service/analytics'
|
||||
import { collection, deleteDoc, doc } from 'firebase/firestore'
|
||||
import { users } from 'web/lib/firebase/users'
|
||||
import { useContractFollows } from 'web/hooks/use-follows'
|
||||
|
||||
export function ShareRow(props: {
|
||||
contract: Contract
|
||||
|
@ -23,6 +26,7 @@ export function ShareRow(props: {
|
|||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isShareOpen, setShareOpen] = useState(false)
|
||||
const followers = useContractFollows(contract.id)
|
||||
|
||||
return (
|
||||
<Row className="mt-2">
|
||||
|
@ -62,6 +66,38 @@ export function ShareRow(props: {
|
|||
/>
|
||||
</Button>
|
||||
)}
|
||||
{user && (
|
||||
<Button
|
||||
size={'lg'}
|
||||
color={'gray-white'}
|
||||
onClick={async () => {
|
||||
// remove user doc from contract follows collection
|
||||
const followDoc = doc(
|
||||
collection(contracts, contract.id, 'follows'),
|
||||
user.id
|
||||
)
|
||||
await deleteDoc(followDoc)
|
||||
}}
|
||||
>
|
||||
{followers?.includes(user.id) ? (
|
||||
<Row>
|
||||
<EyeOffIcon
|
||||
className={clsx('mr-2 h-[24px] w-5')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Unfollow
|
||||
</Row>
|
||||
) : (
|
||||
<Row>
|
||||
<EyeIcon
|
||||
className={clsx('mr-2 h-[24px] w-5')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Follow
|
||||
</Row>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { listenForFollowers, listenForFollows } from 'web/lib/firebase/users'
|
||||
import { contracts, listenForContractFollows } from 'web/lib/firebase/contracts'
|
||||
|
||||
export const useFollows = (userId: string | null | undefined) => {
|
||||
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
||||
|
@ -29,3 +30,13 @@ export const useFollowers = (userId: string | undefined) => {
|
|||
|
||||
return followerIds
|
||||
}
|
||||
|
||||
export const useContractFollows = (contractId: string) => {
|
||||
const [followIds, setFollowIds] = useState<string[] | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
return listenForContractFollows(contractId, setFollowIds)
|
||||
}, [contractId])
|
||||
|
||||
return followIds
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { Bet } from 'common/bet'
|
|||
import { Comment } from 'common/comment'
|
||||
import { ENV_CONFIG } from 'common/envs/constants'
|
||||
import { getBinaryProb } from 'common/contract-details'
|
||||
import { users } from 'web/lib/firebase/users'
|
||||
|
||||
export const contracts = coll<Contract>('contracts')
|
||||
|
||||
|
@ -212,6 +213,16 @@ export function listenForContract(
|
|||
return listenForValue<Contract>(contractRef, setContract)
|
||||
}
|
||||
|
||||
export function listenForContractFollows(
|
||||
contractId: string,
|
||||
setFollowIds: (followIds: string[]) => void
|
||||
) {
|
||||
const follows = collection(contracts, contractId, 'follows')
|
||||
return listenForValues<{ id: string }>(follows, (docs) =>
|
||||
setFollowIds(docs.map(({ id }) => id))
|
||||
)
|
||||
}
|
||||
|
||||
function chooseRandomSubset(contracts: Contract[], count: number) {
|
||||
const fiveMinutes = 5 * 60 * 1000
|
||||
const seed = Math.round(Date.now() / fiveMinutes).toString()
|
||||
|
|
Loading…
Reference in New Issue
Block a user