// // InstanceFeatures.swift // Tusker // // Created by Shadowfacts on 1/23/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import Foundation import Pachyderm import Sentry struct InstanceFeatures { private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; pleroma (.*)\\)", options: .caseInsensitive) private static let akkomaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; akkoma (.*)\\)", options: .caseInsensitive) private var instanceType: InstanceType = .mastodon(.vanilla, nil) private(set) var maxStatusChars = 500 var localOnlyPosts: Bool { switch instanceType { case .mastodon(.hometown(_), _), .mastodon(.glitch, _): return true default: return false } } var mastodonAttachmentRestrictions: Bool { instanceType.isMastodon } var pollsAndAttachments: Bool { instanceType.isPleroma } var boostToOriginalAudience: Bool { instanceType.isPleroma || instanceType.isMastodon } var profilePinnedStatuses: Bool { switch instanceType { case .pixelfed: return false default: return true } } var trends: Bool { instanceType.isMastodon } var profileSuggestions: Bool { instanceType.isMastodon && hasMastodonVersion(3, 4, 0) } var trendingStatusesAndLinks: Bool { instanceType.isMastodon && hasMastodonVersion(3, 5, 0) } var reblogVisibility: Bool { (instanceType.isMastodon && hasMastodonVersion(2, 8, 0)) || (instanceType.isPleroma && hasPleromaVersion(2, 0, 0)) } var probablySupportsMarkdown: Bool { switch instanceType { case .pleroma(_), .mastodon(.glitch, _), .mastodon(.hometown(_), _): return true default: return false } } var needsLocalOnlyEmojiHack: Bool { if case .mastodon(.glitch, _) = instanceType { return true } else { return false } } var needsWideColorGamutHack: Bool { if case .mastodon(_, .some(let version)) = instanceType { return version < Version(4, 0, 0) } else { return true } } var canFollowHashtags: Bool { hasMastodonVersion(4, 0, 0) } var filtersV2: Bool { hasMastodonVersion(4, 0, 0) } var notificationsAllowedTypes: Bool { hasMastodonVersion(3, 5, 0) } var pollVotersCount: Bool { instanceType.isMastodon } mutating 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 setInstanceBreadcrumb(instance: instance, nodeInfo: nodeInfo) } 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 } } } } private func setInstanceBreadcrumb(instance: Instance, nodeInfo: NodeInfo?) { let crumb = Breadcrumb(level: .info, category: "MastodonController") crumb.data = [ "instance": [ "version": instance.version ], ] if let nodeInfo { crumb.data!["nodeInfo"] = [ "software": nodeInfo.software.name, "version": nodeInfo.software.version, ] } SentrySDK.addBreadcrumb(crumb) }