raise boost minimum to 25k, enhance editing boost, JIT fund editing costs

This commit is contained in:
keyan 2023-09-25 19:54:35 -05:00
parent 374cc26224
commit 370e3c1c48
11 changed files with 95 additions and 60 deletions

View File

@ -12,7 +12,7 @@ import {
import { msatsToSats, numWithUnits } from '../../lib/format'
import { parse } from 'tldts'
import uu from 'url-unshort'
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
import { sendUserNotification } from '../webPush'
import { proxyImages } from './imgproxy'
import { defaultCommentSort } from '../../lib/item'
@ -624,34 +624,34 @@ export default {
return await models.item.update({ where: { id: Number(id) }, data })
},
upsertLink: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
await ssValidate(linkSchema, item, models, me)
await ssValidate(linkSchema, item, { models, me })
if (id) {
return await updateItem(parent, { id, ...item }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
} else {
return await createItem(parent, item, { me, models, lnd, hash, hmac })
}
},
upsertDiscussion: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
await ssValidate(discussionSchema, item, models, me)
await ssValidate(discussionSchema, item, { models, me })
if (id) {
return await updateItem(parent, { id, ...item }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
} else {
return await createItem(parent, item, { me, models, lnd, hash, hmac })
}
},
upsertBounty: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
await ssValidate(bountySchema, item, models, me)
await ssValidate(bountySchema, item, { models, me })
if (id) {
return await updateItem(parent, { id, ...item }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
} else {
return await createItem(parent, item, { me, models, lnd, hash, hmac })
}
},
upsertPoll: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
const optionCount = id
const numExistingChoices = id
? await models.pollOption.count({
where: {
itemId: Number(id)
@ -659,10 +659,10 @@ export default {
})
: 0
await ssValidate(pollSchema, item, models, me, optionCount)
await ssValidate(pollSchema, item, { models, me, numExistingChoices })
if (id) {
return await updateItem(parent, { id, ...item }, { me, models })
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
} else {
item.pollCost = item.pollCost || POLL_COST
return await createItem(parent, item, { me, models, lnd, hash, hmac })
@ -674,7 +674,7 @@ export default {
}
item.location = item.location?.toLowerCase() === 'remote' ? undefined : item.location
await ssValidate(jobSchema, item, models)
await ssValidate(jobSchema, item, { models })
if (item.logo !== undefined) {
item.uploadId = item.logo
delete item.logo
@ -1119,13 +1119,15 @@ export const createMentions = async (item, models) => {
}
}
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models }) => {
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
// update iff this item belongs to me
const old = await models.item.findUnique({ where: { id: Number(item.id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
}
await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost })
// if it's not the FAQ, not their bio, and older than 10 minutes
const user = await models.user.findUnique({ where: { id: me.id } })
if (![349, 76894, 78763, 81862].includes(old.id) && user.bioId !== old.id &&
@ -1141,18 +1143,43 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
item.url = removeTracking(item.url)
item.url = await proxyImages(item.url)
}
// only update item with the boost delta ... this is a bit of hack given the way
// boost used to work
if (item.boost > 0 && old.boost > 0) {
// only update the boost if it is higher than the old boost
if (item.boost > old.boost) {
item.boost = item.boost - old.boost
} else {
delete item.boost
}
}
item = { subName, userId: me.id, ...item }
const fwdUsers = await getForwardUsers(models, forward)
const [rItem] = await serialize(models,
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac)
}
const trx = [
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)))
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options))
]
if (invoice) {
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
}
const query = await serialize(models, ...trx)
const rItem = trx.length > 1 ? query[1][0] : query[0]
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd })
await createMentions(rItem, models)
item.comments = []
return item
rItem.comments = []
return rItem
}
export const createItem = async (parent, { forward, options, ...item }, { me, models, lnd, hash, hmac }) => {

View File

@ -484,7 +484,7 @@ export default {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
await ssValidate(userSchema, data, models)
await ssValidate(userSchema, data, { models })
try {
await models.user.update({ where: { id: me.id }, data })

View File

@ -1,21 +1,21 @@
import AccordianItem from './accordian-item'
import { Input, InputUserSuggest, VariableInput } from './form'
import InputGroup from 'react-bootstrap/InputGroup'
import { BOOST_MIN, MAX_FORWARDS } from '../lib/constants'
import { BOOST_MIN, BOOST_MULT, MAX_FORWARDS } from '../lib/constants'
import Info from './info'
import { numWithUnits } from '../lib/format'
import styles from './adv-post-form.module.css'
const EMPTY_FORWARD = { nym: '', pct: '' }
export function AdvPostInitial ({ forward }) {
export function AdvPostInitial ({ forward, boost }) {
return {
boost: '',
boost: boost || '',
forward: forward?.length ? forward : [EMPTY_FORWARD]
}
}
export default function AdvPostForm ({ edit }) {
export default function AdvPostForm () {
return (
<AccordianItem
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
@ -23,17 +23,17 @@ export default function AdvPostForm ({ edit }) {
<>
<Input
label={
<div className='d-flex align-items-center'>{edit ? 'add boost' : 'boost'}
<div className='d-flex align-items-center'>boost
<Info>
<ol className='fw-bold'>
<li>Boost ranks posts higher temporarily based on the amount</li>
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
<li>Each {numWithUnits(BOOST_MIN, { abbreviate: false })} of boost is equivalent to one trusted upvote
<li>Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to one trusted upvote
<ul>
<li>e.g. {numWithUnits(BOOST_MIN * 2, { abbreviate: false })} is like 2 votes</li>
<li>e.g. {numWithUnits(BOOST_MULT * 5, { abbreviate: false })} is like 5 votes</li>
</ul>
</li>
<li>The decay of boost "votes" increases at 2x the rate of organic votes
<li>The decay of boost "votes" increases at 1.25x the rate of organic votes
<ul>
<li>i.e. boost votes fall out of ranking faster</li>
</ul>

View File

@ -27,7 +27,7 @@ export function BountyForm ({
const router = useRouter()
const client = useApolloClient()
const me = useMe()
const schema = bountySchema(client, me)
const schema = bountySchema({ client, me, existingBoost: item?.boost })
const [upsertBounty] = useMutation(
gql`
mutation upsertBounty(
@ -89,7 +89,7 @@ export function BountyForm ({
title: item?.title || '',
text: item?.text || '',
bounty: item?.bounty || 1000,
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}

View File

@ -25,7 +25,7 @@ export function DiscussionForm ({
const router = useRouter()
const client = useApolloClient()
const me = useMe()
const schema = discussionSchema(client, me)
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
// if Web Share Target API was used
const shareTitle = router.query.title
@ -81,7 +81,7 @@ export function DiscussionForm ({
initial={{
title: item?.title || shareTitle || '',
text: item?.text || '',
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}

View File

@ -137,7 +137,7 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) {
export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton, variant, text, alwaysShow, parentId }) {
const formik = useFormikContext()
const boost = formik?.values?.boost || 0
const boost = (formik?.values?.boost || 0) - (formik?.initialValues?.boost || 0)
const addImgLink = hasImgLink && !hadImgLink
const cost = (addImgLink ? paidSats * 9 : 0) + Number(boost)
@ -148,7 +148,7 @@ export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton,
const show = alwaysShow || !formik?.isSubmitting
return (
<div className='d-flex align-items-center'>
<ActionTooltip overlayText={numWithUnits(cost, { abbreviate: false })}>
<ActionTooltip overlayText={numWithUnits(cost >= 0 ? cost : 0, { abbreviate: false })}>
<ChildButton variant={variant}>{text}{cost > 0 && show && <small> {numWithUnits(cost, { abbreviate: false })}</small>}</ChildButton>
</ActionTooltip>
{cost > 0 && show &&

View File

@ -22,7 +22,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
const router = useRouter()
const client = useApolloClient()
const me = useMe()
const schema = linkSchema(client, me)
const schema = linkSchema({ client, me, existingBoost: item?.boost })
// if Web Share Target API was used
const shareUrl = router.query.url
const shareTitle = router.query.title
@ -123,7 +123,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
initial={{
title: item?.title || shareTitle || '',
url: item?.url || shareUrl || '',
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}

View File

@ -18,7 +18,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
const router = useRouter()
const client = useApolloClient()
const me = useMe()
const schema = pollSchema(client, me)
const schema = pollSchema({ client, me, existingBoost: item?.boost })
const [upsertPoll] = useMutation(
gql`
@ -65,7 +65,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
title: item?.title || '',
text: item?.text || '',
options: initialOptions || ['', ''],
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
...SubSelectInitial({ sub: item?.subName || sub?.name })
}}
schema={schema}

View File

@ -105,7 +105,7 @@ function NymEdit ({ user, setEditting }) {
}
})
const client = useApolloClient()
const schema = userSchema(client)
const schema = userSchema({ client })
return (
<Form

View File

@ -4,7 +4,8 @@ export const SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
export const SUBS_NO_JOBS = SUBS.filter(s => s !== 'jobs')
export const NOFOLLOW_LIMIT = 100
export const BOOST_MIN = 5000
export const BOOST_MULT = 5000
export const BOOST_MIN = BOOST_MULT * 5
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
export const IMAGE_PIXELS_MAX = 35000000
export const UPLOAD_TYPES_ALLOW = [

View File

@ -1,14 +1,14 @@
import { string, ValidationError, number, object, array, addMethod, boolean } from 'yup'
import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS } from './constants'
import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS, BOOST_MULT } from './constants'
import { NAME_QUERY } from '../fragments/users'
import { URL_REGEXP, WS_REGEXP } from './url'
import { SUPPORTED_CURRENCIES } from './currency'
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
export async function ssValidate (schema, data, ...args) {
export async function ssValidate (schema, data, args) {
try {
if (typeof schema === 'function') {
await schema(...args).validate(data)
await schema(args).validate(data)
} else {
await schema.validate(data)
}
@ -43,28 +43,29 @@ const titleValidator = string().required('required').trim().max(
const intValidator = number().typeError('must be a number').integer('must be whole')
async function usernameExists (client, name) {
if (!client) {
async function usernameExists (name, { client, models }) {
if (!client && !models) {
throw new Error('cannot check for user')
}
// apollo client
if (client.query) {
if (client) {
const { data } = await client.query({ query: NAME_QUERY, variables: { name } })
return !data.nameAvailable
}
// prisma client
const user = await client.user.findUnique({ where: { name } })
const user = await models.user.findUnique({ where: { name } })
return !!user
}
export function advPostSchemaMembers (client, me) {
export function advPostSchemaMembers ({ me, existingBoost = 0, ...args }) {
const boostMin = existingBoost || BOOST_MIN
return {
boost: intValidator
.min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).test({
.min(boostMin, `must be ${existingBoost ? '' : 'blank or '}at least ${boostMin}`).test({
name: 'boost',
test: async boost => !boost || boost % BOOST_MIN === 0,
message: `must be divisble be ${BOOST_MIN}`
test: async boost => (!existingBoost && !boost) || boost % BOOST_MULT === 0,
message: `must be divisble be ${BOOST_MULT}`
}),
forward: array()
.max(MAX_FORWARDS, `you can only configure ${MAX_FORWARDS} forward recipients`)
@ -74,7 +75,7 @@ export function advPostSchemaMembers (client, me) {
name: 'nym',
test: async name => {
if (!name || !name.length) return false
return await usernameExists(client, name)
return await usernameExists(name, args)
},
message: 'stacker does not exist'
})
@ -101,41 +102,47 @@ export function advPostSchemaMembers (client, me) {
}
}
export function subSelectSchemaMembers (client) {
export function subSelectSchemaMembers () {
return {
sub: string().required('required').oneOf(SUBS_NO_JOBS, 'required')
}
}
// for testing advPostSchemaMembers in isolation
export function advSchema (args) {
return object({
...advPostSchemaMembers(args)
})
}
export function bountySchema (client, me) {
export function bountySchema (args) {
return object({
title: titleValidator,
bounty: intValidator
.min(1000, 'must be at least 1000')
.max(1000000, 'must be at most 1m'),
...advPostSchemaMembers(client, me),
...advPostSchemaMembers(args),
...subSelectSchemaMembers()
})
}
export function discussionSchema (client, me) {
export function discussionSchema (args) {
return object({
title: titleValidator,
...advPostSchemaMembers(client, me),
...advPostSchemaMembers(args),
...subSelectSchemaMembers()
})
}
export function linkSchema (client, me) {
export function linkSchema (args) {
return object({
title: titleValidator,
url: string().matches(URL_REGEXP, 'invalid url').required('required'),
...advPostSchemaMembers(client, me),
...advPostSchemaMembers(args),
...subSelectSchemaMembers()
})
}
export function pollSchema (client, me, numExistingChoices = 0) {
export function pollSchema ({ numExistingChoices = 0, ...args }) {
return object({
title: titleValidator,
options: array().of(
@ -151,12 +158,12 @@ export function pollSchema (client, me, numExistingChoices = 0) {
message: `at least ${MIN_POLL_NUM_CHOICES} choices required`,
test: arr => arr.length >= MIN_POLL_NUM_CHOICES - numExistingChoices
}),
...advPostSchemaMembers(client, me),
...advPostSchemaMembers(args),
...subSelectSchemaMembers()
})
}
export function userSchema (client) {
export function userSchema (args) {
return object({
name: string()
.required('required')
@ -166,7 +173,7 @@ export function userSchema (client) {
name: 'name',
test: async name => {
if (!name || !name.length) return false
return !(await usernameExists(client, name))
return !(await usernameExists(name, args))
},
message: 'taken'
})