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"?>
|
||||
<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">
|
||||
<attribute name="category" optional="YES" attributeType="String"/>
|
||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||
|
@ -25,6 +25,20 @@
|
|||
<attribute name="userIdentifier" attributeType="String"/>
|
||||
<relationship name="toots" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="homeTimelineIndexes" inverseEntity="Toot"/>
|
||||
</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">
|
||||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="String"/>
|
||||
|
@ -38,6 +52,7 @@
|
|||
<attribute name="username" attributeType="String"/>
|
||||
<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="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="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"/>
|
||||
|
@ -97,9 +112,10 @@
|
|||
<element name="Emoji" positionX="45" positionY="135" width="128" height="149"/>
|
||||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||
<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="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
<element name="Toot" positionX="0" positionY="0" width="128" height="494"/>
|
||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||
</elements>
|
||||
</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 Foundation
|
||||
|
||||
public final class MastodonUser: NSManagedObject {
|
||||
final public class MastodonUser: NSManagedObject {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
@NSManaged public private(set) var identifier: ID
|
||||
@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 username: String
|
||||
@NSManaged public private(set) var displayName: String
|
||||
|
@ -25,6 +27,7 @@ public final class MastodonUser: NSManagedObject {
|
|||
|
||||
// one-to-one relationship
|
||||
@NSManaged public private(set) var pinnedToot: Toot?
|
||||
@NSManaged public private(set) var mastodonAuthentication: MastodonAuthentication?
|
||||
|
||||
// one-to-many relationship
|
||||
@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 retweets: Set<Toot>?
|
||||
|
||||
}
|
||||
|
||||
public extension MastodonUser {
|
||||
extension MastodonUser {
|
||||
|
||||
@discardableResult
|
||||
static func insert(
|
||||
public static func insert(
|
||||
into context: NSManagedObjectContext,
|
||||
property: Property
|
||||
) -> MastodonUser {
|
||||
|
@ -61,6 +66,38 @@ public extension MastodonUser {
|
|||
|
||||
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 {
|
||||
|
@ -108,3 +145,44 @@ extension MastodonUser: Managed {
|
|||
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 */; };
|
||||
DB427DED25BAA00100D1B89D /* MastodonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DEC25BAA00100D1B89D /* MastodonTests.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 */; };
|
||||
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, ); }; };
|
||||
|
@ -193,6 +200,13 @@
|
|||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
@ -322,13 +336,8 @@
|
|||
2D61335525C1886800CAE157 /* Service */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */,
|
||||
DB98336A25C9420100AD9700 /* APIService+App.swift */,
|
||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */,
|
||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */,
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
||||
2D61335625C1887F00CAE157 /* Persist */,
|
||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -522,6 +531,30 @@
|
|||
path = MastodonUITests;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -566,6 +599,7 @@
|
|||
2D927F0725C7E9A8004F19B8 /* Tag.swift */,
|
||||
2D927F0D25C7E9C9004F19B8 /* History.swift */,
|
||||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
||||
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
||||
);
|
||||
path = Entity;
|
||||
sourceTree = "<group>";
|
||||
|
@ -621,13 +655,16 @@
|
|||
DB8AF56225C138BC002E6C99 /* Extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */,
|
||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */,
|
||||
2D46975D25C2A54100CF4AA9 /* NSLayoutConstraint.swift */,
|
||||
2D46976325C2A71500CF4AA9 /* UIIamge.swift */,
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */,
|
||||
2D42FF6A25C817D2004A627A /* MastodonContent.swift */,
|
||||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
|
@ -998,10 +1035,12 @@
|
|||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */,
|
||||
2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */,
|
||||
2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */,
|
||||
2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */,
|
||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
|
@ -1014,14 +1053,17 @@
|
|||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||
|
@ -1034,6 +1076,7 @@
|
|||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1069,6 +1112,7 @@
|
|||
DB89BA4425C1165F008580ED /* Managed.swift in Sources */,
|
||||
DB89BA4325C1165F008580ED /* NetworkUpdatable.swift in Sources */,
|
||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */,
|
||||
DB45FAED25CA7A9A005A8AC7 /* MastodonAuthentication.swift in Sources */,
|
||||
2D927F0225C7E4F2004F19B8 /* Mention.swift in Sources */,
|
||||
DB89BA1D25C1107F008580ED /* URL.swift in Sources */,
|
||||
2D927F0825C7E9A8004F19B8 /* Tag.swift in Sources */,
|
||||
|
|
|
@ -39,6 +39,8 @@ extension SceneCoordinator {
|
|||
enum Scene {
|
||||
case authentication(viewModel: AuthenticationViewModel)
|
||||
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
|
||||
|
||||
case alertController(alertController: UIAlertController)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,6 +120,15 @@ private extension SceneCoordinator {
|
|||
let _viewController = MastodonPinBasedAuthenticationViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
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)
|
||||
|
|
|
@ -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) } }
|
||||
|
||||
var viewModel: AuthenticationViewModel!
|
||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||
|
||||
let domainTextField: 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(_:)))
|
||||
|
||||
let activityIndicatorBarButtonItem = UIBarButtonItem.activityIndicatorBarButtonItem
|
||||
}
|
||||
|
||||
extension AuthenticationViewController {
|
||||
|
@ -59,10 +58,59 @@ extension AuthenticationViewController {
|
|||
.assign(to: \.value, on: viewModel.input)
|
||||
.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
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: signInBarButtonItem)
|
||||
.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) {
|
||||
|
@ -81,7 +129,51 @@ extension AuthenticationViewController {
|
|||
// TODO: alert error
|
||||
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 UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
|
@ -17,21 +19,24 @@ final class AuthenticationViewModel {
|
|||
// input
|
||||
let context: AppContext
|
||||
let coordinator: SceneCoordinator
|
||||
let isAuthenticationExist: Bool
|
||||
let input = CurrentValueSubject<String, Never>("")
|
||||
let signInAction = PassthroughSubject<String, Never>()
|
||||
|
||||
// output
|
||||
let viewHierarchyShouldReset: Bool
|
||||
let domain = CurrentValueSubject<String?, Never>(nil)
|
||||
let isSignInButtonEnabled = 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)
|
||||
|
||||
private var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator) {
|
||||
init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) {
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
self.isAuthenticationExist = isAuthenticationExist
|
||||
self.viewHierarchyShouldReset = isAuthenticationExist
|
||||
|
||||
input
|
||||
.map { input in
|
||||
|
@ -44,7 +49,10 @@ final class AuthenticationViewModel {
|
|||
return nil
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -55,60 +63,6 @@ final class AuthenticationViewModel {
|
|||
.map { $0 != nil }
|
||||
.assign(to: \.value, on: isSignInButtonEnabled)
|
||||
.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(
|
||||
context: self.context,
|
||||
info: info,
|
||||
token: token
|
||||
userToken: token
|
||||
)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
|
@ -165,7 +119,9 @@ extension AuthenticationViewModel {
|
|||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
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)
|
||||
}
|
||||
|
@ -173,14 +129,52 @@ extension AuthenticationViewModel {
|
|||
static func verifyAndSaveAuthentication(
|
||||
context: AppContext,
|
||||
info: AuthenticateInfo,
|
||||
token: Mastodon.Entity.Token
|
||||
userToken: Mastodon.Entity.Token
|
||||
) -> 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(
|
||||
domain: info.domain,
|
||||
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()
|
||||
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
|
||||
// selectedIndex = 1
|
||||
#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
|
||||
let backgroundManagedObjectContext: NSManagedObjectContext
|
||||
|
||||
// output
|
||||
let error = PassthroughSubject<APIError, Never>()
|
||||
|
||||
init(backgroundManagedObjectContext: NSManagedObjectContext) {
|
||||
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 apiService: APIService
|
||||
let authenticationService: AuthenticationService
|
||||
|
||||
let documentStore: DocumentStore
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
@ -39,6 +40,11 @@ class AppContext: ObservableObject {
|
|||
let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext)
|
||||
apiService = _apiService
|
||||
|
||||
authenticationService = AuthenticationService(
|
||||
managedObjectContext: _managedObjectContext,
|
||||
backgroundManagedObjectContext: _backgroundManagedObjectContext,
|
||||
apiService: _apiService
|
||||
)
|
||||
|
||||
documentStore = DocumentStore()
|
||||
documentStoreSubscription = documentStore.objectWillChange
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
|
@ -25,12 +26,25 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
|
||||
sceneCoordinator.setup()
|
||||
|
||||
#if DEBUG
|
||||
do {
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
if try appContext.managedObjectContext.fetch(request).isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
let authenticationViewModel = AuthenticationViewModel(context: appContext, coordinator: sceneCoordinator)
|
||||
sceneCoordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: false, completion: nil))
|
||||
let authenticationViewModel = AuthenticationViewModel(
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
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(
|
||||
|
|
|
@ -13,7 +13,7 @@ extension Mastodon.Entity {
|
|||
/// - Since: 1.5.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// 2021/2/3
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/source/)
|
||||
public struct Source: Codable {
|
||||
|
@ -25,7 +25,7 @@ extension Mastodon.Entity {
|
|||
public let privacy: Privacy?
|
||||
public let sensitive: Bool?
|
||||
public let language: String? // (ISO 639-1 language two-letter code)
|
||||
public let followRequestsCount: String
|
||||
public let followRequestsCount: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case note
|
||||
|
|
Loading…
Reference in New Issue