slidge 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +111 -44
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -795
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.2.dist-info/RECORD +0 -96
  100. slidge-0.1.2.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
slidge/util/db.py CHANGED
@@ -1,229 +1,5 @@
1
- """
2
- This module covers a backend for storing user data persistently and managing a
3
- pseudo-roster for the gateway component.
4
- """
1
+ # here to allow migration of the user store from v0.1
2
+ # since it relies on shelf, which relies on pickle, we need to keep objects
3
+ # importable where they were when the shelf was written
5
4
 
6
- import dataclasses
7
- import datetime
8
- import logging
9
- import os.path
10
- import shelve
11
- from io import BytesIO
12
- from os import PathLike
13
- from typing import Iterable, Optional, Union
14
-
15
- from pickle_secure import Pickler, Unpickler
16
- from slixmpp import JID, Iq, Message, Presence
17
-
18
- from .sql import db
19
-
20
-
21
- # noinspection PyUnresolvedReferences
22
- class EncryptedShelf(shelve.DbfilenameShelf):
23
- cache: dict
24
- dict: dict
25
- writeback: bool
26
- keyencoding: str
27
- _protocol: int
28
-
29
- def __init__(
30
- self, filename: PathLike, key: str, flag="c", protocol=None, writeback=False
31
- ):
32
- super().__init__(str(filename), flag, protocol, writeback)
33
- self.secret_key = key
34
-
35
- def __getitem__(self, key):
36
- try:
37
- value = self.cache[key]
38
- except KeyError:
39
- f = BytesIO(self.dict[key.encode(self.keyencoding)])
40
- value = Unpickler(f, key=self.secret_key).load() # type:ignore
41
- if self.writeback:
42
- self.cache[key] = value
43
- return value
44
-
45
- def __setitem__(self, key, value):
46
- if self.writeback:
47
- self.cache[key] = value
48
- f = BytesIO()
49
- p = Pickler(f, self._protocol, key=self.secret_key) # type:ignore
50
- p.dump(value)
51
- self.dict[key.encode(self.keyencoding)] = f.getvalue()
52
-
53
-
54
- @dataclasses.dataclass
55
- class GatewayUser:
56
- """
57
- A gateway user
58
- """
59
-
60
- bare_jid: str
61
- """Bare JID of the user"""
62
- registration_form: dict[str, Optional[str]]
63
- """Content of the registration form, as a dict"""
64
- plugin_data: Optional[dict] = None
65
- registration_date: Optional[datetime.datetime] = None
66
-
67
- def __hash__(self):
68
- return hash(self.bare_jid)
69
-
70
- def __repr__(self):
71
- return f"<User {self.bare_jid}>"
72
-
73
- def __post_init__(self):
74
- if self.registration_date is None:
75
- self.registration_date = datetime.datetime.now()
76
-
77
- @property
78
- def jid(self) -> JID:
79
- """
80
- The user's (bare) JID
81
-
82
- :return:
83
- """
84
- return JID(self.bare_jid)
85
-
86
- def get(self, field: str, default: str = "") -> Optional[str]:
87
- # """
88
- # Get fields from the registration form (required to comply with slixmpp backend protocol)
89
- #
90
- # :param field: Name of the field
91
- # :param default: Default value to return if the field is not present
92
- #
93
- # :return: Value of the field
94
- # """
95
- return self.registration_form.get(field, default)
96
-
97
- def commit(self):
98
- db.user_store(self)
99
- user_store.commit(self)
100
-
101
-
102
- class UserStore:
103
- """
104
- Basic user store implementation using shelve from the python standard library
105
-
106
- Set_file must be called before it is usable
107
- """
108
-
109
- def __init__(self):
110
- self._users: shelve.Shelf[GatewayUser] = None # type: ignore
111
-
112
- def set_file(self, filename: PathLike, secret_key: Optional[str] = None):
113
- """
114
- Set the file to use to store user data
115
-
116
- :param filename: Path to the shelf file
117
- :param secret_key: Secret key to store files encrypted on disk
118
- """
119
- if self._users is not None:
120
- raise RuntimeError("Shelf file already set!")
121
- if os.path.exists(filename):
122
- log.info("Using existing slidge DB: %s", filename)
123
- else:
124
- log.info("Creating a new slidge DB: %s", filename)
125
- if secret_key:
126
- self._users = EncryptedShelf(filename, key=secret_key)
127
- else:
128
- self._users = shelve.open(str(filename))
129
- for user in self._users.values():
130
- db.user_store(user)
131
- log.info("Registered users in the DB: %s", list(self._users.keys()))
132
-
133
- def get_all(self) -> Iterable[GatewayUser]:
134
- """
135
- Get all users in the store
136
-
137
- :return: An iterable of GatewayUsers
138
- """
139
- return self._users.values()
140
-
141
- def add(self, jid: JID, registration_form: dict[str, Optional[str]]):
142
- """
143
- Add a user to the store.
144
-
145
- NB: there is no reason to call this manually, as this should be covered
146
- by slixmpp XEP-0077 and XEP-0100 plugins
147
-
148
- :param jid: JID of the gateway user
149
- :param registration_form: Content of the registration form (:xep:`0077`)
150
- """
151
- log.debug("Adding user %s", jid)
152
- self._users[jid.bare] = user = GatewayUser(
153
- bare_jid=jid.bare,
154
- registration_form=registration_form,
155
- registration_date=datetime.datetime.now(),
156
- )
157
- self._users.sync()
158
- user.commit()
159
- log.debug("Store: %s", self._users)
160
-
161
- def commit(self, user: GatewayUser):
162
- self._users[user.bare_jid] = user
163
- self._users.sync()
164
-
165
- def get(self, _gateway_jid, _node, ifrom: JID, iq) -> Optional[GatewayUser]:
166
- """
167
- Get a user from the store
168
-
169
- NB: there is no reason to call this, it is used by SliXMPP internal API
170
-
171
- :param _gateway_jid:
172
- :param _node:
173
- :param ifrom:
174
- :param iq:
175
- :return:
176
- """
177
- if ifrom is None: # bug in SliXMPP's XEP_0100 plugin
178
- ifrom = iq["from"]
179
- log.debug("Getting user %s", ifrom.bare)
180
- return self._users.get(ifrom.bare)
181
-
182
- def remove(self, _gateway_jid, _node, ifrom: JID, _iq):
183
- """
184
- Remove a user from the store
185
-
186
- NB: there is no reason to call this, it is used by SliXMPP internal API
187
- """
188
- self.remove_by_jid(ifrom)
189
-
190
- def remove_by_jid(self, jid: JID):
191
- """
192
- Remove a user from the store, by JID
193
- """
194
- j = jid.bare
195
- log.debug("Removing user %s", j)
196
- db.user_del(self._users[j])
197
- del self._users[j]
198
- self._users.sync()
199
-
200
- def get_by_jid(self, jid: JID) -> Optional[GatewayUser]:
201
- """
202
- Convenience function to get a user from their JID.
203
-
204
- :param jid: JID of the gateway user
205
- :return:
206
- """
207
- return self._users.get(jid.bare)
208
-
209
- def get_by_stanza(self, s: Union[Presence, Message, Iq]) -> Optional[GatewayUser]:
210
- """
211
- Convenience function to get a user from a stanza they sent.
212
-
213
- :param s: A stanza sent by the gateway user
214
- :return:
215
- """
216
- return self.get_by_jid(s.get_from())
217
-
218
- def close(self):
219
- self._users.sync()
220
- self._users.close()
221
-
222
-
223
- user_store = UserStore()
224
- """
225
- A persistent store for slidge users. Not public, but I didn't find how to hide
226
- it from the docs!
227
- """
228
-
229
- log = logging.getLogger(__name__)
5
+ from ..db.alembic.old_user_store import * # noqa:F403
slidge/util/test.py CHANGED
@@ -7,6 +7,7 @@ from xml.dom.minidom import parseString
7
7
 
8
8
  import xmldiff.main
9
9
  from slixmpp import (
10
+ JID,
10
11
  ElementBase,
11
12
  Iq,
12
13
  MatcherId,
@@ -20,6 +21,7 @@ from slixmpp.stanza.error import Error
20
21
  from slixmpp.test import SlixTest, TestTransport
21
22
  from slixmpp.xmlstream import highlight, tostring
22
23
  from slixmpp.xmlstream.matcher import MatchIDSender
24
+ from sqlalchemy import create_engine, delete
23
25
 
24
26
  from slidge import (
25
27
  BaseGateway,
@@ -29,12 +31,16 @@ from slidge import (
29
31
  LegacyMUC,
30
32
  LegacyParticipant,
31
33
  LegacyRoster,
32
- user_store,
33
34
  )
34
35
 
35
36
  from ..command import Command
36
37
  from ..core import config
37
38
  from ..core.config import _TimedeltaSeconds
39
+ from ..core.pubsub import PepAvatar, PepNick
40
+ from ..db import SlidgeStore
41
+ from ..db.avatar import avatar_cache
42
+ from ..db.meta import Base
43
+ from ..db.models import Contact
38
44
 
39
45
 
40
46
  class SlixTestPlus(SlixTest):
@@ -187,10 +193,10 @@ class SlidgeTest(SlixTestPlus):
187
193
  no_roster_push = False
188
194
  upload_requester = None
189
195
  ignore_delay_threshold = _TimedeltaSeconds("300")
196
+ last_seen_fallback = True
190
197
 
191
198
  @classmethod
192
199
  def setUpClass(cls):
193
- user_store.set_file(Path(tempfile.mkdtemp()) / "test.db")
194
200
  for k, v in vars(cls.Config).items():
195
201
  setattr(config, k.upper(), v)
196
202
 
@@ -209,9 +215,27 @@ class SlidgeTest(SlixTestPlus):
209
215
  self.plugin, LegacyBookmarks, base_ok=True
210
216
  )
211
217
 
212
- self.xmpp = BaseGateway.get_self_or_unique_subclass()()
218
+ # workaround for duplicate output of sql alchemy's log, cf
219
+ # https://stackoverflow.com/a/76498428/5902284
220
+ from sqlalchemy import log as sqlalchemy_log
213
221
 
222
+ sqlalchemy_log._add_default_handler = lambda x: None
223
+
224
+ engine = self.db_engine = create_engine("sqlite+pysqlite:///:memory:")
225
+ Base.metadata.create_all(engine)
226
+ BaseGateway.store = SlidgeStore(engine)
227
+ BaseGateway._test_mode = True
228
+ try:
229
+ self.xmpp = BaseGateway.get_self_or_unique_subclass()()
230
+ except Exception:
231
+ raise
232
+ self.xmpp.TEST_MODE = True
233
+ PepNick.contact_store = self.xmpp.store.contacts
234
+ PepAvatar.store = self.xmpp.store
235
+ avatar_cache.store = self.xmpp.store.avatars
236
+ avatar_cache.set_dir(Path(tempfile.mkdtemp()))
214
237
  self.xmpp._always_send_everything = True
238
+ engine.echo = True
215
239
 
216
240
  self.xmpp.connection_made(TestTransport(self.xmpp))
217
241
  self.xmpp.session_bind_event.set()
@@ -242,10 +266,73 @@ class SlidgeTest(SlixTestPlus):
242
266
  self.xmpp.use_presence_ids = False
243
267
  Error.namespace = "jabber:component:accept"
244
268
 
269
+ def tearDown(self):
270
+ self.db_engine.echo = False
271
+ super().tearDown()
272
+ import slidge.db.store
273
+
274
+ if slidge.db.store._session is not None:
275
+ slidge.db.store._session.commit()
276
+ slidge.db.store._session = None
277
+ Base.metadata.drop_all(self.xmpp.store._engine)
278
+
279
+ def setup_logged_session(self, n_contacts=0):
280
+ user = self.xmpp.store.users.new(
281
+ JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""}
282
+ )
283
+ user.preferences = {"sync_avatar": True, "sync_presence": True}
284
+ self.xmpp.store.users.update(user)
285
+
286
+ with self.xmpp.store.session() as session:
287
+ session.execute(delete(Contact))
288
+ session.commit()
289
+
290
+ self.run_coro(
291
+ self.xmpp._BaseGateway__dispatcher._on_user_register(
292
+ Iq(sfrom="romeo@montague.lit/gajim")
293
+ )
294
+ )
295
+ welcome = self.next_sent()
296
+ assert welcome["body"], welcome
297
+ stanza = self.next_sent()
298
+ assert "logging in" in stanza["status"].lower(), stanza
299
+ stanza = self.next_sent()
300
+ assert "syncing contacts" in stanza["status"].lower(), stanza
301
+ if BaseGateway.get_self_or_unique_subclass().GROUPS:
302
+ stanza = self.next_sent()
303
+ assert "syncing groups" in stanza["status"].lower(), stanza
304
+ for _ in range(n_contacts):
305
+ probe = self.next_sent()
306
+ assert probe.get_type() == "probe"
307
+ stanza = self.next_sent()
308
+ assert "yup" in stanza["status"].lower(), stanza
309
+ self.romeo: BaseSession = BaseSession.get_self_or_unique_subclass().from_jid(
310
+ JID("romeo@montague.lit")
311
+ )
312
+
313
+ self.juliet: LegacyContact = self.run_coro(
314
+ self.romeo.contacts.by_legacy_id("juliet")
315
+ )
316
+ self.room: LegacyMUC = self.run_coro(self.romeo.bookmarks.by_legacy_id("room"))
317
+ self.first_witch: LegacyParticipant = self.run_coro(
318
+ self.room.get_participant("firstwitch")
319
+ )
320
+ self.send( # language=XML
321
+ """
322
+ <iq type="get"
323
+ to="romeo@montague.lit"
324
+ id="1"
325
+ from="aim.shakespeare.lit">
326
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
327
+ <items node="urn:xmpp:avatar:metadata" />
328
+ </pubsub>
329
+ </iq>
330
+ """
331
+ )
332
+
245
333
  @classmethod
246
334
  def tearDownClass(cls):
247
335
  reset_subclasses()
248
- user_store._users = None
249
336
 
250
337
 
251
338
  def format_stanza(stanza):
slidge/util/types.py CHANGED
@@ -3,6 +3,7 @@ Typing stuff
3
3
  """
4
4
 
5
5
  from dataclasses import dataclass
6
+ from datetime import datetime
6
7
  from enum import IntEnum
7
8
  from pathlib import Path
8
9
  from typing import (
@@ -20,14 +21,13 @@ from typing import (
20
21
  )
21
22
 
22
23
  from slixmpp import Message, Presence
23
- from slixmpp.types import PresenceShows
24
+ from slixmpp.types import PresenceShows, PresenceTypes
24
25
 
25
26
  if TYPE_CHECKING:
26
27
  from ..contact import LegacyContact
27
28
  from ..core.pubsub import PepItem
28
29
  from ..core.session import BaseSession
29
30
  from ..group.participant import LegacyMUC, LegacyParticipant
30
- from .db import GatewayUser
31
31
 
32
32
  AnyBaseSession = BaseSession[Any, Any]
33
33
  else:
@@ -75,6 +75,10 @@ FieldType = Literal[
75
75
  ]
76
76
  MucAffiliation = Literal["owner", "admin", "member", "outcast", "none"]
77
77
  MucRole = Literal["visitor", "participant", "moderator", "none"]
78
+ # https://xmpp.org/registrar/disco-categories.html#client
79
+ ClientType = Literal[
80
+ "bot", "console", "game", "handheld", "pc", "phone", "sms", "tablet", "web"
81
+ ]
78
82
 
79
83
 
80
84
  @dataclass
@@ -84,13 +88,16 @@ class MessageReference(Generic[LegacyMessageType]):
84
88
 
85
89
  At the very minimum, the legacy message ID attribute must be set, but to
86
90
  ensure that the quote is displayed in all XMPP clients, the author must also
87
- be set.
91
+ be set (use the string "user" if the slidge user is the author of the referenced
92
+ message).
88
93
  The body is used as a fallback for XMPP clients that do not support :xep:`0461`
89
94
  of that failed to find the referenced message.
90
95
  """
91
96
 
92
97
  legacy_id: LegacyMessageType
93
- author: Optional[Union["GatewayUser", "LegacyParticipant", "LegacyContact"]] = None
98
+ author: Optional[Union[Literal["user"], "LegacyParticipant", "LegacyContact"]] = (
99
+ None
100
+ )
94
101
  body: Optional[str] = None
95
102
 
96
103
 
@@ -178,3 +185,31 @@ class Mention(NamedTuple):
178
185
  class Hat(NamedTuple):
179
186
  uri: str
180
187
  title: str
188
+
189
+
190
+ class UserPreferences(TypedDict):
191
+ sync_avatar: bool
192
+ sync_presence: bool
193
+
194
+
195
+ class MamMetadata(NamedTuple):
196
+ id: str
197
+ sent_on: datetime
198
+
199
+
200
+ class HoleBound(NamedTuple):
201
+ id: int | str
202
+ timestamp: datetime
203
+
204
+
205
+ class CachedPresence(NamedTuple):
206
+ last_seen: Optional[datetime] = None
207
+ ptype: Optional[PresenceTypes] = None
208
+ pstatus: Optional[str] = None
209
+ pshow: Optional[PresenceShows] = None
210
+
211
+
212
+ class Sticker(NamedTuple):
213
+ path: Path
214
+ content_type: Optional[str]
215
+ hashes: dict[str, str]
slidge/util/util.py CHANGED
@@ -4,8 +4,17 @@ import re
4
4
  import subprocess
5
5
  import warnings
6
6
  from abc import ABCMeta
7
+ from functools import wraps
7
8
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Type
9
+ from time import time
10
+ from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Type, TypeVar
11
+
12
+ try:
13
+ import emoji
14
+ except ImportError:
15
+ EMOJI_LIB_AVAILABLE = False
16
+ else:
17
+ EMOJI_LIB_AVAILABLE = True
9
18
 
10
19
  from .types import Mention, ResourceDict
11
20
 
@@ -274,7 +283,10 @@ def deprecated(name: str, new: Callable):
274
283
  return wrapped
275
284
 
276
285
 
277
- def dict_to_named_tuple(data: dict, cls: Type[NamedTuple]):
286
+ T = TypeVar("T", bound=NamedTuple)
287
+
288
+
289
+ def dict_to_named_tuple(data: dict, cls: Type[T]) -> T:
278
290
  return cls(*(data.get(f) for f in cls._fields)) # type:ignore
279
291
 
280
292
 
@@ -293,3 +305,34 @@ def replace_mentions(
293
305
  cursor = mention.end
294
306
  pieces.append(text[cursor:])
295
307
  return "".join(pieces)
308
+
309
+
310
+ def with_session(func):
311
+ @wraps(func)
312
+ async def wrapped(self, *args, **kwargs):
313
+ with self.xmpp.store.session():
314
+ return await func(self, *args, **kwargs)
315
+
316
+ return wrapped
317
+
318
+
319
+ def timeit(func):
320
+ @wraps(func)
321
+ async def wrapped(self, *args, **kwargs):
322
+ start = time()
323
+ r = await func(self, *args, **kwargs)
324
+ self.log.info("%s took %s ms", func.__name__, round((time() - start) * 1000))
325
+ return r
326
+
327
+ return wrapped
328
+
329
+
330
+ def strip_leading_emoji(text: str) -> str:
331
+ if not EMOJI_LIB_AVAILABLE:
332
+ return text
333
+ words = text.split(" ")
334
+ # is_emoji returns False for 🛷️ for obscure reasons,
335
+ # purely_emoji seems better
336
+ if len(words) > 1 and emoji.purely_emoji(words[0]):
337
+ return " ".join(words[1:])
338
+ return text
@@ -1,25 +1,30 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: XMPP bridging framework
5
5
  Home-page: https://sr.ht/~nicoco/slidge/
6
6
  License: AGPL-3.0-or-later
7
7
  Author: Nicolas Cedilnik
8
8
  Author-email: nicoco@nicoco.fr
9
- Requires-Python: >=3.9,<4.0
9
+ Requires-Python: >=3.11,<4.0
10
10
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
12
  Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Topic :: Internet :: XMPP
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
15
  Requires-Dist: ConfigArgParse (>=1.5.3,<2.0.0)
16
16
  Requires-Dist: Pillow (>=10,<11)
17
17
  Requires-Dist: aiohttp[speedups] (>=3.8.3,<4.0.0)
18
- Requires-Dist: blurhash-python (>=1.2.1,<2.0.0)
18
+ Requires-Dist: alembic (>=1.13.1,<2.0.0)
19
+ Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
19
20
  Requires-Dist: pickle-secure (>=0.99.9,<0.100.0)
20
21
  Requires-Dist: python-magic (>=0.4.27,<0.5.0)
21
22
  Requires-Dist: qrcode (>=7.4.1,<8.0.0)
22
23
  Requires-Dist: slixmpp (>=1.8.5,<2.0.0)
24
+ Requires-Dist: sqlalchemy (>=2.0.29,<3.0.0)
25
+ Requires-Dist: thumbhash (>=0.1.2,<0.2.0)
26
+ Project-URL: Bug tracker, https://todo.sr.ht/~nicoco/slidge
27
+ Project-URL: Chat room, https://conference.nicoco.fr:5281/muc_log/slidge/
23
28
  Project-URL: Documentation, https://slidge.im/
24
29
  Project-URL: Repository, https://git.sr.ht/~nicoco/slidge/
25
30
  Description-Content-Type: text/markdown