slidge 0.2.0a10__py3-none-any.whl → 0.2.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. slidge/__main__.py +2 -3
  2. slidge/__version__.py +1 -1
  3. slidge/command/adhoc.py +31 -15
  4. slidge/command/admin.py +11 -4
  5. slidge/command/base.py +5 -2
  6. slidge/command/categories.py +13 -3
  7. slidge/command/chat_command.py +14 -1
  8. slidge/command/user.py +22 -10
  9. slidge/contact/roster.py +2 -0
  10. slidge/core/config.py +6 -3
  11. slidge/core/dispatcher/message/marker.py +2 -7
  12. slidge/core/dispatcher/muc/misc.py +3 -0
  13. slidge/core/dispatcher/muc/owner.py +1 -1
  14. slidge/core/dispatcher/util.py +23 -23
  15. slidge/core/mixins/attachment.py +24 -8
  16. slidge/core/mixins/lock.py +10 -8
  17. slidge/core/mixins/message.py +5 -205
  18. slidge/core/mixins/message_text.py +211 -0
  19. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  20. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +1 -1
  21. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +12 -1
  22. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +6 -0
  23. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +7 -6
  24. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +4 -0
  25. slidge/db/models.py +4 -2
  26. slidge/db/store.py +18 -11
  27. slidge/group/bookmarks.py +23 -1
  28. slidge/group/participant.py +5 -5
  29. slidge/group/room.py +10 -1
  30. slidge/util/test.py +4 -4
  31. slidge/util/util.py +18 -0
  32. {slidge-0.2.0a10.dist-info → slidge-0.2.0b1.dist-info}/METADATA +1 -1
  33. {slidge-0.2.0a10.dist-info → slidge-0.2.0b1.dist-info}/RECORD +36 -34
  34. slidge-0.2.0b1.dist-info/entry_points.txt +3 -0
  35. slidge-0.2.0a10.dist-info/entry_points.txt +0 -3
  36. {slidge-0.2.0a10.dist-info → slidge-0.2.0b1.dist-info}/LICENSE +0 -0
  37. {slidge-0.2.0a10.dist-info → slidge-0.2.0b1.dist-info}/WHEEL +0 -0
slidge/__main__.py CHANGED
@@ -1,4 +1,3 @@
1
- if __name__ == "__main__":
2
- from slidge.main import main
1
+ from slidge.main import main
3
2
 
4
- main()
3
+ main()
slidge/__version__.py CHANGED
@@ -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"
slidge/command/adhoc.py CHANGED
@@ -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__)
slidge/command/admin.py CHANGED
@@ -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(
slidge/command/base.py CHANGED
@@ -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.
@@ -1,3 +1,13 @@
1
- ADMINISTRATION = "🛷️ Slidge administration"
2
- CONTACTS = "👤 Contacts"
3
- GROUPS = "👥 Groups"
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)
slidge/command/user.py CHANGED
@@ -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.")
slidge/contact/roster.py CHANGED
@@ -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)
slidge/core/config.py CHANGED
@@ -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_)