stacker.news/api/resolvers/user.js

1135 lines
37 KiB
JavaScript
Raw Normal View History

import { readFile } from 'fs/promises'
import { join, resolve } from 'path'
import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { msatsToSats } from '@/lib/format'
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '@/lib/validate'
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
import { getItem, updateItem, filterClause, createItem, whereClause, muteClause, activeOrMine } from './item'
import { USER_ID, RESERVED_MAX_USER_ID, SN_NO_REWARDS_IDS, INVOICE_ACTION_NOTIFICATION_TYPES } from '@/lib/constants'
2024-01-19 21:19:26 +00:00
import { viewGroup } from './growth'
import { timeUnitForRange, whenRange } from '@/lib/time'
import assertApiKeyNotPermitted from './apiKey'
Store hashed and salted email addresses (#1111) * first pass of hashing user emails * use salt * add a salt to .env.development (prod salt needs to be kept a secret) * move `hashEmail` util to a new util module * trigger a one-time job to migrate existing emails via the worker so we can use the salt from an env var * move newsletter signup move newsletter signup to prisma adapter create user with email code path so we can still auto-enroll email accounts without having to persist the email address in plaintext * remove `email` from api key session lookup query * drop user email index before dropping column * restore email column, just null values instead * fix function name * fix salt and hash raw sql statement * update auth methods email type in typedefs from str to bool * remove todo comment * lowercase email before hashing during migration * check for emailHash and email to accommodate migration window update our lookups to check for a matching emailHash, and then a matching email, in that order, to accommodate the case that a user tries to login via email while the migration is running, and their account has not yet been migrated also update sndev to have a command `./sndev email` to launch the mailhog inbox in your browser also update `./sndev login` to hash the generated email address and insert it into the db record * update sndev help * update awards.csv * update the hack in next-auth to re-use the email supplied on input to `getUserByEmail` * consolidate console.error logs * create generic open command --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-04 23:06:15 +00:00
import { hashEmail } from '@/lib/crypto'
import { isMuted } from '@/lib/user'
2021-09-23 17:42:00 +00:00
const contributors = new Set()
const loadContributors = async (set) => {
try {
const fileContent = await readFile(resolve(join(process.cwd(), 'contributors.txt')), 'utf-8')
fileContent.split('\n')
.map(line => line.trim())
.filter(line => !!line)
.forEach(name => set.add(name))
} catch (err) {
console.error('Error loading contributors', err)
}
}
2022-06-02 22:55:23 +00:00
async function authMethods (user, args, { models, me }) {
2023-11-10 01:05:35 +00:00
if (!me || me.id !== user.id) {
return {
lightning: false,
twitter: false,
github: false,
nostr: false
}
}
2022-06-02 22:55:23 +00:00
const accounts = await models.account.findMany({
where: {
userId: me.id
}
})
const oauth = accounts.map(a => a.provider)
2022-06-02 22:55:23 +00:00
return {
lightning: !!user.pubkey,
Store hashed and salted email addresses (#1111) * first pass of hashing user emails * use salt * add a salt to .env.development (prod salt needs to be kept a secret) * move `hashEmail` util to a new util module * trigger a one-time job to migrate existing emails via the worker so we can use the salt from an env var * move newsletter signup move newsletter signup to prisma adapter create user with email code path so we can still auto-enroll email accounts without having to persist the email address in plaintext * remove `email` from api key session lookup query * drop user email index before dropping column * restore email column, just null values instead * fix function name * fix salt and hash raw sql statement * update auth methods email type in typedefs from str to bool * remove todo comment * lowercase email before hashing during migration * check for emailHash and email to accommodate migration window update our lookups to check for a matching emailHash, and then a matching email, in that order, to accommodate the case that a user tries to login via email while the migration is running, and their account has not yet been migrated also update sndev to have a command `./sndev email` to launch the mailhog inbox in your browser also update `./sndev login` to hash the generated email address and insert it into the db record * update sndev help * update awards.csv * update the hack in next-auth to re-use the email supplied on input to `getUserByEmail` * consolidate console.error logs * create generic open command --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-04 23:06:15 +00:00
email: !!(user.emailVerified && user.emailHash),
2022-06-02 22:55:23 +00:00
twitter: oauth.indexOf('twitter') >= 0,
2023-01-18 18:49:20 +00:00
github: oauth.indexOf('github') >= 0,
nostr: !!user.nostrAuthPubkey,
apiKey: user.apiKeyEnabled ? !!user.apiKeyHash : null
2022-06-02 22:55:23 +00:00
}
}
export async function topUsers (parent, { cursor, when, by, from, to, limit = LIMIT }, { models, me }) {
const decodedCursor = decodeCursor(cursor)
const range = whenRange(when, from, to || decodeCursor.time)
let column
switch (by) {
case 'spending':
case 'spent': column = 'spent'; break
case 'posts': column = 'nposts'; break
case 'comments': column = 'ncomments'; break
case 'referrals': column = 'referrals'; break
case 'stacking': column = 'stacked'; break
default: column = 'proportion'; break
}
const users = (await models.$queryRawUnsafe(`
SELECT *
FROM
(SELECT users.*,
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
COALESCE(sum(posts), 0) as nposts,
COALESCE(sum(comments), 0) as ncomments,
COALESCE(sum(referrals), 0) as referrals,
COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked
FROM ${viewGroup(range, 'user_stats')}
JOIN users on users.id = u.id
GROUP BY users.id) uu
${column === 'proportion' ? `JOIN ${viewValueGroup()} ON uu.id = vv.id` : ''}
ORDER BY ${column} DESC NULLS LAST, uu.created_at ASC
OFFSET $3
LIMIT $4`, ...range, decodedCursor.offset, limit)
).map(
u => u.hideFromTopUsers && (!me || me.id !== u.id) ? null : u
)
return {
cursor: users.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
users
}
}
export function viewValueGroup () {
return `(
SELECT v.id, sum(proportion) as proportion
FROM (
(SELECT *
FROM user_values_days
WHERE user_values_days.t >= date_trunc('day', timezone('America/Chicago', $1))
AND date_trunc('day', user_values_days.t) <= date_trunc('day', timezone('America/Chicago', $2)))
UNION ALL
(SELECT * FROM
user_values_today
WHERE user_values_today.t >= date_trunc('day', timezone('America/Chicago', $1))
AND date_trunc('day', user_values_today.t) <= date_trunc('day', timezone('America/Chicago', $2)))
) v
2024-03-31 21:53:57 +00:00
WHERE v.id NOT IN (${SN_NO_REWARDS_IDS.join(',')})
GROUP BY v.id
) vv`
}
2021-03-25 19:29:24 +00:00
export default {
Query: {
2023-10-23 23:19:36 +00:00
me: async (parent, args, { models, me }) => {
if (!me?.id) {
return null
}
2023-05-07 15:44:57 +00:00
return await models.user.findUnique({ where: { id: me.id } })
},
2022-06-02 22:55:23 +00:00
settings: async (parent, args, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2022-06-02 22:55:23 +00:00
}
return await models.user.findUnique({ where: { id: me.id } })
},
2021-04-22 22:14:32 +00:00
user: async (parent, { name }, { models }) => {
return await models.user.findUnique({ where: { name } })
},
2021-03-25 19:29:24 +00:00
users: async (parent, args, { models }) =>
2021-05-21 22:32:21 +00:00
await models.user.findMany(),
nameAvailable: async (parent, { name }, { models, me }) => {
let user
if (me) {
user = await models.user.findUnique({ where: { id: me.id } })
2021-05-21 22:32:21 +00:00
}
return user?.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } }))
2021-12-17 00:01:02 +00:00
},
mySubscribedUsers: async (parent, { cursor }, { models, me }) => {
if (!me) {
throw new GraphQLError('You must be logged in to view subscribed users', { extensions: { code: 'UNAUTHENTICATED' } })
}
const decodedCursor = decodeCursor(cursor)
const users = await models.$queryRaw`
SELECT users.*
FROM "UserSubscription"
JOIN users ON "UserSubscription"."followeeId" = users.id
WHERE "UserSubscription"."followerId" = ${me.id}
AND ("UserSubscription"."postsSubscribedAt" IS NOT NULL OR "UserSubscription"."commentsSubscribedAt" IS NOT NULL)
OFFSET ${decodedCursor.offset}
LIMIT ${LIMIT}
`
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
},
myMutedUsers: async (parent, { cursor }, { models, me }) => {
if (!me) {
throw new GraphQLError('You must be logged in to view muted users', { extensions: { code: 'UNAUTHENTICATED' } })
}
const decodedCursor = decodeCursor(cursor)
const users = await models.$queryRaw`
SELECT users.*
FROM "Mute"
JOIN users ON "Mute"."mutedId" = users.id
WHERE "Mute"."muterId" = ${me.id}
OFFSET ${decodedCursor.offset}
LIMIT ${LIMIT}
`
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
},
2023-02-09 18:41:28 +00:00
topCowboys: async (parent, { cursor }, { models, me }) => {
const decodedCursor = decodeCursor(cursor)
2024-01-19 21:19:26 +00:00
const range = whenRange('forever')
const users = (await models.$queryRawUnsafe(`
2023-08-29 01:12:33 +00:00
SELECT users.*,
coalesce(floor(sum(msats_spent)/1000),0) as spent,
coalesce(sum(posts),0) as nposts,
coalesce(sum(comments),0) as ncomments,
coalesce(sum(referrals),0) as referrals,
coalesce(floor(sum(msats_stacked)/1000),0) as stacked
2024-01-19 21:19:26 +00:00
FROM ${viewGroup(range, 'user_stats')}
JOIN users on users.id = u.id
WHERE streak IS NOT NULL
GROUP BY users.id
2023-02-09 18:41:28 +00:00
ORDER BY streak DESC, created_at ASC
2024-01-19 21:19:26 +00:00
OFFSET $3
LIMIT ${LIMIT}`, ...range, decodedCursor.offset)
).map(
u => (u.hideFromTopUsers || u.hideCowboyHat) && (!me || me.id !== u.id) ? null : u
)
2023-02-09 18:41:28 +00:00
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
},
2023-11-21 20:49:39 +00:00
userSuggestions: async (parent, { q, limit = 5 }, { models }) => {
let users = []
if (q) {
users = await models.$queryRaw`
SELECT name
FROM users
WHERE (
id > ${RESERVED_MAX_USER_ID} OR id IN (${USER_ID.anon}, ${USER_ID.delete})
2023-11-21 20:49:39 +00:00
)
AND SIMILARITY(name, ${q}) > 0.1
ORDER BY SIMILARITY(name, ${q}) DESC
LIMIT ${limit}`
} else {
users = await models.$queryRaw`
SELECT name
FROM user_stats_days
JOIN users on users.id = user_stats_days.id
WHERE NOT users."hideFromTopUsers"
2024-02-16 18:58:50 +00:00
AND user_stats_days.t = (SELECT max(t) FROM user_stats_days)
2023-11-21 20:49:39 +00:00
ORDER BY msats_stacked DESC, users.created_at ASC
LIMIT ${limit}`
}
return users
},
topUsers,
hasNewNotes: async (parent, args, { me, models }) => {
if (!me) {
return false
}
const user = await models.user.findUnique({ where: { id: me.id } })
const lastChecked = user.checkedNotesAt || new Date(0)
// if we've already recorded finding notes after they last checked, return true
// this saves us from rechecking notifications
if (user.foundNotesAt > lastChecked) {
return true
}
2023-10-23 23:19:36 +00:00
const foundNotes = () =>
models.user.update({
where: { id: me.id },
data: {
foundNotesAt: new Date(),
lastSeenAt: new Date()
}
}).catch(console.error)
// check if any votes have been cast for them since checkedNotesAt
if (user.noteItemSats) {
2023-09-28 20:02:25 +00:00
const [newSats] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "Item"
WHERE "Item"."lastZapAt" > $2
AND "Item"."userId" = $1)`, me.id, lastChecked)
2023-09-28 20:02:25 +00:00
if (newSats.exists) {
foundNotes()
return true
}
}
// break out thread subscription to decrease the search space of the already expensive reply query
2023-09-28 20:02:25 +00:00
const [newThreadSubReply] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "ThreadSubscription"
2024-03-24 04:15:00 +00:00
JOIN "Reply" r ON "ThreadSubscription"."itemId" = r."ancestorId"
JOIN "Item" ON r."itemId" = "Item".id
2023-09-28 20:02:25 +00:00
${whereClause(
'"ThreadSubscription"."userId" = $1',
2024-03-24 04:15:00 +00:00
'r.created_at > $2',
'r.created_at >= "ThreadSubscription".created_at',
'r."userId" <> $1',
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
activeOrMine(me),
2023-09-28 20:02:25 +00:00
await filterClause(me, models),
2024-03-24 04:15:00 +00:00
muteClause(me),
...(user.noteAllDescendants ? [] : ['r.level = 1'])
2023-09-28 20:02:25 +00:00
)})`, me.id, lastChecked)
if (newThreadSubReply.exists) {
foundNotes()
return true
}
2023-09-28 20:02:25 +00:00
const [newUserSubs] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "UserSubscription"
JOIN "Item" ON "UserSubscription"."followeeId" = "Item"."userId"
${whereClause(
'"UserSubscription"."followerId" = $1',
2023-10-22 17:47:46 +00:00
'"Item".created_at > $2',
2023-09-28 20:02:25 +00:00
`(
("Item"."parentId" IS NULL AND "UserSubscription"."postsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."postsSubscribedAt")
OR ("Item"."parentId" IS NOT NULL AND "UserSubscription"."commentsSubscribedAt" IS NOT NULL AND "Item".created_at >= "UserSubscription"."commentsSubscribedAt")
)`,
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
activeOrMine(me),
2023-09-28 20:02:25 +00:00
await filterClause(me, models),
muteClause(me))})`, me.id, lastChecked)
if (newUserSubs.exists) {
foundNotes()
return true
}
const [newSubPost] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "SubSubscription"
JOIN "Item" ON "SubSubscription"."subName" = "Item"."subName"
${whereClause(
'"SubSubscription"."userId" = $1',
'"Item".created_at > $2',
'"Item"."parentId" IS NULL',
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
'"Item"."userId" <> $1',
activeOrMine(me),
await filterClause(me, models),
muteClause(me))})`, me.id, lastChecked)
if (newSubPost.exists) {
foundNotes()
return true
}
// check if they have any mentions since checkedNotesAt
if (user.noteMentions) {
2023-09-28 20:02:25 +00:00
const [newMentions] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id
2023-09-28 20:02:25 +00:00
${whereClause(
'"Mention"."userId" = $1',
'"Mention".created_at > $2',
'"Item"."userId" <> $1',
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
activeOrMine(me),
2023-09-28 20:02:25 +00:00
await filterClause(me, models),
muteClause(me)
)})`, me.id, lastChecked)
if (newMentions.exists) {
foundNotes()
return true
}
}
if (user.noteItemMentions) {
const [newMentions] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "ItemMention"
JOIN "Item" "Referee" ON "ItemMention"."refereeId" = "Referee".id
JOIN "Item" ON "ItemMention"."referrerId" = "Item".id
${whereClause(
'"ItemMention".created_at > $2',
'"Item"."userId" <> $1',
'"Referee"."userId" = $1',
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
activeOrMine(me),
await filterClause(me, models),
muteClause(me)
)})`, me.id, lastChecked)
if (newMentions.exists) {
foundNotes()
return true
}
}
if (user.noteForwardedSats) {
2023-09-28 20:02:25 +00:00
const [newFwdSats] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM "Item"
JOIN "ItemForward" ON
"ItemForward"."itemId" = "Item".id
AND "ItemForward"."userId" = $1
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
${whereClause(
'"Item"."lastZapAt" > $2',
'"Item"."userId" <> $1',
activeOrMine(me),
await filterClause(me, models),
muteClause(me)
)})`, me.id, lastChecked)
2023-09-28 20:02:25 +00:00
if (newFwdSats.exists) {
foundNotes()
return true
}
}
const job = await models.item.findFirst({
where: {
maxBid: {
not: null
},
userId: me.id,
statusUpdatedAt: {
gt: lastChecked
}
}
})
2022-11-29 17:28:57 +00:00
if (job && job.statusUpdatedAt > job.createdAt) {
foundNotes()
return true
}
if (user.noteEarning) {
const earn = await models.earn.findFirst({
where: {
userId: me.id,
createdAt: {
gt: lastChecked
},
msats: {
gte: 1000
}
}
})
if (earn) {
foundNotes()
return true
}
}
if (user.noteDeposits) {
const invoice = await models.invoice.findFirst({
where: {
userId: me.id,
confirmedAt: {
gt: lastChecked
2023-08-31 16:38:45 +00:00
},
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
isHeld: null,
actionType: null
}
})
if (invoice) {
foundNotes()
return true
}
}
2024-03-25 20:20:11 +00:00
if (user.noteWithdrawals) {
const wdrwl = await models.withdrawl.findFirst({
where: {
userId: me.id,
status: 'CONFIRMED',
updatedAt: {
gt: lastChecked
}
}
})
if (wdrwl) {
foundNotes()
return true
}
}
// check if new invites have been redeemed
if (user.noteInvites) {
2023-09-28 20:02:25 +00:00
const [newInvites] = await models.$queryRawUnsafe(`
SELECT EXISTS(
SELECT *
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
WHERE "Invite"."userId" = $1
AND users.created_at > $2)`, me.id, lastChecked)
if (newInvites.exists) {
foundNotes()
return true
}
2022-12-19 22:27:52 +00:00
const referral = await models.user.findFirst({
where: {
referrerId: me.id,
createdAt: {
gt: lastChecked
}
}
})
if (referral) {
foundNotes()
2022-12-19 22:27:52 +00:00
return true
}
}
2023-02-01 14:44:35 +00:00
if (user.noteCowboyHat) {
const streak = await models.streak.findFirst({
where: {
userId: me.id,
updatedAt: {
gt: lastChecked
}
}
})
if (streak) {
foundNotes()
2023-02-01 14:44:35 +00:00
return true
}
}
2024-01-03 02:05:49 +00:00
const subStatus = await models.sub.findFirst({
where: {
userId: me.id,
statusUpdatedAt: {
gt: lastChecked
},
status: {
not: 'ACTIVE'
}
}
})
if (subStatus) {
foundNotes()
return true
}
const newReminder = await models.reminder.findFirst({
where: {
userId: me.id,
remindAt: {
gt: lastChecked,
lt: new Date()
}
}
})
if (newReminder) {
foundNotes()
return true
}
backend payment optimism (#1195) * wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
2024-07-01 17:02:29 +00:00
const invoiceActionFailed = await models.invoice.findFirst({
where: {
userId: me.id,
updatedAt: {
gt: lastChecked
},
actionType: {
in: INVOICE_ACTION_NOTIFICATION_TYPES
},
actionState: 'FAILED'
}
})
if (invoiceActionFailed) {
foundNotes()
return true
}
2023-10-23 23:19:36 +00:00
// update checkedNotesAt to prevent rechecking same time period
models.user.update({
where: { id: me.id },
data: {
checkedNotesAt: new Date(),
lastSeenAt: new Date()
}
}).catch(console.error)
return false
},
2022-10-25 17:13:06 +00:00
searchUsers: async (parent, { q, limit, similarity }, { models }) => {
2022-08-26 22:20:09 +00:00
return await models.$queryRaw`
2023-10-23 22:55:48 +00:00
SELECT *
FROM users
WHERE (id > ${RESERVED_MAX_USER_ID} OR id IN (${USER_ID.anon}, ${USER_ID.delete}))
2023-10-23 22:55:48 +00:00
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 }) => {
const range = whenRange(when, from, to)
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
json_build_array(
json_build_object('name', 'comments', 'value', COALESCE(SUM(comments), 0)),
json_build_object('name', 'posts', 'value', COALESCE(SUM(posts), 0)),
json_build_object('name', 'territories', 'value', COALESCE(SUM(territories), 0)),
json_build_object('name', 'referrals', 'value', COALESCE(SUM(referrals), 0)),
json_build_object('name', 'one day referrals', 'value', COALESCE(SUM(one_day_referrals), 0))
) AS data
FROM ${viewGroup(range, 'user_stats')}
WHERE id = ${me.id}
GROUP BY time
ORDER BY time ASC`, ...range)
},
userStatsIncomingSats: async (parent, { when, from, to }, { me, models }) => {
const range = whenRange(when, from, to)
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
json_build_array(
json_build_object('name', 'zaps', 'value', ROUND(COALESCE(SUM(msats_tipped), 0) / 1000)),
json_build_object('name', 'rewards', 'value', ROUND(COALESCE(SUM(msats_rewards), 0) / 1000)),
json_build_object('name', 'referrals', 'value', ROUND( COALESCE(SUM(msats_referrals), 0) / 1000)),
json_build_object('name', 'one day referrals', 'value', ROUND( COALESCE(SUM(msats_one_day_referrals), 0) / 1000)),
json_build_object('name', 'territories', 'value', ROUND(COALESCE(SUM(msats_revenue), 0) / 1000))
) AS data
FROM ${viewGroup(range, 'user_stats')}
WHERE id = ${me.id}
GROUP BY time
ORDER BY time ASC`, ...range)
},
userStatsOutgoingSats: async (parent, { when, from, to }, { me, models }) => {
const range = whenRange(when, from, to)
return await models.$queryRawUnsafe(`
SELECT date_trunc('${timeUnitForRange(range)}', t) at time zone 'America/Chicago' as time,
json_build_array(
json_build_object('name', 'fees', 'value', FLOOR(COALESCE(SUM(msats_fees), 0) / 1000)),
json_build_object('name', 'zapping', 'value', FLOOR(COALESCE(SUM(msats_zaps), 0) / 1000)),
json_build_object('name', 'donations', 'value', FLOOR(COALESCE(SUM(msats_donated), 0) / 1000)),
json_build_object('name', 'territories', 'value', FLOOR(COALESCE(SUM(msats_billing), 0) / 1000))
) AS data
FROM ${viewGroup(range, 'user_stats')}
WHERE id = ${me.id}
GROUP BY time
ORDER BY time ASC`, ...range)
2021-05-21 22:32:21 +00:00
}
2021-03-25 19:29:24 +00:00
},
2021-05-22 00:09:11 +00:00
Mutation: {
disableFreebies: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
// disable freebies if it hasn't been set yet
await models.user.update({
where: { id: me.id, disableFreebies: null },
data: { disableFreebies: true }
})
return true
},
2023-02-08 19:38:04 +00:00
setName: async (parent, data, { me, models }) => {
2021-05-22 00:09:11 +00:00
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2021-05-22 00:09:11 +00:00
}
await ssValidate(userSchema, data, { models })
2022-08-26 22:26:42 +00:00
2021-05-22 00:09:11 +00:00
try {
2023-02-08 19:38:04 +00:00
await models.user.update({ where: { id: me.id }, data })
return data.name
2021-05-22 00:09:11 +00:00
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
2021-05-22 00:09:11 +00:00
}
throw error
}
2021-09-23 17:42:00 +00:00
},
2023-11-10 01:05:35 +00:00
setSettings: async (parent, { settings: { nostrRelays, ...data } }, { me, models }) => {
2021-10-30 16:20:11 +00:00
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2021-10-30 16:20:11 +00:00
}
2023-02-08 19:38:04 +00:00
await ssValidate(settingsSchema, { nostrRelays, ...data })
2023-01-07 00:53:09 +00:00
if (nostrRelays?.length) {
const connectOrCreate = []
for (const nr of nostrRelays) {
await models.nostrRelay.upsert({
where: { addr: nr },
update: { addr: nr },
create: { addr: nr }
})
connectOrCreate.push({
where: { userId_nostrRelayAddr: { userId: me.id, nostrRelayAddr: nr } },
create: { nostrRelayAddr: nr }
})
}
return await models.user.update({ where: { id: me.id }, data: { ...data, nostrRelays: { deleteMany: {}, connectOrCreate } } })
} else {
return await models.user.update({ where: { id: me.id }, data: { ...data, nostrRelays: { deleteMany: {} } } })
}
2021-10-30 16:20:11 +00:00
},
2021-12-09 20:40:40 +00:00
setWalkthrough: async (parent, { upvotePopover, tipPopover }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2021-12-09 20:40:40 +00:00
}
await models.user.update({ where: { id: me.id }, data: { upvotePopover, tipPopover } })
return true
},
2022-05-16 20:51:22 +00:00
setPhoto: async (parent, { photoId }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2022-05-16 20:51:22 +00:00
}
await models.user.update({
where: { id: me.id },
data: { photoId: Number(photoId) }
})
return Number(photoId)
},
2021-09-24 21:28:21 +00:00
upsertBio: async (parent, { bio }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2021-09-24 21:28:21 +00:00
}
2023-02-08 19:38:04 +00:00
await ssValidate(bioSchema, { bio })
2021-09-24 21:28:21 +00:00
const user = await models.user.findUnique({ where: { id: me.id } })
if (user.bioId) {
2023-08-27 22:48:46 +00:00
await updateItem(parent, { id: user.bioId, text: bio, title: `@${user.name}'s bio` }, { me, models })
2021-09-24 21:28:21 +00:00
} else {
2023-08-27 22:48:46 +00:00
await createItem(parent, { bio: true, text: bio, title: `@${user.name}'s bio` }, { me, models })
2022-08-18 18:15:24 +00:00
}
2021-09-24 21:28:21 +00:00
return await models.user.findUnique({ where: { id: me.id } })
2022-06-02 22:55:23 +00:00
},
generateApiKey: async (parent, { id }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
const user = await models.user.findUnique({ where: { id: me.id } })
if (!user.apiKeyEnabled) {
throw new GraphQLError('you are not allowed to generate api keys', { extensions: { code: 'FORBIDDEN' } })
}
// I trust postgres CSPRNG more than the one from JS
const [{ apiKey, apiKeyHash }] = await models.$queryRaw`
SELECT "apiKey", encode(digest("apiKey", 'sha256'), 'hex') AS "apiKeyHash"
FROM (
SELECT encode(gen_random_bytes(32), 'base64')::CHAR(32) as "apiKey"
) rng`
await models.user.update({ where: { id: me.id }, data: { apiKeyHash } })
return apiKey
},
deleteApiKey: async (parent, { id }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
return await models.user.update({ where: { id: me.id }, data: { apiKeyHash: null } })
},
2022-06-02 22:55:23 +00:00
unlinkAuth: async (parent, { authType }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2022-06-02 22:55:23 +00:00
}
assertApiKeyNotPermitted({ me })
2022-06-02 22:55:23 +00:00
2023-01-18 18:49:20 +00:00
let user
2022-06-02 22:55:23 +00:00
if (authType === 'twitter' || authType === 'github') {
2023-01-18 18:49:20 +00:00
user = await models.user.findUnique({ where: { id: me.id } })
const account = await models.account.findFirst({ where: { userId: me.id, provider: authType } })
2022-06-02 22:55:23 +00:00
if (!account) {
throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } })
2022-06-02 22:55:23 +00:00
}
await models.account.delete({ where: { id: account.id } })
if (authType === 'twitter') {
await models.user.update({ where: { id: me.id }, data: { hideTwitter: true, twitterId: null } })
} else {
await models.user.update({ where: { id: me.id }, data: { hideGithub: true, githubId: null } })
}
2023-01-18 18:49:20 +00:00
} else if (authType === 'lightning') {
user = await models.user.update({ where: { id: me.id }, data: { pubkey: null } })
} else if (authType === 'nostr') {
user = await models.user.update({ where: { id: me.id }, data: { hideNostr: true, nostrAuthPubkey: null } })
2023-01-18 18:49:20 +00:00
} else if (authType === 'email') {
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null, emailHash: null } })
2023-01-18 18:49:20 +00:00
} else {
throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } })
2022-06-02 22:55:23 +00:00
}
2023-01-18 18:49:20 +00:00
return await authMethods(user, undefined, { models, me })
2022-06-02 22:55:23 +00:00
},
linkUnverifiedEmail: async (parent, { email }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
2022-06-02 22:55:23 +00:00
}
assertApiKeyNotPermitted({ me })
2022-06-02 22:55:23 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate(emailSchema, { email })
2022-06-02 22:55:23 +00:00
try {
2022-09-12 19:10:15 +00:00
await models.user.update({
where: { id: me.id },
Store hashed and salted email addresses (#1111) * first pass of hashing user emails * use salt * add a salt to .env.development (prod salt needs to be kept a secret) * move `hashEmail` util to a new util module * trigger a one-time job to migrate existing emails via the worker so we can use the salt from an env var * move newsletter signup move newsletter signup to prisma adapter create user with email code path so we can still auto-enroll email accounts without having to persist the email address in plaintext * remove `email` from api key session lookup query * drop user email index before dropping column * restore email column, just null values instead * fix function name * fix salt and hash raw sql statement * update auth methods email type in typedefs from str to bool * remove todo comment * lowercase email before hashing during migration * check for emailHash and email to accommodate migration window update our lookups to check for a matching emailHash, and then a matching email, in that order, to accommodate the case that a user tries to login via email while the migration is running, and their account has not yet been migrated also update sndev to have a command `./sndev email` to launch the mailhog inbox in your browser also update `./sndev login` to hash the generated email address and insert it into the db record * update sndev help * update awards.csv * update the hack in next-auth to re-use the email supplied on input to `getUserByEmail` * consolidate console.error logs * create generic open command --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
2024-05-04 23:06:15 +00:00
data: { emailHash: hashEmail({ email }) }
2022-09-12 19:10:15 +00:00
})
2022-06-02 22:55:23 +00:00
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('email taken', { extensions: { code: 'BAD_INPUT' } })
2022-06-02 22:55:23 +00:00
}
throw error
}
return true
},
subscribeUserPosts: async (parent, { id }, { me, models }) => {
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
const muted = await isMuted({ models, muterId: me?.id, mutedId: id })
if (existing) {
if (muted && !existing.postsSubscribedAt) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
}
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { postsSubscribedAt: existing.postsSubscribedAt ? null : new Date() } })
} else {
if (muted) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
}
await models.userSubscription.create({ data: { ...lookupData, postsSubscribedAt: new Date() } })
}
return { id }
},
subscribeUserComments: async (parent, { id }, { me, models }) => {
const lookupData = { followerId: Number(me.id), followeeId: Number(id) }
const existing = await models.userSubscription.findUnique({ where: { followerId_followeeId: lookupData } })
const muted = await isMuted({ models, muterId: me?.id, mutedId: id })
if (existing) {
if (muted && !existing.commentsSubscribedAt) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
}
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { commentsSubscribedAt: existing.commentsSubscribedAt ? null : new Date() } })
} else {
if (muted) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
}
await models.userSubscription.create({ data: { ...lookupData, commentsSubscribedAt: new Date() } })
}
return { id }
},
2023-09-28 20:02:25 +00:00
toggleMute: async (parent, { id }, { me, models }) => {
const lookupData = { muterId: Number(me.id), mutedId: Number(id) }
const where = { muterId_mutedId: lookupData }
const existing = await models.mute.findUnique({ where })
if (existing) {
await models.mute.delete({ where })
} else {
// check to see if current user is subscribed to the target user, and disallow mute if so
const subscription = await models.userSubscription.findUnique({
where: {
followerId_followeeId: {
followerId: Number(me.id),
followeeId: Number(id)
}
}
})
2024-05-17 13:29:52 +00:00
if (subscription?.postsSubscribedAt || subscription?.commentsSubscribedAt) {
throw new GraphQLError("you can't mute a stacker to whom you've subscribed", { extensions: { code: 'BAD_INPUT' } })
}
2023-09-28 20:02:25 +00:00
await models.mute.create({ data: { ...lookupData } })
}
return { id }
},
hideWelcomeBanner: async (parent, data, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
}
await models.user.update({ where: { id: me.id }, data: { hideWelcomeBanner: true } })
return true
2021-09-24 21:28:21 +00:00
}
2021-05-22 00:09:11 +00:00
},
2021-03-25 19:29:24 +00:00
User: {
2023-11-10 01:05:35 +00:00
privates: async (user, args, { me, models }) => {
if (!me || me.id !== user.id) {
return null
}
return user
},
optional: user => user,
meSubscriptionPosts: async (user, args, { me, models }) => {
if (!me) return false
if (typeof user.meSubscriptionPosts !== 'undefined') return user.meSubscriptionPosts
const subscription = await models.userSubscription.findUnique({
where: {
followerId_followeeId: {
followerId: Number(me.id),
followeeId: Number(user.id)
}
}
})
return !!subscription?.postsSubscribedAt
},
meSubscriptionComments: async (user, args, { me, models }) => {
if (!me) return false
if (typeof user.meSubscriptionComments !== 'undefined') return user.meSubscriptionComments
const subscription = await models.userSubscription.findUnique({
where: {
followerId_followeeId: {
followerId: Number(me.id),
followeeId: Number(user.id)
}
}
})
return !!subscription?.commentsSubscribedAt
},
meMute: async (user, args, { me, models }) => {
if (!me) return false
if (typeof user.meMute !== 'undefined') return user.meMute
return await isMuted({ models, muterId: me.id, mutedId: user.id })
2023-11-10 01:05:35 +00:00
},
2023-06-03 00:55:45 +00:00
since: async (user, args, { models }) => {
// get the user's first item
const item = await models.item.findFirst({
where: {
userId: user.id
},
orderBy: {
createdAt: 'asc'
}
})
return item?.id
},
nitems: async (user, { when, from, to }, { models }) => {
2023-07-27 00:18:42 +00:00
if (typeof user.nitems !== 'undefined') {
2022-10-25 21:35:32 +00:00
return user.nitems
}
const [gte, lte] = whenRange(when, from, to)
return await models.item.count({
where: {
userId: user.id,
createdAt: {
gte,
lte
}
}
})
},
nposts: async (user, { when, from, to }, { models }) => {
2023-07-27 00:18:42 +00:00
if (typeof user.nposts !== 'undefined') {
return user.nposts
}
const [gte, lte] = whenRange(when, from, to)
2022-10-26 14:56:22 +00:00
return await models.item.count({
where: {
userId: user.id,
parentId: null,
createdAt: {
gte,
lte
2022-10-26 14:56:22 +00:00
}
}
})
2021-04-22 22:14:32 +00:00
},
ncomments: async (user, { when, from, to }, { models }) => {
2023-07-27 00:18:42 +00:00
if (typeof user.ncomments !== 'undefined') {
2022-10-25 21:35:32 +00:00
return user.ncomments
}
2022-10-26 14:56:22 +00:00
const [gte, lte] = whenRange(when, from, to)
2022-10-26 14:56:22 +00:00
return await models.item.count({
where: {
userId: user.id,
parentId: { not: null },
createdAt: {
gte,
lte
2022-10-26 14:56:22 +00:00
}
}
})
2021-04-22 22:14:32 +00:00
},
nterritories: async (user, { when, from, to }, { models }) => {
if (typeof user.nterritories !== 'undefined') {
return user.nterritories
}
const [gte, lte] = whenRange(when, from, to)
return await models.sub.count({
where: {
userId: user.id,
status: 'ACTIVE',
createdAt: {
gte,
lte
}
}
})
},
2023-11-10 01:05:35 +00:00
bio: async (user, args, { models, me }) => {
return getItem(user, { id: user.bioId }, { models, me })
}
},
UserPrivates: {
sats: async (user, args, { models, me }) => {
if (!me || me.id !== user.id) {
return 0
}
return msatsToSats(user.msats)
},
authMethods,
hasInvites: async (user, args, { models }) => {
const invites = await models.user.findUnique({
where: { id: user.id }
}).invites({ take: 1 })
return invites.length > 0
},
nostrRelays: async (user, args, { models, me }) => {
if (user.id !== me.id) {
return []
}
2023-11-10 01:05:35 +00:00
const relays = await models.userNostrRelay.findMany({
where: { userId: user.id }
})
2023-11-10 01:05:35 +00:00
return relays?.map(r => r.nostrRelayAddr)
},
tipRandom: async (user, args, { me }) => {
if (!me || me.id !== user.id) {
return false
}
return !!user.tipRandomMin && !!user.tipRandomMax
2023-11-10 01:05:35 +00:00
}
},
UserOptional: {
streak: async (user, args, { models }) => {
if (user.hideCowboyHat) {
return null
}
return user.streak
},
2023-11-10 01:05:35 +00:00
maxStreak: async (user, args, { models }) => {
if (user.hideCowboyHat) {
return null
}
const [{ max }] = await models.$queryRaw`
SELECT MAX(COALESCE("endedAt", (now() AT TIME ZONE 'America/Chicago')::date) - "startedAt")
FROM "Streak" WHERE "userId" = ${user.id}`
return max
},
isContributor: async (user, args, { me }) => {
// lazy init contributors only once
if (contributors.size === 0) {
await loadContributors(contributors)
}
if (me?.id === user.id) {
return contributors.has(user.name)
}
return !user.hideIsContributor && contributors.has(user.name)
},
stacked: async (user, { when, from, to }, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideFromTopUsers) {
return null
}
2023-07-27 00:18:42 +00:00
if (typeof user.stacked !== 'undefined') {
2021-12-17 00:01:02 +00:00
return user.stacked
}
2022-03-17 20:13:19 +00:00
if (!when || when === 'forever') {
2022-10-26 14:56:22 +00:00
// forever
2022-11-15 20:51:55 +00:00
return (user.stackedMsats && msatsToSats(user.stackedMsats)) || 0
2024-01-19 21:19:26 +00:00
}
const range = whenRange(when, from, to)
const [{ stacked }] = await models.$queryRawUnsafe(`
SELECT sum(msats_stacked) as stacked
FROM ${viewGroup(range, 'user_stats')}
WHERE id = $3`, ...range, Number(user.id))
return (stacked && msatsToSats(stacked)) || 0
2021-04-27 21:30:58 +00:00
},
2023-11-10 01:05:35 +00:00
spent: async (user, { when, from, to }, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideFromTopUsers) {
return null
}
2023-07-27 00:18:42 +00:00
if (typeof user.spent !== 'undefined') {
2022-10-25 21:35:32 +00:00
return user.spent
}
2024-01-19 21:19:26 +00:00
const range = whenRange(when, from, to)
const [{ spent }] = await models.$queryRawUnsafe(`
SELECT sum(msats_spent) as spent
FROM ${viewGroup(range, 'user_stats')}
WHERE id = $3`, ...range, Number(user.id))
2022-10-25 17:13:06 +00:00
2024-01-19 21:19:26 +00:00
return (spent && msatsToSats(spent)) || 0
2022-10-25 17:13:06 +00:00
},
2023-11-10 01:05:35 +00:00
referrals: async (user, { when, from, to }, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideFromTopUsers) {
return null
}
2023-07-27 00:18:42 +00:00
if (typeof user.referrals !== 'undefined') {
return user.referrals
}
2023-07-27 00:18:42 +00:00
const [gte, lte] = whenRange(when, from, to)
2022-12-19 23:00:53 +00:00
return await models.user.count({
where: {
referrerId: user.id,
createdAt: {
gte,
lte
2022-12-19 23:00:53 +00:00
}
}
})
},
githubId: async (user, args, { me }) => {
if ((!me || me.id !== user.id) && user.hideGithub) {
return null
}
return user.githubId
},
twitterId: async (user, args, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideTwitter) {
return null
}
return user.twitterId
},
nostrAuthPubkey: async (user, args, { models, me }) => {
if ((!me || me.id !== user.id) && user.hideNostr) {
return null
}
return user.nostrAuthPubkey
2021-05-11 15:52:50 +00:00
}
2021-03-25 19:29:24 +00:00
}
}