Handle uploads in territory descriptions (#2379)

* Remove unused parameter

* Mark uploads as paid on territory create and update

* Refactor upload expiry check

* Check upload expiry on territory create

* Include upload fees in territory create/update cost

* Also check for expired uploads on edits

* Find deleted uploads with one query
This commit is contained in:
ekzyis 2025-08-02 02:40:15 +02:00 committed by GitHub
parent 45acbaa4fa
commit 6d244a5de6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 61 additions and 21 deletions

View File

@ -3,6 +3,7 @@ import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySub
import { getItemMentions, getMentions, performBotBehavior } from './lib/item' import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { msatsToSats, satsToMsats } from '@/lib/format' import { msatsToSats, satsToMsats } from '@/lib/format'
import { GqlInputError } from '@/lib/error' import { GqlInputError } from '@/lib/error'
import { throwOnExpiredUploads } from '@/api/resolvers/upload'
export const anonable = true export const anonable = true
@ -61,15 +62,7 @@ export async function perform (args, context) {
const { tx, me, cost } = context const { tx, me, cost } = context
const boostMsats = satsToMsats(boost) const boostMsats = satsToMsats(boost)
const deletedUploads = [] await throwOnExpiredUploads(uploadIds, { tx })
for (const uploadId of uploadIds) {
if (!await tx.upload.findUnique({ where: { id: uploadId } })) {
deletedUploads.push(uploadId)
}
}
if (deletedUploads.length > 0) {
throw new Error(`upload(s) ${deletedUploads.join(', ')} are expired, consider reuploading.`)
}
let invoiceData = {} let invoiceData = {}
if (invoiceId) { if (invoiceId) {

View File

@ -1,5 +1,5 @@
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants' import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { uploadFees } from '../resolvers/upload' import { throwOnExpiredUploads, uploadFees } from '@/api/resolvers/upload'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item' import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { notifyItemMention, notifyMention } from '@/lib/webPush' import { notifyItemMention, notifyMention } from '@/lib/webPush'
import { satsToMsats } from '@/lib/format' import { satsToMsats } from '@/lib/format'
@ -60,6 +60,7 @@ export async function perform (args, context) {
const itemMentions = await getItemMentions(args, context) const itemMentions = await getItemMentions(args, context)
const itemUploads = uploadIds.map(id => ({ uploadId: id })) const itemUploads = uploadIds.map(id => ({ uploadId: id }))
await throwOnExpiredUploads(uploadIds, { tx })
await tx.upload.updateMany({ await tx.upload.updateMany({
where: { id: { in: uploadIds } }, where: { id: { in: uploadIds } },
data: { paid: true } data: { paid: true }

View File

@ -2,6 +2,7 @@ import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/consta
import { satsToMsats } from '@/lib/format' import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory' import { nextBilling } from '@/lib/territory'
import { initialTrust } from './lib/territory' import { initialTrust } from './lib/territory'
import { throwOnExpiredUploads, uploadFees } from '@/api/resolvers/upload'
export const anonable = false export const anonable = false
@ -11,8 +12,9 @@ export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
] ]
export async function getCost ({ billingType }) { export async function getCost ({ billingType, uploadIds }, { models, me }) {
return satsToMsats(TERRITORY_PERIOD_COST(billingType)) const { totalFees } = await uploadFees(uploadIds, { models, me })
return satsToMsats(TERRITORY_PERIOD_COST(billingType) + totalFees)
} }
export async function perform ({ invoiceId, ...data }, { me, cost, tx }) { export async function perform ({ invoiceId, ...data }, { me, cost, tx }) {
@ -21,6 +23,19 @@ export async function perform ({ invoiceId, ...data }, { me, cost, tx }) {
const billedLastAt = new Date() const billedLastAt = new Date()
const billPaidUntil = nextBilling(billedLastAt, billingType) const billPaidUntil = nextBilling(billedLastAt, billingType)
await throwOnExpiredUploads(data.uploadIds, { tx })
if (data.uploadIds.length > 0) {
await tx.upload.updateMany({
where: {
id: { in: data.uploadIds }
},
data: {
paid: true
}
})
}
delete data.uploadIds
const sub = await tx.sub.create({ const sub = await tx.sub.create({
data: { data: {
...data, ...data,

View File

@ -2,6 +2,7 @@ import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/consta
import { satsToMsats } from '@/lib/format' import { satsToMsats } from '@/lib/format'
import { proratedBillingCost } from '@/lib/territory' import { proratedBillingCost } from '@/lib/territory'
import { datePivot } from '@/lib/time' import { datePivot } from '@/lib/time'
import { throwOnExpiredUploads, uploadFees } from '@/api/resolvers/upload'
export const anonable = false export const anonable = false
@ -11,18 +12,16 @@ export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
] ]
export async function getCost ({ oldName, billingType }, { models }) { export async function getCost ({ oldName, billingType, uploadIds }, { models, me }) {
const oldSub = await models.sub.findUnique({ const oldSub = await models.sub.findUnique({
where: { where: {
name: oldName name: oldName
} }
}) })
const cost = proratedBillingCost(oldSub, billingType) const { totalFees } = await uploadFees(uploadIds, { models, me })
if (!cost) {
return 0n
}
const cost = proratedBillingCost(oldSub, billingType) + totalFees
return satsToMsats(cost) return satsToMsats(cost)
} }
@ -63,6 +62,19 @@ export async function perform ({ oldName, invoiceId, ...data }, { me, cost, tx }
}) })
} }
await throwOnExpiredUploads(data.uploadIds, { tx })
if (data.uploadIds.length > 0) {
await tx.upload.updateMany({
where: {
id: { in: data.uploadIds }
},
data: {
paid: true
}
})
}
delete data.uploadIds
return await tx.sub.update({ return await tx.sub.update({
data, data,
where: { where: {

View File

@ -1506,7 +1506,7 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
item = { subName, ...item } item = { subName, ...item }
item.forwardUsers = await getForwardUsers(models, forward) item.forwardUsers = await getForwardUsers(models, forward)
} }
item.uploadIds = uploadIdsFromText(item.text, { models }) item.uploadIds = uploadIdsFromText(item.text)
// never change author of item // never change author of item
item.userId = old.userId item.userId = old.userId
@ -1525,7 +1525,7 @@ export const createItem = async (parent, { forward, ...item }, { me, models, lnd
item.userId = me ? Number(me.id) : USER_ID.anon item.userId = me ? Number(me.id) : USER_ID.anon
item.forwardUsers = await getForwardUsers(models, forward) item.forwardUsers = await getForwardUsers(models, forward)
item.uploadIds = uploadIdsFromText(item.text, { models }) item.uploadIds = uploadIdsFromText(item.text)
if (item.url && !isJob(item)) { if (item.url && !isJob(item)) {
item.url = ensureProtocol(item.url) item.url = ensureProtocol(item.url)

View File

@ -5,6 +5,7 @@ import { viewGroup } from './growth'
import { notifyTerritoryTransfer } from '@/lib/webPush' import { notifyTerritoryTransfer } from '@/lib/webPush'
import performPaidAction from '../paidAction' import performPaidAction from '../paidAction'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error' import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
import { uploadIdsFromText } from './upload'
export async function getSub (parent, { name }, { models, me }) { export async function getSub (parent, { name }, { models, me }) {
if (!name) return null if (!name) return null
@ -210,6 +211,8 @@ export default {
await validateSchema(territorySchema, data, { models, me, sub: { name: data.oldName } }) await validateSchema(territorySchema, data, { models, me, sub: { name: data.oldName } })
data.uploadIds = uploadIdsFromText(data.desc)
if (data.oldName) { if (data.oldName) {
return await updateSub(parent, data, { me, models, lnd }) return await updateSub(parent, data, { me, models, lnd })
} else { } else {

View File

@ -54,7 +54,7 @@ export default {
} }
} }
export function uploadIdsFromText (text, { models }) { export function uploadIdsFromText (text) {
if (!text) return [] if (!text) return []
return [...new Set([...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])))] return [...new Set([...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])))]
} }
@ -68,3 +68,19 @@ export async function uploadFees (s3Keys, { models, me }) {
const totalFees = msatsToSats(totalFeesMsats) const totalFees = msatsToSats(totalFeesMsats)
return { ...info, uploadFees, totalFees, totalFeesMsats } return { ...info, uploadFees, totalFees, totalFeesMsats }
} }
export async function throwOnExpiredUploads (uploadIds, { tx }) {
if (uploadIds.length === 0) return
const existingUploads = await tx.upload.findMany({
where: { id: { in: uploadIds } },
select: { id: true }
})
const existingIds = new Set(existingUploads.map(upload => upload.id))
const deletedIds = uploadIds.filter(id => !existingIds.has(id))
if (deletedIds.length > 0) {
throw new Error(`upload(s) ${deletedIds.join(', ')} are expired, consider reuploading.`)
}
}

View File

@ -19,7 +19,7 @@ export function purchasedType (sub) {
export function proratedBillingCost (sub, newBillingType) { export function proratedBillingCost (sub, newBillingType) {
if (!sub || if (!sub ||
sub.billingType === 'ONCE' || sub.billingType === 'ONCE' ||
sub.billingType === newBillingType.toUpperCase()) return null sub.billingType === newBillingType.toUpperCase()) return 0
return TERRITORY_PERIOD_COST(newBillingType) - TERRITORY_PERIOD_COST(purchasedType(sub)) return TERRITORY_PERIOD_COST(newBillingType) - TERRITORY_PERIOD_COST(purchasedType(sub))
} }