From 6d244a5de6bf7ef1b3b2059435c751da3bd43a89 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Sat, 2 Aug 2025 02:40:15 +0200 Subject: [PATCH] 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 --- api/paidAction/itemCreate.js | 11 ++--------- api/paidAction/itemUpdate.js | 3 ++- api/paidAction/territoryCreate.js | 19 +++++++++++++++++-- api/paidAction/territoryUpdate.js | 22 +++++++++++++++++----- api/resolvers/item.js | 4 ++-- api/resolvers/sub.js | 3 +++ api/resolvers/upload.js | 18 +++++++++++++++++- lib/territory.js | 2 +- 8 files changed, 61 insertions(+), 21 deletions(-) diff --git a/api/paidAction/itemCreate.js b/api/paidAction/itemCreate.js index dcdf4d35..0c242734 100644 --- a/api/paidAction/itemCreate.js +++ b/api/paidAction/itemCreate.js @@ -3,6 +3,7 @@ import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySub import { getItemMentions, getMentions, performBotBehavior } from './lib/item' import { msatsToSats, satsToMsats } from '@/lib/format' import { GqlInputError } from '@/lib/error' +import { throwOnExpiredUploads } from '@/api/resolvers/upload' export const anonable = true @@ -61,15 +62,7 @@ export async function perform (args, context) { const { tx, me, cost } = context const boostMsats = satsToMsats(boost) - const deletedUploads = [] - 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.`) - } + await throwOnExpiredUploads(uploadIds, { tx }) let invoiceData = {} if (invoiceId) { diff --git a/api/paidAction/itemUpdate.js b/api/paidAction/itemUpdate.js index 8d63bed7..7c6b8436 100644 --- a/api/paidAction/itemUpdate.js +++ b/api/paidAction/itemUpdate.js @@ -1,5 +1,5 @@ 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 { notifyItemMention, notifyMention } from '@/lib/webPush' import { satsToMsats } from '@/lib/format' @@ -60,6 +60,7 @@ export async function perform (args, context) { const itemMentions = await getItemMentions(args, context) const itemUploads = uploadIds.map(id => ({ uploadId: id })) + await throwOnExpiredUploads(uploadIds, { tx }) await tx.upload.updateMany({ where: { id: { in: uploadIds } }, data: { paid: true } diff --git a/api/paidAction/territoryCreate.js b/api/paidAction/territoryCreate.js index a9316cdb..f30572ab 100644 --- a/api/paidAction/territoryCreate.js +++ b/api/paidAction/territoryCreate.js @@ -2,6 +2,7 @@ import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/consta import { satsToMsats } from '@/lib/format' import { nextBilling } from '@/lib/territory' import { initialTrust } from './lib/territory' +import { throwOnExpiredUploads, uploadFees } from '@/api/resolvers/upload' export const anonable = false @@ -11,8 +12,9 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] -export async function getCost ({ billingType }) { - return satsToMsats(TERRITORY_PERIOD_COST(billingType)) +export async function getCost ({ billingType, uploadIds }, { models, me }) { + const { totalFees } = await uploadFees(uploadIds, { models, me }) + return satsToMsats(TERRITORY_PERIOD_COST(billingType) + totalFees) } 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 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({ data: { ...data, diff --git a/api/paidAction/territoryUpdate.js b/api/paidAction/territoryUpdate.js index 30040a80..a40900a1 100644 --- a/api/paidAction/territoryUpdate.js +++ b/api/paidAction/territoryUpdate.js @@ -2,6 +2,7 @@ import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/consta import { satsToMsats } from '@/lib/format' import { proratedBillingCost } from '@/lib/territory' import { datePivot } from '@/lib/time' +import { throwOnExpiredUploads, uploadFees } from '@/api/resolvers/upload' export const anonable = false @@ -11,18 +12,16 @@ export const paymentMethods = [ 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({ where: { name: oldName } }) - const cost = proratedBillingCost(oldSub, billingType) - if (!cost) { - return 0n - } + const { totalFees } = await uploadFees(uploadIds, { models, me }) + const cost = proratedBillingCost(oldSub, billingType) + totalFees 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({ data, where: { diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 43a2c7ec..deb1255e 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -1506,7 +1506,7 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, .. item = { subName, ...item } item.forwardUsers = await getForwardUsers(models, forward) } - item.uploadIds = uploadIdsFromText(item.text, { models }) + item.uploadIds = uploadIdsFromText(item.text) // never change author of item 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.forwardUsers = await getForwardUsers(models, forward) - item.uploadIds = uploadIdsFromText(item.text, { models }) + item.uploadIds = uploadIdsFromText(item.text) if (item.url && !isJob(item)) { item.url = ensureProtocol(item.url) diff --git a/api/resolvers/sub.js b/api/resolvers/sub.js index d2718f1d..15a7369a 100644 --- a/api/resolvers/sub.js +++ b/api/resolvers/sub.js @@ -5,6 +5,7 @@ import { viewGroup } from './growth' import { notifyTerritoryTransfer } from '@/lib/webPush' import performPaidAction from '../paidAction' import { GqlAuthenticationError, GqlInputError } from '@/lib/error' +import { uploadIdsFromText } from './upload' export async function getSub (parent, { name }, { models, me }) { if (!name) return null @@ -210,6 +211,8 @@ export default { await validateSchema(territorySchema, data, { models, me, sub: { name: data.oldName } }) + data.uploadIds = uploadIdsFromText(data.desc) + if (data.oldName) { return await updateSub(parent, data, { me, models, lnd }) } else { diff --git a/api/resolvers/upload.js b/api/resolvers/upload.js index 020c2bc9..9b012af1 100644 --- a/api/resolvers/upload.js +++ b/api/resolvers/upload.js @@ -54,7 +54,7 @@ export default { } } -export function uploadIdsFromText (text, { models }) { +export function uploadIdsFromText (text) { if (!text) return [] 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) 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.`) + } +} diff --git a/lib/territory.js b/lib/territory.js index 7e460cb6..03d74b83 100644 --- a/lib/territory.js +++ b/lib/territory.js @@ -19,7 +19,7 @@ export function purchasedType (sub) { export function proratedBillingCost (sub, newBillingType) { if (!sub || 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)) }