slidge 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. {slidge-0.3.1 → slidge-0.3.2}/PKG-INFO +1 -1
  2. slidge-0.3.2/docs/source/admin/config/index.rst +57 -0
  3. {slidge-0.3.1 → slidge-0.3.2}/docs/source/conf.py +1 -1
  4. {slidge-0.3.1 → slidge-0.3.2}/pyproject.toml +3 -0
  5. {slidge-0.3.1 → slidge-0.3.2}/slidge/contact/contact.py +1 -1
  6. {slidge-0.3.1 → slidge-0.3.2}/slidge/contact/roster.py +6 -1
  7. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/config.py +43 -9
  8. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/message/message.py +1 -0
  9. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/presence.py +1 -0
  10. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/registration.py +1 -1
  11. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/search.py +1 -5
  12. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/util.py +3 -1
  13. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/gateway.py +12 -1
  14. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/attachment.py +62 -8
  15. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/message_maker.py +1 -1
  16. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/message_text.py +1 -1
  17. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/presence.py +26 -8
  18. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/session.py +2 -0
  19. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/models.py +1 -1
  20. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/store.py +29 -1
  21. {slidge-0.3.1 → slidge-0.3.2}/slidge/group/room.py +5 -2
  22. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/test.py +3 -2
  23. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/util.py +6 -4
  24. {slidge-0.3.1 → slidge-0.3.2}/slidge.egg-info/PKG-INFO +1 -1
  25. {slidge-0.3.1 → slidge-0.3.2}/slidge.egg-info/SOURCES.txt +1 -0
  26. {slidge-0.3.1 → slidge-0.3.2}/tests/conftest.py +9 -5
  27. {slidge-0.3.1 → slidge-0.3.2}/tests/test_attachment.py +131 -8
  28. {slidge-0.3.1 → slidge-0.3.2}/tests/test_backfill.py +0 -1
  29. {slidge-0.3.1 → slidge-0.3.2}/tests/test_config.py +1 -1
  30. {slidge-0.3.1 → slidge-0.3.2}/tests/test_db/test_store.py +41 -2
  31. slidge-0.3.2/tests/test_gateway_wide_reaction_restrictions.py +187 -0
  32. {slidge-0.3.1 → slidge-0.3.2}/tests/test_muc.py +23 -4
  33. {slidge-0.3.1 → slidge-0.3.2}/tests/test_muc_subject.py +0 -4
  34. {slidge-0.3.1 → slidge-0.3.2}/tests/test_session.py +0 -4
  35. {slidge-0.3.1 → slidge-0.3.2}/tests/test_session_2.py +0 -4
  36. {slidge-0.3.1 → slidge-0.3.2}/tests/test_set_name_before_fill.py +0 -4
  37. {slidge-0.3.1 → slidge-0.3.2}/tests/test_shakespeare.py +10 -11
  38. {slidge-0.3.1 → slidge-0.3.2}/tests/test_util.py +2 -2
  39. {slidge-0.3.1 → slidge-0.3.2}/tests/test_vcard.py +0 -4
  40. {slidge-0.3.1 → slidge-0.3.2}/uv.lock +17 -1
  41. slidge-0.3.1/docs/source/admin/config/index.rst +0 -35
  42. {slidge-0.3.1 → slidge-0.3.2}/.gitignore +0 -0
  43. {slidge-0.3.1 → slidge-0.3.2}/.pre-commit-config.yaml +0 -0
  44. {slidge-0.3.1 → slidge-0.3.2}/.woodpecker/container-ci.yaml +0 -0
  45. {slidge-0.3.1 → slidge-0.3.2}/.woodpecker/docs.yaml +0 -0
  46. {slidge-0.3.1 → slidge-0.3.2}/.woodpecker/package.yaml +0 -0
  47. {slidge-0.3.1 → slidge-0.3.2}/.woodpecker/test.yaml +0 -0
  48. {slidge-0.3.1 → slidge-0.3.2}/Dockerfile +0 -0
  49. {slidge-0.3.1 → slidge-0.3.2}/LICENSE +0 -0
  50. {slidge-0.3.1 → slidge-0.3.2}/README.md +0 -0
  51. {slidge-0.3.1 → slidge-0.3.2}/commitlint.config.js +0 -0
  52. {slidge-0.3.1 → slidge-0.3.2}/dev/assets/5x5.png +0 -0
  53. {slidge-0.3.1 → slidge-0.3.2}/dev/assets/slidge-color-small.png +0 -0
  54. {slidge-0.3.1 → slidge-0.3.2}/dev/assets/slidge-color.png +0 -0
  55. {slidge-0.3.1 → slidge-0.3.2}/dev/assets/slidge-mono-black.png +0 -0
  56. {slidge-0.3.1 → slidge-0.3.2}/dev/assets/slidge-mono-white.png +0 -0
  57. {slidge-0.3.1 → slidge-0.3.2}/dev/assets/slidge.svg +0 -0
  58. {slidge-0.3.1 → slidge-0.3.2}/dev/confs/movim.env +0 -0
  59. {slidge-0.3.1 → slidge-0.3.2}/dev/confs/nginx.conf +0 -0
  60. {slidge-0.3.1 → slidge-0.3.2}/dev/confs/slidge-dev.ini +0 -0
  61. {slidge-0.3.1 → slidge-0.3.2}/dev/confs/slidge-example.ini +0 -0
  62. {slidge-0.3.1 → slidge-0.3.2}/dev/hot-reload.sh +0 -0
  63. {slidge-0.3.1 → slidge-0.3.2}/dev/prettify_tests.py +0 -0
  64. {slidge-0.3.1 → slidge-0.3.2}/doap.xml +0 -0
  65. {slidge-0.3.1 → slidge-0.3.2}/docker-compose.yml +0 -0
  66. {slidge-0.3.1 → slidge-0.3.2}/docs/Makefile +0 -0
  67. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/attachments.rst +0 -0
  68. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/component.rst +0 -0
  69. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/daemon.rst +0 -0
  70. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/examples/ejabberd.yaml +0 -0
  71. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/examples/index.rst +0 -0
  72. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/examples/prosody.cfg.lua +0 -0
  73. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/index.rst +0 -0
  74. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/install.rst +0 -0
  75. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/note.rst +0 -0
  76. {slidge-0.3.1 → slidge-0.3.2}/docs/source/admin/privilege.rst +0 -0
  77. {slidge-0.3.1 → slidge-0.3.2}/docs/source/codeberg.svg +0 -0
  78. {slidge-0.3.1 → slidge-0.3.2}/docs/source/dev/contributing.rst +0 -0
  79. {slidge-0.3.1 → slidge-0.3.2}/docs/source/dev/design.rst +0 -0
  80. {slidge-0.3.1 → slidge-0.3.2}/docs/source/dev/howto.rst +0 -0
  81. {slidge-0.3.1 → slidge-0.3.2}/docs/source/dev/index.rst +0 -0
  82. {slidge-0.3.1 → slidge-0.3.2}/docs/source/dev/tutorial.rst +0 -0
  83. {slidge-0.3.1 → slidge-0.3.2}/docs/source/glossary.rst +0 -0
  84. {slidge-0.3.1 → slidge-0.3.2}/docs/source/index.rst +0 -0
  85. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/commands.rst +0 -0
  86. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/contacts.rst +0 -0
  87. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/foxyproxy.png +0 -0
  88. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/gajim.png +0 -0
  89. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/index.rst +0 -0
  90. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/low_profile.rst +0 -0
  91. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/movim1.png +0 -0
  92. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/movim2.png +0 -0
  93. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/note.rst +0 -0
  94. {slidge-0.3.1 → slidge-0.3.2}/docs/source/user/register.rst +0 -0
  95. {slidge-0.3.1 → slidge-0.3.2}/setup.cfg +0 -0
  96. {slidge-0.3.1 → slidge-0.3.2}/slidge/__init__.py +0 -0
  97. {slidge-0.3.1 → slidge-0.3.2}/slidge/__main__.py +0 -0
  98. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/__init__.py +0 -0
  99. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/adhoc.py +0 -0
  100. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/admin.py +0 -0
  101. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/base.py +0 -0
  102. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/categories.py +0 -0
  103. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/chat_command.py +0 -0
  104. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/register.py +0 -0
  105. {slidge-0.3.1 → slidge-0.3.2}/slidge/command/user.py +0 -0
  106. {slidge-0.3.1 → slidge-0.3.2}/slidge/contact/__init__.py +0 -0
  107. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/__init__.py +0 -0
  108. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/__init__.py +0 -0
  109. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/caps.py +0 -0
  110. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/disco.py +0 -0
  111. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/message/__init__.py +0 -0
  112. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/message/chat_state.py +0 -0
  113. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/message/marker.py +0 -0
  114. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/muc/__init__.py +0 -0
  115. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/muc/admin.py +0 -0
  116. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/muc/mam.py +0 -0
  117. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/muc/misc.py +0 -0
  118. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/muc/owner.py +0 -0
  119. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/muc/ping.py +0 -0
  120. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/session_dispatcher.py +0 -0
  121. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/dispatcher/vcard.py +0 -0
  122. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/__init__.py +0 -0
  123. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/avatar.py +0 -0
  124. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/base.py +0 -0
  125. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/db.py +0 -0
  126. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/disco.py +0 -0
  127. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/message.py +0 -0
  128. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/mixins/recipient.py +0 -0
  129. {slidge-0.3.1 → slidge-0.3.2}/slidge/core/pubsub.py +0 -0
  130. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/__init__.py +0 -0
  131. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/alembic/__init__.py +0 -0
  132. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/alembic/env.py +0 -0
  133. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/alembic/script.py.mako +0 -0
  134. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/alembic/versions/cef02a8b1451_initial_schema.py +0 -0
  135. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/avatar.py +0 -0
  136. {slidge-0.3.1 → slidge-0.3.2}/slidge/db/meta.py +0 -0
  137. {slidge-0.3.1 → slidge-0.3.2}/slidge/group/__init__.py +0 -0
  138. {slidge-0.3.1 → slidge-0.3.2}/slidge/group/archive.py +0 -0
  139. {slidge-0.3.1 → slidge-0.3.2}/slidge/group/bookmarks.py +0 -0
  140. {slidge-0.3.1 → slidge-0.3.2}/slidge/group/participant.py +0 -0
  141. {slidge-0.3.1 → slidge-0.3.2}/slidge/main.py +0 -0
  142. {slidge-0.3.1 → slidge-0.3.2}/slidge/migration.py +0 -0
  143. {slidge-0.3.1 → slidge-0.3.2}/slidge/py.typed +0 -0
  144. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/__init__.py +0 -0
  145. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/delivery_receipt.py +0 -0
  146. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/link_preview/__init__.py +0 -0
  147. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/link_preview/link_preview.py +0 -0
  148. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/link_preview/stanza.py +0 -0
  149. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/roster.py +0 -0
  150. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0077/__init__.py +0 -0
  151. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0077/register.py +0 -0
  152. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0077/stanza.py +0 -0
  153. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0100/__init__.py +0 -0
  154. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0100/gateway.py +0 -0
  155. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0100/stanza.py +0 -0
  156. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0153/__init__.py +0 -0
  157. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  158. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0292/__init__.py +0 -0
  159. {slidge-0.3.1 → slidge-0.3.2}/slidge/slixfix/xep_0292/vcard4.py +0 -0
  160. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/__init__.py +0 -0
  161. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/archive_msg.py +0 -0
  162. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/conf.py +0 -0
  163. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/jid_escaping.py +0 -0
  164. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/lock.py +0 -0
  165. {slidge-0.3.1 → slidge-0.3.2}/slidge/util/types.py +0 -0
  166. {slidge-0.3.1 → slidge-0.3.2}/slidge.egg-info/dependency_links.txt +0 -0
  167. {slidge-0.3.1 → slidge-0.3.2}/slidge.egg-info/entry_points.txt +0 -0
  168. {slidge-0.3.1 → slidge-0.3.2}/slidge.egg-info/requires.txt +0 -0
  169. {slidge-0.3.1 → slidge-0.3.2}/slidge.egg-info/top_level.txt +0 -0
  170. {slidge-0.3.1 → slidge-0.3.2}/superduper/__init__.py +0 -0
  171. {slidge-0.3.1 → slidge-0.3.2}/superduper/__main__.py +0 -0
  172. {slidge-0.3.1 → slidge-0.3.2}/superduper/contact.py +0 -0
  173. {slidge-0.3.1 → slidge-0.3.2}/superduper/gateway.py +0 -0
  174. {slidge-0.3.1 → slidge-0.3.2}/superduper/group.py +0 -0
  175. {slidge-0.3.1 → slidge-0.3.2}/superduper/legacy_client.py +0 -0
  176. {slidge-0.3.1 → slidge-0.3.2}/superduper/session.py +0 -0
  177. {slidge-0.3.1 → slidge-0.3.2}/superduper/util.py +0 -0
  178. {slidge-0.3.1 → slidge-0.3.2}/tests/test_adhoc/test_access.py +0 -0
  179. {slidge-0.3.1 → slidge-0.3.2}/tests/test_adhoc/test_confirmation.py +0 -0
  180. {slidge-0.3.1 → slidge-0.3.2}/tests/test_adhoc/test_form.py +0 -0
  181. {slidge-0.3.1 → slidge-0.3.2}/tests/test_adhoc/test_reported.py +0 -0
  182. {slidge-0.3.1 → slidge-0.3.2}/tests/test_avatar.py +0 -0
  183. {slidge-0.3.1 → slidge-0.3.2}/tests/test_chat_commands.py +0 -0
  184. {slidge-0.3.1 → slidge-0.3.2}/tests/test_db/test_user.py +0 -0
  185. {slidge-0.3.1 → slidge-0.3.2}/tests/test_feature_restriction.py +0 -0
  186. {slidge-0.3.1 → slidge-0.3.2}/tests/test_mam_archivable.py +0 -0
  187. {slidge-0.3.1 → slidge-0.3.2}/tests/test_mds.py +0 -0
  188. {slidge-0.3.1 → slidge-0.3.2}/tests/test_resourceprep.py +0 -0
  189. {slidge-0.3.1 → slidge-0.3.2}/tests/test_stanza_link_preview.py +0 -0
  190. {slidge-0.3.1 → slidge-0.3.2}/tests/test_type_conversion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidge
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: XMPP bridging framework
5
5
  Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -0,0 +1,57 @@
1
+ Configuration
2
+ =============
3
+
4
+ .. include:: ../note.rst
5
+
6
+ By default, slidge uses all config files found in ``/etc/slidge/conf.d/*``.
7
+ You can change this using the ``SLIDGE_CONF_DIR`` env var, eg
8
+ ``SLIDGE_CONF_DIR=/path/dir1:/path/dir2:/path/dir3``.
9
+
10
+ It is recommended to use ``/etc/slidge/conf.d/`` to store configuration options
11
+ common to all slidge components (eg, attachment handling, logging options,
12
+ etc.), and to specify a plugin-specific file on startup, eg:
13
+
14
+ .. code-block:: bash
15
+
16
+ slidge -c /etc/slidge/superduper.conf
17
+
18
+ .. note::
19
+ For the debian unofficial package, just edit the ``/etc/slidge/conf.d/common.conf`` and
20
+ ``/etc/slidge/*.conf`` files, and use :ref:`Debian packages (systemd)` to
21
+ launch slidge.
22
+
23
+ Command-line arguments
24
+ ----------------------
25
+
26
+ .. code-block:: text
27
+
28
+ -h, --help show this help message and exit
29
+ -c, --config CONFIG Path to a INI config file. [env var: SLIDGE_CONFIG]
30
+ --log-config LOG_CONFIG
31
+ Path to a INI config file to personalise logging output.
32
+ -q, --quiet loglevel=WARNING (unused if --log-config is specified) [env var: SLIDGE_QUIET]
33
+ -d, --debug loglevel=DEBUG (unused if --log-config is specified) [env var: SLIDGE_DEBUG]
34
+ --version show program's version number and exit
35
+
36
+
37
+ Regarding the ``--log-config`` argument, refer to the `official python documentation
38
+ <https://docs.python.org/3/library/logging.config.html#configuration-file-format>`_
39
+ for the syntax.
40
+
41
+ Other options
42
+ -------------
43
+
44
+ .. warning::
45
+
46
+ Because of an ugly mess that will soon™ be fixed, it is impossible to use
47
+ the config file to turn off boolean arguments that are true by default.
48
+ As a workaround, use CLI args instead, e.g., ``--some-opt=false``.
49
+
50
+ The following options can be used:
51
+
52
+ * in a config file (see top of this page);
53
+ * as command line arguments, prepended with ``--``, e.g., ``--some-option=value``;
54
+ * as environment variables, upper case, prepended with ``SLIDGE_``,
55
+ and with dashes substituted with underscores, e.g., ``SLIDGE_SOME_OPTION=value``.
56
+
57
+ .. config-obj:: slidge.core.config
@@ -39,9 +39,9 @@ extensions = [
39
39
  "sphinx.ext.extlinks",
40
40
  # "sphinx.ext.viewcode", # crashes build unfortunately
41
41
  "sphinx.ext.autodoc.typehints",
42
- "sphinx.ext.autosectionlabel",
43
42
  "sphinxarg.ext",
44
43
  "autoapi.extension",
44
+ "slidge_sphinx_extensions.config_obj",
45
45
  ]
46
46
 
47
47
  autodoc_typehints = "description"
@@ -63,6 +63,7 @@ dev = [
63
63
  "types-pillow>=10.2.0.20240822",
64
64
  "utidylib>=0.10",
65
65
  "xmldiff>=2.7.0",
66
+ "slidge-sphinx-extensions>=0.2.1,<0.3",
66
67
  ]
67
68
 
68
69
  [tool.mypy]
@@ -106,6 +107,8 @@ filterwarnings = [
106
107
  "ignore::UserWarning:slidge",
107
108
  "ignore:coroutine 'XMLStream._end_stream_wait' was never awaited.*:RuntimeWarning"
108
109
  ]
110
+ log_cli = true
111
+ log_level = "DEBUG"
109
112
 
110
113
  [[tool.uv.index]]
111
114
  name = "codeberg"
@@ -411,7 +411,7 @@ class LegacyContact(
411
411
  # we only broadcast pubsub events for contacts added to the roster
412
412
  # so if something was set before, we need to push it now
413
413
  self.added_to_roster = True
414
- self.send_last_presence()
414
+ self.send_last_presence(force=True)
415
415
 
416
416
  async def __broadcast_pubsub_items(self) -> None:
417
417
  if not self.is_friend:
@@ -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
 
@@ -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)
@@ -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)
@@ -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:
@@ -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,22 @@ 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)
99
+ stored_contact = self.__stored()
100
+ if stored_contact is None:
101
+ return
102
+ stored_contact.cached_presence = True
103
+ for k, v in new._asdict().items():
104
+ setattr(stored_contact, k, v)
105
+ if self.__is_contact() and self._updating_info:
106
+ return
107
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
93
108
  try:
94
- self.commit()
109
+ orm.add(stored_contact)
95
110
  except InvalidRequestError:
96
- self.commit(merge=True)
111
+ stored_contact = orm.merge(stored_contact)
112
+ orm.add(stored_contact)
113
+
114
+ orm.commit()
97
115
 
98
116
  def _make_presence(
99
117
  self,
@@ -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)