Skip to content

Commit

Permalink
Migrate comments (manifoldmarkets#2411)
Browse files Browse the repository at this point in the history
* make updateData more general

* Migrate comment writes to supabase

* autoformat

* alter column fs_updated_time drop not null

* Inline on create comment trigger into api call

* Don't update denormalized user data in comments

* don't cascade visibility changes

we'll have to implement - tho imo this isn't correct behavior anyways

* Fix typo and make more robust

* undelete delete-comments.ts to be fair

there's a bunch of other scripts that also won't work
  • Loading branch information
sipec authored Feb 5, 2024
1 parent e4690b7 commit ebea55f
Show file tree
Hide file tree
Showing 21 changed files with 441 additions and 466 deletions.
63 changes: 37 additions & 26 deletions backend/api/src/award-bounty.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { ContractComment } from 'common/comment'
import * as admin from 'firebase-admin'
import { runAwardBountyTxn } from 'shared/txn/run-bounty-txn'
import { APIError, type APIHandler } from './helpers/endpoint'
import { FieldValue } from 'firebase-admin/firestore'
import { type APIHandler } from './helpers/endpoint'
import { createBountyAwardedNotification } from 'shared/create-notification'
import { getContract } from 'shared/utils'
import {
createSupabaseClient,
createSupabaseDirectClient,
} from 'shared/supabase/init'
import { getComment } from 'shared/supabase/contract_comments'
import { updateData } from 'shared/supabase/utils'

export const awardBounty: APIHandler<
'market/:contractId/award-bounty'
> = async (props, auth) => {
> = async (props, auth, { logError }) => {
const { contractId, commentId, amount } = props

// run as transaction to prevent race conditions
return await firestore.runTransaction(async (transaction) => {
const commentDoc = firestore.doc(
`contracts/${contractId}/comments/${commentId}`
)
const commentSnap = await transaction.get(commentDoc)
if (!commentSnap.exists) throw new APIError(404, 'Comment not found')
const comment = commentSnap.data() as ContractComment
const db = createSupabaseClient()
const comment = await getComment(db, commentId)

const txn = await runAwardBountyTxn(
// run as transaction to prevent race conditions
const txn = await firestore.runTransaction((transaction) =>
runAwardBountyTxn(
transaction,
{
fromId: contractId,
Expand All @@ -34,21 +34,32 @@ export const awardBounty: APIHandler<
},
auth.uid
)
)

transaction.update(commentDoc, {
bountyAwarded: FieldValue.increment(amount),
try {
const pg = createSupabaseDirectClient()
await updateData(pg, 'contract_comments', 'comment_id', {
comment_id: commentId,
bountyAwarded: (comment.bountyAwarded ?? 0) + amount,
})
const contract = await getContract(contractId)
if (contract) {
await createBountyAwardedNotification(
comment.userId,
contract,
contractId,
amount
)
}
return txn
})
} catch (e) {
logError(
'Bounty awarded but error updating denormed bounty amount on comment. Need to manually reconocile'
)
logError(e)
}

const contract = await getContract(contractId)
if (contract) {
await createBountyAwardedNotification(
comment.userId,
contract,
contractId,
amount
)
}

return txn
}

const firestore = admin.firestore()
20 changes: 0 additions & 20 deletions backend/api/src/change-user-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,26 +96,6 @@ export const changeUser = async (
}
log(`Updated ${contractRows.length} contracts.`)

log('Updating denormalized user data on comments...')
const commentRows = await pg.manyOrNone(
`select contract_id, comment_id from contract_comments where user_id = $1`,
[user.id]
)
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
for (const row of commentRows) {
const ref = firestore
.collection('contracts')
.doc(row.contract_id)
.collection('comments')
.doc(row.comment_id)
bulkWriter.update(ref, commentUpdate)
}
log(`Updated ${commentRows.length} comments.`)

log('Updating denormalized user data on bets...')
const betRows = await pg.manyOrNone(
`select contract_id, bet_id from contract_bets where user_id = $1`,
Expand Down
116 changes: 111 additions & 5 deletions backend/api/src/create-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import { removeUndefinedProps } from 'common/util/object'
import { getContract, getUserFirebase } from 'shared/utils'
import { APIError, AuthedUser, type APIHandler } from './helpers/endpoint'
import { anythingToRichText } from 'shared/tiptap'
import {
SupabaseDirectClient,
createSupabaseClient,
createSupabaseDirectClient,
} from 'shared/supabase/init'
import { first } from 'lodash'
import { onCreateCommentOnContract } from './on-create-comment-on-contract'
import { millisToTs } from 'common/supabase/utils'

export const MAX_COMMENT_JSON_LENGTH = 20000

Expand Down Expand Up @@ -63,21 +71,32 @@ export const createCommentOnContractInternal = async (
contentJson,
} = await validateComment(contractId, auth.uid, content, html, markdown)

const ref = firestore.collection(`contracts/${contractId}/comments`).doc()
const pg = createSupabaseDirectClient()
const now = Date.now()

const bet = replyToBetId
? await firestore
.collection(`contracts/${contract.id}/bets`)
.doc(replyToBetId)
.get()
.then((doc) => doc.data() as Bet)
: undefined
: await getMostRecentCommentableBet(
pg,
contract.id,
creator.id,
now,
replyToAnswerId
)

const position = await getLargestPosition(pg, contract.id, creator.id)

const isApi = auth.creds.kind === 'key'

const comment = removeUndefinedProps({
id: ref.id,
// TODO: generate ids in supabase instead
id: Math.random().toString(36).substring(2, 15),
content: contentJson,
createdTime: Date.now(),
createdTime: now,

userId: creator.id,
userName: creator.name,
Expand All @@ -93,6 +112,12 @@ export const createCommentOnContractInternal = async (
answerOutcome: replyToAnswerId,
visibility: contract.visibility,

commentorPositionShares: position?.shares,
commentorPositionOutcome: position?.outcome,
commentorPositionAnswerId: position?.answer_id,
commentorPositionProb:
position && contract.mechanism === 'cpmm-1' ? contract.prob : undefined,

// Response to another user's bet fields
betId: bet?.id,
betAmount: bet?.amount,
Expand All @@ -106,7 +131,18 @@ export const createCommentOnContractInternal = async (
isRepost,
} as ContractComment)

await ref.set(comment)
const db = createSupabaseClient()
const ret = await db.from('contract_comments').insert({
contract_id: contractId,
comment_id: comment.id,
user_id: creator.id,
created_time: millisToTs(now),
data: comment,
})

if (ret.error) {
throw new APIError(500, 'Failed to create comment: ' + ret.error.message)
}

if (isApi) {
const userRef = firestore.doc(`users/${creator.id}`)
Expand All @@ -116,6 +152,12 @@ export const createCommentOnContractInternal = async (
})
}

try {
await onCreateCommentOnContract({ contractId, comment, creator, bet })
} catch (e) {
console.error('Failed to run onCreateCommentOnContract: ' + e)
}

return comment
}

Expand Down Expand Up @@ -148,3 +190,67 @@ export const validateComment = async (
}
return { contentJson, you, contract }
}

async function getMostRecentCommentableBet(
pg: SupabaseDirectClient,
contractId: string,
userId: string,
commentCreatedTime: number,
answerOutcome?: string
) {
const maxAge = '5 minutes'
const bet = await pg
.map(
`with prior_user_comments_with_bets as (
select created_time, data->>'betId' as bet_id from contract_comments
where contract_id = $1 and user_id = $2
and created_time < millis_to_ts($3)
and data ->> 'betId' is not null
and created_time > millis_to_ts($3) - interval $5
order by created_time desc
limit 1
),
cutoff_time as (
select coalesce(
(select created_time from prior_user_comments_with_bets),
millis_to_ts($3) - interval $5)
as cutoff
)
select data from contract_bets
where contract_id = $1
and user_id = $2
and ($4 is null or answer_id = $4)
and created_time < millis_to_ts($3)
and created_time > (select cutoff from cutoff_time)
and not is_ante
and not is_redemption
order by created_time desc
limit 1
`,
[contractId, userId, commentCreatedTime, answerOutcome, maxAge],
(r) => (r.data ? (r.data as Bet) : undefined)
)
.catch((e) => console.error('Failed to get bet: ' + e))
return first(bet ?? [])
}

async function getLargestPosition(
pg: SupabaseDirectClient,
contractId: string,
userId: string
) {
// mqp: should probably use user_contract_metrics for this, i am just lazily porting
return await pg
.oneOrNone(
`with user_positions as (
select answer_id, outcome, sum(shares) as shares
from contract_bets
where contract_id = $1
and user_id = $2
group by answer_id, outcome
)
select * from user_positions order by shares desc limit 1`,
[contractId, userId]
)
.catch((e) => console.error('Failed to get position: ' + e))
}
27 changes: 14 additions & 13 deletions backend/api/src/edit-comment.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { APIError, authEndpoint, validate } from 'api/helpers/endpoint'
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { validateComment } from 'api/create-comment'
import { Comment } from 'common/comment'
import { createSupabaseClient } from 'shared/supabase/init'
import {
createSupabaseClient,
createSupabaseDirectClient,
} from 'shared/supabase/init'
import { run } from 'common/supabase/utils'
import { contentSchema } from 'common/api/zod-types'
import { isAdminId } from 'common/envs/constants'
import { getDomainForContract, revalidateStaticProps } from 'shared/utils'
import { contractPath } from 'common/contract'
import { getComment } from 'shared/supabase/contract_comments'
import { updateData } from 'shared/supabase/utils'

const editSchema = z
.object({
Expand All @@ -20,7 +23,6 @@ const editSchema = z
})
.strict()
export const editcomment = authEndpoint(async (req, auth) => {
const firestore = admin.firestore()
const { commentId, contractId, content, html, markdown } = validate(
editSchema,
req.body
Expand All @@ -31,20 +33,19 @@ export const editcomment = authEndpoint(async (req, auth) => {
contentJson,
} = await validateComment(contractId, auth.uid, content, html, markdown)

const ref = firestore
.collection(`contracts/${contractId}/comments`)
.doc(commentId)
const refSnap = await ref.get()
if (!refSnap.exists) throw new APIError(404, 'Comment not found')
const comment = refSnap.data() as Comment
const db = createSupabaseClient()
const comment = await getComment(db, commentId)

if (editor.id !== comment.userId && !isAdminId(editor.id))
throw new APIError(403, 'User is not the creator of the comment.')

await ref.update({
const pg = createSupabaseDirectClient()
await updateData(pg, 'contract_comments', 'comment_id', {
comment_id: commentId,
content: contentJson,
editedTime: Date.now(),
})
const db = createSupabaseClient()

await run(
db.from('contract_comment_edits').insert({
contract_id: contract.id,
Expand All @@ -58,5 +59,5 @@ export const editcomment = authEndpoint(async (req, auth) => {
getDomainForContract(contract)
)

return { commentId: ref.id }
return { success: true }
})
Loading

0 comments on commit ebea55f

Please sign in to comment.