spam fees
This commit is contained in:
parent
7cf5396ef3
commit
ddb4a30c4b
|
@ -4,7 +4,8 @@ import serialize from './serial'
|
|||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
||||
import domino from 'domino'
|
||||
import { BOOST_MIN } from '../../lib/constants'
|
||||
import { BOOST_MIN, ITEM_SPAM_INTERVAL } from '../../lib/constants'
|
||||
import { mdHas } from '../../lib/md'
|
||||
|
||||
async function comments (models, id, sort) {
|
||||
let orderBy
|
||||
|
@ -68,6 +69,13 @@ function topClause (within) {
|
|||
|
||||
export default {
|
||||
Query: {
|
||||
itemRepetition: async (parent, { parentId }, { me, models }) => {
|
||||
if (!me) return 0
|
||||
// how many of the parents starting at parentId belong to me
|
||||
const [{ item_spam: count }] = await models.$queryRaw(`SELECT item_spam($1, $2, '${ITEM_SPAM_INTERVAL}')`, Number(parentId), Number(me.id))
|
||||
|
||||
return count
|
||||
},
|
||||
items: async (parent, { sub, sort, cursor, name, within }, { me, models }) => {
|
||||
const decodedCursor = decodeCursor(cursor)
|
||||
let items; let user; let pins; let subFull
|
||||
|
@ -851,6 +859,10 @@ const updateItem = async (parent, { id, data }, { me, models }) => {
|
|||
throw new UserInputError('item can no longer be editted')
|
||||
}
|
||||
|
||||
if (data?.text && !old.paidImgLink && mdHas(data.text, ['link', 'image'])) {
|
||||
throw new UserInputError('adding links or images on edit is not allowed yet')
|
||||
}
|
||||
|
||||
const item = await models.item.update({
|
||||
where: { id: Number(id) },
|
||||
data
|
||||
|
@ -878,21 +890,16 @@ const createItem = async (parent, { title, url, text, boost, forward, parentId }
|
|||
}
|
||||
}
|
||||
|
||||
const hasImgLink = mdHas(text, ['link', 'image'])
|
||||
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6) AS "Item"`,
|
||||
title, url, text, Number(boost || 0), Number(parentId), Number(me.id)))
|
||||
models.$queryRaw(
|
||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||
title, url, text, Number(boost || 0), Number(parentId), Number(me.id),
|
||||
Number(fwdUser?.id), hasImgLink))
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
if (fwdUser) {
|
||||
await models.item.update({
|
||||
where: { id: item.id },
|
||||
data: {
|
||||
fwdUserId: fwdUser.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item.comments = []
|
||||
return item
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ export default gql`
|
|||
allItems(cursor: String): Items
|
||||
search(q: String, sub: String, cursor: String): Items
|
||||
auctionPosition(sub: String, id: ID, bid: Int!): Int!
|
||||
itemRepetition(parentId: ID): Int!
|
||||
}
|
||||
|
||||
type ItemActResult {
|
||||
|
|
|
@ -2,11 +2,12 @@ import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
|||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import ActionTooltip from '../components/action-tooltip'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import { useState } from 'react'
|
||||
import FeeButton from './fee-button'
|
||||
|
||||
export function DiscussionForm ({
|
||||
item, editThreshold, titleLabel = 'title',
|
||||
|
@ -15,6 +16,8 @@ export function DiscussionForm ({
|
|||
}) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const [hasImgLink, setHasImgLink] = useState()
|
||||
// const me = useMe()
|
||||
const [upsertDiscussion] = useMutation(
|
||||
gql`
|
||||
mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
||||
|
@ -31,6 +34,8 @@ export function DiscussionForm ({
|
|||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
// const cost = linkOrImg ? 10 : me?.freePosts ? 0 : 1
|
||||
|
||||
return (
|
||||
<Form
|
||||
initial={{
|
||||
|
@ -70,11 +75,17 @@ export function DiscussionForm ({
|
|||
hint={editThreshold
|
||||
? <div className='text-muted font-weight-bold'><Countdown date={editThreshold} /></div>
|
||||
: null}
|
||||
setHasImgLink={setHasImgLink}
|
||||
/>
|
||||
{!item && adv && <AdvPostForm />}
|
||||
<ActionTooltip>
|
||||
<SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : buttonText}</SubmitButton>
|
||||
</ActionTooltip>
|
||||
<div className='mt-3'>
|
||||
{item
|
||||
? <SubmitButton variant='secondary'>save</SubmitButton>
|
||||
: <FeeButton
|
||||
baseFee={1} hasImgLink={hasImgLink} parentId={null} text={buttonText}
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>}
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { Table } from 'react-bootstrap'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import Info from './info'
|
||||
import styles from './fee-button.module.css'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { useFormikContext } from 'formik'
|
||||
|
||||
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
||||
return (
|
||||
<Table className={styles.receipt} borderless size='sm'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{baseFee} sats</td>
|
||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
||||
</tr>
|
||||
{hasImgLink &&
|
||||
<tr>
|
||||
<td>x 10</td>
|
||||
<td align='right' className='font-weight-light'>image/link fee</td>
|
||||
</tr>}
|
||||
{repetition > 0 &&
|
||||
<tr>
|
||||
<td>x 10<sup>{repetition}</sup></td>
|
||||
<td className='font-weight-light' align='right'>{repetition} {parentId ? 'repeat or self replies' : 'posts'} in 10m</td>
|
||||
</tr>}
|
||||
{boost > 0 &&
|
||||
<tr>
|
||||
<td>+ {boost} sats</td>
|
||||
<td className='font-weight-light' align='right'>boost</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td className='font-weight-bold'>{cost} sats</td>
|
||||
<td align='right' className='font-weight-light'>total fee</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, variant, text, alwaysShow }) {
|
||||
const query = parentId
|
||||
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
||||
: gql`{ itemRepetition }`
|
||||
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||
const repetition = data?.itemRepetition || 0
|
||||
const formik = useFormikContext()
|
||||
const boost = formik?.values?.boost || 0
|
||||
const cost = baseFee * (hasImgLink ? 10 : 1) * Math.pow(10, repetition) + Number(boost)
|
||||
|
||||
const show = alwaysShow || !formik?.isSubmitting
|
||||
return (
|
||||
<div className='d-flex align-items-center'>
|
||||
<ActionTooltip overlayText={`${cost} sats`}>
|
||||
<ChildButton variant={variant}>{text}{cost > baseFee && show && <small> {cost} sats</small>}</ChildButton>
|
||||
</ActionTooltip>
|
||||
{cost > baseFee && show &&
|
||||
<Info>
|
||||
<Receipt baseFee={baseFee} hasImgLink={hasImgLink} repetition={repetition} cost={cost} parentId={parentId} boost={boost} />
|
||||
</Info>}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
.receipt {
|
||||
background-color: var(--theme-inputBg);
|
||||
max-width: 250px;
|
||||
margin: auto;
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.receipt td {
|
||||
padding: .25rem .1rem;
|
||||
}
|
||||
|
||||
.receipt tfoot {
|
||||
border-top: 2px solid var(--theme-borderColor);
|
||||
}
|
|
@ -11,6 +11,7 @@ import Markdown from '../svgs/markdown-line.svg'
|
|||
import styles from './form.module.css'
|
||||
import Text from '../components/text'
|
||||
import AddIcon from '../svgs/add-fill.svg'
|
||||
import { mdHas } from '../lib/md'
|
||||
|
||||
export function SubmitButton ({
|
||||
children, variant, value, onClick, ...props
|
||||
|
@ -72,7 +73,7 @@ export function InputSkeleton ({ label, hint }) {
|
|||
)
|
||||
}
|
||||
|
||||
export function MarkdownInput ({ label, topLevel, groupClassName, ...props }) {
|
||||
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, ...props }) {
|
||||
const [tab, setTab] = useState('write')
|
||||
const [, meta] = useField(props)
|
||||
|
||||
|
@ -99,7 +100,12 @@ export function MarkdownInput ({ label, topLevel, groupClassName, ...props }) {
|
|||
</Nav>
|
||||
<div className={tab !== 'write' ? 'd-none' : ''}>
|
||||
<InputInner
|
||||
{...props}
|
||||
{...props} onChange={(formik, e) => {
|
||||
if (onChange) onChange(formik, e)
|
||||
if (setHasImgLink) {
|
||||
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
|
||||
|
|
|
@ -2,13 +2,13 @@ import { Form, Input, SubmitButton } from '../components/form'
|
|||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import ActionTooltip from '../components/action-tooltip'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
import { ITEM_FIELDS } from '../fragments/items'
|
||||
import Item from './item'
|
||||
import AccordianItem from './accordian-item'
|
||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import FeeButton from './fee-button'
|
||||
|
||||
// eslint-disable-next-line
|
||||
const URL = /^((https?|ftp):\/\/)?(www.)?(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
|
||||
|
@ -99,9 +99,14 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
}}
|
||||
/>
|
||||
{!item && <AdvPostForm />}
|
||||
<ActionTooltip>
|
||||
<SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : 'post'}</SubmitButton>
|
||||
</ActionTooltip>
|
||||
<div className='mt-3'>
|
||||
{item
|
||||
? <SubmitButton variant='secondary'>save</SubmitButton>
|
||||
: <FeeButton
|
||||
baseFee={1} parentId={null} text='post'
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>}
|
||||
</div>
|
||||
{dupesData?.dupes?.length > 0 &&
|
||||
<div className='mt-3'>
|
||||
<AccordianItem
|
||||
|
|
|
@ -4,11 +4,10 @@ import { gql, useMutation } from '@apollo/client'
|
|||
import styles from './reply.module.css'
|
||||
import { COMMENTS } from '../fragments/comments'
|
||||
import { useMe } from './me'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Info from './info'
|
||||
import Link from 'next/link'
|
||||
import FeeButton from './fee-button'
|
||||
|
||||
export const CommentSchema = Yup.object({
|
||||
text: Yup.string().required('required').trim()
|
||||
|
@ -25,6 +24,7 @@ export function ReplyOnAnotherPage ({ parentId }) {
|
|||
export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
||||
const [reply, setReply] = useState(replyOpen)
|
||||
const me = useMe()
|
||||
const [hasImgLink, setHasImgLink] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
setReply(replyOpen || !!localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
||||
|
@ -65,7 +65,7 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
|||
}
|
||||
)
|
||||
|
||||
const cost = me?.freeComments ? 0 : Math.pow(10, meComments)
|
||||
// const cost = me?.freeComments ? 0 : Math.pow(10, meComments)
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -91,6 +91,7 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
|||
}
|
||||
resetForm({ text: '' })
|
||||
setReply(replyOpen || false)
|
||||
setHasImgLink(false)
|
||||
}}
|
||||
storageKeyPrefix={'reply-' + parentId}
|
||||
>
|
||||
|
@ -100,18 +101,16 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
|||
minRows={6}
|
||||
autoFocus={!replyOpen}
|
||||
required
|
||||
setHasImgLink={setHasImgLink}
|
||||
hint={me?.freeComments ? <span className='text-success'>{me.freeComments} free comments left</span> : null}
|
||||
/>
|
||||
<div className='d-flex align-items-center mt-1'>
|
||||
<ActionTooltip overlayText={`${cost} sats`}>
|
||||
<SubmitButton variant='secondary'>reply{cost > 1 && <small> {cost} sats</small>}</SubmitButton>
|
||||
</ActionTooltip>
|
||||
{cost > 1 && (
|
||||
<Info>
|
||||
<div className='font-weight-bold'>Multiple replies on the same level get pricier, but we still love your thoughts!</div>
|
||||
</Info>
|
||||
)}
|
||||
</div>
|
||||
{reply &&
|
||||
<div className='mt-1'>
|
||||
<FeeButton
|
||||
baseFee={1} hasImgLink={hasImgLink} parentId={parentId} text='reply'
|
||||
ChildButton={SubmitButton} variant='secondary' alwaysShow
|
||||
/>
|
||||
</div>}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -82,6 +82,11 @@ export default function Text ({ topLevel, noFragments, nofollow, children }) {
|
|||
)
|
||||
},
|
||||
a: ({ node, href, children, ...props }) => {
|
||||
if (children?.some(e => e?.props?.node?.tagName === 'img')) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
// map: fix any highlighted links
|
||||
children = children?.map(e =>
|
||||
typeof e === 'string'
|
||||
? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||
|
|
|
@ -12,3 +12,4 @@ export const UPLOAD_TYPES_ALLOW = [
|
|||
export const COMMENT_DEPTH_LIMIT = 10
|
||||
export const MAX_TITLE_LENGTH = 80
|
||||
export const MAX_POLL_CHOICE_LENGTH = 30
|
||||
export const ITEM_SPAM_INTERVAL = '10m'
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { gfm } from 'micromark-extension-gfm'
|
||||
|
||||
export function mdHas (md, test) {
|
||||
const tree = fromMarkdown(md, {
|
||||
extensions: [gfm()],
|
||||
mdastExtensions: [gfmFromMarkdown()]
|
||||
})
|
||||
|
||||
let found = false
|
||||
visit(tree, test, () => {
|
||||
found = true
|
||||
return false
|
||||
})
|
||||
|
||||
return found
|
||||
}
|
|
@ -8,12 +8,12 @@ import { useState } from 'react'
|
|||
import ItemFull from '../../components/item-full'
|
||||
import * as Yup from 'yup'
|
||||
import { Form, MarkdownInput, SubmitButton } from '../../components/form'
|
||||
import ActionTooltip from '../../components/action-tooltip'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { useMe } from '../../components/me'
|
||||
import { USER_FULL } from '../../fragments/users'
|
||||
import { ITEM_FIELDS } from '../../fragments/items'
|
||||
import { getGetServerSideProps } from '../../api/ssrApollo'
|
||||
import FeeButton from '../../components/fee-button'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(USER_FULL, null,
|
||||
data => !data.user)
|
||||
|
@ -23,6 +23,8 @@ const BioSchema = Yup.object({
|
|||
})
|
||||
|
||||
export function BioForm ({ handleSuccess, bio }) {
|
||||
const [hasImgLink, setHasImgLink] = useState()
|
||||
|
||||
const [upsertBio] = useMutation(
|
||||
gql`
|
||||
${ITEM_FIELDS}
|
||||
|
@ -68,10 +70,16 @@ export function BioForm ({ handleSuccess, bio }) {
|
|||
name='bio'
|
||||
as={TextareaAutosize}
|
||||
minRows={6}
|
||||
setHasImgLink={setHasImgLink}
|
||||
/>
|
||||
<ActionTooltip>
|
||||
<SubmitButton variant='secondary' className='mt-3'>{bio?.text ? 'save' : 'create'}</SubmitButton>
|
||||
</ActionTooltip>
|
||||
<div className='mt-3'>
|
||||
{bio?.text
|
||||
? <SubmitButton variant='secondary'>save</SubmitButton>
|
||||
: <FeeButton
|
||||
baseFee={1} hasImgLink={hasImgLink} parentId={null} text='create'
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Item" ADD COLUMN "paidImgLink" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ALTER COLUMN "freePosts" SET DEFAULT 0;
|
|
@ -0,0 +1,83 @@
|
|||
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
|
||||
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;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION create_item(
|
||||
title TEXT, url TEXT, text TEXT, boost INTEGER,
|
||||
parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
|
||||
has_img_link BOOLEAN, spam_within INTERVAL)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats INTEGER;
|
||||
cost INTEGER;
|
||||
free_posts INTEGER;
|
||||
free_comments INTEGER;
|
||||
freebie BOOLEAN;
|
||||
item "Item";
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT msats, "freePosts", "freeComments"
|
||||
INTO user_msats, free_posts, free_comments
|
||||
FROM users WHERE id = user_id;
|
||||
|
||||
freebie := (parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0);
|
||||
cost := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within)) * CASE WHEN has_img_link THEN 10 ELSE 1 END;
|
||||
|
||||
IF NOT freebie AND cost > user_msats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", "paidImgLink", created_at, updated_at)
|
||||
VALUES (title, url, text, user_id, parent_id, fwd_user_id, has_img_link, now_utc(), now_utc()) RETURNING * INTO item;
|
||||
|
||||
IF freebie THEN
|
||||
IF parent_id IS NULL THEN
|
||||
UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
|
||||
ELSE
|
||||
UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
|
||||
END IF;
|
||||
ELSE
|
||||
UPDATE users SET msats = msats - cost WHERE id = user_id;
|
||||
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (cost / 1000, item.id, user_id, 'VOTE', now_utc(), now_utc());
|
||||
END IF;
|
||||
|
||||
IF boost > 0 THEN
|
||||
PERFORM item_act(item.id, user_id, 'BOOST', boost);
|
||||
END IF;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
|
@ -33,7 +33,7 @@ model User {
|
|||
msats Int @default(0)
|
||||
stackedMsats Int @default(0)
|
||||
freeComments Int @default(0)
|
||||
freePosts Int @default(2)
|
||||
freePosts Int @default(0)
|
||||
checkedNotesAt DateTime?
|
||||
tipDefault Int @default(10)
|
||||
pubkey String? @unique
|
||||
|
@ -166,6 +166,7 @@ model Item {
|
|||
boost Int @default(0)
|
||||
uploadId Int?
|
||||
upload Upload?
|
||||
paidImgLink Boolean @default(false)
|
||||
|
||||
// if sub is null, this is the main sub
|
||||
sub Sub? @relation(fields: [subName], references: [name])
|
||||
|
|
Loading…
Reference in New Issue