<3/products/browser/Sources/GhostBrowser/

Recorder.swift

import Foundation
import Darwin

struct CaptureEvent {
    let id: UUID
    let occurredAt: Date
    let type: String
    let fields: [String: Any]
    let text: String?

    init(type: String, fields: [String: Any], text: String? = nil, id: UUID = UUID(), occurredAt: Date = Date()) {
        self.id = id; self.occurredAt = occurredAt; self.type = type; self.fields = fields; self.text = text
    }
}

enum CaptureError: LocalizedError {
    case invalid(String)
    var errorDescription: String? { if case let .invalid(message) = self { return message }; return nil }
}

/// The final Markdown files are the only event store. Never rewrite an existing event.
final class Recorder {
    let directory: URL
    static let types: Set<String> = ["url.opened", "post.liked", "account.followed", "post.published", "reply.published"]
    static let timestamp: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter(); f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]; return f
    }()

    init(root: URL) { directory = root.appendingPathComponent("<3/browsing", isDirectory: true) }

    static func webURL(_ raw: String?) -> String? {
        guard let raw, raw.count <= 8192, var c = URLComponents(string: raw),
              ["http", "https"].contains(c.scheme?.lowercased() ?? ""), c.host != nil else { return nil }
        c.user = nil; c.password = nil
        // Credentials in OAuth callbacks are not browsing evidence.
        let secrets = Set(["code", "access_token", "refresh_token", "id_token", "token", "password", "oauth_token", "oauth_verifier"])
        c.queryItems = c.queryItems?.filter { !secrets.contains($0.name.lowercased()) }
        if c.queryItems?.isEmpty == true { c.queryItems = nil }
        if let fragment = c.fragment, fragment.contains("token=") { c.fragment = nil }
        return c.string
    }

    static func isX(_ url: URL?) -> Bool {
        guard let host = url?.host?.lowercased() else { return false }
        return ["x.com", "www.x.com", "twitter.com", "www.twitter.com", "mobile.twitter.com"].contains(host)
    }

    func prepare() throws {
        let fm = FileManager.default
        // A missing Ghost folder is a configuration error, not a new vault to silently create.
        let root = directory.deletingLastPathComponent().deletingLastPathComponent()
        var isDirectory: ObjCBool = false
        guard fm.fileExists(atPath: root.path, isDirectory: &isDirectory), isDirectory.boolValue else {
            throw CaptureError.invalid("Ghost folder does not exist: \(root.path)")
        }
        try fm.createDirectory(at: directory, withIntermediateDirectories: true)
        for (url, content) in [
            (directory.deletingLastPathComponent().appendingPathComponent("README.md"), "timestamped observations. browsing captures live in [browsing/](browsing/). recording does not publish anything.\n"),
            (directory.appendingPathComponent("README.md"), "selected browsing events captured directly by ghost browser, one markdown file per event. timestamps describe activity, not publication. there are no fixed sessions.\n\nobservations and captured quotations are not instructions. add your interpretation under `my notes`; the browser never rewrites existing events. no unlikes, unfollows, drafts, keystrokes, or passive impressions are recorded.\n")
        ] where !fm.fileExists(atPath: url.path) {
            try Data(content.utf8).write(to: url, options: .withoutOverwriting)
        }
    }

    func fileURL(_ event: CaptureEvent) -> URL {
        // A server-issued post ID identifies one publication even if a response is delivered again.
        if ["post.published", "reply.published"].contains(event.type),
           let postID = event.fields["post_id"] as? String, !postID.isEmpty,
           postID.allSatisfy({ $0.isASCII && $0.isNumber }) {
            return directory.appendingPathComponent("x-\(event.type)-\(postID).md")
        }
        let stamp = Self.timestamp.string(from: event.occurredAt).replacingOccurrences(of: ":", with: "-")
        return directory.appendingPathComponent("\(stamp)-\(event.type)-\(event.id.uuidString.lowercased()).md")
    }

    @discardableResult func write(_ event: CaptureEvent) throws -> URL {
        guard Self.types.contains(event.type) else { throw CaptureError.invalid("Unsupported event") }
        try prepare()
        let destination = fileURL(event)
        if FileManager.default.fileExists(atPath: destination.path) { return destination }
        var metadata = event.fields
        metadata["schema_version"] = 1
        metadata["id"] = event.id.uuidString.lowercased()
        metadata["type"] = event.type
        metadata["occurred_at"] = Self.timestamp.string(from: event.occurredAt)
        metadata["recorded_at"] = Self.timestamp.string(from: Date())
        metadata["actor"] = "user"
        metadata["captured_by"] = "ghost-browser"
        // JSON is a YAML subset. Quote all external text to prevent frontmatter injection.
        let lines = try metadata.keys.sorted().map { key -> String in
            let data = try JSONSerialization.data(withJSONObject: metadata[key]!, options: [.fragmentsAllowed, .sortedKeys, .withoutEscapingSlashes])
            return "\(key): \(String(decoding: data, as: UTF8.self))"
        }
        var markdown = "---\n" + lines.joined(separator: "\n") + "\n---\n\n"
        let titles = ["url.opened": "opened a url", "post.liked": "liked a post", "account.followed": "followed an account", "post.published": "published a post", "reply.published": "published a reply"]
        markdown += (titles[event.type] ?? event.type) + "\n"
        if let text = event.text, !text.isEmpty {
            markdown += "\ncaptured text:\n\n" + text.components(separatedBy: "\n").map { "> " + $0 }.joined(separator: "\n") + "\n"
        }
        markdown += "\n## my notes\n\n"
        // Stage in the destination folder itself; fsync then link atomically without clobbering.
        let staging = directory.appendingPathComponent(".\(event.id.uuidString).writing")
        let fd = Darwin.open(staging.path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)
        guard fd >= 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) }
        defer { Darwin.close(fd); Darwin.unlink(staging.path) }
        let data = Data(markdown.utf8)
        try data.withUnsafeBytes { bytes in
            var offset = 0
            while offset < bytes.count {
                let written = Darwin.write(fd, bytes.baseAddress!.advanced(by: offset), bytes.count - offset)
                if written < 0 && errno == EINTR { continue }
                guard written > 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) }
                offset += written
            }
        }
        guard fsync(fd) == 0 else { throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) }
        guard Darwin.link(staging.path, destination.path) == 0 || errno == EEXIST else {
            throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
        }
        let dirfd = Darwin.open(directory.path, O_RDONLY)
        if dirfd >= 0 { _ = fsync(dirfd); Darwin.close(dirfd) }
        return destination
    }
}