mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 17:50:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc83b76317 | ||
|
|
9c699eb36b | ||
|
|
3cba4c7e9f | ||
|
|
69ca9f6411 | ||
|
|
994744ee60 | ||
|
|
15f881e603 | ||
|
|
8662e2b16d | ||
|
|
71ae96c841 | ||
|
|
765c0fa2a0 |
No files matched your search
@@ -35,6 +35,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
pip install pyinstaller pyinstaller-hooks-contrib
|
||||
|
||||
- name: Download Tor Binaries
|
||||
@@ -48,6 +49,8 @@ jobs:
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then SEP=";"; else SEP=":"; fi
|
||||
pyinstaller --onefile \
|
||||
--hidden-import pkg_resources \
|
||||
--hidden-import setuptools \
|
||||
--collect-all onionshare_cli \
|
||||
--collect-all flask_socketio \
|
||||
--collect-all engineio \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
# VoxVera v0.2.11
|
||||
|
||||
## Changes and Improvements
|
||||
|
||||
### Logic and UI Fixes
|
||||
- **Attachment overriding URL:** Modified the core HTML template and config generation so that a provided file attachment fully replaces the external URL in the main content area and QR codes, eliminating redundancy. The "Download Attachment" button at the bottom was removed, keeping the main interface clean and focusing on the attachment directly via the primary URL slot.
|
||||
- **Interactive Prompt:** The interactive CLI now treats the URL and attachment path as an "either/or" choice conditionally, improving clarity.
|
||||
|
||||
### Stability and Bug Fixes
|
||||
- **Nano Terminal Crash:** Fixed an issue where the `nano` editor would crash with "Code: 11" when launched from the interactive interface within a PyInstaller binary. The application now properly inherits standard input/output streams for standard terminal editors.
|
||||
- **ModuleNotFoundError Fix:** Added explicit `pkg_resources` and `setuptools` hidden imports to the PyInstaller build configuration, permanently fixing an error where `onionshare-cli` would fail to initialize in the standalone binary with a `ModuleNotFoundError: No module named 'pkg_resources'`.
|
||||
+6
-3
@@ -137,15 +137,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
|
||||
|
||||
+3
-1
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.2.3"
|
||||
version = "0.2.11"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
@@ -29,6 +29,8 @@ dependencies = [
|
||||
"pypdf",
|
||||
"wcwidth",
|
||||
"onionshare-cli",
|
||||
"flask<2.3.0",
|
||||
"werkzeug<3.0.0",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
||||
@@ -7,4 +7,6 @@ htmlmin2
|
||||
pypdf
|
||||
wcwidth
|
||||
onionshare-cli
|
||||
flask<2.3.0
|
||||
werkzeug<3.0.0
|
||||
|
||||
+90
-50
@@ -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()
|
||||
+42
-23
@@ -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))
|
||||
|
||||
|
||||
+6
-3
@@ -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
|
||||
|
||||
+6
-3
@@ -1,13 +1,16 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.2.3"
|
||||
__version__ = "0.2.11"
|
||||
|
||||
# Add the vendor directory to sys.path as a fallback.
|
||||
# Add the vendor directory to sys.path. We prepend it to ensure our bundled
|
||||
# dependencies take priority over potentially incompatible system packages
|
||||
# (e.g. Flask/Werkzeug version mismatches on Debian).
|
||||
if getattr(sys, 'frozen', False):
|
||||
vendor_dir = Path(sys._MEIPASS).joinpath("voxvera", "vendor")
|
||||
else:
|
||||
vendor_dir = Path(__file__).parent / "vendor"
|
||||
|
||||
if vendor_dir.exists():
|
||||
if str(vendor_dir) not in sys.path:
|
||||
sys.path.append(str(vendor_dir))
|
||||
sys.path.insert(0, str(vendor_dir))
|
||||
+126
-63
@@ -17,9 +17,15 @@ from voxvera import __version__
|
||||
# package root (contains bundled templates and src/)
|
||||
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.cwd()))
|
||||
|
||||
# Global locale state
|
||||
CURRENT_LOCALE = {}
|
||||
|
||||
@@ -174,7 +180,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:
|
||||
@@ -369,11 +375,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),
|
||||
},
|
||||
@@ -385,15 +398,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))
|
||||
|
||||
@@ -406,9 +410,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)
|
||||
|
||||
@@ -454,8 +462,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:
|
||||
@@ -528,11 +534,43 @@ 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)
|
||||
# We don't replace the URL here if it's already an onion link (e.g. during serve rebuild)
|
||||
if not data.get("url", "").startswith("http://") or ".onion" not in data.get("url", ""):
|
||||
data["url"] = f"download/{att_filename}"
|
||||
# Must write back to config so generate_qr uses the new URL
|
||||
save_config(data, config_path)
|
||||
|
||||
# Also update the HTML we just replaced if we changed it
|
||||
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:
|
||||
@@ -546,16 +584,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}")
|
||||
|
||||
|
||||
@@ -627,12 +662,15 @@ def _internal_onionshare():
|
||||
|
||||
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 is ['voxvera', '_internal_onionshare', '--website', ... ]
|
||||
# We need to make it look like ['onionshare-cli', '--website', ... ]
|
||||
sys.argv = ["onionshare-cli"] + sys.argv[2:]
|
||||
sys.exit(main())
|
||||
except ImportError:
|
||||
print("Error: onionshare-cli not bundled or installed.", file=sys.stderr)
|
||||
except ImportError as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"Error: onionshare-cli not bundled or installed correctly: {e}", file=sys.stderr)
|
||||
print(f"DEBUG: sys.path = {sys.path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -640,7 +678,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)
|
||||
@@ -758,9 +796,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:
|
||||
@@ -816,7 +854,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:
|
||||
@@ -907,25 +945,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)
|
||||
@@ -1005,11 +1049,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
|
||||
@@ -1049,32 +1094,39 @@ 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 get_servers() -> list[str]:
|
||||
host_dir = ROOT / "host"
|
||||
if not host_dir.exists():
|
||||
return []
|
||||
servers = []
|
||||
for d in host_dir.iterdir():
|
||||
if d.is_dir() and (d / "config.json").exists():
|
||||
servers.append(d.name)
|
||||
return sorted(servers)
|
||||
# Check both bundled ROOT/host and local DATA_DIR/host
|
||||
search_paths = []
|
||||
if ROOT != DATA_DIR:
|
||||
search_paths.append(ROOT / "host")
|
||||
search_paths.append(DATA_DIR / "host")
|
||||
|
||||
servers = set()
|
||||
for host_dir in search_paths:
|
||||
if host_dir.exists():
|
||||
for d in host_dir.iterdir():
|
||||
if d.is_dir() and (d / "config.json").exists():
|
||||
servers.add(d.name)
|
||||
return sorted(list(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:
|
||||
@@ -1113,7 +1165,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
|
||||
@@ -1135,7 +1187,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}")
|
||||
@@ -1157,7 +1209,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})"
|
||||
@@ -1182,7 +1234,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")
|
||||
@@ -1206,7 +1258,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:
|
||||
@@ -1238,6 +1290,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"))
|
||||
@@ -1252,11 +1305,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":
|
||||
@@ -1305,7 +1370,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(
|
||||
@@ -1418,7 +1483,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":
|
||||
@@ -1446,7 +1510,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":
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
Reference in new issue
Block a user