slidge 0.1.3__py3-none-any.whl → 0.2.0a1__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 (74) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +6 -7
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -6
  9. slidge/contact/contact.py +165 -49
  10. slidge/contact/roster.py +122 -47
  11. slidge/core/config.py +14 -11
  12. slidge/core/gateway/base.py +148 -36
  13. slidge/core/gateway/caps.py +7 -5
  14. slidge/core/gateway/disco.py +2 -4
  15. slidge/core/gateway/mam.py +1 -4
  16. slidge/core/gateway/muc_admin.py +1 -1
  17. slidge/core/gateway/ping.py +2 -3
  18. slidge/core/gateway/presence.py +1 -1
  19. slidge/core/gateway/registration.py +32 -21
  20. slidge/core/gateway/search.py +3 -5
  21. slidge/core/gateway/session_dispatcher.py +120 -57
  22. slidge/core/gateway/vcard_temp.py +7 -5
  23. slidge/core/mixins/__init__.py +11 -1
  24. slidge/core/mixins/attachment.py +32 -14
  25. slidge/core/mixins/avatar.py +90 -25
  26. slidge/core/mixins/base.py +8 -2
  27. slidge/core/mixins/db.py +18 -0
  28. slidge/core/mixins/disco.py +0 -10
  29. slidge/core/mixins/message.py +18 -8
  30. slidge/core/mixins/message_maker.py +17 -9
  31. slidge/core/mixins/presence.py +17 -4
  32. slidge/core/pubsub.py +54 -220
  33. slidge/core/session.py +69 -34
  34. slidge/db/__init__.py +4 -0
  35. slidge/db/alembic/env.py +64 -0
  36. slidge/db/alembic/script.py.mako +26 -0
  37. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  38. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  39. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  40. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  41. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  42. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  43. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
  44. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  45. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  46. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  47. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  48. slidge/db/avatar.py +235 -0
  49. slidge/db/meta.py +65 -0
  50. slidge/db/models.py +375 -0
  51. slidge/db/store.py +1078 -0
  52. slidge/group/archive.py +58 -14
  53. slidge/group/bookmarks.py +72 -57
  54. slidge/group/participant.py +87 -28
  55. slidge/group/room.py +369 -211
  56. slidge/main.py +201 -0
  57. slidge/migration.py +30 -0
  58. slidge/slixfix/__init__.py +35 -2
  59. slidge/slixfix/roster.py +11 -4
  60. slidge/slixfix/xep_0292/vcard4.py +3 -0
  61. slidge/util/archive_msg.py +2 -1
  62. slidge/util/db.py +1 -47
  63. slidge/util/test.py +71 -4
  64. slidge/util/types.py +29 -4
  65. slidge/util/util.py +22 -0
  66. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
  67. slidge-0.2.0a1.dist-info/RECORD +114 -0
  68. slidge/core/cache.py +0 -183
  69. slidge/util/schema.sql +0 -126
  70. slidge/util/sql.py +0 -508
  71. slidge-0.1.3.dist-info/RECORD +0 -96
  72. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  73. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  74. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
slidge/main.py ADDED
@@ -0,0 +1,201 @@
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 logging
19
+ import os
20
+ import re
21
+ import signal
22
+ from pathlib import Path
23
+
24
+ import configargparse
25
+
26
+ from slidge import BaseGateway
27
+ from slidge.__version__ import __version__
28
+ from slidge.core import config
29
+ from slidge.core.pubsub import PepAvatar, PepNick
30
+ from slidge.db import SlidgeStore
31
+ from slidge.db.avatar import avatar_cache
32
+ from slidge.db.meta import get_engine
33
+ from slidge.migration import migrate
34
+ from slidge.util.conf import ConfigModule
35
+ from slidge.util.db import user_store
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
+ db_file = config.HOME_DIR / "slidge.db"
118
+ user_store.set_file(db_file, args.secret_key)
119
+
120
+ config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare
121
+
122
+ return unknown_argv
123
+
124
+
125
+ def handle_sigterm(_signum, _frame):
126
+ logging.info("Caught SIGTERM")
127
+ raise SigTermInterrupt
128
+
129
+
130
+ def main():
131
+ signal.signal(signal.SIGTERM, handle_sigterm)
132
+
133
+ unknown_argv = configure()
134
+ logging.info("Starting slidge version %s", __version__)
135
+
136
+ legacy_module = importlib.import_module(config.LEGACY_MODULE)
137
+ logging.debug("Legacy module: %s", dir(legacy_module))
138
+ logging.info(
139
+ "Starting legacy module: '%s' version %s",
140
+ config.LEGACY_MODULE,
141
+ getattr(legacy_module, "__version__", "No version"),
142
+ )
143
+
144
+ if plugin_config_obj := getattr(
145
+ legacy_module, "config", getattr(legacy_module, "Config", None)
146
+ ):
147
+ logging.debug("Found a config object in plugin: %r", plugin_config_obj)
148
+ ConfigModule.ENV_VAR_PREFIX += (
149
+ f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_"
150
+ )
151
+ logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX)
152
+ ConfigModule(plugin_config_obj).set_conf(unknown_argv)
153
+ else:
154
+ if unknown_argv:
155
+ raise RuntimeError("Some arguments have not been recognized", unknown_argv)
156
+
157
+ migrate()
158
+
159
+ BaseGateway.store = SlidgeStore(get_engine(config.DB_URL))
160
+ gateway: BaseGateway = BaseGateway.get_unique_subclass()()
161
+ avatar_cache.http = gateway.http
162
+ avatar_cache.store = gateway.store.avatars
163
+ avatar_cache.set_dir(config.HOME_DIR / "slidge_avatars_v3")
164
+ avatar_cache.legacy_avatar_type = gateway.AVATAR_ID_TYPE
165
+
166
+ PepAvatar.store = gateway.store
167
+ PepNick.contact_store = gateway.store.contacts
168
+
169
+ gateway.connect()
170
+
171
+ return_code = 0
172
+ try:
173
+ gateway.loop.run_forever()
174
+ except KeyboardInterrupt:
175
+ logging.debug("Received SIGINT")
176
+ except SigTermInterrupt:
177
+ logging.debug("Received SIGTERM")
178
+ except SystemExit as e:
179
+ return_code = e.code # type: ignore
180
+ logging.debug("Exit called")
181
+ except Exception as e:
182
+ return_code = 2
183
+ logging.exception("Exception in __main__")
184
+ logging.exception(e)
185
+ finally:
186
+ if gateway.has_crashed:
187
+ if return_code != 0:
188
+ logging.warning("Return code has been set twice. Please report this.")
189
+ return_code = 3
190
+ if gateway.is_connected():
191
+ logging.debug("Gateway is connected, cleaning up")
192
+ gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown()))
193
+ gateway.disconnect()
194
+ gateway.loop.run_until_complete(gateway.disconnected)
195
+ else:
196
+ logging.debug("Gateway is not connected, no need to clean up")
197
+ avatar_cache.close()
198
+ gateway.loop.run_until_complete(gateway.http.close())
199
+ logging.info("Successful clean shut down")
200
+ logging.debug("Exiting with code %s", return_code)
201
+ exit(return_code)
slidge/migration.py CHANGED
@@ -1,5 +1,10 @@
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
3
8
 
4
9
  from .core import config
5
10
 
@@ -11,8 +16,33 @@ def remove_avatar_cache_v1():
11
16
  shutil.rmtree(old_dir)
12
17
 
13
18
 
19
+ def get_alembic_cfg() -> Config:
20
+ alembic_cfg = Config()
21
+ alembic_cfg.set_section_option(
22
+ "alembic",
23
+ "script_location",
24
+ str(Path(__file__).parent / "db" / "alembic"),
25
+ )
26
+ return alembic_cfg
27
+
28
+
14
29
  def migrate():
15
30
  remove_avatar_cache_v1()
31
+ command.upgrade(get_alembic_cfg(), "head")
32
+
33
+
34
+ def main():
35
+ """
36
+ Updates the (dev) database in ./dev/slidge.sqlite and generates a revision
37
+
38
+ Usage: python -m slidge.migration "Revision message blah blah blah"
39
+ """
40
+ alembic_cfg = get_alembic_cfg()
41
+ command.upgrade(alembic_cfg, "head")
42
+ command.revision(alembic_cfg, sys.argv[1], autogenerate=True)
16
43
 
17
44
 
18
45
  log = logging.getLogger(__name__)
46
+
47
+ if __name__ == "__main__":
48
+ main()
@@ -1,11 +1,12 @@
1
1
  # This module contains patches for slixmpp; some have pending requests upstream
2
2
  # and should be removed on the next slixmpp release.
3
-
4
- # ruff: noqa: F401
3
+ import logging
4
+ from collections import defaultdict
5
5
 
6
6
  import slixmpp.plugins
7
7
  from slixmpp import Message
8
8
  from slixmpp.plugins.xep_0050 import XEP_0050, Command
9
+ from slixmpp.plugins.xep_0356.privilege import _VALID_ACCESSES, XEP_0356
9
10
  from slixmpp.xmlstream import StanzaBase
10
11
 
11
12
  from . import ( # xep_0356,
@@ -22,6 +23,37 @@ from . import ( # xep_0356,
22
23
  xep_0490,
23
24
  )
24
25
 
26
+ # ruff: noqa: F401
27
+
28
+
29
+ # TODO: Remove me once https://codeberg.org/poezio/slixmpp/pulls/3541 makes it
30
+ # to a slixmpp release
31
+ def _handle_privilege(self, msg: StanzaBase):
32
+ """
33
+ Called when the XMPP server advertise the component's privileges.
34
+
35
+ Stores the privileges in this instance's granted_privileges attribute (a dict)
36
+ and raises the privileges_advertised event
37
+ """
38
+ permissions = self.granted_privileges[msg.get_from()]
39
+ for perm in msg["privilege"]["perms"]:
40
+ access = perm["access"]
41
+ if access == "iq":
42
+ if not perm.get_plugin("namespace", check=True):
43
+ permissions.iq = defaultdict(lambda: perm["type"])
44
+ else:
45
+ for ns in perm["namespaces"]:
46
+ permissions.iq[ns["ns"]] = ns["type"]
47
+ elif access in _VALID_ACCESSES:
48
+ setattr(permissions, access, perm["type"])
49
+ else:
50
+ log.warning("Received an invalid privileged access: %s", access)
51
+ log.debug("Privileges: %s", self.granted_privileges)
52
+ self.xmpp.event("privileges_advertised")
53
+
54
+
55
+ XEP_0356._handle_privilege = _handle_privilege
56
+
25
57
 
26
58
  def session_bind(self, jid):
27
59
  self.xmpp["xep_0030"].add_feature(Command.namespace)
@@ -66,3 +98,4 @@ slixmpp.plugins.PLUGINS.extend(
66
98
 
67
99
 
68
100
  Message.reply = reply # type: ignore
101
+ log = logging.getLogger(__name__)
slidge/slixfix/roster.py CHANGED
@@ -1,6 +1,11 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from slixmpp import JID
2
4
 
3
- from ..util.db import log, user_store
5
+ from ..util.db import log
6
+
7
+ if TYPE_CHECKING:
8
+ from .. import BaseGateway
4
9
 
5
10
 
6
11
  class YesSet(set):
@@ -23,6 +28,9 @@ class RosterBackend:
23
28
  This is rudimentary but the only sane way I could come up with so far.
24
29
  """
25
30
 
31
+ def __init__(self, xmpp: "BaseGateway"):
32
+ self.xmpp = xmpp
33
+
26
34
  @staticmethod
27
35
  def entries(_owner_jid, _default=None):
28
36
  return YesSet()
@@ -31,10 +39,9 @@ class RosterBackend:
31
39
  def save(_owner_jid, _jid, _item_state, _db_state):
32
40
  pass
33
41
 
34
- @staticmethod
35
- def load(_owner_jid, jid, _db_state):
42
+ def load(self, _owner_jid, jid, _db_state):
36
43
  log.debug("Load %s", jid)
37
- user = user_store.get_by_jid(JID(jid))
44
+ user = self.xmpp.store.users.get(JID(jid))
38
45
  log.debug("User %s", user)
39
46
  if user is None:
40
47
  return {
@@ -26,6 +26,7 @@ class VCard4Provider(BasePlugin):
26
26
 
27
27
  def __init__(self, *a, **k):
28
28
  super(VCard4Provider, self).__init__(*a, **k)
29
+ # TODO: store that in DB and not in RAM
29
30
  self._vcards = dict[JidStr, StoredVCard]()
30
31
 
31
32
  def plugin_init(self):
@@ -56,6 +57,8 @@ class VCard4Provider(BasePlugin):
56
57
  if not hasattr(self.xmpp, "get_session_from_jid"):
57
58
  return None
58
59
  jid = JID(jid)
60
+ if not jid.local:
61
+ return None
59
62
  requested_by = JID(requested_by)
60
63
  session = self.xmpp.get_session_from_jid(requested_by)
61
64
  if session is None:
@@ -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
  )
slidge/util/db.py CHANGED
@@ -15,8 +15,6 @@ from typing import Iterable, Optional, Union
15
15
  from pickle_secure import Pickler, Unpickler
16
16
  from slixmpp import JID, Iq, Message, Presence
17
17
 
18
- from .sql import db
19
-
20
18
 
21
19
  # noinspection PyUnresolvedReferences
22
20
  class EncryptedShelf(shelve.DbfilenameShelf):
@@ -94,10 +92,6 @@ class GatewayUser:
94
92
  # """
95
93
  return self.registration_form.get(field, default)
96
94
 
97
- def commit(self):
98
- db.user_store(self)
99
- user_store.commit(self)
100
-
101
95
 
102
96
  class UserStore:
103
97
  """
@@ -126,8 +120,6 @@ class UserStore:
126
120
  self._users = EncryptedShelf(filename, key=secret_key)
127
121
  else:
128
122
  self._users = shelve.open(str(filename))
129
- for user in self._users.values():
130
- db.user_store(user)
131
123
  log.info("Registered users in the DB: %s", list(self._users.keys()))
132
124
 
133
125
  def get_all(self) -> Iterable[GatewayUser]:
@@ -138,28 +130,8 @@ class UserStore:
138
130
  """
139
131
  return self._users.values()
140
132
 
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
133
  def commit(self, user: GatewayUser):
162
- self._users[user.bare_jid] = user
134
+ self._users[user.jid.bare] = user
163
135
  self._users.sync()
164
136
 
165
137
  def get(self, _gateway_jid, _node, ifrom: JID, iq) -> Optional[GatewayUser]:
@@ -179,24 +151,6 @@ class UserStore:
179
151
  log.debug("Getting user %s", ifrom.bare)
180
152
  return self._users.get(ifrom.bare)
181
153
 
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
154
  def get_by_jid(self, jid: JID) -> Optional[GatewayUser]:
201
155
  """
202
156
  Convenience function to get a user from their JID.
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,17 @@ class SlidgeTest(SlixTestPlus):
209
215
  self.plugin, LegacyBookmarks, base_ok=True
210
216
  )
211
217
 
218
+ engine = self.db_engine = create_engine("sqlite+pysqlite:///:memory:")
219
+ Base.metadata.create_all(engine)
220
+ BaseGateway.store = SlidgeStore(engine)
212
221
  self.xmpp = BaseGateway.get_self_or_unique_subclass()()
213
-
222
+ self.xmpp.TEST_MODE = True
223
+ PepNick.contact_store = self.xmpp.store.contacts
224
+ PepAvatar.store = self.xmpp.store
225
+ avatar_cache.store = self.xmpp.store.avatars
226
+ avatar_cache.set_dir(Path(tempfile.mkdtemp()))
214
227
  self.xmpp._always_send_everything = True
228
+ engine.echo = True
215
229
 
216
230
  self.xmpp.connection_made(TestTransport(self.xmpp))
217
231
  self.xmpp.session_bind_event.set()
@@ -242,10 +256,63 @@ class SlidgeTest(SlixTestPlus):
242
256
  self.xmpp.use_presence_ids = False
243
257
  Error.namespace = "jabber:component:accept"
244
258
 
259
+ def tearDown(self):
260
+ self.db_engine.echo = False
261
+ super().tearDown()
262
+ import slidge.db.store
263
+
264
+ if slidge.db.store._session is not None:
265
+ slidge.db.store._session.commit()
266
+ slidge.db.store._session = None
267
+ Base.metadata.drop_all(self.xmpp.store._engine)
268
+
269
+ def setup_logged_session(self):
270
+ user = self.xmpp.store.users.new(
271
+ JID("romeo@montague.lit/gajim"), {"username": "romeo", "city": ""}
272
+ )
273
+ user.preferences = {"sync_avatar": True, "sync_presence": True}
274
+ self.xmpp.store.users.update(user)
275
+
276
+ with self.xmpp.store.session() as session:
277
+ session.execute(delete(Contact))
278
+ session.commit()
279
+
280
+ self.run_coro(self.xmpp._on_user_register(Iq(sfrom="romeo@montague.lit/gajim")))
281
+ welcome = self.next_sent()
282
+ assert welcome["body"]
283
+ stanza = self.next_sent()
284
+ assert "logging in" in stanza["status"].lower(), stanza
285
+ stanza = self.next_sent()
286
+ assert "syncing contacts" in stanza["status"].lower(), stanza
287
+ stanza = self.next_sent()
288
+ assert "yup" in stanza["status"].lower(), stanza
289
+
290
+ self.romeo = BaseSession.get_self_or_unique_subclass().from_jid(
291
+ JID("romeo@montague.lit")
292
+ )
293
+ self.juliet: LegacyContact = self.run_coro(
294
+ self.romeo.contacts.by_legacy_id("juliet")
295
+ )
296
+ self.room: LegacyMUC = self.run_coro(self.romeo.bookmarks.by_legacy_id("room"))
297
+ self.first_witch: LegacyParticipant = self.run_coro(
298
+ self.room.get_participant("firstwitch")
299
+ )
300
+ self.send( # language=XML
301
+ """
302
+ <iq type="get"
303
+ to="romeo@montague.lit"
304
+ id="1"
305
+ from="aim.shakespeare.lit">
306
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
307
+ <items node="urn:xmpp:avatar:metadata" />
308
+ </pubsub>
309
+ </iq>
310
+ """
311
+ )
312
+
245
313
  @classmethod
246
314
  def tearDownClass(cls):
247
315
  reset_subclasses()
248
- user_store._users = None
249
316
 
250
317
 
251
318
  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:
@@ -84,13 +84,16 @@ class MessageReference(Generic[LegacyMessageType]):
84
84
 
85
85
  At the very minimum, the legacy message ID attribute must be set, but to
86
86
  ensure that the quote is displayed in all XMPP clients, the author must also
87
- be set.
87
+ be set (use the string "user" if the slidge user is the author of the referenced
88
+ message).
88
89
  The body is used as a fallback for XMPP clients that do not support :xep:`0461`
89
90
  of that failed to find the referenced message.
90
91
  """
91
92
 
92
93
  legacy_id: LegacyMessageType
93
- author: Optional[Union["GatewayUser", "LegacyParticipant", "LegacyContact"]] = None
94
+ author: Optional[Union[Literal["user"], "LegacyParticipant", "LegacyContact"]] = (
95
+ None
96
+ )
94
97
  body: Optional[str] = None
95
98
 
96
99
 
@@ -178,3 +181,25 @@ class Mention(NamedTuple):
178
181
  class Hat(NamedTuple):
179
182
  uri: str
180
183
  title: str
184
+
185
+
186
+ class UserPreferences(TypedDict):
187
+ sync_avatar: bool
188
+ sync_presence: bool
189
+
190
+
191
+ class MamMetadata(NamedTuple):
192
+ id: str
193
+ sent_on: datetime
194
+
195
+
196
+ class HoleBound(NamedTuple):
197
+ id: int | str
198
+ timestamp: datetime
199
+
200
+
201
+ class CachedPresence(NamedTuple):
202
+ last_seen: Optional[datetime] = None
203
+ ptype: Optional[PresenceTypes] = None
204
+ pstatus: Optional[str] = None
205
+ pshow: Optional[PresenceShows] = None
slidge/util/util.py CHANGED
@@ -4,7 +4,9 @@ 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
9
+ from time import time
8
10
  from typing import TYPE_CHECKING, Callable, NamedTuple, Optional, Type
9
11
 
10
12
  from .types import Mention, ResourceDict
@@ -293,3 +295,23 @@ def replace_mentions(
293
295
  cursor = mention.end
294
296
  pieces.append(text[cursor:])
295
297
  return "".join(pieces)
298
+
299
+
300
+ def with_session(func):
301
+ @wraps(func)
302
+ async def wrapped(self, *args, **kwargs):
303
+ with self.xmpp.store.session():
304
+ return await func(self, *args, **kwargs)
305
+
306
+ return wrapped
307
+
308
+
309
+ def timeit(func):
310
+ @wraps(func)
311
+ async def wrapped(self, *args, **kwargs):
312
+ start = time()
313
+ r = await func(self, *args, **kwargs)
314
+ self.log.info("%s took %s ms", func.__name__, round((time() - start) * 1000))
315
+ return r
316
+
317
+ return wrapped