stacker.news/api/resolvers/user.js

609 lines
19 KiB
JavaScript
Raw Normal View History

2021-05-22 00:09:11 +00:00
import { AuthenticationError, UserInputError } from 'apollo-server-errors'
2021-12-17 00:01:02 +00:00
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
2022-11-15 20:51:55 +00:00
import { msatsToSats } from '../../lib/format'
2023-02-08 19:38:04 +00:00
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '../../lib/validate'
2022-09-21 19:57:36 +00:00
import { createMentions, getItem, SELECT, updateItem, filterClause } from './item'
2021-09-23 17:42:00 +00:00
import serialize from './serial'
2022-10-26 14:56:22 +00:00
export function within (table, within) {
let interval = ' AND "' + table + '".created_at >= $1 - INTERVAL '
2021-12-17 00:01:02 +00:00
switch (within) {
2022-10-26 14:56:22 +00:00
case 'day':
2022-10-25 21:35:32 +00:00
interval += "'1 day'"
2021-12-17 00:01:02 +00:00
break
2022-07-14 00:55:10 +00:00
case 'week':
interval += "'7 days'"
break
case 'month':
interval += "'1 month'"
break
case 'year':
interval += "'1 year'"
break
default:
2022-10-26 14:56:22 +00:00
interval = ''
2022-10-25 21:35:32 +00:00
break
}
return interval
}
2022-10-26 14:56:22 +00:00
export function withinDate (within) {
2022-10-25 21:35:32 +00:00
switch (within) {
2022-10-26 14:56:22 +00:00
case 'day':
return new Date(new Date().setDate(new Date().getDate() - 1))
2022-10-25 21:35:32 +00:00
case 'week':
2022-10-26 14:56:22 +00:00
return new Date(new Date().setDate(new Date().getDate() - 7))
2022-10-25 21:35:32 +00:00
case 'month':
2022-10-26 14:56:22 +00:00
return new Date(new Date().setDate(new Date().getDate() - 30))
2022-10-25 21:35:32 +00:00
case 'year':
2022-10-26 14:56:22 +00:00
return new Date(new Date().setDate(new Date().getDate() - 365))
2022-10-25 21:35:32 +00:00
default:
2022-10-26 14:56:22 +00:00
return new Date(0)
2022-07-14 00:55:10 +00:00
}
}
2022-06-02 22:55:23 +00:00
async function authMethods (user, args, { models, me }) {
const accounts = await models.account.findMany({
where: {
userId: me.id
}
})
const oauth = accounts.map(a => a.providerId)
return {
lightning: !!user.pubkey,
email: user.emailVerified && user.email,
twitter: oauth.indexOf('twitter') >= 0,
2023-01-18 18:49:20 +00:00
github: oauth.indexOf('github') >= 0,
slashtags: !!user.slashtagId
2022-06-02 22:55:23 +00:00
}
}
2021-03-25 19:29:24 +00:00
export default {
Query: {
2023-05-07 15:44:57 +00:00
me: async (parent, { skipUpdate }, { models, me }) => {
if (!me) {
return null
}
2023-05-07 15:44:57 +00:00
if (!skipUpdate) {
models.user.update({ where: { id: me.id }, data: { lastSeenAt: new Date() } }).catch(console.error)
}
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 AuthenticationError('you must be logged in')
}
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 }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
const user = await models.user.findUnique({ where: { id: me.id } })
return user.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } }))
2021-12-17 00:01:02 +00:00
},
2023-02-09 18:41:28 +00:00
topCowboys: async (parent, { cursor }, { models, me }) => {
const decodedCursor = decodeCursor(cursor)
const users = await models.$queryRaw(`
SELECT users.*
FROM users
2023-05-01 21:49:47 +00:00
WHERE NOT "hideFromTopUsers" AND NOT "hideCowboyHat" AND streak IS NOT NULL
2023-02-09 18:41:28 +00:00
ORDER BY streak DESC, created_at ASC
OFFSET $1
LIMIT ${LIMIT}`, decodedCursor.offset)
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
},
2022-10-25 21:35:32 +00:00
topUsers: async (parent, { cursor, when, sort }, { models, me }) => {
2021-12-17 00:01:02 +00:00
const decodedCursor = decodeCursor(cursor)
2022-02-02 21:50:12 +00:00
let users
2022-10-25 21:35:32 +00:00
if (sort === 'spent') {
2022-02-02 21:50:12 +00:00
users = await models.$queryRaw(`
2022-12-08 00:04:02 +00:00
SELECT users.*, sum(sats_spent) as spent
FROM
((SELECT "userId", floor(sum("ItemAct".msats)/1000) as sats_spent
FROM "ItemAct"
WHERE "ItemAct".created_at <= $1
${within('ItemAct', when)}
GROUP BY "userId")
UNION ALL
(SELECT "userId", sats as sats_spent
FROM "Donation"
WHERE created_at <= $1
${within('Donation', when)})) spending
JOIN users on spending."userId" = users.id
2022-12-01 21:31:04 +00:00
AND NOT users."hideFromTopUsers"
2022-02-02 21:50:12 +00:00
GROUP BY users.id, users.name
2022-10-25 21:35:32 +00:00
ORDER BY spent DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
} else if (sort === 'posts') {
users = await models.$queryRaw(`
SELECT users.*, count(*) as nitems
FROM users
JOIN "Item" on "Item"."userId" = users.id
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NULL
2022-12-01 21:31:04 +00:00
AND NOT users."hideFromTopUsers"
2022-10-26 14:56:22 +00:00
${within('Item', when)}
2022-10-25 21:35:32 +00:00
GROUP BY users.id
ORDER BY nitems DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
} else if (sort === 'comments') {
users = await models.$queryRaw(`
SELECT users.*, count(*) as ncomments
FROM users
JOIN "Item" on "Item"."userId" = users.id
WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NOT NULL
2022-12-01 21:31:04 +00:00
AND NOT users."hideFromTopUsers"
2022-10-26 14:56:22 +00:00
${within('Item', when)}
2022-10-25 21:35:32 +00:00
GROUP BY users.id
ORDER BY ncomments DESC NULLS LAST, users.created_at DESC
2022-02-02 21:50:12 +00:00
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
2022-12-19 23:00:53 +00:00
} else if (sort === 'referrals') {
users = await models.$queryRaw(`
SELECT users.*, count(*) as referrals
FROM users
JOIN "users" referree on users.id = referree."referrerId"
WHERE referree.created_at <= $1
AND NOT users."hideFromTopUsers"
${within('referree', when)}
GROUP BY users.id
ORDER BY referrals DESC NULLS LAST, users.created_at DESC
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
2022-02-02 21:50:12 +00:00
} else {
users = await models.$queryRaw(`
2023-05-02 22:02:08 +00:00
SELECT u.id, u.name, u.streak, u."photoId", u."hideCowboyHat", floor(sum(amount)/1000) as stacked
2022-07-14 00:55:10 +00:00
FROM
2022-11-15 20:51:55 +00:00
((SELECT users.*, "ItemAct".msats as amount
2022-07-14 00:55:10 +00:00
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
JOIN users on "Item"."userId" = users.id
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1
2022-12-01 21:31:04 +00:00
AND NOT users."hideFromTopUsers"
2022-10-26 14:56:22 +00:00
${within('ItemAct', when)})
2022-07-14 00:55:10 +00:00
UNION ALL
2022-11-15 20:51:55 +00:00
(SELECT users.*, "Earn".msats as amount
2022-07-14 00:55:10 +00:00
FROM "Earn"
JOIN users on users.id = "Earn"."userId"
2022-12-01 21:31:04 +00:00
WHERE "Earn".msats > 0 ${within('Earn', when)}
2022-12-19 23:00:53 +00:00
AND NOT users."hideFromTopUsers")
UNION ALL
(SELECT users.*, "ReferralAct".msats as amount
FROM "ReferralAct"
JOIN users on users.id = "ReferralAct"."referrerId"
WHERE "ReferralAct".msats > 0 ${within('ReferralAct', when)}
AND NOT users."hideFromTopUsers")) u
2023-05-02 22:02:08 +00:00
GROUP BY u.id, u.name, u.created_at, u."photoId", u.streak, u."hideCowboyHat"
2022-10-25 21:35:32 +00:00
ORDER BY stacked DESC NULLS LAST, created_at DESC
2022-02-02 21:50:12 +00:00
OFFSET $2
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
}
2021-12-17 00:01:02 +00:00
return {
cursor: users.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
users
}
2022-08-26 22:20:09 +00:00
},
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)
// check if any votes have been cast for them since checkedNotesAt
if (user.noteItemSats) {
const votes = await models.$queryRaw(`
SELECT "ItemAct".id, "ItemAct".created_at
FROM "Item"
JOIN "ItemAct" on "ItemAct"."itemId" = "Item".id
WHERE "ItemAct"."userId" <> $1
AND "ItemAct".created_at > $2
AND "Item"."userId" = $1
2022-11-23 18:12:09 +00:00
AND "ItemAct".act = 'TIP'
LIMIT 1`, me.id, lastChecked)
if (votes.length > 0) {
return true
}
}
// check if they have any replies since checkedNotesAt
const newReplies = await models.$queryRaw(`
SELECT "Item".id, "Item".created_at
FROM "Item"
JOIN "Item" p ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
WHERE p."userId" = $1
AND "Item".created_at > $2 AND "Item"."userId" <> $1
${await filterClause(me, models)}
LIMIT 1`, me.id, lastChecked)
if (newReplies.length > 0) {
return true
}
// check if they have any mentions since checkedNotesAt
if (user.noteMentions) {
const newMentions = await models.$queryRaw(`
SELECT "Item".id, "Item".created_at
FROM "Mention"
JOIN "Item" ON "Mention"."itemId" = "Item".id
WHERE "Mention"."userId" = $1
AND "Mention".created_at > $2
AND "Item"."userId" <> $1
LIMIT 1`, me.id, lastChecked)
if (newMentions.length > 0) {
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) {
return true
}
if (user.noteEarning) {
const earn = await models.earn.findFirst({
where: {
userId: me.id,
createdAt: {
gt: lastChecked
},
msats: {
gte: 1000
}
}
})
if (earn) {
return true
}
}
if (user.noteDeposits) {
const invoice = await models.invoice.findFirst({
where: {
userId: me.id,
confirmedAt: {
gt: lastChecked
}
}
})
if (invoice) {
return true
}
}
// check if new invites have been redeemed
if (user.noteInvites) {
const newInvitees = await models.$queryRaw(`
SELECT "Invite".id
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
WHERE "Invite"."userId" = $1
AND users.created_at > $2
LIMIT 1`, me.id, lastChecked)
if (newInvitees.length > 0) {
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) {
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) {
return true
}
}
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`
2022-10-25 17:13:06 +00:00
SELECT * FROM users where id > 615 AND SIMILARITY(name, ${q}) > ${Number(similarity) || 0.1} ORDER BY SIMILARITY(name, ${q}) DESC LIMIT ${Number(limit) || 5}`
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: {
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 AuthenticationError('you must be logged in')
}
2023-02-08 19:38:04 +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 })
2021-05-22 00:09:11 +00:00
} catch (error) {
if (error.code === 'P2002') {
throw new UserInputError('name taken')
}
throw error
}
2021-09-23 17:42:00 +00:00
},
2023-01-07 00:53:09 +00:00
setSettings: async (parent, { nostrRelays, ...data }, { me, models }) => {
2021-10-30 16:20:11 +00:00
if (!me) {
throw new AuthenticationError('you must be logged in')
}
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 AuthenticationError('you must be logged in')
}
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 AuthenticationError('you must be logged in')
}
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 AuthenticationError('you must be logged in')
}
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) {
2022-08-18 18:15:24 +00:00
await updateItem(parent, { id: user.bioId, data: { text: bio, title: `@${user.name}'s bio` } }, { me, models })
2021-09-24 21:28:21 +00:00
} else {
2022-08-18 18:15:24 +00:00
const [item] = await serialize(models,
2022-09-27 21:19:15 +00:00
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
`@${user.name}'s bio`, bio, Number(me.id)))
2022-08-18 18:15:24 +00:00
await createMentions(item, models)
}
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
},
unlinkAuth: async (parent, { authType }, { models, me }) => {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
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 } })
2022-06-02 22:55:23 +00:00
const account = await models.account.findFirst({ where: { userId: me.id, providerId: authType } })
if (!account) {
throw new UserInputError('no such account')
}
await models.account.delete({ where: { id: account.id } })
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 === 'slashtags') {
user = await models.user.update({ where: { id: me.id }, data: { slashtagId: null } })
} else if (authType === 'email') {
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } })
} else {
throw new UserInputError('no such account')
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 AuthenticationError('you must be logged in')
}
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 },
data: { email: email.toLowerCase() }
})
2022-06-02 22:55:23 +00:00
} catch (error) {
if (error.code === 'P2002') {
throw new UserInputError('email taken')
}
throw error
}
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: {
2022-06-02 22:55:23 +00:00
authMethods,
2022-10-26 14:56:22 +00:00
nitems: async (user, { when }, { models }) => {
2022-10-25 21:35:32 +00:00
if (user.nitems) {
return user.nitems
}
2022-10-26 14:56:22 +00:00
return await models.item.count({
where: {
userId: user.id,
parentId: null,
createdAt: {
gte: withinDate(when)
}
}
})
2021-04-22 22:14:32 +00:00
},
2022-10-26 14:56:22 +00:00
ncomments: async (user, { when }, { models }) => {
2022-10-25 21:35:32 +00:00
if (user.ncomments) {
return user.ncomments
}
2022-10-26 14:56:22 +00:00
return await models.item.count({
where: {
userId: user.id,
parentId: { not: null },
createdAt: {
gte: withinDate(when)
}
}
})
2021-04-22 22:14:32 +00:00
},
nbookmarks: async (user, { when }, { models }) => {
if (user.nBookmarks) {
return user.nBookmarks
}
return await models.bookmark.count({
where: {
userId: user.id,
createdAt: {
gte: withinDate(when)
}
}
})
},
2022-10-26 14:56:22 +00:00
stacked: async (user, { when }, { models }) => {
2021-12-17 00:01:02 +00:00
if (user.stacked) {
return user.stacked
}
2022-03-17 20:13:19 +00:00
2022-10-26 14:56:22 +00:00
if (!when) {
// forever
2022-11-15 20:51:55 +00:00
return (user.stackedMsats && msatsToSats(user.stackedMsats)) || 0
2022-10-26 14:56:22 +00:00
} else {
const [{ stacked }] = await models.$queryRaw(`
SELECT sum(amount) as stacked
FROM
2022-12-19 23:00:53 +00:00
((SELECT coalesce(sum("ItemAct".msats),0) as amount
2022-10-26 14:56:22 +00:00
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
WHERE act <> 'BOOST' AND "ItemAct"."userId" <> $2 AND "Item"."userId" = $2
AND "ItemAct".created_at >= $1)
UNION ALL
2022-12-19 23:00:53 +00:00
(SELECT coalesce(sum("ReferralAct".msats),0) as amount
FROM "ReferralAct"
WHERE "ReferralAct".msats > 0 AND "ReferralAct"."referrerId" = $2
AND "ReferralAct".created_at >= $1)
UNION ALL
(SELECT coalesce(sum("Earn".msats), 0) as amount
2022-10-26 14:56:22 +00:00
FROM "Earn"
WHERE "Earn".msats > 0 AND "Earn"."userId" = $2
AND "Earn".created_at >= $1)) u`, withinDate(when), Number(user.id))
2022-11-15 20:51:55 +00:00
return (stacked && msatsToSats(stacked)) || 0
2022-10-26 14:56:22 +00:00
}
2021-04-27 21:30:58 +00:00
},
2022-10-26 14:56:22 +00:00
spent: async (user, { when }, { models }) => {
2022-10-25 21:35:32 +00:00
if (user.spent) {
return user.spent
}
2022-11-15 20:51:55 +00:00
const { sum: { msats } } = await models.itemAct.aggregate({
2022-10-25 17:13:06 +00:00
sum: {
2022-11-15 20:51:55 +00:00
msats: true
2022-10-25 17:13:06 +00:00
},
where: {
2022-10-26 14:56:22 +00:00
userId: user.id,
createdAt: {
gte: withinDate(when)
}
2022-10-25 17:13:06 +00:00
}
})
2022-11-15 20:51:55 +00:00
return (msats && msatsToSats(msats)) || 0
2022-10-25 17:13:06 +00:00
},
2022-12-19 23:00:53 +00:00
referrals: async (user, { when }, { models }) => {
return await models.user.count({
where: {
referrerId: user.id,
createdAt: {
gte: withinDate(when)
}
}
})
},
sats: async (user, args, { models, me }) => {
if (me?.id !== user.id) {
return 0
}
2022-11-15 20:51:55 +00:00
return msatsToSats(user.msats)
2021-06-24 23:56:01 +00:00
},
2023-05-07 20:21:58 +00:00
bio: async (user, args, { models, me }) => {
return getItem(user, { id: user.bioId }, { models, me })
2021-09-23 17:42:00 +00:00
},
2021-10-15 23:07:51 +00:00
hasInvites: async (user, args, { models }) => {
const invites = await models.user.findUnique({
where: { id: user.id }
}).invites({ take: 1 })
return invites.length > 0
2023-01-07 00:53:09 +00:00
},
nostrRelays: async (user, args, { models }) => {
const relays = await models.userNostrRelay.findMany({
where: { userId: user.id }
})
return relays?.map(r => r.nostrRelayAddr)
2021-05-11 15:52:50 +00:00
}
2021-03-25 19:29:24 +00:00
}
}