Compare commits
3 Commits
8ce975d1e8
...
d63cdafb2c
Author | SHA1 | Date |
---|---|---|
nicoco | d63cdafb2c | |
nicoco | 4d9c94eeaf | |
nicoco | 1d4e801f99 |
|
@ -72,7 +72,7 @@ class PairPhone(Command):
|
|||
p = form_values.get("phone")
|
||||
if not is_valid_phone_number(p):
|
||||
raise ValueError("Not a valid phone number", p)
|
||||
code = session.whatsapp.PairPhone(p)
|
||||
code = await session.run_in_executor(session.whatsapp.PairPhone, p)
|
||||
return f"Please open the official WhatsApp client and input the following code: {code}"
|
||||
|
||||
|
||||
|
@ -114,9 +114,13 @@ class ChangePresence(Command):
|
|||
async def finish(form_values: dict, session: "Session", _ifrom: JID):
|
||||
p = form_values.get("presence")
|
||||
if p == "available":
|
||||
session.whatsapp.SendPresence(whatsapp.PresenceAvailable, "")
|
||||
await session.run_in_executor(
|
||||
session.whatsapp.SendPresence, whatsapp.PresenceAvailable, ""
|
||||
)
|
||||
elif p == "unavailable":
|
||||
session.whatsapp.SendPresence(whatsapp.PresenceUnavailable, "")
|
||||
await session.run_in_executor(
|
||||
session.whatsapp.SendPresence, whatsapp.PresenceUnavailable, ""
|
||||
)
|
||||
else:
|
||||
raise ValueError("Not a valid presence kind.", p)
|
||||
return f"Presence succesfully set to {p}"
|
||||
|
@ -140,5 +144,5 @@ class SubscribeToPresences(Command):
|
|||
*args,
|
||||
) -> str:
|
||||
assert session is not None
|
||||
session.whatsapp.GetContacts(False)
|
||||
await session.run_in_executor(session.whatsapp.GetContacts, False)
|
||||
return "Looks like no exception was raised. Success, I guess?"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from slidge import LegacyContact, LegacyRoster
|
||||
from slixmpp.exceptions import XMPPError
|
||||
|
@ -32,12 +33,18 @@ class Contact(LegacyContact[str]):
|
|||
class Roster(LegacyRoster[str, Contact]):
|
||||
session: "Session"
|
||||
|
||||
def __init__(self, *a, **k):
|
||||
super().__init__(*a, **k)
|
||||
self.__fetch_avatar_task: Optional[asyncio.Task] = None
|
||||
|
||||
async def fill(self):
|
||||
"""
|
||||
Retrieve contacts from remote WhatsApp service, subscribing to their presence and adding to
|
||||
local roster.
|
||||
"""
|
||||
contacts = self.session.whatsapp.GetContacts(refresh=config.ALWAYS_SYNC_ROSTER)
|
||||
contacts = await self.session.run_in_executor(
|
||||
self.session.whatsapp.GetContacts, config.ALWAYS_SYNC_ROSTER
|
||||
)
|
||||
for contact in contacts:
|
||||
await self.add_whatsapp_contact(contact)
|
||||
|
||||
|
@ -51,16 +58,27 @@ class Roster(LegacyRoster[str, Contact]):
|
|||
contact = await self.by_legacy_id(data.JID)
|
||||
contact.name = data.Name
|
||||
contact.is_friend = True
|
||||
contact.set_vcard(full_name=contact.name, phone=str(contact.jid.local))
|
||||
await contact.add_to_roster()
|
||||
if self.__fetch_avatar_task is not None and not self.__fetch_avatar_task.done():
|
||||
self.__fetch_avatar_task.cancel()
|
||||
self.__fetch_avatar_task = asyncio.create_task(
|
||||
self.__fetch_avatar(data, contact)
|
||||
)
|
||||
|
||||
async def __fetch_avatar(self, data: whatsapp.Contact, contact: Contact):
|
||||
try:
|
||||
avatar = self.session.whatsapp.GetAvatar(data.JID, contact.avatar or "")
|
||||
avatar = await self.session.run_in_executor(
|
||||
self.session.whatsapp.GetAvatar, data.JID, contact.avatar or ""
|
||||
)
|
||||
if avatar.URL:
|
||||
await contact.set_avatar(avatar.URL, avatar.ID)
|
||||
except RuntimeError as err:
|
||||
self.session.log.error(
|
||||
"Failed getting avatar for contact %s: %s", data.JID, err
|
||||
)
|
||||
contact.set_vcard(full_name=contact.name, phone=str(contact.jid.local))
|
||||
await contact.add_to_roster()
|
||||
except asyncio.CancelledError:
|
||||
self.log.debug("Cancelled avatar fetch")
|
||||
|
||||
async def legacy_id_to_jid_username(self, legacy_id: str) -> str:
|
||||
return "+" + legacy_id[: legacy_id.find("@")]
|
||||
|
|
|
@ -68,17 +68,22 @@ class Gateway(BaseGateway):
|
|||
:meth:`.Session.disconnect` function.
|
||||
"""
|
||||
session: "Session" = self.get_session_from_user(user) # type:ignore
|
||||
session.whatsapp.Logout()
|
||||
await self.run_in_executor(session.whatsapp.Logout)
|
||||
with open(str(session.user_shelf_path)) as shelf:
|
||||
try:
|
||||
device = whatsapp.LinkedDevice(ID=shelf["device_id"])
|
||||
self.whatsapp.CleanupSession(device)
|
||||
device = await self.run_in_executor(
|
||||
whatsapp.LinkedDevice, shelf["device_id"]
|
||||
)
|
||||
await self.run_in_executor(self.whatsapp.CleanupSession, device)
|
||||
except KeyError:
|
||||
pass
|
||||
except RuntimeError as err:
|
||||
log.error("Failed to clean up WhatsApp session: %s", err)
|
||||
session.user_shelf_path.unlink()
|
||||
|
||||
async def run_in_executor(self, func, *args):
|
||||
return await self.loop.run_in_executor(None, func, *args)
|
||||
|
||||
|
||||
def handle_log(level, msg: str):
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import asyncio
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
@ -47,14 +48,29 @@ class MUC(LegacyMUC[str, str, Participant, str]):
|
|||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
self.sent = dict[str, str]()
|
||||
self.__avatar_fetch_task: Optional[asyncio.Task] = None
|
||||
|
||||
async def update_info(self):
|
||||
self.log.debug("Updating info")
|
||||
if self.__avatar_fetch_task is not None and not self.__avatar_fetch_task.done():
|
||||
self.log.debug("Already fetching avatar in background, cancelling")
|
||||
self.__avatar_fetch_task.cancel()
|
||||
self.__avatar_fetch_task = asyncio.create_task(self.__fetch_avatar())
|
||||
|
||||
async def __fetch_avatar(self):
|
||||
try:
|
||||
avatar = self.session.whatsapp.GetAvatar(self.legacy_id, self.avatar or "")
|
||||
self.log.debug("Fetching avatar")
|
||||
avatar = await self.session.run_in_executor(
|
||||
self.session.whatsapp.GetAvatar, self.legacy_id, self.avatar or ""
|
||||
)
|
||||
except RuntimeError:
|
||||
# no avatar
|
||||
self.log.debug("No avatar")
|
||||
await self.set_avatar(None)
|
||||
except asyncio.CancelledError:
|
||||
self.log.debug("Cancelled avatar fetching")
|
||||
else:
|
||||
self.log.debug("Avatar URL: %r", avatar.URL)
|
||||
if avatar.URL:
|
||||
await self.set_avatar(avatar.URL, avatar.ID)
|
||||
|
||||
|
@ -83,7 +99,9 @@ class MUC(LegacyMUC[str, str, Participant, str]):
|
|||
int(oldest_message_date.timestamp()) if oldest_message_date else 0
|
||||
),
|
||||
)
|
||||
self.session.whatsapp.RequestMessageHistory(self.legacy_id, oldest_message)
|
||||
await self.session.run_in_executor(
|
||||
self.session.whatsapp.RequestMessageHistory, self.legacy_id, oldest_message
|
||||
)
|
||||
|
||||
def get_message_sender(self, legacy_msg_id: str):
|
||||
sender_legacy_id = self.sent.get(legacy_msg_id)
|
||||
|
@ -147,8 +165,10 @@ class MUC(LegacyMUC[str, str, Participant, str]):
|
|||
)
|
||||
|
||||
async def on_avatar(self, data: Optional[bytes], mime: Optional[str]) -> None:
|
||||
return self.session.whatsapp.SetAvatar(
|
||||
self.legacy_id, await get_bytes_temp(data) if data else ""
|
||||
return await self.session.run_in_executor(
|
||||
self.session.whatsapp.SetAvatar,
|
||||
self.legacy_id,
|
||||
await get_bytes_temp(data) if data else "",
|
||||
)
|
||||
|
||||
async def on_set_config(
|
||||
|
@ -158,11 +178,15 @@ class MUC(LegacyMUC[str, str, Participant, str]):
|
|||
):
|
||||
# there are no group descriptions in WA, but topics=subjects
|
||||
if self.name != name:
|
||||
self.session.whatsapp.SetGroupName(self.legacy_id, name)
|
||||
await self.session.run_in_executor(
|
||||
self.session.whatsapp.SetGroupName, self.legacy_id, name
|
||||
)
|
||||
|
||||
async def on_set_subject(self, subject: str):
|
||||
if self.subject != subject:
|
||||
self.session.whatsapp.SetGroupTopic(self.legacy_id, subject)
|
||||
await self.session.run_in_executor(
|
||||
self.session.whatsapp.SetGroupTopic, self.legacy_id, subject
|
||||
)
|
||||
|
||||
async def on_set_affiliation(
|
||||
self,
|
||||
|
@ -185,21 +209,21 @@ class MUC(LegacyMUC[str, str, Participant, str]):
|
|||
"bad-request",
|
||||
f"You can't make a participant '{affiliation}' in whatsapp",
|
||||
)
|
||||
self.session.whatsapp.SetAffiliation(self.legacy_id, contact.legacy_id, change)
|
||||
await self.session.run_in_executor(
|
||||
self.session.whatsapp.SetAffiliation,
|
||||
self.legacy_id,
|
||||
contact.legacy_id,
|
||||
change,
|
||||
)
|
||||
|
||||
|
||||
class Bookmarks(LegacyBookmarks[str, MUC]):
|
||||
session: "Session"
|
||||
|
||||
def __init__(self, session: "Session"):
|
||||
super().__init__(session)
|
||||
self.__filled = False
|
||||
|
||||
async def fill(self):
|
||||
groups = self.session.whatsapp.GetGroups()
|
||||
groups = await self.session.run_in_executor(self.session.whatsapp.GetGroups)
|
||||
for group in groups:
|
||||
await self.add_whatsapp_group(group)
|
||||
self.__filled = True
|
||||
|
||||
async def add_whatsapp_group(self, data: whatsapp.Group):
|
||||
muc = await self.by_legacy_id(data.JID)
|
||||
|
@ -213,18 +237,10 @@ class Bookmarks(LegacyBookmarks[str, MUC]):
|
|||
if not local_part.startswith("#"):
|
||||
raise XMPPError("bad-request", "Invalid group ID, expected '#' prefix")
|
||||
|
||||
if not self.__filled:
|
||||
raise XMPPError(
|
||||
"recipient-unavailable", "Still fetching group info, please retry later"
|
||||
)
|
||||
|
||||
whatsapp_group_id = (
|
||||
local_part.removeprefix("#") + "@" + whatsapp.DefaultGroupServer
|
||||
)
|
||||
|
||||
if whatsapp_group_id not in self._mucs_by_legacy_id:
|
||||
raise XMPPError("item-not-found", f"No group found for {whatsapp_group_id}")
|
||||
|
||||
return whatsapp_group_id
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from asyncio import iscoroutine, run_coroutine_threadsafe
|
||||
from asyncio import Event, iscoroutine, run_coroutine_threadsafe
|
||||
from datetime import datetime, timezone
|
||||
from functools import wraps
|
||||
from os import fdopen
|
||||
|
@ -75,26 +75,29 @@ class Session(BaseSession[str, Recipient]):
|
|||
self.whatsapp = self.xmpp.whatsapp.NewSession(device)
|
||||
self._handle_event = make_sync(self.handle_event, self.xmpp.loop)
|
||||
self.whatsapp.SetEventHandler(self._handle_event)
|
||||
self._connected = self.xmpp.loop.create_future()
|
||||
self._connected = Event()
|
||||
self.user_phone: Optional[str] = None
|
||||
self._lock = Lock()
|
||||
|
||||
async def run_in_executor(self, func, *args):
|
||||
return await self.xmpp.run_in_executor(func, *args)
|
||||
|
||||
async def login(self):
|
||||
"""
|
||||
Initiate login process and connect session to WhatsApp. Depending on existing state, login
|
||||
might either return having initiated the Linked Device registration process in the background,
|
||||
or will re-connect to a previously existing Linked Device session.
|
||||
"""
|
||||
self.whatsapp.Login()
|
||||
self._connected = self.xmpp.loop.create_future()
|
||||
return await self._connected
|
||||
await self.run_in_executor(self.whatsapp.Login)
|
||||
await self._connected.wait()
|
||||
return self.__get_connected_status_message()
|
||||
|
||||
async def logout(self):
|
||||
"""
|
||||
Disconnect the active WhatsApp session. This will not remove any local or remote state, and
|
||||
will thus allow previously authenticated sessions to re-authenticate without needing to pair.
|
||||
"""
|
||||
self.whatsapp.Disconnect()
|
||||
await self.run_in_executor(self.whatsapp.Disconnect)
|
||||
self.logged = False
|
||||
|
||||
@ignore_contact_is_user
|
||||
|
@ -104,26 +107,24 @@ class Session(BaseSession[str, Recipient]):
|
|||
state required for processing by the Gateway itself, and will do minimal processing themselves.
|
||||
"""
|
||||
data = whatsapp.EventPayload(handle=ptr)
|
||||
if event == whatsapp.EventQRCode:
|
||||
if event == whatsapp.EventConnected:
|
||||
self.contacts.user_legacy_id = data.ConnectedJID
|
||||
self.user_phone = "+" + data.ConnectedJID.split("@")[0]
|
||||
self._connected.set()
|
||||
# this sends the gateway status twice on first login,
|
||||
# but ensures the gateway status is updated when re-pairing
|
||||
self.send_gateway_status(self.__get_connected_status_message(), show="chat")
|
||||
elif event == whatsapp.EventQRCode:
|
||||
self.send_gateway_status("QR Scan Needed", show="dnd")
|
||||
await self.send_qr(data.QRCode)
|
||||
elif event == whatsapp.EventPair:
|
||||
await self._connected.wait()
|
||||
if event == whatsapp.EventPair:
|
||||
self.send_gateway_message(MESSAGE_PAIR_SUCCESS)
|
||||
with open(str(self.user_shelf_path)) as shelf:
|
||||
shelf["device_id"] = data.PairDeviceID
|
||||
elif event == whatsapp.EventConnected:
|
||||
if self._connected.done():
|
||||
# On re-pair, Session.login() is not called by slidge core, so
|
||||
# the status message is not updated
|
||||
self.send_gateway_status(
|
||||
self.__get_connected_status_message(), show="chat"
|
||||
)
|
||||
else:
|
||||
self.contacts.user_legacy_id = data.ConnectedJID
|
||||
self.user_phone = "+" + data.ConnectedJID.split("@")[0]
|
||||
self._connected.set_result(self.__get_connected_status_message())
|
||||
elif event == whatsapp.EventLoggedOut:
|
||||
self.logged = False
|
||||
self._connected.clear()
|
||||
self.send_gateway_message(MESSAGE_LOGGED_OUT)
|
||||
self.send_gateway_status("Logged out", show="away")
|
||||
elif event == whatsapp.EventContact:
|
||||
|
@ -265,7 +266,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
MentionJIDs=go.Slice_string([m.contact.legacy_id for m in mentions or []]),
|
||||
)
|
||||
set_reply_to(chat, message, reply_to_msg_id, reply_to_fallback_text, reply_to)
|
||||
self.whatsapp.SendMessage(message)
|
||||
await self.run_in_executor(self.whatsapp.SendMessage, message)
|
||||
return message_id
|
||||
|
||||
async def on_file(
|
||||
|
@ -295,7 +296,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
Attachments=whatsapp.Slice_whatsapp_Attachment([message_attachment]),
|
||||
)
|
||||
set_reply_to(chat, message, reply_to_msg_id, reply_to_fallback_text, reply_to)
|
||||
self.whatsapp.SendMessage(message)
|
||||
await self.run_in_executor(self.whatsapp.SendMessage, message)
|
||||
return message_id
|
||||
|
||||
async def on_presence(
|
||||
|
@ -311,15 +312,19 @@ class Session(BaseSession[str, Recipient]):
|
|||
XMPP clients.
|
||||
"""
|
||||
if not merged_resource:
|
||||
self.whatsapp.SendPresence(whatsapp.PresenceUnavailable, "")
|
||||
await self.run_in_executor(
|
||||
self.whatsapp.SendPresence, whatsapp.PresenceUnavailable, ""
|
||||
)
|
||||
else:
|
||||
presence = (
|
||||
whatsapp.PresenceAvailable
|
||||
if merged_resource["show"] in ["chat", ""]
|
||||
else whatsapp.PresenceUnavailable
|
||||
)
|
||||
self.whatsapp.SendPresence(
|
||||
presence, merged_resource["status"] if merged_resource["status"] else ""
|
||||
await self.run_in_executor(
|
||||
self.whatsapp.SendPresence,
|
||||
presence,
|
||||
merged_resource["status"] if merged_resource["status"] else "",
|
||||
)
|
||||
|
||||
async def on_active(self, c: Recipient, thread=None):
|
||||
|
@ -340,7 +345,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
being composed.
|
||||
"""
|
||||
state = whatsapp.ChatState(JID=c.legacy_id, Kind=whatsapp.ChatStateComposing)
|
||||
self.whatsapp.SendChatState(state)
|
||||
await self.run_in_executor(self.whatsapp.SendChatState, state)
|
||||
|
||||
async def on_paused(self, c: Recipient, thread=None):
|
||||
"""
|
||||
|
@ -348,7 +353,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
longer being composed.
|
||||
"""
|
||||
state = whatsapp.ChatState(JID=c.legacy_id, Kind=whatsapp.ChatStatePaused)
|
||||
self.whatsapp.SendChatState(state)
|
||||
await self.run_in_executor(self.whatsapp.SendChatState, state)
|
||||
|
||||
async def on_displayed(self, c: Recipient, legacy_msg_id: str, thread=None):
|
||||
"""
|
||||
|
@ -364,7 +369,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
),
|
||||
GroupJID=c.legacy_id if c.is_group else "",
|
||||
)
|
||||
self.whatsapp.SendReceipt(receipt)
|
||||
await self.run_in_executor(self.whatsapp.SendReceipt, receipt)
|
||||
|
||||
async def on_react(
|
||||
self, c: Recipient, legacy_msg_id: str, emojis: list[str], thread=None
|
||||
|
@ -388,7 +393,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
Body=emojis[0] if emojis else "",
|
||||
IsCarbon=is_carbon,
|
||||
)
|
||||
self.whatsapp.SendMessage(message)
|
||||
await self.run_in_executor(self.whatsapp.SendMessage, message)
|
||||
|
||||
async def on_retract(self, c: Recipient, legacy_msg_id: str, thread=None):
|
||||
"""
|
||||
|
@ -397,7 +402,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
message = whatsapp.Message(
|
||||
Kind=whatsapp.MessageRevoke, ID=legacy_msg_id, JID=c.legacy_id
|
||||
)
|
||||
self.whatsapp.SendMessage(message)
|
||||
await self.run_in_executor(self.whatsapp.SendMessage, message)
|
||||
|
||||
async def on_moderate(
|
||||
self,
|
||||
|
@ -411,7 +416,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
JID=muc.legacy_id,
|
||||
OriginJID=muc.get_message_sender(legacy_msg_id),
|
||||
)
|
||||
self.whatsapp.SendMessage(message)
|
||||
await self.run_in_executor(self.whatsapp.SendMessage, message)
|
||||
# Apparently, no revoke event is received by whatsmeow after sending
|
||||
# the revoke message, so we need to "echo" it here.
|
||||
part = await muc.get_user_participant()
|
||||
|
@ -432,7 +437,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
message = whatsapp.Message(
|
||||
Kind=whatsapp.MessageEdit, ID=legacy_msg_id, JID=c.legacy_id, Body=text
|
||||
)
|
||||
self.whatsapp.SendMessage(message)
|
||||
await self.run_in_executor(self.whatsapp.SendMessage, message)
|
||||
|
||||
async def on_avatar(
|
||||
self,
|
||||
|
@ -445,7 +450,9 @@ class Session(BaseSession[str, Recipient]):
|
|||
"""
|
||||
Update profile picture in WhatsApp for corresponding avatar change in XMPP.
|
||||
"""
|
||||
self.whatsapp.SetAvatar("", await get_bytes_temp(bytes_) if bytes_ else "")
|
||||
await self.run_in_executor(
|
||||
self.whatsapp.SetAvatar, "", await get_bytes_temp(bytes_) if bytes_ else ""
|
||||
)
|
||||
|
||||
async def on_create_group(
|
||||
self, name: str, contacts: list[Contact] # type:ignore
|
||||
|
@ -453,8 +460,10 @@ class Session(BaseSession[str, Recipient]):
|
|||
"""
|
||||
Creates a WhatsApp group for the given human-readable name and participant list.
|
||||
"""
|
||||
group = self.whatsapp.CreateGroup(
|
||||
name, go.Slice_string([c.legacy_id for c in contacts])
|
||||
group = await self.run_in_executor(
|
||||
self.whatsapp.CreateGroup,
|
||||
name,
|
||||
go.Slice_string([c.legacy_id for c in contacts]),
|
||||
)
|
||||
return await self.bookmarks.legacy_id_to_jid_local_part(group.JID)
|
||||
|
||||
|
@ -467,7 +476,7 @@ class Session(BaseSession[str, Recipient]):
|
|||
if not is_valid_phone_number(phone):
|
||||
raise ValueError("Not a valid phone number", phone)
|
||||
|
||||
data = self.whatsapp.FindContact(phone)
|
||||
data = await self.run_in_executor(self.whatsapp.FindContact, phone)
|
||||
if not data.JID:
|
||||
return
|
||||
|
||||
|
|
Loading…
Reference in New Issue