Private user (#32)
* PrivateUser * createUser: create private user; detect multiple signups * include user properties in private user * script: create private users * unsubscribing from market resolution emails * track total deposits
This commit is contained in:
parent
21949abbe1
commit
4528615863
|
@ -7,8 +7,20 @@ export type User = {
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
|
|
||||||
balance: number
|
balance: number
|
||||||
|
totalDeposits: number
|
||||||
totalPnLCached: number
|
totalPnLCached: number
|
||||||
creatorVolumeCached: number
|
creatorVolumeCached: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STARTING_BALANCE = 1000
|
export const STARTING_BALANCE = 1000
|
||||||
|
export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person
|
||||||
|
|
||||||
|
export type PrivateUser = {
|
||||||
|
id: string // same as User.id
|
||||||
|
username: string // denormalized from User
|
||||||
|
|
||||||
|
email?: string
|
||||||
|
unsubscribedFromResolutionEmails?: boolean
|
||||||
|
initialDeviceToken?: string
|
||||||
|
initialIpAddress?: string
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { STARTING_BALANCE, User } from '../../common/user'
|
import {
|
||||||
|
PrivateUser,
|
||||||
|
STARTING_BALANCE,
|
||||||
|
SUS_STARTING_BALANCE,
|
||||||
|
User,
|
||||||
|
} from '../../common/user'
|
||||||
import { getUser, getUserByUsername } from './utils'
|
import { getUser, getUserByUsername } from './utils'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
|
|
||||||
export const createUser = functions
|
export const createUser = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
.https.onCall(async (_, context) => {
|
.https.onCall(async (data: { deviceToken?: string }, context) => {
|
||||||
const userId = context?.auth?.uid
|
const userId = context?.auth?.uid
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
|
@ -34,12 +39,23 @@ export const createUser = functions
|
||||||
|
|
||||||
const avatarUrl = fbUser.photoURL
|
const avatarUrl = fbUser.photoURL
|
||||||
|
|
||||||
|
const { deviceToken } = data
|
||||||
|
const deviceUsedBefore =
|
||||||
|
!deviceToken || (await isPrivateUserWithDeviceToken(deviceToken))
|
||||||
|
|
||||||
|
const ipAddress = context.rawRequest.ip
|
||||||
|
const ipCount = ipAddress ? await numberUsersWithIp(ipAddress) : 0
|
||||||
|
|
||||||
|
const balance =
|
||||||
|
deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE
|
||||||
|
|
||||||
const user: User = {
|
const user: User = {
|
||||||
id: userId,
|
id: userId,
|
||||||
name,
|
name,
|
||||||
username,
|
username,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
balance: STARTING_BALANCE,
|
balance,
|
||||||
|
totalDeposits: balance,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
totalPnLCached: 0,
|
totalPnLCached: 0,
|
||||||
creatorVolumeCached: 0,
|
creatorVolumeCached: 0,
|
||||||
|
@ -48,6 +64,16 @@ export const createUser = functions
|
||||||
await firestore.collection('users').doc(userId).create(user)
|
await firestore.collection('users').doc(userId).create(user)
|
||||||
console.log('created user', username, 'firebase id:', userId)
|
console.log('created user', username, 'firebase id:', userId)
|
||||||
|
|
||||||
|
const privateUser: PrivateUser = {
|
||||||
|
id: userId,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
initialIpAddress: ipAddress,
|
||||||
|
initialDeviceToken: deviceToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
await firestore.collection('private-users').doc(userId).create(privateUser)
|
||||||
|
|
||||||
return { status: 'success', user }
|
return { status: 'success', user }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -60,3 +86,21 @@ const cleanUsername = (name: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
const isPrivateUserWithDeviceToken = async (deviceToken: string) => {
|
||||||
|
const snap = await firestore
|
||||||
|
.collection('private-users')
|
||||||
|
.where('initialDeviceToken', '==', deviceToken)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
return !snap.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberUsersWithIp = async (ipAddress: string) => {
|
||||||
|
const snap = await firestore
|
||||||
|
.collection('private-users')
|
||||||
|
.where('initialIpAddress', '==', ipAddress)
|
||||||
|
.get()
|
||||||
|
|
||||||
|
return snap.docs.length
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import * as admin from 'firebase-admin'
|
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { sendTemplateEmail } from './send-email'
|
import { sendTemplateEmail } from './send-email'
|
||||||
import { getUser } from './utils'
|
import { getPrivateUser, getUser } from './utils'
|
||||||
|
|
||||||
type market_resolved_template = {
|
type market_resolved_template = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -21,13 +19,17 @@ export const sendMarketResolutionEmail = async (
|
||||||
contract: Contract,
|
contract: Contract,
|
||||||
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
resolution: 'YES' | 'NO' | 'CANCEL' | 'MKT'
|
||||||
) => {
|
) => {
|
||||||
|
const privateUser = await getPrivateUser(userId)
|
||||||
|
if (
|
||||||
|
!privateUser ||
|
||||||
|
privateUser.unsubscribedFromResolutionEmails ||
|
||||||
|
!privateUser.email
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
const user = await getUser(userId)
|
const user = await getUser(userId)
|
||||||
if (!user) return
|
if (!user) return
|
||||||
|
|
||||||
const fbUser = await admin.auth().getUser(userId)
|
|
||||||
const email = fbUser.email
|
|
||||||
if (!email) return
|
|
||||||
|
|
||||||
const outcome = toDisplayResolution[resolution]
|
const outcome = toDisplayResolution[resolution]
|
||||||
|
|
||||||
const subject = `Resolved ${outcome}: ${contract.question}`
|
const subject = `Resolved ${outcome}: ${contract.question}`
|
||||||
|
@ -45,7 +47,12 @@ export const sendMarketResolutionEmail = async (
|
||||||
// https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
|
// https://app.mailgun.com/app/sending/domains/mg.manifold.markets/templates/edit/market-resolved/initial
|
||||||
// Mailgun username: james@mantic.markets
|
// Mailgun username: james@mantic.markets
|
||||||
|
|
||||||
await sendTemplateEmail(email, subject, 'market-resolved', templateData)
|
await sendTemplateEmail(
|
||||||
|
privateUser.email,
|
||||||
|
subject,
|
||||||
|
'market-resolved',
|
||||||
|
templateData
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' }
|
const toDisplayResolution = { YES: 'YES', NO: 'NO', CANCEL: 'N/A', MKT: 'MKT' }
|
||||||
|
|
|
@ -10,5 +10,6 @@ export * from './stripe'
|
||||||
export * from './sell-bet'
|
export * from './sell-bet'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
|
export * from './unsubscribe'
|
||||||
export * from './update-contract-metrics'
|
export * from './update-contract-metrics'
|
||||||
export * from './update-user-metrics'
|
export * from './update-user-metrics'
|
||||||
|
|
54
functions/src/scripts/create-private-users.ts
Normal file
54
functions/src/scripts/create-private-users.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
|
||||||
|
import { PrivateUser, STARTING_BALANCE, User } from '../../../common/user'
|
||||||
|
|
||||||
|
// Generate your own private key, and set the path below:
|
||||||
|
// https://console.firebase.google.com/u/0/project/mantic-markets/settings/serviceaccounts/adminsdk
|
||||||
|
|
||||||
|
const serviceAccount = require('../../../../../../Downloads/dev-mantic-markets-firebase-adminsdk-sir5m-b2d27f8970.json')
|
||||||
|
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount),
|
||||||
|
})
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const snap = await firestore.collection('users').get()
|
||||||
|
const users = snap.docs.map((d) => d.data() as User)
|
||||||
|
|
||||||
|
for (let user of users) {
|
||||||
|
const fbUser = await admin.auth().getUser(user.id)
|
||||||
|
const email = fbUser.email
|
||||||
|
const { username } = user
|
||||||
|
|
||||||
|
const privateUser: PrivateUser = {
|
||||||
|
id: user.id,
|
||||||
|
email,
|
||||||
|
username,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.totalDeposits === undefined) {
|
||||||
|
await firestore
|
||||||
|
.collection('users')
|
||||||
|
.doc(user.id)
|
||||||
|
.update({ totalDeposits: STARTING_BALANCE })
|
||||||
|
|
||||||
|
console.log('set starting balance for:', user.username)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await firestore
|
||||||
|
.collection('private-users')
|
||||||
|
.doc(user.id)
|
||||||
|
.create(privateUser)
|
||||||
|
|
||||||
|
console.log('created private user for:', user.username)
|
||||||
|
} catch (_) {
|
||||||
|
// private user already created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) main().then(() => process.exit())
|
|
@ -129,7 +129,7 @@ const issueMoneys = async (session: any) => {
|
||||||
|
|
||||||
await firestore.collection('stripe-transactions').add(transaction)
|
await firestore.collection('stripe-transactions').add(transaction)
|
||||||
|
|
||||||
await payUser(userId, payout)
|
await payUser(userId, payout, true)
|
||||||
|
|
||||||
console.log('user', userId, 'paid M$', payout)
|
console.log('user', userId, 'paid M$', payout)
|
||||||
}
|
}
|
||||||
|
|
33
functions/src/unsubscribe.ts
Normal file
33
functions/src/unsubscribe.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import * as _ from 'lodash'
|
||||||
|
import { getPrivateUser } from './utils'
|
||||||
|
import { PrivateUser } from '../../common/user'
|
||||||
|
|
||||||
|
export const unsubscribe = functions
|
||||||
|
.runWith({ minInstances: 1 })
|
||||||
|
.https.onRequest(async (req, res) => {
|
||||||
|
let id = req.query.id as string
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
let privateUser = await getPrivateUser(id)
|
||||||
|
|
||||||
|
if (privateUser) {
|
||||||
|
let { username } = privateUser
|
||||||
|
|
||||||
|
const update: Partial<PrivateUser> = {
|
||||||
|
unsubscribedFromResolutionEmails: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
await firestore.collection('private-users').doc(id).update(update)
|
||||||
|
|
||||||
|
res.send(
|
||||||
|
username +
|
||||||
|
', you have been unsubscribed from market resolution emails on Manifold Markets.'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
res.send('This user is not currently subscribed or does not exist.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const firestore = admin.firestore()
|
|
@ -1,7 +1,7 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { PrivateUser, User } from '../../common/user'
|
||||||
|
|
||||||
export const getValue = async <T>(collection: string, doc: string) => {
|
export const getValue = async <T>(collection: string, doc: string) => {
|
||||||
const snap = await admin.firestore().collection(collection).doc(doc).get()
|
const snap = await admin.firestore().collection(collection).doc(doc).get()
|
||||||
|
@ -22,6 +22,10 @@ export const getUser = (userId: string) => {
|
||||||
return getValue<User>('users', userId)
|
return getValue<User>('users', userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPrivateUser = (userId: string) => {
|
||||||
|
return getValue<PrivateUser>('private-users', userId)
|
||||||
|
}
|
||||||
|
|
||||||
export const getUserByUsername = async (username: string) => {
|
export const getUserByUsername = async (username: string) => {
|
||||||
const snap = await firestore
|
const snap = await firestore
|
||||||
.collection('users')
|
.collection('users')
|
||||||
|
@ -33,7 +37,11 @@ export const getUserByUsername = async (username: string) => {
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
const updateUserBalance = (userId: string, delta: number) => {
|
const updateUserBalance = (
|
||||||
|
userId: string,
|
||||||
|
delta: number,
|
||||||
|
isDeposit = false
|
||||||
|
) => {
|
||||||
return firestore.runTransaction(async (transaction) => {
|
return firestore.runTransaction(async (transaction) => {
|
||||||
const userDoc = firestore.doc(`users/${userId}`)
|
const userDoc = firestore.doc(`users/${userId}`)
|
||||||
const userSnap = await transaction.get(userDoc)
|
const userSnap = await transaction.get(userDoc)
|
||||||
|
@ -47,15 +55,20 @@ const updateUserBalance = (userId: string, delta: number) => {
|
||||||
`User (${userId}) balance cannot be negative: ${newUserBalance}`
|
`User (${userId}) balance cannot be negative: ${newUserBalance}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isDeposit) {
|
||||||
|
const newTotalDeposits = (user.totalDeposits || 0) + delta
|
||||||
|
transaction.update(userDoc, { totalDeposits: newTotalDeposits })
|
||||||
|
}
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newUserBalance })
|
transaction.update(userDoc, { balance: newUserBalance })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const payUser = (userId: string, payout: number) => {
|
export const payUser = (userId: string, payout: number, isDeposit = false) => {
|
||||||
if (!isFinite(payout) || payout <= 0)
|
if (!isFinite(payout) || payout <= 0)
|
||||||
throw new Error('Payout is not positive: ' + payout)
|
throw new Error('Payout is not positive: ' + payout)
|
||||||
|
|
||||||
return updateUserBalance(userId, payout)
|
return updateUserBalance(userId, payout, isDeposit)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chargeUser = (userId: string, charge: number) => {
|
export const chargeUser = (userId: string, charge: number) => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { getFunctions, httpsCallable } from 'firebase/functions'
|
import { getFunctions, httpsCallable } from 'firebase/functions'
|
||||||
import { User } from '../../../common/user'
|
import { User } from '../../../common/user'
|
||||||
|
import { randomString } from '../../../common/util/random'
|
||||||
|
|
||||||
const functions = getFunctions()
|
const functions = getFunctions()
|
||||||
|
|
||||||
|
@ -13,7 +14,14 @@ export const resolveMarket = cloudFunction('resolveMarket')
|
||||||
|
|
||||||
export const sellBet = cloudFunction('sellBet')
|
export const sellBet = cloudFunction('sellBet')
|
||||||
|
|
||||||
export const createUser: () => Promise<User | null> = () =>
|
export const createUser: () => Promise<User | null> = () => {
|
||||||
cloudFunction('createUser')({})
|
let deviceToken = window.localStorage.getItem('device-token')
|
||||||
|
if (!deviceToken) {
|
||||||
|
deviceToken = randomString()
|
||||||
|
window.localStorage.setItem('device-token', deviceToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloudFunction('createUser')({ deviceToken })
|
||||||
.then((r) => (r.data as any)?.user || null)
|
.then((r) => (r.data as any)?.user || null)
|
||||||
.catch(() => null)
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user