Create MastodonKit

This commit is contained in:
Marcin Czachursk 2023-01-10 08:04:25 +01:00
parent 476e515423
commit 44f224768c
75 changed files with 2286 additions and 64 deletions

37
MastodonKit/Package.swift Normal file
View File

@ -0,0 +1,37 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MastodonKit",
platforms: [
.iOS(.v13),
.macOS(.v12),
.watchOS(.v8)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "MastodonKit",
targets: ["MastodonKit"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(
url: "https://github.com/OAuthSwift/OAuthSwift.git",
.upToNextMajor(from: "2.2.0")
)
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "MastodonKit",
dependencies: ["OAuthSwift"]),
.testTarget(
name: "MastodonKitTests",
dependencies: ["MastodonKit"]),
]
)

View File

@ -0,0 +1,15 @@
import Foundation
import OAuthSwift
public struct AccessToken: Codable {
public let token: String
private enum CodingKeys: String, CodingKey {
case token = "access_token"
}
#warning("This needs to be refactored, refresh token and other properties need to be available")
public init(credential: OAuthSwiftCredential) {
self.token = credential.oauthToken
}
}

View File

@ -0,0 +1,33 @@
import Foundation
public struct Account: Codable {
public let id: String
public let username: String
public let acct: String
public let displayName: String?
public let note: String?
public let url: URL?
public let avatar: URL?
public let header: URL?
public let locked: Bool
public let createdAt: String
public let followersCount: Int
public let followingCount: Int
public let statusesCount: Int
private enum CodingKeys: String, CodingKey {
case id
case username
case acct
case locked
case createdAt = "created_at"
case followersCount = "followers_count"
case followingCount = "following_count"
case statusesCount = "statuses_count"
case displayName = "display_name"
case note
case url
case avatar
case header
}
}

View File

@ -0,0 +1,31 @@
import Foundation
public struct App: Codable {
public let id: String
public let name: String
public let redirectUri: String
public let clientId: String
public let clientSecret: String
public let website: String?
public let vapidKey: String?
public init(clientId: String, clientSecret: String, vapidKey: String = "") {
self.id = ""
self.name = ""
self.redirectUri = "urn:ietf:wg:oauth:2.0:oob"
self.clientId = clientId
self.clientSecret = clientSecret
self.website = nil
self.vapidKey = vapidKey
}
private enum CodingKeys: String, CodingKey {
case id
case name
case redirectUri = "redirect_uri"
case clientId = "client_id"
case clientSecret = "client_secret"
case website
case vapidKey = "vapid_key"
}
}

View File

@ -0,0 +1,6 @@
import Foundation
public struct Application: Codable {
public let name: String
public let website: URL?
}

View File

@ -0,0 +1,81 @@
import Foundation
public class Attachment: Codable {
public enum AttachmentType: String, Codable {
case unknown = "unknown"
case image = "image"
case gifv = "gifv"
case video = "video"
case audio = "audio"
}
public let id: String
public let type: AttachmentType
public let url: URL
public let previewUrl: URL?
public let remoteUrl: URL?
public let description: String?
public let blurhash: String?
public let meta: Metadata?
private enum CodingKeys: String, CodingKey {
case id
case type
case url
case previewUrl = "preview_url"
case remoteUrl = "remote_url"
case description
case blurhash
case meta
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(StatusId.self, forKey: .id)
self.type = try container.decode(AttachmentType.self, forKey: .type)
self.url = try container.decode(URL.self, forKey: .url)
self.previewUrl = try? container.decode(URL.self, forKey: .previewUrl)
self.remoteUrl = try? container.decode(URL.self, forKey: .remoteUrl)
self.description = try? container.decode(String.self, forKey: .description)
self.blurhash = try? container.decode(String.self, forKey: .blurhash)
switch self.type {
case .image:
self.meta = try? container.decode(ImageMetadata.self, forKey: .meta)
default:
self.meta = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(type, forKey: .type)
try container.encode(url, forKey: .url)
if let previewUrl {
try container.encode(previewUrl, forKey: .previewUrl)
}
if let remoteUrl {
try container.encode(remoteUrl, forKey: .remoteUrl)
}
if let description {
try container.encode(description, forKey: .description)
}
if let blurhash {
try container.encode(blurhash, forKey: .blurhash)
}
if let meta {
try container.encode(meta, forKey: .meta)
}
}
}

View File

@ -0,0 +1,44 @@
import Foundation
public struct Card: Codable {
public enum CardType: String, Codable {
case link = "link" // Link OEmbed
case photo = "photo" // Photo OEmbed
case video = "video" // Video OEmbed
case rich = "rich" // iframe OEmbed. Not currently accepted, so won't show up in practice.
}
public let url: URL
public let title: String
public let description: String
public let type: CardType
public let authorName: String?
public let authorUrl: String?
public let providerName: String?
public let providerUrl: String?
public let html: String?
public let width: Int?
public let height: Int?
public let image: String?
public let embedUrl: String?
public let blurhash: String?
private enum CodingKeys: String, CodingKey {
case url
case title
case description
case type
case authorName = "author_name"
case authorUrl = "author_url"
case providerName = "provider_name"
case providerUrl = "provider_url"
case html
case width
case height
case image
case embedUrl = "embed_url"
case blurhash
}
}

View File

@ -0,0 +1,17 @@
import Foundation
public struct Context: Codable {
public let ancestors: [Status]
public let descendants: [Status]
public enum CodingKeys: CodingKey {
case ancestors
case descendants
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.ancestors = try container.decode([Status].self, forKey: .ancestors)
self.descendants = try container.decode([Status].self, forKey: .descendants)
}
}

View File

@ -0,0 +1,5 @@
import Foundation
public struct ErrorMessage: Codable {
public let error: String
}

View File

@ -0,0 +1,11 @@
import Foundation
public struct Focus: Codable {
public let x: Int
public let y: Int
private enum CodingKeys: String, CodingKey {
case x
case y
}
}

View File

@ -0,0 +1,15 @@
import Foundation
public struct ImageInfo: Codable {
public let width: Int
public let height: Int
public let size: String
public let aspect: Double
private enum CodingKeys: String, CodingKey {
case width
case height
case size
case aspect
}
}

View File

@ -0,0 +1,13 @@
import Foundation
public struct ImageMetadata: Metadata {
public let original: ImageInfo?
public let small: ImageInfo?
public let focus: Focus?
private enum CodingKeys: String, CodingKey {
case original
case small
case focus
}
}

View File

@ -0,0 +1,26 @@
import Foundation
public struct Instance: Codable {
public let uri: String
public let title: String?
public let description: String?
public let email: String?
public let thumbnail: String?
enum CodingKeys: CodingKey {
case uri
case title
case description
case email
case thumbnail
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uri = try container.decode(String.self, forKey: .uri)
self.title = try? container.decodeIfPresent(String.self, forKey: .title)
self.description = try? container.decodeIfPresent(String.self, forKey: .description)
self.email = try? container.decodeIfPresent(String.self, forKey: .email)
self.thumbnail = try? container.decodeIfPresent(String.self, forKey: .thumbnail)
}
}

View File

@ -0,0 +1,23 @@
import Foundation
public struct Marker: Codable {
public let lastReadId: StatusId
public let version: Int64
public let updatedAt: String
private enum CodingKeys: String, CodingKey {
case lastReadId = "last_read_id"
case version
case updatedAt = "updated_at"
}
}
public struct Markers: Codable {
public let home: Marker?
public let notifications: Marker?
private enum CodingKeys: String, CodingKey {
case home
case notifications
}
}

View File

@ -0,0 +1,8 @@
import Foundation
public struct Mention: Codable {
public let url: String
public let username: String
public let acct: String
public let id: String
}

View File

@ -0,0 +1,3 @@
public protocol Metadata: Codable {
}

View File

@ -0,0 +1,41 @@
import Foundation
public struct Notification: Codable {
public enum NotificationType: String, Codable {
case mention = "mention"
case reblog = "reblog"
case favourite = "favourite"
case follow = "follow"
}
public let id: String
public let type: NotificationType
public let createdAt: String
public let account: Account
public let status: Status
private enum CodingKeys: String, CodingKey {
case id
case type
case createdAat = "created_at"
case account
case status
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.type = try container.decode(NotificationType.self, forKey: .type)
self.createdAt = try container.decode(String.self, forKey: .createdAat)
self.account = try container.decode(Account.self, forKey: .account)
self.status = try container.decode(Status.self, forKey: .status)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(type, forKey: .type)
try container.encode(createdAt, forKey: .createdAat)
try container.encode(account, forKey: .account)
try container.encode(status, forKey: .status)
}
}

View File

@ -0,0 +1,64 @@
import Foundation
public struct Relationship: Codable {
public let id: String
public let following: Bool
public let followedBy: Bool
public let blocking: Bool
public let blockedBy: Bool
public let muting: Bool
public let mutingNotifications: Bool
public let requested: Bool
public let showingReblogs: Bool
public let notifying: Bool
public let domainBlocking: Bool
public let endorsed: Bool
private enum CodingKeys: String, CodingKey {
case id
case following
case followedBy = "followed_by"
case blocking
case blockedBy = "blocked_by"
case muting
case mutingNotifications = "muting_notifications"
case requested
case showingReblogs = "showing_reblogs"
case notifying
case domainBlocking = "domain_blocking"
case endorsed
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.following = (try? container.decode(Bool.self, forKey: .following)) ?? false
self.followedBy = (try? container.decode(Bool.self, forKey: .followedBy)) ?? false
self.blocking = (try? container.decode(Bool.self, forKey: .blocking)) ?? false
self.blockedBy = (try? container.decode(Bool.self, forKey: .blockedBy)) ?? false
self.muting = (try? container.decode(Bool.self, forKey: .muting)) ?? false
self.mutingNotifications = (try? container.decode(Bool.self, forKey: .mutingNotifications)) ?? false
self.requested = (try? container.decode(Bool.self, forKey: .requested)) ?? false
self.showingReblogs = (try? container.decode(Bool.self, forKey: .showingReblogs)) ?? false
self.notifying = (try? container.decode(Bool.self, forKey: .notifying)) ?? false
self.domainBlocking = (try? container.decode(Bool.self, forKey: .domainBlocking)) ?? false
self.endorsed = (try? container.decode(Bool.self, forKey: .endorsed)) ?? false
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(following, forKey: .following)
try container.encode(followedBy, forKey: .followedBy)
try container.encode(blocking, forKey: .blocking)
try container.encode(blockedBy, forKey: .blockedBy)
try container.encode(muting, forKey: .muting)
try container.encode(mutingNotifications, forKey: .mutingNotifications)
try container.encode(requested, forKey: .requested)
try container.encode(showingReblogs, forKey: .showingReblogs)
try container.encode(notifying, forKey: .notifying)
try container.encode(domainBlocking, forKey: .domainBlocking)
try container.encode(endorsed, forKey: .endorsed)
}
}

View File

@ -0,0 +1,17 @@
import Foundation
public struct Report: Codable {
public let id: String
public let actionTaken: String?
public enum CodingKeys: CodingKey {
case id
case actionTaken
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.actionTaken = try? container.decodeIfPresent(String.self, forKey: .actionTaken)
}
}

View File

@ -0,0 +1,22 @@
import Foundation
public typealias Hashtag = String
public struct Result: Codable {
public let accounts: [Account]
public let statuses: [Status]
public let hashtags: [Hashtag]
public enum CodingKeys: CodingKey {
case accounts
case statuses
case hashtags
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.accounts = (try? container.decode([Account].self, forKey: .accounts)) ?? []
self.statuses = (try? container.decode([Status].self, forKey: .statuses)) ?? []
self.hashtags = (try? container.decode([Hashtag].self, forKey: .hashtags)) ?? []
}
}

View File

@ -0,0 +1,142 @@
import Foundation
public typealias StatusId = String
public typealias Html = String
public class Status: Codable {
public enum Visibility: String, Codable {
case pub = "public"
case unlisted = "unlisted"
case priv = "private"
case direct = "direct"
}
public let id: StatusId
public let uri: String
public let url: URL?
public let account: Account?
public let inReplyToId: AccountId?
public let inReplyToAccount: StatusId?
public let reblog: Status?
public let content: Html
public let createdAt: String
public let reblogsCount: Int
public let favouritesCount: Int
public let repliesCount: Int
public let reblogged: Bool
public let favourited: Bool
public let sensitive: Bool
public let bookmarked: Bool
public let pinned: Bool
public let muted: Bool
public let spoilerText: String?
public let visibility: Visibility
public let mediaAttachments: [Attachment]
public let card: Card?
public let mentions: [Mention]
public let tags: [Tag]
public let application: Application?
private enum CodingKeys: String, CodingKey {
case id
case uri
case url
case account
case inReplyToId = "in_reply_to_id"
case inReplyToAccount = "in_reply_to_account_id"
case reblog
case content
case createdAt = "created_at"
case reblogsCount = "reblogs_count"
case favouritesCount = "favourites_count"
case repliesCount = "replies_count"
case reblogged
case favourited
case sensitive
case bookmarked
case pinned
case muted
case spoilerText = "spoiler_text"
case visibility
case mediaAttachments = "media_attachments"
case card
case mentions
case tags
case application
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(StatusId.self, forKey: .id)
self.uri = try container.decode(String.self, forKey: .uri)
self.url = try? container.decode(URL.self, forKey: .url)
self.account = try? container.decode(Account.self, forKey: .account)
self.content = try container.decode(Html.self, forKey: .content)
self.createdAt = try container.decode(String.self, forKey: .createdAt)
self.inReplyToId = try? container.decode(AccountId.self, forKey: .inReplyToId)
self.inReplyToAccount = try? container.decode(StatusId.self, forKey: .inReplyToAccount)
self.reblog = try? container.decode(Status.self, forKey: .reblog)
self.spoilerText = try? container.decode(String.self, forKey: .spoilerText)
self.reblogsCount = (try? container.decode(Int.self, forKey: .reblogsCount)) ?? 0
self.repliesCount = (try? container.decode(Int.self, forKey: .repliesCount)) ?? 0
self.favouritesCount = (try? container.decode(Int.self, forKey: .favouritesCount)) ?? 0
self.reblogged = (try? container.decode(Bool.self, forKey: .reblogged)) ?? false
self.favourited = (try? container.decode(Bool.self, forKey: .favourited)) ?? false
self.sensitive = (try? container.decode(Bool.self, forKey: .sensitive)) ?? false
self.bookmarked = (try? container.decode(Bool.self, forKey: .bookmarked)) ?? false
self.pinned = (try? container.decode(Bool.self, forKey: .pinned)) ?? false
self.muted = (try? container.decode(Bool.self, forKey: .muted)) ?? false
self.visibility = try container.decode(Visibility.self, forKey: .visibility)
self.mediaAttachments = (try? container.decode([Attachment].self, forKey: .mediaAttachments)) ?? []
self.card = try? container.decode(Card.self, forKey: .card)
self.mentions = (try? container.decode([Mention].self, forKey: .mentions)) ?? []
self.tags = (try? container.decode([Tag].self, forKey: .tags)) ?? []
self.application = try? container.decode(Application.self, forKey: .application)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(uri, forKey: .uri)
if let url {
try container.encode(url, forKey: .url)
}
if let account {
try container.encode(account, forKey: .account)
}
try container.encode(content, forKey: .content)
try container.encode(createdAt, forKey: .createdAt)
if let inReplyToId {
try container.encode(inReplyToId, forKey: .inReplyToId)
}
if let inReplyToAccount {
try container.encode(inReplyToAccount, forKey: .inReplyToAccount)
}
if let reblog {
try container.encode(reblog, forKey: .reblog)
}
if let spoilerText {
try container.encode(spoilerText, forKey: .spoilerText)
}
try container.encode(reblogsCount, forKey: .reblogsCount)
try container.encode(favouritesCount, forKey: .favouritesCount)
try container.encode(repliesCount, forKey: .repliesCount)
try container.encode(reblogged, forKey: .reblogged)
try container.encode(favourited, forKey: .favourited)
try container.encode(bookmarked, forKey: .bookmarked)
try container.encode(pinned, forKey: .pinned)
try container.encode(muted, forKey: .muted)
try container.encode(sensitive, forKey: .sensitive)
try container.encode(visibility, forKey: .visibility)
try container.encode(mediaAttachments, forKey: .mediaAttachments)
if let card {
try container.encode(card, forKey: .card)
}
try container.encode(mentions, forKey: .mentions)
try container.encode(tags, forKey: .tags)
if let application {
try container.encode(application, forKey: .application)
}
}
}

View File

@ -0,0 +1,6 @@
import Foundation
public struct Tag: Codable {
public let name: String
public let url: URL?
}

View File

@ -6,7 +6,7 @@
import Foundation
public enum NetworkError: Error {
public enum NetworkError: Error {
case notSuccessResponse(URLResponse)
}

View File

@ -0,0 +1,13 @@
import Foundation
extension Bool {
var asString: String {
return self == true ? "true" : "false"
}
}
extension Int {
var asString: String {
return "\(self)"
}
}

View File

@ -0,0 +1,11 @@
import Foundation
extension String {
func asURL() -> URL? {
return URL(string: self)
}
}
extension String {
static let showTimeline = "ShowTimeline"
}

View File

@ -0,0 +1,10 @@
import Foundation
extension URL {
static func fromOptional(string: String?) -> URL? {
guard let string = string else {
return nil
}
return URL(string: string)
}
}

View File

@ -5,9 +5,8 @@
//
import Foundation
import MastodonSwift
extension MastodonClientAuthenticated {
public extension MastodonClientAuthenticated {
func getAccount(for accountId: String) async throws -> Account {
let request = try Self.request(
for: baseURL,

View File

@ -0,0 +1,15 @@
import Foundation
public extension MastodonClientAuthenticated {
func verifyCredentials() async throws -> Account {
let request = try Self.request(
for: baseURL,
target: Mastodon.Account.verifyCredentials,
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Account.self, from: data)
}
}

View File

@ -5,9 +5,8 @@
//
import Foundation
import MastodonSwift
extension MastodonClientAuthenticated {
public extension MastodonClientAuthenticated {
func getContext(for statusId: String) async throws -> Context {
let request = try Self.request(
for: baseURL,

View File

@ -0,0 +1,78 @@
import Foundation
import OAuthSwift
public extension MastodonClient {
func createApp(named name: String,
redirectUri: String = "urn:ietf:wg:oauth:2.0:oob",
scopes: Scopes,
website: URL) async throws -> App {
let request = try Self.request(
for: baseURL,
target: Mastodon.Apps.register(
clientName: name,
redirectUris: redirectUri,
scopes: scopes.reduce("") { $0 == "" ? $1 : $0 + " " + $1},
website: website.absoluteString
)
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(App.self, from: data)
}
func authenticate(app: App, scope: Scopes) async throws -> OAuthSwiftCredential { // todo: we should not load OAuthSwift objects here
oauthClient = OAuth2Swift(
consumerKey: app.clientId,
consumerSecret: app.clientSecret,
authorizeUrl: baseURL.appendingPathComponent("oauth/authorize"),
accessTokenUrl: baseURL.appendingPathComponent("oauth/token"),
responseType: "code"
)
return try await withCheckedThrowingContinuation { [weak self] continuation in
self?.oAuthContinuation = continuation
oAuthHandle = oauthClient?.authorize(
withCallbackURL: app.redirectUri,
scope: scope.asScopeString,
state: "MASToDON_AUTH",
completionHandler: { result in
switch result {
case let .success((credentials, _, _)):
continuation.resume(with: .success(credentials))
case let .failure(error):
continuation.resume(throwing: error)
}
self?.oAuthContinuation = nil
}
)
}
}
static func handleOAuthResponse(url: URL) {
OAuthSwift.handle(url: url)
}
@available(*, deprecated, message: "The password flow is discoured and won't support 2FA. Please use authentiate(app:, scope:)")
func getToken(withApp app: App,
username: String,
password: String,
scope: Scopes) async throws -> AccessToken {
let request = try Self.request(
for: baseURL,
target: Mastodon.OAuth.authenticate(app, username, password, scope.asScopeString)
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(AccessToken.self, from: data)
}
}
private extension [String] {
var asScopeString: String {
joined(separator: " ")
}
}

View File

@ -0,0 +1,10 @@
import Foundation
public extension MastodonClient {
func readInstanceInformation() async throws -> Instance {
let request = try Self.request(for: baseURL, target: Mastodon.Instances.instance )
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Instance.self, from: data)
}
}

View File

@ -0,0 +1,98 @@
import Foundation
public extension MastodonClientAuthenticated {
func read(statusId: StatusId) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.status(statusId),
withBearerToken: token)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func boost(statusId: StatusId) async throws -> Status {
// TODO: Check whether the current user already boosted the status
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.reblog(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func unboost(statusId: StatusId) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.unreblog(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func bookmark(statusId: StatusId) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.bookmark(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func unbookmark(statusId: StatusId) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.unbookmark(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func favourite(statusId: StatusId) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.favourite(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func unfavourite(statusId: StatusId) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.unfavourite(statusId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
func new(statusComponents: Mastodon.Statuses.Components) async throws -> Status {
let request = try Self.request(
for: baseURL,
target: Mastodon.Statuses.new(statusComponents),
withBearerToken: token)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Status.self, from: data)
}
}

View File

@ -0,0 +1,149 @@
import Foundation
import OAuthSwift
public typealias Scope = String
public typealias Scopes = [Scope]
public typealias Token = String
public enum MastodonClientError: Swift.Error {
case oAuthCancelled
}
public protocol MastodonClientProtocol {
static func request(for baseURL: URL, target: TargetType, withBearerToken token: String?) throws -> URLRequest
}
public extension MastodonClientProtocol {
static func request(for baseURL: URL, target: TargetType, withBearerToken token: String? = nil) throws -> URLRequest {
var urlComponents = URLComponents(url: baseURL.appendingPathComponent(target.path), resolvingAgainstBaseURL: false)
urlComponents?.queryItems = target.queryItems?.map { URLQueryItem(name: $0.0, value: $0.1) }
guard let url = urlComponents?.url else { throw NetworkingError.cannotCreateUrlRequest }
var request = URLRequest(url: url)
target.headers?.forEach { header in
request.setValue(header.1, forHTTPHeaderField: header.0)
}
if let token = token {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
request.httpMethod = target.method.rawValue
request.httpBody = target.httpBody
return request
}
}
public class MastodonClient: MastodonClientProtocol {
let urlSession: URLSession
let baseURL: URL
/// oAuth
var oauthClient: OAuth2Swift?
var oAuthHandle: OAuthSwiftRequestHandle?
var oAuthContinuation: CheckedContinuation<OAuthSwiftCredential, Swift.Error>?
public init(baseURL: URL, urlSession: URLSession = .shared) {
self.baseURL = baseURL
self.urlSession = urlSession
}
public func getAuthenticated(token: Token) -> MastodonClientAuthenticated {
MastodonClientAuthenticated(baseURL: baseURL, urlSession: urlSession, token: token)
}
deinit {
oAuthContinuation?.resume(throwing: MastodonClientError.oAuthCancelled)
oAuthHandle?.cancel()
}
}
public class MastodonClientAuthenticated: MastodonClientProtocol {
public let token: Token
public let baseURL: URL
public let urlSession: URLSession
init(baseURL: URL, urlSession: URLSession, token: Token) {
self.token = token
self.baseURL = baseURL
self.urlSession = urlSession
}
public func getHomeTimeline(
maxId: StatusId? = nil,
sinceId: StatusId? = nil,
minId: StatusId? = nil,
limit: Int? = nil) async throws -> [Status] {
let request = try Self.request(
for: baseURL,
target: Mastodon.Timelines.home(maxId, sinceId, minId, limit),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode([Status].self, from: data)
}
public func getPublicTimeline(isLocal: Bool = false,
maxId: StatusId? = nil,
sinceId: StatusId? = nil) async throws -> [Status] {
let request = try Self.request(
for: baseURL,
target: Mastodon.Timelines.pub(isLocal, maxId, sinceId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode([Status].self, from: data)
}
public func getTagTimeline(tag: String,
isLocal: Bool = false,
maxId: StatusId? = nil,
sinceId: StatusId? = nil) async throws -> [Status] {
let request = try Self.request(
for: baseURL,
target: Mastodon.Timelines.tag(tag, isLocal, maxId, sinceId),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode([Status].self, from: data)
}
public func saveMarkers(_ markers: [Mastodon.Markers.Timeline: StatusId]) async throws -> Markers {
let request = try Self.request(
for: baseURL,
target: Mastodon.Markers.set(markers),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Markers.self, from: data)
}
public func readMarkers(_ markers: Set<Mastodon.Markers.Timeline>) async throws -> Markers {
let request = try Self.request(
for: baseURL,
target: Mastodon.Markers.read(markers),
withBearerToken: token
)
let (data, _) = try await urlSession.data(for: request)
return try JSONDecoder().decode(Markers.self, from: data)
}
}

View File

@ -12,7 +12,7 @@ import Foundation
public enum HTTPStatusCode: Int, Error {
/// The response class representation of status codes, these get grouped by their first digit.
enum ResponseType {
public enum ResponseType {
/// - informational: This class of status code indicates a provisional response, consisting only of the Status-Line and optional headers, and is terminated by an empty line.
case informational
@ -253,7 +253,7 @@ public enum HTTPStatusCode: Int, Error {
case networkAuthenticationRequired = 511
/// The class (or group) which the status code belongs to.
var responseType: ResponseType {
public var responseType: ResponseType {
switch self.rawValue {
@ -281,7 +281,7 @@ public enum HTTPStatusCode: Int, Error {
}
extension HTTPURLResponse {
public extension HTTPURLResponse {
var status: HTTPStatusCode? {
return HTTPStatusCode(rawValue: statusCode)
}

View File

@ -0,0 +1,65 @@
import Foundation
extension Data {
func getMultipartFormDataBuilder(withBoundary boundary: String) -> MultipartFormDataBuilder? {
return MultipartFormDataBuilder(data: self, boundary: boundary)
}
struct MultipartFormDataBuilder {
private let boundary: String
private var httpBody = NSMutableData()
private let data: Data
fileprivate init(data: Data, boundary: String) {
self.data = data
self.boundary = boundary
}
func addTextField(named name: String, value: String) -> Self {
httpBody.append(textFormField(named: name, value: value))
return self
}
private func textFormField(named name: String, value: String) -> String {
var fieldString = "--\(boundary)\r\n"
fieldString += "Content-Disposition: form-data; name=\"\(name)\"\r\n"
fieldString += "Content-Type: text/plain; charset=UTF-8\r\n"
fieldString += "\r\n"
fieldString += "\(value)\r\n"
return fieldString
}
func addDataField(named name: String, data: Data, mimeType: String) -> Self {
httpBody.append(dataFormField(named: name, data: data, mimeType: mimeType))
return self
}
private func dataFormField(named name: String, data: Data, mimeType: String) -> Data {
let fieldData = NSMutableData()
fieldData.append("--\(boundary)\r\n")
fieldData.append("Content-Disposition: form-data; name=\"\(name)\"\r\n")
fieldData.append("Content-Type: \(mimeType)\r\n")
fieldData.append("\r\n")
fieldData.append(data)
fieldData.append("\r\n")
return fieldData as Data
}
func build() -> Data {
return httpBody as Data
}
}
}
extension NSMutableData {
func append(_ string: String) {
if let data = string.data(using: .utf8) {
self.append(data)
}
}
}

View File

@ -0,0 +1,25 @@
import Foundation
public enum NetworkingError: String, Swift.Error {
case cannotCreateUrlRequest
}
public enum Method: String {
case delete = "DELETE", get = "GET", head = "HEAD", patch = "PATCH", post = "POST", put = "PUT"
}
public protocol TargetType {
var path: String { get }
var method: Method { get }
var headers: [String: String]? { get }
var queryItems: [(String, String)]? { get }
var httpBody: Data? { get }
}
extension [String: String] {
var contentTypeApplicationJson: [String: String] {
var selfCopy = self
selfCopy["content-type"] = "application/json"
return selfCopy
}
}

View File

@ -0,0 +1,134 @@
import Foundation
extension Mastodon {
public enum Account {
case account(AccountId)
case verifyCredentials
case followers(AccountId, MaxId?, SinceId?, MinId?, Limit?, Page?)
case following(AccountId, MaxId?, SinceId?, MinId?, Limit?, Page?)
case statuses(AccountId, Bool, Bool, MaxId?, SinceId?, MinId?, Limit?)
case follow(AccountId)
case unfollow(AccountId)
case block(AccountId)
case unblock(AccountId)
case mute(AccountId)
case unmute(AccountId)
case relationships([AccountId])
case search(SearchQuery, Int)
}
}
extension Mastodon.Account: TargetType {
fileprivate var apiPath: String { return "/api/v1/accounts" }
public var path: String {
switch self {
case .account(let id):
return "\(apiPath)/\(id)"
case .verifyCredentials:
return "\(apiPath)/verify_credentials"
case .followers(let id, _, _, _, _, _):
return "\(apiPath)/\(id)/followers"
case .following(let id, _, _, _, _, _):
return "\(apiPath)/\(id)/following"
case .statuses(let id, _, _, _, _, _, _):
return "\(apiPath)/\(id)/statuses"
case .follow(let id):
return "\(apiPath)/\(id)/follow"
case .unfollow(let id):
return "\(apiPath)/\(id)/unfollow"
case .block(let id):
return "\(apiPath)/\(id)/block"
case .unblock(let id):
return "\(apiPath)/\(id)/unblock"
case .mute(let id):
return "\(apiPath)/\(id)/mute"
case .unmute(let id):
return "\(apiPath)/\(id)/unmute"
case .relationships(_):
return "\(apiPath)/relationships"
case .search(_, _):
return "\(apiPath)/search"
}
}
public var method: Method {
switch self {
case .follow(_), .unfollow(_), .block(_), .unblock(_), .mute(_), .unmute(_):
return .post
default:
return .get
}
}
public var queryItems: [(String, String)]? {
var params: [(String, String)] = []
var maxId: MaxId? = nil
var sinceId: SinceId? = nil
var minId: MinId? = nil
var limit: Limit? = nil
var page: Page? = nil
switch self {
case .statuses(_, let onlyMedia, let excludeReplies, let _maxId, let _sinceId, let _minId, let _limit):
params.append(contentsOf: [
("only_media", onlyMedia.asString),
("exclude_replies", excludeReplies.asString)
])
maxId = _maxId
sinceId = _sinceId
minId = _minId
limit = _limit
case .relationships(let id):
return id.map({ id in
("id[]", id)
})
case .search(let query, let limit):
return [
("q", query),
("limit", limit.asString)
]
case .following(_, let _maxId, let _sinceId, let _minId, let _limit, let _page):
maxId = _maxId
sinceId = _sinceId
minId = _minId
limit = _limit
page = _page
case .followers(_, let _maxId, let _sinceId, let _minId, let _limit, let _page):
maxId = _maxId
sinceId = _sinceId
minId = _minId
limit = _limit
page = _page
default:
return nil
}
if let maxId {
params.append(("max_id", maxId))
}
if let sinceId {
params.append(("since_id", sinceId))
}
if let minId {
params.append(("min_id", minId))
}
if let limit {
params.append(("limit", "\(limit)"))
}
if let page {
params.append(("page", "\(page)"))
}
return params
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,67 @@
import Foundation
extension Mastodon {
public enum Apps {
case register(clientName: String, redirectUris: String, scopes: String?, website: String?)
}
}
extension Mastodon.Apps: TargetType {
struct Request: Encodable {
let clientName: String
let redirectUris: String
let scopes: String?
let website: String?
enum CodingKeys: String, CodingKey {
case clientName = "client_name"
case redirectUris = "redirect_uris"
case scopes
case website
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<Mastodon.Apps.Request.CodingKeys> = encoder.container(keyedBy: Mastodon.Apps.Request.CodingKeys.self)
try container.encode(self.clientName, forKey: Mastodon.Apps.Request.CodingKeys.clientName)
try container.encode(self.redirectUris, forKey: Mastodon.Apps.Request.CodingKeys.redirectUris)
try container.encode(self.scopes, forKey: Mastodon.Apps.Request.CodingKeys.scopes)
try container.encode(self.website, forKey: Mastodon.Apps.Request.CodingKeys.website)
}
}
fileprivate var apiPath: String { return "/api/v1/apps" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .register(_, _, _, _):
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .register(_, _, _, _):
return .post
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
switch self {
case .register(let clientName, let redirectUris, let scopes, let website):
return try? JSONEncoder().encode(
Request(clientName: clientName, redirectUris: redirectUris, scopes: scopes, website: website)
)
}
}
}

View File

@ -0,0 +1,43 @@
import Foundation
extension Mastodon {
public enum Blocks {
case blocks
}
}
extension Mastodon.Blocks: TargetType {
fileprivate var apiPath: String { return "/api/v1/blocks" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .blocks:
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .blocks:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
case .blocks:
return nil
}
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,43 @@
import Foundation
extension Mastodon {
public enum Favourites {
case favourites
}
}
extension Mastodon.Favourites: TargetType {
fileprivate var apiPath: String { return "/api/v1/favourites" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .favourites:
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .favourites:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
case .favourites:
return nil
}
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,57 @@
import Foundation
extension Mastodon {
public enum FollowRequests {
case followRequests
case authorize(String)
case reject(String)
}
}
extension Mastodon.FollowRequests: TargetType {
fileprivate var apiPath: String { return "/api/v1/follow_requests" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .followRequests:
return "\(apiPath)"
case .authorize(_):
return "\(apiPath)/authorize"
case .reject(_):
return "\(apiPath)/reject"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .followRequests:
return .get
case .authorize(_), .reject(_):
return .post
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
switch self {
case .followRequests:
return nil
case .authorize(let id):
return try? JSONEncoder().encode(
["id": id]
)
case .reject:
return nil
}
}
}

View File

@ -0,0 +1,45 @@
import Foundation
extension Mastodon {
public enum Follows {
case follow(String)
}
}
extension Mastodon.Follows: TargetType {
fileprivate var apiPath: String { return "/api/v1/follows" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .follow(_):
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .follow(_):
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
switch self {
case .follow(let uri):
return try? JSONEncoder().encode(
["uri": uri]
)
}
}
}

View File

@ -0,0 +1,43 @@
import Foundation
extension Mastodon {
public enum Instances {
case instance
}
}
extension Mastodon.Instances: TargetType {
fileprivate var apiPath: String { return "/api/v1/instance" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .instance:
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .instance:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
case .instance:
return nil
}
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,59 @@
import Foundation
extension Mastodon {
public enum Markers {
public enum Timeline: String, Encodable {
case home
case notifications
}
case set([Timeline: StatusId])
case read(Set<Timeline>)
}
}
extension Mastodon.Markers: TargetType {
fileprivate var apiPath: String { return "/api/v1/markers" }
public var path: String {
return apiPath
}
public var method: Method {
switch self {
case .set(_):
return .post
case .read(_):
return .get
}
}
public var headers: [String : String]? {
[:].contentTypeApplicationJson
}
public var queryItems: [(String, String)]? {
switch self {
case .set(_):
return nil
case .read(let markers):
return Array(markers)
.map { ("timeline[]", $0.rawValue) }
}
}
public var httpBody: Data? {
switch self {
case .set(let markers):
let dict = Dictionary(uniqueKeysWithValues: markers.map { ($0.rawValue, ["last_read_id": $1]) })
let data = try? JSONEncoder().encode(dict)
return data
case .read(_):
return nil
}
}
}

View File

@ -0,0 +1,8 @@
import Foundation
public typealias AccountId = String
public typealias SearchQuery = String
public class Mastodon {
}

View File

@ -0,0 +1,53 @@
import Foundation
fileprivate let multipartBoundary = UUID().uuidString
extension Mastodon {
public enum Media {
case upload(Data, String)
}
}
extension Mastodon.Media: TargetType {
fileprivate var apiPath: String { return "/api/v1/media" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .upload:
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .upload:
return .post
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
case .upload:
return nil
}
}
public var headers: [String: String]? {
switch self {
case .upload:
return ["content-type": "multipart/form-data; boundary=\(multipartBoundary)"]
}
}
public var httpBody: Data? {
switch self {
case .upload(let data, let mimeType):
return data.getMultipartFormDataBuilder(withBoundary: multipartBoundary)?
.addDataField(named: "file", data: data, mimeType: mimeType)
.build()
}
}
}

View File

@ -0,0 +1,43 @@
import Foundation
extension Mastodon {
public enum Mutes {
case mutes
}
}
extension Mastodon.Mutes: TargetType {
fileprivate var apiPath: String { return "/api/v1/mutes" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .mutes:
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .mutes:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
case .mutes:
return nil
}
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,51 @@
import Foundation
extension Mastodon {
public enum Notifications {
case notifications
case notification(String)
case clear
}
}
extension Mastodon.Notifications: TargetType {
fileprivate var apiPath: String { return "/api/v1/notifications" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .notifications:
return "\(apiPath)"
case .notification(let id):
return "\(apiPath)/\(id)"
case .clear:
return "\(apiPath)/clear"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .notifications, .notification(_):
return .get
case .clear:
return .post
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
default:
return nil
}
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,85 @@
import Foundation
public typealias ClientId = String
public typealias ClientSecret = String
public typealias UsernameType = String
public typealias PasswordType = String
extension Mastodon {
public enum OAuth {
case authenticate(App, UsernameType, PasswordType, String?)
}
}
extension Mastodon.OAuth: TargetType {
struct Request: Encodable {
let clientId: String
let clientSecret: String
let grantType: String
let username: String
let password: String
let scope: String
enum CodingKeys: String, CodingKey {
case clientId = "client_id"
case clientSecret = "client_secret"
case grantType = "grant_type"
case username
case password
case scope
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<Mastodon.OAuth.Request.CodingKeys> = encoder.container(keyedBy: Mastodon.OAuth.Request.CodingKeys.self)
try container.encode(self.clientId, forKey: Mastodon.OAuth.Request.CodingKeys.clientId)
try container.encode(self.clientSecret, forKey: Mastodon.OAuth.Request.CodingKeys.clientSecret)
try container.encode(self.grantType, forKey: Mastodon.OAuth.Request.CodingKeys.grantType)
try container.encode(self.username, forKey: Mastodon.OAuth.Request.CodingKeys.username)
try container.encode(self.password, forKey: Mastodon.OAuth.Request.CodingKeys.password)
try container.encode(self.scope, forKey: Mastodon.OAuth.Request.CodingKeys.scope)
}
}
fileprivate var apiPath: String { return "/oauth/token" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case.authenticate(_, _, _, _):
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .authenticate(_, _, _, _):
return .post
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
switch self {
case .authenticate(let app, let username, let password, let scope):
return try? JSONEncoder().encode(
Request(
clientId: app.clientId,
clientSecret: app.clientSecret,
grantType: "password",
username: username,
password: password,
scope: scope ?? ""
)
)
}
}
}

View File

@ -0,0 +1,69 @@
import Foundation
extension Mastodon {
public enum Reports {
case list
case report(String, [String], String)
}
}
extension Mastodon.Reports: TargetType {
private struct Request: Encodable {
let accountId: String
let statusIds: [String]
let comment: String
private enum CodingKeys: String, CodingKey {
case accountId = "account_id"
case statusIds = "status_ids"
case comment
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<Mastodon.Reports.Request.CodingKeys> = encoder.container(keyedBy: Mastodon.Reports.Request.CodingKeys.self)
try container.encode(self.accountId, forKey: Mastodon.Reports.Request.CodingKeys.accountId)
try container.encode(self.statusIds, forKey: Mastodon.Reports.Request.CodingKeys.statusIds)
try container.encode(self.comment, forKey: Mastodon.Reports.Request.CodingKeys.comment)
}
}
fileprivate var apiPath: String { return "/api/v1/reports" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .list, .report(_, _, _):
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .list:
return .get
case .report(_, _, _):
return .post
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
switch self {
case .list:
return nil
case .report(let accountId, let statusIds, let comment):
return try? JSONEncoder().encode(
Request(accountId: accountId, statusIds: statusIds, comment: comment)
)
}
}
}

View File

@ -0,0 +1,47 @@
import Foundation
extension Mastodon {
public enum Search {
case search(SearchQuery, Bool)
}
}
extension Mastodon.Search: TargetType {
fileprivate var apiPath: String { return "/api/v1/search" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .search:
return "\(apiPath)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .search:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
switch self {
case .search(let query, let resolveNonLocal):
return [
("q", query),
("resolve", resolveNonLocal.asString)
]
}
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,169 @@
import Foundation
extension Mastodon {
public enum Statuses {
public enum Visibility: String, Encodable {
case direct = "direct"
case priv = "private"
case unlisted = "unlisted"
case pub = "public"
}
case status(String)
case context(String)
case card(String)
case rebloggedBy(String)
case favouritedBy(String)
case new(Components)
case delete(String)
case reblog(String)
case unreblog(String)
case favourite(String)
case unfavourite(String)
case bookmark(String)
case unbookmark(String)
}
}
extension Mastodon.Statuses {
public struct Components {
public let inReplyToId: StatusId?
public let text: String
public let spoilerText: String
public let mediaIds: [String]
public let visibility: Visibility
public let sensitive: Bool
public let pollOptions: [String]
public let pollExpiresIn: Int
public let pollMultipleChoice: Bool
public init(
inReplyToId: StatusId? = nil,
text: String,
spoilerText: String = "",
mediaIds: [String] = [],
visibility: Visibility = .pub,
sensitive: Bool = false,
pollOptions: [String] = [],
pollExpiresIn: Int = 0,
pollMultipleChoice: Bool = false) {
self.inReplyToId = inReplyToId
self.text = text
self.spoilerText = spoilerText
self.mediaIds = mediaIds
self.visibility = visibility
self.sensitive = sensitive
self.pollOptions = pollOptions
self.pollExpiresIn = pollExpiresIn
self.pollMultipleChoice = pollMultipleChoice
}
}
}
extension Mastodon.Statuses: TargetType {
struct Request: Encodable {
let status: String
let inReplyToId: String?
let mediaIds: [String]?
let sensitive: Bool
let spoilerText: String?
let visibility: Visibility
enum CodingKeys: String, CodingKey {
case status
case inReplyToId = "in_reply_to_id"
case mediaIds = "media_ids"
case sensitive
case spoilerText = "spoiler_text"
case visibility
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<Mastodon.Statuses.Request.CodingKeys> = encoder.container(keyedBy: Mastodon.Statuses.Request.CodingKeys.self)
try container.encode(self.status, forKey: Mastodon.Statuses.Request.CodingKeys.status)
try container.encode(self.inReplyToId, forKey: Mastodon.Statuses.Request.CodingKeys.inReplyToId)
try container.encode(self.mediaIds, forKey: Mastodon.Statuses.Request.CodingKeys.mediaIds)
try container.encode(self.sensitive, forKey: Mastodon.Statuses.Request.CodingKeys.sensitive)
try container.encodeIfPresent(self.spoilerText, forKey: Mastodon.Statuses.Request.CodingKeys.spoilerText)
try container.encode(self.visibility, forKey: Mastodon.Statuses.Request.CodingKeys.visibility)
}
}
fileprivate var apiPath: String { return "/api/v1/statuses" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .status(let id):
return "\(apiPath)/\(id)"
case .context(let id):
return "\(apiPath)/\(id)/context"
case .card(let id):
return "\(apiPath)/\(id)/card"
case .rebloggedBy(let id):
return "\(apiPath)/\(id)/reblogged_by"
case .favouritedBy(let id):
return "\(apiPath)/\(id)/favourited_by"
case .new(_):
return "\(apiPath)"
case .delete(let id):
return "\(apiPath)/\(id)"
case .reblog(let id):
return "\(apiPath)/\(id)/reblog"
case .unreblog(let id):
return "\(apiPath)/\(id)/unreblog"
case .favourite(let id):
return "\(apiPath)/\(id)/favourite"
case .unfavourite(let id):
return "\(apiPath)/\(id)/unfavourite"
case .bookmark(let id):
return "\(apiPath)/\(id)/bookmark"
case .unbookmark(let id):
return "\(apiPath)/\(id)/unbookmark"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
case .new(_),
.reblog(_),
.unreblog(_),
.favourite(_),
.unfavourite(_),
.bookmark(_),
.unbookmark(_):
return .post
case .delete(_):
return .delete
default:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
nil
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
switch self {
case .new(let components):
return try? JSONEncoder().encode(
Request(
status: components.text,
inReplyToId: components.inReplyToId,
mediaIds: components.mediaIds,
sensitive: components.sensitive,
spoilerText: components.spoilerText,
visibility: components.visibility)
)
default:
return nil
}
}
}

View File

@ -0,0 +1,87 @@
import Foundation
public typealias SinceId = StatusId
public typealias MaxId = StatusId
public typealias MinId = StatusId
public typealias Limit = Int
public typealias Page = Int
extension Mastodon {
public enum Timelines {
case home(MaxId?, SinceId?, MinId?, Limit?)
case pub(Bool, MaxId?, SinceId?) // Bool = local
case tag(String, Bool, MaxId?, SinceId?) // Bool = local
}
}
extension Mastodon.Timelines: TargetType {
fileprivate var apiPath: String { return "/api/v1/timelines" }
/// The path to be appended to `baseURL` to form the full `URL`.
public var path: String {
switch self {
case .home:
return "\(apiPath)/home"
case .pub:
return "\(apiPath)/public"
case .tag(let hashtag, _, _, _):
return "\(apiPath)/tag/\(hashtag)"
}
}
/// The HTTP method used in the request.
public var method: Method {
switch self {
default:
return .get
}
}
/// The parameters to be incoded in the request.
public var queryItems: [(String, String)]? {
var params: [(String, String)] = []
var local: Bool? = nil
var maxId: MaxId? = nil
var sinceId: SinceId? = nil
var minId: MinId? = nil
var limit: Limit? = nil
switch self {
case .tag(_, let _local, let _maxId, let _sinceId),
.pub(let _local, let _maxId, let _sinceId):
local = _local
maxId = _maxId
sinceId = _sinceId
case .home(let _maxId, let _sinceId, let _minId, let _limit):
maxId = _maxId
sinceId = _sinceId
minId = _minId
limit = _limit
}
if let maxId {
params.append(("max_id", maxId))
}
if let sinceId {
params.append(("since_id", sinceId))
}
if let minId {
params.append(("min_id", minId))
}
if let limit {
params.append(("limit", "\(limit)"))
}
if let local = local {
params.append(("local", local.asString))
}
return params
}
public var headers: [String: String]? {
[:].contentTypeApplicationJson
}
public var httpBody: Data? {
nil
}
}

View File

@ -0,0 +1,11 @@
import XCTest
@testable import MastodonKit
final class MastodonKitTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(MastodonKit().text, "Hello, World!")
}
}

View File

@ -34,7 +34,6 @@
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497A29640C8200751DF7 /* UsernameRow.swift */; };
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497C29640D5900751DF7 /* InteractionRow.swift */; };
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497E296416C800751DF7 /* CommentsSection.swift */; };
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */; };
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */; };
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49842964301800751DF7 /* StatusData+Attachments.swift */; };
F85D49872964334100751DF7 /* String+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49862964334100751DF7 /* String+Date.swift */; };
@ -48,7 +47,6 @@
F866F6A729604629002E8F88 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A629604629002E8F88 /* SignInView.swift */; };
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A929605AFA002E8F88 /* SceneDelegate.swift */; };
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */; };
F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F866F6B629608467002E8F88 /* MastodonSwift */; };
F86B7214296BFDCE00EE59EC /* UserProfileHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */; };
F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */; };
F86B7218296C27C100EE59EC /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86B7217296C27C100EE59EC /* ActionButton.swift */; };
@ -79,13 +77,11 @@
F897978D2968369600B22335 /* HapticService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897978C2968369600B22335 /* HapticService.swift */; };
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897978E29684BCB00B22335 /* LoadingView.swift */; };
F8984E4D296B648000A2610F /* UIImage+Blurhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */; };
F89992C7296D3DF8005994BF /* MastodonKit in Frameworks */ = {isa = PBXBuildFile; productRef = F89992C6296D3DF8005994BF /* MastodonKit */; };
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */; };
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14393296AF21B001FE31D /* Double+Round.swift */; };
F8C14398296B208A001FE31D /* HTTPStatusCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14397296B208A001FE31D /* HTTPStatusCode.swift */; };
F8C1439B296B227C001FE31D /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C1439A296B227C001FE31D /* NetworkError.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -113,7 +109,6 @@
F85D497A29640C8200751DF7 /* UsernameRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRow.swift; sourceTree = "<group>"; };
F85D497C29640D5900751DF7 /* InteractionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionRow.swift; sourceTree = "<group>"; };
F85D497E296416C800751DF7 /* CommentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsSection.swift; sourceTree = "<group>"; };
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Context.swift"; sourceTree = "<group>"; };
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Comperable.swift"; sourceTree = "<group>"; };
F85D49842964301800751DF7 /* StatusData+Attachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Attachments.swift"; sourceTree = "<group>"; };
F85D49862964334100751DF7 /* String+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Date.swift"; sourceTree = "<group>"; };
@ -128,6 +123,7 @@
F866F6A829604FFF002E8F88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = "<group>"; };
F86728AD296D3CE200475EC9 /* MastodonKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MastodonKit; sourceTree = "<group>"; };
F86B7213296BFDCE00EE59EC /* UserProfileHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeader.swift; sourceTree = "<group>"; };
F86B7215296BFFDA00EE59EC /* UserProfileStatuses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileStatuses.swift; sourceTree = "<group>"; };
F86B7217296C27C100EE59EC /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
@ -162,11 +158,8 @@
F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Blurhash.swift"; sourceTree = "<group>"; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Account.swift"; sourceTree = "<group>"; };
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
F8C14393296AF21B001FE31D /* Double+Round.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Round.swift"; sourceTree = "<group>"; };
F8C14397296B208A001FE31D /* HTTPStatusCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatusCode.swift; sourceTree = "<group>"; };
F8C1439A296B227C001FE31D /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -174,7 +167,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */,
F89992C7296D3DF8005994BF /* MastodonKit in Frameworks */,
F8210DD52966BB7E001D9973 /* Nuke in Frameworks */,
F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */,
F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */,
@ -215,8 +208,6 @@
isa = PBXGroup;
children = (
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */,
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */,
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */,
F85D49862964334100751DF7 /* String+Date.swift */,
F8C14391296AF0B3001FE31D /* String+Exif.swift */,
F8210DE22966D256001D9973 /* Status+StatusData.swift */,
@ -232,10 +223,8 @@
F8341F95295C640C009C8EE6 /* Models */ = {
isa = PBXGroup;
children = (
F8C14399296B2150001FE31D /* Errors */,
F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */,
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */,
F8C14397296B208A001FE31D /* HTTPStatusCode.swift */,
);
path = Models;
sourceTree = "<group>";
@ -313,9 +302,11 @@
F88C245F295C37B80006098B = {
isa = PBXGroup;
children = (
F86728AD296D3CE200475EC9 /* MastodonKit */,
F88ABD9529687D4D004EF61E /* README.md */,
F88C246A295C37B80006098B /* Vernissage */,
F88C2469295C37B80006098B /* Products */,
F89992C5296D3DF8005994BF /* Frameworks */,
);
sourceTree = "<group>";
};
@ -380,12 +371,11 @@
path = Haptics;
sourceTree = "<group>";
};
F8C14399296B2150001FE31D /* Errors */ = {
F89992C5296D3DF8005994BF /* Frameworks */ = {
isa = PBXGroup;
children = (
F8C1439A296B227C001FE31D /* NetworkError.swift */,
);
path = Errors;
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
@ -405,10 +395,10 @@
);
name = Vernissage;
packageProductDependencies = (
F866F6B629608467002E8F88 /* MastodonSwift */,
F8210DD42966BB7E001D9973 /* Nuke */,
F8210DD62966BB7E001D9973 /* NukeExtensions */,
F8210DD82966BB7E001D9973 /* NukeUI */,
F89992C6296D3DF8005994BF /* MastodonKit */,
);
productName = Vernissage;
productReference = F88C2468295C37B80006098B /* Vernissage.app */;
@ -439,7 +429,6 @@
);
mainGroup = F88C245F295C37B80006098B;
packageReferences = (
F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */,
F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */,
);
productRefGroup = F88C2469295C37B80006098B /* Products */;
@ -479,7 +468,6 @@
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */,
F85DBF93296760790069BF89 /* CacheAvatarService.swift in Sources */,
F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */,
F8C1439B296B227C001FE31D /* NetworkError.swift in Sources */,
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */,
F85D4975296407F100751DF7 /* TimelineService.swift in Sources */,
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
@ -488,7 +476,6 @@
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
F8210DE12966D0C4001D9973 /* StatusService.swift in Sources */,
F85DBF8F296732E20069BF89 /* FollowersView.swift in Sources */,
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */,
F85D49872964334100751DF7 /* String+Date.swift in Sources */,
F897978829681B9C00B22335 /* UserAvatar.swift in Sources */,
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
@ -507,7 +494,6 @@
F897978D2968369600B22335 /* HapticService.swift in Sources */,
F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */,
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */,
F88C246E295C37B80006098B /* MainView.swift in Sources */,
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */,
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
@ -518,7 +504,6 @@
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */,
F8210DE32966D256001D9973 /* Status+StatusData.swift in Sources */,
F8C14398296B208A001FE31D /* HTTPStatusCode.swift in Sources */,
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */,
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
@ -757,16 +742,8 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke";
requirement = {
branch = master;
kind = branch;
};
};
F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mczachurski/Mastodon.swift";
requirement = {
branch = main;
kind = branch;
kind = upToNextMajorVersion;
minimumVersion = 11.5.3;
};
};
/* End XCRemoteSwiftPackageReference section */
@ -787,10 +764,9 @@
package = F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = NukeUI;
};
F866F6B629608467002E8F88 /* MastodonSwift */ = {
F89992C6296D3DF8005994BF /* MastodonKit */ = {
isa = XCSwiftPackageProductDependency;
package = F866F6B529608467002E8F88 /* XCRemoteSwiftPackageReference "Mastodon" */;
productName = MastodonSwift;
productName = MastodonKit;
};
/* End XCSwiftPackageProductDependency section */

View File

@ -6,7 +6,7 @@
import Foundation
import MastodonSwift
import MastodonKit
extension AttachmentData {
func copyFrom(_ attachment: Attachment) {

View File

@ -5,7 +5,7 @@
//
import Foundation
import MastodonSwift
import MastodonKit
extension StatusData {
func copyFrom(_ status: Status) {

View File

@ -7,7 +7,7 @@
import Foundation
import CoreData
import MastodonSwift
import MastodonKit
class StatusDataHandler {
public static let shared = StatusDataHandler()

View File

@ -5,7 +5,7 @@
//
import Foundation
import MastodonSwift
import MastodonKit
extension Status {
public func getImageWidth() -> Int32? {

View File

@ -5,7 +5,7 @@
//
import Foundation
import MastodonSwift
import MastodonKit
extension Status {
func createStatusData() async throws -> StatusData {

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
import OAuthSwift
class SceneDelegate: NSObject, UISceneDelegate {

View File

@ -5,7 +5,7 @@
//
import Foundation
import MastodonSwift
import MastodonKit
public class AccountService {
public static let shared = AccountService()

View File

@ -5,7 +5,7 @@
//
import Foundation
import MastodonSwift
import MastodonKit
public class AuthorizationService {
public static let shared = AuthorizationService()

View File

@ -4,8 +4,8 @@
// Licensed under the MIT License.
//
import Foundation
import MastodonKit
public class RemoteFileService {
public static let shared = RemoteFileService()

View File

@ -5,7 +5,7 @@
//
import Foundation
import MastodonSwift
import MastodonKit
public class StatusService {
public static let shared = StatusService()

View File

@ -6,7 +6,7 @@
import Foundation
import CoreData
import MastodonSwift
import MastodonKit
public class TimelineService {
public static let shared = TimelineService()

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
struct FollowersView: View {
@EnvironmentObject var applicationState: ApplicationState

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
struct FollowingView: View {
@EnvironmentObject var applicationState: ApplicationState

View File

@ -7,7 +7,7 @@
import SwiftUI
import UIKit
import CoreData
import MastodonSwift
import MastodonKit
struct MainView: View {
@Environment(\.managedObjectContext) private var viewContext

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
import AVFoundation
struct StatusView: View {

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
struct UserProfileView: View {
@EnvironmentObject private var applicationState: ApplicationState

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
struct CommentsSection: View {
@Environment(\.colorScheme) var colorScheme

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
import NukeUI
struct ImageRowAsync: View {

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
struct UserProfileHeader: View {
@EnvironmentObject private var applicationState: ApplicationState

View File

@ -5,7 +5,7 @@
//
import SwiftUI
import MastodonSwift
import MastodonKit
struct UserProfileStatuses: View {
@EnvironmentObject private var applicationState: ApplicationState