Browse Source

Replace MastodonKit with Pachyderm

pixelfed
Shadowfacts 3 years ago
parent
commit
1119a861d8
Signed by: shadowfacts GPG Key ID: 94A5AB95422746E5
  1. 3
      .gitmodules
  2. 1
      MastodonKit
  3. 61
      MyPlayground.playground/Contents.swift
  4. 346
      Pachyderm/Client.swift
  5. 39
      Pachyderm/ClientModel.swift
  6. 16
      Pachyderm/Extensions/Data.swift
  7. 22
      Pachyderm/Info.plist
  8. 146
      Pachyderm/Model/Account.swift
  9. 21
      Pachyderm/Model/Application.swift
  10. 100
      Pachyderm/Model/Attachment.swift
  11. 50
      Pachyderm/Model/Card.swift
  12. 26
      Pachyderm/Model/ConversationContext.swift
  13. 32
      Pachyderm/Model/Emoji.swift
  14. 70
      Pachyderm/Model/Filter.swift
  15. 43
      Pachyderm/Model/Hashtag.swift
  16. 60
      Pachyderm/Model/Instance.swift
  17. 53
      Pachyderm/Model/List.swift
  18. 23
      Pachyderm/Model/LoginSettings.swift
  19. 17
      Pachyderm/Model/MastodonError.swift
  20. 25
      Pachyderm/Model/Mention.swift
  21. 48
      Pachyderm/Model/Notification.swift
  22. 26
      Pachyderm/Model/PushSubscription.swift
  23. 21
      Pachyderm/Model/RegisteredApplication.swift
  24. 35
      Pachyderm/Model/Relationship.swift
  25. 21
      Pachyderm/Model/Report.swift
  26. 21
      Pachyderm/Model/Scope.swift
  27. 28
      Pachyderm/Model/SearchResults.swift
  28. 222
      Pachyderm/Model/Status.swift
  29. 40
      Pachyderm/Model/Timeline.swift
  30. 19
      Pachyderm/Pachyderm.h
  31. 63
      Pachyderm/Request/Body.swift
  32. 35
      Pachyderm/Request/FormAttachment.swift
  33. 30
      Pachyderm/Request/Method.swift
  34. 80
      Pachyderm/Request/Parameter.swift
  35. 57
      Pachyderm/Request/Request.swift
  36. 31
      Pachyderm/Request/RequestRange.swift
  37. 13
      Pachyderm/Response/Empty.swift
  38. 73
      Pachyderm/Response/Pagination.swift
  39. 14
      Pachyderm/Response/Response.swift
  40. 22
      PachydermTests/Info.plist
  41. 34
      PachydermTests/PachydermTests.swift
  42. 481
      Tusker.xcodeproj/project.pbxproj
  43. 5
      Tusker.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist
  44. 3
      Tusker.xcworkspace/contents.xcworkspacedata
  45. 23
      Tusker/Controllers/MastodonController.swift
  46. 2
      Tusker/Extensions/Account+Preferences.swift
  47. 2
      Tusker/Extensions/Mastodon+Equatable.swift
  48. 4
      Tusker/Extensions/UIViewController+Delegates.swift
  49. 8
      Tusker/Extensions/Visibility+Helpers.swift
  50. 4
      Tusker/LocalData.swift
  51. 4
      Tusker/Preferences/Preferences.swift
  52. 39
      Tusker/Screens/Compose/ComposeViewController.swift
  53. 7
      Tusker/Screens/Conversation/ConversationViewController.swift
  54. 4
      Tusker/Screens/Main/MainTabBarViewController.swift
  55. 23
      Tusker/Screens/Notifications/NotificationsTableViewController.swift
  56. 3
      Tusker/Screens/Onboarding/OnboardingViewController.swift
  57. 10
      Tusker/Screens/Preferences/VisibilityTableViewController.swift
  58. 35
      Tusker/Screens/Profile/ProfileTableViewController.swift
  59. 43
      Tusker/Screens/Timeline/TimelineTableViewController.swift
  60. 27
      Tusker/Timeline.swift
  61. 5
      Tusker/Views/AttachmentView.swift
  62. 8
      Tusker/Views/HTMLContentLabel.swift
  63. 44
      Tusker/Views/Notifications/ActionNotificationTableViewCell.swift
  64. 18
      Tusker/Views/Notifications/FollowNotificationTableViewCell.swift
  65. 30
      Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift
  66. 71
      Tusker/Views/Status/ConversationMainStatusTableViewCell.swift
  67. 73
      Tusker/Views/Status/StatusTableViewCell.swift
  68. 10
      Tusker/Views/StatusContentLabel.swift

3
.gitmodules

@ -1,6 +1,3 @@
[submodule "MastodonKit"]
path = MastodonKit
url = git://github.com/shadowfacts/MastodonKit.git
[submodule "SwiftSoup"]
path = SwiftSoup
url = git://github.com/scinfu/SwiftSoup.git

1
MastodonKit

@ -1 +0,0 @@
Subproject commit cfece3083acfeda2f124a84dc35f268682681d49

61
MyPlayground.playground/Contents.swift

@ -1,13 +1,58 @@
import UIKit
func test(_ nillable: String?) {
defer {
print("defer")
class Client {
func test<A>(_ thing: A) {
if var thing = thing as? ClientModel {
thing.client = self
} else if var arr = thing as? [ClientModel] {
arr.client = self
}
// } else if let arr = thing as? Array<Any> {
// for el in arr {
// if var el = el as? ClientModel {
// el.client = self
// }
// }
// }
}
guard let value = nillable else { return }
print(value)
}
test("test")
print("------")
test(nil)
protocol ClientModel {
var client: Client! { get set }
}
class Something: ClientModel {
var client: Client!
}
extension Array: ClientModel where Element: ClientModel {
var client: Client! {
get {
return first?.client
}
set {
for var el in self {
el.client = newValue
}
}
}
}
//extension Array: ClientModel where Element == ClientModel {
// var client: Client! {
// get {
// return first?.client
// }
// set {
// for var el in self {
// el.client = newValue
// }
// }
// }
//}
var array = [Something(), Something()]
let client = Client()
client.test(array)
array[0].client
array[1].client

346
Pachyderm/Client.swift

@ -0,0 +1,346 @@
//
// Client.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
/**
The base Mastodon API client.
*/
public class Client {
public typealias Callback<Result: Decodable> = (Response<Result>) -> Void
let baseURL: URL
let session: URLSession
public var accessToken: String?
public var appID: String?
public var clientID: String?
public var clientSecret: String?
public var timeoutInterval: TimeInterval = 60
lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
decoder.dateDecodingStrategy = .formatted(formatter)
return decoder
}()
public init(baseURL: URL, accessToken: String? = nil, session: URLSession = .shared) {
self.baseURL = baseURL
self.accessToken = accessToken
self.session = session
}
// MARK: - Internal Helpers
func run<Result>(_ request: Request<Result>, completion: @escaping Callback<Result>) {
guard let request = createURLRequest(request: request) else {
completion(.failure(Error.invalidRequest))
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let response = response as? HTTPURLResponse else {
completion(.failure(Error.invalidResponse))
return
}
guard response.statusCode == 200 else {
let mastodonError = try? self.decoder.decode(MastodonError.self, from: data)
let error = mastodonError.flatMap { Error.mastodonError($0.description) } ?? Error.unknownError
completion(.failure(error))
return
}
guard let result = try? self.decoder.decode(Result.self, from: data) else {
completion(.failure(Error.invalidModel))
return
}
if var result = result as? ClientModel {
result.client = self
} else if var result = result as? [ClientModel] {
result.client = self
}
let pagination = response.allHeaderFields["Link"].flatMap { $0 as? String }.flatMap(Pagination.init)
completion(.success(result, pagination))
}
task.resume()
}
func createURLRequest<Result>(request: Request<Result>) -> URLRequest? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else { return nil }
components.path = request.path
components.queryItems = request.queryParameters.queryItems
guard let url = components.url else { return nil }
var urlRequest = URLRequest(url: url, timeoutInterval: timeoutInterval)
urlRequest.httpMethod = request.method.name
urlRequest.httpBody = request.body.data
urlRequest.setValue(request.body.mimeType, forHTTPHeaderField: "Content-Type")
if let accessToken = accessToken {
urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
return urlRequest
}
// MARK: - Authorization
public func registerApp(name: String, redirectURI: String, scopes: [Scope], website: URL? = nil, completion: @escaping Callback<RegisteredApplication>) {
let request = Request<RegisteredApplication>(method: .post, path: "/api/v1/apps", body: .parameters([
"client_name" => name,
"redirect_uris" => redirectURI,
"scopes" => scopes.scopeString,
"website" => website?.absoluteString
]))
run(request) { result in
defer { completion(result) }
guard case let .success(application, _) = result else { return }
self.appID = application.id
self.clientID = application.clientID
self.clientSecret = application.clientSecret
}
}
public func getAccessToken(authorizationCode: String, redirectURI: String, completion: @escaping Callback<LoginSettings>) {
let request = Request<LoginSettings>(method: .post, path: "/oauth/token", body: .parameters([
"client_id" => clientID,
"client_secret" => clientSecret,
"grant_type" => "authorization_code",
"code" => authorizationCode,
"redirect_uri" => redirectURI
]))
run(request) { result in
defer { completion(result) }
guard case let .success(loginSettings, _) = result else { return }
self.accessToken = loginSettings.accessToken
}
}
// MARK: - Self
public func getSelfAccount(completion: @escaping Callback<Account>) {
let request = Request<Account>(method: .get, path: "/api/v1/accounts/verify_credentials")
run(request, completion: completion)
}
public func getFavourites(completion: @escaping Callback<[Status]>) {
let request = Request<[Status]>(method: .get, path: "/api/v1/favourites")
run(request, completion: completion)
}
public func getRelationships(accounts: [Account]? = nil, completion: @escaping Callback<[Relationship]>) {
let request = Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts?.map { $0.id })
run(request, completion: completion)
}
public func getInstance(completion: @escaping Callback<Instance>) {
let request = Request<Instance>(method: .get, path: "/api/v1/instance")
run(request, completion: completion)
}
public func getCustomEmoji(completion: @escaping Callback<[Emoji]>) {
let request = Request<[Emoji]>(method: .get, path: "/api/v1/custom_emojis")
run(request, completion: completion)
}
// MARK: - Accounts
public func getAccount(id: String, completion: @escaping Callback<Account>) {
let request = Request<Account>(method: .get, path: "/api/v1/accounts/\(id)")
run(request, completion: completion)
}
public func searchForAccount(query: String, limit: Int? = nil, following: Bool? = nil, completion: @escaping Callback<[Account]>) {
let request = Request<[Account]>(method: .get, path: "/api/v1/accounts/search", queryParameters: [
"q" => query,
"limit" => limit,
"following" => following
])
run(request, completion: completion)
}
// MARK: - Blocks
public func getBlocks(completion: @escaping Callback<[Account]>) {
let request = Request<[Account]>(method: .get, path: "/api/v1/blocks")
run(request, completion: completion)
}
public func getDomainBlocks(completion: @escaping Callback<[String]>) {
let request = Request<[String]>(method: .get, path: "api/v1/domain_blocks")
run(request, completion: completion)
}
public func block(domain: String, completion: @escaping Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain
]))
run(request, completion: completion)
}
public func unblock(domain: String, completion: @escaping Callback<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/domain_blocks", body: .parameters([
"domain" => domain
]))
run(request, completion: completion)
}
// MARK: - Filters
public func getFilters(completion: @escaping Callback<[Filter]>) {
let request = Request<[Filter]>(method: .get, path: "/api/v1/filters")
run(request, completion: completion)
}
public func createFilter(phrase: String, context: [Filter.Context], irreversible: Bool? = nil, wholeWord: Bool? = nil, expiresAt: Date? = nil, completion: @escaping Callback<Filter>) {
let request = Request<Filter>(method: .post, path: "/api/v1/filters", body: .parameters([
"phrase" => phrase,
"irreversible" => irreversible,
"whole_word" => wholeWord,
"expires_at" => expiresAt
] + "context" => context.contextStrings))
run(request, completion: completion)
}
public func getFilter(id: String, completion: @escaping Callback<Filter>) {
let request = Request<Filter>(method: .get, path: "/api/v1/filters/\(id)")
run(request, completion: completion)
}
// MARK: - Follows
public func getFollowRequests(range: RequestRange = .default, completion: @escaping Callback<[Account]>) {
var request = Request<[Account]>(method: .get, path: "/api/v1/follow_requests")
request.range = range
run(request, completion: completion)
}
public func getFollowSuggestions(completion: @escaping Callback<[Account]>) {
let request = Request<[Account]>(method: .get, path: "/api/v1/suggestions")
run(request, completion: completion)
}
public func followRemote(acct: String, completion: @escaping Callback<Account>) {
let request = Request<Account>(method: .post, path: "/api/v1/follows", body: .parameters(["uri" => acct]))
run(request, completion: completion)
}
// MARK: - Lists
public func getLists(completion: @escaping Callback<[List]>) {
let request = Request<[List]>(method: .get, path: "/api/v1/lists")
run(request, completion: completion)
}
public func getList(id: String, completion: @escaping Callback<List>) {
let request = Request<List>(method: .get, path: "/api/v1/lists/\(id)")
run(request, completion: completion)
}
public func createList(title: String, completion: @escaping Callback<List>) {
let request = Request<List>(method: .post, path: "/api/v1/lists", body: .parameters(["title" => title]))
run(request, completion: completion)
}
// MARK: - Media
public func upload(attachment: FormAttachment, description: String? = nil, focus: (Float, Float)? = nil, completion: @escaping Callback<Attachment>) {
let request = Request<Attachment>(method: .post, path: "/api/v1/media", body: .formData([
"description" => description,
"focus" => focus
], attachment))
run(request, completion: completion)
}
// MARK: - Mutes
public func getMutes(range: RequestRange, completion: @escaping Callback<[Account]>) {
var request = Request<[Account]>(method: .get, path: "/api/v1/mutes")
request.range = range
run(request, completion: completion)
}
// MARK: - Notifications
public func getNotifications(range: RequestRange = .default, completion: @escaping Callback<[Notification]>) {
var request = Request<[Notification]>(method: .get, path: "/api/v1/notifications")
request.range = range
run(request, completion: completion)
}
public func clearNotifications(completion: @escaping Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/notifications/clear")
run(request, completion: completion)
}
// MARK: - Reports
public func getReports(completion: @escaping Callback<[Report]>) {
let request = Request<[Report]>(method: .get, path: "/api/v1/reports")
run(request, completion: completion)
}
public func report(account: Account, statuses: [Status], comment: String, completion: @escaping Callback<Report>) {
let request = Request<Report>(method: .post, path: "/api/v1/reports", body: .parameters([
"account_id" => account.id,
"comment" => comment
] + "status_ids" => statuses.map { $0.id }))
run(request, completion: completion)
}
// MARK: - Search
public func search(query: String, resolve: Bool? = nil, completion: @escaping Callback<SearchResults>) {
let request = Request<SearchResults>(method: .get, path: "/api/v2/search", queryParameters: [
"q" => query,
"resolve" => resolve
])
run(request, completion: completion)
}
// MARK: - Statuses
public func getStatus(id: String, completion: @escaping Callback<Status>) {
let request = Request<Status>(method: .get, path: "/api/v1/statuses/\(id)")
run(request, completion: completion)
}
public func createStatus(text: String,
inReplyTo: Status? = nil,
media: [Attachment]? = nil,
sensitive: Bool? = nil,
spoilerText: String? = nil,
visiblity: Status.Visibility? = nil,
language: String? = nil,
completion: @escaping Callback<Status>) {
let request = Request<Status>(method: .post, path: "/api/v1/statuses", body: .parameters([
"status" => text,
"in_reply_to_id" => inReplyTo?.id,
"sensitive" => sensitive,
"spoiler_text" => spoilerText,
"visibility" => visiblity?.rawValue,
"language" => language
] + "media" => media?.map { $0.id }))
run(request, completion: completion)
}
// MARK: - Timelines
public func getStatuses(timeline: Timeline, range: RequestRange = .default, completion: @escaping Callback<[Status]>) {
let request = timeline.request(range: range)
run(request, completion: completion)
}
}
extension Client {
public enum Error: Swift.Error {
case unknownError
case invalidRequest
case invalidResponse
case invalidModel
case mastodonError(String)
}
}

39
Pachyderm/ClientModel.swift

@ -0,0 +1,39 @@
//
// ClientModel.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
protocol ClientModel {
var client: Client! { get set }
}
extension Array where Element == ClientModel {
var client: Client! {
get {
return first?.client
}
set {
for var el in self {
el.client = newValue
}
}
}
}
extension Array where Element: ClientModel {
var client: Client! {
get {
return first?.client
}
set {
for var el in self {
el.client = newValue
}
}
}
}

16
Pachyderm/Extensions/Data.swift

@ -0,0 +1,16 @@
//
// Data.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
extension Data {
mutating func append(_ string: String, encoding: String.Encoding = .utf8) {
guard let data = string.data(using: encoding) else { return }
append(data)
}
}

22
Pachyderm/Info.plist

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

146
Pachyderm/Model/Account.swift

@ -0,0 +1,146 @@
//
// Account.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Account: Decodable, ClientModel {
var client: Client! {
didSet {
emojis.client = client
}
}
public let id: String
public let username: String
public let acct: String
public let displayName: String
public let locked: Bool
public let createdAt: Date
public let followersCount: Int
public let followingCount: Int
public let statusesCount: Int
public let note: String
public let url: URL
public let avatar: URL
public let avatarStatic: URL
public let header: URL
public let headerStatic: URL
public private(set) var emojis: [Emoji]
public let moved: Bool?
public let fields: [Field]?
public let bot: Bool?
public func authorizeFollowRequest(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(id)/authorize")
client.run(request, completion: completion)
}
public func rejectFollowRequest(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/follow_requests/\(id)/reject")
client.run(request, completion: completion)
}
public func removeFromFollowRequests(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/suggestions/\(id)")
client.run(request, completion: completion)
}
public func getFollowers(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) {
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(id)/followers")
request.range = range
client.run(request, completion: completion)
}
public func getFollowing(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) {
var request = Request<[Account]>(method: .get, path: "/api/v1/accounts/\(id)/following")
request.range = range
client.run(request, completion: completion)
}
public func getStatuses(range: RequestRange = .default, onlyMedia: Bool? = nil, pinned: Bool? = nil, excludeReplies: Bool? = nil, completion: @escaping Client.Callback<[Status]>) {
var request = Request<[Status]>(method: .get, path: "/api/v1/accounts/\(id)/statuses", queryParameters: [
"only_media" => onlyMedia,
"pinned" => pinned,
"exclude_replies" => excludeReplies
])
request.range = range
client.run(request, completion: completion)
}
public func follow(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/follow")
client.run(request, completion: completion)
}
public func unfollow(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/unfollow")
client.run(request, completion: completion)
}
public func block(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/block")
client.run(request, completion: completion)
}
public func unblock(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/unblock")
client.run(request, completion: completion)
}
public func mute(notifications: Bool? = nil, completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/mute", body: .parameters([
"notifications" => notifications
]))
client.run(request, completion: completion)
}
public func unmute(completion: @escaping Client.Callback<Relationship>) {
let request = Request<Relationship>(method: .post, path: "/api/v1/accounts/\(id)/unmute")
client.run(request, completion: completion)
}
public func getLists(completion: @escaping Client.Callback<[List]>) {
let request = Request<[List]>(method: .get, path: "/api/v1/accounts/\(id)/lists")
client.run(request, completion: completion)
}
private enum CodingKeys: String, CodingKey {
case id
case username
case acct
case displayName = "display_name"
case locked
case createdAt = "created_at"
case followersCount = "followers_count"
case followingCount = "following_count"
case statusesCount = "statuses_count"
case note
case url
case avatar
case avatarStatic = "avatar_static"
case header
case headerStatic = "header_static"
case emojis
case moved
case fields
case bot
}
}
extension Account: CustomDebugStringConvertible {
public var debugDescription: String {
return "Account(\(id), \(acct))"
}
}
extension Account {
public struct Field: Codable {
let name: String
let value: String
}
}

21
Pachyderm/Model/Application.swift

@ -0,0 +1,21 @@
//
// Application.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Application: Decodable, ClientModel {
var client: Client!
public let name: String
public let website: URL?
private enum CodingKeys: String, CodingKey {
case name
case website
}
}

100
Pachyderm/Model/Attachment.swift

@ -0,0 +1,100 @@
//
// Attachment.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Attachment: Decodable, ClientModel {
var client: Client!
public let id: String
public let kind: Kind
public let url: URL
public let remoteURL: URL?
public let previewURL: URL
public let textURL: URL?
public let meta: Metadata?
public var description: String?
public func update(focus: (Float, Float)?, completion: Client.Callback<Attachment>?) {
let request = Request<Attachment>(method: .put, path: "/api/v1/media/\(id)", body: .formData([
"description" => description,
"focus" => focus
], nil))
client.run(request) { result in
completion?(result)
}
}
private enum CodingKeys: String, CodingKey {
case id
case kind = "type"
case url
case remoteURL = "remote_url"
case previewURL = "preview_url"
case textURL = "text_url"
case meta
case description
}
}
extension Attachment {
public enum Kind: String, Decodable {
case image
case video
case gifv
case audio
case unknown
}
}
extension Attachment {
public class Metadata: Decodable {
public let length: String?
public let duration: Float?
public let audioEncoding: String?
public let audioBitrate: String?
public let audioChannels: String?
public let fps: Float?
public let width: Int?
public let height: Int?
public let size: String?
public let aspect: Float?
public let small: ImageMetadata?
public let original: ImageMetadata?
private enum CodingKeys: String, CodingKey {
case length
case duration
case audioEncoding = "audio_encode"
case audioBitrate = "audio_bitrate"
case audioChannels = "audio_channels"
case fps
case width
case height
case size
case aspect
case small
case original
}
}
public class ImageMetadata: Decodable {
public let width: Int?
public let height: Int?
public let size: String?
public let aspect: Float?
private enum CodingKeys: String, CodingKey {
case width
case height
case size
case aspect
}
}
}

50
Pachyderm/Model/Card.swift

@ -0,0 +1,50 @@
//
// Card.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Card: Decodable, ClientModel {
var client: Client!
public let url: URL
public let title: String
public let description: String
public let image: URL?
public let kind: Kind
public let authorName: String?
public let authorURL: URL?
public let providerName: String?
public let providerURL: URL?
public let html: String?
public let width: Int?
public let height: Int?
private enum CodingKeys: String, CodingKey {
case url
case title
case description
case image
case kind = "type"
case authorName = "author_name"
case authorURL = "author_url"
case providerName = "provider_name"
case providerURL = "provider_url"
case html
case width
case height
}
}
extension Card {
public enum Kind: String, Decodable {
case link
case photo
case video
case rich
}
}

26
Pachyderm/Model/ConversationContext.swift

@ -0,0 +1,26 @@
//
// Context.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class ConversationContext: Decodable, ClientModel {
var client: Client! {
didSet {
ancestors.client = client
descendants.client = client
}
}
public private(set) var ancestors: [Status]
public private(set) var descendants: [Status]
private enum CodingKeys: String, CodingKey {
case ancestors
case descendants
}
}

32
Pachyderm/Model/Emoji.swift

@ -0,0 +1,32 @@
//
// Emoji.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Emoji: Decodable, ClientModel {
var client: Client!
let shortcode: String
let url: URL
let staticURL: URL
// TODO: missing in pleroma
// let visibleInPicker: Bool
private enum CodingKeys: String, CodingKey {
case shortcode
case url
case staticURL = "static_url"
// case visibleInPicker = "visible_in_picker"
}
}
extension Emoji: CustomDebugStringConvertible {
public var debugDescription: String {
return ":\(shortcode):"
}
}

70
Pachyderm/Model/Filter.swift

@ -0,0 +1,70 @@
//
// Filter.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Filter: Decodable, ClientModel {
var client: Client!
public let id: String
public var phrase: String
private var context: [String]
public var expiresAt: Date?
public var irreversible: Bool
public var wholeWord: Bool
public var contexts: [Context] {
get {
return context.compactMap(Context.init)
}
set {
context = contexts.contextStrings
}
}
public func update(completion: Client.Callback<Filter>?) {
let request = Request<Filter>(method: .put, path: "/api/v1/filters/\(id)", body: .parameters([
"phrase" => phrase,
"irreversible" => irreversible,
"whole_word" => wholeWord,
"expires_at" => expiresAt
] + "context" => context))
client.run(request) { result in
completion?(result)
}
}
public func delete(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/filters/\(id)")
client.run(request, completion: completion)
}
private enum CodingKeys: String, CodingKey {
case id
case phrase
case context
case expiresAt = "expires_at"
case irreversible
case wholeWord = "whole_word"
}
}
extension Filter {
public enum Context: String, Decodable {
case home
case notifications
case `public`
case thread
}
}
extension Array where Element == Filter.Context {
var contextStrings: [String] {
return map { $0.rawValue }
}
}

43
Pachyderm/Model/Hashtag.swift

@ -0,0 +1,43 @@
//
// Hashtag.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Hashtag: Decodable, ClientModel {
var client: Client!
public let name: String
public let url: URL
public let history: [History]?
public init(name: String, url: URL) {
self.name = name
self.url = url
self.history = nil
}
private enum CodingKeys: String, CodingKey {
case name
case url
case history
}
}
extension Hashtag {
public class History: Decodable {
public let day: Date
public let uses: Int
public let accounts: Int
private enum CodingKeys: String, CodingKey {
case day
case uses
case accounts
}
}
}

60
Pachyderm/Model/Instance.swift

@ -0,0 +1,60 @@
//
// Instance.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Instance: Decodable, ClientModel {
var client: Client! {
didSet {
contactAccount.client = client
}
}
public let uri: String
public let title: String
public let description: String
public let email: String
public let version: String
public let urls: [String: URL]
public let languages: [String]
public let contactAccount: Account
// MARK: Unofficial additions to the Mastodon API.
public let stats: Stats?
public let thumbnail: URL?
public let maxStatusCharacters: Int?
private enum CodingKeys: String, CodingKey {
case uri
case title
case description
case email
case version
case urls
case languages
case contactAccount = "contact_account"
case stats
case thumbnail
case maxStatusCharacters = "max_toot_chars"
}
}
extension Instance {
public class Stats: Decodable {
public let domainCount: Int?
public let statusCount: Int?
public let userCount: Int?
private enum CodingKeys: String, CodingKey {
case domainCount = "domain_count"
case statusCount = "status_count"
case userCount = "user_count"
}
}
}

53
Pachyderm/Model/List.swift

@ -0,0 +1,53 @@
//
// List.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class List: Decodable, ClientModel {
var client: Client!
public let id: String
public var title: String
public func getAccounts(range: RequestRange = .default, completion: @escaping Client.Callback<[Account]>) {
var request = Request<[Account]>(method: .get, path: "/api/v1/lists/\(id)/accounts")
request.range = range
client.run(request, completion: completion)
}
public func update(completion: Client.Callback<List>?) {
let request = Request<List>(method: .put, path: "/api/v1/lists/\(id)", body: .parameters(["title" => title]))
client.run(request) { result in
completion?(result)
}
}
public func delete(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/lists/\(id)")
client.run(request, completion: completion)
}
public func add(accounts: [Account], completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/lists/\(id)/accounts", body: .parameters(
"account_ids" => accounts.map { $0.id }
))
client.run(request, completion: completion)
}
public func remove(accounts: [Account], completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .delete, path: "/api/v1/lists/\(id)/accounts", body: .parameters(
"account_ids" => accounts.map { $0.id }
))
client.run(request, completion: completion)
}
private enum CodingKeys: String, CodingKey {
case id
case title
}
}

23
Pachyderm/Model/LoginSettings.swift

@ -0,0 +1,23 @@
//
// LoginSettings.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class LoginSettings: Decodable {
public let accessToken: String
private let scope: String
public var scopes: [Scope] {
return scope.components(separatedBy: .whitespaces).compactMap(Scope.init)
}
private enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case scope
}
}

17
Pachyderm/Model/MastodonError.swift

@ -0,0 +1,17 @@
//
// MastodonError.swift
// Pachyderm
//
// Created by Shadowfacts on 9/8/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
struct MastodonError: Decodable, CustomStringConvertible {
var description: String
private enum CodingKeys: String, CodingKey {
case description = "error"
}
}

25
Pachyderm/Model/Mention.swift

@ -0,0 +1,25 @@
//
// Mention.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Mention: Decodable, ClientModel {
var client: Client!
public let url: URL
public let username: String
public let acct: String
public let id: String
private enum CodingKeys: String, CodingKey {
case url
case username
case acct
case id
}
}

48
Pachyderm/Model/Notification.swift

@ -0,0 +1,48 @@
//
// Notification.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Notification: Decodable, ClientModel {
var client: Client! {
didSet {
account.client = client
status?.client = client
}
}
public let id: String
public let kind: Kind
public let createdAt: Date
public let account: Account
public let status: Status?
public func dismiss(completion: @escaping Client.Callback<Empty>) {
let request = Request<Empty>(method: .post, path: "/api/v1/notifications/dismiss", body: .parameters([
"id" => id
]))
client.run(request, completion: completion)
}
private enum CodingKeys: String, CodingKey {
case id
case kind = "type"
case createdAt = "created_at"
case account
case status
}
}
extension Notification {
public enum Kind: String, Decodable {
case mention
case reblog
case favourite
case follow
}
}

26
Pachyderm/Model/PushSubscription.swift

@ -0,0 +1,26 @@
//
// PushSubscription.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class PushSubscription: Decodable, ClientModel {
var client: Client!
public let id: String
public let endpoint: URL
public let serverKey: String
// TODO: WTF is this?
// public let alerts
private enum CodingKeys: String, CodingKey {
case id
case endpoint
case serverKey = "server_key"
// case alerts
}
}

21
Pachyderm/Model/RegisteredApplication.swift

@ -0,0 +1,21 @@
//
// RegisteredApplication.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class RegisteredApplication: Decodable {
public let id: String
public let clientID: String
public let clientSecret: String
private enum CodingKeys: String, CodingKey {
case id
case clientID = "client_id"
case clientSecret = "client_secret"
}
}

35
Pachyderm/Model/Relationship.swift

@ -0,0 +1,35 @@
//
// Relationship.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Relationship: Decodable, ClientModel {
var client: Client!
public let id: String
public let following: Bool
public let followedBy: Bool
public let blocked: Bool
public let muting: Bool
public let mutingNotifications: Bool
public let followRequested: Bool
public let domainBlocking: Bool
public let showingReblogs: Bool
private enum CodingKeys: String, CodingKey {
case id
case following
case followedBy = "followed_by"
case blocked
case muting
case mutingNotifications = "muting_notifications"
case followRequested = "requested"
case domainBlocking = "domain_blocking"
case showingReblogs = "showing_reblogs"
}
}

21
Pachyderm/Model/Report.swift

@ -0,0 +1,21 @@
//
// Report.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class Report: Decodable, ClientModel {
var client: Client!
public let id: String
public let actionTaken: Bool
private enum CodingKeys: String, CodingKey {
case id
case actionTaken = "action_taken"
}
}

21
Pachyderm/Model/Scope.swift

@ -0,0 +1,21 @@
//
// Scope.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public enum Scope: String {
case read
case write
case follow
}
extension Array where Element == Scope {
var scopeString: String {
return map { $0.rawValue }.joined(separator: " ")
}
}

28
Pachyderm/Model/SearchResults.swift

@ -0,0 +1,28 @@
//
// SearchResults.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import Foundation
public class SearchResults: Decodable, ClientModel {
var client: Client! {
didSet {
accounts.client = client
statuses.client = client
}
}
public private(set) var accounts: [Account]
public private(set) var statuses: [Status]
public let hashtags: [String]
private enum CodingKeys: String, CodingKey {
case accounts
case statuses
case hashtags
}
}

222
Pachyderm/Model/Status.swift

@ -0,0 +1,222 @@
//
// Status.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//