Barazo AppView backend
barazo.forum
1import { sql } from 'drizzle-orm'
2import type { Database } from '../db/index.js'
3import type { Logger } from '../lib/logger.js'
4import { interactionGraph } from '../db/schema/interaction-graph.js'
5import { replies } from '../db/schema/replies.js'
6
7// ---------------------------------------------------------------------------
8// Types
9// ---------------------------------------------------------------------------
10
11export interface InteractionGraphService {
12 recordReply(replierDid: string, topicAuthorDid: string, communityId: string): Promise<void>
13 recordReaction(reactorDid: string, contentAuthorDid: string, communityId: string): Promise<void>
14 recordCoParticipation(topicUri: string, communityId: string): Promise<void>
15}
16
17// ---------------------------------------------------------------------------
18// Constants
19// ---------------------------------------------------------------------------
20
21const MAX_COPARTICIPATION_AUTHORS = 50
22
23// ---------------------------------------------------------------------------
24// Factory
25// ---------------------------------------------------------------------------
26
27export function createInteractionGraphService(
28 db: Database,
29 logger: Logger
30): InteractionGraphService {
31 async function upsertInteraction(
32 sourceDid: string,
33 targetDid: string,
34 communityId: string,
35 interactionType: 'reply' | 'reaction' | 'topic_coparticipation'
36 ): Promise<void> {
37 // Skip self-interaction
38 if (sourceDid === targetDid) return
39
40 await db
41 .insert(interactionGraph)
42 .values({
43 sourceDid,
44 targetDid,
45 communityId,
46 interactionType,
47 weight: 1,
48 firstInteractionAt: new Date(),
49 lastInteractionAt: new Date(),
50 })
51 .onConflictDoUpdate({
52 target: [
53 interactionGraph.sourceDid,
54 interactionGraph.targetDid,
55 interactionGraph.communityId,
56 interactionGraph.interactionType,
57 ],
58 set: {
59 weight: sql`${interactionGraph.weight} + 1`,
60 lastInteractionAt: new Date(),
61 },
62 })
63 }
64
65 async function recordReply(
66 replierDid: string,
67 topicAuthorDid: string,
68 communityId: string
69 ): Promise<void> {
70 await upsertInteraction(replierDid, topicAuthorDid, communityId, 'reply')
71 logger.debug({ replierDid, topicAuthorDid, communityId }, 'Recorded reply interaction')
72 }
73
74 async function recordReaction(
75 reactorDid: string,
76 contentAuthorDid: string,
77 communityId: string
78 ): Promise<void> {
79 await upsertInteraction(reactorDid, contentAuthorDid, communityId, 'reaction')
80 logger.debug({ reactorDid, contentAuthorDid, communityId }, 'Recorded reaction interaction')
81 }
82
83 async function recordCoParticipation(topicUri: string, communityId: string): Promise<void> {
84 // Get unique reply authors for the topic
85 const authorRows = await db
86 .select({ authorDid: replies.authorDid })
87 .from(replies)
88 .where(sql`${replies.rootUri} = ${topicUri}`)
89
90 // Deduplicate
91 const uniqueAuthors = [...new Set(authorRows.map((r) => r.authorDid))]
92
93 // Skip if too many authors or not enough for pairs
94 if (uniqueAuthors.length > MAX_COPARTICIPATION_AUTHORS || uniqueAuthors.length < 2) {
95 if (uniqueAuthors.length > MAX_COPARTICIPATION_AUTHORS) {
96 logger.debug(
97 { topicUri, authorCount: uniqueAuthors.length },
98 'Skipping co-participation: too many authors'
99 )
100 }
101 return
102 }
103
104 // Create pairwise interactions
105 for (let i = 0; i < uniqueAuthors.length; i++) {
106 const authorA = uniqueAuthors[i]
107 if (!authorA) continue
108 for (let j = i + 1; j < uniqueAuthors.length; j++) {
109 const authorB = uniqueAuthors[j]
110 if (!authorB) continue
111 await upsertInteraction(authorA, authorB, communityId, 'topic_coparticipation')
112 }
113 }
114
115 logger.debug(
116 { topicUri, authorCount: uniqueAuthors.length, communityId },
117 'Recorded co-participation interactions'
118 )
119 }
120
121 return { recordReply, recordReaction, recordCoParticipation }
122}