Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35069e24c5 | ||
|
|
51ab72239f | ||
|
|
4e571b62ea | ||
|
|
3bb8627d20 | ||
|
|
4e84466d38 | ||
|
|
f530e54f45 | ||
|
|
8be7d0761b | ||
|
|
6d747f719f | ||
|
|
dc83b76317 | ||
|
|
9c699eb36b | ||
|
|
3cba4c7e9f | ||
|
|
69ca9f6411 | ||
|
|
994744ee60 | ||
|
|
15f881e603 | ||
|
|
8662e2b16d | ||
|
|
71ae96c841 | ||
|
|
765c0fa2a0 | ||
|
|
d816880073 | ||
|
|
fef1f2b5a8 | ||
|
|
6105ca7a18 | ||
|
|
9d1210cd51 | ||
|
|
89a92a2a2d |
No files matched your search
@@ -34,8 +34,10 @@ 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
|
||||
@@ -48,6 +50,22 @@ jobs:
|
||||
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" \
|
||||
@@ -126,5 +144,6 @@ jobs:
|
||||
files: |
|
||||
release_assets/*
|
||||
fail_on_unmatched_files: false
|
||||
overwrite: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -76,15 +76,25 @@ if [ "${VOXVERA_E2E_OFFLINE:-}" != "1" ]; then
|
||||
TOR_PID=$!
|
||||
|
||||
# Wait for Tor to bootstrap
|
||||
echo "Waiting for Tor to bootstrap..."
|
||||
for i in {1..60}; do
|
||||
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
|
||||
@@ -97,6 +107,15 @@ if [ "${VOXVERA_E2E_OFFLINE:-}" != "1" ]; then
|
||||
"$host_dir" >"$LOG_DIR/onionshare.log" 2>&1 &
|
||||
OS_PID=$!
|
||||
|
||||
# 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
|
||||
|
||||
# Wait for URL with timeout and feedback
|
||||
echo "Waiting for OnionShare to generate URL (up to 120s)..."
|
||||
URL=""
|
||||
@@ -118,17 +137,41 @@ if [ "${VOXVERA_E2E_OFFLINE:-}" != "1" ]; then
|
||||
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 -q "$URL" "$host_dir/config.json"; then
|
||||
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
|
||||
|
||||
@@ -96,6 +96,28 @@ 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 can shadow system packages (e.g. Werkzeug) and break onionshare-cli.
|
||||
msg "Cleaning up stale user-local packages that may conflict with system onionshare-cli..."
|
||||
PIP_CLEANUP_OPTS=""
|
||||
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_CLEANUP_OPTS="--break-system-packages"
|
||||
fi
|
||||
fi
|
||||
for pkg in werkzeug flask flask-socketio onionshare-cli; do
|
||||
if pip3 show --quiet "$pkg" 2>/dev/null && \
|
||||
pip3 show "$pkg" 2>/dev/null | grep -q "Location.*\.local"; then
|
||||
pip3 uninstall -y $PIP_CLEANUP_OPTS "$pkg" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
# Also uninstall any previous pip --user install of voxvera itself
|
||||
if pip3 show --quiet voxvera 2>/dev/null && \
|
||||
pip3 show voxvera 2>/dev/null | grep -q "Location.*\.local"; then
|
||||
pip3 uninstall -y $PIP_CLEANUP_OPTS voxvera 2>/dev/null || true
|
||||
fi
|
||||
|
||||
download_binary() {
|
||||
local url=$1
|
||||
local dest=$2
|
||||
@@ -137,15 +159,18 @@ if command_exists curl; then
|
||||
# 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")
|
||||
set +e
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest")
|
||||
curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
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
|
||||
warn "Binary download failed (status: $status). Falling back to source installation."
|
||||
warn "Binary download failed (status: $status, exit: $curl_exit). Falling back to source installation."
|
||||
[ -f "$dest" ] && rm "$dest"
|
||||
fi
|
||||
fi
|
||||
@@ -153,6 +178,9 @@ fi
|
||||
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'."
|
||||
|
||||
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.2.0"
|
||||
version = "0.2.18"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
@@ -28,7 +28,7 @@ dependencies = [
|
||||
"htmlmin2",
|
||||
"pypdf",
|
||||
"wcwidth",
|
||||
"onionshare-cli",
|
||||
"setuptools",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
||||
@@ -7,4 +7,6 @@ htmlmin2
|
||||
pypdf
|
||||
wcwidth
|
||||
onionshare-cli
|
||||
flask<2.3.0
|
||||
werkzeug<3.0.0
|
||||
|
||||
@@ -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
|
||||
@@ -129,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()
|
||||
|
||||
|
||||
@@ -143,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"
|
||||
@@ -197,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
|
||||
@@ -232,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()
|
||||
@@ -285,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()
|
||||
@@ -314,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()
|
||||
@@ -339,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()
|
||||
@@ -400,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()
|
||||
|
||||
@@ -417,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()
|
||||
@@ -107,13 +107,18 @@ 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")
|
||||
# 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, "_locale_res", lambda *p: tmp_path / "locales" / 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 = {
|
||||
@@ -129,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)
|
||||
|
||||
@@ -203,12 +208,17 @@ 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")
|
||||
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", 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))
|
||||
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",
|
||||
@@ -223,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)
|
||||
|
||||
@@ -333,12 +343,17 @@ 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")
|
||||
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", 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))
|
||||
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",
|
||||
@@ -353,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)
|
||||
|
||||
@@ -383,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(
|
||||
{
|
||||
@@ -407,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)
|
||||
@@ -440,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))
|
||||
|
||||
|
||||
@@ -87,15 +87,18 @@ install_voxvera() {
|
||||
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" || echo "failed")
|
||||
status=$(curl -w "%{http_code}" -fsSL "$url" -o "$dest")
|
||||
local curl_exit=$?
|
||||
set -e
|
||||
|
||||
if [ "$status" = "200" ]; then
|
||||
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). Falling back to source installation."
|
||||
warn "Binary download failed (status: $status, exit: $curl_exit). Falling back to source installation."
|
||||
[ -f "$dest" ] && rm "$dest"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.2.18"
|
||||
|
||||
# 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,12 +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 = {}
|
||||
@@ -171,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:
|
||||
@@ -366,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),
|
||||
},
|
||||
@@ -382,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))
|
||||
|
||||
@@ -403,9 +424,13 @@ 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)
|
||||
|
||||
@@ -451,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:
|
||||
@@ -525,11 +548,41 @@ 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)
|
||||
|
||||
# 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:
|
||||
@@ -543,16 +596,13 @@ def build_assets(
|
||||
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}")
|
||||
|
||||
|
||||
@@ -619,17 +669,26 @@ def _internal_onionshare():
|
||||
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)
|
||||
|
||||
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.
|
||||
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 is ['voxvera', '_internal_onionshare', '--website', ...]
|
||||
# We need to make it look like ['onionshare-cli', '--website', ...]
|
||||
sys.argv = ["onionshare-cli"] + sys.argv[2:]
|
||||
sys.argv = ["onionshare-cli"] + onionshare_args
|
||||
sys.exit(main())
|
||||
except ImportError:
|
||||
print("Error: onionshare-cli not bundled or installed.", file=sys.stderr)
|
||||
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)
|
||||
|
||||
|
||||
@@ -637,7 +696,7 @@ 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)
|
||||
@@ -648,11 +707,7 @@ def serve(config_path: str) -> str | None:
|
||||
env["TOR_SOCKS_PORT"] = socks
|
||||
env["TOR_CONTROL_PORT"] = ctl
|
||||
|
||||
# Run the bundled onionshare-cli via our hidden command
|
||||
# sys.argv[0] is the current executable (python script or PyInstaller binary)
|
||||
cmd = [
|
||||
sys.argv[0],
|
||||
"_internal_onionshare",
|
||||
onionshare_args = [
|
||||
"--website",
|
||||
"--public",
|
||||
"--persistent",
|
||||
@@ -664,10 +719,16 @@ def serve(config_path: str) -> str | None:
|
||||
str(dir_path),
|
||||
]
|
||||
|
||||
# If running via 'python -m voxvera.cli', sys.argv[0] might be the script path,
|
||||
# so we should ensure it's executed correctly if not a PyInstaller binary.
|
||||
if not getattr(sys, 'frozen', False) and sys.argv[0].endswith('.py'):
|
||||
cmd = [sys.executable, sys.argv[0]] + cmd[1:]
|
||||
# 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)
|
||||
@@ -755,9 +816,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:
|
||||
@@ -813,7 +874,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:
|
||||
@@ -904,25 +965,31 @@ def build_docs():
|
||||
|
||||
def build_site():
|
||||
"""Refresh the main site/ directory with the latest template, QRs, and portable bundle."""
|
||||
# When frozen, site/ source is in sys._MEIPASS, but we might want to output to CWD/site
|
||||
if getattr(sys, 'frozen', False):
|
||||
site_dir = Path(sys._MEIPASS).joinpath("site")
|
||||
src_site = Path(sys._MEIPASS).joinpath("site")
|
||||
dest_site = Path.cwd() / "site"
|
||||
else:
|
||||
site_dir = ROOT.parent / "site"
|
||||
src_site = ROOT.parent / "site"
|
||||
dest_site = src_site
|
||||
|
||||
if not site_dir.exists():
|
||||
print(f"Error: {site_dir} directory not found.")
|
||||
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(f"Error: {config_path} 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)
|
||||
@@ -1002,11 +1069,12 @@ def build_site():
|
||||
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)
|
||||
|
||||
# 5. Create the download assets in site/download/
|
||||
download_dir = site_dir / "download"
|
||||
download_dir = dest_site / "download"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
# Bundle source version
|
||||
@@ -1046,32 +1114,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:
|
||||
@@ -1110,7 +1195,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
|
||||
@@ -1132,7 +1217,7 @@ 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}")
|
||||
@@ -1143,6 +1228,9 @@ def manage_servers():
|
||||
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()
|
||||
|
||||
@@ -1154,7 +1242,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})"
|
||||
@@ -1179,7 +1267,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")
|
||||
@@ -1203,7 +1291,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:
|
||||
@@ -1235,6 +1323,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"))
|
||||
@@ -1249,11 +1338,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":
|
||||
@@ -1302,7 +1403,7 @@ 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(
|
||||
@@ -1415,7 +1516,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":
|
||||
@@ -1443,7 +1543,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "انقر فوق الزر أدناه للطباعة",
|
||||
"built_with": "بني بواسطة",
|
||||
"print_button": "اطبع المنشور",
|
||||
"download_button": "تحميل الأداة والكود"
|
||||
"download_button": "تحميل الأداة والكود",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "سري للغاية",
|
||||
@@ -90,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",
|
||||
@@ -79,7 +81,8 @@
|
||||
"tear_off_footer": "Klicken Sie auf die Schaltfläche unten zum Drucken",
|
||||
"built_with": "erstellt mit",
|
||||
"print_button": "Flyer drucken",
|
||||
"download_button": "Tool & Quellcode herunterladen"
|
||||
"download_button": "Tool & Quellcode herunterladen",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "STRENG GEHEIM",
|
||||
@@ -89,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,7 +81,8 @@
|
||||
"tear_off_footer": "click the button below to print",
|
||||
"built_with": "built with",
|
||||
"print_button": "Print this page",
|
||||
"download_button": "Download Source Code"
|
||||
"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",
|
||||
@@ -79,7 +81,8 @@
|
||||
"tear_off_footer": "haga clic en el botón de abajo para imprimir",
|
||||
"built_with": "construido con",
|
||||
"print_button": "Imprimir volante",
|
||||
"download_button": "Descargar Herramienta y Código"
|
||||
"download_button": "Descargar Herramienta y Código",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "ALTO SECRETO",
|
||||
@@ -89,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 استفاده کنید",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "برای چاپ روی دکمه زیر کلیک کنید",
|
||||
"built_with": "ساخته شده با",
|
||||
"print_button": "چاپ اعلامیه",
|
||||
"download_button": "دانلود ابزار و کد منبع"
|
||||
"download_button": "دانلود ابزار و کد منبع",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "بهکلی محرمانه",
|
||||
@@ -90,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "cliquez sur le bouton ci-dessous pour imprimer",
|
||||
"built_with": "créé avec",
|
||||
"print_button": "Imprimer le flyer",
|
||||
"download_button": "Télécharger l'outil et le code"
|
||||
"download_button": "Télécharger l'outil et le code",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "TOP SECRET",
|
||||
@@ -90,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "לחץ על הכפתור למטה להדפסה",
|
||||
"built_with": "נבנה עם",
|
||||
"print_button": "הדפס פלייר",
|
||||
"download_button": "הורד כלי וקוד מקור"
|
||||
"download_button": "הורד כלי וקוד מקור",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "סודי ביותר",
|
||||
@@ -90,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 का उपयोग करें",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "प्रिंट करने के लिए नीचे दिए गए बटन पर क्लिक करें",
|
||||
"built_with": "बनाया गया",
|
||||
"print_button": "फ्लायर प्रिंट करें",
|
||||
"download_button": "टूल और कोड डाउनलोड करें"
|
||||
"download_button": "टूल और कोड डाउनलोड करें",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "अति गोपनीय",
|
||||
@@ -90,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を使用してください",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "下のボタンをクリックして印刷してください",
|
||||
"built_with": "built with",
|
||||
"print_button": "フライヤーを印刷",
|
||||
"download_button": "ツールとソースをダウンロード"
|
||||
"download_button": "ツールとソースをダウンロード",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "極秘事項",
|
||||
@@ -90,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "clique no botão abaixo para imprimir",
|
||||
"built_with": "construído com",
|
||||
"print_button": "Imprimir Panfleto",
|
||||
"download_button": "Baixar Ferramenta e Código"
|
||||
"download_button": "Baixar Ferramenta e Código",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "ALTO SECRETO",
|
||||
@@ -90,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "нажмите кнопку ниже, чтобы распечатать",
|
||||
"built_with": "создано с помощью",
|
||||
"print_button": "Печать листовки",
|
||||
"download_button": "Скачать инструмент и код"
|
||||
"download_button": "Скачать инструмент и код",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "СОВЕРШЕННО СЕКРЕТНО",
|
||||
@@ -90,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "bonyeza kitufe hapa chini ili kuchapisha",
|
||||
"built_with": "imejengwa na",
|
||||
"print_button": "Chapa Kipeperushi",
|
||||
"download_button": "Pakua Zana na Chanzo"
|
||||
"download_button": "Pakua Zana na Chanzo",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "SIRI KUBWA",
|
||||
@@ -90,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",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "yazdırmak için aşağıdaki düğmeye tıklayın",
|
||||
"built_with": "ile yapıldı",
|
||||
"print_button": "İlanı Yazdır",
|
||||
"download_button": "Aracı ve Kaynak Kodunu İndir"
|
||||
"download_button": "Aracı ve Kaynak Kodunu İndir",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "ÇOK GİZLİ",
|
||||
@@ -90,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 浏览器",
|
||||
@@ -80,7 +82,8 @@
|
||||
"tear_off_footer": "点击下方按钮进行打印",
|
||||
"built_with": "构建自",
|
||||
"print_button": "打印传单",
|
||||
"download_button": "下载工具与源码"
|
||||
"download_button": "下载工具与源码",
|
||||
"download_attachment": "Download Attachment"
|
||||
},
|
||||
"landing": {
|
||||
"title": "绝密",
|
||||
@@ -90,4 +93,4 @@
|
||||
"url_message": "获取 VoxVera 并开始传播你的消息。",
|
||||
"name": "Vox Vera Printable Flyers"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,7 @@
|
||||
"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"
|
||||
"binary_message": "0110010 0101011 0110010 0111101 0110100",
|
||||
"attachment_path": "",
|
||||
"attachment_filename": ""
|
||||
}
|
||||
|
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": ""
|
||||
}
|
||||