Skip to content

Commit

Permalink
another inch
Browse files Browse the repository at this point in the history
  • Loading branch information
huumn committed May 29, 2024
1 parent 40fc7f0 commit 7e4bddf
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 71 deletions.
35 changes: 35 additions & 0 deletions api/paidAction/buyCredits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// XXX we don't use this yet ...
// it's just showing that even buying credits
// can eventually be a paid action

import { ANON_USER_ID } from '@/lib/constants'

export const anonable = true
export const supportsPessimism = true
export const supportsOptimism = true

export async function getCost ({ amount }) {
return BigInt(amount) * BigInt(1000)
}

export async function doStatements () {
return []
}

export async function onPaidStatements ({ invoice }, { models }) {
return [
models.users.update({
where: { id: invoice.userId },
data: { balance: { increment: invoice.msatsReceived } }
})
]
}

export async function resultsToResponse (results, args, context) {
return null
}

export async function describe ({ amount }, { models, me }) {
const user = await models.user.findUnique({ where: { id: me?.id ?? ANON_USER_ID } })
return `SN: buying credits for @${user.name}`
}
165 changes: 127 additions & 38 deletions api/paidAction/createItem.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { ANON_USER_ID } from '@/lib/constants'
import { getDeleteAt, getRemindAt } from '@/lib/item'
import { notifyItemParents, notifyTerritorySubscribers, notifyUserSubscribers } from '@/lib/webPush'

export const anonable = true
export const supportsPessimism = true
export const supportsOptimism = true

export async function getCost () {
// TODO
return null
export async function getCost ({ subName, parentId, uploadIds, boost }, { models, user }) {
const sub = await models.sub.findUnique({ where: { name: subName } })
const baseCost = sub.baseCost * BigInt(1000)
// baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + image fees
const [cost] = await models.$queryRaw`
SELECT ${baseCost}::INTEGER
* POWER(10, item_spam(${parentId}::INTEGER, ${user?.id || ANON_USER_ID}::INTEGER, '10m'::INTERVAL))
* ${user ? 1 : 100}::INTEGER
+ (SELECT "nUnpaid" * "imageFeeMsats" FROM image_fees_info(${user?.id || ANON_USER_ID}::INTEGER, ${uploadIds}))`
// freebies must be allowed, cost must be less than baseCost, no boost, user must exist, and cost must be less or equal to user's balance
const freebie = sub.allowFreebies && cost <= baseCost && !boost && !!user && cost <= user?.privates?.msats
return freebie ? 0 : cost
}

export async function performStatements (
export async function doStatements (
{ invoiceId, uploadIds = [], itemForwards = [], pollOptions = [], boost = 0, ...data },
{ me, models, cost }) {
const boostMsats = BigInt(boost) * BigInt(1000)
Expand All @@ -22,60 +35,136 @@ export async function performStatements (
itemActs.push({
msats: cost - boostMsats, act: 'FEE', invoiceId, invoiceActionState: 'PENDING', userId: me.id
})
} else {
data.freebie = true
}

const mentions = []
const text = data.text
if (text) {
const mentionPattern = /\B@[\w_]+/gi
const names = text.match(mentionPattern)?.map(m => m.slice(1))
if (names?.length > 0) {
const users = await models.user.findMany({ where: { name: { in: names } } })
mentions.push(...users.map(({ id }) => ({ userId: id }))
.filter(({ userId }) => userId !== me.id))
}
data.deleteAt = getDeleteAt(text)
data.remindAt = getRemindAt(text)
}

if (me) {
const [medianVote] = await models.$executeRaw`
SELECT LEAST(
percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0)
FROM "Item" WHERE "userId" = ${me.id}`

data.weightedDownVotes = Math.abs(medianVote)
}

const itemData = {
...data,
text,
boost,
invoiceId,
userId: me.id || ANON_USER_ID,
actionInvoiceState: 'PENDING',
threadSubscription: {
createMany: [
{ userId: me.id },
...itemForwards.map(({ userId }) => ({ userId }))
]
},
itemForwards: {
createMany: itemForwards
},
pollOptions: {
createMany: pollOptions
},
itemUploads: {
connect: uploadIds.map(id => ({ uploadId: id }))
},
itemAct: {
createMany: itemActs
},
mention: {
createMany: mentions
}
}

const stmts = []
if (data.bio && me) {
stmts.push(models.user.update({
where: { id: me.id },
data: {
bio: {
create: itemData
}
}
}))
} else {
stmts.push(models.item.create({
data: itemData
}))
}

return [
...stmts,
models.upload.updateMany({
where: { id: { in: uploadIds } },
data: { invoiceId, actionInvoiceState: 'PENDING' }
}),
models.item.create({
data: {
...data,
boost,
invoiceId,
actionInvoiceState: 'PENDING'
},
threadSubscription: {
createMany: [
{ userId: me.id },
...itemForwards.map(({ userId }) => ({ userId }))
]
},
itemForwards: {
createMany: itemForwards
},
pollOptions: {
createMany: pollOptions
},
itemUploads: {
connect: uploadIds.map(id => ({ uploadId: id }))
},
itemAct: {
createMany: itemActs
}
})
]
// TODO: run_auction for job or just remove jobs?
}

export async function resultsToResponse (results, args, context) {
// TODO
return null
return args.bio ? results[0].bio : results[0]
}

export async function onPaidStatements ({ invoice }, { models }) {
const item = await models.item.findFirst({ where: { invoiceId: invoice.id } })

const stmts = []
if (item.deleteAt) {
stmts.push(models.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, expirein)
VALUES (
'deleteItem',
jsonb_build_object('id', ${item.id}),
${item.deleteAt},
${item.deleteAt} - now() + interval '1 minute')`)
}
if (item.remindAt) {
stmts.push(models.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, expirein)
VALUES (
'remindItem',
jsonb_build_object('id', ${item.id}),
${item.remindAt},
${item.remindAt} - now() + interval '1 minute')`)
}
if (item.maxBid) {
stmts.push(models.$executeRaw`SELECT run_auction(${item.id}::INTEGER)`)
}

return [
models.itemAct.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID' } }),
models.item.updateMany({ where: { invoiceId: invoice.id }, data: { invoiceActionState: 'PAID' } }),
models.upload.updateMany({ where: { invoiceId: invoice.id }, data: { actionInvoiceState: 'PAID' } }),
// TODO: this doesn't work because it's a trigger
models.$executeRaw`SELECT ncomments_after_comment(${item.id}::INTEGER)`
// TODO: create mentions
// TODO: bot stuff
// TODO: notifications
// TODO: this don't work because it's a trigger
models.$executeRaw`SELECT ncomments_after_comment(${item.id}::INTEGER)`,
// jobs ... TODO: remove the triggers for these
models.$executeRaw`INSERT INTO pgboss.job (name, data, startafter, priority)
VALUES ('timestampItem', jsonb_build_object('id', ${item.id}), now() + interval '10 minutes', -2)`,
models.$executeRaw`INSERT INTO pgboss.job (name, data, priority) VALUES ('indexItem', jsonb_build_object('id', NEW.id), -100)`,
models.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('imgproxy', jsonb_build_object('id', item.id), 21, true, now() + interval '5 seconds')`,
...stmts,
// TODO: this doesn't work because we expect prisma queries
notifyItemParents({ item, me: item.userId, models }),
notifyUserSubscribers({ models, item }),
notifyTerritorySubscribers({ models, item })
]
}

Expand Down
3 changes: 2 additions & 1 deletion api/paidAction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { verifyPayment } from '../resolvers/serial'
import { ANON_USER_ID } from '@/lib/constants'

export const paidActions = {
BUY_CREDITS: require('./buyCredits'),
CREATE_ITEM: require('./createItem'),
UPDATE_ITEM: require('./updateItem'),
ZAP: require('./zap'),
Expand Down Expand Up @@ -31,8 +32,8 @@ export default async function doPaidAction (actionType, args, context) {
return await doPessimiticAction(actionType, args, context)
}

context.cost = await paidAction.getCost(args, context)
context.user = await models.user.findUnique({ where: { id: me.id } })
context.cost = await paidAction.getCost(args, context)
const isRich = context.cost <= context.user.privates.msats

if (!isRich && !paidAction.supportsOptimism) {
Expand Down
18 changes: 18 additions & 0 deletions lib/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ export const getDeleteCommand = (text) => {
return commands.length ? commands[commands.length - 1] : undefined
}

export const getDeleteAt = (text) => {
const command = getDeleteCommand(text)
if (command) {
const { number, unit } = command
return datePivot(new Date(), { [`${unit}s`]: number })
}
return null
}

export const getRemindAt = (text) => {
const command = getReminderCommand(text)
if (command) {
const { number, unit } = command
return datePivot(new Date(), { [`${unit}s`]: number })
}
return null
}

export const hasDeleteCommand = (text) => !!getDeleteCommand(text)

export const hasReminderMention = (text) => reminderMentionPattern.test(text ?? '')
Expand Down
30 changes: 18 additions & 12 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -284,18 +284,20 @@ model ItemUpload {
}

model Upload {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
type String
size Int
width Int?
height Int?
userId Int
paid Boolean?
user User @relation("Uploads", fields: [userId], references: [id], onDelete: Cascade)
User User[]
ItemUpload ItemUpload[]
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
type String
size Int
width Int?
height Int?
userId Int
invoiceId Int?
invoiceActionState InvoiceActionState?
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
user User @relation("Uploads", fields: [userId], references: [id], onDelete: Cascade)
User User[]
ItemUpload ItemUpload[]
@@index([createdAt], map: "Upload.created_at_index")
@@index([userId], map: "Upload.userId_index")
Expand Down Expand Up @@ -398,6 +400,8 @@ model Item {
bounty Int?
noteId String? @unique(map: "Item.noteId_unique")
rootId Int?
deleteAt DateTime?
remindAt DateTime?
bountyPaidTo Int[]
upvotes Int @default(0)
weightedComments Float @default(0)
Expand Down Expand Up @@ -668,6 +672,7 @@ model Mention {
}

enum InvoiceActionType {
BUY_CREDITS
ITEM_CREATE
ITEM_UPDATE
ZAP
Expand Down Expand Up @@ -709,6 +714,7 @@ model Invoice {
actionType InvoiceActionType?
ItemAct ItemAct[]
Item Item[]
Upload Upload[]
@@index([createdAt], map: "Invoice.created_at_index")
@@index([userId], map: "Invoice.userId_index")
Expand Down
3 changes: 3 additions & 0 deletions worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ofac } from './ofac.js'
import { autoWithdraw } from './autowithdraw.js'
import { saltAndHashEmails } from './saltAndHashEmails.js'
import { remindUser } from './reminder.js'
import { settleAction, settleActionError } from './paidAction.js'

const { loadEnvConfig } = nextEnv
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
Expand Down Expand Up @@ -104,6 +105,8 @@ async function work () {
await boss.work('ofac', jobWrapper(ofac))
await boss.work('saltAndHashEmails', jobWrapper(saltAndHashEmails))
await boss.work('reminder', jobWrapper(remindUser))
await boss.work('settleActionError', jobWrapper(settleActionError))
await boss.work('settleAction', jobWrapper(settleAction))

console.log('working jobs')
}
Expand Down
Loading

0 comments on commit 7e4bddf

Please sign in to comment.