From 8c5897be190b4c2ad055cfb4a56e35e02c51a7c4 Mon Sep 17 00:00:00 2001 From: Soxasora Date: Thu, 20 Feb 2025 13:22:52 +0100 Subject: [PATCH 1/3] Allow switching to a more expensive territory --- api/paidAction/itemUpdate.js | 13 +++++++-- api/resolvers/item.js | 8 ------ pages/items/[id]/edit.js | 53 ++++++++++++++++++++++++++++-------- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/api/paidAction/itemUpdate.js b/api/paidAction/itemUpdate.js index 3aad8e448..40f50b1b8 100644 --- a/api/paidAction/itemUpdate.js +++ b/api/paidAction/itemUpdate.js @@ -12,12 +12,19 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] -export async function getCost ({ id, boost = 0, uploadIds, bio }, { me, models }) { +export async function getCost ({ subName, id, boost = 0, uploadIds, bio }, { me, models }) { // the only reason updating items costs anything is when it has new uploads - // or more boost + // or more boost or is switch to a more expensive sub const old = await models.item.findUnique({ where: { id: parseInt(id) } }) const { totalFeesMsats } = await uploadFees(uploadIds, { models, me }) - const cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost) + let cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost) + if (old.subName !== subName) { + const oldSub = await models.sub.findUnique({ where: { name: old.subName } }) + const newSub = await models.sub.findUnique({ where: { name: subName } }) + const oldCost = oldSub?.baseCost ?? 0 + const newCost = newSub?.baseCost ?? 0 + cost += satsToMsats(Math.max(0, newCost - oldCost)) // user will pay just the difference + } if (cost > 0 && old.invoiceActionState && old.invoiceActionState !== 'PAID') { throw new Error('creation invoice not paid') diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 2ff086337..df7b55318 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -1474,14 +1474,6 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, .. throw new GqlInputError('item does not belong to you') } - const differentSub = subName && old.subName !== subName - if (differentSub) { - const sub = await models.sub.findUnique({ where: { name: subName } }) - if (sub.baseCost > old.sub.baseCost) { - throw new GqlInputError('cannot change to a more expensive sub') - } - } - // in case they lied about their existing boost await validateSchema(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost }) diff --git a/pages/items/[id]/edit.js b/pages/items/[id]/edit.js index 707da6fd1..9dc97d57b 100644 --- a/pages/items/[id]/edit.js +++ b/pages/items/[id]/edit.js @@ -7,26 +7,49 @@ import JobForm from '@/components/job-form' import { PollForm } from '@/components/poll-form' import { BountyForm } from '@/components/bounty-form' import { useState } from 'react' -import { useQuery } from '@apollo/client' +import { useQuery, gql } from '@apollo/client' import { useRouter } from 'next/router' import PageLoading from '@/components/page-loading' -import { FeeButtonProvider } from '@/components/fee-button' +import { FeeButtonProvider, postCommentBaseLineItems } from '@/components/fee-button' import SubSelect from '@/components/sub-select' import useCanEdit from '@/components/use-can-edit' +import { useMe } from '@/components/me' export const getServerSideProps = getGetServerSideProps({ query: ITEM, notFound: data => !data.item }) +// TODO: cleanup +const SUB_QUERY = gql` + query Sub($name: String!) { + sub(name: $name) { + name + baseCost + } + } +` + export default function PostEdit ({ ssrData }) { const router = useRouter() const { data } = useQuery(ITEM, { variables: { id: router.query.id } }) if (!data && !ssrData) return const { item } = data || ssrData + const { me } = useMe() const [sub, setSub] = useState(item.subName) + // TODO: cleanup + const { data: oldSubData } = useQuery(SUB_QUERY, { + variables: { name: item.subName }, + skip: !item.subName + }) + + const { data: newSubData } = useQuery(SUB_QUERY, { + variables: { name: sub }, + skip: !sub + }) + const [,, editThreshold] = useCanEdit(item) let FormType = DiscussionForm @@ -45,20 +68,26 @@ export default function PostEdit ({ ssrData }) { itemType = 'BOUNTY' } - const existingBoostLineItem = item.boost - ? { - existingBoost: { - label: 'old boost', - term: `- ${item.boost}`, - op: '-', - modifier: cost => cost - item.boost + function editLineItems (oldSub, newSub) { + const existingBoostLineItem = item.boost + ? { + existingBoost: { + label: 'old boost', + term: `- ${item.boost}`, + op: '-', + modifier: cost => cost - item.boost + } } - } - : undefined + : undefined + return { + ...(item.subName !== newSub?.name ? postCommentBaseLineItems({ baseCost: Math.max(0, newSub?.baseCost - oldSub?.baseCost), me: !!me }) : undefined), + ...existingBoostLineItem + } + } return ( - + {!item.isJob && Date: Thu, 20 Feb 2025 14:30:01 +0100 Subject: [PATCH 2/3] cleanup; better query; better readability --- api/paidAction/itemUpdate.js | 19 ++++++++++--------- fragments/items.js | 1 + pages/items/[id]/edit.js | 30 +++++++++++++++--------------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/api/paidAction/itemUpdate.js b/api/paidAction/itemUpdate.js index 40f50b1b8..c6fb58d58 100644 --- a/api/paidAction/itemUpdate.js +++ b/api/paidAction/itemUpdate.js @@ -12,19 +12,20 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] +async function getBaseCostDifference (oldSubName, newSubName, { models }) { + if (oldSubName === newSubName) return 0 + const oldSub = await models.sub.findUnique({ where: { name: oldSubName }, select: { baseCost: true } }) + const newSub = await models.sub.findUnique({ where: { name: newSubName }, select: { baseCost: true } }) + return Math.max(0, (newSub?.baseCost ?? 0) - (oldSub?.baseCost ?? 0)) +} + export async function getCost ({ subName, id, boost = 0, uploadIds, bio }, { me, models }) { // the only reason updating items costs anything is when it has new uploads - // or more boost or is switch to a more expensive sub + // or more boost or is switching to a more expensive sub const old = await models.item.findUnique({ where: { id: parseInt(id) } }) const { totalFeesMsats } = await uploadFees(uploadIds, { models, me }) - let cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost) - if (old.subName !== subName) { - const oldSub = await models.sub.findUnique({ where: { name: old.subName } }) - const newSub = await models.sub.findUnique({ where: { name: subName } }) - const oldCost = oldSub?.baseCost ?? 0 - const newCost = newSub?.baseCost ?? 0 - cost += satsToMsats(Math.max(0, newCost - oldCost)) // user will pay just the difference - } + const baseCostDifference = await getBaseCostDifference(old.subName, subName, { models }) + const cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost) + satsToMsats(baseCostDifference) if (cost > 0 && old.invoiceActionState && old.invoiceActionState !== 'PAID') { throw new Error('creation invoice not paid') diff --git a/fragments/items.js b/fragments/items.js index c9c2a8da2..0e55049e6 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -35,6 +35,7 @@ export const ITEM_FIELDS = gql` meMuteSub meSubscription nsfw + baseCost replyCost } otsHash diff --git a/pages/items/[id]/edit.js b/pages/items/[id]/edit.js index 9dc97d57b..563a7e92b 100644 --- a/pages/items/[id]/edit.js +++ b/pages/items/[id]/edit.js @@ -20,8 +20,7 @@ export const getServerSideProps = getGetServerSideProps({ notFound: data => !data.item }) -// TODO: cleanup -const SUB_QUERY = gql` +const SUB_BASECOST = gql` query Sub($name: String!) { sub(name: $name) { name @@ -39,16 +38,8 @@ export default function PostEdit ({ ssrData }) { const { me } = useMe() const [sub, setSub] = useState(item.subName) - // TODO: cleanup - const { data: oldSubData } = useQuery(SUB_QUERY, { - variables: { name: item.subName }, - skip: !item.subName - }) - - const { data: newSubData } = useQuery(SUB_QUERY, { - variables: { name: sub }, - skip: !sub - }) + // we need to fetch the new sub to calculate the cost difference + const { data: newSubData } = useQuery(SUB_BASECOST, { variables: { name: sub } }) const [,, editThreshold] = useCanEdit(item) @@ -68,7 +59,7 @@ export default function PostEdit ({ ssrData }) { itemType = 'BOUNTY' } - function editLineItems (oldSub, newSub) { + const editLineItems = (newSub) => { const existingBoostLineItem = item.boost ? { existingBoost: { @@ -79,15 +70,24 @@ export default function PostEdit ({ ssrData }) { } } : undefined + + const isSwitchingSub = item.subName !== newSub?.name + const subCostDifference = isSwitchingSub && { + ...postCommentBaseLineItems({ + baseCost: Math.max(0, (newSub?.baseCost ?? 0) - (item?.sub?.baseCost ?? 0)), + me: !!me + }) + } + return { - ...(item.subName !== newSub?.name ? postCommentBaseLineItems({ baseCost: Math.max(0, newSub?.baseCost - oldSub?.baseCost), me: !!me }) : undefined), + ...subCostDifference, ...existingBoostLineItem } } return ( - + {!item.isJob && Date: Sun, 2 Mar 2025 01:07:11 +0000 Subject: [PATCH 3/3] Check item repetition for sub switch fee --- api/paidAction/itemUpdate.js | 28 +++++++++++++++++++++------- pages/items/[id]/edit.js | 4 ++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/api/paidAction/itemUpdate.js b/api/paidAction/itemUpdate.js index c6fb58d58..5c54b347b 100644 --- a/api/paidAction/itemUpdate.js +++ b/api/paidAction/itemUpdate.js @@ -1,4 +1,4 @@ -import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants' +import { PAID_ACTION_PAYMENT_METHODS, USER_ID, ITEM_SPAM_INTERVAL, ANON_ITEM_SPAM_INTERVAL } from '@/lib/constants' import { uploadFees } from '../resolvers/upload' import { getItemMentions, getMentions, performBotBehavior } from './lib/item' import { notifyItemMention, notifyMention } from '@/lib/webPush' @@ -12,11 +12,25 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] -async function getBaseCostDifference (oldSubName, newSubName, { models }) { +async function getSubSwitchFee (oldSubName, newSubName, id, { models, me }) { if (oldSubName === newSubName) return 0 - const oldSub = await models.sub.findUnique({ where: { name: oldSubName }, select: { baseCost: true } }) - const newSub = await models.sub.findUnique({ where: { name: newSubName }, select: { baseCost: true } }) - return Math.max(0, (newSub?.baseCost ?? 0) - (oldSub?.baseCost ?? 0)) + + const [oldSub, newSub] = await Promise.all([ + models.sub.findUnique({ where: { name: oldSubName }, select: { baseCost: true } }), + models.sub.findUnique({ where: { name: newSubName }, select: { baseCost: true } }) + ]) + + const subDifferenceCost = Math.max(0, (newSub?.baseCost ?? 0) - (oldSub?.baseCost ?? 0)) + if (subDifferenceCost === 0) return 0 + + // calculate spam cost exclusively for sub switch + const [{ totalCost }] = await models.$queryRaw` + SELECT ${satsToMsats(subDifferenceCost)}::INTEGER + * POWER(10, item_spam(NULL, ${me?.id ?? USER_ID.anon}::INTEGER, + ${me?.id ? ITEM_SPAM_INTERVAL : ANON_ITEM_SPAM_INTERVAL}::INTERVAL)) + * ${me ? 1 : 100}::INTEGER as "totalCost"` + + return totalCost } export async function getCost ({ subName, id, boost = 0, uploadIds, bio }, { me, models }) { @@ -24,8 +38,8 @@ export async function getCost ({ subName, id, boost = 0, uploadIds, bio }, { me, // or more boost or is switching to a more expensive sub const old = await models.item.findUnique({ where: { id: parseInt(id) } }) const { totalFeesMsats } = await uploadFees(uploadIds, { models, me }) - const baseCostDifference = await getBaseCostDifference(old.subName, subName, { models }) - const cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost) + satsToMsats(baseCostDifference) + const subSwitchFee = await getSubSwitchFee(old.subName, subName, id, { models, me }) + const cost = BigInt(totalFeesMsats) + satsToMsats(boost - old.boost) + BigInt(subSwitchFee) if (cost > 0 && old.invoiceActionState && old.invoiceActionState !== 'PAID') { throw new Error('creation invoice not paid') diff --git a/pages/items/[id]/edit.js b/pages/items/[id]/edit.js index 563a7e92b..ab4227cd9 100644 --- a/pages/items/[id]/edit.js +++ b/pages/items/[id]/edit.js @@ -10,7 +10,7 @@ import { useState } from 'react' import { useQuery, gql } from '@apollo/client' import { useRouter } from 'next/router' import PageLoading from '@/components/page-loading' -import { FeeButtonProvider, postCommentBaseLineItems } from '@/components/fee-button' +import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from '@/components/fee-button' import SubSelect from '@/components/sub-select' import useCanEdit from '@/components/use-can-edit' import { useMe } from '@/components/me' @@ -87,7 +87,7 @@ export default function PostEdit ({ ssrData }) { return ( - + {!item.isJob &&