slidge 0.1.3__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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")