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 { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
 | 
				
			||||||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
 | 
					import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
 | 
				
			||||||
import domino from 'domino'
 | 
					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) {
 | 
					async function comments (models, id, sort) {
 | 
				
			||||||
  let orderBy
 | 
					  let orderBy
 | 
				
			||||||
@ -68,6 +69,13 @@ function topClause (within) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  Query: {
 | 
					  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 }) => {
 | 
					    items: async (parent, { sub, sort, cursor, name, within }, { me, models }) => {
 | 
				
			||||||
      const decodedCursor = decodeCursor(cursor)
 | 
					      const decodedCursor = decodeCursor(cursor)
 | 
				
			||||||
      let items; let user; let pins; let subFull
 | 
					      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')
 | 
					    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({
 | 
					  const item = await models.item.update({
 | 
				
			||||||
    where: { id: Number(id) },
 | 
					    where: { id: Number(id) },
 | 
				
			||||||
    data
 | 
					    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,
 | 
					  const [item] = await serialize(models,
 | 
				
			||||||
    models.$queryRaw(`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6) AS "Item"`,
 | 
					    models.$queryRaw(
 | 
				
			||||||
      title, url, text, Number(boost || 0), Number(parentId), Number(me.id)))
 | 
					      `${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)
 | 
					  await createMentions(item, models)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (fwdUser) {
 | 
					 | 
				
			||||||
    await models.item.update({
 | 
					 | 
				
			||||||
      where: { id: item.id },
 | 
					 | 
				
			||||||
      data: {
 | 
					 | 
				
			||||||
        fwdUserId: fwdUser.id
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  item.comments = []
 | 
					  item.comments = []
 | 
				
			||||||
  return item
 | 
					  return item
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ export default gql`
 | 
				
			|||||||
    allItems(cursor: String): Items
 | 
					    allItems(cursor: String): Items
 | 
				
			||||||
    search(q: String, sub: String, cursor: String): Items
 | 
					    search(q: String, sub: String, cursor: String): Items
 | 
				
			||||||
    auctionPosition(sub: String, id: ID, bid: Int!): Int!
 | 
					    auctionPosition(sub: String, id: ID, bid: Int!): Int!
 | 
				
			||||||
 | 
					    itemRepetition(parentId: ID): Int!
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type ItemActResult {
 | 
					  type ItemActResult {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,12 @@ import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
 | 
				
			|||||||
import { useRouter } from 'next/router'
 | 
					import { useRouter } from 'next/router'
 | 
				
			||||||
import * as Yup from 'yup'
 | 
					import * as Yup from 'yup'
 | 
				
			||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
 | 
					import { gql, useApolloClient, useMutation } from '@apollo/client'
 | 
				
			||||||
import ActionTooltip from '../components/action-tooltip'
 | 
					 | 
				
			||||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
					import TextareaAutosize from 'react-textarea-autosize'
 | 
				
			||||||
import Countdown from './countdown'
 | 
					import Countdown from './countdown'
 | 
				
			||||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
 | 
					import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
 | 
				
			||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
 | 
					import { MAX_TITLE_LENGTH } from '../lib/constants'
 | 
				
			||||||
 | 
					import { useState } from 'react'
 | 
				
			||||||
 | 
					import FeeButton from './fee-button'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function DiscussionForm ({
 | 
					export function DiscussionForm ({
 | 
				
			||||||
  item, editThreshold, titleLabel = 'title',
 | 
					  item, editThreshold, titleLabel = 'title',
 | 
				
			||||||
@ -15,6 +16,8 @@ export function DiscussionForm ({
 | 
				
			|||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
  const client = useApolloClient()
 | 
					  const client = useApolloClient()
 | 
				
			||||||
 | 
					  const [hasImgLink, setHasImgLink] = useState()
 | 
				
			||||||
 | 
					  // const me = useMe()
 | 
				
			||||||
  const [upsertDiscussion] = useMutation(
 | 
					  const [upsertDiscussion] = useMutation(
 | 
				
			||||||
    gql`
 | 
					    gql`
 | 
				
			||||||
      mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
 | 
					      mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
 | 
				
			||||||
@ -31,6 +34,8 @@ export function DiscussionForm ({
 | 
				
			|||||||
    ...AdvPostSchema(client)
 | 
					    ...AdvPostSchema(client)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // const cost = linkOrImg ? 10 : me?.freePosts ? 0 : 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Form
 | 
					    <Form
 | 
				
			||||||
      initial={{
 | 
					      initial={{
 | 
				
			||||||
@ -70,11 +75,17 @@ export function DiscussionForm ({
 | 
				
			|||||||
        hint={editThreshold
 | 
					        hint={editThreshold
 | 
				
			||||||
          ? <div className='text-muted font-weight-bold'><Countdown date={editThreshold} /></div>
 | 
					          ? <div className='text-muted font-weight-bold'><Countdown date={editThreshold} /></div>
 | 
				
			||||||
          : null}
 | 
					          : null}
 | 
				
			||||||
 | 
					        setHasImgLink={setHasImgLink}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      {!item && adv && <AdvPostForm />}
 | 
					      {!item && adv && <AdvPostForm />}
 | 
				
			||||||
      <ActionTooltip>
 | 
					      <div className='mt-3'>
 | 
				
			||||||
        <SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : buttonText}</SubmitButton>
 | 
					        {item
 | 
				
			||||||
      </ActionTooltip>
 | 
					          ? <SubmitButton variant='secondary'>save</SubmitButton>
 | 
				
			||||||
 | 
					          : <FeeButton
 | 
				
			||||||
 | 
					              baseFee={1} hasImgLink={hasImgLink} parentId={null} text={buttonText}
 | 
				
			||||||
 | 
					              ChildButton={SubmitButton} variant='secondary'
 | 
				
			||||||
 | 
					            />}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </Form>
 | 
					    </Form>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										64
									
								
								components/fee-button.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								components/fee-button.js
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								components/fee-button.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								components/fee-button.module.css
									
									
									
									
									
										Normal file
									
								
							@ -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 styles from './form.module.css'
 | 
				
			||||||
import Text from '../components/text'
 | 
					import Text from '../components/text'
 | 
				
			||||||
import AddIcon from '../svgs/add-fill.svg'
 | 
					import AddIcon from '../svgs/add-fill.svg'
 | 
				
			||||||
 | 
					import { mdHas } from '../lib/md'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function SubmitButton ({
 | 
					export function SubmitButton ({
 | 
				
			||||||
  children, variant, value, onClick, ...props
 | 
					  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 [tab, setTab] = useState('write')
 | 
				
			||||||
  const [, meta] = useField(props)
 | 
					  const [, meta] = useField(props)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -99,7 +100,12 @@ export function MarkdownInput ({ label, topLevel, groupClassName, ...props }) {
 | 
				
			|||||||
        </Nav>
 | 
					        </Nav>
 | 
				
			||||||
        <div className={tab !== 'write' ? 'd-none' : ''}>
 | 
					        <div className={tab !== 'write' ? 'd-none' : ''}>
 | 
				
			||||||
          <InputInner
 | 
					          <InputInner
 | 
				
			||||||
            {...props}
 | 
					            {...props} onChange={(formik, e) => {
 | 
				
			||||||
 | 
					              if (onChange) onChange(formik, e)
 | 
				
			||||||
 | 
					              if (setHasImgLink) {
 | 
				
			||||||
 | 
					                setHasImgLink(mdHas(e.target.value, ['link', 'image']))
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
 | 
					        <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 { useRouter } from 'next/router'
 | 
				
			||||||
import * as Yup from 'yup'
 | 
					import * as Yup from 'yup'
 | 
				
			||||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
 | 
					import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
 | 
				
			||||||
import ActionTooltip from '../components/action-tooltip'
 | 
					 | 
				
			||||||
import Countdown from './countdown'
 | 
					import Countdown from './countdown'
 | 
				
			||||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
 | 
					import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
 | 
				
			||||||
import { ITEM_FIELDS } from '../fragments/items'
 | 
					import { ITEM_FIELDS } from '../fragments/items'
 | 
				
			||||||
import Item from './item'
 | 
					import Item from './item'
 | 
				
			||||||
import AccordianItem from './accordian-item'
 | 
					import AccordianItem from './accordian-item'
 | 
				
			||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
 | 
					import { MAX_TITLE_LENGTH } from '../lib/constants'
 | 
				
			||||||
 | 
					import FeeButton from './fee-button'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line
 | 
					// 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
 | 
					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 />}
 | 
					      {!item && <AdvPostForm />}
 | 
				
			||||||
      <ActionTooltip>
 | 
					      <div className='mt-3'>
 | 
				
			||||||
        <SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : 'post'}</SubmitButton>
 | 
					        {item
 | 
				
			||||||
      </ActionTooltip>
 | 
					          ? <SubmitButton variant='secondary'>save</SubmitButton>
 | 
				
			||||||
 | 
					          : <FeeButton
 | 
				
			||||||
 | 
					              baseFee={1} parentId={null} text='post'
 | 
				
			||||||
 | 
					              ChildButton={SubmitButton} variant='secondary'
 | 
				
			||||||
 | 
					            />}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
      {dupesData?.dupes?.length > 0 &&
 | 
					      {dupesData?.dupes?.length > 0 &&
 | 
				
			||||||
        <div className='mt-3'>
 | 
					        <div className='mt-3'>
 | 
				
			||||||
          <AccordianItem
 | 
					          <AccordianItem
 | 
				
			||||||
 | 
				
			|||||||
@ -4,11 +4,10 @@ import { gql, useMutation } from '@apollo/client'
 | 
				
			|||||||
import styles from './reply.module.css'
 | 
					import styles from './reply.module.css'
 | 
				
			||||||
import { COMMENTS } from '../fragments/comments'
 | 
					import { COMMENTS } from '../fragments/comments'
 | 
				
			||||||
import { useMe } from './me'
 | 
					import { useMe } from './me'
 | 
				
			||||||
import ActionTooltip from './action-tooltip'
 | 
					 | 
				
			||||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
					import TextareaAutosize from 'react-textarea-autosize'
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import { useEffect, useState } from 'react'
 | 
				
			||||||
import Info from './info'
 | 
					 | 
				
			||||||
import Link from 'next/link'
 | 
					import Link from 'next/link'
 | 
				
			||||||
 | 
					import FeeButton from './fee-button'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CommentSchema = Yup.object({
 | 
					export const CommentSchema = Yup.object({
 | 
				
			||||||
  text: Yup.string().required('required').trim()
 | 
					  text: Yup.string().required('required').trim()
 | 
				
			||||||
@ -25,6 +24,7 @@ export function ReplyOnAnotherPage ({ parentId }) {
 | 
				
			|||||||
export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
 | 
					export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
 | 
				
			||||||
  const [reply, setReply] = useState(replyOpen)
 | 
					  const [reply, setReply] = useState(replyOpen)
 | 
				
			||||||
  const me = useMe()
 | 
					  const me = useMe()
 | 
				
			||||||
 | 
					  const [hasImgLink, setHasImgLink] = useState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    setReply(replyOpen || !!localStorage.getItem('reply-' + parentId + '-' + 'text'))
 | 
					    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 (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
@ -91,6 +91,7 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            resetForm({ text: '' })
 | 
					            resetForm({ text: '' })
 | 
				
			||||||
            setReply(replyOpen || false)
 | 
					            setReply(replyOpen || false)
 | 
				
			||||||
 | 
					            setHasImgLink(false)
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
          storageKeyPrefix={'reply-' + parentId}
 | 
					          storageKeyPrefix={'reply-' + parentId}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
@ -100,18 +101,16 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
 | 
				
			|||||||
            minRows={6}
 | 
					            minRows={6}
 | 
				
			||||||
            autoFocus={!replyOpen}
 | 
					            autoFocus={!replyOpen}
 | 
				
			||||||
            required
 | 
					            required
 | 
				
			||||||
 | 
					            setHasImgLink={setHasImgLink}
 | 
				
			||||||
            hint={me?.freeComments ? <span className='text-success'>{me.freeComments} free comments left</span> : null}
 | 
					            hint={me?.freeComments ? <span className='text-success'>{me.freeComments} free comments left</span> : null}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
          <div className='d-flex align-items-center mt-1'>
 | 
					          {reply &&
 | 
				
			||||||
            <ActionTooltip overlayText={`${cost} sats`}>
 | 
					            <div className='mt-1'>
 | 
				
			||||||
              <SubmitButton variant='secondary'>reply{cost > 1 && <small> {cost} sats</small>}</SubmitButton>
 | 
					              <FeeButton
 | 
				
			||||||
            </ActionTooltip>
 | 
					                baseFee={1} hasImgLink={hasImgLink} parentId={parentId} text='reply'
 | 
				
			||||||
            {cost > 1 && (
 | 
					                ChildButton={SubmitButton} variant='secondary' alwaysShow
 | 
				
			||||||
              <Info>
 | 
					              />
 | 
				
			||||||
                <div className='font-weight-bold'>Multiple replies on the same level get pricier, but we still love your thoughts!</div>
 | 
					            </div>}
 | 
				
			||||||
              </Info>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </Form>
 | 
					        </Form>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -82,6 +82,11 @@ export default function Text ({ topLevel, noFragments, nofollow, children }) {
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          a: ({ node, href, children, ...props }) => {
 | 
					          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 =>
 | 
					            children = children?.map(e =>
 | 
				
			||||||
              typeof e === 'string'
 | 
					              typeof e === 'string'
 | 
				
			||||||
              ? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
 | 
					              ? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -12,3 +12,4 @@ export const UPLOAD_TYPES_ALLOW = [
 | 
				
			|||||||
export const COMMENT_DEPTH_LIMIT = 10
 | 
					export const COMMENT_DEPTH_LIMIT = 10
 | 
				
			||||||
export const MAX_TITLE_LENGTH = 80
 | 
					export const MAX_TITLE_LENGTH = 80
 | 
				
			||||||
export const MAX_POLL_CHOICE_LENGTH = 30
 | 
					export const MAX_POLL_CHOICE_LENGTH = 30
 | 
				
			||||||
 | 
					export const ITEM_SPAM_INTERVAL = '10m'
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										19
									
								
								lib/md.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/md.js
									
									
									
									
									
										Normal file
									
								
							@ -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 ItemFull from '../../components/item-full'
 | 
				
			||||||
import * as Yup from 'yup'
 | 
					import * as Yup from 'yup'
 | 
				
			||||||
import { Form, MarkdownInput, SubmitButton } from '../../components/form'
 | 
					import { Form, MarkdownInput, SubmitButton } from '../../components/form'
 | 
				
			||||||
import ActionTooltip from '../../components/action-tooltip'
 | 
					 | 
				
			||||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
					import TextareaAutosize from 'react-textarea-autosize'
 | 
				
			||||||
import { useMe } from '../../components/me'
 | 
					import { useMe } from '../../components/me'
 | 
				
			||||||
import { USER_FULL } from '../../fragments/users'
 | 
					import { USER_FULL } from '../../fragments/users'
 | 
				
			||||||
import { ITEM_FIELDS } from '../../fragments/items'
 | 
					import { ITEM_FIELDS } from '../../fragments/items'
 | 
				
			||||||
import { getGetServerSideProps } from '../../api/ssrApollo'
 | 
					import { getGetServerSideProps } from '../../api/ssrApollo'
 | 
				
			||||||
 | 
					import FeeButton from '../../components/fee-button'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getServerSideProps = getGetServerSideProps(USER_FULL, null,
 | 
					export const getServerSideProps = getGetServerSideProps(USER_FULL, null,
 | 
				
			||||||
  data => !data.user)
 | 
					  data => !data.user)
 | 
				
			||||||
@ -23,6 +23,8 @@ const BioSchema = Yup.object({
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function BioForm ({ handleSuccess, bio }) {
 | 
					export function BioForm ({ handleSuccess, bio }) {
 | 
				
			||||||
 | 
					  const [hasImgLink, setHasImgLink] = useState()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [upsertBio] = useMutation(
 | 
					  const [upsertBio] = useMutation(
 | 
				
			||||||
    gql`
 | 
					    gql`
 | 
				
			||||||
      ${ITEM_FIELDS}
 | 
					      ${ITEM_FIELDS}
 | 
				
			||||||
@ -68,10 +70,16 @@ export function BioForm ({ handleSuccess, bio }) {
 | 
				
			|||||||
          name='bio'
 | 
					          name='bio'
 | 
				
			||||||
          as={TextareaAutosize}
 | 
					          as={TextareaAutosize}
 | 
				
			||||||
          minRows={6}
 | 
					          minRows={6}
 | 
				
			||||||
 | 
					          setHasImgLink={setHasImgLink}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <ActionTooltip>
 | 
					        <div className='mt-3'>
 | 
				
			||||||
          <SubmitButton variant='secondary' className='mt-3'>{bio?.text ? 'save' : 'create'}</SubmitButton>
 | 
					          {bio?.text
 | 
				
			||||||
        </ActionTooltip>
 | 
					            ? <SubmitButton variant='secondary'>save</SubmitButton>
 | 
				
			||||||
 | 
					            : <FeeButton
 | 
				
			||||||
 | 
					                baseFee={1} hasImgLink={hasImgLink} parentId={null} text='create'
 | 
				
			||||||
 | 
					                ChildButton={SubmitButton} variant='secondary'
 | 
				
			||||||
 | 
					              />}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </Form>
 | 
					      </Form>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								prisma/migrations/20220810162813_item_spam/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								prisma/migrations/20220810162813_item_spam/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
				
			||||||
							
								
								
									
										83
									
								
								prisma/migrations/20220810203210_item_spam2/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								prisma/migrations/20220810203210_item_spam2/migration.sql
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
					  msats           Int         @default(0)
 | 
				
			||||||
  stackedMsats    Int         @default(0)
 | 
					  stackedMsats    Int         @default(0)
 | 
				
			||||||
  freeComments    Int         @default(0)
 | 
					  freeComments    Int         @default(0)
 | 
				
			||||||
  freePosts       Int         @default(2)
 | 
					  freePosts       Int         @default(0)
 | 
				
			||||||
  checkedNotesAt  DateTime?
 | 
					  checkedNotesAt  DateTime?
 | 
				
			||||||
  tipDefault      Int         @default(10)
 | 
					  tipDefault      Int         @default(10)
 | 
				
			||||||
  pubkey          String?     @unique
 | 
					  pubkey          String?     @unique
 | 
				
			||||||
@ -166,6 +166,7 @@ model Item {
 | 
				
			|||||||
  boost         Int                   @default(0)
 | 
					  boost         Int                   @default(0)
 | 
				
			||||||
  uploadId      Int?
 | 
					  uploadId      Int?
 | 
				
			||||||
  upload        Upload?
 | 
					  upload        Upload?
 | 
				
			||||||
 | 
					  paidImgLink   Boolean               @default(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // if sub is null, this is the main sub
 | 
					  // if sub is null, this is the main sub
 | 
				
			||||||
  sub     Sub?    @relation(fields: [subName], references: [name])
 | 
					  sub     Sub?    @relation(fields: [subName], references: [name])
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user