anon func mods, e.g. inv limits
This commit is contained in:
parent
41f46cf41e
commit
6ba1c3e8ab
|
@ -8,7 +8,7 @@ import {
|
|||
BOOST_MIN, ITEM_SPAM_INTERVAL,
|
||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
|
||||
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE
|
||||
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL
|
||||
} from '../../lib/constants'
|
||||
import { msatsToSats, numWithUnits } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
|
@ -627,10 +627,12 @@ export default {
|
|||
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
||||
const { sub, forward, boost, title, text, options, invoiceHash, invoiceHmac } = data
|
||||
let author = me
|
||||
let spamInterval = ITEM_SPAM_INTERVAL
|
||||
const trx = []
|
||||
if (!me && invoiceHash) {
|
||||
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, ANON_POST_FEE)
|
||||
author = invoice.user
|
||||
spamInterval = ANON_ITEM_SPAM_INTERVAL
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
||||
|
@ -670,7 +672,7 @@ export default {
|
|||
return item
|
||||
} else {
|
||||
const [query] = await serialize(models,
|
||||
models.$queryRawUnsafe(`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
models.$queryRawUnsafe(`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${spamInterval}') AS "Item"`,
|
||||
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(author.id), options, Number(fwdUser?.id)), ...trx)
|
||||
const item = trx.length > 0 ? query[0] : query
|
||||
|
||||
|
@ -1107,10 +1109,12 @@ export const updateItem = async (parent, { id, data: { sub, title, url, text, bo
|
|||
|
||||
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash, invoiceHmac }) => {
|
||||
let author = me
|
||||
let spamInterval = ITEM_SPAM_INTERVAL
|
||||
const trx = []
|
||||
if (!me && invoiceHash) {
|
||||
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||
author = invoice.user
|
||||
spamInterval = ANON_ITEM_SPAM_INTERVAL
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
||||
|
@ -1140,7 +1144,7 @@ const createItem = async (parent, { sub, title, url, text, boost, forward, bount
|
|||
const [query] = await serialize(
|
||||
models,
|
||||
models.$queryRawUnsafe(
|
||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${spamInterval}') AS "Item"`,
|
||||
parentId ? null : sub || 'bitcoin',
|
||||
title,
|
||||
url,
|
||||
|
|
|
@ -8,7 +8,7 @@ import { SELECT } from './item'
|
|||
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
|
||||
import { msatsToSats, msatsToSatsDecimal } from '../../lib/format'
|
||||
import { amountSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate'
|
||||
import { ANON_USER_ID } from '../../lib/constants'
|
||||
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT } from '../../lib/constants'
|
||||
import { datePivot } from '../../lib/time'
|
||||
|
||||
export async function getInvoice (parent, { id }, { me, models }) {
|
||||
|
@ -210,10 +210,20 @@ export default {
|
|||
createInvoice: async (parent, { amount }, { me, models, lnd }) => {
|
||||
await ssValidate(amountSchema, { amount })
|
||||
|
||||
const user = await models.user.findUnique({ where: { id: me ? me.id : ANON_USER_ID } })
|
||||
const pivot = me ? { hours: 3 } : { minutes: 3 }
|
||||
let expirePivot = { hours: 3 }
|
||||
let invLimit = INV_PENDING_LIMIT
|
||||
let balanceLimit = BALANCE_LIMIT_MSATS
|
||||
let id = me?.id
|
||||
if (!me) {
|
||||
expirePivot = { minutes: 3 }
|
||||
invLimit = ANON_INV_PENDING_LIMIT
|
||||
balanceLimit = ANON_BALANCE_LIMIT_MSATS
|
||||
id = ANON_USER_ID
|
||||
}
|
||||
|
||||
const expiresAt = datePivot(new Date(), pivot)
|
||||
const user = await models.user.findUnique({ where: { id } })
|
||||
|
||||
const expiresAt = datePivot(new Date(), expirePivot)
|
||||
const description = `Funding @${user.name} on stacker.news`
|
||||
try {
|
||||
const invoice = await createInvoice({
|
||||
|
@ -225,7 +235,8 @@ export default {
|
|||
|
||||
const [inv] = await serialize(models,
|
||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description})`)
|
||||
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description},
|
||||
${invLimit}::INTEGER, ${balanceLimit})`)
|
||||
|
||||
// the HMAC is only returned during invoice creation
|
||||
// this makes sure that only the person who created this invoice
|
||||
|
|
|
@ -19,6 +19,11 @@ module.exports = {
|
|||
MAX_TITLE_LENGTH: 80,
|
||||
MAX_POLL_CHOICE_LENGTH: 30,
|
||||
ITEM_SPAM_INTERVAL: '10m',
|
||||
ANON_ITEM_SPAM_INTERVAL: '0',
|
||||
INV_PENDING_LIMIT: 10,
|
||||
BALANCE_LIMIT_MSATS: 1000000000, // 1m sats
|
||||
ANON_INV_PENDING_LIMIT: 100,
|
||||
ANON_BALANCE_LIMIT_MSATS: 100000000000, // 100m sats
|
||||
MAX_POLL_NUM_CHOICES: 10,
|
||||
MIN_POLL_NUM_CHOICES: 2,
|
||||
ITEM_FILTER_THRESHOLD: 1.2,
|
||||
|
|
|
@ -6,6 +6,7 @@ import serialize from '../../../../api/resolvers/serial'
|
|||
import { schnorr } from '@noble/curves/secp256k1'
|
||||
import { createHash } from 'crypto'
|
||||
import { datePivot } from '../../../../lib/time'
|
||||
import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT } from '../../../../lib/constants'
|
||||
|
||||
export default async ({ query: { username, amount, nostr } }, res) => {
|
||||
const user = await models.user.findUnique({ where: { name: username } })
|
||||
|
@ -13,7 +14,7 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
|||
return res.status(400).json({ status: 'ERROR', reason: `user @${username} does not exist` })
|
||||
}
|
||||
try {
|
||||
// if nostr, decode, validate sig, check tags, set description hash
|
||||
// if nostr, decode, validate sig, check tags, set description hash
|
||||
let description, descriptionHash, noteStr
|
||||
if (nostr) {
|
||||
noteStr = decodeURIComponent(nostr)
|
||||
|
@ -48,7 +49,8 @@ export default async ({ query: { username, amount, nostr } }, res) => {
|
|||
|
||||
await serialize(models,
|
||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description})`)
|
||||
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
|
||||
${INV_PENDING_LIMIT}::INTEGER, ${BALANCE_LIMIT_MSATS})`)
|
||||
|
||||
return res.status(200).json({
|
||||
pr: invoice.request,
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
DROP FUNCTION IF EXISTS create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone, msats_req BIGINT, user_id INTEGER, idesc TEXT);
|
||||
-- make invoice limit and balance limit configurable
|
||||
CREATE OR REPLACE FUNCTION create_invoice(hash TEXT, bolt11 TEXT, expires_at timestamp(3) without time zone,
|
||||
msats_req BIGINT, user_id INTEGER, idesc TEXT, inv_limit INTEGER, balance_limit_msats BIGINT)
|
||||
RETURNS "Invoice"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
invoice "Invoice";
|
||||
inv_limit_reached BOOLEAN;
|
||||
balance_limit_reached BOOLEAN;
|
||||
inv_pending_msats BIGINT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
-- prevent too many pending invoices
|
||||
SELECT inv_limit > 0 AND count(*) >= inv_limit, sum("msatsRequested") INTO inv_limit_reached, inv_pending_msats
|
||||
FROM "Invoice"
|
||||
WHERE "userId" = user_id AND "expiresAt" > now_utc() AND "confirmedAt" IS NULL AND cancelled = false;
|
||||
|
||||
IF inv_limit_reached THEN
|
||||
RAISE EXCEPTION 'SN_INV_PENDING_LIMIT';
|
||||
END IF;
|
||||
|
||||
-- prevent pending invoices + msats from exceeding the limit
|
||||
SELECT balance_limit_msats > 0 AND inv_pending_msats+msats_req+msats > balance_limit_msats INTO balance_limit_reached
|
||||
FROM users
|
||||
WHERE id = user_id;
|
||||
|
||||
IF balance_limit_reached THEN
|
||||
RAISE EXCEPTION 'SN_INV_EXCEED_BALANCE';
|
||||
END IF;
|
||||
|
||||
-- we good, proceed frens
|
||||
INSERT INTO "Invoice" (hash, bolt11, "expiresAt", "msatsRequested", "userId", created_at, updated_at, "desc")
|
||||
VALUES (hash, bolt11, expires_at, msats_req, user_id, now_utc(), now_utc(), idesc) RETURNING * INTO invoice;
|
||||
|
||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
||||
VALUES ('checkInvoice', jsonb_build_object('hash', hash), 21, true, now() + interval '10 seconds');
|
||||
|
||||
RETURN invoice;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- don't presume outlaw status for anon posters
|
||||
CREATE OR REPLACE FUNCTION create_item(
|
||||
sub TEXT, title TEXT, url TEXT, text TEXT, boost INTEGER, bounty INTEGER,
|
||||
parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
|
||||
spam_within INTERVAL)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats BIGINT;
|
||||
cost_msats BIGINT;
|
||||
freebie BOOLEAN;
|
||||
item "Item";
|
||||
med_votes FLOAT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT msats INTO user_msats FROM users WHERE id = user_id;
|
||||
|
||||
cost_msats := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within));
|
||||
-- it's only a freebie if it's a 1 sat cost, they have < 1 sat, and boost = 0
|
||||
freebie := (cost_msats <= 1000) AND (user_msats < 1000) AND (boost = 0);
|
||||
|
||||
IF NOT freebie AND cost_msats > user_msats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
-- get this user's median item score
|
||||
SELECT COALESCE(percentile_cont(0.5) WITHIN GROUP(ORDER BY "weightedVotes" - "weightedDownVotes"), 0)
|
||||
INTO med_votes FROM "Item" WHERE "userId" = user_id;
|
||||
|
||||
-- if their median votes are positive, start at 0
|
||||
-- if the median votes are negative, start their post with that many down votes
|
||||
-- basically: if their median post is bad, presume this post is too
|
||||
-- addendum: if they're an anon poster, always start at 0
|
||||
IF med_votes >= 0 OR user_id = 27 THEN
|
||||
med_votes := 0;
|
||||
ELSE
|
||||
med_votes := ABS(med_votes);
|
||||
END IF;
|
||||
|
||||
INSERT INTO "Item"
|
||||
("subName", title, url, text, bounty, "userId", "parentId", "fwdUserId",
|
||||
freebie, "weightedDownVotes", created_at, updated_at)
|
||||
VALUES
|
||||
(sub, title, url, text, bounty, user_id, parent_id, fwd_user_id,
|
||||
freebie, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
|
||||
|
||||
IF NOT freebie THEN
|
||||
UPDATE users SET msats = msats - cost_msats WHERE id = user_id;
|
||||
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (cost_msats, item.id, user_id, 'FEE', now_utc(), now_utc());
|
||||
END IF;
|
||||
|
||||
IF boost > 0 THEN
|
||||
PERFORM item_act(item.id, user_id, 'BOOST', boost);
|
||||
END IF;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- keep item_spam unaware of anon user
|
||||
CREATE OR REPLACE FUNCTION item_spam(parent_id INTEGER, user_id INTEGER, within INTERVAL)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
repeats INTEGER;
|
||||
self_replies INTEGER;
|
||||
BEGIN
|
||||
-- no fee escalation
|
||||
IF within = interval '0' THEN
|
||||
RETURN 0;
|
||||
END IF;
|
||||
|
||||
SELECT count(*) INTO repeats
|
||||
FROM "Item"
|
||||
WHERE (parent_id IS NULL AND "parentId" IS NULL OR "parentId" = parent_id)
|
||||
AND "userId" = user_id
|
||||
AND created_at > now_utc() - within;
|
||||
|
||||
IF parent_id IS NULL THEN
|
||||
RETURN repeats;
|
||||
END IF;
|
||||
|
||||
WITH RECURSIVE base AS (
|
||||
SELECT "Item".id, "Item"."parentId", "Item"."userId"
|
||||
FROM "Item"
|
||||
WHERE id = parent_id AND "userId" = user_id AND created_at > now_utc() - within
|
||||
UNION ALL
|
||||
SELECT "Item".id, "Item"."parentId", "Item"."userId"
|
||||
FROM base p
|
||||
JOIN "Item" ON "Item".id = p."parentId" AND "Item"."userId" = p."userId" AND "Item".created_at > now_utc() - within)
|
||||
SELECT count(*) INTO self_replies FROM base;
|
||||
|
||||
RETURN repeats + self_replies;
|
||||
END;
|
||||
$$;
|
Loading…
Reference in New Issue