slidge 0.2.0a10__tar.gz → 0.2.0b1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. {slidge-0.2.0a10 → slidge-0.2.0b1}/PKG-INFO +1 -1
  2. {slidge-0.2.0a10 → slidge-0.2.0b1}/pyproject.toml +3 -3
  3. slidge-0.2.0b1/slidge/__main__.py +3 -0
  4. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/__version__.py +1 -1
  5. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/adhoc.py +31 -15
  6. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/admin.py +11 -4
  7. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/base.py +5 -2
  8. slidge-0.2.0b1/slidge/command/categories.py +13 -0
  9. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/chat_command.py +14 -1
  10. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/user.py +22 -10
  11. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/contact/roster.py +2 -0
  12. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/config.py +6 -3
  13. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/message/marker.py +2 -7
  14. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/muc/misc.py +3 -0
  15. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/muc/owner.py +1 -1
  16. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/util.py +23 -23
  17. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/attachment.py +24 -8
  18. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/lock.py +10 -8
  19. slidge-0.2.0b1/slidge/core/mixins/message.py +214 -0
  20. slidge-0.2.0a10/slidge/core/mixins/message.py → slidge-0.2.0b1/slidge/core/mixins/message_text.py +5 -208
  21. slidge-0.2.0b1/slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  22. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +1 -1
  23. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +12 -1
  24. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +6 -0
  25. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +7 -6
  26. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +4 -0
  27. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/models.py +4 -2
  28. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/store.py +18 -11
  29. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/group/bookmarks.py +23 -1
  30. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/group/participant.py +5 -5
  31. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/group/room.py +10 -1
  32. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/test.py +4 -4
  33. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/util.py +18 -0
  34. slidge-0.2.0a10/slidge/__main__.py +0 -4
  35. slidge-0.2.0a10/slidge/command/categories.py +0 -3
  36. {slidge-0.2.0a10 → slidge-0.2.0b1}/LICENSE +0 -0
  37. {slidge-0.2.0a10 → slidge-0.2.0b1}/README.md +0 -0
  38. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/__init__.py +0 -0
  39. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/__init__.py +0 -0
  40. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/command/register.py +0 -0
  41. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/contact/__init__.py +0 -0
  42. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/contact/contact.py +0 -0
  43. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/__init__.py +0 -0
  44. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/__init__.py +0 -0
  45. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/caps.py +0 -0
  46. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/disco.py +0 -0
  47. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/message/__init__.py +0 -0
  48. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/message/chat_state.py +0 -0
  49. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/message/message.py +0 -0
  50. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/muc/__init__.py +0 -0
  51. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/muc/admin.py +0 -0
  52. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/muc/mam.py +0 -0
  53. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/muc/ping.py +0 -0
  54. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/presence.py +0 -0
  55. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/registration.py +0 -0
  56. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/search.py +0 -0
  57. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/session_dispatcher.py +0 -0
  58. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/dispatcher/vcard.py +0 -0
  59. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/gateway.py +0 -0
  60. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/__init__.py +0 -0
  61. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/avatar.py +0 -0
  62. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/base.py +0 -0
  63. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/db.py +0 -0
  64. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/disco.py +0 -0
  65. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/message_maker.py +0 -0
  66. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/presence.py +0 -0
  67. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/mixins/recipient.py +0 -0
  68. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/pubsub.py +0 -0
  69. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/core/session.py +0 -0
  70. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/__init__.py +0 -0
  71. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/__init__.py +0 -0
  72. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/env.py +0 -0
  73. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/old_user_store.py +0 -0
  74. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/script.py.mako +0 -0
  75. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -0
  76. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -0
  77. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -0
  78. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -0
  79. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/45c24cc73c91_add_bob.py +0 -0
  80. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -0
  81. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -0
  82. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +0 -0
  83. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -0
  84. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -0
  85. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -0
  86. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/avatar.py +0 -0
  87. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/db/meta.py +0 -0
  88. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/group/__init__.py +0 -0
  89. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/group/archive.py +0 -0
  90. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/main.py +0 -0
  91. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/migration.py +0 -0
  92. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/py.typed +0 -0
  93. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/__init__.py +0 -0
  94. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/delivery_receipt.py +0 -0
  95. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/link_preview/__init__.py +0 -0
  96. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/link_preview/link_preview.py +0 -0
  97. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/link_preview/stanza.py +0 -0
  98. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/roster.py +0 -0
  99. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0077/__init__.py +0 -0
  100. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0077/register.py +0 -0
  101. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0077/stanza.py +0 -0
  102. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0100/__init__.py +0 -0
  103. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0100/gateway.py +0 -0
  104. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0100/stanza.py +0 -0
  105. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0153/__init__.py +0 -0
  106. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0153/stanza.py +0 -0
  107. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  108. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0264/__init__.py +0 -0
  109. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0264/stanza.py +0 -0
  110. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0264/thumbnail.py +0 -0
  111. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0292/__init__.py +0 -0
  112. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0292/vcard4.py +0 -0
  113. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0313/__init__.py +0 -0
  114. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0313/mam.py +0 -0
  115. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0313/stanza.py +0 -0
  116. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0317/__init__.py +0 -0
  117. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0317/hats.py +0 -0
  118. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0317/stanza.py +0 -0
  119. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0356_old/__init__.py +0 -0
  120. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0356_old/privilege.py +0 -0
  121. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0356_old/stanza.py +0 -0
  122. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0424/__init__.py +0 -0
  123. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0424/retraction.py +0 -0
  124. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0424/stanza.py +0 -0
  125. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0490/__init__.py +0 -0
  126. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0490/mds.py +0 -0
  127. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/slixfix/xep_0490/stanza.py +0 -0
  128. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/__init__.py +0 -0
  129. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/archive_msg.py +0 -0
  130. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/conf.py +0 -0
  131. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/db.py +0 -0
  132. {slidge-0.2.0a10 → slidge-0.2.0b1}/slidge/util/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge
3
- Version: 0.2.0a10
3
+ Version: 0.2.0b1
4
4
  Summary: XMPP bridging framework
5
5
  Home-page: https://sr.ht/~nicoco/slidge/
6
6
  License: AGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "slidge"
3
- version = "0.2.0alpha10"
3
+ version = "0.2.0beta1"
4
4
  description = "XMPP bridging framework"
5
5
  authors = ["Nicolas Cedilnik <nicoco@nicoco.fr>"]
6
6
  readme = "README.md"
@@ -51,13 +51,14 @@ xmldiff = "^2.5"
51
51
  types-pillow = "^9.5.0.0"
52
52
  pre-commit = "^3.3.0"
53
53
  coverage = "^7.2.7"
54
+ emoji = "*"
54
55
 
55
56
  [tool.poetry.group.dev.dependencies.slidge-dev-helpers]
56
57
  git = "https://git.sr.ht/~nicoco/slidge-dev-helpers"
57
58
  branch = "master"
58
59
 
59
60
  [tool.poetry.scripts]
60
- slidge = 'slidge.__main__:main'
61
+ slidge = 'slidge.main:main'
61
62
 
62
63
  [tool.mypy]
63
64
  check_untyped_defs = true
@@ -89,7 +90,6 @@ exclude_lines = [
89
90
  ]
90
91
 
91
92
  [tool.pytest.ini_options]
92
- log_cli = true
93
93
  log_level = "DEBUG"
94
94
  asyncio_mode = "strict"
95
95
  filterwarnings = [
@@ -0,0 +1,3 @@
1
+ from slidge.main import main
2
+
3
+ main()
@@ -2,4 +2,4 @@ from slidge.util.util import get_version # noqa: F401
2
2
 
3
3
  # this is modified before publish, but if someone cloned from the repo,
4
4
  # it can help
5
- __version__ = "0.2.0alpha10"
5
+ __version__ = "0.2.0beta1"
@@ -9,8 +9,11 @@ from slixmpp.exceptions import XMPPError
9
9
  from slixmpp.plugins.xep_0004 import Form as SlixForm # type: ignore[attr-defined]
10
10
  from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
11
11
 
12
+ from ..core import config
13
+ from ..util.util import strip_leading_emoji
12
14
  from . import Command, CommandResponseType, Confirmation, Form, TableResult
13
15
  from .base import FormField
16
+ from .categories import CommandCategory
14
17
 
15
18
  if TYPE_CHECKING:
16
19
  from ..core.gateway import BaseGateway
@@ -46,19 +49,19 @@ class AdhocProvider:
46
49
  return await self.__handle_result(session, result, adhoc_session)
47
50
 
48
51
  async def __handle_category_list(
49
- self, category: str, iq: Iq, adhoc_session: AdhocSessionType
52
+ self, category: CommandCategory, iq: Iq, adhoc_session: AdhocSessionType
50
53
  ) -> AdhocSessionType:
51
54
  try:
52
55
  session = self.xmpp.get_session_from_stanza(iq)
53
56
  except XMPPError:
54
57
  session = None
55
- commands = []
56
- for command in self._categories[category]:
58
+ commands: dict[str, Command] = {}
59
+ for command in self._categories[category.node]:
57
60
  try:
58
61
  command.raise_if_not_authorized(iq.get_from())
59
62
  except XMPPError:
60
63
  continue
61
- commands.append(command)
64
+ commands[command.NODE] = command
62
65
  if len(commands) == 0:
63
66
  raise XMPPError(
64
67
  "not-authorized", "There is no command you can run in this category"
@@ -66,7 +69,7 @@ class AdhocProvider:
66
69
  return await self.__handle_result(
67
70
  session,
68
71
  Form(
69
- category,
72
+ category.name,
70
73
  "",
71
74
  [
72
75
  FormField(
@@ -74,8 +77,11 @@ class AdhocProvider:
74
77
  label="Command",
75
78
  type="list-single",
76
79
  options=[
77
- {"label": command.NAME, "value": str(i)}
78
- for i, command in enumerate(commands)
80
+ {
81
+ "label": strip_leading_emoji_if_needed(command.NAME),
82
+ "value": command.NODE,
83
+ }
84
+ for command in commands.values()
79
85
  ],
80
86
  )
81
87
  ],
@@ -86,12 +92,12 @@ class AdhocProvider:
86
92
 
87
93
  async def __handle_category_choice(
88
94
  self,
89
- commands: list[Command],
95
+ commands: dict[str, Command],
90
96
  form_values: dict[str, str],
91
97
  session: "BaseSession[Any, Any]",
92
98
  jid: JID,
93
99
  ):
94
- command = commands[int(form_values["command"])]
100
+ command = commands[form_values["command"]]
95
101
  result = await self.__wrap_handler(command.run, session, jid)
96
102
  return result
97
103
 
@@ -207,19 +213,23 @@ class AdhocProvider:
207
213
  self.xmpp.plugin["xep_0050"].add_command( # type: ignore[no-untyped-call]
208
214
  jid=jid,
209
215
  node=command.NODE,
210
- name=command.NAME,
216
+ name=strip_leading_emoji_if_needed(command.NAME),
211
217
  handler=partial(self.__wrap_initial_handler, command),
212
218
  )
213
219
  else:
214
- if category not in self._categories:
215
- self._categories[category] = list[Command]()
220
+ if isinstance(category, str):
221
+ category = CommandCategory(category, category)
222
+ node = category.node
223
+ name = category.name
224
+ if node not in self._categories:
225
+ self._categories[node] = list[Command]()
216
226
  self.xmpp.plugin["xep_0050"].add_command( # type: ignore[no-untyped-call]
217
227
  jid=jid,
218
- node=category,
219
- name=category,
228
+ node=node,
229
+ name=strip_leading_emoji_if_needed(name),
220
230
  handler=partial(self.__handle_category_list, category),
221
231
  )
222
- self._categories[category].append(command)
232
+ self._categories[node].append(command)
223
233
 
224
234
  async def get_items(self, jid: JID, node: str, iq: Iq) -> DiscoItems:
225
235
  """
@@ -262,4 +272,10 @@ class AdhocProvider:
262
272
  return filtered_items
263
273
 
264
274
 
275
+ def strip_leading_emoji_if_needed(text: str) -> str:
276
+ if config.STRIP_LEADING_EMOJI_ADHOC:
277
+ return strip_leading_emoji(text)
278
+ return text
279
+
280
+
265
281
  log = logging.getLogger(__name__)
@@ -11,6 +11,7 @@ from slixmpp.exceptions import XMPPError
11
11
  from ..core import config
12
12
  from ..util.types import AnyBaseSession
13
13
  from .base import (
14
+ NODE_PREFIX,
14
15
  Command,
15
16
  CommandAccess,
16
17
  Confirmation,
@@ -21,6 +22,8 @@ from .base import (
21
22
  )
22
23
  from .categories import ADMINISTRATION
23
24
 
25
+ NODE_PREFIX = NODE_PREFIX + "admin/"
26
+
24
27
 
25
28
  class AdminCommand(Command):
26
29
  ACCESS = CommandAccess.ADMIN_ONLY
@@ -30,7 +33,8 @@ class AdminCommand(Command):
30
33
  class ListUsers(AdminCommand):
31
34
  NAME = "👤 List registered users"
32
35
  HELP = "List the users registered to this gateway"
33
- NODE = CHAT_COMMAND = "list_users"
36
+ CHAT_COMMAND = "list_users"
37
+ NODE = NODE_PREFIX + CHAT_COMMAND
34
38
 
35
39
  async def run(self, _session, _ifrom, *_):
36
40
  items = []
@@ -51,7 +55,8 @@ class ListUsers(AdminCommand):
51
55
  class SlidgeInfo(AdminCommand):
52
56
  NAME = "ℹ️ Server information"
53
57
  HELP = "List the users registered to this gateway"
54
- NODE = CHAT_COMMAND = "info"
58
+ CHAT_COMMAND = "info"
59
+ NODE = NODE_PREFIX + CHAT_COMMAND
55
60
  ACCESS = CommandAccess.ANY
56
61
 
57
62
  async def run(self, _session, _ifrom, *_):
@@ -105,7 +110,8 @@ class SlidgeInfo(AdminCommand):
105
110
  class DeleteUser(AdminCommand):
106
111
  NAME = "❌ Delete a user"
107
112
  HELP = "Unregister a user from the gateway"
108
- NODE = CHAT_COMMAND = "delete_user"
113
+ CHAT_COMMAND = "delete_user"
114
+ NODE = NODE_PREFIX + CHAT_COMMAND
109
115
 
110
116
  async def run(self, _session, _ifrom, *_):
111
117
  return Form(
@@ -141,7 +147,8 @@ class DeleteUser(AdminCommand):
141
147
  class ChangeLoglevel(AdminCommand):
142
148
  NAME = "📋 Change the verbosity of the logs"
143
149
  HELP = "Set the logging level"
144
- NODE = CHAT_COMMAND = "loglevel"
150
+ CHAT_COMMAND = "loglevel"
151
+ NODE = NODE_PREFIX + CHAT_COMMAND
145
152
 
146
153
  async def run(self, _session, _ifrom, *_):
147
154
  return Form(
@@ -25,9 +25,12 @@ from slixmpp.types import JidStr
25
25
  from ..core import config
26
26
  from ..util.types import AnyBaseSession, FieldType
27
27
 
28
+ NODE_PREFIX = "https://slidge.im/command/core/"
29
+
28
30
  if TYPE_CHECKING:
29
31
  from ..core.gateway import BaseGateway
30
32
  from ..core.session import BaseSession
33
+ from .categories import CommandCategory
31
34
 
32
35
 
33
36
  HandlerType = Union[
@@ -178,8 +181,8 @@ class Form:
178
181
  """
179
182
  form = SlixForm() # type: ignore[no-untyped-call]
180
183
  form["type"] = "form"
181
- form["instructions"] = self.instructions
182
184
  form["title"] = self.title
185
+ form["instructions"] = self.instructions
183
186
  for fi in self.fields:
184
187
  form.append(fi.get_xml())
185
188
  return form
@@ -347,7 +350,7 @@ class Command(ABC):
347
350
  Who can use this command
348
351
  """
349
352
 
350
- CATEGORY: Optional[str] = None
353
+ CATEGORY: Optional[Union[str, "CommandCategory"]] = None
351
354
  """
352
355
  If used, the command will be under this top-level category.
353
356
  Use the same string for several commands to group them.
@@ -0,0 +1,13 @@
1
+ from typing import NamedTuple
2
+
3
+ from .base import NODE_PREFIX
4
+
5
+
6
+ class CommandCategory(NamedTuple):
7
+ name: str
8
+ node: str
9
+
10
+
11
+ ADMINISTRATION = CommandCategory("🛷️ Slidge administration", NODE_PREFIX + "admin")
12
+ CONTACTS = CommandCategory("👤 Contacts", NODE_PREFIX + "contacts")
13
+ GROUPS = CommandCategory("👥 Groups", NODE_PREFIX + "groups")
@@ -13,6 +13,7 @@ from slixmpp.exceptions import XMPPError
13
13
  from slixmpp.types import JidStr, MessageTypes
14
14
 
15
15
  from . import Command, CommandResponseType, Confirmation, Form, TableResult
16
+ from .categories import CommandCategory
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from ..core.gateway import BaseGateway
@@ -280,7 +281,19 @@ class ChatCommandProvider:
280
281
  def _help(self, mfrom: JID):
281
282
  msg = "Available commands:"
282
283
  for c in sorted(
283
- self._commands.values(), key=lambda co: (co.CATEGORY or "", co.CHAT_COMMAND)
284
+ self._commands.values(),
285
+ key=lambda co: (
286
+ (
287
+ co.CATEGORY
288
+ if isinstance(co.CATEGORY, str)
289
+ else (
290
+ co.CATEGORY.name
291
+ if isinstance(co.CATEGORY, CommandCategory)
292
+ else ""
293
+ )
294
+ ),
295
+ co.CHAT_COMMAND,
296
+ ),
284
297
  ):
285
298
  try:
286
299
  c.raise_if_not_authorized(mfrom)
@@ -26,8 +26,8 @@ if TYPE_CHECKING:
26
26
  class Search(Command):
27
27
  NAME = "🔎 Search for contacts"
28
28
  HELP = "Search for contacts via this gateway"
29
- NODE = "search"
30
29
  CHAT_COMMAND = "find"
30
+ NODE = CONTACTS.node + "/" + CHAT_COMMAND
31
31
  ACCESS = CommandAccess.USER_LOGGED
32
32
  CATEGORY = CONTACTS
33
33
 
@@ -64,7 +64,8 @@ class SyncContacts(Command):
64
64
  "Synchronize your XMPP roster with your legacy contacts. "
65
65
  "Slidge will only add/remove/modify contacts in its dedicated roster group"
66
66
  )
67
- NODE = CHAT_COMMAND = "sync-contacts"
67
+ CHAT_COMMAND = "sync-contacts"
68
+ NODE = CONTACTS.node + "/" + CHAT_COMMAND
68
69
  ACCESS = CommandAccess.USER_LOGGED
69
70
  CATEGORY = CONTACTS
70
71
 
@@ -123,7 +124,8 @@ class SyncContacts(Command):
123
124
 
124
125
  class ListContacts(Command):
125
126
  NAME = HELP = "👤 List your legacy contacts"
126
- NODE = CHAT_COMMAND = "contacts"
127
+ CHAT_COMMAND = "contacts"
128
+ NODE = CONTACTS.node + "/" + CHAT_COMMAND
127
129
  ACCESS = CommandAccess.USER_LOGGED
128
130
  CATEGORY = CONTACTS
129
131
 
@@ -143,7 +145,8 @@ class ListContacts(Command):
143
145
 
144
146
  class ListGroups(Command):
145
147
  NAME = HELP = "👥 List your legacy groups"
146
- NODE = CHAT_COMMAND = "groups"
148
+ CHAT_COMMAND = "groups"
149
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
147
150
  ACCESS = CommandAccess.USER_LOGGED
148
151
  CATEGORY = GROUPS
149
152
 
@@ -162,7 +165,8 @@ class ListGroups(Command):
162
165
  class Login(Command):
163
166
  NAME = "🔐 Re-login to the legacy network"
164
167
  HELP = "Login to the legacy service"
165
- NODE = CHAT_COMMAND = "re-login"
168
+ CHAT_COMMAND = "re-login"
169
+ NODE = "https://slidge.im/command/core/" + CHAT_COMMAND
166
170
 
167
171
  ACCESS = CommandAccess.USER_NON_LOGGED
168
172
 
@@ -184,7 +188,8 @@ class Login(Command):
184
188
  class CreateGroup(Command):
185
189
  NAME = "🆕 New legacy group"
186
190
  HELP = "Create a group on the legacy service"
187
- NODE = CHAT_COMMAND = "create-group"
191
+ CHAT_COMMAND = "create-group"
192
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
188
193
  CATEGORY = GROUPS
189
194
 
190
195
  ACCESS = CommandAccess.USER_LOGGED
@@ -233,7 +238,8 @@ class CreateGroup(Command):
233
238
  class Preferences(Command):
234
239
  NAME = "⚙️ Preferences"
235
240
  HELP = "Customize the gateway behaviour to your liking"
236
- NODE = CHAT_COMMAND = "preferences"
241
+ CHAT_COMMAND = "preferences"
242
+ NODE = "https://slidge.im/command/core/preferences"
237
243
  ACCESS = CommandAccess.USER
238
244
 
239
245
  async def run(
@@ -268,7 +274,8 @@ class Preferences(Command):
268
274
  class Unregister(Command):
269
275
  NAME = "❌ Unregister from the gateway"
270
276
  HELP = "Unregister from the gateway"
271
- NODE = CHAT_COMMAND = "unregister"
277
+ CHAT_COMMAND = "unregister"
278
+ NODE = "https://slidge.im/command/core/unregister"
272
279
  ACCESS = CommandAccess.USER
273
280
 
274
281
  async def run(
@@ -290,7 +297,8 @@ class Unregister(Command):
290
297
 
291
298
  class LeaveGroup(Command):
292
299
  NAME = HELP = "❌ Leave a legacy group"
293
- NODE = CHAT_COMMAND = "leave-group"
300
+ CHAT_COMMAND = "leave-group"
301
+ NODE = GROUPS.node + "/" + CHAT_COMMAND
294
302
  ACCESS = CommandAccess.USER_LOGGED
295
303
  CATEGORY = GROUPS
296
304
 
@@ -305,7 +313,10 @@ class LeaveGroup(Command):
305
313
  FormField(
306
314
  "group",
307
315
  "Group name",
308
- options=[{"label": g.name, "value": g.name} for g in groups],
316
+ type="list-single",
317
+ options=[
318
+ {"label": g.name, "value": str(i)} for i, g in enumerate(groups)
319
+ ],
309
320
  )
310
321
  ],
311
322
  handler=self.confirm, # type:ignore
@@ -329,3 +340,4 @@ class LeaveGroup(Command):
329
340
  @staticmethod
330
341
  async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC):
331
342
  await session.on_leave_group(group.legacy_id)
343
+ await session.bookmarks.remove(group, reason="You left this group via slidge.")
@@ -154,6 +154,8 @@ class LegacyRoster(
154
154
  try:
155
155
  with contact.updating_info():
156
156
  await contact.avatar_wrap_update_info()
157
+ except XMPPError:
158
+ raise
157
159
  except Exception as e:
158
160
  raise XMPPError("internal-server-error", str(e))
159
161
  contact._caps_ver = await contact.get_caps_ver(contact.jid)
@@ -154,9 +154,6 @@ LAST_SEEN_FALLBACK__DOC = (
154
154
  QR_TIMEOUT = 60
155
155
  QR_TIMEOUT__DOC = "Timeout for QR code flashing confirmation."
156
156
 
157
- DOWNLOAD_CHUNK_SIZE = 1024
158
- DOWNLOAD_CHUNK_SIZE__DOC = "Chunk size when slidge needs to download files using HTTP."
159
-
160
157
  LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND = False
161
158
  LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND__DOC = (
162
159
  "If the legacy service does not support last message correction but supports"
@@ -217,3 +214,9 @@ DEV_MODE__DOC = (
217
214
  "Enables an interactive python shell via chat commands, for admins."
218
215
  "Not safe to use in prod, but great during dev."
219
216
  )
217
+
218
+ STRIP_LEADING_EMOJI_ADHOC = False
219
+ STRIP_LEADING_EMOJI_ADHOC__DOC = (
220
+ "Strip the leading emoji in ad-hoc command names, if present, in case you "
221
+ "are a emoji-hater."
222
+ )
@@ -3,12 +3,7 @@ from slixmpp.xmlstream import StanzaBase
3
3
 
4
4
  from ....group.room import LegacyMUC
5
5
  from ....util.types import Recipient
6
- from ..util import (
7
- DispatcherMixin,
8
- _get_entity,
9
- _xmpp_to_legacy_thread,
10
- exceptions_to_xmpp_errors,
11
- )
6
+ from ..util import DispatcherMixin, _get_entity, exceptions_to_xmpp_errors
12
7
 
13
8
 
14
9
  class MarkerMixin(DispatcherMixin):
@@ -26,7 +21,7 @@ class MarkerMixin(DispatcherMixin):
26
21
  session = await self._get_session(msg)
27
22
 
28
23
  e: Recipient = await _get_entity(session, msg)
29
- legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
24
+ legacy_thread = await self._xmpp_to_legacy_thread(session, msg, e)
30
25
  displayed_msg_id = msg["displayed"]["id"]
31
26
  if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES:
32
27
  to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore
@@ -54,6 +54,9 @@ class MucMiscMixin(DispatcherMixin):
54
54
  muc = await self.get_muc_from_stanza(iq)
55
55
  await muc.session.on_leave_group(muc.legacy_id)
56
56
  iq.reply().send()
57
+ await muc.session.bookmarks.remove(
58
+ muc, "You left this chat from an XMPP client."
59
+ )
57
60
  return
58
61
 
59
62
  raise XMPPError("feature-not-implemented")
@@ -88,7 +88,7 @@ class MucOwnerMixin(DispatcherMixin):
88
88
  if reason is not None:
89
89
  presence["muc"]["destroy"]["reason"] = reason
90
90
  user_participant._send(presence)
91
- muc.session.bookmarks.remove(muc)
91
+ await muc.session.bookmarks.remove(muc, kick=False)
92
92
  clear = True
93
93
  else:
94
94
  raise XMPPError("bad-request")
@@ -96,9 +96,31 @@ class DispatcherMixin:
96
96
  ) -> tuple["BaseSession", Recipient, int | str]:
97
97
  session = await self._get_session(msg)
98
98
  e: Recipient = await _get_entity(session, msg)
99
- legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
99
+ legacy_thread = await self._xmpp_to_legacy_thread(session, msg, e)
100
100
  return session, e, legacy_thread
101
101
 
102
+ async def _xmpp_to_legacy_thread(
103
+ self, session: "BaseSession", msg: Message, recipient: RecipientType
104
+ ):
105
+ xmpp_thread = msg["thread"]
106
+ if not xmpp_thread:
107
+ return None
108
+
109
+ if session.MESSAGE_IDS_ARE_THREAD_IDS:
110
+ return self._xmpp_msg_id_to_legacy(session, xmpp_thread)
111
+
112
+ legacy_thread_str = session.xmpp.store.sent.get_legacy_thread(
113
+ session.user_pk, xmpp_thread
114
+ )
115
+ if legacy_thread_str is not None:
116
+ return session.xmpp.LEGACY_MSG_ID_TYPE(legacy_thread_str)
117
+ async with session.thread_creation_lock:
118
+ legacy_thread = await recipient.create_thread(xmpp_thread)
119
+ session.xmpp.store.sent.set_thread(
120
+ session.user_pk, str(legacy_thread), xmpp_thread
121
+ )
122
+ return legacy_thread
123
+
102
124
 
103
125
  def _ignore(session: "BaseSession", msg: Message):
104
126
  i = msg.get_id()
@@ -111,28 +133,6 @@ def _ignore(session: "BaseSession", msg: Message):
111
133
  return True
112
134
 
113
135
 
114
- async def _xmpp_to_legacy_thread(
115
- session: "BaseSession", msg: Message, recipient: RecipientType
116
- ):
117
- xmpp_thread = msg["thread"]
118
- if not xmpp_thread:
119
- return
120
-
121
- if session.MESSAGE_IDS_ARE_THREAD_IDS:
122
- return session.xmpp.store.sent.get_legacy_thread(session.user_pk, xmpp_thread)
123
-
124
- async with session.thread_creation_lock:
125
- legacy_thread_str = session.xmpp.store.sent.get_legacy_thread(
126
- session.user_pk, xmpp_thread
127
- )
128
- if legacy_thread_str is None:
129
- legacy_thread = str(await recipient.create_thread(xmpp_thread))
130
- session.xmpp.store.sent.set_thread(
131
- session.user_pk, xmpp_thread, legacy_thread
132
- )
133
- return session.xmpp.LEGACY_MSG_ID_TYPE(legacy_thread)
134
-
135
-
136
136
  async def _get_entity(session: "BaseSession", m: Message) -> RecipientType:
137
137
  session.raise_if_not_logged()
138
138
  if m.get_type() == "groupchat":
@@ -33,17 +33,14 @@ from ...util.types import (
33
33
  )
34
34
  from ...util.util import fix_suffix
35
35
  from .. import config
36
- from .message_maker import MessageMaker
36
+ from .message_text import TextMessageMixin
37
37
 
38
38
 
39
- class AttachmentMixin(MessageMaker):
39
+ class AttachmentMixin(TextMessageMixin):
40
40
  def __init__(self, *a, **kw):
41
41
  super().__init__(*a, **kw)
42
42
  self.__store = self.xmpp.store.attachments
43
43
 
44
- def send_text(self, *_, **k) -> Optional[Message]:
45
- raise NotImplementedError
46
-
47
44
  async def __upload(
48
45
  self,
49
46
  file_path: Path,
@@ -261,7 +258,7 @@ class AttachmentMixin(MessageMaker):
261
258
  thumbnail["width"] = x
262
259
  thumbnail["height"] = y
263
260
  thumbnail["media-type"] = "image/thumbhash"
264
- thumbnail["uri"] = "data:image/thumbhash," + urlquote(h)
261
+ thumbnail["uri"] = "data:image/thumbhash;base64," + urlquote(h)
265
262
 
266
263
  self.__store.set_sims(uploaded_url, str(ref))
267
264
 
@@ -304,6 +301,7 @@ class AttachmentMixin(MessageMaker):
304
301
  caption: Optional[str] = None,
305
302
  carbon=False,
306
303
  when: Optional[datetime] = None,
304
+ correction=False,
307
305
  **kwargs,
308
306
  ) -> list[Message]:
309
307
  msg["oob"]["url"] = uploaded_url
@@ -311,11 +309,19 @@ class AttachmentMixin(MessageMaker):
311
309
  if caption:
312
310
  m1 = self._send(msg, carbon=carbon, **kwargs)
313
311
  m2 = self.send_text(
314
- caption, legacy_msg_id=legacy_msg_id, when=when, carbon=carbon, **kwargs
312
+ caption,
313
+ legacy_msg_id=legacy_msg_id,
314
+ when=when,
315
+ carbon=carbon,
316
+ correction=correction,
317
+ **kwargs,
315
318
  )
316
319
  return [m1, m2] if m2 else [m1]
317
320
  else:
318
- self._set_msg_id(msg, legacy_msg_id)
321
+ if correction:
322
+ msg["replace"]["id"] = self._replace_id(legacy_msg_id)
323
+ else:
324
+ self._set_msg_id(msg, legacy_msg_id)
319
325
  return [self._send(msg, carbon=carbon, **kwargs)]
320
326
 
321
327
  async def send_file(
@@ -358,6 +364,16 @@ class AttachmentMixin(MessageMaker):
358
364
  carbon = kwargs.pop("carbon", False)
359
365
  mto = kwargs.pop("mto", None)
360
366
  store_multi = kwargs.pop("store_multi", True)
367
+ correction = kwargs.get("correction", False)
368
+ if correction and (original_xmpp_id := self._legacy_to_xmpp(legacy_msg_id)):
369
+ xmpp_ids = self.xmpp.store.multi.get_xmpp_ids(
370
+ self.session.user_pk, original_xmpp_id
371
+ )
372
+
373
+ for xmpp_id in xmpp_ids:
374
+ if xmpp_id == original_xmpp_id:
375
+ continue
376
+ self.retract(xmpp_id, thread)
361
377
  msg = self._make_message(
362
378
  when=when,
363
379
  reply_to=reply_to,
@@ -15,14 +15,16 @@ class NamedLockMixin:
15
15
  locks = self.__locks
16
16
  if not locks.get(id_):
17
17
  locks[id_] = asyncio.Lock()
18
- async with locks[id_]:
19
- log.trace("acquired %s", id_) # type:ignore
20
- yield
21
- log.trace("releasing %s", id_) # type:ignore
22
- waiters = locks[id_]._waiters # type:ignore
23
- if not waiters:
24
- del locks[id_]
25
- log.trace("erasing %s", id_) # type:ignore
18
+ try:
19
+ async with locks[id_]:
20
+ log.trace("acquired %s", id_) # type:ignore
21
+ yield
22
+ finally:
23
+ log.trace("releasing %s", id_) # type:ignore
24
+ waiters = locks[id_]._waiters # type:ignore
25
+ if not waiters:
26
+ del locks[id_]
27
+ log.trace("erasing %s", id_) # type:ignore
26
28
 
27
29
  def get_lock(self, id_: Hashable):
28
30
  return self.__locks.get(id_)