// // Filterer.swift // Tusker // // Created by Shadowfacts on 12/3/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import Foundation import Pachyderm import Combine class FilterState { static var unknown: FilterState { FilterState(state: .unknown) } private var state: State var isWarning: Bool { switch state { case .known(.warn(_)): return true default: return false } } private init(state: State) { self.state = state } // Use a closure for the status in case the result is cached and we don't need to look it up @MainActor func resolveFor(status: () -> StatusMO, resolver: Filterer) -> Filterer.Result { switch state { case .unknown: let result = resolver.resolve(status: status()) state = .known(result) return result case .known(let result): return result } } func setResult(_ result: Filterer.Result) { self.state = .known(result) } private enum State { case unknown case known(Filterer.Result) } } @MainActor class Filterer { let mastodonController: MastodonController let context: FilterV1.Context private let htmlConverter = HTMLConverter() private var needsSetupFilters = true private var matchers = [(NSRegularExpression, Result)]() private var filtersChanged: AnyCancellable? private var filterObservers = Set() init(mastodonController: MastodonController, context: FilterV1.Context) { self.mastodonController = mastodonController self.context = context filtersChanged = mastodonController.$filters .sink { [unowned self] _ in self.needsSetupFilters = true } } private func setupFilters(filters: [FilterMO]) { print("setting up filters") matchers = [] filterObservers = [] for filter in filters where filter.contexts.contains(context) { guard let matcher = filter.createMatcher() else { continue } matchers.append(matcher) filter.objectWillChange .sink { [unowned self] _ in self.needsSetupFilters = true } .store(in: &filterObservers) } needsSetupFilters = false } func resolve(status: StatusMO) -> Result { if needsSetupFilters { setupFilters(filters: mastodonController.filters) } if matchers.isEmpty { return .allow } lazy var text = htmlConverter.convert(status.content).string for (regex, result) in matchers { if (!status.spoilerText.isEmpty && regex.numberOfMatches(in: status.spoilerText, range: NSRange(location: 0, length: status.spoilerText.utf16.count)) > 0) || regex.numberOfMatches(in: text, range: NSRange(location: 0, length: text.utf16.count)) > 0 { return result } } return .allow } enum Result { case allow case hide case warn(String) } } private extension FilterMO { func createMatcher() -> (NSRegularExpression, Filterer.Result)? { guard keywords.count > 0 else { return nil } // TODO: it would be cool to use the Regex builder stuff for this, but it's iOS 16 only var pattern = "" var isFirst = true for keyword in keywordMOs { if isFirst { isFirst = false } else { pattern += "|" } pattern += "(" if keyword.wholeWord { pattern += "\\b" } pattern += NSRegularExpression.escapedPattern(for: keyword.keyword) if keyword.wholeWord { pattern += "\\b" } pattern += ")" } guard let regex = try? NSRegularExpression(pattern: pattern, options: [.useUnicodeWordBoundaries, .caseInsensitive]) else { return nil } let result: Filterer.Result switch filterAction { case .hide: result = .hide case .warn: result = .warn(titleOrKeyword) } return (regex, result) } }