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-08-18 18:15:24 +00:00
|
|
|
import { mdHas } from '../../lib/md'
|
|
|
|
import { createMentions, getItem, SELECT, updateItem } from './item'
|
2021-09-23 17:42:00 +00:00
|
|
|
import serialize from './serial'
|
|
|
|
|
2021-12-17 00:01:02 +00:00
|
|
|
export function topClause (within) {
|
|
|
|
let interval = ' AND "ItemAct".created_at >= $1 - INTERVAL '
|
|
|
|
switch (within) {
|
|
|
|
case 'day':
|
|
|
|
interval += "'1 day'"
|
|
|
|
break
|
|
|
|
case 'week':
|
|
|
|
interval += "'7 days'"
|
|
|
|
break
|
|
|
|
case 'month':
|
|
|
|
interval += "'1 month'"
|
|
|
|
break
|
|
|
|
case 'year':
|
|
|
|
interval += "'1 year'"
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
interval = ''
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return interval
|
|
|
|
}
|
|
|
|
|
2022-07-14 00:55:10 +00:00
|
|
|
export function earnWithin (within) {
|
|
|
|
let interval = ' AND "Earn".created_at >= $1 - INTERVAL '
|
|
|
|
switch (within) {
|
|
|
|
case 'day':
|
|
|
|
interval += "'1 day'"
|
|
|
|
break
|
|
|
|
case 'week':
|
|
|
|
interval += "'7 days'"
|
|
|
|
break
|
|
|
|
case 'month':
|
|
|
|
interval += "'1 month'"
|
|
|
|
break
|
|
|
|
case 'year':
|
|
|
|
interval += "'1 year'"
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
interval = ''
|
|
|
|
break
|
|
|
|
}
|
|
|
|
return interval
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
github: oauth.indexOf('github') >= 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-25 19:29:24 +00:00
|
|
|
export default {
|
|
|
|
Query: {
|
2022-03-14 16:43:21 +00:00
|
|
|
me: async (parent, args, { models, me }) => {
|
|
|
|
if (!me) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2022-05-06 19:32:20 +00:00
|
|
|
return await models.user.update({ where: { id: me.id }, data: { lastSeenAt: new Date() } })
|
2022-03-14 16:43:21 +00:00
|
|
|
},
|
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')
|
|
|
|
}
|
|
|
|
|
2022-05-06 19:32:20 +00:00
|
|
|
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
|
|
|
},
|
2022-02-02 21:50:12 +00:00
|
|
|
topUsers: async (parent, { cursor, within, userType }, { models, me }) => {
|
2021-12-17 00:01:02 +00:00
|
|
|
const decodedCursor = decodeCursor(cursor)
|
2022-02-02 21:50:12 +00:00
|
|
|
let users
|
|
|
|
if (userType === 'spent') {
|
|
|
|
users = await models.$queryRaw(`
|
|
|
|
SELECT users.name, users.created_at, sum("ItemAct".sats) as amount
|
|
|
|
FROM "ItemAct"
|
|
|
|
JOIN users on "ItemAct"."userId" = users.id
|
|
|
|
WHERE "ItemAct".created_at <= $1
|
|
|
|
${topClause(within)}
|
|
|
|
GROUP BY users.id, users.name
|
|
|
|
ORDER BY amount DESC NULLS LAST, users.created_at DESC
|
|
|
|
OFFSET $2
|
|
|
|
LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset)
|
|
|
|
} else {
|
|
|
|
users = await models.$queryRaw(`
|
2022-07-14 00:55:10 +00:00
|
|
|
SELECT name, created_at, sum(sats) as amount
|
|
|
|
FROM
|
|
|
|
((SELECT users.name, users.created_at, "ItemAct".sats as sats
|
|
|
|
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
|
|
|
|
${topClause(within)})
|
|
|
|
UNION ALL
|
|
|
|
(SELECT users.name, users.created_at, "Earn".msats/1000 as sats
|
|
|
|
FROM "Earn"
|
|
|
|
JOIN users on users.id = "Earn"."userId"
|
|
|
|
WHERE "Earn".msats > 0 ${earnWithin(within)})) u
|
|
|
|
GROUP BY name, created_at
|
|
|
|
ORDER BY amount 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
|
|
|
},
|
|
|
|
searchUsers: async (parent, { name }, { models }) => {
|
|
|
|
return await models.$queryRaw`
|
|
|
|
SELECT * FROM users where id > 615 AND SIMILARITY(name, ${name}) > .1 ORDER BY SIMILARITY(name, ${name}) DESC 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: {
|
|
|
|
setName: async (parent, { name }, { me, models }) => {
|
|
|
|
if (!me) {
|
|
|
|
throw new AuthenticationError('you must be logged in')
|
|
|
|
}
|
|
|
|
|
2022-08-26 22:26:42 +00:00
|
|
|
if (!/^[\w_]+$/.test(name)) {
|
|
|
|
throw new UserInputError('only letters, numbers, and _')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (name.length > 32) {
|
|
|
|
throw new UserInputError('too long')
|
|
|
|
}
|
|
|
|
|
2021-05-22 00:09:11 +00:00
|
|
|
try {
|
2021-06-27 03:09:39 +00:00
|
|
|
await models.user.update({ where: { id: me.id }, data: { name } })
|
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
|
|
|
},
|
2022-04-21 22:50:02 +00:00
|
|
|
setSettings: async (parent, data, { me, models }) => {
|
2021-10-30 16:20:11 +00:00
|
|
|
if (!me) {
|
|
|
|
throw new AuthenticationError('you must be logged in')
|
|
|
|
}
|
|
|
|
|
2022-09-06 13:01:49 +00:00
|
|
|
return await models.user.update({ where: { id: me.id }, data })
|
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')
|
|
|
|
}
|
|
|
|
|
|
|
|
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 hasImgLink = !!(bio && mdHas(bio, ['link', 'image']))
|
2021-09-24 21:28:21 +00:00
|
|
|
|
2022-08-18 18:15:24 +00:00
|
|
|
const [item] = await serialize(models,
|
|
|
|
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3, $4) AS "Item"`,
|
|
|
|
`@${user.name}'s bio`, bio, Number(me.id), hasImgLink))
|
|
|
|
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')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (authType === 'twitter' || authType === 'github') {
|
|
|
|
const user = await models.user.findUnique({ where: { id: me.id } })
|
|
|
|
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 } })
|
|
|
|
return await authMethods(user, undefined, { models, me })
|
|
|
|
}
|
|
|
|
|
|
|
|
if (authType === 'lightning') {
|
|
|
|
const user = await models.user.update({ where: { id: me.id }, data: { pubkey: null } })
|
|
|
|
return await authMethods(user, undefined, { models, me })
|
|
|
|
}
|
|
|
|
|
|
|
|
if (authType === 'email') {
|
|
|
|
const user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null } })
|
|
|
|
return await authMethods(user, undefined, { models, me })
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new UserInputError('no such account')
|
|
|
|
},
|
|
|
|
linkUnverifiedEmail: async (parent, { email }, { models, me }) => {
|
|
|
|
if (!me) {
|
|
|
|
throw new AuthenticationError('you must be logged in')
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
await models.user.update({ where: { id: me.id }, data: { email } })
|
|
|
|
} 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,
|
2021-04-22 22:14:32 +00:00
|
|
|
nitems: async (user, args, { models }) => {
|
|
|
|
return await models.item.count({ where: { userId: user.id, parentId: null } })
|
|
|
|
},
|
|
|
|
ncomments: async (user, args, { models }) => {
|
|
|
|
return await models.item.count({ where: { userId: user.id, parentId: { not: null } } })
|
|
|
|
},
|
2021-04-27 21:30:58 +00:00
|
|
|
stacked: async (user, args, { models }) => {
|
2021-12-17 00:01:02 +00:00
|
|
|
if (user.stacked) {
|
|
|
|
return user.stacked
|
|
|
|
}
|
2022-03-17 20:13:19 +00:00
|
|
|
|
2022-04-04 21:54:31 +00:00
|
|
|
return Math.floor((user.stackedMsats || 0) / 1000)
|
2021-04-27 21:30:58 +00:00
|
|
|
},
|
2021-12-30 22:02:18 +00:00
|
|
|
sats: async (user, args, { models, me }) => {
|
|
|
|
if (me?.id !== user.id) {
|
|
|
|
return 0
|
|
|
|
}
|
2022-02-26 21:42:38 +00:00
|
|
|
return Math.floor(user.msats / 1000.0)
|
2021-06-24 23:56:01 +00:00
|
|
|
},
|
2021-09-23 17:42:00 +00:00
|
|
|
bio: async (user, args, { models }) => {
|
|
|
|
return getItem(user, { id: user.bioId }, { models })
|
|
|
|
},
|
2021-10-15 23:07:51 +00:00
|
|
|
hasInvites: async (user, args, { models }) => {
|
2022-04-28 22:00:09 +00:00
|
|
|
const invites = await models.user.findUnique({
|
|
|
|
where: { id: user.id }
|
|
|
|
}).invites({ take: 1 })
|
|
|
|
|
|
|
|
return invites.length > 0
|
2021-10-15 23:07:51 +00:00
|
|
|
},
|
2022-04-21 22:50:02 +00:00
|
|
|
hasNewNotes: async (user, args, { me, models }) => {
|
2022-04-04 21:54:31 +00:00
|
|
|
const lastChecked = user.checkedNotesAt || new Date(0)
|
2022-04-21 22:50:02 +00:00
|
|
|
|
|
|
|
// check if any votes have been cast for them since checkedNotesAt
|
2022-05-02 01:01:33 +00:00
|
|
|
if (user.noteItemSats) {
|
2022-04-21 22:50:02 +00:00
|
|
|
const votes = await models.$queryRaw(`
|
2021-09-08 21:51:23 +00:00
|
|
|
SELECT "ItemAct".id, "ItemAct".created_at
|
2022-04-04 21:54:31 +00:00
|
|
|
FROM "Item"
|
|
|
|
JOIN "ItemAct" on "ItemAct"."itemId" = "Item".id
|
2022-01-19 21:02:38 +00:00
|
|
|
WHERE "ItemAct"."userId" <> $1
|
2022-04-04 21:54:31 +00:00
|
|
|
AND "ItemAct".created_at > $2
|
2022-01-19 21:02:38 +00:00
|
|
|
AND "Item"."userId" = $1
|
2022-05-18 16:43:37 +00:00
|
|
|
AND "ItemAct".act IN ('VOTE', 'TIP')
|
2022-05-02 01:01:33 +00:00
|
|
|
LIMIT 1`, me.id, lastChecked)
|
2022-04-21 22:50:02 +00:00
|
|
|
if (votes.length > 0) {
|
|
|
|
return true
|
|
|
|
}
|
2021-06-24 23:56:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// check if they have any replies since checkedNotesAt
|
|
|
|
const newReplies = await models.$queryRaw(`
|
|
|
|
SELECT "Item".id, "Item".created_at
|
2022-01-19 21:02:38 +00:00
|
|
|
FROM "Item"
|
2022-05-02 01:01:33 +00:00
|
|
|
JOIN "Item" p ON ${user.noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id'}
|
2022-01-19 21:02:38 +00:00
|
|
|
WHERE p."userId" = $1
|
2022-04-04 21:54:31 +00:00
|
|
|
AND "Item".created_at > $2 AND "Item"."userId" <> $1
|
2022-05-02 01:01:33 +00:00
|
|
|
LIMIT 1`, me.id, lastChecked)
|
2021-08-18 23:00:54 +00:00
|
|
|
if (newReplies.length > 0) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if they have any mentions since checkedNotesAt
|
2022-05-02 01:01:33 +00:00
|
|
|
if (user.noteMentions) {
|
2022-04-21 22:50:02 +00:00
|
|
|
const newMentions = await models.$queryRaw(`
|
2021-08-18 23:00:54 +00:00
|
|
|
SELECT "Item".id, "Item".created_at
|
2022-01-19 21:02:38 +00:00
|
|
|
FROM "Mention"
|
|
|
|
JOIN "Item" ON "Mention"."itemId" = "Item".id
|
|
|
|
WHERE "Mention"."userId" = $1
|
2022-04-04 21:54:31 +00:00
|
|
|
AND "Mention".created_at > $2
|
2022-01-19 21:02:38 +00:00
|
|
|
AND "Item"."userId" <> $1
|
2022-05-02 01:01:33 +00:00
|
|
|
LIMIT 1`, me.id, lastChecked)
|
2022-04-21 22:50:02 +00:00
|
|
|
if (newMentions.length > 0) {
|
|
|
|
return true
|
|
|
|
}
|
2022-01-19 21:02:38 +00:00
|
|
|
}
|
|
|
|
|
2022-03-17 20:13:19 +00:00
|
|
|
const job = await models.item.findFirst({
|
|
|
|
where: {
|
|
|
|
status: {
|
|
|
|
not: 'STOPPED'
|
|
|
|
},
|
|
|
|
maxBid: {
|
|
|
|
not: null
|
|
|
|
},
|
2022-05-02 01:01:33 +00:00
|
|
|
userId: me.id,
|
2022-03-17 20:13:19 +00:00
|
|
|
statusUpdatedAt: {
|
2022-04-04 21:54:31 +00:00
|
|
|
gt: lastChecked
|
2022-03-17 20:13:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (job) {
|
|
|
|
return true
|
2022-03-01 17:04:44 +00:00
|
|
|
}
|
|
|
|
|
2022-05-02 01:01:33 +00:00
|
|
|
if (user.noteEarning) {
|
2022-04-21 22:50:02 +00:00
|
|
|
const earn = await models.earn.findFirst({
|
|
|
|
where: {
|
2022-05-02 01:01:33 +00:00
|
|
|
userId: me.id,
|
2022-04-21 22:50:02 +00:00
|
|
|
createdAt: {
|
|
|
|
gt: lastChecked
|
|
|
|
},
|
|
|
|
msats: {
|
|
|
|
gte: 1000
|
|
|
|
}
|
2022-03-17 20:13:19 +00:00
|
|
|
}
|
2022-04-21 22:50:02 +00:00
|
|
|
})
|
|
|
|
if (earn) {
|
|
|
|
return true
|
2022-03-01 17:04:44 +00:00
|
|
|
}
|
2022-02-28 20:09:21 +00:00
|
|
|
}
|
|
|
|
|
2022-05-02 01:01:33 +00:00
|
|
|
if (user.noteDeposits) {
|
2022-04-21 22:50:02 +00:00
|
|
|
const invoice = await models.invoice.findFirst({
|
|
|
|
where: {
|
2022-05-02 01:01:33 +00:00
|
|
|
userId: me.id,
|
2022-04-21 22:50:02 +00:00
|
|
|
confirmedAt: {
|
|
|
|
gt: lastChecked
|
|
|
|
}
|
2022-03-23 18:54:39 +00:00
|
|
|
}
|
2022-04-21 22:50:02 +00:00
|
|
|
})
|
|
|
|
if (invoice) {
|
|
|
|
return true
|
2022-03-23 18:54:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-19 21:02:38 +00:00
|
|
|
// check if new invites have been redeemed
|
2022-05-02 01:01:33 +00:00
|
|
|
if (user.noteInvites) {
|
2022-04-21 22:50:02 +00:00
|
|
|
const newInvitees = await models.$queryRaw(`
|
2022-01-19 21:02:38 +00:00
|
|
|
SELECT "Invite".id
|
|
|
|
FROM users JOIN "Invite" on users."inviteId" = "Invite".id
|
|
|
|
WHERE "Invite"."userId" = $1
|
2022-04-04 21:54:31 +00:00
|
|
|
AND users.created_at > $2
|
2022-05-02 01:01:33 +00:00
|
|
|
LIMIT 1`, me.id, lastChecked)
|
2022-04-21 22:50:02 +00:00
|
|
|
if (newInvitees.length > 0) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
2021-05-11 15:52:50 +00:00
|
|
|
}
|
2021-03-25 19:29:24 +00:00
|
|
|
}
|
|
|
|
}
|