Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

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: ())
}
}
}
}
}