This commit is contained in:
thePR0M3TH3AN committed 2026-03-13 06:12:00 -04:00
1 parent feb62bc428
commit 28429aa380
11 files changed
+421 -169

No files matched your search

+74 -42
View File
@@ -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 <name>` copies built‑in templates (`blank`, `voxvera`).
* **Build assets**: `voxvera build [--pdf <path>] [--download <file.zip>]` 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 <name>` copies built-in templates (`blank`, `voxvera`).
* **Build assets**: `voxvera build [--pdf <path>] [--download <file.zip>]` 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
+17 -4
View File
@@ -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/<subdomain>/.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'
```
+45 -5
View File
@@ -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/<subdomain>` 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/<subdomain>/.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.
+9 -5
View File
@@ -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."
+5
View File
@@ -22,6 +22,11 @@ requires-python = ">=3.9"
dependencies = [
"InquirerPy",
"rich",
"qrcode[pil]",
"Pillow",
"jsmin",
"htmlmin2",
"pypdf",
]
[project.scripts]
+5
View File
@@ -1,2 +1,7 @@
InquirerPy
rich
qrcode[pil]
Pillow
jsmin
htmlmin2
pypdf
+12 -41
View File
@@ -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 - <<EOF >/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."
+33 -4
View File
@@ -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):
+10 -18
View File
@@ -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 "------------------------------------------------------------------"
+151
View File
@@ -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 <script> tags and minify the overall HTML.
Replaces obfuscate_index.sh / obfuscate_nostr.sh
(javascript-obfuscator + html-minifier-terser + node).
"""
if not input_file.exists():
raise FileNotFoundError(f"Input file {input_file} does not exist.")
html = input_file.read_text(encoding="utf-8")
def _minify_script(match):
js = match.group(1)
return f"<script>{jsmin(js)}</script>"
html = re.sub(r"<script>(.*?)</script>", _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)
+60 -50
View File
@@ -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)