slidge 0.3.1__py3-none-any.whl → 0.3.3__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.
slidge/command/user.py CHANGED
@@ -6,7 +6,7 @@ from slixmpp import JID
6
6
  from slixmpp.exceptions import XMPPError
7
7
 
8
8
  from ..group.room import LegacyMUC
9
- from ..util.types import AnyBaseSession, LegacyGroupIdType, UserPreferences
9
+ from ..util.types import AnyBaseSession, LegacyGroupIdType, MucType, UserPreferences
10
10
  from .base import (
11
11
  Command,
12
12
  CommandAccess,
@@ -157,7 +157,9 @@ class ListGroups(Command):
157
157
  async def run(self, session, _ifrom, *_):
158
158
  assert session is not None
159
159
  await session.ready
160
- groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
160
+ groups = sorted(
161
+ session.bookmarks, key=lambda g: (g.name or g.jid.localpart).casefold()
162
+ )
161
163
  return TableResult(
162
164
  description="Your groups",
163
165
  fields=[FormField("name"), FormField("jid", type="jid-single")],
@@ -364,3 +366,21 @@ class LeaveGroup(Command):
364
366
  async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC) -> None:
365
367
  await session.on_leave_group(group.legacy_id)
366
368
  await session.bookmarks.remove(group, reason="You left this group via slidge.")
369
+
370
+
371
+ class InviteInGroups(Command):
372
+ NAME = "💌 Re-invite me in my groups"
373
+ HELP = "Ask the gateway to send invitations for all your private groups"
374
+ CHAT_COMMAND = "re-invite"
375
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
376
+ ACCESS = CommandAccess.USER_LOGGED
377
+ CATEGORY = GROUPS
378
+
379
+ async def run(self, session, _ifrom, *_):
380
+ assert session is not None
381
+ await session.ready
382
+ for muc in session.bookmarks:
383
+ if muc.type == MucType.GROUP:
384
+ session.send_gateway_invite(
385
+ muc, reason="You asked to be re-invited in all groups."
386
+ )
slidge/contact/contact.py CHANGED
@@ -120,8 +120,7 @@ class LegacyContact(
120
120
  def is_friend(self, value: bool) -> None:
121
121
  if value == self.is_friend:
122
122
  return
123
- self.stored.is_friend = value
124
- self.commit()
123
+ self.update_stored_attribute(is_friend=value)
125
124
 
126
125
  @property
127
126
  def added_to_roster(self) -> bool:
@@ -131,8 +130,7 @@ class LegacyContact(
131
130
  def added_to_roster(self, value: bool) -> None:
132
131
  if value == self.added_to_roster:
133
132
  return
134
- self.stored.added_to_roster = value
135
- self.commit()
133
+ self.update_stored_attribute(added_to_roster=value)
136
134
 
137
135
  @property
138
136
  def participants(self) -> Iterator["LegacyParticipant"]:
@@ -170,8 +168,7 @@ class LegacyContact(
170
168
  def client_type(self, value: ClientType) -> None:
171
169
  if self.stored.client_type == value:
172
170
  return
173
- self.stored.client_type = value
174
- self.commit()
171
+ self.update_stored_attribute(client_type=value)
175
172
 
176
173
  def _set_logger(self) -> None:
177
174
  self.log = logging.getLogger(f"{self.user_jid.bare}:contact:{self}")
@@ -290,13 +287,12 @@ class LegacyContact(
290
287
  def name(self, n: Optional[str]) -> None:
291
288
  if self.stored.nick == n:
292
289
  return
293
- self.stored.nick = n
290
+ self.update_stored_attribute(nick=n)
294
291
  self._set_logger()
295
292
  if self.is_friend and self.added_to_roster:
296
293
  self.xmpp.pubsub.broadcast_nick(
297
294
  user_jid=self.user_jid, jid=self.jid.bare, nick=n
298
295
  )
299
- self.commit()
300
296
  for p in self.participants:
301
297
  p.nickname = n or str(self.legacy_id)
302
298
 
@@ -361,14 +357,11 @@ class LegacyContact(
361
357
  if pronouns:
362
358
  vcard["pronouns"]["text"] = pronouns
363
359
 
364
- self.stored.vcard = str(vcard)
365
- self.stored.vcard_fetched = True
360
+ self.update_stored_attribute(vcard=str(vcard), vcard_fetched=True)
366
361
  self.session.create_task(
367
362
  self.xmpp.pubsub.broadcast_vcard_event(self.jid, self.user_jid, vcard)
368
363
  )
369
364
 
370
- self.commit()
371
-
372
365
  def get_roster_item(self):
373
366
  item = {
374
367
  "subscription": self.__get_subscription_string(),
@@ -411,7 +404,7 @@ class LegacyContact(
411
404
  # we only broadcast pubsub events for contacts added to the roster
412
405
  # so if something was set before, we need to push it now
413
406
  self.added_to_roster = True
414
- self.send_last_presence()
407
+ self.send_last_presence(force=True)
415
408
 
416
409
  async def __broadcast_pubsub_items(self) -> None:
417
410
  if not self.is_friend:
slidge/contact/roster.py CHANGED
@@ -4,7 +4,7 @@ import warnings
4
4
  from typing import TYPE_CHECKING, AsyncIterator, Generic, Iterator, Optional, Type
5
5
 
6
6
  from slixmpp import JID
7
- from slixmpp.exceptions import IqError, IqTimeout
7
+ from slixmpp.exceptions import IqError, IqTimeout, XMPPError
8
8
  from sqlalchemy.orm import Session
9
9
  from sqlalchemy.orm import Session as OrmSession
10
10
 
@@ -92,6 +92,10 @@ class LegacyRoster(
92
92
  # :return:
93
93
  # """
94
94
  username = contact_jid.node
95
+ if not username:
96
+ raise XMPPError(
97
+ "bad-request", "Contacts must have a local part in their JID"
98
+ )
95
99
  contact_jid = JID(contact_jid.bare)
96
100
  async with self.lock(("username", username)):
97
101
  legacy_id = await self.jid_username_to_legacy_id(username)
@@ -239,6 +243,7 @@ class LegacyRoster(
239
243
  warnings.warn(f"Could not add to roster: {e}")
240
244
  else:
241
245
  contact.added_to_roster = True
246
+ contact.send_last_presence(force=True)
242
247
  orm.commit()
243
248
  self.__filling = False
244
249
 
slidge/core/config.py CHANGED
@@ -1,22 +1,26 @@
1
- from datetime import timedelta
2
1
  from pathlib import Path
3
- from typing import Optional, Self
2
+ from typing import Optional
4
3
 
5
4
  from slixmpp import JID as JIDType
6
5
 
6
+ # REQUIRED, so not default value
7
7
 
8
- class _TimedeltaSeconds(timedelta):
9
- def __new__(cls, s: str) -> Self:
10
- return super().__new__(cls, seconds=int(s))
11
8
 
9
+ class _Categories:
10
+ MANDATORY = (0, "Mandatory settings")
11
+ BASE = (10, "Basic configuration")
12
+ ATTACHMENTS = (20, "Attachments")
13
+ LOG = (30, "Logging")
14
+ ADVANCED = (40, "Advanced settings")
12
15
 
13
- # REQUIRED, so not default value
14
16
 
15
17
  LEGACY_MODULE: str
16
18
  LEGACY_MODULE__DOC = (
17
- "Importable python module containing (at least) "
18
- "a BaseGateway and a LegacySession subclass"
19
+ "Importable python module containing (at least) a BaseGateway and a LegacySession subclass. "
20
+ "NB: this is not needed if you use a gateway-specific entrypoint, e.g., `slidgram` or "
21
+ "`python -m slidgram`."
19
22
  )
23
+ LEGACY_MODULE__CATEGORY = _Categories.BASE
20
24
 
21
25
  SERVER: str = "localhost"
22
26
  SERVER__DOC = (
@@ -27,17 +31,21 @@ SERVER__DOC = (
27
31
  "you change this."
28
32
  )
29
33
  SERVER__SHORT = "s"
34
+ SERVER__CATEGORY = _Categories.BASE
30
35
 
31
36
  SECRET: str
32
37
  SECRET__DOC = "The gateway component's secret (required to connect to the XMPP server)"
38
+ SECRET__CATEGORY = _Categories.MANDATORY
33
39
 
34
40
  JID: JIDType
35
41
  JID__DOC = "The gateway component's JID"
36
42
  JID__SHORT = "j"
43
+ JID__CATEGORY = _Categories.MANDATORY
37
44
 
38
45
  PORT: str = "5347"
39
46
  PORT__DOC = "The XMPP server's port for incoming component connections"
40
47
  PORT__SHORT = "p"
48
+ PORT__CATEGORY = _Categories.BASE
41
49
 
42
50
  # Dynamic default (depends on other values)
43
51
 
@@ -47,6 +55,7 @@ HOME_DIR__DOC = (
47
55
  "Defaults to /var/lib/slidge/${SLIDGE_JID}. "
48
56
  )
49
57
  HOME_DIR__DYNAMIC_DEFAULT = True
58
+ HOME_DIR__CATEGORY = _Categories.BASE
50
59
 
51
60
  DB_URL: str
52
61
  DB_URL__DOC = (
@@ -54,6 +63,7 @@ DB_URL__DOC = (
54
63
  "Defaults to sqlite:///${HOME_DIR}/slidge.sqlite"
55
64
  )
56
65
  DB_URL__DYNAMIC_DEFAULT = True
66
+ DB_URL__CATEGORY = _Categories.ADVANCED
57
67
 
58
68
  USER_JID_VALIDATOR: str
59
69
  USER_JID_VALIDATOR__DOC = (
@@ -62,11 +72,13 @@ USER_JID_VALIDATOR__DOC = (
62
72
  "you probably want to change that to .*@example.com"
63
73
  )
64
74
  USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True
75
+ USER_JID_VALIDATOR__CATEGORY = _Categories.BASE
65
76
 
66
77
  # Optional, so default value + type hint if default is None
67
78
 
68
79
  ADMINS: tuple[JIDType, ...] = ()
69
80
  ADMINS__DOC = "JIDs of the gateway admins"
81
+ ADMINS__CATEGORY = _Categories.BASE
70
82
 
71
83
  UPLOAD_SERVICE: Optional[str] = None
72
84
  UPLOAD_SERVICE__DOC = (
@@ -74,11 +86,13 @@ UPLOAD_SERVICE__DOC = (
74
86
  "This is optional, as it should be automatically determined via service"
75
87
  "discovery."
76
88
  )
89
+ UPLOAD_SERVICE__CATEGORY = _Categories.ATTACHMENTS
77
90
 
78
91
  AVATAR_SIZE = 200
79
92
  AVATAR_SIZE__DOC = (
80
93
  "Maximum image size (width and height), image ratio will be preserved"
81
94
  )
95
+ AVATAR_SIZE__CATEGORY = _Categories.ADVANCED
82
96
 
83
97
  USE_ATTACHMENT_ORIGINAL_URLS = False
84
98
  USE_ATTACHMENT_ORIGINAL_URLS__DOC = (
@@ -86,11 +100,13 @@ USE_ATTACHMENT_ORIGINAL_URLS__DOC = (
86
100
  "let XMPP clients directly download them from this URL. Note that this will "
87
101
  "probably leak your client IP to the legacy network."
88
102
  )
103
+ USE_ATTACHMENT_ORIGINAL_URLS__CATEGORY = _Categories.ATTACHMENTS
89
104
 
90
105
  UPLOAD_REQUESTER: Optional[str] = None
91
106
  UPLOAD_REQUESTER__DOC = (
92
107
  "Set which JID should request the upload slots. Defaults to the component JID."
93
108
  )
109
+ UPLOAD_REQUESTER__CATEGORY = _Categories.ATTACHMENTS
94
110
 
95
111
  NO_UPLOAD_PATH: Optional[str] = None
96
112
  NO_UPLOAD_PATH__DOC = (
@@ -98,38 +114,45 @@ NO_UPLOAD_PATH__DOC = (
98
114
  "You need to set NO_UPLOAD_URL_PREFIX too if you use this option, and configure "
99
115
  "an web server to serve files in this dir."
100
116
  )
117
+ NO_UPLOAD_PATH__CATEGORY = _Categories.ATTACHMENTS
101
118
 
102
119
  NO_UPLOAD_URL_PREFIX: Optional[str] = None
103
120
  NO_UPLOAD_URL_PREFIX__DOC = (
104
121
  "Base URL that servers files in the dir set in the no-upload-path option, "
105
122
  "eg https://example.com:666/slidge-attachments/"
106
123
  )
124
+ NO_UPLOAD_URL_PREFIX__CATEGORY = _Categories.ATTACHMENTS
107
125
 
108
126
  NO_UPLOAD_METHOD: str = "copy"
109
127
  NO_UPLOAD_METHOD__DOC = (
110
128
  "Whether to 'copy', 'move', 'hardlink' or 'symlink' the files in no-upload-path."
111
129
  )
130
+ NO_UPLOAD_METHOD__CATEGORY = _Categories.ATTACHMENTS
112
131
 
113
132
  NO_UPLOAD_FILE_READ_OTHERS = False
114
133
  NO_UPLOAD_FILE_READ_OTHERS__DOC = (
115
134
  "After writing a file in NO_UPLOAD_PATH, change its permission so that 'others' can"
116
135
  " read it."
117
136
  )
137
+ NO_UPLOAD_FILE_READ_OTHERS__CATEGORY = _Categories.ATTACHMENTS
118
138
 
119
- IGNORE_DELAY_THRESHOLD = _TimedeltaSeconds("300")
139
+ IGNORE_DELAY_THRESHOLD = 300
120
140
  IGNORE_DELAY_THRESHOLD__DOC = (
121
141
  "Threshold, in seconds, below which the <delay> information is stripped "
122
142
  "out of emitted stanzas."
123
143
  )
144
+ IGNORE_DELAY_THRESHOLD__CATEGORY = _Categories.ADVANCED
124
145
 
125
146
  PARTIAL_REGISTRATION_TIMEOUT = 3600
126
147
  PARTIAL_REGISTRATION_TIMEOUT__DOC = (
127
148
  "Timeout before registration and login. Only useful for legacy networks where "
128
149
  "a single step registration process is not enough."
129
150
  )
151
+ PARTIAL_REGISTRATION_TIMEOUT__CATEGORY = _Categories.ADVANCED
130
152
 
131
153
  QR_TIMEOUT = 60
132
154
  QR_TIMEOUT__DOC = "Timeout for QR code flashing confirmation."
155
+ QR_TIMEOUT__CATEGORY = _Categories.ADVANCED
133
156
 
134
157
  FIX_FILENAME_SUFFIX_MIME_TYPE = False
135
158
  FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = (
@@ -138,9 +161,11 @@ FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = (
138
161
  " Therefore the MIME Type of the file is checked, if the suffix is not valid for"
139
162
  " that MIME Type, a valid one will be picked."
140
163
  )
164
+ FIX_FILENAME_SUFFIX_MIME_TYPE__CATEGORY = _Categories.ATTACHMENTS
141
165
 
142
166
  LOG_FILE: Optional[Path] = None
143
167
  LOG_FILE__DOC = "Log to a file instead of stdout/err"
168
+ LOG_FILE__CATEGORY = _Categories.LOG
144
169
 
145
170
  LOG_FORMAT: str = "%(levelname)s:%(name)s:%(message)s"
146
171
  LOG_FORMAT__DOC = (
@@ -148,41 +173,50 @@ LOG_FORMAT__DOC = (
148
173
  "https://docs.python.org/3/library/logging.html#logrecord-attributes "
149
174
  "for available options."
150
175
  )
176
+ LOG_FORMAT__CATEGORY = _Categories.LOG
151
177
 
152
178
  MAM_MAX_DAYS = 7
153
179
  MAM_MAX_DAYS__DOC = "Maximum number of days for group archive retention."
180
+ MAM_MAX_DAYS__CATEGORY = _Categories.BASE
154
181
 
155
182
  ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH = 200
156
183
  ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__DOC = (
157
184
  "Some legacy network provide ridiculously long filenames, strip above this limit, "
158
185
  "preserving suffix."
159
186
  )
187
+ ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__CATEGORY = _Categories.ATTACHMENTS
160
188
 
161
189
  AVATAR_RESAMPLING_THREADS = 2
162
190
  AVATAR_RESAMPLING_THREADS__DOC = (
163
191
  "Number of additional threads to use for avatar resampling. Even in a single-core "
164
192
  "context, this makes avatar resampling non-blocking."
165
193
  )
194
+ AVATAR_RESAMPLING_THREADS__CATEGORY = _Categories.ADVANCED
166
195
 
167
196
  DEV_MODE = False
168
197
  DEV_MODE__DOC = (
169
198
  "Enables an interactive python shell via chat commands, for admins."
170
199
  "Not safe to use in prod, but great during dev."
171
200
  )
201
+ DEV_MODE__CATEGORY = _Categories.ADVANCED
202
+
172
203
 
173
204
  STRIP_LEADING_EMOJI_ADHOC = False
174
205
  STRIP_LEADING_EMOJI_ADHOC__DOC = (
175
206
  "Strip the leading emoji in ad-hoc command names, if present, in case you "
176
207
  "are a emoji-hater."
177
208
  )
209
+ STRIP_LEADING_EMOJI_ADHOC__CATEGORY = _Categories.ADVANCED
178
210
 
179
211
  COMPONENT_NAME: Optional[str] = None
180
212
  COMPONENT_NAME__DOC = (
181
213
  "Overrides the default component name with a custom one. This is seen in service discovery and as the nickname "
182
214
  "of the component in chat windows."
183
215
  )
216
+ COMPONENT_NAME__CATEGORY = _Categories.ADVANCED
184
217
 
185
218
  WELCOME_MESSAGE: Optional[str] = None
186
219
  WELCOME_MESSAGE__DOC = (
187
220
  "Overrides the default welcome message received by newly registered users."
188
221
  )
222
+ WELCOME_MESSAGE__CATEGORY = _Categories.ADVANCED
@@ -57,8 +57,7 @@ class CapsMixin(DispatcherMixin):
57
57
  ver = contact.stored.caps_ver
58
58
  else:
59
59
  ver = await contact.get_caps_ver(pfrom)
60
- contact.stored.caps_ver = ver
61
- contact.commit()
60
+ contact.update_stored_attribute(caps_ver=ver)
62
61
  else:
63
62
  ver = await caps.get_verstring(pfrom)
64
63
 
@@ -315,6 +315,7 @@ class MessageContentMixin(DispatcherMixin):
315
315
  raise XMPPError(
316
316
  "policy-violation",
317
317
  text=error_msg,
318
+ clear=False,
318
319
  )
319
320
 
320
321
  await session.on_react(recipient, legacy_id, emojis, thread=thread)
@@ -71,6 +71,7 @@ class PresenceHandlerMixin(DispatcherMixin):
71
71
  return
72
72
 
73
73
  await contact.on_friend_accept()
74
+ contact.send_last_presence(force=True)
74
75
 
75
76
  @exceptions_to_xmpp_errors
76
77
  async def _handle_unsubscribed(self, pres: Presence) -> None:
@@ -60,7 +60,7 @@ class RegistrationMixin(DispatcherMixin):
60
60
  )
61
61
  orm.add(user)
62
62
  orm.commit()
63
- log.info("New user: %s", user)
63
+ log.info("New user: %s", user)
64
64
 
65
65
  async def _user_modify(
66
66
  self, _gateway_jid, _node, ifrom: JID, form_dict: dict[str, Optional[str]]
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
3
3
  from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
4
4
  from slixmpp.exceptions import XMPPError
5
5
 
6
- from ...db.models import GatewayUser
7
6
  from .util import DispatcherMixin, exceptions_to_xmpp_errors
8
7
 
9
8
  if TYPE_CHECKING:
@@ -32,10 +31,7 @@ class SearchMixin(DispatcherMixin):
32
31
  """
33
32
  Prepare the search form using :attr:`.BaseSession.SEARCH_FIELDS`
34
33
  """
35
- with self.xmpp.store.session() as orm:
36
- user = orm.query(GatewayUser).one_or_none()
37
- if user is None:
38
- raise XMPPError(text="Search is only allowed for registered users")
34
+ await self._get_session(iq)
39
35
 
40
36
  xmpp = self.xmpp
41
37
 
@@ -172,7 +172,9 @@ def exceptions_to_xmpp_errors(cb: HandlerType) -> HandlerType:
172
172
  except NotImplementedError:
173
173
  log.debug("NotImplementedError raised in %s", cb)
174
174
  raise XMPPError(
175
- "feature-not-implemented", "Not implemented by the legacy module"
175
+ "feature-not-implemented",
176
+ f"{cb.__name__} is not implemented by the legacy module",
177
+ clear=False,
176
178
  )
177
179
  except Exception as e:
178
180
  log.error("Failed to handle incoming stanza: %s", args, exc_info=e)
slidge/core/gateway.py CHANGED
@@ -330,6 +330,7 @@ class BaseGateway(
330
330
  self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR)
331
331
  self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]()
332
332
 
333
+ self.register_plugins()
333
334
  self.__setup_legacy_module_subclasses()
334
335
 
335
336
  self.get_session_from_stanza: Callable[
@@ -339,7 +340,6 @@ class BaseGateway(
339
340
  self._session_cls.from_user
340
341
  )
341
342
 
342
- self.register_plugins()
343
343
  self.__register_slixmpp_events()
344
344
  self.__register_slixmpp_api()
345
345
  self.roster.set_backend(RosterBackend(self))
@@ -390,6 +390,16 @@ class BaseGateway(
390
390
  bookmarks_cls = LegacyBookmarks.get_self_or_unique_subclass()
391
391
  roster_cls = LegacyRoster.get_self_or_unique_subclass()
392
392
 
393
+ if contact_cls.REACTIONS_SINGLE_EMOJI: # type:ignore[attr-defined]
394
+ form = Form()
395
+ form["type"] = "result"
396
+ form.add_field(
397
+ "FORM_TYPE", "hidden", value="urn:xmpp:reactions:0:restrictions"
398
+ )
399
+ form.add_field("max_reactions_per_user", value="1", type="number")
400
+ form.add_field("scope", value="domain")
401
+ self.plugin["xep_0128"].add_extended_info(data=form)
402
+
393
403
  session_cls.xmpp = self # type:ignore[attr-defined]
394
404
  contact_cls.xmpp = self # type:ignore[attr-defined]
395
405
  muc_cls.xmpp = self # type:ignore[attr-defined]
@@ -1002,6 +1012,7 @@ SLIXMPP_PLUGINS = [
1002
1012
  "xep_0106", # JID Escaping
1003
1013
  "xep_0115", # Entity capabilities
1004
1014
  "xep_0122", # Data Forms Validation
1015
+ "xep_0128", # Service Discovery Extensions
1005
1016
  "xep_0153", # vCard-Based Avatars (for MUC avatars)
1006
1017
  "xep_0172", # User nickname
1007
1018
  "xep_0184", # Message Delivery Receipts
@@ -162,15 +162,12 @@ class AttachmentMixin(TextMessageMixin):
162
162
  legacy_file_id=None
163
163
  if attachment.legacy_file_id is None
164
164
  else str(attachment.legacy_file_id),
165
- url=attachment.url,
165
+ url=attachment.url if config.USE_ATTACHMENT_ORIGINAL_URLS else None,
166
166
  )
167
167
 
168
168
  async def __get_url(
169
169
  self, attachment: LegacyAttachment, stored: Attachment
170
170
  ) -> tuple[bool, Optional[Path], str]:
171
- if attachment.url and config.USE_ATTACHMENT_ORIGINAL_URLS:
172
- return False, None, attachment.url
173
-
174
171
  file_name = attachment.name
175
172
  content_type = attachment.content_type
176
173
  file_path = attachment.path
@@ -221,7 +218,9 @@ class AttachmentMixin(TextMessageMixin):
221
218
 
222
219
  assert isinstance(file_path, Path)
223
220
  if config.FIX_FILENAME_SUFFIX_MIME_TYPE:
224
- file_name = str(fix_suffix(file_path, content_type, file_name))
221
+ file_name, content_type = fix_suffix(file_path, content_type, file_name)
222
+ attachment.content_type = content_type
223
+ attachment.name = file_name
225
224
 
226
225
  if config.NO_UPLOAD_PATH:
227
226
  local_path, new_url = await self.__no_upload(
@@ -312,6 +311,50 @@ class AttachmentMixin(TextMessageMixin):
312
311
  stored.sfs = str(sfs)
313
312
  msg.append(sfs)
314
313
 
314
+ async def __set_sfs_and_sims_without_download(
315
+ self, msg: Message, attachment: LegacyAttachment
316
+ ) -> None:
317
+ assert attachment.url is not None
318
+
319
+ if not any(
320
+ (
321
+ attachment.content_type,
322
+ attachment.name,
323
+ attachment.disposition,
324
+ )
325
+ ):
326
+ return
327
+
328
+ sims = self.xmpp.plugin["xep_0385"].stanza.Sims()
329
+ ref = self.xmpp["xep_0372"].stanza.Reference()
330
+
331
+ ref["uri"] = attachment.url
332
+ ref["type"] = "data"
333
+ sims["sources"].append(ref)
334
+ sims.enable("file")
335
+
336
+ xep_0447_stanza = self.xmpp.plugin["xep_0447"].stanza
337
+ sfs = xep_0447_stanza.StatelessFileSharing()
338
+ url_data = xep_0447_stanza.UrlData()
339
+ url_data["target"] = attachment.url
340
+ sfs["sources"].append(url_data)
341
+ sfs.enable("file")
342
+
343
+ if attachment.content_type:
344
+ sims["file"]["media-type"] = attachment.content_type
345
+ sfs["file"]["media-type"] = attachment.content_type
346
+ if attachment.caption:
347
+ sims["file"]["desc"] = attachment.caption
348
+ sfs["file"]["desc"] = attachment.caption
349
+ if attachment.name:
350
+ sims["file"]["name"] = attachment.name
351
+ sfs["file"]["name"] = attachment.name
352
+ if attachment.disposition:
353
+ sfs["disposition"] = attachment.disposition
354
+
355
+ msg.append(sims)
356
+ msg.append(sfs)
357
+
315
358
  def __send_url(
316
359
  self,
317
360
  msg: Message,
@@ -325,6 +368,9 @@ class AttachmentMixin(TextMessageMixin):
325
368
  ) -> list[Message]:
326
369
  msg["oob"]["url"] = uploaded_url
327
370
  msg["body"] = uploaded_url
371
+ if msg.get_plugin("sfs", check=True):
372
+ msg["fallback"].enable("body")
373
+ msg["fallback"]["for"] = self.xmpp.plugin["xep_0447"].stanza.NAMESPACE
328
374
  if caption:
329
375
  m1 = self._send(msg, carbon=carbon, **kwargs)
330
376
  m2 = self.send_text(
@@ -465,7 +511,10 @@ class AttachmentMixin(TextMessageMixin):
465
511
  new_url = stored.url
466
512
  else:
467
513
  is_temp, local_path, new_url = await self.__get_url(attachment, stored)
468
- if new_url is None:
514
+ if new_url is None or (
515
+ local_path is not None and local_path.stat().st_size == 0
516
+ ):
517
+ log.warning("Something went wrong with this attachment: %s", attachment)
469
518
  msg["body"] = (
470
519
  "I tried to send a file, but something went wrong. "
471
520
  "Tell your slidge admin to check the logs."
@@ -474,8 +523,13 @@ class AttachmentMixin(TextMessageMixin):
474
523
  return None, [self._send(msg, **kwargs)]
475
524
 
476
525
  stored.url = new_url
477
- thumbnail = await self.__set_sims(msg, new_url, local_path, attachment, stored)
478
- self.__set_sfs(msg, new_url, local_path, attachment, stored, thumbnail)
526
+ if config.USE_ATTACHMENT_ORIGINAL_URLS and attachment.url:
527
+ await self.__set_sfs_and_sims_without_download(msg, attachment)
528
+ else:
529
+ thumbnail = await self.__set_sims(
530
+ msg, new_url, local_path, attachment, stored
531
+ )
532
+ self.__set_sfs(msg, new_url, local_path, attachment, stored, thumbnail)
479
533
 
480
534
  if self.session is not NotImplemented:
481
535
  with self.xmpp.store.session(expire_on_commit=False) as orm:
slidge/core/mixins/db.py CHANGED
@@ -2,6 +2,8 @@ import logging
2
2
  import typing
3
3
  from contextlib import contextmanager
4
4
 
5
+ import sqlalchemy as sa
6
+
5
7
  from ...db.models import Base, Contact, Room
6
8
 
7
9
  if typing.TYPE_CHECKING:
@@ -86,3 +88,16 @@ class UpdateInfoMixin(DBMixin):
86
88
  else:
87
89
  self.stored.extra_attributes = self.serialize_extra_attributes()
88
90
  super().commit(merge=merge)
91
+
92
+ def update_stored_attribute(self, **kwargs) -> None:
93
+ for key, value in kwargs.items():
94
+ setattr(self.stored, key, value)
95
+ if self._updating_info:
96
+ return
97
+ with self.xmpp.store.session() as orm:
98
+ orm.execute(
99
+ sa.update(self.stored.__class__)
100
+ .where(self.stored.__class__.id == self.stored.id)
101
+ .values(**kwargs)
102
+ )
103
+ orm.commit()
@@ -112,7 +112,7 @@ class MessageMaker(BaseSender):
112
112
  if when.tzinfo is None:
113
113
  when = when.astimezone(timezone.utc)
114
114
  if self.STRIP_SHORT_DELAY:
115
- delay = datetime.now().astimezone(timezone.utc) - when
115
+ delay = (datetime.now().astimezone(timezone.utc) - when).seconds
116
116
  if delay < config.IGNORE_DELAY_THRESHOLD:
117
117
  return
118
118
  msg["delay"].set_stamp(when)
@@ -191,7 +191,7 @@ class TextMessageMixin(MessageMaker):
191
191
  xmpp_id = kwargs.pop("xmpp_id", None)
192
192
  if not xmpp_id:
193
193
  xmpp_id = self._legacy_to_xmpp(legacy_msg_id)
194
- self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=emojis)
194
+ self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=set(emojis))
195
195
  self.__add_reaction_fallback(msg, legacy_msg_id, emojis)
196
196
  self._send(msg, **kwargs)
197
197
 
@@ -46,6 +46,8 @@ def _clear_last_seen_task(contact_pk: int, _task) -> None:
46
46
  class PresenceMixin(BaseSender, DBMixin):
47
47
  _ONLY_SEND_PRESENCE_CHANGES = False
48
48
 
49
+ # this attribute actually only exists for contacts and not participants
50
+ _updating_info: bool
49
51
  stored: Contact | Participant
50
52
 
51
53
  def __init__(self, *a, **k) -> None:
@@ -55,15 +57,24 @@ class PresenceMixin(BaseSender, DBMixin):
55
57
  # to DB at the end of update_info()
56
58
  self.cached_presence: Optional[CachedPresence] = None
57
59
 
60
+ def __is_contact(self) -> bool:
61
+ return isinstance(self.stored, Contact)
62
+
58
63
  def __stored(self) -> Contact | None:
59
- if isinstance(self.stored, Contact):
64
+ if self.__is_contact():
65
+ assert isinstance(self.stored, Contact)
60
66
  return self.stored
61
67
  else:
68
+ assert isinstance(self.stored, Participant)
62
69
  try:
63
70
  return self.stored.contact
64
71
  except DetachedInstanceError:
65
72
  with self.xmpp.store.session() as orm:
66
73
  orm.add(self.stored)
74
+ if self.stored.contact is None:
75
+ return None
76
+ orm.refresh(self.stored.contact)
77
+ orm.merge(self.stored)
67
78
  return self.stored.contact
68
79
 
69
80
  @property
@@ -85,15 +96,14 @@ class PresenceMixin(BaseSender, DBMixin):
85
96
  )
86
97
 
87
98
  def _store_last_presence(self, new: CachedPresence) -> None:
88
- stored = self.__stored()
89
- if stored is not None:
90
- stored.cached_presence = True
91
- for k, v in new._asdict().items():
92
- setattr(stored, k, v)
93
- try:
94
- self.commit()
95
- except InvalidRequestError:
96
- self.commit(merge=True)
99
+ if self.__is_contact():
100
+ contact = self
101
+ elif (contact := getattr(self, "contact", None)) is None: # type:ignore[assignment]
102
+ return
103
+ contact.update_stored_attribute( # type:ignore[attr-defined]
104
+ cached_presence=True,
105
+ **new._asdict(),
106
+ )
97
107
 
98
108
  def _make_presence(
99
109
  self,
slidge/core/session.py CHANGED
@@ -719,6 +719,8 @@ class BaseSession(
719
719
  log.warning("User not found during unregistration")
720
720
  return
721
721
 
722
+ session.cancel_all_tasks()
723
+
722
724
  await cls.xmpp.unregister(session)
723
725
  with cls.xmpp.store.session() as orm:
724
726
  orm.delete(session.user)
slidge/db/models.py CHANGED
@@ -7,9 +7,9 @@ import sqlalchemy as sa
7
7
  from slixmpp import JID
8
8
  from slixmpp.types import MucAffiliation, MucRole
9
9
  from sqlalchemy import JSON, ForeignKey, Index, UniqueConstraint
10
- from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
10
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
11
11
 
12
- from ..util.types import ClientType, MucType
12
+ from ..util.types import ClientType, Hat, MucType
13
13
  from .meta import Base, JSONSerializable, JSONSerializableTypes
14
14
 
15
15
 
@@ -344,7 +344,7 @@ class Participant(Base):
344
344
  nickname: Mapped[str] = mapped_column(nullable=False, default=None)
345
345
  nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
346
346
 
347
- hats: Mapped[list[tuple[str, str]]] = mapped_column(JSON, default=list)
347
+ hats: Mapped[list[Hat]] = mapped_column(JSON, default=list)
348
348
 
349
349
  extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
350
350
 
slidge/db/store.py CHANGED
@@ -7,9 +7,10 @@ from datetime import datetime, timedelta, timezone
7
7
  from mimetypes import guess_extension
8
8
  from typing import Collection, Iterator, Optional, Type
9
9
 
10
+ import sqlalchemy as sa
10
11
  from slixmpp.exceptions import XMPPError
11
12
  from slixmpp.plugins.xep_0231.stanza import BitsOfBinary
12
- from sqlalchemy import Engine, delete, select, update
13
+ from sqlalchemy import Engine, delete, event, select, update
13
14
  from sqlalchemy.exc import InvalidRequestError
14
15
  from sqlalchemy.orm import Session, attributes, sessionmaker
15
16
 
@@ -20,6 +21,7 @@ from .meta import Base
20
21
  from .models import (
21
22
  ArchivedMessage,
22
23
  ArchivedMessageSource,
24
+ Avatar,
23
25
  Bob,
24
26
  Contact,
25
27
  ContactSent,
@@ -213,6 +215,7 @@ class ContactStore(UpdatedMixin):
213
215
  def __init__(self, session: Session) -> None:
214
216
  super().__init__(session)
215
217
  session.execute(update(Contact).values(cached_presence=False))
218
+ session.execute(update(Contact).values(caps_ver=None))
216
219
 
217
220
  @staticmethod
218
221
  def add_to_sent(session: Session, contact_pk: int, msg_id: str) -> None:
@@ -589,4 +592,29 @@ class BobStore:
589
592
  session.add(row)
590
593
 
591
594
 
595
+ @event.listens_for(sa.orm.Session, "after_flush")
596
+ def _check_avatar_orphans(session, flush_context):
597
+ if not session.deleted:
598
+ return
599
+
600
+ potentially_orphaned = set()
601
+ for obj in session.deleted:
602
+ if isinstance(obj, (Contact, Room)) and obj.avatar_id:
603
+ potentially_orphaned.add(obj.avatar_id)
604
+ if not potentially_orphaned:
605
+ return
606
+
607
+ result = session.execute(
608
+ sa.delete(Avatar).where(
609
+ sa.and_(
610
+ Avatar.id.in_(potentially_orphaned),
611
+ sa.not_(sa.exists().where(Contact.avatar_id == Avatar.id)),
612
+ sa.not_(sa.exists().where(Room.avatar_id == Avatar.id)),
613
+ )
614
+ )
615
+ )
616
+ deleted_count = result.rowcount
617
+ log.debug(f"Auto-deleted %s orphaned avatars", deleted_count)
618
+
619
+
592
620
  log = logging.getLogger(__name__)
slidge/group/room.py CHANGED
@@ -169,8 +169,7 @@ class LegacyMUC(
169
169
  def type(self, type_: MucType) -> None:
170
170
  if self.type == type_:
171
171
  return
172
- self.stored.muc_type = type_
173
- self.commit()
172
+ self.update_stored_attribute(muc_type=type_)
174
173
 
175
174
  @property
176
175
  def n_participants(self):
@@ -180,8 +179,7 @@ class LegacyMUC(
180
179
  def n_participants(self, n_participants: Optional[int]) -> None:
181
180
  if self.stored.n_participants == n_participants:
182
181
  return
183
- self.stored.n_participants = n_participants
184
- self.commit()
182
+ self.update_stored_attribute(n_participants=n_participants)
185
183
 
186
184
  @property
187
185
  def user_jid(self):
@@ -203,8 +201,7 @@ class LegacyMUC(
203
201
  def subject_date(self, when: Optional[datetime]) -> None:
204
202
  if self.subject_date == when:
205
203
  return
206
- self.stored.subject_date = when
207
- self.commit()
204
+ self.update_stored_attribute(subject_date=when)
208
205
 
209
206
  def __send_configuration_change(self, codes) -> None:
210
207
  part = self.get_system_participant()
@@ -222,18 +219,16 @@ class LegacyMUC(
222
219
  def user_nick(self, nick: str) -> None:
223
220
  if nick == self.user_nick:
224
221
  return
225
- self.stored.user_nick = nick
226
- self.commit()
222
+ self.update_stored_attribute(user_nick=nick)
227
223
 
228
224
  def add_user_resource(self, resource: str) -> None:
229
225
  stored_set = self.get_user_resources()
230
226
  if resource in stored_set:
231
227
  return
232
228
  stored_set.add(resource)
233
- self.stored.user_resources = (
234
- json.dumps(list(stored_set)) if stored_set else None
229
+ self.update_stored_attribute(
230
+ user_resources=(json.dumps(list(stored_set)) if stored_set else None)
235
231
  )
236
- self.commit()
237
232
 
238
233
  def get_user_resources(self) -> set[str]:
239
234
  stored_str = self.stored.user_resources
@@ -246,10 +241,9 @@ class LegacyMUC(
246
241
  if resource not in stored_set:
247
242
  return
248
243
  stored_set.remove(resource)
249
- self.stored.user_resources = (
250
- json.dumps(list(stored_set)) if stored_set else None
244
+ self.update_stored_attribute(
245
+ user_resources=(json.dumps(list(stored_set)) if stored_set else None)
251
246
  )
252
- self.commit()
253
247
 
254
248
  @asynccontextmanager
255
249
  async def lock(self, id_: str) -> AsyncIterator[None]:
@@ -296,7 +290,7 @@ class LegacyMUC(
296
290
  self, affiliation: Optional[MucAffiliation] = None
297
291
  ) -> AsyncIterator[LegacyParticipantType]:
298
292
  await self.__fill_participants()
299
- with self.xmpp.store.session(expire_on_commit=False) as orm:
293
+ with self.xmpp.store.session(expire_on_commit=False, autoflush=False) as orm:
300
294
  orm.add(self.stored)
301
295
  for db_participant in self.stored.participants:
302
296
  if (
@@ -346,8 +340,7 @@ class LegacyMUC(
346
340
  def name(self, n: str | None) -> None:
347
341
  if self.name == n:
348
342
  return
349
- self.stored.name = n
350
- self.commit()
343
+ self.update_stored_attribute(name=n)
351
344
  self._set_logger()
352
345
  self.__send_configuration_change((104,))
353
346
 
@@ -359,8 +352,7 @@ class LegacyMUC(
359
352
  def description(self, d: str) -> None:
360
353
  if self.description == d:
361
354
  return
362
- self.stored.description = d
363
- self.commit()
355
+ self.update_stored_attribute(description=d)
364
356
  self.__send_configuration_change((104,))
365
357
 
366
358
  def on_presence_unavailable(self, p: Presence) -> None:
@@ -440,8 +432,7 @@ class LegacyMUC(
440
432
  if s == self.subject:
441
433
  return
442
434
 
443
- self.stored.subject = s
444
- self.commit()
435
+ self.update_stored_attribute(subject=s)
445
436
  self.__get_subject_setter_participant().set_room_subject(
446
437
  s, None, self.subject_date, False
447
438
  )
@@ -464,8 +455,7 @@ class LegacyMUC(
464
455
  if subject_setter == self.subject_setter:
465
456
  return
466
457
  assert isinstance(subject_setter, str | None)
467
- self.stored.subject_setter = subject_setter
468
- self.commit()
458
+ self.update_stored_attribute(subject_setter=subject_setter)
469
459
 
470
460
  def __get_subject_setter_participant(self) -> LegacyParticipant:
471
461
  if self.subject_setter is None:
@@ -524,6 +514,9 @@ class LegacyMUC(
524
514
  if s := self.subject:
525
515
  form.add_field("muc#roominfo_subject", value=s)
526
516
 
517
+ if name := self.name:
518
+ form.add_field("muc#roomconfig_roomname", value=name)
519
+
527
520
  if self._set_avatar_task is not None:
528
521
  await self._set_avatar_task
529
522
  avatar = self.get_avatar()
@@ -652,7 +645,7 @@ class LegacyMUC(
652
645
  with orm.no_autoflush:
653
646
  orm.refresh(self.stored, ["participants"])
654
647
  if not user_participant.is_user:
655
- self.log.warning("is_user flag not set participant on user_participant")
648
+ self.log.warning("is_user flag not set on user_participant")
656
649
  user_participant.is_user = True
657
650
  user_participant.send_initial_presence(
658
651
  user_full_jid,
slidge/util/test.py CHANGED
@@ -42,7 +42,7 @@ from slidge import (
42
42
 
43
43
  from ..command import Command
44
44
  from ..core import config
45
- from ..core.config import _TimedeltaSeconds
45
+ from ..core.session import _sessions
46
46
  from ..db import SlidgeStore
47
47
  from ..db.avatar import avatar_cache
48
48
  from ..db.meta import Base
@@ -206,7 +206,7 @@ class SlidgeTest(SlixTestPlus):
206
206
  user_jid_validator = ".*"
207
207
  admins: list[str] = []
208
208
  upload_requester = None
209
- ignore_delay_threshold = _TimedeltaSeconds("300")
209
+ ignore_delay_threshold = 300
210
210
 
211
211
  @classmethod
212
212
  def setUpClass(cls) -> None:
@@ -281,6 +281,7 @@ class SlidgeTest(SlixTestPlus):
281
281
  self.db_engine.echo = False
282
282
  super().tearDown()
283
283
  Base.metadata.drop_all(self.xmpp.store._engine)
284
+ _sessions.clear()
284
285
 
285
286
  def setup_logged_session(self, n_contacts: int = 0) -> None:
286
287
  with self.xmpp.store.session() as orm:
slidge/util/types.py CHANGED
@@ -178,6 +178,8 @@ class Mention(NamedTuple):
178
178
  class Hat(NamedTuple):
179
179
  uri: str
180
180
  title: str
181
+ hue: float = 0.0 # Default value is not great here, but an OK workaround for the
182
+ # slixmpp 1.11→1.12 API update
181
183
 
182
184
 
183
185
  class UserPreferences(TypedDict):
slidge/util/util.py CHANGED
@@ -34,7 +34,9 @@ except ImportError as e:
34
34
  )
35
35
 
36
36
 
37
- def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]) -> Path:
37
+ def fix_suffix(
38
+ path: Path, mime_type: Optional[str], file_name: Optional[str]
39
+ ) -> tuple[str, str]:
38
40
  guessed = magic.from_file(path, mime=True)
39
41
  if guessed == mime_type:
40
42
  log.debug("Magic and given MIME match")
@@ -53,15 +55,15 @@ def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]) -
53
55
 
54
56
  if suffix in valid_suffix_list:
55
57
  log.debug("Suffix %s is in %s", suffix, valid_suffix_list)
56
- return name
58
+ return str(name), guessed
57
59
 
58
60
  valid_suffix = mimetypes.guess_extension(mime_type.split(";")[0], strict=False)
59
61
  if valid_suffix is None:
60
62
  log.debug("No valid suffix found")
61
- return name
63
+ return str(name), guessed
62
64
 
63
65
  log.debug("Changing suffix of %s to %s", file_name or path.name, valid_suffix)
64
- return name.with_suffix(valid_suffix)
66
+ return str(name.with_suffix(valid_suffix)), guessed
65
67
 
66
68
 
67
69
  class SubclassableOnce(type):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidge
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: XMPP bridging framework
5
5
  Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -10,28 +10,28 @@ slidge/command/base.py,sha256=h6lQ6ypb5uNrppZ_N8mS7ObJpnOINYZS6QokGlWVgAk,13527
10
10
  slidge/command/categories.py,sha256=vF0KGDV9sEn8TNkcMoDRw-u3gEyNHSXghOU2JRHQtKs,351
11
11
  slidge/command/chat_command.py,sha256=r_qalygOCsEoS-OgWw8IIlAzTufhXNjduONbeoepUIA,11557
12
12
  slidge/command/register.py,sha256=BduDI31Kx8CbWWEdjybimTA5Wcfhn-Jkt8sSPsySCpo,6724
13
- slidge/command/user.py,sha256=xt7NTb8Fdj_Mx91BgiszSii6QF67LtsyowQ1vhBJSIU,12660
13
+ slidge/command/user.py,sha256=3aJ60bvXGyCqjgBH1r6HPbBh4IwgH40rHBilTkeFiv4,13325
14
14
  slidge/contact/__init__.py,sha256=WMMaHk7UW7YT9EH2LtPdkU0bHQaOp4ikBhbBQskmoc8,191
15
- slidge/contact/contact.py,sha256=uXHxfEsBNjTE0Tdk1s1_YETIazU03-pvaScd3QQmlu0,19607
16
- slidge/contact/roster.py,sha256=uZ3iCl8oa7kEpIptUVa9p1k51bSvbuQq1pUGnf_zru8,9791
15
+ slidge/contact/contact.py,sha256=pexE6DU1WsxUoTeF-nENuEqOi7quYQtQot1hD816cl8,19565
16
+ slidge/contact/roster.py,sha256=ZbhPC_5opMXdKrdCaQZiHTErxQD4EyWn5Susl6W7M6k,10003
17
17
  slidge/core/__init__.py,sha256=RG7Jj5JCJERjhqJ31lOLYV-7bH_oblClQD1KF9LsTXo,68
18
- slidge/core/config.py,sha256=1HxHQ5BOnYghi8V5KbCQ6sUsNnXzAZAIoFA5PrMqH4E,6060
19
- slidge/core/gateway.py,sha256=5GImsd1b7KKcwIZcNSWgVieezrnp5dJCv7IlmOvWkRc,41192
18
+ slidge/core/config.py,sha256=yFD1RGBZ6Xl115oMqDiF7DFkShlNjF9HEB56Eb_BR_E,7664
19
+ slidge/core/gateway.py,sha256=G_ZX8ibKFe-oEXXOY-eRVkZjqRb_VdadIhTezwZEUCA,41698
20
20
  slidge/core/pubsub.py,sha256=2Em3PvYz-PX-WM7ZqkEU9estNYCyQ--JdJq22DhrUlA,12145
21
- slidge/core/session.py,sha256=leHLahwpI5_hqR6xv0juw_BQJnpuPqVmhOIwYEiPnx8,29861
21
+ slidge/core/session.py,sha256=XJRZrtAmwqqjaqkLEUPK8xUBxdJrKeraubYYNiiuT7I,29897
22
22
  slidge/core/dispatcher/__init__.py,sha256=1EXcjXietUKlxEqdrCWCV3xZ3q_DSsjHoqWrPMbtYao,84
23
- slidge/core/dispatcher/caps.py,sha256=gISaHtFwFDXtkSrSsAkZfPiHQyXfmXg3v_YYU0w9iDg,2254
23
+ slidge/core/dispatcher/caps.py,sha256=7gvJLeushBtiyqCzBapzbwallH7l3tVN9PYe8MXiKIo,2237
24
24
  slidge/core/dispatcher/disco.py,sha256=xVPyBFnnkON-JjjM1oydRa-dqnDbwAZER2MGQACRhVk,2309
25
- slidge/core/dispatcher/presence.py,sha256=3qGsiMGzTDnOpMbCRJ8RCrRGiSU-pAyOLJHphbZF5pw,7043
26
- slidge/core/dispatcher/registration.py,sha256=Qa3fYZFJ4NaVz-FsnXorCmMQ9WyO7oZte1Zvro5f74E,3415
27
- slidge/core/dispatcher/search.py,sha256=bL5cwMOtsfnX5IH-t60S1PudpeMWZnF4-qksrKDuDyc,3411
25
+ slidge/core/dispatcher/presence.py,sha256=MkSOY7uZQnujTwhIU_nVxygTul63VBaPjTT3X4iTMu4,7090
26
+ slidge/core/dispatcher/registration.py,sha256=R4bsyiR8elbLKiFMYv2E54VAUg6qTo-_2CzdbCOZjrY,3419
27
+ slidge/core/dispatcher/search.py,sha256=j7LGht2M36FfhXRzcdZiV-8OdCVsVHb2vJb6p3pIbpk,3202
28
28
  slidge/core/dispatcher/session_dispatcher.py,sha256=ysgPhns7NgUxhmkgEwRv-yDkSnUIXEdc-FsgqDqQAkE,3466
29
- slidge/core/dispatcher/util.py,sha256=DsMWz7yS67E2Y2pcnXUkuVMTa9F_Ok0JIxjhqhdYpD0,6161
29
+ slidge/core/dispatcher/util.py,sha256=yxNevqjjLOzApLpqIRFHNQmpsahwZn55qCsNkOSXohc,6225
30
30
  slidge/core/dispatcher/vcard.py,sha256=qHZZShq3Iyvgh1FkcAgGhdKXF5m1VUqeb4EWkY0cbFw,5203
31
31
  slidge/core/dispatcher/message/__init__.py,sha256=gNeZZ0wtCI9JBqMe6tpumwV1TjY0mnPWTJc94uFTN-I,244
32
32
  slidge/core/dispatcher/message/chat_state.py,sha256=RbtM_nlZyvOHusZkDEP0TXA4wMp_N435490eE4wW8U0,2143
33
33
  slidge/core/dispatcher/message/marker.py,sha256=WZyf72_SM6sDGPMEOzhu93o2KbgxSxNF25jwsiM7h2A,2439
34
- slidge/core/dispatcher/message/message.py,sha256=RK1sX4cBSvPZLiUGYoT-pdCB01ll1761Ryr7Tu0AuBc,16202
34
+ slidge/core/dispatcher/message/message.py,sha256=h-oNEC6In3dH2TpMgRVyMAvZjvWDczaq8nowOzVz_R4,16231
35
35
  slidge/core/dispatcher/muc/__init__.py,sha256=60YUr0i8PCZEPyNNTynJueRbbxF5pqzdyVf8z_XFXmM,290
36
36
  slidge/core/dispatcher/muc/admin.py,sha256=1tDZ9hHD6q5SqCjsYOpDimPB3Iyl21YO5RnK1goEGso,3284
37
37
  slidge/core/dispatcher/muc/mam.py,sha256=7vfmMI_mJOIrc9KCbtTibJSowhZTBBFwXWc84Ikpu3I,2994
@@ -39,21 +39,21 @@ slidge/core/dispatcher/muc/misc.py,sha256=DgUCSVwcv7kD5xmkS59E-TGf9yWDZiu6NBHgVS
39
39
  slidge/core/dispatcher/muc/owner.py,sha256=dDAxpRaA8H_NJQNIyBNixck2oG4GHZeEQqPhKd7MmDQ,3359
40
40
  slidge/core/dispatcher/muc/ping.py,sha256=EgKKS9AvMnW-vINGcoGbtk6NdbN9A7zVaGfT5T7F6YE,1699
41
41
  slidge/core/mixins/__init__.py,sha256=Zea39CCwjJU5XfHwcYPEZ9Sin8z1BZxoV68G2RwC3nE,386
42
- slidge/core/mixins/attachment.py,sha256=6c1gZNydVZHgHB-_PjBLbT_9ntBSNG0AWqlv9Cgcku8,21906
42
+ slidge/core/mixins/attachment.py,sha256=k-4nwteql4TmhsZDLqPVgzVyuaRQzk-JEOG-k7w-VxM,23938
43
43
  slidge/core/mixins/avatar.py,sha256=0E0mQxdTUcJQrYXlBkYqkNl4bYuga4cIC1s4XA2rED8,5559
44
44
  slidge/core/mixins/base.py,sha256=getXMptzJwIc4fEbeMoJCSKcC3awi8UbKnx5FVCthjc,783
45
- slidge/core/mixins/db.py,sha256=lryMfRB-1-St2f7TZqW4sCl4xrWPWNlwgvn37T8wQPE,2678
45
+ slidge/core/mixins/db.py,sha256=MlaTJcZjOZAL77oise0kY12tvx-SzPLcxS29QxxwEMM,3160
46
46
  slidge/core/mixins/disco.py,sha256=mrYhWO9qpnLMAVtKKqwbDh6CNOH2dPNERpyfmWzZGg8,3684
47
47
  slidge/core/mixins/message.py,sha256=xk4bgiJF9ATe-rgtH4sHU8hUwecBF4KjGIujm90mbXQ,8014
48
- slidge/core/mixins/message_maker.py,sha256=NaIQzpjYHKMuuqfqncviB6AE-q50-YhhstxZ-WhLWhE,6546
49
- slidge/core/mixins/message_text.py,sha256=-hlGDzQ9dya-ZCPBe3v7UrpBetQa36N3YIACRxBt8Yc,9632
50
- slidge/core/mixins/presence.py,sha256=DPNatkVIkw7GmHrkyUlUgsGhp0VUgaUgSBuhGalbe8c,9791
48
+ slidge/core/mixins/message_maker.py,sha256=jdY0dhpN_RcKwajI0XrV194ruNW120Gfdq333B1Fv1o,6556
49
+ slidge/core/mixins/message_text.py,sha256=Unzmwr8TnPC2uuZ2fFiIalOGpQF7YYyT1AbB8i0l6Qc,9637
50
+ slidge/core/mixins/presence.py,sha256=SiKd-RUIHybrxs5Ie6Ig3aunQ7Is9t6c7TOm-2HbjbA,10252
51
51
  slidge/core/mixins/recipient.py,sha256=b0uFnpym-hOFgYxGjXT1xQcZ4YRbDSBftPcNWLzSwEI,1336
52
52
  slidge/db/__init__.py,sha256=EBDH1JSEhgqYcli2Bw11CRC749wJk8AOucgBzmhDSvU,105
53
53
  slidge/db/avatar.py,sha256=MXFd1oe0eL5CCUYbc5CpsIcbio3cY3xVoKt39RAoj9I,8240
54
54
  slidge/db/meta.py,sha256=NtjGWcqPfG7uPfwR_cC6_23zyo8ftqgKX8CbP9IBq6U,2185
55
- slidge/db/models.py,sha256=8z5bbaEINfU1Qx12iyHu4zRD9s3PwC6oUOz-PyBoqYg,12841
56
- slidge/db/store.py,sha256=ZksmZlFaTia7eWy_trALc_iTEkk2x7CIlEQeYkHW21g,19408
55
+ slidge/db/models.py,sha256=n_Xmgtfug-ygE2CL0ezvEgeyaSM6dgn7YfaYm78B_7c,12819
56
+ slidge/db/store.py,sha256=yhvYgoXpbueprjVUXBmA25wFyEbzDXMxVzqv8aZi5tw,20308
57
57
  slidge/db/alembic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  slidge/db/alembic/env.py,sha256=hsBlRNs0zF5diSHGRSa8Fi3qRVQDA2rJdR41AEIdvxc,1642
59
59
  slidge/db/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
@@ -62,7 +62,7 @@ slidge/group/__init__.py,sha256=yFt7cHqeaKIMN6f9ZyhhspOcJJvBtLedGv-iICG7lto,258
62
62
  slidge/group/archive.py,sha256=gNHpGlUstPtBcnBgOarSYzkZHo9E8bNxl_ZY3wM6wHg,6108
63
63
  slidge/group/bookmarks.py,sha256=eiXtlgirE59dBi1BT1349wCrGuHDkCoak1phCepzkqI,7653
64
64
  slidge/group/participant.py,sha256=2yDJL2bL0w-EiWjq6V96UEhXUgmih5-GpKsoKfG2zdY,18734
65
- slidge/group/room.py,sha256=lXzICLkv980o0JS-3HZTbvf2MD0ZlIw4-Y-xiU6lEew,49481
65
+ slidge/group/room.py,sha256=03c6WrSptld6SqA_ndxPJOU9Cz7s0Qq-R8PvXdi-r3A,49523
66
66
  slidge/slixfix/__init__.py,sha256=LvaYZQYjr4l_45AYYpod1dB3MUaZye18vKF-4H8Bm20,4758
67
67
  slidge/slixfix/delivery_receipt.py,sha256=JmogxsiXYEbTmCM4fvC5wkQs0jBsaJtKl4j_B_18riE,1415
68
68
  slidge/slixfix/roster.py,sha256=DjjHQqCsKsPChUxV7S0Pm4IAgjfrwgm5tcTdJi3N_gY,1670
@@ -84,12 +84,12 @@ slidge/util/archive_msg.py,sha256=hGNquu38ouSWSc-kz_oAYPXwjhUVZNSedIpwkrXHSd0,18
84
84
  slidge/util/conf.py,sha256=Wv-xr1fQfz6jDCBpj2e5Nm-igMpdIjsYsVfoY8grJoo,7380
85
85
  slidge/util/jid_escaping.py,sha256=QJ2Yj_j1gTmiO9g2r187iVCu7kia_O5ABhRiLAO2TG4,1073
86
86
  slidge/util/lock.py,sha256=ZnUi3LGiz271-YeYKo9JzxovJCoSwlP9P65pNyHIO9o,1029
87
- slidge/util/test.py,sha256=_E6er2BtQlpyzTUmp4u8C9KhBYzbLrmTwSVgxGObKHU,13988
88
- slidge/util/types.py,sha256=nilphTeJU3yb0MSqb86tZeWXis495oDvHSDDBs0hn_4,5747
89
- slidge/util/util.py,sha256=PBdHtcRIQi6Dy-yHASS_xiRGQ4Pv4i6QSSZAAELPnmQ,9536
90
- slidge-0.3.1.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
91
- slidge-0.3.1.dist-info/METADATA,sha256=UEfy2L6fzCOoHu2yfUhlh0SI7cQW6LrC6XNhJySfYoo,5054
92
- slidge-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
93
- slidge-0.3.1.dist-info/entry_points.txt,sha256=py3_x834fFJ2TEzPd18Wt2DnysdAfuVqJ5zzBrXbAZs,44
94
- slidge-0.3.1.dist-info/top_level.txt,sha256=2LRjDYHaGZ5ieCMF8xy58JIiabRMzX-MGMbCZwfE17c,7
95
- slidge-0.3.1.dist-info/RECORD,,
87
+ slidge/util/test.py,sha256=o86wX1ksnvAgN47k5facDPc4zgiK7J5oWyThgJsv_vQ,13986
88
+ slidge/util/types.py,sha256=BA1vhByeaeni3Z5FTIVYafORpU44OOJOTMByL-QqfLs,5870
89
+ slidge/util/util.py,sha256=g6jFu5xgAtRA9FBCIBJT3079t_bQKZYOUvi9L5pTbbk,9595
90
+ slidge-0.3.3.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
91
+ slidge-0.3.3.dist-info/METADATA,sha256=Q_WFVFDDTzOc3M9AuY_eRg5zpclCTxaNzqeP0Rp7gRU,5054
92
+ slidge-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
93
+ slidge-0.3.3.dist-info/entry_points.txt,sha256=py3_x834fFJ2TEzPd18Wt2DnysdAfuVqJ5zzBrXbAZs,44
94
+ slidge-0.3.3.dist-info/top_level.txt,sha256=2LRjDYHaGZ5ieCMF8xy58JIiabRMzX-MGMbCZwfE17c,7
95
+ slidge-0.3.3.dist-info/RECORD,,
File without changes