1
0
Fork 0

Compare commits

...

3 Commits

5 changed files with 120 additions and 68 deletions

View File

@ -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?"

View File

@ -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("@")]

View File

@ -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):
"""

View File

@ -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

View File

@ -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