slidge 0.2.11__py3-none-any.whl → 0.3.0__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.
Files changed (93) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +123 -210
  8. slidge/contact/roster.py +108 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +120 -93
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +26 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +177 -87
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +200 -147
  31. slidge/core/mixins/avatar.py +105 -177
  32. slidge/core/mixins/base.py +3 -1
  33. slidge/core/mixins/db.py +50 -2
  34. slidge/core/mixins/disco.py +1 -1
  35. slidge/core/mixins/message.py +19 -17
  36. slidge/core/mixins/message_maker.py +29 -15
  37. slidge/core/mixins/message_text.py +67 -30
  38. slidge/core/mixins/presence.py +94 -37
  39. slidge/core/pubsub.py +42 -47
  40. slidge/core/session.py +95 -60
  41. slidge/db/alembic/versions/cef02a8b1451_initial_schema.py +361 -0
  42. slidge/db/avatar.py +150 -119
  43. slidge/db/meta.py +33 -22
  44. slidge/db/models.py +69 -117
  45. slidge/db/store.py +414 -1094
  46. slidge/group/archive.py +65 -55
  47. slidge/group/bookmarks.py +96 -59
  48. slidge/group/participant.py +150 -144
  49. slidge/group/room.py +351 -328
  50. slidge/main.py +34 -22
  51. slidge/migration.py +17 -29
  52. slidge/slixfix/__init__.py +20 -4
  53. slidge/slixfix/delivery_receipt.py +6 -4
  54. slidge/slixfix/link_preview/link_preview.py +1 -1
  55. slidge/slixfix/link_preview/stanza.py +1 -1
  56. slidge/slixfix/roster.py +5 -7
  57. slidge/slixfix/xep_0077/register.py +8 -8
  58. slidge/slixfix/xep_0077/stanza.py +7 -7
  59. slidge/slixfix/xep_0100/gateway.py +12 -13
  60. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  61. slidge/slixfix/xep_0292/vcard4.py +12 -2
  62. slidge/util/archive_msg.py +11 -5
  63. slidge/util/conf.py +27 -21
  64. slidge/util/jid_escaping.py +1 -1
  65. slidge/{core/mixins → util}/lock.py +6 -6
  66. slidge/util/test.py +30 -29
  67. slidge/util/types.py +24 -18
  68. slidge/util/util.py +26 -22
  69. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/METADATA +1 -1
  70. slidge-0.3.0.dist-info/RECORD +95 -0
  71. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/WHEEL +1 -1
  72. slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py +0 -33
  73. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -36
  74. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +0 -85
  75. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -36
  76. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -37
  77. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -41
  78. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +0 -52
  79. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +0 -42
  80. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +0 -61
  81. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -48
  82. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -43
  83. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -139
  84. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +0 -50
  85. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +0 -79
  86. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -214
  87. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +0 -52
  88. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -34
  89. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -26
  90. slidge-0.2.11.dist-info/RECORD +0 -112
  91. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/entry_points.txt +0 -0
  92. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/licenses/LICENSE +0 -0
  93. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/top_level.txt +0 -0
slidge/util/conf.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from functools import cached_property
3
3
  from types import GenericAlias
4
- from typing import Optional, Union, get_args, get_origin, get_type_hints
4
+ from typing import Any, Optional, Union, cast, get_args, get_origin, get_type_hints
5
5
 
6
6
  import configargparse
7
7
 
@@ -11,31 +11,31 @@ class Option:
11
11
  DYNAMIC_DEFAULT_SUFFIX = "__DYNAMIC_DEFAULT"
12
12
  SHORT_SUFFIX = "__SHORT"
13
13
 
14
- def __init__(self, parent: "ConfigModule", name: str):
14
+ def __init__(self, parent: "ConfigModule", name: str) -> None:
15
15
  self.parent = parent
16
16
  self.config_obj = parent.config_obj
17
17
  self.name = name
18
18
 
19
19
  @cached_property
20
- def doc(self):
21
- return getattr(self.config_obj, self.name + self.DOC_SUFFIX)
20
+ def doc(self) -> str:
21
+ return getattr(self.config_obj, self.name + self.DOC_SUFFIX) # type:ignore
22
22
 
23
23
  @cached_property
24
- def required(self):
24
+ def required(self) -> bool:
25
25
  return not hasattr(
26
26
  self.config_obj, self.name + self.DYNAMIC_DEFAULT_SUFFIX
27
27
  ) and not hasattr(self.config_obj, self.name)
28
28
 
29
29
  @cached_property
30
- def default(self):
30
+ def default(self) -> Any:
31
31
  return getattr(self.config_obj, self.name, None)
32
32
 
33
33
  @cached_property
34
- def short(self):
34
+ def short(self) -> str | None:
35
35
  return getattr(self.config_obj, self.name + self.SHORT_SUFFIX, None)
36
36
 
37
37
  @cached_property
38
- def nargs(self):
38
+ def nargs(self) -> str | int | None:
39
39
  type_ = get_type_hints(self.config_obj).get(self.name, type(self.default))
40
40
 
41
41
  if isinstance(type_, GenericAlias):
@@ -44,9 +44,10 @@ class Option:
44
44
  return "*"
45
45
  else:
46
46
  return len(args)
47
+ return None
47
48
 
48
49
  @cached_property
49
- def type(self):
50
+ def type(self) -> Any:
50
51
  type_ = get_type_hints(self.config_obj).get(self.name, type(self.default))
51
52
 
52
53
  if _is_optional(type_):
@@ -58,14 +59,14 @@ class Option:
58
59
  return type_
59
60
 
60
61
  @cached_property
61
- def names(self):
62
+ def names(self) -> list[str]:
62
63
  res = ["--" + self.name.lower().replace("_", "-")]
63
64
  if s := self.short:
64
65
  res.append("-" + s)
65
66
  return res
66
67
 
67
68
  @cached_property
68
- def kwargs(self):
69
+ def kwargs(self) -> dict[str, Any]:
69
70
  kwargs = dict(
70
71
  required=self.required,
71
72
  help=self.doc,
@@ -87,7 +88,7 @@ class Option:
87
88
  kwargs["nargs"] = n
88
89
  return kwargs
89
90
 
90
- def name_to_env_var(self):
91
+ def name_to_env_var(self) -> str:
91
92
  return self.parent.ENV_VAR_PREFIX + self.name
92
93
 
93
94
 
@@ -96,10 +97,10 @@ class ConfigModule:
96
97
 
97
98
  def __init__(
98
99
  self,
99
- config_obj,
100
+ config_obj: Any,
100
101
  parser: Optional[configargparse.ArgumentParser] = None,
101
102
  skip_options: tuple[str, ...] = (),
102
- ):
103
+ ) -> None:
103
104
  self.config_obj = config_obj
104
105
  if parser is None:
105
106
  parser = configargparse.ArgumentParser()
@@ -108,7 +109,7 @@ class ConfigModule:
108
109
  self.skip_options = skip_options
109
110
  self.add_options_to_parser(skip_options)
110
111
 
111
- def _list_options(self):
112
+ def _list_options(self) -> set[str]:
112
113
  return {
113
114
  o
114
115
  for o in (set(dir(self.config_obj)) | set(get_type_hints(self.config_obj)))
@@ -118,7 +119,9 @@ class ConfigModule:
118
119
  and o.lower() not in self.skip_options
119
120
  }
120
121
 
121
- def set_conf(self, argv: Optional[list[str]] = None):
122
+ def set_conf(
123
+ self, argv: Optional[list[str]] = None
124
+ ) -> tuple[configargparse.Namespace, list[str]]:
122
125
  if argv is not None:
123
126
  # this is ugly, but necessary because for plugin config, we used
124
127
  # remaining argv.
@@ -156,7 +159,10 @@ class ConfigModule:
156
159
  upper = _argv_to_option_name(a)
157
160
  opt = options_long.get(upper)
158
161
  if opt and opt.type is bool:
159
- if _argv_to_option_name(aa) not in options_long:
162
+ if (
163
+ not aa.startswith("-")
164
+ and _argv_to_option_name(aa) not in options_long
165
+ ):
160
166
  log.debug("Removing %s from argv", aa)
161
167
  skip_next = True
162
168
 
@@ -186,7 +192,7 @@ class ConfigModule:
186
192
  res.append(Option(self, opt))
187
193
  return res
188
194
 
189
- def add_options_to_parser(self, skip_options: tuple[str, ...]):
195
+ def add_options_to_parser(self, skip_options: tuple[str, ...]) -> None:
190
196
  skip_options = tuple(o.lower() for o in skip_options)
191
197
  p = self.parser
192
198
  for o in sorted(self.options, key=lambda x: (not x.required, x.name)):
@@ -194,11 +200,11 @@ class ConfigModule:
194
200
  continue
195
201
  p.add_argument(*o.names, **o.kwargs)
196
202
 
197
- def update_dynamic_defaults(self, args):
203
+ def update_dynamic_defaults(self, args: configargparse.Namespace) -> None:
198
204
  pass
199
205
 
200
206
 
201
- def _is_optional(t):
207
+ def _is_optional(t: Any) -> bool:
202
208
  if get_origin(t) is Union:
203
209
  args = get_args(t)
204
210
  if len(args) == 2 and isinstance(None, args[1]):
@@ -206,7 +212,7 @@ def _is_optional(t):
206
212
  return False
207
213
 
208
214
 
209
- def _argv_to_option_name(arg: str):
215
+ def _argv_to_option_name(arg: str) -> str:
210
216
  return arg.upper().removeprefix("--").replace("-", "_")
211
217
 
212
218
 
@@ -29,7 +29,7 @@ JID_UNESCAPE_TRANSFORMATIONS = {
29
29
 
30
30
 
31
31
  @lru_cache(1000)
32
- def unescape_node(node: str):
32
+ def unescape_node(node: str) -> str:
33
33
  """Unescape a local portion of a JID."""
34
34
  unescaped = []
35
35
  seq = ""
@@ -1,16 +1,16 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from contextlib import asynccontextmanager
4
- from typing import Hashable
4
+ from typing import Any, AsyncIterator, Hashable
5
5
 
6
6
 
7
7
  class NamedLockMixin:
8
- def __init__(self, *a, **k):
8
+ def __init__(self, *a: Any, **k: Any) -> None:
9
9
  super().__init__(*a, **k)
10
10
  self.__locks = dict[Hashable, asyncio.Lock]()
11
11
 
12
12
  @asynccontextmanager
13
- async def lock(self, id_: Hashable):
13
+ async def lock(self, id_: Hashable) -> AsyncIterator[None]:
14
14
  log.trace("getting %s", id_) # type:ignore
15
15
  locks = self.__locks
16
16
  if not locks.get(id_):
@@ -21,13 +21,13 @@ class NamedLockMixin:
21
21
  yield
22
22
  finally:
23
23
  log.trace("releasing %s", id_) # type:ignore
24
- waiters = locks[id_]._waiters # type:ignore
24
+ waiters = locks[id_]._waiters
25
25
  if not waiters:
26
26
  del locks[id_]
27
27
  log.trace("erasing %s", id_) # type:ignore
28
28
 
29
- def get_lock(self, id_: Hashable):
29
+ def get_lock(self, id_: Hashable) -> asyncio.Lock | None:
30
30
  return self.__locks.get(id_)
31
31
 
32
32
 
33
- log = logging.getLogger(__name__) # type:ignore
33
+ log = logging.getLogger(__name__)
slidge/util/test.py CHANGED
@@ -43,19 +43,25 @@ from slidge import (
43
43
  from ..command import Command
44
44
  from ..core import config
45
45
  from ..core.config import _TimedeltaSeconds
46
- from ..core.pubsub import PepAvatar, PepNick
47
46
  from ..db import SlidgeStore
48
47
  from ..db.avatar import avatar_cache
49
48
  from ..db.meta import Base
50
- from ..db.models import Contact
49
+ from ..db.models import Contact, GatewayUser
51
50
 
52
51
 
53
52
  class SlixTestPlus(SlixTest):
54
- def setUp(self):
53
+ def setUp(self) -> None:
55
54
  super().setUp()
56
55
  Error.namespace = "jabber:component:accept"
57
56
 
58
- def check(self, stanza, criteria, method="exact", defaults=None, use_values=True):
57
+ def check(
58
+ self,
59
+ stanza,
60
+ criteria,
61
+ method: str = "exact",
62
+ defaults=None,
63
+ use_values: bool = True,
64
+ ):
59
65
  """
60
66
  Create and compare several stanza objects to a correct XML string.
61
67
 
@@ -174,7 +180,9 @@ class SlixTestPlus(SlixTest):
174
180
  )
175
181
  self.assertTrue(result, debug)
176
182
 
177
- def next_sent(self, timeout=0.05) -> Optional[Union[Message, Iq, Presence]]:
183
+ def next_sent(
184
+ self, timeout: float = 0.05
185
+ ) -> Optional[Union[Message, Iq, Presence]]:
178
186
  self.wait_for_send_queue()
179
187
  sent = self.xmpp.socket.next_sent(timeout=timeout)
180
188
  if sent is None:
@@ -197,13 +205,11 @@ class SlidgeTest(SlixTestPlus):
197
205
  home_dir = Path(tempfile.mkdtemp())
198
206
  user_jid_validator = ".*"
199
207
  admins: list[str] = []
200
- no_roster_push = False
201
208
  upload_requester = None
202
209
  ignore_delay_threshold = _TimedeltaSeconds("300")
203
- last_seen_fallback = True
204
210
 
205
211
  @classmethod
206
- def setUpClass(cls):
212
+ def setUpClass(cls) -> None:
207
213
  for k, v in vars(cls.Config).items():
208
214
  setattr(config, k.upper(), v)
209
215
 
@@ -237,8 +243,6 @@ class SlidgeTest(SlixTestPlus):
237
243
  except Exception:
238
244
  raise
239
245
  self.xmpp.TEST_MODE = True
240
- PepNick.contact_store = self.xmpp.store.contacts
241
- PepAvatar.store = self.xmpp.store
242
246
  avatar_cache.store = self.xmpp.store.avatars
243
247
  avatar_cache.set_dir(Path(tempfile.mkdtemp()))
244
248
  self.xmpp._always_send_everything = True
@@ -273,22 +277,20 @@ class SlidgeTest(SlixTestPlus):
273
277
  self.xmpp.use_presence_ids = False
274
278
  Error.namespace = "jabber:component:accept"
275
279
 
276
- def tearDown(self):
280
+ def tearDown(self) -> None:
277
281
  self.db_engine.echo = False
278
282
  super().tearDown()
279
- import slidge.db.store
280
-
281
- if slidge.db.store._session is not None:
282
- slidge.db.store._session.commit()
283
- slidge.db.store._session = None
284
283
  Base.metadata.drop_all(self.xmpp.store._engine)
285
284
 
286
- def setup_logged_session(self, n_contacts=0):
287
- user = self.xmpp.store.users.new(
288
- JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""}
289
- )
290
- user.preferences = {"sync_avatar": True, "sync_presence": True}
291
- self.xmpp.store.users.update(user)
285
+ def setup_logged_session(self, n_contacts: int = 0) -> None:
286
+ with self.xmpp.store.session() as orm:
287
+ user = GatewayUser(
288
+ jid=JID("romeo@montague.lit/gajim").bare,
289
+ legacy_module_data={"username": "romeo", "city": ""},
290
+ preferences={"sync_avatar": True, "sync_presence": True},
291
+ )
292
+ orm.add(user)
293
+ orm.commit()
292
294
 
293
295
  with self.xmpp.store.session() as session:
294
296
  session.execute(delete(Contact))
@@ -308,9 +310,8 @@ class SlidgeTest(SlixTestPlus):
308
310
  if BaseGateway.get_self_or_unique_subclass().GROUPS:
309
311
  stanza = self.next_sent()
310
312
  assert "syncing groups" in stanza["status"].lower(), stanza
311
- for _ in range(n_contacts):
312
- probe = self.next_sent()
313
- assert probe.get_type() == "probe"
313
+ probe = self.next_sent()
314
+ assert probe.get_type() == "probe"
314
315
  stanza = self.next_sent()
315
316
  assert "yup" in stanza["status"].lower(), stanza
316
317
  self.romeo: BaseSession = BaseSession.get_self_or_unique_subclass().from_jid(
@@ -338,7 +339,7 @@ class SlidgeTest(SlixTestPlus):
338
339
  )
339
340
 
340
341
  @classmethod
341
- def tearDownClass(cls):
342
+ def tearDownClass(cls) -> None:
342
343
  reset_subclasses()
343
344
 
344
345
 
@@ -348,7 +349,7 @@ def format_stanza(stanza):
348
349
  )
349
350
 
350
351
 
351
- def find_subclass(o, parent, base_ok=False):
352
+ def find_subclass(o, parent, base_ok: bool = False):
352
353
  try:
353
354
  vals = vars(o).values()
354
355
  except TypeError:
@@ -366,7 +367,7 @@ def find_subclass(o, parent, base_ok=False):
366
367
  raise RuntimeError
367
368
 
368
369
 
369
- def reset_subclasses():
370
+ def reset_subclasses() -> None:
370
371
  """
371
372
  Reset registered subclasses between test classes.
372
373
 
@@ -383,7 +384,7 @@ def reset_subclasses():
383
384
  # reset_commands()
384
385
 
385
386
 
386
- def reset_commands():
387
+ def reset_commands() -> None:
387
388
  Command.subclasses = [
388
389
  c for c in Command.subclasses if str(c).startswith("<class 'slidge.core")
389
390
  ]
slidge/util/types.py CHANGED
@@ -10,6 +10,7 @@ from typing import (
10
10
  IO,
11
11
  TYPE_CHECKING,
12
12
  Any,
13
+ AsyncIterator,
13
14
  Generic,
14
15
  Hashable,
15
16
  Literal,
@@ -25,19 +26,15 @@ from slixmpp.types import PresenceShows, PresenceTypes, ResourceDict
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from ..contact import LegacyContact
28
- from ..core.pubsub import PepItem
29
29
  from ..core.session import BaseSession
30
- from ..group.participant import LegacyMUC, LegacyParticipant
30
+ from ..group import LegacyMUC
31
+ from ..group.participant import LegacyParticipant
31
32
 
32
33
  AnyBaseSession = BaseSession[Any, Any]
33
34
  else:
34
35
  AnyBaseSession = None
35
36
 
36
37
 
37
- class URL(str):
38
- pass
39
-
40
-
41
38
  LegacyGroupIdType = TypeVar("LegacyGroupIdType", bound=Hashable)
42
39
  """
43
40
  Type of the unique identifier for groups, usually a str or an int,
@@ -47,18 +44,14 @@ LegacyMessageType = TypeVar("LegacyMessageType", bound=Hashable)
47
44
  LegacyThreadType = TypeVar("LegacyThreadType", bound=Hashable)
48
45
  LegacyUserIdType = TypeVar("LegacyUserIdType", bound=Hashable)
49
46
 
50
- LegacyContactType = TypeVar("LegacyContactType", bound="LegacyContact")
51
- LegacyMUCType = TypeVar("LegacyMUCType", bound="LegacyMUC")
47
+ LegacyContactType = TypeVar("LegacyContactType", bound="LegacyContact[Any]")
48
+ LegacyMUCType = TypeVar("LegacyMUCType", bound="LegacyMUC[Any, Any, Any, Any]")
52
49
  LegacyParticipantType = TypeVar("LegacyParticipantType", bound="LegacyParticipant")
53
50
 
54
- PepItemType = TypeVar("PepItemType", bound="PepItem")
55
-
56
- Recipient = Union["LegacyMUC", "LegacyContact"]
51
+ Recipient = Union["LegacyMUC[Any, Any, Any, Any]", "LegacyContact[Any]"]
57
52
  RecipientType = TypeVar("RecipientType", bound=Recipient)
58
- Sender = Union["LegacyContact", "LegacyParticipant"]
59
- AvatarType = Union[bytes, str, Path]
53
+ Sender = Union["LegacyContact[Any]", "LegacyParticipant"]
60
54
  LegacyFileIdType = Union[int, str]
61
- AvatarIdType = Union[LegacyFileIdType, URL]
62
55
 
63
56
  ChatState = Literal["active", "composing", "gone", "inactive", "paused"]
64
57
  ProcessingHint = Literal["no-store", "markable", "store"]
@@ -79,6 +72,7 @@ MucRole = Literal["visitor", "participant", "moderator", "none"]
79
72
  ClientType = Literal[
80
73
  "bot", "console", "game", "handheld", "pc", "phone", "sms", "tablet", "web"
81
74
  ]
75
+ AttachmentDisposition = Literal["attachment", "inline"]
82
76
 
83
77
 
84
78
  @dataclass
@@ -116,21 +110,26 @@ class LegacyAttachment:
116
110
  path: Optional[Union[Path, str]] = None
117
111
  name: Optional[Union[str]] = None
118
112
  stream: Optional[IO[bytes]] = None
113
+ aio_stream: Optional[AsyncIterator[bytes]] = None
119
114
  data: Optional[bytes] = None
120
115
  content_type: Optional[str] = None
121
116
  legacy_file_id: Optional[Union[str, int]] = None
122
117
  url: Optional[str] = None
123
118
  caption: Optional[str] = None
119
+ disposition: Optional[AttachmentDisposition] = None
124
120
  """
125
121
  A caption for this specific image. For a global caption for a list of attachments,
126
122
  use the ``body`` parameter of :meth:`.AttachmentMixin.send_files`
127
123
  """
128
124
 
129
- def __post_init__(self):
130
- if not any(
131
- x is not None for x in (self.path, self.stream, self.data, self.url)
125
+ def __post_init__(self) -> None:
126
+ if all(
127
+ x is None
128
+ for x in (self.path, self.stream, self.data, self.url, self.aio_stream)
132
129
  ):
133
130
  raise TypeError("There is not data in this attachment", self)
131
+ if isinstance(self.path, str):
132
+ self.path = Path(self.path)
134
133
 
135
134
 
136
135
  class MucType(IntEnum):
@@ -171,7 +170,7 @@ class LinkPreview(NamedTuple):
171
170
 
172
171
 
173
172
  class Mention(NamedTuple):
174
- contact: "LegacyContact"
173
+ contact: "LegacyContact[Any]"
175
174
  start: int
176
175
  end: int
177
176
 
@@ -207,3 +206,10 @@ class Sticker(NamedTuple):
207
206
  path: Path
208
207
  content_type: Optional[str]
209
208
  hashes: dict[str, str]
209
+
210
+
211
+ class Avatar(NamedTuple):
212
+ path: Optional[Path] = None
213
+ unique_id: Optional[str | int] = None
214
+ url: Optional[str] = None
215
+ data: Optional[bytes] = None
slidge/util/util.py CHANGED
@@ -7,7 +7,7 @@ from abc import ABCMeta
7
7
  from functools import wraps
8
8
  from pathlib import Path
9
9
  from time import time
10
- from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Type, TypeVar
10
+ from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, Type, TypeVar
11
11
 
12
12
  try:
13
13
  import emoji
@@ -34,7 +34,7 @@ except ImportError as e:
34
34
  )
35
35
 
36
36
 
37
- def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]):
37
+ def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]) -> Path:
38
38
  guessed = magic.from_file(path, mime=True)
39
39
  if guessed == mime_type:
40
40
  log.debug("Magic and given MIME match")
@@ -65,9 +65,15 @@ def fix_suffix(path: Path, mime_type: Optional[str], file_name: Optional[str]):
65
65
 
66
66
 
67
67
  class SubclassableOnce(type):
68
- TEST_MODE = False # To allow importing everything, including plugins, during tests
69
-
70
- def __init__(cls, name, bases, dct):
68
+ # To allow importing everything, including plugins, during tests
69
+ TEST_MODE: bool = False
70
+
71
+ def __init__(
72
+ cls,
73
+ name: str,
74
+ bases: tuple[Type[Any], ...],
75
+ dct: dict[str, Any],
76
+ ) -> None:
71
77
  for b in bases:
72
78
  if type(b) in (SubclassableOnce, ABCSubclassableOnceAtMost):
73
79
  if hasattr(b, "_subclass") and not cls.TEST_MODE:
@@ -84,19 +90,19 @@ class SubclassableOnce(type):
84
90
 
85
91
  super().__init__(name, bases, dct)
86
92
 
87
- def get_self_or_unique_subclass(cls):
93
+ def get_self_or_unique_subclass(cls) -> "SubclassableOnce":
88
94
  try:
89
95
  return cls.get_unique_subclass()
90
96
  except AttributeError:
91
97
  return cls
92
98
 
93
- def get_unique_subclass(cls):
99
+ def get_unique_subclass(cls) -> "SubclassableOnce":
94
100
  r = getattr(cls, "_subclass", None)
95
101
  if r is None:
96
102
  raise AttributeError("Could not find any subclass", cls)
97
103
  return r
98
104
 
99
- def reset_subclass(cls):
105
+ def reset_subclass(cls) -> None:
100
106
  try:
101
107
  log.debug("Resetting subclass of %s", cls)
102
108
  delattr(cls, "_subclass")
@@ -156,7 +162,7 @@ ILLEGAL_XML_CHARS_RE = re.compile(XML_ILLEGAL_CHARACTER_REGEX)
156
162
  # from https://stackoverflow.com/a/35804945/5902284
157
163
  def addLoggingLevel(
158
164
  levelName: str = "TRACE", levelNum: int = logging.DEBUG - 5, methodName=None
159
- ):
165
+ ) -> None:
160
166
  """
161
167
  Comprehensively adds a new logging level to the `logging` module and the
162
168
  currently configured logging class.
@@ -197,11 +203,11 @@ def addLoggingLevel(
197
203
  # This method was inspired by the answers to Stack Overflow post
198
204
  # http://stackoverflow.com/q/2183233/2988730, especially
199
205
  # http://stackoverflow.com/a/13638084/2988730
200
- def logForLevel(self, message, *args, **kwargs):
206
+ def logForLevel(self, message, *args, **kwargs) -> None:
201
207
  if self.isEnabledFor(levelNum):
202
208
  self._log(levelNum, message, args, **kwargs)
203
209
 
204
- def logToRoot(message, *args, **kwargs):
210
+ def logToRoot(message, *args, **kwargs) -> None:
205
211
  logging.log(levelNum, message, *args, **kwargs)
206
212
 
207
213
  logging.addLevelName(levelNum, levelName)
@@ -211,7 +217,7 @@ def addLoggingLevel(
211
217
 
212
218
 
213
219
  class SlidgeLogger(logging.Logger):
214
- def trace(self):
220
+ def trace(self) -> None:
215
221
  pass
216
222
 
217
223
 
@@ -294,15 +300,6 @@ def replace_mentions(
294
300
  return "".join(pieces)
295
301
 
296
302
 
297
- def with_session(func):
298
- @wraps(func)
299
- async def wrapped(self, *args, **kwargs):
300
- with self.xmpp.store.session():
301
- return await func(self, *args, **kwargs)
302
-
303
- return wrapped
304
-
305
-
306
303
  def timeit(func):
307
304
  @wraps(func)
308
305
  async def wrapped(self, *args, **kwargs):
@@ -325,5 +322,12 @@ def strip_leading_emoji(text: str) -> str:
325
322
  return text
326
323
 
327
324
 
328
- async def noop_coro():
325
+ async def noop_coro() -> None:
329
326
  pass
327
+
328
+
329
+ def add_quote_prefix(text: str):
330
+ """
331
+ Return multi-line text with leading quote marks (i.e. the ">" character).
332
+ """
333
+ return "\n".join(("> " + x).strip() for x in text.split("\n")).strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidge
3
- Version: 0.2.11
3
+ Version: 0.3.0
4
4
  Summary: XMPP bridging framework
5
5
  Author-email: Nicolas Cedilnik <nicoco@nicoco.fr>
6
6
  License-Expression: AGPL-3.0-or-later