Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download and parse user lists #1699

Merged
merged 13 commits into from
Dec 17, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed display of mastodon usernames so it shows @[email protected] rather than [email protected]

### Internal Changes
- Download and parse an author’s lists when viewing their profile. [#49](https://github.com/verse-pbc/issues/issues/49)

## [1.0.3] - 2024-12-04Z

Expand Down
44 changes: 40 additions & 4 deletions Nos.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Nos/Models/AuthorListError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

/// Errors for an ``AuthorList``.
enum AuthorListError: LocalizedError, Equatable {
/// The event kind is invalid; that is, an ``AuthorList`` can't be created with the given kind.
case invalidKind

/// The signature is invalid.
case invalidSignature(AuthorList)

/// The replaceable ID is missing (the `d` tag from the JSON event).
case missingReplaceableID
}
90 changes: 90 additions & 0 deletions Nos/Models/CoreData/AuthorList+CoreDataClass.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Foundation
import CoreData

@objc(AuthorList)
public class AuthorList: NSManagedObject {
static func createOrUpdate(
from jsonEvent: JSONEvent,
in context: NSManagedObjectContext
) throws -> AuthorList {
guard jsonEvent.kind == EventKind.followSet.rawValue else { throw AuthorListError.invalidKind }
guard let replaceableID = jsonEvent.replaceableID else { throw AuthorListError.missingReplaceableID }
let owner = try Author.findOrCreate(by: jsonEvent.pubKey, context: context)

// Fetch existing AuthorList if it exists
let fetchRequest = AuthorList.authorList(by: replaceableID, owner: owner, kind: EventKind.followSet.rawValue)
let existingAuthorList = try context.fetch(fetchRequest).first
existingAuthorList?.authors.removeAll()

let authorList = existingAuthorList ?? AuthorList(context: context)
authorList.createdAt = jsonEvent.createdDate
authorList.owner = owner
authorList.identifier = jsonEvent.id
authorList.replaceableIdentifier = replaceableID
authorList.kind = EventKind.followSet.rawValue
authorList.signature = jsonEvent.signature
authorList.allTags = jsonEvent.tags as NSObject
authorList.content = jsonEvent.content

let tags = jsonEvent.tags

for tag in tags {
if tag[safe: 0] == "p", let authorID = tag[safe: 1] {
let author = try Author.findOrCreate(by: authorID, context: context)
authorList.addToAuthors(author)
} else if tag[safe: 0] == "title" {
authorList.title = tag[safe: 1]
} else if tag[safe: 0] == "image" {
if let urlString = tag[safe: 1] {
authorList.image = URL(string: urlString)
} else {
authorList.image = nil
}
} else if tag[safe: 0] == "description" {
authorList.listDescription = tag[safe: 1]
}
}

return authorList
}

@nonobjc public class func authorList(
by replaceableID: RawReplaceableID,
owner: Author,
kind: Int64
) -> NSFetchRequest<AuthorList> {
let fetchRequest = NSFetchRequest<AuthorList>(entityName: "AuthorList")
fetchRequest.predicate = NSPredicate(
format: "replaceableIdentifier = %@ AND owner = %@ AND kind = %i",
replaceableID,
owner,
kind
)
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \AuthorList.identifier, ascending: true)]
fetchRequest.fetchLimit = 1
return fetchRequest
}
}

extension AuthorList: VerifiableEvent {
var pubKey: String { owner.hexadecimalPublicKey ?? "" }

var serializedListForSigning: [Any?] {
[
0,
owner.hexadecimalPublicKey,
Int64(createdAt.timeIntervalSince1970),
kind,
allTags,
content
]
}

func calculateIdentifier() throws -> String {
let serializedEventData = try JSONSerialization.data(
withJSONObject: serializedListForSigning,
options: [.withoutEscapingSlashes]
)
return serializedEventData.sha256
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
import CoreData

extension AuthorList {

@nonobjc public class func fetchRequest() -> NSFetchRequest<AuthorList> {
NSFetchRequest<AuthorList>(entityName: "AuthorList")
}

@NSManaged public var allTags: NSObject?
@NSManaged public var content: String?
@NSManaged public var createdAt: Date
@NSManaged public var identifier: RawEventID?
@NSManaged public var image: URL?
@NSManaged public var isVerified: Bool
@NSManaged public var kind: Int64
@NSManaged public var listDescription: String?
@NSManaged public var replaceableIdentifier: String
@NSManaged public var signature: String?
@NSManaged public var title: String?

@NSManaged public var authors: Set<Author>
@NSManaged public var owner: Author
}

// MARK: Generated accessors for authors
extension AuthorList {

@objc(addAuthorsObject:)
@NSManaged public func addToAuthors(_ value: Author)

@objc(removeAuthorsObject:)
@NSManaged public func removeFromAuthors(_ value: Author)

@objc(addAuthors:)
@NSManaged public func addToAuthors(_ values: NSSet)

@objc(removeAuthors:)
@NSManaged public func removeFromAuthors(_ values: NSSet)
}

extension AuthorList: Identifiable {}
2 changes: 1 addition & 1 deletion Nos/Models/CoreData/Nos.xcdatamodeld/.xccurrentversion
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Nos 20.xcdatamodel</string>
<string>Nos 21.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Nos.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Author" representedClassName=".Author" syncable="YES" codeGenerationType="category">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="hexadecimalPublicKey" attributeType="String"/>
<attribute name="lastUpdatedContactList" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastUpdatedMetadata" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="muted" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="nip05" optional="YES" attributeType="String"/>
<attribute name="profilePhotoURL" optional="YES" attributeType="URI"/>
<attribute name="rawMetadata" optional="YES" attributeType="Binary"/>
<relationship name="events" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Event" inverseName="author" inverseEntity="Event"/>
<relationship name="followers" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Follow" inverseName="destination" inverseEntity="Follow"/>
<relationship name="follows" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Follow" inverseName="source" inverseEntity="Follow"/>
<relationship name="relays" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
</entity>
<entity name="AuthorReference" representedClassName="AuthorReference" syncable="YES" codeGenerationType="category">
<attribute name="pubkey" optional="YES" attributeType="String"/>
<attribute name="recommendedRelayUrl" optional="YES" attributeType="String"/>
<relationship name="event" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="authorReferences" inverseEntity="Event"/>
</entity>
<entity name="Event" representedClassName=".Event" syncable="YES" codeGenerationType="category">
<attribute name="allTags" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName"/>
<attribute name="content" optional="YES" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="identifier" attributeType="String"/>
<attribute name="kind" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sendAttempts" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="signature" optional="YES" attributeType="String"/>
<relationship name="author" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="events" inverseEntity="Author"/>
<relationship name="authorReferences" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="AuthorReference" inverseName="event" inverseEntity="AuthorReference"/>
<relationship name="deletedOn" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
<relationship name="eventReferences" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="EventReference" inverseName="referencingEvent" inverseEntity="EventReference"/>
<relationship name="publishedTo" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Relay"/>
<relationship name="referencingEvents" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="EventReference" inverseName="referencedEvent" inverseEntity="EventReference"/>
</entity>
<entity name="EventReference" representedClassName="EventReference" syncable="YES" codeGenerationType="category">
<attribute name="eventId" optional="YES" attributeType="String"/>
<attribute name="marker" optional="YES" attributeType="String"/>
<attribute name="recommendedRelayUrl" optional="YES" attributeType="String"/>
<relationship name="referencedEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="referencingEvents" inverseEntity="Event"/>
<relationship name="referencingEvent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Event" inverseName="eventReferences" inverseEntity="Event"/>
</entity>
<entity name="Follow" representedClassName=".Follow" syncable="YES" codeGenerationType="category">
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="petName" optional="YES" attributeType="String"/>
<relationship name="destination" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="followers" inverseEntity="Author"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Author" inverseName="follows" inverseEntity="Author"/>
</entity>
<entity name="Relay" representedClassName=".Relay" syncable="YES" codeGenerationType="category">
<attribute name="address" attributeType="String"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
</model>
Loading
Loading