mirror of
https://github.com/elder-plinius/STEGOSAURUS-WRECKS.git
synced 2026-07-11 06:53:48 +02:00
Add subprocess CLI wrapper and skill for agent integration
stegg_cli.py — thin argparse CLI outputting JSON, designed for agent subprocess consumption (zero context cost). 13 commands matching the MCP server 1:1: encode, decode, analyze, detect, capacity, inject-chunk, read-chunks, inject-exif, inject-name, templates, analysis-tool, list-tools, crypto-status. Skills: - skills/stegg-cli/SKILL.md — 97% tessl review score (description 100%, content 92%) Entry point: stegg-cli (pip install stegg)
This commit is contained in:
+2
-1
@@ -61,6 +61,7 @@ all = [
|
||||
stegg = "cli:main_cli"
|
||||
stegg-tui = "tui:main"
|
||||
stegg-web = "webui:main"
|
||||
stegg-cli = "stegg_cli:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://ste.gg"
|
||||
@@ -69,7 +70,7 @@ Documentation = "https://github.com/elder-plinius/st3gg#readme"
|
||||
Issues = "https://github.com/elder-plinius/st3gg/issues"
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["steg_core", "crypto", "analysis_tools", "cli", "tui", "webui", "app", "injector", "ascii_art"]
|
||||
py-modules = ["steg_core", "crypto", "analysis_tools", "cli", "tui", "webui", "app", "injector", "ascii_art", "stegg_cli"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["index.html", "f5stego-lib.js", "_headers", "wrangler.jsonc"]
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: stegg-cli
|
||||
description: "Subprocess CLI for ST3GG steganography — encode/decode, steganalysis, PNG chunk/EXIF injection, and AI red-team payloads. Runs as subprocess so output stays out of context. Use when hiding data in images, analyzing images for hidden content, injecting metadata, crafting steganographic PoCs, or detecting steganographic content. Triggers on stegg, steganography, steg, LSB, hide data in image, hidden data, steganalysis."
|
||||
---
|
||||
|
||||
# ST3GG CLI
|
||||
|
||||
Subprocess steganography CLI. Output stays out of LLM context — call via `python3 stegg_cli.py <command>`. All output is JSON.
|
||||
|
||||
**This is the primary interface for steganography operations.** Prefer this over the MCP server for routine use (zero context cost). Fall back to MCP tools when you need results inline.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
encode -i IMAGE [-t TEXT|-f FILE] [-o OUT] [--channels C] [--bits N] [--password P]
|
||||
decode -i IMAGE [-o OUT] [--no-auto] [--channels C] [--bits N] [--password P]
|
||||
analyze IMAGE [--full]
|
||||
detect IMAGE
|
||||
capacity IMAGE [--channels C] [--bits N]
|
||||
inject-chunk -i IMAGE -o OUT --text TEXT [--type tEXt] [--keyword Comment]
|
||||
read-chunks IMAGE
|
||||
inject-exif -i IMAGE -o OUT [--comment C] [--author A] [--custom-fields JSON]
|
||||
inject-name [--template T] [--channels C] [--count N]
|
||||
templates
|
||||
analysis-tool IMAGE ACTION
|
||||
list-tools
|
||||
crypto-status
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Encode + Verify + Decode
|
||||
|
||||
```bash
|
||||
# Hide text
|
||||
python3 stegg_cli.py encode -i carrier.png -t "secret message" -o stegged.png
|
||||
# Always verify roundtrip after encoding
|
||||
python3 stegg_cli.py decode -i stegged.png
|
||||
|
||||
# Hide file with encryption
|
||||
python3 stegg_cli.py encode -i carrier.png -f payload.bin -o stegged.png --password s3cret
|
||||
python3 stegg_cli.py decode -i stegged.png --password s3cret
|
||||
|
||||
# Decode with manual config (non-interleaved strategies)
|
||||
python3 stegg_cli.py decode -i stegged.png --no-auto --strategy sequential
|
||||
```
|
||||
|
||||
### Analyze + Detect
|
||||
|
||||
```bash
|
||||
# Quick header check
|
||||
python3 stegg_cli.py detect suspect.png
|
||||
|
||||
# Chi-square anomaly analysis
|
||||
python3 stegg_cli.py analyze suspect.png
|
||||
|
||||
# Full 264-function sweep (PNG only)
|
||||
python3 stegg_cli.py analyze suspect.png --full
|
||||
|
||||
# Specific analysis function
|
||||
python3 stegg_cli.py analysis-tool suspect.png rs_analysis
|
||||
python3 stegg_cli.py analysis-tool suspect.png sample_pairs_analysis
|
||||
```
|
||||
|
||||
### Metadata Injection
|
||||
|
||||
```bash
|
||||
# PNG chunk
|
||||
python3 stegg_cli.py inject-chunk -i image.png -o out.png --text "hidden metadata"
|
||||
|
||||
# Private chunk type
|
||||
python3 stegg_cli.py inject-chunk -i image.png -o out.png --type stEg --text "private"
|
||||
|
||||
# EXIF fields
|
||||
python3 stegg_cli.py inject-exif -i image.png -o out.png --comment "payload" --author "red-team"
|
||||
|
||||
# Read chunks
|
||||
python3 stegg_cli.py read-chunks image.png
|
||||
```
|
||||
|
||||
### AI Red Team
|
||||
|
||||
```bash
|
||||
# Generate injection filenames
|
||||
python3 stegg_cli.py inject-name --template claude_decoder --channels R --count 3
|
||||
|
||||
# List jailbreak templates
|
||||
python3 stegg_cli.py templates
|
||||
```
|
||||
|
||||
## Key Constraints
|
||||
|
||||
- **Use `interleaved` strategy** (default) — only strategy with auto-detect on decode
|
||||
- `spread` and `randomized` have upstream decode bugs
|
||||
- `sequential` works but needs `--no-auto` on decode
|
||||
- All output is JSON; errors: `{"error": "..."}` with exit code 1
|
||||
- **Always check for `error` key** in JSON output before proceeding in multi-step operations
|
||||
- **Always verify encode with decode** before distributing stegged images
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install stegg
|
||||
# CLI is available as stegg-cli, or:
|
||||
python3 /path/to/st3gg/stegg_cli.py <command>
|
||||
```
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
stegg-cli: Thin CLI wrapper around the ST3GG steganography engine.
|
||||
|
||||
Designed for agent consumption — compact JSON output, subprocess-friendly,
|
||||
zero context cost. Runs as a subprocess so output stays out of LLM context.
|
||||
|
||||
Subcommands:
|
||||
encode -i IMAGE [-t TEXT|-f FILE] [-o OUT] [--channels C] [--bits N] [--password P]
|
||||
decode -i IMAGE [-o OUT] [--no-auto] [--channels C] [--bits N] [--password P]
|
||||
analyze IMAGE [--full]
|
||||
detect IMAGE
|
||||
capacity IMAGE [--channels C] [--bits N]
|
||||
inject-chunk -i IMAGE -o OUT [--type TYPE] [--keyword K] --text TEXT
|
||||
read-chunks IMAGE
|
||||
inject-exif -i IMAGE -o OUT [--comment C] [--author A] [--custom-fields JSON]
|
||||
inject-name [--template T] [--channels C] [--count N]
|
||||
templates
|
||||
analysis-tool IMAGE ACTION
|
||||
list-tools
|
||||
crypto-status
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add repo root to path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from steg_core import (
|
||||
encode, decode, create_config, calculate_capacity,
|
||||
analyze_image, detect_encoding,
|
||||
)
|
||||
from analysis_tools import execute_action, list_available_tools
|
||||
from injector import (
|
||||
generate_injection_filename, get_jailbreak_template,
|
||||
get_jailbreak_names, inject_text_chunk, inject_itxt_chunk,
|
||||
inject_private_chunk, read_png_chunks, extract_text_chunks,
|
||||
inject_metadata_pil,
|
||||
)
|
||||
|
||||
try:
|
||||
from crypto import encrypt, decrypt, get_available_methods, crypto_status
|
||||
except Exception:
|
||||
encrypt = decrypt = None
|
||||
def get_available_methods(): return ["xor"]
|
||||
def crypto_status(): return {"cryptography_available": False, "available_methods": ["xor"]}
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class _Encoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, (np.bool_,)): return bool(obj)
|
||||
if isinstance(obj, (np.integer,)): return int(obj)
|
||||
if isinstance(obj, (np.floating,)): return float(obj)
|
||||
if isinstance(obj, np.ndarray): return obj.tolist()
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
def _out(obj):
|
||||
print(json.dumps(obj, cls=_Encoder))
|
||||
|
||||
|
||||
def _err(msg):
|
||||
_out({"error": msg})
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _load_image(path):
|
||||
p = Path(path).expanduser().resolve()
|
||||
if not p.exists():
|
||||
_err(f"Image not found: {p}")
|
||||
return Image.open(p), p
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Subcommands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_encode(args):
|
||||
image, _ = _load_image(args.input)
|
||||
|
||||
if args.file:
|
||||
fp = Path(args.file).expanduser().resolve()
|
||||
if not fp.exists():
|
||||
_err(f"Payload file not found: {fp}")
|
||||
payload = fp.read_bytes()
|
||||
elif args.text:
|
||||
payload = args.text.encode("utf-8")
|
||||
else:
|
||||
_err("Provide --text or --file")
|
||||
|
||||
out = Path(args.output).expanduser().resolve() if args.output else Path(args.input).with_name(
|
||||
Path(args.input).stem + "_steg.png"
|
||||
)
|
||||
|
||||
config = create_config(
|
||||
channels=args.channels, bits=args.bits,
|
||||
compress=not args.no_compress,
|
||||
strategy=args.strategy, seed=args.seed or None,
|
||||
)
|
||||
|
||||
cap = calculate_capacity(image, config)
|
||||
if len(payload) > cap["usable_bytes"]:
|
||||
_err(f"Payload too large: {len(payload)} > {cap['usable_bytes']} bytes")
|
||||
|
||||
encrypted = False
|
||||
if args.password and encrypt:
|
||||
payload = encrypt(payload, args.password)
|
||||
encrypted = True
|
||||
|
||||
encode(image, payload, config, str(out))
|
||||
_out({
|
||||
"output": str(out), "payload_bytes": len(payload),
|
||||
"capacity": cap["human"], "encrypted": encrypted,
|
||||
})
|
||||
|
||||
|
||||
def cmd_decode(args):
|
||||
image, _ = _load_image(args.input)
|
||||
|
||||
config = None
|
||||
detected = False
|
||||
if not args.no_auto:
|
||||
det = detect_encoding(image)
|
||||
if det:
|
||||
detected = True
|
||||
config = None
|
||||
else:
|
||||
config = create_config(
|
||||
channels=args.channels, bits=args.bits,
|
||||
strategy=args.strategy, seed=args.seed or None,
|
||||
)
|
||||
else:
|
||||
config = create_config(
|
||||
channels=args.channels, bits=args.bits,
|
||||
strategy=args.strategy, seed=args.seed or None,
|
||||
)
|
||||
|
||||
try:
|
||||
data = decode(image, config)
|
||||
except Exception as e:
|
||||
_err(f"Decode failed: {e}")
|
||||
|
||||
if args.password and decrypt:
|
||||
try:
|
||||
data = decrypt(data, args.password)
|
||||
except Exception as e:
|
||||
_err(f"Decryption failed: {e}")
|
||||
|
||||
result = {"bytes": len(data), "auto_detected": detected}
|
||||
|
||||
if args.output:
|
||||
out = Path(args.output).expanduser().resolve()
|
||||
out.write_bytes(data)
|
||||
result["output"] = str(out)
|
||||
|
||||
try:
|
||||
result["text"] = data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
result["hex_preview"] = data[:512].hex()
|
||||
|
||||
_out(result)
|
||||
|
||||
|
||||
def cmd_analyze(args):
|
||||
image, p = _load_image(args.image)
|
||||
analysis = analyze_image(image)
|
||||
|
||||
channels = {}
|
||||
max_ind = 0.0
|
||||
for ch, d in analysis["channels"].items():
|
||||
lsb = d["lsb_ratio"]
|
||||
ind = d.get("chi_square_indicator", 0.0)
|
||||
max_ind = max(max_ind, ind)
|
||||
channels[ch] = {
|
||||
"mean": round(d["mean"], 2), "std": round(d["std"], 2),
|
||||
"lsb_zeros_pct": round(lsb["zeros"] * 100, 1),
|
||||
"chi_sq_indicator": round(ind, 4),
|
||||
"anomaly": "HIGH" if ind > 0.3 else ("slight" if ind > 0.1 else "normal"),
|
||||
}
|
||||
|
||||
verdict = ("HIGH PROBABILITY" if max_ind > 0.3
|
||||
else "Possible" if max_ind > 0.1
|
||||
else "No indicators")
|
||||
|
||||
result = {
|
||||
"dimensions": analysis["dimensions"], "mode": analysis["mode"],
|
||||
"pixels": analysis["total_pixels"], "channels": channels,
|
||||
"capacity": analysis["capacity_by_config"], "verdict": verdict,
|
||||
}
|
||||
|
||||
if args.full:
|
||||
from analysis_tools import png_full_analysis
|
||||
try:
|
||||
result["full_analysis"] = png_full_analysis(p.read_bytes())
|
||||
except Exception as e:
|
||||
result["full_analysis_error"] = str(e)
|
||||
|
||||
_out(result)
|
||||
|
||||
|
||||
def cmd_detect(args):
|
||||
image, _ = _load_image(args.image)
|
||||
det = detect_encoding(image)
|
||||
_out({"detected": bool(det), "config": det} if det else {"detected": False})
|
||||
|
||||
|
||||
def cmd_capacity(args):
|
||||
image, _ = _load_image(args.image)
|
||||
config = create_config(channels=args.channels, bits=args.bits)
|
||||
cap = calculate_capacity(image, config)
|
||||
_out({
|
||||
"usable_bytes": cap["usable_bytes"], "human": cap["human"],
|
||||
"pixels": image.width * image.height,
|
||||
})
|
||||
|
||||
|
||||
def cmd_inject_chunk(args):
|
||||
p = Path(args.input).expanduser().resolve()
|
||||
if not p.exists():
|
||||
_err(f"Image not found: {p}")
|
||||
raw = p.read_bytes()
|
||||
|
||||
ct = args.type
|
||||
if ct == "iTXt":
|
||||
modified = inject_itxt_chunk(raw, args.keyword, args.text)
|
||||
elif len(ct) == 4 and ct not in ("tEXt", "zTXt", "iTXt"):
|
||||
modified = inject_private_chunk(raw, ct, args.text.encode("utf-8"))
|
||||
else:
|
||||
modified = inject_text_chunk(raw, args.keyword, args.text, compressed=args.compressed)
|
||||
|
||||
out = Path(args.output).expanduser().resolve()
|
||||
out.write_bytes(modified)
|
||||
_out({"output": str(out), "chunk_type": ct, "keyword": args.keyword, "bytes": len(args.text)})
|
||||
|
||||
|
||||
def cmd_read_chunks(args):
|
||||
p = Path(args.image).expanduser().resolve()
|
||||
if not p.exists():
|
||||
_err(f"Image not found: {p}")
|
||||
raw = p.read_bytes()
|
||||
chunks = read_png_chunks(raw)
|
||||
text = extract_text_chunks(raw)
|
||||
|
||||
summary = [{"type": c.get("type", "?"), "size": c.get("length", 0)} for c in chunks]
|
||||
_out({"chunks": summary, "text": text, "total": len(chunks)})
|
||||
|
||||
|
||||
def cmd_inject_exif(args):
|
||||
p = Path(args.input).expanduser().resolve()
|
||||
if not p.exists():
|
||||
_err(f"Image not found: {p}")
|
||||
|
||||
meta = {}
|
||||
if args.comment: meta["Comment"] = args.comment
|
||||
if args.author: meta["Author"] = args.author
|
||||
if args.description: meta["Description"] = args.description
|
||||
if args.title: meta["Title"] = args.title
|
||||
if args.custom_fields:
|
||||
try:
|
||||
meta.update(json.loads(args.custom_fields))
|
||||
except json.JSONDecodeError:
|
||||
_err("--custom-fields must be valid JSON")
|
||||
|
||||
if not meta:
|
||||
_err("Provide at least one metadata field")
|
||||
|
||||
image = Image.open(p)
|
||||
_, png_bytes = inject_metadata_pil(image, meta)
|
||||
out = Path(args.output).expanduser().resolve()
|
||||
out.write_bytes(png_bytes)
|
||||
_out({"output": str(out), "fields": list(meta.keys())})
|
||||
|
||||
|
||||
def cmd_inject_name(args):
|
||||
names = [generate_injection_filename(args.template, args.channels) for _ in range(args.count)]
|
||||
_out({"filenames": names})
|
||||
|
||||
|
||||
def cmd_templates(_args):
|
||||
templates = {}
|
||||
for name in get_jailbreak_names():
|
||||
content = get_jailbreak_template(name)
|
||||
templates[name] = content[:120] + ("..." if len(content) > 120 else "")
|
||||
_out({"templates": templates, "count": len(templates)})
|
||||
|
||||
|
||||
def cmd_analysis_tool(args):
|
||||
p = Path(args.image).expanduser().resolve()
|
||||
if not p.exists():
|
||||
_err(f"File not found: {p}")
|
||||
|
||||
result = execute_action(args.action, p.read_bytes())
|
||||
if hasattr(result, "to_dict"):
|
||||
_out(result.to_dict())
|
||||
else:
|
||||
_out({"result": str(result)})
|
||||
|
||||
|
||||
def cmd_list_tools(_args):
|
||||
tools = list_available_tools()
|
||||
_out({"tools": tools, "count": len(tools)})
|
||||
|
||||
|
||||
def cmd_crypto_status(_args):
|
||||
s = crypto_status()
|
||||
_out(s if isinstance(s, dict) else {"status": str(s)})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_parser():
|
||||
p = argparse.ArgumentParser(prog="stegg-cli", description="ST3GG steganography CLI for agents")
|
||||
sub = p.add_subparsers(dest="command", required=True)
|
||||
|
||||
# encode
|
||||
enc = sub.add_parser("encode", help="Encode data into an image")
|
||||
enc.add_argument("-i", "--input", required=True, help="Carrier image")
|
||||
enc.add_argument("-t", "--text", help="Text payload")
|
||||
enc.add_argument("-f", "--file", help="File payload")
|
||||
enc.add_argument("-o", "--output", help="Output path")
|
||||
enc.add_argument("--channels", default="RGB")
|
||||
enc.add_argument("--bits", type=int, default=1)
|
||||
enc.add_argument("--strategy", default="interleaved")
|
||||
enc.add_argument("--seed", type=int, default=0)
|
||||
enc.add_argument("--password", default="")
|
||||
enc.add_argument("--no-compress", action="store_true")
|
||||
enc.set_defaults(func=cmd_encode)
|
||||
|
||||
# decode
|
||||
dec = sub.add_parser("decode", help="Decode data from an image")
|
||||
dec.add_argument("-i", "--input", required=True, help="Encoded image")
|
||||
dec.add_argument("-o", "--output", help="Save extracted data to file")
|
||||
dec.add_argument("--no-auto", action="store_true", help="Disable auto-detect")
|
||||
dec.add_argument("--channels", default="RGB")
|
||||
dec.add_argument("--bits", type=int, default=1)
|
||||
dec.add_argument("--strategy", default="interleaved")
|
||||
dec.add_argument("--seed", type=int, default=0)
|
||||
dec.add_argument("--password", default="")
|
||||
dec.set_defaults(func=cmd_decode)
|
||||
|
||||
# analyze
|
||||
ana = sub.add_parser("analyze", help="Analyze image for steg indicators")
|
||||
ana.add_argument("image", help="Image to analyze")
|
||||
ana.add_argument("--full", action="store_true", help="Full 264-function analysis")
|
||||
ana.set_defaults(func=cmd_analyze)
|
||||
|
||||
# detect
|
||||
det = sub.add_parser("detect", help="Quick STEG v3 header check")
|
||||
det.add_argument("image")
|
||||
det.set_defaults(func=cmd_detect)
|
||||
|
||||
# capacity
|
||||
cap = sub.add_parser("capacity", help="Calculate carrier capacity")
|
||||
cap.add_argument("image")
|
||||
cap.add_argument("--channels", default="RGB")
|
||||
cap.add_argument("--bits", type=int, default=1)
|
||||
cap.set_defaults(func=cmd_capacity)
|
||||
|
||||
# inject-chunk
|
||||
ic = sub.add_parser("inject-chunk", help="Inject PNG text chunk")
|
||||
ic.add_argument("-i", "--input", required=True)
|
||||
ic.add_argument("-o", "--output", required=True)
|
||||
ic.add_argument("--type", default="tEXt")
|
||||
ic.add_argument("--keyword", default="Comment")
|
||||
ic.add_argument("--text", required=True)
|
||||
ic.add_argument("--compressed", action="store_true")
|
||||
ic.set_defaults(func=cmd_inject_chunk)
|
||||
|
||||
# read-chunks
|
||||
rc = sub.add_parser("read-chunks", help="Read PNG chunks")
|
||||
rc.add_argument("image")
|
||||
rc.set_defaults(func=cmd_read_chunks)
|
||||
|
||||
# inject-exif
|
||||
ie = sub.add_parser("inject-exif", help="Inject EXIF metadata")
|
||||
ie.add_argument("-i", "--input", required=True)
|
||||
ie.add_argument("-o", "--output", required=True)
|
||||
ie.add_argument("--comment", default="")
|
||||
ie.add_argument("--author", default="")
|
||||
ie.add_argument("--description", default="")
|
||||
ie.add_argument("--title", default="")
|
||||
ie.add_argument("--custom-fields", default="")
|
||||
ie.set_defaults(func=cmd_inject_exif)
|
||||
|
||||
# inject-name
|
||||
inj = sub.add_parser("inject-name", help="Generate injection filenames")
|
||||
inj.add_argument("--template", default="universal_decoder")
|
||||
inj.add_argument("--channels", default="RGB")
|
||||
inj.add_argument("--count", type=int, default=1)
|
||||
inj.set_defaults(func=cmd_inject_name)
|
||||
|
||||
# templates
|
||||
tmpl = sub.add_parser("templates", help="List jailbreak templates")
|
||||
tmpl.set_defaults(func=cmd_templates)
|
||||
|
||||
# analysis-tool
|
||||
at = sub.add_parser("analysis-tool", help="Run a specific analysis function")
|
||||
at.add_argument("image")
|
||||
at.add_argument("action")
|
||||
at.set_defaults(func=cmd_analysis_tool)
|
||||
|
||||
# list-tools
|
||||
lt = sub.add_parser("list-tools", help="List all analysis actions")
|
||||
lt.set_defaults(func=cmd_list_tools)
|
||||
|
||||
# crypto-status
|
||||
cs = sub.add_parser("crypto-status", help="Check encryption availability")
|
||||
cs.set_defaults(func=cmd_crypto_status)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
args.func(args)
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as e:
|
||||
_err(str(e))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user