Allow SN users to edit special items forever (#1204)

* Allow SN users to edit special items

* Refactor item edit validation

* Create object for user IDs

* Remove anon from SN_USER_IDS

* Fix isMine and myBio checks

* Don't update author

* remove anon from trust graph

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
ekzyis 2024-06-03 11:26:19 -05:00 committed by GitHub
parent e9aa268996
commit 86b857b8d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 75 additions and 56 deletions

View File

@ -1,4 +1,4 @@
import { ANON_USER_ID, AWS_S3_URL_REGEXP } from '@/lib/constants' import { USER_ID, AWS_S3_URL_REGEXP } from '@/lib/constants'
import { msatsToSats } from '@/lib/format' import { msatsToSats } from '@/lib/format'
export default { export default {
@ -17,7 +17,7 @@ export function uploadIdsFromText (text, { models }) {
export async function imageFeesInfo (s3Keys, { models, me }) { export async function imageFeesInfo (s3Keys, { models, me }) {
// returns info object in this format: // returns info object in this format:
// { bytes24h: int, bytesUnpaid: int, nUnpaid: int, imageFeeMsats: BigInt } // { bytes24h: int, bytesUnpaid: int, nUnpaid: int, imageFeeMsats: BigInt }
const [info] = await models.$queryRawUnsafe('SELECT * FROM image_fees_info($1::INTEGER, $2::INTEGER[])', me ? me.id : ANON_USER_ID, s3Keys) const [info] = await models.$queryRawUnsafe('SELECT * FROM image_fees_info($1::INTEGER, $2::INTEGER[])', me ? me.id : USER_ID.anon, s3Keys)
const imageFee = msatsToSats(info.imageFeeMsats) const imageFee = msatsToSats(info.imageFeeMsats)
const totalFeesMsats = info.nUnpaid * Number(info.imageFeeMsats) const totalFeesMsats = info.nUnpaid * Number(info.imageFeeMsats)
const totalFees = msatsToSats(totalFeesMsats) const totalFees = msatsToSats(totalFeesMsats)

View File

@ -8,8 +8,8 @@ import domino from 'domino'
import { import {
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD, ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
ANON_USER_ID, ANON_ITEM_SPAM_INTERVAL, POLL_COST, USER_ID, ANON_ITEM_SPAM_INTERVAL, POLL_COST,
ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_USER_IDS
} from '@/lib/constants' } from '@/lib/constants'
import { msatsToSats } from '@/lib/format' import { msatsToSats } from '@/lib/format'
import { parse } from 'tldts' import { parse } from 'tldts'
@ -876,7 +876,7 @@ export default {
models.$queryRaw` models.$queryRaw`
SELECT SELECT
item_act(${Number(id)}::INTEGER, item_act(${Number(id)}::INTEGER,
${me?.id || ANON_USER_ID}::INTEGER, ${act}::"ItemActType", ${Number(sats)}::INTEGER)`, ${me?.id || USER_ID.anon}::INTEGER, ${act}::"ItemActType", ${Number(sats)}::INTEGER)`,
{ models, lnd, me, hash, hmac, fee: sats } { models, lnd, me, hash, hmac, fee: sats }
) )
} }
@ -1157,7 +1157,7 @@ export default {
return parent.otsHash return parent.otsHash
}, },
deleteScheduledAt: async (item, args, { me, models }) => { deleteScheduledAt: async (item, args, { me, models }) => {
const meId = me?.id ?? ANON_USER_ID const meId = me?.id ?? USER_ID.anon
if (meId !== item.userId) { if (meId !== item.userId) {
// Only query for deleteScheduledAt for your own items to keep DB queries minimized // Only query for deleteScheduledAt for your own items to keep DB queries minimized
return null return null
@ -1166,8 +1166,8 @@ export default {
return deleteJobs[0]?.startafter ?? null return deleteJobs[0]?.startafter ?? null
}, },
reminderScheduledAt: async (item, args, { me, models }) => { reminderScheduledAt: async (item, args, { me, models }) => {
const meId = me?.id ?? ANON_USER_ID const meId = me?.id ?? USER_ID.anon
if (meId !== item.userId || meId === ANON_USER_ID) { if (meId !== item.userId || meId === USER_ID.anon) {
// don't show reminders on an item if it isn't yours // don't show reminders on an item if it isn't yours
// don't support reminders for ANON // don't support reminders for ANON
return null return null
@ -1227,10 +1227,21 @@ export const createMentions = async (item, models) => {
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models, lnd, hash, hmac }) => { export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
// update iff this item belongs to me // update iff this item belongs to me
const old = await models.item.findUnique({ where: { id: Number(item.id) }, include: { sub: true } }) const old = await models.item.findUnique({ where: { id: Number(item.id) }, include: { sub: true } })
if (Number(old.userId) !== Number(me?.id)) {
// author can always edit their own item
const mid = Number(me?.id)
const isMine = Number(old.userId) === mid
// allow admins to edit special items
const allowEdit = ITEM_ALLOW_EDITS.includes(old.id)
const adminEdit = SN_USER_IDS.includes(mid) && allowEdit
if (!isMine && !adminEdit) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } }) throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
} }
if (subName && old.subName !== subName) {
const differentSub = subName && old.subName !== subName
if (differentSub) {
const sub = await models.sub.findUnique({ where: { name: subName } }) const sub = await models.sub.findUnique({ where: { name: subName } })
if (old.freebie) { if (old.freebie) {
if (!sub.allowFreebies) { if (!sub.allowFreebies) {
@ -1244,10 +1255,13 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
// in case they lied about their existing boost // in case they lied about their existing boost
await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost }) await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost })
// prevent update if it's not explicitly allowed, not their bio, not their job and older than 10 minutes
const user = await models.user.findUnique({ where: { id: me.id } }) const user = await models.user.findUnique({ where: { id: me.id } })
if (!ITEM_ALLOW_EDITS.includes(old.id) && user.bioId !== old.id &&
!isJob(item) && Date.now() > new Date(old.createdAt).getTime() + 10 * 60000) { // prevent update if it's not explicitly allowed, not their bio, not their job and older than 10 minutes
const myBio = user.bioId === old.id
const timer = Date.now() < new Date(old.createdAt).getTime() + 10 * 60_000
if (!allowEdit && !myBio && !timer) {
throw new GraphQLError('item can no longer be editted', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('item can no longer be editted', { extensions: { code: 'BAD_INPUT' } })
} }
@ -1266,7 +1280,7 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
} }
} }
item = { subName, userId: me.id, ...item } item = { subName, userId: old.userId, ...item }
const fwdUsers = await getForwardUsers(models, forward) const fwdUsers = await getForwardUsers(models, forward)
const uploadIds = uploadIdsFromText(item.text, { models }) const uploadIds = uploadIdsFromText(item.text, { models })
@ -1303,7 +1317,7 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
item.subName = item.sub item.subName = item.sub
delete item.sub delete item.sub
item.userId = me ? Number(me.id) : ANON_USER_ID item.userId = me ? Number(me.id) : USER_ID.anon
const fwdUsers = await getForwardUsers(models, forward) const fwdUsers = await getForwardUsers(models, forward)
if (item.url && !isJob(item)) { if (item.url && !isJob(item)) {
@ -1367,7 +1381,7 @@ const enqueueDeletionJob = async (item, models) => {
} }
const deleteReminderAndJob = async ({ me, item, models }) => { const deleteReminderAndJob = async ({ me, item, models }) => {
if (me?.id && me.id !== ANON_USER_ID) { if (me?.id && me.id !== USER_ID.anon) {
await models.$transaction([ await models.$transaction([
models.$queryRawUnsafe(` models.$queryRawUnsafe(`
DELETE FROM pgboss.job DELETE FROM pgboss.job
@ -1389,7 +1403,7 @@ const deleteReminderAndJob = async ({ me, item, models }) => {
const createReminderAndJob = async ({ me, item, models }) => { const createReminderAndJob = async ({ me, item, models }) => {
// disallow anon to use reminder // disallow anon to use reminder
if (!me || me.id === ANON_USER_ID) { if (!me || me.id === USER_ID.anon) {
return return
} }
const reminderCommand = getReminderCommand(item.text) const reminderCommand = getReminderCommand(item.text)

View File

@ -1,7 +1,7 @@
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { amountSchema, ssValidate } from '@/lib/validate' import { amountSchema, ssValidate } from '@/lib/validate'
import serialize from './serial' import serialize from './serial'
import { ANON_USER_ID } from '@/lib/constants' import { USER_ID } from '@/lib/constants'
import { getItem } from './item' import { getItem } from './item'
import { topUsers } from './user' import { topUsers } from './user'
@ -165,7 +165,7 @@ export default {
await ssValidate(amountSchema, { amount: sats }) await ssValidate(amountSchema, { amount: sats })
await serialize( await serialize(
models.$queryRaw`SELECT donate(${sats}::INTEGER, ${me?.id || ANON_USER_ID}::INTEGER)`, models.$queryRaw`SELECT donate(${sats}::INTEGER, ${me?.id || USER_ID.anon}::INTEGER)`,
{ models, lnd, me, hash, hmac, fee: sats } { models, lnd, me, hash, hmac, fee: sats }
) )

View File

@ -1,5 +1,5 @@
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { ANON_USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW } from '@/lib/constants' import { USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW } from '@/lib/constants'
import { createPresignedPost } from '@/api/s3' import { createPresignedPost } from '@/api/s3'
export default { export default {
@ -26,7 +26,7 @@ export default {
size, size,
width, width,
height, height,
userId: me?.id || ANON_USER_ID, userId: me?.id || USER_ID.anon,
paid: false paid: false
} }

View File

@ -5,7 +5,7 @@ import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { msatsToSats } from '@/lib/format' import { msatsToSats } from '@/lib/format'
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '@/lib/validate' import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '@/lib/validate'
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item' import { getItem, updateItem, filterClause, createItem, whereClause, muteClause } from './item'
import { ANON_USER_ID, DELETE_USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS } from '@/lib/constants' import { USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS } from '@/lib/constants'
import { viewGroup } from './growth' import { viewGroup } from './growth'
import { timeUnitForRange, whenRange } from '@/lib/time' import { timeUnitForRange, whenRange } from '@/lib/time'
import assertApiKeyNotPermitted from './apiKey' import assertApiKeyNotPermitted from './apiKey'
@ -217,7 +217,7 @@ export default {
SELECT name SELECT name
FROM users FROM users
WHERE ( WHERE (
id > ${RESERVED_MAX_USER_ID} OR id IN (${ANON_USER_ID}, ${DELETE_USER_ID}) id > ${RESERVED_MAX_USER_ID} OR id IN (${USER_ID.anon}, ${USER_ID.delete})
) )
AND SIMILARITY(name, ${q}) > 0.1 AND SIMILARITY(name, ${q}) > 0.1
ORDER BY SIMILARITY(name, ${q}) DESC ORDER BY SIMILARITY(name, ${q}) DESC
@ -518,7 +518,7 @@ export default {
return await models.$queryRaw` return await models.$queryRaw`
SELECT * SELECT *
FROM users FROM users
WHERE (id > ${RESERVED_MAX_USER_ID} OR id IN (${ANON_USER_ID}, ${DELETE_USER_ID})) WHERE (id > ${RESERVED_MAX_USER_ID} OR id IN (${USER_ID.anon}, ${USER_ID.delete}))
AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit) || 5}` AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit) || 5}`
}, },
userStatsActions: async (parent, { when, from, to }, { me, models }) => { userStatsActions: async (parent, { when, from, to }, { me, models }) => {

View File

@ -7,7 +7,7 @@ import { SELECT } from './item'
import { lnAddrOptions } from '@/lib/lnurl' import { lnAddrOptions } from '@/lib/lnurl'
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format' import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
import { CLNAutowithdrawSchema, LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '@/lib/validate' import { CLNAutowithdrawSchema, LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '@/lib/validate'
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, Wallet } from '@/lib/constants' import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, Wallet } from '@/lib/constants'
import { datePivot } from '@/lib/time' import { datePivot } from '@/lib/time'
import assertGofacYourself from './ofac' import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey' import assertApiKeyNotPermitted from './apiKey'
@ -28,7 +28,7 @@ export async function getInvoice (parent, { id }, { me, models, lnd }) {
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
} }
if (inv.user.id === ANON_USER_ID) { if (inv.user.id === USER_ID.anon) {
return inv return inv
} }
if (!me) { if (!me) {
@ -339,7 +339,7 @@ export default {
expirePivot = { seconds: Math.min(expireSecs, 180) } expirePivot = { seconds: Math.min(expireSecs, 180) }
invLimit = ANON_INV_PENDING_LIMIT invLimit = ANON_INV_PENDING_LIMIT
balanceLimit = ANON_BALANCE_LIMIT_MSATS balanceLimit = ANON_BALANCE_LIMIT_MSATS
id = ANON_USER_ID id = USER_ID.anon
} }
const user = await models.user.findUnique({ where: { id } }) const user = await models.user.findUnique({ where: { id } })

View File

@ -2,7 +2,7 @@ import { useApolloClient } from '@apollo/client'
import { useMe } from './me' import { useMe } from './me'
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { datePivot, timeSince } from '@/lib/time' import { datePivot, timeSince } from '@/lib/time'
import { ANON_USER_ID, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants' import { USER_ID, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants'
import { HAS_NOTIFICATIONS } from '@/fragments/notifications' import { HAS_NOTIFICATIONS } from '@/fragments/notifications'
import Item from './item' import Item from './item'
import { RootProvider } from './root' import { RootProvider } from './root'
@ -25,7 +25,7 @@ export function ClientNotificationProvider ({ children }) {
const me = useMe() const me = useMe()
// anons don't have access to /notifications // anons don't have access to /notifications
// but we'll store client notifications anyway for simplicity's sake // but we'll store client notifications anyway for simplicity's sake
const storageKey = `client-notifications:${me?.id || ANON_USER_ID}` const storageKey = `client-notifications:${me?.id || USER_ID.anon}`
useEffect(() => { useEffect(() => {
const loaded = loadNotifications(storageKey, client) const loaded = loadNotifications(storageKey, client)

View File

@ -9,7 +9,7 @@ import Eye from '@/svgs/eye-fill.svg'
import EyeClose from '@/svgs/eye-close-line.svg' import EyeClose from '@/svgs/eye-close-line.svg'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import CommentEdit from './comment-edit' import CommentEdit from './comment-edit'
import { ANON_USER_ID, COMMENT_DEPTH_LIMIT, UNKNOWN_LINK_REL } from '@/lib/constants' import { USER_ID, COMMENT_DEPTH_LIMIT, UNKNOWN_LINK_REL } from '@/lib/constants'
import PayBounty from './pay-bounty' import PayBounty from './pay-bounty'
import BountyIcon from '@/svgs/bounty-bag.svg' import BountyIcon from '@/svgs/bounty-bag.svg'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
@ -128,9 +128,9 @@ export default function Comment ({
const bottomedOut = depth === COMMENT_DEPTH_LIMIT const bottomedOut = depth === COMMENT_DEPTH_LIMIT
// Don't show OP badge when anon user comments on anon user posts // Don't show OP badge when anon user comments on anon user posts
const op = root.user.name === item.user.name && Number(item.user.id) !== ANON_USER_ID const op = root.user.name === item.user.name && Number(item.user.id) !== USER_ID.anon
? 'OP' ? 'OP'
: root.forwards?.some(f => f.user.name === item.user.name) && Number(item.user.id) !== ANON_USER_ID : root.forwards?.some(f => f.user.name === item.user.name) && Number(item.user.id) !== USER_ID.anon
? 'fwd' ? 'fwd'
: null : null
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id)) const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))

View File

@ -4,11 +4,11 @@ import Tooltip from 'react-bootstrap/Tooltip'
import CowboyHatIcon from '@/svgs/cowboy.svg' import CowboyHatIcon from '@/svgs/cowboy.svg'
import AnonIcon from '@/svgs/spy-fill.svg' import AnonIcon from '@/svgs/spy-fill.svg'
import { numWithUnits } from '@/lib/format' import { numWithUnits } from '@/lib/format'
import { AD_USER_ID, ANON_USER_ID } from '@/lib/constants' import { USER_ID } from '@/lib/constants'
export default function Hat ({ user, badge, className = 'ms-1', height = 16, width = 16 }) { export default function Hat ({ user, badge, className = 'ms-1', height = 16, width = 16 }) {
if (!user || Number(user.id) === AD_USER_ID) return null if (!user || Number(user.id) === USER_ID.ad) return null
if (Number(user.id) === ANON_USER_ID) { if (Number(user.id) === USER_ID.anon) {
return ( return (
<HatTooltip overlayText='anonymous'> <HatTooltip overlayText='anonymous'>
{badge {badge

View File

@ -15,7 +15,7 @@ import BookmarkDropdownItem from './bookmark'
import SubscribeDropdownItem from './subscribe' import SubscribeDropdownItem from './subscribe'
import { CopyLinkDropdownItem, CrosspostDropdownItem } from './share' import { CopyLinkDropdownItem, CrosspostDropdownItem } from './share'
import Hat from './hat' import Hat from './hat'
import { AD_USER_ID } from '@/lib/constants' import { USER_ID } from '@/lib/constants'
import ActionDropdown from './action-dropdown' import ActionDropdown from './action-dropdown'
import MuteDropdownItem from './mute' import MuteDropdownItem from './mute'
import { DropdownItemUpVote } from './upvote' import { DropdownItemUpVote } from './upvote'
@ -58,7 +58,7 @@ export default function ItemInfo ({
return ( return (
<div className={className || `${styles.other}`}> <div className={className || `${styles.other}`}>
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === AD_USER_ID) && {!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
<> <>
<span title={`from ${numWithUnits(item.upvotes, { <span title={`from ${numWithUnits(item.upvotes, {
abbreviate: false, abbreviate: false,

View File

@ -2,7 +2,7 @@ import Link from 'next/link'
import styles from './item.module.css' import styles from './item.module.css'
import UpVote from './upvote' import UpVote from './upvote'
import { useRef } from 'react' import { useRef } from 'react'
import { AD_USER_ID, UNKNOWN_LINK_REL } from '@/lib/constants' import { USER_ID, UNKNOWN_LINK_REL } from '@/lib/constants'
import Pin from '@/svgs/pushpin-fill.svg' import Pin from '@/svgs/pushpin-fill.svg'
import reactStringReplace from 'react-string-replace' import reactStringReplace from 'react-string-replace'
import PollIcon from '@/svgs/bar-chart-horizontal-fill.svg' import PollIcon from '@/svgs/bar-chart-horizontal-fill.svg'
@ -64,7 +64,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
? <Pin width={24} height={24} className={styles.pin} /> ? <Pin width={24} height={24} className={styles.pin} />
: item.meDontLikeSats > item.meSats : item.meDontLikeSats > item.meSats
? <DownZap width={24} height={24} className={styles.dontLike} item={item} /> ? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
: Number(item.user?.id) === AD_USER_ID : Number(item.user?.id) === USER_ID.ad
? <AdIcon width={24} height={24} className={styles.ad} /> ? <AdIcon width={24} height={24} className={styles.ad} />
: <UpVote item={item} className={styles.upvote} />} : <UpVote item={item} className={styles.upvote} />}
<div className={styles.hunk}> <div className={styles.hunk}>
@ -99,7 +99,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
full={full} item={item} full={full} item={item}
onQuoteReply={onQuoteReply} onQuoteReply={onQuoteReply}
pinnable={pinnable} pinnable={pinnable}
extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>} extraBadges={Number(item?.user?.id) === USER_ID.ad && <Badge className={styles.newComment} bg={null}>AD</Badge>}
/> />
{belowTitle} {belowTitle}
</div> </div>
@ -130,7 +130,7 @@ export function ItemSummary ({ item }) {
item={item} item={item}
showUser={false} showUser={false}
showActionDropdown={false} showActionDropdown={false}
extraBadges={item.title && Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>} extraBadges={item.title && Number(item?.user?.id) === USER_ID.ad && <Badge className={styles.newComment} bg={null}>AD</Badge>}
/> />
) )

View File

@ -6,7 +6,7 @@ import BackArrow from '../../svgs/arrow-left-line.svg'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import Price from '../price' import Price from '../price'
import SubSelect from '../sub-select' import SubSelect from '../sub-select'
import { ANON_USER_ID, BALANCE_LIMIT_MSATS, Wallet } from '../../lib/constants' import { USER_ID, BALANCE_LIMIT_MSATS, Wallet } from '../../lib/constants'
import Head from 'next/head' import Head from 'next/head'
import NoteIcon from '../../svgs/notification-4-fill.svg' import NoteIcon from '../../svgs/notification-4-fill.svg'
import { useMe } from '../me' import { useMe } from '../me'
@ -307,7 +307,7 @@ export function AnonDropdown ({ path }) {
<Dropdown className={styles.dropdown} align='end'> <Dropdown className={styles.dropdown} align='end'>
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'> <Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'> <Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
@anon<Hat user={{ id: ANON_USER_ID }} /> @anon<Hat user={{ id: USER_ID.anon }} />
</Nav.Link> </Nav.Link>
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu className='p-3'> <Dropdown.Menu className='p-3'>

View File

@ -36,8 +36,16 @@ export const ITEM_SPAM_INTERVAL = '10m'
export const ANON_ITEM_SPAM_INTERVAL = '0' export const ANON_ITEM_SPAM_INTERVAL = '0'
export const INV_PENDING_LIMIT = 100 export const INV_PENDING_LIMIT = 100
export const BALANCE_LIMIT_MSATS = 100000000 // 100k sat export const BALANCE_LIMIT_MSATS = 100000000 // 100k sat
export const SN_USER_IDS = [616, 6030, 4502, 27] export const USER_ID = {
export const SN_NO_REWARDS_IDS = [27, 4502] k00b: 616,
ek: 6030,
sn: 4502,
anon: 27,
ad: 9,
delete: 106
}
export const SN_USER_IDS = [USER_ID.k00b, USER_ID.ek, USER_ID.sn]
export const SN_NO_REWARDS_IDS = [USER_ID.anon, USER_ID.sn]
export const ANON_INV_PENDING_LIMIT = 1000 export const ANON_INV_PENDING_LIMIT = 1000
export const ANON_BALANCE_LIMIT_MSATS = 0 // disable export const ANON_BALANCE_LIMIT_MSATS = 0 // disable
export const MAX_POLL_NUM_CHOICES = 10 export const MAX_POLL_NUM_CHOICES = 10
@ -54,17 +62,14 @@ export const ITEM_TYPES_USER = ['all', 'posts', 'comments', 'bounties', 'links',
export const ITEM_TYPES = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls', 'freebies', 'bios', 'jobs'] export const ITEM_TYPES = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls', 'freebies', 'bios', 'jobs']
export const ITEM_TYPES_UNIVERSAL = ['all', 'posts', 'comments', 'freebies'] export const ITEM_TYPES_UNIVERSAL = ['all', 'posts', 'comments', 'freebies']
export const OLD_ITEM_DAYS = 3 export const OLD_ITEM_DAYS = 3
export const ANON_USER_ID = 27
export const DELETE_USER_ID = 106
export const AD_USER_ID = 9
export const ANON_FEE_MULTIPLIER = 100 export const ANON_FEE_MULTIPLIER = 100
export const SSR = typeof window === 'undefined' export const SSR = typeof window === 'undefined'
export const MAX_FORWARDS = 5 export const MAX_FORWARDS = 5
export const LNURLP_COMMENT_MAX_LENGTH = 1000 export const LNURLP_COMMENT_MAX_LENGTH = 1000
export const RESERVED_MAX_USER_ID = 615 export const RESERVED_MAX_USER_ID = 615
export const GLOBAL_SEED = 616 export const GLOBAL_SEED = USER_ID.k00b
export const FREEBIE_BASE_COST_THRESHOLD = 10 export const FREEBIE_BASE_COST_THRESHOLD = 10
export const USER_IDS_BALANCE_NO_LIMIT = [...SN_USER_IDS, AD_USER_ID] export const USER_IDS_BALANCE_NO_LIMIT = [...SN_USER_IDS, USER_ID.anon, USER_ID.ad]
// WIP ultimately subject to this list: https://ofac.treasury.gov/sanctions-programs-and-country-information // WIP ultimately subject to this list: https://ofac.treasury.gov/sanctions-programs-and-country-information
// From lawyers: north korea, cuba, iran, ukraine, syria // From lawyers: north korea, cuba, iran, ukraine, syria

View File

@ -1,6 +1,6 @@
import webPush from 'web-push' import webPush from 'web-push'
import removeMd from 'remove-markdown' import removeMd from 'remove-markdown'
import { ANON_USER_ID, COMMENT_DEPTH_LIMIT, FOUND_BLURBS, LOST_BLURBS } from './constants' import { USER_ID, COMMENT_DEPTH_LIMIT, FOUND_BLURBS, LOST_BLURBS } from './constants'
import { msatsToSats, numWithUnits } from './format' import { msatsToSats, numWithUnits } from './format'
import models from '@/api/models' import models from '@/api/models'
import { isMuted } from '@/lib/user' import { isMuted } from '@/lib/user'
@ -179,7 +179,7 @@ export const notifyTerritorySubscribers = async ({ models, item }) => {
export const notifyItemParents = async ({ models, item, me }) => { export const notifyItemParents = async ({ models, item, me }) => {
try { try {
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } }) const user = await models.user.findUnique({ where: { id: me?.id || USER_ID.anon } })
const parents = await models.$queryRawUnsafe( 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 ' + 'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2 ' +
'AND NOT EXISTS (SELECT 1 FROM "Mute" m WHERE m."muterId" = p."userId" AND m."mutedId" = $2)', 'AND NOT EXISTS (SELECT 1 FROM "Mute" m WHERE m."muterId" = p."userId" AND m."mutedId" = $2)',

View File

@ -1,5 +1,5 @@
import { deleteObjects } from '@/api/s3' import { deleteObjects } from '@/api/s3'
import { ANON_USER_ID } from '@/lib/constants' import { USER_ID } from '@/lib/constants'
export async function deleteUnusedImages ({ models }) { export async function deleteUnusedImages ({ models }) {
// delete all images in database and S3 which weren't paid in the last 24 hours // delete all images in database and S3 which weren't paid in the last 24 hours
@ -14,7 +14,7 @@ export async function deleteUnusedImages ({ models }) {
AND NOT EXISTS (SELECT * FROM users WHERE "photoId" = "Upload".id) AND NOT EXISTS (SELECT * FROM users WHERE "photoId" = "Upload".id)
AND NOT EXISTS (SELECT * FROM "Item" WHERE "uploadId" = "Upload".id) AND NOT EXISTS (SELECT * FROM "Item" WHERE "uploadId" = "Upload".id)
)) ))
AND created_at < date_trunc('hour', now() - CASE WHEN "userId" = ${ANON_USER_ID} THEN interval '1 hour' ELSE interval '24 hours' END)` AND created_at < date_trunc('hour', now() - CASE WHEN "userId" = ${USER_ID.anon} THEN interval '1 hour' ELSE interval '24 hours' END)`
const s3Keys = unpaidImages.map(({ id }) => id) const s3Keys = unpaidImages.map(({ id }) => id)
if (s3Keys.length === 0) { if (s3Keys.length === 0) {

View File

@ -1,5 +1,5 @@
import * as math from 'mathjs' import * as math from 'mathjs'
import { ANON_USER_ID, SN_USER_IDS } from '@/lib/constants.js' import { USER_ID, SN_USER_IDS } from '@/lib/constants.js'
export async function trust ({ boss, models }) { export async function trust ({ boss, models }) {
try { try {
@ -127,7 +127,7 @@ async function getGraph (models) {
FROM "ItemAct" FROM "ItemAct"
JOIN "Item" ON "Item".id = "ItemAct"."itemId" AND "ItemAct".act IN ('FEE', 'TIP', 'DONT_LIKE_THIS') JOIN "Item" ON "Item".id = "ItemAct"."itemId" AND "ItemAct".act IN ('FEE', 'TIP', 'DONT_LIKE_THIS')
AND "Item"."parentId" IS NULL AND NOT "Item".bio AND "Item"."userId" <> "ItemAct"."userId" AND "Item"."parentId" IS NULL AND NOT "Item".bio AND "Item"."userId" <> "ItemAct"."userId"
JOIN users ON "ItemAct"."userId" = users.id AND users.id <> ${ANON_USER_ID} JOIN users ON "ItemAct"."userId" = users.id AND users.id <> ${USER_ID.anon}
GROUP BY user_id, name, item_id, user_at, against GROUP BY user_id, name, item_id, user_at, against
HAVING CASE WHEN HAVING CASE WHEN
"ItemAct".act = 'DONT_LIKE_THIS' THEN sum("ItemAct".msats) > ${AGAINST_MSAT_MIN} "ItemAct".act = 'DONT_LIKE_THIS' THEN sum("ItemAct".msats) > ${AGAINST_MSAT_MIN}