mirror of
https://github.com/docwho76/sendmsg.git
synced 2026-08-31 01:41:10 -07:00
more bug fixes and updates
This commit is contained in:
parent
ec6312e021
commit
9a598f6aa4
17
CHANGELOG.md
17
CHANGELOG.md
@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
All notable changes to `sendmsg` are documented here.
|
All notable changes to `sendmsg` are documented here.
|
||||||
|
|
||||||
|
## 5.1.0
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Multiple recipients per Signal send:** `--recipients` now accepts more than one value, and `--to` accepts multiple numbers, so a single invocation can fan out to several groups and/or direct recipients. Each target is sent independently and reported individually; the command exits non-zero if any target fails.
|
||||||
|
- **`--json` CSV summary:** Emits a machine-readable JSON summary (totals, per-bucket counts, and notes) for unattended runs, as an alternative to the formatted text summary.
|
||||||
|
- **SMS timeout and retry:** `imsg` invocations now run with a 60-second timeout and are retried up to 3 times with backoff on timeout or transient failure, matching the resilience of the Signal REST path. Previously a hung Messages app could stall an entire batch and SMS sends had no retry.
|
||||||
|
- **Startup configuration warnings:** A `signal_rest_url` that is not a well-formed `http(s)://` URL now warns at startup, and attempting a Signal send from the built-in placeholder account (`+1234567890`) warns that no real account is configured.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Skip vs. fail accounting corrected.** Intentional skips — unknown `method`, missing/invalid SMS `service`, `voice` on an SMS row, and empty messages — are now counted under **Skipped** rather than **Failed**. The process exit code is `1` only when a genuine send fails, so deliberate skips no longer cause unattended runs to report failure.
|
||||||
|
- **Dry runs no longer report sends as successful.** A `--dry-run` now reports a separate "would send" count instead of incrementing the success total for rows that were never sent.
|
||||||
|
- Empty-message skip notes now distinguish SMS rows (which always require text) from Signal rows (which may be attachment- or voice-only).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`--link-signal` false success:** The account snapshot used for new-link detection now tolerates all response shapes returned by the REST API (bare list, `{"accounts": [...]}`, and `{"number": ...}`). Previously a dict-shaped response produced an empty baseline, so any pre-existing account could be misreported as a newly linked device. Account listing and link detection now share one shape-tolerant helper.
|
||||||
|
- The `--help` epilog and module docstring CSV column lists now include the `voice` column, matching the documented CSV format and the code.
|
||||||
|
|
||||||
## 5.0.0
|
## 5.0.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -329,7 +329,7 @@ $ sendmsg --show-config
|
|||||||
==================================================
|
==================================================
|
||||||
⚙️ sendmsg configuration
|
⚙️ sendmsg configuration
|
||||||
==================================================
|
==================================================
|
||||||
Version: 5.0.0
|
Version: 5.1.0
|
||||||
|
|
||||||
Config file: /Users/you/.sendmsg.conf (found)
|
Config file: /Users/you/.sendmsg.conf (found)
|
||||||
|
|
||||||
@ -442,6 +442,12 @@ After processing all rows, a summary is printed:
|
|||||||
==================================================
|
==================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Intentional skips — unknown method, missing/invalid SMS service, voice on
|
||||||
|
> an SMS row, and empty messages — are counted under **Skipped**, not
|
||||||
|
> **Failed**. Only genuine send failures count as **Failed**, and the exit
|
||||||
|
> code is `1` only when something actually failed to send. Add `--json` for a
|
||||||
|
> machine-readable summary in unattended runs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|||||||
240
sendmsg
240
sendmsg
@ -17,11 +17,11 @@ Usage:
|
|||||||
sendmsg --link-signal # Link a new device to your existing Signal account
|
sendmsg --link-signal # Link a new device to your existing Signal account
|
||||||
|
|
||||||
CSV format (messages.csv):
|
CSV format (messages.csv):
|
||||||
method,recipient,name,message,account,service,file,delay
|
method,recipient,name,message,account,service,file,voice,delay
|
||||||
signal,+1888222333,Alice,Hello via Signal,+1888222333,,,
|
signal,+1888222333,Alice,Hello via Signal,+1888222333,,,,
|
||||||
signal,group.ZzBHd3NZO...,Group alert,Hi team,,,,
|
signal,group.ZzBHd3NZO...,Group alert,Hi team,,,,,
|
||||||
sms,+1234567890,Bob,SMS test,,imessage,~/pic.jpg,
|
sms,+1234567890,Bob,SMS test,,imessage,~/pic.jpg,,
|
||||||
signal,+1888222333,Carol,With delay,,,,3
|
signal,+1888222333,Carol,With delay,,,,,3
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@ -39,7 +39,7 @@ import urllib.error
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
VERSION = "5.0.0"
|
VERSION = "5.1.0"
|
||||||
|
|
||||||
CONFIG_PATHS = [
|
CONFIG_PATHS = [
|
||||||
os.path.expanduser("~/.sendmsg.conf"),
|
os.path.expanduser("~/.sendmsg.conf"),
|
||||||
@ -108,6 +108,18 @@ CFG = load_config()
|
|||||||
SIGNAL_REST_URL = CFG["signal_rest_url"]
|
SIGNAL_REST_URL = CFG["signal_rest_url"]
|
||||||
SIGNAL_DEFAULT_ACCOUNT = CFG["signal_default_account"]
|
SIGNAL_DEFAULT_ACCOUNT = CFG["signal_default_account"]
|
||||||
|
|
||||||
|
# The built-in default account is a placeholder; sending from it will fail at
|
||||||
|
# the Signal server. Track whether the resolved account is still the default
|
||||||
|
# so we can warn before attempting a real send.
|
||||||
|
PLACEHOLDER_ACCOUNT = "+1234567890"
|
||||||
|
|
||||||
|
if not (SIGNAL_REST_URL.startswith("http://") or SIGNAL_REST_URL.startswith("https://")):
|
||||||
|
print(
|
||||||
|
f"⚠️ Warning: signal_rest_url '{SIGNAL_REST_URL}' does not look like a "
|
||||||
|
f"valid URL (expected http:// or https://).",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers — reusable without argparse objects (needed for CSV dispatch)
|
# Helpers — reusable without argparse objects (needed for CSV dispatch)
|
||||||
@ -237,6 +249,14 @@ def send_one_signal(account, recipient, message, group_id=None,
|
|||||||
"""
|
"""
|
||||||
account = normalize_account(account)
|
account = normalize_account(account)
|
||||||
|
|
||||||
|
if account == PLACEHOLDER_ACCOUNT:
|
||||||
|
print(
|
||||||
|
f"⚠️ Warning: sending from the placeholder account {PLACEHOLDER_ACCOUNT}. "
|
||||||
|
f"Set SIGNAL_ACCOUNT or signal_default_account in ~/.sendmsg.conf, "
|
||||||
|
f"or pass --account.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
target = group_id if group_id else normalize_recipient(recipient)
|
target = group_id if group_id else normalize_recipient(recipient)
|
||||||
payload = {"number": account, "recipients": [target]}
|
payload = {"number": account, "recipients": [target]}
|
||||||
|
|
||||||
@ -314,19 +334,47 @@ def send_one_sms(to, message, service, file=None):
|
|||||||
print("❌ SMS send failed: 'imsg' CLI not found on PATH.", file=sys.stderr)
|
print("❌ SMS send failed: 'imsg' CLI not found on PATH.", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
# imsg can hang if the Messages app is unresponsive; bound each attempt
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
# with a timeout and retry a couple of times on transient failures.
|
||||||
except OSError as e:
|
max_attempts = 3
|
||||||
print(f"❌ SMS send failed: could not run 'imsg': {e}", file=sys.stderr)
|
backoff = 2.0
|
||||||
return False
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
if attempt < max_attempts:
|
||||||
|
wait = backoff * attempt
|
||||||
|
print(
|
||||||
|
f"⚠️ SMS send to {to} timed out; retrying in {wait:.0f}s "
|
||||||
|
f"(attempt {attempt}/{max_attempts})...",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
print(f"❌ SMS send failed: 'imsg' timed out after {max_attempts} attempts.", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
except OSError as e:
|
||||||
|
print(f"❌ SMS send failed: could not run 'imsg': {e}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(f"✅ SMS/iMessage sent to {to} (service: {service.upper()})")
|
print(f"✅ SMS/iMessage sent to {to} (service: {service.upper()})")
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
|
if attempt < max_attempts:
|
||||||
|
wait = backoff * attempt
|
||||||
|
print(
|
||||||
|
f"⚠️ SMS send to {to} failed (exit {result.returncode}); retrying in "
|
||||||
|
f"{wait:.0f}s (attempt {attempt}/{max_attempts})...",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
print(f"❌ SMS send failed (exit {result.returncode}): {result.stderr.strip()}", file=sys.stderr)
|
print(f"❌ SMS send failed (exit {result.returncode}): {result.stderr.strip()}", file=sys.stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# REST API
|
# REST API
|
||||||
@ -456,34 +504,39 @@ def cmd_signal(args):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
account = normalize_account(args.account or SIGNAL_DEFAULT_ACCOUNT)
|
account = normalize_account(args.account or SIGNAL_DEFAULT_ACCOUNT)
|
||||||
|
|
||||||
if args.recipients:
|
|
||||||
recipient = None
|
|
||||||
group_id = args.recipients
|
|
||||||
else:
|
|
||||||
recipient = args.to[0]
|
|
||||||
group_id = None
|
|
||||||
|
|
||||||
message = "\n".join(args.text) if args.text else None
|
message = "\n".join(args.text) if args.text else None
|
||||||
|
|
||||||
ok = send_one_signal(
|
# Build the list of targets. --recipients values are treated as group IDs
|
||||||
account=account,
|
# or raw recipients; --to values are direct phone numbers. Either may
|
||||||
recipient=recipient,
|
# contain more than one entry, so a single invocation can fan out.
|
||||||
message=message,
|
targets = [] # list of (recipient, group_id, label)
|
||||||
group_id=group_id,
|
if args.recipients:
|
||||||
attach=attach_files,
|
for r in args.recipients:
|
||||||
voice=voice_file,
|
if is_group_id(r):
|
||||||
)
|
targets.append((None, r, f"group {r}"))
|
||||||
|
else:
|
||||||
|
targets.append((r, None, normalize_recipient(r)))
|
||||||
|
if args.to:
|
||||||
|
for t in args.to:
|
||||||
|
targets.append((t, None, normalize_recipient(t)))
|
||||||
|
|
||||||
if ok:
|
all_ok = True
|
||||||
if args.recipients:
|
for recipient, group_id, label in targets:
|
||||||
print(f"✅ Signal message sent to group {args.recipients}")
|
ok = send_one_signal(
|
||||||
elif args.to:
|
account=account,
|
||||||
print(f"✅ Signal message sent to {normalize_recipient(args.to[0])}")
|
recipient=recipient,
|
||||||
|
message=message,
|
||||||
|
group_id=group_id,
|
||||||
|
attach=attach_files,
|
||||||
|
voice=voice_file,
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
print(f"✅ Signal message sent to {label}")
|
||||||
else:
|
else:
|
||||||
print("✅ Signal message sent")
|
print(f"❌ Signal message send failed: {label}", file=sys.stderr)
|
||||||
else:
|
all_ok = False
|
||||||
print("❌ Signal message send failed.", file=sys.stderr)
|
|
||||||
|
if not all_ok:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@ -575,6 +628,7 @@ def cmd_csv(args):
|
|||||||
success = 0
|
success = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
skipped = 0
|
skipped = 0
|
||||||
|
previewed = 0
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for i, row in enumerate(rows, 1):
|
for i, row in enumerate(rows, 1):
|
||||||
@ -593,7 +647,7 @@ def cmd_csv(args):
|
|||||||
|
|
||||||
if method not in ("signal", "sms"):
|
if method not in ("signal", "sms"):
|
||||||
print(f"⚠️ Row {i}: unknown method '{method}' — skipping", file=sys.stderr)
|
print(f"⚠️ Row {i}: unknown method '{method}' — skipping", file=sys.stderr)
|
||||||
failed += 1
|
skipped += 1
|
||||||
errors.append(f"Row {i}: unknown method '{method}'")
|
errors.append(f"Row {i}: unknown method '{method}'")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -613,9 +667,13 @@ def cmd_csv(args):
|
|||||||
# requires text.
|
# requires text.
|
||||||
has_signal_media = method == "signal" and (file_path or voice_path)
|
has_signal_media = method == "signal" and (file_path or voice_path)
|
||||||
if not message and not has_signal_media:
|
if not message and not has_signal_media:
|
||||||
print(f"⚠️ Row {i}: empty message — skipping", file=sys.stderr)
|
if method == "sms":
|
||||||
|
reason = "empty message (SMS requires text)"
|
||||||
|
else:
|
||||||
|
reason = "empty message (no text, attachment, or voice)"
|
||||||
|
print(f"⚠️ Row {i}: {reason} — skipping", file=sys.stderr)
|
||||||
skipped += 1
|
skipped += 1
|
||||||
errors.append(f"Row {i}: empty message")
|
errors.append(f"Row {i}: {reason}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if method == "sms":
|
if method == "sms":
|
||||||
@ -625,7 +683,7 @@ def cmd_csv(args):
|
|||||||
f"(method 'signal') — skipping",
|
f"(method 'signal') — skipping",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
failed += 1
|
skipped += 1
|
||||||
errors.append(f"Row {i}: voice not supported for SMS")
|
errors.append(f"Row {i}: voice not supported for SMS")
|
||||||
continue
|
continue
|
||||||
if service not in ("sms", "imessage"):
|
if service not in ("sms", "imessage"):
|
||||||
@ -634,7 +692,7 @@ def cmd_csv(args):
|
|||||||
f"(got '{service or ''}') — skipping",
|
f"(got '{service or ''}') — skipping",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
failed += 1
|
skipped += 1
|
||||||
errors.append(f"Row {i}: invalid/missing SMS service")
|
errors.append(f"Row {i}: invalid/missing SMS service")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -655,7 +713,7 @@ def cmd_csv(args):
|
|||||||
extras.append(f"service={service}")
|
extras.append(f"service={service}")
|
||||||
if extras:
|
if extras:
|
||||||
print(f" ({', '.join(extras)})")
|
print(f" ({', '.join(extras)})")
|
||||||
success += 1
|
previewed += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if method == "signal":
|
if method == "signal":
|
||||||
@ -691,20 +749,36 @@ def cmd_csv(args):
|
|||||||
time.sleep(effective_delay)
|
time.sleep(effective_delay)
|
||||||
|
|
||||||
# Summary
|
# Summary
|
||||||
print()
|
if getattr(args, "json_output", False):
|
||||||
print("=" * 50)
|
summary = {
|
||||||
print("📊 CSV Send Summary" + (" (DRY RUN)" if args.dry_run else ""))
|
"total": total,
|
||||||
print("=" * 50)
|
"success": success,
|
||||||
print(f" Total rows: {total}")
|
"skipped": skipped,
|
||||||
print(f" ✅ Success: {success}")
|
"failed": failed,
|
||||||
print(f" ⏭️ Skipped: {skipped}")
|
"dry_run": bool(args.dry_run),
|
||||||
print(f" ❌ Failed: {failed}")
|
"notes": errors,
|
||||||
if errors:
|
}
|
||||||
|
if args.dry_run:
|
||||||
|
summary["would_send"] = previewed
|
||||||
|
print(json.dumps(summary, indent=2))
|
||||||
|
else:
|
||||||
print()
|
print()
|
||||||
print(" Notes:")
|
print("=" * 50)
|
||||||
for e in errors:
|
print("📊 CSV Send Summary" + (" (DRY RUN)" if args.dry_run else ""))
|
||||||
print(f" • {e}")
|
print("=" * 50)
|
||||||
print("=" * 50)
|
print(f" Total rows: {total}")
|
||||||
|
if args.dry_run:
|
||||||
|
print(f" 🔎 Would send: {previewed}")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Success: {success}")
|
||||||
|
print(f" ⏭️ Skipped: {skipped}")
|
||||||
|
print(f" ❌ Failed: {failed}")
|
||||||
|
if errors:
|
||||||
|
print()
|
||||||
|
print(" Notes:")
|
||||||
|
for e in errors:
|
||||||
|
print(f" • {e}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
if failed > 0:
|
if failed > 0:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@ -750,6 +824,24 @@ def cmd_show_config(args):
|
|||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_accounts(result):
|
||||||
|
"""Normalize the various shapes /v1/accounts may return into a list of str.
|
||||||
|
|
||||||
|
The REST API has returned a bare list, a {'accounts': [...]} dict, and a
|
||||||
|
single {'number': ...} dict across versions. Returns [] for None/unknown.
|
||||||
|
"""
|
||||||
|
if result is None:
|
||||||
|
return []
|
||||||
|
if isinstance(result, list):
|
||||||
|
return [str(a) for a in result]
|
||||||
|
if isinstance(result, dict):
|
||||||
|
if "accounts" in result and isinstance(result["accounts"], list):
|
||||||
|
return [str(a) for a in result["accounts"]]
|
||||||
|
if "number" in result:
|
||||||
|
return [str(result["number"])]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def cmd_list_signal(args):
|
def cmd_list_signal(args):
|
||||||
"""List linked Signal accounts via the REST API."""
|
"""List linked Signal accounts via the REST API."""
|
||||||
result = signal_rest_get("/v1/accounts")
|
result = signal_rest_get("/v1/accounts")
|
||||||
@ -757,14 +849,7 @@ def cmd_list_signal(args):
|
|||||||
print(f"❌ Failed to list accounts (cannot reach {SIGNAL_REST_URL})", file=sys.stderr)
|
print(f"❌ Failed to list accounts (cannot reach {SIGNAL_REST_URL})", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if isinstance(result, list):
|
accounts = _extract_accounts(result)
|
||||||
accounts = [str(a) for a in result]
|
|
||||||
elif isinstance(result, dict) and "accounts" in result:
|
|
||||||
accounts = result["accounts"]
|
|
||||||
elif isinstance(result, dict) and "number" in result:
|
|
||||||
accounts = [result["number"]]
|
|
||||||
else:
|
|
||||||
accounts = [str(result)]
|
|
||||||
|
|
||||||
if not accounts:
|
if not accounts:
|
||||||
print("No Signal accounts linked. Use --link-signal to set one up.")
|
print("No Signal accounts linked. Use --link-signal to set one up.")
|
||||||
@ -785,9 +870,7 @@ def cmd_link_signal(args):
|
|||||||
# Snapshot existing accounts so we can detect a genuinely NEW link rather
|
# Snapshot existing accounts so we can detect a genuinely NEW link rather
|
||||||
# than reporting success just because a primary account already exists.
|
# than reporting success just because a primary account already exists.
|
||||||
before = signal_rest_get("/v1/accounts")
|
before = signal_rest_get("/v1/accounts")
|
||||||
before_set = set()
|
before_set = set(_extract_accounts(before))
|
||||||
if isinstance(before, list):
|
|
||||||
before_set = {str(a) for a in before}
|
|
||||||
|
|
||||||
payload = {"name": name}
|
payload = {"name": name}
|
||||||
result = signal_rest_post("/v1/link", payload=payload)
|
result = signal_rest_post("/v1/link", payload=payload)
|
||||||
@ -811,13 +894,11 @@ def cmd_link_signal(args):
|
|||||||
start = time.time()
|
start = time.time()
|
||||||
while time.time() - start < 120:
|
while time.time() - start < 120:
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
current = signal_rest_get("/v1/accounts")
|
current_set = set(_extract_accounts(signal_rest_get("/v1/accounts")))
|
||||||
if isinstance(current, list):
|
new_accounts = current_set - before_set
|
||||||
current_set = {str(a) for a in current}
|
if new_accounts:
|
||||||
new_accounts = current_set - before_set
|
print(f"✅ Device linked! New account(s): {', '.join(sorted(new_accounts))}")
|
||||||
if new_accounts:
|
return
|
||||||
print(f"✅ Device linked! New account(s): {', '.join(sorted(new_accounts))}")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("⚠️ Linking timed out (no new account detected). Please try again.", file=sys.stderr)
|
print("⚠️ Linking timed out (no new account detected). Please try again.", file=sys.stderr)
|
||||||
|
|
||||||
@ -843,7 +924,7 @@ def main():
|
|||||||
" %(prog)s --list-signal\n"
|
" %(prog)s --list-signal\n"
|
||||||
" %(prog)s --link-signal --name 'my-automation'\n"
|
" %(prog)s --link-signal --name 'my-automation'\n"
|
||||||
"\n"
|
"\n"
|
||||||
"CSV columns: method, recipient, name, message, account, service, file, delay\n"
|
"CSV columns: method, recipient, name, message, account, service, file, voice, delay\n"
|
||||||
"The 'name' field is printed in the status output during CSV sends.\n"
|
"The 'name' field is printed in the status output during CSV sends.\n"
|
||||||
"For SMS rows the 'service' column must be 'sms' or 'imessage'.\n"
|
"For SMS rows the 'service' column must be 'sms' or 'imessage'.\n"
|
||||||
f"Signal REST API: {SIGNAL_REST_URL}\n"
|
f"Signal REST API: {SIGNAL_REST_URL}\n"
|
||||||
@ -873,7 +954,8 @@ def main():
|
|||||||
# Signal-specific
|
# Signal-specific
|
||||||
signal_grp = parser.add_argument_group("Signal options")
|
signal_grp = parser.add_argument_group("Signal options")
|
||||||
signal_grp.add_argument("--account", metavar="PHONE", help="Signal account phone number to use")
|
signal_grp.add_argument("--account", metavar="PHONE", help="Signal account phone number to use")
|
||||||
signal_grp.add_argument("--recipients", metavar="RECIPIENTS", help="Send to Signal group ID (no --to needed)")
|
signal_grp.add_argument("--recipients", nargs="+", metavar="RECIPIENT",
|
||||||
|
help="Send to one or more Signal group IDs / recipients (no --to needed)")
|
||||||
signal_grp.add_argument("--voice", metavar="AUDIO", help="Send an audio file as a Signal voice message")
|
signal_grp.add_argument("--voice", metavar="AUDIO", help="Send an audio file as a Signal voice message")
|
||||||
signal_grp.add_argument("--name", metavar="NAME", help="Device name for Signal linking (default: 'automation')")
|
signal_grp.add_argument("--name", metavar="NAME", help="Device name for Signal linking (default: 'automation')")
|
||||||
|
|
||||||
@ -895,6 +977,8 @@ def main():
|
|||||||
csv_grp.add_argument("--dry-run", action="store_true", help="Preview a CSV batch without sending")
|
csv_grp.add_argument("--dry-run", action="store_true", help="Preview a CSV batch without sending")
|
||||||
csv_grp.add_argument("--yes", "-y", action="store_true",
|
csv_grp.add_argument("--yes", "-y", action="store_true",
|
||||||
help="Skip the large-batch confirmation prompt")
|
help="Skip the large-batch confirmation prompt")
|
||||||
|
csv_grp.add_argument("--json", action="store_true", dest="json_output",
|
||||||
|
help="Emit a machine-readable JSON summary (for unattended runs)")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user