Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc64d1c3b5 | ||
|
|
b5ab447207 | ||
|
|
35069e24c5 | ||
|
|
51ab72239f | ||
|
|
4e571b62ea | ||
|
|
3bb8627d20 | ||
|
|
4e84466d38 | ||
|
|
f530e54f45 | ||
|
|
8be7d0761b | ||
|
|
6d747f719f | ||
|
|
dc83b76317 | ||
|
|
9c699eb36b | ||
|
|
3cba4c7e9f | ||
|
|
69ca9f6411 | ||
|
|
994744ee60 | ||
|
|
15f881e603 | ||
|
|
8662e2b16d | ||
|
|
71ae96c841 | ||
|
|
765c0fa2a0 | ||
|
|
d816880073 | ||
|
|
fef1f2b5a8 | ||
|
|
6105ca7a18 | ||
|
|
9d1210cd51 | ||
|
|
89a92a2a2d | ||
|
|
283d365744 | ||
|
|
cc569a0f67 | ||
|
|
68b155e901 | ||
|
|
7f533560c9 | ||
|
|
3fe62f94be | ||
|
|
30ca68895f | ||
|
|
b3ee6869a2 | ||
|
|
dff82c6abd | ||
|
|
98e51c4712 | ||
|
|
4567563b1f | ||
|
|
bded7b870f | ||
|
|
cbcc7b1d15 | ||
|
|
da33c5cfd1 | ||
|
|
e6d12ba53e | ||
|
|
4717304e1e | ||
|
|
70d214a702 | ||
|
|
0400199795 | ||
|
|
00d55a2fda | ||
|
|
e1aa0513e9 | ||
|
|
79207a0feb | ||
|
|
2aebc93507 | ||
|
|
c5b989767f | ||
|
|
246bbb5730 | ||
|
|
9ab26ff943 | ||
|
|
0dec5b7928 | ||
|
|
da987d028f | ||
|
|
031d84b8b2 | ||
|
|
024680fbce | ||
|
|
858f58d19c | ||
|
|
06c8565b94 | ||
|
|
a4bb4dce23 | ||
|
|
66db78c950 | ||
|
|
f0caf2323e | ||
|
|
c5573075b6 | ||
|
|
6313db3f05 | ||
|
|
3ae321fcbd | ||
|
|
a017bf876f | ||
|
|
193345e616 | ||
|
|
3165effa8a | ||
|
|
6efd049851 | ||
|
|
0fdba633f4 | ||
|
|
b2b1e75ec6 |
No files matched your search
@@ -10,19 +10,18 @@ jobs:
|
||||
build:
|
||||
name: Build for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact_name: voxvera-linux
|
||||
asset_name: voxvera-linux
|
||||
- os: ubuntu-22.04
|
||||
artifact_name: voxvera-linux-x86_64
|
||||
- os: windows-latest
|
||||
artifact_name: voxvera-windows.exe
|
||||
asset_name: voxvera-windows.exe
|
||||
- os: macos-latest
|
||||
artifact_name: voxvera-macos
|
||||
asset_name: voxvera-macos
|
||||
artifact_name: voxvera-macos-x86_64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -35,14 +34,38 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade setuptools pkg_resources || pip install --upgrade setuptools
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
pip install .
|
||||
pip install pyinstaller pyinstaller-hooks-contrib
|
||||
|
||||
- name: Download Tor Binaries
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x scripts/download_tor.sh
|
||||
./scripts/download_tor.sh
|
||||
|
||||
- name: Build with PyInstaller
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then SEP=";"; else SEP=":"; fi
|
||||
pyinstaller --onefile \
|
||||
--collect-all pkg_resources \
|
||||
--collect-all setuptools \
|
||||
--hidden-import pkg_resources \
|
||||
--hidden-import setuptools \
|
||||
--hidden-import jaraco.text \
|
||||
--hidden-import jaraco.functools \
|
||||
--hidden-import jaraco.context \
|
||||
--hidden-import more_itertools \
|
||||
--hidden-import packaging \
|
||||
--hidden-import platformdirs \
|
||||
--collect-all onionshare_cli \
|
||||
--collect-all flask_socketio \
|
||||
--collect-all engineio \
|
||||
--collect-all socketio \
|
||||
--collect-all eventlet \
|
||||
--collect-all gevent \
|
||||
--add-data "voxvera/locales${SEP}voxvera/locales" \
|
||||
--add-data "voxvera/src${SEP}voxvera/src" \
|
||||
--add-data "voxvera/templates${SEP}voxvera/templates" \
|
||||
@@ -51,8 +74,76 @@ jobs:
|
||||
--name "${{ matrix.artifact_name }}" \
|
||||
voxvera/cli.py
|
||||
|
||||
- name: Build .deb (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
mkdir -p voxvera/resources/bin
|
||||
cp dist/${{ matrix.artifact_name }} voxvera/resources/bin/voxvera-linux-x86_64
|
||||
chmod +x packaging/build_deb.sh
|
||||
./packaging/build_deb.sh dist/${{ matrix.artifact_name }}
|
||||
|
||||
- name: Build AppImage (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
mkdir -p voxvera/resources/bin
|
||||
cp dist/${{ matrix.artifact_name }} voxvera/resources/bin/voxvera-linux-x86_64
|
||||
chmod +x packaging/build_appimage.sh
|
||||
./packaging/build_appimage.sh dist/${{ matrix.artifact_name }}
|
||||
|
||||
- name: Build Flatpak (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
mkdir -p voxvera/resources/bin
|
||||
cp dist/${{ matrix.artifact_name }} voxvera/resources/bin/voxvera-linux-x86_64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y flatpak flatpak-builder
|
||||
chmod +x packaging/build_flatpak.sh
|
||||
./packaging/build_flatpak.sh dist/${{ matrix.artifact_name }} || echo "Flatpak build failed"
|
||||
|
||||
- name: Build .dmg (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
mkdir -p voxvera/resources/bin
|
||||
cp dist/${{ matrix.artifact_name }} voxvera/resources/bin/voxvera-macos-x86_64
|
||||
chmod +x packaging/build_dmg.sh
|
||||
./packaging/build_dmg.sh dist/${{ matrix.artifact_name }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: dist/${{ matrix.artifact_name }}
|
||||
name: binaries-${{ matrix.os }}
|
||||
path: |
|
||||
dist/${{ matrix.artifact_name }}
|
||||
voxvera/resources/bin/*.deb
|
||||
voxvera/resources/bin/*.dmg
|
||||
voxvera/resources/bin/*.AppImage
|
||||
voxvera/resources/bin/*.flatpak
|
||||
|
||||
publish:
|
||||
name: Create GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Flatten artifacts for release
|
||||
run: |
|
||||
mkdir -p release_assets
|
||||
find artifacts -type f -exec cp {} release_assets/ \;
|
||||
ls -R release_assets/
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
release_assets/*
|
||||
fail_on_unmatched_files: false
|
||||
overwrite: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
jobs:
|
||||
lint-test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
@@ -16,8 +17,8 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
pip install -e .
|
||||
pip install flake8
|
||||
pip install -e ".[test]"
|
||||
- name: Lint
|
||||
run: flake8 voxvera tests --exclude=voxvera/vendor --ignore=E501,W503,W293,E402,E306,E704,E261,F841,E302,F401,E401,F541
|
||||
- name: Test
|
||||
|
||||
@@ -8,8 +8,6 @@ jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
VOXVERA_E2E_OFFLINE: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
@@ -20,9 +18,11 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y tor
|
||||
pip install ".[e2e]"
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[e2e]"
|
||||
- name: Run end-to-end tests
|
||||
run: ci/test-e2e.sh
|
||||
continue-on-error: true
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install build tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build pyinstaller
|
||||
- name: Build wheel
|
||||
run: python -m build --wheel --sdist
|
||||
- name: Build binary
|
||||
run: |
|
||||
pyinstaller --onefile \
|
||||
--add-data "voxvera/locales:voxvera/locales" \
|
||||
--add-data "voxvera/src:voxvera/src" \
|
||||
--add-data "voxvera/templates:voxvera/templates" \
|
||||
--add-data "voxvera/resources:voxvera/resources" \
|
||||
--add-data "requirements.txt:." \
|
||||
--name voxvera-linux \
|
||||
voxvera/cli.py
|
||||
- name: Create AppImage
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wget
|
||||
bash packaging/build_appimage.sh
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
docker build -t ghcr.io/${OWNER_LC}/voxvera:${{ github.ref_name }} .
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Push Docker image
|
||||
run: |
|
||||
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
docker push ghcr.io/${OWNER_LC}/voxvera:${{ github.ref_name }}
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
dist/voxvera-linux
|
||||
dist/VoxVera.AppImage
|
||||
@@ -46,6 +46,14 @@ voxvera/src/qrcode-tear-offs.png
|
||||
# PyInstaller
|
||||
*.spec
|
||||
dist_bin/
|
||||
voxvera/resources/bin/voxvera-*
|
||||
site/download/voxvera-portable.zip
|
||||
voxvera/resources/bin/*
|
||||
!voxvera/resources/bin/.gitkeep
|
||||
site/download/*.zip
|
||||
!site/download/voxvera-source.zip
|
||||
site/download/*.exe
|
||||
site/download/*.deb
|
||||
site/download/*.dmg
|
||||
site/download/*.AppImage
|
||||
site/download/*.flatpak
|
||||
site/download/voxvera-linux
|
||||
site/download/voxvera-linux-x86_64
|
||||
@@ -0,0 +1,97 @@
|
||||
# VoxVera Release Process
|
||||
|
||||
**⚠️ ATTENTION AI AGENTS (CRITICAL WORKFLOW):**
|
||||
Do **NOT** automatically package, tag, or push a new release after completing a set of changes. Releases should **ONLY** be triggered when the user explicitly requests one (e.g., "Package a release now" or "Let's release this").
|
||||
Instead of releasing immediately, you must append a summary of your completed work to the `UPCOMING_RELEASE.md` file. This allows development to span multiple context sessions and ensures that new releases contain a substantial amount of value. When a release is finally requested, use the contents of `UPCOMING_RELEASE.md` to format the GitHub Release notes, and then clear the document for the next cycle.
|
||||
|
||||
This document outlines the mandatory steps to perform before every release to ensure code quality, cross-language integrity, and professional distribution assets.
|
||||
|
||||
## 1. Pre-Flight Checks (Quality Assurance)
|
||||
|
||||
Before any release, the codebase must pass all internal quality checks **without taking any shortcuts**. All tests must be rigorous, use real data where appropriate, and passing them should be a true mark of quality.
|
||||
|
||||
### Run Tests
|
||||
Ensure all core CLI, E2E, and Integrity tests pass:
|
||||
```bash
|
||||
pytest tests/
|
||||
```
|
||||
*Note: The `test_integrity.py` check is critical as it verifies that all 14+ language locales have synchronized keys and all binary bundles are 100% self-contained.*
|
||||
|
||||
### Run Linting
|
||||
Ensure the code adheres to style guidelines (no trailing whitespace, proper spacing, etc.):
|
||||
```bash
|
||||
# Using flake8 (excluding vendored dependencies)
|
||||
flake8 voxvera/ --exclude=voxvera/vendor/
|
||||
```
|
||||
|
||||
## 2. Update & Generate Localized Assets
|
||||
|
||||
Before releasing, you must ensure that **all documentation accurately reflects the new features and functions** introduced in the release.
|
||||
|
||||
All documentation and site templates must be synchronized with the translation system. This ensures that any changes to documentation are reflected across all 14+ supported languages.
|
||||
|
||||
### Synchronize Documentation
|
||||
If you have modified any documentation templates in `docs/templates/` (which you should, to cover new features), you **must** use the translation system to regenerate the localized documentation before release:
|
||||
```bash
|
||||
# Regenerate docs/ folders for all languages from templates and locales
|
||||
python3 -m voxvera.cli build-docs
|
||||
```
|
||||
|
||||
### Synchronize Site Assets
|
||||
The main `site/` directory (including QRs, HTML, and portable Zips) must also be refreshed to include the latest binary names and locale strings:
|
||||
```bash
|
||||
# Refresh site/ directory
|
||||
python3 -m voxvera.cli build-site
|
||||
```
|
||||
|
||||
## 3. Build Distribution Binaries
|
||||
|
||||
Standardized binaries and AppImages must be generated for the current platform.
|
||||
|
||||
### Standard Binary
|
||||
Generates an architecture-specific binary (e.g., `voxvera-linux-x86_64`) in `voxvera/resources/bin/`.
|
||||
```bash
|
||||
./scripts/build_binaries.sh
|
||||
```
|
||||
|
||||
### AppImage (Linux)
|
||||
Wraps the standard binary into a self-contained AppImage.
|
||||
```bash
|
||||
./packaging/build_appimage.sh
|
||||
```
|
||||
|
||||
### Flatpak (Linux)
|
||||
Generates a standalone `.flatpak` bundle. Requires `flatpak-builder`.
|
||||
```bash
|
||||
./packaging/build_flatpak.sh
|
||||
```
|
||||
|
||||
### Debian Package (Linux)
|
||||
Generates a `.deb` package for Debian/Ubuntu systems.
|
||||
```bash
|
||||
./packaging/build_deb.sh
|
||||
```
|
||||
|
||||
### DMG (macOS)
|
||||
Generates a `.dmg` installer for macOS (must be run on macOS).
|
||||
```bash
|
||||
./packaging/build_dmg.sh
|
||||
```
|
||||
|
||||
## 4. Release Execution
|
||||
|
||||
1. **Version Bump:** Update `__version__` in `voxvera/__init__.py` and `version` in `pyproject.toml`.
|
||||
2. **Commit & Tag:**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "chore: release vX.Y.Z"
|
||||
git tag -a vX.Y.Z -m "VoxVera vX.Y.Z"
|
||||
git push origin main --tags
|
||||
```
|
||||
## 5. Automated Releases (GitHub Actions)
|
||||
|
||||
The repository is configured with a **Build Standalone Binaries** workflow that automatically runs when a new tag (`v*`) is pushed. This workflow:
|
||||
- Builds native executables for **Linux**, **Windows (.exe)**, and **macOS**.
|
||||
- Automatically attaches these binaries to the corresponding GitHub Release.
|
||||
|
||||
This ensures that Windows users receive a professional `voxvera-windows.exe` rather than having to rely on `.bat` or `.sh` scripts.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,86 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Robustness: ensure we are in the project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
LOG_DIR="$(pwd)/ci-logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
# Use a subshell for tee to avoid issues with some shell environments
|
||||
exec > >(tee "$LOG_DIR/run.log") 2>&1
|
||||
|
||||
echo "Starting E2E verification at $(date)"
|
||||
|
||||
# Install VoxVera
|
||||
if [ -n "${VOXVERA_BIN:-}" ]; then
|
||||
if [ -n "${VOXVERA_BIN:-}" ] && [ -f "$VOXVERA_BIN" ]; then
|
||||
voxvera_cmd="$VOXVERA_BIN"
|
||||
echo "Using provided VoxVera binary: $voxvera_cmd" >&2
|
||||
else
|
||||
{ time pip install --break-system-packages -e .; } 2>&1
|
||||
voxvera_cmd="$(command -v voxvera)"
|
||||
echo "Installing VoxVera in editable mode..."
|
||||
python3 -m pip install --break-system-packages -e ".[e2e]"
|
||||
voxvera_cmd="$(command -v voxvera || echo "$HOME/.local/bin/voxvera")"
|
||||
fi
|
||||
|
||||
echo "VoxVera command: $voxvera_cmd"
|
||||
|
||||
# Generate demo flyer
|
||||
echo "Initializing demo flyer..."
|
||||
"$voxvera_cmd" --lang en init --template voxvera <<EOI
|
||||
DemoUser
|
||||
demosite
|
||||
voxvera
|
||||
EOI
|
||||
|
||||
echo "Building demo flyer..."
|
||||
"$voxvera_cmd" --lang en build
|
||||
|
||||
# Verify build output exists in host/
|
||||
folder_name=$(python3 -c "import json; print(json.load(open('voxvera/host/voxvera/config.json'))['folder_name'])")
|
||||
folder_name="voxvera"
|
||||
host_dir="voxvera/host/$folder_name"
|
||||
|
||||
if [ ! -d "$host_dir" ]; then
|
||||
echo "CRITICAL: Host directory $host_dir not found!" >&2
|
||||
ls -R voxvera/host/ || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ls -R "$host_dir" >>"$LOG_DIR/tree.txt"
|
||||
|
||||
# Verify essential files
|
||||
for f in index.html config.json; do
|
||||
[ -f "$host_dir/$f" ] || { echo "Missing $f in $host_dir" >&2; exit 1; }
|
||||
if [ ! -f "$host_dir/$f" ]; then
|
||||
echo "CRITICAL: Missing $f in $host_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Build output verified in $host_dir"
|
||||
|
||||
# Run pytest unit tests
|
||||
echo "Running pytest unit tests..."
|
||||
python3 -m pytest tests/ -v 2>&1 | tee "$LOG_DIR/pytest.log"
|
||||
|
||||
# Optional network tests
|
||||
if [ "${VOXVERA_E2E_OFFLINE:-}" != "1" ]; then
|
||||
# Start Tor
|
||||
tor >"$LOG_DIR/tor.log" 2>&1 &
|
||||
echo "Starting network-dependent tests..."
|
||||
|
||||
# GitHub Actions Ubuntu runners have Tor pre-installed and running as a service.
|
||||
# We stop it to ensure we can run our own instance with known configuration on default ports.
|
||||
echo "Stopping system Tor service if running..."
|
||||
sudo systemctl stop tor || true
|
||||
|
||||
# Kill any lingering tor processes
|
||||
sudo pkill -x tor || true
|
||||
|
||||
echo "Starting a fresh Tor instance on default ports (9050/9051)..."
|
||||
mkdir -p "$LOG_DIR/tor_data"
|
||||
tor --SocksPort 9050 --ControlPort 9051 --CookieAuthentication 0 --DataDirectory "$LOG_DIR/tor_data" >"$LOG_DIR/tor.log" 2>&1 &
|
||||
TOR_PID=$!
|
||||
sleep 10
|
||||
|
||||
# Wait for Tor to bootstrap
|
||||
echo "Waiting for Tor to bootstrap (up to 120s)..."
|
||||
BOOTSTRAPPED=0
|
||||
for i in {1..120}; do
|
||||
if grep -q "Bootstrapped 100%" "$LOG_DIR/tor.log"; then
|
||||
echo "Tor bootstrapped successfully."
|
||||
BOOTSTRAPPED=1
|
||||
break
|
||||
fi
|
||||
[ $((i % 10)) -eq 0 ] && echo "... still bootstrapping Tor ($i s) ..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ $BOOTSTRAPPED -eq 0 ]; then
|
||||
echo "CRITICAL: Tor failed to bootstrap after 120s" >&2
|
||||
tail -n 20 "$LOG_DIR/tor.log"
|
||||
kill ${TOR_PID:-} || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start OnionShare
|
||||
echo "Starting OnionShare..."
|
||||
# Explicitly set environment variables that OnionShare-cli respects
|
||||
export TOR_SOCKS_PORT=9050
|
||||
export TOR_CONTROL_PORT=9051
|
||||
|
||||
onionshare-cli --website --public \
|
||||
--persistent "$host_dir/.onionshare-session" \
|
||||
-v \
|
||||
"$host_dir" >"$LOG_DIR/onionshare.log" 2>&1 &
|
||||
OS_PID=$!
|
||||
|
||||
# Wait for URL
|
||||
URL=""
|
||||
i=0
|
||||
while [ $i -lt 90 ]; do
|
||||
if grep -Eo 'http[^ ]+\.onion' "$LOG_DIR/onionshare.log" | head -n1 >"$LOG_DIR/url.txt"; then
|
||||
URL=$(cat "$LOG_DIR/url.txt")
|
||||
[ -n "$URL" ] && break
|
||||
fi
|
||||
i=$((i + 1))
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "$URL" ]; then
|
||||
echo "Onion URL not found" >&2
|
||||
kill $OS_PID || true
|
||||
kill $TOR_PID || true
|
||||
# Check if OnionShare process is still alive after a few seconds
|
||||
sleep 5
|
||||
if ! kill -0 $OS_PID 2>/dev/null; then
|
||||
echo "CRITICAL: OnionShare-cli failed to start" >&2
|
||||
cat "$LOG_DIR/onionshare.log"
|
||||
kill ${TOR_PID:-} || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify URL in config
|
||||
grep -q "$URL" "$host_dir/config.json"
|
||||
# Wait for URL with timeout and feedback
|
||||
echo "Waiting for OnionShare to generate URL (up to 120s)..."
|
||||
URL=""
|
||||
for i in {1..120}; do
|
||||
# Use -a to process as text even if there are binary chars (ANSI codes)
|
||||
if grep -aEo 'http[s]?://[a-z2-7]{56}\.onion' "$LOG_DIR/onionshare.log" | head -n1 >"$LOG_DIR/url.txt"; then
|
||||
URL=$(cat "$LOG_DIR/url.txt" | tr -d '\r\n ')
|
||||
if [ -n "$URL" ]; then
|
||||
echo "Successfully generated Onion URL: $URL"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
[ $((i % 10)) -eq 0 ] && echo "... still waiting ($i s) ..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Fetch page via Tor
|
||||
curl --socks5-hostname 127.0.0.1:9050 "$URL" | grep -q '<title>'
|
||||
if [ -z "$URL" ]; then
|
||||
echo "CRITICAL: Onion URL not found after 120s" >&2
|
||||
echo "--- Last 20 lines of OnionShare log ---"
|
||||
tail -n 20 "$LOG_DIR/onionshare.log"
|
||||
kill $OS_PID || true
|
||||
kill ${TOR_PID:-} || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Robustness: Manually update the config.json with the URL so verification passes
|
||||
# and the site is actually using the live onion address.
|
||||
echo "Updating $host_dir/config.json with live URL: $URL"
|
||||
python3 <<EOF
|
||||
import json
|
||||
import os
|
||||
path = "$host_dir/config.json"
|
||||
with open(path, 'r') as f:
|
||||
data = json.load(f)
|
||||
data['url'] = "$URL"
|
||||
with open(path + '.new', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
os.replace(path + '.new', path)
|
||||
EOF
|
||||
|
||||
echo "Verifying content of $host_dir/config.json:"
|
||||
cat "$host_dir/config.json"
|
||||
|
||||
# Verify URL in config
|
||||
if ! grep -Fq "$URL" "$host_dir/config.json"; then
|
||||
echo "CRITICAL: URL $URL not found in $host_dir/config.json" >&2
|
||||
cat "$host_dir/config.json"
|
||||
kill $OS_PID || true
|
||||
kill ${TOR_PID:-} || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Re-build to update QR codes with live onion URL
|
||||
echo "Re-building site with live URL..."
|
||||
"$voxvera_cmd" --lang en build --config "$host_dir/config.json"
|
||||
|
||||
# Fetch page via Tor (retry a few times as circuits can be flaky)
|
||||
echo "Verifying reachability via Tor (on port 9050)..."
|
||||
FETCHED=0
|
||||
for attempt in {1..5}; do
|
||||
echo "Attempt $attempt..."
|
||||
if curl --socks5-hostname 127.0.0.1:9050 -fsSL "$URL" | grep -q '<title>'; then
|
||||
echo "Success: Site is reachable via Tor."
|
||||
FETCHED=1
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
if [ $FETCHED -eq 0 ]; then
|
||||
echo "WARNING: Site could not be reached via Tor after 5 attempts, but OnionShare is running."
|
||||
# In CI, we might skip hard failure if Tor network is exceptionally slow/blocked
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
kill $OS_PID
|
||||
kill $TOR_PID
|
||||
echo "Cleaning up..."
|
||||
kill $OS_PID || true
|
||||
kill ${TOR_PID:-} || true
|
||||
wait $OS_PID 2>/dev/null || true
|
||||
wait $TOR_PID 2>/dev/null || true
|
||||
else
|
||||
echo "Skipping network-dependent tests" >&2
|
||||
exit 0
|
||||
echo "Skipping network-dependent tests (VOXVERA_E2E_OFFLINE=1)" >&2
|
||||
fi
|
||||
|
||||
echo "E2E verification complete at $(date)"
|
||||
exit 0
|
||||
@@ -4,31 +4,39 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instruct
|
||||
|
||||
## Prerequisites
|
||||
|
||||
VoxVera is designed to be highly portable and requires minimal system dependencies.
|
||||
VoxVera is designed to be highly portable. You can run it directly from the source code on any platform with Python installed.
|
||||
|
||||
### 1. Standalone Binaries (Recommended)
|
||||
You can download standalone, zero-dependency binaries for your operating system:
|
||||
- **Linux:** [voxvera-linux](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-linux)
|
||||
- **Windows:** [voxvera-windows.exe](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe)
|
||||
- **macOS:** [voxvera-macos](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-macos)
|
||||
|
||||
These binaries include everything needed to run VoxVera (except `onionshare-cli`).
|
||||
|
||||
### 2. One-Line Installer
|
||||
Alternatively, install via our automated script:
|
||||
### 1. Automated Install (Recommended)
|
||||
The easiest way to install VoxVera is using our cross-platform installer script:
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
|
||||
### 3. Manual Python Install
|
||||
If you prefer to run from source:
|
||||
|
||||
```bash
|
||||
pipx install 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'
|
||||
sudo apt install tor onionshare-cli # Debian/Ubuntu
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
### 2. Manual Source Install
|
||||
If you have downloaded the source code ZIP:
|
||||
|
||||
1. **Extract** the ZIP file.
|
||||
2. **Open a terminal** in the extracted folder.
|
||||
3. **Run the setup script**:
|
||||
- **Linux/macOS:** `./setup.sh`
|
||||
- **Windows:** `.\voxvera-install.sh` (or run `python -m pip install .`)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
If you wish to remove VoxVera from your system, you can use the provided uninstall scripts from the source directory. This will safely remove the CLI tool and any downloaded dependencies.
|
||||
|
||||
- **Linux / macOS:** Run `./uninstall.sh`
|
||||
- **Windows (PowerShell):** Run `.\uninstall.ps1`
|
||||
|
||||
*Note: Uninstallation will not delete your generated flyers or your `.onion` keys stored in the `~/voxvera-exports` or local `host/` folders.*
|
||||
|
||||
## Step-by-Step
|
||||
|
||||
1. **Initialize:** Run `voxvera init` and follow the prompts. You will be asked to select your language first.
|
||||
@@ -79,10 +87,10 @@ Features:
|
||||
|
||||
To ensure VoxVera remains accessible even if central repositories are censored, every flyer acts as a mirror for the tool.
|
||||
|
||||
When you host a flyer, the **"Download"** button on the landing page provides a `voxvera-portable.zip` containing:
|
||||
When you host a flyer, the **"Download Source Code"** button on the landing page provides a `voxvera-source.zip` containing:
|
||||
- The full source code and all supported languages.
|
||||
- All Python dependencies (pre-vendored).
|
||||
- Cross-platform Tor binaries.
|
||||
- Installation and setup scripts for all platforms.
|
||||
- Complete documentation and templates.
|
||||
|
||||
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
|
||||
|
||||
|
||||
@@ -4,31 +4,39 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instruct
|
||||
|
||||
## Prerequisites
|
||||
|
||||
VoxVera is designed to be highly portable and requires minimal system dependencies.
|
||||
VoxVera is designed to be highly portable. You can run it directly from the source code on any platform with Python installed.
|
||||
|
||||
### 1. Standalone Binaries (Recommended)
|
||||
You can download standalone, zero-dependency binaries for your operating system:
|
||||
- **Linux:** [voxvera-linux](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-linux)
|
||||
- **Windows:** [voxvera-windows.exe](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe)
|
||||
- **macOS:** [voxvera-macos](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-macos)
|
||||
|
||||
These binaries include everything needed to run VoxVera (except `onionshare-cli`).
|
||||
|
||||
### 2. One-Line Installer
|
||||
Alternatively, install via our automated script:
|
||||
### 1. Automated Install (Recommended)
|
||||
The easiest way to install VoxVera is using our cross-platform installer script:
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
|
||||
### 3. Manual Python Install
|
||||
If you prefer to run from source:
|
||||
|
||||
```bash
|
||||
pipx install 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'
|
||||
sudo apt install tor onionshare-cli # Debian/Ubuntu
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
### 2. Manual Source Install
|
||||
If you have downloaded the source code ZIP:
|
||||
|
||||
1. **Extract** the ZIP file.
|
||||
2. **Open a terminal** in the extracted folder.
|
||||
3. **Run the setup script**:
|
||||
- **Linux/macOS:** `./setup.sh`
|
||||
- **Windows:** `.\voxvera-install.sh` (or run `python -m pip install .`)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
If you wish to remove VoxVera from your system, you can use the provided uninstall scripts from the source directory. This will safely remove the CLI tool and any downloaded dependencies.
|
||||
|
||||
- **Linux / macOS:** Run `./uninstall.sh`
|
||||
- **Windows (PowerShell):** Run `.\uninstall.ps1`
|
||||
|
||||
*Note: Uninstallation will not delete your generated flyers or your `.onion` keys stored in the `~/voxvera-exports` or local `host/` folders.*
|
||||
|
||||
## Step-by-Step
|
||||
|
||||
1. **Initialize:** Run `voxvera init` and follow the prompts. You will be asked to select your language first.
|
||||
@@ -79,10 +87,10 @@ Features:
|
||||
|
||||
To ensure VoxVera remains accessible even if central repositories are censored, every flyer acts as a mirror for the tool.
|
||||
|
||||
When you host a flyer, the **"הורד כלי וקוד מקור"** button on the landing page provides a `voxvera-portable.zip` containing:
|
||||
When you host a flyer, the **"הורד כלי וקוד מקור"** button on the landing page provides a `voxvera-source.zip` containing:
|
||||
- The full source code and all supported languages.
|
||||
- All Python dependencies (pre-vendored).
|
||||
- Cross-platform Tor binaries.
|
||||
- Installation and setup scripts for all platforms.
|
||||
- Complete documentation and templates.
|
||||
|
||||
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
|
||||
|
||||
|
||||
@@ -4,31 +4,39 @@ This guide covers common CLI workflows. See `docs/docker.md` for Docker instruct
|
||||
|
||||
## Prerequisites
|
||||
|
||||
VoxVera is designed to be highly portable and requires minimal system dependencies.
|
||||
VoxVera is designed to be highly portable. You can run it directly from the source code on any platform with Python installed.
|
||||
|
||||
### 1. Standalone Binaries (Recommended)
|
||||
You can download standalone, zero-dependency binaries for your operating system:
|
||||
- **Linux:** [voxvera-linux](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-linux)
|
||||
- **Windows:** [voxvera-windows.exe](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-windows.exe)
|
||||
- **macOS:** [voxvera-macos](https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/voxvera-macos)
|
||||
|
||||
These binaries include everything needed to run VoxVera (except `onionshare-cli`).
|
||||
|
||||
### 2. One-Line Installer
|
||||
Alternatively, install via our automated script:
|
||||
### 1. Automated Install (Recommended)
|
||||
The easiest way to install VoxVera is using our cross-platform installer script:
|
||||
|
||||
**Linux / macOS:**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.sh | bash
|
||||
```
|
||||
|
||||
### 3. Manual Python Install
|
||||
If you prefer to run from source:
|
||||
|
||||
```bash
|
||||
pipx install 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'
|
||||
sudo apt install tor onionshare-cli # Debian/Ubuntu
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/PR0M3TH3AN/VoxVera/main/install.ps1 | iex
|
||||
```
|
||||
|
||||
### 2. Manual Source Install
|
||||
If you have downloaded the source code ZIP:
|
||||
|
||||
1. **Extract** the ZIP file.
|
||||
2. **Open a terminal** in the extracted folder.
|
||||
3. **Run the setup script**:
|
||||
- **Linux/macOS:** `./setup.sh`
|
||||
- **Windows:** `.\voxvera-install.sh` (or run `python -m pip install .`)
|
||||
|
||||
## Uninstallation
|
||||
|
||||
If you wish to remove VoxVera from your system, you can use the provided uninstall scripts from the source directory. This will safely remove the CLI tool and any downloaded dependencies.
|
||||
|
||||
- **Linux / macOS:** Run `./uninstall.sh`
|
||||
- **Windows (PowerShell):** Run `.\uninstall.ps1`
|
||||
|
||||
*Note: Uninstallation will not delete your generated flyers or your `.onion` keys stored in the `~/voxvera-exports` or local `host/` folders.*
|
||||
|
||||
## Step-by-Step
|
||||
|
||||
1. **Initialize:** Run `voxvera init` and follow the prompts. You will be asked to select your language first.
|
||||
@@ -79,10 +87,10 @@ Features:
|
||||
|
||||
To ensure VoxVera remains accessible even if central repositories are censored, every flyer acts as a mirror for the tool.
|
||||
|
||||
When you host a flyer, the **"{{t('web.download_button')}}"** button on the landing page provides a `voxvera-portable.zip` containing:
|
||||
When you host a flyer, the **"{{t('web.download_button')}}"** button on the landing page provides a `voxvera-source.zip` containing:
|
||||
- The full source code and all supported languages.
|
||||
- All Python dependencies (pre-vendored).
|
||||
- Cross-platform Tor binaries.
|
||||
- Installation and setup scripts for all platforms.
|
||||
- Complete documentation and templates.
|
||||
|
||||
This allows anyone who scans your flyer to become a new distributor of the VoxVera tool.
|
||||
|
||||
|
||||
@@ -5,7 +5,16 @@ command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Detect package manager
|
||||
# Detect package manager and sudo
|
||||
if command_exists sudo; then
|
||||
SUDO="sudo"
|
||||
elif [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO=""
|
||||
else
|
||||
echo "This script requires root privileges. Please install sudo or run as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command_exists apt-get; then
|
||||
PM="apt"
|
||||
elif command_exists dnf; then
|
||||
@@ -23,165 +32,167 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg() { echo -e "\e[32m$*\e[0m"; }
|
||||
warn() { echo -e "\e[33m$*\e[0m" >&2; }
|
||||
die() { echo -e "\e[31m$*\e[0m" >&2; exit 1; }
|
||||
|
||||
install_pkg() {
|
||||
case "$PM" in
|
||||
apt)
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y "$@"
|
||||
$SUDO apt-get install -y "$@"
|
||||
;;
|
||||
dnf)
|
||||
sudo dnf install -y "$@"
|
||||
$SUDO dnf install -y "$@"
|
||||
;;
|
||||
yum)
|
||||
sudo yum install -y "$@"
|
||||
$SUDO yum install -y "$@"
|
||||
;;
|
||||
pacman)
|
||||
sudo pacman -Sy --noconfirm "$@"
|
||||
$SUDO pacman -S --noconfirm "$@"
|
||||
;;
|
||||
brew)
|
||||
brew install "$@"
|
||||
;;
|
||||
apk)
|
||||
sudo apk add --no-cache "$@"
|
||||
$SUDO apk add --no-cache "$@"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_pkg() {
|
||||
local cmd=$1
|
||||
local pkg=$2
|
||||
if ! command_exists "$cmd"; then
|
||||
install_pkg "$pkg"
|
||||
fi
|
||||
}
|
||||
|
||||
# Only external runtime dependencies needed (for voxvera serve)
|
||||
require_pkg tor tor
|
||||
if [ "$PM" = "brew" ]; then
|
||||
require_pkg onionshare onionshare
|
||||
# Ensure core dependencies are present (always run to ensure they are up to date)
|
||||
msg "Checking and updating system dependencies..."
|
||||
if [ "$PM" = "apt" ]; then
|
||||
$SUDO apt-get update
|
||||
SYSTEM_PKGS=(tor curl git python3-pip python3-venv)
|
||||
[ -n "$(command -v pipx)" ] || SYSTEM_PKGS+=(pipx)
|
||||
install_pkg "${SYSTEM_PKGS[@]}"
|
||||
elif [ "$PM" = "brew" ]; then
|
||||
brew install tor onionshare curl git
|
||||
else
|
||||
require_pkg onionshare-cli onionshare-cli
|
||||
install_pkg tor curl git
|
||||
fi
|
||||
|
||||
# Onionshare-cli - ensure it's installed and working (always attempt update if using pipx)
|
||||
msg "Ensuring onionshare-cli is installed and up-to-date..."
|
||||
if command_exists pipx && (pipx list | grep -q onionshare || ! command_exists onionshare-cli); then
|
||||
msg "Installing/Updating onionshare-cli via pipx..."
|
||||
pipx install --force git+https://github.com/onionshare/onionshare.git#subdirectory=cli || warn "pipx onionshare-cli install/update failed"
|
||||
elif ! command_exists onionshare-cli && ! command_exists onionshare; then
|
||||
if ! install_pkg onionshare-cli; then
|
||||
warn "Onionshare-cli not found in system repositories. Attempting pipx install fallback."
|
||||
if ! command_exists pipx; then
|
||||
install_pkg pipx
|
||||
pipx ensurepath --force
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
if command_exists pipx; then
|
||||
msg "Installing onionshare-cli via pipx..."
|
||||
pipx install --force git+https://github.com/onionshare/onionshare.git#subdirectory=cli || warn "pipx onionshare-cli install failed"
|
||||
else
|
||||
warn "Could not install pipx. Please install onionshare-cli manually."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
msg "onionshare-cli found (system package), skipping pipx install. Run 'pipx install --force ...' if you want the latest git version."
|
||||
fi
|
||||
|
||||
# Clean up stale user-local packages from prior pip --user installs of voxvera.
|
||||
# These shadow system packages (e.g. Werkzeug) and break onionshare-cli.
|
||||
msg "Cleaning up stale user-local packages that may conflict with system onionshare-cli..."
|
||||
for pydir in "$HOME"/.local/lib/python3.*/site-packages; do
|
||||
[ -d "$pydir" ] || continue
|
||||
for pkg in werkzeug flask flask_socketio onionshare_cli voxvera; do
|
||||
if [ -d "$pydir/$pkg" ]; then
|
||||
warn "Removing stale $pkg from $pydir"
|
||||
rm -rf "$pydir/$pkg" "$pydir/${pkg}-"*.dist-info 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
download_binary() {
|
||||
local url=$1
|
||||
local dest=$2
|
||||
if command_exists curl; then
|
||||
local status
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest" || true)
|
||||
if [ "$status" = "404" ]; then
|
||||
return 2
|
||||
elif [ "$status" != "200" ]; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$status" = "404" ]; then return 2; elif [ "$status" != "200" ]; then return 1; fi
|
||||
elif command_exists wget; then
|
||||
local out
|
||||
out=$(wget --server-response -q "$url" -O "$dest" 2>&1 || true)
|
||||
if echo "$out" | grep -q "404 Not Found"; then
|
||||
return 2
|
||||
elif ! echo "$out" | grep -q "200 OK"; then
|
||||
return 1
|
||||
fi
|
||||
if echo "$out" | grep -q "404 Not Found"; then return 2; elif ! echo "$out" | grep -q "200 OK"; then return 1; fi
|
||||
else
|
||||
echo "Install curl or wget to download voxvera" >&2
|
||||
warn "Install curl or wget to download voxvera"
|
||||
return 1
|
||||
fi
|
||||
chmod +x "$dest"
|
||||
}
|
||||
|
||||
check_local_bin() {
|
||||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||||
echo "Add \$HOME/.local/bin to your PATH to run VoxVera."
|
||||
fi
|
||||
}
|
||||
|
||||
pip_fallback() {
|
||||
if command_exists pip; then
|
||||
echo "Attempting pip install as fallback..."
|
||||
if pip install --user voxvera; then
|
||||
echo "VoxVera installed successfully via pip."
|
||||
exit 0
|
||||
fi
|
||||
echo "pip installation failed." >&2
|
||||
else
|
||||
echo "pip not found for fallback installation" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
pip_repo_fallback() {
|
||||
if command_exists pip; then
|
||||
echo "Attempting pip install from repository as fallback..."
|
||||
if pip install --user git+https://github.com/PR0M3TH3AN/VoxVera; then
|
||||
echo "VoxVera installed successfully from repository."
|
||||
exit 0
|
||||
fi
|
||||
echo "pip installation from repository failed." >&2
|
||||
else
|
||||
echo "pip not found for fallback installation" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
if command_exists pipx; 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"
|
||||
|
||||
# Detect OS for binary selection
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
case "$OS" in
|
||||
linux*) BINARY="voxvera-linux" ;;
|
||||
darwin*) BINARY="voxvera-macos" ;;
|
||||
msys*|cygwin*|mingw*) BINARY="voxvera-windows.exe" ;;
|
||||
*) BINARY="voxvera-linux" ;; # fallback to linux
|
||||
esac
|
||||
|
||||
url="https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/${BINARY}"
|
||||
dest="$install_dir/voxvera"
|
||||
if download_binary "$url" "$dest"; then
|
||||
check_local_bin
|
||||
else
|
||||
rc=$?
|
||||
echo "Binary download failed." >&2
|
||||
if [ $rc -eq 2 ]; then
|
||||
echo "Release asset not found, installing from repository." >&2
|
||||
pip_repo_fallback
|
||||
else
|
||||
pip_fallback
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Install VoxVera (Always use --force to ensure fresh installation)
|
||||
if command_exists curl; then
|
||||
msg "Attempting to download latest VoxVera binary..."
|
||||
# Get the latest release tag
|
||||
latest=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/PR0M3TH3AN/VoxVera/releases/latest | grep -oE '[^/]+$')
|
||||
|
||||
install_dir="$HOME/.local/bin"
|
||||
mkdir -p "$install_dir"
|
||||
|
||||
# Detect OS for binary selection
|
||||
dest="$install_dir/voxvera"
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case "$OS" in
|
||||
linux*) BINARY="voxvera-linux" ;;
|
||||
darwin*) BINARY="voxvera-macos" ;;
|
||||
msys*|cygwin*|mingw*) BINARY="voxvera-windows.exe" ;;
|
||||
*) BINARY="voxvera-linux" ;; # fallback to linux
|
||||
*) BINARY="voxvera-linux" ;;
|
||||
esac
|
||||
|
||||
url="https://github.com/PR0M3TH3AN/VoxVera/releases/latest/download/${BINARY}"
|
||||
dest="$install_dir/voxvera"
|
||||
if download_binary "$url" "$dest"; then
|
||||
check_local_bin
|
||||
|
||||
# Try to download architecture-specific binary (e.g. voxvera-linux-x86_64)
|
||||
url="https://github.com/PR0M3TH3AN/VoxVera/releases/download/${latest}/${BINARY}-${ARCH}"
|
||||
# For Windows, PyInstaller might add .exe but here we focus on linux/mac
|
||||
|
||||
msg "Downloading from: $url"
|
||||
set +e
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest")
|
||||
curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ $curl_exit -eq 0 ] && [ "$status" = "200" ]; then
|
||||
chmod +x "$dest"
|
||||
msg "VoxVera binary (${ARCH}) installed to $dest"
|
||||
[[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && warn "Add \$HOME/.local/bin to your PATH."
|
||||
exit 0
|
||||
else
|
||||
rc=$?
|
||||
echo "Binary download failed." >&2
|
||||
if [ $rc -eq 2 ]; then
|
||||
echo "Release asset not found, installing from repository." >&2
|
||||
pip_repo_fallback
|
||||
else
|
||||
pip_fallback
|
||||
fi
|
||||
warn "Binary download failed (status: $status, exit: $curl_exit). Falling back to source installation."
|
||||
[ -f "$dest" ] && rm "$dest"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "VoxVera installed successfully."
|
||||
echo "Run 'voxvera check' to verify your setup."
|
||||
if command_exists pipx; then
|
||||
msg "Falling back to pipx installation (this may take a few minutes)..."
|
||||
if pipx install --force 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'; then
|
||||
# Ensure setuptools (pkg_resources) is available in the venv — pipx can
|
||||
# sometimes skip it if the resolver thinks the build-system copy suffices.
|
||||
pipx inject voxvera setuptools 2>/dev/null || true
|
||||
pipx ensurepath --force
|
||||
msg "\nVoxVera installed/updated successfully via pipx."
|
||||
msg "IMPORTANT: Please restart your terminal or run 'source ~/.bashrc' (or your shell config) to use 'voxvera'."
|
||||
exit 0
|
||||
fi
|
||||
warn "pipx install failed, trying pip fallback..."
|
||||
fi
|
||||
|
||||
# Fallback to pip
|
||||
msg "Installing/Updating VoxVera via pip..."
|
||||
PIP_OPTS="--user --upgrade"
|
||||
if [ -f /etc/debian_version ]; then
|
||||
DEB_VER=$(cat /etc/debian_version | cut -d. -f1)
|
||||
if [[ "$DEB_VER" =~ ^[0-9]+$ ]] && [ "$DEB_VER" -ge 12 ]; then
|
||||
PIP_OPTS="$PIP_OPTS --break-system-packages"
|
||||
fi
|
||||
fi
|
||||
if pip install $PIP_OPTS 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'; then
|
||||
msg "VoxVera installed via pip."
|
||||
else
|
||||
die "Installation failed."
|
||||
fi
|
||||
|
||||
msg "\nVoxVera installed/updated successfully. Run 'voxvera check' to verify."
|
||||
@@ -1,21 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ ! -f dist/voxvera-linux ]; then
|
||||
echo "Run PyInstaller first" >&2
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ARCH=$(uname -m)
|
||||
BINARY=${1:-"voxvera/resources/bin/voxvera-linux-$ARCH"}
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "Binary not found: $BINARY. Run scripts/build_binaries.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APPDIR=dist/AppDir
|
||||
APPDIR=build/AppDir
|
||||
mkdir -p "$APPDIR/usr/bin"
|
||||
cp dist/voxvera-linux "$APPDIR/usr/bin/voxvera"
|
||||
cp "$BINARY" "$APPDIR/usr/bin/voxvera"
|
||||
chmod +x "$APPDIR/usr/bin/voxvera"
|
||||
mkdir -p "$APPDIR/usr/lib/voxvera/resources"
|
||||
cp -r voxvera/resources/tor "$APPDIR/usr/lib/voxvera/resources/"
|
||||
# also bundle Tor for the Electron GUI
|
||||
mkdir -p gui/electron/voxvera/resources
|
||||
cp -r voxvera/resources/tor gui/electron/voxvera/resources/
|
||||
|
||||
# AppRun symlink (Crucial for AppImage to launch)
|
||||
ln -sf usr/bin/voxvera "$APPDIR/AppRun"
|
||||
chmod +x "$APPDIR/AppRun"
|
||||
|
||||
# Desktop file
|
||||
cat > "$APPDIR/voxvera.desktop" <<EOD
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
@@ -23,13 +29,24 @@ Name=VoxVera
|
||||
Exec=voxvera
|
||||
Icon=voxvera
|
||||
Categories=Utility;
|
||||
Terminal=true
|
||||
EOD
|
||||
|
||||
touch "$APPDIR/voxvera.png"
|
||||
# Use a generic icon since we don't have one
|
||||
# (AppImageTool requires an icon file to exist)
|
||||
# We will copy a qrcode as a placeholder if nothing else exists
|
||||
cp site/qrcode-content.png "$APPDIR/voxvera.png"
|
||||
|
||||
wget -q https://github.com/AppImage/AppImageKit/releases/latest/download/appimagetool-x86_64.AppImage -O appimagetool
|
||||
# Download appimagetool
|
||||
echo "Downloading appimagetool..."
|
||||
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool
|
||||
chmod +x appimagetool
|
||||
./appimagetool --appimage-extract-and-run "$APPDIR" dist/VoxVera.AppImage
|
||||
|
||||
# Run appimagetool
|
||||
echo "Running appimagetool..."
|
||||
export ARCH
|
||||
mkdir -p voxvera/resources/bin
|
||||
./appimagetool --appimage-extract-and-run "$APPDIR" voxvera/resources/bin/VoxVera-x86_64.AppImage
|
||||
rm appimagetool
|
||||
|
||||
echo "AppImage created at dist/VoxVera.AppImage"
|
||||
echo "AppImage created at voxvera/resources/bin/VoxVera-x86_64.AppImage"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ARCH=$(uname -m)
|
||||
# Debian uses amd64 instead of x86_64
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
DEB_ARCH="amd64"
|
||||
elif [ "$ARCH" == "aarch64" ] || [ "$ARCH" == "arm64" ]; then
|
||||
DEB_ARCH="arm64"
|
||||
else
|
||||
DEB_ARCH="$ARCH"
|
||||
fi
|
||||
BINARY=${1:-"voxvera/resources/bin/voxvera-linux-$ARCH"}
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "Binary not found: $BINARY." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
VERSION=$(grep "__version__" voxvera/__init__.py | cut -d '"' -f 2)
|
||||
PKG_DIR="build/voxvera_${VERSION}_${DEB_ARCH}"
|
||||
|
||||
echo "Building .deb package for version $VERSION ($DEB_ARCH)..."
|
||||
|
||||
rm -rf "$PKG_DIR"
|
||||
mkdir -p "$PKG_DIR/usr/bin"
|
||||
mkdir -p "$PKG_DIR/DEBIAN"
|
||||
|
||||
cp "$BINARY" "$PKG_DIR/usr/bin/voxvera"
|
||||
chmod +x "$PKG_DIR/usr/bin/voxvera"
|
||||
|
||||
# Copy control file and update version/arch
|
||||
cp packaging/deb/DEBIAN/control "$PKG_DIR/DEBIAN/control"
|
||||
sed -i "s/^Version: .*/Version: $VERSION/" "$PKG_DIR/DEBIAN/control"
|
||||
sed -i "s/^Architecture: .*/Architecture: $DEB_ARCH/" "$PKG_DIR/DEBIAN/control"
|
||||
|
||||
# Build the package
|
||||
mkdir -p voxvera/resources/bin
|
||||
dpkg-deb --build "$PKG_DIR" "voxvera/resources/bin/voxvera_${VERSION}_${DEB_ARCH}.deb"
|
||||
|
||||
echo "Debian package created at voxvera/resources/bin/voxvera_${VERSION}_${DEB_ARCH}.deb"
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# This script is intended to be run on macOS
|
||||
if [ "$(uname)" != "Darwin" ]; then
|
||||
echo "Error: .dmg creation requires macOS." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH=$(uname -m)
|
||||
BINARY=${1:-"voxvera/resources/bin/voxvera-macos-$ARCH"}
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "Binary not found: $BINARY." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(grep "__version__" voxvera/__init__.py | cut -d '"' -f 2)
|
||||
DMG_NAME="VoxVera_${VERSION}_${ARCH}.dmg"
|
||||
STAGING_DIR="build/dmg_staging"
|
||||
|
||||
echo "Building .dmg package for version $VERSION ($ARCH)..."
|
||||
|
||||
rm -rf "$STAGING_DIR"
|
||||
mkdir -p "$STAGING_DIR"
|
||||
|
||||
# For a CLI tool, we just put the binary in the DMG.
|
||||
# In the future, we could create an .app bundle if there's a GUI.
|
||||
cp "$BINARY" "$STAGING_DIR/voxvera"
|
||||
chmod +x "$STAGING_DIR/voxvera"
|
||||
|
||||
# Create the DMG
|
||||
mkdir -p voxvera/resources/bin
|
||||
hdiutil create -volname "VoxVera $VERSION" -srcfolder "$STAGING_DIR" -ov -format UDZO "voxvera/resources/bin/$DMG_NAME"
|
||||
|
||||
echo "DMG created at voxvera/resources/bin/$DMG_NAME"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Ensure we are in project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ARCH=$(uname -m)
|
||||
BINARY=${1:-"voxvera/resources/bin/voxvera-linux-$ARCH"}
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "Binary not found: $BINARY. Run scripts/build_binaries.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v flatpak-builder &> /dev/null; then
|
||||
echo "flatpak-builder not found. Please install it (e.g., sudo apt install flatpak-builder)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building Flatpak package..."
|
||||
|
||||
BUILD_DIR=build/flatpak-build
|
||||
REPO_DIR=build/flatpak-repo
|
||||
MANIFEST=packaging/flatpak/org.voxvera.VoxVera.yml
|
||||
|
||||
# Clean previous
|
||||
rm -rf "$BUILD_DIR" "$REPO_DIR"
|
||||
|
||||
# Build
|
||||
flatpak-builder --force-clean --repo="$REPO_DIR" "$BUILD_DIR" "$MANIFEST"
|
||||
|
||||
# Generate a standalone .flatpak file
|
||||
mkdir -p voxvera/resources/bin
|
||||
flatpak build-bundle "$REPO_DIR" voxvera/resources/bin/VoxVera-x86_64.flatpak org.voxvera.VoxVera
|
||||
|
||||
echo "Flatpak bundle created at voxvera/resources/bin/VoxVera-x86_64.flatpak"
|
||||
@@ -0,0 +1,9 @@
|
||||
Package: voxvera
|
||||
Version: 0.1.4
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: VoxVera <[email protected]>
|
||||
Description: VoxVera CLI utilities
|
||||
Anonymous message-spreading and flyer-generation tool.
|
||||
Depends: libc6
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=VoxVera
|
||||
Comment=Anonymous message-spreading and flyer-generation tool
|
||||
Exec=voxvera
|
||||
Icon=org.voxvera.VoxVera
|
||||
Categories=Utility;Network;
|
||||
Terminal=true
|
||||
StartupWMClass=voxvera
|
||||
|
After Width: | Height: | Size: 436 B |
@@ -0,0 +1,27 @@
|
||||
app-id: org.voxvera.VoxVera
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: '23.08'
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: voxvera
|
||||
finish-args:
|
||||
- --share=network
|
||||
- --socket=fallback-x11
|
||||
- --socket=wayland
|
||||
- --device=dri
|
||||
- --share=ipc
|
||||
- --filesystem=xdg-run/tor:ro # If using external Tor
|
||||
modules:
|
||||
- name: voxvera
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -D voxvera /app/bin/voxvera
|
||||
- install -D org.voxvera.VoxVera.desktop /app/share/applications/org.voxvera.VoxVera.desktop
|
||||
- install -D org.voxvera.VoxVera.png /app/share/icons/hicolor/512x512/apps/org.voxvera.VoxVera.png
|
||||
sources:
|
||||
- type: file
|
||||
path: ../../voxvera/resources/bin/voxvera-linux-x86_64
|
||||
dest-filename: voxvera
|
||||
- type: file
|
||||
path: org.voxvera.VoxVera.desktop
|
||||
- type: file
|
||||
path: org.voxvera.VoxVera.png
|
||||
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.1.4"
|
||||
version = "0.3.0"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
@@ -26,9 +26,10 @@ dependencies = [
|
||||
"Pillow",
|
||||
"jsmin",
|
||||
"htmlmin2",
|
||||
"pypdf",
|
||||
"wcwidth",
|
||||
"setuptools",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest",
|
||||
|
||||
@@ -6,3 +6,7 @@ jsmin
|
||||
htmlmin2
|
||||
pypdf
|
||||
wcwidth
|
||||
onionshare-cli
|
||||
flask<2.3.0
|
||||
werkzeug<3.0.0
|
||||
|
||||
@@ -19,17 +19,24 @@ mkdir -p voxvera/resources/bin
|
||||
|
||||
echo "Building VoxVera binary for $(uname)..."
|
||||
|
||||
echo "Ensuring Tor binaries are downloaded..."
|
||||
bash scripts/download_tor.sh
|
||||
|
||||
# Use python module to call pyinstaller
|
||||
PY_CMD="python3 -m PyInstaller"
|
||||
|
||||
# Build the binary
|
||||
ARCH=$(uname -m)
|
||||
OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
BINARY_NAME="voxvera-$OS_NAME-$ARCH"
|
||||
|
||||
$PY_CMD --onefile \
|
||||
--add-data "voxvera/locales:voxvera/locales" \
|
||||
--add-data "voxvera/src:voxvera/src" \
|
||||
--add-data "voxvera/templates:voxvera/templates" \
|
||||
--add-data "voxvera/resources:voxvera/resources" \
|
||||
--add-data "requirements.txt:." \
|
||||
--name "voxvera-$(uname -s | tr '[:upper:]' '[:lower:]')" \
|
||||
--name "$BINARY_NAME" \
|
||||
--distpath voxvera/resources/bin \
|
||||
voxvera/cli.py
|
||||
|
||||
|
||||
@@ -1,42 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${VERSION:-13.5.18}"
|
||||
BASE_URL="https://www.torproject.org/dist/torbrowser"
|
||||
VERSION="${VERSION:-15.0.7}"
|
||||
BASE_URL="https://dist.torproject.org/torbrowser"
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux*) PLATFORM=linux; ARCHIVE="tor-expert-bundle-linux-x86_64-${VERSION}.tar.gz"; EXE=tor;;
|
||||
Darwin*) PLATFORM=mac; ARCHIVE="tor-expert-bundle-macos-x86_64-${VERSION}.tar.gz"; EXE=tor;;
|
||||
MINGW*|MSYS*|CYGWIN*) PLATFORM=win; ARCHIVE="tor-expert-bundle-windows-x86_64-${VERSION}.tar.gz"; EXE=tor.exe;;
|
||||
*) echo "Unsupported OS" >&2; exit 1;;
|
||||
esac
|
||||
# ARCH=$(uname -m) # Not needed for multi-platform download if we hardcode/loop targets
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
download_platform() {
|
||||
local platform=$1
|
||||
local arch=$2
|
||||
local archive=$3
|
||||
local exe=$4
|
||||
|
||||
local tmpdir=$(mktemp -d)
|
||||
# trap 'rm -rf "$tmpdir"' EXIT # We'll clean up manually in the loop
|
||||
|
||||
URL="$BASE_URL/${VERSION}/${ARCHIVE}"
|
||||
local url="$BASE_URL/${VERSION}/${archive}"
|
||||
local dest="$(dirname "$0")/../voxvera/resources/tor/$platform"
|
||||
|
||||
echo "--- Platform: $platform ($arch) ---"
|
||||
echo "Downloading $url"
|
||||
if ! curl -L "$url" -o "$tmpdir/$archive"; then
|
||||
echo "Failed to download $url" >&2
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Downloading $URL"
|
||||
curl -L "$URL" -o "$TMPDIR/$ARCHIVE"
|
||||
echo "Extracting..."
|
||||
if [[ "$archive" == *.zip ]]; then
|
||||
unzip -q "$tmpdir/$archive" -d "$tmpdir"
|
||||
else
|
||||
tar -xf "$tmpdir/$archive" -C "$tmpdir"
|
||||
fi
|
||||
|
||||
echo "Extracting..."
|
||||
tar -xf "$TMPDIR/$ARCHIVE" -C "$TMPDIR"
|
||||
mkdir -p "$dest"
|
||||
|
||||
DEST="$(dirname "$0")/../voxvera/resources/tor/$PLATFORM"
|
||||
mkdir -p "$DEST"
|
||||
local tor_bin=$(find "$tmpdir" -type f -name "$exe" | head -n 1)
|
||||
local obfs_bin=$(find "$tmpdir" -type f \( -name "lyrebird*" -o -name "obfs4proxy*" \) | head -n 1)
|
||||
|
||||
TOR_BIN=$(find "$TMPDIR" -type f -name "$EXE" | head -n 1)
|
||||
OBFS_BIN=$(find "$TMPDIR" -type f -name "obfs4proxy*" | head -n 1)
|
||||
if [[ -z "$tor_bin" || -z "$obfs_bin" ]]; then
|
||||
echo "Failed to locate tor or lyrebird/obfs4proxy in archive for $platform" >&2
|
||||
rm -rf "$tmpdir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "$TOR_BIN" || -z "$OBFS_BIN" ]]; then
|
||||
echo "Failed to locate tor or obfs4proxy in archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Installing binaries to $dest"
|
||||
cp "$tor_bin" "$dest/$(basename "$exe")"
|
||||
cp "$obfs_bin" "$dest/$(basename "$obfs_bin")"
|
||||
|
||||
cp "$TOR_BIN" "$DEST/$(basename "$EXE")"
|
||||
cp "$OBFS_BIN" "$DEST/$(basename "$OBFS_BIN")"
|
||||
chmod +x "$dest/$(basename "$exe")" "$dest/$(basename "$obfs_bin")"
|
||||
|
||||
# If it's lyrebird, create a symlink for obfs4proxy for compatibility
|
||||
if [[ "$(basename "$obfs_bin")" == "lyrebird"* ]]; then
|
||||
local obfs_name="obfs4proxy"
|
||||
[[ "$platform" == "win" ]] && obfs_name="obfs4proxy.exe"
|
||||
echo "Creating symlink $obfs_name -> $(basename "$obfs_bin")"
|
||||
(cd "$dest" && rm -f "$obfs_name" && ln -s "$(basename "$obfs_bin")" "$obfs_name")
|
||||
fi
|
||||
|
||||
chmod +x "$DEST/$(basename "$EXE")" "$DEST/$(basename "$OBFS_BIN")"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
echo "Installed binaries to $DEST"
|
||||
# Linux x86_64
|
||||
download_platform "linux" "x86_64" "tor-expert-bundle-linux-x86_64-${VERSION}.tar.gz" "tor"
|
||||
|
||||
# macOS x86_64 (Intel) - works on M1/M2 via Rosetta if needed, but expert bundle usually has universal or specific
|
||||
download_platform "mac" "x86_64" "tor-expert-bundle-macos-x86_64-${VERSION}.tar.gz" "tor"
|
||||
|
||||
# Windows x86_64
|
||||
download_platform "win" "x86_64" "tor-expert-bundle-windows-x86_64-${VERSION}.tar.gz" "tor.exe"
|
||||
|
||||
echo "All platforms updated."
|
||||
@@ -10,15 +10,29 @@ check_cmd() {
|
||||
}
|
||||
|
||||
# Ensure pip is available
|
||||
if ! check_cmd pip3 && ! check_cmd pip; then
|
||||
echo "pip not found. Install Python 3 and pip first."
|
||||
if ! check_cmd pip3 && ! check_cmd pip && ! check_cmd pipx; then
|
||||
echo "pip or pipx not found. Install Python 3 and pip/pipx first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PIP=$(check_cmd pip3 && echo pip3 || echo pip)
|
||||
|
||||
# Install Python packages used by the CLI and build pipeline
|
||||
$PIP install --user InquirerPy rich qrcode Pillow jsmin htmlmin2 pypdf
|
||||
if check_cmd pipx; then
|
||||
echo "Using pipx/pip for dependency installation..."
|
||||
# For development, we might want to install in a local venv instead of pipx
|
||||
# but for simple setup, pipx is safest on modern systems.
|
||||
# However, setup.sh usually implies installing into the current env for development.
|
||||
PIP_OPTS="--user --upgrade"
|
||||
if [ -f /etc/debian_version ]; then
|
||||
DEB_VER=$(cat /etc/debian_version | cut -d. -f1)
|
||||
if [[ "$DEB_VER" =~ ^[0-9]+$ ]] && [ "$DEB_VER" -ge 12 ]; then
|
||||
PIP_OPTS="$PIP_OPTS --break-system-packages"
|
||||
fi
|
||||
fi
|
||||
pip3 install $PIP_OPTS InquirerPy rich qrcode Pillow jsmin htmlmin2 pypdf wcwidth || \
|
||||
pip install $PIP_OPTS InquirerPy rich qrcode Pillow jsmin htmlmin2 pypdf wcwidth
|
||||
else
|
||||
PIP=$(check_cmd pip3 && echo pip3 || echo pip)
|
||||
$PIP install --user --upgrade InquirerPy rich qrcode Pillow jsmin htmlmin2 pypdf wcwidth
|
||||
fi
|
||||
|
||||
echo "All dependencies are installed."
|
||||
echo "Run 'voxvera check' to verify your setup."
|
||||
@@ -378,7 +378,7 @@
|
||||
</style>
|
||||
<script>
|
||||
// VoxVera Multi-Language Engine
|
||||
const locales = {"ar": {"meta": {"language_name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "flag": "\ud83c\uddf8\ud83c\udde6", "direction": "rtl"}, "web": {"tear_off_tor_browser": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u062a\u0635\u0641\u062d Tor", "tear_off_desc": "\u0644\u0644\u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0646\u0634\u0631 \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629\u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649", "tear_off_footer": "\u0627\u0646\u0642\u0631 \u0641\u0648\u0642 \u0627\u0644\u0632\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0644\u0637\u0628\u0627\u0639\u0629", "built_with": "\u0628\u0646\u064a \u0628\u0648\u0627\u0633\u0637\u0629", "print_button": "\u0627\u0637\u0628\u0639 \u0627\u0644\u0645\u0646\u0634\u0648\u0631", "download_button": "\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0623\u062f\u0627\u0629 \u0648\u0627\u0644\u0643\u0648\u062f", "dl_portable": "Portable (Source + Dependencies)", "dl_linux": "Linux Standalone", "dl_windows": "Windows Standalone (.exe)", "dl_macos": "macOS Standalone", "dl_source": "Source Code Only (.zip)"}, "landing": {"title": "\u0633\u0631\u064a \u0644\u0644\u063a\u0627\u064a\u0629", "subtitle": "~~\u0644\u0627~~ \u0644\u0644\u0646\u0634\u0631", "headline": "\u0639\u0645\u0644\u064a\u0629 VOX VERA", "content": "\u062a\u062d\u0631\u0631 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629 \u0645\u0639 VoxVera. \u0623\u062f\u0627\u0629 \u062a\u0633\u0648\u064a\u0642 \u0639\u0635\u0627\u0628\u0627\u062a \u0645\u062c\u0647\u0648\u0644\u0629 \u0648\u0646\u0634\u0631 \u0631\u0633\u0627\u0626\u0644. \u0633\u0648\u0627\u0621 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0623\u0648 \u0641\u064a \u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0645\u0627\u062f\u064a\u060c \u064a\u0645\u0643\u0646\u0647\u0627 \u062a\u0645\u0643\u064a\u0646\u0643 \u0645\u0646 \u0646\u0634\u0631 \u0623\u0641\u0643\u0627\u0631\u0643 \u0628\u062c\u0631\u0623\u0629\u060c \u0645\u062d\u0645\u064a\u0629 \u0628\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0647\u0648\u064a\u0629 \u0627\u0644\u0643\u0627\u0645\u0644 (\u0625\u0630\u0627 \u0643\u0646\u062a \u062a\u0633\u062a\u0636\u064a\u0641 \u0639\u0628\u0631 Tor). \u0642\u0645 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0643\u0648\u062f\u060c \u0648\u0635\u0645\u0645 \u0645\u0648\u0642\u0639 \u0645\u0646\u0634\u0648\u0631\u060c \u0648\u0627\u0633\u062a\u0636\u064a\u0641\u0647 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0648\u0642\u0645 \u0628\u062a\u0636\u062e\u064a\u0645 \u0631\u0633\u0627\u0644\u062a\u0643.\n\n\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u0648\u0629 \u0627\u0644\u0645\u064a\u0645\u064a\u0629 \u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0623\u0641\u0643\u0627\u0631\u0643 \u0641\u064a \u0645\u062f\u0631\u0633\u062a\u0643\u060c \u0623\u0648 \u0645\u0643\u0627\u0646 \u0639\u0645\u0644\u0643\u060c \u0623\u0648 \u0645\u062c\u062a\u0645\u0639\u0627\u062a\u0643 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a\u060c \u0623\u0648 \u062d\u062a\u0649 \u0639\u0644\u0649 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0639\u0627\u0644\u0645. \u062a\u0636\u0645\u0646 VoxVera \u062a\u0631\u062f\u062f \u0635\u062f\u0649 \u0631\u0633\u0627\u0644\u062a\u0643 \u0628\u0639\u064a\u062f\u0627\u064b \u0648\u0639\u0644\u0649 \u0646\u0637\u0627\u0642 \u0648\u0627\u0633\u0639\u060c \u0645\u0639 \u0623\u0642\u0633\u0627\u0645 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u0646\u0632\u0639 \u062a\u062a\u0645\u064a\u0632 \u0628\u0631\u0648\u0627\u0628\u0637 \u0641\u0631\u064a\u062f\u0629 \u0648\u0623\u0643\u0648\u0627\u062f QR \u0644\u0633\u0647\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u0637\u0628\u0627\u0639\u0629.\n\n\u064a\u0645\u0643\u0646 \u0627\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629. \u064a\u0645\u0643\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0646\u0634\u0648\u0631\u0627\u062a \u0639\u0628\u0631 \u0634\u0628\u0643\u0629 Tor\u060c \u0645\u0645\u0627 \u064a\u062d\u0645\u064a \u0627\u0644\u0645\u0636\u064a\u0641 \u0648\u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629.\n\n\u0627\u0646\u0636\u0645 \u0625\u0644\u064a\u0646\u0627 \u0641\u064a \u062b\u0648\u0631\u0629 \u062a\u0642\u062f\u0631 \u0627\u0644\u062d\u0642\u064a\u0642\u0629 \u0648\u0627\u0644\u0634\u0641\u0627\u0641\u064a\u0629. \u0645\u0639\u0627\u064b\u060c \u064a\u0645\u0643\u0646\u0646\u0627 \u0628\u0646\u0627\u0621 \u0634\u0628\u0643\u0629 \u0645\u0646 \u0627\u0644\u0645\u0648\u0627\u0637\u0646\u064a\u0646 \u0627\u0644\u0645\u0633\u062a\u0646\u064a\u0631\u064a\u0646 \u0627\u0644\u0630\u064a\u0646 \u0644\u0627 \u064a\u062e\u0634\u0648\u0646 \u0627\u0644\u062a\u062d\u062f\u062b.", "url_message": "\u0627\u062d\u0635\u0644 \u0639\u0644\u0649 VoxVera \u0648\u0627\u0628\u062f\u062Line truncated
|
||||
const locales = {"ar": {"meta": {"language_name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "flag": "\ud83c\uddf8\ud83c\udde6", "direction": "rtl"}, "web": {"tear_off_tor_browser": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u062a\u0635\u0641\u062d Tor", "tear_off_desc": "\u0644\u0644\u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0646\u0634\u0631 \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629\u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649", "tear_off_footer": "\u0627\u0646\u0642\u0631 \u0641\u0648\u0642 \u0627\u0644\u0632\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0644\u0637\u0628\u0627\u0639\u0629", "built_with": "\u0628\u0646\u064a \u0628\u0648\u0627\u0633\u0637\u0629", "print_button": "\u0627\u0637\u0628\u0639 \u0627\u0644\u0645\u0646\u0634\u0648\u0631", "download_button": "\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0623\u062f\u0627\u0629 \u0648\u0627\u0644\u0643\u0648\u062f"}, "landing": {"title": "\u0633\u0631\u064a \u0644\u0644\u063a\u0627\u064a\u0629", "subtitle": "~~\u0644\u0627~~ \u0644\u0644\u0646\u0634\u0631", "headline": "\u0639\u0645\u0644\u064a\u0629 VOX VERA", "content": "\u062a\u062d\u0631\u0631 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629 \u0645\u0639 VoxVera. \u0623\u062f\u0627\u0629 \u062a\u0633\u0648\u064a\u0642 \u0639\u0635\u0627\u0628\u0627\u062a \u0645\u062c\u0647\u0648\u0644\u0629 \u0648\u0646\u0634\u0631 \u0631\u0633\u0627\u0626\u0644. \u0633\u0648\u0627\u0621 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0623\u0648 \u0641\u064a \u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0645\u0627\u062f\u064a\u060c \u064a\u0645\u0643\u0646\u0647\u0627 \u062a\u0645\u0643\u064a\u0646\u0643 \u0645\u0646 \u0646\u0634\u0631 \u0623\u0641\u0643\u0627\u0631\u0643 \u0628\u062c\u0631\u0623\u0629\u060c \u0645\u062d\u0645\u064a\u0629 \u0628\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0647\u0648\u064a\u0629 \u0627\u0644\u0643\u0627\u0645\u0644 (\u0625\u0630\u0627 \u0643\u0646\u062a \u062a\u0633\u062a\u0636\u064a\u0641 \u0639\u0628\u0631 Tor). \u0642\u0645 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0643\u0648\u062f\u060c \u0648\u0635\u0645\u0645 \u0645\u0648\u0642\u0639 \u0645\u0646\u0634\u0648\u0631\u060c \u0648\u0627\u0633\u062a\u0636\u064a\u0641\u0647 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0648\u0642\u0645 \u0628\u062a\u0636\u062e\u064a\u0645 \u0631\u0633\u0627\u0644\u062a\u0643.\n\n\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u0648\u0629 \u0627\u0644\u0645\u064a\u0645\u064a\u0629 \u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0623\u0641\u0643\u0627\u0631\u0643 \u0641\u064a \u0645\u062f\u0631\u0633\u062a\u0643\u060c \u0623\u0648 \u0645\u0643\u0627\u0646 \u0639\u0645\u0644\u0643\u060c \u0623\u0648 \u0645\u062c\u062a\u0645\u0639\u0627\u062a\u0643 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a\u060c \u0623\u0648 \u062d\u062a\u0649 \u0639\u0644\u0649 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0639\u0627\u0644\u0645. \u062a\u0636\u0645\u0646 VoxVera \u062a\u0631\u062f\u062f \u0635\u062f\u0649 \u0631\u0633\u0627\u0644\u062a\u0643 \u0628\u0639\u064a\u062f\u0627\u064b \u0648\u0639\u0644\u0649 \u0646\u0637\u0627\u0642 \u0648\u0627\u0633\u0639\u060c \u0645\u0639 \u0623\u0642\u0633\u0627\u0645 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u0646\u0632\u0639 \u062a\u062a\u0645\u064a\u0632 \u0628\u0631\u0648\u0627\u0628\u0637 \u0641\u0631\u064a\u062f\u0629 \u0648\u0623\u0643\u0648\u0627\u062f QR \u0644\u0633\u0647\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u0637\u0628\u0627\u0639\u0629.\n\n\u064a\u0645\u0643\u0646 \u0627\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629. \u064a\u0645\u0643\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0646\u0634\u0648\u0631\u0627\u062a \u0639\u0628\u0631 \u0634\u0628\u0643\u0629 Tor\u060c \u0645\u0645\u0627 \u064a\u062d\u0645\u064a \u0627\u0644\u0645\u0636\u064a\u0641 \u0648\u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629.\n\n\u0627\u0646\u0636\u0645 \u0625\u0644\u064a\u0646\u0627 \u0641\u064a \u062b\u0648\u0631\u0629 \u062a\u0642\u062f\u0631 \u0627\u0644\u062d\u0642\u064a\u0642\u0629 \u0648\u0627\u0644\u0634\u0641\u0627\u0641\u064a\u0629. \u0645\u0639\u0627\u064b\u060c \u064a\u0645\u0643\u0646\u0646\u0627 \u0628\u0646\u0627\u0621 \u0634\u0628\u0643\u0629 \u0645\u0646 \u0627\u0644\u0645\u0648\u0627\u0637\u0646\u064a\u0646 \u0627\u0644\u0645\u0633\u062a\u0646\u064a\u0631\u064a\u0646 \u0627\u0644\u0630\u064a\u0646 \u0644\u0627 \u064a\u062e\u0634\u0648\u0646 \u0627\u0644\u062a\u062d\u062f\u062b.", "url_message": "\u0627\u062d\u0635\u0644 \u0639\u0644\u0649 VoxVera \u0648\u0627\u0628\u062f\u0623 \u0628\u0646\u0634\u0631 \u0631\u0633\u0627\u0644\u062a\u0643.", "name": "Vox Vera Printable Flyers"}}, "de": {"meta": {"language_name": "Deutsch", "flag": "\ud83c\udde9\ud83c\uddea"}, "web": {"teLine truncated
|
||||
|
||||
function t(path, lang) {
|
||||
const parts = path.split('.');
|
||||
@@ -557,13 +557,14 @@
|
||||
<h1 id="operation-title-2" data-i18n="landing.headline">OPERATION VOX VERA</h1>
|
||||
<hr>
|
||||
<div class="message">
|
||||
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message.
|
||||
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message.
|
||||
|
||||
Use memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting.
|
||||
Use memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting.
|
||||
|
||||
Privacy can be maintained. Flyers can be shared via the Tor network, protecting hoster and users from censorship.
|
||||
|
||||
Join us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.</div>
|
||||
Join us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.
|
||||
</div>
|
||||
<div class="qr-code-body">
|
||||
|
||||
<div class="qr-code-url">
|
||||
@@ -582,16 +583,9 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">Print this page</button>
|
||||
<div class="dropdown">
|
||||
<button class="download-button" data-i18n="web.download_button">Download</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="download/voxvera-portable.zip" download data-i18n="web.dl_portable">Portable (Source + Dependencies)</a>
|
||||
<a href="download/voxvera-linux" download data-i18n="web.dl_linux">Linux Standalone</a>
|
||||
<a href="download/voxvera-windows.exe" download data-i18n="web.dl_windows">Windows Standalone (.exe)</a>
|
||||
<a href="download/voxvera-macos" download data-i18n="web.dl_macos">macOS Standalone</a>
|
||||
<a href="download/voxvera-source.zip" download data-i18n="web.dl_source">Source Code Only (.zip)</a>
|
||||
</div>
|
||||
</div>
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">Download Source Code</button>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
import setuptools
|
||||
print("pkg_resources available")
|
||||
except ImportError as e:
|
||||
print(f"Failed to import: {e}")
|
||||
|
||||
from voxvera import cli
|
||||
cli._internal_onionshare()
|
||||
@@ -0,0 +1,2 @@
|
||||
import pkg_resources
|
||||
print("Success")
|
||||
@@ -0,0 +1,2 @@
|
||||
import pkg_resources
|
||||
print("Success")
|
||||
@@ -14,11 +14,26 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")
|
||||
|
||||
def _setup_tmp(monkeypatch, tmp_path):
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", tmp_path / "locales")
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
|
||||
# Use a unique subdirectory for this test run to ensure isolation
|
||||
test_data_dir = tmp_path / "data"
|
||||
test_data_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Internal resources (ROOT)
|
||||
res_root = test_data_dir / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", test_data_dir)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
# Create a default config.json in DATA_DIR
|
||||
shutil.copy(res_root / "src" / "config.json", test_data_dir / "config.json")
|
||||
return repo_root
|
||||
|
||||
|
||||
@@ -31,10 +46,11 @@ def test_help(capsys):
|
||||
|
||||
def test_init_template(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
test_data_dir = tmp_path / "data"
|
||||
# Mock choose_language to return 'en'
|
||||
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
|
||||
cli.main(["init", "--template", "voxvera"])
|
||||
dest = tmp_path / "host" / "voxvera"
|
||||
dest = test_data_dir / "host" / "voxvera"
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -43,7 +59,8 @@ def test_init_template(tmp_path, monkeypatch):
|
||||
def test_build(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
dest = tmp_path / "host" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / "voxvera"
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -54,14 +71,16 @@ def test_build_with_download(tmp_path, monkeypatch):
|
||||
download_file = tmp_path / "sample.zip"
|
||||
download_file.write_text("dummy")
|
||||
cli.main(["build", "--download", str(download_file)])
|
||||
dest = tmp_path / "host" / "voxvera" / "download" / "extra-content.zip"
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / "voxvera" / "download" / "extra-content.zip"
|
||||
assert dest.is_file()
|
||||
|
||||
|
||||
def test_import(tmp_path, monkeypatch):
|
||||
repo = _setup_tmp(monkeypatch, tmp_path)
|
||||
imports_dir = tmp_path / "imports"
|
||||
imports_dir.mkdir()
|
||||
test_data_dir = tmp_path / "data"
|
||||
imports_dir = test_data_dir / "imports"
|
||||
imports_dir.mkdir(parents=True, exist_ok=True)
|
||||
base_data = json.load(open(repo / "imports" / "example.json"))
|
||||
for sub in ["foo", "bar"]:
|
||||
data = dict(base_data)
|
||||
@@ -70,7 +89,8 @@ def test_import(tmp_path, monkeypatch):
|
||||
json.dump(data, fh)
|
||||
cli.main(["batch-import"])
|
||||
for sub in ["foo", "bar"]:
|
||||
dest = tmp_path / "host" / sub
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / sub
|
||||
assert dest.is_dir()
|
||||
assert (dest / "index.html").exists()
|
||||
|
||||
@@ -78,8 +98,9 @@ def test_import(tmp_path, monkeypatch):
|
||||
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()
|
||||
test_data_dir = tmp_path / "data"
|
||||
imports_dir = test_data_dir / "imports"
|
||||
imports_dir.mkdir(parents=True, exist_ok=True)
|
||||
base_data = json.load(open(repo / "imports" / "example.json"))
|
||||
base_data["folder_name"] = "keep"
|
||||
with open(imports_dir / "keep.json", "w") as fh:
|
||||
@@ -87,7 +108,8 @@ def test_import_preserves_session(tmp_path, monkeypatch):
|
||||
|
||||
# first import creates the host dir
|
||||
cli.main(["batch-import"])
|
||||
host_dir = tmp_path / "host" / "keep"
|
||||
test_data_dir = tmp_path / "data"
|
||||
host_dir = test_data_dir / "host" / "keep"
|
||||
assert host_dir.is_dir()
|
||||
|
||||
# simulate OnionShare having written a session key
|
||||
@@ -112,10 +134,15 @@ def test_check_all_present(capsys, monkeypatch):
|
||||
|
||||
|
||||
def test_check_missing(capsys, monkeypatch):
|
||||
def fake_which(cmd):
|
||||
return None if cmd == "onionshare-cli" else "/usr/bin/" + cmd
|
||||
import builtins
|
||||
original_import = builtins.__import__
|
||||
|
||||
monkeypatch.setattr(shutil, "which", fake_which)
|
||||
def fake_import(name, *args, **kwargs):
|
||||
if name == "onionshare_cli":
|
||||
raise ImportError("Mocked missing")
|
||||
return original_import(name, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(builtins, "__import__", fake_import)
|
||||
cli.main(["check"])
|
||||
captured = capsys.readouterr()
|
||||
assert "onionshare-cli" in captured.out
|
||||
@@ -124,13 +151,15 @@ def test_check_missing(capsys, monkeypatch):
|
||||
|
||||
def test_build_download_zip(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
folder_name = config["folder_name"]
|
||||
zip_path = tmp_path / "file.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as zf:
|
||||
zf.writestr("dummy.txt", "data")
|
||||
cli.main(["build", "--download", str(zip_path)])
|
||||
dest = tmp_path / "host" / folder_name / "download" / "extra-content.zip"
|
||||
dest = test_data_dir / "host" / folder_name / "download" / "extra-content.zip"
|
||||
assert dest.is_file()
|
||||
|
||||
|
||||
@@ -138,9 +167,11 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
"""Test that serve() auto-generates onion URL for tear-off links."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
cli.main(["build"])
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
folder_name = config["folder_name"]
|
||||
dir_path = tmp_path / "host" / folder_name
|
||||
dir_path = test_data_dir / "host" / folder_name
|
||||
|
||||
# Set a custom URL that should be preserved (content link)
|
||||
config["url"] = "https://example.com/external-resource"
|
||||
@@ -163,7 +194,7 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
onion_url = "http://test123.onion"
|
||||
|
||||
class FakePopen:
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None):
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None, **kwargs):
|
||||
self.cmd = cmd
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
@@ -192,7 +223,9 @@ def test_serve_updates_url(tmp_path, monkeypatch):
|
||||
def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
"""Test quickstart auto-generates onion URL for tear-off links."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
folder_name = config["folder_name"]
|
||||
|
||||
# No need for TOR_* env vars anymore - auto-detection should work
|
||||
@@ -211,7 +244,7 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
onion_url = "http://quick.onion"
|
||||
|
||||
class FakePopen:
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None):
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None, **kwargs):
|
||||
self.cmd = cmd
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
@@ -227,9 +260,12 @@ def test_quickstart_noninteractive(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(cli.subprocess, "Popen", FakePopen)
|
||||
|
||||
monkeypatch.setattr(cli, "choose_language", lambda *a: "en")
|
||||
config_path = test_data_dir / "config.json"
|
||||
cli.main(["quickstart", "--non-interactive"])
|
||||
# quickstart no longer calls serve, so we call it manually to test URL update
|
||||
cli.serve(str(config_path))
|
||||
|
||||
dir_path = tmp_path / "host" / folder_name
|
||||
dir_path = test_data_dir / "host" / folder_name
|
||||
assert (dir_path / "index.html").exists()
|
||||
log_file = dir_path / "onionshare.log"
|
||||
assert onion_url in log_file.read_text()
|
||||
@@ -280,22 +316,23 @@ def test_build_without_url_skips_qr(tmp_path, monkeypatch, capsys):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Remove URLs from config
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = ""
|
||||
config["tear_off_link"] = ""
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
# Remove any existing QR files from src to test fresh generation
|
||||
# Remove any existing QR files from src and host to test fresh generation
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_path = tmp_path / "src" / qr_file
|
||||
if qr_path.exists():
|
||||
qr_path.unlink()
|
||||
|
||||
(res_root / "src" / qr_file).unlink(missing_ok=True)
|
||||
(test_data_dir / "host" / config["folder_name"] / qr_file).unlink(missing_ok=True)
|
||||
# Build should complete without error, just skip QR generation
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
assert dest.is_dir()
|
||||
assert (dest / "config.json").exists()
|
||||
assert (dest / "index.html").exists()
|
||||
@@ -309,21 +346,22 @@ def test_build_with_only_url(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Set only url (content link)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = "https://example.com/external-resource"
|
||||
config["tear_off_link"] = ""
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
# Remove any existing QR files from src to test fresh generation
|
||||
# Remove any existing QR files from src and host to test fresh generation
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_path = tmp_path / "src" / qr_file
|
||||
if qr_path.exists():
|
||||
qr_path.unlink()
|
||||
|
||||
(res_root / "src" / qr_file).unlink(missing_ok=True)
|
||||
(test_data_dir / "host" / config["folder_name"] / qr_file).unlink(missing_ok=True)
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
# Only content QR should exist (tear-off will be set by serve)
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert not (dest / "qrcode-tear-offs.png").exists()
|
||||
@@ -334,21 +372,22 @@ def test_build_with_only_tear_off_link(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Set only tear_off_link (before serve sets it to onion)
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = ""
|
||||
config["tear_off_link"] = "http://preconfigured.onion"
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
# Remove any existing QR files from src to test fresh generation
|
||||
# Remove any existing QR files from src and host to test fresh generation
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_path = tmp_path / "src" / qr_file
|
||||
if qr_path.exists():
|
||||
qr_path.unlink()
|
||||
|
||||
(res_root / "src" / qr_file).unlink(missing_ok=True)
|
||||
(test_data_dir / "host" / config["folder_name"] / qr_file).unlink(missing_ok=True)
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
# Only tear-off QR should exist (will be updated by serve)
|
||||
assert not (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
@@ -362,7 +401,7 @@ def test_serve_passes_tor_ports_to_env(monkeypatch, tmp_path):
|
||||
captured_env = {}
|
||||
|
||||
class FakePopen:
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None):
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None, **kwargs):
|
||||
self.cmd = cmd
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
@@ -395,15 +434,18 @@ def test_backward_compatibility_with_https_config(tmp_path, monkeypatch):
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
# Simulate old config with HTTPS URLs
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
config["url"] = "https://example.com/old-style"
|
||||
config["tear_off_link"] = "https://example.com/tear-off"
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
|
||||
@@ -412,15 +454,18 @@ def test_long_urls_in_qr(tmp_path, monkeypatch):
|
||||
"""Test that long URLs still generate valid QR codes."""
|
||||
_setup_tmp(monkeypatch, tmp_path)
|
||||
|
||||
config = json.load(open(tmp_path / "src" / "config.json"))
|
||||
res_root = tmp_path / "data" / "voxvera"
|
||||
test_data_dir = tmp_path / "data"
|
||||
config = json.load(open(res_root / "src" / "config.json"))
|
||||
# Very long URL (close to 200 char limit)
|
||||
config["url"] = "http://" + "a" * 190 + ".onion"
|
||||
config["tear_off_link"] = "https://example.com/" + "path/" * 20
|
||||
with open(tmp_path / "src" / "config.json", "w") as f:
|
||||
with open(test_data_dir / "config.json", "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
cli.main(["build"])
|
||||
|
||||
dest = tmp_path / "host" / config["folder_name"]
|
||||
test_data_dir = tmp_path / "data"
|
||||
dest = test_data_dir / "host" / config["folder_name"]
|
||||
assert (dest / "qrcode-content.png").exists()
|
||||
assert (dest / "qrcode-tear-offs.png").exists()
|
||||
@@ -15,6 +15,7 @@ Requirements:
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
@@ -46,7 +47,11 @@ def check_tor_running():
|
||||
|
||||
def check_onionshare_cli():
|
||||
"""Check if onionshare-cli is installed."""
|
||||
return shutil.which("onionshare-cli") is not None
|
||||
try:
|
||||
import onionshare_cli # noqa: F401
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@@ -102,11 +107,18 @@ class TestFullWorkflow:
|
||||
|
||||
# Setup temp workspace
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
shutil.copytree(repo_root / "voxvera" / "src", tmp_path / "src")
|
||||
# Internal resources (ROOT)
|
||||
res_root = tmp_path / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
# Monkeypatch ROOT to use temp directory
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
# Create test config
|
||||
config = {
|
||||
@@ -122,7 +134,7 @@ class TestFullWorkflow:
|
||||
"binary_message": "0101010",
|
||||
}
|
||||
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path = tmp_path / "config.json"
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
@@ -141,7 +153,7 @@ class TestFullWorkflow:
|
||||
onion_url = "http://test123abc.onion"
|
||||
|
||||
class MockPopen:
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None):
|
||||
def __init__(self, cmd, stdout=None, stderr=None, env=None, **kwargs):
|
||||
self.cmd = cmd
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
@@ -196,9 +208,17 @@ class TestFullWorkflow:
|
||||
|
||||
# Setup
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
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))
|
||||
res_root = tmp_path / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
config = {
|
||||
"name": "Reachability Test",
|
||||
@@ -213,7 +233,7 @@ class TestFullWorkflow:
|
||||
"binary_message": "101010",
|
||||
}
|
||||
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path = tmp_path / "config.json"
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
@@ -222,12 +242,21 @@ class TestFullWorkflow:
|
||||
host_dir = tmp_path / "host" / "reachtest"
|
||||
assert host_dir.is_dir(), f"Build did not create {host_dir}"
|
||||
|
||||
# Launch onionshare-cli directly against the built host directory
|
||||
# Launch onionshare-cli directly against the built host directory using our internal wrapper
|
||||
logfile = host_dir / "onionshare.log"
|
||||
log_fh = open(logfile, "w")
|
||||
|
||||
# Pass Tor environment variables to ensure it uses the running Tor
|
||||
env = os.environ.copy()
|
||||
tor_port = check_tor_running()
|
||||
if tor_port:
|
||||
env["TOR_SOCKS_PORT"] = str(tor_port)
|
||||
env["TOR_CONTROL_PORT"] = str(tor_port + 1)
|
||||
|
||||
cmd = [
|
||||
"onionshare-cli",
|
||||
sys.executable,
|
||||
"-m", "voxvera.cli",
|
||||
"_internal_onionshare",
|
||||
"--website",
|
||||
"--public",
|
||||
"--persistent",
|
||||
@@ -235,7 +264,7 @@ class TestFullWorkflow:
|
||||
"-v",
|
||||
str(host_dir),
|
||||
]
|
||||
proc = subprocess.Popen(cmd, stdout=log_fh, stderr=subprocess.STDOUT)
|
||||
proc = subprocess.Popen(cmd, stdout=log_fh, stderr=subprocess.STDOUT, env=env)
|
||||
|
||||
try:
|
||||
# Wait for onion URL to appear in the log
|
||||
@@ -314,9 +343,17 @@ class TestSiteFiles:
|
||||
|
||||
# Setup
|
||||
repo_root = Path(__file__).resolve().parent.parent
|
||||
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))
|
||||
res_root = tmp_path / "voxvera"
|
||||
res_root.mkdir()
|
||||
shutil.copytree(repo_root / "voxvera" / "src", res_root / "src")
|
||||
shutil.copytree(repo_root / "voxvera" / "locales", res_root / "locales")
|
||||
shutil.copytree(repo_root / "voxvera" / "templates", res_root / "templates")
|
||||
|
||||
monkeypatch.setattr(cli, "ROOT", res_root)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: res_root / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: res_root / "locales" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_template_res", lambda *p: res_root / "templates" / Path(*p))
|
||||
|
||||
config = {
|
||||
"name": "Test",
|
||||
@@ -331,7 +368,7 @@ class TestSiteFiles:
|
||||
"binary_message": "",
|
||||
}
|
||||
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path = tmp_path / "config.json"
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
@@ -361,8 +398,8 @@ class TestErrorHandling:
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
|
||||
# Try to serve without building first
|
||||
config_path = tmp_path / "src" / "config.json"
|
||||
config_path.parent.mkdir(parents=True)
|
||||
config_path = tmp_path / "config.json"
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -385,11 +422,13 @@ class TestErrorHandling:
|
||||
class TestTorIntegration:
|
||||
"""Tests for Tor integration."""
|
||||
|
||||
def test_tor_auto_detection_falls_back_to_defaults(self, monkeypatch):
|
||||
def test_tor_auto_detection_falls_back_to_defaults(self, monkeypatch, tmp_path):
|
||||
"""Test that Tor port detection falls back to defaults when Tor isn't running."""
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from voxvera import cli
|
||||
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
|
||||
# Clear environment variables
|
||||
monkeypatch.delenv("TOR_SOCKS_PORT", raising=False)
|
||||
monkeypatch.delenv("TOR_CONTROL_PORT", raising=False)
|
||||
@@ -418,11 +457,13 @@ class TestTorIntegration:
|
||||
assert socks == "9050"
|
||||
assert ctl == "9051"
|
||||
|
||||
def test_tor_env_vars_override_detection(self, monkeypatch):
|
||||
def test_tor_env_vars_override_detection(self, monkeypatch, tmp_path):
|
||||
"""Test that environment variables override auto-detection."""
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from voxvera import cli
|
||||
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
|
||||
monkeypatch.setenv("TOR_SOCKS_PORT", "9999")
|
||||
monkeypatch.setenv("TOR_CONTROL_PORT", "8888")
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ def test_export_import_roundtrip(tmp_path, monkeypatch):
|
||||
shutil.copytree(REPO_ROOT / "voxvera" / "src", tmp_path / "src")
|
||||
shutil.copytree(REPO_ROOT / "voxvera" / "locales", tmp_path / "locales")
|
||||
monkeypatch.setattr(cli, "ROOT", tmp_path)
|
||||
monkeypatch.setattr(cli, "DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(cli, "_src_res", lambda *p: tmp_path / "src" / Path(*p))
|
||||
monkeypatch.setattr(cli, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
|
||||
@@ -115,7 +116,11 @@ def test_portable_bundle_integrity(tmp_path, monkeypatch):
|
||||
shutil.copytree(REPO_ROOT / "voxvera" / "locales", voxvera_dir / "locales")
|
||||
os.makedirs(voxvera_dir / "vendor", exist_ok=True)
|
||||
(voxvera_dir / "vendor" / "anchor.txt").write_text("anchor")
|
||||
(project_root / "voxvera-run.sh").write_text("echo run")
|
||||
|
||||
# Copy real scripts from repo
|
||||
for script in ["voxvera-run.sh", "uninstall.sh", "uninstall.ps1", "install.sh", "install.ps1", "setup.sh"]:
|
||||
if (REPO_ROOT / script).exists():
|
||||
shutil.copy(REPO_ROOT / script, project_root / script)
|
||||
|
||||
# ROOT in cli.py points to the voxvera/ directory
|
||||
monkeypatch.setattr(cli, "ROOT", voxvera_dir)
|
||||
@@ -133,7 +138,9 @@ def test_portable_bundle_integrity(tmp_path, monkeypatch):
|
||||
assert any("voxvera/locales/en.json" in n for n in names)
|
||||
# Verify vendored libs
|
||||
assert any("voxvera/vendor/anchor.txt" in n for n in names)
|
||||
# Verify launchers
|
||||
assert "voxvera.bat" in names
|
||||
# Verify uninstallers
|
||||
assert "uninstall.sh" in names
|
||||
assert "uninstall.ps1" in names
|
||||
|
||||
# Verify root scripts
|
||||
assert "voxvera-run.sh" in names
|
||||
@@ -0,0 +1,22 @@
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
Write-Host "Uninstalling VoxVera..."
|
||||
|
||||
# 1. Uninstall via pipx
|
||||
if (Get-Command pipx -ErrorAction SilentlyContinue) {
|
||||
pipx uninstall voxvera | Out-Null
|
||||
}
|
||||
|
||||
# 2. Uninstall via pip
|
||||
if (Get-Command pip -ErrorAction SilentlyContinue) {
|
||||
pip uninstall -y voxvera | Out-Null
|
||||
}
|
||||
|
||||
# 3. Remove manual binary
|
||||
$home = [Environment]::GetFolderPath('UserProfile')
|
||||
$exePath = Join-Path $home '.local\bin\voxvera.exe'
|
||||
if (Test-Path $exePath) {
|
||||
Remove-Item $exePath -Force
|
||||
}
|
||||
|
||||
Write-Host "`nVoxVera has been uninstalled."
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Uninstalling VoxVera..."
|
||||
|
||||
# 1. Uninstall via pipx if present
|
||||
if command -v pipx >/dev/null 2>&1; then
|
||||
if pipx list | grep -q "package voxvera"; then
|
||||
echo "Removing voxvera via pipx..."
|
||||
pipx uninstall voxvera
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Uninstall via pip if present
|
||||
if command -v pip >/dev/null 2>&1; then
|
||||
echo "Attempting to remove voxvera via pip..."
|
||||
pip uninstall -y voxvera >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# 3. Remove manual binary installation
|
||||
BINARY_PATH="$HOME/.local/bin/voxvera"
|
||||
if [ -f "$BINARY_PATH" ]; then
|
||||
echo "Removing manual binary at $BINARY_PATH..."
|
||||
rm "$BINARY_PATH"
|
||||
fi
|
||||
|
||||
# 4. Remove configuration and host data (Optional - ask user)
|
||||
# We will leave the host data by default to prevent data loss,
|
||||
# but we can remove the internal src config.
|
||||
# ROOT_DIR="$(dirname "$(readlink -f "$0")")" # This might not be reliable if run via curl
|
||||
|
||||
echo ""
|
||||
echo "VoxVera has been uninstalled."
|
||||
echo "Note: Your generated sites in ~/voxvera-exports or within the source folder were not removed."
|
||||
@@ -2,8 +2,6 @@
|
||||
# --------------------------------------------------------------------
|
||||
# VoxVera all-in-one installer (Linux) – run once as a normal user.
|
||||
# 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
|
||||
|
||||
@@ -14,46 +12,116 @@ msg() { printf "\e[32m%s\e[0m\n" "$*"; }
|
||||
warn() { printf "\e[33m%s\e[0m\n" "$*" >&2; }
|
||||
die() { printf "\e[31mERROR: %s\e[0m\n" "$*" >&2; exit 1; }
|
||||
|
||||
# Detect sudo/root
|
||||
if command_exists sudo; then
|
||||
SUDO="sudo"
|
||||
elif [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO=""
|
||||
else
|
||||
die "This script requires root privileges. Please install sudo or run as root."
|
||||
fi
|
||||
|
||||
## -------- determine package manager --------------------------------
|
||||
if command_exists apt-get; then PM=apt ; UPDATE="sudo apt-get update" ; INSTALL="sudo apt-get install -y"
|
||||
elif command_exists dnf; then PM=dnf ; UPDATE="sudo dnf -y makecache" ; INSTALL="sudo dnf install -y"
|
||||
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"
|
||||
if command_exists apt-get; then PM=apt ; UPDATE="$SUDO apt-get update" ; INSTALL="$SUDO apt-get install -y"
|
||||
elif command_exists dnf; then PM=dnf ; UPDATE="$SUDO dnf -y makecache" ; INSTALL="$SUDO dnf install -y"
|
||||
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."
|
||||
fi
|
||||
|
||||
msg ">> Detected package manager: $PM"
|
||||
|
||||
## -------- install system packages ----------------------------------
|
||||
# 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)
|
||||
|
||||
msg ">> Installing system dependencies..."
|
||||
SYSTEM_PKGS=(tor curl git python3-pip python3-venv)
|
||||
$UPDATE && $INSTALL "${SYSTEM_PKGS[@]}"
|
||||
|
||||
## -------- install VoxVera (prefers pipx, falls back) ---------------
|
||||
install_voxvera() {
|
||||
if command_exists pipx; then
|
||||
pipx install --force 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main' && return 0
|
||||
# Onionshare-cli - ensure it's installed and working (always attempt update if using pipx)
|
||||
msg ">> Ensuring onionshare-cli is installed and up-to-date..."
|
||||
if command_exists pipx && (pipx list | grep -q onionshare || ! command_exists onionshare-cli); then
|
||||
msg "Installing/Updating onionshare-cli via pipx..."
|
||||
pipx install --force git+https://github.com/onionshare/onionshare.git#subdirectory=cli || warn "pipx OnionShare install/update failed"
|
||||
elif ! command_exists onionshare-cli && ! command_exists onionshare; then
|
||||
msg "Attempting to install OnionShare..."
|
||||
OS_PKG="onionshare-cli"
|
||||
[[ ! $PM =~ (apt|dnf|yum) ]] && OS_PKG="onionshare"
|
||||
|
||||
if ! $INSTALL "$OS_PKG"; then
|
||||
warn "OnionShare not found in system repositories. Attempting pipx install fallback."
|
||||
if ! command_exists pipx; then
|
||||
msg "Installing pipx..."
|
||||
$INSTALL pipx
|
||||
pipx ensurepath --force
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
fi
|
||||
if command_exists pipx; then
|
||||
msg "Installing/Updating onionshare-cli via pipx..."
|
||||
pipx install --force git+https://github.com/onionshare/onionshare.git#subdirectory=cli || warn "pipx OnionShare install failed"
|
||||
else
|
||||
warn "pipx not found. Skipping OnionShare installation."
|
||||
fi
|
||||
fi
|
||||
# fallback: binary
|
||||
fi
|
||||
|
||||
## -------- install VoxVera (prefers binary, falls back) -------------
|
||||
install_voxvera() {
|
||||
local install_dir="$HOME/.local/bin"
|
||||
mkdir -p "$install_dir"
|
||||
local url latest dest="$install_dir/voxvera"
|
||||
url=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
|
||||
https://github.com/PR0M3TH3AN/VoxVera/releases/latest)
|
||||
latest="${url%/}" ; latest="${latest##*/}" # vX.Y.Z tag
|
||||
url="https://github.com/PR0M3TH3AN/VoxVera/releases/download/${latest/\/}/voxvera-linux"
|
||||
if curl -fsSL "$url" -o "$dest"; then
|
||||
chmod +x "$dest"
|
||||
PATH="$install_dir:$PATH"
|
||||
return 0
|
||||
|
||||
if command_exists curl; then
|
||||
msg "Attempting to download latest VoxVera binary..."
|
||||
# Get the latest release tag
|
||||
latest=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/PR0M3TH3AN/VoxVera/releases/latest | grep -oE '[^/]+$')
|
||||
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
ARCH=$(uname -m)
|
||||
case "$OS" in
|
||||
linux*) BINARY="voxvera-linux" ;;
|
||||
darwin*) BINARY="voxvera-macos" ;;
|
||||
*) BINARY="voxvera-linux" ;;
|
||||
esac
|
||||
|
||||
# Try to download architecture-specific binary (e.g. voxvera-linux-x86_64)
|
||||
url="https://github.com/PR0M3TH3AN/VoxVera/releases/download/${latest}/${BINARY}-${ARCH}"
|
||||
|
||||
msg "Downloading from: $url"
|
||||
set +e
|
||||
local status
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest")
|
||||
local curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ $curl_exit -eq 0 ] && [ "$status" = "200" ]; then
|
||||
chmod +x "$dest"
|
||||
msg "VoxVera binary (${ARCH}) installed to $dest"
|
||||
return 0
|
||||
else
|
||||
warn "Binary download failed (status: $status, exit: $curl_exit). Falling back to source installation."
|
||||
[ -f "$dest" ] && rm "$dest"
|
||||
fi
|
||||
fi
|
||||
# fallback: pip
|
||||
pip install --user 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'
|
||||
|
||||
# Fallback to pipx
|
||||
if command_exists pipx; then
|
||||
msg "Falling back to pipx installation (this may take a few minutes)..."
|
||||
if pipx install --force 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'; then
|
||||
pipx ensurepath --force
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to pip
|
||||
msg "Installing/Updating VoxVera via pip..."
|
||||
PIP_OPTS="--user --upgrade"
|
||||
if [ -f /etc/debian_version ]; then
|
||||
DEB_VER=$(cat /etc/debian_version | cut -d. -f1)
|
||||
if [[ "$DEB_VER" =~ ^[0-9]+$ ]] && [ "$DEB_VER" -ge 12 ]; then
|
||||
PIP_OPTS="$PIP_OPTS --break-system-packages"
|
||||
fi
|
||||
fi
|
||||
pip install $PIP_OPTS 'voxvera@git+https://github.com/PR0M3TH3AN/VoxVera.git@main'
|
||||
}
|
||||
|
||||
msg ">> Installing VoxVera"
|
||||
@@ -70,13 +138,16 @@ EOF
|
||||
|
||||
# enable + start tor via systemctl if available
|
||||
if command_exists systemctl; then
|
||||
sudo systemctl enable tor || true
|
||||
sudo systemctl restart tor
|
||||
$SUDO systemctl enable tor || true
|
||||
$SUDO systemctl restart tor || true
|
||||
fi
|
||||
|
||||
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 ""
|
||||
msg " IMPORTANT: Please restart your terminal or run 'source ~/.bashrc'"
|
||||
msg " (or your shell config) to use 'voxvera'."
|
||||
msg ""
|
||||
msg " Run 'voxvera check' to verify your setup after refreshing."
|
||||
msg "------------------------------------------------------------------"
|
||||
@@ -1,12 +1,12 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.1.4"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
# Add the vendor directory to sys.path as a fallback.
|
||||
# For standard installs (pip/pipx), the installed dependencies in the venv
|
||||
# should take priority. We append here so vendor acts as a fallback.
|
||||
vendor_dir = Path(__file__).parent / "vendor"
|
||||
if vendor_dir.exists():
|
||||
if str(vendor_dir) not in sys.path:
|
||||
sys.path.append(str(vendor_dir))
|
||||
# Add the vendor directory to sys.path ONLY for PyInstaller (frozen) builds.
|
||||
# For pip/pipx installs the system/venv packages are correct and the vendored
|
||||
# copies are incomplete (missing data files), so we must not shadow them.
|
||||
if getattr(sys, 'frozen', False):
|
||||
vendor_dir = Path(sys._MEIPASS).joinpath("voxvera", "vendor")
|
||||
if vendor_dir.exists() and str(vendor_dir) not in sys.path:
|
||||
sys.path.insert(0, str(vendor_dir))
|
||||
@@ -10,11 +10,35 @@ import time
|
||||
from pathlib import Path
|
||||
from importlib import resources
|
||||
from importlib.resources.abc import Traversable
|
||||
|
||||
# Force PyInstaller to bundle these critical dependencies for onionshare-cli.
|
||||
# Only needed at freeze-time analysis; skip for normal pip/pipx installs to
|
||||
# avoid pulling in vendored copies that may be incomplete.
|
||||
if getattr(sys, 'frozen', False):
|
||||
try:
|
||||
import pkg_resources
|
||||
import setuptools
|
||||
import jaraco
|
||||
import platformdirs
|
||||
import packaging
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from InquirerPy import prompt, inquirer
|
||||
from rich.console import Console
|
||||
from voxvera import __version__
|
||||
|
||||
# package root (contains bundled templates and src/)
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
if getattr(sys, 'frozen', False):
|
||||
ROOT = Path(sys._MEIPASS).joinpath("voxvera")
|
||||
# Ensure the bundle root is in sys.path for collected packages
|
||||
if sys._MEIPASS not in sys.path:
|
||||
sys.path.insert(0, sys._MEIPASS)
|
||||
else:
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# Persistent data directory (where host/ and projects live)
|
||||
DATA_DIR = Path(os.environ.get("VOXVERA_DIR", Path.home()))
|
||||
|
||||
# Global locale state
|
||||
CURRENT_LOCALE = {}
|
||||
@@ -22,16 +46,23 @@ CURRENT_LOCALE = {}
|
||||
|
||||
def _template_res(*parts) -> Traversable:
|
||||
"""Return a Traversable for files under the packaged ``templates`` folder."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# PyInstaller bundle: use sys._MEIPASS
|
||||
return Path(sys._MEIPASS).joinpath("voxvera", "templates", *parts)
|
||||
return resources.files(__package__).joinpath("templates", *parts)
|
||||
|
||||
|
||||
def _src_res(*parts) -> Traversable:
|
||||
"""Return a Traversable for files under the packaged ``src`` folder."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
return Path(sys._MEIPASS).joinpath("voxvera", "src", *parts)
|
||||
return resources.files(__package__).joinpath("src", *parts)
|
||||
|
||||
|
||||
def _locale_res(*parts) -> Traversable:
|
||||
"""Return a Traversable for files under the packaged ``locales`` folder."""
|
||||
if getattr(sys, 'frozen', False):
|
||||
return Path(sys._MEIPASS).joinpath("voxvera", "locales", *parts)
|
||||
return resources.files(__package__).joinpath("locales", *parts)
|
||||
|
||||
|
||||
@@ -39,13 +70,22 @@ def load_locale(lang_code: str = "en"):
|
||||
"""Load the specified locale JSON file."""
|
||||
global CURRENT_LOCALE
|
||||
try:
|
||||
with resources.as_file(_locale_res(f"{lang_code}.json")) as locale_file:
|
||||
res = _locale_res(f"{lang_code}.json")
|
||||
if getattr(sys, 'frozen', False):
|
||||
# When frozen, we already have a Path object
|
||||
locale_file = res
|
||||
if not locale_file.exists():
|
||||
lang_code = "en" # Fallback
|
||||
with resources.as_file(_locale_res("en.json")) as fallback_file:
|
||||
CURRENT_LOCALE = json.loads(fallback_file.read_text(encoding="utf-8"))
|
||||
else:
|
||||
CURRENT_LOCALE = json.loads(locale_file.read_text(encoding="utf-8"))
|
||||
lang_code = "en"
|
||||
locale_file = _locale_res("en.json")
|
||||
CURRENT_LOCALE = json.loads(locale_file.read_text(encoding="utf-8"))
|
||||
else:
|
||||
with resources.as_file(res) as locale_file:
|
||||
if not locale_file.exists():
|
||||
lang_code = "en" # Fallback
|
||||
with resources.as_file(_locale_res("en.json")) as fallback_file:
|
||||
CURRENT_LOCALE = json.loads(fallback_file.read_text(encoding="utf-8"))
|
||||
else:
|
||||
CURRENT_LOCALE = json.loads(locale_file.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
print(f"Error loading locale {lang_code}: {e}")
|
||||
# Final safety fallback to empty dict to avoid KeyErrors
|
||||
@@ -64,6 +104,14 @@ def t(path: str, **kwargs) -> str:
|
||||
|
||||
|
||||
def require_cmd(cmd: str):
|
||||
if cmd == "onionshare-cli":
|
||||
try:
|
||||
import onionshare_cli # noqa: F401
|
||||
return True
|
||||
except ImportError:
|
||||
print("Required tool 'onionshare-cli' not bundled or installed.", file=sys.stderr)
|
||||
return False
|
||||
|
||||
if shutil.which(cmd) is None:
|
||||
print(
|
||||
f"Required command '{cmd}' not found. Please install it.", file=sys.stderr
|
||||
@@ -75,10 +123,11 @@ def require_cmd(cmd: str):
|
||||
def check_deps():
|
||||
console = Console()
|
||||
|
||||
# External CLI tools still required at runtime
|
||||
cli_tools = ["onionshare-cli"]
|
||||
# External CLI tools still required at runtime (none anymore if bundled)
|
||||
cli_tools = []
|
||||
# Python packages used by the build pipeline
|
||||
py_packages = {
|
||||
"onionshare-cli": "onionshare_cli",
|
||||
"qrcode": "qrcode",
|
||||
"PIL (Pillow)": "PIL",
|
||||
"jsmin": "jsmin",
|
||||
@@ -145,7 +194,7 @@ def _open_editor_terminal(initial: str) -> str:
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fh.write(initial or "")
|
||||
subprocess.call([editor, path])
|
||||
subprocess.run([editor, path], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
except Exception as e:
|
||||
@@ -340,11 +389,18 @@ def interactive_update(config_path: str):
|
||||
|
||||
console.rule(t("cli.init_links"))
|
||||
link_qs = [
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.attachment_path_label"),
|
||||
"name": "attachment_path",
|
||||
"default": data.get("attachment_path", ""),
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.url_label"),
|
||||
"name": "url",
|
||||
"default": data.get("url", "https://voxvera.org/"),
|
||||
"default": data.get("url", "https://voxvera.org/") if not data.get("url", "").startswith("download/") else "",
|
||||
"when": lambda result: not result.get("attachment_path", "").strip(),
|
||||
"transformer": _len_transform(200),
|
||||
"validate": _len_validator(200),
|
||||
},
|
||||
@@ -356,15 +412,6 @@ def interactive_update(config_path: str):
|
||||
"transformer": _len_transform(120),
|
||||
"validate": _len_validator(120),
|
||||
},
|
||||
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.binary_message_label"),
|
||||
"name": "binary_message",
|
||||
"default": data.get("binary_message", ""),
|
||||
"transformer": _len_transform(120),
|
||||
"validate": _len_validator(120),
|
||||
},
|
||||
]
|
||||
data.update(prompt(link_qs))
|
||||
|
||||
@@ -377,19 +424,24 @@ def copy_template(name: str) -> str:
|
||||
if not src.is_dir():
|
||||
print(f"Template {name} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
dest = ROOT / "host" / name
|
||||
os.makedirs(ROOT / "host", exist_ok=True)
|
||||
dest = DATA_DIR / "host" / name
|
||||
os.makedirs(DATA_DIR / "host", exist_ok=True)
|
||||
shutil.copytree(src, dest, dirs_exist_ok=True)
|
||||
# Clear any QR codes that might have been in the template
|
||||
# to ensure a clean build slate.
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
(dest / qr_file).unlink(missing_ok=True)
|
||||
print(f"Template copied to {dest}")
|
||||
return str(dest)
|
||||
|
||||
|
||||
def bundle_source(dest_zip: Path):
|
||||
"""Bundle only the VoxVera source code and root scripts (no binaries or vendor)."""
|
||||
"""Bundle the VoxVera source code, dependencies (vendor), and root scripts."""
|
||||
with zipfile.ZipFile(dest_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
# 1. Include the voxvera/ source directory (excluding vendor, host, bin, pycache)
|
||||
# 1. Include the voxvera/ source directory (excluding host, bin, pycache)
|
||||
# We DO include vendor here so the source zip is fully functional offline.
|
||||
for root, dirs, files in os.walk(ROOT):
|
||||
if "__pycache__" in root or "host" in root or "vendor" in root or "resources/bin" in root:
|
||||
if "__pycache__" in root or "host" in root or "resources/bin" in root:
|
||||
continue
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
@@ -399,7 +451,8 @@ def bundle_source(dest_zip: Path):
|
||||
# 2. Include the run scripts and root files
|
||||
root_files = [
|
||||
"voxvera-run.sh", "voxvera-install.sh", "README.md",
|
||||
"requirements.txt", "setup.sh", "install.sh", "CONTRIBUTING.md", "LICENSE"
|
||||
"requirements.txt", "setup.sh", "install.sh", "uninstall.sh",
|
||||
"install.ps1", "uninstall.ps1", "CONTRIBUTING.md", "LICENSE"
|
||||
]
|
||||
for script in root_files:
|
||||
script_path = ROOT.parent / script
|
||||
@@ -410,43 +463,10 @@ def bundle_source(dest_zip: Path):
|
||||
|
||||
|
||||
def bundle_portable(dest_zip: Path):
|
||||
"""Bundle the entire VoxVera source, standalone binaries, and dependencies into a ZIP."""
|
||||
with zipfile.ZipFile(dest_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
# 1. Include standalone binaries from resources/bin if they exist
|
||||
bin_dir = ROOT / "resources" / "bin"
|
||||
if bin_dir.exists():
|
||||
for bin_file in bin_dir.iterdir():
|
||||
if bin_file.is_file():
|
||||
zipf.write(bin_file, bin_file.name)
|
||||
|
||||
# 2. Include the voxvera/ source directory
|
||||
for root, dirs, files in os.walk(ROOT):
|
||||
if "__pycache__" in root or "host" in root or "vendor" in root or "resources/bin" in root:
|
||||
if "vendor" in root: # We DO want vendor
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
arcname = file_path.relative_to(ROOT.parent)
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
# 3. Include the run scripts and root files
|
||||
root_files = [
|
||||
"voxvera-run.sh", "voxvera-install.sh", "README.md",
|
||||
"requirements.txt", "setup.sh", "install.sh"
|
||||
]
|
||||
for script in root_files:
|
||||
script_path = ROOT.parent / script
|
||||
if script_path.exists():
|
||||
zipf.write(script_path, script)
|
||||
|
||||
# 4. Create a simple Windows batch launcher if it doesn't exist
|
||||
win_launcher = "voxvera.bat"
|
||||
launcher_content = "@echo off\npython -m voxvera.cli manage\npause"
|
||||
zipf.writestr(win_launcher, launcher_content)
|
||||
|
||||
print(f"Portable bundle created at {dest_zip}")
|
||||
"""Bundle the entire VoxVera source and dependencies into a ZIP (Viral distribution)."""
|
||||
# For now, we prioritize source code as requested by the user.
|
||||
# We rename it to voxvera-source.zip for clarity.
|
||||
bundle_source(dest_zip)
|
||||
|
||||
|
||||
def build_assets(
|
||||
@@ -456,8 +476,6 @@ def build_assets(
|
||||
|
||||
with resources.as_file(_src_res()) as src_dir:
|
||||
config_path = Path(config_path)
|
||||
# generate QR codes (pure Python)
|
||||
generate_qr(config_path, src_dir)
|
||||
|
||||
data = load_config(config_path)
|
||||
with open(src_dir / "index-master.html", "r") as fh:
|
||||
@@ -466,6 +484,14 @@ def build_assets(
|
||||
# Bundle locales for the web engine
|
||||
all_locales = {}
|
||||
locale_files = sorted(glob.glob(str(ROOT / "locales" / "*.json")))
|
||||
|
||||
# Load master defaults from src/config.json
|
||||
src_config_path = ROOT / "src" / "config.json"
|
||||
master_defaults = {}
|
||||
if src_config_path.exists():
|
||||
with open(src_config_path, "r", encoding="utf-8") as sf:
|
||||
master_defaults = json.load(sf)
|
||||
|
||||
for lp in locale_files:
|
||||
code = Path(lp).stem
|
||||
with open(lp, "r", encoding="utf-8") as lf:
|
||||
@@ -476,6 +502,13 @@ def build_assets(
|
||||
"landing": l_data.get("landing", {})
|
||||
}
|
||||
|
||||
# Override landing data with user custom config data if it differs from master defaults
|
||||
for field in ["title", "subtitle", "headline", "content", "url_message"]:
|
||||
if field in data and data[field]:
|
||||
# Only override if the value is different from the master default
|
||||
if data[field] != master_defaults.get(field):
|
||||
all_locales[code]["landing"][field] = data[field]
|
||||
|
||||
html = html.replace("{{locales}}", json.dumps(all_locales))
|
||||
|
||||
# 1. Statically replace localization tokens {{t_web_...}}
|
||||
@@ -497,6 +530,13 @@ def build_assets(
|
||||
for field in landing_fields:
|
||||
# Use data from config if available, otherwise use localized default
|
||||
value = data.get(field, landing_defaults.get(field, ""))
|
||||
|
||||
# If the value from config is just the master default,
|
||||
# and we are NOT in English, use the localized default instead.
|
||||
if data.get("lang", "en") != "en":
|
||||
if value == master_defaults.get(field):
|
||||
value = landing_defaults.get(field, value)
|
||||
|
||||
# Support Markdown-style redaction ~~text~~
|
||||
val_str = re.sub(r"~~(.*?)~~", r'<span class="redacted">\1</span>', str(value))
|
||||
html = html.replace(f"{{{{t_landing_{field}}}}}", val_str)
|
||||
@@ -508,34 +548,61 @@ def build_assets(
|
||||
val_str = re.sub(r"~~(.*?)~~", r'<span class="redacted">\1</span>', val_str)
|
||||
html = html.replace(f"{{{{{key}}}}}", val_str)
|
||||
|
||||
# Handle optional file attachment logic
|
||||
attachment_path = data.get("attachment_path")
|
||||
if attachment_path and os.path.isfile(attachment_path):
|
||||
att_filename = os.path.basename(attachment_path)
|
||||
# Only default to the local attachment path if no onion URL is set
|
||||
current_url = data.get("url", "")
|
||||
if not (current_url.startswith("http") and ".onion" in current_url):
|
||||
data["url"] = f"download/{att_filename}"
|
||||
save_config(data, config_path)
|
||||
html = html.replace("{{url}}", data["url"])
|
||||
|
||||
folder_name = data["folder_name"]
|
||||
dest = ROOT / "host" / folder_name
|
||||
dest = DATA_DIR / "host" / folder_name
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
# Update download link to point to our viral portable bundle
|
||||
html = html.replace("download/download.zip", "download/voxvera-portable.zip")
|
||||
# 1. Refresh QR codes directly in destination
|
||||
# generate_qr only creates them if URLs are present in config
|
||||
|
||||
# We generate to a temp dir first to ensure we ONLY get what's in current config
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as qr_tmp:
|
||||
qr_tmp_path = Path(qr_tmp)
|
||||
generate_qr(config_path, qr_tmp_path)
|
||||
|
||||
# Clear old QR codes from destination and copy new ones
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_dest = dest / qr_file
|
||||
qr_src = qr_tmp_path / qr_file
|
||||
if qr_src.exists():
|
||||
shutil.copy(qr_src, qr_dest)
|
||||
elif qr_dest.exists():
|
||||
qr_dest.unlink()
|
||||
|
||||
# Update download link to point to our viral source bundle
|
||||
|
||||
html = html.replace("download/download.zip", "download/voxvera-source.zip")
|
||||
|
||||
with open(dest / "index.html", "w") as fh:
|
||||
fh.write(html)
|
||||
|
||||
# Create the viral portable bundle
|
||||
# Create the viral source bundle
|
||||
download_dir = dest / "download"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
bundle_portable(download_dir / "voxvera-portable.zip")
|
||||
bundle_source(download_dir / "voxvera-source.zip")
|
||||
|
||||
if download_path:
|
||||
shutil.copy(download_path, download_dir / "extra-content.zip")
|
||||
|
||||
if attachment_path and os.path.isfile(attachment_path):
|
||||
shutil.copy(attachment_path, download_dir / os.path.basename(attachment_path))
|
||||
|
||||
# Copy config if it's not already there
|
||||
if Path(config_path) != dest / "config.json":
|
||||
shutil.copy(config_path, dest / "config.json")
|
||||
|
||||
# Copy QR codes if they exist
|
||||
for qr_file in ["qrcode-content.png", "qrcode-tear-offs.png"]:
|
||||
qr_src = Path(src_dir) / qr_file
|
||||
if qr_src.exists():
|
||||
shutil.copy(qr_src, dest / qr_file)
|
||||
|
||||
print(f"Flyer files created under {dest}")
|
||||
|
||||
|
||||
@@ -585,14 +652,54 @@ def get_tor_ports():
|
||||
return socks_port, ctl_port
|
||||
|
||||
|
||||
def serve(config_path: str) -> str | None:
|
||||
if not require_cmd("onionshare-cli"):
|
||||
def _internal_onionshare():
|
||||
"""Hidden command to run the bundled onionshare-cli."""
|
||||
import platform
|
||||
|
||||
# Try to add bundled Tor to PATH so OnionShare can find it
|
||||
system = platform.system().lower()
|
||||
platform_dir = "win" if "windows" in system else "mac" if "darwin" in system else "linux"
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
tor_dir = Path(sys._MEIPASS).joinpath("voxvera", "resources", "tor", platform_dir)
|
||||
else:
|
||||
tor_dir = ROOT / "resources" / "tor" / platform_dir
|
||||
|
||||
# Check if bundled Tor is a real binary and not a placeholder (size > 100 bytes)
|
||||
bundled_tor = tor_dir / ("tor.exe" if "windows" in system else "tor")
|
||||
if bundled_tor.exists() and bundled_tor.stat().st_size > 100:
|
||||
os.environ["PATH"] = str(tor_dir) + os.pathsep + os.environ.get("PATH", "")
|
||||
os.environ["TOR_BINARY"] = str(bundled_tor)
|
||||
|
||||
onionshare_args = sys.argv[2:] # strip 'voxvera _internal_onionshare'
|
||||
|
||||
# Always prefer the system onionshare-cli binary. This avoids
|
||||
# pkg_resources / setuptools isolation issues in both PyInstaller
|
||||
# builds and pipx venvs.
|
||||
# Prevent user-local pip packages from shadowing system packages
|
||||
os.environ["PYTHONNOUSERSITE"] = "1"
|
||||
|
||||
onionshare_bin = shutil.which("onionshare-cli") or shutil.which("onionshare")
|
||||
if onionshare_bin:
|
||||
os.execvp(onionshare_bin, [onionshare_bin] + onionshare_args)
|
||||
# execvp replaces the process; unreachable unless execvp itself fails
|
||||
|
||||
# Last resort: try to import onionshare_cli as a Python module
|
||||
try:
|
||||
from onionshare_cli import main
|
||||
sys.argv = ["onionshare-cli"] + onionshare_args
|
||||
sys.exit(main())
|
||||
except Exception as e:
|
||||
print(f"Error: onionshare-cli not available: {e}", file=sys.stderr)
|
||||
print("Install onionshare-cli via your system package manager.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def serve(config_path: str) -> str | None:
|
||||
socks, ctl = get_tor_ports()
|
||||
|
||||
folder_name = load_config(config_path)["folder_name"]
|
||||
dir_path = ROOT / "host" / folder_name
|
||||
dir_path = DATA_DIR / "host" / folder_name
|
||||
if not dir_path.is_dir():
|
||||
print(f"Directory {dir_path} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -602,9 +709,12 @@ def serve(config_path: str) -> str | None:
|
||||
env = os.environ.copy()
|
||||
env["TOR_SOCKS_PORT"] = socks
|
||||
env["TOR_CONTROL_PORT"] = ctl
|
||||
# Prevent user-local pip packages (~/.local) from shadowing system packages.
|
||||
# onionshare-cli depends on specific Flask/Werkzeug versions from the system;
|
||||
# stale user-local copies break it.
|
||||
env["PYTHONNOUSERSITE"] = "1"
|
||||
|
||||
cmd = [
|
||||
"onionshare-cli",
|
||||
onionshare_args = [
|
||||
"--website",
|
||||
"--public",
|
||||
"--persistent",
|
||||
@@ -615,8 +725,29 @@ def serve(config_path: str) -> str | None:
|
||||
"-v",
|
||||
str(dir_path),
|
||||
]
|
||||
|
||||
# Always prefer the system onionshare-cli binary to avoid pkg_resources
|
||||
# and dependency-isolation issues inside both PyInstaller and pipx.
|
||||
onionshare_bin = shutil.which("onionshare-cli") or shutil.which("onionshare")
|
||||
if onionshare_bin:
|
||||
cmd = [onionshare_bin] + onionshare_args
|
||||
else:
|
||||
# Fallback: route through our hidden subcommand
|
||||
cmd = [sys.argv[0], "_internal_onionshare"] + onionshare_args
|
||||
if not getattr(sys, 'frozen', False) and sys.argv[0].endswith('.py'):
|
||||
cmd = [sys.executable] + cmd
|
||||
|
||||
log_fh = open(logfile, "w")
|
||||
proc = subprocess.Popen(cmd, stdout=log_fh, stderr=subprocess.STDOUT, env=env)
|
||||
# Detach the child so it survives when the parent terminal closes.
|
||||
# Unix: start_new_session prevents SIGHUP propagation.
|
||||
# Windows: CREATE_NEW_PROCESS_GROUP detaches from the console.
|
||||
import platform as _platform
|
||||
popen_kwargs = dict(stdout=log_fh, stderr=subprocess.STDOUT, env=env)
|
||||
if _platform.system() == "Windows":
|
||||
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
else:
|
||||
popen_kwargs["start_new_session"] = True
|
||||
proc = subprocess.Popen(cmd, **popen_kwargs)
|
||||
|
||||
pid_file = dir_path / "server.pid"
|
||||
with open(pid_file, "w") as f:
|
||||
@@ -701,9 +832,9 @@ def get_export_dir() -> Path:
|
||||
|
||||
def export_site(folder_name: str, output_path: str = None):
|
||||
"""Export a site folder and its keys into a zip archive."""
|
||||
source_dir = ROOT / "host" / folder_name
|
||||
source_dir = DATA_DIR / "host" / folder_name
|
||||
if not source_dir.exists():
|
||||
print(f"Error: Site folder '{folder_name}' not found in {ROOT / 'host'}")
|
||||
print(f"Error: Site folder '{folder_name}' not found in {DATA_DIR / 'host'}")
|
||||
return
|
||||
|
||||
if not output_path:
|
||||
@@ -759,7 +890,7 @@ def import_site(zip_path: str):
|
||||
print("Error: Invalid config.json. Missing 'folder_name'.")
|
||||
return
|
||||
|
||||
dest_dir = ROOT / "host" / folder_name
|
||||
dest_dir = DATA_DIR / "host" / folder_name
|
||||
if dest_dir.exists():
|
||||
confirm = inquirer.confirm(message=f"Site '{folder_name}' already exists. Overwrite?").execute()
|
||||
if not confirm:
|
||||
@@ -797,9 +928,15 @@ def import_multiple_sites(source_dir: str = None):
|
||||
|
||||
def build_docs():
|
||||
"""Generate localized documentation from templates and locale data."""
|
||||
template_dir = ROOT.parent / "docs" / "templates"
|
||||
if getattr(sys, 'frozen', False):
|
||||
template_dir = Path(sys._MEIPASS).joinpath("docs", "templates")
|
||||
doc_root = Path(sys._MEIPASS).joinpath("docs")
|
||||
else:
|
||||
template_dir = ROOT.parent / "docs" / "templates"
|
||||
doc_root = ROOT.parent / "docs"
|
||||
|
||||
if not template_dir.exists():
|
||||
print("Error: docs/templates/ not found.")
|
||||
print(f"Error: {template_dir} not found.")
|
||||
return
|
||||
|
||||
# List all supported languages
|
||||
@@ -809,7 +946,7 @@ def build_docs():
|
||||
for lang in langs:
|
||||
print(f"Generating documentation for: {lang}")
|
||||
load_locale(lang)
|
||||
dest_dir = ROOT.parent / "docs" / lang
|
||||
dest_dir = doc_root / lang
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
# Process each template
|
||||
@@ -844,31 +981,55 @@ def build_docs():
|
||||
|
||||
def build_site():
|
||||
"""Refresh the main site/ directory with the latest template, QRs, and portable bundle."""
|
||||
site_dir = ROOT.parent / "site"
|
||||
if not site_dir.exists():
|
||||
print("Error: site/ directory not found.")
|
||||
# When frozen, site/ source is in sys._MEIPASS, but we might want to output to CWD/site
|
||||
if getattr(sys, 'frozen', False):
|
||||
src_site = Path(sys._MEIPASS).joinpath("site")
|
||||
dest_site = Path.cwd() / "site"
|
||||
else:
|
||||
src_site = ROOT.parent / "site"
|
||||
dest_site = src_site
|
||||
|
||||
if not src_site.exists():
|
||||
print(f"Error: {src_site} directory not found.")
|
||||
return
|
||||
|
||||
config_path = site_dir / "config.json"
|
||||
config_path = dest_site / "config.json"
|
||||
if not config_path.exists():
|
||||
print("Error: site/config.json not found.")
|
||||
return
|
||||
# Fallback to source if dest doesn't have it (e.g. first run)
|
||||
config_path = src_site / "config.json"
|
||||
if not config_path.exists():
|
||||
print(f"Error: {config_path} not found.")
|
||||
return
|
||||
|
||||
print("Refreshing site/ assets...")
|
||||
print(f"Refreshing {dest_site} assets...")
|
||||
|
||||
# 1. Refresh QR codes using site/config.json
|
||||
from voxvera.build import generate_qr
|
||||
generate_qr(config_path, site_dir)
|
||||
generate_qr(config_path, dest_site)
|
||||
|
||||
# 2. Build the HTML from master template
|
||||
data = load_config(config_path)
|
||||
with resources.as_file(_src_res("index-master.html")) as template_path:
|
||||
res = _src_res("index-master.html")
|
||||
if getattr(sys, 'frozen', False):
|
||||
template_path = res
|
||||
with open(template_path, "r") as fh:
|
||||
html = fh.read()
|
||||
else:
|
||||
with resources.as_file(res) as template_path:
|
||||
with open(template_path, "r") as fh:
|
||||
html = fh.read()
|
||||
|
||||
# Bundle locales (Include landing tokens for the main site)
|
||||
all_locales = {}
|
||||
locale_files = sorted(glob.glob(str(ROOT / "locales" / "*.json")))
|
||||
|
||||
# Load master defaults from src/config.json
|
||||
src_config_path = ROOT / "src" / "config.json"
|
||||
master_defaults = {}
|
||||
if src_config_path.exists():
|
||||
with open(src_config_path, "r", encoding="utf-8") as sf:
|
||||
master_defaults = json.load(sf)
|
||||
|
||||
for lp in locale_files:
|
||||
code = Path(lp).stem
|
||||
with open(lp, "r", encoding="utf-8") as lf:
|
||||
@@ -879,6 +1040,13 @@ def build_site():
|
||||
"landing": l_data.get("landing", {})
|
||||
}
|
||||
|
||||
# Override landing data with user custom config data if it differs from master defaults
|
||||
for field in ["title", "subtitle", "headline", "content", "url_message"]:
|
||||
if field in data and data[field]:
|
||||
# Only override if the value is different from the master default
|
||||
if data[field] != master_defaults.get(field):
|
||||
all_locales[code]["landing"][field] = data[field]
|
||||
|
||||
html = html.replace("{{locales}}", json.dumps(all_locales))
|
||||
|
||||
# 1. Statically replace localization tokens (web tokens)
|
||||
@@ -899,28 +1067,33 @@ def build_site():
|
||||
for field in landing_fields:
|
||||
# Use data from config if available, otherwise use localized default
|
||||
value = data.get(field, landing_defaults.get(field, ""))
|
||||
|
||||
# If the value from config is just the master default,
|
||||
# and we are NOT in English, use the localized default instead.
|
||||
if current_lang != "en":
|
||||
if value == master_defaults.get(field):
|
||||
value = landing_defaults.get(field, value)
|
||||
|
||||
# Support Markdown-style redaction ~~text~~
|
||||
val_str = re.sub(r"~~(.*?)~~", r'<span class="redacted">\1</span>', str(value))
|
||||
html = html.replace(f"{{{{t_landing_{field}}}}}", val_str)
|
||||
|
||||
# 3. Update relative path for download
|
||||
html = html.replace("download/download.zip", "download/voxvera-portable.zip")
|
||||
html = html.replace("download/download.zip", "download/voxvera-source.zip")
|
||||
|
||||
# 3. Inject site-specific config data (handles any remaining {{key}} placeholders)
|
||||
# 4. Inject site-specific config data (handles any remaining {{key}} placeholders)
|
||||
for key, value in data.items():
|
||||
html = html.replace(f"{{{{{key}}}}}", str(value))
|
||||
|
||||
with open(site_dir / "index.html", "w") as fh:
|
||||
os.makedirs(dest_site, exist_ok=True)
|
||||
with open(dest_site / "index.html", "w") as fh:
|
||||
fh.write(html)
|
||||
|
||||
# 3. Create the download assets in site/download/
|
||||
download_dir = site_dir / "download"
|
||||
# 5. Create the download assets in site/download/
|
||||
download_dir = dest_site / "download"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
# Bundle portable version (source + vendor + binaries)
|
||||
bundle_portable(download_dir / "voxvera-portable.zip")
|
||||
|
||||
# Bundle source version (source only)
|
||||
# Bundle source version
|
||||
bundle_source(download_dir / "voxvera-source.zip")
|
||||
|
||||
# Copy standalone binaries if they exist
|
||||
@@ -957,32 +1130,49 @@ def vendorize():
|
||||
|
||||
|
||||
def batch_import_configs():
|
||||
files = sorted(glob.glob(str(ROOT / "imports" / "*.json")))
|
||||
files = sorted(glob.glob(str(DATA_DIR / "imports" / "*.json")))
|
||||
if not files:
|
||||
print("No JSON files found in imports")
|
||||
return
|
||||
for json_file in files:
|
||||
print(f"Processing {json_file}")
|
||||
dest_config = ROOT / "src" / "config.json"
|
||||
# Use a temp config path in DATA_DIR instead of bundled ROOT
|
||||
dest_config = DATA_DIR / "temp_config.json"
|
||||
shutil.copy(json_file, dest_config)
|
||||
folder_name = load_config(json_file)["folder_name"]
|
||||
_clear_host_dir(ROOT / "host" / folder_name)
|
||||
build_assets(dest_config)
|
||||
_clear_host_dir(DATA_DIR / "host" / folder_name)
|
||||
build_assets(str(dest_config))
|
||||
dest_config.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _seed_bundled_sites():
|
||||
"""Copy bundled example sites from ROOT/host/ to DATA_DIR/host/ if they
|
||||
don't already exist there. This runs once — after that, only DATA_DIR is
|
||||
the source of truth so deletes actually stick."""
|
||||
bundled_host = ROOT / "host"
|
||||
if not bundled_host.exists() or ROOT == DATA_DIR:
|
||||
return
|
||||
dest_host = DATA_DIR / "host"
|
||||
for src in bundled_host.iterdir():
|
||||
if src.is_dir() and (src / "config.json").exists():
|
||||
dest = dest_host / src.name
|
||||
if not dest.exists():
|
||||
shutil.copytree(src, dest)
|
||||
|
||||
|
||||
def get_servers() -> list[str]:
|
||||
host_dir = ROOT / "host"
|
||||
if not host_dir.exists():
|
||||
return []
|
||||
"""Return server names found in DATA_DIR/host only."""
|
||||
host_dir = DATA_DIR / "host"
|
||||
servers = []
|
||||
for d in host_dir.iterdir():
|
||||
if d.is_dir() and (d / "config.json").exists():
|
||||
servers.append(d.name)
|
||||
if host_dir.exists():
|
||||
for d in host_dir.iterdir():
|
||||
if d.is_dir() and (d / "config.json").exists():
|
||||
servers.append(d.name)
|
||||
return sorted(servers)
|
||||
|
||||
|
||||
def is_server_running(folder_name: str) -> bool:
|
||||
pid_file = ROOT / "host" / folder_name / "server.pid"
|
||||
pid_file = DATA_DIR / "host" / folder_name / "server.pid"
|
||||
if not pid_file.exists():
|
||||
return False
|
||||
try:
|
||||
@@ -1021,7 +1211,7 @@ def is_server_running(folder_name: str) -> bool:
|
||||
|
||||
|
||||
def stop_server(folder_name: str):
|
||||
pid_file = ROOT / "host" / folder_name / "server.pid"
|
||||
pid_file = DATA_DIR / "host" / folder_name / "server.pid"
|
||||
if not pid_file.exists():
|
||||
print(f"Server {folder_name} is not running.")
|
||||
return
|
||||
@@ -1043,17 +1233,166 @@ def stop_server(folder_name: str):
|
||||
def delete_server(folder_name: str):
|
||||
if is_server_running(folder_name):
|
||||
stop_server(folder_name)
|
||||
dir_path = ROOT / "host" / folder_name
|
||||
dir_path = DATA_DIR / "host" / folder_name
|
||||
if dir_path.exists():
|
||||
shutil.rmtree(dir_path, ignore_errors=True)
|
||||
print(f"Deleted server {folder_name}")
|
||||
|
||||
|
||||
def start_all_servers():
|
||||
"""Non-interactive: start every configured site that isn't already running."""
|
||||
_seed_bundled_sites()
|
||||
servers = get_servers()
|
||||
if not servers:
|
||||
print("No sites configured.")
|
||||
return
|
||||
for s in servers:
|
||||
if is_server_running(s):
|
||||
print(f"[SKIP] {s} already running")
|
||||
continue
|
||||
config_path = DATA_DIR / "host" / s / "config.json"
|
||||
try:
|
||||
url = serve(str(config_path))
|
||||
if url:
|
||||
print(f"[OK] {s}: {url}")
|
||||
else:
|
||||
print(f"[FAIL] {s}: could not obtain onion URL")
|
||||
except Exception as e:
|
||||
print(f"[FAIL] {s}: {e}")
|
||||
|
||||
|
||||
def stop_all_servers():
|
||||
"""Non-interactive: stop every running site."""
|
||||
servers = get_servers()
|
||||
stopped = False
|
||||
for s in servers:
|
||||
if is_server_running(s):
|
||||
stop_server(s)
|
||||
stopped = True
|
||||
if not stopped:
|
||||
print("No running sites.")
|
||||
|
||||
|
||||
def install_autostart():
|
||||
"""Install a platform-appropriate mechanism to start sites on boot."""
|
||||
import platform
|
||||
system = platform.system()
|
||||
|
||||
if system == "Linux":
|
||||
_install_systemd_autostart()
|
||||
elif system == "Darwin":
|
||||
_install_launchd_autostart()
|
||||
elif system == "Windows":
|
||||
_install_windows_autostart()
|
||||
else:
|
||||
print(f"Autostart not supported on {system}. "
|
||||
"Run 'voxvera start-all' manually or add it to your init system.")
|
||||
|
||||
|
||||
def _find_voxvera_bin() -> str:
|
||||
"""Return the absolute path to the voxvera binary/script."""
|
||||
voxvera_bin = shutil.which("voxvera")
|
||||
if voxvera_bin:
|
||||
return str(Path(voxvera_bin).resolve())
|
||||
# Fallback: current executable
|
||||
return str(Path(sys.argv[0]).resolve())
|
||||
|
||||
|
||||
def _install_systemd_autostart():
|
||||
"""Install a systemd user service for Linux."""
|
||||
service_dir = Path.home() / ".config" / "systemd" / "user"
|
||||
service_dir.mkdir(parents=True, exist_ok=True)
|
||||
service_file = service_dir / "voxvera.service"
|
||||
voxvera_bin = _find_voxvera_bin()
|
||||
|
||||
unit = f"""\
|
||||
[Unit]
|
||||
Description=VoxVera Onion Sites
|
||||
After=network-online.target tor.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart={voxvera_bin} start-all
|
||||
ExecStop={voxvera_bin} stop-all
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
"""
|
||||
service_file.write_text(unit)
|
||||
os.system("systemctl --user daemon-reload")
|
||||
os.system("systemctl --user enable voxvera.service")
|
||||
os.system("systemctl --user start voxvera.service")
|
||||
# loginctl enable-linger ensures user services start at boot even without login
|
||||
os.system(f"loginctl enable-linger {os.environ.get('USER', '')}")
|
||||
print(f"Systemd user service installed: {service_file}")
|
||||
print("Sites will start automatically on boot.")
|
||||
print(" Manage with: systemctl --user [start|stop|status|disable] voxvera")
|
||||
|
||||
|
||||
def _install_launchd_autostart():
|
||||
"""Install a launchd plist for macOS."""
|
||||
agents_dir = Path.home() / "Library" / "LaunchAgents"
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
plist_file = agents_dir / "org.voxvera.start-all.plist"
|
||||
voxvera_bin = _find_voxvera_bin()
|
||||
|
||||
plist = f"""\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>org.voxvera.start-all</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{voxvera_bin}</string>
|
||||
<string>start-all</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{Path.home()}/host/voxvera-autostart.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{Path.home()}/host/voxvera-autostart.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
plist_file.write_text(plist)
|
||||
os.system(f"launchctl load -w {plist_file}")
|
||||
print(f"LaunchAgent installed: {plist_file}")
|
||||
print("Sites will start automatically on login.")
|
||||
print(f" Disable with: launchctl unload {plist_file}")
|
||||
|
||||
|
||||
def _install_windows_autostart():
|
||||
"""Add a startup shortcut or scheduled task on Windows."""
|
||||
voxvera_bin = _find_voxvera_bin()
|
||||
# Use Task Scheduler for a reliable boot-time trigger
|
||||
task_name = "VoxVera Start All"
|
||||
cmd = (
|
||||
f'schtasks /Create /F /SC ONLOGON /TN "{task_name}" '
|
||||
f'/TR "\"{voxvera_bin}\" start-all" /RL HIGHEST'
|
||||
)
|
||||
ret = os.system(cmd)
|
||||
if ret == 0:
|
||||
print(f"Windows scheduled task '{task_name}' created.")
|
||||
print("Sites will start automatically on login.")
|
||||
print(f' Remove with: schtasks /Delete /TN "{task_name}" /F')
|
||||
else:
|
||||
print("Failed to create scheduled task. Try running as Administrator.")
|
||||
|
||||
|
||||
def manage_servers():
|
||||
from InquirerPy import inquirer
|
||||
from InquirerPy.base.control import Choice
|
||||
console = Console()
|
||||
|
||||
# On first run, copy any bundled example sites so they're user-editable.
|
||||
_seed_bundled_sites()
|
||||
|
||||
while True:
|
||||
servers = get_servers()
|
||||
|
||||
@@ -1065,7 +1404,7 @@ def manage_servers():
|
||||
label = f"{status_text} {s}"
|
||||
if running:
|
||||
try:
|
||||
site_data = load_config(ROOT / "host" / s / "config.json")
|
||||
site_data = load_config(DATA_DIR / "host" / s / "config.json")
|
||||
url = site_data.get("tear_off_link")
|
||||
if url:
|
||||
label += f" ({url})"
|
||||
@@ -1090,7 +1429,7 @@ def manage_servers():
|
||||
break
|
||||
|
||||
if selected == "create_new":
|
||||
config_path = ROOT / "src" / "config.json"
|
||||
config_path = DATA_DIR / "config.json"
|
||||
# Reset config from template for a fresh start
|
||||
try:
|
||||
template_config = _template_res("voxvera", "config.json")
|
||||
@@ -1114,7 +1453,7 @@ def manage_servers():
|
||||
results = []
|
||||
for s in servers:
|
||||
if not is_server_running(s):
|
||||
config_path = ROOT / "host" / s / "config.json"
|
||||
config_path = DATA_DIR / "host" / s / "config.json"
|
||||
try:
|
||||
url = serve(str(config_path))
|
||||
if url:
|
||||
@@ -1146,6 +1485,7 @@ def manage_servers():
|
||||
running = is_server_running(selected)
|
||||
action_choices = [
|
||||
Choice("toggle", t("cli.manage_action_toggle_stop") if running else t("cli.manage_action_toggle_start")),
|
||||
Choice("edit", t("cli.manage_action_edit")),
|
||||
Choice("export", t("cli.manage_action_export")),
|
||||
Choice("delete", t("cli.manage_action_delete")),
|
||||
Choice(None, t("cli.manage_action_back"))
|
||||
@@ -1160,11 +1500,23 @@ def manage_servers():
|
||||
if running:
|
||||
stop_server(selected)
|
||||
else:
|
||||
config_path = ROOT / "host" / selected / "config.json"
|
||||
config_path = DATA_DIR / "host" / selected / "config.json"
|
||||
try:
|
||||
serve(str(config_path))
|
||||
except SystemExit:
|
||||
pass
|
||||
elif action == "edit":
|
||||
config_path = DATA_DIR / "host" / selected / "config.json"
|
||||
try:
|
||||
interactive_update(str(config_path))
|
||||
# Re-build assets after update
|
||||
build_assets(str(config_path))
|
||||
console.print(f"[green]✔ {selected} updated and rebuilt successfully.[/green]")
|
||||
except (KeyboardInterrupt, Exception) as e:
|
||||
if not isinstance(e, KeyboardInterrupt):
|
||||
console.print(f"[red]Error updating site: {e}[/red]")
|
||||
else:
|
||||
console.print("\n[yellow]Update cancelled.[/yellow]")
|
||||
elif action == "export":
|
||||
export_site(selected)
|
||||
elif action == "delete":
|
||||
@@ -1213,13 +1565,19 @@ def main(argv=None):
|
||||
parser = argparse.ArgumentParser(prog="voxvera")
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
default=str(ROOT / "src" / "config.json"),
|
||||
default=str(DATA_DIR / "config.json"),
|
||||
help="Path to config.json",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--lang", "--language", "--sprache", "--idioma",
|
||||
help="Force language code (e.g. en, es, de)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version", "-V",
|
||||
action="version",
|
||||
version=f"VoxVera {__version__}",
|
||||
help="Show version information and exit",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
|
||||
sub.add_parser("lang", help="Change language preference")
|
||||
@@ -1262,8 +1620,17 @@ def main(argv=None):
|
||||
sub.add_parser("build-docs", help="Generate localized documentation from templates")
|
||||
sub.add_parser("build-site", help="Refresh the main site/ directory assets and bundle")
|
||||
sub.add_parser("manage", help="Manage VoxVera servers interactively")
|
||||
sub.add_parser("start-all", help="Start all configured sites (non-interactive)")
|
||||
sub.add_parser("stop-all", help="Stop all running sites (non-interactive)")
|
||||
sub.add_parser("autostart", help="Install/remove a systemd user service to start sites on boot")
|
||||
sub.add_parser("_internal_onionshare", help=argparse.SUPPRESS)
|
||||
|
||||
args, unknown_args = parser.parse_known_args(argv)
|
||||
|
||||
if args.command == "_internal_onionshare":
|
||||
_internal_onionshare()
|
||||
return
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
config_path = Path(args.config).resolve()
|
||||
|
||||
# Determine language
|
||||
@@ -1277,7 +1644,7 @@ def main(argv=None):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not current_lang and args.command in ["init", "quickstart", "manage"]:
|
||||
if not current_lang and args.command in ("init", "quickstart", "manage"):
|
||||
current_lang = choose_language()
|
||||
# Save choice to config if possible
|
||||
if config_path.exists():
|
||||
@@ -1314,7 +1681,6 @@ def main(argv=None):
|
||||
elif not args.non_interactive:
|
||||
interactive_update(config_path)
|
||||
build_assets(config_path)
|
||||
serve(config_path)
|
||||
elif args.command == "build":
|
||||
build_assets(config_path, download_path=args.download)
|
||||
elif args.command == "export":
|
||||
@@ -1331,6 +1697,12 @@ def main(argv=None):
|
||||
batch_import_configs()
|
||||
elif args.command == "manage":
|
||||
manage_servers()
|
||||
elif args.command == "start-all":
|
||||
start_all_servers()
|
||||
elif args.command == "stop-all":
|
||||
stop_all_servers()
|
||||
elif args.command == "autostart":
|
||||
install_autostart()
|
||||
elif args.command == "quickstart":
|
||||
if not args.non_interactive:
|
||||
if not sys.stdin.isatty():
|
||||
@@ -1342,7 +1714,6 @@ def main(argv=None):
|
||||
sys.exit(1)
|
||||
interactive_update(config_path)
|
||||
build_assets(config_path)
|
||||
serve(config_path)
|
||||
elif args.command == "check":
|
||||
check_deps()
|
||||
elif args.command == "vendorize":
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"subtitle": "DO ~~NOT~~ DISTRIBUTE",
|
||||
"headline": "OPERATION VOX VERA",
|
||||
"content": "Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message. \n\nUse memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting. \n\nPrivacy can be maintained. Flyers can be shared via the Tor network, protecting hoster and users from censorship.\n\nJoin us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.\n",
|
||||
"url_message": "Follow this link to learn more. Use Tor Browser.",
|
||||
"url": "https://voxvera.org/",
|
||||
"tear_off_link": "http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion",
|
||||
"url_message": "Get VoxVera and start spreading your message.",
|
||||
"url": "https://github.com/PR0M3TH3AN/VoxVera",
|
||||
"tear_off_link": "https://voxvera.org/",
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100"
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<title id="site-title">Vox Vera Printable Flyers</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-family: 'Courier New', Courier, 'Nimbus Mono L', 'Liberation Mono', 'Noto Sans Arabic', 'Noto Sans Hebrew', 'Noto Sans Devanagari', 'Noto Sans JP', 'Noto Sans SC', 'Noto Sans Arabic UI', monospace;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
@@ -19,6 +19,45 @@
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* RTL Layout Overrides */
|
||||
[dir="rtl"] .container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
[dir="rtl"] .left-tear-offs {
|
||||
border-right: none;
|
||||
border-left: 1px dashed #000;
|
||||
}
|
||||
|
||||
[dir="rtl"] .tear-off {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
[dir="rtl"] .qr-code {
|
||||
margin-left: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .qr-code-body {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
[dir="rtl"] .qr-code-url {
|
||||
text-align: right;
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
[dir="rtl"] .message {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .text-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ... existing styles ... */
|
||||
|
||||
a {
|
||||
color: #b80000;
|
||||
text-decoration: none;
|
||||
@@ -87,12 +126,14 @@
|
||||
|
||||
.tear-off {
|
||||
display: flex;
|
||||
height: 1in; /* Fixed height for consistency */
|
||||
padding: 5px;
|
||||
border: 1px dashed #000;
|
||||
font-size: 10px;
|
||||
box-sizing: border-box;
|
||||
page-break-inside: avoid;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tear-off-text {
|
||||
@@ -143,11 +184,19 @@
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
letter-spacing: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.distribute {
|
||||
font-size: 18px;
|
||||
letter-spacing: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.redacted {
|
||||
@@ -155,15 +204,15 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
flex: 1;
|
||||
height: 6in; /* Fixed height to prevent footer push-out */
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
white-space: pre-wrap;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
@@ -281,10 +330,55 @@
|
||||
padding: 20px 0 30px;
|
||||
}
|
||||
|
||||
/* Dropdown styles */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
background-color: #f9f9f9;
|
||||
min-width: 200px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
border: 1px solid #000;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dropdown-content a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown:hover .download-button {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
// VoxVera Multi-Language Engine
|
||||
const locales = {"de": {"meta": {"language_name": "Deutsch", "flag": "\ud83c\udde9\ud83c\uddea"}, "web": {"tear_off_tor_browser": "Tor-Browser verwenden", "tear_off_desc": "um diese Nachricht zu verbreiten, gehen Sie zu", "tear_off_footer": "Klicken Sie auf die Schaltfl\u00e4che unten zum Drucken", "built_with": "erstellt mit", "print_button": "Flyer drucken", "download_button": "Tool & Quellcode herunterladen"}}, "en": {"meta": {"language_name": "English", "flag": "\ud83c\uddfa\ud83c\uddf8"}, "web": {"tear_off_tor_browser": "Use Tor Browser", "tear_off_desc": "to help spread this message go to", "tear_off_footer": "click the button below to print", "built_with": "built with", "print_button": "Print this page", "download_button": "Download Tool & Source"}}, "es": {"meta": {"language_name": "Espa\u00f1ol", "flag": "\ud83c\uddea\ud83c\uddf8"}, "web": {"tear_off_tor_browser": "Use el Navegador Tor", "tear_off_desc": "para ayudar a difundir este mensaje vaya a", "tear_off_footer": "haga clic en el bot\u00f3n de abajo para imprimir", "built_with": "construido con", "print_button": "Imprimir volante", "download_button": "Descargar Herramienta y C\u00f3digo"}}}; // Injected at build time
|
||||
const locales = {"ar": {"meta": {"language_name": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", "flag": "\ud83c\uddf8\ud83c\udde6", "direction": "rtl"}, "web": {"tear_off_tor_browser": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u062a\u0635\u0641\u062d Tor", "tear_off_desc": "\u0644\u0644\u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0646\u0634\u0631 \u0647\u0630\u0647 \u0627\u0644\u0631\u0633\u0627\u0644\u0629\u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649", "tear_off_footer": "\u0627\u0646\u0642\u0631 \u0641\u0648\u0642 \u0627\u0644\u0632\u0631 \u0623\u062f\u0646\u0627\u0647 \u0644\u0644\u0637\u0628\u0627\u0639\u0629", "built_with": "\u0628\u0646\u064a \u0628\u0648\u0627\u0633\u0637\u0629", "print_button": "\u0627\u0637\u0628\u0639 \u0627\u0644\u0645\u0646\u0634\u0648\u0631", "download_button": "\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0623\u062f\u0627\u0629 \u0648\u0627\u0644\u0643\u0648\u062f"}, "landing": {"title": "\u0633\u0631\u064a \u0644\u0644\u063a\u0627\u064a\u0629", "subtitle": "~~\u0644\u0627~~ \u0644\u0644\u0646\u0634\u0631", "headline": "\u0639\u0645\u0644\u064a\u0629 VOX VERA", "content": "\u062a\u062d\u0631\u0631 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629 \u0645\u0639 VoxVera. \u0623\u062f\u0627\u0629 \u062a\u0633\u0648\u064a\u0642 \u0639\u0635\u0627\u0628\u0627\u062a \u0645\u062c\u0647\u0648\u0644\u0629 \u0648\u0646\u0634\u0631 \u0631\u0633\u0627\u0626\u0644. \u0633\u0648\u0627\u0621 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0623\u0648 \u0641\u064a \u0627\u0644\u0639\u0627\u0644\u0645 \u0627\u0644\u0645\u0627\u062f\u064a\u060c \u064a\u0645\u0643\u0646\u0647\u0627 \u062a\u0645\u0643\u064a\u0646\u0643 \u0645\u0646 \u0646\u0634\u0631 \u0623\u0641\u0643\u0627\u0631\u0643 \u0628\u062c\u0631\u0623\u0629\u060c \u0645\u062d\u0645\u064a\u0629 \u0628\u0625\u062e\u0641\u0627\u0621 \u0627\u0644\u0647\u0648\u064a\u0629 \u0627\u0644\u0643\u0627\u0645\u0644 (\u0625\u0630\u0627 \u0643\u0646\u062a \u062a\u0633\u062a\u0636\u064a\u0641 \u0639\u0628\u0631 Tor). \u0642\u0645 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0643\u0648\u062f\u060c \u0648\u0635\u0645\u0645 \u0645\u0648\u0642\u0639 \u0645\u0646\u0634\u0648\u0631\u060c \u0648\u0627\u0633\u062a\u0636\u064a\u0641\u0647 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a \u0648\u0642\u0645 \u0628\u062a\u0636\u062e\u064a\u0645 \u0631\u0633\u0627\u0644\u062a\u0643.\n\n\u0627\u0633\u062a\u062e\u062f\u0645 \u0627\u0644\u0642\u0648\u0629 \u0627\u0644\u0645\u064a\u0645\u064a\u0629 \u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0623\u0641\u0643\u0627\u0631\u0643 \u0641\u064a \u0645\u062f\u0631\u0633\u062a\u0643\u060c \u0623\u0648 \u0645\u0643\u0627\u0646 \u0639\u0645\u0644\u0643\u060c \u0623\u0648 \u0645\u062c\u062a\u0645\u0639\u0627\u062a\u0643 \u0639\u0628\u0631 \u0627\u0644\u0625\u0646\u062a\u0631\u0646\u062a\u060c \u0623\u0648 \u062d\u062a\u0649 \u0639\u0644\u0649 \u0645\u0633\u062a\u0648\u0649 \u0627\u0644\u0639\u0627\u0644\u0645. \u062a\u0636\u0645\u0646 VoxVera \u062a\u0631\u062f\u062f \u0635\u062f\u0649 \u0631\u0633\u0627\u0644\u062a\u0643 \u0628\u0639\u064a\u062f\u0627\u064b \u0648\u0639\u0644\u0649 \u0646\u0637\u0627\u0642 \u0648\u0627\u0633\u0639\u060c \u0645\u0639 \u0623\u0642\u0633\u0627\u0645 \u0642\u0627\u0628\u0644\u0629 \u0644\u0644\u0646\u0632\u0639 \u062a\u062a\u0645\u064a\u0632 \u0628\u0631\u0648\u0627\u0628\u0637 \u0641\u0631\u064a\u062f\u0629 \u0648\u0623\u0643\u0648\u0627\u062f QR \u0644\u0633\u0647\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u0637\u0628\u0627\u0639\u0629.\n\n\u064a\u0645\u0643\u0646 \u0627\u0644\u062d\u0641\u0627\u0638 \u0639\u0644\u0649 \u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629. \u064a\u0645\u0643\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u0645\u0646\u0634\u0648\u0631\u0627\u062a \u0639\u0628\u0631 \u0634\u0628\u0643\u0629 Tor\u060c \u0645\u0645\u0627 \u064a\u062d\u0645\u064a \u0627\u0644\u0645\u0636\u064a\u0641 \u0648\u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645\u064a\u0646 \u0645\u0646 \u0627\u0644\u0631\u0642\u0627\u0628\u0629.\n\n\u0627\u0646\u0636\u0645 \u0625\u0644\u064a\u0646\u0627 \u0641\u064a \u062b\u0648\u0631\u0629 \u062a\u0642\u062f\u0631 \u0627\u0644\u062d\u0642\u064a\u0642\u0629 \u0648\u0627\u0644\u0634\u0641\u0627\u0641\u064a\u0629. \u0645\u0639\u0627\u064b\u060c \u064a\u0645\u0643\u0646\u0646\u0627 \u0628\u0646\u0627\u0621 \u0634\u0628\u0643\u0629 \u0645\u0646 \u0627\u0644\u0645\u0648\u0627\u0637\u0646\u064a\u0646 \u0627\u0644\u0645\u0633\u062a\u0646\u064a\u0631\u064a\u0646 \u0627\u0644\u0630\u064a\u0646 \u0644\u0627 \u064a\u062e\u0634\u0648\u0646 \u0627\u0644\u062a\u062d\u062f\u062b.", "url_message": "\u0627\u062d\u0635\u0644 \u0639\u0644\u0649 VoxVera \u0648\u0627\u0628\u062f\u0623 \u0628\u0646\u0634\u0631 \u0631\u0633\u0627\u0644\u062a\u0643.", "name": "Vox Vera Printable Flyers"}}, "de": {"meta": {"language_name": "Deutsch", "flag": "\ud83c\udde9\ud83c\uddea"}, "web": {"teLine truncated
|
||||
|
||||
function t(path, lang) {
|
||||
const parts = path.split('.');
|
||||
@@ -299,9 +393,25 @@
|
||||
function setLanguage(lang) {
|
||||
if (!locales[lang]) lang = 'en';
|
||||
localStorage.setItem('voxvera_lang', lang);
|
||||
|
||||
// Set text direction
|
||||
const direction = locales[lang].meta.direction || 'ltr';
|
||||
document.documentElement.dir = direction;
|
||||
document.documentElement.lang = lang;
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const path = el.getAttribute('data-i18n');
|
||||
el.innerText = t(path, lang);
|
||||
let translation = t(path, lang);
|
||||
if (translation) {
|
||||
// Support Markdown-style redaction ~~text~~ -> <span class="redacted">text</span>
|
||||
translation = translation.replace(/~~(.*?)~~/g, '<span class="redacted">$1</span>');
|
||||
|
||||
if (path.startsWith('landing.') || path.includes('subtitle') || path.includes('content')) {
|
||||
el.innerHTML = translation;
|
||||
} else {
|
||||
el.innerText = translation;
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById('lang-selector').value = lang;
|
||||
document.documentElement.lang = lang;
|
||||
@@ -333,7 +443,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -345,7 +455,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -356,7 +466,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -367,7 +477,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -378,7 +488,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -389,7 +499,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -400,7 +510,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -411,7 +521,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -422,7 +532,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -433,7 +543,7 @@
|
||||
<div class="tear-off-text">
|
||||
<span data-i18n="web.tear_off_tor_browser">Use Tor Browser</span><br><br>
|
||||
<span data-i18n="web.tear_off_desc">to help spread this message go to</span><br>
|
||||
<a class="tear-off-link" href="http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion" target="_blank">http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion</a><br>
|
||||
<a class="tear-off-link" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a><br>
|
||||
<span data-i18n="web.tear_off_footer">click the button below to print</span>
|
||||
</div>
|
||||
<div class="qr-code">
|
||||
@@ -442,12 +552,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1 id="top-secret-title">TOP SECRET</h1>
|
||||
<div class="distribute" id="do-not-distribute">DO <span class="redacted">NOT</span> DISTRIBUTE</div>
|
||||
<h1 id="operation-title-2">OPERATION VOX VERA</h1>
|
||||
<h1 id="top-secret-title" data-i18n="landing.title">TOP SECRET</h1>
|
||||
<div class="distribute" id="do-not-distribute" data-i18n="landing.subtitle">DO <span class="redacted">NOT</span> DISTRIBUTE</div>
|
||||
<h1 id="operation-title-2" data-i18n="landing.headline">OPERATION VOX VERA</h1>
|
||||
<hr>
|
||||
<div class="message">
|
||||
<div class="text-container" id="main-content" contenteditable="false">Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message.
|
||||
<div class="text-container" id="main-content" contenteditable="false" data-i18n="landing.content">Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message.
|
||||
|
||||
Use memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting.
|
||||
|
||||
@@ -456,9 +566,10 @@ Privacy can be maintained. Flyers can be shared via the Tor network, protecting
|
||||
Join us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.
|
||||
</div>
|
||||
<div class="qr-code-body">
|
||||
|
||||
<div class="qr-code-url">
|
||||
<span class="url-message">Follow this link to learn more. Use Tor Browser.</span><br><br>
|
||||
<a id="qr-link-2" href="https://voxvera.org/" target="_blank">https://voxvera.org/</a>
|
||||
<span class="url-message" data-i18n="landing.url_message">Get VoxVera and start spreading your message.</span><br><br>
|
||||
<a id="qr-link-2" href="https://github.com/PR0M3TH3AN/VoxVera" target="_blank">https://github.com/PR0M3TH3AN/VoxVera</a>
|
||||
</div>
|
||||
<img src="qrcode-content.png" alt="QR Code">
|
||||
</div>
|
||||
@@ -471,8 +582,10 @@ Join us in a revolution that values truth and transparency. Together, we can bui
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">Print Flyer</button>
|
||||
<a class="download-button" href="download/voxvera-portable.zip" download data-i18n="web.download_button">Download</a>
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">Print this page</button>
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">Download Source Code</button>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 376 B |
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] جاري الاتصال بمرحلات Tor وإعداد الخدمة المخفية...",
|
||||
"bootstrapping_progress": "[{name}] تمهيد Tor: {progress}% ...",
|
||||
"no_urls_qr_skip": "لم يتم تكوين أي روابط بعد. سيتم إنشاء أكواد QR بعد 'voxvera serve'",
|
||||
"input_file_not_found": "ملف الإدخال {path} غير موجود."
|
||||
"input_file_not_found": "ملف الإدخال {path} غير موجود.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "استخدم متصفح Tor",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "بني بواسطة",
|
||||
"print_button": "اطبع المنشور",
|
||||
"download_button": "تحميل الأداة والكود",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "سري للغاية",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "احصل على VoxVera وابدأ بنشر رسالتك.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,9 @@
|
||||
"bootstrapping_msg": "[{name}] Verbindung zu Tor-Relays herstellen und Hidden Service einrichten...",
|
||||
"bootstrapping_progress": "[{name}] Tor-Bootstrapping: {progress}% ...",
|
||||
"no_urls_qr_skip": "Noch keine URLs konfiguriert. QR-Codes werden nach 'voxvera serve' generiert",
|
||||
"input_file_not_found": "Eingabedatei {path} existiert nicht."
|
||||
"input_file_not_found": "Eingabedatei {path} existiert nicht.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor-Browser verwenden",
|
||||
@@ -80,11 +82,7 @@
|
||||
"built_with": "erstellt mit",
|
||||
"print_button": "Flyer drucken",
|
||||
"download_button": "Tool & Quellcode herunterladen",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "STRENG GEHEIM",
|
||||
@@ -94,4 +92,4 @@
|
||||
"url_message": "Holen Sie sich VoxVera und beginnen Sie, Ihre Nachricht zu verbreiten.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"url_label": "URL (content link - external resource)",
|
||||
"url_message_label": "URL message",
|
||||
"binary_message_label": "Binary message",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"template_not_found": "Template {name} not found",
|
||||
"template_copied": "Template copied to {dest}",
|
||||
"flyer_created": "Flyer files created under {dest}",
|
||||
@@ -64,6 +65,7 @@
|
||||
"manage_action_menu": "Manage {name}:",
|
||||
"manage_action_toggle_stop": "Stop",
|
||||
"manage_action_toggle_start": "Start",
|
||||
"manage_action_edit": "Edit",
|
||||
"manage_action_export": "Export to Zip",
|
||||
"manage_action_delete": "Delete",
|
||||
"manage_action_back": "Back",
|
||||
@@ -79,12 +81,8 @@
|
||||
"tear_off_footer": "click the button below to print",
|
||||
"built_with": "built with",
|
||||
"print_button": "Print this page",
|
||||
"download_button": "Download",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_button": "Download Source Code",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "TOP SECRET",
|
||||
|
||||
@@ -71,7 +71,9 @@
|
||||
"bootstrapping_msg": "[{name}] Conectando a repetidores Tor y configurando servicio oculto...",
|
||||
"bootstrapping_progress": "[{name}] Arranque de Tor: {progress}% ...",
|
||||
"no_urls_qr_skip": "No hay URLs configuradas todavía. Los códigos QR se generarán después de 'voxvera serve'",
|
||||
"input_file_not_found": "El archivo de entrada {path} no existe."
|
||||
"input_file_not_found": "El archivo de entrada {path} no existe.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Use el Navegador Tor",
|
||||
@@ -80,11 +82,7 @@
|
||||
"built_with": "construido con",
|
||||
"print_button": "Imprimir volante",
|
||||
"download_button": "Descargar Herramienta y Código",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "ALTO SECRETO",
|
||||
@@ -94,4 +92,4 @@
|
||||
"url_message": "Obtenga VoxVera y comience a difundir su mensaje.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] در حال اتصال به رلههای Tor و راهاندازی سرویس مخفی...",
|
||||
"bootstrapping_progress": "[{name}] راهاندازی Tor: {progress}% ...",
|
||||
"no_urls_qr_skip": "هنوز آدرسی پیکربندی نشده است. کدهای QR پس از 'voxvera serve' ایجاد میشوند",
|
||||
"input_file_not_found": "فایل ورودی {path} وجود ندارد."
|
||||
"input_file_not_found": "فایل ورودی {path} وجود ندارد.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "از مرورگر Tor استفاده کنید",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "ساخته شده با",
|
||||
"print_button": "چاپ اعلامیه",
|
||||
"download_button": "دانلود ابزار و کد منبع",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "بهکلی محرمانه",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "VoxVera را دریافت کنید و گسترش پیام خود را آغاز کنید.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Connexion aux relais Tor et configuration du service caché...",
|
||||
"bootstrapping_progress": "[{name}] Amorçage de Tor : {progress}% ...",
|
||||
"no_urls_qr_skip": "Aucune URL configurée pour le moment. Les codes QR seront générés après 'voxvera serve'",
|
||||
"input_file_not_found": "Le fichier d'entrée {path} n'existe pas."
|
||||
"input_file_not_found": "Le fichier d'entrée {path} n'existe pas.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Utilisez le navigateur Tor",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "créé avec",
|
||||
"print_button": "Imprimer le flyer",
|
||||
"download_button": "Télécharger l'outil et le code",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "TOP SECRET",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "Obtenez VoxVera et commencez à diffuser votre message.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] מתחבר לממסרי Tor ומגדיר שירות נסתר...",
|
||||
"bootstrapping_progress": "[{name}] איתחול Tor: {progress}% ...",
|
||||
"no_urls_qr_skip": "עדיין לא הוגדרו כתובות URL. קודי QR ייווצרו לאחר 'voxvera serve'",
|
||||
"input_file_not_found": "קובץ הקלט {path} אינו קיים."
|
||||
"input_file_not_found": "קובץ הקלט {path} אינו קיים.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "השתמש בדפדפן Tor",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "נבנה עם",
|
||||
"print_button": "הדפס פלייר",
|
||||
"download_button": "הורד כלי וקוד מקור",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "סודי ביותר",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "קבלו את VoxVera והתחילו להפיץ את המסר שלכם.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Tor रिले से जुड़ रहा है और हिडन सर्विस सेटअप कर रहा है...",
|
||||
"bootstrapping_progress": "[{name}] Tor बूटस्ट्रैपिंग: {progress}% ...",
|
||||
"no_urls_qr_skip": "अभी तक कोई URL कॉन्फ़िगर नहीं किया गया है। QR कोड 'voxvera serve' के बाद जेनरेट किए जाएंगे",
|
||||
"input_file_not_found": "इनपुट फ़ाइल {path} मौजूद नहीं है।"
|
||||
"input_file_not_found": "इनपुट फ़ाइल {path} मौजूद नहीं है।",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor Browser का उपयोग करें",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "बनाया गया",
|
||||
"print_button": "फ्लायर प्रिंट करें",
|
||||
"download_button": "टूल और कोड डाउनलोड करें",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "अति गोपनीय",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "VoxVera प्राप्त करें और अपना संदेश फैलाना शुरू करें।",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Tor リレーに接続し、隠しサービスをセットアップ中...",
|
||||
"bootstrapping_progress": "[{name}] Tor 起動中: {progress}% ...",
|
||||
"no_urls_qr_skip": "URLがまだ設定されていません。QRコードは 'voxvera serve' の後に生成されます",
|
||||
"input_file_not_found": "入力ファイル {path} が存在しません。"
|
||||
"input_file_not_found": "入力ファイル {path} が存在しません。",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor Browserを使用してください",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "built with",
|
||||
"print_button": "フライヤーを印刷",
|
||||
"download_button": "ツールとソースをダウンロード",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "極秘事項",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "VoxVeraを入手して、メッセージの拡散を始めましょう。",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Conectando aos repetidores Tor e configurando serviço oculto...",
|
||||
"bootstrapping_progress": "[{name}] Inicialização do Tor: {progress}% ...",
|
||||
"no_urls_qr_skip": "Nenhuma URL configurada ainda. Os códigos QR serão gerados após 'voxvera serve'",
|
||||
"input_file_not_found": "Arquivo de entrada {path} não existe."
|
||||
"input_file_not_found": "Arquivo de entrada {path} não existe.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Use o Tor Browser",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "construído com",
|
||||
"print_button": "Imprimir Panfleto",
|
||||
"download_button": "Baixar Ferramenta e Código",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "ALTO SECRETO",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "Obtenha o VoxVera e comece a espalhar sua mensagem.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Подключение к реле Tor и настройка скрытого сервиса...",
|
||||
"bootstrapping_progress": "[{name}] Загрузка Tor: {progress}% ...",
|
||||
"no_urls_qr_skip": "URL еще не настроены. QR-коды будут созданы после 'voxvera serve'",
|
||||
"input_file_not_found": "Входной файл {path} не существует."
|
||||
"input_file_not_found": "Входной файл {path} не существует.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Используйте Tor Browser",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "создано с помощью",
|
||||
"print_button": "Печать листовки",
|
||||
"download_button": "Скачать инструмент и код",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "СОВЕРШЕННО СЕКРЕТНО",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "Получите VoxVera и начните распространять свое сообщение.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Inajunganisha na seva za Tor na kuandaa huduma ya siri...",
|
||||
"bootstrapping_progress": "[{name}] Tor Inapakia: {progress}% ...",
|
||||
"no_urls_qr_skip": "Hakuna URL zilizowekwa bado. QR codes zitaundwa baada ya 'voxvera serve'",
|
||||
"input_file_not_found": "Faili ya kuingiza {path} haipo."
|
||||
"input_file_not_found": "Faili ya kuingiza {path} haipo.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tumia Kivinjari cha Tor",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "imejengwa na",
|
||||
"print_button": "Chapa Kipeperushi",
|
||||
"download_button": "Pakua Zana na Chanzo",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "SIRI KUBWA",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "Pata VoxVera na uanze kueneza ujumbe wako.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] Tor rölelerine bağlanılıyor ve gizli servis kuruluyor...",
|
||||
"bootstrapping_progress": "[{name}] Tor Başlatılıyor: %{progress} ...",
|
||||
"no_urls_qr_skip": "Henüz URL yapılandırılmadı. QR kodları 'voxvera serve' komutundan sonra oluşturulacak",
|
||||
"input_file_not_found": "{path} giriş dosyası mevcut değil."
|
||||
"input_file_not_found": "{path} giriş dosyası mevcut değil.",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "Tor Tarayıcı Kullanın",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "ile yapıldı",
|
||||
"print_button": "İlanı Yazdır",
|
||||
"download_button": "Aracı ve Kaynak Kodunu İndir",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "ÇOK GİZLİ",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "VoxVera'yı edinin ve mesajınızı yaymaya başlayın.",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,9 @@
|
||||
"bootstrapping_msg": "[{name}] 正在连接 Tor 中继并设置隐藏服务...",
|
||||
"bootstrapping_progress": "[{name}] Tor 引导中: {progress}% ...",
|
||||
"no_urls_qr_skip": "尚未配置 URL。QR 码将在 'voxvera serve' 后生成",
|
||||
"input_file_not_found": "输入文件 {path} 不存在。"
|
||||
"input_file_not_found": "输入文件 {path} 不存在。",
|
||||
"attachment_path_label": "Attachment file path (optional - will be served from Tor site)",
|
||||
"manage_action_edit": "Edit"
|
||||
},
|
||||
"web": {
|
||||
"tear_off_tor_browser": "使用 Tor 浏览器",
|
||||
@@ -81,11 +83,7 @@
|
||||
"built_with": "构建自",
|
||||
"print_button": "打印传单",
|
||||
"download_button": "下载工具与源码",
|
||||
"dl_portable": "Portable (Source + Dependencies)",
|
||||
"dl_linux": "Linux Standalone",
|
||||
"dl_windows": "Windows Standalone (.exe)",
|
||||
"dl_macos": "macOS Standalone",
|
||||
"dl_source": "Source Code Only (.zip)"
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "绝密",
|
||||
@@ -95,4 +93,4 @@
|
||||
"url_message": "获取 VoxVera 并开始传播你的消息。",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
placeholder
|
||||
@@ -0,0 +1 @@
|
||||
lyrebird
|
||||
@@ -1 +0,0 @@
|
||||
placeholder
|
||||
@@ -0,0 +1 @@
|
||||
lyrebird
|
||||
@@ -1 +0,0 @@
|
||||
placeholder
|
||||
@@ -0,0 +1 @@
|
||||
lyrebird.exe
|
||||
@@ -2,11 +2,13 @@
|
||||
"name": "Vox Vera Printable Flyers",
|
||||
"folder_name": "voxvera",
|
||||
"title": "TOP SECRET",
|
||||
"subtitle": "DO <span class=\"redacted\">NOT</span> DISTRIBUTE",
|
||||
"subtitle": "DO ~~NOT~~ DISTRIBUTE",
|
||||
"headline": "OPERATION VOX VERA",
|
||||
"content": "Break free from censorship with VoxVera. An anonymous guerrilla marketing and message-spreading tool. Whether online or in the physical world, it can empower you to spread your ideas boldly, shielded by complete anonymity (if you host over Tor). Download the code, design a flyer site, host it online and amplify your message. \n\nUse memetic power to share your ideas in your school, workplace, online communities, or even globally. VoxVera ensures your message resonates far and wide, with tear-off sections featuring unique URLs and QR codes for easy reprinting. \n\nPrivacy can be maintained. Flyers can be shared via the Tor network, protecting hoster and users from censorship.\n\nJoin us in a revolution that values truth and transparency. Together, we can build a network of informed citizens who are unafraid to speak out.\n",
|
||||
"url_message": "Follow this link to learn more. Use Tor Browser.",
|
||||
"url": "https://voxvera.org/",
|
||||
"tear_off_link": "http://5gwnlvdopooskthgkvgiwdrcmdgd5uv7yrkajpwa6jdrz6oxnhyjdeqd.onion",
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100"
|
||||
}
|
||||
"url_message": "Get VoxVera and start spreading your message.",
|
||||
"url": "https://github.com/PR0M3TH3AN/VoxVera",
|
||||
"tear_off_link": "https://voxvera.org/",
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100",
|
||||
"attachment_path": "",
|
||||
"attachment_filename": ""
|
||||
}
|
||||
@@ -576,16 +576,9 @@
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="print-button" onclick="window.print()" data-i18n="web.print_button">{{t_web_print_button}}</button>
|
||||
<div class="dropdown">
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
|
||||
<div class="dropdown-content">
|
||||
<a href="download/voxvera-portable.zip" download data-i18n="web.dl_portable">Portable (Source + Dependencies)</a>
|
||||
<a href="download/voxvera-linux" download data-i18n="web.dl_linux">Linux Standalone</a>
|
||||
<a href="download/voxvera-windows.exe" download data-i18n="web.dl_windows">Windows Standalone (.exe)</a>
|
||||
<a href="download/voxvera-macos" download data-i18n="web.dl_macos">macOS Standalone</a>
|
||||
<a href="download/voxvera-source.zip" download data-i18n="web.dl_source">Source Code Only (.zip)</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 376 B |
@@ -8,5 +8,7 @@
|
||||
"url_message": "Follow this link to learn more. Use Tor Browser.",
|
||||
"url": "https://voxvera.org/",
|
||||
"tear_off_link": "https://voxvera.org/",
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100"
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100",
|
||||
"attachment_path": "",
|
||||
"attachment_filename": ""
|
||||
}
|
||||