Begin WhatsApp integration
This commit is contained in:
@@ -238,7 +238,23 @@ async def send_message_raw(service: str, recipient: str, text=None, attachments=
|
|||||||
if service_key == "signal":
|
if service_key == "signal":
|
||||||
return await signalapi.send_message_raw(recipient, text, attachments or [])
|
return await signalapi.send_message_raw(recipient, text, attachments or [])
|
||||||
|
|
||||||
if service_key in {"whatsapp", "instagram"}:
|
if service_key == "whatsapp":
|
||||||
|
runtime_client = get_runtime_client(service_key)
|
||||||
|
if runtime_client and hasattr(runtime_client, "send_message_raw"):
|
||||||
|
try:
|
||||||
|
runtime_result = await runtime_client.send_message_raw(
|
||||||
|
recipient,
|
||||||
|
text=text,
|
||||||
|
attachments=attachments or [],
|
||||||
|
)
|
||||||
|
if runtime_result is not False and runtime_result is not None:
|
||||||
|
return runtime_result
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("%s runtime send failed: %s", service_key, exc)
|
||||||
|
log.warning("whatsapp send skipped: runtime is unavailable or not paired")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if service_key == "instagram":
|
||||||
runtime_client = get_runtime_client(service_key)
|
runtime_client = get_runtime_client(service_key)
|
||||||
if runtime_client and hasattr(runtime_client, "send_message_raw"):
|
if runtime_client and hasattr(runtime_client, "send_message_raw"):
|
||||||
try:
|
try:
|
||||||
@@ -269,7 +285,18 @@ async def start_typing(service: str, recipient: str):
|
|||||||
await signalapi.start_typing(recipient)
|
await signalapi.start_typing(recipient)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if service_key in {"whatsapp", "instagram"}:
|
if service_key == "whatsapp":
|
||||||
|
runtime_client = get_runtime_client(service_key)
|
||||||
|
if runtime_client and hasattr(runtime_client, "start_typing"):
|
||||||
|
try:
|
||||||
|
result = await runtime_client.start_typing(recipient)
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("%s runtime start_typing failed: %s", service_key, exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if service_key == "instagram":
|
||||||
runtime_client = get_runtime_client(service_key)
|
runtime_client = get_runtime_client(service_key)
|
||||||
if runtime_client and hasattr(runtime_client, "start_typing"):
|
if runtime_client and hasattr(runtime_client, "start_typing"):
|
||||||
try:
|
try:
|
||||||
@@ -288,7 +315,18 @@ async def stop_typing(service: str, recipient: str):
|
|||||||
await signalapi.stop_typing(recipient)
|
await signalapi.stop_typing(recipient)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if service_key in {"whatsapp", "instagram"}:
|
if service_key == "whatsapp":
|
||||||
|
runtime_client = get_runtime_client(service_key)
|
||||||
|
if runtime_client and hasattr(runtime_client, "stop_typing"):
|
||||||
|
try:
|
||||||
|
result = await runtime_client.stop_typing(recipient)
|
||||||
|
if result:
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("%s runtime stop_typing failed: %s", service_key, exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if service_key == "instagram":
|
||||||
runtime_client = get_runtime_client(service_key)
|
runtime_client = get_runtime_client(service_key)
|
||||||
if runtime_client and hasattr(runtime_client, "stop_typing"):
|
if runtime_client and hasattr(runtime_client, "stop_typing"):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class WhatsAppClient(ClientBase):
|
|||||||
self._connected = False
|
self._connected = False
|
||||||
self._last_qr_payload = ""
|
self._last_qr_payload = ""
|
||||||
self._accounts = []
|
self._accounts = []
|
||||||
|
self._chat_presence = None
|
||||||
|
self._chat_presence_media = None
|
||||||
|
|
||||||
self.enabled = bool(
|
self.enabled = bool(
|
||||||
str(getattr(settings, "WHATSAPP_ENABLED", "false")).lower()
|
str(getattr(settings, "WHATSAPP_ENABLED", "false")).lower()
|
||||||
@@ -71,6 +73,11 @@ class WhatsAppClient(ClientBase):
|
|||||||
try:
|
try:
|
||||||
from neonize.aioze.client import NewAClient
|
from neonize.aioze.client import NewAClient
|
||||||
from neonize.aioze import events as wa_events
|
from neonize.aioze import events as wa_events
|
||||||
|
try:
|
||||||
|
from neonize.utils.enum import ChatPresence, ChatPresenceMedia
|
||||||
|
except Exception:
|
||||||
|
ChatPresence = None
|
||||||
|
ChatPresenceMedia = None
|
||||||
try:
|
try:
|
||||||
from neonize.utils import build_jid as wa_build_jid
|
from neonize.utils import build_jid as wa_build_jid
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -85,6 +92,8 @@ class WhatsAppClient(ClientBase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._build_jid = wa_build_jid
|
self._build_jid = wa_build_jid
|
||||||
|
self._chat_presence = ChatPresence
|
||||||
|
self._chat_presence_media = ChatPresenceMedia
|
||||||
self._client = self._build_client(NewAClient)
|
self._client = self._build_client(NewAClient)
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._publish_state(
|
self._publish_state(
|
||||||
@@ -113,6 +122,59 @@ class WhatsAppClient(ClientBase):
|
|||||||
while not self._stopping:
|
while not self._stopping:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
def _register_event(self, event_cls, callback):
|
||||||
|
if event_cls is None:
|
||||||
|
return
|
||||||
|
if self._client is None:
|
||||||
|
return
|
||||||
|
event_hook = getattr(self._client, "event", None)
|
||||||
|
if not callable(event_hook):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
decorator = event_hook(event_cls)
|
||||||
|
decorator(callback)
|
||||||
|
except Exception as exc:
|
||||||
|
self.log.warning(
|
||||||
|
"whatsapp event registration failed (%s): %s",
|
||||||
|
getattr(event_cls, "__name__", str(event_cls)),
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_qr_handler(self):
|
||||||
|
if self._client is None or not hasattr(self._client, "qr"):
|
||||||
|
return
|
||||||
|
|
||||||
|
async def on_qr(client, raw_payload):
|
||||||
|
qr_payload = self._decode_qr_payload(raw_payload)
|
||||||
|
if not qr_payload:
|
||||||
|
return
|
||||||
|
self._last_qr_payload = qr_payload
|
||||||
|
self._publish_state(
|
||||||
|
connected=False,
|
||||||
|
pair_qr=qr_payload,
|
||||||
|
warning="Scan QR in WhatsApp Linked Devices.",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._client.qr(on_qr)
|
||||||
|
except Exception as exc:
|
||||||
|
self.log.warning("whatsapp qr handler registration failed: %s", exc)
|
||||||
|
|
||||||
|
def _decode_qr_payload(self, raw_payload):
|
||||||
|
if raw_payload is None:
|
||||||
|
return ""
|
||||||
|
if isinstance(raw_payload, memoryview):
|
||||||
|
raw_payload = raw_payload.tobytes()
|
||||||
|
if isinstance(raw_payload, bytes):
|
||||||
|
return raw_payload.decode("utf-8", errors="ignore").strip()
|
||||||
|
if isinstance(raw_payload, (list, tuple)):
|
||||||
|
for item in raw_payload:
|
||||||
|
candidate = self._decode_qr_payload(item)
|
||||||
|
if candidate:
|
||||||
|
return candidate
|
||||||
|
return ""
|
||||||
|
return str(raw_payload).strip()
|
||||||
|
|
||||||
def _build_client(self, cls):
|
def _build_client(self, cls):
|
||||||
candidates = []
|
candidates = []
|
||||||
if self.database_url:
|
if self.database_url:
|
||||||
@@ -137,8 +199,12 @@ class WhatsAppClient(ClientBase):
|
|||||||
connected_ev = getattr(wa_events, "ConnectedEv", None)
|
connected_ev = getattr(wa_events, "ConnectedEv", None)
|
||||||
message_ev = getattr(wa_events, "MessageEv", None)
|
message_ev = getattr(wa_events, "MessageEv", None)
|
||||||
receipt_ev = getattr(wa_events, "ReceiptEv", None)
|
receipt_ev = getattr(wa_events, "ReceiptEv", None)
|
||||||
|
chat_presence_ev = getattr(wa_events, "ChatPresenceEv", None)
|
||||||
presence_ev = getattr(wa_events, "PresenceEv", None)
|
presence_ev = getattr(wa_events, "PresenceEv", None)
|
||||||
pair_ev = getattr(wa_events, "PairStatusEv", None)
|
pair_ev = getattr(wa_events, "PairStatusEv", None)
|
||||||
|
qr_ev = getattr(wa_events, "QREv", None)
|
||||||
|
|
||||||
|
self._register_qr_handler()
|
||||||
|
|
||||||
if connected_ev is not None:
|
if connected_ev is not None:
|
||||||
|
|
||||||
@@ -149,30 +215,38 @@ class WhatsAppClient(ClientBase):
|
|||||||
connected=True,
|
connected=True,
|
||||||
warning="",
|
warning="",
|
||||||
accounts=[account] if account else [self.client_name],
|
accounts=[account] if account else [self.client_name],
|
||||||
|
pair_qr="",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._client.event(on_connected)
|
self._register_event(connected_ev, on_connected)
|
||||||
|
|
||||||
if message_ev is not None:
|
if message_ev is not None:
|
||||||
|
|
||||||
async def on_message(client, event: message_ev):
|
async def on_message(client, event: message_ev):
|
||||||
await self._handle_message_event(event)
|
await self._handle_message_event(event)
|
||||||
|
|
||||||
self._client.event(on_message)
|
self._register_event(message_ev, on_message)
|
||||||
|
|
||||||
if receipt_ev is not None:
|
if receipt_ev is not None:
|
||||||
|
|
||||||
async def on_receipt(client, event: receipt_ev):
|
async def on_receipt(client, event: receipt_ev):
|
||||||
await self._handle_receipt_event(event)
|
await self._handle_receipt_event(event)
|
||||||
|
|
||||||
self._client.event(on_receipt)
|
self._register_event(receipt_ev, on_receipt)
|
||||||
|
|
||||||
|
if chat_presence_ev is not None:
|
||||||
|
|
||||||
|
async def on_chat_presence(client, event: chat_presence_ev):
|
||||||
|
await self._handle_chat_presence_event(event)
|
||||||
|
|
||||||
|
self._register_event(chat_presence_ev, on_chat_presence)
|
||||||
|
|
||||||
if presence_ev is not None:
|
if presence_ev is not None:
|
||||||
|
|
||||||
async def on_presence(client, event: presence_ev):
|
async def on_presence(client, event: presence_ev):
|
||||||
await self._handle_presence_event(event)
|
await self._handle_presence_event(event)
|
||||||
|
|
||||||
self._client.event(on_presence)
|
self._register_event(presence_ev, on_presence)
|
||||||
|
|
||||||
if pair_ev is not None:
|
if pair_ev is not None:
|
||||||
|
|
||||||
@@ -182,10 +256,41 @@ class WhatsAppClient(ClientBase):
|
|||||||
self._last_qr_payload = qr_payload
|
self._last_qr_payload = qr_payload
|
||||||
self._publish_state(
|
self._publish_state(
|
||||||
pair_qr=qr_payload,
|
pair_qr=qr_payload,
|
||||||
warning="Scan QR to pair WhatsApp account.",
|
warning="Scan QR in WhatsApp Linked Devices.",
|
||||||
|
)
|
||||||
|
status_raw = self._pluck(event, "Status")
|
||||||
|
status_text = str(status_raw or "").strip().lower()
|
||||||
|
if status_text in {"2", "success"}:
|
||||||
|
account = await self._resolve_account_identifier()
|
||||||
|
self._connected = True
|
||||||
|
self._publish_state(
|
||||||
|
connected=True,
|
||||||
|
warning="",
|
||||||
|
accounts=[account] if account else [self.client_name],
|
||||||
|
pair_qr="",
|
||||||
|
)
|
||||||
|
elif status_text in {"1", "error"}:
|
||||||
|
error_text = str(self._pluck(event, "Error") or "").strip()
|
||||||
|
self._publish_state(
|
||||||
|
warning=error_text or "WhatsApp pairing failed. Retry scan.",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._client.event(on_pair_status)
|
self._register_event(pair_ev, on_pair_status)
|
||||||
|
|
||||||
|
if qr_ev is not None:
|
||||||
|
|
||||||
|
async def on_qr_event(client, event: qr_ev):
|
||||||
|
qr_payload = self._extract_pair_qr(event)
|
||||||
|
if not qr_payload:
|
||||||
|
return
|
||||||
|
self._last_qr_payload = qr_payload
|
||||||
|
self._publish_state(
|
||||||
|
connected=False,
|
||||||
|
pair_qr=qr_payload,
|
||||||
|
warning="Scan QR in WhatsApp Linked Devices.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self._register_event(qr_ev, on_qr_event)
|
||||||
|
|
||||||
async def _maybe_await(self, value):
|
async def _maybe_await(self, value):
|
||||||
if asyncio.iscoroutine(value):
|
if asyncio.iscoroutine(value):
|
||||||
@@ -202,6 +307,13 @@ class WhatsAppClient(ClientBase):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return self.client_name
|
return self.client_name
|
||||||
# Support both dict-like and object-like payloads.
|
# Support both dict-like and object-like payloads.
|
||||||
|
for path in (
|
||||||
|
("JID",),
|
||||||
|
("jid",),
|
||||||
|
):
|
||||||
|
value = self._jid_to_identifier(self._pluck(me, *path))
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
for path in (
|
for path in (
|
||||||
("JID", "User"),
|
("JID", "User"),
|
||||||
("jid",),
|
("jid",),
|
||||||
@@ -242,7 +354,7 @@ class WhatsAppClient(ClientBase):
|
|||||||
def _normalize_identifier_candidates(self, *values):
|
def _normalize_identifier_candidates(self, *values):
|
||||||
out = set()
|
out = set()
|
||||||
for value in values:
|
for value in values:
|
||||||
raw = str(value or "").strip()
|
raw = self._jid_to_identifier(value)
|
||||||
if not raw:
|
if not raw:
|
||||||
continue
|
continue
|
||||||
out.add(raw)
|
out.add(raw)
|
||||||
@@ -255,6 +367,19 @@ class WhatsAppClient(ClientBase):
|
|||||||
out.add(f"+{digits}")
|
out.add(f"+{digits}")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def _jid_to_identifier(self, value):
|
||||||
|
raw = str(value or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return ""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
user = self._pluck(value, "User") or self._pluck(value, "user")
|
||||||
|
server = self._pluck(value, "Server") or self._pluck(value, "server")
|
||||||
|
if user and server:
|
||||||
|
return f"{user}@{server}"
|
||||||
|
if user:
|
||||||
|
return str(user)
|
||||||
|
return raw
|
||||||
|
|
||||||
def _is_media_message(self, message_obj):
|
def _is_media_message(self, message_obj):
|
||||||
media_fields = (
|
media_fields = (
|
||||||
"imageMessage",
|
"imageMessage",
|
||||||
@@ -277,7 +402,7 @@ class WhatsAppClient(ClientBase):
|
|||||||
async def _download_event_media(self, event):
|
async def _download_event_media(self, event):
|
||||||
if not self._client:
|
if not self._client:
|
||||||
return []
|
return []
|
||||||
msg_obj = self._pluck(event, "message")
|
msg_obj = self._pluck(event, "message") or self._pluck(event, "Message")
|
||||||
if msg_obj is None or not self._is_media_message(msg_obj):
|
if msg_obj is None or not self._is_media_message(msg_obj):
|
||||||
return []
|
return []
|
||||||
if not hasattr(self._client, "download_any"):
|
if not hasattr(self._client, "download_any"):
|
||||||
@@ -324,27 +449,42 @@ class WhatsAppClient(ClientBase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
async def _handle_message_event(self, event):
|
async def _handle_message_event(self, event):
|
||||||
msg_obj = self._pluck(event, "message")
|
msg_obj = self._pluck(event, "message") or self._pluck(event, "Message")
|
||||||
text = (
|
text = (
|
||||||
self._pluck(msg_obj, "conversation")
|
self._pluck(msg_obj, "conversation")
|
||||||
|
or self._pluck(msg_obj, "Conversation")
|
||||||
or self._pluck(msg_obj, "extendedTextMessage", "text")
|
or self._pluck(msg_obj, "extendedTextMessage", "text")
|
||||||
|
or self._pluck(msg_obj, "ExtendedTextMessage", "Text")
|
||||||
or self._pluck(msg_obj, "extended_text_message", "text")
|
or self._pluck(msg_obj, "extended_text_message", "text")
|
||||||
or ""
|
or ""
|
||||||
)
|
)
|
||||||
|
source = (
|
||||||
sender = (
|
self._pluck(event, "Info", "MessageSource")
|
||||||
self._pluck(event, "info", "message_source", "sender")
|
or self._pluck(event, "info", "message_source")
|
||||||
or self._pluck(event, "info", "messageSource", "sender")
|
or self._pluck(event, "info", "messageSource")
|
||||||
or ""
|
|
||||||
)
|
)
|
||||||
chat = (
|
is_from_me = bool(
|
||||||
self._pluck(event, "info", "message_source", "chat")
|
self._pluck(source, "IsFromMe")
|
||||||
or self._pluck(event, "info", "messageSource", "chat")
|
or self._pluck(source, "isFromMe")
|
||||||
or ""
|
)
|
||||||
|
if is_from_me:
|
||||||
|
return
|
||||||
|
|
||||||
|
sender = self._jid_to_identifier(
|
||||||
|
self._pluck(source, "Sender")
|
||||||
|
or self._pluck(source, "sender")
|
||||||
|
or self._pluck(source, "SenderAlt")
|
||||||
|
or self._pluck(source, "senderAlt")
|
||||||
|
)
|
||||||
|
chat = self._jid_to_identifier(
|
||||||
|
self._pluck(source, "Chat")
|
||||||
|
or self._pluck(source, "chat")
|
||||||
)
|
)
|
||||||
raw_ts = (
|
raw_ts = (
|
||||||
self._pluck(event, "info", "timestamp")
|
self._pluck(event, "Info", "Timestamp")
|
||||||
|
or self._pluck(event, "info", "timestamp")
|
||||||
or self._pluck(event, "info", "message_timestamp")
|
or self._pluck(event, "info", "message_timestamp")
|
||||||
|
or self._pluck(event, "Timestamp")
|
||||||
or self._pluck(event, "timestamp")
|
or self._pluck(event, "timestamp")
|
||||||
)
|
)
|
||||||
ts = self._normalize_timestamp(raw_ts)
|
ts = self._normalize_timestamp(raw_ts)
|
||||||
@@ -401,26 +541,33 @@ class WhatsAppClient(ClientBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_receipt_event(self, event):
|
async def _handle_receipt_event(self, event):
|
||||||
sender = (
|
source = (
|
||||||
self._pluck(event, "info", "message_source", "sender")
|
self._pluck(event, "MessageSource")
|
||||||
or self._pluck(event, "info", "messageSource", "sender")
|
or self._pluck(event, "info", "message_source")
|
||||||
or ""
|
or self._pluck(event, "info", "messageSource")
|
||||||
)
|
)
|
||||||
chat = (
|
sender = self._jid_to_identifier(
|
||||||
self._pluck(event, "info", "message_source", "chat")
|
self._pluck(source, "Sender")
|
||||||
or self._pluck(event, "info", "messageSource", "chat")
|
or self._pluck(source, "sender")
|
||||||
or ""
|
)
|
||||||
|
chat = self._jid_to_identifier(
|
||||||
|
self._pluck(source, "Chat") or self._pluck(source, "chat")
|
||||||
)
|
)
|
||||||
timestamps = []
|
timestamps = []
|
||||||
raw_ids = self._pluck(event, "message_ids") or []
|
raw_ids = self._pluck(event, "MessageIDs") or self._pluck(event, "message_ids") or []
|
||||||
if isinstance(raw_ids, list):
|
if isinstance(raw_ids, (list, tuple, set)):
|
||||||
for item in raw_ids:
|
for item in raw_ids:
|
||||||
try:
|
try:
|
||||||
value = int(item)
|
value = int(item)
|
||||||
timestamps.append(value * 1000 if value < 10**12 else value)
|
timestamps.append(value * 1000 if value < 10**12 else value)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
read_ts = self._normalize_timestamp(self._pluck(event, "timestamp") or int(time.time() * 1000))
|
read_ts = self._normalize_timestamp(
|
||||||
|
self._pluck(event, "Timestamp")
|
||||||
|
or self._pluck(event, "timestamp")
|
||||||
|
or int(time.time() * 1000)
|
||||||
|
)
|
||||||
|
receipt_type = str(self._pluck(event, "Type") or "").strip()
|
||||||
|
|
||||||
for candidate in self._normalize_identifier_candidates(sender, chat):
|
for candidate in self._normalize_identifier_candidates(sender, chat):
|
||||||
await self.ur.message_read(
|
await self.ur.message_read(
|
||||||
@@ -429,37 +576,68 @@ class WhatsAppClient(ClientBase):
|
|||||||
message_timestamps=timestamps,
|
message_timestamps=timestamps,
|
||||||
read_ts=read_ts,
|
read_ts=read_ts,
|
||||||
read_by=sender or chat,
|
read_by=sender or chat,
|
||||||
payload={"event": "receipt", "sender": str(sender), "chat": str(chat)},
|
payload={
|
||||||
|
"event": "receipt",
|
||||||
|
"type": receipt_type,
|
||||||
|
"sender": str(sender),
|
||||||
|
"chat": str(chat),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _handle_presence_event(self, event):
|
async def _handle_chat_presence_event(self, event):
|
||||||
sender = (
|
source = (
|
||||||
self._pluck(event, "message_source", "sender")
|
self._pluck(event, "MessageSource")
|
||||||
or self._pluck(event, "info", "message_source", "sender")
|
or self._pluck(event, "message_source")
|
||||||
or ""
|
or {}
|
||||||
)
|
)
|
||||||
chat = (
|
sender = self._jid_to_identifier(
|
||||||
self._pluck(event, "message_source", "chat")
|
self._pluck(source, "Sender")
|
||||||
or self._pluck(event, "info", "message_source", "chat")
|
or self._pluck(source, "sender")
|
||||||
or ""
|
|
||||||
)
|
)
|
||||||
presence = str(self._pluck(event, "presence") or "").strip().lower()
|
chat = self._jid_to_identifier(
|
||||||
|
self._pluck(source, "Chat") or self._pluck(source, "chat")
|
||||||
|
)
|
||||||
|
state = self._pluck(event, "State") or self._pluck(event, "state")
|
||||||
|
state_text = str(state or "").strip().lower()
|
||||||
|
is_typing = state_text in {"1", "composing", "chat_presence_composing"}
|
||||||
|
|
||||||
for candidate in self._normalize_identifier_candidates(sender, chat):
|
for candidate in self._normalize_identifier_candidates(sender, chat):
|
||||||
if presence in {"composing", "typing", "recording"}:
|
if is_typing:
|
||||||
await self.ur.started_typing(
|
await self.ur.started_typing(
|
||||||
self.service,
|
self.service,
|
||||||
identifier=candidate,
|
identifier=candidate,
|
||||||
payload={"presence": presence, "sender": str(sender), "chat": str(chat)},
|
payload={"presence": state_text, "sender": str(sender), "chat": str(chat)},
|
||||||
)
|
)
|
||||||
elif presence:
|
else:
|
||||||
await self.ur.stopped_typing(
|
await self.ur.stopped_typing(
|
||||||
self.service,
|
self.service,
|
||||||
identifier=candidate,
|
identifier=candidate,
|
||||||
payload={"presence": presence, "sender": str(sender), "chat": str(chat)},
|
payload={"presence": state_text, "sender": str(sender), "chat": str(chat)},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _handle_presence_event(self, event):
|
||||||
|
sender = self._jid_to_identifier(
|
||||||
|
self._pluck(event, "From", "User")
|
||||||
|
or self._pluck(event, "from", "user")
|
||||||
|
)
|
||||||
|
is_unavailable = bool(
|
||||||
|
self._pluck(event, "Unavailable") or self._pluck(event, "unavailable")
|
||||||
|
)
|
||||||
|
|
||||||
|
for candidate in self._normalize_identifier_candidates(sender):
|
||||||
|
if is_unavailable:
|
||||||
|
await self.ur.stopped_typing(
|
||||||
|
self.service,
|
||||||
|
identifier=candidate,
|
||||||
|
payload={"presence": "offline", "sender": str(sender)},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _extract_pair_qr(self, event):
|
def _extract_pair_qr(self, event):
|
||||||
|
codes = self._pluck(event, "Codes") or self._pluck(event, "codes") or []
|
||||||
|
decoded_codes = self._decode_qr_payload(codes)
|
||||||
|
if decoded_codes:
|
||||||
|
return decoded_codes
|
||||||
|
|
||||||
for path in (
|
for path in (
|
||||||
("qr",),
|
("qr",),
|
||||||
("qr_code",),
|
("qr_code",),
|
||||||
@@ -537,6 +715,7 @@ class WhatsAppClient(ClientBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
sent_any = False
|
sent_any = False
|
||||||
|
sent_ts = 0
|
||||||
for attachment in attachments or []:
|
for attachment in attachments or []:
|
||||||
payload = await self._fetch_attachment_payload(attachment)
|
payload = await self._fetch_attachment_payload(attachment)
|
||||||
if not payload:
|
if not payload:
|
||||||
@@ -547,13 +726,17 @@ class WhatsAppClient(ClientBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if mime.startswith("image/") and hasattr(self._client, "send_image"):
|
if mime.startswith("image/") and hasattr(self._client, "send_image"):
|
||||||
await self._maybe_await(self._client.send_image(jid, data, caption=""))
|
response = await self._maybe_await(
|
||||||
|
self._client.send_image(jid, data, caption="")
|
||||||
|
)
|
||||||
elif mime.startswith("video/") and hasattr(self._client, "send_video"):
|
elif mime.startswith("video/") and hasattr(self._client, "send_video"):
|
||||||
await self._maybe_await(self._client.send_video(jid, data, caption=""))
|
response = await self._maybe_await(
|
||||||
|
self._client.send_video(jid, data, caption="")
|
||||||
|
)
|
||||||
elif mime.startswith("audio/") and hasattr(self._client, "send_audio"):
|
elif mime.startswith("audio/") and hasattr(self._client, "send_audio"):
|
||||||
await self._maybe_await(self._client.send_audio(jid, data))
|
response = await self._maybe_await(self._client.send_audio(jid, data))
|
||||||
elif hasattr(self._client, "send_document"):
|
elif hasattr(self._client, "send_document"):
|
||||||
await self._maybe_await(
|
response = await self._maybe_await(
|
||||||
self._client.send_document(
|
self._client.send_document(
|
||||||
jid,
|
jid,
|
||||||
data,
|
data,
|
||||||
@@ -562,22 +745,36 @@ class WhatsAppClient(ClientBase):
|
|||||||
caption="",
|
caption="",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
response = None
|
||||||
|
sent_ts = max(
|
||||||
|
sent_ts,
|
||||||
|
self._normalize_timestamp(self._pluck(response, "Timestamp") or 0),
|
||||||
|
)
|
||||||
sent_any = True
|
sent_any = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log.warning("whatsapp attachment send failed: %s", exc)
|
self.log.warning("whatsapp attachment send failed: %s", exc)
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
try:
|
try:
|
||||||
await self._maybe_await(self._client.send_message(jid, text))
|
response = await self._maybe_await(self._client.send_message(jid, text))
|
||||||
sent_any = True
|
sent_any = True
|
||||||
except TypeError:
|
except TypeError:
|
||||||
await self._maybe_await(self._client.send_message(jid, message=text))
|
response = await self._maybe_await(
|
||||||
|
self._client.send_message(jid, message=text)
|
||||||
|
)
|
||||||
sent_any = True
|
sent_any = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.log.warning("whatsapp text send failed: %s", exc)
|
self.log.warning("whatsapp text send failed: %s", exc)
|
||||||
return False
|
return False
|
||||||
|
sent_ts = max(
|
||||||
|
sent_ts,
|
||||||
|
self._normalize_timestamp(self._pluck(response, "Timestamp") or 0),
|
||||||
|
)
|
||||||
|
|
||||||
return int(time.time() * 1000) if sent_any else False
|
if not sent_any:
|
||||||
|
return False
|
||||||
|
return sent_ts or int(time.time() * 1000)
|
||||||
|
|
||||||
async def start_typing(self, identifier):
|
async def start_typing(self, identifier):
|
||||||
if not self._client:
|
if not self._client:
|
||||||
@@ -585,14 +782,28 @@ class WhatsAppClient(ClientBase):
|
|||||||
jid = self._to_jid(identifier)
|
jid = self._to_jid(identifier)
|
||||||
if not jid:
|
if not jid:
|
||||||
return False
|
return False
|
||||||
for method_name in ("send_chat_presence", "set_chat_presence"):
|
if (
|
||||||
if hasattr(self._client, method_name):
|
hasattr(self._client, "send_chat_presence")
|
||||||
method = getattr(self._client, method_name)
|
and self._chat_presence is not None
|
||||||
|
and self._chat_presence_media is not None
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
await self._maybe_await(method(jid, "composing"))
|
await self._maybe_await(
|
||||||
|
self._client.send_chat_presence(
|
||||||
|
jid,
|
||||||
|
self._chat_presence.CHAT_PRESENCE_COMPOSING,
|
||||||
|
self._chat_presence_media.CHAT_PRESENCE_MEDIA_TEXT,
|
||||||
|
)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
pass
|
||||||
|
if hasattr(self._client, "set_chat_presence"):
|
||||||
|
try:
|
||||||
|
await self._maybe_await(self._client.set_chat_presence(jid, "composing"))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def stop_typing(self, identifier):
|
async def stop_typing(self, identifier):
|
||||||
@@ -601,14 +812,28 @@ class WhatsAppClient(ClientBase):
|
|||||||
jid = self._to_jid(identifier)
|
jid = self._to_jid(identifier)
|
||||||
if not jid:
|
if not jid:
|
||||||
return False
|
return False
|
||||||
for method_name in ("send_chat_presence", "set_chat_presence"):
|
if (
|
||||||
if hasattr(self._client, method_name):
|
hasattr(self._client, "send_chat_presence")
|
||||||
method = getattr(self._client, method_name)
|
and self._chat_presence is not None
|
||||||
|
and self._chat_presence_media is not None
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
await self._maybe_await(method(jid, "paused"))
|
await self._maybe_await(
|
||||||
|
self._client.send_chat_presence(
|
||||||
|
jid,
|
||||||
|
self._chat_presence.CHAT_PRESENCE_PAUSED,
|
||||||
|
self._chat_presence_media.CHAT_PRESENCE_MEDIA_TEXT,
|
||||||
|
)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
pass
|
||||||
|
if hasattr(self._client, "set_chat_presence"):
|
||||||
|
try:
|
||||||
|
await self._maybe_await(self._client.set_chat_presence(jid, "paused"))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def fetch_attachment(self, attachment_ref):
|
async def fetch_attachment(self, attachment_ref):
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from mixins.views import ObjectList, ObjectRead
|
from mixins.views import ObjectList, ObjectRead
|
||||||
@@ -56,9 +55,6 @@ class WhatsAppAccounts(SuperUserRequiredMixin, ObjectList):
|
|||||||
"service_label": "WhatsApp",
|
"service_label": "WhatsApp",
|
||||||
"account_add_url_name": "whatsapp_account_add",
|
"account_add_url_name": "whatsapp_account_add",
|
||||||
"show_contact_actions": False,
|
"show_contact_actions": False,
|
||||||
"endpoint_base": str(
|
|
||||||
getattr(settings, "WHATSAPP_HTTP_URL", "http://whatsapp:8080")
|
|
||||||
).rstrip("/"),
|
|
||||||
"service_warning": transport.get_service_warning("whatsapp"),
|
"service_warning": transport.get_service_warning("whatsapp"),
|
||||||
}
|
}
|
||||||
return self._normalize_accounts(transport.list_accounts("whatsapp"))
|
return self._normalize_accounts(transport.list_accounts("whatsapp"))
|
||||||
|
|||||||
Reference in New Issue
Block a user