2023-09-18 18:57:02 +00:00
import { readFile } from 'fs/promises'
import { join , resolve } from 'path'
2023-07-23 15:08:43 +00:00
import { GraphQLError } from 'graphql'
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'
2024-02-13 19:17:56 +00:00
import { bioSchema , emailSchema , settingsSchema , ssValidate , userSchema } from '../../lib/validate'
2023-11-14 16:23:44 +00:00
import { getItem , updateItem , filterClause , createItem , whereClause , muteClause } from './item'
2024-03-01 16:28:55 +00:00
import { ANON _USER _ID , DELETE _USER _ID , RESERVED _MAX _USER _ID , SN _USER _IDS } from '../../lib/constants'
2024-01-19 21:19:26 +00:00
import { viewGroup } from './growth'
2023-11-14 16:23:44 +00:00
import { whenRange } from '../../lib/time'
2021-09-23 17:42:00 +00:00
2023-09-18 18:57:02 +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
}
} )
2023-07-29 19:38:20 +00:00
const oauth = accounts . map ( a => a . provider )
2022-06-02 22:55:23 +00:00
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 ,
2024-03-14 20:32:34 +00:00
nostr : ! ! user . nostrAuthPubkey ,
apiKey : user . apiKeyEnabled ? user . apiKey : null
2022-06-02 22:55:23 +00:00
}
}
2024-03-01 16:28:55 +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 '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 (
2024-03-14 00:26:59 +00:00
u => u . hideFromTopUsers && ( ! me || me . id !== u . id ) ? null : u
2024-03-01 16:28:55 +00:00
)
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
WHERE v . id NOT IN ( $ { SN _USER _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 } ) => {
2023-07-29 19:38:20 +00:00
if ( ! me ? . id ) {
2022-03-14 16:43:21 +00:00
return null
}
2023-05-07 15:44:57 +00:00
return await models . user . findUnique ( { where : { id : me . id } } )
2022-03-14 16:43:21 +00:00
} ,
2022-06-02 22:55:23 +00:00
settings : async ( parent , args , { models , me } ) => {
if ( ! me ) {
2023-07-23 15:08:43 +00:00
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 } ) => {
2023-08-25 19:22:02 +00:00
let user
if ( me ) {
user = await models . user . findUnique ( { where : { id : me . id } } )
2021-05-21 22:32:21 +00:00
}
2023-08-25 19:22:02 +00:00
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 )
2024-01-19 21:19:26 +00:00
const range = whenRange ( 'forever' )
2024-03-01 16:28:55 +00:00
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
2024-03-01 16:28:55 +00:00
WHERE streak IS NOT NULL
2023-05-19 22:33:54 +00:00
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)
2024-03-01 16:28:55 +00:00
) . map (
2024-03-14 00:26:59 +00:00
u => ( u . hideFromTopUsers || u . hideCowboyHat ) && ( ! me || me . id !== u . id ) ? null : u
2024-03-01 16:28:55 +00:00
)
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 ( $ { ANON _USER _ID } , $ { DELETE _USER _ID } )
)
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
} ,
2024-03-01 16:28:55 +00:00
topUsers ,
2022-11-07 23:31:29 +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 )
2023-10-22 22:51:07 +00:00
// 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 )
2023-10-22 22:51:07 +00:00
2022-11-07 23:31:29 +00:00
// 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"
JOIN "ItemAct" ON
"ItemAct" . "itemId" = "Item" . id
AND "ItemAct" . "userId" < > "Item" . "userId"
WHERE "ItemAct" . created _at > $2
AND "Item" . "userId" = $1
AND "ItemAct" . act = 'TIP' ) ` , me.id, lastChecked)
if ( newSats . exists ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-11-07 23:31:29 +00:00
return true
}
}
2023-06-01 00:44:06 +00:00
// 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"
JOIN "Item" p ON "ThreadSubscription" . "itemId" = p . id
JOIN "Item" ON $ { user . noteAllDescendants ? '"Item".path <@ p.path' : '"Item"."parentId" = p.id' }
$ { whereClause (
'"ThreadSubscription"."userId" = $1' ,
2023-10-22 17:47:46 +00:00
'"Item".created_at > $2' ,
'"Item".created_at >= "ThreadSubscription".created_at' ,
2023-10-14 00:23:57 +00:00
'"Item"."userId" <> $1' ,
2023-09-28 20:02:25 +00:00
await filterClause ( me , models ) ,
muteClause ( me )
) } ) ` , me.id, lastChecked)
if ( newThreadSubReply . exists ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2023-06-01 00:44:06 +00:00
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" )
) ` ,
await filterClause ( me , models ) ,
muteClause ( me ) ) } ) ` , me.id, lastChecked)
if ( newUserSubs . exists ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2023-08-29 01:27:56 +00:00
return true
}
2022-11-07 23:31:29 +00:00
// 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 *
2022-11-07 23:31:29 +00:00
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' ,
await filterClause ( me , models ) ,
muteClause ( me )
) } ) ` , me.id, lastChecked)
if ( newMentions . exists ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-11-07 23:31:29 +00:00
return true
}
}
2023-09-12 15:31:46 +00:00
if ( user . noteForwardedSats ) {
2023-09-28 20:02:25 +00:00
const [ newFwdSats ] = await models . $queryRawUnsafe ( `
SELECT EXISTS (
SELECT *
2023-09-12 15:31:46 +00:00
FROM "Item"
JOIN "ItemAct" ON
"ItemAct" . "itemId" = "Item" . id
AND "ItemAct" . "userId" < > "Item" . "userId"
JOIN "ItemForward" ON
"ItemForward" . "itemId" = "Item" . id
AND "ItemForward" . "userId" = $1
WHERE "ItemAct" . created _at > $2
AND "Item" . "userId" < > $1
2023-09-28 20:02:25 +00:00
AND "ItemAct" . act = 'TIP' ) ` , me.id, lastChecked)
if ( newFwdSats . exists ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2023-09-12 15:31:46 +00:00
return true
}
}
2022-11-07 23:31:29 +00:00
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 ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-11-07 23:31:29 +00:00
return true
}
if ( user . noteEarning ) {
const earn = await models . earn . findFirst ( {
where : {
userId : me . id ,
createdAt : {
gt : lastChecked
} ,
msats : {
gte : 1000
}
}
} )
if ( earn ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-11-07 23:31:29 +00:00
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
} ,
2023-09-19 00:30:26 +00:00
isHeld : null
2022-11-07 23:31:29 +00:00
}
} )
if ( invoice ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-11-07 23:31:29 +00:00
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 ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-11-07 23:31:29 +00:00
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 ) {
2023-10-22 22:51:07 +00:00
foundNotes ( )
2022-12-19 22:27:52 +00:00
return true
}
2022-11-07 23:31:29 +00:00
}
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 ) {
2023-10-22 22:51:07 +00:00
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
}
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 )
2023-10-22 22:38:24 +00:00
2022-11-07 23:31:29 +00:00
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 ( $ { ANON _USER _ID } , $ { DELETE _USER _ID } ) )
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 ) {
2023-07-23 15:08:43 +00:00
throw new GraphQLError ( 'you must be logged in' , { extensions : { code : 'UNAUTHENTICATED' } } )
2021-05-22 00:09:11 +00:00
}
2023-09-26 00:54:35 +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 } )
2023-07-23 15:08:43 +00:00
return data . name
2021-05-22 00:09:11 +00:00
} catch ( error ) {
if ( error . code === 'P2002' ) {
2023-07-23 15:08:43 +00:00
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 ) {
2023-07-23 15:08:43 +00:00
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 ) {
2023-07-23 15:08:43 +00:00
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 ) {
2023-07-23 15:08:43 +00:00
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 ) {
2023-07-23 15:08:43 +00:00
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
} ,
2024-03-14 20:32:34 +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' } } )
}
const [ { apiKey } ] = await models . $queryRaw ` UPDATE users SET "apiKey" = encode(gen_random_bytes(32), 'base64')::CHAR(32) WHERE id = ${ me . id } RETURNING "apiKey" `
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 : { apiKey : null } } )
} ,
2022-06-02 22:55:23 +00:00
unlinkAuth : async ( parent , { authType } , { models , me } ) => {
if ( ! me ) {
2023-07-23 15:08:43 +00:00
throw new GraphQLError ( 'you must be logged in' , { extensions : { code : 'UNAUTHENTICATED' } } )
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 } } )
2023-07-29 19:38:20 +00:00
const account = await models . account . findFirst ( { where : { userId : me . id , provider : authType } } )
2022-06-02 22:55:23 +00:00
if ( ! account ) {
2023-07-23 15:08:43 +00:00
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 } } )
2024-02-14 19:33:31 +00:00
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 } } )
2023-08-08 00:50:01 +00:00
} else if ( authType === 'nostr' ) {
2024-02-14 19:33:31 +00:00
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 } } )
} else {
2023-07-23 15:08:43 +00:00
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 ) {
2023-07-23 15:08:43 +00:00
throw new GraphQLError ( 'you must be logged in' , { extensions : { code : 'UNAUTHENTICATED' } } )
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 } ,
data : { email : email . toLowerCase ( ) }
} )
2022-06-02 22:55:23 +00:00
} catch ( error ) {
if ( error . code === 'P2002' ) {
2023-07-23 15:08:43 +00:00
throw new GraphQLError ( 'email taken' , { extensions : { code : 'BAD_INPUT' } } )
2022-06-02 22:55:23 +00:00
}
throw error
}
return true
2023-08-29 01:27:56 +00:00
} ,
2023-09-18 18:20:02 +00:00
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 } } )
if ( existing ) {
await models . userSubscription . update ( { where : { followerId _followeeId : lookupData } , data : { postsSubscribedAt : existing . postsSubscribedAt ? null : new Date ( ) } } )
2023-08-29 01:27:56 +00:00
} else {
2023-09-18 18:20:02 +00:00
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 } } )
if ( existing ) {
await models . userSubscription . update ( { where : { followerId _followeeId : lookupData } , data : { commentsSubscribedAt : existing . commentsSubscribedAt ? null : new Date ( ) } } )
} else {
await models . userSubscription . create ( { data : { ... lookupData , commentsSubscribedAt : new Date ( ) } } )
2023-08-29 01:27:56 +00:00
}
return { id }
2023-09-11 21:29:45 +00:00
} ,
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 {
await models . mute . create ( { data : { ... lookupData } } )
}
return { id }
} ,
2023-09-11 21:29:45 +00:00
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
const mute = await models . mute . findUnique ( {
where : {
muterId _mutedId : {
muterId : Number ( me . id ) ,
mutedId : Number ( user . id )
}
}
} )
return ! ! mute
} ,
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
} ,
2023-11-09 00:15:36 +00:00
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
}
2023-05-19 22:33:54 +00:00
2023-11-09 00:15:36 +00:00
const [ gte , lte ] = whenRange ( when , from , to )
2023-07-23 15:08:43 +00:00
return await models . item . count ( {
where : {
userId : user . id ,
createdAt : {
2023-11-09 00:15:36 +00:00
gte ,
lte
2023-07-23 15:08:43 +00:00
}
}
} )
} ,
2023-11-09 00:15:36 +00:00
nposts : async ( user , { when , from , to } , { models } ) => {
2023-07-27 00:18:42 +00:00
if ( typeof user . nposts !== 'undefined' ) {
2023-07-23 15:08:43 +00:00
return user . nposts
}
2023-11-09 00:15:36 +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 : null ,
createdAt : {
2023-11-09 00:15:36 +00:00
gte ,
lte
2022-10-26 14:56:22 +00:00
}
}
} )
2021-04-22 22:14:32 +00:00
} ,
2023-11-09 00:15:36 +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
2023-11-09 00:15:36 +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 : {
2023-11-09 00:15:36 +00:00
gte ,
lte
2022-10-26 14:56:22 +00:00
}
}
} )
2021-04-22 22:14:32 +00:00
} ,
2024-02-22 01:55:48 +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-02-16 22:23:59 +00:00
}
2023-11-10 01:05:35 +00:00
const relays = await models . userNostrRelay . findMany ( {
where : { userId : user . id }
2023-02-16 22:23:59 +00:00
} )
2023-11-10 01:05:35 +00:00
return relays ? . map ( r => r . nostrRelayAddr )
}
} ,
UserOptional : {
streak : async ( user , args , { models } ) => {
if ( user . hideCowboyHat ) {
return null
}
return user . streak
2023-02-16 22:23:59 +00:00
} ,
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
2023-05-19 22:33:54 +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' ) {
2023-05-19 22:33:54 +00:00
return user . referrals
}
2023-07-27 00:18:42 +00:00
2023-11-09 00:15:36 +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 : {
2023-11-09 00:15:36 +00:00
gte ,
lte
2022-12-19 23:00:53 +00:00
}
}
} )
2024-02-14 19:33:31 +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
}
}