slidge 0.3.0b4__tar.gz → 0.3.1__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 (189) hide show
  1. {slidge-0.3.0b4 → slidge-0.3.1}/PKG-INFO +1 -1
  2. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/__init__.py +0 -1
  3. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/user.py +25 -6
  4. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/contact/contact.py +3 -0
  5. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/muc/misc.py +27 -2
  6. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/presence.py +31 -26
  7. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/db.py +23 -1
  8. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/message_maker.py +2 -2
  9. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/presence.py +2 -2
  10. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/pubsub.py +1 -1
  11. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/session.py +11 -0
  12. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/group/archive.py +3 -0
  13. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/group/participant.py +19 -0
  14. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/group/room.py +12 -0
  15. slidge-0.3.1/slidge/slixfix/xep_0292/vcard4.py +24 -0
  16. {slidge-0.3.0b4 → slidge-0.3.1}/slidge.egg-info/PKG-INFO +1 -1
  17. {slidge-0.3.0b4 → slidge-0.3.1}/slidge.egg-info/SOURCES.txt +1 -1
  18. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_avatar.py +1 -1
  19. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_muc.py +2 -5
  20. slidge-0.3.0b4/tests/test_empty_subject.py → slidge-0.3.1/tests/test_muc_subject.py +70 -2
  21. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_session_2.py +1 -1
  22. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_shakespeare.py +2 -2
  23. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_util.py +9 -1
  24. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_vcard.py +4 -1
  25. slidge-0.3.0b4/slidge/slixfix/xep_0292/vcard4.py +0 -14
  26. {slidge-0.3.0b4 → slidge-0.3.1}/.gitignore +0 -0
  27. {slidge-0.3.0b4 → slidge-0.3.1}/.pre-commit-config.yaml +0 -0
  28. {slidge-0.3.0b4 → slidge-0.3.1}/.woodpecker/container-ci.yaml +0 -0
  29. {slidge-0.3.0b4 → slidge-0.3.1}/.woodpecker/docs.yaml +0 -0
  30. {slidge-0.3.0b4 → slidge-0.3.1}/.woodpecker/package.yaml +0 -0
  31. {slidge-0.3.0b4 → slidge-0.3.1}/.woodpecker/test.yaml +0 -0
  32. {slidge-0.3.0b4 → slidge-0.3.1}/Dockerfile +0 -0
  33. {slidge-0.3.0b4 → slidge-0.3.1}/LICENSE +0 -0
  34. {slidge-0.3.0b4 → slidge-0.3.1}/README.md +0 -0
  35. {slidge-0.3.0b4 → slidge-0.3.1}/commitlint.config.js +0 -0
  36. {slidge-0.3.0b4 → slidge-0.3.1}/dev/assets/5x5.png +0 -0
  37. {slidge-0.3.0b4 → slidge-0.3.1}/dev/assets/slidge-color-small.png +0 -0
  38. {slidge-0.3.0b4 → slidge-0.3.1}/dev/assets/slidge-color.png +0 -0
  39. {slidge-0.3.0b4 → slidge-0.3.1}/dev/assets/slidge-mono-black.png +0 -0
  40. {slidge-0.3.0b4 → slidge-0.3.1}/dev/assets/slidge-mono-white.png +0 -0
  41. {slidge-0.3.0b4 → slidge-0.3.1}/dev/assets/slidge.svg +0 -0
  42. {slidge-0.3.0b4 → slidge-0.3.1}/dev/confs/movim.env +0 -0
  43. {slidge-0.3.0b4 → slidge-0.3.1}/dev/confs/nginx.conf +0 -0
  44. {slidge-0.3.0b4 → slidge-0.3.1}/dev/confs/slidge-dev.ini +0 -0
  45. {slidge-0.3.0b4 → slidge-0.3.1}/dev/confs/slidge-example.ini +0 -0
  46. {slidge-0.3.0b4 → slidge-0.3.1}/dev/hot-reload.sh +0 -0
  47. {slidge-0.3.0b4 → slidge-0.3.1}/dev/prettify_tests.py +0 -0
  48. {slidge-0.3.0b4 → slidge-0.3.1}/doap.xml +0 -0
  49. {slidge-0.3.0b4 → slidge-0.3.1}/docker-compose.yml +0 -0
  50. {slidge-0.3.0b4 → slidge-0.3.1}/docs/Makefile +0 -0
  51. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/attachments.rst +0 -0
  52. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/component.rst +0 -0
  53. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/config/index.rst +0 -0
  54. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/daemon.rst +0 -0
  55. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/examples/ejabberd.yaml +0 -0
  56. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/examples/index.rst +0 -0
  57. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/examples/prosody.cfg.lua +0 -0
  58. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/index.rst +0 -0
  59. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/install.rst +0 -0
  60. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/note.rst +0 -0
  61. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/admin/privilege.rst +0 -0
  62. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/codeberg.svg +0 -0
  63. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/conf.py +0 -0
  64. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/dev/contributing.rst +0 -0
  65. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/dev/design.rst +0 -0
  66. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/dev/howto.rst +0 -0
  67. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/dev/index.rst +0 -0
  68. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/dev/tutorial.rst +0 -0
  69. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/glossary.rst +0 -0
  70. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/index.rst +0 -0
  71. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/commands.rst +0 -0
  72. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/contacts.rst +0 -0
  73. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/foxyproxy.png +0 -0
  74. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/gajim.png +0 -0
  75. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/index.rst +0 -0
  76. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/low_profile.rst +0 -0
  77. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/movim1.png +0 -0
  78. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/movim2.png +0 -0
  79. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/note.rst +0 -0
  80. {slidge-0.3.0b4 → slidge-0.3.1}/docs/source/user/register.rst +0 -0
  81. {slidge-0.3.0b4 → slidge-0.3.1}/pyproject.toml +0 -0
  82. {slidge-0.3.0b4 → slidge-0.3.1}/setup.cfg +0 -0
  83. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/__main__.py +0 -0
  84. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/__init__.py +0 -0
  85. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/adhoc.py +0 -0
  86. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/admin.py +0 -0
  87. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/base.py +0 -0
  88. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/categories.py +0 -0
  89. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/chat_command.py +0 -0
  90. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/command/register.py +0 -0
  91. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/contact/__init__.py +0 -0
  92. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/contact/roster.py +0 -0
  93. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/__init__.py +0 -0
  94. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/config.py +0 -0
  95. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/__init__.py +0 -0
  96. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/caps.py +0 -0
  97. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/disco.py +0 -0
  98. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/message/__init__.py +0 -0
  99. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/message/chat_state.py +0 -0
  100. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/message/marker.py +0 -0
  101. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/message/message.py +0 -0
  102. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/muc/__init__.py +0 -0
  103. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/muc/admin.py +0 -0
  104. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/muc/mam.py +0 -0
  105. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/muc/owner.py +0 -0
  106. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/muc/ping.py +0 -0
  107. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/registration.py +0 -0
  108. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/search.py +0 -0
  109. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/session_dispatcher.py +0 -0
  110. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/util.py +0 -0
  111. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/dispatcher/vcard.py +0 -0
  112. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/gateway.py +0 -0
  113. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/__init__.py +0 -0
  114. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/attachment.py +0 -0
  115. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/avatar.py +0 -0
  116. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/base.py +0 -0
  117. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/disco.py +0 -0
  118. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/message.py +0 -0
  119. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/message_text.py +0 -0
  120. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/core/mixins/recipient.py +0 -0
  121. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/__init__.py +0 -0
  122. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/alembic/__init__.py +0 -0
  123. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/alembic/env.py +0 -0
  124. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/alembic/script.py.mako +0 -0
  125. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/alembic/versions/cef02a8b1451_initial_schema.py +0 -0
  126. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/avatar.py +0 -0
  127. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/meta.py +0 -0
  128. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/models.py +0 -0
  129. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/db/store.py +0 -0
  130. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/group/__init__.py +0 -0
  131. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/group/bookmarks.py +0 -0
  132. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/main.py +0 -0
  133. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/migration.py +0 -0
  134. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/py.typed +0 -0
  135. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/__init__.py +0 -0
  136. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/delivery_receipt.py +0 -0
  137. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/link_preview/__init__.py +0 -0
  138. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/link_preview/link_preview.py +0 -0
  139. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/link_preview/stanza.py +0 -0
  140. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/roster.py +0 -0
  141. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0077/__init__.py +0 -0
  142. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0077/register.py +0 -0
  143. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0077/stanza.py +0 -0
  144. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0100/__init__.py +0 -0
  145. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0100/gateway.py +0 -0
  146. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0100/stanza.py +0 -0
  147. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0153/__init__.py +0 -0
  148. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  149. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/slixfix/xep_0292/__init__.py +0 -0
  150. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/__init__.py +0 -0
  151. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/archive_msg.py +0 -0
  152. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/conf.py +0 -0
  153. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/jid_escaping.py +0 -0
  154. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/lock.py +0 -0
  155. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/test.py +0 -0
  156. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/types.py +0 -0
  157. {slidge-0.3.0b4 → slidge-0.3.1}/slidge/util/util.py +0 -0
  158. {slidge-0.3.0b4 → slidge-0.3.1}/slidge.egg-info/dependency_links.txt +0 -0
  159. {slidge-0.3.0b4 → slidge-0.3.1}/slidge.egg-info/entry_points.txt +0 -0
  160. {slidge-0.3.0b4 → slidge-0.3.1}/slidge.egg-info/requires.txt +0 -0
  161. {slidge-0.3.0b4 → slidge-0.3.1}/slidge.egg-info/top_level.txt +0 -0
  162. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/__init__.py +0 -0
  163. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/__main__.py +0 -0
  164. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/contact.py +0 -0
  165. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/gateway.py +0 -0
  166. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/group.py +0 -0
  167. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/legacy_client.py +0 -0
  168. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/session.py +0 -0
  169. {slidge-0.3.0b4 → slidge-0.3.1}/superduper/util.py +0 -0
  170. {slidge-0.3.0b4 → slidge-0.3.1}/tests/conftest.py +0 -0
  171. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_adhoc/test_access.py +0 -0
  172. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_adhoc/test_confirmation.py +0 -0
  173. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_adhoc/test_form.py +0 -0
  174. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_adhoc/test_reported.py +0 -0
  175. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_attachment.py +0 -0
  176. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_backfill.py +0 -0
  177. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_chat_commands.py +0 -0
  178. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_config.py +0 -0
  179. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_db/test_store.py +0 -0
  180. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_db/test_user.py +0 -0
  181. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_feature_restriction.py +0 -0
  182. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_mam_archivable.py +0 -0
  183. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_mds.py +0 -0
  184. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_resourceprep.py +0 -0
  185. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_session.py +0 -0
  186. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_set_name_before_fill.py +0 -0
  187. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_stanza_link_preview.py +0 -0
  188. {slidge-0.3.0b4 → slidge-0.3.1}/tests/test_type_conversion.py +0 -0
  189. {slidge-0.3.0b4 → slidge-0.3.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidge
3
- Version: 0.3.0b4
3
+ Version: 0.3.1
4
4
  Summary: XMPP bridging framework
5
5
  Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -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
@@ -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",
@@ -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
@@ -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
@@ -98,7 +99,7 @@ class PresenceHandlerMixin(DispatcherMixin):
98
99
  reply.send()
99
100
 
100
101
  @exceptions_to_xmpp_errors
101
- async def on_presence(self, p: Presence):
102
+ async def on_presence(self, p: Presence) -> None:
102
103
  if p.get_plugin("muc_join", check=True):
103
104
  # handled in on_groupchat_join
104
105
  # without this early return, since we switch from and to in this
@@ -112,31 +113,8 @@ class PresenceHandlerMixin(DispatcherMixin):
112
113
 
113
114
  pto = p.get_to()
114
115
  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)
116
+ await self._on_presence_to_component(session, p)
117
+ return
140
118
 
141
119
  if p.get_type() == "available":
142
120
  try:
@@ -181,6 +159,33 @@ class PresenceHandlerMixin(DispatcherMixin):
181
159
  )
182
160
  error_stanza.send()
183
161
 
162
+ async def _on_presence_to_component(
163
+ self, session: AnyBaseSession, p: Presence
164
+ ) -> None:
165
+ session.log.debug("Received a presence from %s", p.get_from())
166
+ if (ptype := p.get_type()) not in _USEFUL_PRESENCES:
167
+ return
168
+ if not session.user.preferences.get("sync_presence", False):
169
+ session.log.debug("User does not want to sync their presence")
170
+ return
171
+ # NB: get_type() returns either a proper presence type or
172
+ # a presence show if available. Weird, weird, weird slix.
173
+ resources = self.xmpp.roster[self.xmpp.boundjid.bare][p.get_from()].resources
174
+ try:
175
+ await session.on_presence(
176
+ p.get_from().resource,
177
+ ptype, # type: ignore
178
+ p["status"],
179
+ resources,
180
+ merge_resources(resources),
181
+ )
182
+ except NotImplementedError:
183
+ pass
184
+ if p.get_type() == "available":
185
+ await self.xmpp.pubsub.on_presence_available(p, None)
186
+ for contact in session.contacts:
187
+ await self.xmpp.pubsub.on_presence_available(p, contact)
188
+
184
189
 
185
190
  _USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"}
186
191
 
@@ -2,7 +2,7 @@ import logging
2
2
  import typing
3
3
  from contextlib import contextmanager
4
4
 
5
- from ...db.models import Base, Contact, Participant, Room
5
+ from ...db.models import Base, Contact, Room
6
6
 
7
7
  if typing.TYPE_CHECKING:
8
8
  from slidge import BaseGateway
@@ -41,13 +41,35 @@ class UpdateInfoMixin(DBMixin):
41
41
  def __init__(self, *args, **kwargs) -> None:
42
42
  super().__init__(*args, **kwargs)
43
43
  self._updating_info = False
44
+ self.__deserialize()
45
+
46
+ def __deserialize(self):
44
47
  if self.stored.extra_attributes is not None:
45
48
  self.deserialize_extra_attributes(self.stored.extra_attributes)
46
49
 
50
+ def refresh(self) -> None:
51
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
52
+ orm.add(self.stored)
53
+ orm.refresh(self.stored)
54
+ self.__deserialize()
55
+
47
56
  def serialize_extra_attributes(self) -> dict | None:
57
+ """
58
+ If you want custom attributes of your instance to be stored persistently
59
+ to the DB, here is where you have to return them as a dict to be used in
60
+ `deserialize_extra_attributes()`.
61
+
62
+ """
48
63
  return None
49
64
 
50
65
  def deserialize_extra_attributes(self, data: dict) -> None:
66
+ """
67
+ This is where you get the dict that you passed in
68
+ `serialize_extra_attributes()`.
69
+
70
+ ⚠ Since it is serialized as json, dictionary keys are converted to strings!
71
+ Be sure to convert to other types if necessary.
72
+ """
51
73
  pass
52
74
 
53
75
  @contextmanager
@@ -1,7 +1,7 @@
1
+ import uuid
1
2
  import warnings
2
3
  from datetime import datetime, timezone
3
4
  from typing import TYPE_CHECKING, Iterable, Optional, cast
4
- from uuid import uuid4
5
5
 
6
6
  from slixmpp import JID, Message
7
7
  from slixmpp.types import MessageTypes
@@ -95,7 +95,7 @@ class MessageMaker(BaseSender):
95
95
  msg["stanza_id"]["id"] = i
96
96
  msg["stanza_id"]["by"] = self.muc.jid # type: ignore
97
97
  elif self.USE_STANZA_ID:
98
- msg["stanza_id"]["id"] = str(uuid4())
98
+ msg["stanza_id"]["id"] = str(uuid.uuid4())
99
99
  msg["stanza_id"]["by"] = self.muc.jid # type: ignore
100
100
 
101
101
  def _legacy_to_xmpp(self, legacy_id: LegacyMessageType) -> str:
@@ -279,9 +279,9 @@ class PresenceMixin(BaseSender, DBMixin):
279
279
  pass
280
280
 
281
281
 
282
- def get_last_seen_fallback(last_seen: datetime):
282
+ def get_last_seen_fallback(last_seen: datetime) -> tuple[str, bool]:
283
283
  now = datetime.now(tz=timezone.utc)
284
284
  if now - last_seen < timedelta(days=7):
285
- return f"Last seen {last_seen:%A %H:%M GMT}", True
285
+ return f"Last seen {last_seen:%A %H:%M %p GMT}", True
286
286
  else:
287
287
  return f"Last seen {last_seen:%b %-d %Y}", False
@@ -120,7 +120,7 @@ class PubSubComponent(NamedLockMixin, BasePlugin):
120
120
  try:
121
121
  iq = await self.xmpp.plugin["xep_0030"].get_info(from_)
122
122
  except (IqError, IqTimeout):
123
- log.debug("Could get disco#info of %s, ignoring", from_)
123
+ log.debug("Could not get disco#info of %s, ignoring", from_)
124
124
  return []
125
125
  info = iq["disco_info"]
126
126
  return info["features"]
@@ -539,6 +539,17 @@ class BaseSession(
539
539
  """
540
540
  raise NotImplementedError
541
541
 
542
+ async def on_preferences(
543
+ self, previous: dict[str, Any], new: dict[str, Any]
544
+ ) -> None:
545
+ """
546
+ This is called when the user updates their preferences.
547
+
548
+ Override this if you need set custom preferences field and need to trigger
549
+ something when a preference has changed.
550
+ """
551
+ raise NotImplementedError
552
+
542
553
  def __reset_ready(self) -> None:
543
554
  self.ready = self.xmpp.loop.create_future()
544
555
 
@@ -181,6 +181,9 @@ def archivable(msg: Message) -> bool:
181
181
  if msg.get_plugin("displayed", check=True):
182
182
  return True
183
183
 
184
+ if msg["thread"] and msg["subject"]:
185
+ return True
186
+
184
187
  return False
185
188
 
186
189
 
@@ -22,6 +22,7 @@ from ..util.types import (
22
22
  CachedPresence,
23
23
  Hat,
24
24
  LegacyMessageType,
25
+ LegacyThreadType,
25
26
  MessageOrPresenceTypeVar,
26
27
  MucAffiliation,
27
28
  MucRole,
@@ -526,6 +527,24 @@ class LegacyParticipant(
526
527
  msg.xml.append(ET.Element(f"{{{msg.namespace}}}subject"))
527
528
  self._send(msg, full_jid)
528
529
 
530
+ def set_thread_subject(
531
+ self,
532
+ thread: LegacyThreadType,
533
+ subject: str | None,
534
+ when: Optional[datetime] = None,
535
+ ) -> None:
536
+ msg = self._make_message()
537
+ msg["thread"] = str(thread)
538
+ if when is not None:
539
+ msg["delay"].set_stamp(when)
540
+ msg["delay"]["from"] = self.muc.jid
541
+ if subject:
542
+ msg["subject"] = subject
543
+ else:
544
+ # may be simplified if slixmpp lets it do it more easily some day
545
+ msg.xml.append(ET.Element(f"{{{msg.namespace}}}subject"))
546
+ self._send(msg)
547
+
529
548
 
530
549
  def escape_nickname(muc_jid: JID, nickname: str) -> tuple[str, JID]:
531
550
  nickname = nickname_no_illegal = strip_illegal_chars(nickname)
@@ -43,6 +43,7 @@ from ..util.types import (
43
43
  LegacyGroupIdType,
44
44
  LegacyMessageType,
45
45
  LegacyParticipantType,
46
+ LegacyThreadType,
46
47
  LegacyUserIdType,
47
48
  Mention,
48
49
  MucAffiliation,
@@ -1335,6 +1336,17 @@ class LegacyMUC(
1335
1336
  """
1336
1337
  raise NotImplementedError
1337
1338
 
1339
+ async def on_set_thread_subject(
1340
+ self, thread: LegacyThreadType, subject: str
1341
+ ) -> None:
1342
+ """
1343
+ Triggered when the user requests changing the subject of a specific thread.
1344
+
1345
+ :param thread: Legacy identifier of the thread
1346
+ :param subject: The new subject for this thread.
1347
+ """
1348
+ raise NotImplementedError
1349
+
1338
1350
  @property
1339
1351
  def participants_filled(self) -> bool:
1340
1352
  return self.stored.participants_filled
@@ -0,0 +1,24 @@
1
+ from slixmpp import register_stanza_plugin, __version_info__
2
+ from slixmpp.plugins.base import BasePlugin, register_plugin
3
+ from slixmpp.plugins.xep_0292.stanza import NS, _VCardTextElementBase, VCard4
4
+
5
+
6
+ class VCard4Provider(BasePlugin):
7
+ name = "xep_0292_provider"
8
+ description = "VCard4 Provider"
9
+ dependencies = {"xep_0030"}
10
+
11
+ def plugin_init(self) -> None:
12
+ self.xmpp.plugin["xep_0030"].add_feature(NS)
13
+
14
+
15
+
16
+
17
+ register_plugin(VCard4Provider)
18
+
19
+
20
+ if __version_info__[0] <= 1 and __version_info__[1] <= 11:
21
+ class Pronouns(_VCardTextElementBase):
22
+ name = plugin_attrib = "pronouns"
23
+
24
+ register_stanza_plugin(VCard4, Pronouns)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidge
3
- Version: 0.3.0b4
3
+ Version: 0.3.1
4
4
  Summary: XMPP bridging framework
5
5
  Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -164,11 +164,11 @@ tests/test_avatar.py
164
164
  tests/test_backfill.py
165
165
  tests/test_chat_commands.py
166
166
  tests/test_config.py
167
- tests/test_empty_subject.py
168
167
  tests/test_feature_restriction.py
169
168
  tests/test_mam_archivable.py
170
169
  tests/test_mds.py
171
170
  tests/test_muc.py
171
+ tests/test_muc_subject.py
172
172
  tests/test_resourceprep.py
173
173
  tests/test_session.py
174
174
  tests/test_session_2.py
@@ -205,7 +205,7 @@ class BaseMUC(BaseNoMUC):
205
205
 
206
206
  def setUp(self):
207
207
  self.patch = unittest.mock.patch(
208
- "slidge.core.mixins.message_maker.uuid4", return_value="uuid4"
208
+ "uuid.uuid4", return_value="uuid4"
209
209
  )
210
210
  self.patch.start()
211
211
  super().setUp()
@@ -268,9 +268,6 @@ class Base(ClearSessionMixin, SlidgeTest):
268
268
  cls.patches = [
269
269
  unittest.mock.patch("uuid.uuid4", return_value="uuid"),
270
270
  unittest.mock.patch("slidge.group.room.uuid4", return_value="uuid"),
271
- unittest.mock.patch(
272
- "slidge.core.mixins.message_maker.uuid4", return_value="uuid"
273
- ),
274
271
  ]
275
272
  for p in cls.patches:
276
273
  p.start()
@@ -2903,7 +2900,7 @@ class TestMuc(Base):
2903
2900
  from="room-private@aim.shakespeare.lit/firstwitch"
2904
2901
  to="romeo@montague.lit/movim">
2905
2902
  <show>away</show>
2906
- <status>blabla -- Last seen {last_seen:%A %H:%M GMT}</status>
2903
+ <status>blabla -- Last seen {last_seen:%A %H:%M %p GMT}</status>
2907
2904
  <idle xmlns="urn:xmpp:idle:1"
2908
2905
  since="{dt}" />
2909
2906
  <x xmlns="http://jabber.org/protocol/muc#user">
@@ -2922,7 +2919,7 @@ class TestMuc(Base):
2922
2919
  from="firstwitch@aim.shakespeare.lit/slidge"
2923
2920
  to="romeo@montague.lit">
2924
2921
  <show>away</show>
2925
- <status>blabla -- Last seen {last_seen:%A %H:%M GMT}</status>
2922
+ <status>blabla -- Last seen {last_seen:%A %H:%M %p GMT}</status>
2926
2923
  <idle xmlns="urn:xmpp:idle:1"
2927
2924
  since="{dt}" />
2928
2925
  <c xmlns="http://jabber.org/protocol/caps"
@@ -40,7 +40,7 @@ class MUC(LegacyMUC):
40
40
  self.type = MucType.GROUP
41
41
 
42
42
  @pytest.mark.usefixtures("avatar")
43
- class TestEmptySubject(AvatarFixtureMixin, SlidgeTest):
43
+ class TestMUCSubject(AvatarFixtureMixin, SlidgeTest):
44
44
  plugin = globals()
45
45
  xmpp: Gateway
46
46
 
@@ -97,7 +97,7 @@ class TestEmptySubject(AvatarFixtureMixin, SlidgeTest):
97
97
 
98
98
  def test_empty_subject(self):
99
99
  muc = self.run_coro(self.romeo_session.bookmarks.by_legacy_id("group"))
100
- with unittest.mock.patch("slidge.core.mixins.message_maker.uuid4", return_value="uuid"), unittest.mock.patch("uuid.uuid4", return_value="uuid"):
100
+ with unittest.mock.patch("uuid.uuid4", return_value="uuid"):
101
101
  self.recv( # language=XML
102
102
  f"""
103
103
  <presence from="romeo@montague.lit/movim"
@@ -137,3 +137,71 @@ class TestEmptySubject(AvatarFixtureMixin, SlidgeTest):
137
137
  """,
138
138
  use_values=False,
139
139
  )
140
+
141
+ def test_set_thread_subject(self):
142
+ muc: MUC = self.run_coro(self.romeo_session.bookmarks.by_legacy_id("group"))
143
+ muc.add_user_resource("movim")
144
+ with unittest.mock.patch("uuid.uuid4", return_value="uuid"):
145
+ juliet_participant = self.run_coro(muc.get_participant("juliet"))
146
+ juliet_participant.set_thread_subject("legacy-thread-id", "some-subject")
147
+ self.send( # language=XML
148
+ """
149
+ <message xmlns="jabber:component:accept"
150
+ type="groupchat"
151
+ from="group@aim.shakespeare.lit/juliet"
152
+ to="romeo@montague.lit/movim">
153
+ <stanza-id xmlns="urn:xmpp:sid:0"
154
+ id="uuid"
155
+ by="group@aim.shakespeare.lit" />
156
+ <thread>legacy-thread-id</thread>
157
+ <subject>some-subject</subject>
158
+ <occupant-id xmlns="urn:xmpp:occupant-id:0"
159
+ id="uuid" />
160
+ </message>
161
+ """
162
+ )
163
+
164
+ def test_user_set_thread_subject(self):
165
+ muc: MUC = self.run_coro(self.romeo_session.bookmarks.by_legacy_id("group"))
166
+ muc.add_user_resource("movim")
167
+ with unittest.mock.patch("slidge.group.room.LegacyMUC.on_set_subject") as on_set_subject, unittest.mock.patch("slidge.group.room.LegacyMUC.on_set_thread_subject") as on_set_thread_subject:
168
+ self.recv( # language=XML
169
+ """
170
+ <message xmlns="jabber:component:accept"
171
+ type="groupchat"
172
+ from="romeo@montague.lit/movim"
173
+ to="group@aim.shakespeare.lit">
174
+ <stanza-id xmlns="urn:xmpp:sid:0"
175
+ id="uuid"
176
+ by="group@aim.shakespeare.lit" />
177
+ <thread>thread-id</thread>
178
+ <subject>some-subject</subject>
179
+ <occupant-id xmlns="urn:xmpp:occupant-id:0"
180
+ id="uuid" />
181
+ </message>
182
+ """
183
+ )
184
+ on_set_subject.assert_not_called()
185
+ on_set_thread_subject.assert_awaited_once()
186
+ assert on_set_thread_subject.call_args[0] == ("thread-id", "some-subject")
187
+
188
+ with unittest.mock.patch("slidge.group.room.LegacyMUC.on_set_subject") as on_set_subject, unittest.mock.patch("slidge.group.room.LegacyMUC.on_set_thread_subject") as on_set_thread_subject:
189
+ self.recv( # language=XML
190
+ """
191
+ <message xmlns="jabber:component:accept"
192
+ type="groupchat"
193
+ from="romeo@montague.lit/movim"
194
+ to="group@aim.shakespeare.lit">
195
+ <stanza-id xmlns="urn:xmpp:sid:0"
196
+ id="uuid"
197
+ by="group@aim.shakespeare.lit" />
198
+ <thread>thread-id</thread>
199
+ <subject>some-subject</subject>
200
+ <body>some-body</body>
201
+ <occupant-id xmlns="urn:xmpp:occupant-id:0"
202
+ id="uuid" />
203
+ </message>
204
+ """
205
+ )
206
+ on_set_subject.assert_not_called()
207
+ on_set_thread_subject.assert_not_called()
@@ -639,7 +639,7 @@ class TestSession2(AvatarFixtureMixin, SlidgeTest):
639
639
  """,
640
640
  use_values=False,
641
641
  )
642
- with unittest.mock.patch("slidge.core.mixins.message_maker.uuid4", return_value="uuid"):
642
+ with unittest.mock.patch("uuid.uuid4", return_value="uuid"):
643
643
  part.react("msg-id", "♥")
644
644
  self.send( # language=XML
645
645
  """
@@ -1639,7 +1639,7 @@ class TestContact(ClearSessionMixin, SlidgeTest):
1639
1639
  juliet.away(status="Bye bye", last_seen=now)
1640
1640
 
1641
1641
  p = self.next_sent()
1642
- assert p["status"] == f"Bye bye -- Last seen {now:%A %H:%M GMT}"
1642
+ assert p["status"] == f"Bye bye -- Last seen {now:%A %H:%M %p GMT}"
1643
1643
  assert p["idle"]["since"] == now
1644
1644
  assert p["show"] == "away"
1645
1645
 
@@ -1651,7 +1651,7 @@ class TestContact(ClearSessionMixin, SlidgeTest):
1651
1651
 
1652
1652
  juliet.busy(last_seen=now)
1653
1653
  p = self.next_sent()
1654
- assert p["status"] == f"Last seen {now:%A %H:%M GMT}"
1654
+ assert p["status"] == f"Last seen {now:%A %H:%M %p GMT}"
1655
1655
  assert p["idle"]["since"] == now
1656
1656
  assert p["show"] == "dnd"
1657
1657