slidge 0.2.0a8__py3-none-any.whl → 0.2.0a10__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 (46) hide show
  1. slidge/__version__.py +1 -1
  2. slidge/command/adhoc.py +1 -1
  3. slidge/command/base.py +4 -4
  4. slidge/contact/contact.py +3 -2
  5. slidge/contact/roster.py +7 -0
  6. slidge/core/dispatcher/__init__.py +3 -0
  7. slidge/core/{gateway → dispatcher}/caps.py +6 -4
  8. slidge/core/{gateway → dispatcher}/disco.py +11 -17
  9. slidge/core/dispatcher/message/__init__.py +10 -0
  10. slidge/core/dispatcher/message/chat_state.py +40 -0
  11. slidge/core/dispatcher/message/marker.py +67 -0
  12. slidge/core/dispatcher/message/message.py +397 -0
  13. slidge/core/dispatcher/muc/__init__.py +12 -0
  14. slidge/core/dispatcher/muc/admin.py +98 -0
  15. slidge/core/{gateway → dispatcher/muc}/mam.py +26 -15
  16. slidge/core/dispatcher/muc/misc.py +118 -0
  17. slidge/core/dispatcher/muc/owner.py +96 -0
  18. slidge/core/{gateway → dispatcher/muc}/ping.py +10 -15
  19. slidge/core/dispatcher/presence.py +177 -0
  20. slidge/core/{gateway → dispatcher}/registration.py +23 -2
  21. slidge/core/{gateway → dispatcher}/search.py +9 -14
  22. slidge/core/dispatcher/session_dispatcher.py +84 -0
  23. slidge/core/dispatcher/util.py +174 -0
  24. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +26 -12
  25. slidge/core/{gateway/base.py → gateway.py} +42 -137
  26. slidge/core/mixins/attachment.py +7 -2
  27. slidge/core/mixins/base.py +2 -2
  28. slidge/core/mixins/message.py +10 -4
  29. slidge/core/pubsub.py +2 -1
  30. slidge/core/session.py +28 -2
  31. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  32. slidge/db/models.py +13 -0
  33. slidge/db/store.py +128 -2
  34. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  35. slidge/util/test.py +5 -1
  36. slidge/util/types.py +6 -0
  37. slidge/util/util.py +5 -2
  38. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/METADATA +2 -1
  39. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/RECORD +42 -33
  40. slidge/core/gateway/__init__.py +0 -3
  41. slidge/core/gateway/muc_admin.py +0 -35
  42. slidge/core/gateway/presence.py +0 -95
  43. slidge/core/gateway/session_dispatcher.py +0 -895
  44. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/LICENSE +0 -0
  45. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/WHEEL +0 -0
  46. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/entry_points.txt +0 -0
@@ -8,16 +8,7 @@ import re
8
8
  import tempfile
9
9
  from copy import copy
10
10
  from datetime import datetime
11
- from typing import (
12
- TYPE_CHECKING,
13
- Any,
14
- Callable,
15
- Collection,
16
- Mapping,
17
- Optional,
18
- Sequence,
19
- Union,
20
- )
11
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Sequence, Union
21
12
 
22
13
  import aiohttp
23
14
  import qrcode
@@ -27,40 +18,30 @@ from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation
27
18
  from slixmpp.types import MessageTypes
28
19
  from slixmpp.xmlstream.xmlstream import NotConnectedError
29
20
 
30
- from ... import command # noqa: F401
31
- from ...command.adhoc import AdhocProvider
32
- from ...command.admin import Exec
33
- from ...command.base import Command, FormField
34
- from ...command.chat_command import ChatCommandProvider
35
- from ...command.register import RegistrationType
36
- from ...db import GatewayUser, SlidgeStore
37
- from ...db.avatar import avatar_cache
38
- from ...slixfix.roster import RosterBackend
39
- from ...util import ABCSubclassableOnceAtMost
40
- from ...util.types import AvatarType, MessageOrPresenceTypeVar
41
- from ...util.util import timeit
42
- from .. import config
43
- from ..mixins import MessageMixin
44
- from ..pubsub import PubSubComponent
45
- from ..session import BaseSession
46
- from .caps import Caps
47
- from .delivery_receipt import DeliveryReceipt
48
- from .disco import Disco
49
- from .mam import Mam
50
- from .muc_admin import MucAdmin
51
- from .ping import Ping
52
- from .presence import PresenceHandlerMixin
53
- from .registration import Registration
54
- from .search import Search
55
- from .session_dispatcher import SessionDispatcher
56
- from .vcard_temp import VCardTemp
21
+ from slidge import command # noqa: F401
22
+ from slidge.command.adhoc import AdhocProvider
23
+ from slidge.command.admin import Exec
24
+ from slidge.command.base import Command, FormField
25
+ from slidge.command.chat_command import ChatCommandProvider
26
+ from slidge.command.register import RegistrationType
27
+ from slidge.core import config
28
+ from slidge.core.dispatcher.session_dispatcher import SessionDispatcher
29
+ from slidge.core.mixins import MessageMixin
30
+ from slidge.core.pubsub import PubSubComponent
31
+ from slidge.core.session import BaseSession
32
+ from slidge.db import GatewayUser, SlidgeStore
33
+ from slidge.db.avatar import avatar_cache
34
+ from slidge.slixfix.delivery_receipt import DeliveryReceipt
35
+ from slidge.slixfix.roster import RosterBackend
36
+ from slidge.util import ABCSubclassableOnceAtMost
37
+ from slidge.util.types import AvatarType, MessageOrPresenceTypeVar
38
+ from slidge.util.util import timeit
57
39
 
58
40
  if TYPE_CHECKING:
59
- from ...group.room import LegacyMUC
41
+ pass
60
42
 
61
43
 
62
44
  class BaseGateway(
63
- PresenceHandlerMixin,
64
45
  ComponentXMPP,
65
46
  MessageMixin,
66
47
  metaclass=ABCSubclassableOnceAtMost,
@@ -118,7 +99,7 @@ class BaseGateway(
118
99
  Path, bytes or URL used by the component as an avatar.
119
100
  """
120
101
 
121
- REGISTRATION_FIELDS: Collection[FormField] = [
102
+ REGISTRATION_FIELDS: Sequence[FormField] = [
122
103
  FormField(var="username", label="User name", required=True),
123
104
  FormField(var="password", label="Password", required=True, private=True),
124
105
  ]
@@ -315,7 +296,7 @@ class BaseGateway(
315
296
  self.session_cls: BaseSession = BaseSession.get_unique_subclass()
316
297
  self.session_cls.xmpp = self
317
298
 
318
- from ...group.room import LegacyMUC
299
+ from ..group.room import LegacyMUC
319
300
 
320
301
  LegacyMUC.get_self_or_unique_subclass().xmpp = self
321
302
 
@@ -328,6 +309,7 @@ class BaseGateway(
328
309
 
329
310
  self.register_plugins()
330
311
  self.__register_slixmpp_events()
312
+ self.__register_slixmpp_api()
331
313
  self.roster.set_backend(RosterBackend(self))
332
314
 
333
315
  self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
@@ -355,21 +337,11 @@ class BaseGateway(
355
337
  # why does mypy need these type annotations? no idea
356
338
  self.__adhoc_handler: AdhocProvider = AdhocProvider(self)
357
339
  self.__chat_commands_handler: ChatCommandProvider = ChatCommandProvider(self)
358
- self.__disco_handler = Disco(self)
359
-
360
- self.__ping_handler = Ping(self)
361
- self.__mam_handler = Mam(self)
362
- self.__search_handler = Search(self)
363
- self.__caps_handler = Caps(self)
364
- self.__vcard_temp_handler = VCardTemp(self)
365
- self.__muc_admin_handler = MucAdmin(self)
366
- self.__registration = Registration(self)
340
+
367
341
  self.__dispatcher = SessionDispatcher(self)
368
342
 
369
343
  self.__register_commands()
370
344
 
371
- self.__mam_cleanup_task = self.loop.create_task(self.__mam_cleanup())
372
-
373
345
  MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__()
374
346
 
375
347
  async def __set_http(self):
@@ -378,13 +350,6 @@ class BaseGateway(
378
350
  return
379
351
  avatar_cache.http = self.http
380
352
 
381
- async def __mam_cleanup(self):
382
- if not config.MAM_MAX_DAYS:
383
- return
384
- while True:
385
- await asyncio.sleep(3600 * 6)
386
- self.store.mam.nuke_older_than(config.MAM_MAX_DAYS)
387
-
388
353
  def __register_commands(self):
389
354
  for cls in Command.subclasses:
390
355
  if any(x is NotImplemented for x in [cls.CHAT_COMMAND, cls.NODE, cls.NAME]):
@@ -421,40 +386,27 @@ class BaseGateway(
421
386
  loop.stop()
422
387
 
423
388
  def __register_slixmpp_events(self):
389
+ self.del_event_handler("presence_subscribe", self._handle_subscribe)
390
+ self.del_event_handler("presence_unsubscribe", self._handle_unsubscribe)
391
+ self.del_event_handler("presence_subscribed", self._handle_subscribed)
392
+ self.del_event_handler("presence_unsubscribed", self._handle_unsubscribed)
393
+ self.del_event_handler(
394
+ "roster_subscription_request", self._handle_new_subscription
395
+ )
396
+ self.del_event_handler("presence_probe", self._handle_probe)
424
397
  self.add_event_handler("session_start", self.__on_session_start)
425
398
  self.add_event_handler("disconnected", self.connect)
426
- self.add_event_handler("user_register", self._on_user_register)
427
- self.add_event_handler("user_unregister", self._on_user_unregister)
428
- self.add_event_handler("groupchat_message_error", self.__on_group_chat_error)
399
+
400
+ def __register_slixmpp_api(self) -> None:
401
+ self.plugin["xep_0231"].api.register(self.store.bob.get_bob, "get_bob")
402
+ self.plugin["xep_0231"].api.register(self.store.bob.set_bob, "set_bob")
403
+ self.plugin["xep_0231"].api.register(self.store.bob.del_bob, "del_bob")
429
404
 
430
405
  @property # type: ignore
431
406
  def jid(self):
432
407
  # Override to avoid slixmpp deprecation warnings.
433
408
  return self.boundjid
434
409
 
435
- async def __on_group_chat_error(self, msg: Message):
436
- condition = msg["error"].get_condition()
437
- if condition not in KICKABLE_ERRORS:
438
- return
439
-
440
- try:
441
- muc = await self.get_muc_from_stanza(msg)
442
- except XMPPError as e:
443
- log.debug("Not removing resource", exc_info=e)
444
- return
445
- mfrom = msg.get_from()
446
- resource = mfrom.resource
447
- try:
448
- muc.remove_user_resource(resource)
449
- except KeyError:
450
- # this actually happens quite frequently on for both beagle and monal
451
- # (not sure why?), but is of no consequence
452
- log.debug("%s was not in the resources of %s", resource, muc)
453
- else:
454
- log.info(
455
- "Removed %s from the resources of %s because of error", resource, muc
456
- )
457
-
458
410
  async def __on_session_start(self, event):
459
411
  log.debug("Gateway session start: %s", event)
460
412
 
@@ -490,7 +442,7 @@ class BaseGateway(
490
442
  pto=user.jid.bare, ptype="probe"
491
443
  ) # ensure we get all resources for user
492
444
  session = self.session_cls.from_user(user)
493
- session.create_task(self.__login_wrap(session))
445
+ session.create_task(self.login_wrap(session))
494
446
  if cached_avatar is not None:
495
447
  await self.pubsub.broadcast_avatar(
496
448
  self.boundjid.bare, session.user_jid, cached_avatar
@@ -551,7 +503,7 @@ class BaseGateway(
551
503
  )
552
504
 
553
505
  @timeit
554
- async def __login_wrap(self, session: "BaseSession"):
506
+ async def login_wrap(self, session: "BaseSession"):
555
507
  session.send_gateway_status("Logging in…", show="dnd")
556
508
  try:
557
509
  status = await session.login()
@@ -612,21 +564,6 @@ class BaseGateway(
612
564
  stanza.send()
613
565
  return stanza
614
566
 
615
- async def _on_user_register(self, iq: Iq):
616
- session = self.get_session_from_stanza(iq)
617
- for jid in config.ADMINS:
618
- self.send_message(
619
- mto=jid,
620
- mbody=f"{iq.get_from()} has registered",
621
- mtype="chat",
622
- mfrom=self.boundjid.bare,
623
- )
624
- session.send_gateway_message(self.WELCOME_MESSAGE)
625
- await self.__login_wrap(session)
626
-
627
- async def _on_user_unregister(self, iq: Iq):
628
- await self.session_cls.kill_by_jid(iq.get_from())
629
-
630
567
  def raise_if_not_allowed_jid(self, jid: JID):
631
568
  if not self.jid_validator.match(jid.bare):
632
569
  raise XMPPError(
@@ -665,26 +602,6 @@ class BaseGateway(
665
602
  except XMPPError:
666
603
  pass
667
604
 
668
- async def get_muc_from_stanza(self, iq: Union[Iq, Message]) -> "LegacyMUC":
669
- ito = iq.get_to()
670
-
671
- if ito == self.boundjid.bare:
672
- raise XMPPError(
673
- text="No MAM on the component itself, use a JID with a resource"
674
- )
675
-
676
- ifrom = iq.get_from()
677
- user = self.store.users.get(ifrom)
678
- if user is None:
679
- raise XMPPError("registration-required")
680
-
681
- session = self.get_session_from_user(user)
682
- session.raise_if_not_logged()
683
-
684
- muc = await session.bookmarks.by_jid(ito)
685
-
686
- return muc
687
-
688
605
  def exception(self, exception: Exception):
689
606
  # """
690
607
  # Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception.
@@ -719,7 +636,7 @@ class BaseGateway(
719
636
  async def w():
720
637
  session.cancel_all_tasks()
721
638
  await session.logout()
722
- await self.__login_wrap(session)
639
+ await self.login_wrap(session)
723
640
 
724
641
  session.create_task(w())
725
642
 
@@ -949,20 +866,6 @@ class BaseGateway(
949
866
  return tasks
950
867
 
951
868
 
952
- KICKABLE_ERRORS = {
953
- "gone",
954
- "internal-server-error",
955
- "item-not-found",
956
- "jid-malformed",
957
- "recipient-unavailable",
958
- "redirect",
959
- "remote-server-not-found",
960
- "remote-server-timeout",
961
- "service-unavailable",
962
- "malformed error",
963
- }
964
-
965
-
966
869
  SLIXMPP_PLUGINS = [
967
870
  "link_preview", # https://wiki.soprani.ca/CheogramApp/LinkPreviews
968
871
  "xep_0030", # Service discovery
@@ -972,6 +875,7 @@ SLIXMPP_PLUGINS = [
972
875
  "xep_0055", # Jabber search
973
876
  "xep_0059", # Result Set Management
974
877
  "xep_0066", # Out of Band Data
878
+ "xep_0071", # XHTML-IM (for stickers and custom emojis maybe later)
975
879
  "xep_0077", # In-band registration
976
880
  "xep_0084", # User Avatar
977
881
  "xep_0085", # Chat state notifications
@@ -984,6 +888,7 @@ SLIXMPP_PLUGINS = [
984
888
  "xep_0184", # Message Delivery Receipts
985
889
  "xep_0199", # XMPP Ping
986
890
  "xep_0221", # Data Forms Media Element
891
+ "xep_0231", # Bits of Binary (for stickers and custom emojis maybe later)
987
892
  "xep_0249", # Direct MUC Invitations
988
893
  "xep_0264", # Jingle Content Thumbnails
989
894
  "xep_0280", # Carbons
@@ -9,7 +9,7 @@ import tempfile
9
9
  import warnings
10
10
  from datetime import datetime
11
11
  from itertools import chain
12
- from mimetypes import guess_type
12
+ from mimetypes import guess_extension, guess_type
13
13
  from pathlib import Path
14
14
  from typing import IO, AsyncIterator, Collection, Optional, Sequence, Union
15
15
  from urllib.parse import quote as urlquote
@@ -171,7 +171,12 @@ class AttachmentMixin(MessageMaker):
171
171
  )
172
172
 
173
173
  if file_path is None:
174
- file_name = str(uuid4()) if file_name is None else file_name
174
+ if file_name is None:
175
+ file_name = str(uuid4())
176
+ if content_type is not None:
177
+ ext = guess_extension(content_type, strict=False) # type:ignore
178
+ if ext is not None:
179
+ file_name += ext
175
180
  temp_dir = Path(tempfile.mkdtemp())
176
181
  file_path = temp_dir / file_name
177
182
  if file_url:
@@ -6,8 +6,8 @@ from slixmpp import JID
6
6
  from ...util.types import MessageOrPresenceTypeVar
7
7
 
8
8
  if TYPE_CHECKING:
9
- from slidge.core.gateway import BaseGateway
10
- from slidge.core.session import BaseSession
9
+ from ..gateway import BaseGateway
10
+ from ..session import BaseSession
11
11
 
12
12
 
13
13
  class MetaBase(ABCMeta):
@@ -214,7 +214,7 @@ class ContentMessageMixin(AttachmentMixin):
214
214
  :param archive_only: (only in groups) Do not send this message to user,
215
215
  but store it in the archive. Meant to be used during ``MUC.backfill()``
216
216
  """
217
- if carbon:
217
+ if carbon and not hasattr(self, "muc"):
218
218
  if not correction and self.xmpp.store.sent.was_sent_by_user(
219
219
  self.session.user_pk, str(legacy_msg_id)
220
220
  ):
@@ -223,6 +223,11 @@ class ContentMessageMixin(AttachmentMixin):
223
223
  legacy_msg_id,
224
224
  )
225
225
  return
226
+ if hasattr(self, "muc") and not self.is_user: # type:ignore
227
+ log.warning(
228
+ "send_text() called with carbon=True on a participant who is not the user",
229
+ legacy_msg_id,
230
+ )
226
231
  self.xmpp.store.sent.set_message(
227
232
  self.session.user_pk,
228
233
  str(legacy_msg_id),
@@ -352,11 +357,12 @@ class ContentMessageMixin(AttachmentMixin):
352
357
  class CarbonMessageMixin(ContentMessageMixin, MarkerMixin):
353
358
  def _privileged_send(self, msg: Message):
354
359
  i = msg.get_id()
355
- if not i:
356
- i = str(uuid.uuid4())
360
+ if i:
361
+ self.session.ignore_messages.add(i)
362
+ else:
363
+ i = "slidge-carbon-" + str(uuid.uuid4())
357
364
  msg.set_id(i)
358
365
  msg.del_origin_id()
359
- self.session.ignore_messages.add(i)
360
366
  try:
361
367
  self.xmpp["xep_0356"].send_privileged_message(msg)
362
368
  except PermissionError:
slidge/core/pubsub.py CHANGED
@@ -25,8 +25,9 @@ from ..db.store import ContactStore, SlidgeStore
25
25
  from .mixins.lock import NamedLockMixin
26
26
 
27
27
  if TYPE_CHECKING:
28
+ from slidge.core.gateway import BaseGateway
29
+
28
30
  from ..contact.contact import LegacyContact
29
- from ..core.gateway.base import BaseGateway
30
31
 
31
32
  VCARD4_NAMESPACE = "urn:xmpp:vcard4"
32
33
 
slidge/core/session.py CHANGED
@@ -31,6 +31,7 @@ from ..util.types import (
31
31
  PseudoPresenceShow,
32
32
  RecipientType,
33
33
  ResourceDict,
34
+ Sticker,
34
35
  )
35
36
  from ..util.util import deprecated
36
37
 
@@ -225,6 +226,31 @@ class BaseSession(
225
226
 
226
227
  send_file = deprecated("BaseSession.send_file", on_file)
227
228
 
229
+ async def on_sticker(
230
+ self,
231
+ chat: RecipientType,
232
+ sticker: Sticker,
233
+ *,
234
+ reply_to_msg_id: Optional[LegacyMessageType] = None,
235
+ reply_to_fallback_text: Optional[str] = None,
236
+ reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
237
+ thread: Optional[LegacyThreadType] = None,
238
+ ) -> Optional[LegacyMessageType]:
239
+ """
240
+ Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
241
+
242
+ :param chat: See :meth:`.BaseSession.on_text`
243
+ :param sticker: The sticker sent by the user.
244
+ :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
245
+ :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
246
+ :param reply_to: See :meth:`.BaseSession.on_text`
247
+ :param thread:
248
+
249
+ :return: An ID of some sort that can be used later to ack and mark the message
250
+ as read by the user
251
+ """
252
+ raise NotImplementedError
253
+
228
254
  async def on_active(
229
255
  self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
230
256
  ):
@@ -729,8 +755,8 @@ class BaseSession(
729
755
  self.xmpp.re_login(self)
730
756
 
731
757
  async def get_contact_or_group_or_participant(self, jid: JID, create=True):
732
- if jid.bare in (contacts := self.contacts.known_contacts(only_friends=False)):
733
- return contacts[jid.bare]
758
+ if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None:
759
+ return contact
734
760
  if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None:
735
761
  return await self.__get_muc_or_participant(muc, jid)
736
762
  else:
@@ -0,0 +1,42 @@
1
+ """Add BoB
2
+
3
+ Revision ID: 45c24cc73c91
4
+ Revises: 3071e0fa69d4
5
+ Create Date: 2024-08-01 22:30:07.073935
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = "45c24cc73c91"
16
+ down_revision: Union[str, None] = "3071e0fa69d4"
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table(
24
+ "bob",
25
+ sa.Column("id", sa.Integer(), nullable=False),
26
+ sa.Column("file_name", sa.String(), nullable=False),
27
+ sa.Column("sha_1", sa.String(), nullable=False),
28
+ sa.Column("sha_256", sa.String(), nullable=False),
29
+ sa.Column("sha_512", sa.String(), nullable=False),
30
+ sa.Column("content_type", sa.String(), nullable=False),
31
+ sa.PrimaryKeyConstraint("id"),
32
+ sa.UniqueConstraint("sha_1"),
33
+ sa.UniqueConstraint("sha_256"),
34
+ sa.UniqueConstraint("sha_512"),
35
+ )
36
+ # ### end Alembic commands ###
37
+
38
+
39
+ def downgrade() -> None:
40
+ # ### commands auto generated by Alembic - please adjust! ###
41
+ op.drop_table("bob")
42
+ # ### end Alembic commands ###
slidge/db/models.py CHANGED
@@ -386,3 +386,16 @@ class Participant(Base):
386
386
  )
387
387
 
388
388
  extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
389
+
390
+
391
+ class Bob(Base):
392
+ __tablename__ = "bob"
393
+
394
+ id: Mapped[int] = mapped_column(primary_key=True)
395
+ file_name: Mapped[str] = mapped_column(nullable=False)
396
+
397
+ sha_1: Mapped[str] = mapped_column(nullable=False, unique=True)
398
+ sha_256: Mapped[str] = mapped_column(nullable=False, unique=True)
399
+ sha_512: Mapped[str] = mapped_column(nullable=False, unique=True)
400
+
401
+ content_type: Mapped[Optional[str]] = mapped_column(nullable=False)
slidge/db/store.py CHANGED
@@ -1,27 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  import json
4
5
  import logging
6
+ import uuid
5
7
  from contextlib import contextmanager
6
8
  from datetime import datetime, timedelta, timezone
9
+ from mimetypes import guess_extension
7
10
  from typing import TYPE_CHECKING, Collection, Iterator, Optional, Type
8
11
 
9
12
  from slixmpp import JID, Iq, Message, Presence
10
13
  from slixmpp.exceptions import XMPPError
14
+ from slixmpp.plugins.xep_0231.stanza import BitsOfBinary
11
15
  from sqlalchemy import Engine, delete, select, update
12
- from sqlalchemy.orm import Session, attributes
16
+ from sqlalchemy.orm import Session, attributes, load_only
13
17
  from sqlalchemy.sql.functions import count
14
18
 
19
+ from ..core import config
15
20
  from ..util.archive_msg import HistoryMessage
16
21
  from ..util.types import URL, CachedPresence, ClientType
17
22
  from ..util.types import Hat as HatTuple
18
- from ..util.types import MamMetadata, MucAffiliation, MucRole
23
+ from ..util.types import MamMetadata, MucAffiliation, MucRole, Sticker
19
24
  from .meta import Base
20
25
  from .models import (
21
26
  ArchivedMessage,
22
27
  ArchivedMessageSource,
23
28
  Attachment,
24
29
  Avatar,
30
+ Bob,
25
31
  Contact,
26
32
  ContactSent,
27
33
  GatewayUser,
@@ -87,6 +93,7 @@ class SlidgeStore(EngineMixin):
87
93
  self.rooms = RoomStore(engine)
88
94
  self.sent = SentStore(engine)
89
95
  self.participants = ParticipantStore(engine)
96
+ self.bob = BobStore(engine)
90
97
 
91
98
 
92
99
  class UserStore(EngineMixin):
@@ -973,6 +980,15 @@ class RoomStore(UpdatedMixin):
973
980
  select(Room).where(Room.user_account_id == user_pk)
974
981
  ).scalars()
975
982
 
983
+ def get_all_jid_and_names(self, user_pk: int) -> Iterator[Room]:
984
+ with self.session() as session:
985
+ yield from session.scalars(
986
+ select(Room)
987
+ .filter(Room.user_account_id == user_pk)
988
+ .options(load_only(Room.jid, Room.name))
989
+ .order_by(Room.name)
990
+ ).all()
991
+
976
992
 
977
993
  class ParticipantStore(EngineMixin):
978
994
  def __init__(self, *a, **kw):
@@ -1120,5 +1136,115 @@ class ParticipantStore(EngineMixin):
1120
1136
  ).scalar()
1121
1137
 
1122
1138
 
1139
+ class BobStore(EngineMixin):
1140
+ _ATTR_MAP = {
1141
+ "sha-1": "sha_1",
1142
+ "sha1": "sha_1",
1143
+ "sha-256": "sha_256",
1144
+ "sha256": "sha_256",
1145
+ "sha-512": "sha_512",
1146
+ "sha512": "sha_512",
1147
+ }
1148
+
1149
+ _ALG_MAP = {
1150
+ "sha_1": hashlib.sha1,
1151
+ "sha_256": hashlib.sha256,
1152
+ "sha_512": hashlib.sha512,
1153
+ }
1154
+
1155
+ def __init__(self, *a, **k):
1156
+ super().__init__(*a, **k)
1157
+ self.root_dir = config.HOME_DIR / "slidge_stickers"
1158
+ self.root_dir.mkdir(exist_ok=True)
1159
+
1160
+ @staticmethod
1161
+ def __split_cid(cid: str) -> list[str]:
1162
+ return cid.removesuffix("@bob.xmpp.org").split("+")
1163
+
1164
+ def __get_condition(self, cid: str):
1165
+ alg_name, digest = self.__split_cid(cid)
1166
+ attr = self._ATTR_MAP.get(alg_name)
1167
+ if attr is None:
1168
+ log.warning("Unknown hash algo: %s", alg_name)
1169
+ return None
1170
+ return getattr(Bob, attr) == digest
1171
+
1172
+ def get(self, cid: str) -> Bob | None:
1173
+ with self.session() as session:
1174
+ try:
1175
+ return session.query(Bob).filter(self.__get_condition(cid)).scalar()
1176
+ except ValueError:
1177
+ log.warning("Cannot get Bob with CID: %s", cid)
1178
+ return None
1179
+
1180
+ def get_sticker(self, cid: str) -> Sticker | None:
1181
+ bob = self.get(cid)
1182
+ if bob is None:
1183
+ return None
1184
+ return Sticker(
1185
+ self.root_dir / bob.file_name,
1186
+ bob.content_type,
1187
+ {h: getattr(bob, h) for h in self._ALG_MAP},
1188
+ )
1189
+
1190
+ def get_bob(self, _jid, _node, _ifrom, cid: str) -> BitsOfBinary | None:
1191
+ stored = self.get(cid)
1192
+ if stored is None:
1193
+ return None
1194
+ bob = BitsOfBinary()
1195
+ bob["data"] = (self.root_dir / stored.file_name).read_bytes()
1196
+ if stored.content_type is not None:
1197
+ bob["type"] = stored.content_type
1198
+ bob["cid"] = cid
1199
+ return bob
1200
+
1201
+ def del_bob(self, _jid, _node, _ifrom, cid: str) -> None:
1202
+ with self.session() as orm:
1203
+ try:
1204
+ file_name = orm.scalar(
1205
+ delete(Bob)
1206
+ .where(self.__get_condition(cid))
1207
+ .returning(Bob.file_name)
1208
+ )
1209
+ except ValueError:
1210
+ log.warning("Cannot delete Bob with CID: %s", cid)
1211
+ return None
1212
+ if file_name is None:
1213
+ log.warning("No BoB with CID: %s", cid)
1214
+ return None
1215
+ (self.root_dir / file_name).unlink()
1216
+ orm.commit()
1217
+
1218
+ def set_bob(self, _jid, _node, _ifrom, bob: BitsOfBinary) -> None:
1219
+ cid = bob["cid"]
1220
+ try:
1221
+ alg_name, digest = self.__split_cid(cid)
1222
+ except ValueError:
1223
+ log.warning("Cannot set Bob with CID: %s", cid)
1224
+ return
1225
+ attr = self._ATTR_MAP.get(alg_name)
1226
+ if attr is None:
1227
+ log.warning("Cannot set BoB with unknown hash algo: %s", alg_name)
1228
+ return None
1229
+ with self.session() as orm:
1230
+ existing = self.get(bob["cid"])
1231
+ if existing is not None:
1232
+ log.debug("Bob already known")
1233
+ return
1234
+ bytes_ = bob["data"]
1235
+ path = self.root_dir / uuid.uuid4().hex
1236
+ if bob["type"]:
1237
+ path = path.with_suffix(guess_extension(bob["type"]) or "")
1238
+ path.write_bytes(bytes_)
1239
+ hashes = {k: v(bytes_).hexdigest() for k, v in self._ALG_MAP.items()}
1240
+ if hashes[attr] != digest:
1241
+ raise ValueError(
1242
+ "The given CID does not correspond to the result of our hash"
1243
+ )
1244
+ row = Bob(file_name=path.name, content_type=bob["type"] or None, **hashes)
1245
+ orm.add(row)
1246
+ orm.commit()
1247
+
1248
+
1123
1249
  log = logging.getLogger(__name__)
1124
1250
  _session: Optional[Session] = None
@@ -11,7 +11,7 @@ from slixmpp import JID, Message
11
11
  from slixmpp.types import MessageTypes
12
12
 
13
13
  if TYPE_CHECKING:
14
- from .base import BaseGateway
14
+ from slidge.core.gateway import BaseGateway
15
15
 
16
16
 
17
17
  class DeliveryReceipt: