make item creation easier
This commit is contained in:
parent
137e99cf7f
commit
a847b16b2c
@ -5,10 +5,9 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
||||
import domino from 'domino'
|
||||
import {
|
||||
BOOST_MIN, ITEM_SPAM_INTERVAL,
|
||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
|
||||
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
|
||||
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL
|
||||
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL, POLL_COST
|
||||
} from '../../lib/constants'
|
||||
import { msatsToSats, numWithUnits } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
@ -604,57 +603,34 @@ export default {
|
||||
|
||||
return await models.item.update({ where: { id: Number(id) }, data })
|
||||
},
|
||||
upsertLink: async (parent, args, { me, models }) => {
|
||||
const { id, ...data } = args
|
||||
data.url = ensureProtocol(data.url)
|
||||
data.url = removeTracking(data.url)
|
||||
|
||||
await ssValidate(linkSchema, data, models)
|
||||
upsertLink: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(linkSchema, item, models)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
} else {
|
||||
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
|
||||
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
|
||||
}
|
||||
},
|
||||
upsertDiscussion: async (parent, args, { me, models }) => {
|
||||
const { id, ...data } = args
|
||||
|
||||
await ssValidate(discussionSchema, data, models)
|
||||
upsertDiscussion: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(discussionSchema, item, models)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
} else {
|
||||
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
|
||||
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
|
||||
}
|
||||
},
|
||||
upsertBounty: async (parent, args, { me, models }) => {
|
||||
const { id, ...data } = args
|
||||
|
||||
await ssValidate(bountySchema, data, models)
|
||||
upsertBounty: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(bountySchema, item, models)
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
} else {
|
||||
return await createItem(parent, data, { me, models })
|
||||
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
|
||||
}
|
||||
},
|
||||
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 } }))
|
||||
}
|
||||
|
||||
if (!author) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
upsertPoll: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
|
||||
const optionCount = id
|
||||
? await models.pollOption.count({
|
||||
where: {
|
||||
@ -663,93 +639,60 @@ export default {
|
||||
})
|
||||
: 0
|
||||
|
||||
await ssValidate(pollSchema, data, models, optionCount)
|
||||
|
||||
const fwdUsers = await getForwardUsers(models, forward)
|
||||
await ssValidate(pollSchema, item, models, optionCount)
|
||||
|
||||
if (id) {
|
||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
if (Number(old.userId) !== Number(author.id)) {
|
||||
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRawUnsafe(`${SELECT} FROM update_poll($1, $2::INTEGER, $3, $4, $5::INTEGER, $6, $7::JSON) AS "Item"`,
|
||||
sub || 'bitcoin', Number(id), title, text, Number(boost || 0), options, JSON.stringify(fwdUsers)))
|
||||
|
||||
await createMentions(item, models)
|
||||
item.comments = []
|
||||
return item
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
} else {
|
||||
const [query] = await serialize(models,
|
||||
models.$queryRawUnsafe(
|
||||
`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::JSON, '${spamInterval}') AS "Item"`,
|
||||
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(author.id), options, JSON.stringify(fwdUsers)), ...trx)
|
||||
const item = trx.length > 0 ? query[0] : query
|
||||
|
||||
await createMentions(item, models)
|
||||
item.comments = []
|
||||
return item
|
||||
item.pollCost = item.pollCost || POLL_COST
|
||||
return await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
|
||||
}
|
||||
},
|
||||
upsertJob: async (parent, { id, ...data }, { me, models }) => {
|
||||
upsertJob: async (parent, { id, ...item }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in to create job', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
const { sub, title, company, location, remote, text, url, maxBid, status, logo } = data
|
||||
|
||||
const fullSub = await models.sub.findUnique({ where: { name: sub } })
|
||||
if (!fullSub) {
|
||||
throw new GraphQLError('not a valid sub', { extensions: { code: 'BAD_INPUT' } })
|
||||
item.location = item.location?.toLowerCase() === 'remote' ? undefined : item.location
|
||||
await ssValidate(jobSchema, item, models)
|
||||
if (item.logo) {
|
||||
item.uploadId = item.logo
|
||||
delete item.logo
|
||||
}
|
||||
item.maxBid ??= 0
|
||||
|
||||
await ssValidate(jobSchema, data, models)
|
||||
const loc = location.toLowerCase() === 'remote' ? undefined : location
|
||||
|
||||
let item
|
||||
if (id) {
|
||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
if (Number(old.userId) !== Number(me?.id)) {
|
||||
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
([item] = await serialize(models,
|
||||
models.$queryRawUnsafe(
|
||||
`${SELECT} FROM update_job($1::INTEGER, $2, $3, $4, $5::INTEGER, $6, $7, $8, $9::INTEGER, $10::"Status") AS "Item"`,
|
||||
Number(id), title, url, text, Number(maxBid), company, loc, remote, Number(logo), status)))
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
} else {
|
||||
([item] = await serialize(models,
|
||||
models.$queryRawUnsafe(
|
||||
`${SELECT} FROM create_job($1, $2, $3, $4::INTEGER, $5::INTEGER, $6, $7, $8, $9::INTEGER) AS "Item"`,
|
||||
title, url, text, Number(me.id), Number(maxBid), company, loc, remote, Number(logo))))
|
||||
return await createItem(parent, item, { me, models })
|
||||
}
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
return item
|
||||
},
|
||||
createComment: async (parent, data, { me, models }) => {
|
||||
await ssValidate(commentSchema, data)
|
||||
const item = await createItem(parent, data,
|
||||
{ me, models, invoiceHash: data.invoiceHash, invoiceHmac: data.invoiceHmac })
|
||||
// fetch user to get up-to-date name
|
||||
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
||||
upsertComment: async (parent, { id, invoiceHash, invoiceHmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(commentSchema, item)
|
||||
|
||||
const parents = await models.$queryRawUnsafe(
|
||||
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
|
||||
Number(item.parentId), Number(user.id))
|
||||
Promise.allSettled(
|
||||
parents.map(({ userId }) => sendUserNotification(userId, {
|
||||
title: `@${user.name} replied to you`,
|
||||
body: data.text,
|
||||
item,
|
||||
tag: 'REPLY'
|
||||
}))
|
||||
)
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
} else {
|
||||
const rItem = await createItem(parent, item, { me, models, lnd, invoiceHash, invoiceHmac })
|
||||
|
||||
return item
|
||||
},
|
||||
updateComment: async (parent, { id, ...data }, { me, models }) => {
|
||||
await ssValidate(commentSchema, data)
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
const notify = async () => {
|
||||
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
||||
const parents = await models.$queryRawUnsafe(
|
||||
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
|
||||
Number(item.parentId), Number(user.id))
|
||||
Promise.allSettled(
|
||||
parents.map(({ userId }) => sendUserNotification(userId, {
|
||||
title: `@${user.name} replied to you`,
|
||||
body: item.text,
|
||||
item,
|
||||
tag: 'REPLY'
|
||||
}))
|
||||
)
|
||||
}
|
||||
notify().catch(e => console.error(e))
|
||||
|
||||
return rItem
|
||||
}
|
||||
},
|
||||
pollVote: async (parent, { id }, { me, models }) => {
|
||||
if (!me) {
|
||||
@ -1118,84 +1061,78 @@ export const createMentions = async (item, models) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const updateItem = async (parent, { id, data: { sub, title, url, text, boost, forward, bounty, parentId } }, { me, models }) => {
|
||||
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models }) => {
|
||||
// update iff this item belongs to me
|
||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
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' } })
|
||||
}
|
||||
|
||||
// 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 !== id && Date.now() > new Date(old.createdAt).getTime() + 10 * 60000) {
|
||||
if (![349, 76894, 78763, 81862].includes(old.id) && user.bioId !== old.id &&
|
||||
typeof item.maxBid === 'undefined' && Date.now() > new Date(old.createdAt).getTime() + 10 * 60000) {
|
||||
throw new GraphQLError('item can no longer be editted', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
if (boost && boost < BOOST_MIN) {
|
||||
throw new GraphQLError(`boost must be at least ${BOOST_MIN}`, { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
if (!old.parentId && title.length > MAX_TITLE_LENGTH) {
|
||||
throw new GraphQLError('title too long', { extensions: { code: 'BAD_INPUT' } })
|
||||
if (item.text) {
|
||||
item.text = await proxyImages(item.text)
|
||||
}
|
||||
if (item.url && typeof item.maxBid === 'undefined') {
|
||||
item.url = ensureProtocol(item.url)
|
||||
item.url = removeTracking(item.url)
|
||||
item.url = await proxyImages(item.url)
|
||||
}
|
||||
|
||||
item = { subName, userId: me.id, ...item }
|
||||
const fwdUsers = await getForwardUsers(models, forward)
|
||||
url = await proxyImages(url)
|
||||
text = await proxyImages(text)
|
||||
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRawUnsafe(
|
||||
`${SELECT} FROM update_item($1, $2::INTEGER, $3, $4, $5, $6::INTEGER, $7::INTEGER, $8::JSON) AS "Item"`,
|
||||
old.parentId ? null : sub || 'bitcoin', Number(id), title, url, text,
|
||||
Number(boost || 0), bounty ? Number(bounty) : null, JSON.stringify(fwdUsers)))
|
||||
const [rItem] = await serialize(models,
|
||||
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
|
||||
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)))
|
||||
|
||||
await createMentions(item, models)
|
||||
await createMentions(rItem, models)
|
||||
|
||||
item.comments = []
|
||||
return item
|
||||
}
|
||||
|
||||
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash, invoiceHmac }) => {
|
||||
let author = me
|
||||
const createItem = async (parent, { forward, options, ...item }, { me, models, lnd, invoiceHash, invoiceHmac }) => {
|
||||
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
|
||||
|
||||
// rename to match column name
|
||||
item.subName = item.sub
|
||||
delete item.sub
|
||||
|
||||
if (me) {
|
||||
item.userId = Number(me.id)
|
||||
} else {
|
||||
if (!invoiceHash) {
|
||||
throw new GraphQLError('you must be logged in or pay', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, item.parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||
item.userId = invoice.user.id
|
||||
spamInterval = ANON_ITEM_SPAM_INTERVAL
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
||||
if (!author) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
if (boost && boost < BOOST_MIN) {
|
||||
throw new GraphQLError(`boost must be at least ${BOOST_MIN}`, { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
if (!parentId && title.length > MAX_TITLE_LENGTH) {
|
||||
throw new GraphQLError('title too long', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
const fwdUsers = await getForwardUsers(models, forward)
|
||||
url = await proxyImages(url)
|
||||
text = await proxyImages(text)
|
||||
if (item.text) {
|
||||
item.text = await proxyImages(item.text)
|
||||
}
|
||||
if (item.url && typeof item.maxBid === 'undefined') {
|
||||
item.url = ensureProtocol(item.url)
|
||||
item.url = removeTracking(item.url)
|
||||
item.url = await proxyImages(item.url)
|
||||
}
|
||||
|
||||
const [query] = await serialize(
|
||||
const [result] = await serialize(
|
||||
models,
|
||||
models.$queryRawUnsafe(
|
||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::JSON, '${spamInterval}') AS "Item"`,
|
||||
parentId ? null : sub || 'bitcoin',
|
||||
title,
|
||||
url,
|
||||
text,
|
||||
Number(boost || 0),
|
||||
bounty ? Number(bounty) : null,
|
||||
Number(parentId),
|
||||
Number(author.id),
|
||||
JSON.stringify(fwdUsers)),
|
||||
models.$queryRawUnsafe(`${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL) AS "Item"`,
|
||||
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)),
|
||||
...trx)
|
||||
const item = trx.length > 0 ? query[0] : query
|
||||
item = Array.isArray(result) ? result[0] : result
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
|
@ -32,8 +32,7 @@ export default gql`
|
||||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], invoiceHash: String, invoiceHmac: String): Item!
|
||||
createComment(text: String!, parentId: ID!, invoiceHash: String, invoiceHmac: String): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
upsertComment(id:ID, text: String!, parentId: ID, invoiceHash: String, invoiceHmac: String): Item!
|
||||
dontLikeThis(id: ID!): Boolean!
|
||||
act(id: ID!, sats: Int, invoiceHash: String, invoiceHmac: String): ItemActResult!
|
||||
pollVote(id: ID!): ID!
|
||||
|
@ -7,19 +7,19 @@ import Delete from './delete'
|
||||
import { commentSchema } from '../lib/validate'
|
||||
|
||||
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||
const [updateComment] = useMutation(
|
||||
const [upsertComment] = useMutation(
|
||||
gql`
|
||||
mutation updateComment($id: ID! $text: String!) {
|
||||
updateComment(id: $id, text: $text) {
|
||||
mutation upsertComment($id: ID! $text: String!) {
|
||||
upsertComment(id: $id, text: $text) {
|
||||
text
|
||||
}
|
||||
}`, {
|
||||
update (cache, { data: { updateComment } }) {
|
||||
update (cache, { data: { upsertComment } }) {
|
||||
cache.modify({
|
||||
id: `Item:${comment.id}`,
|
||||
fields: {
|
||||
text () {
|
||||
return updateComment.text
|
||||
return upsertComment.text
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -35,7 +35,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
||||
}}
|
||||
schema={commentSchema}
|
||||
onSubmit={async (values, { resetForm }) => {
|
||||
const { error } = await updateComment({ variables: { ...values, id: comment.id } })
|
||||
const { error } = await upsertComment({ variables: { ...values, id: comment.id } })
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
|
@ -43,24 +43,24 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
||||
setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
||||
}, [])
|
||||
|
||||
const [createComment] = useMutation(
|
||||
const [upsertComment] = useMutation(
|
||||
gql`
|
||||
${COMMENTS}
|
||||
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String, $invoiceHmac: String) {
|
||||
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
|
||||
mutation upsertComment($text: String!, $parentId: ID!, $invoiceHash: String, $invoiceHmac: String) {
|
||||
upsertComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
|
||||
...CommentFields
|
||||
comments {
|
||||
...CommentsRecursive
|
||||
}
|
||||
}
|
||||
}`, {
|
||||
update (cache, { data: { createComment } }) {
|
||||
update (cache, { data: { upsertComment } }) {
|
||||
cache.modify({
|
||||
id: `Item:${parentId}`,
|
||||
fields: {
|
||||
comments (existingCommentRefs = []) {
|
||||
const newCommentRef = cache.writeFragment({
|
||||
data: createComment,
|
||||
data: upsertComment,
|
||||
fragment: COMMENTS,
|
||||
fragmentName: 'CommentsRecursive'
|
||||
})
|
||||
@ -86,20 +86,20 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
||||
// so that we don't see indicator for our own comments, we record this comments as the latest time
|
||||
// but we also have record num comments, in case someone else commented when we did
|
||||
const root = ancestors[0]
|
||||
commentsViewedAfterComment(root, createComment.createdAt)
|
||||
commentsViewedAfterComment(root, upsertComment.createdAt)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const submitComment = useCallback(
|
||||
async (_, values, parentId, resetForm, invoiceHash, invoiceHmac) => {
|
||||
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash, invoiceHmac } })
|
||||
const { error } = await upsertComment({ variables: { ...values, parentId, invoiceHash, invoiceHmac } })
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
resetForm({ text: '' })
|
||||
setReply(replyOpen || false)
|
||||
}, [createComment, setReply])
|
||||
}, [upsertComment, setReply])
|
||||
|
||||
const invoiceableCreateComment = useInvoiceable(submitComment)
|
||||
|
||||
|
@ -26,6 +26,7 @@ module.exports = {
|
||||
ANON_BALANCE_LIMIT_MSATS: 0, // disable
|
||||
MAX_POLL_NUM_CHOICES: 10,
|
||||
MIN_POLL_NUM_CHOICES: 2,
|
||||
POLL_COST: 1,
|
||||
ITEM_FILTER_THRESHOLD: 1.2,
|
||||
DONT_LIKE_THIS_COST: 1,
|
||||
COMMENT_TYPE_QUERY: ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks'],
|
||||
|
@ -1,4 +1,5 @@
|
||||
export function ensureProtocol (value) {
|
||||
if (!value) return value
|
||||
value = value.trim()
|
||||
if (!/^([a-z0-9]+:\/\/|mailto:)/.test(value)) {
|
||||
value = 'http://' + value
|
||||
@ -11,6 +12,7 @@ export function isExternal (url) {
|
||||
}
|
||||
|
||||
export function removeTracking (value) {
|
||||
if (!value) return value
|
||||
const exprs = [
|
||||
// twitter URLs
|
||||
/^(?<url>https?:\/\/twitter\.com\/(?:#!\/)?(?<user>\w+)\/status(?:es)?\/(?<id>\d+))/
|
||||
|
@ -63,10 +63,7 @@ export function advPostSchemaMembers (client) {
|
||||
boost: intValidator
|
||||
.min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).test({
|
||||
name: 'boost',
|
||||
test: async boost => {
|
||||
if (!boost || boost % BOOST_MIN === 0) return true
|
||||
return false
|
||||
},
|
||||
test: async boost => !boost || boost % BOOST_MIN === 0,
|
||||
message: `must be divisble be ${BOOST_MIN}`
|
||||
}),
|
||||
// XXX this lets you forward to youself (it's financially equivalent but it should be disallowed)
|
||||
|
189
prisma/migrations/20230824064857_new_create_item/migration.sql
Normal file
189
prisma/migrations/20230824064857_new_create_item/migration.sql
Normal file
@ -0,0 +1,189 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Invoice" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ItemAct" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Mention" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Withdrawl" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "accounts" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "sessions" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "verification_requests" ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- remove boost denormalization
|
||||
DROP TRIGGER IF EXISTS boost_after_act ON "ItemAct";
|
||||
DROP FUNCTION IF EXISTS boost_after_act();
|
||||
|
||||
-- remove functions that are hereto unused
|
||||
DROP FUNCTION IF EXISTS create_bio(title text, text text, user_id integer);
|
||||
DROP FUNCTION IF EXISTS create_item(sub text, title text, url text, text text, boost integer, bounty integer, parent_id integer, user_id integer, forward json, spam_within interval);
|
||||
DROP FUNCTION IF EXISTS create_poll(sub text, title text, text text, poll_cost integer, boost integer, user_id integer, options text[], forward json, spam_within interval);
|
||||
DROP FUNCTION IF EXISTS create_job(title TEXT, url TEXT, text TEXT, user_id INTEGER, job_bid INTEGER, job_company TEXT, job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER);
|
||||
DROP FUNCTION IF EXISTS update_item(
|
||||
sub TEXT, item_id INTEGER, item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
|
||||
item_bounty INTEGER, forward JSON);
|
||||
DROP FUNCTION IF EXISTS update_job(item_id INTEGER,
|
||||
item_title TEXT, item_url TEXT, item_text TEXT, job_bid INTEGER, job_company TEXT,
|
||||
job_location TEXT, job_remote BOOLEAN, job_upload_id INTEGER, job_status "Status");
|
||||
DROP FUNCTION IF EXISTS update_poll(
|
||||
sub TEXT, id INTEGER, title TEXT, text TEXT, boost INTEGER,
|
||||
options TEXT[], forward JSON);
|
||||
|
||||
-- remove type because table "ItemForward" has an implicit type already
|
||||
DROP TYPE IF EXISTS ItemForwardType;
|
||||
|
||||
-- only have one function to create items
|
||||
CREATE OR REPLACE FUNCTION create_item(
|
||||
jitem JSONB, forward JSONB, poll_options JSONB, spam_within INTERVAL)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats BIGINT;
|
||||
cost_msats BIGINT;
|
||||
freebie BOOLEAN;
|
||||
item "Item";
|
||||
med_votes FLOAT;
|
||||
select_clause TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
-- access fields with appropriate types
|
||||
item := jsonb_populate_record(NULL::"Item", jitem);
|
||||
|
||||
SELECT msats INTO user_msats FROM users WHERE id = item."userId";
|
||||
|
||||
IF item."maxBid" IS NOT NULL THEN
|
||||
cost_msats := 1000000;
|
||||
ELSE
|
||||
cost_msats := 1000 * POWER(10, item_spam(item."parentId", item."userId", spam_within));
|
||||
END IF;
|
||||
-- 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 (item.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" = item."userId";
|
||||
|
||||
-- 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 item."userId" = 27 THEN
|
||||
med_votes := 0;
|
||||
ELSE
|
||||
med_votes := ABS(med_votes);
|
||||
END IF;
|
||||
|
||||
-- there's no great way to set default column values when using json_populate_record
|
||||
-- so we need to only select fields with non-null values that way when func input
|
||||
-- does not include a value, the default value is used instead of null
|
||||
SELECT string_agg('"' || key || '"', ',') INTO select_clause
|
||||
FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key);
|
||||
-- insert the item
|
||||
EXECUTE format($fmt$
|
||||
INSERT INTO "Item" (%s, "weightedDownVotes")
|
||||
SELECT %1$s, %L
|
||||
FROM jsonb_populate_record(NULL::"Item", %L) RETURNING *
|
||||
$fmt$, select_clause, med_votes, jitem) INTO item;
|
||||
|
||||
INSERT INTO "ItemForward" ("itemId", "userId", "pct")
|
||||
SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
|
||||
|
||||
INSERT INTO "PollOption" ("itemId", "option")
|
||||
SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
|
||||
|
||||
IF NOT freebie THEN
|
||||
UPDATE users SET msats = msats - cost_msats WHERE id = item."userId";
|
||||
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act)
|
||||
VALUES (cost_msats, item.id, item."userId", 'FEE');
|
||||
END IF;
|
||||
|
||||
-- if this item has boost
|
||||
IF item.boost > 0 THEN
|
||||
PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
|
||||
END IF;
|
||||
|
||||
-- if this is a job
|
||||
IF item."maxBid" IS NOT NULL THEN
|
||||
PERFORM run_auction(item.id);
|
||||
END IF;
|
||||
|
||||
-- if this is a bio
|
||||
IF item.bio THEN
|
||||
UPDATE users SET "bioId" = item.id WHERE id = item."userId";
|
||||
END IF;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- only have one function to update items
|
||||
CREATE OR REPLACE FUNCTION update_item(
|
||||
jitem JSONB, forward JSONB, poll_options JSONB)
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats INTEGER;
|
||||
item "Item";
|
||||
select_clause TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
item := jsonb_populate_record(NULL::"Item", jitem);
|
||||
|
||||
IF item.boost > 0 THEN
|
||||
UPDATE "Item" SET boost = boost + item.boost WHERE id = item.id;
|
||||
PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
|
||||
END IF;
|
||||
|
||||
IF item.status IS NOT NULL THEN
|
||||
UPDATE "Item" SET "statusUpdatedAt" = now_utc()
|
||||
WHERE id = item.id AND status <> item.status;
|
||||
END IF;
|
||||
|
||||
SELECT string_agg('"' || key || '"', ',') INTO select_clause
|
||||
FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key)
|
||||
WHERE key <> 'boost';
|
||||
|
||||
EXECUTE format($fmt$
|
||||
UPDATE "Item" SET (%s) = (
|
||||
SELECT %1$s
|
||||
FROM jsonb_populate_record(NULL::"Item", %L)
|
||||
) WHERE id = %L RETURNING *
|
||||
$fmt$, select_clause, jitem, item.id) INTO item;
|
||||
|
||||
-- Delete all old forward entries, to recreate in next command
|
||||
DELETE FROM "ItemForward" WHERE "itemId" = item.id;
|
||||
|
||||
INSERT INTO "ItemForward" ("itemId", "userId", "pct")
|
||||
SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
|
||||
|
||||
INSERT INTO "PollOption" ("itemId", "option")
|
||||
SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
|
||||
|
||||
-- if this is a job
|
||||
IF item."maxBid" IS NOT NULL THEN
|
||||
PERFORM run_auction(item.id);
|
||||
END IF;
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
@ -376,7 +376,7 @@ model ReferralAct {
|
||||
model ItemAct {
|
||||
id Int @id(map: "Vote_pkey") @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
msats BigInt
|
||||
act ItemActType
|
||||
itemId Int
|
||||
@ -400,7 +400,7 @@ model ItemAct {
|
||||
model Mention {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
itemId Int
|
||||
userId Int
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
@ -415,7 +415,7 @@ model Mention {
|
||||
model Invoice {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId Int
|
||||
hash String @unique(map: "Invoice.hash_unique")
|
||||
bolt11 String
|
||||
@ -434,7 +434,7 @@ model Invoice {
|
||||
model Withdrawl {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId Int
|
||||
hash String
|
||||
bolt11 String
|
||||
@ -452,7 +452,7 @@ model Withdrawl {
|
||||
model Account {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId Int @map("user_id")
|
||||
type String @map("provider_type")
|
||||
provider String @map("provider_id")
|
||||
@ -480,7 +480,7 @@ model Session {
|
||||
id Int @id @default(autoincrement())
|
||||
sessionToken String @unique(map: "sessions.session_token_unique") @map("session_token")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId Int @map("user_id")
|
||||
expires DateTime
|
||||
|
||||
@ -492,7 +492,7 @@ model Session {
|
||||
model VerificationToken {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
identifier String
|
||||
token String @unique(map: "verification_requests.token_unique")
|
||||
expires DateTime
|
||||
|
Loading…
x
Reference in New Issue
Block a user