Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add browser activity filter #234

Merged
merged 5 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions WakaTime/Extensions/AXUIElementExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ extension AXUIElement {
// swiftlint:enable force_cast
}

func project(for app: MonitoredApp) -> String? {
func address(for app: MonitoredApp) -> String? {
var address: String?
switch app {
case .brave:
Expand All @@ -98,12 +98,12 @@ extension AXUIElement {
address = addressField?.value
default: return nil
}
return address
}

if let address {
return extractProjectName(from: address)
}

return nil
func project(for app: MonitoredApp) -> String? {
guard let address = address(for: app) else { return nil }
return extractProjectName(from: address)
}

// swiftlint:disable cyclomatic_complexity
Expand Down
9 changes: 9 additions & 0 deletions WakaTime/Extensions/NSRunningApplicationExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ enum MonitoredApp: String, CaseIterable {
MonitoredApp.slack.rawValue,
]

static let browserAppIds = [
MonitoredApp.arcbrowser.rawValue,
MonitoredApp.brave.rawValue,
MonitoredApp.chrome.rawValue,
MonitoredApp.firefox.rawValue,
MonitoredApp.safari.rawValue,
MonitoredApp.safaripreview.rawValue,
]

// list apps which are enabled by default on first run
static let defaultEnabledApps = [
MonitoredApp.canva.rawValue,
Expand Down
11 changes: 11 additions & 0 deletions WakaTime/Extensions/StringExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

extension String {
func matchesRegex(_ pattern: String) -> Bool {
if let regex = try? NSRegularExpression(pattern: pattern) {
let range = NSRange(location: 0, length: self.utf16.count)
return regex.firstMatch(in: self, options: [], range: range) != nil
}
return false
}
}
46 changes: 46 additions & 0 deletions WakaTime/Helpers/FilterManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Cocoa

class FilterManager {
static func filterBrowsedSites(app: NSRunningApplication, monitoredApp: MonitoredApp, activeWindow: AXUIElement) -> Bool {
guard MonitoringManager.isAppBrowser(app) else { return true }

if let address = activeWindow.address(for: monitoredApp) {
let patterns = Self.parseList(PropertiesManager.currentFilterList)
if patterns.isEmpty { return true }

// Create scheme-prefixed address versions to allow regular expressions
// that incorporate a scheme to match
let httpAddress = "http://" + address
let httpsAddress = "https://" + address

switch PropertiesManager.filterType {
case .denylist:
for pattern in patterns {
if httpAddress.matchesRegex(pattern) || httpsAddress.matchesRegex(pattern) {
starbugs marked this conversation as resolved.
Show resolved Hide resolved
// Address matches a pattern on the denylist. Filter the site out.
return false
}
}
case .allowlist:
let addressMatchesAllowlist = patterns.contains { pattern in
httpAddress.matchesRegex(pattern) || httpsAddress.matchesRegex(pattern)
}
// If none of the patterns on the allowlist match the given address, filter the site out
if !addressMatchesAllowlist {
return false
}
}
}

// The given address passed all filters and will be included
return true
}

private static func parseList(_ listString: String) -> [String] {
Self.sanitizeList(listString.components(separatedBy: "\n"))
}

private static func sanitizeList(_ urls: [String]) -> [String] {
urls.map { $0.trimmingCharacters(in: CharacterSet.whitespaces) }
}
}
19 changes: 19 additions & 0 deletions WakaTime/Helpers/MonitoringManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ class MonitoringManager {
return bundleId == MonitoredApp.xcode.rawValue
}

static func isAppBrowser(for bundleId: String) -> Bool {
MonitoredApp.browserAppIds.contains(bundleId)
}

static func isAppBrowser(_ app: NSRunningApplication) -> Bool {
guard let bundleId = app.bundleIdentifier else { return false }

return isAppBrowser(for: bundleId)
}

static func heartbeatData(_ app: NSRunningApplication) -> HeartbeatData? {
let pid = app.processIdentifier

Expand All @@ -60,6 +70,15 @@ class MonitoringManager {
let title = activeWindow.title(for: monitoredApp)
else { return nil }

// For browser apps, filter out deny/allowlisted sites and use the predefined project
// from the allowlist if applicable.
let included = FilterManager.filterBrowsedSites(
app: app,
monitoredApp: monitoredApp,
activeWindow: activeWindow
)
guard included else { return nil }

let project = activeWindow.project(for: monitoredApp)

switch monitoredApp {
Expand Down
63 changes: 63 additions & 0 deletions WakaTime/Helpers/PropertiesManager.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import Foundation

class PropertiesManager {
enum FilterType: String {
case denylist
case allowlist
}

enum Keys: String {
case shouldLaunchOnLogin = "launch_on_login"
case shouldLogToFile = "log_to_file"
case shouldAutomaticallyDownloadUpdates = "should_automatically_download_updates"
case hasLaunchedBefore = "has_launched_before"
case filterType = "filter_type"
case denylist = "denylist"
case allowlist = "allowlist"
}

static var shouldLaunchOnLogin: Bool {
Expand Down Expand Up @@ -71,4 +79,59 @@ class PropertiesManager {
UserDefaults.standard.synchronize()
}
}

static var filterType: FilterType {
get {
guard let filterTypeString = UserDefaults.standard.string(forKey: Keys.filterType.rawValue) else {
return .allowlist
}

return FilterType(rawValue: filterTypeString) ?? .denylist
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.filterType.rawValue)
UserDefaults.standard.synchronize()
}
}

static var denylist: String {
get {
guard let denylist = UserDefaults.standard.string(forKey: Keys.denylist.rawValue) else {
return ""
}

return denylist
}
set {
UserDefaults.standard.set(newValue, forKey: Keys.denylist.rawValue)
UserDefaults.standard.synchronize()
}
}

static var allowlist: String {
get {
guard let allowlist = UserDefaults.standard.string(forKey: Keys.allowlist.rawValue) else {
return
"https://github.com/\n" +
alanhamlett marked this conversation as resolved.
Show resolved Hide resolved
"https://gitlab.com/\n" +
"https://stackoverflow.com/\n" +
"https://docs.python.org/\n" +
"https://google.com/\n" +
"https://npmjs.com"
}

return allowlist
}
set {
UserDefaults.standard.set(newValue, forKey: Keys.allowlist.rawValue)
UserDefaults.standard.synchronize()
}
}

static var currentFilterList: String {
switch Self.filterType {
case .denylist: return Self.denylist
case .allowlist: return Self.allowlist
}
}
}
Loading
Loading