slidge 0.3.0b4__py3-none-any.whl → 0.3.2__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/__init__.py CHANGED
@@ -4,7 +4,6 @@ The main slidge package.
4
4
  Contains importable classes for a minimal function :term:`Legacy Module`.
5
5
  """
6
6
 
7
- import sys
8
7
  import warnings
9
8
  from importlib.metadata import PackageNotFoundError, version
10
9
  from typing import Any
slidge/command/user.py CHANGED
@@ -34,8 +34,9 @@ class Search(Command):
34
34
  async def run(
35
35
  self, session: Optional[AnyBaseSession], _ifrom: JID, *args: str
36
36
  ) -> Union[Form, SearchResult, None]:
37
+ assert session is not None
38
+ await session.ready
37
39
  if args:
38
- assert session is not None
39
40
  return await session.on_search(
40
41
  {self.xmpp.SEARCH_FIELDS[0].var: " ".join(args)}
41
42
  )
@@ -70,6 +71,8 @@ class SyncContacts(Command):
70
71
  CATEGORY = CONTACTS
71
72
 
72
73
  async def run(self, session: Optional[AnyBaseSession], _ifrom, *_) -> Confirmation:
74
+ assert session is not None
75
+ await session.ready
73
76
  return Confirmation(
74
77
  prompt="Are you sure you want to sync your roster?",
75
78
  success=None,
@@ -133,6 +136,7 @@ class ListContacts(Command):
133
136
  self, session: Optional[AnyBaseSession], _ifrom: JID, *_
134
137
  ) -> TableResult:
135
138
  assert session is not None
139
+ await session.ready
136
140
  contacts = sorted(
137
141
  session.contacts, key=lambda c: c.name.casefold() if c.name else ""
138
142
  )
@@ -152,7 +156,7 @@ class ListGroups(Command):
152
156
 
153
157
  async def run(self, session, _ifrom, *_):
154
158
  assert session is not None
155
- await session.bookmarks.fill()
159
+ await session.ready
156
160
  groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
157
161
  return TableResult(
158
162
  description="Your groups",
@@ -201,6 +205,7 @@ class CreateGroup(Command):
201
205
 
202
206
  async def run(self, session: Optional[AnyBaseSession], _ifrom, *_):
203
207
  assert session is not None
208
+ await session.ready
204
209
  contacts = session.contacts.known_contacts(only_friends=True)
205
210
  return Form(
206
211
  title="Create a new group",
@@ -260,20 +265,34 @@ class Preferences(Command):
260
265
  instructions=self.HELP,
261
266
  fields=fields,
262
267
  handler=self.finish, # type:ignore
268
+ handler_kwargs={"previous": current},
263
269
  )
264
270
 
265
271
  async def finish(
266
- self, form_values: UserPreferences, session: Optional[AnyBaseSession], *_
272
+ self,
273
+ form_values: UserPreferences,
274
+ session: Optional[AnyBaseSession],
275
+ *_,
276
+ previous,
267
277
  ) -> str:
268
278
  assert session is not None
279
+ if previous == form_values:
280
+ return "No preference was changed"
281
+
269
282
  user = session.user
270
283
  user.preferences.update(form_values) # type:ignore
271
- if form_values["sync_avatar"]:
284
+ self.xmpp.store.users.update(user)
285
+
286
+ try:
287
+ await session.on_preferences(previous, form_values) # type:ignore[arg-type]
288
+ except NotImplementedError:
289
+ pass
290
+
291
+ if not previous["sync_avatar"] and form_values["sync_avatar"]:
272
292
  await self.xmpp.fetch_user_avatar(session)
273
293
  else:
274
294
  user.avatar_hash = None
275
295
 
276
- self.xmpp.store.users.update(user)
277
296
  return "Your preferences have been updated."
278
297
 
279
298
 
@@ -308,7 +327,7 @@ class LeaveGroup(Command):
308
327
 
309
328
  async def run(self, session, _ifrom, *_):
310
329
  assert session is not None
311
- await session.bookmarks.fill()
330
+ await session.ready
312
331
  groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
313
332
  return Form(
314
333
  title="Leave a group",
slidge/contact/contact.py CHANGED
@@ -325,6 +325,7 @@ class LegacyContact(
325
325
  email: Optional[str] = None,
326
326
  country: Optional[str] = None,
327
327
  locality: Optional[str] = None,
328
+ pronouns: Optional[str] = None,
328
329
  ) -> None:
329
330
  vcard = VCard4()
330
331
  vcard.add_impp(f"xmpp:{self.jid.bare}")
@@ -357,6 +358,8 @@ class LegacyContact(
357
358
  vcard.add_address(country, locality)
358
359
  elif country:
359
360
  vcard.add_address(country, locality)
361
+ if pronouns:
362
+ vcard["pronouns"]["text"] = pronouns
360
363
 
361
364
  self.stored.vcard = str(vcard)
362
365
  self.stored.vcard_fetched = True
@@ -408,7 +411,7 @@ class LegacyContact(
408
411
  # we only broadcast pubsub events for contacts added to the roster
409
412
  # so if something was set before, we need to push it now
410
413
  self.added_to_roster = True
411
- self.send_last_presence()
414
+ self.send_last_presence(force=True)
412
415
 
413
416
  async def __broadcast_pubsub_items(self) -> None:
414
417
  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
@@ -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)
@@ -1,6 +1,14 @@
1
1
  import logging
2
2
 
3
- from slixmpp import JID, CoroutineCallback, Iq, Message, Presence, StanzaPath
3
+ from slixmpp import (
4
+ JID,
5
+ CoroutineCallback,
6
+ Iq,
7
+ MatchXMLMask,
8
+ Message,
9
+ Presence,
10
+ StanzaPath,
11
+ )
4
12
  from slixmpp.exceptions import XMPPError
5
13
 
6
14
  from ..util import DispatcherMixin, exceptions_to_xmpp_errors
@@ -23,6 +31,15 @@ class MucMiscMixin(DispatcherMixin):
23
31
  )
24
32
  xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject)
25
33
  xmpp.add_event_handler("groupchat_message_error", self.__on_group_chat_error)
34
+ xmpp.register_handler(
35
+ CoroutineCallback(
36
+ "muc_thread_subject",
37
+ MatchXMLMask(
38
+ "<message xmlns='jabber:component:accept' type='groupchat'><subject/><thread/></message>"
39
+ ),
40
+ self.on_thread_subject,
41
+ )
42
+ )
26
43
 
27
44
  async def __on_group_chat_error(self, msg: Message) -> None:
28
45
  condition = msg["error"].get_condition()
@@ -43,7 +60,7 @@ class MucMiscMixin(DispatcherMixin):
43
60
  # (not sure why?), but is of no consequence
44
61
  log.debug("%s was not in the resources of %s", resource, muc)
45
62
  else:
46
- log.info(
63
+ log.debug(
47
64
  "Removed %s from the resources of %s because of error", resource, muc
48
65
  )
49
66
 
@@ -106,6 +123,14 @@ class MucMiscMixin(DispatcherMixin):
106
123
  )
107
124
  await muc.on_set_subject(msg["subject"])
108
125
 
126
+ @exceptions_to_xmpp_errors
127
+ async def on_thread_subject(self, msg: Message):
128
+ if msg["body"]:
129
+ return
130
+ session, muc, thread = await self._get_session_recipient_thread(msg)
131
+ assert thread is not None
132
+ await muc.on_set_thread_subject(thread, msg["subject"]) # type:ignore[union-attr]
133
+
109
134
 
110
135
  KICKABLE_ERRORS = {
111
136
  "gone",
@@ -4,6 +4,7 @@ from slixmpp import JID, Presence
4
4
  from slixmpp.exceptions import XMPPError
5
5
 
6
6
  from ...contact.roster import ContactIsUser
7
+ from ...util.types import AnyBaseSession
7
8
  from ...util.util import merge_resources
8
9
  from ..session import BaseSession
9
10
  from .util import DispatcherMixin, exceptions_to_xmpp_errors
@@ -70,6 +71,7 @@ class PresenceHandlerMixin(DispatcherMixin):
70
71
  return
71
72
 
72
73
  await contact.on_friend_accept()
74
+ contact.send_last_presence(force=True)
73
75
 
74
76
  @exceptions_to_xmpp_errors
75
77
  async def _handle_unsubscribed(self, pres: Presence) -> None:
@@ -98,7 +100,7 @@ class PresenceHandlerMixin(DispatcherMixin):
98
100
  reply.send()
99
101
 
100
102
  @exceptions_to_xmpp_errors
101
- async def on_presence(self, p: Presence):
103
+ async def on_presence(self, p: Presence) -> None:
102
104
  if p.get_plugin("muc_join", check=True):
103
105
  # handled in on_groupchat_join
104
106
  # without this early return, since we switch from and to in this
@@ -112,31 +114,8 @@ class PresenceHandlerMixin(DispatcherMixin):
112
114
 
113
115
  pto = p.get_to()
114
116
  if pto == self.xmpp.boundjid.bare:
115
- session.log.debug("Received a presence from %s", p.get_from())
116
- if (ptype := p.get_type()) not in _USEFUL_PRESENCES:
117
- return
118
- if not session.user.preferences.get("sync_presence", False):
119
- session.log.debug("User does not want to sync their presence")
120
- return
121
- # NB: get_type() returns either a proper presence type or
122
- # a presence show if available. Weird, weird, weird slix.
123
- resources = self.xmpp.roster[self.xmpp.boundjid.bare][
124
- p.get_from()
125
- ].resources
126
- try:
127
- await session.on_presence(
128
- p.get_from().resource,
129
- ptype, # type: ignore
130
- p["status"],
131
- resources,
132
- merge_resources(resources),
133
- )
134
- except NotImplementedError:
135
- pass
136
- if p.get_type() == "available":
137
- await self.xmpp.pubsub.on_presence_available(p, None)
138
- for contact in session.contacts:
139
- await self.xmpp.pubsub.on_presence_available(p, contact)
117
+ await self._on_presence_to_component(session, p)
118
+ return
140
119
 
141
120
  if p.get_type() == "available":
142
121
  try:
@@ -181,6 +160,33 @@ class PresenceHandlerMixin(DispatcherMixin):
181
160
  )
182
161
  error_stanza.send()
183
162
 
163
+ async def _on_presence_to_component(
164
+ self, session: AnyBaseSession, p: Presence
165
+ ) -> None:
166
+ session.log.debug("Received a presence from %s", p.get_from())
167
+ if (ptype := p.get_type()) not in _USEFUL_PRESENCES:
168
+ return
169
+ if not session.user.preferences.get("sync_presence", False):
170
+ session.log.debug("User does not want to sync their presence")
171
+ return
172
+ # NB: get_type() returns either a proper presence type or
173
+ # a presence show if available. Weird, weird, weird slix.
174
+ resources = self.xmpp.roster[self.xmpp.boundjid.bare][p.get_from()].resources
175
+ try:
176
+ await session.on_presence(
177
+ p.get_from().resource,
178
+ ptype, # type: ignore
179
+ p["status"],
180
+ resources,
181
+ merge_resources(resources),
182
+ )
183
+ except NotImplementedError:
184
+ pass
185
+ if p.get_type() == "available":
186
+ await self.xmpp.pubsub.on_presence_available(p, None)
187
+ for contact in session.contacts:
188
+ await self.xmpp.pubsub.on_presence_available(p, contact)
189
+
184
190
 
185
191
  _USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"}
186
192
 
@@ -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