FeedFilter.ts raw
1 import { Event } from 'nostr-tools'
2 import { ContentFilter, FilterContext, FilterResult, FilterReason } from './ContentFilter'
3
4 /**
5 * Interface for checking if a pubkey is muted
6 */
7 export interface MuteChecker {
8 isMuted(pubkey: string): boolean
9 getMutedPubkeys(): Set<string>
10 }
11
12 /**
13 * Interface for checking if a pubkey is trusted
14 */
15 export interface TrustChecker {
16 isTrusted(pubkey: string): boolean
17 getTrustedPubkeys(): Set<string>
18 }
19
20 /**
21 * Interface for checking if an event is deleted
22 */
23 export interface DeletionChecker {
24 isDeleted(eventId: string): boolean
25 getDeletedEventIds(): Set<string>
26 }
27
28 /**
29 * Interface for checking if an event is pinned
30 */
31 export interface PinnedChecker {
32 isPinned(eventId: string): boolean
33 getPinnedEventIds(): Set<string>
34 }
35
36 /**
37 * Result of filtering with the original event
38 */
39 export interface FilteredEvent {
40 event: Event
41 result: FilterResult
42 }
43
44 /**
45 * Statistics about filtering results
46 */
47 export interface FilterStats {
48 total: number
49 shown: number
50 hidden: number
51 byReason: Map<FilterReason, number>
52 }
53
54 /**
55 * FeedFilter Domain Service
56 *
57 * Coordinates filtering of timeline events using ContentFilter and various
58 * checkers (mute, trust, deletion). This is a domain service because it
59 * requires coordination between multiple domain concepts.
60 *
61 * Usage:
62 * - Inject checkers that provide mute/trust/deletion data
63 * - Call filterEvents() to filter a batch of events
64 * - Call shouldDisplay() to check a single event
65 */
66 export class FeedFilter {
67 constructor(
68 private readonly muteChecker: MuteChecker,
69 private readonly trustChecker?: TrustChecker,
70 private readonly deletionChecker?: DeletionChecker,
71 private readonly pinnedChecker?: PinnedChecker,
72 private readonly currentUserPubkey?: string
73 ) {}
74
75 /**
76 * Create a filter context from the current checker state
77 */
78 private buildContext(): FilterContext {
79 return {
80 mutedPubkeys: this.muteChecker.getMutedPubkeys(),
81 trustedPubkeys: this.trustChecker?.getTrustedPubkeys(),
82 deletedEventIds: this.deletionChecker?.getDeletedEventIds(),
83 pinnedEventIds: this.pinnedChecker?.getPinnedEventIds(),
84 currentUserPubkey: this.currentUserPubkey
85 }
86 }
87
88 /**
89 * Filter a batch of events, returning only those that should be shown
90 */
91 filterEvents(events: Event[], filter: ContentFilter): Event[] {
92 const context = this.buildContext()
93 return events.filter((event) => filter.shouldShow(event, context).shouldShow)
94 }
95
96 /**
97 * Filter events and return both shown and hidden with reasons
98 */
99 filterEventsWithDetails(events: Event[], filter: ContentFilter): FilteredEvent[] {
100 const context = this.buildContext()
101 return events.map((event) => ({
102 event,
103 result: filter.shouldShow(event, context)
104 }))
105 }
106
107 /**
108 * Get only events that should be shown with their filter results
109 */
110 getShownEvents(events: Event[], filter: ContentFilter): FilteredEvent[] {
111 return this.filterEventsWithDetails(events, filter).filter((fe) => fe.result.shouldShow)
112 }
113
114 /**
115 * Get only events that were hidden with their reasons
116 */
117 getHiddenEvents(events: Event[], filter: ContentFilter): FilteredEvent[] {
118 return this.filterEventsWithDetails(events, filter).filter((fe) => !fe.result.shouldShow)
119 }
120
121 /**
122 * Check if a single event should be displayed
123 */
124 shouldDisplay(event: Event, filter: ContentFilter): FilterResult {
125 const context = this.buildContext()
126 return filter.shouldShow(event, context)
127 }
128
129 /**
130 * Get statistics about filtering a batch of events
131 */
132 getFilterStats(events: Event[], filter: ContentFilter): FilterStats {
133 const results = this.filterEventsWithDetails(events, filter)
134 const byReason = new Map<FilterReason, number>()
135
136 let shown = 0
137 let hidden = 0
138
139 for (const { result } of results) {
140 if (result.shouldShow) {
141 shown++
142 } else {
143 hidden++
144 if (result.reason) {
145 byReason.set(result.reason, (byReason.get(result.reason) ?? 0) + 1)
146 }
147 }
148 }
149
150 return {
151 total: events.length,
152 shown,
153 hidden,
154 byReason
155 }
156 }
157
158 /**
159 * Create a new FeedFilter with an updated mute checker
160 */
161 withMuteChecker(muteChecker: MuteChecker): FeedFilter {
162 return new FeedFilter(
163 muteChecker,
164 this.trustChecker,
165 this.deletionChecker,
166 this.pinnedChecker,
167 this.currentUserPubkey
168 )
169 }
170
171 /**
172 * Create a new FeedFilter with an updated trust checker
173 */
174 withTrustChecker(trustChecker: TrustChecker): FeedFilter {
175 return new FeedFilter(
176 this.muteChecker,
177 trustChecker,
178 this.deletionChecker,
179 this.pinnedChecker,
180 this.currentUserPubkey
181 )
182 }
183
184 /**
185 * Create a new FeedFilter with an updated deletion checker
186 */
187 withDeletionChecker(deletionChecker: DeletionChecker): FeedFilter {
188 return new FeedFilter(
189 this.muteChecker,
190 this.trustChecker,
191 deletionChecker,
192 this.pinnedChecker,
193 this.currentUserPubkey
194 )
195 }
196
197 /**
198 * Create a new FeedFilter with an updated pinned checker
199 */
200 withPinnedChecker(pinnedChecker: PinnedChecker): FeedFilter {
201 return new FeedFilter(
202 this.muteChecker,
203 this.trustChecker,
204 this.deletionChecker,
205 pinnedChecker,
206 this.currentUserPubkey
207 )
208 }
209
210 /**
211 * Create a new FeedFilter with an updated current user
212 */
213 withCurrentUser(pubkey: string): FeedFilter {
214 return new FeedFilter(
215 this.muteChecker,
216 this.trustChecker,
217 this.deletionChecker,
218 this.pinnedChecker,
219 pubkey
220 )
221 }
222 }
223
224 /**
225 * Simple in-memory implementation of MuteChecker for testing
226 */
227 export class SimpleMuteChecker implements MuteChecker {
228 constructor(private readonly mutedPubkeys: Set<string> = new Set()) {}
229
230 isMuted(pubkey: string): boolean {
231 return this.mutedPubkeys.has(pubkey)
232 }
233
234 getMutedPubkeys(): Set<string> {
235 return this.mutedPubkeys
236 }
237 }
238
239 /**
240 * Simple in-memory implementation of TrustChecker for testing
241 */
242 export class SimpleTrustChecker implements TrustChecker {
243 constructor(private readonly trustedPubkeys: Set<string> = new Set()) {}
244
245 isTrusted(pubkey: string): boolean {
246 return this.trustedPubkeys.has(pubkey)
247 }
248
249 getTrustedPubkeys(): Set<string> {
250 return this.trustedPubkeys
251 }
252 }
253
254 /**
255 * Simple in-memory implementation of DeletionChecker for testing
256 */
257 export class SimpleDeletionChecker implements DeletionChecker {
258 constructor(private readonly deletedEventIds: Set<string> = new Set()) {}
259
260 isDeleted(eventId: string): boolean {
261 return this.deletedEventIds.has(eventId)
262 }
263
264 getDeletedEventIds(): Set<string> {
265 return this.deletedEventIds
266 }
267 }
268
269 /**
270 * Simple in-memory implementation of PinnedChecker for testing
271 */
272 export class SimplePinnedChecker implements PinnedChecker {
273 constructor(private readonly pinnedEventIds: Set<string> = new Set()) {}
274
275 isPinned(eventId: string): boolean {
276 return this.pinnedEventIds.has(eventId)
277 }
278
279 getPinnedEventIds(): Set<string> {
280 return this.pinnedEventIds
281 }
282 }
283