WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 92 lines 3.0 kB view raw
1import type { DbOrTransaction } from "@atbb/db"; 2import { modActions, posts } from "@atbb/db"; 3import type { Logger } from "@atbb/logger"; 4import { and, eq, gt, isNull, or } from "drizzle-orm"; 5import { isProgrammingError } from "./errors.js"; 6 7/** 8 * Encapsulates ban enforcement logic for the firehose indexer. 9 * 10 * Used by the Indexer to: 11 * - Check ban status before indexing posts (fail closed) 12 * - Soft-delete existing posts when a ban is applied 13 * - Restore posts when a ban is lifted 14 */ 15export class BanEnforcer { 16 constructor(private db: DbOrTransaction, private logger: Logger) {} 17 18 /** 19 * Returns true if the DID has an active (non-expired) ban. 20 * Fails closed: returns true if the DB query throws. 21 */ 22 async isBanned(did: string): Promise<boolean> { 23 try { 24 const now = new Date(); 25 const result = await this.db 26 .select({ id: modActions.id }) 27 .from(modActions) 28 .where( 29 and( 30 eq(modActions.subjectDid, did), 31 eq(modActions.action, "space.atbb.modAction.ban"), 32 or(isNull(modActions.expiresAt), gt(modActions.expiresAt, now)) 33 ) 34 ) 35 .limit(1); 36 37 return result.length > 0; 38 } catch (error) { 39 if (isProgrammingError(error)) throw error; 40 this.logger.error( 41 "Failed to check ban status - denying indexing (fail closed)", 42 { 43 did, 44 error: error instanceof Error ? error.message : String(error), 45 } 46 ); 47 return true; // fail closed 48 } 49 } 50 51 /** 52 * Hides all posts for the given DID from public view. 53 * Called when a ban mod action is indexed. 54 * Uses bannedByMod column (not deleted) so user-initiated deletes are preserved. 55 */ 56 async applyBan(subjectDid: string, dbOrTx: DbOrTransaction = this.db): Promise<void> { 57 try { 58 await dbOrTx 59 .update(posts) 60 .set({ bannedByMod: true }) 61 .where(eq(posts.did, subjectDid)); 62 this.logger.info("Applied ban: hid all posts via bannedByMod", { subjectDid }); 63 } catch (error) { 64 this.logger.error("Failed to apply ban - posts may not be hidden", { 65 subjectDid, 66 error: error instanceof Error ? error.message : String(error), 67 }); 68 throw error; 69 } 70 } 71 72 /** 73 * Unhides all mod-hidden posts for the given DID. 74 * Called when a ban mod action record is deleted (unban). 75 * Only touches bannedByMod; user-initiated deletes (deleted=true) are preserved. 76 */ 77 async liftBan(subjectDid: string, dbOrTx: DbOrTransaction = this.db): Promise<void> { 78 try { 79 await dbOrTx 80 .update(posts) 81 .set({ bannedByMod: false }) 82 .where(eq(posts.did, subjectDid)); 83 this.logger.info("Lifted ban: unhid all posts via bannedByMod", { subjectDid }); 84 } catch (error) { 85 this.logger.error("Failed to lift ban - posts may not be restored", { 86 subjectDid, 87 error: error instanceof Error ? error.message : String(error), 88 }); 89 throw error; 90 } 91 } 92}