From 28429aa3801ac9a0f3dd0853a3711dafbd38e6b0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 13 Mar 2026 06:12:00 -0400 Subject: [PATCH] update --- README.md | 116 +++++++++++++++++++----------- docs/troubleshooting.md | 21 ++++-- docs/usage.md | 50 +++++++++++-- install.sh | 14 ++-- pyproject.toml | 5 ++ requirements.txt | 5 ++ setup.sh | 53 ++++---------- tests/test_cli.py | 37 ++++++++-- voxvera-install.sh | 28 +++----- voxvera/build.py | 151 ++++++++++++++++++++++++++++++++++++++++ voxvera/cli.py | 110 ++++++++++++++++------------- 11 files changed, 421 insertions(+), 169 deletions(-) create mode 100644 voxvera/build.py diff --git a/README.md b/README.md index 40aa35a..60627f1 100644 --- a/README.md +++ b/README.md @@ -4,67 +4,76 @@ Generate printable flyers with QR codes linking to Tor (.onion) or HTTPS sites, --- -## šŸš€ Key Features +## Key Features -* **Interactive setup**: `voxvera init` prompts for metadata or extracts from a PDF form. When editing body text, a small Tkinter GUI window opens with existing content pre‑filled, falling back to the user's `$EDITOR` if the GUI isn't available. -* **Template support**: `voxvera init --template ` copies built‑in templates (`blank`, `voxvera`). -* **Build assets**: `voxvera build [--pdf ] [--download ]` generates HTML, obfuscated JS/CSS, QR codes, and bundles PDFs. +* **Interactive setup**: `voxvera init` prompts for metadata or extracts from a PDF form. When editing body text, a small Tkinter GUI window opens with existing content pre-filled, falling back to the user's `$EDITOR` if the GUI isn't available. +* **Template support**: `voxvera init --template ` copies built-in templates (`blank`, `voxvera`). +* **Build assets**: `voxvera build [--pdf ] [--download ]` generates HTML, minified JS/CSS, QR codes, and bundles PDFs. * **Batch import**: `voxvera import` processes all JSON configs in `imports/`. -* **Onion hosting**: `voxvera serve` publishes via Tor/OnionShare and updates flyer links. -* **All‑in‑one**: `voxvera quickstart` runs init, build, and serve in sequence. -* **Dependency check**: `voxvera check` verifies presence of required tools. -* **GUI**: Minimal Electron wrapper (`gui/electron`) for non‑CLI users. +* **Onion hosting**: `voxvera serve` publishes via Tor/OnionShare and updates flyer links. The onion URL is key-based and persists across content changes. +* **All-in-one**: `voxvera quickstart` runs init, build, and serve in sequence. +* **Dependency check**: `voxvera check` verifies presence of required tools and Python packages. +* **GUI**: Minimal Electron wrapper (`gui/electron`) for non-CLI users. --- -## šŸ“„ Quick Start (Debian & Ubuntu) +## Quick Start -### 1ļøāƒ£ Install VoxVera and all dependencies +### Install with pip/pipx (Linux, macOS, Windows) -```bash -# Download the installer -curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/voxvera-install.sh -o voxvera-install.sh -chmod +x voxvera-install.sh - -# Run the installer (requires sudo for apt packages) -./voxvera-install.sh -``` - -The script will: - -1. Update **apt** and install Tor, OnionShare, ImageMagick, Poppler, Node.js, and other CLI helpers. -2. Fetch the latest VoxVera release and place it in `~/.local/bin` (creating the directory if needed). -3. Create a minimal per‑user `torrc` under `~/.voxvera/` and (re)start the `tor` service. - -After the script finishes, open a new terminal or reload your shell to ensure `~/.local/bin` is on your **PATH**. - -If you prefer a user-level install, run: +VoxVera is a pure Python package. All build dependencies (QR codes, HTML minification, PDF parsing) are handled by Python libraries — no Node.js, ImageMagick, or other system tools needed. ```bash +# Recommended (isolated environment) pipx install 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' + +# Or with pip +pip install 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' ``` -### 2ļøāƒ£ Run VoxVera (every session) +The only external tool required for hosting is **OnionShare** (`onionshare-cli`). Install it separately if you plan to use `voxvera serve`: + +| Platform | Install OnionShare | +|---|---| +| Debian/Ubuntu | `sudo apt install onionshare-cli` | +| Fedora | `sudo dnf install onionshare-cli` | +| macOS | `brew install onionshare` | +| Windows | [onionshare.org](https://onionshare.org/) | + +### One-line installer (Linux/macOS) + +For a batteries-included setup that also installs Tor and OnionShare: ```bash -# First‑time only: make the wrapper executable -chmod +x voxvera-run.sh +curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash +``` -# Start VoxVera +### Windows (PowerShell) + +```powershell +irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex +``` + +### Run VoxVera + +```bash +# Interactive: configure, build, and serve in one step +voxvera quickstart + +# Or use the helper script (starts Tor if needed, then launches quickstart) ./voxvera-run.sh ``` -`voxvera-run.sh` checks that Tor is healthy, starts it if necessary, and then launches `voxvera quickstart`. Use it every time you work with VoxVera. - --- -## šŸ—ļø Typical Workflow +## Typical Workflow ```bash # 1. Interactive metadata voxvera init -# 2. Build flyers oxvera build --pdf form.pdf --download file.zip # optional flags +# 2. Build flyers +voxvera build --pdf form.pdf --download file.zip # optional flags # 3. Host via Tor voxvera serve @@ -80,9 +89,31 @@ Run `voxvera --help` for the full CLI reference. --- -## šŸŽ® GUI (optional) +## Dependencies -If you prefer a point‑and‑click experience: +### Python packages (installed automatically) + +| Package | Replaces | +|---|---| +| `qrcode` + `Pillow` | `qrencode` + ImageMagick | +| `jsmin` + `htmlmin2` | `javascript-obfuscator` + `html-minifier-terser` + Node.js | +| `pypdf` | `pdftotext` / `poppler-utils` | +| `InquirerPy` + `rich` | Interactive prompts and console output | + +### External tools (only for hosting) + +| Tool | Required for | +|---|---| +| `onionshare-cli` | `voxvera serve` (Tor hosting) | +| `tor` | OnionShare runtime (installed by OnionShare or separately) | + +Run `voxvera check` to verify your setup. + +--- + +## GUI (optional) + +If you prefer a point-and-click experience: ```bash cd gui/electron @@ -90,20 +121,21 @@ npm install npm start ``` -The GUI internally calls the same CLI commands, so make sure the installer has run first. +The GUI internally calls the same CLI commands, so make sure VoxVera is installed first. --- -## šŸ“„ Documentation +## Documentation See the `docs/` folder for additional guides: * `docs/usage.md` — CLI workflows * `docs/templates.md` — available flyer templates * `docs/troubleshooting.md` — common fixes +* `docs/docker.md` — Docker usage --- -## šŸ“œ License +## License -MIT © 2025Ā thePR0M3TH3AN +MIT Ā© 2025 thePR0M3TH3AN diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index f27e9f6..8f8332e 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -32,11 +32,24 @@ output. If problems persist, consult the OnionShare and Tor documentation for more advanced configuration tips. +## Onion URL changed unexpectedly +The onion URL is tied to the Ed25519 keypair stored in +`host//.onionshare-session`. If this file is deleted, a new +keypair (and URL) will be generated on the next `voxvera serve`. The +`voxvera import` command preserves this file automatically, but manual +deletion of the `host/` directory will destroy it. + ## Electron GUI If `npm start` fails with `spawn voxvera ENOENT`, the `voxvera` command is not in your `PATH`. Install it with `pipx install git+https://github.com/PR0M3TH3AN/VoxVera.git` or run `./install.sh` from the repository. ## Missing dependencies -Run `voxvera check` to see which required tools are present. The command verifies -`node`, `javascript-obfuscator`, `html-minifier-terser`, `jq`, `qrencode`, -`onionshare-cli`, and other helpers, then prints a summary of any that are -missing so you can install them. +Run `voxvera check` to verify your setup. The command checks for required +Python packages (`qrcode`, `Pillow`, `jsmin`, `htmlmin`, `pypdf`) and +external tools (`onionshare-cli`), then prints a summary of anything +missing. To install all Python dependencies: + +```bash +pip install 'voxvera[all]' +# or reinstall from source +pipx install --force 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' +``` diff --git a/docs/usage.md b/docs/usage.md index 7732651..c00c046 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -2,27 +2,67 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instructions and `docs/templates.md` for available flyer templates. +## Prerequisites + +Install VoxVera with pip or pipx: + +```bash +pipx install 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' +``` + +All build dependencies (QR generation, HTML minification, PDF parsing) are Python packages installed automatically. The only external tool needed is `onionshare-cli` for Tor hosting. + ## Step-by-Step + 1. Edit `src/index-master.html` or `src/nostr-master.html` if you need custom content. -2. Run `voxvera init` and follow the prompts, or use `voxvera init --from-pdf path/to/form.pdf`. -3. Build the flyer assets. Add an optional zip file at `src/download/download.zip` or pass `--download` to include it in the build: +2. Run `voxvera init` and follow the prompts, or use `voxvera init --from-pdf path/to/form.pdf` to extract fields from a filled PDF form. +3. Build the flyer assets. Add an optional zip file with `--download`: ```bash voxvera build --download path/to/file.zip ``` 4. Host the generated `host/` directory. The `index.html` file fetches `config.json`, so the flyer must be served via a local or remote web server rather than opened directly from disk. For a quick test you can run `python3 -m http.server` inside the folder and then visit the provided address. Visitors can use the **Download** button to retrieve the file. ## Batch Import + Place configuration files in an `imports/` directory at the project root and run: + ```bash voxvera import ``` -Each JSON file is copied to `src/config.json` and processed with `voxvera build`. Existing folders under `host/` with the same subdomain are removed before new files are written. + +Each JSON file is copied to `src/config.json` and processed with `voxvera build`. Existing folders under `host/` with the same subdomain are cleaned before new files are written. OnionShare session keys (`.onionshare-session`) are preserved so the onion URL stays the same across re-imports. ## Hosting with OnionShare + Use the CLI to publish the flyer over Tor: + ```bash +export TOR_SOCKS_PORT=9050 +export TOR_CONTROL_PORT=9051 voxvera serve ``` -The script launches `onionshare-cli` in persistent website mode, waits for the generated onion URL, patches `config.json`, regenerates the QR codes and obfuscated HTML, and then copies the updated files back into the `host` directory. Keep OnionShare running to continue hosting. -`index.html` fetches `config.json` dynamically, so the flyer should be viewed through a local or remote web server. For quick testing, run `python3 -m http.server` in the folder and open the provided address instead of loading the file directly. +The command launches `onionshare-cli` in persistent website mode, waits for the generated onion URL, patches `config.json`, regenerates the QR codes and minified HTML, and copies the updated files back into the `host` directory. + +The onion URL is derived from an Ed25519 keypair stored in `host//.onionshare-session`. As long as this file exists, the URL stays the same even if you rebuild with new content. Keep OnionShare running to continue hosting. + +## All-in-One + +```bash +voxvera quickstart # interactive +voxvera quickstart --non-interactive # use existing config.json +``` + +Or use the helper script which ensures Tor is running first: + +```bash +./voxvera-run.sh +``` + +## Checking Dependencies + +```bash +voxvera check +``` + +This verifies that required Python packages and external tools (`onionshare-cli`) are available and prints a summary. diff --git a/install.sh b/install.sh index c4e130d..176444d 100755 --- a/install.sh +++ b/install.sh @@ -55,11 +55,13 @@ require_pkg() { fi } +# Only external runtime dependencies needed (for voxvera serve) require_pkg tor tor -require_pkg onionshare-cli onionshare-cli -require_pkg jq jq -require_pkg qrencode qrencode -require_pkg convert imagemagick +if [ "$PM" = "brew" ]; then + require_pkg onionshare onionshare +else + require_pkg onionshare-cli onionshare-cli +fi download_binary() { local url=$1 @@ -122,7 +124,7 @@ pip_repo_fallback() { } if command_exists pipx; then - if ! pipx install --force voxvera; then + if ! pipx install --force 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'; then echo "pipx install failed, downloading binary" install_dir="$HOME/.local/bin" mkdir -p "$install_dir" @@ -160,4 +162,6 @@ else fi fi +echo "" echo "VoxVera installed successfully." +echo "Run 'voxvera check' to verify your setup." diff --git a/pyproject.toml b/pyproject.toml index 4a0d888..b6fb776 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,11 @@ requires-python = ">=3.9" dependencies = [ "InquirerPy", "rich", + "qrcode[pil]", + "Pillow", + "jsmin", + "htmlmin2", + "pypdf", ] [project.scripts] diff --git a/requirements.txt b/requirements.txt index 014b52b..fb9eaba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,7 @@ InquirerPy rich +qrcode[pil] +Pillow +jsmin +htmlmin2 +pypdf diff --git a/setup.sh b/setup.sh index 17bd121..512e9a0 100755 --- a/setup.sh +++ b/setup.sh @@ -1,53 +1,24 @@ #!/bin/bash set -e +# Development setup: install Python dependencies for VoxVera. +# System tools like jq, qrencode, ImageMagick, Node.js, and poppler-utils +# are no longer required — the build pipeline uses pure Python packages. + check_cmd() { command -v "$1" >/dev/null 2>&1 } -apt_packages=() - -if ! check_cmd jq; then - apt_packages+=(jq) +# Ensure pip is available +if ! check_cmd pip3 && ! check_cmd pip; then + echo "pip not found. Install Python 3 and pip first." + exit 1 fi -if ! check_cmd qrencode; then - apt_packages+=(qrencode) -fi +PIP=$(check_cmd pip3 && echo pip3 || echo pip) -if ! check_cmd convert; then - apt_packages+=(imagemagick) -fi - -if ! check_cmd pdftotext; then - apt_packages+=(poppler-utils) -fi - -if ! check_cmd node || ! check_cmd npm; then - apt_packages+=(nodejs npm) -fi - -if [ ${#apt_packages[@]} -gt 0 ]; then - echo "Installing system packages: ${apt_packages[*]}" - sudo apt-get update - sudo apt-get install -y "${apt_packages[@]}" -fi - -for pkg in javascript-obfuscator html-minifier-terser; do - if ! check_cmd "$pkg"; then - npm install -g "$pkg" - fi -done - -# ensure Python packages used by the CLI are available -for py in InquirerPy rich; do - if ! python3 - </dev/null 2>&1 -import importlib.util, sys -sys.exit(0 if importlib.util.find_spec('$py') else 1) -EOF - then - pip install --user "$py" - fi -done +# Install Python packages used by the CLI and build pipeline +$PIP install --user InquirerPy rich qrcode Pillow jsmin htmlmin2 pypdf echo "All dependencies are installed." +echo "Run 'voxvera check' to verify your setup." diff --git a/tests/test_cli.py b/tests/test_cli.py index 5b44153..24d2db9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -19,7 +19,6 @@ def _setup_tmp(monkeypatch, tmp_path): shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src") monkeypatch.setattr(cli, "ROOT", tmp_path) monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p)) - monkeypatch.setattr(cli, "run", lambda *a, **k: None) return repo_root @@ -77,21 +76,51 @@ def test_import(tmp_path, monkeypatch): assert (dest / "index.html").exists() +def test_import_preserves_session(tmp_path, monkeypatch): + """Re-importing a config must not destroy the OnionShare session key.""" + repo = _setup_tmp(monkeypatch, tmp_path) + imports_dir = tmp_path / "imports" + imports_dir.mkdir() + base_data = json.load(open(repo / "imports" / "example.json")) + base_data["subdomain"] = "keep" + with open(imports_dir / "keep.json", "w") as fh: + json.dump(base_data, fh) + + # first import creates the host dir + cli.main(["import"]) + host_dir = tmp_path / "host" / "keep" + assert host_dir.is_dir() + + # simulate OnionShare having written a session key + session_file = host_dir / ".onionshare-session" + session_file.write_text("FAKE-KEY-DATA") + + # re-import the same config + cli.main(["import"]) + + # session key must still be present and unchanged + assert session_file.exists() + assert session_file.read_text() == "FAKE-KEY-DATA" + # built assets should still be there too + assert (host_dir / "index.html").exists() + + def test_check_all_present(capsys, monkeypatch): monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/bin/" + cmd) cli.main(["check"]) captured = capsys.readouterr() - assert "All required tools are installed." in captured.out + assert "All dependencies are installed." in captured.out def test_check_missing(capsys, monkeypatch): def fake_which(cmd): - return None if cmd == "node" else "/usr/bin/" + cmd + return None if cmd == "onionshare-cli" else "/usr/bin/" + cmd monkeypatch.setattr(shutil, "which", fake_which) cli.main(["check"]) captured = capsys.readouterr() - assert "node: missing" in captured.out + assert "onionshare-cli" in captured.out + assert "missing" in captured.out def test_build_download_zip(tmp_path, monkeypatch): diff --git a/voxvera-install.sh b/voxvera-install.sh index e445613..16e9289 100644 --- a/voxvera-install.sh +++ b/voxvera-install.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # -------------------------------------------------------------------- # VoxVera all-in-one installer (Linux) – run once as a normal user. -# Installs system deps, Tor + OnionShare, npm helpers, and VoxVera itself. +# Installs Tor + OnionShare and VoxVera itself. +# Build dependencies (QR, HTML minification, PDF parsing) are Python +# packages installed automatically — no Node.js or ImageMagick needed. # -------------------------------------------------------------------- set -euo pipefail @@ -18,35 +20,24 @@ elif command_exists dnf; then PM=dnf ; UPDATE="sudo dnf -y makecache" elif command_exists yum; then PM=yum ; UPDATE="sudo yum makecache" ; INSTALL="sudo yum install -y" elif command_exists pacman; then PM=pacman; UPDATE="sudo pacman -Sy" ; INSTALL="sudo pacman -S --noconfirm" elif command_exists apk; then PM=apk ; UPDATE="sudo apk update" ; INSTALL="sudo apk add --no-cache" -else die "Unsupported distro – can’t find apt/yum/dnf/pacman/apk." +else die "Unsupported distro – can't find apt/yum/dnf/pacman/apk." fi msg ">> Detected package manager: $PM" ## -------- install system packages ---------------------------------- -SYSTEM_PKGS=(torsocks tor jq qrencode imagemagick poppler-utils curl wget) +# Only runtime dependencies for Tor hosting are needed as system packages. +# Build tools (QR, minification, PDF parsing) are all Python packages now. +SYSTEM_PKGS=(tor curl) # OnionShare package name differs per repo: [[ $PM =~ (apt|dnf|yum) ]] && SYSTEM_PKGS+=(onionshare-cli) || SYSTEM_PKGS+=(onionshare) $UPDATE && $INSTALL "${SYSTEM_PKGS[@]}" -# node / npm -if ! command_exists node || ! command_exists npm; then - msg ">> Installing Node.js / npm" - $INSTALL nodejs npm -fi - -# npm global helpers (obfuscator + minifier) -for pkg in javascript-obfuscator html-minifier-terser; do - if ! command_exists "$pkg"; then - sudo npm install -g "$pkg" - fi -done - ## -------- install VoxVera (prefers pipx, falls back) --------------- install_voxvera() { if command_exists pipx; then - pipx install --force voxvera && return 0 + pipx install --force 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' && return 0 fi # fallback: binary local install_dir="$HOME/.local/bin" @@ -62,7 +53,7 @@ install_voxvera() { return 0 fi # fallback: pip - pip install --user voxvera + pip install --user 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' } msg ">> Installing VoxVera" @@ -87,4 +78,5 @@ msg "------------------------------------------------------------------" msg " VoxVera install finished!" msg " Tor rc file : $TOR_DIR/torrc" msg " Make sure \$HOME/.local/bin is in your PATH." +msg " Run 'voxvera check' to verify your setup." msg "------------------------------------------------------------------" diff --git a/voxvera/build.py b/voxvera/build.py new file mode 100644 index 0000000..07929e4 --- /dev/null +++ b/voxvera/build.py @@ -0,0 +1,151 @@ +"""Pure-Python replacements for the shell-based build pipeline. + +Eliminates the need for jq, qrencode, ImageMagick, javascript-obfuscator, +html-minifier-terser, node, pdftotext, and poppler-utils. +""" + +import json +import re +from pathlib import Path + +import htmlmin # provided by htmlmin2 package +import qrcode +from jsmin import jsmin +from PIL import Image + + +def generate_qr(config_path: Path, output_dir: Path) -> None: + """Generate QR code PNGs from config URLs. + + Replaces generate_qr.sh (qrencode + ImageMagick convert). + """ + with open(config_path) as f: + config = json.load(f) + + url = config.get("url", "") + tear = config.get("tear_off_link", "") + if not url: + raise ValueError(f"url missing in {config_path}") + if not tear: + raise ValueError(f"tear_off_link missing in {config_path}") + + for data, filename in [(url, "qrcode-content.png"), (tear, "qrcode-tear-offs.png")]: + qr = qrcode.QRCode(box_size=10, border=0) + qr.add_data(data) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + img = img.resize((128, 128), Image.LANCZOS) + img.save(output_dir / filename) + + +def obfuscate_html(input_file: Path, output_file: Path) -> None: + """Minify JS inside " + + html = re.sub(r"", _minify_script, html, flags=re.DOTALL) + html = htmlmin.minify( + html, + remove_comments=True, + remove_empty_space=True, + remove_optional_attribute_quotes=False, + ) + output_file.write_text(html, encoding="utf-8") + + +def _normalize_content(raw: str) -> str: + """Join soft-wrapped lines while preserving paragraph breaks. + + Replicates the awk logic from extract_form_fields.sh. + """ + lines = raw.splitlines() + paragraphs: list[str] = [] + current = "" + + for line in lines: + stripped = line.strip() + if not stripped: + if current: + paragraphs.append(current) + current = "" + continue + + if not current: + current = stripped + elif re.search(r"[.?!]$", current) and stripped[0:1].isupper(): + current += " " + stripped + elif not re.search(r"[.?!]$", current): + current += " " + stripped + else: + paragraphs.append(current) + current = stripped + + if current: + paragraphs.append(current) + + return "\n\n".join(paragraphs) + + +def extract_form_fields(pdf_path: Path, config_path: Path) -> None: + """Extract form data from a filled PDF and update config.json. + + Replaces extract_form_fields.sh (pdftotext + jq + grep/awk/sed). + """ + from pypdf import PdfReader + + reader = PdfReader(str(pdf_path)) + text = "\n".join(page.extract_text(extraction_mode="layout") or "" for page in reader.pages) + + def _extract_field(label: str, lines_after: int = 1) -> str: + pattern = re.compile(re.escape(label), re.IGNORECASE) + text_lines = text.splitlines() + for i, line in enumerate(text_lines): + if pattern.search(line): + target = text_lines[i + lines_after] if i + lines_after < len(text_lines) else "" + return target.strip() + return "" + + fields = { + "name": _extract_field("name (max 25 characters):", lines_after=3), + "subdomain": _extract_field("subdomain (max 30 characters):"), + "title": _extract_field("title (max 30 characters):"), + "subtitle": _extract_field("subtitle (max 45 characters):"), + "headline": _extract_field("headline (max 50 characters):"), + "url_message": _extract_field("url_message (max 60 characters):"), + "url": _extract_field("url (max 90 characters):"), + } + + # Content: multi-line block between "content" label and "url_message" label + content_match = re.search( + r"content \(max 1,400 characters\):\s*\n(.*?)url_message", + text, + re.DOTALL | re.IGNORECASE, + ) + if content_match: + fields["content"] = _normalize_content(content_match.group(1)) + + missing = [k for k, v in fields.items() if not v] + if missing: + raise ValueError(f"Empty fields extracted from PDF: {', '.join(missing)}") + + onion_base = "6dshf2gnj7yzxlfcaczlyi57up4mvbtd5orinuj5bjsfycnhz2w456yd.onion" + + with open(config_path) as f: + config = json.load(f) + + existing_tear = config.get("tear_off_link", "") + fields["tear_off_link"] = existing_tear or f"http://{fields['subdomain']}.{onion_base}" + + config.update(fields) + with open(config_path, "w") as f: + json.dump(config, f, indent=2) diff --git a/voxvera/cli.py b/voxvera/cli.py index fbc002d..a5496e9 100644 --- a/voxvera/cli.py +++ b/voxvera/cli.py @@ -34,44 +34,48 @@ def require_cmd(cmd: str): def check_deps(): console = Console() - tools = [ - "node", - "javascript-obfuscator", - "html-minifier-terser", - "jq", - "qrencode", - "onionshare-cli", - "convert", - "pdftotext", - ] - found = [] - missing = [] - for t in tools: - if shutil.which(t): - found.append(t) - else: - missing.append(t) + # External CLI tools still required at runtime + cli_tools = ["onionshare-cli"] + # Python packages used by the build pipeline + py_packages = { + "qrcode": "qrcode", + "PIL (Pillow)": "PIL", + "jsmin": "jsmin", + "htmlmin": "htmlmin", + "pypdf": "pypdf", + } console.rule("Dependency Check") - for t in tools: - status = "found" if t in found else "missing" - color = "green" if t in found else "red" - console.print(f"{t}: [{color}]{status}[/{color}]") - if missing: - console.print(f"[red]Missing tools:[/red] {', '.join(missing)}") + for t in cli_tools: + if shutil.which(t): + console.print(f"{t}: [green]found[/green]") + else: + console.print(f"{t}: [red]missing[/red]") + + for label, module in py_packages.items(): + try: + __import__(module) + console.print(f"{label}: [green]found[/green]") + except ImportError: + console.print(f"{label}: [red]missing[/red]") + + all_cli = all(shutil.which(t) for t in cli_tools) + all_py = True + for module in py_packages.values(): + try: + __import__(module) + except ImportError: + all_py = False + break + + if all_cli and all_py: + console.print("[green]All dependencies are installed.[/green]") else: - console.print("[green]All required tools are installed.[/green]") + console.print("[red]Some dependencies are missing. Run: pip install 'voxvera[all]'[/red]") -def run(cmd, **kwargs): - try: - subprocess.run(cmd, check=True, **kwargs) - except FileNotFoundError: - print(f"Required command '{cmd[0]}' not found. Please install it.", file=sys.stderr) - sys.exit(1) - def load_config(path: str) -> dict: with open(path, 'r') as fh: @@ -273,16 +277,8 @@ def interactive_update(config_path: str): def update_from_pdf(config_path: str, pdf_path: str): - import tempfile - tmpdir = tempfile.mkdtemp() - os.makedirs(os.path.join(tmpdir, 'from_client'), exist_ok=True) - shutil.copy(pdf_path, os.path.join(tmpdir, 'from_client', 'submission_form.pdf')) - with resources.as_file(_template_res('blank', 'extract_form_fields.sh')) as p: - shutil.copy(p, tmpdir) - shutil.copy(config_path, os.path.join(tmpdir, 'config.json')) - run(['bash', 'extract_form_fields.sh'], cwd=tmpdir) - shutil.copy(os.path.join(tmpdir, 'config.json'), config_path) - shutil.rmtree(tmpdir) + from voxvera.build import extract_form_fields + extract_form_fields(Path(pdf_path), Path(config_path)) def copy_template(name: str) -> str: @@ -301,12 +297,15 @@ def copy_template(name: str) -> str: def build_assets(config_path: str, pdf_path: str | None = None, download_path: str | None = None): + from voxvera.build import generate_qr, obfuscate_html + with resources.as_file(_src_res()) as src_dir: - # generate QR codes - run(['bash', 'generate_qr.sh', config_path], cwd=src_dir) - # obfuscate html - run(['bash', 'obfuscate_index.sh', config_path], cwd=src_dir) - run(['bash', 'obfuscate_nostr.sh', config_path], cwd=src_dir) + config_path = Path(config_path) + # generate QR codes (pure Python) + generate_qr(config_path, src_dir) + # minify HTML + inline JS (pure Python) + obfuscate_html(src_dir / 'index-master.html', src_dir / 'index.html') + obfuscate_html(src_dir / 'nostr-master.html', src_dir / 'nostr.html') data = load_config(config_path) with open(src_dir / 'index.html', 'r') as fh: html = fh.read() @@ -351,8 +350,9 @@ def serve(config_path: str): '--use-running-tor', str(dir_path) ] + log_fh = open(logfile, 'w') proc = subprocess.Popen(cmd, - stdout=open(logfile, 'w'), + stdout=log_fh, stderr=subprocess.STDOUT) try: import time @@ -382,6 +382,18 @@ def serve(config_path: str): print(f"OnionShare running (PID {proc.pid}). See {logfile} for details.") except KeyboardInterrupt: pass + finally: + log_fh.close() + + +def _clear_host_dir(dir_path: Path): + """Remove host directory contents but preserve the OnionShare session key.""" + session = dir_path / '.onionshare-session' + saved = session.read_bytes() if session.exists() else None + shutil.rmtree(dir_path, ignore_errors=True) + if saved is not None: + os.makedirs(dir_path, exist_ok=True) + session.write_bytes(saved) def import_configs(): @@ -395,7 +407,7 @@ def import_configs(): dest_config = ROOT / 'src' / 'config.json' shutil.copy(json_file, dest_config) subdomain = load_config(json_file)['subdomain'] - shutil.rmtree(ROOT / 'host' / subdomain, ignore_errors=True) + _clear_host_dir(ROOT / 'host' / subdomain) build_assets(dest_config) @@ -429,8 +441,6 @@ def main(argv=None): copy_template(args.template) return elif args.from_pdf: - if not require_cmd('pdftotext'): - sys.exit(1) update_from_pdf(config_path, args.from_pdf) elif not args.non_interactive: interactive_update(config_path)