slidge-whatsapp 0.2.2__cp313-cp313-manylinux_2_36_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of slidge-whatsapp might be problematic. Click here for more details.
- slidge_whatsapp/__init__.py +17 -0
- slidge_whatsapp/__main__.py +3 -0
- slidge_whatsapp/command.py +143 -0
- slidge_whatsapp/config.py +32 -0
- slidge_whatsapp/contact.py +77 -0
- slidge_whatsapp/event.go +1175 -0
- slidge_whatsapp/gateway.go +181 -0
- slidge_whatsapp/gateway.py +82 -0
- slidge_whatsapp/generated/__init__.py +0 -0
- slidge_whatsapp/generated/_whatsapp.cpython-313-aarch64-linux-gnu.h +606 -0
- slidge_whatsapp/generated/_whatsapp.cpython-313-aarch64-linux-gnu.so +0 -0
- slidge_whatsapp/generated/build.py +395 -0
- slidge_whatsapp/generated/go.py +1632 -0
- slidge_whatsapp/generated/whatsapp.c +6887 -0
- slidge_whatsapp/generated/whatsapp.go +3572 -0
- slidge_whatsapp/generated/whatsapp.py +2911 -0
- slidge_whatsapp/generated/whatsapp_go.h +606 -0
- slidge_whatsapp/go.mod +29 -0
- slidge_whatsapp/go.sum +62 -0
- slidge_whatsapp/group.py +256 -0
- slidge_whatsapp/media/ffmpeg.go +72 -0
- slidge_whatsapp/media/media.go +542 -0
- slidge_whatsapp/media/mupdf.go +47 -0
- slidge_whatsapp/media/stub.go +19 -0
- slidge_whatsapp/session.go +855 -0
- slidge_whatsapp/session.py +745 -0
- slidge_whatsapp-0.2.2.dist-info/LICENSE +661 -0
- slidge_whatsapp-0.2.2.dist-info/METADATA +744 -0
- slidge_whatsapp-0.2.2.dist-info/RECORD +31 -0
- slidge_whatsapp-0.2.2.dist-info/WHEEL +4 -0
- slidge_whatsapp-0.2.2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp gateway using the multi-device API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from slidge import entrypoint
|
|
6
|
+
from slidge.util.util import get_version # noqa: F401
|
|
7
|
+
|
|
8
|
+
from . import command, config, contact, group, session
|
|
9
|
+
from .gateway import Gateway
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
entrypoint("slidge_whatsapp")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__version__ = "v0.2.2"
|
|
17
|
+
__all__ = "Gateway", "session", "command", "contact", "config", "group", "main"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
|
|
3
|
+
from slidge.command import Command, CommandAccess, Form, FormField
|
|
4
|
+
from slidge.util import is_valid_phone_number
|
|
5
|
+
from slixmpp import JID
|
|
6
|
+
from slixmpp.exceptions import XMPPError
|
|
7
|
+
|
|
8
|
+
from .generated import whatsapp
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .session import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Logout(Command):
|
|
15
|
+
NAME = "🔓 Disconnect from WhatsApp"
|
|
16
|
+
HELP = (
|
|
17
|
+
"Disconnects active WhatsApp session without removing any linked device credentials. "
|
|
18
|
+
"To re-connect, use the 're-login' command."
|
|
19
|
+
)
|
|
20
|
+
NODE = "wa_logout"
|
|
21
|
+
CHAT_COMMAND = "logout"
|
|
22
|
+
ACCESS = CommandAccess.USER_LOGGED
|
|
23
|
+
|
|
24
|
+
async def run(
|
|
25
|
+
self,
|
|
26
|
+
session: Optional["Session"], # type:ignore
|
|
27
|
+
ifrom: JID,
|
|
28
|
+
*args,
|
|
29
|
+
) -> str:
|
|
30
|
+
assert session is not None
|
|
31
|
+
try:
|
|
32
|
+
session.shutdown()
|
|
33
|
+
except Exception as e:
|
|
34
|
+
session.send_gateway_status(f"Logout failed: {e}", show="dnd")
|
|
35
|
+
raise XMPPError(
|
|
36
|
+
"internal-server-error",
|
|
37
|
+
etype="wait",
|
|
38
|
+
text=f"Could not logout WhatsApp session: {e}",
|
|
39
|
+
)
|
|
40
|
+
session.send_gateway_status("Logged out", show="away")
|
|
41
|
+
return "Logged out successfully"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PairPhone(Command):
|
|
45
|
+
NAME = "📱 Complete registration via phone number"
|
|
46
|
+
HELP = (
|
|
47
|
+
"As an alternative to QR code verification, this allows you to complete registration "
|
|
48
|
+
"by inputing a one-time code into the official WhatsApp client; this requires that you "
|
|
49
|
+
"provide the phone number used for the main device, in international format "
|
|
50
|
+
"(e.g. +447700900000). See more information here: https://faq.whatsapp.com/1324084875126592"
|
|
51
|
+
)
|
|
52
|
+
NODE = "wa_pair_phone"
|
|
53
|
+
CHAT_COMMAND = "pair-phone"
|
|
54
|
+
ACCESS = CommandAccess.USER_NON_LOGGED
|
|
55
|
+
|
|
56
|
+
async def run(
|
|
57
|
+
self,
|
|
58
|
+
session: Optional["Session"], # type:ignore
|
|
59
|
+
ifrom: JID,
|
|
60
|
+
*args,
|
|
61
|
+
) -> Form:
|
|
62
|
+
return Form(
|
|
63
|
+
title="Pair to WhatsApp via phone number",
|
|
64
|
+
instructions="Enter your phone number in international format (e.g. +447700900000)",
|
|
65
|
+
fields=[FormField(var="phone", label="Phone number", required=True)],
|
|
66
|
+
handler=self.finish, # type:ignore
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
async def finish(form_values: dict, session: "Session", _ifrom: JID):
|
|
71
|
+
p = form_values.get("phone")
|
|
72
|
+
if not is_valid_phone_number(p):
|
|
73
|
+
raise ValueError("Not a valid phone number", p)
|
|
74
|
+
code = session.whatsapp.PairPhone(p)
|
|
75
|
+
return f"Please open the official WhatsApp client and input the following code: {code}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ChangePresence(Command):
|
|
79
|
+
NAME = "📴 Set WhatsApp web presence"
|
|
80
|
+
HELP = (
|
|
81
|
+
"If you want to receive notifications in the WhatsApp official client,"
|
|
82
|
+
"you need to set your presence to unavailable. As a side effect, you "
|
|
83
|
+
"won't receive receipts and presences from your contacts."
|
|
84
|
+
)
|
|
85
|
+
NODE = "wa_presence"
|
|
86
|
+
CHAT_COMMAND = "presence"
|
|
87
|
+
ACCESS = CommandAccess.USER_LOGGED
|
|
88
|
+
|
|
89
|
+
async def run(
|
|
90
|
+
self,
|
|
91
|
+
session: Optional["Session"], # type:ignore
|
|
92
|
+
ifrom: JID,
|
|
93
|
+
*args,
|
|
94
|
+
) -> Form:
|
|
95
|
+
return Form(
|
|
96
|
+
title="Set WhatsApp web presence",
|
|
97
|
+
instructions="Choose what type of presence you want to set",
|
|
98
|
+
fields=[
|
|
99
|
+
FormField(
|
|
100
|
+
var="presence",
|
|
101
|
+
value="available",
|
|
102
|
+
type="list-single",
|
|
103
|
+
options=[
|
|
104
|
+
{"label": "Available", "value": "available"},
|
|
105
|
+
{"label": "Unavailable", "value": "unavailable"},
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
],
|
|
109
|
+
handler=self.finish, # type:ignore
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
async def finish(form_values: dict, session: "Session", _ifrom: JID):
|
|
114
|
+
p = form_values.get("presence")
|
|
115
|
+
if p == "available":
|
|
116
|
+
session.whatsapp.SendPresence(whatsapp.PresenceAvailable, "")
|
|
117
|
+
elif p == "unavailable":
|
|
118
|
+
session.whatsapp.SendPresence(whatsapp.PresenceUnavailable, "")
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError("Not a valid presence kind.", p)
|
|
121
|
+
return f"Presence succesfully set to {p}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SubscribeToPresences(Command):
|
|
125
|
+
NAME = "🔔 Subscribe to contacts' presences"
|
|
126
|
+
HELP = (
|
|
127
|
+
"Subscribes to and refreshes contacts' presences; typically this is "
|
|
128
|
+
"done automatically, but re-subscribing might be useful in case contact "
|
|
129
|
+
"presences are stuck or otherwise not updating."
|
|
130
|
+
)
|
|
131
|
+
NODE = "wa_subscribe"
|
|
132
|
+
CHAT_COMMAND = "subscribe"
|
|
133
|
+
ACCESS = CommandAccess.USER_LOGGED
|
|
134
|
+
|
|
135
|
+
async def run(
|
|
136
|
+
self,
|
|
137
|
+
session: Optional["Session"], # type:ignore
|
|
138
|
+
ifrom: JID,
|
|
139
|
+
*args,
|
|
140
|
+
) -> str:
|
|
141
|
+
assert session is not None
|
|
142
|
+
session.whatsapp.GetContacts(False)
|
|
143
|
+
return "Looks like no exception was raised. Success, I guess?"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Config contains plugin-specific configuration for WhatsApp, and is loaded automatically by the
|
|
3
|
+
core configuration framework.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from slidge import global_config
|
|
10
|
+
|
|
11
|
+
# workaround because global_config.HOME_DIR is not defined unless
|
|
12
|
+
# called by slidge's main(), which is a problem for tests, docs and the
|
|
13
|
+
# dedicated slidge-whatsapp setuptools entrypoint
|
|
14
|
+
try:
|
|
15
|
+
DB_PATH: Optional[Path] = global_config.HOME_DIR / "whatsapp" / "whatsapp.db"
|
|
16
|
+
except AttributeError:
|
|
17
|
+
DB_PATH: Optional[Path] = None # type:ignore
|
|
18
|
+
|
|
19
|
+
DB_PATH__DOC = (
|
|
20
|
+
"The path to the database used for the WhatsApp plugin. Default to "
|
|
21
|
+
"${SLIDGE_HOME_DIR}/whatsapp/whatsapp.db"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ALWAYS_SYNC_ROSTER = False
|
|
25
|
+
ALWAYS_SYNC_ROSTER__DOC = (
|
|
26
|
+
"Whether or not to perform a full sync of the WhatsApp roster on startup."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
ENABLE_LINK_PREVIEWS = True
|
|
30
|
+
ENABLE_LINK_PREVIEWS__DOC = (
|
|
31
|
+
"Whether or not previews for links (URLs) should be generated on outgoing messages"
|
|
32
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from slidge import LegacyContact, LegacyRoster
|
|
5
|
+
from slixmpp.exceptions import XMPPError
|
|
6
|
+
|
|
7
|
+
from . import config
|
|
8
|
+
from .generated import whatsapp
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .session import Session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Contact(LegacyContact[str]):
|
|
15
|
+
CORRECTION = True
|
|
16
|
+
REACTIONS_SINGLE_EMOJI = True
|
|
17
|
+
|
|
18
|
+
async def update_presence(
|
|
19
|
+
self, presence: whatsapp.PresenceKind, last_seen_timestamp: int
|
|
20
|
+
):
|
|
21
|
+
last_seen = (
|
|
22
|
+
datetime.fromtimestamp(last_seen_timestamp, tz=timezone.utc)
|
|
23
|
+
if last_seen_timestamp > 0
|
|
24
|
+
else None
|
|
25
|
+
)
|
|
26
|
+
if presence == whatsapp.PresenceUnavailable:
|
|
27
|
+
self.away(last_seen=last_seen)
|
|
28
|
+
else:
|
|
29
|
+
self.online(last_seen=last_seen)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Roster(LegacyRoster[str, Contact]):
|
|
33
|
+
session: "Session"
|
|
34
|
+
|
|
35
|
+
async def fill(self):
|
|
36
|
+
"""
|
|
37
|
+
Retrieve contacts from remote WhatsApp service, subscribing to their presence and adding to
|
|
38
|
+
local roster.
|
|
39
|
+
"""
|
|
40
|
+
contacts = self.session.whatsapp.GetContacts(refresh=config.ALWAYS_SYNC_ROSTER)
|
|
41
|
+
for contact in contacts:
|
|
42
|
+
c = await self.add_whatsapp_contact(contact)
|
|
43
|
+
if c is not None:
|
|
44
|
+
yield c
|
|
45
|
+
|
|
46
|
+
async def add_whatsapp_contact(self, data: whatsapp.Contact) -> Contact | None:
|
|
47
|
+
"""
|
|
48
|
+
Adds a WhatsApp contact to local roster, filling all required and optional information.
|
|
49
|
+
"""
|
|
50
|
+
# Don't attempt to add ourselves to the roster.
|
|
51
|
+
if data.JID == self.user_legacy_id:
|
|
52
|
+
return None
|
|
53
|
+
contact = await self.by_legacy_id(data.JID)
|
|
54
|
+
contact.name = data.Name
|
|
55
|
+
contact.is_friend = True
|
|
56
|
+
try:
|
|
57
|
+
avatar = self.session.whatsapp.GetAvatar(data.JID, contact.avatar or "")
|
|
58
|
+
if avatar.URL and contact.avatar != avatar.ID:
|
|
59
|
+
await contact.set_avatar(avatar.URL, avatar.ID)
|
|
60
|
+
elif avatar.URL == "":
|
|
61
|
+
await contact.set_avatar(None)
|
|
62
|
+
except RuntimeError as err:
|
|
63
|
+
self.session.log.error(
|
|
64
|
+
"Failed getting avatar for contact %s: %s", data.JID, err
|
|
65
|
+
)
|
|
66
|
+
contact.set_vcard(full_name=contact.name, phone=str(contact.jid.local))
|
|
67
|
+
return contact
|
|
68
|
+
|
|
69
|
+
async def legacy_id_to_jid_username(self, legacy_id: str) -> str:
|
|
70
|
+
return "+" + legacy_id[: legacy_id.find("@")]
|
|
71
|
+
|
|
72
|
+
async def jid_username_to_legacy_id(self, jid_username: str) -> str:
|
|
73
|
+
if jid_username.startswith("#"):
|
|
74
|
+
raise XMPPError("item-not-found", "Invalid contact ID: group ID given")
|
|
75
|
+
if not jid_username.startswith("+"):
|
|
76
|
+
raise XMPPError("item-not-found", "Invalid contact ID, expected '+' prefix")
|
|
77
|
+
return jid_username.removeprefix("+") + "@" + whatsapp.DefaultUserServer
|