slidge 0.3.1__py3-none-any.whl → 0.3.3__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/command/user.py +22 -2
 - slidge/contact/contact.py +6 -13
 - slidge/contact/roster.py +6 -1
 - slidge/core/config.py +43 -9
 - slidge/core/dispatcher/caps.py +1 -2
 - slidge/core/dispatcher/message/message.py +1 -0
 - slidge/core/dispatcher/presence.py +1 -0
 - slidge/core/dispatcher/registration.py +1 -1
 - slidge/core/dispatcher/search.py +1 -5
 - slidge/core/dispatcher/util.py +3 -1
 - slidge/core/gateway.py +12 -1
 - slidge/core/mixins/attachment.py +62 -8
 - slidge/core/mixins/db.py +15 -0
 - slidge/core/mixins/message_maker.py +1 -1
 - slidge/core/mixins/message_text.py +1 -1
 - slidge/core/mixins/presence.py +20 -10
 - slidge/core/session.py +2 -0
 - slidge/db/models.py +3 -3
 - slidge/db/store.py +29 -1
 - slidge/group/room.py +17 -24
 - slidge/util/test.py +3 -2
 - slidge/util/types.py +2 -0
 - slidge/util/util.py +6 -4
 - {slidge-0.3.1.dist-info → slidge-0.3.3.dist-info}/METADATA +1 -1
 - {slidge-0.3.1.dist-info → slidge-0.3.3.dist-info}/RECORD +29 -29
 - {slidge-0.3.1.dist-info → slidge-0.3.3.dist-info}/WHEEL +0 -0
 - {slidge-0.3.1.dist-info → slidge-0.3.3.dist-info}/entry_points.txt +0 -0
 - {slidge-0.3.1.dist-info → slidge-0.3.3.dist-info}/licenses/LICENSE +0 -0
 - {slidge-0.3.1.dist-info → slidge-0.3.3.dist-info}/top_level.txt +0 -0
 
    
        slidge/command/user.py
    CHANGED
    
    | 
         @@ -6,7 +6,7 @@ from slixmpp import JID 
     | 
|
| 
       6 
6 
     | 
    
         
             
            from slixmpp.exceptions import XMPPError
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
            from ..group.room import LegacyMUC
         
     | 
| 
       9 
     | 
    
         
            -
            from ..util.types import AnyBaseSession, LegacyGroupIdType, UserPreferences
         
     | 
| 
      
 9 
     | 
    
         
            +
            from ..util.types import AnyBaseSession, LegacyGroupIdType, MucType, UserPreferences
         
     | 
| 
       10 
10 
     | 
    
         
             
            from .base import (
         
     | 
| 
       11 
11 
     | 
    
         
             
                Command,
         
     | 
| 
       12 
12 
     | 
    
         
             
                CommandAccess,
         
     | 
| 
         @@ -157,7 +157,9 @@ class ListGroups(Command): 
     | 
|
| 
       157 
157 
     | 
    
         
             
                async def run(self, session, _ifrom, *_):
         
     | 
| 
       158 
158 
     | 
    
         
             
                    assert session is not None
         
     | 
| 
       159 
159 
     | 
    
         
             
                    await session.ready
         
     | 
| 
       160 
     | 
    
         
            -
                    groups = sorted( 
     | 
| 
      
 160 
     | 
    
         
            +
                    groups = sorted(
         
     | 
| 
      
 161 
     | 
    
         
            +
                        session.bookmarks, key=lambda g: (g.name or g.jid.localpart).casefold()
         
     | 
| 
      
 162 
     | 
    
         
            +
                    )
         
     | 
| 
       161 
163 
     | 
    
         
             
                    return TableResult(
         
     | 
| 
       162 
164 
     | 
    
         
             
                        description="Your groups",
         
     | 
| 
       163 
165 
     | 
    
         
             
                        fields=[FormField("name"), FormField("jid", type="jid-single")],
         
     | 
| 
         @@ -364,3 +366,21 @@ class LeaveGroup(Command): 
     | 
|
| 
       364 
366 
     | 
    
         
             
                async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC) -> None:
         
     | 
| 
       365 
367 
     | 
    
         
             
                    await session.on_leave_group(group.legacy_id)
         
     | 
| 
       366 
368 
     | 
    
         
             
                    await session.bookmarks.remove(group, reason="You left this group via slidge.")
         
     | 
| 
      
 369 
     | 
    
         
            +
             
     | 
| 
      
 370 
     | 
    
         
            +
             
     | 
| 
      
 371 
     | 
    
         
            +
            class InviteInGroups(Command):
         
     | 
| 
      
 372 
     | 
    
         
            +
                NAME = "💌 Re-invite me in my groups"
         
     | 
| 
      
 373 
     | 
    
         
            +
                HELP = "Ask the gateway to send invitations for all your private groups"
         
     | 
| 
      
 374 
     | 
    
         
            +
                CHAT_COMMAND = "re-invite"
         
     | 
| 
      
 375 
     | 
    
         
            +
                NODE = GROUPS.node + "/" + CHAT_COMMAND
         
     | 
| 
      
 376 
     | 
    
         
            +
                ACCESS = CommandAccess.USER_LOGGED
         
     | 
| 
      
 377 
     | 
    
         
            +
                CATEGORY = GROUPS
         
     | 
| 
      
 378 
     | 
    
         
            +
             
     | 
| 
      
 379 
     | 
    
         
            +
                async def run(self, session, _ifrom, *_):
         
     | 
| 
      
 380 
     | 
    
         
            +
                    assert session is not None
         
     | 
| 
      
 381 
     | 
    
         
            +
                    await session.ready
         
     | 
| 
      
 382 
     | 
    
         
            +
                    for muc in session.bookmarks:
         
     | 
| 
      
 383 
     | 
    
         
            +
                        if muc.type == MucType.GROUP:
         
     | 
| 
      
 384 
     | 
    
         
            +
                            session.send_gateway_invite(
         
     | 
| 
      
 385 
     | 
    
         
            +
                                muc, reason="You asked to be re-invited in all groups."
         
     | 
| 
      
 386 
     | 
    
         
            +
                            )
         
     | 
    
        slidge/contact/contact.py
    CHANGED
    
    | 
         @@ -120,8 +120,7 @@ class LegacyContact( 
     | 
|
| 
       120 
120 
     | 
    
         
             
                def is_friend(self, value: bool) -> None:
         
     | 
| 
       121 
121 
     | 
    
         
             
                    if value == self.is_friend:
         
     | 
| 
       122 
122 
     | 
    
         
             
                        return
         
     | 
| 
       123 
     | 
    
         
            -
                    self. 
     | 
| 
       124 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 123 
     | 
    
         
            +
                    self.update_stored_attribute(is_friend=value)
         
     | 
| 
       125 
124 
     | 
    
         | 
| 
       126 
125 
     | 
    
         
             
                @property
         
     | 
| 
       127 
126 
     | 
    
         
             
                def added_to_roster(self) -> bool:
         
     | 
| 
         @@ -131,8 +130,7 @@ class LegacyContact( 
     | 
|
| 
       131 
130 
     | 
    
         
             
                def added_to_roster(self, value: bool) -> None:
         
     | 
| 
       132 
131 
     | 
    
         
             
                    if value == self.added_to_roster:
         
     | 
| 
       133 
132 
     | 
    
         
             
                        return
         
     | 
| 
       134 
     | 
    
         
            -
                    self. 
     | 
| 
       135 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 133 
     | 
    
         
            +
                    self.update_stored_attribute(added_to_roster=value)
         
     | 
| 
       136 
134 
     | 
    
         | 
| 
       137 
135 
     | 
    
         
             
                @property
         
     | 
| 
       138 
136 
     | 
    
         
             
                def participants(self) -> Iterator["LegacyParticipant"]:
         
     | 
| 
         @@ -170,8 +168,7 @@ class LegacyContact( 
     | 
|
| 
       170 
168 
     | 
    
         
             
                def client_type(self, value: ClientType) -> None:
         
     | 
| 
       171 
169 
     | 
    
         
             
                    if self.stored.client_type == value:
         
     | 
| 
       172 
170 
     | 
    
         
             
                        return
         
     | 
| 
       173 
     | 
    
         
            -
                    self. 
     | 
| 
       174 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 171 
     | 
    
         
            +
                    self.update_stored_attribute(client_type=value)
         
     | 
| 
       175 
172 
     | 
    
         | 
| 
       176 
173 
     | 
    
         
             
                def _set_logger(self) -> None:
         
     | 
| 
       177 
174 
     | 
    
         
             
                    self.log = logging.getLogger(f"{self.user_jid.bare}:contact:{self}")
         
     | 
| 
         @@ -290,13 +287,12 @@ class LegacyContact( 
     | 
|
| 
       290 
287 
     | 
    
         
             
                def name(self, n: Optional[str]) -> None:
         
     | 
| 
       291 
288 
     | 
    
         
             
                    if self.stored.nick == n:
         
     | 
| 
       292 
289 
     | 
    
         
             
                        return
         
     | 
| 
       293 
     | 
    
         
            -
                    self. 
     | 
| 
      
 290 
     | 
    
         
            +
                    self.update_stored_attribute(nick=n)
         
     | 
| 
       294 
291 
     | 
    
         
             
                    self._set_logger()
         
     | 
| 
       295 
292 
     | 
    
         
             
                    if self.is_friend and self.added_to_roster:
         
     | 
| 
       296 
293 
     | 
    
         
             
                        self.xmpp.pubsub.broadcast_nick(
         
     | 
| 
       297 
294 
     | 
    
         
             
                            user_jid=self.user_jid, jid=self.jid.bare, nick=n
         
     | 
| 
       298 
295 
     | 
    
         
             
                        )
         
     | 
| 
       299 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
       300 
296 
     | 
    
         
             
                    for p in self.participants:
         
     | 
| 
       301 
297 
     | 
    
         
             
                        p.nickname = n or str(self.legacy_id)
         
     | 
| 
       302 
298 
     | 
    
         | 
| 
         @@ -361,14 +357,11 @@ class LegacyContact( 
     | 
|
| 
       361 
357 
     | 
    
         
             
                    if pronouns:
         
     | 
| 
       362 
358 
     | 
    
         
             
                        vcard["pronouns"]["text"] = pronouns
         
     | 
| 
       363 
359 
     | 
    
         | 
| 
       364 
     | 
    
         
            -
                    self. 
     | 
| 
       365 
     | 
    
         
            -
                    self.stored.vcard_fetched = True
         
     | 
| 
      
 360 
     | 
    
         
            +
                    self.update_stored_attribute(vcard=str(vcard), vcard_fetched=True)
         
     | 
| 
       366 
361 
     | 
    
         
             
                    self.session.create_task(
         
     | 
| 
       367 
362 
     | 
    
         
             
                        self.xmpp.pubsub.broadcast_vcard_event(self.jid, self.user_jid, vcard)
         
     | 
| 
       368 
363 
     | 
    
         
             
                    )
         
     | 
| 
       369 
364 
     | 
    
         | 
| 
       370 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
       371 
     | 
    
         
            -
             
     | 
| 
       372 
365 
     | 
    
         
             
                def get_roster_item(self):
         
     | 
| 
       373 
366 
     | 
    
         
             
                    item = {
         
     | 
| 
       374 
367 
     | 
    
         
             
                        "subscription": self.__get_subscription_string(),
         
     | 
| 
         @@ -411,7 +404,7 @@ class LegacyContact( 
     | 
|
| 
       411 
404 
     | 
    
         
             
                        # we only broadcast pubsub events for contacts added to the roster
         
     | 
| 
       412 
405 
     | 
    
         
             
                        # so if something was set before, we need to push it now
         
     | 
| 
       413 
406 
     | 
    
         
             
                        self.added_to_roster = True
         
     | 
| 
       414 
     | 
    
         
            -
                        self.send_last_presence()
         
     | 
| 
      
 407 
     | 
    
         
            +
                        self.send_last_presence(force=True)
         
     | 
| 
       415 
408 
     | 
    
         | 
| 
       416 
409 
     | 
    
         
             
                async def __broadcast_pubsub_items(self) -> None:
         
     | 
| 
       417 
410 
     | 
    
         
             
                    if not self.is_friend:
         
     | 
    
        slidge/contact/roster.py
    CHANGED
    
    | 
         @@ -4,7 +4,7 @@ import warnings 
     | 
|
| 
       4 
4 
     | 
    
         
             
            from typing import TYPE_CHECKING, AsyncIterator, Generic, Iterator, Optional, Type
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
       6 
6 
     | 
    
         
             
            from slixmpp import JID
         
     | 
| 
       7 
     | 
    
         
            -
            from slixmpp.exceptions import IqError, IqTimeout
         
     | 
| 
      
 7 
     | 
    
         
            +
            from slixmpp.exceptions import IqError, IqTimeout, XMPPError
         
     | 
| 
       8 
8 
     | 
    
         
             
            from sqlalchemy.orm import Session
         
     | 
| 
       9 
9 
     | 
    
         
             
            from sqlalchemy.orm import Session as OrmSession
         
     | 
| 
       10 
10 
     | 
    
         | 
| 
         @@ -92,6 +92,10 @@ class LegacyRoster( 
     | 
|
| 
       92 
92 
     | 
    
         
             
                    # :return:
         
     | 
| 
       93 
93 
     | 
    
         
             
                    # """
         
     | 
| 
       94 
94 
     | 
    
         
             
                    username = contact_jid.node
         
     | 
| 
      
 95 
     | 
    
         
            +
                    if not username:
         
     | 
| 
      
 96 
     | 
    
         
            +
                        raise XMPPError(
         
     | 
| 
      
 97 
     | 
    
         
            +
                            "bad-request", "Contacts must have a local part in their JID"
         
     | 
| 
      
 98 
     | 
    
         
            +
                        )
         
     | 
| 
       95 
99 
     | 
    
         
             
                    contact_jid = JID(contact_jid.bare)
         
     | 
| 
       96 
100 
     | 
    
         
             
                    async with self.lock(("username", username)):
         
     | 
| 
       97 
101 
     | 
    
         
             
                        legacy_id = await self.jid_username_to_legacy_id(username)
         
     | 
| 
         @@ -239,6 +243,7 @@ class LegacyRoster( 
     | 
|
| 
       239 
243 
     | 
    
         
             
                            warnings.warn(f"Could not add to roster: {e}")
         
     | 
| 
       240 
244 
     | 
    
         
             
                        else:
         
     | 
| 
       241 
245 
     | 
    
         
             
                            contact.added_to_roster = True
         
     | 
| 
      
 246 
     | 
    
         
            +
                            contact.send_last_presence(force=True)
         
     | 
| 
       242 
247 
     | 
    
         
             
                    orm.commit()
         
     | 
| 
       243 
248 
     | 
    
         
             
                    self.__filling = False
         
     | 
| 
       244 
249 
     | 
    
         | 
    
        slidge/core/config.py
    CHANGED
    
    | 
         @@ -1,22 +1,26 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            from datetime import timedelta
         
     | 
| 
       2 
1 
     | 
    
         
             
            from pathlib import Path
         
     | 
| 
       3 
     | 
    
         
            -
            from typing import Optional 
     | 
| 
      
 2 
     | 
    
         
            +
            from typing import Optional
         
     | 
| 
       4 
3 
     | 
    
         | 
| 
       5 
4 
     | 
    
         
             
            from slixmpp import JID as JIDType
         
     | 
| 
       6 
5 
     | 
    
         | 
| 
      
 6 
     | 
    
         
            +
            # REQUIRED, so not default value
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
     | 
    
         
            -
            class _TimedeltaSeconds(timedelta):
         
     | 
| 
       9 
     | 
    
         
            -
                def __new__(cls, s: str) -> Self:
         
     | 
| 
       10 
     | 
    
         
            -
                    return super().__new__(cls, seconds=int(s))
         
     | 
| 
       11 
8 
     | 
    
         | 
| 
      
 9 
     | 
    
         
            +
            class _Categories:
         
     | 
| 
      
 10 
     | 
    
         
            +
                MANDATORY = (0, "Mandatory settings")
         
     | 
| 
      
 11 
     | 
    
         
            +
                BASE = (10, "Basic configuration")
         
     | 
| 
      
 12 
     | 
    
         
            +
                ATTACHMENTS = (20, "Attachments")
         
     | 
| 
      
 13 
     | 
    
         
            +
                LOG = (30, "Logging")
         
     | 
| 
      
 14 
     | 
    
         
            +
                ADVANCED = (40, "Advanced settings")
         
     | 
| 
       12 
15 
     | 
    
         | 
| 
       13 
     | 
    
         
            -
            # REQUIRED, so not default value
         
     | 
| 
       14 
16 
     | 
    
         | 
| 
       15 
17 
     | 
    
         
             
            LEGACY_MODULE: str
         
     | 
| 
       16 
18 
     | 
    
         
             
            LEGACY_MODULE__DOC = (
         
     | 
| 
       17 
     | 
    
         
            -
                "Importable python module containing (at least) "
         
     | 
| 
       18 
     | 
    
         
            -
                " 
     | 
| 
      
 19 
     | 
    
         
            +
                "Importable python module containing (at least) a BaseGateway and a LegacySession subclass. "
         
     | 
| 
      
 20 
     | 
    
         
            +
                "NB: this is not needed if you use a gateway-specific entrypoint, e.g., `slidgram` or "
         
     | 
| 
      
 21 
     | 
    
         
            +
                "`python -m slidgram`."
         
     | 
| 
       19 
22 
     | 
    
         
             
            )
         
     | 
| 
      
 23 
     | 
    
         
            +
            LEGACY_MODULE__CATEGORY = _Categories.BASE
         
     | 
| 
       20 
24 
     | 
    
         | 
| 
       21 
25 
     | 
    
         
             
            SERVER: str = "localhost"
         
     | 
| 
       22 
26 
     | 
    
         
             
            SERVER__DOC = (
         
     | 
| 
         @@ -27,17 +31,21 @@ SERVER__DOC = ( 
     | 
|
| 
       27 
31 
     | 
    
         
             
                "you change this."
         
     | 
| 
       28 
32 
     | 
    
         
             
            )
         
     | 
| 
       29 
33 
     | 
    
         
             
            SERVER__SHORT = "s"
         
     | 
| 
      
 34 
     | 
    
         
            +
            SERVER__CATEGORY = _Categories.BASE
         
     | 
| 
       30 
35 
     | 
    
         | 
| 
       31 
36 
     | 
    
         
             
            SECRET: str
         
     | 
| 
       32 
37 
     | 
    
         
             
            SECRET__DOC = "The gateway component's secret (required to connect to the XMPP server)"
         
     | 
| 
      
 38 
     | 
    
         
            +
            SECRET__CATEGORY = _Categories.MANDATORY
         
     | 
| 
       33 
39 
     | 
    
         | 
| 
       34 
40 
     | 
    
         
             
            JID: JIDType
         
     | 
| 
       35 
41 
     | 
    
         
             
            JID__DOC = "The gateway component's JID"
         
     | 
| 
       36 
42 
     | 
    
         
             
            JID__SHORT = "j"
         
     | 
| 
      
 43 
     | 
    
         
            +
            JID__CATEGORY = _Categories.MANDATORY
         
     | 
| 
       37 
44 
     | 
    
         | 
| 
       38 
45 
     | 
    
         
             
            PORT: str = "5347"
         
     | 
| 
       39 
46 
     | 
    
         
             
            PORT__DOC = "The XMPP server's port for incoming component connections"
         
     | 
| 
       40 
47 
     | 
    
         
             
            PORT__SHORT = "p"
         
     | 
| 
      
 48 
     | 
    
         
            +
            PORT__CATEGORY = _Categories.BASE
         
     | 
| 
       41 
49 
     | 
    
         | 
| 
       42 
50 
     | 
    
         
             
            # Dynamic default (depends on other values)
         
     | 
| 
       43 
51 
     | 
    
         | 
| 
         @@ -47,6 +55,7 @@ HOME_DIR__DOC = ( 
     | 
|
| 
       47 
55 
     | 
    
         
             
                "Defaults to /var/lib/slidge/${SLIDGE_JID}. "
         
     | 
| 
       48 
56 
     | 
    
         
             
            )
         
     | 
| 
       49 
57 
     | 
    
         
             
            HOME_DIR__DYNAMIC_DEFAULT = True
         
     | 
| 
      
 58 
     | 
    
         
            +
            HOME_DIR__CATEGORY = _Categories.BASE
         
     | 
| 
       50 
59 
     | 
    
         | 
| 
       51 
60 
     | 
    
         
             
            DB_URL: str
         
     | 
| 
       52 
61 
     | 
    
         
             
            DB_URL__DOC = (
         
     | 
| 
         @@ -54,6 +63,7 @@ DB_URL__DOC = ( 
     | 
|
| 
       54 
63 
     | 
    
         
             
                "Defaults to sqlite:///${HOME_DIR}/slidge.sqlite"
         
     | 
| 
       55 
64 
     | 
    
         
             
            )
         
     | 
| 
       56 
65 
     | 
    
         
             
            DB_URL__DYNAMIC_DEFAULT = True
         
     | 
| 
      
 66 
     | 
    
         
            +
            DB_URL__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       57 
67 
     | 
    
         | 
| 
       58 
68 
     | 
    
         
             
            USER_JID_VALIDATOR: str
         
     | 
| 
       59 
69 
     | 
    
         
             
            USER_JID_VALIDATOR__DOC = (
         
     | 
| 
         @@ -62,11 +72,13 @@ USER_JID_VALIDATOR__DOC = ( 
     | 
|
| 
       62 
72 
     | 
    
         
             
                "you probably want to change that to .*@example.com"
         
     | 
| 
       63 
73 
     | 
    
         
             
            )
         
     | 
| 
       64 
74 
     | 
    
         
             
            USER_JID_VALIDATOR__DYNAMIC_DEFAULT = True
         
     | 
| 
      
 75 
     | 
    
         
            +
            USER_JID_VALIDATOR__CATEGORY = _Categories.BASE
         
     | 
| 
       65 
76 
     | 
    
         | 
| 
       66 
77 
     | 
    
         
             
            # Optional, so default value + type hint if default is None
         
     | 
| 
       67 
78 
     | 
    
         | 
| 
       68 
79 
     | 
    
         
             
            ADMINS: tuple[JIDType, ...] = ()
         
     | 
| 
       69 
80 
     | 
    
         
             
            ADMINS__DOC = "JIDs of the gateway admins"
         
     | 
| 
      
 81 
     | 
    
         
            +
            ADMINS__CATEGORY = _Categories.BASE
         
     | 
| 
       70 
82 
     | 
    
         | 
| 
       71 
83 
     | 
    
         
             
            UPLOAD_SERVICE: Optional[str] = None
         
     | 
| 
       72 
84 
     | 
    
         
             
            UPLOAD_SERVICE__DOC = (
         
     | 
| 
         @@ -74,11 +86,13 @@ UPLOAD_SERVICE__DOC = ( 
     | 
|
| 
       74 
86 
     | 
    
         
             
                "This is optional, as it should be automatically determined via service"
         
     | 
| 
       75 
87 
     | 
    
         
             
                "discovery."
         
     | 
| 
       76 
88 
     | 
    
         
             
            )
         
     | 
| 
      
 89 
     | 
    
         
            +
            UPLOAD_SERVICE__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       77 
90 
     | 
    
         | 
| 
       78 
91 
     | 
    
         
             
            AVATAR_SIZE = 200
         
     | 
| 
       79 
92 
     | 
    
         
             
            AVATAR_SIZE__DOC = (
         
     | 
| 
       80 
93 
     | 
    
         
             
                "Maximum image size (width and height), image ratio will be preserved"
         
     | 
| 
       81 
94 
     | 
    
         
             
            )
         
     | 
| 
      
 95 
     | 
    
         
            +
            AVATAR_SIZE__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       82 
96 
     | 
    
         | 
| 
       83 
97 
     | 
    
         
             
            USE_ATTACHMENT_ORIGINAL_URLS = False
         
     | 
| 
       84 
98 
     | 
    
         
             
            USE_ATTACHMENT_ORIGINAL_URLS__DOC = (
         
     | 
| 
         @@ -86,11 +100,13 @@ USE_ATTACHMENT_ORIGINAL_URLS__DOC = ( 
     | 
|
| 
       86 
100 
     | 
    
         
             
                "let XMPP clients directly download them from this URL. Note that this will "
         
     | 
| 
       87 
101 
     | 
    
         
             
                "probably leak your client IP to the legacy network."
         
     | 
| 
       88 
102 
     | 
    
         
             
            )
         
     | 
| 
      
 103 
     | 
    
         
            +
            USE_ATTACHMENT_ORIGINAL_URLS__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       89 
104 
     | 
    
         | 
| 
       90 
105 
     | 
    
         
             
            UPLOAD_REQUESTER: Optional[str] = None
         
     | 
| 
       91 
106 
     | 
    
         
             
            UPLOAD_REQUESTER__DOC = (
         
     | 
| 
       92 
107 
     | 
    
         
             
                "Set which JID should request the upload slots. Defaults to the component JID."
         
     | 
| 
       93 
108 
     | 
    
         
             
            )
         
     | 
| 
      
 109 
     | 
    
         
            +
            UPLOAD_REQUESTER__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       94 
110 
     | 
    
         | 
| 
       95 
111 
     | 
    
         
             
            NO_UPLOAD_PATH: Optional[str] = None
         
     | 
| 
       96 
112 
     | 
    
         
             
            NO_UPLOAD_PATH__DOC = (
         
     | 
| 
         @@ -98,38 +114,45 @@ NO_UPLOAD_PATH__DOC = ( 
     | 
|
| 
       98 
114 
     | 
    
         
             
                "You need to set NO_UPLOAD_URL_PREFIX too if you use this option, and configure "
         
     | 
| 
       99 
115 
     | 
    
         
             
                "an web server to serve files in this dir."
         
     | 
| 
       100 
116 
     | 
    
         
             
            )
         
     | 
| 
      
 117 
     | 
    
         
            +
            NO_UPLOAD_PATH__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       101 
118 
     | 
    
         | 
| 
       102 
119 
     | 
    
         
             
            NO_UPLOAD_URL_PREFIX: Optional[str] = None
         
     | 
| 
       103 
120 
     | 
    
         
             
            NO_UPLOAD_URL_PREFIX__DOC = (
         
     | 
| 
       104 
121 
     | 
    
         
             
                "Base URL that servers files in the dir set in the no-upload-path option, "
         
     | 
| 
       105 
122 
     | 
    
         
             
                "eg https://example.com:666/slidge-attachments/"
         
     | 
| 
       106 
123 
     | 
    
         
             
            )
         
     | 
| 
      
 124 
     | 
    
         
            +
            NO_UPLOAD_URL_PREFIX__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       107 
125 
     | 
    
         | 
| 
       108 
126 
     | 
    
         
             
            NO_UPLOAD_METHOD: str = "copy"
         
     | 
| 
       109 
127 
     | 
    
         
             
            NO_UPLOAD_METHOD__DOC = (
         
     | 
| 
       110 
128 
     | 
    
         
             
                "Whether to 'copy', 'move', 'hardlink' or 'symlink' the files in no-upload-path."
         
     | 
| 
       111 
129 
     | 
    
         
             
            )
         
     | 
| 
      
 130 
     | 
    
         
            +
            NO_UPLOAD_METHOD__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       112 
131 
     | 
    
         | 
| 
       113 
132 
     | 
    
         
             
            NO_UPLOAD_FILE_READ_OTHERS = False
         
     | 
| 
       114 
133 
     | 
    
         
             
            NO_UPLOAD_FILE_READ_OTHERS__DOC = (
         
     | 
| 
       115 
134 
     | 
    
         
             
                "After writing a file in NO_UPLOAD_PATH, change its permission so that 'others' can"
         
     | 
| 
       116 
135 
     | 
    
         
             
                " read it."
         
     | 
| 
       117 
136 
     | 
    
         
             
            )
         
     | 
| 
      
 137 
     | 
    
         
            +
            NO_UPLOAD_FILE_READ_OTHERS__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       118 
138 
     | 
    
         | 
| 
       119 
     | 
    
         
            -
            IGNORE_DELAY_THRESHOLD =  
     | 
| 
      
 139 
     | 
    
         
            +
            IGNORE_DELAY_THRESHOLD = 300
         
     | 
| 
       120 
140 
     | 
    
         
             
            IGNORE_DELAY_THRESHOLD__DOC = (
         
     | 
| 
       121 
141 
     | 
    
         
             
                "Threshold, in seconds, below which the <delay> information is stripped "
         
     | 
| 
       122 
142 
     | 
    
         
             
                "out of emitted stanzas."
         
     | 
| 
       123 
143 
     | 
    
         
             
            )
         
     | 
| 
      
 144 
     | 
    
         
            +
            IGNORE_DELAY_THRESHOLD__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       124 
145 
     | 
    
         | 
| 
       125 
146 
     | 
    
         
             
            PARTIAL_REGISTRATION_TIMEOUT = 3600
         
     | 
| 
       126 
147 
     | 
    
         
             
            PARTIAL_REGISTRATION_TIMEOUT__DOC = (
         
     | 
| 
       127 
148 
     | 
    
         
             
                "Timeout before registration and login. Only useful for legacy networks where "
         
     | 
| 
       128 
149 
     | 
    
         
             
                "a single step registration process is not enough."
         
     | 
| 
       129 
150 
     | 
    
         
             
            )
         
     | 
| 
      
 151 
     | 
    
         
            +
            PARTIAL_REGISTRATION_TIMEOUT__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       130 
152 
     | 
    
         | 
| 
       131 
153 
     | 
    
         
             
            QR_TIMEOUT = 60
         
     | 
| 
       132 
154 
     | 
    
         
             
            QR_TIMEOUT__DOC = "Timeout for QR code flashing confirmation."
         
     | 
| 
      
 155 
     | 
    
         
            +
            QR_TIMEOUT__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       133 
156 
     | 
    
         | 
| 
       134 
157 
     | 
    
         
             
            FIX_FILENAME_SUFFIX_MIME_TYPE = False
         
     | 
| 
       135 
158 
     | 
    
         
             
            FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = (
         
     | 
| 
         @@ -138,9 +161,11 @@ FIX_FILENAME_SUFFIX_MIME_TYPE__DOC = ( 
     | 
|
| 
       138 
161 
     | 
    
         
             
                " Therefore the MIME Type of the file is checked, if the suffix is not valid for"
         
     | 
| 
       139 
162 
     | 
    
         
             
                " that MIME Type, a valid one will be picked."
         
     | 
| 
       140 
163 
     | 
    
         
             
            )
         
     | 
| 
      
 164 
     | 
    
         
            +
            FIX_FILENAME_SUFFIX_MIME_TYPE__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       141 
165 
     | 
    
         | 
| 
       142 
166 
     | 
    
         
             
            LOG_FILE: Optional[Path] = None
         
     | 
| 
       143 
167 
     | 
    
         
             
            LOG_FILE__DOC = "Log to a file instead of stdout/err"
         
     | 
| 
      
 168 
     | 
    
         
            +
            LOG_FILE__CATEGORY = _Categories.LOG
         
     | 
| 
       144 
169 
     | 
    
         | 
| 
       145 
170 
     | 
    
         
             
            LOG_FORMAT: str = "%(levelname)s:%(name)s:%(message)s"
         
     | 
| 
       146 
171 
     | 
    
         
             
            LOG_FORMAT__DOC = (
         
     | 
| 
         @@ -148,41 +173,50 @@ LOG_FORMAT__DOC = ( 
     | 
|
| 
       148 
173 
     | 
    
         
             
                "https://docs.python.org/3/library/logging.html#logrecord-attributes "
         
     | 
| 
       149 
174 
     | 
    
         
             
                "for available options."
         
     | 
| 
       150 
175 
     | 
    
         
             
            )
         
     | 
| 
      
 176 
     | 
    
         
            +
            LOG_FORMAT__CATEGORY = _Categories.LOG
         
     | 
| 
       151 
177 
     | 
    
         | 
| 
       152 
178 
     | 
    
         
             
            MAM_MAX_DAYS = 7
         
     | 
| 
       153 
179 
     | 
    
         
             
            MAM_MAX_DAYS__DOC = "Maximum number of days for group archive retention."
         
     | 
| 
      
 180 
     | 
    
         
            +
            MAM_MAX_DAYS__CATEGORY = _Categories.BASE
         
     | 
| 
       154 
181 
     | 
    
         | 
| 
       155 
182 
     | 
    
         
             
            ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH = 200
         
     | 
| 
       156 
183 
     | 
    
         
             
            ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__DOC = (
         
     | 
| 
       157 
184 
     | 
    
         
             
                "Some legacy network provide ridiculously long filenames, strip above this limit, "
         
     | 
| 
       158 
185 
     | 
    
         
             
                "preserving suffix."
         
     | 
| 
       159 
186 
     | 
    
         
             
            )
         
     | 
| 
      
 187 
     | 
    
         
            +
            ATTACHMENT_MAXIMUM_FILE_NAME_LENGTH__CATEGORY = _Categories.ATTACHMENTS
         
     | 
| 
       160 
188 
     | 
    
         | 
| 
       161 
189 
     | 
    
         
             
            AVATAR_RESAMPLING_THREADS = 2
         
     | 
| 
       162 
190 
     | 
    
         
             
            AVATAR_RESAMPLING_THREADS__DOC = (
         
     | 
| 
       163 
191 
     | 
    
         
             
                "Number of additional threads to use for avatar resampling. Even in a single-core "
         
     | 
| 
       164 
192 
     | 
    
         
             
                "context, this makes avatar resampling non-blocking."
         
     | 
| 
       165 
193 
     | 
    
         
             
            )
         
     | 
| 
      
 194 
     | 
    
         
            +
            AVATAR_RESAMPLING_THREADS__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       166 
195 
     | 
    
         | 
| 
       167 
196 
     | 
    
         
             
            DEV_MODE = False
         
     | 
| 
       168 
197 
     | 
    
         
             
            DEV_MODE__DOC = (
         
     | 
| 
       169 
198 
     | 
    
         
             
                "Enables an interactive python shell via chat commands, for admins."
         
     | 
| 
       170 
199 
     | 
    
         
             
                "Not safe to use in prod, but great during dev."
         
     | 
| 
       171 
200 
     | 
    
         
             
            )
         
     | 
| 
      
 201 
     | 
    
         
            +
            DEV_MODE__CATEGORY = _Categories.ADVANCED
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
       172 
203 
     | 
    
         | 
| 
       173 
204 
     | 
    
         
             
            STRIP_LEADING_EMOJI_ADHOC = False
         
     | 
| 
       174 
205 
     | 
    
         
             
            STRIP_LEADING_EMOJI_ADHOC__DOC = (
         
     | 
| 
       175 
206 
     | 
    
         
             
                "Strip the leading emoji in ad-hoc command names, if present, in case you "
         
     | 
| 
       176 
207 
     | 
    
         
             
                "are a emoji-hater."
         
     | 
| 
       177 
208 
     | 
    
         
             
            )
         
     | 
| 
      
 209 
     | 
    
         
            +
            STRIP_LEADING_EMOJI_ADHOC__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       178 
210 
     | 
    
         | 
| 
       179 
211 
     | 
    
         
             
            COMPONENT_NAME: Optional[str] = None
         
     | 
| 
       180 
212 
     | 
    
         
             
            COMPONENT_NAME__DOC = (
         
     | 
| 
       181 
213 
     | 
    
         
             
                "Overrides the default component name with a custom one. This is seen in service discovery and as the nickname "
         
     | 
| 
       182 
214 
     | 
    
         
             
                "of the component in chat windows."
         
     | 
| 
       183 
215 
     | 
    
         
             
            )
         
     | 
| 
      
 216 
     | 
    
         
            +
            COMPONENT_NAME__CATEGORY = _Categories.ADVANCED
         
     | 
| 
       184 
217 
     | 
    
         | 
| 
       185 
218 
     | 
    
         
             
            WELCOME_MESSAGE: Optional[str] = None
         
     | 
| 
       186 
219 
     | 
    
         
             
            WELCOME_MESSAGE__DOC = (
         
     | 
| 
       187 
220 
     | 
    
         
             
                "Overrides the default welcome message received by newly registered users."
         
     | 
| 
       188 
221 
     | 
    
         
             
            )
         
     | 
| 
      
 222 
     | 
    
         
            +
            WELCOME_MESSAGE__CATEGORY = _Categories.ADVANCED
         
     | 
    
        slidge/core/dispatcher/caps.py
    CHANGED
    
    | 
         @@ -57,8 +57,7 @@ class CapsMixin(DispatcherMixin): 
     | 
|
| 
       57 
57 
     | 
    
         
             
                            ver = contact.stored.caps_ver
         
     | 
| 
       58 
58 
     | 
    
         
             
                        else:
         
     | 
| 
       59 
59 
     | 
    
         
             
                            ver = await contact.get_caps_ver(pfrom)
         
     | 
| 
       60 
     | 
    
         
            -
                            contact. 
     | 
| 
       61 
     | 
    
         
            -
                            contact.commit()
         
     | 
| 
      
 60 
     | 
    
         
            +
                            contact.update_stored_attribute(caps_ver=ver)
         
     | 
| 
       62 
61 
     | 
    
         
             
                    else:
         
     | 
| 
       63 
62 
     | 
    
         
             
                        ver = await caps.get_verstring(pfrom)
         
     | 
| 
       64 
63 
     | 
    
         | 
    
        slidge/core/dispatcher/search.py
    CHANGED
    
    | 
         @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING 
     | 
|
| 
       3 
3 
     | 
    
         
             
            from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
         
     | 
| 
       4 
4 
     | 
    
         
             
            from slixmpp.exceptions import XMPPError
         
     | 
| 
       5 
5 
     | 
    
         | 
| 
       6 
     | 
    
         
            -
            from ...db.models import GatewayUser
         
     | 
| 
       7 
6 
     | 
    
         
             
            from .util import DispatcherMixin, exceptions_to_xmpp_errors
         
     | 
| 
       8 
7 
     | 
    
         | 
| 
       9 
8 
     | 
    
         
             
            if TYPE_CHECKING:
         
     | 
| 
         @@ -32,10 +31,7 @@ class SearchMixin(DispatcherMixin): 
     | 
|
| 
       32 
31 
     | 
    
         
             
                    """
         
     | 
| 
       33 
32 
     | 
    
         
             
                    Prepare the search form using :attr:`.BaseSession.SEARCH_FIELDS`
         
     | 
| 
       34 
33 
     | 
    
         
             
                    """
         
     | 
| 
       35 
     | 
    
         
            -
                     
     | 
| 
       36 
     | 
    
         
            -
                        user = orm.query(GatewayUser).one_or_none()
         
     | 
| 
       37 
     | 
    
         
            -
                    if user is None:
         
     | 
| 
       38 
     | 
    
         
            -
                        raise XMPPError(text="Search is only allowed for registered users")
         
     | 
| 
      
 34 
     | 
    
         
            +
                    await self._get_session(iq)
         
     | 
| 
       39 
35 
     | 
    
         | 
| 
       40 
36 
     | 
    
         
             
                    xmpp = self.xmpp
         
     | 
| 
       41 
37 
     | 
    
         | 
    
        slidge/core/dispatcher/util.py
    CHANGED
    
    | 
         @@ -172,7 +172,9 @@ def exceptions_to_xmpp_errors(cb: HandlerType) -> HandlerType: 
     | 
|
| 
       172 
172 
     | 
    
         
             
                    except NotImplementedError:
         
     | 
| 
       173 
173 
     | 
    
         
             
                        log.debug("NotImplementedError raised in %s", cb)
         
     | 
| 
       174 
174 
     | 
    
         
             
                        raise XMPPError(
         
     | 
| 
       175 
     | 
    
         
            -
                            "feature-not-implemented", 
     | 
| 
      
 175 
     | 
    
         
            +
                            "feature-not-implemented",
         
     | 
| 
      
 176 
     | 
    
         
            +
                            f"{cb.__name__} is not implemented by the legacy module",
         
     | 
| 
      
 177 
     | 
    
         
            +
                            clear=False,
         
     | 
| 
       176 
178 
     | 
    
         
             
                        )
         
     | 
| 
       177 
179 
     | 
    
         
             
                    except Exception as e:
         
     | 
| 
       178 
180 
     | 
    
         
             
                        log.error("Failed to handle incoming stanza: %s", args, exc_info=e)
         
     | 
    
        slidge/core/gateway.py
    CHANGED
    
    | 
         @@ -330,6 +330,7 @@ class BaseGateway( 
     | 
|
| 
       330 
330 
     | 
    
         
             
                    self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR)
         
     | 
| 
       331 
331 
     | 
    
         
             
                    self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]()
         
     | 
| 
       332 
332 
     | 
    
         | 
| 
      
 333 
     | 
    
         
            +
                    self.register_plugins()
         
     | 
| 
       333 
334 
     | 
    
         
             
                    self.__setup_legacy_module_subclasses()
         
     | 
| 
       334 
335 
     | 
    
         | 
| 
       335 
336 
     | 
    
         
             
                    self.get_session_from_stanza: Callable[
         
     | 
| 
         @@ -339,7 +340,6 @@ class BaseGateway( 
     | 
|
| 
       339 
340 
     | 
    
         
             
                        self._session_cls.from_user
         
     | 
| 
       340 
341 
     | 
    
         
             
                    )
         
     | 
| 
       341 
342 
     | 
    
         | 
| 
       342 
     | 
    
         
            -
                    self.register_plugins()
         
     | 
| 
       343 
343 
     | 
    
         
             
                    self.__register_slixmpp_events()
         
     | 
| 
       344 
344 
     | 
    
         
             
                    self.__register_slixmpp_api()
         
     | 
| 
       345 
345 
     | 
    
         
             
                    self.roster.set_backend(RosterBackend(self))
         
     | 
| 
         @@ -390,6 +390,16 @@ class BaseGateway( 
     | 
|
| 
       390 
390 
     | 
    
         
             
                    bookmarks_cls = LegacyBookmarks.get_self_or_unique_subclass()
         
     | 
| 
       391 
391 
     | 
    
         
             
                    roster_cls = LegacyRoster.get_self_or_unique_subclass()
         
     | 
| 
       392 
392 
     | 
    
         | 
| 
      
 393 
     | 
    
         
            +
                    if contact_cls.REACTIONS_SINGLE_EMOJI:  # type:ignore[attr-defined]
         
     | 
| 
      
 394 
     | 
    
         
            +
                        form = Form()
         
     | 
| 
      
 395 
     | 
    
         
            +
                        form["type"] = "result"
         
     | 
| 
      
 396 
     | 
    
         
            +
                        form.add_field(
         
     | 
| 
      
 397 
     | 
    
         
            +
                            "FORM_TYPE", "hidden", value="urn:xmpp:reactions:0:restrictions"
         
     | 
| 
      
 398 
     | 
    
         
            +
                        )
         
     | 
| 
      
 399 
     | 
    
         
            +
                        form.add_field("max_reactions_per_user", value="1", type="number")
         
     | 
| 
      
 400 
     | 
    
         
            +
                        form.add_field("scope", value="domain")
         
     | 
| 
      
 401 
     | 
    
         
            +
                        self.plugin["xep_0128"].add_extended_info(data=form)
         
     | 
| 
      
 402 
     | 
    
         
            +
             
     | 
| 
       393 
403 
     | 
    
         
             
                    session_cls.xmpp = self  # type:ignore[attr-defined]
         
     | 
| 
       394 
404 
     | 
    
         
             
                    contact_cls.xmpp = self  # type:ignore[attr-defined]
         
     | 
| 
       395 
405 
     | 
    
         
             
                    muc_cls.xmpp = self  # type:ignore[attr-defined]
         
     | 
| 
         @@ -1002,6 +1012,7 @@ SLIXMPP_PLUGINS = [ 
     | 
|
| 
       1002 
1012 
     | 
    
         
             
                "xep_0106",  # JID Escaping
         
     | 
| 
       1003 
1013 
     | 
    
         
             
                "xep_0115",  # Entity capabilities
         
     | 
| 
       1004 
1014 
     | 
    
         
             
                "xep_0122",  # Data Forms Validation
         
     | 
| 
      
 1015 
     | 
    
         
            +
                "xep_0128",  # Service Discovery Extensions
         
     | 
| 
       1005 
1016 
     | 
    
         
             
                "xep_0153",  # vCard-Based Avatars (for MUC avatars)
         
     | 
| 
       1006 
1017 
     | 
    
         
             
                "xep_0172",  # User nickname
         
     | 
| 
       1007 
1018 
     | 
    
         
             
                "xep_0184",  # Message Delivery Receipts
         
     | 
    
        slidge/core/mixins/attachment.py
    CHANGED
    
    | 
         @@ -162,15 +162,12 @@ class AttachmentMixin(TextMessageMixin): 
     | 
|
| 
       162 
162 
     | 
    
         
             
                        legacy_file_id=None
         
     | 
| 
       163 
163 
     | 
    
         
             
                        if attachment.legacy_file_id is None
         
     | 
| 
       164 
164 
     | 
    
         
             
                        else str(attachment.legacy_file_id),
         
     | 
| 
       165 
     | 
    
         
            -
                        url=attachment.url,
         
     | 
| 
      
 165 
     | 
    
         
            +
                        url=attachment.url if config.USE_ATTACHMENT_ORIGINAL_URLS else None,
         
     | 
| 
       166 
166 
     | 
    
         
             
                    )
         
     | 
| 
       167 
167 
     | 
    
         | 
| 
       168 
168 
     | 
    
         
             
                async def __get_url(
         
     | 
| 
       169 
169 
     | 
    
         
             
                    self, attachment: LegacyAttachment, stored: Attachment
         
     | 
| 
       170 
170 
     | 
    
         
             
                ) -> tuple[bool, Optional[Path], str]:
         
     | 
| 
       171 
     | 
    
         
            -
                    if attachment.url and config.USE_ATTACHMENT_ORIGINAL_URLS:
         
     | 
| 
       172 
     | 
    
         
            -
                        return False, None, attachment.url
         
     | 
| 
       173 
     | 
    
         
            -
             
     | 
| 
       174 
171 
     | 
    
         
             
                    file_name = attachment.name
         
     | 
| 
       175 
172 
     | 
    
         
             
                    content_type = attachment.content_type
         
     | 
| 
       176 
173 
     | 
    
         
             
                    file_path = attachment.path
         
     | 
| 
         @@ -221,7 +218,9 @@ class AttachmentMixin(TextMessageMixin): 
     | 
|
| 
       221 
218 
     | 
    
         | 
| 
       222 
219 
     | 
    
         
             
                    assert isinstance(file_path, Path)
         
     | 
| 
       223 
220 
     | 
    
         
             
                    if config.FIX_FILENAME_SUFFIX_MIME_TYPE:
         
     | 
| 
       224 
     | 
    
         
            -
                        file_name =  
     | 
| 
      
 221 
     | 
    
         
            +
                        file_name, content_type = fix_suffix(file_path, content_type, file_name)
         
     | 
| 
      
 222 
     | 
    
         
            +
                        attachment.content_type = content_type
         
     | 
| 
      
 223 
     | 
    
         
            +
                        attachment.name = file_name
         
     | 
| 
       225 
224 
     | 
    
         | 
| 
       226 
225 
     | 
    
         
             
                    if config.NO_UPLOAD_PATH:
         
     | 
| 
       227 
226 
     | 
    
         
             
                        local_path, new_url = await self.__no_upload(
         
     | 
| 
         @@ -312,6 +311,50 @@ class AttachmentMixin(TextMessageMixin): 
     | 
|
| 
       312 
311 
     | 
    
         
             
                    stored.sfs = str(sfs)
         
     | 
| 
       313 
312 
     | 
    
         
             
                    msg.append(sfs)
         
     | 
| 
       314 
313 
     | 
    
         | 
| 
      
 314 
     | 
    
         
            +
                async def __set_sfs_and_sims_without_download(
         
     | 
| 
      
 315 
     | 
    
         
            +
                    self, msg: Message, attachment: LegacyAttachment
         
     | 
| 
      
 316 
     | 
    
         
            +
                ) -> None:
         
     | 
| 
      
 317 
     | 
    
         
            +
                    assert attachment.url is not None
         
     | 
| 
      
 318 
     | 
    
         
            +
             
     | 
| 
      
 319 
     | 
    
         
            +
                    if not any(
         
     | 
| 
      
 320 
     | 
    
         
            +
                        (
         
     | 
| 
      
 321 
     | 
    
         
            +
                            attachment.content_type,
         
     | 
| 
      
 322 
     | 
    
         
            +
                            attachment.name,
         
     | 
| 
      
 323 
     | 
    
         
            +
                            attachment.disposition,
         
     | 
| 
      
 324 
     | 
    
         
            +
                        )
         
     | 
| 
      
 325 
     | 
    
         
            +
                    ):
         
     | 
| 
      
 326 
     | 
    
         
            +
                        return
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                    sims = self.xmpp.plugin["xep_0385"].stanza.Sims()
         
     | 
| 
      
 329 
     | 
    
         
            +
                    ref = self.xmpp["xep_0372"].stanza.Reference()
         
     | 
| 
      
 330 
     | 
    
         
            +
             
     | 
| 
      
 331 
     | 
    
         
            +
                    ref["uri"] = attachment.url
         
     | 
| 
      
 332 
     | 
    
         
            +
                    ref["type"] = "data"
         
     | 
| 
      
 333 
     | 
    
         
            +
                    sims["sources"].append(ref)
         
     | 
| 
      
 334 
     | 
    
         
            +
                    sims.enable("file")
         
     | 
| 
      
 335 
     | 
    
         
            +
             
     | 
| 
      
 336 
     | 
    
         
            +
                    xep_0447_stanza = self.xmpp.plugin["xep_0447"].stanza
         
     | 
| 
      
 337 
     | 
    
         
            +
                    sfs = xep_0447_stanza.StatelessFileSharing()
         
     | 
| 
      
 338 
     | 
    
         
            +
                    url_data = xep_0447_stanza.UrlData()
         
     | 
| 
      
 339 
     | 
    
         
            +
                    url_data["target"] = attachment.url
         
     | 
| 
      
 340 
     | 
    
         
            +
                    sfs["sources"].append(url_data)
         
     | 
| 
      
 341 
     | 
    
         
            +
                    sfs.enable("file")
         
     | 
| 
      
 342 
     | 
    
         
            +
             
     | 
| 
      
 343 
     | 
    
         
            +
                    if attachment.content_type:
         
     | 
| 
      
 344 
     | 
    
         
            +
                        sims["file"]["media-type"] = attachment.content_type
         
     | 
| 
      
 345 
     | 
    
         
            +
                        sfs["file"]["media-type"] = attachment.content_type
         
     | 
| 
      
 346 
     | 
    
         
            +
                    if attachment.caption:
         
     | 
| 
      
 347 
     | 
    
         
            +
                        sims["file"]["desc"] = attachment.caption
         
     | 
| 
      
 348 
     | 
    
         
            +
                        sfs["file"]["desc"] = attachment.caption
         
     | 
| 
      
 349 
     | 
    
         
            +
                    if attachment.name:
         
     | 
| 
      
 350 
     | 
    
         
            +
                        sims["file"]["name"] = attachment.name
         
     | 
| 
      
 351 
     | 
    
         
            +
                        sfs["file"]["name"] = attachment.name
         
     | 
| 
      
 352 
     | 
    
         
            +
                    if attachment.disposition:
         
     | 
| 
      
 353 
     | 
    
         
            +
                        sfs["disposition"] = attachment.disposition
         
     | 
| 
      
 354 
     | 
    
         
            +
             
     | 
| 
      
 355 
     | 
    
         
            +
                    msg.append(sims)
         
     | 
| 
      
 356 
     | 
    
         
            +
                    msg.append(sfs)
         
     | 
| 
      
 357 
     | 
    
         
            +
             
     | 
| 
       315 
358 
     | 
    
         
             
                def __send_url(
         
     | 
| 
       316 
359 
     | 
    
         
             
                    self,
         
     | 
| 
       317 
360 
     | 
    
         
             
                    msg: Message,
         
     | 
| 
         @@ -325,6 +368,9 @@ class AttachmentMixin(TextMessageMixin): 
     | 
|
| 
       325 
368 
     | 
    
         
             
                ) -> list[Message]:
         
     | 
| 
       326 
369 
     | 
    
         
             
                    msg["oob"]["url"] = uploaded_url
         
     | 
| 
       327 
370 
     | 
    
         
             
                    msg["body"] = uploaded_url
         
     | 
| 
      
 371 
     | 
    
         
            +
                    if msg.get_plugin("sfs", check=True):
         
     | 
| 
      
 372 
     | 
    
         
            +
                        msg["fallback"].enable("body")
         
     | 
| 
      
 373 
     | 
    
         
            +
                        msg["fallback"]["for"] = self.xmpp.plugin["xep_0447"].stanza.NAMESPACE
         
     | 
| 
       328 
374 
     | 
    
         
             
                    if caption:
         
     | 
| 
       329 
375 
     | 
    
         
             
                        m1 = self._send(msg, carbon=carbon, **kwargs)
         
     | 
| 
       330 
376 
     | 
    
         
             
                        m2 = self.send_text(
         
     | 
| 
         @@ -465,7 +511,10 @@ class AttachmentMixin(TextMessageMixin): 
     | 
|
| 
       465 
511 
     | 
    
         
             
                        new_url = stored.url
         
     | 
| 
       466 
512 
     | 
    
         
             
                    else:
         
     | 
| 
       467 
513 
     | 
    
         
             
                        is_temp, local_path, new_url = await self.__get_url(attachment, stored)
         
     | 
| 
       468 
     | 
    
         
            -
                        if new_url is None 
     | 
| 
      
 514 
     | 
    
         
            +
                        if new_url is None or (
         
     | 
| 
      
 515 
     | 
    
         
            +
                            local_path is not None and local_path.stat().st_size == 0
         
     | 
| 
      
 516 
     | 
    
         
            +
                        ):
         
     | 
| 
      
 517 
     | 
    
         
            +
                            log.warning("Something went wrong with this attachment: %s", attachment)
         
     | 
| 
       469 
518 
     | 
    
         
             
                            msg["body"] = (
         
     | 
| 
       470 
519 
     | 
    
         
             
                                "I tried to send a file, but something went wrong. "
         
     | 
| 
       471 
520 
     | 
    
         
             
                                "Tell your slidge admin to check the logs."
         
     | 
| 
         @@ -474,8 +523,13 @@ class AttachmentMixin(TextMessageMixin): 
     | 
|
| 
       474 
523 
     | 
    
         
             
                            return None, [self._send(msg, **kwargs)]
         
     | 
| 
       475 
524 
     | 
    
         | 
| 
       476 
525 
     | 
    
         
             
                    stored.url = new_url
         
     | 
| 
       477 
     | 
    
         
            -
                     
     | 
| 
       478 
     | 
    
         
            -
             
     | 
| 
      
 526 
     | 
    
         
            +
                    if config.USE_ATTACHMENT_ORIGINAL_URLS and attachment.url:
         
     | 
| 
      
 527 
     | 
    
         
            +
                        await self.__set_sfs_and_sims_without_download(msg, attachment)
         
     | 
| 
      
 528 
     | 
    
         
            +
                    else:
         
     | 
| 
      
 529 
     | 
    
         
            +
                        thumbnail = await self.__set_sims(
         
     | 
| 
      
 530 
     | 
    
         
            +
                            msg, new_url, local_path, attachment, stored
         
     | 
| 
      
 531 
     | 
    
         
            +
                        )
         
     | 
| 
      
 532 
     | 
    
         
            +
                        self.__set_sfs(msg, new_url, local_path, attachment, stored, thumbnail)
         
     | 
| 
       479 
533 
     | 
    
         | 
| 
       480 
534 
     | 
    
         
             
                    if self.session is not NotImplemented:
         
     | 
| 
       481 
535 
     | 
    
         
             
                        with self.xmpp.store.session(expire_on_commit=False) as orm:
         
     | 
    
        slidge/core/mixins/db.py
    CHANGED
    
    | 
         @@ -2,6 +2,8 @@ import logging 
     | 
|
| 
       2 
2 
     | 
    
         
             
            import typing
         
     | 
| 
       3 
3 
     | 
    
         
             
            from contextlib import contextmanager
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
      
 5 
     | 
    
         
            +
            import sqlalchemy as sa
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
       5 
7 
     | 
    
         
             
            from ...db.models import Base, Contact, Room
         
     | 
| 
       6 
8 
     | 
    
         | 
| 
       7 
9 
     | 
    
         
             
            if typing.TYPE_CHECKING:
         
     | 
| 
         @@ -86,3 +88,16 @@ class UpdateInfoMixin(DBMixin): 
     | 
|
| 
       86 
88 
     | 
    
         
             
                    else:
         
     | 
| 
       87 
89 
     | 
    
         
             
                        self.stored.extra_attributes = self.serialize_extra_attributes()
         
     | 
| 
       88 
90 
     | 
    
         
             
                        super().commit(merge=merge)
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                def update_stored_attribute(self, **kwargs) -> None:
         
     | 
| 
      
 93 
     | 
    
         
            +
                    for key, value in kwargs.items():
         
     | 
| 
      
 94 
     | 
    
         
            +
                        setattr(self.stored, key, value)
         
     | 
| 
      
 95 
     | 
    
         
            +
                    if self._updating_info:
         
     | 
| 
      
 96 
     | 
    
         
            +
                        return
         
     | 
| 
      
 97 
     | 
    
         
            +
                    with self.xmpp.store.session() as orm:
         
     | 
| 
      
 98 
     | 
    
         
            +
                        orm.execute(
         
     | 
| 
      
 99 
     | 
    
         
            +
                            sa.update(self.stored.__class__)
         
     | 
| 
      
 100 
     | 
    
         
            +
                            .where(self.stored.__class__.id == self.stored.id)
         
     | 
| 
      
 101 
     | 
    
         
            +
                            .values(**kwargs)
         
     | 
| 
      
 102 
     | 
    
         
            +
                        )
         
     | 
| 
      
 103 
     | 
    
         
            +
                        orm.commit()
         
     | 
| 
         @@ -112,7 +112,7 @@ class MessageMaker(BaseSender): 
     | 
|
| 
       112 
112 
     | 
    
         
             
                        if when.tzinfo is None:
         
     | 
| 
       113 
113 
     | 
    
         
             
                            when = when.astimezone(timezone.utc)
         
     | 
| 
       114 
114 
     | 
    
         
             
                        if self.STRIP_SHORT_DELAY:
         
     | 
| 
       115 
     | 
    
         
            -
                            delay = datetime.now().astimezone(timezone.utc) - when
         
     | 
| 
      
 115 
     | 
    
         
            +
                            delay = (datetime.now().astimezone(timezone.utc) - when).seconds
         
     | 
| 
       116 
116 
     | 
    
         
             
                            if delay < config.IGNORE_DELAY_THRESHOLD:
         
     | 
| 
       117 
117 
     | 
    
         
             
                                return
         
     | 
| 
       118 
118 
     | 
    
         
             
                        msg["delay"].set_stamp(when)
         
     | 
| 
         @@ -191,7 +191,7 @@ class TextMessageMixin(MessageMaker): 
     | 
|
| 
       191 
191 
     | 
    
         
             
                    xmpp_id = kwargs.pop("xmpp_id", None)
         
     | 
| 
       192 
192 
     | 
    
         
             
                    if not xmpp_id:
         
     | 
| 
       193 
193 
     | 
    
         
             
                        xmpp_id = self._legacy_to_xmpp(legacy_msg_id)
         
     | 
| 
       194 
     | 
    
         
            -
                    self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=emojis)
         
     | 
| 
      
 194 
     | 
    
         
            +
                    self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=set(emojis))
         
     | 
| 
       195 
195 
     | 
    
         
             
                    self.__add_reaction_fallback(msg, legacy_msg_id, emojis)
         
     | 
| 
       196 
196 
     | 
    
         
             
                    self._send(msg, **kwargs)
         
     | 
| 
       197 
197 
     | 
    
         | 
    
        slidge/core/mixins/presence.py
    CHANGED
    
    | 
         @@ -46,6 +46,8 @@ def _clear_last_seen_task(contact_pk: int, _task) -> None: 
     | 
|
| 
       46 
46 
     | 
    
         
             
            class PresenceMixin(BaseSender, DBMixin):
         
     | 
| 
       47 
47 
     | 
    
         
             
                _ONLY_SEND_PRESENCE_CHANGES = False
         
     | 
| 
       48 
48 
     | 
    
         | 
| 
      
 49 
     | 
    
         
            +
                # this attribute actually only exists for contacts and not participants
         
     | 
| 
      
 50 
     | 
    
         
            +
                _updating_info: bool
         
     | 
| 
       49 
51 
     | 
    
         
             
                stored: Contact | Participant
         
     | 
| 
       50 
52 
     | 
    
         | 
| 
       51 
53 
     | 
    
         
             
                def __init__(self, *a, **k) -> None:
         
     | 
| 
         @@ -55,15 +57,24 @@ class PresenceMixin(BaseSender, DBMixin): 
     | 
|
| 
       55 
57 
     | 
    
         
             
                    # to DB at the end of update_info()
         
     | 
| 
       56 
58 
     | 
    
         
             
                    self.cached_presence: Optional[CachedPresence] = None
         
     | 
| 
       57 
59 
     | 
    
         | 
| 
      
 60 
     | 
    
         
            +
                def __is_contact(self) -> bool:
         
     | 
| 
      
 61 
     | 
    
         
            +
                    return isinstance(self.stored, Contact)
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
       58 
63 
     | 
    
         
             
                def __stored(self) -> Contact | None:
         
     | 
| 
       59 
     | 
    
         
            -
                    if  
     | 
| 
      
 64 
     | 
    
         
            +
                    if self.__is_contact():
         
     | 
| 
      
 65 
     | 
    
         
            +
                        assert isinstance(self.stored, Contact)
         
     | 
| 
       60 
66 
     | 
    
         
             
                        return self.stored
         
     | 
| 
       61 
67 
     | 
    
         
             
                    else:
         
     | 
| 
      
 68 
     | 
    
         
            +
                        assert isinstance(self.stored, Participant)
         
     | 
| 
       62 
69 
     | 
    
         
             
                        try:
         
     | 
| 
       63 
70 
     | 
    
         
             
                            return self.stored.contact
         
     | 
| 
       64 
71 
     | 
    
         
             
                        except DetachedInstanceError:
         
     | 
| 
       65 
72 
     | 
    
         
             
                            with self.xmpp.store.session() as orm:
         
     | 
| 
       66 
73 
     | 
    
         
             
                                orm.add(self.stored)
         
     | 
| 
      
 74 
     | 
    
         
            +
                                if self.stored.contact is None:
         
     | 
| 
      
 75 
     | 
    
         
            +
                                    return None
         
     | 
| 
      
 76 
     | 
    
         
            +
                                orm.refresh(self.stored.contact)
         
     | 
| 
      
 77 
     | 
    
         
            +
                                orm.merge(self.stored)
         
     | 
| 
       67 
78 
     | 
    
         
             
                                return self.stored.contact
         
     | 
| 
       68 
79 
     | 
    
         | 
| 
       69 
80 
     | 
    
         
             
                @property
         
     | 
| 
         @@ -85,15 +96,14 @@ class PresenceMixin(BaseSender, DBMixin): 
     | 
|
| 
       85 
96 
     | 
    
         
             
                    )
         
     | 
| 
       86 
97 
     | 
    
         | 
| 
       87 
98 
     | 
    
         
             
                def _store_last_presence(self, new: CachedPresence) -> None:
         
     | 
| 
       88 
     | 
    
         
            -
                     
     | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
             
     | 
| 
       91 
     | 
    
         
            -
                         
     | 
| 
       92 
     | 
    
         
            -
             
     | 
| 
       93 
     | 
    
         
            -
                         
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
       95 
     | 
    
         
            -
             
     | 
| 
       96 
     | 
    
         
            -
                            self.commit(merge=True)
         
     | 
| 
      
 99 
     | 
    
         
            +
                    if self.__is_contact():
         
     | 
| 
      
 100 
     | 
    
         
            +
                        contact = self
         
     | 
| 
      
 101 
     | 
    
         
            +
                    elif (contact := getattr(self, "contact", None)) is None:  # type:ignore[assignment]
         
     | 
| 
      
 102 
     | 
    
         
            +
                        return
         
     | 
| 
      
 103 
     | 
    
         
            +
                    contact.update_stored_attribute(  # type:ignore[attr-defined]
         
     | 
| 
      
 104 
     | 
    
         
            +
                        cached_presence=True,
         
     | 
| 
      
 105 
     | 
    
         
            +
                        **new._asdict(),
         
     | 
| 
      
 106 
     | 
    
         
            +
                    )
         
     | 
| 
       97 
107 
     | 
    
         | 
| 
       98 
108 
     | 
    
         
             
                def _make_presence(
         
     | 
| 
       99 
109 
     | 
    
         
             
                    self,
         
     | 
    
        slidge/core/session.py
    CHANGED
    
    
    
        slidge/db/models.py
    CHANGED
    
    | 
         @@ -7,9 +7,9 @@ import sqlalchemy as sa 
     | 
|
| 
       7 
7 
     | 
    
         
             
            from slixmpp import JID
         
     | 
| 
       8 
8 
     | 
    
         
             
            from slixmpp.types import MucAffiliation, MucRole
         
     | 
| 
       9 
9 
     | 
    
         
             
            from sqlalchemy import JSON, ForeignKey, Index, UniqueConstraint
         
     | 
| 
       10 
     | 
    
         
            -
            from sqlalchemy.orm import Mapped,  
     | 
| 
      
 10 
     | 
    
         
            +
            from sqlalchemy.orm import Mapped, mapped_column, relationship
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
       12 
     | 
    
         
            -
            from ..util.types import ClientType, MucType
         
     | 
| 
      
 12 
     | 
    
         
            +
            from ..util.types import ClientType, Hat, MucType
         
     | 
| 
       13 
13 
     | 
    
         
             
            from .meta import Base, JSONSerializable, JSONSerializableTypes
         
     | 
| 
       14 
14 
     | 
    
         | 
| 
       15 
15 
     | 
    
         | 
| 
         @@ -344,7 +344,7 @@ class Participant(Base): 
     | 
|
| 
       344 
344 
     | 
    
         
             
                nickname: Mapped[str] = mapped_column(nullable=False, default=None)
         
     | 
| 
       345 
345 
     | 
    
         
             
                nickname_no_illegal: Mapped[str] = mapped_column(nullable=False, default=None)
         
     | 
| 
       346 
346 
     | 
    
         | 
| 
       347 
     | 
    
         
            -
                hats: Mapped[list[ 
     | 
| 
      
 347 
     | 
    
         
            +
                hats: Mapped[list[Hat]] = mapped_column(JSON, default=list)
         
     | 
| 
       348 
348 
     | 
    
         | 
| 
       349 
349 
     | 
    
         
             
                extra_attributes: Mapped[Optional[JSONSerializable]] = mapped_column(default=None)
         
     | 
| 
       350 
350 
     | 
    
         | 
    
        slidge/db/store.py
    CHANGED
    
    | 
         @@ -7,9 +7,10 @@ from datetime import datetime, timedelta, timezone 
     | 
|
| 
       7 
7 
     | 
    
         
             
            from mimetypes import guess_extension
         
     | 
| 
       8 
8 
     | 
    
         
             
            from typing import Collection, Iterator, Optional, Type
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
      
 10 
     | 
    
         
            +
            import sqlalchemy as sa
         
     | 
| 
       10 
11 
     | 
    
         
             
            from slixmpp.exceptions import XMPPError
         
     | 
| 
       11 
12 
     | 
    
         
             
            from slixmpp.plugins.xep_0231.stanza import BitsOfBinary
         
     | 
| 
       12 
     | 
    
         
            -
            from sqlalchemy import Engine, delete, select, update
         
     | 
| 
      
 13 
     | 
    
         
            +
            from sqlalchemy import Engine, delete, event, select, update
         
     | 
| 
       13 
14 
     | 
    
         
             
            from sqlalchemy.exc import InvalidRequestError
         
     | 
| 
       14 
15 
     | 
    
         
             
            from sqlalchemy.orm import Session, attributes, sessionmaker
         
     | 
| 
       15 
16 
     | 
    
         | 
| 
         @@ -20,6 +21,7 @@ from .meta import Base 
     | 
|
| 
       20 
21 
     | 
    
         
             
            from .models import (
         
     | 
| 
       21 
22 
     | 
    
         
             
                ArchivedMessage,
         
     | 
| 
       22 
23 
     | 
    
         
             
                ArchivedMessageSource,
         
     | 
| 
      
 24 
     | 
    
         
            +
                Avatar,
         
     | 
| 
       23 
25 
     | 
    
         
             
                Bob,
         
     | 
| 
       24 
26 
     | 
    
         
             
                Contact,
         
     | 
| 
       25 
27 
     | 
    
         
             
                ContactSent,
         
     | 
| 
         @@ -213,6 +215,7 @@ class ContactStore(UpdatedMixin): 
     | 
|
| 
       213 
215 
     | 
    
         
             
                def __init__(self, session: Session) -> None:
         
     | 
| 
       214 
216 
     | 
    
         
             
                    super().__init__(session)
         
     | 
| 
       215 
217 
     | 
    
         
             
                    session.execute(update(Contact).values(cached_presence=False))
         
     | 
| 
      
 218 
     | 
    
         
            +
                    session.execute(update(Contact).values(caps_ver=None))
         
     | 
| 
       216 
219 
     | 
    
         | 
| 
       217 
220 
     | 
    
         
             
                @staticmethod
         
     | 
| 
       218 
221 
     | 
    
         
             
                def add_to_sent(session: Session, contact_pk: int, msg_id: str) -> None:
         
     | 
| 
         @@ -589,4 +592,29 @@ class BobStore: 
     | 
|
| 
       589 
592 
     | 
    
         
             
                    session.add(row)
         
     | 
| 
       590 
593 
     | 
    
         | 
| 
       591 
594 
     | 
    
         | 
| 
      
 595 
     | 
    
         
            +
            @event.listens_for(sa.orm.Session, "after_flush")
         
     | 
| 
      
 596 
     | 
    
         
            +
            def _check_avatar_orphans(session, flush_context):
         
     | 
| 
      
 597 
     | 
    
         
            +
                if not session.deleted:
         
     | 
| 
      
 598 
     | 
    
         
            +
                    return
         
     | 
| 
      
 599 
     | 
    
         
            +
             
     | 
| 
      
 600 
     | 
    
         
            +
                potentially_orphaned = set()
         
     | 
| 
      
 601 
     | 
    
         
            +
                for obj in session.deleted:
         
     | 
| 
      
 602 
     | 
    
         
            +
                    if isinstance(obj, (Contact, Room)) and obj.avatar_id:
         
     | 
| 
      
 603 
     | 
    
         
            +
                        potentially_orphaned.add(obj.avatar_id)
         
     | 
| 
      
 604 
     | 
    
         
            +
                if not potentially_orphaned:
         
     | 
| 
      
 605 
     | 
    
         
            +
                    return
         
     | 
| 
      
 606 
     | 
    
         
            +
             
     | 
| 
      
 607 
     | 
    
         
            +
                result = session.execute(
         
     | 
| 
      
 608 
     | 
    
         
            +
                    sa.delete(Avatar).where(
         
     | 
| 
      
 609 
     | 
    
         
            +
                        sa.and_(
         
     | 
| 
      
 610 
     | 
    
         
            +
                            Avatar.id.in_(potentially_orphaned),
         
     | 
| 
      
 611 
     | 
    
         
            +
                            sa.not_(sa.exists().where(Contact.avatar_id == Avatar.id)),
         
     | 
| 
      
 612 
     | 
    
         
            +
                            sa.not_(sa.exists().where(Room.avatar_id == Avatar.id)),
         
     | 
| 
      
 613 
     | 
    
         
            +
                        )
         
     | 
| 
      
 614 
     | 
    
         
            +
                    )
         
     | 
| 
      
 615 
     | 
    
         
            +
                )
         
     | 
| 
      
 616 
     | 
    
         
            +
                deleted_count = result.rowcount
         
     | 
| 
      
 617 
     | 
    
         
            +
                log.debug(f"Auto-deleted %s orphaned avatars", deleted_count)
         
     | 
| 
      
 618 
     | 
    
         
            +
             
     | 
| 
      
 619 
     | 
    
         
            +
             
     | 
| 
       592 
620 
     | 
    
         
             
            log = logging.getLogger(__name__)
         
     | 
    
        slidge/group/room.py
    CHANGED
    
    | 
         @@ -169,8 +169,7 @@ class LegacyMUC( 
     | 
|
| 
       169 
169 
     | 
    
         
             
                def type(self, type_: MucType) -> None:
         
     | 
| 
       170 
170 
     | 
    
         
             
                    if self.type == type_:
         
     | 
| 
       171 
171 
     | 
    
         
             
                        return
         
     | 
| 
       172 
     | 
    
         
            -
                    self. 
     | 
| 
       173 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 172 
     | 
    
         
            +
                    self.update_stored_attribute(muc_type=type_)
         
     | 
| 
       174 
173 
     | 
    
         | 
| 
       175 
174 
     | 
    
         
             
                @property
         
     | 
| 
       176 
175 
     | 
    
         
             
                def n_participants(self):
         
     | 
| 
         @@ -180,8 +179,7 @@ class LegacyMUC( 
     | 
|
| 
       180 
179 
     | 
    
         
             
                def n_participants(self, n_participants: Optional[int]) -> None:
         
     | 
| 
       181 
180 
     | 
    
         
             
                    if self.stored.n_participants == n_participants:
         
     | 
| 
       182 
181 
     | 
    
         
             
                        return
         
     | 
| 
       183 
     | 
    
         
            -
                    self. 
     | 
| 
       184 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 182 
     | 
    
         
            +
                    self.update_stored_attribute(n_participants=n_participants)
         
     | 
| 
       185 
183 
     | 
    
         | 
| 
       186 
184 
     | 
    
         
             
                @property
         
     | 
| 
       187 
185 
     | 
    
         
             
                def user_jid(self):
         
     | 
| 
         @@ -203,8 +201,7 @@ class LegacyMUC( 
     | 
|
| 
       203 
201 
     | 
    
         
             
                def subject_date(self, when: Optional[datetime]) -> None:
         
     | 
| 
       204 
202 
     | 
    
         
             
                    if self.subject_date == when:
         
     | 
| 
       205 
203 
     | 
    
         
             
                        return
         
     | 
| 
       206 
     | 
    
         
            -
                    self. 
     | 
| 
       207 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 204 
     | 
    
         
            +
                    self.update_stored_attribute(subject_date=when)
         
     | 
| 
       208 
205 
     | 
    
         | 
| 
       209 
206 
     | 
    
         
             
                def __send_configuration_change(self, codes) -> None:
         
     | 
| 
       210 
207 
     | 
    
         
             
                    part = self.get_system_participant()
         
     | 
| 
         @@ -222,18 +219,16 @@ class LegacyMUC( 
     | 
|
| 
       222 
219 
     | 
    
         
             
                def user_nick(self, nick: str) -> None:
         
     | 
| 
       223 
220 
     | 
    
         
             
                    if nick == self.user_nick:
         
     | 
| 
       224 
221 
     | 
    
         
             
                        return
         
     | 
| 
       225 
     | 
    
         
            -
                    self. 
     | 
| 
       226 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 222 
     | 
    
         
            +
                    self.update_stored_attribute(user_nick=nick)
         
     | 
| 
       227 
223 
     | 
    
         | 
| 
       228 
224 
     | 
    
         
             
                def add_user_resource(self, resource: str) -> None:
         
     | 
| 
       229 
225 
     | 
    
         
             
                    stored_set = self.get_user_resources()
         
     | 
| 
       230 
226 
     | 
    
         
             
                    if resource in stored_set:
         
     | 
| 
       231 
227 
     | 
    
         
             
                        return
         
     | 
| 
       232 
228 
     | 
    
         
             
                    stored_set.add(resource)
         
     | 
| 
       233 
     | 
    
         
            -
                    self. 
     | 
| 
       234 
     | 
    
         
            -
                        json.dumps(list(stored_set)) if stored_set else None
         
     | 
| 
      
 229 
     | 
    
         
            +
                    self.update_stored_attribute(
         
     | 
| 
      
 230 
     | 
    
         
            +
                        user_resources=(json.dumps(list(stored_set)) if stored_set else None)
         
     | 
| 
       235 
231 
     | 
    
         
             
                    )
         
     | 
| 
       236 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
       237 
232 
     | 
    
         | 
| 
       238 
233 
     | 
    
         
             
                def get_user_resources(self) -> set[str]:
         
     | 
| 
       239 
234 
     | 
    
         
             
                    stored_str = self.stored.user_resources
         
     | 
| 
         @@ -246,10 +241,9 @@ class LegacyMUC( 
     | 
|
| 
       246 
241 
     | 
    
         
             
                    if resource not in stored_set:
         
     | 
| 
       247 
242 
     | 
    
         
             
                        return
         
     | 
| 
       248 
243 
     | 
    
         
             
                    stored_set.remove(resource)
         
     | 
| 
       249 
     | 
    
         
            -
                    self. 
     | 
| 
       250 
     | 
    
         
            -
                        json.dumps(list(stored_set)) if stored_set else None
         
     | 
| 
      
 244 
     | 
    
         
            +
                    self.update_stored_attribute(
         
     | 
| 
      
 245 
     | 
    
         
            +
                        user_resources=(json.dumps(list(stored_set)) if stored_set else None)
         
     | 
| 
       251 
246 
     | 
    
         
             
                    )
         
     | 
| 
       252 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
       253 
247 
     | 
    
         | 
| 
       254 
248 
     | 
    
         
             
                @asynccontextmanager
         
     | 
| 
       255 
249 
     | 
    
         
             
                async def lock(self, id_: str) -> AsyncIterator[None]:
         
     | 
| 
         @@ -296,7 +290,7 @@ class LegacyMUC( 
     | 
|
| 
       296 
290 
     | 
    
         
             
                    self, affiliation: Optional[MucAffiliation] = None
         
     | 
| 
       297 
291 
     | 
    
         
             
                ) -> AsyncIterator[LegacyParticipantType]:
         
     | 
| 
       298 
292 
     | 
    
         
             
                    await self.__fill_participants()
         
     | 
| 
       299 
     | 
    
         
            -
                    with self.xmpp.store.session(expire_on_commit=False) as orm:
         
     | 
| 
      
 293 
     | 
    
         
            +
                    with self.xmpp.store.session(expire_on_commit=False, autoflush=False) as orm:
         
     | 
| 
       300 
294 
     | 
    
         
             
                        orm.add(self.stored)
         
     | 
| 
       301 
295 
     | 
    
         
             
                        for db_participant in self.stored.participants:
         
     | 
| 
       302 
296 
     | 
    
         
             
                            if (
         
     | 
| 
         @@ -346,8 +340,7 @@ class LegacyMUC( 
     | 
|
| 
       346 
340 
     | 
    
         
             
                def name(self, n: str | None) -> None:
         
     | 
| 
       347 
341 
     | 
    
         
             
                    if self.name == n:
         
     | 
| 
       348 
342 
     | 
    
         
             
                        return
         
     | 
| 
       349 
     | 
    
         
            -
                    self. 
     | 
| 
       350 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 343 
     | 
    
         
            +
                    self.update_stored_attribute(name=n)
         
     | 
| 
       351 
344 
     | 
    
         
             
                    self._set_logger()
         
     | 
| 
       352 
345 
     | 
    
         
             
                    self.__send_configuration_change((104,))
         
     | 
| 
       353 
346 
     | 
    
         | 
| 
         @@ -359,8 +352,7 @@ class LegacyMUC( 
     | 
|
| 
       359 
352 
     | 
    
         
             
                def description(self, d: str) -> None:
         
     | 
| 
       360 
353 
     | 
    
         
             
                    if self.description == d:
         
     | 
| 
       361 
354 
     | 
    
         
             
                        return
         
     | 
| 
       362 
     | 
    
         
            -
                    self. 
     | 
| 
       363 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 355 
     | 
    
         
            +
                    self.update_stored_attribute(description=d)
         
     | 
| 
       364 
356 
     | 
    
         
             
                    self.__send_configuration_change((104,))
         
     | 
| 
       365 
357 
     | 
    
         | 
| 
       366 
358 
     | 
    
         
             
                def on_presence_unavailable(self, p: Presence) -> None:
         
     | 
| 
         @@ -440,8 +432,7 @@ class LegacyMUC( 
     | 
|
| 
       440 
432 
     | 
    
         
             
                    if s == self.subject:
         
     | 
| 
       441 
433 
     | 
    
         
             
                        return
         
     | 
| 
       442 
434 
     | 
    
         | 
| 
       443 
     | 
    
         
            -
                    self. 
     | 
| 
       444 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 435 
     | 
    
         
            +
                    self.update_stored_attribute(subject=s)
         
     | 
| 
       445 
436 
     | 
    
         
             
                    self.__get_subject_setter_participant().set_room_subject(
         
     | 
| 
       446 
437 
     | 
    
         
             
                        s, None, self.subject_date, False
         
     | 
| 
       447 
438 
     | 
    
         
             
                    )
         
     | 
| 
         @@ -464,8 +455,7 @@ class LegacyMUC( 
     | 
|
| 
       464 
455 
     | 
    
         
             
                    if subject_setter == self.subject_setter:
         
     | 
| 
       465 
456 
     | 
    
         
             
                        return
         
     | 
| 
       466 
457 
     | 
    
         
             
                    assert isinstance(subject_setter, str | None)
         
     | 
| 
       467 
     | 
    
         
            -
                    self. 
     | 
| 
       468 
     | 
    
         
            -
                    self.commit()
         
     | 
| 
      
 458 
     | 
    
         
            +
                    self.update_stored_attribute(subject_setter=subject_setter)
         
     | 
| 
       469 
459 
     | 
    
         | 
| 
       470 
460 
     | 
    
         
             
                def __get_subject_setter_participant(self) -> LegacyParticipant:
         
     | 
| 
       471 
461 
     | 
    
         
             
                    if self.subject_setter is None:
         
     | 
| 
         @@ -524,6 +514,9 @@ class LegacyMUC( 
     | 
|
| 
       524 
514 
     | 
    
         
             
                    if s := self.subject:
         
     | 
| 
       525 
515 
     | 
    
         
             
                        form.add_field("muc#roominfo_subject", value=s)
         
     | 
| 
       526 
516 
     | 
    
         | 
| 
      
 517 
     | 
    
         
            +
                    if name := self.name:
         
     | 
| 
      
 518 
     | 
    
         
            +
                        form.add_field("muc#roomconfig_roomname", value=name)
         
     | 
| 
      
 519 
     | 
    
         
            +
             
     | 
| 
       527 
520 
     | 
    
         
             
                    if self._set_avatar_task is not None:
         
     | 
| 
       528 
521 
     | 
    
         
             
                        await self._set_avatar_task
         
     | 
| 
       529 
522 
     | 
    
         
             
                    avatar = self.get_avatar()
         
     | 
| 
         @@ -652,7 +645,7 @@ class LegacyMUC( 
     | 
|
| 
       652 
645 
     | 
    
         
             
                            with orm.no_autoflush:
         
     | 
| 
       653 
646 
     | 
    
         
             
                                orm.refresh(self.stored, ["participants"])
         
     | 
| 
       654 
647 
     | 
    
         
             
                    if not user_participant.is_user:
         
     | 
| 
       655 
     | 
    
         
            -
                        self.log.warning("is_user flag not set  
     | 
| 
      
 648 
     | 
    
         
            +
                        self.log.warning("is_user flag not set on user_participant")
         
     | 
| 
       656 
649 
     | 
    
         
             
                        user_participant.is_user = True
         
     | 
| 
       657 
650 
     | 
    
         
             
                    user_participant.send_initial_presence(
         
     | 
| 
       658 
651 
     | 
    
         
             
                        user_full_jid,
         
     | 
    
        slidge/util/test.py
    CHANGED
    
    | 
         @@ -42,7 +42,7 @@ from slidge import ( 
     | 
|
| 
       42 
42 
     | 
    
         | 
| 
       43 
43 
     | 
    
         
             
            from ..command import Command
         
     | 
| 
       44 
44 
     | 
    
         
             
            from ..core import config
         
     | 
| 
       45 
     | 
    
         
            -
            from ..core. 
     | 
| 
      
 45 
     | 
    
         
            +
            from ..core.session import _sessions
         
     | 
| 
       46 
46 
     | 
    
         
             
            from ..db import SlidgeStore
         
     | 
| 
       47 
47 
     | 
    
         
             
            from ..db.avatar import avatar_cache
         
     | 
| 
       48 
48 
     | 
    
         
             
            from ..db.meta import Base
         
     | 
| 
         @@ -206,7 +206,7 @@ class SlidgeTest(SlixTestPlus): 
     | 
|
| 
       206 
206 
     | 
    
         
             
                    user_jid_validator = ".*"
         
     | 
| 
       207 
207 
     | 
    
         
             
                    admins: list[str] = []
         
     | 
| 
       208 
208 
     | 
    
         
             
                    upload_requester = None
         
     | 
| 
       209 
     | 
    
         
            -
                    ignore_delay_threshold =  
     | 
| 
      
 209 
     | 
    
         
            +
                    ignore_delay_threshold = 300
         
     | 
| 
       210 
210 
     | 
    
         | 
| 
       211 
211 
     | 
    
         
             
                @classmethod
         
     | 
| 
       212 
212 
     | 
    
         
             
                def setUpClass(cls) -> None:
         
     | 
| 
         @@ -281,6 +281,7 @@ class SlidgeTest(SlixTestPlus): 
     | 
|
| 
       281 
281 
     | 
    
         
             
                    self.db_engine.echo = False
         
     | 
| 
       282 
282 
     | 
    
         
             
                    super().tearDown()
         
     | 
| 
       283 
283 
     | 
    
         
             
                    Base.metadata.drop_all(self.xmpp.store._engine)
         
     | 
| 
      
 284 
     | 
    
         
            +
                    _sessions.clear()
         
     | 
| 
       284 
285 
     | 
    
         | 
| 
       285 
286 
     | 
    
         
             
                def setup_logged_session(self, n_contacts: int = 0) -> None:
         
     | 
| 
       286 
287 
     | 
    
         
             
                    with self.xmpp.store.session() as orm:
         
     | 
    
        slidge/util/types.py
    CHANGED
    
    
    
        slidge/util/util.py
    CHANGED
    
    | 
         @@ -34,7 +34,9 @@ except ImportError as e: 
     | 
|
| 
       34 
34 
     | 
    
         
             
                )
         
     | 
| 
       35 
35 
     | 
    
         | 
| 
       36 
36 
     | 
    
         | 
| 
       37 
     | 
    
         
            -
            def fix_suffix( 
     | 
| 
      
 37 
     | 
    
         
            +
            def fix_suffix(
         
     | 
| 
      
 38 
     | 
    
         
            +
                path: Path, mime_type: Optional[str], file_name: Optional[str]
         
     | 
| 
      
 39 
     | 
    
         
            +
            ) -> tuple[str, str]:
         
     | 
| 
       38 
40 
     | 
    
         
             
                guessed = magic.from_file(path, mime=True)
         
     | 
| 
       39 
41 
     | 
    
         
             
                if guessed == mime_type:
         
     | 
| 
       40 
42 
     | 
    
         
             
                    log.debug("Magic and given MIME match")
         
     | 
| 
         @@ -53,15 +55,15 @@ def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]) - 
     | 
|
| 
       53 
55 
     | 
    
         | 
| 
       54 
56 
     | 
    
         
             
                if suffix in valid_suffix_list:
         
     | 
| 
       55 
57 
     | 
    
         
             
                    log.debug("Suffix %s is in %s", suffix, valid_suffix_list)
         
     | 
| 
       56 
     | 
    
         
            -
                    return name
         
     | 
| 
      
 58 
     | 
    
         
            +
                    return str(name), guessed
         
     | 
| 
       57 
59 
     | 
    
         | 
| 
       58 
60 
     | 
    
         
             
                valid_suffix = mimetypes.guess_extension(mime_type.split(";")[0], strict=False)
         
     | 
| 
       59 
61 
     | 
    
         
             
                if valid_suffix is None:
         
     | 
| 
       60 
62 
     | 
    
         
             
                    log.debug("No valid suffix found")
         
     | 
| 
       61 
     | 
    
         
            -
                    return name
         
     | 
| 
      
 63 
     | 
    
         
            +
                    return str(name), guessed
         
     | 
| 
       62 
64 
     | 
    
         | 
| 
       63 
65 
     | 
    
         
             
                log.debug("Changing suffix of %s to %s", file_name or path.name, valid_suffix)
         
     | 
| 
       64 
     | 
    
         
            -
                return name.with_suffix(valid_suffix)
         
     | 
| 
      
 66 
     | 
    
         
            +
                return str(name.with_suffix(valid_suffix)), guessed
         
     | 
| 
       65 
67 
     | 
    
         | 
| 
       66 
68 
     | 
    
         | 
| 
       67 
69 
     | 
    
         
             
            class SubclassableOnce(type):
         
     | 
| 
         @@ -10,28 +10,28 @@ slidge/command/base.py,sha256=h6lQ6ypb5uNrppZ_N8mS7ObJpnOINYZS6QokGlWVgAk,13527 
     | 
|
| 
       10 
10 
     | 
    
         
             
            slidge/command/categories.py,sha256=vF0KGDV9sEn8TNkcMoDRw-u3gEyNHSXghOU2JRHQtKs,351
         
     | 
| 
       11 
11 
     | 
    
         
             
            slidge/command/chat_command.py,sha256=r_qalygOCsEoS-OgWw8IIlAzTufhXNjduONbeoepUIA,11557
         
     | 
| 
       12 
12 
     | 
    
         
             
            slidge/command/register.py,sha256=BduDI31Kx8CbWWEdjybimTA5Wcfhn-Jkt8sSPsySCpo,6724
         
     | 
| 
       13 
     | 
    
         
            -
            slidge/command/user.py,sha256= 
     | 
| 
      
 13 
     | 
    
         
            +
            slidge/command/user.py,sha256=3aJ60bvXGyCqjgBH1r6HPbBh4IwgH40rHBilTkeFiv4,13325
         
     | 
| 
       14 
14 
     | 
    
         
             
            slidge/contact/__init__.py,sha256=WMMaHk7UW7YT9EH2LtPdkU0bHQaOp4ikBhbBQskmoc8,191
         
     | 
| 
       15 
     | 
    
         
            -
            slidge/contact/contact.py,sha256= 
     | 
| 
       16 
     | 
    
         
            -
            slidge/contact/roster.py,sha256= 
     | 
| 
      
 15 
     | 
    
         
            +
            slidge/contact/contact.py,sha256=pexE6DU1WsxUoTeF-nENuEqOi7quYQtQot1hD816cl8,19565
         
     | 
| 
      
 16 
     | 
    
         
            +
            slidge/contact/roster.py,sha256=ZbhPC_5opMXdKrdCaQZiHTErxQD4EyWn5Susl6W7M6k,10003
         
     | 
| 
       17 
17 
     | 
    
         
             
            slidge/core/__init__.py,sha256=RG7Jj5JCJERjhqJ31lOLYV-7bH_oblClQD1KF9LsTXo,68
         
     | 
| 
       18 
     | 
    
         
            -
            slidge/core/config.py,sha256= 
     | 
| 
       19 
     | 
    
         
            -
            slidge/core/gateway.py,sha256= 
     | 
| 
      
 18 
     | 
    
         
            +
            slidge/core/config.py,sha256=yFD1RGBZ6Xl115oMqDiF7DFkShlNjF9HEB56Eb_BR_E,7664
         
     | 
| 
      
 19 
     | 
    
         
            +
            slidge/core/gateway.py,sha256=G_ZX8ibKFe-oEXXOY-eRVkZjqRb_VdadIhTezwZEUCA,41698
         
     | 
| 
       20 
20 
     | 
    
         
             
            slidge/core/pubsub.py,sha256=2Em3PvYz-PX-WM7ZqkEU9estNYCyQ--JdJq22DhrUlA,12145
         
     | 
| 
       21 
     | 
    
         
            -
            slidge/core/session.py,sha256= 
     | 
| 
      
 21 
     | 
    
         
            +
            slidge/core/session.py,sha256=XJRZrtAmwqqjaqkLEUPK8xUBxdJrKeraubYYNiiuT7I,29897
         
     | 
| 
       22 
22 
     | 
    
         
             
            slidge/core/dispatcher/__init__.py,sha256=1EXcjXietUKlxEqdrCWCV3xZ3q_DSsjHoqWrPMbtYao,84
         
     | 
| 
       23 
     | 
    
         
            -
            slidge/core/dispatcher/caps.py,sha256= 
     | 
| 
      
 23 
     | 
    
         
            +
            slidge/core/dispatcher/caps.py,sha256=7gvJLeushBtiyqCzBapzbwallH7l3tVN9PYe8MXiKIo,2237
         
     | 
| 
       24 
24 
     | 
    
         
             
            slidge/core/dispatcher/disco.py,sha256=xVPyBFnnkON-JjjM1oydRa-dqnDbwAZER2MGQACRhVk,2309
         
     | 
| 
       25 
     | 
    
         
            -
            slidge/core/dispatcher/presence.py,sha256= 
     | 
| 
       26 
     | 
    
         
            -
            slidge/core/dispatcher/registration.py,sha256= 
     | 
| 
       27 
     | 
    
         
            -
            slidge/core/dispatcher/search.py,sha256= 
     | 
| 
      
 25 
     | 
    
         
            +
            slidge/core/dispatcher/presence.py,sha256=MkSOY7uZQnujTwhIU_nVxygTul63VBaPjTT3X4iTMu4,7090
         
     | 
| 
      
 26 
     | 
    
         
            +
            slidge/core/dispatcher/registration.py,sha256=R4bsyiR8elbLKiFMYv2E54VAUg6qTo-_2CzdbCOZjrY,3419
         
     | 
| 
      
 27 
     | 
    
         
            +
            slidge/core/dispatcher/search.py,sha256=j7LGht2M36FfhXRzcdZiV-8OdCVsVHb2vJb6p3pIbpk,3202
         
     | 
| 
       28 
28 
     | 
    
         
             
            slidge/core/dispatcher/session_dispatcher.py,sha256=ysgPhns7NgUxhmkgEwRv-yDkSnUIXEdc-FsgqDqQAkE,3466
         
     | 
| 
       29 
     | 
    
         
            -
            slidge/core/dispatcher/util.py,sha256= 
     | 
| 
      
 29 
     | 
    
         
            +
            slidge/core/dispatcher/util.py,sha256=yxNevqjjLOzApLpqIRFHNQmpsahwZn55qCsNkOSXohc,6225
         
     | 
| 
       30 
30 
     | 
    
         
             
            slidge/core/dispatcher/vcard.py,sha256=qHZZShq3Iyvgh1FkcAgGhdKXF5m1VUqeb4EWkY0cbFw,5203
         
     | 
| 
       31 
31 
     | 
    
         
             
            slidge/core/dispatcher/message/__init__.py,sha256=gNeZZ0wtCI9JBqMe6tpumwV1TjY0mnPWTJc94uFTN-I,244
         
     | 
| 
       32 
32 
     | 
    
         
             
            slidge/core/dispatcher/message/chat_state.py,sha256=RbtM_nlZyvOHusZkDEP0TXA4wMp_N435490eE4wW8U0,2143
         
     | 
| 
       33 
33 
     | 
    
         
             
            slidge/core/dispatcher/message/marker.py,sha256=WZyf72_SM6sDGPMEOzhu93o2KbgxSxNF25jwsiM7h2A,2439
         
     | 
| 
       34 
     | 
    
         
            -
            slidge/core/dispatcher/message/message.py,sha256= 
     | 
| 
      
 34 
     | 
    
         
            +
            slidge/core/dispatcher/message/message.py,sha256=h-oNEC6In3dH2TpMgRVyMAvZjvWDczaq8nowOzVz_R4,16231
         
     | 
| 
       35 
35 
     | 
    
         
             
            slidge/core/dispatcher/muc/__init__.py,sha256=60YUr0i8PCZEPyNNTynJueRbbxF5pqzdyVf8z_XFXmM,290
         
     | 
| 
       36 
36 
     | 
    
         
             
            slidge/core/dispatcher/muc/admin.py,sha256=1tDZ9hHD6q5SqCjsYOpDimPB3Iyl21YO5RnK1goEGso,3284
         
     | 
| 
       37 
37 
     | 
    
         
             
            slidge/core/dispatcher/muc/mam.py,sha256=7vfmMI_mJOIrc9KCbtTibJSowhZTBBFwXWc84Ikpu3I,2994
         
     | 
| 
         @@ -39,21 +39,21 @@ slidge/core/dispatcher/muc/misc.py,sha256=DgUCSVwcv7kD5xmkS59E-TGf9yWDZiu6NBHgVS 
     | 
|
| 
       39 
39 
     | 
    
         
             
            slidge/core/dispatcher/muc/owner.py,sha256=dDAxpRaA8H_NJQNIyBNixck2oG4GHZeEQqPhKd7MmDQ,3359
         
     | 
| 
       40 
40 
     | 
    
         
             
            slidge/core/dispatcher/muc/ping.py,sha256=EgKKS9AvMnW-vINGcoGbtk6NdbN9A7zVaGfT5T7F6YE,1699
         
     | 
| 
       41 
41 
     | 
    
         
             
            slidge/core/mixins/__init__.py,sha256=Zea39CCwjJU5XfHwcYPEZ9Sin8z1BZxoV68G2RwC3nE,386
         
     | 
| 
       42 
     | 
    
         
            -
            slidge/core/mixins/attachment.py,sha256= 
     | 
| 
      
 42 
     | 
    
         
            +
            slidge/core/mixins/attachment.py,sha256=k-4nwteql4TmhsZDLqPVgzVyuaRQzk-JEOG-k7w-VxM,23938
         
     | 
| 
       43 
43 
     | 
    
         
             
            slidge/core/mixins/avatar.py,sha256=0E0mQxdTUcJQrYXlBkYqkNl4bYuga4cIC1s4XA2rED8,5559
         
     | 
| 
       44 
44 
     | 
    
         
             
            slidge/core/mixins/base.py,sha256=getXMptzJwIc4fEbeMoJCSKcC3awi8UbKnx5FVCthjc,783
         
     | 
| 
       45 
     | 
    
         
            -
            slidge/core/mixins/db.py,sha256= 
     | 
| 
      
 45 
     | 
    
         
            +
            slidge/core/mixins/db.py,sha256=MlaTJcZjOZAL77oise0kY12tvx-SzPLcxS29QxxwEMM,3160
         
     | 
| 
       46 
46 
     | 
    
         
             
            slidge/core/mixins/disco.py,sha256=mrYhWO9qpnLMAVtKKqwbDh6CNOH2dPNERpyfmWzZGg8,3684
         
     | 
| 
       47 
47 
     | 
    
         
             
            slidge/core/mixins/message.py,sha256=xk4bgiJF9ATe-rgtH4sHU8hUwecBF4KjGIujm90mbXQ,8014
         
     | 
| 
       48 
     | 
    
         
            -
            slidge/core/mixins/message_maker.py,sha256= 
     | 
| 
       49 
     | 
    
         
            -
            slidge/core/mixins/message_text.py,sha256 
     | 
| 
       50 
     | 
    
         
            -
            slidge/core/mixins/presence.py,sha256= 
     | 
| 
      
 48 
     | 
    
         
            +
            slidge/core/mixins/message_maker.py,sha256=jdY0dhpN_RcKwajI0XrV194ruNW120Gfdq333B1Fv1o,6556
         
     | 
| 
      
 49 
     | 
    
         
            +
            slidge/core/mixins/message_text.py,sha256=Unzmwr8TnPC2uuZ2fFiIalOGpQF7YYyT1AbB8i0l6Qc,9637
         
     | 
| 
      
 50 
     | 
    
         
            +
            slidge/core/mixins/presence.py,sha256=SiKd-RUIHybrxs5Ie6Ig3aunQ7Is9t6c7TOm-2HbjbA,10252
         
     | 
| 
       51 
51 
     | 
    
         
             
            slidge/core/mixins/recipient.py,sha256=b0uFnpym-hOFgYxGjXT1xQcZ4YRbDSBftPcNWLzSwEI,1336
         
     | 
| 
       52 
52 
     | 
    
         
             
            slidge/db/__init__.py,sha256=EBDH1JSEhgqYcli2Bw11CRC749wJk8AOucgBzmhDSvU,105
         
     | 
| 
       53 
53 
     | 
    
         
             
            slidge/db/avatar.py,sha256=MXFd1oe0eL5CCUYbc5CpsIcbio3cY3xVoKt39RAoj9I,8240
         
     | 
| 
       54 
54 
     | 
    
         
             
            slidge/db/meta.py,sha256=NtjGWcqPfG7uPfwR_cC6_23zyo8ftqgKX8CbP9IBq6U,2185
         
     | 
| 
       55 
     | 
    
         
            -
            slidge/db/models.py,sha256= 
     | 
| 
       56 
     | 
    
         
            -
            slidge/db/store.py,sha256= 
     | 
| 
      
 55 
     | 
    
         
            +
            slidge/db/models.py,sha256=n_Xmgtfug-ygE2CL0ezvEgeyaSM6dgn7YfaYm78B_7c,12819
         
     | 
| 
      
 56 
     | 
    
         
            +
            slidge/db/store.py,sha256=yhvYgoXpbueprjVUXBmA25wFyEbzDXMxVzqv8aZi5tw,20308
         
     | 
| 
       57 
57 
     | 
    
         
             
            slidge/db/alembic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
         
     | 
| 
       58 
58 
     | 
    
         
             
            slidge/db/alembic/env.py,sha256=hsBlRNs0zF5diSHGRSa8Fi3qRVQDA2rJdR41AEIdvxc,1642
         
     | 
| 
       59 
59 
     | 
    
         
             
            slidge/db/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
         
     | 
| 
         @@ -62,7 +62,7 @@ slidge/group/__init__.py,sha256=yFt7cHqeaKIMN6f9ZyhhspOcJJvBtLedGv-iICG7lto,258 
     | 
|
| 
       62 
62 
     | 
    
         
             
            slidge/group/archive.py,sha256=gNHpGlUstPtBcnBgOarSYzkZHo9E8bNxl_ZY3wM6wHg,6108
         
     | 
| 
       63 
63 
     | 
    
         
             
            slidge/group/bookmarks.py,sha256=eiXtlgirE59dBi1BT1349wCrGuHDkCoak1phCepzkqI,7653
         
     | 
| 
       64 
64 
     | 
    
         
             
            slidge/group/participant.py,sha256=2yDJL2bL0w-EiWjq6V96UEhXUgmih5-GpKsoKfG2zdY,18734
         
     | 
| 
       65 
     | 
    
         
            -
            slidge/group/room.py,sha256= 
     | 
| 
      
 65 
     | 
    
         
            +
            slidge/group/room.py,sha256=03c6WrSptld6SqA_ndxPJOU9Cz7s0Qq-R8PvXdi-r3A,49523
         
     | 
| 
       66 
66 
     | 
    
         
             
            slidge/slixfix/__init__.py,sha256=LvaYZQYjr4l_45AYYpod1dB3MUaZye18vKF-4H8Bm20,4758
         
     | 
| 
       67 
67 
     | 
    
         
             
            slidge/slixfix/delivery_receipt.py,sha256=JmogxsiXYEbTmCM4fvC5wkQs0jBsaJtKl4j_B_18riE,1415
         
     | 
| 
       68 
68 
     | 
    
         
             
            slidge/slixfix/roster.py,sha256=DjjHQqCsKsPChUxV7S0Pm4IAgjfrwgm5tcTdJi3N_gY,1670
         
     | 
| 
         @@ -84,12 +84,12 @@ slidge/util/archive_msg.py,sha256=hGNquu38ouSWSc-kz_oAYPXwjhUVZNSedIpwkrXHSd0,18 
     | 
|
| 
       84 
84 
     | 
    
         
             
            slidge/util/conf.py,sha256=Wv-xr1fQfz6jDCBpj2e5Nm-igMpdIjsYsVfoY8grJoo,7380
         
     | 
| 
       85 
85 
     | 
    
         
             
            slidge/util/jid_escaping.py,sha256=QJ2Yj_j1gTmiO9g2r187iVCu7kia_O5ABhRiLAO2TG4,1073
         
     | 
| 
       86 
86 
     | 
    
         
             
            slidge/util/lock.py,sha256=ZnUi3LGiz271-YeYKo9JzxovJCoSwlP9P65pNyHIO9o,1029
         
     | 
| 
       87 
     | 
    
         
            -
            slidge/util/test.py,sha256= 
     | 
| 
       88 
     | 
    
         
            -
            slidge/util/types.py,sha256= 
     | 
| 
       89 
     | 
    
         
            -
            slidge/util/util.py,sha256= 
     | 
| 
       90 
     | 
    
         
            -
            slidge-0.3. 
     | 
| 
       91 
     | 
    
         
            -
            slidge-0.3. 
     | 
| 
       92 
     | 
    
         
            -
            slidge-0.3. 
     | 
| 
       93 
     | 
    
         
            -
            slidge-0.3. 
     | 
| 
       94 
     | 
    
         
            -
            slidge-0.3. 
     | 
| 
       95 
     | 
    
         
            -
            slidge-0.3. 
     | 
| 
      
 87 
     | 
    
         
            +
            slidge/util/test.py,sha256=o86wX1ksnvAgN47k5facDPc4zgiK7J5oWyThgJsv_vQ,13986
         
     | 
| 
      
 88 
     | 
    
         
            +
            slidge/util/types.py,sha256=BA1vhByeaeni3Z5FTIVYafORpU44OOJOTMByL-QqfLs,5870
         
     | 
| 
      
 89 
     | 
    
         
            +
            slidge/util/util.py,sha256=g6jFu5xgAtRA9FBCIBJT3079t_bQKZYOUvi9L5pTbbk,9595
         
     | 
| 
      
 90 
     | 
    
         
            +
            slidge-0.3.3.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
         
     | 
| 
      
 91 
     | 
    
         
            +
            slidge-0.3.3.dist-info/METADATA,sha256=Q_WFVFDDTzOc3M9AuY_eRg5zpclCTxaNzqeP0Rp7gRU,5054
         
     | 
| 
      
 92 
     | 
    
         
            +
            slidge-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
         
     | 
| 
      
 93 
     | 
    
         
            +
            slidge-0.3.3.dist-info/entry_points.txt,sha256=py3_x834fFJ2TEzPd18Wt2DnysdAfuVqJ5zzBrXbAZs,44
         
     | 
| 
      
 94 
     | 
    
         
            +
            slidge-0.3.3.dist-info/top_level.txt,sha256=2LRjDYHaGZ5ieCMF8xy58JIiabRMzX-MGMbCZwfE17c,7
         
     | 
| 
      
 95 
     | 
    
         
            +
            slidge-0.3.3.dist-info/RECORD,,
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |