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

Sign up to get free protection for your applications and to get access to all the features.
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: