Implement getFeed cloud function
This commit is contained in:
		
							parent
							
								
									a4696bc273
								
							
						
					
					
						commit
						db42761143
					
				
							
								
								
									
										188
									
								
								functions/src/get-feed.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								functions/src/get-feed.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,188 @@
 | 
			
		|||
import * as _ from 'lodash'
 | 
			
		||||
import * as functions from 'firebase-functions'
 | 
			
		||||
import * as admin from 'firebase-admin'
 | 
			
		||||
 | 
			
		||||
import { getValue, getValues } from './utils'
 | 
			
		||||
import { Contract } from '../../common/contract'
 | 
			
		||||
import { logInterpolation } from '../../common/util/math'
 | 
			
		||||
import { DAY_MS } from '../../common/util/time'
 | 
			
		||||
import {
 | 
			
		||||
  getProbability,
 | 
			
		||||
  getOutcomeProbability,
 | 
			
		||||
  getTopAnswer,
 | 
			
		||||
} from '../../common/calculate'
 | 
			
		||||
import { Bet } from '../../common/bet'
 | 
			
		||||
import { Comment } from '../../common/comment'
 | 
			
		||||
 | 
			
		||||
const firestore = admin.firestore()
 | 
			
		||||
 | 
			
		||||
const MAX_FEED_CONTRACTS = 60
 | 
			
		||||
 | 
			
		||||
export const getFeed = functions
 | 
			
		||||
  .runWith({ minInstances: 1 })
 | 
			
		||||
  .https.onCall(async (_data, context) => {
 | 
			
		||||
    const userId = context?.auth?.uid
 | 
			
		||||
    if (!userId) return { status: 'error', message: 'Not authorized' }
 | 
			
		||||
 | 
			
		||||
    // Get contracts bet on or created in last week.
 | 
			
		||||
    const contractsPromise = Promise.all([
 | 
			
		||||
      getValues<Contract>(
 | 
			
		||||
        firestore
 | 
			
		||||
          .collection('contracts')
 | 
			
		||||
          .where('isResolved', '==', false)
 | 
			
		||||
          .where('volume7Days', '>', 0)
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      getValues<Contract>(
 | 
			
		||||
        firestore
 | 
			
		||||
          .collection('contracts')
 | 
			
		||||
          .where('isResolved', '==', false)
 | 
			
		||||
          .where('createdTime', '>', Date.now() - DAY_MS * 7)
 | 
			
		||||
          .where('volume7Days', '==', 0)
 | 
			
		||||
      ),
 | 
			
		||||
    ]).then(([activeContracts, inactiveContracts]) => {
 | 
			
		||||
      const combined = [...activeContracts, ...inactiveContracts]
 | 
			
		||||
      // Remove closed contracts.
 | 
			
		||||
      return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const userCacheCollection = firestore.collection(
 | 
			
		||||
      `private-users/${userId}/cached`
 | 
			
		||||
    )
 | 
			
		||||
    const [recommendationScores, lastViewedTime] = await Promise.all([
 | 
			
		||||
      getValue<{ [contractId: string]: number }>(
 | 
			
		||||
        userCacheCollection.doc('contractScores')
 | 
			
		||||
      ),
 | 
			
		||||
      getValue<{ [contractId: string]: number }>(
 | 
			
		||||
        userCacheCollection.doc('lastViewTime')
 | 
			
		||||
      ),
 | 
			
		||||
    ]).then((dicts) => dicts.map((dict) => dict ?? {}))
 | 
			
		||||
 | 
			
		||||
    const contracts = await contractsPromise
 | 
			
		||||
 | 
			
		||||
    const averageRecScore =
 | 
			
		||||
      1 +
 | 
			
		||||
      _.sumBy(
 | 
			
		||||
        contracts.filter((c) => recommendationScores[c.id] !== undefined),
 | 
			
		||||
        (c) => recommendationScores[c.id]
 | 
			
		||||
      ) /
 | 
			
		||||
        (contracts.length + 1)
 | 
			
		||||
 | 
			
		||||
    console.log({ recommendationScores, averageRecScore, lastViewedTime })
 | 
			
		||||
 | 
			
		||||
    const scoredContracts = contracts.map((contract) => {
 | 
			
		||||
      const score = scoreContract(
 | 
			
		||||
        contract,
 | 
			
		||||
        recommendationScores[contract.id] ?? averageRecScore,
 | 
			
		||||
        lastViewedTime[contract.id]
 | 
			
		||||
      )
 | 
			
		||||
      return [contract, score] as [Contract, number]
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const sortedContracts = _.sortBy(
 | 
			
		||||
      scoredContracts,
 | 
			
		||||
      ([_, score]) => score
 | 
			
		||||
    ).reverse()
 | 
			
		||||
 | 
			
		||||
    console.log(sortedContracts.map(([c, score]) => c.question + ': ' + score))
 | 
			
		||||
 | 
			
		||||
    const feedContracts = sortedContracts
 | 
			
		||||
      .slice(0, MAX_FEED_CONTRACTS)
 | 
			
		||||
      .map(([c]) => c)
 | 
			
		||||
 | 
			
		||||
    const feed = await Promise.all(
 | 
			
		||||
      feedContracts.map((contract) => getRecentBetsAndComments(contract))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    console.log('feed', feed)
 | 
			
		||||
 | 
			
		||||
    return { status: 'success', feed }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
function scoreContract(
 | 
			
		||||
  contract: Contract,
 | 
			
		||||
  recommendationScore: number,
 | 
			
		||||
  viewTime: number | undefined
 | 
			
		||||
) {
 | 
			
		||||
  const lastViewedScore = getLastViewedScore(viewTime)
 | 
			
		||||
  const activityScore = getActivityScore(contract, viewTime)
 | 
			
		||||
  return recommendationScore * lastViewedScore * activityScore
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getActivityScore(contract: Contract, viewTime: number | undefined) {
 | 
			
		||||
  const { createdTime, lastBetTime, lastCommentTime, outcomeType } = contract
 | 
			
		||||
  const hasNewComments =
 | 
			
		||||
    lastCommentTime && (!viewTime || lastCommentTime > viewTime)
 | 
			
		||||
  const newCommentScore = hasNewComments ? 1 : 0.5
 | 
			
		||||
 | 
			
		||||
  const timeSinceLastBet = Date.now() - (lastBetTime ?? createdTime)
 | 
			
		||||
  const daysAgo = timeSinceLastBet / DAY_MS
 | 
			
		||||
  const betTimeScore = 1 - logInterpolation(0, 3, daysAgo)
 | 
			
		||||
 | 
			
		||||
  let prob = 0.5
 | 
			
		||||
  if (outcomeType === 'BINARY') {
 | 
			
		||||
    prob = getProbability(contract)
 | 
			
		||||
  } else if (outcomeType === 'FREE_RESPONSE') {
 | 
			
		||||
    const topAnswer = getTopAnswer(contract)
 | 
			
		||||
    if (topAnswer)
 | 
			
		||||
      prob = Math.max(0.5, getOutcomeProbability(contract, topAnswer.id))
 | 
			
		||||
  }
 | 
			
		||||
  const frac = 1 - Math.abs(prob - 0.5) ** 2 / 0.25
 | 
			
		||||
  const probScore = 0.5 + frac * 0.5
 | 
			
		||||
 | 
			
		||||
  const { volume24Hours, volume7Days, volume } = contract
 | 
			
		||||
  const combinedVolume =
 | 
			
		||||
    Math.log(volume24Hours + 1) +
 | 
			
		||||
    Math.log(volume7Days + 1) +
 | 
			
		||||
    Math.log(volume + 1)
 | 
			
		||||
  const volumeScore = 0.5 + 0.5 * logInterpolation(7, 35, combinedVolume)
 | 
			
		||||
 | 
			
		||||
  const score = newCommentScore * betTimeScore * probScore * volumeScore
 | 
			
		||||
 | 
			
		||||
  // Map score to [0.5, 1] since no recent activty is not a deal breaker.
 | 
			
		||||
  const mappedScore = 0.5 + 0.5 * score
 | 
			
		||||
  const newMappedScore = 0.7 + 0.3 * score
 | 
			
		||||
 | 
			
		||||
  const isNew = Date.now() < contract.createdTime + DAY_MS
 | 
			
		||||
  return isNew ? newMappedScore : mappedScore
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getLastViewedScore(viewTime: number | undefined) {
 | 
			
		||||
  if (viewTime === undefined) {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const daysAgo = (Date.now() - viewTime) / DAY_MS
 | 
			
		||||
 | 
			
		||||
  if (daysAgo < 0.5) {
 | 
			
		||||
    const frac = logInterpolation(0, 0.5, daysAgo)
 | 
			
		||||
    return 0.5 * frac
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const frac = logInterpolation(0.5, 14, daysAgo)
 | 
			
		||||
  return 0.5 + 0.5 * frac
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getRecentBetsAndComments(contract: Contract) {
 | 
			
		||||
  const contractDoc = firestore.collection('contracts').doc(contract.id)
 | 
			
		||||
 | 
			
		||||
  const [recentBets, recentComments] = await Promise.all([
 | 
			
		||||
    getValues<Bet>(
 | 
			
		||||
      contractDoc
 | 
			
		||||
        .collection('bets')
 | 
			
		||||
        .where('createdTime', '>', Date.now() - DAY_MS)
 | 
			
		||||
    ),
 | 
			
		||||
 | 
			
		||||
    getValues<Comment>(
 | 
			
		||||
      contractDoc
 | 
			
		||||
        .collection('comments')
 | 
			
		||||
        .where('createdTime', '>', Date.now() - 3 * DAY_MS)
 | 
			
		||||
    ),
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    contract,
 | 
			
		||||
    recentBets,
 | 
			
		||||
    recentComments,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -25,3 +25,4 @@ export * from './backup-db'
 | 
			
		|||
export * from './change-user-info'
 | 
			
		||||
export * from './market-close-emails'
 | 
			
		||||
export * from './add-liquidity'
 | 
			
		||||
export * from './get-feed'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,6 @@ export const onView = functions.firestore
 | 
			
		|||
      )
 | 
			
		||||
 | 
			
		||||
    await firestore
 | 
			
		||||
      .doc(`private-users/${userId}/cached/lastViewed`)
 | 
			
		||||
      .doc(`private-users/${userId}/cached/lastViewTime`)
 | 
			
		||||
      .set({ [contractId]: timestamp }, { merge: true })
 | 
			
		||||
  })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user