mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 09:40:40 +00:00
update
This commit is contained in:
1 parent
feb62bc428
commit
28429aa380
11 files changed
+421
-169
No files matched your search
@@ -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 flyersoxvera 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
@@ -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
@@ -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
@@ -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."
|
||||
@@ -22,6 +22,11 @@ requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"InquirerPy",
|
||||
"rich",
|
||||
"qrcode[pil]",
|
||||
"Pillow",
|
||||
"jsmin",
|
||||
"htmlmin2",
|
||||
"pypdf",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
InquirerPy
|
||||
rich
|
||||
qrcode[pil]
|
||||
Pillow
|
||||
jsmin
|
||||
htmlmin2
|
||||
pypdf
|
||||
@@ -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
@@ -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
@@ -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 "------------------------------------------------------------------"
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user