import { doc, setDoc, getDoc, collection, query, where, limit, getDocs, orderBy, updateDoc, deleteDoc, collectionGroup, onSnapshot, Query, } from 'firebase/firestore' import { getAuth } from 'firebase/auth' import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth' import { app, db } from './init' import { PortfolioMetrics, PrivateUser, User } from 'common/user' import { coll, getValues, listenForValue, listenForValues } from './utils' import { safeLocalStorage } from '../util/local' import { filterDefined } from 'common/util/array' import { addUserToGroupViaId } from 'web/lib/firebase/groups' import { removeUndefinedProps } from 'common/util/object' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' dayjs.extend(utc) import { track } from '@amplitude/analytics-browser' import { Like } from 'common/like' export const users = coll('users') export const privateUsers = coll('private-users') export type { User } export type UserAndPrivateUser = { user: User; privateUser: PrivateUser } export type Period = 'daily' | 'weekly' | 'monthly' | 'allTime' export const auth = getAuth(app) export async function getUser(userId: string) { /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ return (await getDoc(doc(users, userId))).data()! } export async function getPrivateUser(userId: string) { /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ return (await getDoc(doc(privateUsers, userId))).data()! } export async function getUserAndPrivateUser(userId: string) { const [user, privateUser] = ( await Promise.all([ getDoc(doc(users, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion getDoc(doc(privateUsers, userId))!, // eslint-disable-line @typescript-eslint/no-non-null-assertion ]) ).map((d) => d.data()) as [User, PrivateUser] return { user, privateUser } as UserAndPrivateUser } export async function getUserByUsername(username: string) { // Find a user whose username matches the given username, or null if no such user exists. const q = query(users, where('username', '==', username), limit(1)) const docs = (await getDocs(q)).docs return docs.length > 0 ? docs[0].data() : null } export async function setUser(userId: string, user: User) { await setDoc(doc(users, userId), user) } export async function updateUser(userId: string, update: Partial) { await updateDoc(doc(users, userId), { ...update }) } export async function updatePrivateUser( userId: string, update: Partial ) { await updateDoc(doc(privateUsers, userId), { ...update }) } export function listenForUser( userId: string, setUser: (user: User | null) => void ) { const userRef = doc(users, userId) return listenForValue(userRef, setUser) } export function listenForPrivateUser( userId: string, setPrivateUser: (privateUser: PrivateUser | null) => void ) { const userRef = doc(privateUsers, userId) return listenForValue(userRef, setPrivateUser) } const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY' const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY' const CACHED_REFERRAL_GROUP_ID_KEY = 'CACHED_REFERRAL_GROUP_KEY' export function writeReferralInfo( defaultReferrerUsername: string, otherOptions?: { contractId?: string overwriteReferralUsername?: string groupId?: string } ) { const local = safeLocalStorage() const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) const { contractId, overwriteReferralUsername, groupId } = otherOptions || {} // Write the first referral username we see. if (!cachedReferralUser) local?.setItem( CACHED_REFERRAL_USERNAME_KEY, overwriteReferralUsername || defaultReferrerUsername ) // If an explicit referral query is passed, overwrite the cached referral username. if (overwriteReferralUsername) local?.setItem(CACHED_REFERRAL_USERNAME_KEY, overwriteReferralUsername) // Always write the most recent explicit group invite query value if (groupId) local?.setItem(CACHED_REFERRAL_GROUP_ID_KEY, groupId) // Write the first contract id that we see. const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY) if (!cachedReferralContract && contractId) local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId) } export async function setCachedReferralInfoForUser(user: User | null) { if (!user || user.referredByUserId) return // if the user wasn't created in the last minute, don't bother const now = dayjs().utc() const userCreatedTime = dayjs(user.createdTime) if (now.diff(userCreatedTime, 'minute') > 5) return const local = safeLocalStorage() const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY) const cachedReferralContractId = local?.getItem( CACHED_REFERRAL_CONTRACT_ID_KEY ) const cachedReferralGroupId = local?.getItem(CACHED_REFERRAL_GROUP_ID_KEY) // get user via username if (cachedReferralUsername) getUserByUsername(cachedReferralUsername).then((referredByUser) => { if (!referredByUser) return // update user's referralId updateUser( user.id, removeUndefinedProps({ referredByUserId: referredByUser.id, referredByContractId: cachedReferralContractId ? cachedReferralContractId : undefined, referredByGroupId: cachedReferralGroupId ? cachedReferralGroupId : undefined, }) ) .catch((err) => { console.log('error setting referral details', err) }) .then(() => { track('Referral', { userId: user.id, referredByUserId: referredByUser.id, referredByContractId: cachedReferralContractId, referredByGroupId: cachedReferralGroupId, }) }) }) if (cachedReferralGroupId) addUserToGroupViaId(cachedReferralGroupId, user.id) local?.removeItem(CACHED_REFERRAL_GROUP_ID_KEY) local?.removeItem(CACHED_REFERRAL_USERNAME_KEY) local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY) } export async function firebaseLogin() { const provider = new GoogleAuthProvider() return signInWithPopup(auth, provider) } export async function firebaseLogout() { await auth.signOut() } export async function listUsers(userIds: string[]) { if (userIds.length > 10) { throw new Error('Too many users requested at once; Firestore limits to 10') } const q = query(users, where('id', 'in', userIds)) const docs = (await getDocs(q)).docs return docs.map((doc) => doc.data()) } export async function listAllUsers() { const docs = (await getDocs(users)).docs return docs.map((doc) => doc.data()) } export function getTopTraders(period: Period) { const topTraders = query( users, orderBy('profitCached.' + period, 'desc'), limit(20) ) return getValues(topTraders) } export function getTopCreators(period: Period) { const topCreators = query( users, orderBy('creatorVolumeCached.' + period, 'desc'), limit(20) ) return getValues(topCreators) } export async function getTopFollowed() { return (await getValues(topFollowedQuery)).slice(0, 20) } const topFollowedQuery = query( users, orderBy('followerCountCached', 'desc'), limit(20) ) export function getUsers() { return getValues(users) } export async function follow(userId: string, followedUserId: string) { const followDoc = doc(collection(users, userId, 'follows'), followedUserId) await setDoc(followDoc, { userId: followedUserId, timestamp: Date.now(), }) } export async function unfollow(userId: string, unfollowedUserId: string) { const followDoc = doc(collection(users, userId, 'follows'), unfollowedUserId) await deleteDoc(followDoc) } export function getPortfolioHistory(userId: string, since: number) { return getValues(getPortfolioHistoryQuery(userId, since)) } export function getPortfolioHistoryQuery(userId: string, since: number) { return query( collectionGroup(db, 'portfolioHistory'), where('userId', '==', userId), where('timestamp', '>=', since), orderBy('timestamp', 'asc') ) as Query } export function listenForFollows( userId: string, setFollowIds: (followIds: string[]) => void ) { const follows = collection(users, userId, 'follows') return listenForValues<{ userId: string }>(follows, (docs) => setFollowIds(docs.map(({ userId }) => userId)) ) } export function listenForFollowers( userId: string, setFollowerIds: (followerIds: string[]) => void ) { const followersQuery = query( collectionGroup(db, 'follows'), where('userId', '==', userId) ) return onSnapshot( followersQuery, { includeMetadataChanges: true }, (snapshot) => { if (snapshot.metadata.fromCache) return const values = snapshot.docs.map((doc) => doc.ref.parent.parent?.id) setFollowerIds(filterDefined(values)) } ) } export function listenForReferrals( userId: string, setReferralIds: (referralIds: string[]) => void ) { const referralsQuery = query( collection(db, 'users'), where('referredByUserId', '==', userId) ) return onSnapshot( referralsQuery, { includeMetadataChanges: true }, (snapshot) => { if (snapshot.metadata.fromCache) return const values = snapshot.docs.map((doc) => doc.ref.id) setReferralIds(filterDefined(values)) } ) } export function listenForLikes( userId: string, setLikes: (likes: Like[]) => void ) { const likes = collection(users, userId, 'likes') return listenForValues(likes, (docs) => setLikes(docs)) }