From a308b7032825d33497634ee5cd8c1d32e555f513 Mon Sep 17 00:00:00 2001 From: Satoshi Nakamoto Date: Sun, 29 Oct 2023 10:54:49 -0400 Subject: [PATCH] handle uppercase addl fees for edit item --- api/resolvers/item.js | 31 ++++++- api/typeDefs/item.js | 1 + components/bounty-form.js | 1 + components/discussion-form.js | 1 + components/fee-button.js | 33 ++----- components/link-form.js | 2 +- components/poll-form.js | 2 +- fragments/items.js | 1 + pages/[name]/index.js | 2 +- .../migration.sql | 93 ++++++++++++++++++- prisma/schema.prisma | 1 + 11 files changed, 131 insertions(+), 37 deletions(-) rename prisma/migrations/{20231026204718_uppercase_title => X20231026204718_uppercase_title}/migration.sql (51%) diff --git a/api/resolvers/item.js b/api/resolvers/item.js index aca5dbeba..78ce17060 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -1089,10 +1089,32 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it item = { subName, userId: me.id, ...item } const fwdUsers = await getForwardUsers(models, forward) - + const hasPaidUpperTitleFee = old.upperTitleFeePaid + const titleUpperMult = !hasPaidUpperTitleFee && getUppercaseCharCountInTitle(item) > MAX_FREE_UPPER_CHARS_TITLE ? UPPER_CHARS_TITLE_FEE_MULT : 1 + let additionalFeeMsats = 0 + if (titleUpperMult > 1) { + const { _sum: { msats: paidMsats } } = await models.itemAct.aggregate({ + _sum: { + msats: true + }, + where: { + itemId: Number(item.id), + userId: me.id, + act: 'FEE' + } + }) + // If the original post was a freebie, that doesn't mean this edit should be free + if (Number(paidMsats) === 0) { + additionalFeeMsats = titleUpperMult // implicit 1 sat fee, since the post was a freebie + item.freebie = false + } else { + additionalFeeMsats += (titleUpperMult - 1) * Number(paidMsats) + } + item.upperTitleFeePaid = true + } item = await serializeInvoicable( - models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`, - JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)), + models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB, $4::BIGINT) AS "Item"`, + JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), additionalFeeMsats), { models, lnd, hash, hmac, me } ) @@ -1180,7 +1202,8 @@ export const SELECT = "Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item".boost, "Item".msats, "Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes", "Item"."weightedDownVotes", "Item".freebie, "Item"."otsHash", "Item"."bountyPaidTo", - ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls"` + ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls", + "Item"."upperTitleFeePaid"` function topOrderByWeightedSats (me, models) { return `ORDER BY ${orderByNumerator(models)} DESC NULLS LAST, "Item".id DESC` diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 1b10eaf02..618316039 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -115,6 +115,7 @@ export default gql` parentOtsHash: String forwards: [ItemForward] imgproxyUrls: JSONObject + upperTitleFeePaid: Boolean } input ItemForwardInput { diff --git a/components/bounty-form.js b/components/bounty-form.js index f1b14bf46..8aee2dfe9 100644 --- a/components/bounty-form.js +++ b/components/bounty-form.js @@ -144,6 +144,7 @@ export function BountyForm ({ text='save' ChildButton={SubmitButton} variant='secondary' + hasPaidUpperTitleFee={item.upperTitleFeePaid} /> ) diff --git a/components/discussion-form.js b/components/discussion-form.js index caf06a7b8..38f49d078 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -146,6 +146,7 @@ export function DiscussionForm ({ diff --git a/components/fee-button.js b/components/fee-button.js index 3ebeaaaf1..99f803ed6 100644 --- a/components/fee-button.js +++ b/components/fee-button.js @@ -107,25 +107,10 @@ export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, ) } -function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId, addTooManyCapitalsMultiplier }) { +function EditReceipt ({ cost, paidSats, boost, parentId, addTooManyCapitalsMultiplier }) { return ( - {addImgLink && - <> - - - - - - - - - - - - - } {addTooManyCapitalsMultiplier > 1 && <> @@ -157,20 +142,14 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId, addTooManyC ) } -export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton, variant, text, alwaysShow, parentId }) { +export function EditFeeButton ({ paidSats, ChildButton, variant, text, alwaysShow, parentId, hasPaidUpperTitleFee }) { const formik = useFormikContext() const boost = (formik?.values?.boost || 0) - (formik?.initialValues?.boost || 0) - const addImgLink = hasImgLink && !hadImgLink const tooManyCapitals = getUppercaseCharCountInTitle(formik?.values) > MAX_FREE_UPPER_CHARS_TITLE - const hadTooManyCapitals = getUppercaseCharCountInTitle(formik?.initialValues) > MAX_FREE_UPPER_CHARS_TITLE - let cost = (addImgLink ? paidSats * 9 : 0) - if (tooManyCapitals && !hadTooManyCapitals) { + let cost = 0 + if (tooManyCapitals && !hasPaidUpperTitleFee) { // only apply cost if the capital letters threshold is newly exceeded - if (cost === 0) { - cost = paidSats * (UPPER_CHARS_TITLE_FEE_MULT - 1) - } else { - cost *= (UPPER_CHARS_TITLE_FEE_MULT - 1) - } + cost = paidSats * (UPPER_CHARS_TITLE_FEE_MULT - 1) } cost += Number(boost) @@ -186,7 +165,7 @@ export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton, {cost > 0 && show && - + } ) diff --git a/components/link-form.js b/components/link-form.js index b3b947dc8..f880e548c 100644 --- a/components/link-form.js +++ b/components/link-form.js @@ -199,7 +199,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
diff --git a/components/poll-form.js b/components/poll-form.js index 6e6788782..4eb656f89 100644 --- a/components/poll-form.js +++ b/components/poll-form.js @@ -108,7 +108,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
diff --git a/fragments/items.js b/fragments/items.js index bb5d4f024..2be5f96c8 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -46,6 +46,7 @@ export const ITEM_FIELDS = gql` uploadId mine imgproxyUrls + upperTitleFeePaid }` export const ITEM_FULL_FIELDS = gql` diff --git a/pages/[name]/index.js b/pages/[name]/index.js index a86ae4cdf..f7255524d 100644 --- a/pages/[name]/index.js +++ b/pages/[name]/index.js @@ -71,7 +71,7 @@ export function BioForm ({ handleDone, bio }) { {bio?.text ? : 1, jitem) INTO item; INSERT INTO "ItemForward" ("itemId", "userId", "pct") SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward); @@ -102,3 +106,86 @@ BEGIN RETURN item; END; $$; + +DROP FUNCTION IF EXISTS update_item(JSONB, JSONB, JSONB); +-- support an additional fee parameter to incur on edits +CREATE OR REPLACE FUNCTION update_item( + jitem JSONB, forward JSONB, poll_options JSONB, additional_fee_msats BIGINT) +RETURNS "Item" +LANGUAGE plpgsql +AS $$ +DECLARE + user_msats INTEGER; + item "Item"; + select_clause TEXT; +BEGIN + PERFORM ASSERT_SERIALIZED(); + + item := jsonb_populate_record(NULL::"Item", jitem); + + SELECT msats INTO user_msats FROM users WHERE id = item."userId"; + IF additional_fee_msats > 0 AND additional_fee_msats > user_msats THEN + RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; + END IF; + + UPDATE users SET msats = user_msats - additional_fee_msats WHERE id = item."userId"; + + INSERT INTO "ItemAct" (msats, "itemId", "userId", act) + VALUES (additional_fee_msats, item.id, item."userId", 'FEE'); + + IF item.boost > 0 THEN + UPDATE "Item" SET boost = boost + item.boost WHERE id = item.id; + PERFORM item_act(item.id, item."userId", 'BOOST', item.boost); + END IF; + + IF item.status IS NOT NULL THEN + UPDATE "Item" SET "statusUpdatedAt" = now_utc() + WHERE id = item.id AND status <> item.status; + END IF; + + SELECT string_agg(quote_ident(key), ',') INTO select_clause + FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key) + WHERE key <> 'boost'; + + EXECUTE format($fmt$ + UPDATE "Item" SET (%s) = ( + SELECT %1$s + FROM jsonb_populate_record(NULL::"Item", %L) + ) WHERE id = %L RETURNING * + $fmt$, select_clause, jitem, item.id) INTO item; + + -- Delete any old thread subs if the user is no longer a fwd recipient + DELETE FROM "ThreadSubscription" + WHERE "itemId" = item.id + -- they aren't in the new forward list + AND NOT EXISTS (SELECT 1 FROM jsonb_populate_recordset(NULL::"ItemForward", forward) as nf WHERE "ThreadSubscription"."userId" = nf."userId") + -- and they are in the old forward list + AND EXISTS (SELECT 1 FROM "ItemForward" WHERE "ItemForward"."itemId" = item.id AND "ItemForward"."userId" = "ThreadSubscription"."userId" ); + + -- Automatically subscribe any new forward recipients to the post + INSERT INTO "ThreadSubscription" ("itemId", "userId") + SELECT item.id, "userId" FROM jsonb_populate_recordset(NULL::"ItemForward", forward) + EXCEPT + SELECT item.id, "userId" FROM "ItemForward" WHERE "itemId" = item.id; + + -- Delete all old forward entries, to recreate in next command + DELETE FROM "ItemForward" WHERE "itemId" = item.id; + + INSERT INTO "ItemForward" ("itemId", "userId", "pct") + SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward); + + INSERT INTO "PollOption" ("itemId", "option") + SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option"); + + -- if this is a job + IF item."maxBid" IS NOT NULL THEN + PERFORM run_auction(item.id); + END IF; + + -- schedule imgproxy job + INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter) + VALUES ('imgproxy', jsonb_build_object('id', item.id), 21, true, now() + interval '5 seconds'); + + RETURN item; +END; +$$; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3cf4506ad..0a1578bd2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -286,6 +286,7 @@ model Item { bountyPaidTo Int[] upvotes Int @default(0) weightedComments Float @default(0) + upperTitleFeePaid Boolean @default(false) Bookmark Bookmark[] parent Item? @relation("ParentChildren", fields: [parentId], references: [id]) children Item[] @relation("ParentChildren")
{numWithUnits(paidSats, { abbreviate: false })}{parentId ? 'reply' : 'post'} fee
x 10image/link fee
- {numWithUnits(paidSats, { abbreviate: false })}already paid