V2 instance API, add translation to InstanceFeatures

This commit is contained in:
Shadowfacts 2023-12-04 17:55:03 -05:00
parent 158940f8e6
commit 5e609aa40d
11 changed files with 210 additions and 44 deletions

View File

@ -21,7 +21,8 @@ public class InstanceFeatures: ObservableObject {
@Published public private(set) var charsReservedPerURL = 23
@Published public private(set) var maxPollOptionChars: Int?
@Published public private(set) var maxPollOptionsCount: Int?
@Published public private(set) var mediaAttachmentsConfiguration: Instance.MediaAttachmentsConfiguration?
@Published public private(set) var mediaAttachmentsConfiguration: InstanceV1.MediaAttachmentsConfiguration?
@Published public private(set) var translation: Bool = false
public var localOnlyPosts: Bool {
switch instanceType {
@ -240,6 +241,7 @@ public class InstanceFeatures: ObservableObject {
maxPollOptionsCount = pollsConfig.maxOptions
}
mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
translation = instance.translation
_featuresUpdated.send()
}

View File

@ -9,26 +9,39 @@ import Foundation
import Pachyderm
public struct InstanceInfo {
public let version: String
public let maxStatusCharacters: Int?
public let configuration: Instance.Configuration?
public let pollsConfiguration: Instance.PollsConfiguration?
public var version: String
public var maxStatusCharacters: Int?
public var configuration: InstanceV1.Configuration?
public var pollsConfiguration: InstanceV1.PollsConfiguration?
public var translation: Bool
public init(version: String, maxStatusCharacters: Int?, configuration: Instance.Configuration?, pollsConfiguration: Instance.PollsConfiguration?) {
public init(
version: String,
maxStatusCharacters: Int?,
configuration: InstanceV1.Configuration?,
pollsConfiguration: InstanceV1.PollsConfiguration?,
translation: Bool
) {
self.version = version
self.maxStatusCharacters = maxStatusCharacters
self.configuration = configuration
self.pollsConfiguration = pollsConfiguration
self.translation = translation
}
}
extension InstanceInfo {
public init(instance: Instance) {
public init(v1 instance: InstanceV1) {
self.init(
version: instance.version,
maxStatusCharacters: instance.maxStatusCharacters,
configuration: instance.configuration,
pollsConfiguration: instance.pollsConfiguration
pollsConfiguration: instance.pollsConfiguration,
translation: false
)
}
public mutating func update(v2: InstanceV2) {
translation = v2.configuration.translation.enabled
}
}

View File

@ -231,8 +231,12 @@ public class Client {
return Request<[Relationship]>(method: .get, path: "/api/v1/accounts/relationships", queryParameters: "id" => accounts)
}
public static func getInstance() -> Request<Instance> {
return Request<Instance>(method: .get, path: "/api/v1/instance")
public static func getInstanceV1() -> Request<InstanceV1> {
return Request<InstanceV1>(method: .get, path: "/api/v1/instance")
}
public static func getInstanceV2() -> Request<InstanceV2> {
return Request<InstanceV2>(method: .get, path: "/api/v2/instance")
}
public static func getCustomEmoji() -> Request<[Emoji]> {

View File

@ -1,5 +1,5 @@
//
// Instance.swift
// InstanceV1.swift
// Pachyderm
//
// Created by Shadowfacts on 9/9/18.
@ -8,7 +8,7 @@
import Foundation
public struct Instance: Decodable, Sendable {
public struct InstanceV1: Decodable, Sendable {
public let uri: String
public let title: String
public let description: String
@ -92,7 +92,7 @@ public struct Instance: Decodable, Sendable {
}
}
extension Instance {
extension InstanceV1 {
public struct Stats: Decodable, Sendable {
public let domainCount: Int?
public let statusCount: Int?
@ -106,7 +106,7 @@ extension Instance {
}
}
extension Instance {
extension InstanceV1 {
public struct Configuration: Codable, Sendable {
public let statuses: StatusesConfiguration
public let mediaAttachments: MediaAttachmentsConfiguration
@ -121,7 +121,8 @@ extension Instance {
}
}
extension Instance {
extension InstanceV1 {
// note: also used by InstanceV2
public struct StatusesConfiguration: Codable, Sendable {
public let maxCharacters: Int
public let maxMediaAttachments: Int
@ -135,7 +136,8 @@ extension Instance {
}
}
extension Instance {
extension InstanceV1 {
// note: also used by InstanceV2
public struct MediaAttachmentsConfiguration: Codable, Sendable {
public let supportedMIMETypes: [String]
public let imageSizeLimit: Int
@ -155,7 +157,8 @@ extension Instance {
}
}
extension Instance {
extension InstanceV1 {
// note: also used by InstanceV2
public struct PollsConfiguration: Codable, Sendable {
public let maxOptions: Int
public let maxCharactersPerOption: Int
@ -171,7 +174,8 @@ extension Instance {
}
}
extension Instance {
extension InstanceV1 {
// note: also used by InstanceV2
public struct Rule: Decodable, Identifiable, Sendable {
public let id: String
public let text: String

View File

@ -0,0 +1,125 @@
//
// InstanceV2.swift
// Pachyderm
//
// Created by Shadowfacts on 12/4/23.
//
import Foundation
public struct InstanceV2: Decodable, Sendable {
public let domain: String
public let title: String
public let version: String
public let sourceURL: String
public let description: String
public let usage: Usage
public let thumbnail: Thumbnail
public let languages: [String]
public let configuration: Configuration
public let registrations: Registrations
public let contact: Contact
public let rules: [InstanceV1.Rule]
private enum CodingKeys: String, CodingKey {
case domain
case title
case version
case sourceURL = "source_url"
case description
case usage
case thumbnail
case languages
case configuration
case registrations
case contact
case rules
}
}
extension InstanceV2 {
public struct Usage: Decodable, Sendable {
public let users: Users
}
public struct Users: Decodable, Sendable {
public let activeMonth: Int
private enum CodingKeys: String, CodingKey {
case activeMonth = "active_month"
}
}
}
extension InstanceV2 {
public struct Thumbnail: Decodable, Sendable {
public let url: String
public let blurhash: String?
public let versions: ThumbnailVersions
}
public struct ThumbnailVersions: Decodable, Sendable {
public let oneX: String?
public let twoX: String?
private enum CodingKeys: String, CodingKey {
case oneX = "@1x"
case twoX = "@2x"
}
}
}
extension InstanceV2 {
public struct Configuration: Decodable, Sendable {
public let urls: URLs
public let accounts: Accounts
public let statuses: InstanceV1.StatusesConfiguration
public let mediaAttachments: InstanceV1.MediaAttachmentsConfiguration
public let polls: InstanceV1.PollsConfiguration
public let translation: Translation
private enum CodingKeys: String, CodingKey {
case urls
case accounts
case statuses
case mediaAttachments = "media_attachments"
case polls
case translation
}
}
public struct URLs: Decodable, Sendable {
// the docs incorrectly say the key for this is "streaming_api"
public let streaming: String
}
public struct Accounts: Decodable, Sendable {
public let maxFeaturedTags: Int
private enum CodingKeys: String, CodingKey {
case maxFeaturedTags = "max_featured_tags"
}
}
public struct Translation: Decodable, Sendable {
public let enabled: Bool
}
}
extension InstanceV2 {
public struct Registrations: Decodable, Sendable {
public let enabled: Bool
public let approvalRequired: Bool
public let message: String?
private enum CodingKeys: String, CodingKey {
case enabled
case approvalRequired = "approval_required"
case message
}
}
}
extension InstanceV2 {
public struct Contact: Decodable, Sendable {
public let email: String
public let account: Account
}
}

View File

@ -28,7 +28,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
self.instanceFeatures = InstanceFeatures()
Task { @MainActor in
async let instance = try? await run(Client.getInstance()).0
async let instance = try? await run(Client.getInstanceV1()).0
async let nodeInfo: NodeInfo? = await withCheckedContinuation({ continuation in
self.client.nodeInfo { response in
switch response {
@ -40,7 +40,7 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject {
}
})
guard let instance = await instance else { return }
self.instanceFeatures.update(instance: InstanceInfo(instance: instance), nodeInfo: await nodeInfo)
self.instanceFeatures.update(instance: InstanceInfo(v1: instance), nodeInfo: await nodeInfo)
}
Task { @MainActor in

View File

@ -53,7 +53,8 @@ class MastodonController: ObservableObject {
let instanceFeatures = InstanceFeatures()
@Published private(set) var account: AccountMO?
@Published private(set) var instance: Instance?
@Published private(set) var instance: InstanceV1?
@Published private var instanceV2: InstanceV2?
@Published private(set) var instanceInfo: InstanceInfo!
@Published private(set) var nodeInfo: NodeInfo!
@Published private(set) var lists: [List] = []
@ -63,7 +64,7 @@ class MastodonController: ObservableObject {
private var cancellables = Set<AnyCancellable>()
private var pendingOwnInstanceRequestCallbacks = [(Result<Instance, Client.Error>) -> Void]()
private var pendingOwnInstanceRequestCallbacks = [(Result<InstanceV1, Client.Error>) -> Void]()
private var ownInstanceRequest: URLSessionTask?
var loggedIn: Bool {
@ -107,9 +108,14 @@ class MastodonController: ObservableObject {
$instance
.compactMap { $0 }
.sink { [unowned self] in
self.updateActiveInstance(from: $0)
self.instanceInfo = InstanceInfo(instance: $0)
.combineLatest($instanceV2)
.sink {[unowned self] (instance, v2) in
var info = InstanceInfo(v1: instance)
if let v2 {
info.update(v2: v2)
}
self.instanceInfo = info
self.updateActiveInstance(from: info)
}
.store(in: &cancellables)
@ -217,6 +223,10 @@ class MastodonController: ObservableObject {
_ = try await (ownAccount, ownInstance)
if instanceFeatures.hasMastodonVersion(4, 0, 0) {
async let _ = try? getOwnInstanceV2()
}
loadLists()
_ = await loadFilters()
await loadServerPreferences()
@ -277,7 +287,7 @@ class MastodonController: ObservableObject {
}
}
func getOwnInstance(completion: ((Instance) -> Void)? = nil) {
func getOwnInstance(completion: ((InstanceV1) -> Void)? = nil) {
getOwnInstanceInternal(retryAttempt: 0) {
if case let .success(instance) = $0 {
completion?(instance)
@ -286,7 +296,7 @@ class MastodonController: ObservableObject {
}
@MainActor
func getOwnInstance() async throws -> Instance {
func getOwnInstance() async throws -> InstanceV1 {
return try await withCheckedThrowingContinuation({ continuation in
getOwnInstanceInternal(retryAttempt: 0) { result in
continuation.resume(with: result)
@ -294,7 +304,7 @@ class MastodonController: ObservableObject {
})
}
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<Instance, Client.Error>) -> Void)?) {
private func getOwnInstanceInternal(retryAttempt: Int, completion: ((Result<InstanceV1, Client.Error>) -> Void)?) {
// this is main thread only to prevent concurrent access to ownInstanceRequest and pendingOwnInstanceRequestCallbacks
assert(Thread.isMainThread)
@ -306,7 +316,7 @@ class MastodonController: ObservableObject {
}
if ownInstanceRequest == nil {
let request = Client.getInstance()
let request = Client.getInstanceV1()
ownInstanceRequest = run(request) { (response) in
switch response {
case .failure(let error):
@ -361,6 +371,10 @@ class MastodonController: ObservableObject {
}
}
private func getOwnInstanceV2() async throws {
self.instanceV2 = try await client.run(Client.getInstanceV2()).0
}
// MainActor because the accountPreferences instance is bound to the view context
@MainActor
private func loadServerPreferences() async {
@ -376,13 +390,13 @@ class MastodonController: ObservableObject {
}
}
private func updateActiveInstance(from instance: Instance) {
private func updateActiveInstance(from info: InstanceInfo) {
persistentContainer.performBackgroundTask { context in
if let existing = try? context.fetch(ActiveInstance.fetchRequest()).first {
existing.update(from: instance)
existing.update(from: info)
} else {
let new = ActiveInstance(context: context)
new.update(from: instance)
new.update(from: info)
}
if context.hasChanges {
try? context.save()

View File

@ -22,18 +22,20 @@ public final class ActiveInstance: NSManagedObject {
@NSManaged public var maxStatusCharacters: Int
@NSManaged private var configurationData: Data?
@NSManaged private var pollsConfigurationData: Data?
@NSManaged public var translation: Bool
@LazilyDecoding(from: \ActiveInstance.configurationData, fallback: nil)
public var configuration: Instance.Configuration?
public var configuration: InstanceV1.Configuration?
@LazilyDecoding(from: \ActiveInstance.pollsConfigurationData, fallback: nil)
public var pollsConfiguration: Instance.PollsConfiguration?
public var pollsConfiguration: InstanceV1.PollsConfiguration?
func update(from instance: Instance) {
self.version = instance.version
self.maxStatusCharacters = instance.maxStatusCharacters ?? 500
self.configuration = instance.configuration
self.pollsConfiguration = instance.pollsConfiguration
func update(from info: InstanceInfo) {
self.version = info.version
self.maxStatusCharacters = info.maxStatusCharacters ?? 500
self.configuration = info.configuration
self.pollsConfiguration = info.pollsConfiguration
self.translation = info.translation
}
}
@ -43,7 +45,8 @@ extension InstanceInfo {
version: activeInstance.version,
maxStatusCharacters: activeInstance.maxStatusCharacters,
configuration: activeInstance.configuration,
pollsConfiguration: activeInstance.pollsConfiguration
pollsConfiguration: activeInstance.pollsConfiguration,
translation: activeInstance.translation
)
}
}

View File

@ -41,6 +41,7 @@
<attribute name="configurationData" optional="YES" attributeType="Binary"/>
<attribute name="maxStatusCharacters" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="pollsConfigurationData" optional="YES" attributeType="Binary"/>
<attribute name="translation" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="version" optional="YES" attributeType="String"/>
</entity>
<entity name="Filter" representedClassName="FilterMO" syncable="YES">

View File

@ -162,7 +162,7 @@ class InstanceSelectorTableViewController: UITableViewController {
}
let client = Client(baseURL: url, session: .appDefault)
let request = Client.getInstance()
let request = Client.getInstanceV1()
client.run(request) { (response) in
var snapshot = self.dataSource.snapshot()
if snapshot.indexOfSection(.selected) != nil {
@ -309,7 +309,7 @@ extension InstanceSelectorTableViewController {
case recommendedInstances
}
enum Item: Equatable, Hashable {
case selected(URL, Instance)
case selected(URL, InstanceV1)
case recommended(InstanceSelector.Instance)
static func ==(lhs: Item, rhs: Item) -> Bool {

View File

@ -16,7 +16,7 @@ class InstanceTableViewCell: UITableViewCell {
@IBOutlet weak var adultLabel: UILabel!
@IBOutlet weak var descriptionTextView: ContentTextView!
var instance: Instance?
var instance: InstanceV1?
var selectorInstance: InstanceSelector.Instance?
var thumbnailURL: URL?
@ -53,7 +53,7 @@ class InstanceTableViewCell: UITableViewCell {
updateThumbnail(url: instance.proxiedThumbnailURL)
}
func updateUI(instance: Instance) {
func updateUI(instance: InstanceV1) {
self.instance = instance
self.selectorInstance = nil