2021-04-12 18:05:09 +00:00
import { UserInputError , AuthenticationError } from 'apollo-server-micro'
2022-11-15 23:51:00 +00:00
import { ensureProtocol , removeTracking } from '../../lib/url'
2021-05-20 01:09:32 +00:00
import serialize from './serial'
2021-09-06 22:36:08 +00:00
import { decodeCursor , LIMIT , nextCursorEncoded } from '../../lib/cursor'
2021-08-22 15:25:17 +00:00
import { getMetadata , metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'
2022-09-21 19:57:36 +00:00
import {
2023-02-08 19:38:04 +00:00
BOOST _MIN , ITEM _SPAM _INTERVAL ,
2023-05-06 21:51:17 +00:00
MAX _TITLE _LENGTH , ITEM _FILTER _THRESHOLD , DONT _LIKE _THIS _COST , COMMENT _DEPTH _LIMIT
2022-09-21 19:57:36 +00:00
} from '../../lib/constants'
2022-11-15 20:51:55 +00:00
import { msatsToSats } from '../../lib/format'
2023-01-11 22:20:14 +00:00
import { parse } from 'tldts'
2023-01-12 18:05:47 +00:00
import uu from 'url-unshort'
2023-02-08 19:38:04 +00:00
import { amountSchema , bountySchema , commentSchema , discussionSchema , jobSchema , linkSchema , pollSchema , ssValidate } from '../../lib/validate'
2021-06-22 17:47:49 +00:00
2023-05-07 01:25:00 +00:00
async function comments ( me , models , id , sort ) {
2021-12-21 21:29:42 +00:00
let orderBy
switch ( sort ) {
case 'top' :
2023-06-04 01:01:50 +00:00
orderBy = ` ORDER BY ${ await orderByNumerator ( me , models ) } DESC, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC `
2021-12-21 21:29:42 +00:00
break
case 'recent' :
2023-06-04 01:01:50 +00:00
orderBy = 'ORDER BY "Item".created_at DESC, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC'
2021-12-21 21:29:42 +00:00
break
default :
2023-06-04 01:01:50 +00:00
orderBy = ` ORDER BY ${ await orderByNumerator ( me , models ) } /POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC `
2021-12-21 21:29:42 +00:00
break
}
2023-06-04 01:01:50 +00:00
const filter = await commentFilterClause ( me , models )
2023-05-07 01:25:00 +00:00
if ( me ) {
const [ { item _comments _with _me : comments } ] = await models . $queryRaw (
'SELECT item_comments_with_me($1, $2, $3, $4, $5)' , Number ( id ) , Number ( me . id ) , COMMENT _DEPTH _LIMIT , filter , orderBy )
return comments
}
const [ { item _comments : comments } ] = await models . $queryRaw (
'SELECT item_comments($1, $2, $3, $4)' , Number ( id ) , COMMENT _DEPTH _LIMIT , filter , orderBy )
2023-05-06 21:51:17 +00:00
return comments
2021-04-29 15:56:28 +00:00
}
2022-09-21 19:57:36 +00:00
export async function getItem ( parent , { id } , { me , models } ) {
2023-05-08 20:06:42 +00:00
const [ item ] = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE id = $1 `
} , Number ( id ) )
2021-09-23 17:42:00 +00:00
return item
}
2021-12-16 23:05:31 +00:00
function topClause ( within ) {
2022-04-02 22:39:15 +00:00
let interval = ' AND "Item".created_at >= $1 - INTERVAL '
2021-12-16 23:05:31 +00:00
switch ( within ) {
2022-10-25 21:35:32 +00:00
case 'forever' :
interval = ''
2021-12-16 23:05:31 +00:00
break
case 'week' :
interval += "'7 days'"
break
case 'month' :
interval += "'1 month'"
break
case 'year' :
interval += "'1 year'"
break
default :
2022-10-25 21:35:32 +00:00
interval += "'1 day'"
2021-12-16 23:05:31 +00:00
break
}
return interval
}
2022-10-25 21:35:32 +00:00
async function topOrderClause ( sort , me , models ) {
switch ( sort ) {
case 'comments' :
return 'ORDER BY ncomments DESC'
case 'sats' :
2022-11-15 20:51:55 +00:00
return 'ORDER BY msats DESC'
2022-10-25 21:35:32 +00:00
default :
return await topOrderByWeightedSats ( me , models )
}
}
2022-09-21 19:57:36 +00:00
export async function orderByNumerator ( me , models ) {
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
if ( user . wildWestMode ) {
2023-05-09 20:07:23 +00:00
return '(GREATEST("Item"."weightedVotes", POWER("Item"."weightedVotes", 1.2)) + "Item"."weightedComments"/2)'
2022-09-21 19:57:36 +00:00
}
}
return ` (CASE WHEN "Item"."weightedVotes" > "Item"."weightedDownVotes"
THEN 1
ELSE - 1 END
2023-05-09 18:52:35 +00:00
* GREATEST ( ABS ( "Item" . "weightedVotes" - "Item" . "weightedDownVotes" ) , POWER ( ABS ( "Item" . "weightedVotes" - "Item" . "weightedDownVotes" ) , 1.2 ) )
+ "Item" . "weightedComments" / 2 ) `
2022-09-21 19:57:36 +00:00
}
2023-05-23 14:21:04 +00:00
export async function joinSatRankView ( me , models ) {
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
if ( user . wildWestMode ) {
return 'JOIN sat_rank_wwm_view ON "Item".id = sat_rank_wwm_view.id'
}
}
return 'JOIN sat_rank_tender_view ON "Item".id = sat_rank_tender_view.id'
}
2023-06-04 01:01:50 +00:00
export async function commentFilterClause ( me , models ) {
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > - ${ ITEM _FILTER _THRESHOLD } `
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
// wild west mode has everything
if ( user . wildWestMode ) {
return ''
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${ me . id } `
}
// close the clause
clause += ')'
return clause
}
2022-09-21 19:57:36 +00:00
export async function filterClause ( me , models ) {
2022-09-27 21:19:15 +00:00
// by default don't include freebies unless they have upvotes
let clause = ' AND (NOT "Item".freebie OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0'
2022-09-21 19:57:36 +00:00
if ( me ) {
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-09-27 21:19:15 +00:00
// wild west mode has everything
2022-09-21 19:57:36 +00:00
if ( user . wildWestMode ) {
return ''
}
2022-09-27 21:19:15 +00:00
// greeter mode includes freebies if feebies haven't been flagged
if ( user . greeterMode ) {
2023-05-06 23:17:47 +00:00
clause = ' AND (NOT "Item".freebie OR ("Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0 AND "Item".freebie)'
2022-09-27 21:19:15 +00:00
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${ me . id } ) `
} else {
// close default freebie clause
clause += ')'
2022-09-21 19:57:36 +00:00
}
// if the item is above the threshold or is mine
2022-09-27 21:19:15 +00:00
clause += ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > - ${ ITEM _FILTER _THRESHOLD } `
2022-09-21 19:57:36 +00:00
if ( me ) {
clause += ` OR "Item"."userId" = ${ me . id } `
}
clause += ')'
return clause
}
2022-12-01 22:22:13 +00:00
function recentClause ( type ) {
switch ( type ) {
case 'links' :
return ' AND url IS NOT NULL'
case 'discussions' :
return ' AND url IS NULL AND bio = false AND "pollCost" IS NULL'
case 'polls' :
return ' AND "pollCost" IS NOT NULL'
case 'bios' :
return ' AND bio = true'
2023-01-26 16:11:55 +00:00
case 'bounties' :
return ' AND bounty IS NOT NULL'
2022-12-01 22:22:13 +00:00
default :
return ''
}
}
2023-05-07 01:25:00 +00:00
// this grabs all the stuff we need to display the item list and only
2023-05-08 20:06:42 +00:00
// hits the db once ... orderBy needs to be duplicated on the outer query because
// joining does not preserve the order of the inner query
async function itemQueryWithMeta ( { me , models , query , orderBy = '' } , ... args ) {
2023-05-07 01:25:00 +00:00
if ( ! me ) {
return await models . $queryRaw ( `
SELECT "Item" . * , to _json ( users . * ) as user
FROM (
$ { query }
) "Item"
2023-05-08 20:06:42 +00:00
JOIN users ON "Item" . "userId" = users . id
$ { orderBy } ` , ...args)
2023-05-07 01:25:00 +00:00
} else {
return await models . $queryRaw ( `
SELECT "Item" . * , to _json ( users . * ) as user , COALESCE ( "ItemAct" . "meMsats" , 0 ) as "meMsats" ,
2023-06-01 00:44:06 +00:00
COALESCE ( "ItemAct" . "meDontLike" , false ) as "meDontLike" , "Bookmark" . "itemId" IS NOT NULL AS "meBookmark" ,
"ThreadSubscription" . "itemId" IS NOT NULL AS "meSubscription"
2023-05-07 01:25:00 +00:00
FROM (
$ { query }
) "Item"
JOIN users ON "Item" . "userId" = users . id
LEFT JOIN "Bookmark" ON "Bookmark" . "itemId" = "Item" . id AND "Bookmark" . "userId" = $ { me . id }
2023-06-01 00:44:06 +00:00
LEFT JOIN "ThreadSubscription" ON "ThreadSubscription" . "itemId" = "Item" . id AND "ThreadSubscription" . "userId" = $ { me . id }
2023-05-07 01:25:00 +00:00
LEFT JOIN LATERAL (
SELECT "itemId" , sum ( "ItemAct" . msats ) FILTER ( WHERE act = 'FEE' OR act = 'TIP' ) AS "meMsats" ,
bool _or ( act = 'DONT_LIKE_THIS' ) AS "meDontLike"
FROM "ItemAct"
WHERE "ItemAct" . "userId" = $ { me . id }
AND "ItemAct" . "itemId" = "Item" . id
GROUP BY "ItemAct" . "itemId"
2023-05-08 20:06:42 +00:00
) "ItemAct" ON true
$ { orderBy } ` , ...args)
2023-05-07 01:25:00 +00:00
}
2023-05-06 23:17:47 +00:00
}
2023-05-23 14:21:04 +00:00
const subClause = ( sub , num , table , solo ) => {
return sub ? ` ${ solo ? 'WHERE' : 'AND' } ${ table ? ` " ${ table } ". ` : '' } "subName" = $ ${ num } ` : ''
2023-05-01 20:58:30 +00:00
}
2021-04-12 18:05:09 +00:00
export default {
Query : {
2022-08-10 15:06:31 +00:00
itemRepetition : async ( parent , { parentId } , { me , models } ) => {
if ( ! me ) return 0
// how many of the parents starting at parentId belong to me
2022-09-01 21:06:11 +00:00
const [ { item _spam : count } ] = await models . $queryRaw ( ` SELECT item_spam( $ 1, $ 2, ' ${ ITEM _SPAM _INTERVAL } ') ` ,
Number ( parentId ) , Number ( me . id ) )
2022-08-10 15:06:31 +00:00
return count
} ,
2023-06-12 22:15:20 +00:00
topItems : async ( parent , { sub , cursor , sort , when } , { me , models } ) => {
2022-10-25 21:35:32 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-06-12 22:15:20 +00:00
const subArr = sub ? [ sub ] : [ ]
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
AND "pinId" IS NULL AND "deletedAt" IS NULL
2023-06-12 22:15:20 +00:00
$ { subClause ( sub , 3 ) }
2023-05-08 20:06:42 +00:00
$ { topClause ( when ) }
$ { await filterClause ( me , models ) }
$ { await topOrderClause ( sort , me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderClause ( sort , me , models )
2023-06-12 22:15:20 +00:00
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-10-25 21:35:32 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2023-06-12 22:15:20 +00:00
topComments : async ( parent , { sub , cursor , sort , when } , { me , models } ) => {
2022-10-25 21:35:32 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-06-12 22:15:20 +00:00
const subArr = sub ? [ sub ] : [ ]
2023-05-08 20:06:42 +00:00
const comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
2023-06-12 22:15:20 +00:00
JOIN "Item" root ON "Item" . "rootId" = root . id
WHERE "Item" . "parentId" IS NOT NULL
AND "Item" . created _at <= $1 AND "Item" . "deletedAt" IS NULL
$ { subClause ( sub , 3 , 'root' ) }
2023-05-08 20:06:42 +00:00
$ { topClause ( when ) }
$ { await filterClause ( me , models ) }
$ { await topOrderClause ( sort , me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderClause ( sort , me , models )
2023-06-12 22:15:20 +00:00
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-10-25 21:35:32 +00:00
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
2022-12-01 22:22:13 +00:00
items : async ( parent , { sub , sort , type , cursor , name , within } , { me , models } ) => {
2021-06-22 17:47:49 +00:00
const decodedCursor = decodeCursor ( cursor )
2022-02-17 17:23:43 +00:00
let items ; let user ; let pins ; let subFull
2022-02-26 16:41:30 +00:00
const activeOrMine = ( ) => {
2022-06-02 23:25:21 +00:00
return me ? ` AND (status <> 'STOPPED' OR "userId" = ${ me . id } ) ` : ' AND status <> \'STOPPED\' '
2022-02-17 17:23:43 +00:00
}
2021-10-26 20:49:37 +00:00
2023-05-05 18:06:53 +00:00
// HACK we want to optionally include the subName in the query
// but the query planner doesn't like unused parameters
const subArr = sub ? [ sub ] : [ ]
2021-06-24 23:56:01 +00:00
switch ( sort ) {
case 'user' :
2021-10-26 20:49:37 +00:00
if ( ! name ) {
throw new UserInputError ( 'must supply name' , { argumentName : 'name' } )
}
user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NULL AND created _at <= $2
AND "pinId" IS NULL
$ { activeOrMine ( ) }
$ { await filterClause ( me , models ) }
ORDER BY created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , user . id , decodedCursor . time , decodedCursor . offset )
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
case 'recent' :
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
$ { subClause ( sub , 3 ) }
$ { activeOrMine ( ) }
$ { await filterClause ( me , models ) }
$ { recentClause ( type ) }
ORDER BY created _at DESC
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-02-17 17:23:43 +00:00
break
case 'top' :
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "parentId" IS NULL AND "Item" . created _at <= $1
AND "pinId" IS NULL AND "deletedAt" IS NULL
$ { topClause ( within ) }
$ { await filterClause ( me , models ) }
$ { await topOrderByWeightedSats ( me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderByWeightedSats ( me , models )
} , decodedCursor . time , decodedCursor . offset )
2021-06-24 23:56:01 +00:00
break
2022-02-17 17:23:43 +00:00
default :
// sub so we know the default ranking
if ( sub ) {
subFull = await models . sub . findUnique ( { where : { name : sub } } )
}
switch ( subFull ? . rankingType ) {
case 'AUCTION' :
2023-05-08 20:06:42 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
2023-05-08 22:32:37 +00:00
$ { SELECT } ,
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
THEN 0 ELSE 1 END AS group _rank ,
CASE WHEN status = 'ACTIVE' AND "maxBid" > 0
THEN rank ( ) OVER ( ORDER BY "maxBid" DESC , created _at ASC )
ELSE rank ( ) OVER ( ORDER BY created _at DESC ) END AS rank
2023-05-08 20:06:42 +00:00
FROM "Item"
WHERE "parentId" IS NULL AND created _at <= $1
AND "pinId" IS NULL
$ { subClause ( sub , 3 ) }
2023-05-08 22:32:37 +00:00
AND status IN ( 'ACTIVE' , 'NOSATS' )
ORDER BY group _rank , rank
2023-05-08 20:06:42 +00:00
OFFSET $2
2023-05-08 22:32:37 +00:00
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY group_rank, rank'
2023-05-08 20:06:42 +00:00
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-02-17 17:23:43 +00:00
break
default :
2023-05-23 14:21:04 +00:00
items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT } , rank
2023-05-06 23:17:47 +00:00
FROM "Item"
2023-05-23 14:21:04 +00:00
$ { await joinSatRankView ( me , models ) }
$ { subClause ( sub , 2 , 'Item' , true ) }
2023-05-24 07:14:46 +00:00
ORDER BY rank ASC
2023-05-23 14:21:04 +00:00
OFFSET $1
2023-05-08 20:06:42 +00:00
LIMIT $ { LIMIT } ` ,
2023-05-24 07:14:46 +00:00
orderBy : 'ORDER BY rank ASC'
2023-05-23 14:21:04 +00:00
} , decodedCursor . offset , ... subArr )
2022-02-17 17:23:43 +00:00
if ( decodedCursor . offset === 0 ) {
// get pins for the page and return those separately
2023-05-08 20:06:42 +00:00
pins = await itemQueryWithMeta ( {
me ,
models ,
query : `
SELECT rank _filter . *
FROM (
$ { SELECT } ,
rank ( ) OVER (
PARTITION BY "pinId"
ORDER BY created _at DESC
)
FROM "Item"
WHERE "pinId" IS NOT NULL
$ { subClause ( sub , 1 ) }
) rank _filter WHERE RANK = 1 `
} , ... subArr )
2022-02-17 17:23:43 +00:00
}
break
}
break
2021-06-24 23:56:01 +00:00
}
2021-06-22 17:47:49 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
2022-01-07 16:32:31 +00:00
items ,
pins
2021-06-22 17:47:49 +00:00
}
2021-04-24 21:05:07 +00:00
} ,
2022-09-21 19:57:36 +00:00
allItems : async ( parent , { cursor } , { me , models } ) => {
2022-01-25 19:34:51 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-01-25 19:34:51 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-22 18:44:50 +00:00
outlawedItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const notMine = ( ) => {
return me ? ` AND "userId" <> ${ me . id } ` : ''
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . "weightedVotes" - "Item" . "weightedDownVotes" <= - $ { ITEM _FILTER _THRESHOLD }
$ { notMine ( ) }
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-09-22 18:44:50 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-22 20:42:04 +00:00
borderlandItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const notMine = ( ) => {
return me ? ` AND "userId" <> ${ me . id } ` : ''
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . "weightedVotes" - "Item" . "weightedDownVotes" < 0
AND "Item" . "weightedVotes" - "Item" . "weightedDownVotes" > - $ { ITEM _FILTER _THRESHOLD }
$ { notMine ( ) }
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-09-22 20:42:04 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2022-09-27 21:19:15 +00:00
freebieItems : async ( parent , { cursor } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . freebie
ORDER BY created _at DESC
OFFSET $1
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . offset )
2022-09-27 21:19:15 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2023-05-07 01:25:00 +00:00
getBountiesByUserName : async ( parent , { name , cursor , limit } , { me , models } ) => {
2023-01-26 16:11:55 +00:00
const decodedCursor = decodeCursor ( cursor )
const user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'user not found' , {
argumentName : 'name'
} )
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1
AND "bounty" IS NOT NULL
ORDER BY created _at DESC
OFFSET $2
LIMIT $3 ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , user . id , decodedCursor . offset , limit || LIMIT )
2023-01-26 16:11:55 +00:00
return {
cursor : items . length === ( limit || LIMIT ) ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2023-05-01 20:58:30 +00:00
moreFlatComments : async ( parent , { sub , cursor , name , sort , within } , { me , models } ) => {
2021-06-24 23:56:01 +00:00
const decodedCursor = decodeCursor ( cursor )
2023-05-05 18:06:53 +00:00
// HACK we want to optionally include the subName in the query
// but the query planner doesn't like unused parameters
const subArr = sub ? [ sub ] : [ ]
2021-08-18 22:20:33 +00:00
2021-12-16 23:05:31 +00:00
let comments , user
switch ( sort ) {
2022-08-18 22:05:58 +00:00
case 'recent' :
2023-05-08 20:06:42 +00:00
comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
JOIN "Item" root ON "Item" . "rootId" = root . id
WHERE "Item" . "parentId" IS NOT NULL AND "Item" . created _at <= $1
$ { subClause ( sub , 3 , 'root' ) }
$ { await filterClause ( me , models ) }
ORDER BY "Item" . created _at DESC
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , decodedCursor . time , decodedCursor . offset , ... subArr )
2022-08-18 22:05:58 +00:00
break
2021-12-16 23:05:31 +00:00
case 'user' :
if ( ! name ) {
throw new UserInputError ( 'must supply name' , { argumentName : 'name' } )
}
2021-10-26 20:49:37 +00:00
2021-12-16 23:05:31 +00:00
user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2021-08-18 22:20:33 +00:00
2023-05-08 20:06:42 +00:00
comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "userId" = $1 AND "parentId" IS NOT NULL
AND created _at <= $2
$ { await filterClause ( me , models ) }
ORDER BY created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "Item"."createdAt" DESC'
} , user . id , decodedCursor . time , decodedCursor . offset )
2021-12-16 23:05:31 +00:00
break
case 'top' :
2023-05-08 20:06:42 +00:00
comments = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE "Item" . "parentId" IS NOT NULL AND "Item" . "deletedAt" IS NULL
AND "Item" . created _at <= $1
$ { topClause ( within ) }
$ { await filterClause ( me , models ) }
$ { await topOrderByWeightedSats ( me , models ) }
OFFSET $2
LIMIT $ { LIMIT } ` ,
orderBy : await topOrderByWeightedSats ( me , models )
} , decodedCursor . time , decodedCursor . offset )
2021-12-16 23:05:31 +00:00
break
default :
throw new UserInputError ( 'invalid sort type' , { argumentName : 'sort' } )
}
2021-08-18 22:20:33 +00:00
2021-06-24 23:56:01 +00:00
return {
cursor : comments . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
comments
}
} ,
2023-02-16 22:23:59 +00:00
moreBookmarks : async ( parent , { cursor , name } , { me , models } ) => {
const decodedCursor = decodeCursor ( cursor )
const user = await models . user . findUnique ( { where : { name } } )
if ( ! user ) {
throw new UserInputError ( 'no user has that name' , { argumentName : 'name' } )
}
2023-05-08 20:06:42 +00:00
const items = await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT } , "Bookmark" . created _at as "bookmarkCreatedAt"
FROM "Item"
JOIN "Bookmark" ON "Bookmark" . "itemId" = "Item" . "id" AND "Bookmark" . "userId" = $1
AND "Bookmark" . created _at <= $2
ORDER BY "Bookmark" . created _at DESC
OFFSET $3
LIMIT $ { LIMIT } ` ,
orderBy : 'ORDER BY "bookmarkCreatedAt" DESC'
} , user . id , decodedCursor . time , decodedCursor . offset )
2023-02-16 22:23:59 +00:00
return {
cursor : items . length === LIMIT ? nextCursorEncoded ( decodedCursor ) : null ,
items
}
} ,
2021-09-23 17:42:00 +00:00
item : getItem ,
2023-01-12 18:05:47 +00:00
pageTitleAndUnshorted : async ( parent , { url } , { models } ) => {
const res = { }
2021-08-22 15:25:17 +00:00
try {
const response = await fetch ( ensureProtocol ( url ) , { redirect : 'follow' } )
const html = await response . text ( )
const doc = domino . createWindow ( html ) . document
const metadata = getMetadata ( doc , url , { title : metadataRuleSets . title } )
2023-01-12 18:05:47 +00:00
res . title = metadata ? . title
} catch { }
try {
const unshorted = await uu ( ) . expand ( url )
if ( unshorted ) {
res . unshorted = unshorted
}
} catch { }
return res
2021-10-28 20:49:51 +00:00
} ,
2023-05-07 01:25:00 +00:00
dupes : async ( parent , { url } , { me , models } ) => {
2021-10-28 20:49:51 +00:00
const urlObj = new URL ( ensureProtocol ( url ) )
2023-06-01 00:49:28 +00:00
let uri = urlObj . hostname + '(:[0-9]+)?' + urlObj . pathname
2021-12-20 22:26:22 +00:00
uri = uri . endsWith ( '/' ) ? uri . slice ( 0 , - 1 ) : uri
2023-01-11 22:20:14 +00:00
const parseResult = parse ( urlObj . hostname )
if ( parseResult ? . subdomain ? . length ) {
const { subdomain } = parseResult
uri = uri . replace ( subdomain , '(%)?' )
} else {
uri = ` (%.)? ${ uri } `
}
let similar = ` (http(s)?://)? ${ uri } /? `
2022-03-10 21:44:46 +00:00
const whitelist = [ 'news.ycombinator.com/item' , 'bitcointalk.org/index.php' ]
2023-01-11 22:04:50 +00:00
const youtube = [ 'www.youtube.com' , 'youtube.com' , 'm.youtube.com' , 'youtu.be' ]
2021-12-20 22:26:22 +00:00
if ( whitelist . includes ( uri ) ) {
similar += ` \\ ${ urlObj . search } `
2022-03-10 21:44:46 +00:00
} else if ( youtube . includes ( urlObj . hostname ) ) {
// extract id and create both links
const matches = url . match ( /(https?:\/\/)?((www\.)?(youtube(-nocookie)?|youtube.googleapis)\.com.*(v\/|v=|vi=|vi\/|e\/|embed\/|user\/.*\/u\/\d+\/)|youtu\.be\/)(?<id>[_0-9a-z-]+)/i )
2023-01-24 14:37:33 +00:00
similar = ` (http(s)?://)?((www.|m.)?youtube.com/(watch \\ ?v=|v/|live/) ${ matches ? . groups ? . id } |youtu.be/ ${ matches ? . groups ? . id } )(( \\ ?|&|#)%)? `
2021-12-20 22:26:22 +00:00
} else {
2022-05-18 18:21:24 +00:00
similar += '((\\?|#)%)?'
2021-12-20 22:26:22 +00:00
}
2023-05-08 20:06:42 +00:00
return await itemQueryWithMeta ( {
me ,
models ,
query : `
$ { SELECT }
FROM "Item"
WHERE LOWER ( url ) SIMILAR TO LOWER ( $1 )
ORDER BY created _at DESC
LIMIT 3 `
} , similar )
2021-12-21 21:29:42 +00:00
} ,
2022-09-21 19:57:36 +00:00
comments : async ( parent , { id , sort } , { me , models } ) => {
return comments ( me , models , id , sort )
2022-01-26 15:35:14 +00:00
} ,
2022-09-29 20:42:33 +00:00
auctionPosition : async ( parent , { id , sub , bid } , { models , me } ) => {
2022-10-05 18:55:30 +00:00
const createdAt = id ? ( await getItem ( parent , { id } , { models , me } ) ) . createdAt : new Date ( )
let where
2022-09-29 20:42:33 +00:00
if ( bid > 0 ) {
2022-10-05 18:55:30 +00:00
// if there's a bid
// it's ACTIVE and has a larger bid than ours, or has an equal bid and is older
// count items: (bid > ours.bid OR (bid = ours.bid AND create_at < ours.created_at)) AND status = 'ACTIVE'
where = {
status : 'ACTIVE' ,
OR : [
{ maxBid : { gt : bid } } ,
{ maxBid : bid , createdAt : { lt : createdAt } }
]
}
2022-09-29 20:42:33 +00:00
} else {
2022-10-05 18:55:30 +00:00
// else
// it's an active with a bid gt ours, or its newer than ours and not STOPPED
// count items: ((bid > ours.bid AND status = 'ACTIVE') OR (created_at > ours.created_at AND status <> 'STOPPED'))
where = {
OR : [
{ maxBid : { gt : 0 } , status : 'ACTIVE' } ,
{ createdAt : { gt : createdAt } , status : { not : 'STOPPED' } }
]
}
2022-09-29 20:42:33 +00:00
}
2022-10-05 18:55:30 +00:00
where . subName = sub
2022-02-17 17:23:43 +00:00
if ( id ) {
2022-10-05 18:55:30 +00:00
where . id = { not : Number ( id ) }
2022-02-17 17:23:43 +00:00
}
2022-10-05 18:55:30 +00:00
return await models . item . count ( { where } ) + 1
2021-04-12 18:05:09 +00:00
}
} ,
Mutation : {
2023-02-16 22:23:59 +00:00
bookmarkItem : async ( parent , { id } , { me , models } ) => {
const data = { itemId : Number ( id ) , userId : me . id }
const old = await models . bookmark . findUnique ( { where : { userId _itemId : data } } )
if ( old ) {
await models . bookmark . delete ( { where : { userId _itemId : data } } )
} else await models . bookmark . create ( { data } )
return { id }
} ,
2023-06-01 00:44:06 +00:00
subscribeItem : async ( parent , { id } , { me , models } ) => {
const data = { itemId : Number ( id ) , userId : me . id }
const old = await models . threadSubscription . findUnique ( { where : { userId _itemId : data } } )
if ( old ) {
await models . threadSubscription . delete ( { where : { userId _itemId : data } } )
} else await models . threadSubscription . create ( { data } )
return { id }
} ,
2023-01-12 23:53:09 +00:00
deleteItem : async ( parent , { id } , { me , models } ) => {
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
const data = { deletedAt : new Date ( ) }
if ( old . text ) {
data . text = '*deleted by author*'
}
if ( old . title ) {
data . title = 'deleted by author'
}
if ( old . url ) {
data . url = null
}
if ( old . pollCost ) {
data . pollCost = null
}
return await models . item . update ( { where : { id : Number ( id ) } , data } )
} ,
2022-04-18 22:10:26 +00:00
upsertLink : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
data . url = ensureProtocol ( data . url )
2022-11-15 23:51:00 +00:00
data . url = removeTracking ( data . url )
2021-08-11 20:13:10 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate ( linkSchema , data , models )
2022-04-18 22:10:26 +00:00
if ( id ) {
2022-08-18 18:15:24 +00:00
return await updateItem ( parent , { id , data } , { me , models } )
2022-04-18 22:10:26 +00:00
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2022-04-18 22:10:26 +00:00
upsertDiscussion : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
2021-08-11 20:13:10 +00:00
2023-02-08 19:38:04 +00:00
await ssValidate ( discussionSchema , data , models )
2022-04-18 22:10:26 +00:00
if ( id ) {
2022-08-18 18:15:24 +00:00
return await updateItem ( parent , { id , data } , { me , models } )
2022-04-18 22:10:26 +00:00
} else {
return await createItem ( parent , data , { me , models } )
2021-08-11 20:13:10 +00:00
}
} ,
2023-01-26 16:11:55 +00:00
upsertBounty : async ( parent , args , { me , models } ) => {
const { id , ... data } = args
2023-02-08 19:38:04 +00:00
await ssValidate ( bountySchema , data , models )
2023-01-26 16:11:55 +00:00
if ( id ) {
return await updateItem ( parent , { id , data } , { me , models } )
} else {
return await createItem ( parent , data , { me , models } )
}
} ,
2023-02-08 19:38:04 +00:00
upsertPoll : async ( parent , { id , ... data } , { me , models } ) => {
2023-05-01 20:58:30 +00:00
const { sub , forward , boost , title , text , options } = data
2022-07-30 13:25:46 +00:00
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
2023-02-08 19:38:04 +00:00
const optionCount = id
? await models . pollOption . count ( {
where : {
itemId : Number ( id )
}
} )
: 0
await ssValidate ( pollSchema , data , models , optionCount )
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
2022-07-30 13:25:46 +00:00
2022-08-18 18:15:24 +00:00
if ( id ) {
2023-02-08 19:38:04 +00:00
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
2022-08-18 18:15:24 +00:00
}
const [ item ] = await serialize ( models ,
2023-05-01 20:58:30 +00:00
models . $queryRaw ( ` ${ SELECT } FROM update_poll( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7) AS "Item" ` ,
sub || 'bitcoin' , Number ( id ) , title , text , Number ( boost || 0 ) , options , Number ( fwdUser ? . id ) ) )
2022-08-18 18:15:24 +00:00
2023-02-08 19:38:04 +00:00
await createMentions ( item , models )
item . comments = [ ]
2022-07-30 13:25:46 +00:00
return item
} else {
const [ item ] = await serialize ( models ,
2023-05-01 20:58:30 +00:00
models . $queryRaw ( ` ${ SELECT } FROM create_poll( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, ' ${ ITEM _SPAM _INTERVAL } ') AS "Item" ` ,
sub || 'bitcoin' , title , text , 1 , Number ( boost || 0 ) , Number ( me . id ) , options , Number ( fwdUser ? . id ) ) )
2022-08-18 18:15:24 +00:00
await createMentions ( item , models )
2022-07-30 13:25:46 +00:00
item . comments = [ ]
return item
}
} ,
2023-02-08 19:38:04 +00:00
upsertJob : async ( parent , { id , ... data } , { me , models } ) => {
2022-02-17 17:23:43 +00:00
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in to create job' )
}
2023-02-08 19:38:04 +00:00
const { sub , title , company , location , remote , text , url , maxBid , status , logo } = data
2022-02-17 17:23:43 +00:00
const fullSub = await models . sub . findUnique ( { where : { name : sub } } )
if ( ! fullSub ) {
throw new UserInputError ( 'not a valid sub' , { argumentName : 'sub' } )
}
2023-02-08 19:38:04 +00:00
await ssValidate ( jobSchema , data , models )
const loc = location . toLowerCase ( ) === 'remote' ? undefined : location
2022-02-17 17:23:43 +00:00
2022-09-29 20:42:33 +00:00
let item
2022-02-17 17:23:43 +00:00
if ( id ) {
2022-04-18 22:10:26 +00:00
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
2022-09-29 20:42:33 +00:00
( [ item ] = await serialize ( models ,
models . $queryRaw (
` ${ SELECT } FROM update_job( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, $ 10) AS "Item" ` ,
2023-02-08 19:38:04 +00:00
Number ( id ) , title , url , text , Number ( maxBid ) , company , loc , remote , Number ( logo ) , status ) ) )
2022-09-29 20:42:33 +00:00
} else {
( [ item ] = await serialize ( models ,
models . $queryRaw (
` ${ SELECT } FROM create_job( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9) AS "Item" ` ,
2023-02-08 19:38:04 +00:00
title , url , text , Number ( me . id ) , Number ( maxBid ) , company , loc , remote , Number ( logo ) ) ) )
2022-02-17 17:23:43 +00:00
}
2022-09-29 20:42:33 +00:00
await createMentions ( item , models )
return item
2022-02-17 17:23:43 +00:00
} ,
2023-02-08 19:38:04 +00:00
createComment : async ( parent , data , { me , models } ) => {
await ssValidate ( commentSchema , data )
return await createItem ( parent , data , { me , models } )
2021-04-26 21:55:15 +00:00
} ,
2023-02-08 19:38:04 +00:00
updateComment : async ( parent , { id , ... data } , { me , models } ) => {
await ssValidate ( commentSchema , data )
return await updateItem ( parent , { id , data } , { me , models } )
2021-08-10 22:59:06 +00:00
} ,
2022-07-30 13:25:46 +00:00
pollVote : async ( parent , { id } , { me , models } ) => {
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
await serialize ( models ,
models . $queryRaw ( ` ${ SELECT } FROM poll_vote( $ 1, $ 2) AS "Item" ` ,
Number ( id ) , Number ( me . id ) ) )
return id
} ,
2022-01-20 23:04:12 +00:00
act : async ( parent , { id , sats } , { me , models } ) => {
2021-04-26 21:55:15 +00:00
// need to make sure we are logged in
if ( ! me ) {
2021-05-11 20:29:44 +00:00
throw new AuthenticationError ( 'you must be logged in' )
2021-04-26 21:55:15 +00:00
}
2023-02-08 19:38:04 +00:00
await ssValidate ( amountSchema , { amount : sats } )
2021-04-27 21:30:58 +00:00
2022-01-20 23:04:12 +00:00
// disallow self tips
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 AND "userId" = $2 ` , Number(id), me.id)
if ( item ) {
2023-06-19 18:21:55 +00:00
throw new UserInputError ( 'cannot zap your self' )
2021-09-10 21:13:52 +00:00
}
2022-01-20 23:04:12 +00:00
const [ { item _act : vote } ] = await serialize ( models , models . $queryRaw ` SELECT item_act( ${ Number ( id ) } , ${ me . id } , 'TIP', ${ Number ( sats ) } ) ` )
2021-09-10 21:13:52 +00:00
return {
2022-01-20 23:04:12 +00:00
vote ,
sats
2021-09-10 21:13:52 +00:00
}
2022-09-21 19:57:36 +00:00
} ,
dontLikeThis : async ( parent , { id } , { me , models } ) => {
// need to make sure we are logged in
if ( ! me ) {
throw new AuthenticationError ( 'you must be logged in' )
}
// disallow self down votes
const [ item ] = await models . $queryRaw ( `
$ { SELECT }
FROM "Item"
WHERE id = $1 AND "userId" = $2 ` , Number(id), me.id)
if ( item ) {
throw new UserInputError ( 'cannot downvote your self' )
}
await serialize ( models , models . $queryRaw ` SELECT item_act( ${ Number ( id ) } , ${ me . id } , 'DONT_LIKE_THIS', ${ DONT _LIKE _THIS _COST } ) ` )
return true
2021-04-12 18:05:09 +00:00
}
} ,
Item : {
2022-11-15 20:51:55 +00:00
sats : async ( item , args , { models } ) => {
return msatsToSats ( item . msats )
} ,
commentSats : async ( item , args , { models } ) => {
return msatsToSats ( item . commentMsats )
} ,
2022-09-29 20:42:33 +00:00
isJob : async ( item , args , { models } ) => {
return item . subName === 'jobs'
} ,
2022-02-17 17:23:43 +00:00
sub : async ( item , args , { models } ) => {
if ( ! item . subName ) {
return null
}
return await models . sub . findUnique ( { where : { name : item . subName } } )
} ,
2022-01-07 16:32:31 +00:00
position : async ( item , args , { models } ) => {
if ( ! item . pinId ) {
return null
}
const pin = await models . pin . findUnique ( { where : { id : item . pinId } } )
if ( ! pin ) {
return null
}
return pin . position
} ,
2022-01-13 19:05:43 +00:00
prior : async ( item , args , { models } ) => {
if ( ! item . pinId ) {
return null
}
const prior = await models . item . findFirst ( {
where : {
pinId : item . pinId ,
createdAt : {
lt : item . createdAt
}
} ,
orderBy : {
createdAt : 'desc'
}
} )
if ( ! prior ) {
return null
}
return prior . id
} ,
2022-07-30 13:25:46 +00:00
poll : async ( item , args , { models , me } ) => {
if ( ! item . pollCost ) {
return null
}
const options = await models . $queryRaw `
SELECT "PollOption" . id , option , count ( "PollVote" . "userId" ) as count ,
coalesce ( bool _or ( "PollVote" . "userId" = $ { me ? . id } ) , 'f' ) as "meVoted"
FROM "PollOption"
LEFT JOIN "PollVote" on "PollVote" . "pollOptionId" = "PollOption" . id
WHERE "PollOption" . "itemId" = $ { item . id }
GROUP BY "PollOption" . id
ORDER BY "PollOption" . id ASC
`
const poll = { }
poll . options = options
poll . meVoted = options . some ( o => o . meVoted )
poll . count = options . reduce ( ( t , o ) => t + o . count , 0 )
return poll
} ,
2023-05-06 23:17:47 +00:00
user : async ( item , args , { models } ) => {
if ( item . user ) {
return item . user
}
return await models . user . findUnique ( { where : { id : item . userId } } )
} ,
2022-04-19 18:32:39 +00:00
fwdUser : async ( item , args , { models } ) => {
if ( ! item . fwdUserId ) {
return null
}
return await models . user . findUnique ( { where : { id : item . fwdUserId } } )
} ,
2022-09-21 19:57:36 +00:00
comments : async ( item , args , { me , models } ) => {
2022-03-31 18:05:11 +00:00
if ( item . comments ) {
return item . comments
}
2023-05-07 01:25:00 +00:00
return comments ( me , models , item . id , item . pinId ? 'recent' : 'hot' )
2022-03-04 18:05:16 +00:00
} ,
2022-10-28 15:58:31 +00:00
wvotes : async ( item ) => {
return item . weightedVotes - item . weightedDownVotes
} ,
2021-12-05 17:37:55 +00:00
meSats : async ( item , args , { me , models } ) => {
2021-09-10 21:13:52 +00:00
if ( ! me ) return 0
2023-05-07 01:25:00 +00:00
if ( typeof item . meMsats === 'number' ) return msatsToSats ( item . meMsats )
2021-09-10 21:13:52 +00:00
2022-11-15 20:51:55 +00:00
const { sum : { msats } } = await models . itemAct . aggregate ( {
2021-09-10 21:13:52 +00:00
sum : {
2022-11-15 20:51:55 +00:00
msats : true
2021-09-10 21:13:52 +00:00
} ,
where : {
2022-01-27 19:18:48 +00:00
itemId : Number ( item . id ) ,
2021-09-10 21:13:52 +00:00
userId : me . id ,
2021-12-05 17:37:55 +00:00
OR : [
{
act : 'TIP'
} ,
{
2022-11-23 18:12:09 +00:00
act : 'FEE'
2021-12-05 17:37:55 +00:00
}
]
2021-09-10 21:13:52 +00:00
}
} )
2022-11-15 20:51:55 +00:00
return ( msats && msatsToSats ( msats ) ) || 0
2021-09-10 21:13:52 +00:00
} ,
2022-09-21 19:57:36 +00:00
meDontLike : async ( item , args , { me , models } ) => {
if ( ! me ) return false
2023-05-07 01:25:00 +00:00
if ( typeof item . meDontLike === 'boolean' ) return item . meDontLike
2022-09-21 19:57:36 +00:00
const dontLike = await models . itemAct . findFirst ( {
where : {
itemId : Number ( item . id ) ,
userId : me . id ,
act : 'DONT_LIKE_THIS'
}
} )
return ! ! dontLike
} ,
2023-02-16 22:23:59 +00:00
meBookmark : async ( item , args , { me , models } ) => {
if ( ! me ) return false
2023-05-07 01:25:00 +00:00
if ( typeof item . meBookmark === 'boolean' ) return item . meBookmark
2023-02-16 22:23:59 +00:00
const bookmark = await models . bookmark . findUnique ( {
where : {
userId _itemId : {
itemId : Number ( item . id ) ,
userId : me . id
}
}
} )
return ! ! bookmark
} ,
2023-06-01 00:44:06 +00:00
meSubscription : async ( item , args , { me , models } ) => {
if ( ! me ) return false
if ( typeof item . meSubscription === 'boolean' ) return item . meSubscription
const subscription = await models . threadSubscription . findUnique ( {
where : {
userId _itemId : {
itemId : Number ( item . id ) ,
userId : me . id
}
}
} )
return ! ! subscription
} ,
2022-09-22 18:44:50 +00:00
outlawed : async ( item , args , { me , models } ) => {
if ( me && Number ( item . userId ) === Number ( me . id ) ) {
return false
}
return item . weightedVotes - item . weightedDownVotes <= - ITEM _FILTER _THRESHOLD
} ,
2021-12-05 17:37:55 +00:00
mine : async ( item , args , { me , models } ) => {
return me ? . id === item . userId
} ,
2021-07-08 00:15:27 +00:00
root : async ( item , args , { models } ) => {
2023-01-26 19:37:51 +00:00
if ( ! item . rootId ) {
2021-07-08 00:15:27 +00:00
return null
}
2023-02-04 00:08:08 +00:00
if ( item . root ) {
return item . root
}
2023-01-26 19:09:57 +00:00
return await models . item . findUnique ( { where : { id : item . rootId } } )
2021-07-08 00:15:27 +00:00
} ,
parent : async ( item , args , { models } ) => {
if ( ! item . parentId ) {
return null
}
return await models . item . findUnique ( { where : { id : item . parentId } } )
2023-01-22 20:17:50 +00:00
} ,
parentOtsHash : async ( item , args , { models } ) => {
if ( ! item . parentId ) {
return null
}
const parent = await models . item . findUnique ( { where : { id : item . parentId } } )
return parent . otsHash
2021-04-26 21:55:15 +00:00
}
}
}
2021-08-18 22:20:33 +00:00
const namePattern = /\B@[\w_]+/gi
2021-09-23 17:42:00 +00:00
export const createMentions = async ( item , models ) => {
2021-08-18 22:20:33 +00:00
// if we miss a mention, in the rare circumstance there's some kind of
// failure, it's not a big deal so we don't do it transactionally
// ideally, we probably would
if ( ! item . text ) {
return
}
try {
2021-08-19 19:53:11 +00:00
const mentions = item . text . match ( namePattern ) ? . map ( m => m . slice ( 1 ) )
if ( mentions ? . length > 0 ) {
2021-08-18 22:20:33 +00:00
const users = await models . user . findMany ( {
where : {
name : { in : mentions }
}
} )
users . forEach ( async user => {
const data = {
itemId : item . id ,
userId : user . id
}
await models . mention . upsert ( {
where : {
itemId _userId : data
} ,
update : data ,
create : data
} )
} )
}
} catch ( e ) {
console . log ( 'mention failure' , e )
}
}
2023-05-01 20:58:30 +00:00
export const updateItem = async ( parent , { id , data : { sub , title , url , text , boost , forward , bounty , parentId } } , { me , models } ) => {
2022-04-18 22:10:26 +00:00
// update iff this item belongs to me
const old = await models . item . findUnique ( { where : { id : Number ( id ) } } )
if ( Number ( old . userId ) !== Number ( me ? . id ) ) {
throw new AuthenticationError ( 'item does not belong to you' )
}
2022-08-18 18:15:24 +00:00
// if it's not the FAQ, not their bio, and older than 10 minutes
const user = await models . user . findUnique ( { where : { id : me . id } } )
2022-10-23 15:43:39 +00:00
if ( ! [ 349 , 76894 , 78763 , 81862 ] . includes ( old . id ) && user . bioId !== id && Date . now ( ) > new Date ( old . createdAt ) . getTime ( ) + 10 * 60000 ) {
2022-04-18 22:10:26 +00:00
throw new UserInputError ( 'item can no longer be editted' )
}
2022-08-18 18:15:24 +00:00
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
2022-08-10 15:06:31 +00:00
}
2022-08-27 02:57:41 +00:00
if ( ! old . parentId && title . length > MAX _TITLE _LENGTH ) {
2022-08-26 23:31:51 +00:00
throw new UserInputError ( 'title too long' )
}
2022-08-18 18:15:24 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
const [ item ] = await serialize ( models ,
models . $queryRaw (
2023-05-01 20:58:30 +00:00
` ${ SELECT } FROM update_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8) AS "Item" ` ,
old . parentId ? null : sub || 'bitcoin' , Number ( id ) , title , url , text ,
Number ( boost || 0 ) , bounty ? Number ( bounty ) : null , Number ( fwdUser ? . id ) ) )
2021-08-18 22:20:33 +00:00
await createMentions ( item , models )
return item
}
2023-05-01 20:58:30 +00:00
const createItem = async ( parent , { sub , title , url , text , boost , forward , bounty , parentId } , { me , models } ) => {
2021-04-26 21:55:15 +00:00
if ( ! me ) {
2021-05-11 20:29:44 +00:00
throw new AuthenticationError ( 'you must be logged in' )
2021-04-26 21:55:15 +00:00
}
2022-03-09 19:44:50 +00:00
if ( boost && boost < BOOST _MIN ) {
throw new UserInputError ( ` boost must be at least ${ BOOST _MIN } ` , { argumentName : 'boost' } )
2021-09-11 21:52:19 +00:00
}
2022-08-27 01:27:57 +00:00
if ( ! parentId && title . length > MAX _TITLE _LENGTH ) {
2022-08-26 23:31:51 +00:00
throw new UserInputError ( 'title too long' )
}
2022-04-19 18:32:39 +00:00
let fwdUser
if ( forward ) {
fwdUser = await models . user . findUnique ( { where : { name : forward } } )
if ( ! fwdUser ) {
throw new UserInputError ( 'forward user does not exist' , { argumentName : 'forward' } )
}
}
2023-01-26 16:11:55 +00:00
const [ item ] = await serialize (
models ,
2022-08-10 15:06:31 +00:00
models . $queryRaw (
2023-05-01 20:58:30 +00:00
` ${ SELECT } FROM create_item( $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, ' ${ ITEM _SPAM _INTERVAL } ') AS "Item" ` ,
parentId ? null : sub || 'bitcoin' ,
2023-01-26 16:11:55 +00:00
title ,
url ,
text ,
Number ( boost || 0 ) ,
bounty ? Number ( bounty ) : null ,
Number ( parentId ) ,
Number ( me . id ) ,
Number ( fwdUser ? . id ) ) )
2021-08-18 22:20:33 +00:00
await createMentions ( item , models )
2021-05-20 01:09:32 +00:00
item . comments = [ ]
return item
2021-04-26 21:55:15 +00:00
}
// we have to do our own query because ltree is unsupported
2021-09-23 17:42:00 +00:00
export const SELECT =
2023-05-08 20:06:42 +00:00
` SELECT "Item".id, "Item".created_at, "Item".created_at as "createdAt", "Item".updated_at,
"Item" . updated _at as "updatedAt" , "Item" . title , "Item" . text , "Item" . url , "Item" . "bounty" ,
"Item" . "userId" , "Item" . "fwdUserId" , "Item" . "parentId" , "Item" . "pinId" , "Item" . "maxBid" ,
"Item" . "rootId" , "Item" . upvotes , "Item" . company , "Item" . location , "Item" . remote , "Item" . "deletedAt" ,
"Item" . "subName" , "Item" . status , "Item" . "uploadId" , "Item" . "pollCost" , "Item" . boost , "Item" . msats ,
"Item" . ncomments , "Item" . "commentMsats" , "Item" . "lastCommentAt" , "Item" . "weightedVotes" ,
"Item" . "weightedDownVotes" , "Item" . freebie , "Item" . "otsHash" , "Item" . "bountyPaidTo" ,
2023-05-09 18:52:35 +00:00
ltree2text ( "Item" . "path" ) AS "path" , "Item" . "weightedComments" `
2021-04-27 21:30:58 +00:00
2022-09-21 19:57:36 +00:00
async function topOrderByWeightedSats ( me , models ) {
return ` ORDER BY ${ await orderByNumerator ( me , models ) } DESC NULLS LAST, "Item".id DESC `
}