426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
295 lines
10 KiB
Swift
295 lines
10 KiB
Swift
import Foundation
|
|
import SafariServices
|
|
|
|
struct WebsiteBlockerStoredState: Codable {
|
|
let websites: [String]
|
|
let endsAtEpochMs: Double?
|
|
let requestedWebsites: [String]?
|
|
let blockedWebsites: [String]?
|
|
let allowedWebsites: [String]?
|
|
let matchMode: String?
|
|
}
|
|
|
|
enum WebsiteBlockerShared {
|
|
static var appGroupIdentifier: String {
|
|
"group.\(Bundle.main.bundleIdentifier ?? "ai.elizaos.app")"
|
|
}
|
|
|
|
static var contentBlockerIdentifier: String {
|
|
"\(Bundle.main.bundleIdentifier ?? "ai.elizaos.app").WebsiteBlockerContentExtension"
|
|
}
|
|
|
|
static let stateKey = "website_blocker_state_v1"
|
|
static let iso8601Formatter = ISO8601DateFormatter()
|
|
static let exactMatchMode = "exact"
|
|
static let subdomainMatchMode = "subdomain"
|
|
static let xTwitterBlockedWebsites = [
|
|
"x.com",
|
|
"www.x.com",
|
|
"mobile.x.com",
|
|
"twitter.com",
|
|
"www.twitter.com",
|
|
"mobile.twitter.com",
|
|
"t.co",
|
|
"abs.twimg.com",
|
|
"pbs.twimg.com",
|
|
"video.twimg.com",
|
|
"ton.twimg.com",
|
|
"platform.twitter.com",
|
|
"tweetdeck.twitter.com",
|
|
]
|
|
static let xTwitterAllowedWebsites = [
|
|
"api.x.com",
|
|
"api.twitter.com",
|
|
]
|
|
static let googleNewsBlockedWebsites = ["news.google.com"]
|
|
static let googleNewsAllowedWebsites = [
|
|
"accounts.google.com",
|
|
"oauth2.googleapis.com",
|
|
"openidconnect.googleapis.com",
|
|
"www.googleapis.com",
|
|
]
|
|
|
|
static func normalizeHostname(_ value: String) -> String? {
|
|
let trimmed = value
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.trimmingCharacters(in: CharacterSet(charactersIn: "."))
|
|
.lowercased()
|
|
guard !trimmed.isEmpty else {
|
|
return nil
|
|
}
|
|
guard trimmed.contains(".") else {
|
|
return nil
|
|
}
|
|
guard trimmed.range(of: "^[a-z0-9.-]+$", options: .regularExpression) != nil else {
|
|
return nil
|
|
}
|
|
guard !trimmed.hasPrefix("."), !trimmed.hasSuffix(".") else {
|
|
return nil
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
static func parseWebsites(explicit: [Any], text: String?) -> [String] {
|
|
var websites: [String] = []
|
|
for value in explicit {
|
|
if let hostname = value as? String,
|
|
let normalized = normalizeHostname(hostname) {
|
|
websites.append(normalized)
|
|
}
|
|
}
|
|
|
|
if let text, !text.isEmpty {
|
|
let parts = text.components(separatedBy: CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ",")))
|
|
for part in parts {
|
|
if let normalized = normalizeHostname(part) {
|
|
websites.append(normalized)
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array(Set(websites)).sorted()
|
|
}
|
|
|
|
static func parseDurationMinutes(_ rawDuration: Any?) -> Int? {
|
|
switch rawDuration {
|
|
case let value as NSNumber:
|
|
let minutes = value.intValue
|
|
return minutes > 0 ? minutes : nil
|
|
case let value as String:
|
|
guard let minutes = Int(value), minutes > 0 else {
|
|
return nil
|
|
}
|
|
return minutes
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func buildPolicy(for requestedWebsites: [String]) -> WebsiteBlockerStoredState {
|
|
let normalizedRequested = Array(Set(requestedWebsites.compactMap(normalizeHostname))).sorted()
|
|
var blockedWebsites = Set<String>()
|
|
var allowedWebsites = Set<String>()
|
|
|
|
for website in normalizedRequested {
|
|
blockedWebsites.insert(website)
|
|
if shouldAddWwwVariant(website) {
|
|
blockedWebsites.insert("www.\(website)")
|
|
}
|
|
|
|
if ["x.com", "twitter.com"].contains(website) {
|
|
blockedWebsites.formUnion(xTwitterBlockedWebsites)
|
|
allowedWebsites.formUnion(xTwitterAllowedWebsites)
|
|
} else if website == "news.google.com" {
|
|
blockedWebsites.formUnion(googleNewsBlockedWebsites)
|
|
allowedWebsites.formUnion(googleNewsAllowedWebsites)
|
|
}
|
|
}
|
|
|
|
return WebsiteBlockerStoredState(
|
|
websites: normalizedRequested,
|
|
endsAtEpochMs: nil,
|
|
requestedWebsites: normalizedRequested,
|
|
blockedWebsites: Array(blockedWebsites).sorted(),
|
|
allowedWebsites: Array(allowedWebsites).sorted(),
|
|
matchMode: exactMatchMode
|
|
)
|
|
}
|
|
|
|
static func sharedDefaults() -> UserDefaults? {
|
|
UserDefaults(suiteName: appGroupIdentifier)
|
|
}
|
|
|
|
static func loadState() -> WebsiteBlockerStoredState? {
|
|
guard let defaults = sharedDefaults(),
|
|
let data = defaults.data(forKey: stateKey) else {
|
|
return nil
|
|
}
|
|
|
|
guard var decoded = try? JSONDecoder().decode(WebsiteBlockerStoredState.self, from: data) else {
|
|
defaults.removeObject(forKey: stateKey)
|
|
return nil
|
|
}
|
|
|
|
if decoded.requestedWebsites == nil || decoded.blockedWebsites == nil || decoded.allowedWebsites == nil || decoded.matchMode == nil {
|
|
decoded = WebsiteBlockerStoredState(
|
|
websites: decoded.websites,
|
|
endsAtEpochMs: decoded.endsAtEpochMs,
|
|
requestedWebsites: decoded.websites,
|
|
blockedWebsites: decoded.websites,
|
|
allowedWebsites: [],
|
|
matchMode: exactMatchMode
|
|
)
|
|
}
|
|
|
|
if let endsAtEpochMs = decoded.endsAtEpochMs,
|
|
endsAtEpochMs <= Date().timeIntervalSince1970 * 1000 {
|
|
defaults.removeObject(forKey: stateKey)
|
|
return nil
|
|
}
|
|
|
|
if decoded.websites.isEmpty {
|
|
defaults.removeObject(forKey: stateKey)
|
|
return nil
|
|
}
|
|
|
|
return decoded
|
|
}
|
|
|
|
static func saveState(websites: [String], durationMinutes: Int?) throws -> WebsiteBlockerStoredState {
|
|
let policy = buildPolicy(for: websites)
|
|
let endsAtEpochMs = durationMinutes.map { Date().timeIntervalSince1970 * 1000 + Double($0 * 60_000) }
|
|
let state = WebsiteBlockerStoredState(
|
|
websites: policy.websites,
|
|
endsAtEpochMs: endsAtEpochMs,
|
|
requestedWebsites: policy.requestedWebsites,
|
|
blockedWebsites: policy.blockedWebsites,
|
|
allowedWebsites: policy.allowedWebsites,
|
|
matchMode: policy.matchMode
|
|
)
|
|
|
|
guard let defaults = sharedDefaults() else {
|
|
throw NSError(
|
|
domain: "ElizaWebsiteBlocker",
|
|
code: 1,
|
|
userInfo: [NSLocalizedDescriptionKey: "The iPhone website blocker app group is not available in this build."]
|
|
)
|
|
}
|
|
|
|
let encoded = try JSONEncoder().encode(state)
|
|
defaults.set(encoded, forKey: stateKey)
|
|
return state
|
|
}
|
|
|
|
static func clearState() {
|
|
sharedDefaults()?.removeObject(forKey: stateKey)
|
|
}
|
|
|
|
static func shouldAddWwwVariant(_ hostname: String) -> Bool {
|
|
let labels = hostname.split(separator: ".")
|
|
return labels.count == 2 && labels.first != "www"
|
|
}
|
|
|
|
static func endsAtString(for state: WebsiteBlockerStoredState?) -> String? {
|
|
guard let endsAtEpochMs = state?.endsAtEpochMs else {
|
|
return nil
|
|
}
|
|
return iso8601Formatter.string(from: Date(timeIntervalSince1970: endsAtEpochMs / 1000))
|
|
}
|
|
|
|
static func buildContentBlockerRules(for websites: [String]) -> [[String: Any]] {
|
|
let policy = buildPolicy(for: websites)
|
|
let blockedRules = policy.blockedWebsites?.map { website -> [String: Any] in
|
|
[
|
|
"trigger": [
|
|
"url-filter": "^https?://([A-Za-z0-9-]+\\\\.)*\(NSRegularExpression.escapedPattern(for: website))([/:?#]|$)",
|
|
],
|
|
"action": [
|
|
"type": "block",
|
|
],
|
|
]
|
|
} ?? []
|
|
let allowRules = policy.allowedWebsites?.map { website -> [String: Any] in
|
|
[
|
|
"trigger": [
|
|
"url-filter": "^https?://([A-Za-z0-9-]+\\\\.)*\(NSRegularExpression.escapedPattern(for: website))([/:?#]|$)",
|
|
],
|
|
"action": [
|
|
"type": "ignore-previous-rules",
|
|
],
|
|
]
|
|
} ?? []
|
|
return blockedRules + allowRules
|
|
}
|
|
|
|
static func writeRulesFile() throws -> URL {
|
|
let websites = loadState()?.websites ?? []
|
|
let rules = buildContentBlockerRules(for: websites)
|
|
let data = try JSONSerialization.data(withJSONObject: rules, options: [])
|
|
let url = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("eliza-website-blocker-rules", isDirectory: true)
|
|
.appendingPathComponent("blockerList.json")
|
|
try FileManager.default.createDirectory(
|
|
at: url.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true,
|
|
attributes: nil
|
|
)
|
|
try data.write(to: url, options: .atomic)
|
|
return url
|
|
}
|
|
|
|
static func contentBlockerState() async throws -> SFContentBlockerState {
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
SFContentBlockerManager.getStateOfContentBlocker(withIdentifier: contentBlockerIdentifier) { state, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
return
|
|
}
|
|
guard let state else {
|
|
continuation.resume(
|
|
throwing: NSError(
|
|
domain: "ElizaWebsiteBlocker",
|
|
code: 2,
|
|
userInfo: [NSLocalizedDescriptionKey: "Safari did not return the Website Blocker extension state."]
|
|
)
|
|
)
|
|
return
|
|
}
|
|
continuation.resume(returning: state)
|
|
}
|
|
}
|
|
}
|
|
|
|
static func reloadContentBlocker() async throws {
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
SFContentBlockerManager.reloadContentBlocker(withIdentifier: contentBlockerIdentifier) { error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else {
|
|
continuation.resume(returning: ())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|