slidge 0.1.3__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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