mirror of
https://github.com/PR0M3TH3AN/VoxVera.git
synced 2026-10-27 17:50:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5ab447207 | ||
|
|
35069e24c5 | ||
|
|
51ab72239f | ||
|
|
4e571b62ea | ||
|
|
3bb8627d20 | ||
|
|
4e84466d38 | ||
|
|
f530e54f45 | ||
|
|
8be7d0761b | ||
|
|
6d747f719f | ||
|
|
dc83b76317 |
No files matched your search
@@ -34,6 +34,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install --upgrade setuptools pkg_resources || pip install --upgrade setuptools
|
||||
pip install -r requirements.txt
|
||||
pip install .
|
||||
pip install pyinstaller pyinstaller-hooks-contrib
|
||||
@@ -49,6 +50,16 @@ 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 \
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
# VoxVera v0.2.4
|
||||
|
||||
## New Features
|
||||
- **Optional File Attachment:** Users can now attach a file (e.g., a .torrent, image, or document) to their flyers. This file is packaged into the Tor-hosted site and made available via a "Download Attachment" button on the landing page.
|
||||
- **Interactive Attachment Selection:** Added a prompt during `voxvera init` to easily specify the path to an attachment.
|
||||
|
||||
## Localization
|
||||
- Added new tokens for attachment handling across all 14+ supported languages.
|
||||
+16
@@ -96,6 +96,19 @@ else
|
||||
msg "onionshare-cli found (system package), skipping pipx install. Run 'pipx install --force ...' if you want the latest git version."
|
||||
fi
|
||||
|
||||
# Clean up stale user-local packages from prior pip --user installs of voxvera.
|
||||
# These shadow system packages (e.g. Werkzeug) and break onionshare-cli.
|
||||
msg "Cleaning up stale user-local packages that may conflict with system onionshare-cli..."
|
||||
for pydir in "$HOME"/.local/lib/python3.*/site-packages; do
|
||||
[ -d "$pydir" ] || continue
|
||||
for pkg in werkzeug flask flask_socketio onionshare_cli voxvera; do
|
||||
if [ -d "$pydir/$pkg" ]; then
|
||||
warn "Removing stale $pkg from $pydir"
|
||||
rm -rf "$pydir/$pkg" "$pydir/${pkg}-"*.dist-info 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
download_binary() {
|
||||
local url=$1
|
||||
local dest=$2
|
||||
@@ -156,6 +169,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'."
|
||||
|
||||
+2
-4
@@ -14,7 +14,7 @@ voxvera = ["templates/**", "src/**", "resources/**", "locales/**", "resources/bi
|
||||
|
||||
[project]
|
||||
name = "voxvera"
|
||||
version = "0.2.10"
|
||||
version = "0.2.19"
|
||||
description = "VoxVera CLI utilities"
|
||||
readme = "README.md"
|
||||
authors = [{name = "VoxVera"}]
|
||||
@@ -28,9 +28,7 @@ dependencies = [
|
||||
"htmlmin2",
|
||||
"pypdf",
|
||||
"wcwidth",
|
||||
"onionshare-cli",
|
||||
"flask<2.3.0",
|
||||
"werkzeug<3.0.0",
|
||||
"setuptools",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
|
||||
@@ -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")
|
||||
+5
-9
@@ -1,16 +1,12 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.2.10"
|
||||
__version__ = "0.2.19"
|
||||
|
||||
# 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).
|
||||
# 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")
|
||||
else:
|
||||
vendor_dir = Path(__file__).parent / "vendor"
|
||||
|
||||
if vendor_dir.exists():
|
||||
if str(vendor_dir) not in sys.path:
|
||||
if vendor_dir.exists() and str(vendor_dir) not in sys.path:
|
||||
sys.path.insert(0, str(vendor_dir))
|
||||
+91
-58
@@ -10,6 +10,20 @@ 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__
|
||||
@@ -24,7 +38,7 @@ else:
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
# Persistent data directory (where host/ and projects live)
|
||||
DATA_DIR = Path(os.environ.get("VOXVERA_DIR", Path.cwd()))
|
||||
DATA_DIR = Path(os.environ.get("VOXVERA_DIR", Path.home()))
|
||||
|
||||
# Global locale state
|
||||
CURRENT_LOCALE = {}
|
||||
@@ -180,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:
|
||||
@@ -375,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),
|
||||
},
|
||||
@@ -391,21 +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),
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"message": t("cli.attachment_path_label"),
|
||||
"name": "attachment_path",
|
||||
"default": data.get("attachment_path", ""),
|
||||
},
|
||||
]
|
||||
data.update(prompt(link_qs))
|
||||
|
||||
@@ -542,18 +548,17 @@ 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
|
||||
# Handle optional file attachment logic
|
||||
attachment_path = data.get("attachment_path")
|
||||
attachment_url = ""
|
||||
attachment_display = "none"
|
||||
if attachment_path and os.path.isfile(attachment_path):
|
||||
att_filename = os.path.basename(attachment_path)
|
||||
attachment_url = f"download/{att_filename}"
|
||||
attachment_display = "inline-block"
|
||||
|
||||
html = html.replace("{{attachment_url}}", attachment_url)
|
||||
html = html.replace("{{attachment_display}}", attachment_display)
|
||||
|
||||
# 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 = DATA_DIR / "host" / folder_name
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
@@ -664,20 +669,29 @@ 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.
|
||||
# Prevent user-local pip packages from shadowing system packages
|
||||
os.environ["PYTHONNOUSERSITE"] = "1"
|
||||
|
||||
onionshare_bin = shutil.which("onionshare-cli") or shutil.which("onionshare")
|
||||
if onionshare_bin:
|
||||
os.execvp(onionshare_bin, [onionshare_bin] + onionshare_args)
|
||||
# execvp replaces the process; unreachable unless execvp itself fails
|
||||
|
||||
# Last resort: try to import onionshare_cli as a Python module
|
||||
try:
|
||||
from onionshare_cli import main
|
||||
# sys.argv 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 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)
|
||||
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)
|
||||
|
||||
|
||||
@@ -695,12 +709,12 @@ def serve(config_path: str) -> str | None:
|
||||
env = os.environ.copy()
|
||||
env["TOR_SOCKS_PORT"] = socks
|
||||
env["TOR_CONTROL_PORT"] = ctl
|
||||
# Prevent user-local pip packages (~/.local) from shadowing system packages.
|
||||
# onionshare-cli depends on specific Flask/Werkzeug versions from the system;
|
||||
# stale user-local copies break it.
|
||||
env["PYTHONNOUSERSITE"] = "1"
|
||||
|
||||
# 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",
|
||||
@@ -712,10 +726,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)
|
||||
@@ -1116,20 +1136,30 @@ def batch_import_configs():
|
||||
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]:
|
||||
# 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))
|
||||
"""Return server names found in DATA_DIR/host only."""
|
||||
host_dir = DATA_DIR / "host"
|
||||
servers = []
|
||||
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:
|
||||
@@ -1205,6 +1235,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()
|
||||
|
||||
|
||||
@@ -579,9 +579,6 @@
|
||||
<a id="download-link" href="download/voxvera-source.zip" download>
|
||||
<button class="download-button" data-i18n="web.download_button">{{t_web_download_button}}</button>
|
||||
</a>
|
||||
<a id="attachment-link" href="{{attachment_url}}" download style="display: {{attachment_display}};">
|
||||
<button class="download-button" data-i18n="web.download_attachment">{{t_web_download_attachment}}</button>
|
||||
</a>
|
||||
<select class="lang-select" id="lang-selector" onchange="setLanguage(this.value)"></select>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user