Tusker/Tusker/Filterer.swift

160 lines
4.4 KiB
Swift

//
// 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<AnyCancellable>()
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)
}
}