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
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}