98 lines
3.4 KiB
Python
98 lines
3.4 KiB
Python
"""Signal ingestion via a signal-cli JSON-RPC daemon.
|
|
|
|
signal-cli is NOT containerised by digest-engine — assume a separate compose
|
|
service (e.g. `signal-cli`, image `bbernhard/signal-cli-rest-api` or a hand-rolled
|
|
`signal-cli daemon --http`) already linked as a secondary device to the real
|
|
account, reachable on the compose network at SIGNAL_CLI_URL (e.g.
|
|
http://signal-cli:8080). Method names/params follow
|
|
https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-jsonrpc.5.adoc
|
|
|
|
`receive` drains the account's server-side envelope queue. That drain is
|
|
protocol-mandated to read anything at all (Signal has no "peek" primitive) and is
|
|
the single exception permitted by the read-only rule — it sends no message, sets
|
|
no read receipt (`sendReadReceipts` is left at its default off) and mutates no
|
|
conversation.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import requests
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
RPC_PATH = "/api/v1/rpc"
|
|
|
|
|
|
def _envelope_to_record(envelope):
|
|
data_message = envelope.get("dataMessage") or {}
|
|
body = data_message.get("message")
|
|
if not body:
|
|
return None
|
|
timestamp_ms = envelope.get("timestamp") or data_message.get("timestamp")
|
|
sent_at = (
|
|
datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc) if timestamp_ms else None
|
|
)
|
|
group_info = data_message.get("groupInfo") or {}
|
|
return {
|
|
"source": "signal",
|
|
"from": envelope.get("sourceName") or envelope.get("sourceNumber") or envelope.get("source"),
|
|
"group": group_info.get("groupName") or group_info.get("groupId"),
|
|
"timestamp": sent_at.isoformat() if sent_at else None,
|
|
"body": body,
|
|
}
|
|
|
|
|
|
def fetch(lookback_hours):
|
|
base_url = os.environ.get("SIGNAL_CLI_URL", "").strip().rstrip("/")
|
|
account = os.environ.get("SIGNAL_ACCOUNT", "").strip()
|
|
timeout = float(os.environ.get("SIGNAL_RECEIVE_TIMEOUT", "10"))
|
|
|
|
if not base_url:
|
|
LOG.warning("signal: SIGNAL_CLI_URL not set, skipping")
|
|
return []
|
|
|
|
params = {"timeout": timeout, "sendReadReceipts": False}
|
|
if account:
|
|
params["account"] = account
|
|
|
|
payload = {
|
|
"jsonrpc": "2.0",
|
|
"id": str(uuid.uuid4()),
|
|
"method": "receive",
|
|
"params": params,
|
|
}
|
|
|
|
try:
|
|
response = requests.post(base_url + RPC_PATH, json=payload, timeout=timeout + 15)
|
|
response.raise_for_status()
|
|
result = response.json()
|
|
except Exception:
|
|
LOG.warning("signal: JSON-RPC call to %s failed, returning nothing", base_url, exc_info=True)
|
|
return []
|
|
|
|
if "error" in result:
|
|
LOG.warning("signal: daemon returned an error: %s", result["error"])
|
|
return []
|
|
|
|
since = datetime.now(timezone.utc) - timedelta(hours=lookback_hours)
|
|
messages = []
|
|
for item in result.get("result") or []:
|
|
try:
|
|
envelope = item.get("envelope") if isinstance(item, dict) else None
|
|
if not envelope:
|
|
continue
|
|
record = _envelope_to_record(envelope)
|
|
if not record:
|
|
continue
|
|
if record["timestamp"] and datetime.fromisoformat(record["timestamp"]) < since:
|
|
continue
|
|
messages.append(record)
|
|
except Exception:
|
|
LOG.warning("signal: could not parse an envelope, skipping", exc_info=True)
|
|
|
|
LOG.info("signal: %d message(s) in the last %sh", len(messages), lookback_hours)
|
|
return messages
|