Merge pull request #4 from tootsuite/feature/authentication into /develop

Add authentication scene
This commit is contained in:
CMK 2021-02-03 16:32:19 +08:00 committed by GitHub
commit 3e1d2bcc16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 3162 additions and 154 deletions

View File

@ -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>

View File

@ -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)
])
}
}

View File

@ -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)
])
}
}

View File

@ -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

View File

@ -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" */;

View File

@ -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>

View File

@ -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",

View File

@ -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)

View File

@ -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
)
}
}

View File

@ -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
}()
}

View File

@ -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
)
}
}

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -14,7 +14,6 @@ final class HomeViewController: UIViewController, NeedsDependency {
}
extension HomeViewController {
override func viewDidLoad() {

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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
)
}
}

View File

@ -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
)
}
}

View File

@ -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()
}
}

View File

@ -25,6 +25,8 @@ final class APIService {
// input
let backgroundManagedObjectContext: NSManagedObjectContext
// output
let error = PassthroughSubject<APIError, Never>()
init(backgroundManagedObjectContext: NSManagedObjectContext) {
self.backgroundManagedObjectContext = backgroundManagedObjectContext

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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 ?? []
}
}
}

View File

@ -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

View File

@ -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()
}

View File

@ -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",

View File

@ -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
}
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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)",

View File

@ -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?

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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"
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}
}

View File

@ -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]
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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?
}
}

View File

@ -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
}
}
}

View File

@ -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"
}
}
}

View File

@ -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]
}
}

View File

@ -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
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -7,7 +7,9 @@
import Foundation
extension Mastodon.Entity { }
extension Mastodon.Entity {
public enum V2 { }
}
// MARK: - Entity Document Template
/// Entity Name

View File

@ -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)
}
}

View File

@ -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"
)
}
}

View File

@ -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 {