// // 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() public var featuresUpdated: some Publisher { _featuresUpdated } @Published private 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 init() { } public func update(instance: Instance, nodeInfo: NodeInfo?) { let ver = instance.version.lowercased() if ver.contains("glitch") { instanceType = .mastodon(.glitch, 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 { 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 { enum InstanceType { case mastodon(MastodonType, Version?) case pleroma(PleromaType) case pixelfed var isMastodon: Bool { if case .mastodon(_, _) = self { return true } else { return false } } var isPleroma: Bool { if case .pleroma(_) = self { return true } else { return false } } } enum MastodonType { case vanilla case hometown(Version?) case glitch } enum PleromaType { case vanilla(Version?) case akkoma(Version?) } } extension InstanceFeatures { struct Version: Equatable, Comparable { 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 } static func ==(lhs: Version, rhs: Version) -> Bool { return lhs.major == rhs.major && lhs.minor == rhs.minor && lhs.patch == rhs.patch } 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 } } } }