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')
|
||||
}
|
||||
|
||||
console.log('we are anon so can only perform pessimistic action')
|
||||
return await performPessimisticAction(actionType, args, context)
|
||||
if (context.cost > 0) {
|
||||
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) {
|
||||
try {
|
||||
console.log('enough fee credits available, performing fee credit action')
|
||||
|
@ -100,7 +102,7 @@ async function performFeeCreditAction (actionType, args, context) {
|
|||
|
||||
await tx.user.update({
|
||||
where: {
|
||||
id: me.id
|
||||
id: me?.id ?? USER_ID.anon
|
||||
},
|
||||
data: {
|
||||
msats: {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
|
|||
import { notifyItemMention, notifyMention } from '@/lib/webPush'
|
||||
import { satsToMsats } from '@/lib/format'
|
||||
|
||||
export const anonable = false
|
||||
export const anonable = true
|
||||
export const supportsPessimism = true
|
||||
export const supportsOptimism = false
|
||||
|
||||
|
@ -17,7 +17,7 @@ export async function getCost ({ id, boost = 0, uploadIds }, { me, models }) {
|
|||
}
|
||||
|
||||
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 old = await tx.item.findUnique({
|
||||
where: { id: parseInt(id) },
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
|
||||
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||
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'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
import { parse } from 'tldts'
|
||||
|
@ -20,6 +20,7 @@ import assertGofacYourself from './ofac'
|
|||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import performPaidAction from '../paidAction'
|
||||
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
|
||||
import { verifyHmac } from './wallet'
|
||||
|
||||
function commentsOrderByClause (me, models, sort) {
|
||||
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
|
||||
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) {
|
||||
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')
|
||||
}
|
||||
|
||||
// author can always edit their own item
|
||||
const mid = Number(me?.id)
|
||||
const isMine = Number(old.userId) === mid
|
||||
// author can edit their own item (except anon)
|
||||
const meId = Number(me?.id ?? USER_ID.anon)
|
||||
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
|
||||
const allowEdit = ITEM_ALLOW_EDITS.includes(old.id)
|
||||
const adminEdit = SN_USER_IDS.includes(mid) && allowEdit
|
||||
|
||||
if (!isMine && !adminEdit) {
|
||||
// ownership permission check
|
||||
if (!authorEdit && !adminEdit && !hmacEdit) {
|
||||
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
|
||||
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
|
||||
const myBio = user.bioId === old.id
|
||||
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')
|
||||
}
|
||||
|
||||
|
@ -1309,12 +1315,12 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
|
|||
|
||||
if (old.bio) {
|
||||
// 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) {
|
||||
// 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 {
|
||||
item = { subName, userId: me.id, ...item }
|
||||
item = { subName, userId: meId, ...item }
|
||||
item.forwardUsers = await getForwardUsers(models, forward)
|
||||
}
|
||||
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')
|
||||
}
|
||||
|
||||
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 = {
|
||||
Query: {
|
||||
invoice: getInvoice,
|
||||
|
@ -411,10 +419,7 @@ const resolvers = {
|
|||
createWithdrawl: createWithdrawal,
|
||||
sendToLnAddr,
|
||||
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
||||
const hmac2 = createHmac(hash)
|
||||
if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) {
|
||||
throw new GqlAuthorizationError('bad hmac')
|
||||
}
|
||||
verifyHmac(hash, hmac)
|
||||
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
||||
return await models.invoice.findFirst({ where: { hash } })
|
||||
},
|
||||
|
|
|
@ -35,14 +35,23 @@ export default gql`
|
|||
pinItem(id: ID): Item
|
||||
subscribeItem(id: ID): Item
|
||||
deleteItem(id: ID): Item
|
||||
upsertLink(id: ID, sub: String, title: String!, url: String!, text: String, boost: Int, forward: [ItemForwardInput]): ItemPaidAction!
|
||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: [ItemForwardInput]): ItemPaidAction!
|
||||
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int, boost: Int, forward: [ItemForwardInput]): ItemPaidAction!
|
||||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
upsertLink(
|
||||
id: ID, sub: String, title: String!, url: String!, text: String, boost: Int, forward: [ItemForwardInput],
|
||||
hash: String, hmac: String): ItemPaidAction!
|
||||
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!
|
||||
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!
|
||||
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!
|
||||
pollVote(id: ID!): PollVotePaidAction!
|
||||
toggleOutlaw(id: ID!): Item!
|
||||
|
|
|
@ -36,8 +36,7 @@ export default function ItemInfo ({
|
|||
const { me } = useMe()
|
||||
const toaster = useToast()
|
||||
const router = useRouter()
|
||||
const [canEdit, setCanEdit] =
|
||||
useState(item.mine && (Date.now() < editThreshold))
|
||||
const [canEdit, setCanEdit] = useState(item.mine && (Date.now() < editThreshold))
|
||||
const [hasNewComments, setHasNewComments] = useState(false)
|
||||
const root = useRoot()
|
||||
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
||||
|
@ -50,8 +49,9 @@ export default function ItemInfo ({
|
|||
}, [item])
|
||||
|
||||
useEffect(() => {
|
||||
setCanEdit(item.mine && (Date.now() < editThreshold))
|
||||
}, [item.mine, editThreshold])
|
||||
const invoice = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
|
||||
setCanEdit((item.mine || invoice) && (Date.now() < editThreshold))
|
||||
}, [item.id, item.mine, editThreshold])
|
||||
|
||||
// territory founders can pin any post in their territory
|
||||
// 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)
|
||||
}
|
||||
|
||||
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({
|
||||
variables: {
|
||||
id: item?.id,
|
||||
|
@ -55,6 +64,7 @@ export default function useItemSubmit (mutation,
|
|||
onCompleted: (data) => {
|
||||
onSuccessfulSubmit?.(data, { resetForm })
|
||||
paidMutationOptions?.onCompleted?.(data)
|
||||
saveItemInvoiceHmac(data)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -114,3 +124,16 @@ export function useRetryCreateItem ({ id }) {
|
|||
|
||||
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
|
||||
const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } })
|
||||
// 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
|
||||
update?.(client.cache, { data })
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ import { COMMENTS } from './comments'
|
|||
import { SUB_FULL_FIELDS } from './subs'
|
||||
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`
|
||||
${INVOICE_FIELDS}
|
||||
fragment PaidActionFields on PaidAction {
|
||||
|
@ -115,9 +118,9 @@ export const ACT_MUTATION = gql`
|
|||
export const UPSERT_DISCUSSION = gql`
|
||||
${PAID_ACTION}
|
||||
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,
|
||||
forward: $forward) {
|
||||
forward: $forward, ${HASH_HMAC_INPUT_2}) {
|
||||
result {
|
||||
id
|
||||
deleteScheduledAt
|
||||
|
@ -147,9 +150,9 @@ export const UPSERT_JOB = gql`
|
|||
export const UPSERT_LINK = gql`
|
||||
${PAID_ACTION}
|
||||
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,
|
||||
boost: $boost, forward: $forward) {
|
||||
boost: $boost, forward: $forward, ${HASH_HMAC_INPUT_2}) {
|
||||
result {
|
||||
id
|
||||
deleteScheduledAt
|
||||
|
@ -162,9 +165,11 @@ export const UPSERT_LINK = gql`
|
|||
export const UPSERT_POLL = gql`
|
||||
${PAID_ACTION}
|
||||
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,
|
||||
options: $options, boost: $boost, forward: $forward, pollExpiresAt: $pollExpiresAt) {
|
||||
options: $options, boost: $boost, forward: $forward, pollExpiresAt: $pollExpiresAt,
|
||||
${HASH_HMAC_INPUT_2}) {
|
||||
result {
|
||||
id
|
||||
deleteScheduledAt
|
||||
|
@ -213,8 +218,8 @@ export const CREATE_COMMENT = gql`
|
|||
export const UPDATE_COMMENT = gql`
|
||||
${ITEM_PAID_ACTION_FIELDS}
|
||||
${PAID_ACTION}
|
||||
mutation upsertComment($id: ID!, $text: String!) {
|
||||
upsertComment(id: $id, text: $text) {
|
||||
mutation upsertComment($id: ID!, $text: String!, ${HASH_HMAC_INPUT_1}) {
|
||||
upsertComment(id: $id, text: $text, ${HASH_HMAC_INPUT_2}) {
|
||||
...ItemPaidActionFields
|
||||
...PaidActionFields
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export const USER_ID = {
|
|||
delete: 106,
|
||||
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 ANON_INV_PENDING_LIMIT = 1000
|
||||
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 GLOBAL_SEED = USER_ID.k00b
|
||||
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
|
||||
// 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.'
|
||||
]
|
||||
|
||||
export const ITEM_ALLOW_EDITS = [
|
||||
export const ADMIN_ITEMS = [
|
||||
// FAQ, old privacy policy, changelog, content guidelines, tos, new privacy policy, copyright policy
|
||||
349, 76894, 78763, 81862, 338393, 338369, 338453
|
||||
]
|
||||
|
|
|
@ -15,8 +15,7 @@ import SubSelect from '@/components/sub-select'
|
|||
|
||||
export const getServerSideProps = getGetServerSideProps({
|
||||
query: ITEM,
|
||||
notFound: data => !data.item,
|
||||
authRequired: true
|
||||
notFound: data => !data.item
|
||||
})
|
||||
|
||||
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 { 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 }) {
|
||||
try {
|
||||
|
@ -68,7 +68,7 @@ function trustGivenGraph (graph) {
|
|||
|
||||
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 sqapply = (mat, fn) => {
|
||||
let idx = 0
|
||||
|
@ -151,10 +151,10 @@ async function getGraph (models) {
|
|||
confidence(before - disagree, b_total - after, ${Z_CONFIDENCE})
|
||||
ELSE 0 END AS trust
|
||||
FROM user_pair
|
||||
WHERE NOT (b_id = ANY (${SN_USER_IDS}))
|
||||
WHERE NOT (b_id = ANY (${SN_ADMIN_IDS}))
|
||||
UNION ALL
|
||||
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
|
||||
UNION ALL
|
||||
SELECT a_id AS id, a_id AS oid, ${MAX_TRUST}::float as trust
|
||||
|
|
Loading…
Reference in New Issue