import { sql } from 'kysely' import { GateRecord, PostRecord, PostReplyRef } from '../../views/types.js' import { parseThreadGate } from '../../views/util.js' import { DatabaseSchema } from './db/database-schema.js' import { valuesList } from './db/util.js' export const getDescendentsQb = ( db: DatabaseSchema, opts: { uri: string depth: number // required, protects against cycles }, ) => { const { uri, depth } = opts const query = db.withRecursive('descendent(uri, depth)', (cte) => { return cte .selectFrom('post') .select(['post.uri as uri', sql`1`.as('depth')]) .where(sql`1`, '<=', depth) .where('replyParent', '=', uri) .unionAll( cte .selectFrom('post') .innerJoin('descendent', 'descendent.uri', 'post.replyParent') .where('descendent.depth', '<', depth) .select([ 'post.uri as uri', sql`descendent.depth + 1`.as('depth'), ]), ) }) return query } export const getAncestorsAndSelfQb = ( db: DatabaseSchema, opts: { uri: string parentHeight: number // required, protects against cycles }, ) => { const { uri, parentHeight } = opts const query = db.withRecursive( 'ancestor(uri, ancestorUri, height)', (cte) => { return cte .selectFrom('post') .select([ 'post.uri as uri', 'post.replyParent as ancestorUri', sql`0`.as('height'), ]) .where('uri', '=', uri) .unionAll( cte .selectFrom('post') .innerJoin('ancestor', 'ancestor.ancestorUri', 'post.uri') .where('ancestor.height', '<', parentHeight) .select([ 'post.uri as uri', 'post.replyParent as ancestorUri', sql`ancestor.height + 1`.as('height'), ]), ) }, ) return query } export const invalidReplyRoot = ( reply: PostReplyRef, parent: { record: PostRecord invalidReplyRoot: boolean | null }, ) => { const replyRoot = reply.root.uri const replyParent = reply.parent.uri // if parent is not a valid reply, transitively this is not a valid one either if (parent.invalidReplyRoot) { return true } // replying to root post: ensure the root looks correct if (replyParent === replyRoot) { return !!parent.record.reply } // replying to a reply: ensure the parent is a reply for the same root post return parent.record.reply?.root.uri !== replyRoot } export const violatesThreadGate = async ( db: DatabaseSchema, replierDid: string, ownerDid: string, rootPost: PostRecord | null, gate: GateRecord | null, ) => { const { canReply, allowFollower, allowFollowing, allowListUris = [], } = parseThreadGate(replierDid, ownerDid, rootPost, gate) if (canReply) { return false } if (!allowFollower && !allowFollowing && !allowListUris?.length) { return true } const { ref } = db.dynamic const nullResult = sql`${null}` const check = await db .selectFrom(valuesList([replierDid]).as(sql`subject (did)`)) .select([ allowFollower ? db .selectFrom('follow') .where('subjectDid', '=', ownerDid) .whereRef('creator', '=', ref('subject.did')) .select('subjectDid') .as('isFollower') : nullResult.as('isFollower'), allowFollowing ? db .selectFrom('follow') .where('creator', '=', ownerDid) .whereRef('subjectDid', '=', ref('subject.did')) .select('creator') .as('isFollowed') : nullResult.as('isFollowed'), allowListUris.length ? db .selectFrom('list_item') .where('list_item.listUri', 'in', allowListUris) .whereRef('list_item.subjectDid', '=', ref('subject.did')) .limit(1) .select('listUri') .as('isInList') : nullResult.as('isInList'), ]) .executeTakeFirst() if (allowFollowing && check?.isFollowed) { return false } else if (allowFollower && check?.isFollower) { return false } else if (allowListUris.length && check?.isInList) { return false } return true } // @NOTE: This type is not complete with all supported options. // Only the ones that we needed to apply custom logic on are currently present. export type PostSearchQuery = { q: string author: string | undefined } export const parsePostSearchQuery = ( qParam: string, params?: { author?: string }, ): PostSearchQuery => { // Accept individual params, but give preference to options embedded in `q`. let author = params?.author const parts: string[] = [] let curr = '' let quoted = false for (const c of qParam) { if (c === ' ' && !quoted) { if (curr.trim()) parts.push(curr) curr = '' continue } if (c === '"') { quoted = !quoted } curr += c } if (curr.trim()) parts.push(curr) const qParts: string[] = [] for (const p of parts) { const tokens = p.split(':') if (tokens[0] === 'did') { author = p } else if (tokens[0] === 'author' || tokens[0] === 'from') { author = tokens[1] } else { qParts.push(p) } } return { q: qParts.join(' '), author, } }