forked from zelo72/mastodon-ios
Merge pull request #4 from tootsuite/feature/authentication into /develop
Add authentication scene
This commit is contained in:
commit
3e1d2bcc16
|
@ -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)
|
||||
])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ public extension Toot {
|
|||
}
|
||||
|
||||
if let emojis = property.emojis {
|
||||
toot.mutableSetValue(forKey: #keyPath(Toot.mentions)).addObjects(from: emojis)
|
||||
toot.mutableSetValue(forKey: #keyPath(Toot.emojis)).addObjects(from: emojis)
|
||||
}
|
||||
|
||||
if let tags = property.tags {
|
||||
|
@ -123,10 +123,11 @@ public extension Toot {
|
|||
if let bookmarkedBy = property.bookmarkedBy {
|
||||
toot.mutableSetValue(forKey: #keyPath(Toot.bookmarkedBy)).add(bookmarkedBy)
|
||||
}
|
||||
if let pinnedBy = property.pinnedBy {
|
||||
toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy))
|
||||
}
|
||||
|
||||
|
||||
// TODO: not implement yet
|
||||
// if let pinnedBy = property.pinnedBy {
|
||||
// toot.mutableSetValue(forKey: #keyPath(Toot.pinnedBy))
|
||||
// }
|
||||
|
||||
toot.updatedAt = property.updatedAt
|
||||
toot.deletedAt = property.deletedAt
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */; };
|
||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
||||
|
@ -33,13 +34,19 @@
|
|||
2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; };
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; };
|
||||
3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; };
|
||||
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
|
||||
7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; };
|
||||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */; };
|
||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; };
|
||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; };
|
||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; };
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; };
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; };
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||
DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; };
|
||||
DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102225BAA7B400EAA174 /* Assets.swift */; };
|
||||
DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102325BAA7B400EAA174 /* Strings.swift */; };
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; };
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; };
|
||||
DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; };
|
||||
|
@ -47,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, ); }; };
|
||||
|
@ -70,6 +84,13 @@
|
|||
DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55625C137A8002E6C99 /* HomeViewController.swift */; };
|
||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
|
||||
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
|
||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */; };
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; };
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -149,17 +170,23 @@
|
|||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
||||
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
|
||||
2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
|
||||
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = "<group>"; };
|
||||
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = "<group>"; };
|
||||
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = "<group>"; };
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||
DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB3D102225BAA7B400EAA174 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||
DB3D102325BAA7B400EAA174 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||
DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -173,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>"; };
|
||||
|
@ -198,6 +232,13 @@
|
|||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
|
||||
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
|
||||
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
|
||||
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
|
||||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
|
||||
DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||
DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
||||
DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = "<group>"; };
|
||||
DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = "<group>"; };
|
||||
EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -208,12 +249,14 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */,
|
||||
DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */,
|
||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */,
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -222,6 +265,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */,
|
||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -230,6 +274,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */,
|
||||
18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -291,9 +336,8 @@
|
|||
2D61335525C1886800CAE157 /* Service */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2D61335D25C1894B00CAE157 /* APIService.swift */,
|
||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */,
|
||||
2D61335625C1887F00CAE157 /* Persist */,
|
||||
DB45FB0425CA87B4005A8AC7 /* APIService */,
|
||||
DB45FB0E25CA87D0005A8AC7 /* AuthenticationService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -356,6 +400,8 @@
|
|||
2D7631A625C1533800929FB9 /* TableviewCell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */,
|
||||
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */,
|
||||
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
|
||||
);
|
||||
path = TableviewCell;
|
||||
|
@ -369,16 +415,36 @@
|
|||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4E8E8B18DB8471A676012CF9 /* Frameworks */ = {
|
||||
3FE14AD363ED19AE7FF210A6 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */,
|
||||
A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */,
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */,
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */,
|
||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */,
|
||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB01409B25C40BB600F9F3CF /* Authentication */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0140A625C40C0900F9F3CF /* PinBased */,
|
||||
DB01409525C40B6700F9F3CF /* AuthenticationViewController.swift */,
|
||||
DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */,
|
||||
);
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB0140A625C40C0900F9F3CF /* PinBased */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */,
|
||||
DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */,
|
||||
DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */,
|
||||
);
|
||||
path = PinBased;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB3D0FF725BAA68500EAA174 /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -399,15 +465,6 @@
|
|||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB3D101B25BAA79200EAA174 /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB3D102225BAA7B400EAA174 /* Assets.swift */,
|
||||
DB3D102325BAA7B400EAA174 /* Strings.swift */,
|
||||
);
|
||||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB427DC925BAA00100D1B89D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -421,7 +478,8 @@
|
|||
DB89B9FC25C10FD0008580ED /* CoreDataStackTests */,
|
||||
DB427DD325BAA00100D1B89D /* Products */,
|
||||
1EBA4F56E920856A3FC84ACB /* Pods */,
|
||||
4E8E8B18DB8471A676012CF9 /* Frameworks */,
|
||||
3FE14AD363ED19AE7FF210A6 /* Frameworks */,
|
||||
DB98335F25C93B0400AD9700 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
@ -440,15 +498,15 @@
|
|||
DB427DD425BAA00100D1B89D /* Mastodon */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||
DB427DE325BAA00100D1B89D /* Info.plist */,
|
||||
DB89BA1025C10FF5008580ED /* Mastodon.entitlements */,
|
||||
2D76319C25C151DE00929FB9 /* Diffiable */,
|
||||
DB8AF52A25C13561002E6C99 /* State */,
|
||||
2D61335525C1886800CAE157 /* Service */,
|
||||
DB8AF56225C138BC002E6C99 /* Extension */,
|
||||
DB8AF55525C1379F002E6C99 /* Scene */,
|
||||
DB8AF54125C13647002E6C99 /* Coordinator */,
|
||||
DB3D101B25BAA79200EAA174 /* Generated */,
|
||||
DB8AF56225C138BC002E6C99 /* Extension */,
|
||||
DB98338425C945ED00AD9700 /* Generated */,
|
||||
DB3D0FF825BAA6B200EAA174 /* Resources */,
|
||||
DB3D0FF725BAA68500EAA174 /* Supporting Files */,
|
||||
);
|
||||
|
@ -473,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 = (
|
||||
|
@ -517,6 +599,7 @@
|
|||
2D927F0725C7E9A8004F19B8 /* Tag.swift */,
|
||||
2D927F0D25C7E9C9004F19B8 /* History.swift */,
|
||||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
||||
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
||||
);
|
||||
path = Entity;
|
||||
sourceTree = "<group>";
|
||||
|
@ -533,9 +616,9 @@
|
|||
DB8AF52A25C13561002E6C99 /* State */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB8AF52D25C13561002E6C99 /* AppContext.swift */,
|
||||
DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */,
|
||||
DB8AF52C25C13561002E6C99 /* DocumentStore.swift */,
|
||||
DB8AF52D25C13561002E6C99 /* AppContext.swift */,
|
||||
);
|
||||
path = State;
|
||||
sourceTree = "<group>";
|
||||
|
@ -561,6 +644,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D7631A425C1532200929FB9 /* Share */,
|
||||
DB01409B25C40BB600F9F3CF /* Authentication */,
|
||||
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
DB8AF55625C137A8002E6C99 /* HomeViewController.swift */,
|
||||
|
@ -571,16 +655,37 @@
|
|||
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>";
|
||||
};
|
||||
DB98335F25C93B0400AD9700 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98338425C945ED00AD9700 /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB98338525C945ED00AD9700 /* Strings.swift */,
|
||||
DB98338625C945ED00AD9700 /* Assets.swift */,
|
||||
);
|
||||
path = Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -618,6 +723,7 @@
|
|||
5D526FE125BE9AC400460CB9 /* MastodonSDK */,
|
||||
2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */,
|
||||
2D42FF6025C8177C004A627A /* ActiveLabel */,
|
||||
DB0140BC25C40D7500F9F3CF /* CommonOSLog */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
|
@ -743,6 +849,7 @@
|
|||
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */,
|
||||
2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */,
|
||||
2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */,
|
||||
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -924,36 +1031,52 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
||||
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 */,
|
||||
2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */,
|
||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||
2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */,
|
||||
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
|
||||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||
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 */,
|
||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||
DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */,
|
||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||
2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */,
|
||||
DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */,
|
||||
2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */,
|
||||
DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */,
|
||||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -989,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 */,
|
||||
|
@ -1479,6 +1603,14 @@
|
|||
minimumVersion = 3.1.0;
|
||||
};
|
||||
};
|
||||
DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/CommonOSLog";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.1.1;
|
||||
};
|
||||
};
|
||||
DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/Alamofire/AlamofireImage.git";
|
||||
|
@ -1504,6 +1636,11 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastodonSDK;
|
||||
};
|
||||
DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */;
|
||||
productName = CommonOSLog;
|
||||
};
|
||||
DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */;
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>6</integer>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -37,6 +37,15 @@
|
|||
"version": "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "CommonOSLog",
|
||||
"repositoryURL": "https://github.com/MainasuK/CommonOSLog",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c121624a30698e9886efe38aebb36ff51c01b6c2",
|
||||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
|
|
|
@ -37,7 +37,10 @@ extension SceneCoordinator {
|
|||
}
|
||||
|
||||
enum Scene {
|
||||
case authentication(viewModel: AuthenticationViewModel)
|
||||
case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel)
|
||||
|
||||
case alertController(alertController: UIAlertController)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,8 +111,25 @@ private extension SceneCoordinator {
|
|||
func get(scene: Scene) -> UIViewController? {
|
||||
let viewController: UIViewController?
|
||||
|
||||
// TODO:
|
||||
viewController = nil
|
||||
switch scene {
|
||||
case .authentication(let viewModel):
|
||||
let _viewController = AuthenticationViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .mastodonPinBasedAuthentication(let viewModel):
|
||||
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,20 @@
|
|||
//
|
||||
// OSLog.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021/1/29
|
||||
//
|
||||
|
||||
import os
|
||||
import Foundation
|
||||
import CommonOSLog
|
||||
|
||||
extension OSLog {
|
||||
static let api: OSLog = {
|
||||
#if DEBUG
|
||||
return OSLog(subsystem: OSLog.subsystem + ".api", category: "api")
|
||||
#else
|
||||
return OSLog.disabled
|
||||
#endif
|
||||
}()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,10 +23,10 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
|
|||
internal enum Asset {
|
||||
internal static let accentColor = ColorAsset(name: "AccentColor")
|
||||
internal enum Colors {
|
||||
internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark")
|
||||
internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray")
|
||||
internal static let tootWhite = ColorAsset(name: "Colors/Toot.White")
|
||||
internal static let likeOrange = ColorAsset(name: "Colors/like.orange")
|
||||
internal static let tootDark = ColorAsset(name: "Colors/toot.dark")
|
||||
internal static let tootGray = ColorAsset(name: "Colors/toot.gray")
|
||||
internal static let tootWhite = ColorAsset(name: "Colors/toot.white")
|
||||
}
|
||||
internal enum ToolBar {
|
||||
internal static let bookmark = ImageAsset(name: "ToolBar/bookmark")
|
||||
|
@ -37,10 +37,10 @@ internal enum Asset {
|
|||
internal static let star = ImageAsset(name: "ToolBar/star")
|
||||
}
|
||||
internal enum TootTimeline {
|
||||
internal static let global = ImageAsset(name: "TootTimeline/Global")
|
||||
internal static let textlock = ImageAsset(name: "TootTimeline/Textlock")
|
||||
internal static let email = ImageAsset(name: "TootTimeline/email")
|
||||
internal static let global = ImageAsset(name: "TootTimeline/global")
|
||||
internal static let lock = ImageAsset(name: "TootTimeline/lock")
|
||||
internal static let textlock = ImageAsset(name: "TootTimeline/textlock")
|
||||
internal static let unlock = ImageAsset(name: "TootTimeline/unlock")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
//
|
||||
// AuthenticationViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
final class AuthenticationViewController: UIViewController, NeedsDependency {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: AuthenticationViewModel!
|
||||
|
||||
let domainTextField: UITextField = {
|
||||
let textField = UITextField()
|
||||
textField.placeholder = "example.com"
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
textField.keyboardType = .URL
|
||||
return textField
|
||||
}()
|
||||
|
||||
private(set) lazy var signInBarButtonItem = UIBarButtonItem(title: "Sign In", style: .plain, target: self, action: #selector(AuthenticationViewController.signInBarButtonItemPressed(_:)))
|
||||
let activityIndicatorBarButtonItem = UIBarButtonItem.activityIndicatorBarButtonItem
|
||||
}
|
||||
|
||||
extension AuthenticationViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Authentication"
|
||||
view.backgroundColor = .systemBackground
|
||||
navigationItem.rightBarButtonItem = signInBarButtonItem
|
||||
|
||||
domainTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(domainTextField)
|
||||
NSLayoutConstraint.activate([
|
||||
domainTextField.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 8),
|
||||
domainTextField.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor, constant: 8),
|
||||
domainTextField.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor, constant: 8),
|
||||
domainTextField.heightAnchor.constraint(equalToConstant: 44), // FIXME:
|
||||
])
|
||||
|
||||
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: domainTextField)
|
||||
.compactMap { notification in
|
||||
guard let textField = notification.object as? UITextField? else { return nil }
|
||||
return textField?.text ?? ""
|
||||
}
|
||||
.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) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
domainTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AuthenticationViewController {
|
||||
|
||||
@objc private func signInBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
guard let domain = viewModel.domain.value else {
|
||||
// TODO: alert error
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension AuthenticationViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
||||
return .fullScreen
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
//
|
||||
// AuthenticationViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/2/1.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Combine
|
||||
import MastodonSDK
|
||||
|
||||
final class AuthenticationViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let coordinator: SceneCoordinator
|
||||
let isAuthenticationExist: Bool
|
||||
let input = CurrentValueSubject<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<(domain: String, account: Mastodon.Entity.Account), Never>()
|
||||
let error = CurrentValueSubject<Error?, Never>(nil)
|
||||
|
||||
var mastodonPinBasedAuthenticationViewController: UIViewController?
|
||||
|
||||
init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) {
|
||||
self.context = context
|
||||
self.coordinator = coordinator
|
||||
self.isAuthenticationExist = isAuthenticationExist
|
||||
self.viewHierarchyShouldReset = isAuthenticationExist
|
||||
|
||||
input
|
||||
.map { input in
|
||||
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
|
||||
let urlString = trimmed.hasPrefix("https://") ? trimmed : "https://" + trimmed
|
||||
guard let url = URL(string: urlString),
|
||||
let host = url.host else {
|
||||
return nil
|
||||
}
|
||||
let components = host.components(separatedBy: ".")
|
||||
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
|
||||
}
|
||||
.assign(to: \.value, on: domain)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
domain
|
||||
.map { $0 != nil }
|
||||
.assign(to: \.value, on: isSignInButtonEnabled)
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AuthenticationViewModel {
|
||||
|
||||
struct AuthenticateInfo {
|
||||
let domain: String
|
||||
let clientID: String
|
||||
let clientSecret: String
|
||||
let url: URL
|
||||
}
|
||||
|
||||
func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject<String, Never>) {
|
||||
pinCodePublisher
|
||||
.handleEvents(receiveOutput: { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.isAuthenticating.value = true
|
||||
self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil)
|
||||
self.mastodonPinBasedAuthenticationViewController = nil
|
||||
})
|
||||
.compactMap { [weak self] code -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error>? in
|
||||
guard let self = self else { return nil }
|
||||
return self.context.apiService
|
||||
.userAccessToken(
|
||||
domain: info.domain,
|
||||
clientID: info.clientID,
|
||||
clientSecret: info.clientSecret,
|
||||
code: code
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> in
|
||||
let token = response.value
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in success. Token: %s", ((#file as NSString).lastPathComponent), #line, #function, token.accessToken)
|
||||
return AuthenticationViewModel.verifyAndSaveAuthentication(
|
||||
context: self.context,
|
||||
info: info,
|
||||
userToken: token
|
||||
)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.switchToLatest()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: swap user access token swap fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
self.isAuthenticating.value = false
|
||||
self.error.value = error
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let account = response.value
|
||||
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)
|
||||
}
|
||||
|
||||
static func verifyAndSaveAuthentication(
|
||||
context: AppContext,
|
||||
info: AuthenticateInfo,
|
||||
userToken: Mastodon.Entity.Token
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken)
|
||||
let managedObjectContext = context.backgroundManagedObjectContext
|
||||
|
||||
return context.apiService.accountVerifyCredentials(
|
||||
domain: info.domain,
|
||||
authorization: authorization
|
||||
)
|
||||
.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()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// MastodonPinBasedAuthenticationViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import WebKit
|
||||
|
||||
final class MastodonPinBasedAuthenticationViewController: UIViewController, NeedsDependency {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: MastodonPinBasedAuthenticationViewModel!
|
||||
|
||||
let webView: WKWebView = {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.processPool = WKProcessPool()
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
return webView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
// cleanup cookie
|
||||
let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore
|
||||
httpCookieStore.getAllCookies { cookies in
|
||||
for cookie in cookies {
|
||||
httpCookieStore.delete(cookie, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
extension MastodonPinBasedAuthenticationViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = "Authentication"
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(MastodonPinBasedAuthenticationViewController.cancelBarButtonItemPressed(_:)))
|
||||
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(webView)
|
||||
NSLayoutConstraint.activate([
|
||||
webView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
let request = URLRequest(url: viewModel.authenticateURL)
|
||||
webView.navigationDelegate = viewModel.navigationDelegate
|
||||
webView.load(request)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: authenticate via: %s", ((#file as NSString).lastPathComponent), #line, #function, viewModel.authenticateURL.debugDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonPinBasedAuthenticationViewController {
|
||||
|
||||
@objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// MastodonPinBasedAuthenticationViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import Combine
|
||||
import WebKit
|
||||
|
||||
final class MastodonPinBasedAuthenticationViewModel {
|
||||
|
||||
// input
|
||||
let authenticateURL: URL
|
||||
|
||||
// output
|
||||
let pinCodePublisher = PassthroughSubject<String, Never>()
|
||||
private var navigationDelegateShim: MastodonPinBasedAuthenticationViewModelNavigationDelegateShim?
|
||||
|
||||
init(authenticateURL: URL) {
|
||||
self.authenticateURL = authenticateURL
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonPinBasedAuthenticationViewModel {
|
||||
|
||||
var navigationDelegate: WKNavigationDelegate {
|
||||
let navigationDelegateShim = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim(viewModel: self)
|
||||
self.navigationDelegateShim = navigationDelegateShim
|
||||
return navigationDelegateShim
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021/1/29.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
final class MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: NSObject {
|
||||
|
||||
weak var viewModel: MastodonPinBasedAuthenticationViewModel?
|
||||
|
||||
init(viewModel: MastodonPinBasedAuthenticationViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - WKNavigationDelegate
|
||||
extension MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: WKNavigationDelegate {
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
guard let url = webView.url,
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }),
|
||||
let code = codeQueryItem.value else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel?.pinCodePublisher.send(code)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ final class HomeViewController: UIViewController, NeedsDependency {
|
|||
|
||||
}
|
||||
|
||||
|
||||
extension HomeViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
|
|
@ -24,7 +24,7 @@ class MainTabBarController: UITabBarController {
|
|||
var title: String {
|
||||
switch self {
|
||||
case .home: return "Home"
|
||||
case .publicTimeline : return "public"
|
||||
case .publicTimeline : return "Public"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,26 @@ extension MainTabBarController {
|
|||
let tabBarAppearance = UITabBarAppearance()
|
||||
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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// APIService+App.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/2/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
#if DEBUG
|
||||
private static let clientName = "Skimming"
|
||||
#else
|
||||
private static let clientName = "Mastodon for iOS"
|
||||
#endif
|
||||
|
||||
func createApplication(domain: String) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> {
|
||||
let query = Mastodon.API.App.CreateQuery(clientName: APIService.clientName, website: nil)
|
||||
return Mastodon.API.App.create(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// APIService+Authentication.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/2/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func userAccessToken(
|
||||
domain: String,
|
||||
clientID: String,
|
||||
clientSecret: String,
|
||||
code: String
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
|
||||
let query = Mastodon.API.OAuth.AccessTokenQuery(
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
code: code,
|
||||
grantType: "authorization_code"
|
||||
)
|
||||
return Mastodon.API.OAuth.accessToken(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -26,7 +26,7 @@ extension APIService {
|
|||
domain: domain,
|
||||
query: Mastodon.API.Timeline.PublicTimelineQuery()
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>,Error> in
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> in
|
||||
return APIService.Persist.persistTimeline(
|
||||
domain: domain,
|
||||
managedObjectContext: self.backgroundManagedObjectContext,
|
||||
|
@ -46,4 +46,5 @@ extension APIService {
|
|||
}
|
||||
.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)
|
||||
}
|
||||
|
||||
}
|
|
@ -23,8 +23,8 @@ extension APIService.Persist {
|
|||
persistType: PersistTimelineType
|
||||
) -> AnyPublisher<Result<Void, Error>, Never> {
|
||||
return managedObjectContext.performChanges {
|
||||
let toot = response.value
|
||||
let _ = toot.map {
|
||||
let toots = response.value
|
||||
let _ = toots.map {
|
||||
let userProperty = MastodonUser.Property(id: $0.account.id, domain: domain, acct: $0.account.acct, username: $0.account.username, displayName: $0.account.displayName,avatar: $0.account.avatar,avatarStatic: $0.account.avatarStatic, createdAt: $0.createdAt, networkDate: $0.createdAt)
|
||||
let author = MastodonUser.insert(into: managedObjectContext, property: userProperty)
|
||||
let metions = $0.mentions?.compactMap({ (mention) -> Mention in
|
||||
|
@ -46,7 +46,7 @@ extension APIService.Persist {
|
|||
uri: $0.uri,
|
||||
createdAt: $0.createdAt,
|
||||
content: $0.content,
|
||||
visibility: $0.visibility,
|
||||
visibility: $0.visibility?.rawValue,
|
||||
sensitive: $0.sensitive ?? false,
|
||||
spoilerText: $0.spoilerText,
|
||||
mentions: metions,
|
||||
|
@ -72,6 +72,18 @@ extension APIService.Persist {
|
|||
homeTimelineIndexes: nil)
|
||||
Toot.insert(into: managedObjectContext, property: tootProperty, author: author)
|
||||
}
|
||||
}.eraseToAnyPublisher()
|
||||
}
|
||||
.handleEvents(receiveOutput: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
#if DEBUG
|
||||
debugPrint(error)
|
||||
#endif
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
@ -24,6 +25,26 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
self.coordinator = sceneCoordinator
|
||||
|
||||
sceneCoordinator.setup()
|
||||
|
||||
do {
|
||||
let request = MastodonAuthentication.sortedFetchRequest
|
||||
if try appContext.managedObjectContext.fetch(request).isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
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)
|
||||
}
|
||||
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
|
|
@ -2,9 +2,26 @@
|
|||
"configurations" : [
|
||||
{
|
||||
"id" : "5119353D-C795-4264-89FD-8376D9B144F8",
|
||||
"name" : "Configuration 1",
|
||||
"name" : "mstdn.jp",
|
||||
"options" : {
|
||||
|
||||
"environmentVariableEntries" : [
|
||||
{
|
||||
"key" : "domain",
|
||||
"value" : "mstdn.jp"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id" : "C5184AF3-B83B-4A7E-949C-6B1AA3ABE7D1",
|
||||
"name" : "pawoo.net",
|
||||
"options" : {
|
||||
"environmentVariableEntries" : [
|
||||
{
|
||||
"key" : "domain",
|
||||
"value" : "pawoo.net"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -13,6 +30,10 @@
|
|||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"skippedTests" : [
|
||||
"MastodonSDKTests\/testCreateAnAnpplication()",
|
||||
"MastodonSDKTests\/testVerifyAppCredentials()"
|
||||
],
|
||||
"target" : {
|
||||
"containerPath" : "container:MastodonSDK",
|
||||
"identifier" : "MastodonSDKTests",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Mastodon+API+Account.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/2/2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func verifyCredentialsEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/verify_credentials")
|
||||
}
|
||||
|
||||
public static func verifyCredentials(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Account>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: verifyCredentialsEndpointURL(domain: domain),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Account.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
|
@ -14,12 +14,31 @@ extension Mastodon.API.App {
|
|||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps")
|
||||
}
|
||||
|
||||
static func verifyCredentialsEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("apps/verify_credentials")
|
||||
}
|
||||
|
||||
/// Create an application
|
||||
///
|
||||
/// Using this endpoint to obtain `client_id` and `client_secret` for later OAuth token exchange
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/apps/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `CreateQuery`
|
||||
/// - Returns: `AnyPublisher` contains `Application` nested in the response
|
||||
public static func create(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: CreateQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> {
|
||||
let request = Mastodon.API.request(
|
||||
let request = Mastodon.API.post(
|
||||
url: appEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
|
@ -31,6 +50,39 @@ extension Mastodon.API.App {
|
|||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Verify application token
|
||||
///
|
||||
/// Using this endpoint to verify App token
|
||||
///
|
||||
/// - Since: 2.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/apps/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - authorization: App token
|
||||
/// - Returns: `AnyPublisher` contains `Application` nested in the response
|
||||
public static func verifyCredentials(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Application>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: verifyCredentialsEndpointURL(domain: domain),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Application.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.OAuth {
|
||||
|
||||
|
@ -13,6 +14,159 @@ extension Mastodon.API.OAuth {
|
|||
|
||||
public struct Authorization {
|
||||
public let accessToken: String
|
||||
|
||||
public init(accessToken: String) {
|
||||
self.accessToken = accessToken
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.OAuth {
|
||||
|
||||
static func authorizeEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("authorize")
|
||||
}
|
||||
static func accessTokenEndpointURL(domain: String) -> URL {
|
||||
return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("token")
|
||||
}
|
||||
|
||||
/// Construct user authorize endpoint URL
|
||||
///
|
||||
/// This method construct a URL for user authorize
|
||||
///
|
||||
/// - Since: 0.1.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/apps/oauth/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `AuthorizeQuery`
|
||||
public static func authorizeURL(
|
||||
domain: String,
|
||||
query: AuthorizeQuery
|
||||
) -> URL {
|
||||
let request = Mastodon.API.get(
|
||||
url: authorizeEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
)
|
||||
let url = request.url!
|
||||
return url
|
||||
}
|
||||
|
||||
/// Obtain User Access Token
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/2/2
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/apps/oauth/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `AccessTokenQuery`
|
||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||
public static func accessToken(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: AccessTokenQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Token>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: accessTokenEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.OAuth {
|
||||
|
||||
public struct AuthorizeQuery: GetQuery {
|
||||
|
||||
public let forceLogin: String?
|
||||
public let responseType: String
|
||||
public let clientID: String
|
||||
public let redirectURI: String
|
||||
public let scope: String?
|
||||
|
||||
public init(
|
||||
forceLogin: String? = nil,
|
||||
responseType: String = "code",
|
||||
clientID: String,
|
||||
redirectURI: String = "urn:ietf:wg:oauth:2.0:oob",
|
||||
scope: String? = "read write follow push"
|
||||
) {
|
||||
self.forceLogin = forceLogin
|
||||
self.responseType = responseType
|
||||
self.clientID = clientID
|
||||
self.redirectURI = redirectURI
|
||||
self.scope = scope
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items: [URLQueryItem] = []
|
||||
forceLogin.flatMap { items.append(URLQueryItem(name: "force_login", value: $0)) }
|
||||
items.append(URLQueryItem(name: "response_type", value: responseType))
|
||||
items.append(URLQueryItem(name: "client_id", value: clientID))
|
||||
items.append(URLQueryItem(name: "redirect_uri", value: redirectURI))
|
||||
scope.flatMap { items.append(URLQueryItem(name: "scope", value: $0)) }
|
||||
guard !items.isEmpty else { return nil }
|
||||
return items
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct AccessTokenQuery: Codable, PostQuery {
|
||||
public init(
|
||||
clientID: String,
|
||||
clientSecret: String,
|
||||
redirectURI: String = "urn:ietf:wg:oauth:2.0:oob",
|
||||
scope: String? = "read write follow push",
|
||||
code: String?,
|
||||
grantType: String
|
||||
) {
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
self.redirectURI = redirectURI
|
||||
self.scope = scope
|
||||
self.code = code
|
||||
self.grantType = grantType
|
||||
}
|
||||
|
||||
|
||||
public let clientID: String
|
||||
public let clientSecret: String
|
||||
public let redirectURI: String
|
||||
public let scope: String?
|
||||
public let code: String?
|
||||
public let grantType: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientID = "client_id"
|
||||
case clientSecret = "client_secret"
|
||||
case redirectURI = "redirect_uri"
|
||||
case scope
|
||||
case code
|
||||
case grantType = "grant_type"
|
||||
|
||||
}
|
||||
|
||||
var body: Data? {
|
||||
return try? Mastodon.API.encoder.encode(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ extension Mastodon.API.Timeline {
|
|||
domain: String,
|
||||
query: PublicTimelineQuery
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
let request = Mastodon.API.request(
|
||||
let request = Mastodon.API.get(
|
||||
url: publicTimelineEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: nil
|
||||
|
@ -65,27 +65,13 @@ extension Mastodon.API.Timeline {
|
|||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items: [URLQueryItem] = []
|
||||
local.flatMap {
|
||||
items.append(URLQueryItem(name: "local", value: $0.queryItemValue))
|
||||
}
|
||||
remote.flatMap {
|
||||
items.append(URLQueryItem(name: "remote", value: $0.queryItemValue))
|
||||
}
|
||||
onlyMedia.flatMap {
|
||||
items.append(URLQueryItem(name: "only_media", value: $0.queryItemValue))
|
||||
}
|
||||
maxID.flatMap {
|
||||
items.append(URLQueryItem(name: "max_id", value: $0))
|
||||
}
|
||||
sinceID.flatMap {
|
||||
items.append(URLQueryItem(name: "since_id", value: $0))
|
||||
}
|
||||
minID.flatMap {
|
||||
items.append(URLQueryItem(name: "min_id", value: $0))
|
||||
}
|
||||
limit.flatMap {
|
||||
items.append(URLQueryItem(name: "limit", value: String($0)))
|
||||
}
|
||||
local.flatMap { items.append(URLQueryItem(name: "local", value: $0.queryItemValue)) }
|
||||
remote.flatMap { items.append(URLQueryItem(name: "remote", value: $0.queryItemValue)) }
|
||||
onlyMedia.flatMap { items.append(URLQueryItem(name: "only_media", value: $0.queryItemValue)) }
|
||||
maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) }
|
||||
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
|
||||
minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) }
|
||||
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
||||
guard !items.isEmpty else { return nil }
|
||||
return items
|
||||
}
|
||||
|
|
|
@ -37,29 +37,52 @@ extension Mastodon.API {
|
|||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom { decoder throws -> Date in
|
||||
let container = try decoder.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
|
||||
|
||||
if let date = fractionalSecondsPreciseISO8601Formatter.date(from: string) {
|
||||
return date
|
||||
}
|
||||
if let date = fullDatePreciseISO8601Formatter.date(from: string) {
|
||||
return date
|
||||
var logInfo = ""
|
||||
do {
|
||||
let string = try container.decode(String.self)
|
||||
logInfo += string
|
||||
|
||||
if let date = fractionalSecondsPreciseISO8601Formatter.date(from: string) {
|
||||
return date
|
||||
}
|
||||
if let date = fullDatePreciseISO8601Formatter.date(from: string) {
|
||||
return date
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
|
||||
var numberValue = ""
|
||||
do {
|
||||
let number = try container.decode(Double.self)
|
||||
logInfo += "\(number)"
|
||||
|
||||
return Date(timeIntervalSince1970: number)
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "[Decoder] Invalid date: \(logInfo)")
|
||||
}
|
||||
|
||||
return decoder
|
||||
}()
|
||||
|
||||
static func oauthEndpointURL(domain: String) -> URL {
|
||||
return URL(string: "https://" + domain + "/oauth/")!
|
||||
}
|
||||
static func endpointURL(domain: String) -> URL {
|
||||
return URL(string: "https://" + domain + "/api/v1/")!
|
||||
}
|
||||
static func endpointV2URL(domain: String) -> URL {
|
||||
return URL(string: "https://" + domain + "/api/v2/")!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
public enum Account { }
|
||||
public enum App { }
|
||||
public enum OAuth { }
|
||||
public enum Timeline { }
|
||||
|
@ -67,13 +90,15 @@ extension Mastodon.API {
|
|||
|
||||
extension Mastodon.API {
|
||||
|
||||
static func request(
|
||||
static func get(
|
||||
url: URL,
|
||||
query: GetQuery,
|
||||
query: GetQuery?,
|
||||
authorization: OAuth.Authorization?
|
||||
) -> URLRequest {
|
||||
var components = URLComponents(string: url.absoluteString)!
|
||||
components.queryItems = query.queryItems
|
||||
if let query = query {
|
||||
components.queryItems = query.queryItems
|
||||
}
|
||||
|
||||
let requestURL = components.url!
|
||||
var request = URLRequest(
|
||||
|
@ -91,9 +116,9 @@ extension Mastodon.API {
|
|||
return request
|
||||
}
|
||||
|
||||
static func request(
|
||||
static func post(
|
||||
url: URL,
|
||||
query: PostQuery,
|
||||
query: PostQuery?,
|
||||
authorization: OAuth.Authorization?
|
||||
) -> URLRequest {
|
||||
let components = URLComponents(string: url.absoluteString)!
|
||||
|
@ -104,7 +129,9 @@ extension Mastodon.API {
|
|||
timeoutInterval: Mastodon.API.timeoutInterval
|
||||
)
|
||||
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
||||
request.httpBody = query.body
|
||||
if let query = query {
|
||||
request.httpBody = query.body
|
||||
}
|
||||
if let authorization = authorization {
|
||||
request.setValue(
|
||||
"Bearer \(authorization.accessToken)",
|
||||
|
|
|
@ -9,9 +9,6 @@ import Foundation
|
|||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
// FIXME: prefer `Account`. `User` will be deprecated
|
||||
public typealias User = Account
|
||||
|
||||
/// Account
|
||||
///
|
||||
/// - Since: 0.1.0
|
||||
|
@ -48,7 +45,7 @@ extension Mastodon.Entity {
|
|||
public let followersCount: Int
|
||||
public let followingCount: Int
|
||||
|
||||
public let moved: User?
|
||||
public let moved: Account?
|
||||
public let fields: [Field]?
|
||||
public let bot: Bool?
|
||||
public let source: Source?
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// Mastodon+Entity+Activity.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Activity
|
||||
///
|
||||
/// - Since: 2.1.2
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/activity/)
|
||||
public struct Activity: Codable {
|
||||
public let week: Date
|
||||
public let statuses: Int
|
||||
public let logins: Int
|
||||
public let registrations: Int
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// Mastodon+Entity+Announcement.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Announcement
|
||||
///
|
||||
/// - Since: 3.1.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/announcement/)
|
||||
public struct Announcement: Codable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
// Base
|
||||
public let id: ID
|
||||
public let text: String
|
||||
public let published: Bool?
|
||||
public let allDay: Bool
|
||||
public let createdAt: Date
|
||||
public let updatedAt: Date
|
||||
public let read: Bool
|
||||
public let reactions: [AnnouncementReaction]
|
||||
|
||||
public let scheduledAt: Date?
|
||||
public let startsAt: Date?
|
||||
public let endsAt: Date?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case text
|
||||
case published
|
||||
case allDay
|
||||
case createdAt = "created_at"
|
||||
case updatedAt = "updated_at"
|
||||
case read
|
||||
case reactions
|
||||
|
||||
case scheduledAt = "scheduled_at"
|
||||
case startsAt = "starts_at"
|
||||
case endsAt
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// Mastodon+Entity+AnnouncementReaction.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// AnnouncementReaction
|
||||
///
|
||||
/// - Since: 3.1.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/announcementreaction/)
|
||||
public struct AnnouncementReaction: Codable {
|
||||
// Base
|
||||
public let name: String
|
||||
public let count: Int
|
||||
public let me: Bool
|
||||
|
||||
// Custom Emoji
|
||||
public let url: String?
|
||||
public let staticURL: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case count
|
||||
case me
|
||||
case url
|
||||
case staticURL = "static_url"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
//
|
||||
// Mastodon+Entity+Attachment.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Attachment
|
||||
///
|
||||
/// - Since: 0.6.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/attachment/)
|
||||
public struct Attachment: Codable {
|
||||
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let type: Type
|
||||
public let url: String
|
||||
public let previewURL: String
|
||||
|
||||
public let remoteURL: String?
|
||||
public let textURL: String?
|
||||
public let meta: Meta?
|
||||
public let description: String?
|
||||
public let blurhash: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case url
|
||||
case previewURL = "preview_url"
|
||||
|
||||
case remoteURL = "remote_url"
|
||||
case textURL = "text_url"
|
||||
case meta
|
||||
case description
|
||||
case blurhash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Attachment {
|
||||
public enum `Type`: RawRepresentable, Codable {
|
||||
case unknown
|
||||
case image
|
||||
case gifv
|
||||
case video
|
||||
case audio
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "unknown": self = .unknown
|
||||
case "image": self = .image
|
||||
case "gifv": self = .gifv
|
||||
case "video": self = .video
|
||||
case "audio": self = .audio
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .unknown: return "unknown"
|
||||
case .image: return "image"
|
||||
case .gifv: return "gifv"
|
||||
case .video: return "video"
|
||||
case .audio: return "audio"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Mastodon.Entity.Attachment {
|
||||
/// # Reference
|
||||
/// https://github.com/tootsuite/mastodon/blob/v3.3.0/app/models/media_attachment.rb
|
||||
public struct Meta: Codable {
|
||||
public let original: Format?
|
||||
public let small: Format?
|
||||
public let focus: Focus?
|
||||
|
||||
public let length: String?
|
||||
public let duration: Double?
|
||||
public let fps: Int?
|
||||
public let size: String?
|
||||
public let width: Int?
|
||||
public let height: Int?
|
||||
public let aspect: Double?
|
||||
public let audioEncode: String?
|
||||
public let audioBitrate: String?
|
||||
public let audioChannels: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case original
|
||||
case small
|
||||
case focus
|
||||
|
||||
case length
|
||||
case duration
|
||||
case fps
|
||||
case size
|
||||
case width
|
||||
case height
|
||||
case aspect
|
||||
case audioEncode = "audio_encode"
|
||||
case audioBitrate = "audio_bitrate"
|
||||
case audioChannels = "audio_channels"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Attachment.Meta {
|
||||
public struct Format: Codable {
|
||||
public let width: Int?
|
||||
public let height: Int?
|
||||
public let size: String?
|
||||
public let aspect: Double?
|
||||
public let frameRate: String?
|
||||
public let duration: Double?
|
||||
public let bitrate: Int?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case width
|
||||
case height
|
||||
case size
|
||||
case aspect
|
||||
case frameRate = "frame_rate"
|
||||
case duration
|
||||
case bitrate
|
||||
}
|
||||
}
|
||||
|
||||
public struct Focus: Codable {
|
||||
public let x: Double
|
||||
public let y: Double
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ extension Mastodon.Entity {
|
|||
public let url: String
|
||||
public let title: String
|
||||
public let description: String
|
||||
public let type: Type?
|
||||
public let type: Type
|
||||
|
||||
public let authorName: String?
|
||||
public let authorURL: String?
|
||||
|
@ -54,10 +54,32 @@ extension Mastodon.Entity {
|
|||
}
|
||||
|
||||
extension Mastodon.Entity.Card {
|
||||
public enum `Type`: String, Codable {
|
||||
public enum `Type`: RawRepresentable, Codable {
|
||||
case link
|
||||
case photo
|
||||
case video
|
||||
case rich
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "link": self = .link
|
||||
case "photo": self = .photo
|
||||
case "video": self = .video
|
||||
case "rich": self = .rich
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .link: return "link"
|
||||
case .photo: return "photo"
|
||||
case .video: return "video"
|
||||
case .rich: return "rich"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// Mastodon+Entity+Context.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Context
|
||||
///
|
||||
/// - Since: 0.6.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/context/)
|
||||
public struct Context: Codable {
|
||||
public let ancestors: [Status]
|
||||
public let descendants: [Status]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Mastodon+Entity+Conversation.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Conversation
|
||||
///
|
||||
/// - Since: 2.6.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/conversation/)
|
||||
public struct Conversation: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let accounts: [Account]
|
||||
public let unread: Bool
|
||||
|
||||
public let lastStatus: Status?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case accounts
|
||||
case unread
|
||||
|
||||
case lastStatus = "last_status"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// Mastodon+Entity+FeaturedTag.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// FeaturedTag
|
||||
///
|
||||
/// - Since: 3.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/featuredtag/)
|
||||
public struct FeaturedTag: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let name: String
|
||||
public let url: String?
|
||||
public let statusesCount: Int
|
||||
public let lastStatusAt: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case url
|
||||
case statusesCount = "statuses_count"
|
||||
case lastStatusAt = "last_status_at"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// Mastodon+Entity+Filter.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Field
|
||||
///
|
||||
/// - Since: 2.4.3
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/filter/)
|
||||
public struct Filter: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let phrase: String
|
||||
public let context: [Context]
|
||||
public let expiresAt: Date
|
||||
public let irreversible: Bool
|
||||
public let wholeWord: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case phrase
|
||||
case context
|
||||
case expiresAt = "expires_at"
|
||||
case irreversible
|
||||
case wholeWord = "whole_word"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Filter {
|
||||
public enum Context: RawRepresentable, Codable {
|
||||
case home
|
||||
case notifications
|
||||
case `public`
|
||||
case thread
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "home": self = .home
|
||||
case "notifications": self = .notifications
|
||||
case "public": self = .`public`
|
||||
case "thread": self = .thread
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .home: return "home"
|
||||
case .notifications: return "notifications"
|
||||
case .public: return "public"
|
||||
case .thread: return "thread"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// Mastodon+Entity+IdentityProof.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// IdentityProof
|
||||
///
|
||||
/// - Since: 2.8.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/identityproof/)
|
||||
public struct IdentityProof: Codable {
|
||||
public let provider: String
|
||||
public let providerUsername: String
|
||||
public let profileURL: String
|
||||
public let proofURL: String
|
||||
public let updatedAt: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case provider = "provider"
|
||||
case providerUsername = "provider_username"
|
||||
case profileURL = "profile_url"
|
||||
case proofURL = "proof_url"
|
||||
case updatedAt = "updated_at"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ extension Mastodon.Entity {
|
|||
public let statistics: Statistics?
|
||||
|
||||
public let thumbnail: String?
|
||||
public let contactAccount: User?
|
||||
public let contactAccount: Account?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case uri
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// Mastodon+Entity+List.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// List
|
||||
///
|
||||
/// - Since: 2.1.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/28
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/list/)
|
||||
public struct List: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let title: String
|
||||
|
||||
public let repliesPolicy: ReplyPolicy?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case title
|
||||
case repliesPolicy = "replies_policy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity {
|
||||
public enum ReplyPolicy: RawRepresentable, Codable {
|
||||
case followed
|
||||
case list
|
||||
case none
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "followed": self = .followed
|
||||
case "list": self = .list
|
||||
case "none": self = .none
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .followed: return "followed"
|
||||
case .list: return "list"
|
||||
case .none: return "none"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// Mastodon+Entity+Marker.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/28.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Marker
|
||||
///
|
||||
/// - Since: 3.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/marker/)
|
||||
public struct Marker: Codable {
|
||||
// Base
|
||||
public let home: Position
|
||||
public let notifications: Position
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Marker {
|
||||
public struct Position: Codable {
|
||||
public let lastReadID: Mastodon.Entity.Status.ID
|
||||
public let updatedAt: Date
|
||||
public let version: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case lastReadID = "last_read_id"
|
||||
case updatedAt = "updated_at"
|
||||
case version
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// Mastodon+Entity+Notification.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Notification
|
||||
///
|
||||
/// - Since: 0.9.9
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/notification/)
|
||||
public struct Notification: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let type: Type
|
||||
public let createdAt: Date
|
||||
public let account: Account
|
||||
|
||||
public let status: Status?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case createdAt = "created_at"
|
||||
case account
|
||||
case status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Notification {
|
||||
public enum `Type`: RawRepresentable, Codable {
|
||||
case follow
|
||||
case followRequest
|
||||
case mention
|
||||
case reblog
|
||||
case favourite
|
||||
case poll
|
||||
case status
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "follow": self = .follow
|
||||
case "follow_request": self = .followRequest
|
||||
case "mention": self = .mention
|
||||
case "reblog": self = .reblog
|
||||
case "favourite": self = .favourite
|
||||
case "poll": self = .poll
|
||||
case "status": self = .status
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .follow: return "follow"
|
||||
case .followRequest: return "follow_request"
|
||||
case .mention: return "mention"
|
||||
case .reblog: return "reblog"
|
||||
case .favourite: return "favourite"
|
||||
case .poll: return "poll"
|
||||
case .status: return "status"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// Mastodon+Entity+Preferences.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Preferences
|
||||
///
|
||||
/// - Since: 2.8.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/preferences/)
|
||||
public struct Preferences: Codable {
|
||||
public let postingDefaultVisibility: Visibility
|
||||
public let postingDefaultSensitive: Bool
|
||||
public let postingDefaultLanguage: String? // (ISO 639-1 language two-letter code)
|
||||
public let readingExpandMedia: ExpandMedia
|
||||
public let readingExpandSpoilers: Bool
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Preferences {
|
||||
public typealias Visibility = Mastodon.Entity.Source.Privacy
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.Preferences {
|
||||
public enum ExpandMedia: RawRepresentable, Codable {
|
||||
case `default`
|
||||
case showAll
|
||||
case hideAll
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "default": self = .default
|
||||
case "showAll": self = .showAll
|
||||
case "hideAll": self = .hideAll
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .default: return "default"
|
||||
case .showAll: return "showAll"
|
||||
case .hideAll: return "hideAll"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// Mastodon+Entity+PushSubscription.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// PushSubscription
|
||||
///
|
||||
/// - Since: 2.4.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/pushsubscription/)
|
||||
public struct PushSubscription: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let endpoint: String
|
||||
public let serverKey: String
|
||||
public let alerts: Alerts
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case endpoint
|
||||
case serverKey = "server_key"
|
||||
case alerts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.PushSubscription {
|
||||
public struct Alerts: Codable {
|
||||
public let follow: Bool
|
||||
public let favourite: Bool
|
||||
public let reblog: Bool
|
||||
public let mention: Bool
|
||||
public let poll: Bool?
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// Mastodon+Entity+Relationship.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Relationship
|
||||
///
|
||||
/// - Since: 0.6.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/relationship/)
|
||||
public struct Relationship: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let following: Bool
|
||||
public let requested: Bool?
|
||||
public let endorsed: Bool?
|
||||
public let followedBy: Bool
|
||||
public let muting: Bool?
|
||||
public let mutingNotifications: Bool?
|
||||
public let showingReblogs: Bool?
|
||||
public let notifying: Bool?
|
||||
public let blocking: Bool
|
||||
public let domainBlocking: Bool?
|
||||
public let blockedBy: Bool?
|
||||
public let note: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case following
|
||||
case requested
|
||||
case endorsed
|
||||
case followedBy = "followed_by"
|
||||
case muting
|
||||
case mutingNotifications = "muting_notifications"
|
||||
case showingReblogs = "showing_reblogs"
|
||||
case notifying
|
||||
case blocking
|
||||
case domainBlocking = "domain_blocking"
|
||||
case blockedBy = "blocked_by"
|
||||
case note
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Mastodon+Entity+Report.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Report
|
||||
///
|
||||
/// - Since: ?
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/report/)
|
||||
public struct Report: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID // undocumented
|
||||
public let actionTaken: Bool? // undocumented
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case actionTaken = "action_taken"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// Mastodon+Entity+Results.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Results (v1)
|
||||
///
|
||||
/// - Since: ?
|
||||
/// - Version: 3.0.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/results/)
|
||||
public struct Results: Codable {
|
||||
public let accounts: [Account]
|
||||
public let statuses: [Status]
|
||||
public let hashtags: [String]
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.V2 {
|
||||
/// Results (v2)
|
||||
///
|
||||
/// - Since: 2.4.1
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/results/)
|
||||
public struct Results: Codable {
|
||||
public let accounts: [Mastodon.Entity.Account]
|
||||
public let statuses: [Mastodon.Entity.Status]
|
||||
public let hashtags: [Mastodon.Entity.Tag]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// Mastodon+Entity+ScheduledStatus.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// ScheduledStatus
|
||||
///
|
||||
/// - Since: 2.7.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/scheduledstatus/)
|
||||
public struct ScheduledStatus: Codable {
|
||||
public typealias ID = String
|
||||
|
||||
public let id: ID
|
||||
public let scheduledAt: Date
|
||||
public let params: Parameters
|
||||
public let mediaAttachments: [Attachment]
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.ScheduledStatus {
|
||||
public struct Parameters: Codable {
|
||||
public let text: String
|
||||
public let inReplyToID: Mastodon.Entity.Account.ID?
|
||||
public let mediaIDs: [Mastodon.Entity.Attachment.ID]?
|
||||
public let sensitive: Bool?
|
||||
public let spoilerText: String?
|
||||
public let visibility: Visibility
|
||||
public let scheduledAt: Date?
|
||||
public let poll: Mastodon.Entity.Poll? // undocumented
|
||||
public let applicationID: String
|
||||
|
||||
// public let idempotency: Bool? // undoumented
|
||||
// public let withRateLimit // undoumented
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case text
|
||||
case inReplyToID = "in_reply_to_id"
|
||||
case mediaIDs = "media_ids"
|
||||
case sensitive
|
||||
case spoilerText = "spoiler_text"
|
||||
case visibility
|
||||
case scheduledAt = "scheduled_at"
|
||||
case poll
|
||||
case applicationID = "application_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mastodon.Entity.ScheduledStatus.Parameters {
|
||||
public typealias Visibility = Mastodon.Entity.Source.Privacy
|
||||
}
|
|
@ -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
|
||||
|
@ -40,10 +40,32 @@ extension Mastodon.Entity {
|
|||
}
|
||||
|
||||
extension Mastodon.Entity.Source {
|
||||
public enum Privacy: String, Codable {
|
||||
public enum Privacy: RawRepresentable, Codable {
|
||||
case `public`
|
||||
case unlisted
|
||||
case `private`
|
||||
case direct
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "public": self = .public
|
||||
case "unlisted": self = .unlisted
|
||||
case "private": self = .private
|
||||
case "direct": self = .direct
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .public: return "public"
|
||||
case .unlisted: return "unlisted"
|
||||
case .private: return "private"
|
||||
case .direct: return "direct"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import Foundation
|
|||
|
||||
extension Mastodon.Entity {
|
||||
|
||||
// FIXME: prefer `Status`. `Toot` will be deprecated
|
||||
public typealias Toot = Status
|
||||
|
||||
/// Status
|
||||
|
@ -31,9 +30,10 @@ extension Mastodon.Entity {
|
|||
public let account: Account
|
||||
public let content: String
|
||||
|
||||
public let visibility: String?
|
||||
public let visibility: Visibility?
|
||||
public let sensitive: Bool?
|
||||
public let spoilerText: String?
|
||||
public let mediaAttachments: [Attachment]
|
||||
public let application: Application?
|
||||
|
||||
// Rendering
|
||||
|
@ -73,6 +73,7 @@ extension Mastodon.Entity {
|
|||
case visibility
|
||||
case sensitive
|
||||
case spoilerText = "spoiler_text"
|
||||
case mediaAttachments = "media_attachments"
|
||||
case application
|
||||
|
||||
case mentions
|
||||
|
@ -103,10 +104,32 @@ extension Mastodon.Entity {
|
|||
}
|
||||
|
||||
extension Mastodon.Entity.Status {
|
||||
public enum Visibility: String, Codable {
|
||||
public enum Visibility: RawRepresentable, Codable {
|
||||
case `public`
|
||||
case unlisted
|
||||
case `private`
|
||||
case direct
|
||||
|
||||
case _other(String)
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "public": self = .public
|
||||
case "unlisted": self = .unlisted
|
||||
case "private": self = .private
|
||||
case "direct": self = .direct
|
||||
default: self = ._other(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .public: return "public"
|
||||
case .unlisted: return "unlisted"
|
||||
case .private: return "private"
|
||||
case .direct: return "direct"
|
||||
case ._other(let value): return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// Mastodon+Entity+Token.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity {
|
||||
/// Token
|
||||
///
|
||||
/// - Since: 0.1.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/1/29
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/entities/token/)
|
||||
public struct Token: Codable {
|
||||
public let accessToken: String
|
||||
public let tokenType: String
|
||||
public let scope: String
|
||||
public let createdAt: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case tokenType = "token_type"
|
||||
case scope
|
||||
case createdAt = "created_at"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,9 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension Mastodon.Entity { }
|
||||
extension Mastodon.Entity {
|
||||
public enum V2 { }
|
||||
}
|
||||
|
||||
// MARK: - Entity Document Template
|
||||
/// Entity Name
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// MastodonSDK+API+AppTests.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import XCTest
|
||||
import Combine
|
||||
@testable import MastodonSDK
|
||||
|
||||
extension MastodonSDKTests {
|
||||
|
||||
func testCreateAnAnpplication() throws {
|
||||
try _testCreateAnAnpplication(domain: domain)
|
||||
}
|
||||
|
||||
func _testCreateAnAnpplication(domain: String) throws {
|
||||
let theExpectation = expectation(description: "Create An Application")
|
||||
|
||||
let query = Mastodon.API.App.CreateQuery(
|
||||
clientName: "XCTest",
|
||||
website: nil
|
||||
)
|
||||
Mastodon.API.App.create(session: session, domain: domain, query: query)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
XCTFail(error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
XCTAssertEqual(response.value.name, "XCTest")
|
||||
XCTAssertEqual(response.value.website, nil)
|
||||
XCTAssertEqual(response.value.redirectURI, "urn:ietf:wg:oauth:2.0:oob")
|
||||
os_log("%{public}s[%{public}ld], %{public}s: (%s) clientID %s", ((#file as NSString).lastPathComponent), #line, #function, domain, response.value.clientID ?? "nil")
|
||||
os_log("%{public}s[%{public}ld], %{public}s: (%s) clientSecret %s", ((#file as NSString).lastPathComponent), #line, #function, domain, response.value.clientSecret ?? "nil")
|
||||
theExpectation.fulfill()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
wait(for: [theExpectation], timeout: 5.0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonSDKTests {
|
||||
|
||||
func testVerifyAppCredentials() throws {
|
||||
try _testVerifyAppCredentials(domain: domain, accessToken: "")
|
||||
}
|
||||
|
||||
func _testVerifyAppCredentials(domain: String, accessToken: String) throws {
|
||||
let theExpectation = expectation(description: "Verify App Credentials")
|
||||
|
||||
let authorization = Mastodon.API.OAuth.Authorization(accessToken: accessToken)
|
||||
Mastodon.API.App.verifyCredentials(
|
||||
session: session,
|
||||
domain: domain,
|
||||
authorization: authorization
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
XCTFail(error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
XCTAssertEqual(response.value.name, "XCTest")
|
||||
XCTAssertEqual(response.value.website, nil)
|
||||
theExpectation.fulfill()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
wait(for: [theExpectation], timeout: 5.0)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// MastodonSDK+API+OAuthTests.swift
|
||||
//
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021/1/29.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import XCTest
|
||||
import Combine
|
||||
@testable import MastodonSDK
|
||||
|
||||
extension MastodonSDKTests {
|
||||
|
||||
func testOAuthAuthorize() throws {
|
||||
try _testOAuthAuthorize(domain: domain)
|
||||
}
|
||||
|
||||
func _testOAuthAuthorize(domain: String) throws {
|
||||
let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: "StubClientID")
|
||||
let authorizeURL = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query)
|
||||
os_log("%{public}s[%{public}ld], %{public}s: (%s) authorizeURL %s", ((#file as NSString).lastPathComponent), #line, #function, domain, authorizeURL.absoluteString)
|
||||
XCTAssertEqual(
|
||||
authorizeURL.absoluteString,
|
||||
"https://\(domain)/oauth/authorize?response_type=code&client_id=StubClientID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=read%20write%20follow%20push"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -5,59 +5,20 @@ import Combine
|
|||
final class MastodonSDKTests: XCTestCase {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let mstdnDomain = "mstdn.jp"
|
||||
let pawooDomain = "pawoo.net"
|
||||
|
||||
let session = URLSession(configuration: .ephemeral)
|
||||
var domain: String { MastodonSDKTests.environmentVariable(key: "domain") }
|
||||
|
||||
static func environmentVariable(key: String) -> String {
|
||||
return ProcessInfo.processInfo.environment[key]!
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension MastodonSDKTests {
|
||||
|
||||
func testCreateAnAnpplication_mstdn() throws {
|
||||
try _testCreateAnAnpplication(domain: pawooDomain)
|
||||
}
|
||||
|
||||
func testCreateAnAnpplication_pawoo() throws {
|
||||
try _testCreateAnAnpplication(domain: pawooDomain)
|
||||
}
|
||||
|
||||
func _testCreateAnAnpplication(domain: String) throws {
|
||||
let theExpectation = expectation(description: "Create An Application")
|
||||
|
||||
let query = Mastodon.API.App.CreateQuery(
|
||||
clientName: "XCTest",
|
||||
website: nil
|
||||
)
|
||||
Mastodon.API.App.create(session: session, domain: domain, query: query)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
XCTFail(error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
XCTAssertEqual(response.value.name, "XCTest")
|
||||
XCTAssertEqual(response.value.website, nil)
|
||||
XCTAssertEqual(response.value.redirectURI, "urn:ietf:wg:oauth:2.0:oob")
|
||||
theExpectation.fulfill()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
wait(for: [theExpectation], timeout: 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
extension MastodonSDKTests {
|
||||
|
||||
func testPublicTimeline_mstdn() throws {
|
||||
try _testPublicTimeline(domain: mstdnDomain)
|
||||
}
|
||||
|
||||
func testPublicTimeline_pawoo() throws {
|
||||
try _testPublicTimeline(domain: pawooDomain)
|
||||
func testPublicTimeline() throws {
|
||||
try _testPublicTimeline(domain: domain)
|
||||
}
|
||||
|
||||
private func _testPublicTimeline(domain: String) throws {
|
||||
|
|
Loading…
Reference in New Issue