Skip to content

Commit

Permalink
Merge pull request #1228 from planetary-social/dont-heal-event-refere…
Browse files Browse the repository at this point in the history
…nces

Dont heal event references
  • Loading branch information
mplorentz authored Jun 21, 2024
2 parents d852ef8 + 1fd9bed commit ff778a5
Show file tree
Hide file tree
Showing 12 changed files with 551 additions and 155 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.1.17] - 2024-06-10Z

- Fixed a bug where infinite spinners would be shown on reposted notes.
- Added support for opening njump.me content in Nos.
- Fixed a crash on logout
- Fixed a bug where some profiles wouldn't load old notes.
Expand Down
4 changes: 4 additions & 0 deletions Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
C96D39272B61B6D200D3D0A1 /* RawNostrID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96D39262B61B6D200D3D0A1 /* RawNostrID.swift */; };
C96D39282B61B6D200D3D0A1 /* RawNostrID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96D39262B61B6D200D3D0A1 /* RawNostrID.swift */; };
C973364F2A7968220012D8B8 /* SetUpUNSBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C973364E2A7968220012D8B8 /* SetUpUNSBanner.swift */; };
C9736E5E2C13B718005BCE70 /* EventFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9736E5D2C13B718005BCE70 /* EventFixture.swift */; };
C973AB5B2A323167002AED16 /* Follow+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C973AB552A323167002AED16 /* Follow+CoreDataProperties.swift */; };
C973AB5C2A323167002AED16 /* Follow+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C973AB552A323167002AED16 /* Follow+CoreDataProperties.swift */; };
C973AB5D2A323167002AED16 /* Event+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C973AB562A323167002AED16 /* Event+CoreDataProperties.swift */; };
Expand Down Expand Up @@ -680,6 +681,7 @@
C96D391A2B61AFD500D3D0A1 /* RawNostrIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawNostrIDTests.swift; sourceTree = "<group>"; };
C96D39262B61B6D200D3D0A1 /* RawNostrID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawNostrID.swift; sourceTree = "<group>"; };
C973364E2A7968220012D8B8 /* SetUpUNSBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetUpUNSBanner.swift; sourceTree = "<group>"; };
C9736E5D2C13B718005BCE70 /* EventFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFixture.swift; sourceTree = "<group>"; };
C973AB552A323167002AED16 /* Follow+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Follow+CoreDataProperties.swift"; sourceTree = "<group>"; };
C973AB562A323167002AED16 /* Event+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Event+CoreDataProperties.swift"; sourceTree = "<group>"; };
C973AB572A323167002AED16 /* AuthorReference+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthorReference+CoreDataProperties.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1242,6 +1244,7 @@
C91400232B2A3894009B13B4 /* SQLiteStoreTestCase.swift */,
C9C9444129F6F0E2002F2C7A /* XCTest+Eventually.swift */,
0373CE982C0910250027C856 /* XCTestCase+JSONData.swift */,
C9736E5D2C13B718005BCE70 /* EventFixture.swift */,
);
path = "Test Helpers";
sourceTree = "<group>";
Expand Down Expand Up @@ -2200,6 +2203,7 @@
035729B92BE416A6005FEE85 /* GiftWrapperTests.swift in Sources */,
032634702C10C40B00E489B5 /* NostrBuildAPIClientTests.swift in Sources */,
C9646EAA29B7A506007239A4 /* Analytics.swift in Sources */,
C9736E5E2C13B718005BCE70 /* EventFixture.swift in Sources */,
035729AF2BE4167E005FEE85 /* KeyPairTests.swift in Sources */,
035729AE2BE4167E005FEE85 /* FollowTests.swift in Sources */,
5BFBB2952BD9D7EB002E909F /* URLParserTests.swift in Sources */,
Expand Down
6 changes: 3 additions & 3 deletions Nos/Controller/PersistenceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,14 @@ class PersistenceController {
/// invalidate old items to keep it from growing indefinitely.
///
/// This should only be called once right at app launch.
func cleanupEntities() async {
guard let authorKey = await currentUser.author?.hexadecimalPublicKey else {
@MainActor func cleanupEntities() async {
guard let authorKey = currentUser.author?.hexadecimalPublicKey else {
return
}

let context = newBackgroundContext()
do {
try await DatabaseCleaner.cleanupEntities(before: Date.now, for: authorKey, in: context)
try await DatabaseCleaner.cleanupEntities(for: authorKey, in: context)
} catch {
Log.optional(error)
crashReporting.report("Error in database cleanup: \(error.localizedDescription)")
Expand Down
98 changes: 97 additions & 1 deletion Nos/Models/CoreData/Event+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ public class Event: NosManagedObject, VerifiableEvent {

@nonobjc public class func allEventsRequest() -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(entityName: "Event")
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Event.createdAt, ascending: true)]
fetchRequest.sortDescriptors = [
NSSortDescriptor(keyPath: \Event.createdAt, ascending: true),
NSSortDescriptor(keyPath: \Event.receivedAt, ascending: true)
]
return fetchRequest
}

Expand Down Expand Up @@ -342,6 +345,81 @@ public class Event: NosManagedObject, VerifiableEvent {
return fetchRequest
}

/// A fetch request for all the events that should be cleared out of the database by
/// `DatabaseCleaner.cleanupEntities(...)`.
///
/// It will save the events for the given `user`, as well as other important events matching various other
/// criteria.
/// - Parameter before: The date before which events will be considered for cleanup.
/// - Parameter user: The Author record for the currently logged in user. Special treatment is given to their data.
@nonobjc public class func cleanupRequest(before date: Date, for user: Author) -> NSFetchRequest<Event> {
let oldStoryCutoff = Calendar.current.date(byAdding: .day, value: -2, to: .now) ?? .now

let request = NSFetchRequest<Event>(entityName: "Event")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Event.receivedAt, ascending: true)]
let oldUnreferencedEventsClause = "(receivedAt < %@ OR receivedAt == nil) AND referencingEvents.@count = 0"
let notOwnEventClause = "(author != %@)"
let readStoryClause = "(isRead = 1 AND receivedAt > %@)"
let userReportClause = "(kind == \(EventKind.report.rawValue) AND " +
"authorReferences.@count > 0 AND eventReferences.@count == 0)"
let clauses = "\(oldUnreferencedEventsClause) AND" +
"\(notOwnEventClause) AND " +
"NOT \(readStoryClause) AND " +
"NOT \(userReportClause)"
request.predicate = NSPredicate(
format: clauses,
date as CVarArg,
user,
oldStoryCutoff as CVarArg
)

return request
}

/// This constructs a predicate for events that should be protected from deletion when we are purging the database.
/// - Parameter user: The Author record for the currently logged in user. Special treatment is given to their data.
/// - Parameter asSubquery: If true then each attribute in the predicate will prefixed with "$event." so the
/// predicate can be used in a SUBQUERY.
@nonobjc public class func protectedFromCleanupPredicate(
for user: Author,
asSubquery: Bool = false
) -> NSPredicate {
guard let userKey = user.hexadecimalPublicKey else {
return NSPredicate.false
}

// The string we use to reference the current event if we are constructing this predicate to be used in a
// subquery
let eventReference = asSubquery ? "$event." : ""

// protect all events authored by the current user
let userEventsPredicate = NSPredicate(format: "\(eventReference)author.hexadecimalPublicKey = '\(userKey)'")

// protect stories that were read recently, so we don't redownload and show them as unread again
let oldStoryCutoffDate = Calendar.current.date(byAdding: .day, value: -2, to: .now) ?? .now
let recentlyReadStoriesPredicate = NSPredicate(
format: "(\(eventReference)isRead = 1 AND \(eventReference)receivedAt > %@)",
oldStoryCutoffDate as CVarArg
)

// keep author reports from people we follow
let userReportPredicate = NSPredicate(
format: "(\(eventReference)kind == \(EventKind.report.rawValue) AND " +
"SUBQUERY(\(eventReference)authorReferences, $references, TRUEPREDICATE).@count > 0 AND " +
"SUBQUERY(\(eventReference)eventReferences, $references, TRUEPREDICATE).@count == 0 AND " +
"ANY \(eventReference)author.followers.source.hexadecimalPublicKey == %@)",
userKey
)

return NSCompoundPredicate(
orPredicateWithSubpredicates: [
userEventsPredicate,
recentlyReadStoriesPredicate,
userReportPredicate
]
)
}

@nonobjc public class func expiredRequest() -> NSFetchRequest<Event> {
let fetchRequest = NSFetchRequest<Event>(entityName: "Event")
fetchRequest.predicate = NSPredicate(format: "expirationDate <= %@", Date.now as CVarArg)
Expand Down Expand Up @@ -1175,6 +1253,24 @@ public class Event: NosManagedObject, VerifiableEvent {
return "https://njump.me"
}
}

/// Converts an event back to a stubbed event by deleting all data except the `identifier`.
func stub() {
allTags = nil
content = nil
createdAt = nil
isVerified = false
receivedAt = nil
sendAttempts = 0
signature = nil
author = nil
authorReferences = NSOrderedSet()
deletedOn = Set()
eventReferences = NSOrderedSet()
publishedTo = Set()
seenOnRelays = Set()
shouldBePublishedTo = Set()
}
}
// swiftlint:enable type_body_length
// swiftlint:enable file_length
35 changes: 35 additions & 0 deletions Nos/Models/CoreData/EventReference+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,41 @@ public class EventReference: NosManagedObject {
marker.unwrap { EventReferenceMarker(rawValue: $0) }
}

/// Retreives all the EventReferences
static func all() -> NSFetchRequest<EventReference> {
let fetchRequest = NSFetchRequest<EventReference>(entityName: "EventReference")
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \EventReference.eventId, ascending: false)]
return fetchRequest
}

/// This fetches all the references that can be deleted during the `DatabaseCleaner` routine. It takes care
/// to only select references before a given date that are not referenced by events we are keeping, and it also
/// accounts for "protected events" from `Event.protectedFromCleanupPredicate(...)` to make sure we keep
/// events published by the current user etc.
static func cleanupRequest(before date: Date, user: Author) -> NSFetchRequest<EventReference> {
let fetchRequest = NSFetchRequest<EventReference>(entityName: "EventReference")
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \EventReference.eventId, ascending: false)]
let protectedEventsPredicate = Event.protectedFromCleanupPredicate(for: user, asSubquery: true)
let referencedEventIsNotProtected = NSPredicate(
format: "SUBQUERY(referencedEvent, $event, \(protectedEventsPredicate.predicateFormat)).@count == 0"
)
let referencingEventIsNotProtected = NSPredicate(
format: "SUBQUERY(referencingEvent, $event, \(protectedEventsPredicate.predicateFormat)).@count == 0"
)
let eventsAreOld = NSPredicate(
format: "referencedEvent.receivedAt < %@ AND referencingEvent.receivedAt < %@",
date as CVarArg,
date as CVarArg
)
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
referencedEventIsNotProtected,
referencingEventIsNotProtected,
eventsAreOld
])

return fetchRequest
}

/// Retreives all the EventReferences whose referencing Event has been deleted.
static func orphanedRequest() -> NSFetchRequest<EventReference> {
let fetchRequest = NSFetchRequest<EventReference>(entityName: "EventReference")
Expand Down
9 changes: 9 additions & 0 deletions Nos/Models/CoreData/Follow+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ public class Follow: NosManagedObject {
fetchRequest.predicate = NSPredicate(format: "destination IN %@", authors)
return fetchRequest
}

@nonobjc public class func allFollowsRequest() -> NSFetchRequest<Follow> {
let fetchRequest = NSFetchRequest<Follow>(entityName: "Follow")
fetchRequest.sortDescriptors = [
NSSortDescriptor(keyPath: \Follow.source?.hexadecimalPublicKey, ascending: false),
NSSortDescriptor(keyPath: \Follow.destination?.hexadecimalPublicKey, ascending: false),
]
return fetchRequest
}

/// Retreives all the Follows whose source Author has been deleted.
static func orphanedRequest() -> NSFetchRequest<Follow> {
Expand Down
Loading

0 comments on commit ff778a5

Please sign in to comment.