slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- slidge/__init__.py +54 -31
- slidge/__main__.py +51 -5
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +2 -0
- slidge/core/cache.py +121 -39
- slidge/core/config.py +116 -11
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +895 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +795 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +9 -1
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +6 -19
- slidge/core/mixins/disco.py +66 -15
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +254 -252
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +128 -31
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +275 -116
- slidge/core/session.py +586 -518
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +458 -0
- slidge/group/room.py +1103 -0
- slidge/migration.py +18 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/{util → slixfix}/xep_0077/register.py +1 -2
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +4 -6
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +25 -4
- slidge/util/db.py +23 -69
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +136 -86
- slidge/util/types.py +155 -14
- slidge/util/util.py +225 -51
- slidge-0.1.2.dist-info/METADATA +111 -0
- slidge-0.1.2.dist-info/RECORD +96 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
- slidge/core/adhoc.py +0 -492
- slidge/core/chat_command.py +0 -197
- slidge/core/contact.py +0 -441
- slidge/core/disco.py +0 -59
- slidge/core/gateway.py +0 -899
- slidge/core/muc/__init__.py +0 -3
- slidge/core/muc/bookmarks.py +0 -74
- slidge/core/muc/participant.py +0 -152
- slidge/core/muc/room.py +0 -348
- slidge/plugins/discord/__init__.py +0 -121
- slidge/plugins/discord/client.py +0 -121
- slidge/plugins/discord/session.py +0 -172
- slidge/plugins/dummy.py +0 -334
- slidge/plugins/facebook.py +0 -591
- slidge/plugins/hackernews.py +0 -209
- slidge/plugins/mattermost/__init__.py +0 -1
- slidge/plugins/mattermost/api.py +0 -288
- slidge/plugins/mattermost/gateway.py +0 -417
- slidge/plugins/mattermost/websocket.py +0 -248
- slidge/plugins/signal/__init__.py +0 -4
- slidge/plugins/signal/config.py +0 -4
- slidge/plugins/signal/contact.py +0 -104
- slidge/plugins/signal/gateway.py +0 -379
- slidge/plugins/signal/group.py +0 -76
- slidge/plugins/signal/session.py +0 -515
- slidge/plugins/signal/txt.py +0 -13
- slidge/plugins/signal/util.py +0 -32
- slidge/plugins/skype.py +0 -310
- slidge/plugins/steam.py +0 -400
- slidge/plugins/telegram/__init__.py +0 -6
- slidge/plugins/telegram/client.py +0 -325
- slidge/plugins/telegram/config.py +0 -21
- slidge/plugins/telegram/contact.py +0 -154
- slidge/plugins/telegram/gateway.py +0 -182
- slidge/plugins/telegram/group.py +0 -184
- slidge/plugins/telegram/session.py +0 -275
- slidge/plugins/telegram/util.py +0 -153
- slidge/plugins/whatsapp/__init__.py +0 -6
- slidge/plugins/whatsapp/config.py +0 -17
- slidge/plugins/whatsapp/contact.py +0 -33
- slidge/plugins/whatsapp/event.go +0 -455
- slidge/plugins/whatsapp/gateway.go +0 -156
- slidge/plugins/whatsapp/gateway.py +0 -69
- slidge/plugins/whatsapp/go.mod +0 -17
- slidge/plugins/whatsapp/go.sum +0 -22
- slidge/plugins/whatsapp/session.go +0 -371
- slidge/plugins/whatsapp/session.py +0 -370
- slidge/util/xep_0030/__init__.py +0 -13
- slidge/util/xep_0030/disco.py +0 -811
- slidge/util/xep_0030/stanza/__init__.py +0 -7
- slidge/util/xep_0030/stanza/info.py +0 -270
- slidge/util/xep_0030/stanza/items.py +0 -147
- slidge/util/xep_0030/static.py +0 -467
- slidge/util/xep_0050/adhoc.py +0 -631
- slidge/util/xep_0050/stanza.py +0 -180
- slidge/util/xep_0077/stanza.py +0 -71
- slidge/util/xep_0292/__init__.py +0 -1
- slidge/util/xep_0292/stanza.py +0 -167
- slidge/util/xep_0292/vcard4.py +0 -74
- slidge/util/xep_0356/__init__.py +0 -7
- slidge/util/xep_0356/permissions.py +0 -35
- slidge/util/xep_0356/privilege.py +0 -160
- slidge/util/xep_0356/stanza.py +0 -44
- slidge/util/xep_0461/__init__.py +0 -6
- slidge/util/xep_0461/reply.py +0 -48
- slidge/util/xep_0461/stanza.py +0 -80
- slidge-0.1.0rc1.dist-info/METADATA +0 -171
- slidge-0.1.0rc1.dist-info/RECORD +0 -99
- /slidge/{plugins/__init__.py → py.typed} +0 -0
- /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,452 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import warnings
|
4
|
+
from datetime import date
|
5
|
+
from typing import TYPE_CHECKING, Generic, Iterable, Optional, Union
|
6
|
+
|
7
|
+
from slixmpp import JID, Message, Presence
|
8
|
+
from slixmpp.exceptions import IqError
|
9
|
+
from slixmpp.plugins.xep_0292.stanza import VCard4
|
10
|
+
from slixmpp.types import MessageTypes
|
11
|
+
|
12
|
+
from ..core import config
|
13
|
+
from ..core.mixins import FullCarbonMixin
|
14
|
+
from ..core.mixins.avatar import AvatarMixin
|
15
|
+
from ..core.mixins.disco import ContactAccountDiscoMixin
|
16
|
+
from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
|
17
|
+
from ..util import SubclassableOnce
|
18
|
+
from ..util.types import LegacyUserIdType, MessageOrPresenceTypeVar
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from ..core.session import BaseSession
|
22
|
+
from ..group.participant import LegacyParticipant
|
23
|
+
|
24
|
+
|
25
|
+
class LegacyContact(
|
26
|
+
Generic[LegacyUserIdType],
|
27
|
+
AvatarMixin,
|
28
|
+
ContactAccountDiscoMixin,
|
29
|
+
FullCarbonMixin,
|
30
|
+
ReactionRecipientMixin,
|
31
|
+
ThreadRecipientMixin,
|
32
|
+
metaclass=SubclassableOnce,
|
33
|
+
):
|
34
|
+
"""
|
35
|
+
This class centralizes actions in relation to a specific legacy contact.
|
36
|
+
|
37
|
+
You shouldn't create instances of contacts manually, but rather rely on
|
38
|
+
:meth:`.LegacyRoster.by_legacy_id` to ensure that contact instances are
|
39
|
+
singletons. The :class:`.LegacyRoster` instance of a session is accessible
|
40
|
+
through the :attr:`.BaseSession.contacts` attribute.
|
41
|
+
|
42
|
+
Typically, your plugin should have methods hook to the legacy events and
|
43
|
+
call appropriate methods here to transmit the "legacy action" to the xmpp
|
44
|
+
user. This should look like this:
|
45
|
+
|
46
|
+
.. code-block:python
|
47
|
+
|
48
|
+
class Session(BaseSession):
|
49
|
+
...
|
50
|
+
|
51
|
+
async def on_cool_chat_network_new_text_message(self, legacy_msg_event):
|
52
|
+
contact = self.contacts.by_legacy_id(legacy_msg_event.from)
|
53
|
+
contact.send_text(legacy_msg_event.text)
|
54
|
+
|
55
|
+
async def on_cool_chat_network_new_typing_event(self, legacy_typing_event):
|
56
|
+
contact = self.contacts.by_legacy_id(legacy_msg_event.from)
|
57
|
+
contact.composing()
|
58
|
+
...
|
59
|
+
|
60
|
+
Use ``carbon=True`` as a keyword arg for methods to represent an action FROM
|
61
|
+
the user TO the contact, typically when the user uses an official client to
|
62
|
+
do an action such as sending a message or marking as message as read.
|
63
|
+
This will use :xep:`0363` to impersonate the XMPP user in order.
|
64
|
+
"""
|
65
|
+
|
66
|
+
session: "BaseSession"
|
67
|
+
|
68
|
+
RESOURCE: str = "slidge"
|
69
|
+
"""
|
70
|
+
A full JID, including a resource part is required for chat states (and maybe other stuff)
|
71
|
+
to work properly. This is the name of the resource the contacts will use.
|
72
|
+
"""
|
73
|
+
PROPAGATE_PRESENCE_TO_GROUPS = True
|
74
|
+
|
75
|
+
mtype: MessageTypes = "chat"
|
76
|
+
_can_send_carbon = True
|
77
|
+
is_group = False
|
78
|
+
|
79
|
+
_ONLY_SEND_PRESENCE_CHANGES = True
|
80
|
+
|
81
|
+
STRIP_SHORT_DELAY = True
|
82
|
+
_NON_FRIEND_PRESENCES_FILTER = {"subscribe", "unsubscribed"}
|
83
|
+
|
84
|
+
_avatar_pubsub_broadcast = True
|
85
|
+
_avatar_bare_jid = True
|
86
|
+
|
87
|
+
INVITATION_RECIPIENT = True
|
88
|
+
|
89
|
+
def __init__(
|
90
|
+
self,
|
91
|
+
session: "BaseSession",
|
92
|
+
legacy_id: LegacyUserIdType,
|
93
|
+
jid_username: str,
|
94
|
+
):
|
95
|
+
"""
|
96
|
+
:param session: The session this contact is part of
|
97
|
+
:param legacy_id: The contact's legacy ID
|
98
|
+
:param jid_username: User part of this contact's 'puppet' JID.
|
99
|
+
NB: case-insensitive, and some special characters are not allowed
|
100
|
+
"""
|
101
|
+
super().__init__()
|
102
|
+
self.session = session
|
103
|
+
self.user = session.user
|
104
|
+
self.legacy_id: LegacyUserIdType = legacy_id
|
105
|
+
"""
|
106
|
+
The legacy identifier of the :term:`Legacy Contact`.
|
107
|
+
By default, this is the :term:`JID Local Part` of this
|
108
|
+
:term:`XMPP Entity`.
|
109
|
+
|
110
|
+
Controlling what values are valid and how they are translated from a
|
111
|
+
:term:`JID Local Part` is done in :meth:`.jid_username_to_legacy_id`.
|
112
|
+
Reciprocally, in :meth:`legacy_id_to_jid_username` the inverse
|
113
|
+
transformation is defined.
|
114
|
+
"""
|
115
|
+
self.jid_username = jid_username
|
116
|
+
|
117
|
+
self._name: Optional[str] = None
|
118
|
+
|
119
|
+
if self.xmpp.MARK_ALL_MESSAGES:
|
120
|
+
self._sent_order = list[str]()
|
121
|
+
|
122
|
+
self.xmpp = session.xmpp
|
123
|
+
self.jid = JID(self.jid_username + "@" + self.xmpp.boundjid.bare)
|
124
|
+
self.jid.resource = self.RESOURCE
|
125
|
+
self.log = logging.getLogger(f"{self.user.bare_jid}:{self.jid.bare}")
|
126
|
+
self.participants = set["LegacyParticipant"]()
|
127
|
+
self.is_friend: bool = False
|
128
|
+
self.__added_to_roster = False
|
129
|
+
|
130
|
+
def __repr__(self):
|
131
|
+
return f"<Contact '{self.legacy_id}'/'{self.jid.bare}'>"
|
132
|
+
|
133
|
+
def __get_subscription_string(self):
|
134
|
+
if self.is_friend:
|
135
|
+
return "both"
|
136
|
+
return "none"
|
137
|
+
|
138
|
+
def __propagate_to_participants(self, stanza: Presence):
|
139
|
+
if not self.PROPAGATE_PRESENCE_TO_GROUPS:
|
140
|
+
return
|
141
|
+
|
142
|
+
ptype = stanza["type"]
|
143
|
+
if ptype in ("available", "chat"):
|
144
|
+
func_name = "online"
|
145
|
+
elif ptype in ("xa", "unavailable"):
|
146
|
+
# we map unavailable to extended_away, because offline is
|
147
|
+
# "participant leaves the MUC"
|
148
|
+
# TODO: improve this with a clear distinction between participant
|
149
|
+
# and member list
|
150
|
+
func_name = "extended_away"
|
151
|
+
elif ptype == "busy":
|
152
|
+
func_name = "busy"
|
153
|
+
elif ptype == "away":
|
154
|
+
func_name = "away"
|
155
|
+
else:
|
156
|
+
return
|
157
|
+
|
158
|
+
last_seen: Optional[datetime.datetime] = (
|
159
|
+
stanza["idle"]["since"] if stanza.get_plugin("idle", check=True) else None
|
160
|
+
)
|
161
|
+
|
162
|
+
kw = dict(status=stanza["status"], last_seen=last_seen)
|
163
|
+
|
164
|
+
for part in self.participants:
|
165
|
+
func = getattr(part, func_name)
|
166
|
+
func(**kw)
|
167
|
+
|
168
|
+
def _send(
|
169
|
+
self, stanza: MessageOrPresenceTypeVar, carbon=False, nick=False, **send_kwargs
|
170
|
+
) -> MessageOrPresenceTypeVar:
|
171
|
+
if carbon and isinstance(stanza, Message):
|
172
|
+
stanza["to"] = self.jid.bare
|
173
|
+
stanza["from"] = self.user.jid
|
174
|
+
self._privileged_send(stanza)
|
175
|
+
return stanza # type:ignore
|
176
|
+
|
177
|
+
if isinstance(stanza, Presence):
|
178
|
+
self.__propagate_to_participants(stanza)
|
179
|
+
if (
|
180
|
+
not self.is_friend
|
181
|
+
and stanza["type"] not in self._NON_FRIEND_PRESENCES_FILTER
|
182
|
+
):
|
183
|
+
return stanza # type:ignore
|
184
|
+
if self.name and (nick or not self.is_friend):
|
185
|
+
n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
|
186
|
+
n["nick"] = self.name
|
187
|
+
stanza.append(n)
|
188
|
+
if self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza):
|
189
|
+
self._sent_order.append(stanza["id"])
|
190
|
+
stanza["to"] = self.user.jid
|
191
|
+
stanza.send()
|
192
|
+
return stanza
|
193
|
+
|
194
|
+
def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str):
|
195
|
+
"""
|
196
|
+
Return XMPP msg ids sent by this contact up to a given XMPP msg id.
|
197
|
+
|
198
|
+
Plugins have no reason to use this, but it is used by slidge core
|
199
|
+
for legacy networks that need to mark all messages as read (most XMPP
|
200
|
+
clients only send a read marker for the latest message).
|
201
|
+
|
202
|
+
This has side effects, if the horizon XMPP id is found, messages up to
|
203
|
+
this horizon are not cleared, to avoid sending the same read mark twice.
|
204
|
+
|
205
|
+
:param horizon_xmpp_id: The latest message
|
206
|
+
:return: A list of XMPP ids or None if horizon_xmpp_id was not found
|
207
|
+
"""
|
208
|
+
for i, xmpp_id in enumerate(self._sent_order):
|
209
|
+
if xmpp_id == horizon_xmpp_id:
|
210
|
+
break
|
211
|
+
else:
|
212
|
+
return
|
213
|
+
i += 1
|
214
|
+
res = self._sent_order[:i]
|
215
|
+
self._sent_order = self._sent_order[i:]
|
216
|
+
return res
|
217
|
+
|
218
|
+
@property
|
219
|
+
def name(self):
|
220
|
+
"""
|
221
|
+
Friendly name of the contact, as it should appear in the user's roster
|
222
|
+
"""
|
223
|
+
return self._name
|
224
|
+
|
225
|
+
@name.setter
|
226
|
+
def name(self, n: Optional[str]):
|
227
|
+
if self._name == n:
|
228
|
+
return
|
229
|
+
for p in self.participants:
|
230
|
+
p.nickname = n
|
231
|
+
self._name = n
|
232
|
+
self.xmpp.pubsub.set_nick(user=self.user, jid=self.jid.bare, nick=n)
|
233
|
+
|
234
|
+
def _post_avatar_update(self):
|
235
|
+
for p in self.participants:
|
236
|
+
self.log.debug("Propagating new avatar to %s", p.muc)
|
237
|
+
p.send_last_presence(force=True, no_cache_online=True)
|
238
|
+
|
239
|
+
def set_vcard(
|
240
|
+
self,
|
241
|
+
/,
|
242
|
+
full_name: Optional[str] = None,
|
243
|
+
given: Optional[str] = None,
|
244
|
+
surname: Optional[str] = None,
|
245
|
+
birthday: Optional[date] = None,
|
246
|
+
phone: Optional[str] = None,
|
247
|
+
phones: Iterable[str] = (),
|
248
|
+
note: Optional[str] = None,
|
249
|
+
url: Optional[str] = None,
|
250
|
+
email: Optional[str] = None,
|
251
|
+
country: Optional[str] = None,
|
252
|
+
locality: Optional[str] = None,
|
253
|
+
):
|
254
|
+
vcard = VCard4()
|
255
|
+
vcard.add_impp(f"xmpp:{self.jid.bare}")
|
256
|
+
|
257
|
+
if n := self.name:
|
258
|
+
vcard.add_nickname(n)
|
259
|
+
if full_name:
|
260
|
+
vcard["full_name"] = full_name
|
261
|
+
elif n:
|
262
|
+
vcard["full_name"] = n
|
263
|
+
|
264
|
+
if given:
|
265
|
+
vcard["given"] = given
|
266
|
+
if surname:
|
267
|
+
vcard["surname"] = surname
|
268
|
+
if birthday:
|
269
|
+
vcard["birthday"] = birthday
|
270
|
+
|
271
|
+
if note:
|
272
|
+
vcard.add_note(note)
|
273
|
+
if url:
|
274
|
+
vcard.add_url(url)
|
275
|
+
if email:
|
276
|
+
vcard.add_email(email)
|
277
|
+
if phone:
|
278
|
+
vcard.add_tel(phone)
|
279
|
+
for p in phones:
|
280
|
+
vcard.add_tel(p)
|
281
|
+
if country and locality:
|
282
|
+
vcard.add_address(country, locality)
|
283
|
+
elif country:
|
284
|
+
vcard.add_address(country, locality)
|
285
|
+
|
286
|
+
self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user.jid.bare})
|
287
|
+
|
288
|
+
async def add_to_roster(self, force=False):
|
289
|
+
"""
|
290
|
+
Add this contact to the user roster using :xep:`0356`
|
291
|
+
|
292
|
+
:param force: add even if the contact was already added successfully
|
293
|
+
"""
|
294
|
+
if self.__added_to_roster and not force:
|
295
|
+
return
|
296
|
+
if config.NO_ROSTER_PUSH:
|
297
|
+
log.debug("Roster push request by plugin ignored (--no-roster-push)")
|
298
|
+
return
|
299
|
+
item = {
|
300
|
+
"subscription": self.__get_subscription_string(),
|
301
|
+
"groups": [self.xmpp.ROSTER_GROUP],
|
302
|
+
}
|
303
|
+
if (n := self.name) is not None:
|
304
|
+
item["name"] = n
|
305
|
+
kw = dict(
|
306
|
+
jid=self.user.jid,
|
307
|
+
roster_items={self.jid.bare: item},
|
308
|
+
)
|
309
|
+
try:
|
310
|
+
await self._set_roster(**kw)
|
311
|
+
except PermissionError:
|
312
|
+
warnings.warn(
|
313
|
+
"Slidge does not have privileges to add contacts to the roster. Refer"
|
314
|
+
" to https://slidge.readthedocs.io/en/latest/admin/xmpp_server.html for"
|
315
|
+
" more info."
|
316
|
+
)
|
317
|
+
if config.ROSTER_PUSH_PRESENCE_SUBSCRIPTION_REQUEST_FALLBACK:
|
318
|
+
self.send_friend_request(
|
319
|
+
f"I'm already your friend on {self.xmpp.COMPONENT_TYPE}, but "
|
320
|
+
"slidge is not allowed to manage your roster."
|
321
|
+
)
|
322
|
+
return
|
323
|
+
except IqError as e:
|
324
|
+
self.log.warning("Could not add to roster", exc_info=e)
|
325
|
+
else:
|
326
|
+
# we only broadcast pubsub events for contacts added to the roster
|
327
|
+
# so if something was set before, we need to push it now
|
328
|
+
self.__added_to_roster = True
|
329
|
+
self.session.create_task(self.__broadcast_pubsub_items())
|
330
|
+
self.send_last_presence()
|
331
|
+
|
332
|
+
async def __broadcast_pubsub_items(self):
|
333
|
+
await self.xmpp.pubsub.broadcast_all(JID(self.jid.bare), self.user.jid)
|
334
|
+
|
335
|
+
async def _set_roster(self, **kw):
|
336
|
+
try:
|
337
|
+
return await self.xmpp["xep_0356"].set_roster(**kw)
|
338
|
+
except PermissionError:
|
339
|
+
return await self.xmpp["xep_0356_old"].set_roster(**kw)
|
340
|
+
|
341
|
+
def send_friend_request(self, text: Optional[str] = None):
|
342
|
+
presence = self._make_presence(ptype="subscribe", pstatus=text, bare=True)
|
343
|
+
self._send(presence, nick=True)
|
344
|
+
|
345
|
+
async def accept_friend_request(self, text: Optional[str] = None):
|
346
|
+
"""
|
347
|
+
Call this to signify that this Contact has accepted to be a friend
|
348
|
+
of the user.
|
349
|
+
|
350
|
+
:param text: Optional message from the friend to the user
|
351
|
+
"""
|
352
|
+
self.is_friend = True
|
353
|
+
self.log.debug("Accepting friend request")
|
354
|
+
presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True)
|
355
|
+
self._send(presence, nick=True)
|
356
|
+
self.send_last_presence()
|
357
|
+
await self.__broadcast_pubsub_items()
|
358
|
+
self.log.debug("Accepted friend request")
|
359
|
+
|
360
|
+
def reject_friend_request(self, text: Optional[str] = None):
|
361
|
+
"""
|
362
|
+
Call this to signify that this Contact has refused to be a contact
|
363
|
+
of the user (or that they don't want to be friends anymore)
|
364
|
+
|
365
|
+
:param text: Optional message from the non-friend to the user
|
366
|
+
"""
|
367
|
+
presence = self._make_presence(ptype="unsubscribed", pstatus=text, bare=True)
|
368
|
+
self.offline()
|
369
|
+
self._send(presence, nick=True)
|
370
|
+
self.is_friend = False
|
371
|
+
|
372
|
+
async def on_friend_request(self, text=""):
|
373
|
+
"""
|
374
|
+
Called when receiving a "subscribe" presence, ie, "I would like to add
|
375
|
+
you to my contacts/friends", from the user to this contact.
|
376
|
+
|
377
|
+
In XMPP terms: "I would like to receive your presence updates"
|
378
|
+
|
379
|
+
This is only called if self.is_friend = False. If self.is_friend = True,
|
380
|
+
slidge will automatically "accept the friend request", ie, reply with
|
381
|
+
a "subscribed" presence.
|
382
|
+
|
383
|
+
When called, a 'friend request event' should be sent to the legacy
|
384
|
+
service, and when the contact responds, you should either call
|
385
|
+
self.accept_subscription() or self.reject_subscription()
|
386
|
+
"""
|
387
|
+
pass
|
388
|
+
|
389
|
+
async def on_friend_delete(self, text=""):
|
390
|
+
"""
|
391
|
+
Called when receiving an "unsubscribed" presence, ie, "I would like to
|
392
|
+
remove you to my contacts/friends" or "I refuse your friend request"
|
393
|
+
from the user to this contact.
|
394
|
+
|
395
|
+
In XMPP terms: "You won't receive my presence updates anymore (or you
|
396
|
+
never have)".
|
397
|
+
"""
|
398
|
+
pass
|
399
|
+
|
400
|
+
async def on_friend_accept(self):
|
401
|
+
"""
|
402
|
+
Called when receiving a "subscribed" presence, ie, "I accept to be
|
403
|
+
your/confirm that you are my friend" from the user to this contact.
|
404
|
+
|
405
|
+
In XMPP terms: "You will receive my presence updates".
|
406
|
+
"""
|
407
|
+
pass
|
408
|
+
|
409
|
+
def unsubscribe(self):
|
410
|
+
"""
|
411
|
+
(internal use by slidge)
|
412
|
+
|
413
|
+
Send an "unsubscribe", "unsubscribed", "unavailable" presence sequence
|
414
|
+
from this contact to the user, ie, "this contact has removed you from
|
415
|
+
their 'friends'".
|
416
|
+
"""
|
417
|
+
for ptype in "unsubscribe", "unsubscribed", "unavailable":
|
418
|
+
self.xmpp.send_presence(pfrom=self.jid, pto=self.user.jid.bare, ptype=ptype) # type: ignore
|
419
|
+
|
420
|
+
async def update_info(self):
|
421
|
+
"""
|
422
|
+
Fetch information about this contact from the legacy network
|
423
|
+
|
424
|
+
This is awaited on Contact instantiation, and should be overridden to
|
425
|
+
update the nickname, avatar, vcard [...] of this contact, by making
|
426
|
+
"legacy API calls".
|
427
|
+
|
428
|
+
To take advantage of the slidge avatar cache, you can check the .avatar
|
429
|
+
property to retrieve the "legacy file ID" of the cached avatar. If there
|
430
|
+
is no change, you should not call
|
431
|
+
:py:meth:`slidge.core.mixins.avatar.AvatarMixin.set_avatar` or attempt
|
432
|
+
to modify the ``.avatar`` property.
|
433
|
+
"""
|
434
|
+
pass
|
435
|
+
|
436
|
+
async def fetch_vcard(self):
|
437
|
+
"""
|
438
|
+
It the legacy network doesn't like that you fetch too many profiles on startup,
|
439
|
+
it's also possible to fetch it here, which will be called when XMPP clients
|
440
|
+
of the user request the vcard, if it hasn't been fetched before
|
441
|
+
:return:
|
442
|
+
"""
|
443
|
+
pass
|
444
|
+
|
445
|
+
|
446
|
+
def is_markable(stanza: Union[Message, Presence]):
|
447
|
+
if isinstance(stanza, Presence):
|
448
|
+
return False
|
449
|
+
return bool(stanza["body"])
|
450
|
+
|
451
|
+
|
452
|
+
log = logging.getLogger(__name__)
|
slidge/contact/roster.py
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from typing import TYPE_CHECKING, Generic, Optional, Type
|
4
|
+
|
5
|
+
from slixmpp import JID
|
6
|
+
from slixmpp.jid import JID_UNESCAPE_TRANSFORMATIONS, _unescape_node
|
7
|
+
|
8
|
+
from ..core.mixins.lock import NamedLockMixin
|
9
|
+
from ..util import SubclassableOnce
|
10
|
+
from ..util.types import LegacyContactType, LegacyUserIdType
|
11
|
+
from .contact import LegacyContact
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from ..core.session import BaseSession
|
15
|
+
|
16
|
+
|
17
|
+
class ContactIsUser(Exception):
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class LegacyRoster(
|
22
|
+
Generic[LegacyUserIdType, LegacyContactType],
|
23
|
+
NamedLockMixin,
|
24
|
+
metaclass=SubclassableOnce,
|
25
|
+
):
|
26
|
+
"""
|
27
|
+
Virtual roster of a gateway user, that allows to represent all
|
28
|
+
of their contacts as singleton instances (if used properly and not too bugged).
|
29
|
+
|
30
|
+
Every :class:`.BaseSession` instance will have its own :class:`.LegacyRoster` instance
|
31
|
+
accessible via the :attr:`.BaseSession.contacts` attribute.
|
32
|
+
|
33
|
+
Typically, you will mostly use the :meth:`.LegacyRoster.by_legacy_id` function to
|
34
|
+
retrieve a contact instance.
|
35
|
+
|
36
|
+
You might need to override :meth:`.LegacyRoster.legacy_id_to_jid_username` and/or
|
37
|
+
:meth:`.LegacyRoster.jid_username_to_legacy_id` to incorporate some custom logic
|
38
|
+
if you need some characters when translation JID user parts and legacy IDs.
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(self, session: "BaseSession"):
|
42
|
+
self._contact_cls: Type[LegacyContactType] = (
|
43
|
+
LegacyContact.get_self_or_unique_subclass()
|
44
|
+
)
|
45
|
+
self._contact_cls.xmpp = session.xmpp
|
46
|
+
|
47
|
+
self.session = session
|
48
|
+
self._contacts_by_bare_jid: dict[str, LegacyContactType] = {}
|
49
|
+
self._contacts_by_legacy_id: dict[LegacyUserIdType, LegacyContactType] = {}
|
50
|
+
self.log = logging.getLogger(f"{self.session.user.bare_jid}:roster")
|
51
|
+
self.user_legacy_id: Optional[LegacyUserIdType] = None
|
52
|
+
self.ready: asyncio.Future[bool] = self.session.xmpp.loop.create_future()
|
53
|
+
super().__init__()
|
54
|
+
|
55
|
+
def __repr__(self):
|
56
|
+
return f"<Roster of {self.session.user}>"
|
57
|
+
|
58
|
+
def __iter__(self):
|
59
|
+
return iter(self._contacts_by_legacy_id.values())
|
60
|
+
|
61
|
+
async def __finish_init_contact(
|
62
|
+
self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
|
63
|
+
):
|
64
|
+
c = self._contact_cls(self.session, legacy_id, jid_username, *args, **kwargs)
|
65
|
+
async with self.lock(("finish", c)):
|
66
|
+
if legacy_id in self._contacts_by_legacy_id:
|
67
|
+
self.log.debug("Already updated %s", c)
|
68
|
+
return c
|
69
|
+
await c.avatar_wrap_update_info()
|
70
|
+
self._contacts_by_legacy_id[legacy_id] = c
|
71
|
+
self._contacts_by_bare_jid[c.jid.bare] = c
|
72
|
+
return c
|
73
|
+
|
74
|
+
def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
|
75
|
+
if only_friends:
|
76
|
+
return {j: c for j, c in self._contacts_by_bare_jid.items() if c.is_friend}
|
77
|
+
return self._contacts_by_bare_jid
|
78
|
+
|
79
|
+
async def by_jid(self, contact_jid: JID) -> LegacyContactType:
|
80
|
+
# """
|
81
|
+
# Retrieve a contact by their JID
|
82
|
+
#
|
83
|
+
# If the contact was not instantiated before, it will be created
|
84
|
+
# using :meth:`slidge.LegacyRoster.jid_username_to_legacy_id` to infer their
|
85
|
+
# legacy user ID.
|
86
|
+
#
|
87
|
+
# :param contact_jid:
|
88
|
+
# :return:
|
89
|
+
# """
|
90
|
+
username = contact_jid.node
|
91
|
+
async with self.lock(("username", username)):
|
92
|
+
bare = contact_jid.bare
|
93
|
+
c = self._contacts_by_bare_jid.get(bare)
|
94
|
+
if c is None:
|
95
|
+
legacy_id = await self.jid_username_to_legacy_id(username)
|
96
|
+
log.debug("Contact %s not found", contact_jid)
|
97
|
+
if self.get_lock(("legacy_id", legacy_id)):
|
98
|
+
log.debug("Already updating %s", contact_jid)
|
99
|
+
return await self.by_legacy_id(legacy_id)
|
100
|
+
c = await self.__finish_init_contact(legacy_id, username)
|
101
|
+
return c
|
102
|
+
|
103
|
+
async def by_legacy_id(
|
104
|
+
self, legacy_id: LegacyUserIdType, *args, **kwargs
|
105
|
+
) -> LegacyContactType:
|
106
|
+
"""
|
107
|
+
Retrieve a contact by their legacy_id
|
108
|
+
|
109
|
+
If the contact was not instantiated before, it will be created
|
110
|
+
using :meth:`slidge.LegacyRoster.legacy_id_to_jid_username` to infer their
|
111
|
+
legacy user ID.
|
112
|
+
|
113
|
+
:param legacy_id:
|
114
|
+
:param args: arbitrary additional positional arguments passed to the contact constructor.
|
115
|
+
Requires subclassing LegacyContact.__init__ to accept those.
|
116
|
+
This is useful for networks where you fetch the contact list and information
|
117
|
+
about these contacts in a single request
|
118
|
+
:param kwargs: arbitrary keyword arguments passed to the contact constructor
|
119
|
+
:return:
|
120
|
+
"""
|
121
|
+
if legacy_id == self.user_legacy_id:
|
122
|
+
raise ContactIsUser
|
123
|
+
async with self.lock(("legacy_id", legacy_id)):
|
124
|
+
c = self._contacts_by_legacy_id.get(legacy_id)
|
125
|
+
if c is None:
|
126
|
+
username = await self.legacy_id_to_jid_username(legacy_id)
|
127
|
+
log.debug("Contact %s not found", legacy_id)
|
128
|
+
if self.get_lock(("username", username)):
|
129
|
+
log.debug("Already updating %s", username)
|
130
|
+
jid = JID()
|
131
|
+
jid.node = username
|
132
|
+
jid.domain = self.session.xmpp.boundjid.bare
|
133
|
+
return await self.by_jid(jid)
|
134
|
+
c = await self.__finish_init_contact(
|
135
|
+
legacy_id, username, *args, **kwargs
|
136
|
+
)
|
137
|
+
return c
|
138
|
+
|
139
|
+
async def by_stanza(self, s) -> LegacyContact:
|
140
|
+
# """
|
141
|
+
# Retrieve a contact by the destination of a stanza
|
142
|
+
#
|
143
|
+
# See :meth:`slidge.Roster.by_legacy_id` for more info.
|
144
|
+
#
|
145
|
+
# :param s:
|
146
|
+
# :return:
|
147
|
+
# """
|
148
|
+
return await self.by_jid(s.get_to())
|
149
|
+
|
150
|
+
async def legacy_id_to_jid_username(self, legacy_id: LegacyUserIdType) -> str:
|
151
|
+
"""
|
152
|
+
Convert a legacy ID to a valid 'user' part of a JID
|
153
|
+
|
154
|
+
Should be overridden for cases where the str conversion of
|
155
|
+
the legacy_id is not enough, e.g., if it is case-sensitive or contains
|
156
|
+
forbidden characters not covered by :xep:`0106`.
|
157
|
+
|
158
|
+
:param legacy_id:
|
159
|
+
"""
|
160
|
+
return str(legacy_id).translate(ESCAPE_TABLE)
|
161
|
+
|
162
|
+
async def jid_username_to_legacy_id(self, jid_username: str) -> LegacyUserIdType:
|
163
|
+
"""
|
164
|
+
Convert a JID user part to a legacy ID.
|
165
|
+
|
166
|
+
Should be overridden in case legacy IDs are not strings, or more generally
|
167
|
+
for any case where the username part of a JID (unescaped with to the mapping
|
168
|
+
defined by :xep:`0106`) is not enough to identify a contact on the legacy network.
|
169
|
+
|
170
|
+
Default implementation is an identity operation
|
171
|
+
|
172
|
+
:param jid_username: User part of a JID, ie "user" in "user@example.com"
|
173
|
+
:return: An identifier for the user on the legacy network.
|
174
|
+
"""
|
175
|
+
return _unescape_node(jid_username)
|
176
|
+
|
177
|
+
async def fill(self):
|
178
|
+
"""
|
179
|
+
Populate slidge's "virtual roster".
|
180
|
+
|
181
|
+
Override this and in it, ``await self.by_legacy_id(contact_id)``
|
182
|
+
for the every legacy contacts of the user for which you'd like to
|
183
|
+
set an avatar, nickname, vcard…
|
184
|
+
|
185
|
+
Await ``Contact.add_to_roster()`` in here to add the contact to the
|
186
|
+
user's XMPP roster.
|
187
|
+
"""
|
188
|
+
pass
|
189
|
+
|
190
|
+
|
191
|
+
ESCAPE_TABLE = "".maketrans({v: k for k, v in JID_UNESCAPE_TRANSFORMATIONS.items()})
|
192
|
+
log = logging.getLogger(__name__)
|
slidge/core/__init__.py
CHANGED