slidge 0.1.0b2__py3-none-any.whl → 0.1.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. slidge/__init__.py +55 -31
  2. slidge/__main__.py +118 -116
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +2 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +216 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +789 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +282 -116
  41. slidge/core/session.py +595 -372
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0084 → slixfix/link_preview}/__init__.py +3 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +14 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +25 -15
  56. slidge/slixfix/xep_0100/stanza.py +9 -0
  57. slidge/slixfix/xep_0153/__init__.py +10 -0
  58. slidge/slixfix/xep_0153/stanza.py +25 -0
  59. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  60. slidge/slixfix/xep_0264/__init__.py +5 -0
  61. slidge/slixfix/xep_0264/stanza.py +36 -0
  62. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  63. slidge/slixfix/xep_0292/__init__.py +5 -0
  64. slidge/slixfix/xep_0292/vcard4.py +100 -0
  65. slidge/slixfix/xep_0313/__init__.py +12 -0
  66. slidge/slixfix/xep_0313/mam.py +262 -0
  67. slidge/slixfix/xep_0313/stanza.py +359 -0
  68. slidge/slixfix/xep_0317/__init__.py +5 -0
  69. slidge/slixfix/xep_0317/hats.py +17 -0
  70. slidge/slixfix/xep_0317/stanza.py +28 -0
  71. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  72. slidge/slixfix/xep_0424/__init__.py +9 -0
  73. slidge/slixfix/xep_0424/retraction.py +77 -0
  74. slidge/slixfix/xep_0424/stanza.py +28 -0
  75. slidge/slixfix/xep_0490/__init__.py +8 -0
  76. slidge/slixfix/xep_0490/mds.py +47 -0
  77. slidge/slixfix/xep_0490/stanza.py +17 -0
  78. slidge/util/__init__.py +4 -6
  79. slidge/util/archive_msg.py +61 -0
  80. slidge/util/conf.py +206 -0
  81. slidge/util/db.py +57 -76
  82. slidge/util/schema.sql +126 -0
  83. slidge/util/sql.py +508 -0
  84. slidge/util/test.py +215 -25
  85. slidge/util/types.py +177 -4
  86. slidge/util/util.py +225 -59
  87. slidge-0.1.1.dist-info/METADATA +110 -0
  88. slidge-0.1.1.dist-info/RECORD +96 -0
  89. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/WHEEL +1 -1
  90. slidge/core/contact.py +0 -891
  91. slidge/core/gateway.py +0 -916
  92. slidge/plugins/discord/__init__.py +0 -90
  93. slidge/plugins/discord/client.py +0 -108
  94. slidge/plugins/discord/session.py +0 -162
  95. slidge/plugins/dummy.py +0 -203
  96. slidge/plugins/facebook.py +0 -493
  97. slidge/plugins/hackernews.py +0 -213
  98. slidge/plugins/mattermost/__init__.py +0 -1
  99. slidge/plugins/mattermost/api.py +0 -280
  100. slidge/plugins/mattermost/gateway.py +0 -365
  101. slidge/plugins/mattermost/websocket.py +0 -252
  102. slidge/plugins/signal/__init__.py +0 -3
  103. slidge/plugins/signal/contact.py +0 -106
  104. slidge/plugins/signal/gateway.py +0 -282
  105. slidge/plugins/signal/session.py +0 -448
  106. slidge/plugins/signal/txt.py +0 -53
  107. slidge/plugins/skype.py +0 -325
  108. slidge/plugins/steam.py +0 -310
  109. slidge/plugins/telegram/__init__.py +0 -5
  110. slidge/plugins/telegram/client.py +0 -228
  111. slidge/plugins/telegram/config.py +0 -12
  112. slidge/plugins/telegram/contact.py +0 -176
  113. slidge/plugins/telegram/gateway.py +0 -150
  114. slidge/plugins/telegram/session.py +0 -256
  115. slidge/util/xep_0030/__init__.py +0 -13
  116. slidge/util/xep_0030/disco.py +0 -811
  117. slidge/util/xep_0030/stanza/__init__.py +0 -7
  118. slidge/util/xep_0030/stanza/info.py +0 -270
  119. slidge/util/xep_0030/stanza/items.py +0 -147
  120. slidge/util/xep_0030/static.py +0 -467
  121. slidge/util/xep_0055/__init__.py +0 -5
  122. slidge/util/xep_0055/search.py +0 -75
  123. slidge/util/xep_0055/stanza.py +0 -10
  124. slidge/util/xep_0077/stanza.py +0 -71
  125. slidge/util/xep_0084/avatar.py +0 -137
  126. slidge/util/xep_0084/stanza.py +0 -104
  127. slidge/util/xep_0115/__init__.py +0 -12
  128. slidge/util/xep_0115/caps.py +0 -379
  129. slidge/util/xep_0115/stanza.py +0 -16
  130. slidge/util/xep_0115/static.py +0 -137
  131. slidge/util/xep_0292/__init__.py +0 -1
  132. slidge/util/xep_0292/stanza.py +0 -167
  133. slidge/util/xep_0292/vcard4.py +0 -75
  134. slidge/util/xep_0333/__init__.py +0 -10
  135. slidge/util/xep_0333/markers.py +0 -96
  136. slidge/util/xep_0333/stanza.py +0 -34
  137. slidge/util/xep_0356/__init__.py +0 -7
  138. slidge/util/xep_0356/permissions.py +0 -35
  139. slidge/util/xep_0356/privilege.py +0 -160
  140. slidge/util/xep_0356/stanza.py +0 -44
  141. slidge/util/xep_0363/__init__.py +0 -16
  142. slidge/util/xep_0363/http_upload.py +0 -215
  143. slidge/util/xep_0363/stanza.py +0 -46
  144. slidge/util/xep_0461/__init__.py +0 -6
  145. slidge/util/xep_0461/reply.py +0 -48
  146. slidge/util/xep_0461/stanza.py +0 -47
  147. slidge-0.1.0b2.dist-info/METADATA +0 -171
  148. slidge-0.1.0b2.dist-info/RECORD +0 -81
  149. /slidge/{plugins/__init__.py → py.typed} +0 -0
  150. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  151. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  152. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  153. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  154. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/LICENSE +0 -0
  155. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/entry_points.txt +0 -0
slidge/core/contact.py DELETED
@@ -1,891 +0,0 @@
1
- import functools
2
- import logging
3
- from datetime import date, datetime, timezone
4
- from io import BytesIO
5
- from pathlib import Path
6
- from typing import (
7
- IO,
8
- TYPE_CHECKING,
9
- Any,
10
- Generic,
11
- Iterable,
12
- Literal,
13
- Optional,
14
- Type,
15
- TypeVar,
16
- Union,
17
- )
18
-
19
- import aiohttp
20
- from slixmpp import JID, Message
21
-
22
- from ..util import SubclassableOnce
23
- from ..util.types import (
24
- AvatarType,
25
- LegacyContactIdType,
26
- LegacyMessageType,
27
- LegacyUserIdType,
28
- )
29
- from ..util.xep_0292.stanza import VCard4
30
- from ..util.xep_0363 import FileUploadError
31
-
32
- if TYPE_CHECKING:
33
- from .session import SessionType
34
- else:
35
- SessionType = TypeVar("SessionType")
36
-
37
-
38
- class LegacyContact(Generic[SessionType], metaclass=SubclassableOnce):
39
- """
40
- This class centralizes actions in relation to a specific legacy contact.
41
-
42
- You shouldn't create instances of contacts manually, but rather rely on
43
- :meth:`.LegacyRoster.by_legacy_id` to ensure that contact instances are
44
- singletons. The :class:`.LegacyRoster` instance of a session is accessible
45
- through the :attr:`.BaseSession.contacts` attribute.
46
-
47
- Typically, your plugin should have methods hook to the legacy events and
48
- call appropriate methods here to transmit the "legacy action" to the xmpp
49
- user. This should look like this:
50
-
51
- .. code-block:python
52
-
53
- class Session(BaseSession):
54
- ...
55
-
56
- async def on_cool_chat_network_new_text_message(self, legacy_msg_event):
57
- contact = self.contacts.by_legacy_id(legacy_msg_event.from)
58
- contact.send_text(legacy_msg_event.text)
59
-
60
- async def on_cool_chat_network_new_typing_event(self, legacy_typing_event):
61
- contact = self.contacts.by_legacy_id(legacy_msg_event.from)
62
- contact.composing()
63
- ...
64
- """
65
-
66
- RESOURCE: str = "slidge"
67
- """
68
- A full JID, including a resource part is required for chat states (and maybe other stuff)
69
- to work properly. This is the name of the resource the contacts will use.
70
- """
71
-
72
- AVATAR = True
73
- RECEIPTS = True
74
- MARKS = True
75
- CHAT_STATES = True
76
- UPLOAD = True
77
- CORRECTION = True
78
- REACTION = True
79
- RETRACTION = True
80
- REPLIES = True
81
-
82
- """
83
- A list of features advertised through service discovery and client capabilities.
84
- """
85
-
86
- CLIENT_TYPE = "pc"
87
- """
88
- https://xmpp.org/registrar/disco-categories.html#client
89
- """
90
-
91
- def __init__(
92
- self,
93
- session: "SessionType",
94
- legacy_id: LegacyContactIdType,
95
- jid_username: str,
96
- ):
97
- """
98
- :param session: The session this contact is part of
99
- :param legacy_id: The contact's legacy ID
100
- :param jid_username: User part of this contact's 'puppet' JID.
101
- NB: case-insensitive, and some special characters are not allowed
102
- """
103
- self.session = session
104
- self.user = session.user
105
- self.legacy_id = legacy_id
106
- self.jid_username = jid_username
107
-
108
- self.added_to_roster = False
109
-
110
- self._name: Optional[str] = None
111
- self._avatar: Optional[AvatarType] = None
112
-
113
- self._subscribe_from = True
114
- self._subscribe_to = True
115
-
116
- self.xmpp = session.xmpp
117
- self.xmpp.loop.create_task(self.__make_caps())
118
-
119
- def __repr__(self):
120
- return f"<LegacyContact <{self.jid}> ('{self.legacy_id}') of <{self.user}>"
121
-
122
- def __get_subscription_string(self):
123
- if self._subscribe_from and self._subscribe_to:
124
- return "both"
125
- if self._subscribe_from:
126
- return "from"
127
- if self._subscribe_to:
128
- return "to"
129
- return "none"
130
-
131
- async def __make_caps(self):
132
- """
133
- Configure slixmpp to correctly advertise this contact's capabilities.
134
- """
135
- jid = self.jid
136
- xmpp = self.xmpp
137
-
138
- xmpp["xep_0030"].add_identity(
139
- jid=jid, category="client", itype=self.CLIENT_TYPE
140
- )
141
- add_feature = functools.partial(xmpp["xep_0030"].add_feature, jid=jid)
142
- if self.CHAT_STATES:
143
- await add_feature("http://jabber.org/protocol/chatstates")
144
- if self.RECEIPTS:
145
- await add_feature("urn:xmpp:receipts")
146
- if self.CORRECTION:
147
- await add_feature("urn:xmpp:message-correct:0")
148
- if self.MARKS:
149
- await add_feature("urn:xmpp:chat-markers:0")
150
- if self.UPLOAD:
151
- await add_feature("jabber:x:oob")
152
- if self.REACTION:
153
- await add_feature("urn:xmpp:reactions:0")
154
- if self.RETRACTION:
155
- await add_feature("urn:xmpp:message-retract:0")
156
- if self.REPLIES:
157
- await add_feature("urn:xmpp:reply:0")
158
-
159
- await add_feature("urn:ietf:params:xml:ns:vcard-4.0")
160
- await xmpp["xep_0115"].update_caps(jid=self.jid)
161
-
162
- @property
163
- def jid(self) -> JID:
164
- """
165
- Full JID (including the 'puppet' resource) of the contact
166
- """
167
- j = JID(self.jid_username + "@" + self.xmpp.boundjid.bare)
168
- j.resource = self.RESOURCE
169
- return j
170
-
171
- @property
172
- def name(self):
173
- """
174
- Friendly name of the contact, as it should appear in the user's roster
175
- """
176
- return self._name
177
-
178
- @name.setter
179
- def name(self, n: Optional[str]):
180
- if self._name == n:
181
- return
182
- self._name = n
183
- self.xmpp.pubsub.set_nick(
184
- jid=self.jid.bare, nick=n, restrict_to=self.user.jid.bare
185
- )
186
-
187
- @property
188
- def avatar(self):
189
- """
190
- An image that represents this contact
191
- """
192
- return self._avatar
193
-
194
- @avatar.setter
195
- def avatar(self, a: Optional[AvatarType]):
196
- if a == self._avatar:
197
- return
198
- self.xmpp.loop.create_task(
199
- self.xmpp.pubsub.set_avatar(
200
- jid=self.jid.bare, avatar=a, restrict_to=self.user.jid.bare
201
- )
202
- )
203
- self._avatar = a
204
-
205
- def set_vcard(
206
- self,
207
- /,
208
- full_name: Optional[str] = None,
209
- given: Optional[str] = None,
210
- surname: Optional[str] = None,
211
- birthday: Optional[date] = None,
212
- phone: Optional[str] = None,
213
- note: Optional[str] = None,
214
- url: Optional[str] = None,
215
- email: Optional[str] = None,
216
- country: Optional[str] = None,
217
- locality: Optional[str] = None,
218
- ):
219
- vcard = VCard4()
220
- vcard.add_impp(f"xmpp:{self.jid.bare}")
221
-
222
- if n := self.name:
223
- vcard.add_nickname(n)
224
- if full_name:
225
- vcard["full_name"] = full_name
226
- elif n:
227
- vcard["full_name"] = n
228
-
229
- if given:
230
- vcard["given"] = given
231
- if surname:
232
- vcard["surname"] = surname
233
- if birthday:
234
- vcard["birthday"] = birthday
235
-
236
- if note:
237
- vcard.add_note(note)
238
- if url:
239
- vcard.add_url(url)
240
- if email:
241
- vcard.add_email(email)
242
- if phone:
243
- vcard.add_tel(phone)
244
- if country and locality:
245
- vcard.add_address(country, locality)
246
- elif country:
247
- vcard.add_address(country, locality)
248
-
249
- self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user.jid.bare})
250
-
251
- async def add_to_roster(self):
252
- """
253
- Add this contact to the user roster using :xep:`0356`
254
- """
255
- if self.xmpp.no_roster_push:
256
- log.debug("Roster push request by plugin ignored (--no-roster-push)")
257
- return
258
- item = {
259
- "subscription": self.__get_subscription_string(),
260
- "groups": [self.xmpp.ROSTER_GROUP],
261
- }
262
- if (n := self.name) is not None:
263
- item["name"] = n
264
- kw = dict(
265
- jid=self.user.jid,
266
- roster_items={self.jid.bare: item},
267
- )
268
- try:
269
- await self.xmpp["xep_0356"].set_roster(**kw)
270
- except PermissionError:
271
- try:
272
- await self.xmpp["xep_0356_old"].set_roster(**kw)
273
- except PermissionError:
274
- log.warning(
275
- "Slidge does not have privileges to add contacts to the roster."
276
- "Refer to https://slidge.readthedocs.io/en/latest/admin/xmpp_server.html "
277
- "for more info."
278
- )
279
- return
280
-
281
- self.added_to_roster = True
282
-
283
- def online(self, status: Optional[str] = None):
284
- """
285
- Send an "online" presence from this contact to the user.
286
-
287
- :param status: Arbitrary text, details of the status, eg: "Listening to Britney Spears"
288
- """
289
- self.xmpp.send_presence(pfrom=self.jid, pto=self.user.jid.bare, pstatus=status)
290
-
291
- def away(self, status: Optional[str] = None):
292
- """
293
- Send an "away" presence from this contact to the user.
294
-
295
- This is a global status, as opposed to :meth:`.LegacyContact.inactive`
296
- which concerns a specific conversation, ie a specific "chat window"
297
-
298
- :param status: Arbitrary text, details of the status, eg: "Gone to fight capitalism"
299
- """
300
- self.xmpp.send_presence(
301
- pfrom=self.jid, pto=self.user.jid.bare, pshow="away", pstatus=status
302
- )
303
-
304
- def extended_away(self, status: Optional[str] = None):
305
- """
306
- Send an "extended away" presence from this contact to the user.
307
-
308
- This is a global status, as opposed to :meth:`.LegacyContact.inactive`
309
- which concerns a specific conversation, ie a specific "chat window"
310
-
311
- :param status: Arbitrary text, details of the status, eg: "Gone to fight capitalism"
312
- """
313
- self.xmpp.send_presence(
314
- pfrom=self.jid, pto=self.user.jid.bare, pshow="xa", pstatus=status
315
- )
316
-
317
- def busy(self, status: Optional[str] = None):
318
- """
319
- Send a "busy" presence from this contact to the user,
320
-
321
- :param status: eg: "Trying to make sense of XEP-0100"
322
- """
323
- self.xmpp.send_presence(
324
- pfrom=self.jid, pto=self.user.jid.bare, pshow="busy", pstatus=status
325
- )
326
-
327
- def offline(self):
328
- """
329
- Send an "offline" presence from this contact to the user.
330
- """
331
- self.xmpp.send_presence(
332
- pfrom=self.jid, pto=self.user.jid.bare, ptype="unavailable"
333
- )
334
-
335
- def unsubscribe(self):
336
- """
337
- Send an "unsubscribed" presence from this contact to the user.
338
- """
339
- self.xmpp.send_presence(
340
- pfrom=self.jid, pto=self.user.jid.bare, ptype="unsubscribed"
341
- )
342
-
343
- def status(self, text: str):
344
- """
345
- Set a contact's status
346
- """
347
- self.xmpp.send_presence(pfrom=self.jid, pto=self.user.jid.bare, pstatus=text)
348
-
349
- def __chat_state(self, state: str):
350
- msg = self.xmpp.make_message(mfrom=self.jid, mto=self.user.jid, mtype="chat")
351
- msg["chat_state"] = state
352
- msg.enable("no-store")
353
- msg.send()
354
-
355
- def active(self):
356
- """
357
- Send an "active" chat state (:xep:`0085`) from this contact to the user.
358
- """
359
- self.__chat_state("active")
360
-
361
- def composing(self):
362
- """
363
- Send a "composing" (ie "typing notification") chat state (:xep:`0085`) from this contact to the user.
364
- """
365
- self.__chat_state("composing")
366
-
367
- def paused(self):
368
- """
369
- Send a "paused" (ie "typing paused notification") chat state (:xep:`0085`) from this contact to the user.
370
- """
371
- self.__chat_state("paused")
372
-
373
- def inactive(self):
374
- """
375
- Send an "inactive" (ie "typing paused notification") chat state (:xep:`0085`) from this contact to the user.
376
- """
377
- log.debug("%s go inactive", self)
378
- self.__chat_state("inactive")
379
-
380
- def __send_marker(
381
- self,
382
- legacy_msg_id: LegacyMessageType,
383
- marker: Literal["acknowledged", "received", "displayed"],
384
- ):
385
- """
386
- Send a message marker (:xep:`0333`) from this contact to the user.
387
-
388
- NB: for the 'received' marker, this also sends a message receipt (:xep:`0184`)
389
-
390
- :param legacy_msg_id: ID of the message this marker refers to
391
- :param marker: The marker type
392
-
393
- """
394
- xmpp_id = self.session.sent.get(legacy_msg_id)
395
- if xmpp_id is None:
396
- log.debug("Cannot find the XMPP ID of this msg: %s", legacy_msg_id)
397
- else:
398
- if marker == "received":
399
- receipt = self.xmpp.Message()
400
- receipt["to"] = self.user.jid
401
- receipt["receipt"] = xmpp_id
402
- receipt["from"] = self.jid
403
- receipt.send()
404
- self.xmpp["xep_0333"].send_marker(
405
- mto=self.user.jid,
406
- id=xmpp_id,
407
- marker=marker,
408
- mfrom=self.jid,
409
- )
410
-
411
- def ack(self, legacy_msg_id: LegacyMessageType):
412
- """
413
- Send an "acknowledged" message marker (:xep:`0333`) from this contact to the user.
414
-
415
- :param legacy_msg_id: The message this marker refers to
416
- """
417
- self.__send_marker(legacy_msg_id, "acknowledged")
418
-
419
- def received(self, legacy_msg_id: LegacyMessageType):
420
- """
421
- Send a "received" message marker (:xep:`0333`) and a "message delivery receipt"
422
- (:xep:`0184`)
423
- from this contact to the user
424
-
425
- :param legacy_msg_id: The message this marker refers to
426
- """
427
- self.__send_marker(legacy_msg_id, "received")
428
-
429
- def displayed(self, legacy_msg_id: LegacyMessageType):
430
- """
431
- Send a "displayed" message marker (:xep:`0333`) from this contact to the user.
432
-
433
- :param legacy_msg_id: The message this marker refers to
434
- """
435
- self.__send_marker(legacy_msg_id, "displayed")
436
-
437
- def __make_message(self, mtype="chat", **kwargs) -> Message:
438
- m = self.xmpp.make_message(
439
- mfrom=self.jid, mto=self.user.jid, mtype=mtype, **kwargs
440
- )
441
- m.enable("markable")
442
- return m
443
-
444
- def __send_message(
445
- self,
446
- msg: Message,
447
- legacy_msg_id: Optional[Any] = None,
448
- when: Optional[datetime] = None,
449
- ):
450
- if legacy_msg_id is not None:
451
- msg.set_id(self.session.legacy_msg_id_to_xmpp_msg_id(legacy_msg_id))
452
- self._add_delay(msg, when)
453
- msg.send()
454
-
455
- def __make_reply(self, msg: Message, reply_to_msg_id: Optional[LegacyMessageType]):
456
- if reply_to_msg_id is None:
457
- return
458
- xmpp_id = self.session.sent.get(
459
- reply_to_msg_id
460
- ) or self.session.legacy_msg_id_to_xmpp_msg_id(reply_to_msg_id)
461
- msg["reply"]["id"] = self.session.legacy_msg_id_to_xmpp_msg_id(xmpp_id)
462
- # FIXME: https://xmpp.org/extensions/xep-0461.html#usecases mentions that a full JID must be used here
463
- msg["reply"]["to"] = self.user.jid
464
-
465
- def send_text(
466
- self,
467
- body: str = "",
468
- *,
469
- chat_state: Optional[str] = "active",
470
- legacy_msg_id: Optional[LegacyMessageType] = None,
471
- reply_to_msg_id: Optional[LegacyMessageType] = None,
472
- when: Optional[datetime] = None,
473
- ) -> Message:
474
- """
475
- Transmit a message from the contact to the user
476
-
477
- :param body: Context of the message
478
- :param chat_state: By default, will send an "active" chat state (:xep:`0085`) along with the
479
- message. Set this to ``None`` if this is not desired.
480
- :param legacy_msg_id: If you want to be able to transport read markers from the gateway
481
- user to the legacy network, specify this
482
- :param reply_to_msg_id:
483
- :param when: when the message was sent, for a "delay" tag (:xep:`0203`)
484
-
485
- :return: the XMPP message that was sent
486
- """
487
- msg = self.__make_message(mbody=body)
488
- if self.CHAT_STATES and chat_state is not None:
489
- msg["chat_state"] = chat_state
490
- self.__make_reply(msg, reply_to_msg_id)
491
- self.__send_message(msg, legacy_msg_id, when)
492
- return msg
493
-
494
- async def send_file(
495
- self,
496
- filename: Union[Path, str],
497
- content_type: Optional[str] = None,
498
- input_file: Optional[IO[bytes]] = None,
499
- url: Optional[str] = None,
500
- *,
501
- legacy_msg_id: Optional[LegacyMessageType] = None,
502
- reply_to_msg_id: Optional[LegacyMessageType] = None,
503
- when: Optional[datetime] = None,
504
- ) -> Message:
505
- """
506
- Send a file using HTTP upload (:xep:`0363`)
507
-
508
- :param filename: Filename to use or location on disk to the file to upload
509
- :param content_type: MIME type, inferred from filename if not given
510
- :param input_file: Optionally, a file like object instead of a file on disk.
511
- filename will still be used to give the uploaded file a name
512
- :param legacy_msg_id: If you want to be able to transport read markers from the gateway
513
- user to the legacy network, specify this
514
- :param url: Optionally, a URL of a file that slidge will download and upload to the
515
- default file upload service on the xmpp server it's running on. url and input_file
516
- are mutually exclusive.
517
- :param reply_to_msg_id:
518
- :param when: when the file was sent, for a "delay" tag (:xep:`0203`)
519
-
520
- :return: The msg stanza that was sent
521
- """
522
- msg = self.__make_message()
523
- self.__make_reply(msg, reply_to_msg_id)
524
- if url is not None:
525
- if input_file is not None:
526
- raise TypeError("Either a URL or a file-like object")
527
- async with aiohttp.ClientSession() as session:
528
- async with session.get(url) as r:
529
- input_file = BytesIO(await r.read())
530
- try:
531
- uploaded_url = await self.xmpp["xep_0363"].upload_file(
532
- filename=filename,
533
- content_type=content_type,
534
- input_file=input_file,
535
- ifrom=self.xmpp.upload_requester,
536
- )
537
- except FileUploadError as e:
538
- log.warning(
539
- "Something is wrong with the upload service, see the traceback below"
540
- )
541
- log.exception(e)
542
- if url is not None:
543
- uploaded_url = url
544
- else:
545
- msg["body"] = (
546
- "I tried to send a file, but something went wrong. "
547
- "Tell your XMPP admin to check slidge logs."
548
- )
549
- self.__send_message(msg, legacy_msg_id, when)
550
- return msg
551
-
552
- msg["oob"]["url"] = uploaded_url
553
- msg["body"] = uploaded_url
554
- self.__send_message(msg, legacy_msg_id, when)
555
- return msg
556
-
557
- def __privileged_send(self, msg: Message, when: Optional[datetime] = None):
558
- msg.set_from(self.user.jid.bare)
559
- msg.enable("store")
560
-
561
- self._add_delay(msg, when)
562
-
563
- self.session.ignore_messages.add(msg.get_id())
564
- try:
565
- self.xmpp["xep_0356"].send_privileged_message(msg)
566
- except PermissionError:
567
- try:
568
- self.xmpp["xep_0356_old"].send_privileged_message(msg)
569
- except PermissionError:
570
- log.warning(
571
- "Slidge does not have privileges to send message on behalf of user."
572
- "Refer to https://slidge.readthedocs.io/en/latest/admin/xmpp_server.html "
573
- "for more info."
574
- )
575
- return
576
- return msg.get_id()
577
-
578
- def carbon(
579
- self,
580
- body: str,
581
- legacy_id: Optional[Any] = None,
582
- when: Optional[datetime] = None,
583
- ):
584
- """
585
- Call this when the user sends a message to a legacy network contact.
586
-
587
- This synchronizes the outgoing message history on the XMPP side, using
588
- :xep:`0356` to impersonate the XMPP user and send a message from the user to
589
- the contact. Thw XMPP server should in turn send carbons (:xep:`0280`) to online
590
- XMPP clients +/- write the message in server-side archives (:xep:`0313`),
591
- depending on the user's and the server's archiving policy.
592
-
593
- :param body: Body of the message.
594
- :param legacy_id: Legacy message ID
595
- :param when: When was this message sent.
596
- """
597
- # we use Message() directly because we need xmlns="jabber:client"
598
- msg = Message()
599
- msg["to"] = self.jid.bare
600
- msg["type"] = "chat"
601
- msg["body"] = body
602
- if legacy_id:
603
- xmpp_id = self.session.legacy_msg_id_to_xmpp_msg_id(legacy_id)
604
- msg.set_id(xmpp_id)
605
- self.session.sent[legacy_id] = xmpp_id
606
-
607
- return self.__privileged_send(msg, when)
608
-
609
- def carbon_read(self, legacy_msg_id: Any, when: Optional[datetime] = None):
610
- """
611
- Synchronize user read state from official clients.
612
-
613
- :param legacy_msg_id:
614
- :param when:
615
- """
616
- # we use Message() directly because we need xmlns="jabber:client"
617
- msg = Message()
618
- msg["to"] = self.jid.bare
619
- msg["type"] = "chat"
620
- msg["displayed"]["id"] = self.session.legacy_msg_id_to_xmpp_msg_id(
621
- legacy_msg_id
622
- )
623
-
624
- return self.__privileged_send(msg, when)
625
-
626
- def carbon_correct(
627
- self,
628
- legacy_msg_id: LegacyMessageType,
629
- text: str,
630
- when: Optional[datetime] = None,
631
- ):
632
- """
633
- Call this when the user corrects their own (last) message from an official client
634
-
635
- :param legacy_msg_id:
636
- :param text: The new body of the message
637
- :param when:
638
- """
639
- if (xmpp_id := self.session.sent.get(legacy_msg_id)) is None:
640
- log.debug(
641
- "Cannot find XMPP ID of msg '%s' corrected from the official client",
642
- legacy_msg_id,
643
- )
644
- return
645
- msg = Message()
646
- msg.set_to(self.jid.bare)
647
- msg.set_type("chat")
648
- msg["replace"]["id"] = xmpp_id
649
- msg["body"] = text
650
- return self.__privileged_send(msg, when)
651
-
652
- def carbon_react(
653
- self,
654
- legacy_msg_id: LegacyMessageType,
655
- reactions: Iterable[str] = (),
656
- when: Optional[datetime] = None,
657
- ):
658
- """
659
- Call this to modify the user's own reactions (:xep:`0444`) about a message.
660
-
661
- Can be called when the user reacts from the official client, or to modify a user's
662
- reaction when the legacy network has constraints about acceptable reactions.
663
-
664
- :param legacy_msg_id: Legacy message ID this refers to
665
- :param reactions: iterable of emojis
666
- :param when:
667
- """
668
- if xmpp_id := self.session.sent.inverse.get(str(legacy_msg_id)):
669
- log.debug("This is a reaction to a carbon message")
670
- xmpp_id = str(xmpp_id)
671
- elif xmpp_id := self.session.sent.get(legacy_msg_id):
672
- log.debug("This is a reaction to the user's own message")
673
- else:
674
- log.debug(
675
- "Cannot determine which message this reaction refers to, attempting msg ID conversion"
676
- )
677
- xmpp_id = self.session.legacy_msg_id_to_xmpp_msg_id(legacy_msg_id)
678
- msg = Message()
679
- msg["to"] = self.jid.bare
680
- msg["type"] = "chat"
681
- self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=reactions)
682
- return self.__privileged_send(msg, when)
683
-
684
- def carbon_retract(
685
- self, legacy_msg_id: LegacyMessageType, when: Optional[datetime] = None
686
- ):
687
- """
688
- Call this when the user calls retracts (:xep:`0424`) a message from an official client
689
-
690
- :param legacy_msg_id:
691
- :param when:
692
- :return:
693
- """
694
- if (xmpp_id := self.session.sent.inverse.get(str(legacy_msg_id))) is None:
695
- if (xmpp_id := self.session.sent.get(legacy_msg_id)) is None:
696
- log.debug("Cannot find XMPP ID of retracted msg: %s", legacy_msg_id)
697
- return
698
-
699
- msg = Message()
700
- msg.set_to(self.jid.bare)
701
- msg.set_type("chat")
702
- msg["apply_to"]["id"] = xmpp_id
703
- msg["apply_to"].enable("retract")
704
- return self.__privileged_send(msg, when)
705
-
706
- def correct(self, legacy_msg_id: Any, new_text: str):
707
- """
708
- Call this when a legacy contact has modified his last message content.
709
-
710
- Uses last message correction (:xep:`0308`)
711
-
712
- :param legacy_msg_id: Legacy message ID this correction refers to
713
- :param new_text: The new text
714
- """
715
- msg = self.__make_message()
716
- msg["replace"]["id"] = self.session.legacy_msg_id_to_xmpp_msg_id(legacy_msg_id)
717
- msg["body"] = new_text
718
- self.__send_message(msg)
719
-
720
- def react(self, legacy_msg_id: LegacyMessageType, emojis: Iterable[str] = ()):
721
- """
722
- Call this when a legacy contact reacts to a message
723
-
724
- :param legacy_msg_id: The message which the reaction refers to.
725
- :param emojis: A iterable of emojis used as reactions
726
- :return:
727
- """
728
- if (xmpp_id := self.session.sent.get(legacy_msg_id)) is None:
729
- log.debug(
730
- "Cannot determine which message this reaction refers to, attempting msg ID conversion"
731
- )
732
- xmpp_id = self.session.legacy_msg_id_to_xmpp_msg_id(legacy_msg_id)
733
- msg = self.__make_message()
734
- self.xmpp["xep_0444"].set_reactions(
735
- msg,
736
- to_id=xmpp_id,
737
- reactions=emojis,
738
- )
739
- self.__send_message(msg)
740
- return msg
741
-
742
- def retract(self, legacy_msg_id: LegacyMessageType):
743
- """
744
- Call this when a legacy contact retracts (:XEP:`0424`) a message
745
-
746
- :param legacy_msg_id: Legacy ID of the message to delete
747
- """
748
- self.xmpp["xep_0424"].send_retraction(
749
- mto=self.user.jid,
750
- mfrom=self.jid,
751
- include_fallback=True,
752
- fallback_text="I have deleted the message %s, but your XMPP client does not support that"
753
- % legacy_msg_id, # https://github.com/movim/movim/issues/1074
754
- id=self.session.legacy_msg_id_to_xmpp_msg_id(legacy_msg_id),
755
- )
756
-
757
- def _add_delay(self, msg: Message, when: Optional[datetime] = None):
758
- if not when:
759
- return
760
- if when.tzinfo is None:
761
- when = when.astimezone(timezone.utc)
762
- if (
763
- datetime.now().astimezone(timezone.utc) - when
764
- > self.xmpp.ignore_delay_threshold
765
- ):
766
- msg["delay"].set_stamp(when)
767
-
768
-
769
- LegacyContactType = TypeVar("LegacyContactType", bound=LegacyContact)
770
-
771
-
772
- class LegacyRoster(Generic[LegacyContactType, SessionType], metaclass=SubclassableOnce):
773
- """
774
- Virtual roster of a gateway user, that allows to represent all
775
- of their contacts as singleton instances (if used properly and not too bugged).
776
-
777
- Every :class:`.BaseSession` instance will have its own :class:`.LegacyRoster` instance
778
- accessible via the :attr:`.BaseSession.contacts` attribute.
779
-
780
- Typically, you will mostly use the :meth:`.LegacyRoster.by_legacy_id` function to
781
- retrieve a contact instance.
782
-
783
- You might need to override :meth:`.LegacyRoster.legacy_id_to_jid_username` and/or
784
- :meth:`.LegacyRoster.jid_username_to_legacy_id` to incorporate some custom logic
785
- if you need some characters when translation JID user parts and legacy IDs.
786
- """
787
-
788
- def __init__(self, session: "SessionType"):
789
- self._contact_cls: Type[
790
- LegacyContactType
791
- ] = LegacyContact.get_self_or_unique_subclass()
792
- self._contact_cls.xmpp = session.xmpp
793
-
794
- self.session = session
795
- self._contacts_by_bare_jid: dict[str, LegacyContactType] = {}
796
- self._contacts_by_legacy_id: dict[LegacyContactIdType, LegacyContactType] = {}
797
-
798
- def __iter__(self):
799
- return iter(self._contacts_by_legacy_id.values())
800
-
801
- def by_jid(self, contact_jid: JID) -> LegacyContactType:
802
- """
803
- Retrieve a contact by their JID
804
-
805
- If the contact was not instantiated before, it will be created
806
- using :meth:`slidge.LegacyRoster.jid_username_to_legacy_id` to infer their
807
- legacy user ID.
808
-
809
- :param contact_jid:
810
- :return:
811
- """
812
- bare = contact_jid.bare
813
- c = self._contacts_by_bare_jid.get(bare)
814
- if c is None:
815
- jid_username = str(contact_jid.username)
816
- log.debug("Contact %s not found", contact_jid)
817
- c = self._contact_cls(
818
- self.session,
819
- self.jid_username_to_legacy_id(jid_username),
820
- jid_username,
821
- )
822
- self._contacts_by_legacy_id[c.legacy_id] = self._contacts_by_bare_jid[
823
- bare
824
- ] = c
825
- return c
826
-
827
- def by_legacy_id(self, legacy_id: Any) -> LegacyContactType:
828
- """
829
- Retrieve a contact by their legacy_id
830
-
831
- If the contact was not instantiated before, it will be created
832
- using :meth:`slidge.LegacyRoster.legacy_id_to_jid_username` to infer their
833
- legacy user ID.
834
-
835
- :param legacy_id:
836
- :return:
837
- """
838
- c = self._contacts_by_legacy_id.get(legacy_id)
839
- if c is None:
840
- log.debug("Contact %s not found in roster", legacy_id)
841
- c = self._contact_cls(
842
- self.session, legacy_id, self.legacy_id_to_jid_username(legacy_id)
843
- )
844
- self._contacts_by_bare_jid[c.jid.bare] = self._contacts_by_legacy_id[
845
- legacy_id
846
- ] = c
847
- return c
848
-
849
- def by_stanza(self, s) -> LegacyContactType:
850
- """
851
- Retrieve a contact by the destination of a stanza
852
-
853
- See :meth:`slidge.Roster.by_legacy_id` for more info.
854
-
855
- :param s:
856
- :return:
857
- """
858
- return self.by_jid(s.get_to())
859
-
860
- @staticmethod
861
- def legacy_id_to_jid_username(legacy_id: Any) -> str:
862
- """
863
- Convert a legacy ID to a valid 'user' part of a JID
864
-
865
- Should be overridden for cases where the str conversion of
866
- the legacy_id is not enough, e.g., if it contains forbidden character.
867
-
868
- :param legacy_id:
869
- """
870
- return str(legacy_id)
871
-
872
- @staticmethod
873
- def jid_username_to_legacy_id(jid_username: str) -> LegacyUserIdType:
874
- """
875
- Convert a JID user part to a legacy ID.
876
-
877
- Should be overridden in case legacy IDs are not strings, or more generally
878
- for any case where the username part of a JID is not enough to identify
879
- a contact on the legacy network.
880
-
881
- Default implementation is an identity operation
882
-
883
- :param jid_username: User part of a JID, ie "user" in "user@example.com"
884
- :return: An identifier for the user on the legacy network.
885
- """
886
- return jid_username # type:ignore
887
-
888
-
889
- LegacyRosterType = TypeVar("LegacyRosterType", bound=LegacyRoster)
890
-
891
- log = logging.getLogger(__name__)