Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -10,21 +10,18 @@ jobs:
|
||||
build:
|
||||
name: Build for ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
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
|
||||
@@ -38,7 +35,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
pip install pyinstaller pyinstaller-hooks-contrib
|
||||
|
||||
- name: Download Tor Binaries
|
||||
shell: bash
|
||||
@@ -51,6 +48,12 @@ jobs:
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then SEP=";"; else SEP=":"; fi
|
||||
pyinstaller --onefile \
|
||||
--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" \
|
||||
@@ -60,34 +63,75 @@ jobs:
|
||||
voxvera/cli.py
|
||||
|
||||
- name: Build .deb (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
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 }}
|
||||
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
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
dist/${{ matrix.artifact_name }}
|
||||
voxvera/resources/bin/*.deb
|
||||
voxvera/resources/bin/*.dmg
|
||||
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,7 +18,8 @@ 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
|
||||
- name: Upload logs
|
||||
|
||||
@@ -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
|
||||
@@ -1,14 +1 @@
|
||||
# Upcoming Release Notes
|
||||
|
||||
*This document serves as a running changelog for the NEXT version of VoxVera. AI agents must append notable changes here as they are completed. Once a release is explicitly requested by the user, these notes will be used to format the GitHub Release description, and this file should be cleared/reset for the next cycle.*
|
||||
|
||||
### Version: Unreleased
|
||||
|
||||
#### 🚀 New Features
|
||||
- *(Add new features here)*
|
||||
|
||||
#### 🛠 Improvements
|
||||
- *(Add improvements here)*
|
||||
|
||||
#### 🐛 Bug Fixes
|
||||
- *(Add bug fixes here)*
|
||||
@@ -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
|
||||
@@ -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,44 +32,68 @@ 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
|
||||
|
||||
download_binary() {
|
||||
@@ -69,119 +102,78 @@ download_binary() {
|
||||
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"
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest" || echo "failed")
|
||||
|
||||
if [ "$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). 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
|
||||
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."
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ARCH=$(uname -m)
|
||||
BINARY="voxvera/resources/bin/voxvera-linux-$ARCH"
|
||||
BINARY=${1:-"voxvera/resources/bin/voxvera-linux-$ARCH"}
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "Binary not found: $BINARY. Run scripts/build_binaries.sh first." >&2
|
||||
@@ -17,6 +17,10 @@ mkdir -p "$APPDIR/usr/bin"
|
||||
cp "$BINARY" "$APPDIR/usr/bin/voxvera"
|
||||
chmod +x "$APPDIR/usr/bin/voxvera"
|
||||
|
||||
# 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]
|
||||
@@ -41,6 +45,7 @@ chmod +x appimagetool
|
||||
# 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
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ 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"
|
||||
@@ -33,6 +33,7 @@ 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"
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ARCH=$(uname -m)
|
||||
BINARY="voxvera/resources/bin/voxvera-linux-$ARCH"
|
||||
BINARY=${1:-"voxvera/resources/bin/voxvera-linux-$ARCH"}
|
||||
|
||||
if [ ! -f "$BINARY" ]; then
|
||||
echo "Binary not found: $BINARY. Run scripts/build_binaries.sh first." >&2
|
||||
@@ -30,6 +30,7 @@ rm -rf "$BUILD_DIR" "$REPO_DIR"
|
||||
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"
|
||||
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.1.7"
|
||||
version = "0.2.3"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
|
||||
@@ -4,46 +4,70 @@ set -euo pipefail
|
||||
VERSION="${VERSION:-15.0.7}"
|
||||
BASE_URL="https://dist.torproject.org/torbrowser"
|
||||
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" == "aarch64" ] || [ "$ARCH" == "arm64" ]; then
|
||||
TOR_ARCH="aarch64"
|
||||
else
|
||||
TOR_ARCH="x86_64"
|
||||
fi
|
||||
# ARCH=$(uname -m) # Not needed for multi-platform download if we hardcode/loop targets
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux*) PLATFORM=linux; ARCHIVE="tor-expert-bundle-linux-${TOR_ARCH}-${VERSION}.tar.gz"; EXE=tor;;
|
||||
Darwin*) PLATFORM=mac; ARCHIVE="tor-expert-bundle-macos-${TOR_ARCH}-${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
|
||||
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
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
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
|
||||
|
||||
URL="$BASE_URL/${VERSION}/${ARCHIVE}"
|
||||
echo "Extracting..."
|
||||
if [[ "$archive" == *.zip ]]; then
|
||||
unzip -q "$tmpdir/$archive" -d "$tmpdir"
|
||||
else
|
||||
tar -xf "$tmpdir/$archive" -C "$tmpdir"
|
||||
fi
|
||||
|
||||
echo "Downloading $URL"
|
||||
curl -L "$URL" -o "$TMPDIR/$ARCHIVE"
|
||||
mkdir -p "$dest"
|
||||
|
||||
echo "Extracting..."
|
||||
tar -xf "$TMPDIR/$ARCHIVE" -C "$TMPDIR"
|
||||
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)
|
||||
|
||||
DEST="$(dirname "$0")/../voxvera/resources/tor/$PLATFORM"
|
||||
mkdir -p "$DEST"
|
||||
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
|
||||
|
||||
TOR_BIN=$(find "$TMPDIR" -type f -name "$EXE" | head -n 1)
|
||||
OBFS_BIN=$(find "$TMPDIR" -type f \( -name "lyrebird*" -o -name "obfs4proxy*" \) | head -n 1)
|
||||
echo "Installing binaries to $dest"
|
||||
cp "$tor_bin" "$dest/$(basename "$exe")"
|
||||
cp "$obfs_bin" "$dest/$(basename "$obfs_bin")"
|
||||
|
||||
if [[ -z "$TOR_BIN" || -z "$OBFS_BIN" ]]; then
|
||||
echo "Failed to locate tor or lyrebird/obfs4proxy in archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
|
||||
cp "$TOR_BIN" "$DEST/$(basename "$EXE")"
|
||||
cp "$OBFS_BIN" "$DEST/$(basename "$OBFS_BIN")"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
chmod +x "$DEST/$(basename "$EXE")" "$DEST/$(basename "$OBFS_BIN")"
|
||||
# Linux x86_64
|
||||
download_platform "linux" "x86_64" "tor-expert-bundle-linux-x86_64-${VERSION}.tar.gz" "tor"
|
||||
|
||||
echo "Installed binaries to $DEST"
|
||||
# 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"}, "landing": {"title": "TOP SECRET", "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": "Get VoxVera and start spreading your message.", "name": "Vox Vera Printable Flyers"}}, "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"}, "landing": {"title": "TOP SECRET", "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": "Get VoxVera and start spreading your message.", "name": "Vox Vera Printable Flyers"}}, "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 Source Code"}, "landing": {"title": "TOP SECRET", "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": "Get VoxVera and start spreading your message.", "name": "Vox Vera Printable Flyers"}}, "es": {"meta": {"language_name": "Espa\u00f1ol", "flag": "\ud83c\uddea\ud83c\uddf8"}, "web": {"tear_off_tor_browser": "Use el Navegador Tor",Line 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('.');
|
||||
|
||||
@@ -15,6 +15,7 @@ Requirements:
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
@@ -107,10 +108,12 @@ class TestFullWorkflow:
|
||||
# Setup temp workspace
|
||||
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 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, "_locale_res", lambda *p: tmp_path / "locales" / Path(*p))
|
||||
|
||||
# Create test config
|
||||
config = {
|
||||
@@ -201,8 +204,11 @@ class TestFullWorkflow:
|
||||
# Setup
|
||||
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))
|
||||
|
||||
config = {
|
||||
"name": "Reachability Test",
|
||||
@@ -230,6 +236,13 @@ class TestFullWorkflow:
|
||||
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 = [
|
||||
sys.executable,
|
||||
"-m", "voxvera.cli",
|
||||
@@ -241,7 +254,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
|
||||
@@ -321,8 +334,11 @@ class TestSiteFiles:
|
||||
# Setup
|
||||
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))
|
||||
|
||||
config = {
|
||||
"name": "Test",
|
||||
|
||||
@@ -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,113 @@ 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"
|
||||
local status
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest" || echo "failed")
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
chmod +x "$dest"
|
||||
msg "VoxVera binary (${ARCH}) installed to $dest"
|
||||
return 0
|
||||
else
|
||||
warn "Binary download failed (status: $status). 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 +135,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,13 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.1.7"
|
||||
__version__ = "0.2.3"
|
||||
|
||||
# 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 getattr(sys, 'frozen', False):
|
||||
vendor_dir = Path(sys._MEIPASS).joinpath("voxvera", "vendor")
|
||||
else:
|
||||
vendor_dir = Path(__file__).parent / "vendor"
|
||||
if vendor_dir.exists():
|
||||
if str(vendor_dir) not in sys.path:
|
||||
sys.path.append(str(vendor_dir))
|
||||
@@ -12,9 +12,13 @@ from importlib import resources
|
||||
from importlib.resources.abc import Traversable
|
||||
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")
|
||||
else:
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# Global locale state
|
||||
CURRENT_LOCALE = {}
|
||||
@@ -22,16 +26,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 +50,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
|
||||
@@ -444,6 +464,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:
|
||||
@@ -454,10 +482,12 @@ def build_assets(
|
||||
"landing": l_data.get("landing", {})
|
||||
}
|
||||
|
||||
# Override landing data with user custom config data so JS doesn't overwrite it
|
||||
# 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]:
|
||||
all_locales[code]["landing"][field] = 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))
|
||||
|
||||
@@ -480,6 +510,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)
|
||||
@@ -575,9 +612,18 @@ def _internal_onionshare():
|
||||
# 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"
|
||||
tor_dir = ROOT / "resources" / "tor" / platform_dir
|
||||
if tor_dir.exists():
|
||||
|
||||
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", "")
|
||||
# Robustness: explicitly tell OnionShare where Tor is
|
||||
os.environ["TOR_BINARY"] = str(bundled_tor)
|
||||
|
||||
try:
|
||||
from onionshare_cli import main
|
||||
@@ -808,9 +854,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
|
||||
@@ -820,7 +872,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
|
||||
@@ -855,14 +907,18 @@ 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 getattr(sys, 'frozen', False):
|
||||
site_dir = Path(sys._MEIPASS).joinpath("site")
|
||||
else:
|
||||
site_dir = ROOT.parent / "site"
|
||||
|
||||
if not site_dir.exists():
|
||||
print("Error: site/ directory not found.")
|
||||
print(f"Error: {site_dir} directory not found.")
|
||||
return
|
||||
|
||||
config_path = site_dir / "config.json"
|
||||
if not config_path.exists():
|
||||
print("Error: site/config.json not found.")
|
||||
print(f"Error: {config_path} not found.")
|
||||
return
|
||||
|
||||
print("Refreshing site/ assets...")
|
||||
@@ -873,13 +929,27 @@ def build_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:
|
||||
@@ -890,10 +960,12 @@ def build_site():
|
||||
"landing": l_data.get("landing", {})
|
||||
}
|
||||
|
||||
# Override landing data with user custom config data so JS doesn't overwrite it
|
||||
# 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]:
|
||||
all_locales[code]["landing"][field] = 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))
|
||||
|
||||
@@ -915,6 +987,13 @@ 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)
|
||||
@@ -1233,6 +1312,12 @@ def main(argv=None):
|
||||
"--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")
|
||||
|
||||
@@ -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 |
@@ -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,11 @@
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 376 B |