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
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Optional
5
5
 
6
6
  from slixmpp import JID
7
7
 
8
+ from ...db.avatar import CachedAvatar, avatar_cache
8
9
  from ...util.types import (
9
10
  URL,
10
11
  AnyBaseSession,
@@ -12,7 +13,6 @@ from ...util.types import (
12
13
  AvatarType,
13
14
  LegacyFileIdType,
14
15
  )
15
- from ..cache import avatar_cache
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from ..pubsub import PepAvatar
@@ -28,13 +28,14 @@ class AvatarMixin:
28
28
 
29
29
  jid: JID = NotImplemented
30
30
  session: AnyBaseSession = NotImplemented
31
- _avatar_pubsub_broadcast: bool = NotImplemented
32
31
  _avatar_bare_jid: bool = NotImplemented
33
32
 
34
33
  def __init__(self) -> None:
35
34
  super().__init__()
36
35
  self._set_avatar_task: Optional[Task] = None
36
+ self.__broadcast_task: Optional[Task] = None
37
37
  self.__avatar_unique_id: Optional[AvatarIdType] = None
38
+ self._avatar_pk: Optional[int] = None
38
39
 
39
40
  @property
40
41
  def __avatar_jid(self):
@@ -84,17 +85,42 @@ class AvatarMixin:
84
85
  return None
85
86
  raise TypeError("Bad avatar", a)
86
87
 
87
- async def __set_avatar(self, a: Optional[AvatarType], uid: Optional[AvatarIdType]):
88
+ async def __set_avatar(
89
+ self, a: Optional[AvatarType], uid: Optional[AvatarIdType], delete: bool
90
+ ):
88
91
  self.__avatar_unique_id = uid
89
- await self.session.xmpp.pubsub.set_avatar(
90
- jid=self.__avatar_jid,
91
- avatar=a,
92
- unique_id=None if isinstance(uid, URL) else uid,
93
- broadcast_to=self.session.user.jid.bare,
94
- broadcast=self._avatar_pubsub_broadcast,
95
- )
92
+
93
+ if a is None:
94
+ cached_avatar = None
95
+ self._avatar_pk = None
96
+ else:
97
+ try:
98
+ cached_avatar = await avatar_cache.convert_or_get(
99
+ URL(a) if isinstance(a, URL) else a,
100
+ None if isinstance(uid, URL) else uid,
101
+ )
102
+ except Exception as e:
103
+ self.session.log.error("Failed to set avatar %s", a, exc_info=e)
104
+ self._avatar_pk = None
105
+ self.__avatar_unique_id = uid
106
+ return
107
+ self._avatar_pk = cached_avatar.pk
108
+
109
+ if self.__should_pubsub_broadcast():
110
+ await self.session.xmpp.pubsub.broadcast_avatar(
111
+ self.__avatar_jid, self.session.user_jid, cached_avatar
112
+ )
113
+
114
+ if delete and isinstance(a, Path):
115
+ a.unlink()
116
+
96
117
  self._post_avatar_update()
97
118
 
119
+ def __should_pubsub_broadcast(self):
120
+ return getattr(self, "is_friend", False) and getattr(
121
+ self, "added_to_roster", False
122
+ )
123
+
98
124
  async def _no_change(self, a: Optional[AvatarType], uid: Optional[AvatarIdType]):
99
125
  if a is None:
100
126
  return self.__avatar_unique_id is None
@@ -103,23 +129,27 @@ class AvatarMixin:
103
129
  if isinstance(uid, URL):
104
130
  if self.__avatar_unique_id != uid:
105
131
  return False
106
- return not await avatar_cache.url_has_changed(uid)
132
+ return not await avatar_cache.url_modified(uid)
107
133
  return self.__avatar_unique_id == uid
108
134
 
109
135
  async def set_avatar(
110
136
  self,
111
137
  a: Optional[AvatarType],
112
138
  avatar_unique_id: Optional[LegacyFileIdType] = None,
139
+ delete: bool = False,
113
140
  blocking=False,
114
141
  cancel=True,
115
142
  ) -> None:
116
143
  """
117
144
  Set an avatar for this entity
118
145
 
119
- :param a:
120
- :param avatar_unique_id:
121
- :param blocking:
122
- :param cancel:
146
+ :param a: The avatar, in one of the types slidge supports
147
+ :param avatar_unique_id: A globally unique ID for the avatar on the
148
+ legacy network
149
+ :param delete: If the avatar is provided as a Path, whether to delete
150
+ it once used or not.
151
+ :param blocking: Internal use by slidge for tests, do not use!
152
+ :param cancel: Internal use by slidge, do not use!
123
153
  """
124
154
  if avatar_unique_id is None and a is not None:
125
155
  avatar_unique_id = self.__get_uid(a)
@@ -128,7 +158,7 @@ class AvatarMixin:
128
158
  if cancel and self._set_avatar_task:
129
159
  self._set_avatar_task.cancel()
130
160
  awaitable = create_task(
131
- self.__set_avatar(a, avatar_unique_id),
161
+ self.__set_avatar(a, avatar_unique_id, delete),
132
162
  name=f"Set pubsub avatar of {self}",
133
163
  )
134
164
  if not self._set_avatar_task or self._set_avatar_task.done():
@@ -136,32 +166,67 @@ class AvatarMixin:
136
166
  if blocking:
137
167
  await awaitable
138
168
 
139
- def get_avatar(self) -> Optional["PepAvatar"]:
169
+ def get_cached_avatar(self) -> Optional["CachedAvatar"]:
140
170
  if not self.__avatar_unique_id:
141
171
  return None
142
- return self.session.xmpp.pubsub.get_avatar(self.__avatar_jid)
172
+ return avatar_cache.get(self.__avatar_unique_id)
173
+
174
+ def get_avatar(self) -> Optional["PepAvatar"]:
175
+ cached_avatar = self.get_cached_avatar()
176
+ if cached_avatar is None:
177
+ return None
178
+ from ..pubsub import PepAvatar
179
+
180
+ item = PepAvatar()
181
+ item.set_avatar_from_cache(cached_avatar)
182
+ return item
143
183
 
144
184
  def _post_avatar_update(self) -> None:
145
185
  return
146
186
 
187
+ def __get_cached_avatar_id(self):
188
+ i = self._get_cached_avatar_id()
189
+ if i is None:
190
+ return None
191
+ return self.session.xmpp.AVATAR_ID_TYPE(i)
192
+
193
+ def _get_cached_avatar_id(self) -> Optional[str]:
194
+ raise NotImplementedError
195
+
147
196
  async def avatar_wrap_update_info(self):
148
- cached_id = avatar_cache.get_cached_id_for(self.__avatar_jid)
197
+ cached_id = self.__get_cached_avatar_id()
149
198
  self.__avatar_unique_id = cached_id
150
199
  try:
151
200
  await self.update_info() # type:ignore
152
201
  except NotImplementedError:
153
202
  return
154
203
  new_id = self.avatar
155
- if isinstance(new_id, URL) and not await avatar_cache.url_has_changed(new_id):
204
+ if isinstance(new_id, URL) and not await avatar_cache.url_modified(new_id):
156
205
  return
157
206
  elif new_id != cached_id:
158
207
  # at this point it means that update_info set the avatar, and we don't
159
208
  # need to do anything else
160
209
  return
161
210
 
162
- await self.session.xmpp.pubsub.set_avatar_from_cache(
163
- self.__avatar_jid,
164
- new_id is None and cached_id is not None,
165
- self.session.user.jid.bare,
166
- self._avatar_pubsub_broadcast,
211
+ if self.__should_pubsub_broadcast():
212
+ if new_id is None and cached_id is None:
213
+ return
214
+ cached_avatar = avatar_cache.get(cached_id)
215
+ self.__broadcast_task = self.session.xmpp.loop.create_task(
216
+ self.session.xmpp.pubsub.broadcast_avatar(
217
+ self.__avatar_jid, self.session.user_jid, cached_avatar
218
+ )
219
+ )
220
+
221
+ def _set_avatar_from_store(self, stored):
222
+ if stored.avatar_id is None:
223
+ return
224
+ if stored.avatar is None:
225
+ # seems to happen after avatar cleanup for some reason?
226
+ self.__avatar_unique_id = None
227
+ return
228
+ self.__avatar_unique_id = (
229
+ stored.avatar.legacy_id
230
+ if stored.avatar.legacy_id is not None
231
+ else URL(stored.avatar.url)
167
232
  )
@@ -8,7 +8,6 @@ from ...util.types import MessageOrPresenceTypeVar
8
8
  if TYPE_CHECKING:
9
9
  from slidge.core.gateway import BaseGateway
10
10
  from slidge.core.session import BaseSession
11
- from slidge.util.db import GatewayUser
12
11
 
13
12
 
14
13
  class MetaBase(ABCMeta):
@@ -18,11 +17,18 @@ class MetaBase(ABCMeta):
18
17
  class Base:
19
18
  session: "BaseSession" = NotImplemented
20
19
  xmpp: "BaseGateway" = NotImplemented
21
- user: "GatewayUser" = NotImplemented
22
20
 
23
21
  jid: JID = NotImplemented
24
22
  name: str = NotImplemented
25
23
 
24
+ @property
25
+ def user_jid(self):
26
+ return self.session.user_jid
27
+
28
+ @property
29
+ def user_pk(self):
30
+ return self.session.user_pk
31
+
26
32
 
27
33
  class BaseSender(Base):
28
34
  def _send(
@@ -0,0 +1,18 @@
1
+ from contextlib import contextmanager
2
+
3
+
4
+ class UpdateInfoMixin:
5
+ """
6
+ This mixin just adds a context manager that prevents commiting to the DB
7
+ on every attribute change.
8
+ """
9
+
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+ self._updating_info = False
13
+
14
+ @contextmanager
15
+ def updating_info(self):
16
+ self._updating_info = True
17
+ yield
18
+ self._updating_info = False
@@ -13,10 +13,6 @@ class BaseDiscoMixin(Base):
13
13
  DISCO_NAME: str = NotImplemented
14
14
  DISCO_LANG = None
15
15
 
16
- def __init__(self):
17
- super().__init__()
18
- self.__caps_cache: Optional[str] = None
19
-
20
16
  def _get_disco_name(self):
21
17
  if self.DISCO_NAME is NotImplemented:
22
18
  return self.xmpp.COMPONENT_NAME
@@ -44,17 +40,11 @@ class BaseDiscoMixin(Base):
44
40
  return info
45
41
 
46
42
  async def get_caps_ver(self, jid: OptJid = None, node: Optional[str] = None):
47
- if self.__caps_cache:
48
- return self.__caps_cache
49
43
  info = await self.get_disco_info(jid, node)
50
44
  caps = self.xmpp.plugin["xep_0115"]
51
45
  ver = caps.generate_verstring(info, caps.hash)
52
- self.__caps_cache = ver
53
46
  return ver
54
47
 
55
- def reset_caps_cache(self):
56
- self.__caps_cache = None
57
-
58
48
 
59
49
  class ChatterDiscoMixin(BaseDiscoMixin):
60
50
  AVATAR = True
@@ -111,7 +111,7 @@ class MarkerMixin(MessageMaker):
111
111
  self.xmpp.delivery_receipt.make_ack(
112
112
  self._legacy_to_xmpp(legacy_msg_id),
113
113
  mfrom=self.jid,
114
- mto=self.user.jid,
114
+ mto=self.user_jid,
115
115
  )
116
116
  )
117
117
  self._send(
@@ -144,7 +144,7 @@ class MarkerMixin(MessageMaker):
144
144
  # We'll see if we need to implement that later
145
145
  return
146
146
  xmpp_msg_id = self._legacy_to_xmpp(legacy_msg_id)
147
- iq = Iq(sto=self.user.bare_jid, sfrom=self.user.bare_jid, stype="set")
147
+ iq = Iq(sto=self.user_jid.bare, sfrom=self.user_jid.bare, stype="set")
148
148
  iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0490"].stanza.NS
149
149
  iq["pubsub"]["publish"]["item"]["id"] = muc_jid
150
150
  displayed = self.xmpp["xep_0490"].stanza.Displayed()
@@ -169,8 +169,8 @@ class ContentMessageMixin(AttachmentMixin):
169
169
 
170
170
  def __replace_id(self, legacy_msg_id: LegacyMessageType):
171
171
  if self.mtype == "groupchat":
172
- return self.session.muc_sent_msg_ids.get(
173
- legacy_msg_id
172
+ return self.xmpp.store.sent.get_group_xmpp_id(
173
+ self.session.user_pk, str(legacy_msg_id)
174
174
  ) or self._legacy_to_xmpp(legacy_msg_id)
175
175
  else:
176
176
  return self._legacy_to_xmpp(legacy_msg_id)
@@ -215,14 +215,18 @@ class ContentMessageMixin(AttachmentMixin):
215
215
  but store it in the archive. Meant to be used during ``MUC.backfill()``
216
216
  """
217
217
  if carbon:
218
- if not correction and legacy_msg_id in self.session.sent:
218
+ if not correction and self.xmpp.store.sent.was_sent_by_user(
219
+ self.session.user_pk, str(legacy_msg_id)
220
+ ):
219
221
  log.warning(
220
222
  "Carbon message for a message an XMPP has sent? This is a bug! %s",
221
223
  legacy_msg_id,
222
224
  )
223
225
  return
224
- self.session.sent[legacy_msg_id] = self.session.legacy_to_xmpp_msg_id(
225
- legacy_msg_id
226
+ self.xmpp.store.sent.set_message(
227
+ self.session.user_pk,
228
+ str(legacy_msg_id),
229
+ self.session.legacy_to_xmpp_msg_id(legacy_msg_id),
226
230
  )
227
231
  hints = self.__default_hints(hints)
228
232
  msg = self._make_message(
@@ -237,7 +241,13 @@ class ContentMessageMixin(AttachmentMixin):
237
241
  )
238
242
  if correction:
239
243
  msg["replace"]["id"] = self.__replace_id(legacy_msg_id)
240
- return self._send(msg, archive_only=archive_only, carbon=carbon, **send_kwargs)
244
+ return self._send(
245
+ msg,
246
+ archive_only=archive_only,
247
+ carbon=carbon,
248
+ legacy_msg_id=legacy_msg_id,
249
+ **send_kwargs,
250
+ )
241
251
 
242
252
  def correct(
243
253
  self,
@@ -7,8 +7,8 @@ from uuid import uuid4
7
7
  from slixmpp import Message
8
8
  from slixmpp.types import MessageTypes
9
9
 
10
+ from ...db.models import GatewayUser
10
11
  from ...slixfix.link_preview.stanza import LinkPreview as LinkPreviewStanza
11
- from ...util.db import GatewayUser
12
12
  from ...util.types import (
13
13
  ChatState,
14
14
  LegacyMessageType,
@@ -60,8 +60,9 @@ class MessageMaker(BaseSender):
60
60
  msg["body"] = body
61
61
  state = "active"
62
62
  if thread:
63
- known_threads = self.session.threads.inverse # type:ignore
64
- msg["thread"] = known_threads.get(thread) or str(thread)
63
+ msg["thread"] = self.xmpp.store.sent.get_legacy_thread(
64
+ self.user_pk, str(thread)
65
+ ) or str(thread)
65
66
  if state:
66
67
  msg["chat_state"] = state
67
68
  for hint in hints:
@@ -88,9 +89,9 @@ class MessageMaker(BaseSender):
88
89
  msg["stanza_id"]["by"] = self.muc.jid # type: ignore
89
90
 
90
91
  def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
91
- return self.session.sent.get(legacy_id) or self.session.legacy_to_xmpp_msg_id(
92
- legacy_id
93
- )
92
+ return self.xmpp.store.sent.get_xmpp_id(
93
+ self.session.user_pk, str(legacy_id)
94
+ ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
94
95
 
95
96
  def _add_delay(self, msg: Message, when: Optional[datetime]):
96
97
  if when:
@@ -110,16 +111,23 @@ class MessageMaker(BaseSender):
110
111
  muc = getattr(self, "muc", None)
111
112
 
112
113
  if entity := reply_to.author:
113
- if isinstance(entity, GatewayUser):
114
+ if entity == "user" or isinstance(entity, GatewayUser):
115
+ if isinstance(entity, GatewayUser):
116
+ warnings.warn(
117
+ "Using a GatewayUser as the author of a "
118
+ "MessageReference is deprecated. Use the string 'user' "
119
+ "instead.",
120
+ DeprecationWarning,
121
+ )
114
122
  if muc:
115
123
  jid = copy(muc.jid)
116
124
  jid.resource = fallback_nick = muc.user_nick
117
125
  msg["reply"]["to"] = jid
118
126
  else:
119
- msg["reply"]["to"] = entity.jid
127
+ msg["reply"]["to"] = self.session.user_jid
120
128
  # TODO: here we should use preferably use the PEP nick of the user
121
129
  # (but it doesn't matter much)
122
- fallback_nick = entity.jid.local
130
+ fallback_nick = self.session.user_jid.local
123
131
  else:
124
132
  if muc:
125
133
  if hasattr(entity, "muc"):
@@ -5,7 +5,7 @@ from typing import Optional
5
5
 
6
6
  from slixmpp.types import PresenceShows, PresenceTypes
7
7
 
8
- from ...util.sql import CachedPresence, db
8
+ from ...util.types import CachedPresence
9
9
  from .. import config
10
10
  from .base import BaseSender
11
11
 
@@ -19,20 +19,32 @@ _FRIEND_REQUEST_PRESENCES = {"subscribe", "unsubscribe", "subscribed", "unsubscr
19
19
 
20
20
  class PresenceMixin(BaseSender):
21
21
  _ONLY_SEND_PRESENCE_CHANGES = False
22
+ contact_pk: Optional[int] = None
22
23
 
23
24
  def __init__(self, *a, **k):
24
25
  super().__init__(*a, **k)
26
+ # FIXME: this should not be an attribute of this mixin to allow garbage
27
+ # collection of instances
25
28
  self.__update_last_seen_fallback_task: Optional[Task] = None
29
+ # this is only used when a presence is set during Contact.update_info(),
30
+ # when the contact does not have a DB primary key yet, and is written
31
+ # to DB at the end of update_info()
32
+ self.cached_presence: Optional[CachedPresence] = None
26
33
 
27
34
  async def __update_last_seen_fallback(self):
28
35
  await sleep(3600 * 7)
29
36
  self.send_last_presence(force=True, no_cache_online=False)
30
37
 
31
38
  def _get_last_presence(self) -> Optional[CachedPresence]:
32
- return db.presence_get(self.jid, self.user)
39
+ if self.contact_pk is None:
40
+ return None
41
+ return self.xmpp.store.contacts.get_presence(self.contact_pk)
33
42
 
34
43
  def _store_last_presence(self, new: CachedPresence):
35
- return db.presence_store(self.jid, new, self.user)
44
+ if self.contact_pk is None:
45
+ self.cached_presence = new
46
+ return
47
+ self.xmpp.store.contacts.set_presence(self.contact_pk, new)
36
48
 
37
49
  def _make_presence(
38
50
  self,
@@ -55,7 +67,8 @@ class PresenceMixin(BaseSender):
55
67
  )
56
68
  if old != new:
57
69
  if hasattr(self, "muc") and ptype == "unavailable":
58
- db.presence_delete(self.jid, self.user)
70
+ if self.contact_pk is not None:
71
+ self.xmpp.store.contacts.reset_presence(self.contact_pk)
59
72
  else:
60
73
  self._store_last_presence(new)
61
74
  if old and not force and self._ONLY_SEND_PRESENCE_CHANGES: