370 lines
12 KiB
Swift
370 lines
12 KiB
Swift
//
|
|
// InstanceFeatures.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 1/23/22.
|
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Combine
|
|
import Pachyderm
|
|
|
|
public final class InstanceFeatures: ObservableObject {
|
|
private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive)
|
|
|
|
private let _featuresUpdated = PassthroughSubject<Void, Never>()
|
|
public var featuresUpdated: some Publisher<Void, Never> { _featuresUpdated }
|
|
|
|
@Published @_spi(InstanceType) public private(set) var instanceType: InstanceType = .mastodon(.vanilla, nil)
|
|
@Published public private(set) var maxStatusChars = 500
|
|
@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: InstanceV1.MediaAttachmentsConfiguration?
|
|
@Published public private(set) var translation: Bool = false
|
|
|
|
public var localOnlyPosts: Bool {
|
|
switch instanceType {
|
|
case .mastodon(.hometown(_), _), .mastodon(.glitch, _):
|
|
return true
|
|
case .pleroma(.akkoma(_)):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Instance types that use a separate visibility to indicate local-only posts.
|
|
public var localOnlyPostsVisibility: Bool {
|
|
if case .pleroma(.akkoma(_)) = instanceType {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var mastodonAttachmentRestrictions: Bool {
|
|
instanceType.isMastodon
|
|
}
|
|
|
|
public var pollsAndAttachments: Bool {
|
|
instanceType.isPleroma
|
|
}
|
|
|
|
public var boostToOriginalAudience: Bool {
|
|
instanceType.isPleroma || instanceType.isMastodon
|
|
}
|
|
|
|
public var profilePinnedStatuses: Bool {
|
|
switch instanceType {
|
|
case .pixelfed:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
public var trends: Bool {
|
|
instanceType.isMastodon
|
|
}
|
|
|
|
public var profileSuggestions: Bool {
|
|
instanceType.isMastodon && hasMastodonVersion(3, 4, 0)
|
|
}
|
|
|
|
public var trendingStatusesAndLinks: Bool {
|
|
instanceType.isMastodon && hasMastodonVersion(3, 5, 0)
|
|
}
|
|
|
|
public var reblogVisibility: Bool {
|
|
(instanceType.isMastodon && hasMastodonVersion(2, 8, 0))
|
|
|| (instanceType.isPleroma && hasPleromaVersion(2, 0, 0))
|
|
}
|
|
|
|
public var probablySupportsMarkdown: Bool {
|
|
switch instanceType {
|
|
case .pleroma(_), .mastodon(.glitch, _), .mastodon(.hometown(_), _), .firefish(_):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var needsLocalOnlyEmojiHack: Bool {
|
|
if case .mastodon(.glitch, _) = instanceType {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var needsWideColorGamutHack: Bool {
|
|
if case .mastodon(_, let version) = instanceType {
|
|
return version < Version(4, 0, 0)
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
public var canFollowHashtags: Bool {
|
|
if case .mastodon(_, let version) = instanceType {
|
|
return version >= Version(4, 0, 0)
|
|
} else if case .pleroma(.akkoma(let version)) = instanceType {
|
|
return version >= Version(3, 4, 0)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var filtersV2: Bool {
|
|
hasMastodonVersion(4, 0, 0)
|
|
}
|
|
|
|
public var notificationsAllowedTypes: Bool {
|
|
hasMastodonVersion(3, 5, 0)
|
|
}
|
|
|
|
public var pollVotersCount: Bool {
|
|
instanceType.isMastodon
|
|
}
|
|
|
|
public var createStatusWithLanguage: Bool {
|
|
instanceType.isMastodon || instanceType.isPleroma(.akkoma(nil))
|
|
}
|
|
|
|
public var editStatuses: Bool {
|
|
switch instanceType {
|
|
case .mastodon(_, let v) where v >= Version(3, 5, 0):
|
|
return true
|
|
case .pleroma(.vanilla(let v)) where v >= Version(2, 5, 0):
|
|
return true
|
|
case .pleroma(.akkoma(_)):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var statusEditNotifications: Bool {
|
|
// pleroma doesn't seem to support 'update' type notifications, even though it supports edits
|
|
hasMastodonVersion(3, 5, 0)
|
|
}
|
|
|
|
public var statusNotifications: Bool {
|
|
// pleroma doesn't support notifications for new posts from an account
|
|
hasMastodonVersion(3, 3, 0)
|
|
}
|
|
|
|
public var needsEditAttachmentsInSeparateRequest: Bool {
|
|
instanceType.isPleroma(.akkoma(nil))
|
|
}
|
|
|
|
public var composeDirectStatuses: Bool {
|
|
if case .pixelfed = instanceType {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
public var searchOperators: Bool {
|
|
hasMastodonVersion(4, 2, 0)
|
|
}
|
|
|
|
public var hasServerPreferences: Bool {
|
|
hasMastodonVersion(2, 8, 0)
|
|
}
|
|
|
|
public var listRepliesPolicy: Bool {
|
|
hasMastodonVersion(3, 3, 0)
|
|
}
|
|
|
|
public var exclusiveLists: Bool {
|
|
hasMastodonVersion(4, 2, 0) || instanceType.isMastodon(.hometown(nil))
|
|
}
|
|
|
|
public var pushNotificationTypeStatus: Bool {
|
|
hasMastodonVersion(3, 3, 0)
|
|
}
|
|
|
|
public var pushNotificationTypeFollowRequest: Bool {
|
|
hasMastodonVersion(3, 1, 0)
|
|
}
|
|
|
|
public var pushNotificationTypeUpdate: Bool {
|
|
hasMastodonVersion(3, 5, 0)
|
|
}
|
|
|
|
public var pushNotificationPolicy: Bool {
|
|
hasMastodonVersion(3, 5, 0)
|
|
}
|
|
|
|
public var pushNotificationPolicyMissingFromResponse: Bool {
|
|
switch instanceType {
|
|
case .mastodon(_, let version):
|
|
return version >= Version(3, 5, 0) && version < Version(4, 1, 0)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public init() {
|
|
}
|
|
|
|
public func update(instance: InstanceInfo, nodeInfo: NodeInfo?) {
|
|
let ver = instance.version.lowercased()
|
|
// check glitch first b/c it still reports "mastodon" as the software in nodeinfo
|
|
if ver.contains("glitch") {
|
|
instanceType = .mastodon(.glitch, Version(string: ver))
|
|
} else if nodeInfo?.software.name == "mastodon" {
|
|
instanceType = .mastodon(.vanilla, Version(string: ver))
|
|
} else if nodeInfo?.software.name == "hometown" {
|
|
var mastoVersion: Version?
|
|
var hometownVersion: Version?
|
|
let parts = ver.split(separator: "+")
|
|
if parts.count == 2,
|
|
let first = Version(string: String(parts[0])) {
|
|
if first > Version(1, 0, 8) {
|
|
// like 3.5.5+hometown-1.0.9
|
|
mastoVersion = first
|
|
if parts[1].starts(with: "hometown-") {
|
|
hometownVersion = Version(string: String(parts[1][parts[1].index(parts[1].startIndex, offsetBy: "hometown-".count + 1)...]))
|
|
}
|
|
} else {
|
|
// like "1.0.6+3.5.2"
|
|
hometownVersion = first
|
|
mastoVersion = Version(string: String(parts[1]))
|
|
}
|
|
} else {
|
|
mastoVersion = Version(string: ver)
|
|
}
|
|
instanceType = .mastodon(.hometown(hometownVersion), mastoVersion)
|
|
} else if let match = InstanceFeatures.pleromaVersionRegex.firstMatch(in: ver, range: NSRange(location: 0, length: ver.utf16.count)) {
|
|
var pleromaVersion: Version?
|
|
let type = (ver as NSString).substring(with: match.range(at: 1))
|
|
pleromaVersion = Version(string: (ver as NSString).substring(with: match.range(at: 2)))
|
|
if type == "akkoma" {
|
|
instanceType = .pleroma(.akkoma(pleromaVersion))
|
|
} else {
|
|
instanceType = .pleroma(.vanilla(pleromaVersion))
|
|
}
|
|
} else if ver.contains("pixelfed") {
|
|
instanceType = .pixelfed
|
|
} else if nodeInfo?.software.name == "gotosocial" {
|
|
instanceType = .gotosocial
|
|
} else if ver.contains("firefish") || ver.contains("iceshrimp") || ver.contains("calckey") {
|
|
instanceType = .firefish(nodeInfo?.software.version)
|
|
} else {
|
|
instanceType = .mastodon(.vanilla, Version(string: ver))
|
|
}
|
|
|
|
maxStatusChars = instance.maxStatusCharacters ?? 500
|
|
charsReservedPerURL = instance.configuration?.statuses.charactersReservedPerURL ?? 23 // default Mastodon link length
|
|
if let pollsConfig = instance.pollsConfiguration {
|
|
maxPollOptionChars = pollsConfig.maxCharactersPerOption
|
|
maxPollOptionsCount = pollsConfig.maxOptions
|
|
}
|
|
mediaAttachmentsConfiguration = instance.configuration?.mediaAttachments
|
|
translation = instance.translation
|
|
|
|
_featuresUpdated.send()
|
|
}
|
|
|
|
public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
|
if case .mastodon(_, let version) = instanceType {
|
|
return version >= Version(major, minor, patch)
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func hasPleromaVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
|
|
switch instanceType {
|
|
case .pleroma(.vanilla(let version)), .pleroma(.akkoma(let version)):
|
|
return version >= Version(major, minor, patch)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension InstanceFeatures {
|
|
@_spi(InstanceType) public enum InstanceType {
|
|
case mastodon(MastodonType, Version?)
|
|
case pleroma(PleromaType)
|
|
case pixelfed
|
|
case gotosocial
|
|
case firefish(String?)
|
|
|
|
var isMastodon: Bool {
|
|
if case .mastodon(_, _) = self {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isMastodon(_ subtype: MastodonType) -> Bool {
|
|
if case .mastodon(let t, _) = self,
|
|
t.equalsIgnoreVersion(subtype) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var isPleroma: Bool {
|
|
if case .pleroma(_) = self {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isPleroma(_ subtype: PleromaType) -> Bool {
|
|
if case .pleroma(let t) = self,
|
|
t.equalsIgnoreVersion(subtype) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(InstanceType) public enum MastodonType {
|
|
case vanilla
|
|
case hometown(Version?)
|
|
case glitch
|
|
|
|
func equalsIgnoreVersion(_ other: MastodonType) -> Bool {
|
|
switch (self, other) {
|
|
case (.vanilla, .vanilla):
|
|
return true
|
|
case (.hometown(_), .hometown(_)):
|
|
return true
|
|
case (.glitch, .glitch):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(InstanceType) public enum PleromaType {
|
|
case vanilla(Version?)
|
|
case akkoma(Version?)
|
|
|
|
func equalsIgnoreVersion(_ other: PleromaType) -> Bool {
|
|
switch (self, other) {
|
|
case (.vanilla(_), .vanilla(_)):
|
|
return true
|
|
case (.akkoma(_), .akkoma(_)):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|