Anon edits (#1393)
* Rename vars around edit permission * Allow anon edits with hash+hmac * Fix missing time zone for invoice.confirmedAt of comments * Fix missing invoice update on item update --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
parent
4340a82a62
commit
8a4e67e9f0
|
@ -48,11 +48,13 @@ export default async function performPaidAction (actionType, args, context) {
|
||||||
throw new Error('You must be logged in to perform this action')
|
throw new Error('You must be logged in to perform this action')
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('we are anon so can only perform pessimistic action')
|
if (context.cost > 0) {
|
||||||
return await performPessimisticAction(actionType, args, context)
|
console.log('we are anon so can only perform pessimistic action that require payment')
|
||||||
|
return await performPessimisticAction(actionType, args, context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRich = context.cost <= context.me.msats
|
const isRich = context.cost <= (context.me?.msats ?? 0)
|
||||||
if (isRich) {
|
if (isRich) {
|
||||||
try {
|
try {
|
||||||
console.log('enough fee credits available, performing fee credit action')
|
console.log('enough fee credits available, performing fee credit action')
|
||||||
|
@ -100,7 +102,7 @@ async function performFeeCreditAction (actionType, args, context) {
|
||||||
|
|
||||||
await tx.user.update({
|
await tx.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: me.id
|
id: me?.id ?? USER_ID.anon
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
msats: {
|
msats: {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
|
||||||
import { notifyItemMention, notifyMention } from '@/lib/webPush'
|
import { notifyItemMention, notifyMention } from '@/lib/webPush'
|
||||||
import { satsToMsats } from '@/lib/format'
|
import { satsToMsats } from '@/lib/format'
|
||||||
|
|
||||||
export const anonable = false
|
export const anonable = true
|
||||||
export const supportsPessimism = true
|
export const supportsPessimism = true
|
||||||
export const supportsOptimism = false
|
export const supportsOptimism = false
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ export async function getCost ({ id, boost = 0, uploadIds }, { me, models }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function perform (args, context) {
|
export async function perform (args, context) {
|
||||||
const { id, boost = 0, uploadIds = [], options: pollOptions = [], forwardUsers: itemForwards = [], invoiceId, ...data } = args
|
const { id, boost = 0, uploadIds = [], options: pollOptions = [], forwardUsers: itemForwards = [], ...data } = args
|
||||||
const { tx, me, models } = context
|
const { tx, me, models } = context
|
||||||
const old = await tx.item.findUnique({
|
const old = await tx.item.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { id: parseInt(id) },
|
||||||
|
|
|
@ -7,7 +7,7 @@ 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,
|
||||||
USER_ID, POLL_COST,
|
USER_ID, POLL_COST,
|
||||||
ITEM_ALLOW_EDITS, GLOBAL_SEED, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_USER_IDS
|
ADMIN_ITEMS, GLOBAL_SEED, NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_ADMIN_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'
|
||||||
|
@ -20,6 +20,7 @@ import assertGofacYourself from './ofac'
|
||||||
import assertApiKeyNotPermitted from './apiKey'
|
import assertApiKeyNotPermitted from './apiKey'
|
||||||
import performPaidAction from '../paidAction'
|
import performPaidAction from '../paidAction'
|
||||||
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
||||||
|
import { verifyHmac } from './wallet'
|
||||||
|
|
||||||
function commentsOrderByClause (me, models, sort) {
|
function commentsOrderByClause (me, models, sort) {
|
||||||
if (sort === 'recent') {
|
if (sort === 'recent') {
|
||||||
|
@ -1257,9 +1258,9 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateItem = async (parent, { sub: subName, forward, ...item }, { me, models, lnd }) => {
|
export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ...item }, { me, models, lnd }) => {
|
||||||
// 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: { invoice: true, sub: true } })
|
||||||
|
|
||||||
if (old.deletedAt) {
|
if (old.deletedAt) {
|
||||||
throw new GqlInputError('item is deleted')
|
throw new GqlInputError('item is deleted')
|
||||||
|
@ -1269,15 +1270,19 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
|
||||||
throw new GqlInputError('cannot edit unpaid item')
|
throw new GqlInputError('cannot edit unpaid item')
|
||||||
}
|
}
|
||||||
|
|
||||||
// author can always edit their own item
|
// author can edit their own item (except anon)
|
||||||
const mid = Number(me?.id)
|
const meId = Number(me?.id ?? USER_ID.anon)
|
||||||
const isMine = Number(old.userId) === mid
|
const authorEdit = !!me && Number(old.userId) === meId
|
||||||
|
// admins can edit special items
|
||||||
|
const adminEdit = ADMIN_ITEMS.includes(old.id) && SN_ADMIN_IDS.includes(meId)
|
||||||
|
// anybody can edit with valid hash+hmac
|
||||||
|
let hmacEdit = false
|
||||||
|
if (old.invoice?.hash && hash && hmac) {
|
||||||
|
hmacEdit = old.invoice.hash === hash && verifyHmac(hash, hmac)
|
||||||
|
}
|
||||||
|
|
||||||
// allow admins to edit special items
|
// ownership permission check
|
||||||
const allowEdit = ITEM_ALLOW_EDITS.includes(old.id)
|
if (!authorEdit && !adminEdit && !hmacEdit) {
|
||||||
const adminEdit = SN_USER_IDS.includes(mid) && allowEdit
|
|
||||||
|
|
||||||
if (!isMine && !adminEdit) {
|
|
||||||
throw new GqlInputError('item does not belong to you')
|
throw new GqlInputError('item does not belong to you')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1292,13 +1297,14 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
|
||||||
// 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 })
|
||||||
|
|
||||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
const user = await models.user.findUnique({ where: { id: meId } })
|
||||||
|
|
||||||
// prevent update if it's not explicitly allowed, not their bio, not their job and older than 10 minutes
|
// 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 myBio = user.bioId === old.id
|
||||||
const timer = Date.now() < new Date(old.invoicePaidAt ?? old.createdAt).getTime() + 10 * 60_000
|
const timer = Date.now() < new Date(old.invoicePaidAt ?? old.createdAt).getTime() + 10 * 60_000
|
||||||
|
|
||||||
if (!allowEdit && !myBio && !timer && !isJob(item)) {
|
// timer permission check
|
||||||
|
if (!adminEdit && !myBio && !timer && !isJob(item)) {
|
||||||
throw new GqlInputError('item can no longer be edited')
|
throw new GqlInputError('item can no longer be edited')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1309,12 +1315,12 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
|
||||||
|
|
||||||
if (old.bio) {
|
if (old.bio) {
|
||||||
// prevent editing a bio like a regular item
|
// prevent editing a bio like a regular item
|
||||||
item = { id: Number(item.id), text: item.text, title: `@${user.name}'s bio`, userId: me.id }
|
item = { id: Number(item.id), text: item.text, title: `@${user.name}'s bio`, userId: meId }
|
||||||
} else if (old.parentId) {
|
} else if (old.parentId) {
|
||||||
// prevent editing a comment like a post
|
// prevent editing a comment like a post
|
||||||
item = { id: Number(item.id), text: item.text, userId: me.id }
|
item = { id: Number(item.id), text: item.text, userId: meId }
|
||||||
} else {
|
} else {
|
||||||
item = { subName, userId: me.id, ...item }
|
item = { subName, userId: meId, ...item }
|
||||||
item.forwardUsers = await getForwardUsers(models, forward)
|
item.forwardUsers = await getForwardUsers(models, forward)
|
||||||
}
|
}
|
||||||
item.uploadIds = uploadIdsFromText(item.text, { models })
|
item.uploadIds = uploadIdsFromText(item.text, { models })
|
||||||
|
|
|
@ -114,6 +114,14 @@ export function createHmac (hash) {
|
||||||
return crypto.createHmac('sha256', key).update(Buffer.from(hash, 'hex')).digest('hex')
|
return crypto.createHmac('sha256', key).update(Buffer.from(hash, 'hex')).digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function verifyHmac (hash, hmac) {
|
||||||
|
const hmac2 = createHmac(hash)
|
||||||
|
if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) {
|
||||||
|
throw new GqlAuthorizationError('bad hmac')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
invoice: getInvoice,
|
invoice: getInvoice,
|
||||||
|
@ -411,10 +419,7 @@ const resolvers = {
|
||||||
createWithdrawl: createWithdrawal,
|
createWithdrawl: createWithdrawal,
|
||||||
sendToLnAddr,
|
sendToLnAddr,
|
||||||
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
||||||
const hmac2 = createHmac(hash)
|
verifyHmac(hash, hmac)
|
||||||
if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) {
|
|
||||||
throw new GqlAuthorizationError('bad hmac')
|
|
||||||
}
|
|
||||||
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
||||||
return await models.invoice.findFirst({ where: { hash } })
|
return await models.invoice.findFirst({ where: { hash } })
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,14 +35,23 @@ export default gql`
|
||||||
pinItem(id: ID): Item
|
pinItem(id: ID): Item
|
||||||
subscribeItem(id: ID): Item
|
subscribeItem(id: ID): Item
|
||||||
deleteItem(id: ID): Item
|
deleteItem(id: ID): Item
|
||||||
upsertLink(id: ID, sub: String, title: String!, url: String!, text: String, boost: Int, forward: [ItemForwardInput]): ItemPaidAction!
|
upsertLink(
|
||||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: [ItemForwardInput]): ItemPaidAction!
|
id: ID, sub: String, title: String!, url: String!, text: String, boost: Int, forward: [ItemForwardInput],
|
||||||
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int, boost: Int, forward: [ItemForwardInput]): ItemPaidAction!
|
hash: String, hmac: String): ItemPaidAction!
|
||||||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
upsertDiscussion(
|
||||||
|
id: ID, sub: String, title: String!, text: String, boost: Int, forward: [ItemForwardInput],
|
||||||
|
hash: String, hmac: String): ItemPaidAction!
|
||||||
|
upsertBounty(
|
||||||
|
id: ID, sub: String, title: String!, text: String, bounty: Int, boost: Int, forward: [ItemForwardInput],
|
||||||
|
hash: String, hmac: String): ItemPaidAction!
|
||||||
|
upsertJob(
|
||||||
|
id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): ItemPaidAction!
|
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): ItemPaidAction!
|
||||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], pollExpiresAt: Date): ItemPaidAction!
|
upsertPoll(
|
||||||
|
id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], pollExpiresAt: Date,
|
||||||
|
hash: String, hmac: String): ItemPaidAction!
|
||||||
updateNoteId(id: ID!, noteId: String!): Item!
|
updateNoteId(id: ID!, noteId: String!): Item!
|
||||||
upsertComment(id:ID, text: String!, parentId: ID): ItemPaidAction!
|
upsertComment(id: ID, text: String!, parentId: ID, hash: String, hmac: String): ItemPaidAction!
|
||||||
act(id: ID!, sats: Int, act: String, idempotent: Boolean): ItemActPaidAction!
|
act(id: ID!, sats: Int, act: String, idempotent: Boolean): ItemActPaidAction!
|
||||||
pollVote(id: ID!): PollVotePaidAction!
|
pollVote(id: ID!): PollVotePaidAction!
|
||||||
toggleOutlaw(id: ID!): Item!
|
toggleOutlaw(id: ID!): Item!
|
||||||
|
|
|
@ -36,8 +36,7 @@ export default function ItemInfo ({
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [canEdit, setCanEdit] =
|
const [canEdit, setCanEdit] = useState(item.mine && (Date.now() < editThreshold))
|
||||||
useState(item.mine && (Date.now() < editThreshold))
|
|
||||||
const [hasNewComments, setHasNewComments] = useState(false)
|
const [hasNewComments, setHasNewComments] = useState(false)
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
||||||
|
@ -50,8 +49,9 @@ export default function ItemInfo ({
|
||||||
}, [item])
|
}, [item])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCanEdit(item.mine && (Date.now() < editThreshold))
|
const invoice = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
|
||||||
}, [item.mine, editThreshold])
|
setCanEdit((item.mine || invoice) && (Date.now() < editThreshold))
|
||||||
|
}, [item.id, item.mine, editThreshold])
|
||||||
|
|
||||||
// territory founders can pin any post in their territory
|
// territory founders can pin any post in their territory
|
||||||
// and OPs can pin any root reply in their post
|
// and OPs can pin any root reply in their post
|
||||||
|
|
|
@ -27,6 +27,15 @@ export default function useItemSubmit (mutation,
|
||||||
options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0)
|
options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item?.id) {
|
||||||
|
const invoiceData = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
|
||||||
|
if (invoiceData) {
|
||||||
|
const [hash, hmac] = invoiceData.split(':')
|
||||||
|
values.hash = hash
|
||||||
|
values.hmac = hmac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error, payError } = await upsertItem({
|
const { data, error, payError } = await upsertItem({
|
||||||
variables: {
|
variables: {
|
||||||
id: item?.id,
|
id: item?.id,
|
||||||
|
@ -55,6 +64,7 @@ export default function useItemSubmit (mutation,
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
onSuccessfulSubmit?.(data, { resetForm })
|
onSuccessfulSubmit?.(data, { resetForm })
|
||||||
paidMutationOptions?.onCompleted?.(data)
|
paidMutationOptions?.onCompleted?.(data)
|
||||||
|
saveItemInvoiceHmac(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -114,3 +124,16 @@ export function useRetryCreateItem ({ id }) {
|
||||||
|
|
||||||
return retryPaidAction
|
return retryPaidAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveItemInvoiceHmac (mutationData) {
|
||||||
|
const response = Object.values(mutationData)[0]
|
||||||
|
|
||||||
|
if (!response?.invoice) return
|
||||||
|
|
||||||
|
const id = response.result.id
|
||||||
|
const { hash, hmac } = response.invoice
|
||||||
|
|
||||||
|
if (id && hash && hmac) {
|
||||||
|
window.localStorage.setItem(`item:${id}:hash:hmac`, `${hash}:${hmac}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -100,7 +100,13 @@ export function usePaidMutation (mutation,
|
||||||
// if the mutation didn't return any data, ie pessimistic, we need to fetch it
|
// if the mutation didn't return any data, ie pessimistic, we need to fetch it
|
||||||
const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } })
|
const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } })
|
||||||
// create new data object
|
// create new data object
|
||||||
data = { [Object.keys(data)[0]]: paidAction }
|
// ( hmac is only returned on invoice creation so we need to add it back to the data )
|
||||||
|
data = {
|
||||||
|
[Object.keys(data)[0]]: {
|
||||||
|
...paidAction,
|
||||||
|
invoice: { ...paidAction.invoice, hmac: invoice.hmac }
|
||||||
|
}
|
||||||
|
}
|
||||||
// we need to run update functions on mutations now that we have the data
|
// we need to run update functions on mutations now that we have the data
|
||||||
update?.(client.cache, { data })
|
update?.(client.cache, { data })
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ import { COMMENTS } from './comments'
|
||||||
import { SUB_FULL_FIELDS } from './subs'
|
import { SUB_FULL_FIELDS } from './subs'
|
||||||
import { INVOICE_FIELDS } from './wallet'
|
import { INVOICE_FIELDS } from './wallet'
|
||||||
|
|
||||||
|
const HASH_HMAC_INPUT_1 = '$hash: String, $hmac: String'
|
||||||
|
const HASH_HMAC_INPUT_2 = 'hash: $hash, hmac: $hmac'
|
||||||
|
|
||||||
export const PAID_ACTION = gql`
|
export const PAID_ACTION = gql`
|
||||||
${INVOICE_FIELDS}
|
${INVOICE_FIELDS}
|
||||||
fragment PaidActionFields on PaidAction {
|
fragment PaidActionFields on PaidAction {
|
||||||
|
@ -115,9 +118,9 @@ export const ACT_MUTATION = gql`
|
||||||
export const UPSERT_DISCUSSION = gql`
|
export const UPSERT_DISCUSSION = gql`
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String,
|
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String,
|
||||||
$boost: Int, $forward: [ItemForwardInput]) {
|
$boost: Int, $forward: [ItemForwardInput], ${HASH_HMAC_INPUT_1}) {
|
||||||
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost,
|
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost,
|
||||||
forward: $forward) {
|
forward: $forward, ${HASH_HMAC_INPUT_2}) {
|
||||||
result {
|
result {
|
||||||
id
|
id
|
||||||
deleteScheduledAt
|
deleteScheduledAt
|
||||||
|
@ -147,9 +150,9 @@ export const UPSERT_JOB = gql`
|
||||||
export const UPSERT_LINK = gql`
|
export const UPSERT_LINK = gql`
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!,
|
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!,
|
||||||
$text: String, $boost: Int, $forward: [ItemForwardInput]) {
|
$text: String, $boost: Int, $forward: [ItemForwardInput], ${HASH_HMAC_INPUT_1}) {
|
||||||
upsertLink(sub: $sub, id: $id, title: $title, url: $url, text: $text,
|
upsertLink(sub: $sub, id: $id, title: $title, url: $url, text: $text,
|
||||||
boost: $boost, forward: $forward) {
|
boost: $boost, forward: $forward, ${HASH_HMAC_INPUT_2}) {
|
||||||
result {
|
result {
|
||||||
id
|
id
|
||||||
deleteScheduledAt
|
deleteScheduledAt
|
||||||
|
@ -162,9 +165,11 @@ export const UPSERT_LINK = gql`
|
||||||
export const UPSERT_POLL = gql`
|
export const UPSERT_POLL = gql`
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
||||||
$options: [String!]!, $boost: Int, $forward: [ItemForwardInput], $pollExpiresAt: Date) {
|
$options: [String!]!, $boost: Int, $forward: [ItemForwardInput], $pollExpiresAt: Date,
|
||||||
|
${HASH_HMAC_INPUT_1}) {
|
||||||
upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
|
upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
|
||||||
options: $options, boost: $boost, forward: $forward, pollExpiresAt: $pollExpiresAt) {
|
options: $options, boost: $boost, forward: $forward, pollExpiresAt: $pollExpiresAt,
|
||||||
|
${HASH_HMAC_INPUT_2}) {
|
||||||
result {
|
result {
|
||||||
id
|
id
|
||||||
deleteScheduledAt
|
deleteScheduledAt
|
||||||
|
@ -213,8 +218,8 @@ export const CREATE_COMMENT = gql`
|
||||||
export const UPDATE_COMMENT = gql`
|
export const UPDATE_COMMENT = gql`
|
||||||
${ITEM_PAID_ACTION_FIELDS}
|
${ITEM_PAID_ACTION_FIELDS}
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertComment($id: ID!, $text: String!) {
|
mutation upsertComment($id: ID!, $text: String!, ${HASH_HMAC_INPUT_1}) {
|
||||||
upsertComment(id: $id, text: $text) {
|
upsertComment(id: $id, text: $text, ${HASH_HMAC_INPUT_2}) {
|
||||||
...ItemPaidActionFields
|
...ItemPaidActionFields
|
||||||
...PaidActionFields
|
...PaidActionFields
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const USER_ID = {
|
||||||
delete: 106,
|
delete: 106,
|
||||||
saloon: 17226
|
saloon: 17226
|
||||||
}
|
}
|
||||||
export const SN_USER_IDS = [USER_ID.k00b, USER_ID.ek, USER_ID.sn]
|
export const SN_ADMIN_IDS = [USER_ID.k00b, USER_ID.ek, USER_ID.sn]
|
||||||
export const SN_NO_REWARDS_IDS = [USER_ID.anon, USER_ID.sn, USER_ID.saloon]
|
export const SN_NO_REWARDS_IDS = [USER_ID.anon, USER_ID.sn, USER_ID.saloon]
|
||||||
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
|
||||||
|
@ -76,7 +76,7 @@ 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 = USER_ID.k00b
|
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, USER_ID.anon, USER_ID.ad]
|
export const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_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
|
||||||
|
@ -132,7 +132,7 @@ export const LOST_BLURBS = [
|
||||||
'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.'
|
'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.'
|
||||||
]
|
]
|
||||||
|
|
||||||
export const ITEM_ALLOW_EDITS = [
|
export const ADMIN_ITEMS = [
|
||||||
// FAQ, old privacy policy, changelog, content guidelines, tos, new privacy policy, copyright policy
|
// FAQ, old privacy policy, changelog, content guidelines, tos, new privacy policy, copyright policy
|
||||||
349, 76894, 78763, 81862, 338393, 338369, 338453
|
349, 76894, 78763, 81862, 338393, 338369, 338453
|
||||||
]
|
]
|
||||||
|
|
|
@ -15,8 +15,7 @@ import SubSelect from '@/components/sub-select'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({
|
export const getServerSideProps = getGetServerSideProps({
|
||||||
query: ITEM,
|
query: ITEM,
|
||||||
notFound: data => !data.item,
|
notFound: data => !data.item
|
||||||
authRequired: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function PostEdit ({ ssrData }) {
|
export default function PostEdit ({ ssrData }) {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- fix missing time zone cast for "Item"."invoicePaidAt"
|
||||||
|
CREATE OR REPLACE FUNCTION item_comments(_item_id int, _level int, _where text, _order_by text)
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql VOLATILE PARALLEL SAFE AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
result jsonb;
|
||||||
|
BEGIN
|
||||||
|
IF _level < 1 THEN
|
||||||
|
RETURN '[]'::jsonb;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXECUTE 'CREATE TEMP TABLE IF NOT EXISTS t_item ON COMMIT DROP AS'
|
||||||
|
|| ' SELECT "Item".*, "Item".created_at at time zone ''UTC'' AS "createdAt", "Item".updated_at at time zone ''UTC'' AS "updatedAt", '
|
||||||
|
|| ' "Item"."invoicePaidAt" at time zone ''UTC'' AS "invoicePaidAtUTC", '
|
||||||
|
|| ' to_jsonb(users.*) as user '
|
||||||
|
|| ' FROM "Item" '
|
||||||
|
|| ' JOIN users ON users.id = "Item"."userId" '
|
||||||
|
|| ' WHERE "Item".path <@ (SELECT path FROM "Item" WHERE id = $1) ' || _where
|
||||||
|
USING _item_id, _level, _where, _order_by;
|
||||||
|
|
||||||
|
EXECUTE ''
|
||||||
|
|| 'SELECT COALESCE(jsonb_agg(sub), ''[]''::jsonb) AS comments '
|
||||||
|
|| 'FROM ( '
|
||||||
|
|| ' SELECT "Item".*, item_comments("Item".id, $2 - 1, $3, $4) AS comments '
|
||||||
|
|| ' FROM t_item "Item"'
|
||||||
|
|| ' WHERE "Item"."parentId" = $1 '
|
||||||
|
|| _order_by
|
||||||
|
|| ' ) sub'
|
||||||
|
INTO result USING _item_id, _level, _where, _order_by;
|
||||||
|
RETURN result;
|
||||||
|
END
|
||||||
|
$$;
|
|
@ -1,5 +1,5 @@
|
||||||
import * as math from 'mathjs'
|
import * as math from 'mathjs'
|
||||||
import { USER_ID, SN_USER_IDS } from '@/lib/constants.js'
|
import { USER_ID, SN_ADMIN_IDS } from '@/lib/constants.js'
|
||||||
|
|
||||||
export async function trust ({ boss, models }) {
|
export async function trust ({ boss, models }) {
|
||||||
try {
|
try {
|
||||||
|
@ -68,7 +68,7 @@ function trustGivenGraph (graph) {
|
||||||
|
|
||||||
console.timeLog('trust', 'transforming result')
|
console.timeLog('trust', 'transforming result')
|
||||||
|
|
||||||
const seedIdxs = SN_USER_IDS.map(id => posByUserId[id])
|
const seedIdxs = SN_ADMIN_IDS.map(id => posByUserId[id])
|
||||||
const isOutlier = (fromIdx, idx) => [...seedIdxs, fromIdx].includes(idx)
|
const isOutlier = (fromIdx, idx) => [...seedIdxs, fromIdx].includes(idx)
|
||||||
const sqapply = (mat, fn) => {
|
const sqapply = (mat, fn) => {
|
||||||
let idx = 0
|
let idx = 0
|
||||||
|
@ -151,10 +151,10 @@ async function getGraph (models) {
|
||||||
confidence(before - disagree, b_total - after, ${Z_CONFIDENCE})
|
confidence(before - disagree, b_total - after, ${Z_CONFIDENCE})
|
||||||
ELSE 0 END AS trust
|
ELSE 0 END AS trust
|
||||||
FROM user_pair
|
FROM user_pair
|
||||||
WHERE NOT (b_id = ANY (${SN_USER_IDS}))
|
WHERE NOT (b_id = ANY (${SN_ADMIN_IDS}))
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT a_id AS id, seed_id AS oid, ${MAX_TRUST}::numeric as trust
|
SELECT a_id AS id, seed_id AS oid, ${MAX_TRUST}::numeric as trust
|
||||||
FROM user_pair, unnest(${SN_USER_IDS}::int[]) seed_id
|
FROM user_pair, unnest(${SN_ADMIN_IDS}::int[]) seed_id
|
||||||
GROUP BY a_id, a_total, seed_id
|
GROUP BY a_id, a_total, seed_id
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT a_id AS id, a_id AS oid, ${MAX_TRUST}::float as trust
|
SELECT a_id AS id, a_id AS oid, ${MAX_TRUST}::float as trust
|
||||||
|
|
Loading…
Reference in New Issue