slidge 0.1.3__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 +107 -40
  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.3.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 -804
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.3.dist-info/RECORD +0 -96
  100. slidge-0.1.3.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
slidge/main.py ADDED
@@ -0,0 +1,202 @@
1
+ """
2
+ Slidge can be configured via CLI args, environment variables and/or INI files.
3
+
4
+ To use env vars, use this convention: ``--home-dir`` becomes ``HOME_DIR``.
5
+
6
+ Everything in ``/etc/slidge/conf.d/*`` is automatically used.
7
+ To use a plugin-specific INI file, put it in another dir,
8
+ and launch slidge with ``-c /path/to/plugin-specific.conf``.
9
+ Use the long version of the CLI arg without the double dash prefix inside this
10
+ INI file, eg ``debug=true``.
11
+
12
+ An example configuration file is available at
13
+ https://git.sr.ht/~nicoco/slidge/tree/master/item/dev/confs/slidge-example.ini
14
+ """
15
+
16
+ import asyncio
17
+ import importlib
18
+ import inspect
19
+ import logging
20
+ import os
21
+ import re
22
+ import signal
23
+ from pathlib import Path
24
+
25
+ import configargparse
26
+
27
+ from slidge import BaseGateway
28
+ from slidge.__version__ import __version__
29
+ from slidge.core import config
30
+ from slidge.core.pubsub import PepAvatar, PepNick
31
+ from slidge.db import SlidgeStore
32
+ from slidge.db.avatar import avatar_cache
33
+ from slidge.db.meta import get_engine
34
+ from slidge.migration import migrate
35
+ from slidge.util.conf import ConfigModule
36
+
37
+
38
+ class MainConfig(ConfigModule):
39
+ def update_dynamic_defaults(self, args):
40
+ # force=True is needed in case we call a logger before this is reached,
41
+ # or basicConfig has no effect
42
+ logging.basicConfig(
43
+ level=args.loglevel,
44
+ filename=args.log_file,
45
+ force=True,
46
+ format=args.log_format,
47
+ )
48
+
49
+ if args.home_dir is None:
50
+ args.home_dir = Path("/var/lib/slidge") / str(args.jid)
51
+
52
+ if args.user_jid_validator is None:
53
+ args.user_jid_validator = ".*@" + re.escape(args.server)
54
+
55
+ if args.db_url is None:
56
+ args.db_url = f"sqlite:///{args.home_dir}/slidge.sqlite"
57
+
58
+
59
+ class SigTermInterrupt(Exception):
60
+ pass
61
+
62
+
63
+ def get_configurator():
64
+ p = configargparse.ArgumentParser(
65
+ default_config_files=os.getenv(
66
+ "SLIDGE_CONF_DIR", "/etc/slidge/conf.d/*.conf"
67
+ ).split(":"),
68
+ description=__doc__,
69
+ )
70
+ p.add_argument(
71
+ "-c",
72
+ "--config",
73
+ help="Path to a INI config file.",
74
+ env_var="SLIDGE_CONFIG",
75
+ is_config_file=True,
76
+ )
77
+ p.add_argument(
78
+ "-q",
79
+ "--quiet",
80
+ help="loglevel=WARNING",
81
+ action="store_const",
82
+ dest="loglevel",
83
+ const=logging.WARNING,
84
+ default=logging.INFO,
85
+ env_var="SLIDGE_QUIET",
86
+ )
87
+ p.add_argument(
88
+ "-d",
89
+ "--debug",
90
+ help="loglevel=DEBUG",
91
+ action="store_const",
92
+ dest="loglevel",
93
+ const=logging.DEBUG,
94
+ env_var="SLIDGE_DEBUG",
95
+ )
96
+ p.add_argument(
97
+ "--version",
98
+ action="version",
99
+ version=f"%(prog)s {__version__}",
100
+ )
101
+ configurator = MainConfig(config, p)
102
+ return configurator
103
+
104
+
105
+ def get_parser():
106
+ return get_configurator().parser
107
+
108
+
109
+ def configure():
110
+ configurator = get_configurator()
111
+ args, unknown_argv = configurator.set_conf()
112
+
113
+ if not (h := config.HOME_DIR).exists():
114
+ logging.info("Creating directory '%s'", h)
115
+ h.mkdir()
116
+
117
+ config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare
118
+
119
+ return unknown_argv
120
+
121
+
122
+ def handle_sigterm(_signum, _frame):
123
+ logging.info("Caught SIGTERM")
124
+ raise SigTermInterrupt
125
+
126
+
127
+ def main():
128
+ signal.signal(signal.SIGTERM, handle_sigterm)
129
+
130
+ unknown_argv = configure()
131
+ logging.info("Starting slidge version %s", __version__)
132
+
133
+ legacy_module = importlib.import_module(config.LEGACY_MODULE)
134
+ logging.debug("Legacy module: %s", dir(legacy_module))
135
+ logging.info(
136
+ "Starting legacy module: '%s' version %s",
137
+ config.LEGACY_MODULE,
138
+ getattr(legacy_module, "__version__", "No version"),
139
+ )
140
+
141
+ if plugin_config_obj := getattr(
142
+ legacy_module, "config", getattr(legacy_module, "Config", None)
143
+ ):
144
+ # If the legacy module has default parameters that depend on dynamic defaults
145
+ # of the slidge main config, it needs to be refreshed at this point, because
146
+ # now the dynamic defaults are set.
147
+ if inspect.ismodule(plugin_config_obj):
148
+ importlib.reload(plugin_config_obj)
149
+ logging.debug("Found a config object in plugin: %r", plugin_config_obj)
150
+ ConfigModule.ENV_VAR_PREFIX += (
151
+ f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_"
152
+ )
153
+ logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX)
154
+ ConfigModule(plugin_config_obj).set_conf(unknown_argv)
155
+ else:
156
+ if unknown_argv:
157
+ raise RuntimeError("Some arguments have not been recognized", unknown_argv)
158
+
159
+ migrate()
160
+
161
+ store = SlidgeStore(get_engine(config.DB_URL))
162
+ BaseGateway.store = store
163
+ gateway: BaseGateway = BaseGateway.get_unique_subclass()()
164
+ avatar_cache.store = gateway.store.avatars
165
+ avatar_cache.set_dir(config.HOME_DIR / "slidge_avatars_v3")
166
+
167
+ PepAvatar.store = gateway.store
168
+ PepNick.contact_store = gateway.store.contacts
169
+
170
+ gateway.connect()
171
+
172
+ return_code = 0
173
+ try:
174
+ gateway.loop.run_forever()
175
+ except KeyboardInterrupt:
176
+ logging.debug("Received SIGINT")
177
+ except SigTermInterrupt:
178
+ logging.debug("Received SIGTERM")
179
+ except SystemExit as e:
180
+ return_code = e.code # type: ignore
181
+ logging.debug("Exit called")
182
+ except Exception as e:
183
+ return_code = 2
184
+ logging.exception("Exception in __main__")
185
+ logging.exception(e)
186
+ finally:
187
+ if gateway.has_crashed:
188
+ if return_code != 0:
189
+ logging.warning("Return code has been set twice. Please report this.")
190
+ return_code = 3
191
+ if gateway.is_connected():
192
+ logging.debug("Gateway is connected, cleaning up")
193
+ gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown()))
194
+ gateway.disconnect()
195
+ gateway.loop.run_until_complete(gateway.disconnected)
196
+ else:
197
+ logging.debug("Gateway is not connected, no need to clean up")
198
+ avatar_cache.close()
199
+ gateway.loop.run_until_complete(gateway.http.close())
200
+ logging.info("Successful clean shut down")
201
+ logging.debug("Exiting with code %s", return_code)
202
+ exit(return_code)
slidge/migration.py CHANGED
@@ -1,7 +1,16 @@
1
1
  import logging
2
2
  import shutil
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from alembic import command
7
+ from alembic.config import Config
8
+ from slixmpp import JID
3
9
 
4
10
  from .core import config
11
+ from .db.meta import get_engine
12
+ from .db.models import GatewayUser
13
+ from .db.store import SlidgeStore
5
14
 
6
15
 
7
16
  def remove_avatar_cache_v1():
@@ -11,8 +20,43 @@ def remove_avatar_cache_v1():
11
20
  shutil.rmtree(old_dir)
12
21
 
13
22
 
14
- def migrate():
23
+ def get_alembic_cfg() -> Config:
24
+ alembic_cfg = Config()
25
+ alembic_cfg.set_section_option(
26
+ "alembic",
27
+ "script_location",
28
+ str(Path(__file__).parent / "db" / "alembic"),
29
+ )
30
+ return alembic_cfg
31
+
32
+
33
+ def remove_resource_parts_from_users() -> None:
34
+ with SlidgeStore(get_engine(config.DB_URL)).session() as orm:
35
+ for user in orm.query(GatewayUser).all():
36
+ if user.jid.resource:
37
+ user.jid = JID(user.jid.bare)
38
+ orm.add(user)
39
+ orm.commit()
40
+
41
+
42
+ def migrate() -> None:
15
43
  remove_avatar_cache_v1()
44
+ command.upgrade(get_alembic_cfg(), "head")
45
+ remove_resource_parts_from_users()
46
+
47
+
48
+ def main():
49
+ """
50
+ Updates the (dev) database in ./dev/slidge.sqlite and generates a revision
51
+
52
+ Usage: python -m slidge.migration "Revision message blah blah blah"
53
+ """
54
+ alembic_cfg = get_alembic_cfg()
55
+ command.upgrade(alembic_cfg, "head")
56
+ command.revision(alembic_cfg, sys.argv[1], autogenerate=True)
16
57
 
17
58
 
18
59
  log = logging.getLogger(__name__)
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -4,8 +4,10 @@
4
4
  # ruff: noqa: F401
5
5
 
6
6
  import slixmpp.plugins
7
- from slixmpp import Message
7
+ from slixmpp import Iq, Message
8
+ from slixmpp.exceptions import XMPPError
8
9
  from slixmpp.plugins.xep_0050 import XEP_0050, Command
10
+ from slixmpp.plugins.xep_0231 import XEP_0231
9
11
  from slixmpp.xmlstream import StanzaBase
10
12
 
11
13
  from . import ( # xep_0356,
@@ -23,6 +25,34 @@ from . import ( # xep_0356,
23
25
  )
24
26
 
25
27
 
28
+ async def _handle_bob_iq(self, iq: Iq):
29
+ cid = iq["bob"]["cid"]
30
+
31
+ if iq["type"] == "result":
32
+ await self.api["set_bob"](iq["from"], None, iq["to"], args=iq["bob"])
33
+ self.xmpp.event("bob", iq)
34
+ elif iq["type"] == "get":
35
+ data = await self.api["get_bob"](iq["to"], None, iq["from"], args=cid)
36
+
37
+ if data is None:
38
+ raise XMPPError(
39
+ "item-not-found",
40
+ f"Bits of binary '{cid}' is not available.",
41
+ )
42
+
43
+ if isinstance(data, Iq):
44
+ data["id"] = iq["id"]
45
+ data.send()
46
+ return
47
+
48
+ iq = iq.reply()
49
+ iq.append(data)
50
+ iq.send()
51
+
52
+
53
+ XEP_0231._handle_bob_iq = _handle_bob_iq
54
+
55
+
26
56
  def session_bind(self, jid):
27
57
  self.xmpp["xep_0030"].add_feature(Command.namespace)
28
58
  # awful hack to for the disco items: we need to comment this line
@@ -11,7 +11,7 @@ from slixmpp import JID, Message
11
11
  from slixmpp.types import MessageTypes
12
12
 
13
13
  if TYPE_CHECKING:
14
- from .base import BaseGateway
14
+ from slidge.core.gateway import BaseGateway
15
15
 
16
16
 
17
17
  class DeliveryReceipt:
slidge/slixfix/roster.py CHANGED
@@ -1,6 +1,10 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING
3
+
1
4
  from slixmpp import JID
2
5
 
3
- from ..util.db import log, user_store
6
+ if TYPE_CHECKING:
7
+ from .. import BaseGateway
4
8
 
5
9
 
6
10
  class YesSet(set):
@@ -23,6 +27,9 @@ class RosterBackend:
23
27
  This is rudimentary but the only sane way I could come up with so far.
24
28
  """
25
29
 
30
+ def __init__(self, xmpp: "BaseGateway"):
31
+ self.xmpp = xmpp
32
+
26
33
  @staticmethod
27
34
  def entries(_owner_jid, _default=None):
28
35
  return YesSet()
@@ -31,10 +38,9 @@ class RosterBackend:
31
38
  def save(_owner_jid, _jid, _item_state, _db_state):
32
39
  pass
33
40
 
34
- @staticmethod
35
- def load(_owner_jid, jid, _db_state):
41
+ def load(self, _owner_jid, jid, _db_state):
36
42
  log.debug("Load %s", jid)
37
- user = user_store.get_by_jid(JID(jid))
43
+ user = self.xmpp.store.users.get(JID(jid))
38
44
  log.debug("User %s", user)
39
45
  if user is None:
40
46
  return {
@@ -58,3 +64,6 @@ class RosterBackend:
58
64
  "whitelisted": False,
59
65
  "subscription": "none",
60
66
  }
67
+
68
+
69
+ log = logging.getLogger(__name__)
@@ -1,100 +1,14 @@
1
- import logging
2
- from typing import TYPE_CHECKING, NamedTuple, Optional
3
-
4
- from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
5
1
  from slixmpp.plugins.base import BasePlugin, register_plugin
6
- from slixmpp.plugins.xep_0292.stanza import NS, VCard4
7
- from slixmpp.types import JidStr
8
-
9
- from slidge.contact import LegacyContact
10
-
11
- if TYPE_CHECKING:
12
- from slidge.core.gateway import BaseGateway
13
-
14
-
15
- class StoredVCard(NamedTuple):
16
- content: VCard4
17
- authorized_jids: set[JidStr]
2
+ from slixmpp.plugins.xep_0292.stanza import NS
18
3
 
19
4
 
20
5
  class VCard4Provider(BasePlugin):
21
- xmpp: "BaseGateway"
22
-
23
6
  name = "xep_0292_provider"
24
7
  description = "VCard4 Provider"
25
8
  dependencies = {"xep_0030"}
26
9
 
27
- def __init__(self, *a, **k):
28
- super(VCard4Provider, self).__init__(*a, **k)
29
- self._vcards = dict[JidStr, StoredVCard]()
30
-
31
10
  def plugin_init(self):
32
- self.xmpp.register_handler(
33
- CoroutineCallback(
34
- "get_vcard",
35
- StanzaPath(f"iq@type=get/vcard"),
36
- self.handle_vcard_get, # type:ignore
37
- )
38
- )
39
-
40
11
  self.xmpp.plugin["xep_0030"].add_feature(NS)
41
12
 
42
- def _get_cached_vcard(self, jid: JidStr, requested_by: JidStr) -> Optional[VCard4]:
43
- vcard = self._vcards.get(JID(jid).bare)
44
- if vcard:
45
- if auth := vcard.authorized_jids:
46
- if JID(requested_by).bare in auth:
47
- return vcard.content
48
- else:
49
- return vcard.content
50
- return None
51
-
52
- async def get_vcard(self, jid: JidStr, requested_by: JidStr) -> Optional[VCard4]:
53
- if vcard := self._get_cached_vcard(jid, requested_by):
54
- log.debug("Found a cached vcard")
55
- return vcard
56
- if not hasattr(self.xmpp, "get_session_from_jid"):
57
- return None
58
- jid = JID(jid)
59
- requested_by = JID(requested_by)
60
- session = self.xmpp.get_session_from_jid(requested_by)
61
- if session is None:
62
- return
63
- entity = await session.get_contact_or_group_or_participant(jid)
64
- if isinstance(entity, LegacyContact):
65
- log.debug("Fetching vcard")
66
- await entity.fetch_vcard()
67
- return self._get_cached_vcard(jid, requested_by)
68
- return None
69
-
70
- async def handle_vcard_get(self, iq: Iq):
71
- r = iq.reply()
72
- if vcard := await self.get_vcard(iq.get_to().bare, iq.get_from().bare):
73
- r.append(vcard)
74
- else:
75
- r.enable("vcard")
76
- r.send()
77
-
78
- def set_vcard(
79
- self,
80
- jid: JidStr,
81
- vcard: VCard4,
82
- /,
83
- authorized_jids: Optional[set[JidStr]] = None,
84
- ):
85
- cache = self._vcards.get(jid)
86
- new = StoredVCard(
87
- vcard, authorized_jids if authorized_jids is not None else set()
88
- )
89
- self._vcards[jid] = new
90
- if cache == new:
91
- return
92
- if self.xmpp["pubsub"] and authorized_jids:
93
- for to in authorized_jids:
94
- self.xmpp.loop.create_task(
95
- self.xmpp["pubsub"].broadcast_vcard_event(jid, to)
96
- )
97
-
98
13
 
99
14
  register_plugin(VCard4Provider)
100
- log = logging.getLogger(__name__)
@@ -1,6 +1,7 @@
1
1
  from copy import copy
2
2
  from datetime import datetime, timezone
3
3
  from typing import Optional, Union
4
+ from uuid import uuid4
4
5
  from xml.etree import ElementTree as ET
5
6
 
6
7
  from slixmpp import Message
@@ -30,7 +31,7 @@ class HistoryMessage:
30
31
  else:
31
32
  from_db = False
32
33
 
33
- self.id = stanza["stanza_id"]["id"]
34
+ self.id = stanza["stanza_id"]["id"] or uuid4().hex
34
35
  self.when: datetime = (
35
36
  when or stanza["delay"]["stamp"] or datetime.now(tz=timezone.utc)
36
37
  )