Skip to content

Commit 375bc28

Browse files
author
ComputelessComputer
committed
Respect domain exclusions across reads
Apply current domain exclusions when loading, searching, and preparing activity data, drop stale journals whose source events are no longer visible, and add regression coverage for historical loads and prepared activity refreshes.
1 parent 665efcc commit 375bc28

3 files changed

Lines changed: 200 additions & 13 deletions

File tree

Sources/OpenbirdApp/AppModel.swift

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ final class AppModel: ObservableObject {
9393
private let store: OpenbirdStore
9494
private let currentActivityContextService = CurrentActivityContextService()
9595
private let installedApplicationService = InstalledApplicationService()
96+
private let exclusionEngine = ExclusionEngine()
9697
private let journalGenerator: JournalGenerator
9798
private let retrievalService: RetrievalService
9899
private let chatService: ChatService
@@ -446,6 +447,13 @@ final class AppModel: ObservableObject {
446447
}
447448
}
448449

450+
nonisolated static func journalMatchesVisibleEvents(
451+
_ journal: DailyJournal,
452+
visibleEventIDs: Set<String>
453+
) -> Bool {
454+
Set(journal.sections.flatMap(\.sourceEventIDs)).isSubset(of: visibleEventIDs)
455+
}
456+
449457
nonisolated static func updateStatusText(
450458
appVersionAvailable: Bool,
451459
isInstallingUpdate: Bool,
@@ -543,15 +551,18 @@ final class AppModel: ObservableObject {
543551
title: "Reading captured activity",
544552
detail: "Querying the local timeline database for raw events recorded on the selected day."
545553
)
546-
let loadedRawEvents = try await store.loadActivityEvents(in: dayRange, includeExcluded: true)
554+
let loadedRawEvents = try await store.loadActivityEvents(in: dayRange)
547555
await store.prepareActivityEventsInBackground(for: requestedDay)
548556
dayLoadStatus = Self.makeDayLoadStatus(
549557
step: 3,
550558
totalSteps: 5,
551559
title: "Loading saved summary",
552560
detail: "Checking whether Openbird already has a journal summary cached for this day."
553561
)
554-
let loadedJournal = try await store.loadJournal(for: day)
562+
let visibleEventIDs = Set(loadedRawEvents.map(\.id))
563+
let loadedJournal = try await store.loadJournal(for: day).flatMap { journal in
564+
Self.journalMatchesVisibleEvents(journal, visibleEventIDs: visibleEventIDs) ? journal : nil
565+
}
555566
let totalDayLoadSteps = 5
556567

557568
dayLoadStatus = Self.makeDayLoadStatus(
@@ -1141,7 +1152,7 @@ final class AppModel: ObservableObject {
11411152

11421153
private func excludableDomainAction(for context: CurrentActivityContext?) -> StatusMenuExclusionState.Action? {
11431154
guard let domain = context?.domain,
1144-
hasExclusion(kind: .domain, pattern: domain) == false
1155+
hasDomainExclusion(matching: domain) == false
11451156
else {
11461157
return nil
11471158
}
@@ -1154,10 +1165,18 @@ final class AppModel: ObservableObject {
11541165

11551166
private func hasExclusion(kind: ExclusionKind, pattern: String) -> Bool {
11561167
exclusions.contains {
1157-
$0.kind == kind && $0.pattern.caseInsensitiveCompare(pattern) == .orderedSame
1168+
$0.isEnabled && $0.kind == kind && $0.pattern.caseInsensitiveCompare(pattern) == .orderedSame
11581169
}
11591170
}
11601171

1172+
private func hasDomainExclusion(matching domain: String) -> Bool {
1173+
exclusionEngine.isExcluded(
1174+
bundleID: "",
1175+
url: "https://\(domain)",
1176+
rules: exclusions.filter { $0.kind == .domain && $0.isEnabled }
1177+
)
1178+
}
1179+
11611180
private func loadCurrentSettings() async throws -> AppSettings {
11621181
var settings = try await store.loadSettings()
11631182
if settings.normalizeCapturePause(sessionID: collectorOwnerID) {
@@ -1411,8 +1430,11 @@ final class AppModel: ObservableObject {
14111430
lastAutomaticSelectedDayRefreshAt = now
14121431

14131432
do {
1414-
let loadedRawEvents = try await store.loadActivityEvents(in: dayRange, includeExcluded: true)
1415-
let loadedJournal = try await store.loadJournal(for: requestedDayString)
1433+
let loadedRawEvents = try await store.loadActivityEvents(in: dayRange)
1434+
let visibleEventIDs = Set(loadedRawEvents.map(\.id))
1435+
let loadedJournal = try await store.loadJournal(for: requestedDayString).flatMap { journal in
1436+
Self.journalMatchesVisibleEvents(journal, visibleEventIDs: visibleEventIDs) ? journal : nil
1437+
}
14161438

14171439
guard OpenbirdDateFormatting.dayString(for: selectedDay) == requestedDayString else {
14181440
return

Sources/OpenbirdKit/Storage/OpenbirdStore.swift

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22

33
public actor OpenbirdStore {
44
private let database: SQLiteDatabase
5+
private let exclusionEngine = ExclusionEngine()
56
private var pendingPreparedActivityDays: Set<String> = []
67
private var dirtyPreparedActivityDays: Set<String> = []
78
private var preparedActivityRefreshTask: Task<Void, Never>?
@@ -44,10 +45,12 @@ public actor OpenbirdStore {
4445

4546
public func saveExclusion(_ exclusion: ExclusionRule) throws {
4647
try database.saveExclusion(exclusion)
48+
try invalidatePreparedActivityCacheForExclusionChange()
4749
}
4850

4951
public func deleteExclusion(id: String) throws {
5052
try database.deleteExclusion(id: id)
53+
try invalidatePreparedActivityCacheForExclusionChange()
5154
}
5255

5356
public func saveActivityEvent(_ event: ActivityEvent) throws {
@@ -56,7 +59,12 @@ public actor OpenbirdStore {
5659
}
5760

5861
public func loadActivityEvents(in range: ClosedRange<Date>, includeExcluded: Bool = false) throws -> [ActivityEvent] {
59-
try database.loadActivityEvents(in: range, includeExcluded: includeExcluded)
62+
let events = try database.loadActivityEvents(in: range, includeExcluded: includeExcluded)
63+
guard includeExcluded == false else {
64+
return events
65+
}
66+
67+
return try filterCurrentExclusions(from: events)
6068
}
6169

6270
public func searchActivityEvents(
@@ -65,7 +73,8 @@ public actor OpenbirdStore {
6573
appFilters: [String] = [],
6674
topK: Int = 8
6775
) throws -> [ActivityEvent] {
68-
try database.searchActivityEvents(query: query, in: range, appFilters: appFilters, topK: topK)
76+
let events = try database.searchActivityEvents(query: query, in: range, appFilters: appFilters, topK: topK)
77+
return try filterCurrentExclusions(from: events)
6978
}
7079

7180
public func loadJournal(for day: String) throws -> DailyJournal? {
@@ -118,7 +127,7 @@ public actor OpenbirdStore {
118127
let day = OpenbirdDateFormatting.dayString(for: date)
119128
if dirtyPreparedActivityDays.contains(day) == false,
120129
let cached = try database.loadPreparedActivityEvents(for: day) {
121-
return cached
130+
return try filterCurrentExclusions(from: cached)
122131
}
123132

124133
return try await rebuildPreparedActivityEvents(for: day, date: date)
@@ -133,10 +142,7 @@ public actor OpenbirdStore {
133142
}
134143

135144
private func rebuildPreparedActivityEvents(for day: String, date: Date) async throws -> [GroupedActivityEvent] {
136-
let rawEvents = try database.loadActivityEvents(
137-
in: Calendar.autoupdatingCurrent.dayRange(for: date),
138-
includeExcluded: true
139-
)
145+
let rawEvents = try loadActivityEvents(in: Calendar.autoupdatingCurrent.dayRange(for: date))
140146
let groupedEvents = await Task.detached(priority: .utility) {
141147
ActivityEvidencePreprocessor.groupedMeaningfulEvents(from: rawEvents)
142148
}.value
@@ -145,6 +151,48 @@ public actor OpenbirdStore {
145151
return groupedEvents
146152
}
147153

154+
private func filterCurrentExclusions(from events: [ActivityEvent]) throws -> [ActivityEvent] {
155+
let exclusions = try activeExclusions()
156+
guard exclusions.isEmpty == false else {
157+
return events
158+
}
159+
160+
return events.filter { event in
161+
event.isExcluded == false &&
162+
exclusionEngine.isExcluded(
163+
bundleID: event.bundleId,
164+
url: event.url,
165+
rules: exclusions
166+
) == false
167+
}
168+
}
169+
170+
private func filterCurrentExclusions(from events: [GroupedActivityEvent]) throws -> [GroupedActivityEvent] {
171+
let exclusions = try activeExclusions()
172+
guard exclusions.isEmpty == false else {
173+
return events.filter { $0.isExcluded == false }
174+
}
175+
176+
return events.filter { event in
177+
event.isExcluded == false &&
178+
exclusionEngine.isExcluded(
179+
bundleID: event.bundleId,
180+
url: event.url,
181+
rules: exclusions
182+
) == false
183+
}
184+
}
185+
186+
private func activeExclusions() throws -> [ExclusionRule] {
187+
try database.loadExclusions().filter(\.isEnabled)
188+
}
189+
190+
private func invalidatePreparedActivityCacheForExclusionChange() throws {
191+
try database.deleteAllPreparedActivityEvents()
192+
pendingPreparedActivityDays.removeAll()
193+
dirtyPreparedActivityDays.removeAll()
194+
}
195+
148196
private func invalidatePreparedActivityDays<S: Sequence>(for days: S) where S.Element == String {
149197
let normalizedDays = Set(days).filter { $0.isEmpty == false }
150198
guard normalizedDays.isEmpty == false else {

Tests/OpenbirdKitTests/OpenbirdStoreTests.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,78 @@ struct OpenbirdStoreTests {
6868
#expect(results.first?.appName == "VS Code")
6969
}
7070

71+
@Test func loadActivityEventsRespectsCurrentDomainExclusions() async throws {
72+
let databaseURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite")
73+
let store = try OpenbirdStore(databaseURL: databaseURL)
74+
let now = Date()
75+
76+
try await store.saveActivityEvent(
77+
ActivityEvent(
78+
id: "public-event",
79+
startedAt: now.addingTimeInterval(-300),
80+
endedAt: now.addingTimeInterval(-240),
81+
bundleId: "com.apple.Safari",
82+
appName: "Safari",
83+
windowTitle: "Public",
84+
url: "https://openbird.app",
85+
visibleText: "Reviewed the homepage",
86+
source: "accessibility",
87+
contentHash: "public-event",
88+
isExcluded: false
89+
)
90+
)
91+
try await store.saveActivityEvent(
92+
ActivityEvent(
93+
id: "private-event",
94+
startedAt: now.addingTimeInterval(-180),
95+
endedAt: now.addingTimeInterval(-120),
96+
bundleId: "com.apple.Safari",
97+
appName: "Safari",
98+
windowTitle: "Private",
99+
url: "https://mail.google.com",
100+
visibleText: "Read private mail",
101+
source: "accessibility",
102+
contentHash: "private-event",
103+
isExcluded: false
104+
)
105+
)
106+
try await store.saveExclusion(ExclusionRule(kind: .domain, pattern: "google.com"))
107+
108+
let events = try await store.loadActivityEvents(in: Calendar.current.dayRange(for: now))
109+
110+
#expect(events.map(\.id) == ["public-event"])
111+
}
112+
113+
@Test func searchActivityEventsRespectsCurrentDomainExclusions() async throws {
114+
let databaseURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite")
115+
let store = try OpenbirdStore(databaseURL: databaseURL)
116+
let now = Date()
117+
118+
try await store.saveActivityEvent(
119+
ActivityEvent(
120+
startedAt: now.addingTimeInterval(-180),
121+
endedAt: now,
122+
bundleId: "com.apple.Safari",
123+
appName: "Safari",
124+
windowTitle: "Inbox",
125+
url: "https://mail.google.com",
126+
visibleText: "Reviewed a private launch plan",
127+
source: "accessibility",
128+
contentHash: "private-search",
129+
isExcluded: false
130+
)
131+
)
132+
try await store.saveExclusion(ExclusionRule(kind: .domain, pattern: "google.com"))
133+
134+
let results = try await store.searchActivityEvents(
135+
query: "launch plan",
136+
in: Calendar.current.dayRange(for: now),
137+
topK: 5
138+
)
139+
140+
#expect(results.isEmpty)
141+
}
142+
71143
@Test func mergesOverlappingEventsWithSameContentHash() async throws {
72144
let databaseURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite")
73145
let store = try OpenbirdStore(databaseURL: databaseURL)
@@ -391,6 +463,51 @@ struct OpenbirdStoreTests {
391463
#expect(refreshed.first?.sourceEventCount == 2)
392464
}
393465

466+
@Test func preparedActivityEventsRefreshWhenDomainExclusionChanges() async throws {
467+
let databaseURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite")
468+
let store = try OpenbirdStore(databaseURL: databaseURL)
469+
let start = Date(timeIntervalSince1970: 1_720_000_000)
470+
471+
try await store.saveActivityEvent(
472+
ActivityEvent(
473+
id: "allowed-group",
474+
startedAt: start,
475+
endedAt: start.addingTimeInterval(30),
476+
bundleId: "com.apple.Safari",
477+
appName: "Safari",
478+
windowTitle: "Openbird",
479+
url: "https://openbird.app",
480+
visibleText: "Reviewed the Openbird homepage",
481+
source: "accessibility",
482+
contentHash: "allowed-group",
483+
isExcluded: false
484+
)
485+
)
486+
try await store.saveActivityEvent(
487+
ActivityEvent(
488+
id: "excluded-group",
489+
startedAt: start.addingTimeInterval(600),
490+
endedAt: start.addingTimeInterval(630),
491+
bundleId: "com.apple.Safari",
492+
appName: "Safari",
493+
windowTitle: "Mail",
494+
url: "https://mail.google.com",
495+
visibleText: "Reviewed private mail",
496+
source: "accessibility",
497+
contentHash: "excluded-group",
498+
isExcluded: false
499+
)
500+
)
501+
502+
let initial = try await store.preparedActivityEvents(for: start)
503+
#expect(initial.map(\.id) == ["allowed-group", "excluded-group"])
504+
505+
try await store.saveExclusion(ExclusionRule(kind: .domain, pattern: "google.com"))
506+
507+
let refreshed = try await store.preparedActivityEvents(for: start)
508+
#expect(refreshed.map(\.id) == ["allowed-group"])
509+
}
510+
394511
@Test func backgroundPrepareKeepsFreshPreparedActivityCache() async throws {
395512
let databaseURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString).appendingPathExtension("sqlite")
396513
let store = try OpenbirdStore(databaseURL: databaseURL)

0 commit comments

Comments
 (0)