Tusker/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift

344 lines
11 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 class InstanceFeatures: ObservableObject {
private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; pleroma (.*)\\)", options: .caseInsensitive)
private static let akkomaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; 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?
public var localOnlyPosts: Bool {
switch instanceType {
case .mastodon(.hometown(_), _), .mastodon(.glitch, _):
return true
default:
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(_), _):
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(_, .some(let version)) = instanceType {
return version < Version(4, 0, 0)
} else {
return true
}
}
public var canFollowHashtags: Bool {
hasMastodonVersion(4, 0, 0)
}
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 {
// todo: does this require a particular akkoma version?
hasMastodonVersion(3, 5, 0) || instanceType.isPleroma(.akkoma(nil))
}
public var needsEditAttachmentsInSeparateRequest: Bool {
instanceType.isPleroma(.akkoma(nil))
}
public init() {
}
public func update(instance: Instance, 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 ver.contains("pleroma") {
var pleromaVersion: Version?
if let match = InstanceFeatures.pleromaVersionRegex.firstMatch(in: ver, range: NSRange(location: 0, length: ver.utf16.count)) {
pleromaVersion = Version(string: (ver as NSString).substring(with: match.range(at: 1)))
}
instanceType = .pleroma(.vanilla(pleromaVersion))
} else if ver.contains("akkoma") {
var akkomaVersion: Version?
if let match = InstanceFeatures.akkomaVersionRegex.firstMatch(in: ver, range: NSRange(location: 0, length: ver.utf16.count)) {
akkomaVersion = Version(string: (ver as NSString).substring(with: match.range(at: 1)))
}
instanceType = .pleroma(.akkoma(akkomaVersion))
} else if ver.contains("pixelfed") {
instanceType = .pixelfed
} else if nodeInfo?.software.name == "gotosocial" {
instanceType = .gotosocial
} else if ver.contains("calckey") {
instanceType = .calckey(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
}
_featuresUpdated.send()
}
public func hasMastodonVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool {
if case .mastodon(_, .some(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(.some(let version))), .pleroma(.akkoma(.some(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 calckey(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
}
}
}
}
extension InstanceFeatures {
@_spi(InstanceType) public struct Version: Equatable, Comparable, CustomStringConvertible {
private static let regex = try! NSRegularExpression(pattern: "^(\\d+)\\.(\\d+)\\.(\\d+).*$")
let major: Int
let minor: Int
let patch: Int
init(_ major: Int, _ minor: Int, _ patch: Int) {
self.major = major
self.minor = minor
self.patch = patch
}
init?(string: String) {
guard let match = Version.regex.firstMatch(in: string, range: NSRange(location: 0, length: string.utf16.count)),
match.numberOfRanges == 4 else {
return nil
}
let majorStr = (string as NSString).substring(with: match.range(at: 1))
let minorStr = (string as NSString).substring(with: match.range(at: 2))
let patchStr = (string as NSString).substring(with: match.range(at: 3))
guard let major = Int(majorStr),
let minor = Int(minorStr),
let patch = Int(patchStr) else {
return nil
}
self.major = major
self.minor = minor
self.patch = patch
}
public var description: String {
"\(major).\(minor).\(patch)"
}
public static func ==(lhs: Version, rhs: Version) -> Bool {
return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch
}
public static func < (lhs: InstanceFeatures.Version, rhs: InstanceFeatures.Version) -> Bool {
if lhs.major < rhs.major {
return true
} else if lhs.major > rhs.major {
return false
} else if lhs.minor < rhs.minor {
return true
} else if lhs.minor > rhs.minor {
return false
} else if lhs.patch < rhs.patch {
return true
} else {
return false
}
}
}
}