slidge 0.1.3__py3-none-any.whl → 0.2.0a1__py3-none-any.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.
Files changed (74) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +6 -7
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -6
  9. slidge/contact/contact.py +165 -49
  10. slidge/contact/roster.py +122 -47
  11. slidge/core/config.py +14 -11
  12. slidge/core/gateway/base.py +148 -36
  13. slidge/core/gateway/caps.py +7 -5
  14. slidge/core/gateway/disco.py +2 -4
  15. slidge/core/gateway/mam.py +1 -4
  16. slidge/core/gateway/muc_admin.py +1 -1
  17. slidge/core/gateway/ping.py +2 -3
  18. slidge/core/gateway/presence.py +1 -1
  19. slidge/core/gateway/registration.py +32 -21
  20. slidge/core/gateway/search.py +3 -5
  21. slidge/core/gateway/session_dispatcher.py +120 -57
  22. slidge/core/gateway/vcard_temp.py +7 -5
  23. slidge/core/mixins/__init__.py +11 -1
  24. slidge/core/mixins/attachment.py +32 -14
  25. slidge/core/mixins/avatar.py +90 -25
  26. slidge/core/mixins/base.py +8 -2
  27. slidge/core/mixins/db.py +18 -0
  28. slidge/core/mixins/disco.py +0 -10
  29. slidge/core/mixins/message.py +18 -8
  30. slidge/core/mixins/message_maker.py +17 -9
  31. slidge/core/mixins/presence.py +17 -4
  32. slidge/core/pubsub.py +54 -220
  33. slidge/core/session.py +69 -34
  34. slidge/db/__init__.py +4 -0
  35. slidge/db/alembic/env.py +64 -0
  36. slidge/db/alembic/script.py.mako +26 -0
  37. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  38. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  39. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  40. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  41. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  42. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  43. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
  44. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  45. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  46. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  47. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  48. slidge/db/avatar.py +235 -0
  49. slidge/db/meta.py +65 -0
  50. slidge/db/models.py +375 -0
  51. slidge/db/store.py +1078 -0
  52. slidge/group/archive.py +58 -14
  53. slidge/group/bookmarks.py +72 -57
  54. slidge/group/participant.py +87 -28
  55. slidge/group/room.py +369 -211
  56. slidge/main.py +201 -0
  57. slidge/migration.py +30 -0
  58. slidge/slixfix/__init__.py +35 -2
  59. slidge/slixfix/roster.py +11 -4
  60. slidge/slixfix/xep_0292/vcard4.py +3 -0
  61. slidge/util/archive_msg.py +2 -1
  62. slidge/util/db.py +1 -47
  63. slidge/util/test.py +71 -4
  64. slidge/util/types.py +29 -4
  65. slidge/util/util.py +22 -0
  66. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
  67. slidge-0.2.0a1.dist-info/RECORD +114 -0
  68. slidge/core/cache.py +0 -183
  69. slidge/util/schema.sql +0 -126
  70. slidge/util/sql.py +0 -508
  71. slidge-0.1.3.dist-info/RECORD +0 -96
  72. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  73. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  74. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
slidge/core/pubsub.py CHANGED
@@ -1,12 +1,8 @@
1
- import hashlib
2
- import io
3
1
  import logging
4
2
  from copy import copy
5
3
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Optional, Type, Union
4
+ from typing import TYPE_CHECKING, Optional, Union
7
5
 
8
- from PIL import Image, UnidentifiedImageError
9
- from PIL.Image import Image as PILImage
10
6
  from slixmpp import (
11
7
  JID,
12
8
  CoroutineCallback,
@@ -24,12 +20,8 @@ from slixmpp.plugins.xep_0172 import UserNick
24
20
  from slixmpp.plugins.xep_0292.stanza import VCard4
25
21
  from slixmpp.types import JidStr, OptJidStr
26
22
 
27
- from ..contact.contact import LegacyContact
28
- from ..contact.roster import ContactIsUser
29
- from ..util.db import GatewayUser, user_store
30
- from ..util.sql import db
31
- from ..util.types import AvatarType, LegacyFileIdType, PepItemType
32
- from .cache import CachedAvatar, avatar_cache
23
+ from ..db.avatar import CachedAvatar, avatar_cache
24
+ from ..db.store import ContactStore, SlidgeStore
33
25
  from .mixins.lock import NamedLockMixin
34
26
 
35
27
  if TYPE_CHECKING:
@@ -39,19 +31,15 @@ VCARD4_NAMESPACE = "urn:xmpp:vcard4"
39
31
 
40
32
 
41
33
  class PepItem:
42
- @staticmethod
43
- def from_db(jid: JID, user: Optional[GatewayUser] = None) -> Optional["PepItem"]:
44
- raise NotImplementedError
45
-
46
- def to_db(self, jid: JID, user: Optional[GatewayUser] = None):
47
- raise NotImplementedError
34
+ pass
48
35
 
49
36
 
50
37
  class PepAvatar(PepItem):
51
- def __init__(self, jid: JID):
38
+ store: SlidgeStore
39
+
40
+ def __init__(self):
52
41
  self.metadata: Optional[AvatarMetadata] = None
53
42
  self.id: Optional[str] = None
54
- self.jid = jid
55
43
  self._avatar_data_path: Optional[Path] = None
56
44
 
57
45
  @property
@@ -62,63 +50,7 @@ class PepAvatar(PepItem):
62
50
  data.set_value(self._avatar_data_path.read_bytes())
63
51
  return data
64
52
 
65
- @staticmethod
66
- def _sha(b: bytes):
67
- return hashlib.sha1(b).hexdigest()
68
-
69
- def _get_weak_unique_id(self, avatar: AvatarType):
70
- if isinstance(avatar, str):
71
- return avatar # url
72
- elif isinstance(avatar, bytes):
73
- return self._sha(avatar)
74
- elif isinstance(avatar, Path):
75
- return self._sha(avatar.read_bytes())
76
-
77
- @staticmethod
78
- async def _get_image(avatar: AvatarType) -> PILImage:
79
- if isinstance(avatar, str):
80
- # async with aiohttp.ClientSession() as session:
81
- async with avatar_cache.http.get(avatar) as response:
82
- return Image.open(io.BytesIO(await response.read()))
83
- elif isinstance(avatar, bytes):
84
- return Image.open(io.BytesIO(avatar))
85
- elif isinstance(avatar, Path):
86
- return Image.open(avatar)
87
- else:
88
- raise TypeError("Avatar must be bytes, a Path or a str (URL)", avatar)
89
-
90
- async def set_avatar(
91
- self, avatar: AvatarType, unique_id: Optional[LegacyFileIdType] = None
92
- ):
93
- if unique_id is None:
94
- if isinstance(avatar, str):
95
- await self._set_avatar_from_url_alone(avatar)
96
- return
97
- unique_id = self._get_weak_unique_id(avatar)
98
-
99
- await self._set_avatar_from_unique_id(avatar, unique_id)
100
-
101
- async def _set_avatar_from_unique_id(
102
- self, avatar: AvatarType, unique_id: LegacyFileIdType
103
- ):
104
- cached_avatar = avatar_cache.get(unique_id)
105
- if cached_avatar:
106
- # this shouldn't be necessary but here to re-use avatars downloaded
107
- # before the change introducing the JID to unique ID mapping
108
- avatar_cache.store_jid(self.jid, unique_id)
109
- else:
110
- img = await self._get_image(avatar)
111
- cached_avatar = await avatar_cache.convert_and_store(
112
- img, unique_id, self.jid
113
- )
114
-
115
- self._set_avatar_from_cache(cached_avatar)
116
-
117
- async def _set_avatar_from_url_alone(self, url: str):
118
- cached_avatar = await avatar_cache.get_avatar_from_url_alone(url, self.jid)
119
- self._set_avatar_from_cache(cached_avatar)
120
-
121
- def _set_avatar_from_cache(self, cached_avatar: CachedAvatar):
53
+ def set_avatar_from_cache(self, cached_avatar: CachedAvatar):
122
54
  metadata = AvatarMetadata()
123
55
  self.id = cached_avatar.hash
124
56
  metadata.add_info(
@@ -131,31 +63,10 @@ class PepAvatar(PepItem):
131
63
  self.metadata = metadata
132
64
  self._avatar_data_path = cached_avatar.path
133
65
 
134
- @staticmethod
135
- def from_db(jid: JID, user: Optional[GatewayUser] = None) -> Optional["PepAvatar"]:
136
- cached_id = db.avatar_get(jid)
137
- if cached_id is None:
138
- return None
139
- item = PepAvatar(jid)
140
- cached_avatar = avatar_cache.get(cached_id)
141
- if cached_avatar is None:
142
- raise XMPPError("internal-server-error")
143
- item._set_avatar_from_cache(cached_avatar)
144
- return item
145
-
146
- def to_db(self, jid: JID, user=None):
147
- cached_id = avatar_cache.get_cached_id_for(jid)
148
- if cached_id is None:
149
- log.warning("Could not store avatar for %s", jid)
150
- return
151
- db.avatar_store(jid, cached_id)
152
-
153
- @staticmethod
154
- def remove_from_db(jid: JID):
155
- db.avatar_delete(jid)
156
-
157
66
 
158
67
  class PepNick(PepItem):
68
+ contact_store: ContactStore
69
+
159
70
  def __init__(self, nick: Optional[str] = None):
160
71
  nickname = UserNick()
161
72
  if nick is not None:
@@ -163,20 +74,6 @@ class PepNick(PepItem):
163
74
  self.nick = nickname
164
75
  self.__nick_str = nick
165
76
 
166
- @staticmethod
167
- def from_db(jid: JID, user: Optional[GatewayUser] = None) -> Optional["PepNick"]:
168
- if user is None:
169
- raise XMPPError("not-allowed")
170
- nick = db.nick_get(jid, user)
171
- if nick is None:
172
- return None
173
- return PepNick(nick)
174
-
175
- def to_db(self, jid: JID, user: Optional[GatewayUser] = None):
176
- if user is None:
177
- raise XMPPError("not-allowed")
178
- db.nick_store(jid, str(self.__nick_str), user)
179
-
180
77
 
181
78
  class PubSubComponent(NamedLockMixin, BasePlugin):
182
79
  xmpp: "BaseGateway"
@@ -186,7 +83,6 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
186
83
  dependencies = {
187
84
  "xep_0030",
188
85
  "xep_0060",
189
- # "xep_0084",
190
86
  "xep_0115",
191
87
  "xep_0163",
192
88
  }
@@ -276,7 +172,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
276
172
  else:
277
173
  if pep_avatar.metadata is None:
278
174
  raise XMPPError("internal-server-error", "Avatar but no metadata?")
279
- await self._broadcast(
175
+ await self.__broadcast(
280
176
  data=pep_avatar.metadata,
281
177
  from_=p.get_to(),
282
178
  to=from_,
@@ -288,7 +184,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
288
184
  except XMPPError:
289
185
  pass
290
186
  else:
291
- await self._broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_)
187
+ await self.__broadcast(data=pep_nick.nick, from_=p.get_to(), to=from_)
292
188
 
293
189
  if VCARD4_NAMESPACE + "+notify" in features:
294
190
  await self.broadcast_vcard_event(p.get_to(), to=from_)
@@ -303,7 +199,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
303
199
  # but movim expects it to be here, and I guess
304
200
 
305
201
  log.debug("Broadcast vcard4 event: %s", vcard)
306
- await self._broadcast(
202
+ await self.__broadcast(
307
203
  data=vcard,
308
204
  from_=JID(from_).bare,
309
205
  to=to,
@@ -311,26 +207,34 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
311
207
  node=VCARD4_NAMESPACE,
312
208
  )
313
209
 
314
- async def _get_authorized_item(
315
- self, cls: Type[PepItemType], stanza: Union[Iq, Presence]
316
- ) -> PepItemType:
317
- sto = stanza.get_to()
318
- user = user_store.get_by_jid(stanza.get_from())
319
- item = cls.from_db(sto, user)
320
- if item is None:
321
- raise XMPPError("item-not-found")
322
-
323
- if sto != self.xmpp.boundjid.bare:
324
- session = self.xmpp.get_session_from_stanza(stanza)
325
- await session.contacts.by_jid(sto)
326
-
327
- return item # type:ignore
328
-
329
210
  async def _get_authorized_avatar(self, stanza: Union[Iq, Presence]) -> PepAvatar:
330
- return await self._get_authorized_item(PepAvatar, stanza)
211
+ if stanza.get_to() == self.xmpp.boundjid.bare:
212
+ item = PepAvatar()
213
+ item.set_avatar_from_cache(avatar_cache.get_by_pk(self.xmpp.avatar_pk))
214
+ return item
215
+
216
+ session = self.xmpp.get_session_from_stanza(stanza)
217
+ entity = await session.get_contact_or_group_or_participant(stanza.get_to())
218
+
219
+ item = PepAvatar()
220
+ avatar_id = entity.avatar_id
221
+ if avatar_id is not None:
222
+ stored = avatar_cache.get(str(avatar_id))
223
+ assert stored is not None
224
+ item.set_avatar_from_cache(stored)
225
+ return item
331
226
 
332
227
  async def _get_authorized_nick(self, stanza: Union[Iq, Presence]) -> PepNick:
333
- return await self._get_authorized_item(PepNick, stanza)
228
+ if stanza.get_to() == self.xmpp.boundjid.bare:
229
+ return PepNick(self.xmpp.COMPONENT_NAME)
230
+
231
+ session = self.xmpp.get_session_from_stanza(stanza)
232
+ entity = await session.contacts.by_jid(stanza.get_to())
233
+
234
+ if entity.name is not None:
235
+ return PepNick(entity.name)
236
+ else:
237
+ return PepNick()
334
238
 
335
239
  async def _get_avatar_data(self, iq: Iq):
336
240
  pep_avatar = await self._get_authorized_avatar(iq)
@@ -372,10 +276,6 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
372
276
  raise XMPPError("item-not-found")
373
277
  self._reply_with_payload(iq, vcard, "current", VCARD4_NAMESPACE)
374
278
 
375
- @staticmethod
376
- def get_avatar(jid: JID):
377
- return PepAvatar.from_db(jid)
378
-
379
279
  @staticmethod
380
280
  def _reply_with_payload(
381
281
  iq: Iq,
@@ -394,7 +294,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
394
294
  result["pubsub"]["items"].append(item)
395
295
  result.send()
396
296
 
397
- async def _broadcast(self, data, from_: JidStr, to: OptJidStr = None, **kwargs):
297
+ async def __broadcast(self, data, from_: JidStr, to: OptJidStr = None, **kwargs):
398
298
  from_ = JID(from_)
399
299
  if from_ != self.xmpp.boundjid.bare and to is not None:
400
300
  to = JID(to)
@@ -402,12 +302,6 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
402
302
  if session is None:
403
303
  return
404
304
  await session.ready
405
- try:
406
- entity = await session.get_contact_or_group_or_participant(from_)
407
- except ContactIsUser:
408
- return
409
- if isinstance(entity, LegacyContact) and not entity.is_friend:
410
- return
411
305
 
412
306
  item = EventItem()
413
307
  if data:
@@ -428,97 +322,37 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
428
322
  msg.append(event)
429
323
 
430
324
  if to is None:
431
- for u in user_store.get_all():
325
+ for u in self.xmpp.store.users.get_all():
432
326
  new_msg = copy(msg)
433
- new_msg.set_to(u.bare_jid)
327
+ new_msg.set_to(u.jid.bare)
434
328
  new_msg.send()
435
329
  else:
436
330
  msg.set_to(to)
437
331
  msg.send()
438
332
 
439
- async def set_avatar(
440
- self,
441
- jid: JidStr,
442
- avatar: Optional[AvatarType] = None,
443
- broadcast_to: OptJidStr = None,
444
- unique_id=None,
445
- broadcast=True,
446
- ):
447
- jid = JID(jid)
448
- if avatar is None:
449
- PepAvatar.remove_from_db(jid)
450
- await self._broadcast(AvatarMetadata(), jid, broadcast_to)
451
- avatar_cache.delete_jid(jid)
452
- else:
453
- pep_avatar = PepAvatar(jid)
454
- try:
455
- await pep_avatar.set_avatar(avatar, unique_id)
456
- except (UnidentifiedImageError, FileNotFoundError) as e:
457
- log.warning("Failed to set avatar for %s: %r", self, e)
458
- return
459
- pep_avatar.to_db(jid)
460
- if pep_avatar.metadata is None:
461
- raise RuntimeError
462
- if not broadcast:
463
- return
464
- await self._broadcast(
465
- pep_avatar.metadata,
466
- jid,
467
- broadcast_to,
468
- id=pep_avatar.metadata["info"]["id"],
469
- )
470
-
471
- async def set_avatar_from_cache(
472
- self, jid: JID, send_empty: bool, broadcast_to: OptJidStr = None, broadcast=True
473
- ):
474
- uid = avatar_cache.get_cached_id_for(jid)
475
- if uid is None:
476
- if not send_empty:
477
- return
478
- self.xmpp.loop.create_task(
479
- self.set_avatar(jid, None, broadcast_to, uid, broadcast)
480
- )
481
- return
482
- cached_avatar = avatar_cache.get(str(uid))
333
+ async def broadcast_avatar(
334
+ self, from_: JidStr, to: JidStr, cached_avatar: Optional[CachedAvatar]
335
+ ) -> None:
483
336
  if cached_avatar is None:
484
- # should not happen but well…
485
- log.warning(
486
- "Something is wrong with the avatar, %s won't have an "
487
- "avatar because avatar not found in cache",
488
- jid,
337
+ await self.__broadcast(AvatarMetadata(), from_, to)
338
+ else:
339
+ pep_avatar = PepAvatar()
340
+ pep_avatar.set_avatar_from_cache(cached_avatar)
341
+ assert pep_avatar.metadata is not None
342
+ await self.__broadcast(
343
+ pep_avatar.metadata, from_, to, id=pep_avatar.metadata["info"]["id"]
489
344
  )
490
- return
491
- self.xmpp.loop.create_task(
492
- self.set_avatar(jid, cached_avatar.path, broadcast_to, uid, broadcast)
493
- )
494
345
 
495
- def set_nick(
346
+ def broadcast_nick(
496
347
  self,
497
- user: GatewayUser,
348
+ user_jid: JID,
498
349
  jid: JidStr,
499
350
  nick: Optional[str] = None,
500
351
  ):
501
352
  jid = JID(jid)
502
353
  nickname = PepNick(nick)
503
- nickname.to_db(jid, user)
504
354
  log.debug("New nickname: %s", nickname.nick)
505
- self.xmpp.loop.create_task(self._broadcast(nickname.nick, jid, user.bare_jid))
506
-
507
- async def broadcast_all(self, from_: JID, to: JID):
508
- """
509
- Force push avatar and nick for a stored JID.
510
- """
511
- a = PepAvatar.from_db(from_)
512
- if a:
513
- if a.metadata:
514
- await self._broadcast(
515
- a.metadata, from_, to, id=a.metadata["info"]["id"]
516
- )
517
- else:
518
- log.warning("No metadata associated to this cached avatar?!")
519
- n = PepNick.from_db(from_, user_store.get_by_jid(to))
520
- if n:
521
- await self._broadcast(n.nick, from_, to)
355
+ self.xmpp.loop.create_task(self.__broadcast(nickname.nick, jid, user_jid.bare))
522
356
 
523
357
 
524
358
  log = logging.getLogger(__name__)
slidge/core/session.py CHANGED
@@ -18,11 +18,10 @@ from slixmpp.types import PresenceShows
18
18
 
19
19
  from ..command import SearchResult
20
20
  from ..contact import LegacyContact, LegacyRoster
21
+ from ..db.models import GatewayUser
21
22
  from ..group.bookmarks import LegacyBookmarks
22
23
  from ..group.room import LegacyMUC
23
24
  from ..util import ABCSubclassableOnceAtMost
24
- from ..util.db import GatewayUser, user_store
25
- from ..util.sql import SQLBiDict
26
25
  from ..util.types import (
27
26
  LegacyGroupIdType,
28
27
  LegacyMessageType,
@@ -92,16 +91,10 @@ class BaseSession(
92
91
  """
93
92
 
94
93
  def __init__(self, user: GatewayUser):
95
- self.log = logging.getLogger(user.bare_jid)
94
+ self.log = logging.getLogger(user.jid.bare)
96
95
 
97
- self.user = user
98
- self.sent = SQLBiDict[LegacyMessageType, str](
99
- "session_message_sent", "legacy_id", "xmpp_id", self.user
100
- )
101
- # message ids (*not* stanza-ids), needed for last msg correction
102
- self.muc_sent_msg_ids = SQLBiDict[LegacyMessageType, str](
103
- "session_message_sent_muc", "legacy_id", "xmpp_id", self.user
104
- )
96
+ self.user_jid = user.jid
97
+ self.user_pk = user.id
105
98
 
106
99
  self.ignore_messages = set[str]()
107
100
 
@@ -115,26 +108,26 @@ class BaseSession(
115
108
 
116
109
  self.http = self.xmpp.http
117
110
 
118
- self.threads = SQLBiDict[str, LegacyThreadType]( # type:ignore
119
- "session_thread_sent_muc", "legacy_id", "xmpp_id", self.user
120
- )
121
111
  self.thread_creation_lock = asyncio.Lock()
122
112
 
123
113
  self.__cached_presence: Optional[CachedPresence] = None
124
114
 
125
- self.avatar_hash: Optional[str] = None
126
-
127
115
  self.__tasks = set[asyncio.Task]()
128
116
 
117
+ @property
118
+ def user(self) -> GatewayUser:
119
+ return self.xmpp.store.users.get(self.user_jid) # type:ignore
120
+
129
121
  def __remove_task(self, fut):
130
122
  self.log.debug("Removing fut %s", fut)
131
123
  self.__tasks.remove(fut)
132
124
 
133
- def create_task(self, coro) -> None:
125
+ def create_task(self, coro) -> asyncio.Task:
134
126
  task = self.xmpp.loop.create_task(coro)
135
127
  self.__tasks.add(task)
136
128
  self.log.debug("Creating task %s", task)
137
129
  task.add_done_callback(lambda _: self.__remove_task(task))
130
+ return task
138
131
 
139
132
  def cancel_all_tasks(self):
140
133
  for task in self.__tasks:
@@ -488,6 +481,17 @@ class BaseSession(
488
481
  """
489
482
  await muc.on_set_affiliation(contact, "member", reason, None)
490
483
 
484
+ async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType):
485
+ """
486
+ Triggered when the user leaves a group via the dedicated slidge command
487
+ or the :xep:`0077` ``<remove />`` mechanism.
488
+
489
+ This should be interpreted as definitely leaving the group.
490
+
491
+ :param muc_legacy_id: The legacy ID of the group to leave
492
+ """
493
+ raise NotImplementedError
494
+
491
495
  def __reset_ready(self):
492
496
  self.ready = self.xmpp.loop.create_future()
493
497
 
@@ -507,7 +511,7 @@ class BaseSession(
507
511
  self.ready.set_result(True)
508
512
 
509
513
  def __repr__(self):
510
- return f"<Session of {self.user}>"
514
+ return f"<Session of {self.user_jid}>"
511
515
 
512
516
  def shutdown(self) -> asyncio.Task:
513
517
  for c in self.contacts:
@@ -571,9 +575,9 @@ class BaseSession(
571
575
  log.debug("user not found", stack_info=True)
572
576
  raise XMPPError(text="User not found", condition="subscription-required")
573
577
 
574
- session = _sessions.get(user)
578
+ session = _sessions.get(user.jid.bare)
575
579
  if session is None:
576
- _sessions[user] = session = cls(user)
580
+ _sessions[user.jid.bare] = session = cls(user)
577
581
  return session
578
582
 
579
583
  @classmethod
@@ -590,7 +594,7 @@ class BaseSession(
590
594
  # :param s:
591
595
  # :return:
592
596
  # """
593
- return cls._from_user_or_none(user_store.get_by_stanza(s))
597
+ return cls.from_jid(s.get_from())
594
598
 
595
599
  @classmethod
596
600
  def from_jid(cls, jid: JID) -> "BaseSession":
@@ -602,7 +606,11 @@ class BaseSession(
602
606
  # :param jid:
603
607
  # :return:
604
608
  # """
605
- return cls._from_user_or_none(user_store.get_by_jid(jid))
609
+ session = _sessions.get(jid.bare)
610
+ if session is not None:
611
+ return session
612
+ user = cls.xmpp.store.users.get(jid)
613
+ return cls._from_user_or_none(user)
606
614
 
607
615
  @classmethod
608
616
  async def kill_by_jid(cls, jid: JID):
@@ -615,16 +623,21 @@ class BaseSession(
615
623
  # :return:
616
624
  # """
617
625
  log.debug("Killing session of %s", jid)
618
- for user, session in _sessions.items():
619
- if user.jid == jid.bare:
626
+ for user_jid, session in _sessions.items():
627
+ if user_jid == jid.bare:
620
628
  break
621
629
  else:
622
630
  log.debug("Did not find a session for %s", jid)
623
631
  return
624
632
  for c in session.contacts:
625
633
  c.unsubscribe()
634
+ user = cls.xmpp.store.users.get(jid)
635
+ if user is None:
636
+ log.warning("User not found during unregistration")
637
+ return
626
638
  await cls.xmpp.unregister(user)
627
- del _sessions[user]
639
+ cls.xmpp.store.users.delete(user.jid)
640
+ del _sessions[user.jid.bare]
628
641
  del user
629
642
  del session
630
643
 
@@ -649,7 +662,7 @@ class BaseSession(
649
662
  """
650
663
  self.__cached_presence = CachedPresence(status, show, kwargs)
651
664
  self.xmpp.send_presence(
652
- pto=self.user.bare_jid, pstatus=status, pshow=show, **kwargs
665
+ pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs
653
666
  )
654
667
 
655
668
  def send_cached_presence(self, to: JID):
@@ -671,7 +684,7 @@ class BaseSession(
671
684
 
672
685
  :param text: A text
673
686
  """
674
- self.xmpp.send_text(text, mto=self.user.jid, **msg_kwargs)
687
+ self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs)
675
688
 
676
689
  def send_gateway_invite(
677
690
  self,
@@ -686,7 +699,7 @@ class BaseSession(
686
699
  :param reason:
687
700
  :param password:
688
701
  """
689
- self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user.jid)
702
+ self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid)
690
703
 
691
704
  async def input(self, text: str, **msg_kwargs):
692
705
  """
@@ -698,7 +711,7 @@ class BaseSession(
698
711
  :param msg_kwargs: Extra attributes
699
712
  :return:
700
713
  """
701
- return await self.xmpp.input(self.user.jid, text, **msg_kwargs)
714
+ return await self.xmpp.input(self.user_jid, text, **msg_kwargs)
702
715
 
703
716
  async def send_qr(self, text: str):
704
717
  """
@@ -707,7 +720,7 @@ class BaseSession(
707
720
 
708
721
  :param text: Text to encode as a QR code
709
722
  """
710
- await self.xmpp.send_qr(text, mto=self.user.jid)
723
+ await self.xmpp.send_qr(text, mto=self.user_jid)
711
724
 
712
725
  def re_login(self):
713
726
  # Logout then re-login
@@ -715,14 +728,17 @@ class BaseSession(
715
728
  # No reason to override this
716
729
  self.xmpp.re_login(self)
717
730
 
718
- async def get_contact_or_group_or_participant(self, jid: JID):
731
+ async def get_contact_or_group_or_participant(self, jid: JID, create=True):
719
732
  if jid.bare in (contacts := self.contacts.known_contacts(only_friends=False)):
720
733
  return contacts[jid.bare]
721
- if jid.bare in (mucs := self.bookmarks._mucs_by_bare_jid):
722
- return await self.__get_muc_or_participant(mucs[jid.bare], jid)
734
+ if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None:
735
+ return await self.__get_muc_or_participant(muc, jid)
723
736
  else:
724
737
  muc = None
725
738
 
739
+ if not create:
740
+ return None
741
+
726
742
  try:
727
743
  return await self.contacts.by_jid(jid)
728
744
  except XMPPError:
@@ -763,6 +779,25 @@ class BaseSession(
763
779
  "Legacy session is not fully initialized, retry later",
764
780
  )
765
781
 
782
+ def legacy_module_data_update(self, data: dict):
783
+ with self.xmpp.store.session():
784
+ user = self.user
785
+ user.legacy_module_data.update(data)
786
+ self.xmpp.store.users.update(user)
787
+
788
+ def legacy_module_data_set(self, data: dict):
789
+ with self.xmpp.store.session():
790
+ user = self.user
791
+ user.legacy_module_data = data
792
+ self.xmpp.store.users.update(user)
793
+
794
+ def legacy_module_data_clear(self):
795
+ with self.xmpp.store.session():
796
+ user = self.user
797
+ user.legacy_module_data.clear()
798
+ self.xmpp.store.users.update(user)
799
+
766
800
 
767
- _sessions: dict[GatewayUser, BaseSession] = {}
801
+ # keys = user.jid.bare
802
+ _sessions: dict[str, BaseSession] = {}
768
803
  log = logging.getLogger(__name__)
slidge/db/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .models import GatewayUser
2
+ from .store import SlidgeStore
3
+
4
+ __all__ = ("GatewayUser", "SlidgeStore")