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.
- 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
|