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.
- slidge/__version__.py +1 -1
- slidge/command/adhoc.py +1 -1
- slidge/command/base.py +4 -4
- slidge/contact/contact.py +3 -2
- slidge/contact/roster.py +7 -0
- slidge/core/dispatcher/__init__.py +3 -0
- slidge/core/{gateway → dispatcher}/caps.py +6 -4
- slidge/core/{gateway → dispatcher}/disco.py +11 -17
- slidge/core/dispatcher/message/__init__.py +10 -0
- slidge/core/dispatcher/message/chat_state.py +40 -0
- slidge/core/dispatcher/message/marker.py +67 -0
- slidge/core/dispatcher/message/message.py +397 -0
- slidge/core/dispatcher/muc/__init__.py +12 -0
- slidge/core/dispatcher/muc/admin.py +98 -0
- slidge/core/{gateway → dispatcher/muc}/mam.py +26 -15
- slidge/core/dispatcher/muc/misc.py +118 -0
- slidge/core/dispatcher/muc/owner.py +96 -0
- slidge/core/{gateway → dispatcher/muc}/ping.py +10 -15
- slidge/core/dispatcher/presence.py +177 -0
- slidge/core/{gateway → dispatcher}/registration.py +23 -2
- slidge/core/{gateway → dispatcher}/search.py +9 -14
- slidge/core/dispatcher/session_dispatcher.py +84 -0
- slidge/core/dispatcher/util.py +174 -0
- slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +26 -12
- slidge/core/{gateway/base.py → gateway.py} +42 -137
- slidge/core/mixins/attachment.py +7 -2
- slidge/core/mixins/base.py +2 -2
- slidge/core/mixins/message.py +10 -4
- slidge/core/pubsub.py +2 -1
- slidge/core/session.py +28 -2
- slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
- slidge/db/models.py +13 -0
- slidge/db/store.py +128 -2
- slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
- slidge/util/test.py +5 -1
- slidge/util/types.py +6 -0
- slidge/util/util.py +5 -2
- {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/METADATA +2 -1
- {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/RECORD +42 -33
- slidge/core/gateway/__init__.py +0 -3
- slidge/core/gateway/muc_admin.py +0 -35
- slidge/core/gateway/presence.py +0 -95
- slidge/core/gateway/session_dispatcher.py +0 -895
- {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/LICENSE +0 -0
- {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/WHEEL +0 -0
- {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
|
31
|
-
from
|
32
|
-
from
|
33
|
-
from
|
34
|
-
from
|
35
|
-
from
|
36
|
-
from
|
37
|
-
from
|
38
|
-
from
|
39
|
-
from
|
40
|
-
from
|
41
|
-
from
|
42
|
-
from
|
43
|
-
from
|
44
|
-
from
|
45
|
-
from
|
46
|
-
from .
|
47
|
-
from .
|
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
|
-
|
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:
|
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
|
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
|
-
|
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
|
-
|
427
|
-
|
428
|
-
self.
|
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.
|
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
|
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.
|
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
|
slidge/core/mixins/attachment.py
CHANGED
@@ -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
|
-
|
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:
|
slidge/core/mixins/base.py
CHANGED
@@ -6,8 +6,8 @@ from slixmpp import JID
|
|
6
6
|
from ...util.types import MessageOrPresenceTypeVar
|
7
7
|
|
8
8
|
if TYPE_CHECKING:
|
9
|
-
from
|
10
|
-
from
|
9
|
+
from ..gateway import BaseGateway
|
10
|
+
from ..session import BaseSession
|
11
11
|
|
12
12
|
|
13
13
|
class MetaBase(ABCMeta):
|
slidge/core/mixins/message.py
CHANGED
@@ -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
|
356
|
-
|
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
|
733
|
-
return
|
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
|