feat: implement authentication scene
This commit is contained in:
parent
36c1807182
commit
2c6a0e383a
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D5029f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D64" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
||||||
<attribute name="category" optional="YES" attributeType="String"/>
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||||
|
@ -25,6 +25,20 @@
|
||||||
<attribute name="userIdentifier" attributeType="String"/>
|
<attribute name="userIdentifier" attributeType="String"/>
|
||||||
<relationship name="toots" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="homeTimelineIndexes" inverseEntity="Toot"/>
|
<relationship name="toots" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="homeTimelineIndexes" inverseEntity="Toot"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="MastodonAuthentication" representedClassName=".MastodonAuthentication" syncable="YES">
|
||||||
|
<attribute name="activedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="appAccessToken" attributeType="String"/>
|
||||||
|
<attribute name="clientID" attributeType="String"/>
|
||||||
|
<attribute name="clientSecret" attributeType="String"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="identifier" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="userAccessToken" attributeType="String"/>
|
||||||
|
<attribute name="userID" attributeType="String"/>
|
||||||
|
<attribute name="username" attributeType="String"/>
|
||||||
|
<relationship name="user" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="mastodonAuthentication" inverseEntity="MastodonUser"/>
|
||||||
|
</entity>
|
||||||
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
|
<entity name="MastodonUser" representedClassName=".MastodonUser" syncable="YES">
|
||||||
<attribute name="acct" attributeType="String"/>
|
<attribute name="acct" attributeType="String"/>
|
||||||
<attribute name="avatar" attributeType="String"/>
|
<attribute name="avatar" attributeType="String"/>
|
||||||
|
@ -38,6 +52,7 @@
|
||||||
<attribute name="username" attributeType="String"/>
|
<attribute name="username" attributeType="String"/>
|
||||||
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="bookmarked" inverseEntity="Toot"/>
|
<relationship name="bookmarked" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="bookmarked" inverseEntity="Toot"/>
|
||||||
<relationship name="favourite" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="favouritedBy" inverseEntity="Toot"/>
|
<relationship name="favourite" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="favouritedBy" inverseEntity="Toot"/>
|
||||||
|
<relationship name="mastodonAuthentication" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonAuthentication" inverseName="user" inverseEntity="MastodonAuthentication"/>
|
||||||
<relationship name="muted" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="mutedBy" inverseEntity="Toot"/>
|
<relationship name="muted" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="mutedBy" inverseEntity="Toot"/>
|
||||||
<relationship name="pinnedToot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="pinnedBy" inverseEntity="Toot"/>
|
<relationship name="pinnedToot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="pinnedBy" inverseEntity="Toot"/>
|
||||||
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="rebloggedBy" inverseEntity="Toot"/>
|
<relationship name="reblogged" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="rebloggedBy" inverseEntity="Toot"/>
|
||||||
|
@ -97,9 +112,10 @@
|
||||||
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
||||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="104"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="104"/>
|
||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="269"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
||||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||||
<element name="Toot" positionX="0" positionY="0" width="128" height="494"/>
|
<element name="Toot" positionX="0" positionY="0" width="128" height="494"/>
|
||||||
|
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
|
@ -0,0 +1,161 @@
|
||||||
|
//
|
||||||
|
// MastodonAuthentication.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
final public class MastodonAuthentication: NSManagedObject {
|
||||||
|
|
||||||
|
public typealias ID = UUID
|
||||||
|
|
||||||
|
@NSManaged public private(set) var identifier: ID
|
||||||
|
|
||||||
|
@NSManaged public private(set) var domain: String
|
||||||
|
@NSManaged public private(set) var userID: String
|
||||||
|
@NSManaged public private(set) var username: String
|
||||||
|
|
||||||
|
@NSManaged public private(set) var appAccessToken: String
|
||||||
|
@NSManaged public private(set) var userAccessToken: String
|
||||||
|
@NSManaged public private(set) var clientID: String
|
||||||
|
@NSManaged public private(set) var clientSecret: String
|
||||||
|
|
||||||
|
@NSManaged public private(set) var createdAt: Date
|
||||||
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
@NSManaged public private(set) var activedAt: Date
|
||||||
|
|
||||||
|
// one-to-one relationship
|
||||||
|
@NSManaged public private(set) var user: MastodonUser
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAuthentication {
|
||||||
|
|
||||||
|
public override func awakeFromInsert() {
|
||||||
|
super.awakeFromInsert()
|
||||||
|
identifier = UUID()
|
||||||
|
|
||||||
|
let now = Date()
|
||||||
|
createdAt = now
|
||||||
|
updatedAt = now
|
||||||
|
activedAt = now
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
public static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
property: Property,
|
||||||
|
user: MastodonUser
|
||||||
|
) -> MastodonAuthentication {
|
||||||
|
let authentication: MastodonAuthentication = context.insertObject()
|
||||||
|
|
||||||
|
authentication.domain = property.domain
|
||||||
|
authentication.userID = property.userID
|
||||||
|
authentication.username = property.username
|
||||||
|
authentication.appAccessToken = property.appAccessToken
|
||||||
|
authentication.userAccessToken = property.userAccessToken
|
||||||
|
authentication.clientID = property.clientID
|
||||||
|
authentication.clientSecret = property.clientSecret
|
||||||
|
|
||||||
|
authentication.user = user
|
||||||
|
|
||||||
|
return authentication
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(username: String) {
|
||||||
|
if self.username != username {
|
||||||
|
self.username = username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(appAccessToken: String) {
|
||||||
|
if self.appAccessToken != appAccessToken {
|
||||||
|
self.appAccessToken = appAccessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(userAccessToken: String) {
|
||||||
|
if self.userAccessToken != userAccessToken {
|
||||||
|
self.userAccessToken = userAccessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(clientID: String) {
|
||||||
|
if self.clientID != clientID {
|
||||||
|
self.clientID = clientID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(clientSecret: String) {
|
||||||
|
if self.clientSecret != clientSecret {
|
||||||
|
self.clientSecret = clientSecret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(activedAt: Date) {
|
||||||
|
if self.activedAt != activedAt {
|
||||||
|
self.activedAt = activedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func didUpdate(at networkDate: Date) {
|
||||||
|
self.updatedAt = networkDate
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAuthentication {
|
||||||
|
public struct Property {
|
||||||
|
|
||||||
|
public let domain: String
|
||||||
|
public let userID: String
|
||||||
|
public let username: String
|
||||||
|
public let appAccessToken: String
|
||||||
|
public let userAccessToken: String
|
||||||
|
public let clientID: String
|
||||||
|
public let clientSecret: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
domain: String,
|
||||||
|
userID: String,
|
||||||
|
username: String,
|
||||||
|
appAccessToken: String,
|
||||||
|
userAccessToken: String,
|
||||||
|
clientID: String,
|
||||||
|
clientSecret: String
|
||||||
|
) {
|
||||||
|
self.domain = domain
|
||||||
|
self.userID = userID
|
||||||
|
self.username = username
|
||||||
|
self.appAccessToken = appAccessToken
|
||||||
|
self.userAccessToken = userAccessToken
|
||||||
|
self.clientID = clientID
|
||||||
|
self.clientSecret = clientSecret
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAuthentication: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \MastodonAuthentication.createdAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MastodonAuthentication {
|
||||||
|
|
||||||
|
static func predicate(domain: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.domain), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(userID: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonAuthentication.userID), userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, userID: String) -> NSPredicate {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonAuthentication.predicate(domain: domain),
|
||||||
|
MastodonAuthentication.predicate(userID: userID)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,12 +8,14 @@
|
||||||
import CoreData
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public final class MastodonUser: NSManagedObject {
|
final public class MastodonUser: NSManagedObject {
|
||||||
|
|
||||||
public typealias ID = String
|
public typealias ID = String
|
||||||
|
|
||||||
@NSManaged public private(set) var identifier: ID
|
@NSManaged public private(set) var identifier: ID
|
||||||
@NSManaged public private(set) var domain: String
|
@NSManaged public private(set) var domain: String
|
||||||
|
|
||||||
@NSManaged public private(set) var id: String
|
@NSManaged public private(set) var id: ID
|
||||||
@NSManaged public private(set) var acct: String
|
@NSManaged public private(set) var acct: String
|
||||||
@NSManaged public private(set) var username: String
|
@NSManaged public private(set) var username: String
|
||||||
@NSManaged public private(set) var displayName: String
|
@NSManaged public private(set) var displayName: String
|
||||||
|
@ -25,6 +27,7 @@ public final class MastodonUser: NSManagedObject {
|
||||||
|
|
||||||
// one-to-one relationship
|
// one-to-one relationship
|
||||||
@NSManaged public private(set) var pinnedToot: Toot?
|
@NSManaged public private(set) var pinnedToot: Toot?
|
||||||
|
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
||||||
|
|
||||||
// one-to-many relationship
|
// one-to-many relationship
|
||||||
@NSManaged public private(set) var toots: Set<Toot>?
|
@NSManaged public private(set) var toots: Set<Toot>?
|
||||||
|
@ -36,11 +39,13 @@ public final class MastodonUser: NSManagedObject {
|
||||||
@NSManaged public private(set) var bookmarked: Set<Toot>?
|
@NSManaged public private(set) var bookmarked: Set<Toot>?
|
||||||
|
|
||||||
@NSManaged public private(set) var retweets: Set<Toot>?
|
@NSManaged public private(set) var retweets: Set<Toot>?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension MastodonUser {
|
extension MastodonUser {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
static func insert(
|
public static func insert(
|
||||||
into context: NSManagedObjectContext,
|
into context: NSManagedObjectContext,
|
||||||
property: Property
|
property: Property
|
||||||
) -> MastodonUser {
|
) -> MastodonUser {
|
||||||
|
@ -61,6 +66,38 @@ public extension MastodonUser {
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func update(acct: String) {
|
||||||
|
if self.acct != acct {
|
||||||
|
self.acct = acct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(username: String) {
|
||||||
|
if self.username != username {
|
||||||
|
self.username = username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(displayName: String) {
|
||||||
|
if self.displayName != displayName {
|
||||||
|
self.displayName = displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(avatar: String) {
|
||||||
|
if self.avatar != avatar {
|
||||||
|
self.avatar = avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public func update(avatarStatic: String?) {
|
||||||
|
if self.avatarStatic != avatarStatic {
|
||||||
|
self.avatarStatic = avatarStatic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func didUpdate(at networkDate: Date) {
|
||||||
|
self.updatedAt = networkDate
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension MastodonUser {
|
public extension MastodonUser {
|
||||||
|
@ -108,3 +145,44 @@ extension MastodonUser: Managed {
|
||||||
return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)]
|
return [NSSortDescriptor(keyPath: \MastodonUser.createdAt, ascending: false)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MastodonUser {
|
||||||
|
|
||||||
|
static func predicate(domain: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.domain), domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(id: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.id), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, id: String) -> NSPredicate {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonUser.predicate(domain: domain),
|
||||||
|
MastodonUser.predicate(id: id)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(ids: [String]) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K IN %@", #keyPath(MastodonUser.id), ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, ids: [String]) -> NSPredicate {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonUser.predicate(domain: domain),
|
||||||
|
MastodonUser.predicate(ids: ids)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func predicate(username: String) -> NSPredicate {
|
||||||
|
return NSPredicate(format: "%K == %@", #keyPath(MastodonUser.username), username)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func predicate(domain: String, username: String) -> NSPredicate {
|
||||||
|
return NSCompoundPredicate(andPredicateWithSubpredicates: [
|
||||||
|
MastodonUser.predicate(domain: domain),
|
||||||
|
MastodonUser.predicate(username: username)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -54,6 +54,13 @@
|
||||||
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; };
|
DB427DE225BAA00100D1B89D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */; };
|
||||||
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
|
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.swift */; };
|
||||||
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
|
DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; };
|
||||||
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
|
||||||
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
|
||||||
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */; };
|
||||||
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */; };
|
||||||
|
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */; };
|
||||||
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */; };
|
||||||
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */; };
|
||||||
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
DB89B9F725C10FD0008580ED /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
|
||||||
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
DB89B9FE25C10FD0008580ED /* CoreDataStackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB89B9FD25C10FD0008580ED /* CoreDataStackTests.swift */; };
|
||||||
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
DB89BA0025C10FD0008580ED /* CoreDataStack.h in Headers */ = {isa = PBXBuildFile; fileRef = DB89B9F025C10FD0008580ED /* CoreDataStack.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
@ -193,6 +200,13 @@
|
||||||
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MastodonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
|
DB427DF725BAA00100D1B89D /* MastodonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUITests.swift; sourceTree = "<group>"; };
|
||||||
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
|
||||||
|
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
|
||||||
|
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonUser.swift"; sourceTree = "<group>"; };
|
||||||
|
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonUser.swift; sourceTree = "<group>"; };
|
||||||
|
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthentication.swift; sourceTree = "<group>"; };
|
||||||
|
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+MastodonAuthentication.swift"; sourceTree = "<group>"; };
|
||||||
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = "<group>"; };
|
||||||
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataStack.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
DB89B9F025C10FD0008580ED /* CoreDataStack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataStack.h; sourceTree = "<group>"; };
|
||||||
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
DB89B9F125C10FD0008580ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
@ -322,13 +336,8 @@
|
||||||
2D61335525C1886800CAE157 /* Service */ = {
|
2D61335525C1886800CAE157 /* Service */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||||
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
|
||||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
|
||||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
|
||||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
|
||||||
2D61335625C1887F00CAE157 /* Persist */,
|
|
||||||
);
|
);
|
||||||
path = Service;
|
path = Service;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -522,6 +531,30 @@
|
||||||
path = MastodonUITests;
|
path = MastodonUITests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB45FB0425CA87B4005A8AC7 /* APIService */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
2D61335625C1887F00CAE157 /* Persist */,
|
||||||
|
DB45FB0925CA87BC005A8AC7 /* CoreData */,
|
||||||
|
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
||||||
|
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
||||||
|
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
||||||
|
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
||||||
|
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
||||||
|
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
||||||
|
);
|
||||||
|
path = APIService;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB45FB0925CA87BC005A8AC7 /* CoreData */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB45FADC25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift */,
|
||||||
|
DB45FAF825CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift */,
|
||||||
|
);
|
||||||
|
path = CoreData;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
DB89B9EF25C10FD0008580ED /* CoreDataStack */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -566,6 +599,7 @@
|
||||||
2D927F0725C7E9A8004F19B8 /* Tag.swift */,
|
2D927F0725C7E9A8004F19B8 /* Tag.swift */,
|
||||||
2D927F0D25C7E9C9004F19B8 /* History.swift */,
|
2D927F0D25C7E9C9004F19B8 /* History.swift */,
|
||||||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
||||||
|
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
||||||
);
|
);
|
||||||
path = Entity;
|
path = Entity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -621,13 +655,16 @@
|
||||||
DB8AF56225C138BC002E6C99 /* Extension */ = {
|
DB8AF56225C138BC002E6C99 /* Extension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
|
||||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
||||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
|
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
|
||||||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
|
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
|
||||||
2D46976325C2A71500CF4AA9 /* UIIamge.swift */,
|
2D46976325C2A71500CF4AA9 /* UIIamge.swift */,
|
||||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
|
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
|
||||||
2D42FF6A25C817D2004A627A /* MastodonContent.swift */,
|
2D42FF6A25C817D2004A627A /* MastodonContent.swift */,
|
||||||
|
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||||
|
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
|
||||||
);
|
);
|
||||||
path = Extension;
|
path = Extension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -998,10 +1035,12 @@
|
||||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||||
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
||||||
|
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||||
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */,
|
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */,
|
||||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
||||||
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
|
@ -1014,14 +1053,17 @@
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||||
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
||||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||||
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
|
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
|
||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||||
|
@ -1034,6 +1076,7 @@
|
||||||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1069,6 +1112,7 @@
|
||||||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
||||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
||||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
||||||
|
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
||||||
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */,
|
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */,
|
||||||
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
||||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
||||||
|
|
|
@ -39,6 +39,8 @@ extension SceneCoordinator {
|
||||||
enum Scene {
|
enum Scene {
|
||||||
case authentication(viewModel: AuthenticationViewModel)
|
case authentication(viewModel: AuthenticationViewModel)
|
||||||
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
|
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
|
||||||
|
|
||||||
|
case alertController(alertController: UIAlertController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +120,15 @@ private extension SceneCoordinator {
|
||||||
let _viewController = MastodonPinBasedAuthenticationViewController()
|
let _viewController = MastodonPinBasedAuthenticationViewController()
|
||||||
_viewController.viewModel = viewModel
|
_viewController.viewModel = viewModel
|
||||||
viewController = _viewController
|
viewController = _viewController
|
||||||
|
case .alertController(let alertController):
|
||||||
|
if let popoverPresentationController = alertController.popoverPresentationController {
|
||||||
|
assert(
|
||||||
|
popoverPresentationController.sourceView != nil ||
|
||||||
|
popoverPresentationController.sourceRect != .zero ||
|
||||||
|
popoverPresentationController.barButtonItem != nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewController = alertController
|
||||||
}
|
}
|
||||||
|
|
||||||
setupDependency(for: viewController as? NeedsDependency)
|
setupDependency(for: viewController as? NeedsDependency)
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// MastodonUser.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension MastodonUser.Property {
|
||||||
|
init(entity: Mastodon.Entity.Account, domain: String, networkDate: Date) {
|
||||||
|
self.init(
|
||||||
|
id: entity.id,
|
||||||
|
domain: domain,
|
||||||
|
acct: entity.acct,
|
||||||
|
username: entity.username,
|
||||||
|
displayName: entity.displayName,
|
||||||
|
avatar: entity.avatar,
|
||||||
|
avatarStatic: entity.avatarStatic,
|
||||||
|
createdAt: entity.createdAt,
|
||||||
|
networkDate: networkDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
//
|
||||||
|
// UIAlertController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// Reference:
|
||||||
|
// https://nshipster.com/swift-foundation-error-protocols/
|
||||||
|
extension UIAlertController {
|
||||||
|
convenience init(
|
||||||
|
_ error: Error,
|
||||||
|
preferredStyle: UIAlertController.Style
|
||||||
|
) {
|
||||||
|
let title: String
|
||||||
|
let message: String?
|
||||||
|
if let error = error as? LocalizedError {
|
||||||
|
title = error.errorDescription ?? "Unknown Error"
|
||||||
|
message = [
|
||||||
|
error.failureReason,
|
||||||
|
error.recoverySuggestion
|
||||||
|
]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.joined(separator: " ")
|
||||||
|
} else {
|
||||||
|
title = "Internal Error"
|
||||||
|
message = error.localizedDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
preferredStyle: preferredStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// UIBarButtonItem.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIBarButtonItem {
|
||||||
|
|
||||||
|
static var activityIndicatorBarButtonItem: UIBarButtonItem {
|
||||||
|
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||||
|
let barButtonItem = UIBarButtonItem(customView: activityIndicatorView)
|
||||||
|
activityIndicatorView.startAnimating()
|
||||||
|
return barButtonItem
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@ final class AuthenticationViewController: UIViewController, NeedsDependency {
|
||||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
var viewModel: AuthenticationViewModel!
|
var viewModel: AuthenticationViewModel!
|
||||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
|
||||||
|
|
||||||
let domainTextField: UITextField = {
|
let domainTextField: UITextField = {
|
||||||
let textField = UITextField()
|
let textField = UITextField()
|
||||||
|
@ -30,7 +29,7 @@ final class AuthenticationViewController: UIViewController, NeedsDependency {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private(set) lazy var signInBarButtonItem = UIBarButtonItem(title: "Sign In", style: .plain, target: self, action: #selector(AuthenticationViewController.signInBarButtonItemPressed(_:)))
|
private(set) lazy var signInBarButtonItem = UIBarButtonItem(title: "Sign In", style: .plain, target: self, action: #selector(AuthenticationViewController.signInBarButtonItemPressed(_:)))
|
||||||
|
let activityIndicatorBarButtonItem = UIBarButtonItem.activityIndicatorBarButtonItem
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AuthenticationViewController {
|
extension AuthenticationViewController {
|
||||||
|
@ -59,10 +58,59 @@ extension AuthenticationViewController {
|
||||||
.assign(to: \.value, on: viewModel.input)
|
.assign(to: \.value, on: viewModel.input)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.isAuthenticating
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] isAuthenticating in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.navigationItem.rightBarButtonItem = isAuthenticating ? self.activityIndicatorBarButtonItem : self.signInBarButtonItem
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.authenticated
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] domain, user in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// reset view hierarchy only if needs
|
||||||
|
if self.viewModel.viewHierarchyShouldReset {
|
||||||
|
self.context.authenticationService.activeMastodonUser(domain: domain, userID: user.id)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
guard let self = self else { return }
|
||||||
|
switch result {
|
||||||
|
case .failure(let error):
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
case .success(let isActived):
|
||||||
|
assert(isActived)
|
||||||
|
self.coordinator.setup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &self.disposeBag)
|
||||||
|
} else {
|
||||||
|
self.dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
viewModel.isSignInButtonEnabled
|
viewModel.isSignInButtonEnabled
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.isEnabled, on: signInBarButtonItem)
|
.assign(to: \.isEnabled, on: signInBarButtonItem)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
viewModel.error
|
||||||
|
.compactMap { $0 }
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] error in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let alertController = UIAlertController(error, preferredStyle: .alert)
|
||||||
|
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||||
|
alertController.addAction(okAction)
|
||||||
|
self.coordinator.present(
|
||||||
|
scene: .alertController(alertController: alertController),
|
||||||
|
from: nil,
|
||||||
|
transition: .alertController(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
@ -81,7 +129,51 @@ extension AuthenticationViewController {
|
||||||
// TODO: alert error
|
// TODO: alert error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewModel.signInAction.send(domain)
|
guard !viewModel.isAuthenticating.value else { return }
|
||||||
|
viewModel.isAuthenticating.value = true
|
||||||
|
context.apiService.createApplication(domain: domain)
|
||||||
|
.tryMap { response -> AuthenticationViewModel.AuthenticateInfo in
|
||||||
|
let application = response.value
|
||||||
|
guard let clientID = application.clientID,
|
||||||
|
let clientSecret = application.clientSecret else {
|
||||||
|
throw APIService.APIError.explicit(.badResponse)
|
||||||
|
}
|
||||||
|
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
|
||||||
|
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
|
||||||
|
return AuthenticationViewModel.AuthenticateInfo(
|
||||||
|
domain: domain,
|
||||||
|
clientID: clientID,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
url: url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// trigger state update
|
||||||
|
self.viewModel.isAuthenticating.value = false
|
||||||
|
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
self.viewModel.error.value = error
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] info in
|
||||||
|
guard let self = self else { return }
|
||||||
|
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.url)
|
||||||
|
self.viewModel.authenticate(
|
||||||
|
info: info,
|
||||||
|
pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher
|
||||||
|
)
|
||||||
|
self.viewModel.mastodonPinBasedAuthenticationViewController = self.coordinator.present(
|
||||||
|
scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel),
|
||||||
|
from: nil,
|
||||||
|
transition: .modal(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
import os.log
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
import Combine
|
import Combine
|
||||||
import MastodonSDK
|
import MastodonSDK
|
||||||
|
|
||||||
|
@ -17,21 +19,24 @@ final class AuthenticationViewModel {
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let coordinator: SceneCoordinator
|
let coordinator: SceneCoordinator
|
||||||
|
let isAuthenticationExist: Bool
|
||||||
let input = CurrentValueSubject<String, Never>("")
|
let input = CurrentValueSubject<String, Never>("")
|
||||||
let signInAction = PassthroughSubject<String, Never>()
|
|
||||||
|
|
||||||
// output
|
// output
|
||||||
|
let viewHierarchyShouldReset: Bool
|
||||||
let domain = CurrentValueSubject<String?, Never>(nil)
|
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||||
let isSignInButtonEnabled = CurrentValueSubject<Bool, Never>(false)
|
let isSignInButtonEnabled = CurrentValueSubject<Bool, Never>(false)
|
||||||
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
let isAuthenticating = CurrentValueSubject<Bool, Never>(false)
|
||||||
let authenticated = PassthroughSubject<Void, Never>()
|
let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>()
|
||||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||||
|
|
||||||
private var mastodonPinBasedAuthenticationViewController: UIViewController?
|
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||||
|
|
||||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
|
self.isAuthenticationExist = isAuthenticationExist
|
||||||
|
self.viewHierarchyShouldReset = isAuthenticationExist
|
||||||
|
|
||||||
input
|
input
|
||||||
.map { input in
|
.map { input in
|
||||||
|
@ -44,7 +49,10 @@ final class AuthenticationViewModel {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let components = host.components(separatedBy: ".")
|
let components = host.components(separatedBy: ".")
|
||||||
guard (components.filter { !$0.isEmpty }).count >= 2 else { return nil }
|
guard !components.contains(where: { $0.isEmpty }) else { return nil }
|
||||||
|
guard components.count >= 2 else { return nil }
|
||||||
|
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: iput host: %s", ((#file as NSString).lastPathComponent), #line, #function, host)
|
||||||
|
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
@ -55,60 +63,6 @@ final class AuthenticationViewModel {
|
||||||
.map { $0 != nil }
|
.map { $0 != nil }
|
||||||
.assign(to: \.value, on: isSignInButtonEnabled)
|
.assign(to: \.value, on: isSignInButtonEnabled)
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
signInAction
|
|
||||||
.handleEvents(receiveOutput: { [weak self] _ in
|
|
||||||
// trigger state change
|
|
||||||
guard let self = self else { return }
|
|
||||||
self.isAuthenticating.value = true
|
|
||||||
})
|
|
||||||
.flatMap { domain in
|
|
||||||
context.apiService.createApplication(domain: domain)
|
|
||||||
.retry(3)
|
|
||||||
.tryMap { response -> AuthenticateInfo in
|
|
||||||
let application = response.value
|
|
||||||
guard let clientID = application.clientID,
|
|
||||||
let clientSecret = application.clientSecret else {
|
|
||||||
throw APIService.APIError.explicit(.badResponse)
|
|
||||||
}
|
|
||||||
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID)
|
|
||||||
let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
|
|
||||||
return AuthenticateInfo(
|
|
||||||
domain: domain,
|
|
||||||
clientID: clientID,
|
|
||||||
clientSecret: clientSecret,
|
|
||||||
url: url
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] completion in
|
|
||||||
guard let self = self else { return }
|
|
||||||
// trigger state update
|
|
||||||
self.isAuthenticating.value = false
|
|
||||||
|
|
||||||
switch completion {
|
|
||||||
case .failure(let error):
|
|
||||||
// TODO: handle error
|
|
||||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
|
||||||
self.error.value = error
|
|
||||||
case .finished:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} receiveValue: { [weak self] info in
|
|
||||||
guard let self = self else { return }
|
|
||||||
let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.url)
|
|
||||||
self.authenticate(
|
|
||||||
info: info,
|
|
||||||
pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher
|
|
||||||
)
|
|
||||||
self.mastodonPinBasedAuthenticationViewController = self.coordinator.present(
|
|
||||||
scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel),
|
|
||||||
from: nil,
|
|
||||||
transition: .modal(animated: true, completion: nil)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -145,7 +99,7 @@ extension AuthenticationViewModel {
|
||||||
return AuthenticationViewModel.verifyAndSaveAuthentication(
|
return AuthenticationViewModel.verifyAndSaveAuthentication(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
info: info,
|
info: info,
|
||||||
token: token
|
userToken: token
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
|
@ -165,7 +119,9 @@ extension AuthenticationViewModel {
|
||||||
} receiveValue: { [weak self] response in
|
} receiveValue: { [weak self] response in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let account = response.value
|
let account = response.value
|
||||||
// TODO:
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s sign in success", ((#file as NSString).lastPathComponent), #line, #function, account.username)
|
||||||
|
|
||||||
|
self.authenticated.send((domain: info.domain, account: account))
|
||||||
}
|
}
|
||||||
.store(in: &self.disposeBag)
|
.store(in: &self.disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -173,14 +129,52 @@ extension AuthenticationViewModel {
|
||||||
static func verifyAndSaveAuthentication(
|
static func verifyAndSaveAuthentication(
|
||||||
context: AppContext,
|
context: AppContext,
|
||||||
info: AuthenticateInfo,
|
info: AuthenticateInfo,
|
||||||
token: Mastodon.Entity.Token
|
userToken: Mastodon.Entity.Token
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: token.accessToken)
|
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
|
||||||
|
let managedObjectContext = context.backgroundManagedObjectContext
|
||||||
|
|
||||||
return context.apiService.accountVerifyCredentials(
|
return context.apiService.accountVerifyCredentials(
|
||||||
domain: info.domain,
|
domain: info.domain,
|
||||||
authorization: authorization
|
authorization: authorization
|
||||||
)
|
)
|
||||||
// TODO: add persist logic
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||||
|
let account = response.value
|
||||||
|
let mastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||||
|
mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id)
|
||||||
|
mastodonUserRequest.fetchLimit = 1
|
||||||
|
guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else {
|
||||||
|
return Fail(error: APIService.APIError.explicit(.badCredentials)).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
let property = MastodonAuthentication.Property(
|
||||||
|
domain: info.domain,
|
||||||
|
userID: mastodonUser.id,
|
||||||
|
username: mastodonUser.username,
|
||||||
|
appAccessToken: userToken.accessToken, // TODO: swap app token
|
||||||
|
userAccessToken: userToken.accessToken,
|
||||||
|
clientID: info.clientID,
|
||||||
|
clientSecret: info.clientSecret
|
||||||
|
)
|
||||||
|
return managedObjectContext.performChanges {
|
||||||
|
_ = APIService.CoreData.createOrMergeMastodonAuthentication(
|
||||||
|
into: managedObjectContext,
|
||||||
|
for: mastodonUser,
|
||||||
|
in: info.domain,
|
||||||
|
property: property,
|
||||||
|
networkDate: response.networkDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.tryMap { result in
|
||||||
|
switch result {
|
||||||
|
case .failure(let error): throw error
|
||||||
|
case .success: return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,26 @@ extension MainTabBarController {
|
||||||
tabBarAppearance.configureWithDefaultBackground()
|
tabBarAppearance.configureWithDefaultBackground()
|
||||||
tabBar.standardAppearance = tabBarAppearance
|
tabBar.standardAppearance = tabBarAppearance
|
||||||
|
|
||||||
|
context.apiService.error
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] error in
|
||||||
|
guard let self = self, let coordinator = self.coordinator else { return }
|
||||||
|
switch error {
|
||||||
|
case .implicit:
|
||||||
|
break
|
||||||
|
case .explicit:
|
||||||
|
let alertController = UIAlertController(error, preferredStyle: .alert)
|
||||||
|
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||||
|
alertController.addAction(okAction)
|
||||||
|
coordinator.present(
|
||||||
|
scene: .alertController(alertController: alertController),
|
||||||
|
from: nil,
|
||||||
|
transition: .alertController(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// selectedIndex = 1
|
// selectedIndex = 1
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
//
|
|
||||||
// APIService+Error.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by Cirno MainasuK on 2021-2-2.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import MastodonSDK
|
|
||||||
|
|
||||||
extension APIService {
|
|
||||||
enum APIError: Error {
|
|
||||||
|
|
||||||
case implicit(ErrorReason)
|
|
||||||
case explicit(ErrorReason)
|
|
||||||
|
|
||||||
enum ErrorReason {
|
|
||||||
// application internal error
|
|
||||||
case authenticationMissing
|
|
||||||
case badRequest
|
|
||||||
case badResponse
|
|
||||||
case requestThrottle
|
|
||||||
|
|
||||||
// Server API error
|
|
||||||
case mastodonAPIError(Mastodon.API.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
//
|
|
||||||
// APIService+Account.swift
|
|
||||||
// Mastodon
|
|
||||||
//
|
|
||||||
// Created by MainasuK Cirno on 2021/2/2.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
import MastodonSDK
|
|
||||||
|
|
||||||
extension APIService {
|
|
||||||
|
|
||||||
func accountVerifyCredentials(
|
|
||||||
domain: String,
|
|
||||||
authorization: Mastodon.API.OAuth.Authorization
|
|
||||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
|
||||||
return Mastodon.API.Account.verifyCredentials(
|
|
||||||
session: session,
|
|
||||||
domain: domain,
|
|
||||||
authorization: authorization
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// APIService+Error.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by Cirno MainasuK on 2021-2-2.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
enum APIError: Error {
|
||||||
|
|
||||||
|
case implicit(ErrorReason)
|
||||||
|
case explicit(ErrorReason)
|
||||||
|
|
||||||
|
enum ErrorReason {
|
||||||
|
// application internal error
|
||||||
|
case authenticationMissing
|
||||||
|
case badCredentials
|
||||||
|
case badRequest
|
||||||
|
case badResponse
|
||||||
|
case requestThrottle
|
||||||
|
|
||||||
|
// Server API error
|
||||||
|
case mastodonAPIError(Mastodon.API.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var errorReason: ErrorReason {
|
||||||
|
switch self {
|
||||||
|
case .implicit(let errorReason): return errorReason
|
||||||
|
case .explicit(let errorReason): return errorReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - LocalizedError
|
||||||
|
extension APIService.APIError: LocalizedError {
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch errorReason {
|
||||||
|
case .authenticationMissing: return "Fail to Authenticatie"
|
||||||
|
case .badCredentials: return "Bad Credentials"
|
||||||
|
case .badRequest: return "Bad Request"
|
||||||
|
case .badResponse: return "Bad Response"
|
||||||
|
case .requestThrottle: return "Request Throttled"
|
||||||
|
case .mastodonAPIError(let error):
|
||||||
|
guard let responseError = error.mastodonError else {
|
||||||
|
guard error.httpResponseStatus != .ok else {
|
||||||
|
return "Unknown Error"
|
||||||
|
}
|
||||||
|
return error.httpResponseStatus.reasonPhrase
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseError.errorDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var failureReason: String? {
|
||||||
|
switch errorReason {
|
||||||
|
case .authenticationMissing: return "Account credential not found."
|
||||||
|
case .badCredentials: return "Credentials invalid."
|
||||||
|
case .badRequest: return "Request invalid."
|
||||||
|
case .badResponse: return "Response invalid."
|
||||||
|
case .requestThrottle: return "Request too frequency."
|
||||||
|
case .mastodonAPIError(let error):
|
||||||
|
guard let responseError = error.mastodonError else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return responseError.failureReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var helpAnchor: String? {
|
||||||
|
switch errorReason {
|
||||||
|
case .authenticationMissing: return "Please request after authenticated."
|
||||||
|
case .badCredentials: return "Please try again.."
|
||||||
|
case .badRequest: return "Please try again."
|
||||||
|
case .badResponse: return "Please try again."
|
||||||
|
case .requestThrottle: return "Please try again later."
|
||||||
|
case .mastodonAPIError(let error):
|
||||||
|
guard let responseError = error.mastodonError else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return responseError.helpAnchor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
// APIService+Account.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/2.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CommonOSLog
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func accountVerifyCredentials(
|
||||||
|
domain: String,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||||
|
return Mastodon.API.Account.verifyCredentials(
|
||||||
|
session: session,
|
||||||
|
domain: domain,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||||
|
let log = OSLog.api
|
||||||
|
let account = response.value
|
||||||
|
|
||||||
|
return self.backgroundManagedObjectContext.performChanges {
|
||||||
|
let (mastodonUser, isCreated) = APIService.CoreData.createOrMergeMastodonUser(
|
||||||
|
into: self.backgroundManagedObjectContext,
|
||||||
|
for: nil,
|
||||||
|
in: domain,
|
||||||
|
entity: account,
|
||||||
|
networkDate: response.networkDate,
|
||||||
|
log: log)
|
||||||
|
let flag = isCreated ? "+" : "-"
|
||||||
|
os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user [%s](%s)%s verifed", ((#file as NSString).lastPathComponent), #line, #function, flag, mastodonUser.id, mastodonUser.username)
|
||||||
|
}
|
||||||
|
.setFailureType(to: Error.self)
|
||||||
|
.map { _ in return response }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ final class APIService {
|
||||||
// input
|
// input
|
||||||
let backgroundManagedObjectContext: NSManagedObjectContext
|
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
|
// output
|
||||||
|
let error = PassthroughSubject<APIError, Never>()
|
||||||
|
|
||||||
init(backgroundManagedObjectContext: NSManagedObjectContext) {
|
init(backgroundManagedObjectContext: NSManagedObjectContext) {
|
||||||
self.backgroundManagedObjectContext = backgroundManagedObjectContext
|
self.backgroundManagedObjectContext = backgroundManagedObjectContext
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// APIService+CoreData+MastodonAuthentication.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService.CoreData {
|
||||||
|
|
||||||
|
static func createOrMergeMastodonAuthentication(
|
||||||
|
into managedObjectContext: NSManagedObjectContext,
|
||||||
|
for authenticateMastodonUser: MastodonUser,
|
||||||
|
in domain: String,
|
||||||
|
property: MastodonAuthentication.Property,
|
||||||
|
networkDate: Date
|
||||||
|
) -> (mastodonAuthentication: MastodonAuthentication, isCreated: Bool) {
|
||||||
|
// fetch old mastodon authentication
|
||||||
|
let oldMastodonAuthentication: MastodonAuthentication? = {
|
||||||
|
let request = MastodonAuthentication.sortedFetchRequest
|
||||||
|
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: property.userID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if let oldMastodonAuthentication = oldMastodonAuthentication {
|
||||||
|
// merge old mastodon authentication
|
||||||
|
APIService.CoreData.mergeMastodonAuthentication(
|
||||||
|
for: authenticateMastodonUser,
|
||||||
|
old: oldMastodonAuthentication,
|
||||||
|
in: domain,
|
||||||
|
property: property,
|
||||||
|
networkDate: networkDate
|
||||||
|
)
|
||||||
|
return (oldMastodonAuthentication, false)
|
||||||
|
} else {
|
||||||
|
let mastodonAuthentication = MastodonAuthentication.insert(
|
||||||
|
into: managedObjectContext,
|
||||||
|
property: property,
|
||||||
|
user: authenticateMastodonUser
|
||||||
|
)
|
||||||
|
return (mastodonAuthentication, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func mergeMastodonAuthentication(
|
||||||
|
for authenticateMastodonUser: MastodonUser,
|
||||||
|
old authentication: MastodonAuthentication,
|
||||||
|
in domain: String,
|
||||||
|
property: MastodonAuthentication.Property,
|
||||||
|
networkDate: Date
|
||||||
|
) {
|
||||||
|
guard networkDate > authentication.updatedAt else { return }
|
||||||
|
|
||||||
|
|
||||||
|
authentication.update(username: property.username)
|
||||||
|
authentication.update(appAccessToken: property.appAccessToken)
|
||||||
|
authentication.update(userAccessToken: property.userAccessToken)
|
||||||
|
authentication.update(clientID: property.clientID)
|
||||||
|
authentication.update(clientSecret: property.clientSecret)
|
||||||
|
|
||||||
|
authentication.didUpdate(at: networkDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// APIService+CoreData+MastodonUser.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension APIService.CoreData {
|
||||||
|
|
||||||
|
static func createOrMergeMastodonUser(
|
||||||
|
into managedObjectContext: NSManagedObjectContext,
|
||||||
|
for requestMastodonUser: MastodonUser?,
|
||||||
|
in domain: String,
|
||||||
|
entity: Mastodon.Entity.Account,
|
||||||
|
networkDate: Date,
|
||||||
|
log: OSLog
|
||||||
|
) -> (user: MastodonUser, isCreated: Bool) {
|
||||||
|
let processEntityTaskSignpostID = OSSignpostID(log: log)
|
||||||
|
os_signpost(.begin, log: log, name: "update database - process entity: createOrMergeMastodonUser", signpostID: processEntityTaskSignpostID, "process mastodon user %{public}s", entity.id)
|
||||||
|
defer {
|
||||||
|
os_signpost(.end, log: log, name: "update database - process entity: createOrMergeMastodonUser", signpostID: processEntityTaskSignpostID, "process msstodon user %{public}s", entity.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch old mastodon user
|
||||||
|
let oldMastodonUser: MastodonUser? = {
|
||||||
|
let request = MastodonUser.sortedFetchRequest
|
||||||
|
request.predicate = MastodonUser.predicate(domain: domain, id: entity.id)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if let oldMastodonUser = oldMastodonUser {
|
||||||
|
// merge old mastodon usre
|
||||||
|
APIService.CoreData.mergeMastodonUser(
|
||||||
|
for: requestMastodonUser,
|
||||||
|
old: oldMastodonUser,
|
||||||
|
in: domain,
|
||||||
|
entity: entity,
|
||||||
|
networkDate: networkDate
|
||||||
|
)
|
||||||
|
return (oldMastodonUser, false)
|
||||||
|
} else {
|
||||||
|
let mastodonUserProperty = MastodonUser.Property(entity: entity, domain: domain, networkDate: networkDate)
|
||||||
|
let mastodonUser = MastodonUser.insert(
|
||||||
|
into: managedObjectContext,
|
||||||
|
property: mastodonUserProperty
|
||||||
|
)
|
||||||
|
|
||||||
|
os_signpost(.event, log: log, name: "update database - process entity: createOrMergeMastodonUser", signpostID: processEntityTaskSignpostID, "did insert new mastodon user %{public}s: name %s", mastodonUser.identifier, mastodonUser.username)
|
||||||
|
return (mastodonUser, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func mergeMastodonUser(
|
||||||
|
for requestMastodonUser: MastodonUser?,
|
||||||
|
old user: MastodonUser,
|
||||||
|
in domain: String,
|
||||||
|
entity: Mastodon.Entity.Account,
|
||||||
|
networkDate: Date
|
||||||
|
) {
|
||||||
|
guard networkDate > user.updatedAt else { return }
|
||||||
|
let property = MastodonUser.Property(entity: entity, domain: domain, networkDate: networkDate)
|
||||||
|
|
||||||
|
// only fulfill API supported fields
|
||||||
|
user.update(acct: property.acct)
|
||||||
|
user.update(username: property.username)
|
||||||
|
user.update(displayName: property.displayName)
|
||||||
|
user.update(avatar: property.avatar)
|
||||||
|
user.update(avatarStatic: property.avatarStatic)
|
||||||
|
|
||||||
|
user.didUpdate(at: networkDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
//
|
||||||
|
// AuthenticationService.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021/2/3.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
class AuthenticationService: NSObject {
|
||||||
|
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
// input
|
||||||
|
weak var apiService: APIService?
|
||||||
|
let managedObjectContext: NSManagedObjectContext // read-only
|
||||||
|
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||||
|
let mastodonAuthenticationFetchedResultsController: NSFetchedResultsController<MastodonAuthentication>
|
||||||
|
|
||||||
|
// output
|
||||||
|
let mastodonAuthentications = CurrentValueSubject<[MastodonAuthentication], Never>([])
|
||||||
|
let activeMastodonAuthentication = CurrentValueSubject<MastodonAuthentication?, Never>(nil)
|
||||||
|
let activeMastodonAuthenticationBox = CurrentValueSubject<AuthenticationService.MastodonAuthenticationBox?, Never>(nil)
|
||||||
|
|
||||||
|
init(
|
||||||
|
managedObjectContext: NSManagedObjectContext,
|
||||||
|
backgroundManagedObjectContext: NSManagedObjectContext,
|
||||||
|
apiService: APIService
|
||||||
|
) {
|
||||||
|
self.managedObjectContext = managedObjectContext
|
||||||
|
self.backgroundManagedObjectContext = backgroundManagedObjectContext
|
||||||
|
self.apiService = apiService
|
||||||
|
self.mastodonAuthenticationFetchedResultsController = {
|
||||||
|
let fetchRequest = MastodonAuthentication.sortedFetchRequest
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.fetchBatchSize = 20
|
||||||
|
let controller = NSFetchedResultsController(
|
||||||
|
fetchRequest: fetchRequest,
|
||||||
|
managedObjectContext: managedObjectContext,
|
||||||
|
sectionNameKeyPath: nil,
|
||||||
|
cacheName: nil
|
||||||
|
)
|
||||||
|
return controller
|
||||||
|
}()
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
mastodonAuthenticationFetchedResultsController.delegate = self
|
||||||
|
|
||||||
|
// TODO: verify credentials for active authentication
|
||||||
|
|
||||||
|
// bind data
|
||||||
|
mastodonAuthentications
|
||||||
|
.map { $0.sorted(by: { $0.activedAt > $1.activedAt }).first }
|
||||||
|
.assign(to: \.value, on: activeMastodonAuthentication)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
activeMastodonAuthentication
|
||||||
|
.map { authentication -> AuthenticationService.MastodonAuthenticationBox? in
|
||||||
|
guard let authentication = authentication else { return nil }
|
||||||
|
return AuthenticationService.MastodonAuthenticationBox(
|
||||||
|
userID: authentication.userID,
|
||||||
|
appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken),
|
||||||
|
userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.assign(to: \.value, on: activeMastodonAuthenticationBox)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try mastodonAuthenticationFetchedResultsController.performFetch()
|
||||||
|
mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? []
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationService {
|
||||||
|
struct MastodonAuthenticationBox {
|
||||||
|
let userID: MastodonUser.ID
|
||||||
|
let appAuthorization: Mastodon.API.OAuth.Authorization
|
||||||
|
let userAuthorization: Mastodon.API.OAuth.Authorization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AuthenticationService {
|
||||||
|
|
||||||
|
func activeMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
||||||
|
var isActived = false
|
||||||
|
|
||||||
|
return backgroundManagedObjectContext.performChanges {
|
||||||
|
let request = MastodonAuthentication.sortedFetchRequest
|
||||||
|
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
guard let mastodonAutentication = try? self.backgroundManagedObjectContext.fetch(request).first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mastodonAutentication.update(activedAt: Date())
|
||||||
|
isActived = true
|
||||||
|
}
|
||||||
|
.map { result in
|
||||||
|
return result.map { isActived }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signOutMastodonUser(domain: String, userID: MastodonUser.ID) -> AnyPublisher<Result<Bool, Error>, Never> {
|
||||||
|
var isSignOut = false
|
||||||
|
|
||||||
|
return backgroundManagedObjectContext.performChanges {
|
||||||
|
let request = MastodonAuthentication.sortedFetchRequest
|
||||||
|
request.predicate = MastodonAuthentication.predicate(domain: domain, userID: userID)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
guard let mastodonAutentication = try? self.backgroundManagedObjectContext.fetch(request).first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.backgroundManagedObjectContext.delete(mastodonAutentication)
|
||||||
|
isSignOut = true
|
||||||
|
}
|
||||||
|
.map { result in
|
||||||
|
return result.map { isSignOut }
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - NSFetchedResultsControllerDelegate
|
||||||
|
extension AuthenticationService: NSFetchedResultsControllerDelegate {
|
||||||
|
|
||||||
|
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||||
|
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
}
|
||||||
|
|
||||||
|
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||||
|
if controller === mastodonAuthenticationFetchedResultsController {
|
||||||
|
mastodonAuthentications.value = mastodonAuthenticationFetchedResultsController.fetchedObjects ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ class AppContext: ObservableObject {
|
||||||
let backgroundManagedObjectContext: NSManagedObjectContext
|
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
let apiService: APIService
|
let apiService: APIService
|
||||||
|
let authenticationService: AuthenticationService
|
||||||
|
|
||||||
let documentStore: DocumentStore
|
let documentStore: DocumentStore
|
||||||
private var documentStoreSubscription: AnyCancellable!
|
private var documentStoreSubscription: AnyCancellable!
|
||||||
|
@ -39,6 +40,11 @@ class AppContext: ObservableObject {
|
||||||
let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext)
|
let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext)
|
||||||
apiService = _apiService
|
apiService = _apiService
|
||||||
|
|
||||||
|
authenticationService = AuthenticationService(
|
||||||
|
managedObjectContext: _managedObjectContext,
|
||||||
|
backgroundManagedObjectContext: _backgroundManagedObjectContext,
|
||||||
|
apiService: _apiService
|
||||||
|
)
|
||||||
|
|
||||||
documentStore = DocumentStore()
|
documentStore = DocumentStore()
|
||||||
documentStoreSubscription = documentStore.objectWillChange
|
documentStoreSubscription = documentStore.objectWillChange
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
|
@ -25,12 +26,25 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
sceneCoordinator.setup()
|
sceneCoordinator.setup()
|
||||||
|
|
||||||
#if DEBUG
|
do {
|
||||||
|
let request = MastodonAuthentication.sortedFetchRequest
|
||||||
|
if try appContext.managedObjectContext.fetch(request).isEmpty {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let authenticationViewModel = AuthenticationViewModel(context: appContext, coordinator: sceneCoordinator)
|
let authenticationViewModel = AuthenticationViewModel(
|
||||||
sceneCoordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: false, completion: nil))
|
context: appContext,
|
||||||
|
coordinator: sceneCoordinator,
|
||||||
|
isAuthenticationExist: false
|
||||||
|
)
|
||||||
|
sceneCoordinator.present(
|
||||||
|
scene: .authentication(viewModel: authenticationViewModel),
|
||||||
|
from: nil,
|
||||||
|
transition: .modal(animated: false, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,3 +16,22 @@ extension Mastodon.API.Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - LocalizedError
|
||||||
|
extension Mastodon.API.Error.MastodonError: LocalizedError {
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .generic(let error):
|
||||||
|
return error.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var failureReason: String? {
|
||||||
|
switch self {
|
||||||
|
case .generic(let error):
|
||||||
|
return error.errorDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Combine
|
||||||
extension Mastodon.API.Account {
|
extension Mastodon.API.Account {
|
||||||
|
|
||||||
static func verifyCredentialsEndpointURL(domain: String) -> URL {
|
static func verifyCredentialsEndpointURL(domain: String) -> URL {
|
||||||
return Mastodon.API.endpointURL(domain: domain)
|
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/verify_credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func verifyCredentials(
|
public static func verifyCredentials(
|
||||||
|
|
|
@ -13,7 +13,7 @@ extension Mastodon.Entity {
|
||||||
/// - Since: 1.5.0
|
/// - Since: 1.5.0
|
||||||
/// - Version: 3.3.0
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/1/28
|
/// 2021/2/3
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/source/)
|
/// [Document](https://docs.joinmastodon.org/entities/source/)
|
||||||
public struct Source: Codable {
|
public struct Source: Codable {
|
||||||
|
@ -25,7 +25,7 @@ extension Mastodon.Entity {
|
||||||
public let privacy: Privacy?
|
public let privacy: Privacy?
|
||||||
public let sensitive: Bool?
|
public let sensitive: Bool?
|
||||||
public let language: String? // (ISO 639-1 language two-letter code)
|
public let language: String? // (ISO 639-1 language two-letter code)
|
||||||
public let followRequestsCount: String
|
public let followRequestsCount: Int?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case note
|
case note
|
||||||
|
|
Loading…
Reference in New Issue