slidge-whatsapp 0.2.0a0__cp311-cp311-manylinux_2_36_x86_64.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/go.mod ADDED
@@ -0,0 +1,28 @@
1
+ module git.sr.ht/~nicoco/slidge-whatsapp/slidge_whatsapp
2
+
3
+ go 1.21
4
+
5
+ toolchain go1.21.5
6
+
7
+ require (
8
+ github.com/go-python/gopy v0.4.10
9
+ github.com/h2non/filetype v1.1.3
10
+ github.com/mattn/go-sqlite3 v1.14.22
11
+ go.mau.fi/libsignal v0.1.1-0.20240705162345-47e713a595ab
12
+ go.mau.fi/whatsmeow v0.0.0-20240710112833-d732338c041f
13
+ golang.org/x/image v0.15.0
14
+ )
15
+
16
+ require (
17
+ filippo.io/edwards25519 v1.1.0 // indirect
18
+ github.com/google/uuid v1.6.0 // indirect
19
+ github.com/gorilla/websocket v1.5.1 // indirect
20
+ github.com/mattn/go-colorable v0.1.13 // indirect
21
+ github.com/mattn/go-isatty v0.0.20 // indirect
22
+ github.com/rs/zerolog v1.33.0 // indirect
23
+ go.mau.fi/util v0.5.0 // indirect
24
+ golang.org/x/crypto v0.25.0 // indirect
25
+ golang.org/x/net v0.27.0 // indirect
26
+ golang.org/x/sys v0.22.0 // indirect
27
+ google.golang.org/protobuf v1.34.2 // indirect
28
+ )
slidge_whatsapp/go.sum ADDED
@@ -0,0 +1,55 @@
1
+ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2
+ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3
+ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6
+ github.com/go-python/gopy v0.4.10 h1:Ec3x+NTSzLsw9f6FTdDLwQCQlmlNmJIu4J6nSnyugqE=
7
+ github.com/go-python/gopy v0.4.10/go.mod h1:zMV/gSSYa9u/8Zp0WYR+L/z+kOIqIUtMg/a1/GRy5uw=
8
+ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
9
+ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
10
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
11
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13
+ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
14
+ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
15
+ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
16
+ github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
17
+ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
18
+ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
19
+ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
20
+ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
21
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
22
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
23
+ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
24
+ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
25
+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
26
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
27
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
28
+ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
29
+ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
30
+ github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
31
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
32
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
33
+ go.mau.fi/libsignal v0.1.1-0.20240705162345-47e713a595ab h1:/tnRxsaaG/xBGjXTb6tzTr+XY4T5fQlrGHE1p9ir/wM=
34
+ go.mau.fi/libsignal v0.1.1-0.20240705162345-47e713a595ab/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
35
+ go.mau.fi/util v0.5.0 h1:8yELAl+1CDRrwGe9NUmREgVclSs26Z68pTWePHVxuDo=
36
+ go.mau.fi/util v0.5.0/go.mod h1:DsJzUrJAG53lCZnnYvq9/mOyLuPScWwYhvETiTrpdP4=
37
+ go.mau.fi/whatsmeow v0.0.0-20240710112833-d732338c041f h1:ni8K5zVngwOWrrZ1HzwEmvAovj0+p0cq464l9g0/dt0=
38
+ go.mau.fi/whatsmeow v0.0.0-20240710112833-d732338c041f/go.mod h1:lMW+LxRTakgyNasZwYNB+2uqjKox75GcEfeUXSJhe8I=
39
+ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
40
+ golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
41
+ golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
42
+ golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
43
+ golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
44
+ golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
45
+ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
47
+ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48
+ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
49
+ golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
50
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
51
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
52
+ google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
53
+ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
54
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
55
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,240 @@
1
+ import re
2
+ from datetime import datetime, timezone
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from slidge.group import LegacyBookmarks, LegacyMUC, LegacyParticipant, MucType
6
+ from slidge.util.archive_msg import HistoryMessage
7
+ from slidge.util.types import Hat, HoleBound, Mention, MucAffiliation
8
+ from slixmpp.exceptions import XMPPError
9
+
10
+ from .generated import whatsapp
11
+ from .util import get_bytes_temp
12
+
13
+ if TYPE_CHECKING:
14
+ from .contact import Contact
15
+ from .session import Session
16
+
17
+
18
+ class Participant(LegacyParticipant):
19
+ contact: "Contact"
20
+ muc: "MUC"
21
+
22
+
23
+ class MUC(LegacyMUC[str, str, Participant, str]):
24
+ session: "Session"
25
+ type = MucType.GROUP
26
+
27
+ REACTIONS_SINGLE_EMOJI = True
28
+ _ALL_INFO_FILLED_ON_STARTUP = True
29
+
30
+ HAS_DESCRIPTION = False
31
+
32
+ async def update_info(self):
33
+ try:
34
+ avatar = self.session.whatsapp.GetAvatar(self.legacy_id, self.avatar or "")
35
+ except RuntimeError:
36
+ # no avatar
37
+ await self.set_avatar(None)
38
+ else:
39
+ if avatar.URL:
40
+ await self.set_avatar(avatar.URL, avatar.ID)
41
+
42
+ async def backfill(
43
+ self,
44
+ after: HoleBound | None = None,
45
+ before: HoleBound | None = None,
46
+ ):
47
+ """
48
+ Request history for messages older than the oldest message given by ID and date.
49
+ """
50
+
51
+ if before is None:
52
+ return
53
+ # WhatsApp requires a full reference to the last seen message in performing on-demand sync.
54
+
55
+ assert isinstance(before.id, str)
56
+ oldest_message = whatsapp.Message(
57
+ ID=before.id,
58
+ IsCarbon=self.session.message_is_carbon(self, before.id),
59
+ Timestamp=int(before.timestamp.timestamp()),
60
+ )
61
+ self.session.whatsapp.RequestMessageHistory(self.legacy_id, oldest_message)
62
+
63
+ def get_message_sender(self, legacy_msg_id: str):
64
+ assert self.pk is not None
65
+ stored = self.xmpp.store.mam.get_by_legacy_id(self.pk, legacy_msg_id)
66
+ if stored is None:
67
+ raise XMPPError("internal-server-error", "Unable to find message sender")
68
+ msg = HistoryMessage(stored.stanza)
69
+ occupant_id = msg.stanza["occupant-id"]["id"]
70
+ if occupant_id == "slidge-user":
71
+ return self.session.contacts.user_legacy_id
72
+ if "@" in occupant_id:
73
+ jid_username = occupant_id.split("@")[0]
74
+ return jid_username.removeprefix("+") + "@" + whatsapp.DefaultUserServer
75
+ raise XMPPError("internal-server-error", "Unable to find message sender")
76
+
77
+ async def update_whatsapp_info(self, info: whatsapp.Group):
78
+ """
79
+ Set MUC information based on WhatsApp group information, which may or may not be partial in
80
+ case of updates to existing MUCs.
81
+ """
82
+ if info.Nickname:
83
+ self.user_nick = info.Nickname
84
+ if info.Name:
85
+ self.name = info.Name
86
+ if info.Subject.Subject:
87
+ self.subject = info.Subject.Subject
88
+ if info.Subject.SetAt:
89
+ set_at = datetime.fromtimestamp(info.Subject.SetAt, tz=timezone.utc)
90
+ self.subject_date = set_at
91
+ if info.Subject.SetByJID:
92
+ participant = await self.get_participant_by_legacy_id(
93
+ info.Subject.SetByJID
94
+ )
95
+ if name := participant.nickname:
96
+ self.subject_setter = name
97
+ for data in info.Participants:
98
+ participant = await self.get_participant_by_legacy_id(data.JID)
99
+ if data.Action == whatsapp.GroupParticipantActionRemove:
100
+ self.remove_participant(participant)
101
+ else:
102
+ if data.Affiliation == whatsapp.GroupAffiliationAdmin:
103
+ # Only owners can change the group name according to
104
+ # XEP-0045, so we make all "WA admins" "XMPP owners"
105
+ participant.affiliation = "owner"
106
+ participant.role = "moderator"
107
+ elif data.Affiliation == whatsapp.GroupAffiliationOwner:
108
+ # The WA owner is in fact the person who created the room
109
+ participant.set_hats(
110
+ [Hat("https://slidge.im/hats/slidge-whatsapp/owner", "Owner")]
111
+ )
112
+ participant.affiliation = "owner"
113
+ participant.role = "moderator"
114
+ else:
115
+ participant.affiliation = "member"
116
+ participant.role = "participant"
117
+
118
+ async def replace_mentions(self, t: str):
119
+ return replace_whatsapp_mentions(
120
+ t,
121
+ participants=(
122
+ {
123
+ p.contact.jid_username: p.nickname
124
+ async for p in self.get_participants()
125
+ if p.contact is not None # should not happen
126
+ }
127
+ | {self.session.user_phone: self.user_nick}
128
+ if self.session.user_phone # user_phone *should* be set at this point,
129
+ else {} # but better safe than sorry
130
+ ),
131
+ )
132
+
133
+ async def on_avatar(self, data: Optional[bytes], mime: Optional[str]) -> None:
134
+ return self.session.whatsapp.SetAvatar(
135
+ self.legacy_id, await get_bytes_temp(data) if data else ""
136
+ )
137
+
138
+ async def on_set_config(
139
+ self,
140
+ name: Optional[str],
141
+ description: Optional[str],
142
+ ):
143
+ # there are no group descriptions in WA, but topics=subjects
144
+ if self.name != name:
145
+ self.session.whatsapp.SetGroupName(self.legacy_id, name)
146
+
147
+ async def on_set_subject(self, subject: str):
148
+ if self.subject != subject:
149
+ self.session.whatsapp.SetGroupTopic(self.legacy_id, subject)
150
+
151
+ async def on_set_affiliation(
152
+ self,
153
+ contact: "Contact", # type:ignore
154
+ affiliation: MucAffiliation,
155
+ reason: Optional[str],
156
+ nickname: Optional[str],
157
+ ):
158
+ assert contact.contact_pk is not None
159
+ assert self.pk is not None
160
+ if affiliation == "member":
161
+ if (
162
+ self.xmpp.store.participants.get_by_contact(self.pk, contact.contact_pk)
163
+ is not None
164
+ ):
165
+ change = "demote"
166
+ else:
167
+ change = "add"
168
+ elif affiliation == "admin":
169
+ change = "promote"
170
+ elif affiliation == "outcast" or affiliation == "none":
171
+ change = "remove"
172
+ else:
173
+ raise XMPPError(
174
+ "bad-request",
175
+ f"You can't make a participant '{affiliation}' in whatsapp",
176
+ )
177
+ self.session.whatsapp.SetAffiliation(self.legacy_id, contact.legacy_id, change)
178
+
179
+
180
+ class Bookmarks(LegacyBookmarks[str, MUC]):
181
+ session: "Session"
182
+
183
+ def __init__(self, session: "Session"):
184
+ super().__init__(session)
185
+ self.__filled = False
186
+
187
+ async def fill(self):
188
+ groups = self.session.whatsapp.GetGroups()
189
+ for group in groups:
190
+ await self.add_whatsapp_group(group)
191
+ self.__filled = True
192
+
193
+ async def add_whatsapp_group(self, data: whatsapp.Group):
194
+ muc = await self.by_legacy_id(data.JID)
195
+ await muc.update_whatsapp_info(data)
196
+ await muc.add_to_bookmarks()
197
+
198
+ async def legacy_id_to_jid_local_part(self, legacy_id: str):
199
+ return "#" + legacy_id[: legacy_id.find("@")]
200
+
201
+ async def jid_local_part_to_legacy_id(self, local_part: str):
202
+ if not local_part.startswith("#"):
203
+ raise XMPPError("bad-request", "Invalid group ID, expected '#' prefix")
204
+
205
+ if not self.__filled:
206
+ raise XMPPError(
207
+ "recipient-unavailable", "Still fetching group info, please retry later"
208
+ )
209
+
210
+ whatsapp_group_id = (
211
+ local_part.removeprefix("#") + "@" + whatsapp.DefaultGroupServer
212
+ )
213
+
214
+ if (
215
+ self.xmpp.store.rooms.get_by_legacy_id(
216
+ self.session.user_pk, whatsapp_group_id
217
+ )
218
+ is None
219
+ ):
220
+ raise XMPPError("item-not-found", f"No group found for {whatsapp_group_id}")
221
+
222
+ return whatsapp_group_id
223
+
224
+
225
+ def replace_xmpp_mentions(text: str, mentions: list[Mention]):
226
+ offset: int = 0
227
+ result: str = ""
228
+ for m in mentions:
229
+ legacy_id = "@" + m.contact.legacy_id[: m.contact.legacy_id.find("@")]
230
+ result = result + text[offset : m.start] + legacy_id
231
+ offset = m.end
232
+ return result + text[offset:] if offset > 0 else text
233
+
234
+
235
+ def replace_whatsapp_mentions(text: str, participants: dict[str, str]):
236
+ def match(m: re.Match):
237
+ group = m.group(0)
238
+ return participants.get(group.replace("@", "+"), group)
239
+
240
+ return re.sub(r"@\d+", match, text)