From f889c6129d32fa563f6dc4b80ef82484018f75ff Mon Sep 17 00:00:00 2001 From: Clavdij Date: Tue, 6 Oct 2026 21:26:42 +0200 Subject: [PATCH] Add Chrome extension and Windows native host --- README.md | 14 +++++ extension/config.js | 7 +++ extension/manifest.json | 23 +++++++ extension/service-worker.js | 62 +++++++++++++++++++ install-native-host.ps1 | 32 ++++++++++ native-host/history_archive_host.bat | 2 + native-host/history_archive_host.py | 54 ++++++++++++++++ ...komelt.local_history_archive.json.template | 7 +++ 8 files changed, 201 insertions(+) create mode 100644 extension/config.js create mode 100644 extension/manifest.json create mode 100644 extension/service-worker.js create mode 100644 install-native-host.ps1 create mode 100644 native-host/history_archive_host.bat create mode 100644 native-host/history_archive_host.py create mode 100644 native-host/si.komelt.local_history_archive.json.template diff --git a/README.md b/README.md index 61f92b6..bced324 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,17 @@ V `.env` nastavi `DISCORD_WEBHOOK_URL`. Najprej se zapis shrani v SQLite, nato s ## Nastavitev razširitve V `extension/config.js` nastavi `collectorUrl`, `apiToken` in `deviceId`. Token naj ostane v tej lokalni datoteki in naj ne konča v repozitoriju. `manifest.json` vsebuje široko dovoljenje za HTTP/HTTPS gostitelje zato, da lahko izbereš lasten API naslov; če želiš strožje dovoljenje, zamenjaj `host_permissions` samo s svojim HTTPS naslovom. + +## Windows native arhiv (neobvezno) + +Repozitorij vključuje tudi Chrome razširitev in Windows Native Messaging host za lokalni, append-only arhiv obiskov v SQLite. + +1. V Chromu odpri `chrome://extensions`, vklopi **Developer mode**, klikni **Load unpacked** in izberi mapo `extension`. +2. Kopiraj 32-znakovni ID razširitve. +3. V PowerShellu iz korena repozitorija zaženi: + + ```powershell + powershell -ExecutionPolicy Bypass -File .\install-native-host.ps1 -ExtensionId TUKAJ_PRILEPI_ID + ``` + +Lokalni arhiv se privzeto shrani v `%USERPROFILE%\ChromeHistoryArchive\history.sqlite3`. Za pošiljanje obiskov tudi na API uredi lokalni `extension/config.js`; vanj ne zapisuj tokena v commitu. diff --git a/extension/config.js b/extension/config.js new file mode 100644 index 0000000..0ffd654 --- /dev/null +++ b/extension/config.js @@ -0,0 +1,7 @@ +// Copy/edit locally after unpacking the extension. Do not commit a real token. +export const CONFIG = { + // Example: "https://history.example.com/v1/visits" + collectorUrl: "", + apiToken: "", + deviceId: "windows-chrome" +}; diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..e45033f --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 3, + "name": "Local History Archive", + "version": "1.0.0", + "description": "Writes Chrome visits to a local, append-only archive via Native Messaging.", + "permissions": [ + "history", + "nativeMessaging", + "storage", + "alarms" + ], + "background": { + "service_worker": "service-worker.js", + "type": "module" + }, + "action": { + "default_title": "Local History Archive" + }, + "host_permissions": [ + "https://*/*", + "http://*/*" + ] +} diff --git a/extension/service-worker.js b/extension/service-worker.js new file mode 100644 index 0000000..ce15084 --- /dev/null +++ b/extension/service-worker.js @@ -0,0 +1,62 @@ +import { CONFIG } from "./config.js"; + +const HOST = "si.komelt.local_history_archive"; +const QUEUE_KEY = "pendingVisits"; +const MAX_QUEUE = 2000; + +async function queued() { + const { [QUEUE_KEY]: visits = [] } = await chrome.storage.local.get(QUEUE_KEY); + return visits; +} +async function persist(visits) { + await chrome.storage.local.set({ [QUEUE_KEY]: visits.slice(-MAX_QUEUE) }); +} +async function archiveLocally(visits) { + if (!visits.length) return true; + try { + const reply = await chrome.runtime.sendNativeMessage(HOST, { type: "visits", visits }); + return reply?.ok === true; + } catch (error) { + console.warn("Local archive unavailable", error.message); + return false; + } +} +async function sendToCollector(visits) { + // Empty settings deliberately disable sending; the local SQLite archive still works. + if (!CONFIG.collectorUrl || !CONFIG.apiToken) return true; + try { + const response = await fetch(CONFIG.collectorUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${CONFIG.apiToken}` + }, + body: JSON.stringify({ device_id: CONFIG.deviceId, visits }) + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return true; + } catch (error) { + console.warn("Collector unavailable", error.message); + return false; + } +} +async function flush() { + const visits = await queued(); + if (!visits.length) return; + const [localOk, remoteOk] = await Promise.all([archiveLocally(visits), sendToCollector(visits)]); + if (localOk && remoteOk) await persist([]); +} +chrome.history.onVisited.addListener(async (item) => { + const visits = await queued(); + visits.push({ + url: item.url, title: item.title || "", + visit_time: new Date(item.lastVisitTime || Date.now()).toISOString(), + transition: "observed", source: "chrome-extension" + }); + await persist(visits); + await flush(); +}); +chrome.runtime.onStartup.addListener(flush); +chrome.runtime.onInstalled.addListener(flush); +chrome.alarms.create("local-history-archive-flush", { periodInMinutes: 1 }); +chrome.alarms.onAlarm.addListener(flush); diff --git a/install-native-host.ps1 b/install-native-host.ps1 new file mode 100644 index 0000000..7c94007 --- /dev/null +++ b/install-native-host.ps1 @@ -0,0 +1,32 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidatePattern('^[a-p]{32}$')] + [string]$ExtensionId, + [string]$ArchivePath = "$env:USERPROFILE\ChromeHistoryArchive" +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $MyInvocation.MyCommand.Path +$source = Join-Path $root 'native-host' +$target = Join-Path $env:LOCALAPPDATA 'LocalHistoryArchive' + +if (-not (Get-Command py -ErrorAction SilentlyContinue)) { + throw 'Python 3 ni nameščen. Namesti ga z https://www.python.org/downloads/windows/ in označi možnost Add python.exe to PATH.' +} + +New-Item -ItemType Directory -Force -Path $target, $ArchivePath | Out-Null +Copy-Item "$source\history_archive_host.py" $target -Force +Copy-Item "$source\history_archive_host.bat" $target -Force + +$manifest = (Get-Content "$source\si.komelt.local_history_archive.json.template" -Raw).Replace('C:\\LOCAL_HISTORY_ARCHIVE', ($target -replace '\\', '\\')).Replace('YOUR_EXTENSION_ID', $ExtensionId) +$manifestPath = Join-Path $target 'si.komelt.local_history_archive.json' +Set-Content -Path $manifestPath -Value $manifest -Encoding UTF8 + +[Environment]::SetEnvironmentVariable('LOCAL_HISTORY_ARCHIVE_DIR', $ArchivePath, 'User') +$regPath = 'HKCU:\Software\Google\Chrome\NativeMessagingHosts\si.komelt.local_history_archive' +New-Item -Path $regPath -Force | Out-Null +New-ItemProperty -Path $regPath -Name '(Default)' -Value $manifestPath -PropertyType String -Force | Out-Null + +Write-Host "Nameščeno. Arhiv bo v: $ArchivePath\history.sqlite3" +Write-Host 'Zapri in znova odpri Chrome, nato obišči poljubno stran za preizkus.' diff --git a/native-host/history_archive_host.bat b/native-host/history_archive_host.bat new file mode 100644 index 0000000..2baeb37 --- /dev/null +++ b/native-host/history_archive_host.bat @@ -0,0 +1,2 @@ +@echo off +py -3 "%~dp0history_archive_host.py" diff --git a/native-host/history_archive_host.py b/native-host/history_archive_host.py new file mode 100644 index 0000000..4c1f5d3 --- /dev/null +++ b/native-host/history_archive_host.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Chrome Native Messaging host: append browser visits to local SQLite.""" +import json, os, sqlite3, struct, sys +from datetime import datetime, timezone +from pathlib import Path + +ARCHIVE_DIR = Path(os.environ.get("LOCAL_HISTORY_ARCHIVE_DIR", Path.home() / "ChromeHistoryArchive")) +DB_PATH = ARCHIVE_DIR / "history.sqlite3" + +def read_message(): + raw = sys.stdin.buffer.read(4) + if not raw: return None + length = struct.unpack("=I", raw)[0] + if length > 20_000_000: raise ValueError("Message too large") + return json.loads(sys.stdin.buffer.read(length).decode("utf-8")) + +def send_message(value): + body = json.dumps(value, ensure_ascii=False).encode("utf-8") + sys.stdout.buffer.write(struct.pack("=I", len(body))) + sys.stdout.buffer.write(body) + sys.stdout.buffer.flush() + +def db(): + ARCHIVE_DIR.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(DB_PATH) + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("""CREATE TABLE IF NOT EXISTS visits ( + id INTEGER PRIMARY KEY, url TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', + visit_time TEXT NOT NULL, transition TEXT, source TEXT NOT NULL, + archived_at TEXT NOT NULL, UNIQUE(url, visit_time, source))""") + conn.execute("CREATE INDEX IF NOT EXISTS idx_visits_time ON visits(visit_time)") + return conn + +def handle(message): + if message.get("type") != "visits" or not isinstance(message.get("visits"), list): + return {"ok": False, "error": "Expected a visits message"} + now = datetime.now(timezone.utc).isoformat() + rows = [] + for visit in message["visits"]: + url, when = visit.get("url"), visit.get("visit_time") + if isinstance(url, str) and url and isinstance(when, str) and when: + rows.append((url, str(visit.get("title") or ""), when, + str(visit.get("transition") or "observed"), + str(visit.get("source") or "chrome-extension"), now)) + with db() as conn: + conn.executemany("INSERT OR IGNORE INTO visits (url,title,visit_time,transition,source,archived_at) VALUES (?,?,?,?,?,?)", rows) + return {"ok": True, "archived": len(rows), "database": str(DB_PATH)} + +try: + while message := read_message(): + try: send_message(handle(message)) + except Exception as exc: send_message({"ok": False, "error": str(exc)}) +except Exception: + sys.exit(1) diff --git a/native-host/si.komelt.local_history_archive.json.template b/native-host/si.komelt.local_history_archive.json.template new file mode 100644 index 0000000..75249c7 --- /dev/null +++ b/native-host/si.komelt.local_history_archive.json.template @@ -0,0 +1,7 @@ +{ + "name": "si.komelt.local_history_archive", + "description": "Local Chrome History Archive native host", + "path": "C:\\LOCAL_HISTORY_ARCHIVE\\history_archive_host.bat", + "type": "stdio", + "allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID/"] +}