Implement executing tasks

This commit is contained in:
2026-03-03 16:41:28 +00:00
parent d6bd56dace
commit 9c14e51b43
42 changed files with 3410 additions and 121 deletions

View File

@@ -193,6 +193,28 @@ def _extract_signal_reaction(envelope):
}
def _extract_signal_text(raw_payload, default_text=""):
text = str(default_text or "").strip()
if text:
return text
payload = dict(raw_payload or {})
envelope = dict(payload.get("envelope") or {})
candidates = [
envelope.get("dataMessage"),
_get_nested(envelope, ("syncMessage", "sentMessage", "message")),
_get_nested(envelope, ("syncMessage", "sentMessage")),
payload.get("dataMessage"),
payload,
]
for item in candidates:
if isinstance(item, dict):
for key in ("message", "text", "body", "caption"):
value = str(item.get(key) or "").strip()
if value:
return value
return ""
def _typing_started(typing_payload):
action = str(typing_payload.get("action") or "").strip().lower()
if action in {"started", "start", "typing", "composing"}:
@@ -368,6 +390,7 @@ class HandleMessage(Command):
source_number = c.message.source_number
source_uuid = c.message.source_uuid
text = c.message.text
text = _extract_signal_text(raw, text)
ts = c.message.timestamp
source_value = c.message.source
envelope = raw.get("envelope", {})
@@ -1209,14 +1232,17 @@ class SignalClient(ClientBase):
if isinstance(sync_sent_message, dict) and sync_sent_message:
raw_text = sync_sent_message.get("message")
if isinstance(raw_text, dict):
text = str(
raw_text.get("message")
or raw_text.get("text")
or raw_text.get("body")
or ""
).strip()
text = _extract_signal_text(
{"envelope": {"syncMessage": {"sentMessage": {"message": raw_text}}}},
str(
raw_text.get("message")
or raw_text.get("text")
or raw_text.get("body")
or ""
).strip(),
)
else:
text = str(raw_text or "").strip()
text = _extract_signal_text(payload, str(raw_text or "").strip())
destination_uuid = str(
sync_sent_message.get("destinationUuid")
@@ -1373,7 +1399,7 @@ class SignalClient(ClientBase):
)
return
text = str(data_message.get("message") or "").strip()
text = _extract_signal_text(payload, str(data_message.get("message") or "").strip())
if not text:
return

View File

@@ -2355,8 +2355,29 @@ class WhatsAppClient(ClientBase):
return "application/octet-stream"
def _extract_reaction_event(self, message_obj):
node = self._pluck(message_obj, "reactionMessage") or self._pluck(
message_obj, "reaction_message"
node = (
self._pluck(message_obj, "reactionMessage")
or self._pluck(message_obj, "reaction_message")
or self._pluck(message_obj, "ephemeralMessage", "message", "reactionMessage")
or self._pluck(message_obj, "ephemeral_message", "message", "reaction_message")
or self._pluck(message_obj, "viewOnceMessage", "message", "reactionMessage")
or self._pluck(message_obj, "view_once_message", "message", "reaction_message")
or self._pluck(message_obj, "viewOnceMessageV2", "message", "reactionMessage")
or self._pluck(message_obj, "view_once_message_v2", "message", "reaction_message")
or self._pluck(
message_obj,
"viewOnceMessageV2Extension",
"message",
"reactionMessage",
)
or self._pluck(
message_obj,
"view_once_message_v2_extension",
"message",
"reaction_message",
)
or self._pluck(message_obj, "protocolMessage", "reactionMessage")
or self._pluck(message_obj, "protocol_message", "reaction_message")
)
if not node:
return None
@@ -2366,17 +2387,34 @@ class WhatsAppClient(ClientBase):
target_msg_id = str(
self._pluck(node, "key", "id")
or self._pluck(node, "key", "ID")
or self._pluck(node, "messageKey", "id")
or self._pluck(node, "message_key", "id")
or self._pluck(node, "targetMessageKey", "id")
or self._pluck(node, "target_message_key", "id")
or self._pluck(node, "stanzaId")
or self._pluck(node, "stanza_id")
or ""
).strip()
remove = bool(not emoji)
target_ts = self._normalize_timestamp(
self._pluck(node, "key", "messageTimestamp")
or self._pluck(node, "targetMessageKey", "messageTimestamp")
or self._pluck(node, "target_message_key", "message_timestamp")
or self._pluck(node, "targetTimestamp")
or self._pluck(node, "target_timestamp")
or 0
)
explicit_remove = self._pluck(node, "remove") or self._pluck(node, "isRemove")
if explicit_remove is None:
explicit_remove = self._pluck(node, "is_remove")
remove = bool(explicit_remove) if explicit_remove is not None else bool(not emoji)
if not target_msg_id:
return None
return {
"emoji": emoji,
"target_message_id": target_msg_id,
"remove": remove,
"target_ts": int(target_ts or 0),
"raw": self._proto_to_dict(node) or dict(node or {}) if isinstance(node, dict) else {},
}
async def _download_event_media(self, event):
@@ -2438,6 +2476,10 @@ class WhatsAppClient(ClientBase):
async def _handle_message_event(self, event):
event_obj = self._proto_to_dict(event) or event
msg_obj = self._pluck(event_obj, "message") or self._pluck(event_obj, "Message")
if self._pluck(msg_obj, "protocolMessage") or self._pluck(
msg_obj, "protocol_message"
):
return
text = self._message_text(msg_obj, event_obj)
if not text:
self.log.debug(
@@ -2482,7 +2524,7 @@ class WhatsAppClient(ClientBase):
).strip()
ts = self._normalize_timestamp(raw_ts)
reaction_payload = self._extract_reaction_event(msg_obj)
reaction_payload = self._extract_reaction_event(msg_obj or event_obj)
if reaction_payload:
self.log.debug(
"reaction-bridge whatsapp-inbound msg_id=%s target_id=%s emoji=%s remove=%s sender=%s chat=%s",
@@ -2508,6 +2550,26 @@ class WhatsAppClient(ClientBase):
)
)
for identifier in identifiers:
try:
await history.apply_reaction(
identifier.user,
identifier,
target_message_id=str(
reaction_payload.get("target_message_id") or ""
),
target_ts=int(reaction_payload.get("target_ts") or 0),
emoji=str(reaction_payload.get("emoji") or ""),
source_service="whatsapp",
actor=str(sender or chat or ""),
remove=bool(reaction_payload.get("remove")),
payload={
"event": "reaction",
"message_id": msg_id,
"raw": reaction_payload.get("raw") or {},
},
)
except Exception as exc:
self.log.warning("whatsapp reaction local apply failed: %s", exc)
try:
await self.ur.xmpp.client.apply_external_reaction(
identifier.user,
@@ -2527,6 +2589,21 @@ class WhatsAppClient(ClientBase):
)
except Exception as exc:
self.log.warning("whatsapp reaction relay to XMPP failed: %s", exc)
try:
await self.ur.presence_changed(
self.service,
identifier=identifier.identifier,
state="available",
confidence=0.9,
ts=int(ts or int(time.time() * 1000)),
payload={
"event": "reaction",
"inferred_from": "reaction",
"message_id": msg_id,
},
)
except Exception:
pass
return
self._remember_contact(
@@ -2907,14 +2984,42 @@ class WhatsAppClient(ClientBase):
is_unavailable = bool(
self._pluck(event, "Unavailable") or self._pluck(event, "unavailable")
)
last_seen_raw = (
self._pluck(event, "LastSeen")
or self._pluck(event, "lastSeen")
or self._pluck(event, "last_seen")
or self._pluck(event, "Timestamp")
or self._pluck(event, "timestamp")
or 0
)
last_seen_ts = self._normalize_timestamp(last_seen_raw)
self._remember_contact(sender, jid=sender)
for candidate in self._normalize_identifier_candidates(sender):
try:
await self.ur.presence_changed(
self.service,
identifier=candidate,
state=("unavailable" if is_unavailable else "available"),
confidence=0.9 if not is_unavailable else 0.8,
ts=int(last_seen_ts or int(time.time() * 1000)),
payload={
"presence": ("offline" if is_unavailable else "online"),
"sender": str(sender),
"last_seen_ts": int(last_seen_ts or 0),
},
)
except Exception:
pass
if is_unavailable:
await self.ur.stopped_typing(
self.service,
identifier=candidate,
payload={"presence": "offline", "sender": str(sender)},
payload={
"presence": "offline",
"sender": str(sender),
"last_seen_ts": int(last_seen_ts or 0),
},
)
def _extract_pair_qr(self, event):