slidge 0.1.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 (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. slidge-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,77 @@
1
+ # Slixmpp: The Slick XMPP Library
2
+ # Copyright (C) 2020 Mathieu Pasquet <mathieui@mathieui.net>
3
+ # This file is part of Slixmpp.
4
+ # See the file LICENSE for copying permission.
5
+ from typing import Optional
6
+
7
+ from slixmpp import JID, Message
8
+ from slixmpp.exceptions import IqError, IqTimeout
9
+ from slixmpp.plugins import BasePlugin
10
+ from slixmpp.xmlstream.handler import Callback
11
+ from slixmpp.xmlstream.matcher import StanzaPath
12
+
13
+ from . import stanza
14
+
15
+ DEFAULT_FALLBACK = (
16
+ "This person attempted to retract a previous message, but your client "
17
+ "does not support it."
18
+ )
19
+
20
+
21
+ class XEP_0424(BasePlugin):
22
+ """XEP-0424: Message Retraction"""
23
+
24
+ name = "xep_0424"
25
+ description = "XEP-0424: Message Retraction (fix Slidge)"
26
+ dependencies = {"xep_0422", "xep_0030", "xep_0359", "xep_0428", "xep_0334"}
27
+ stanza = stanza
28
+ namespace = stanza.NS
29
+
30
+ def plugin_init(self) -> None:
31
+ stanza.register_plugins()
32
+ self.xmpp.register_handler(
33
+ Callback(
34
+ "Message Retracted",
35
+ StanzaPath("message/retract"),
36
+ self._handle_retract_message,
37
+ )
38
+ )
39
+
40
+ def session_bind(self, jid):
41
+ self.xmpp.plugin["xep_0030"].add_feature(feature=stanza.NS)
42
+
43
+ def plugin_end(self):
44
+ self.xmpp.plugin["xep_0030"].del_feature(feature=stanza.NS)
45
+
46
+ def _handle_retract_message(self, message: Message):
47
+ self.xmpp.event("message_retract", message)
48
+
49
+ def send_retraction(
50
+ self,
51
+ mto: JID,
52
+ id: str,
53
+ mtype: str = "chat",
54
+ include_fallback: bool = True,
55
+ fallback_text: Optional[str] = None,
56
+ *,
57
+ mfrom: Optional[JID] = None
58
+ ):
59
+ """
60
+ Send a message retraction
61
+
62
+ :param JID mto: The JID to retract the message from
63
+ :param str id: Message ID to retract
64
+ :param str mtype: Message type
65
+ :param bool include_fallback: Whether to include a fallback body
66
+ :param Optional[str] fallback_text: The content of the fallback
67
+ body. None will set the default value.
68
+ """
69
+ if fallback_text is None:
70
+ fallback_text = DEFAULT_FALLBACK
71
+ msg = self.xmpp.make_message(mto=mto, mtype=mtype, mfrom=mfrom)
72
+ if include_fallback:
73
+ msg["body"] = fallback_text
74
+ msg.enable("fallback")
75
+ msg["retract"]["id"] = id
76
+ msg.enable("store")
77
+ msg.send()
@@ -0,0 +1,28 @@
1
+ # Slixmpp: The Slick XMPP Library
2
+ # Copyright (C) 2020 Mathieu Pasquet <mathieui@mathieui.net>
3
+ # This file is part of Slixmpp.
4
+ # See the file LICENSE for copying permissio
5
+ from slixmpp.plugins.xep_0359 import OriginID
6
+ from slixmpp.stanza import Message
7
+ from slixmpp.xmlstream import ElementBase, register_stanza_plugin
8
+
9
+ NS = "urn:xmpp:message-retract:1"
10
+
11
+
12
+ class Retract(ElementBase):
13
+ namespace = NS
14
+ name = "retract"
15
+ plugin_attrib = "retract"
16
+
17
+
18
+ class Retracted(ElementBase):
19
+ namespace = NS
20
+ name = "retracted"
21
+ plugin_attrib = "retracted"
22
+ interfaces = {"stamp"}
23
+
24
+
25
+ def register_plugins():
26
+ register_stanza_plugin(Message, Retract)
27
+ register_stanza_plugin(Message, Retracted)
28
+ register_stanza_plugin(Retracted, OriginID)
@@ -0,0 +1,8 @@
1
+ from slixmpp.plugins.base import register_plugin
2
+
3
+ from . import stanza
4
+ from .mds import XEP_0490
5
+
6
+ register_plugin(XEP_0490)
7
+
8
+ __all__ = ["stanza", "XEP_0490"]
@@ -0,0 +1,47 @@
1
+ from slixmpp import Iq
2
+ from slixmpp.plugins import BasePlugin
3
+ from slixmpp.plugins.xep_0004 import Form
4
+ from slixmpp.types import JidStr
5
+
6
+ from . import stanza
7
+
8
+
9
+ class XEP_0490(BasePlugin):
10
+ """
11
+ XEP-0490: Message Displayed Synchronization
12
+ """
13
+
14
+ name = "xep_0490"
15
+ description = "XEP-0490: Message Displayed Synchronization"
16
+ dependencies = {"xep_0060", "xep_0163", "xep_0359"}
17
+ stanza = stanza
18
+
19
+ def plugin_init(self):
20
+ stanza.register_plugin()
21
+ self.xmpp.plugin["xep_0163"].register_pep(
22
+ "message_displayed_synchronization",
23
+ stanza.Displayed,
24
+ )
25
+
26
+ def flag_chat(self, chat: JidStr, stanza_id: str, **kwargs) -> Iq:
27
+ displayed = stanza.Displayed()
28
+ displayed["stanza_id"]["id"] = stanza_id
29
+ return self.xmpp.plugin["xep_0163"].publish(
30
+ displayed, node=stanza.NS, options=PUBLISH_OPTIONS, id=str(chat), **kwargs
31
+ )
32
+
33
+ def catch_up(self, **kwargs):
34
+ return self.xmpp.plugin["xep_0060"].get_items(
35
+ self.xmpp.boundjid.bare, stanza.NS, **kwargs
36
+ )
37
+
38
+
39
+ PUBLISH_OPTIONS = Form()
40
+ PUBLISH_OPTIONS["type"] = "submit"
41
+ PUBLISH_OPTIONS.add_field(
42
+ "FORM_TYPE", "hidden", value="http://jabber.org/protocol/pubsub#publish-options"
43
+ )
44
+ PUBLISH_OPTIONS.add_field("pubsub#persist_items", value="true")
45
+ PUBLISH_OPTIONS.add_field("pubsub#max_items", value="max")
46
+ PUBLISH_OPTIONS.add_field("pubsub#send_last_published_item", value="never")
47
+ PUBLISH_OPTIONS.add_field("pubsub#access_model", value="whitelist")
@@ -0,0 +1,17 @@
1
+ from slixmpp import register_stanza_plugin
2
+ from slixmpp.plugins.xep_0060.stanza import Item
3
+ from slixmpp.plugins.xep_0359.stanza import StanzaID
4
+ from slixmpp.xmlstream import ElementBase
5
+
6
+ NS = "urn:xmpp:mds:displayed:0"
7
+
8
+
9
+ class Displayed(ElementBase):
10
+ namespace = NS
11
+ name = "displayed"
12
+ plugin_attrib = "displayed"
13
+
14
+
15
+ def register_plugin():
16
+ register_stanza_plugin(Displayed, StanzaID)
17
+ register_stanza_plugin(Item, Displayed)
@@ -0,0 +1,15 @@
1
+ from .util import (
2
+ ABCSubclassableOnceAtMost,
3
+ SubclassableOnce,
4
+ is_valid_phone_number,
5
+ replace_mentions,
6
+ strip_illegal_chars,
7
+ )
8
+
9
+ __all__ = [
10
+ "SubclassableOnce",
11
+ "ABCSubclassableOnceAtMost",
12
+ "is_valid_phone_number",
13
+ "replace_mentions",
14
+ "strip_illegal_chars",
15
+ ]
@@ -0,0 +1,61 @@
1
+ from copy import copy
2
+ from datetime import datetime, timezone
3
+ from typing import Optional, Union
4
+ from xml.etree import ElementTree as ET
5
+
6
+ from slixmpp import Message
7
+ from slixmpp.plugins.xep_0297 import Forwarded
8
+
9
+
10
+ def fix_namespaces(xml, old="{jabber:component:accept}", new="{jabber:client}"):
11
+ """
12
+ Hack to fix namespaces between jabber:component and jabber:client
13
+
14
+ Acts in-place.
15
+
16
+ :param xml:
17
+ :param old:
18
+ :param new:
19
+ """
20
+ xml.tag = xml.tag.replace(old, new)
21
+ for child in xml:
22
+ fix_namespaces(child, old, new)
23
+
24
+
25
+ class HistoryMessage:
26
+ def __init__(self, stanza: Union[Message, str], when: Optional[datetime] = None):
27
+ if isinstance(stanza, str):
28
+ from_db = True
29
+ stanza = Message(xml=ET.fromstring(stanza))
30
+ else:
31
+ from_db = False
32
+
33
+ self.id = stanza["stanza_id"]["id"]
34
+ self.when: datetime = (
35
+ when or stanza["delay"]["stamp"] or datetime.now(tz=timezone.utc)
36
+ )
37
+
38
+ if not from_db:
39
+ del stanza["delay"]
40
+ del stanza["markable"]
41
+ del stanza["hint"]
42
+ del stanza["chat_state"]
43
+ if not stanza["body"]:
44
+ del stanza["body"]
45
+ fix_namespaces(stanza.xml)
46
+
47
+ self.stanza: Message = stanza
48
+
49
+ @property
50
+ def stanza_component_ns(self):
51
+ stanza = copy(self.stanza)
52
+ fix_namespaces(
53
+ stanza.xml, old="{jabber:client}", new="{jabber:component:accept}"
54
+ )
55
+ return stanza
56
+
57
+ def forwarded(self):
58
+ forwarded = Forwarded()
59
+ forwarded["delay"]["stamp"] = self.when
60
+ forwarded.append(self.stanza)
61
+ return forwarded
slidge/util/conf.py ADDED
@@ -0,0 +1,206 @@
1
+ import logging
2
+ from functools import cached_property
3
+ from types import GenericAlias
4
+ from typing import Optional, Union, get_args, get_origin, get_type_hints
5
+
6
+ import configargparse
7
+
8
+
9
+ class Option:
10
+ DOC_SUFFIX = "__DOC"
11
+ DYNAMIC_DEFAULT_SUFFIX = "__DYNAMIC_DEFAULT"
12
+ SHORT_SUFFIX = "__SHORT"
13
+
14
+ def __init__(self, parent: "ConfigModule", name: str):
15
+ self.parent = parent
16
+ self.config_obj = parent.config_obj
17
+ self.name = name
18
+
19
+ @cached_property
20
+ def doc(self):
21
+ return getattr(self.config_obj, self.name + self.DOC_SUFFIX)
22
+
23
+ @cached_property
24
+ def required(self):
25
+ return not hasattr(
26
+ self.config_obj, self.name + self.DYNAMIC_DEFAULT_SUFFIX
27
+ ) and not hasattr(self.config_obj, self.name)
28
+
29
+ @cached_property
30
+ def default(self):
31
+ return getattr(self.config_obj, self.name, None)
32
+
33
+ @cached_property
34
+ def short(self):
35
+ return getattr(self.config_obj, self.name + self.SHORT_SUFFIX, None)
36
+
37
+ @cached_property
38
+ def nargs(self):
39
+ type_ = get_type_hints(self.config_obj).get(self.name, type(self.default))
40
+
41
+ if isinstance(type_, GenericAlias):
42
+ args = get_args(type_)
43
+ if args[1] is Ellipsis:
44
+ return "*"
45
+ else:
46
+ return len(args)
47
+
48
+ @cached_property
49
+ def type(self):
50
+ type_ = get_type_hints(self.config_obj).get(self.name, type(self.default))
51
+
52
+ if _is_optional(type_):
53
+ type_ = get_args(type_)[0]
54
+ elif isinstance(type_, GenericAlias):
55
+ args = get_args(type_)
56
+ type_ = args[0]
57
+
58
+ return type_
59
+
60
+ @cached_property
61
+ def names(self):
62
+ res = ["--" + self.name.lower().replace("_", "-")]
63
+ if s := self.short:
64
+ res.append("-" + s)
65
+ return res
66
+
67
+ @cached_property
68
+ def kwargs(self):
69
+ kwargs = dict(
70
+ required=self.required,
71
+ help=self.doc,
72
+ env_var=self.name_to_env_var(),
73
+ )
74
+ t = self.type
75
+ if t is bool:
76
+ if self.default:
77
+ kwargs["action"] = "store_false"
78
+ else:
79
+ kwargs["action"] = "store_true"
80
+ else:
81
+ kwargs["type"] = t
82
+ if self.required:
83
+ kwargs["required"] = True
84
+ else:
85
+ kwargs["default"] = self.default
86
+ if n := self.nargs:
87
+ kwargs["nargs"] = n
88
+ return kwargs
89
+
90
+ def name_to_env_var(self):
91
+ return self.parent.ENV_VAR_PREFIX + self.name
92
+
93
+
94
+ class ConfigModule:
95
+ ENV_VAR_PREFIX = "SLIDGE_"
96
+
97
+ def __init__(
98
+ self, config_obj, parser: Optional[configargparse.ArgumentParser] = None
99
+ ):
100
+ self.config_obj = config_obj
101
+ if parser is None:
102
+ parser = configargparse.ArgumentParser()
103
+ self.parser = parser
104
+
105
+ self.add_options_to_parser()
106
+
107
+ def _list_options(self):
108
+ return {
109
+ o
110
+ for o in (set(dir(self.config_obj)) | set(get_type_hints(self.config_obj)))
111
+ if o.upper() == o and not o.startswith("_") and "__" not in o
112
+ }
113
+
114
+ def set_conf(self, argv: Optional[list[str]] = None):
115
+ if argv is not None:
116
+ # this is ugly, but necessary because for plugin config, we used
117
+ # remaining argv.
118
+ # when using (a) .ini file(s), for bool options, we end-up with
119
+ # remaining pseudo-argv such as --some-bool-opt=true when we really
120
+ # should have just --some-bool-opt
121
+ # TODO: get rid of configargparse and make this cleaner
122
+ options_long = {o.name: o for o in self.options}
123
+ no_explicit_bool = []
124
+ skip_next = False
125
+ for a, aa in zip(argv, argv[1:] + [""]):
126
+ if skip_next:
127
+ skip_next = False
128
+ continue
129
+ force_keep = False
130
+ if "=" in a:
131
+ real_name, _value = a.split("=")
132
+ opt: Optional[Option] = options_long.get(
133
+ _argv_to_option_name(real_name)
134
+ )
135
+ if opt and opt.type is bool:
136
+ if opt.default:
137
+ if _value in _TRUEISH or not _value:
138
+ continue
139
+ else:
140
+ a = real_name
141
+ force_keep = True
142
+ else:
143
+ if _value in _TRUEISH:
144
+ a = real_name
145
+ force_keep = True
146
+ else:
147
+ continue
148
+ else:
149
+ upper = _argv_to_option_name(a)
150
+ opt = options_long.get(upper)
151
+ if opt and opt.type is bool:
152
+ if _argv_to_option_name(aa) not in options_long:
153
+ log.debug("Removing %s from argv", aa)
154
+ skip_next = True
155
+
156
+ if opt:
157
+ if opt.type is bool:
158
+ if force_keep or not opt.default:
159
+ no_explicit_bool.append(a)
160
+ else:
161
+ no_explicit_bool.append(a)
162
+ else:
163
+ no_explicit_bool.append(a)
164
+ log.debug("Removed boolean values from %s to %s", argv, no_explicit_bool)
165
+ argv = no_explicit_bool
166
+
167
+ args, rest = self.parser.parse_known_args(argv)
168
+ self.update_dynamic_defaults(args)
169
+ for name in self._list_options():
170
+ value = getattr(args, name.lower())
171
+ log.debug("Setting '%s' to %r", name, value)
172
+ setattr(self.config_obj, name, value)
173
+ return args, rest
174
+
175
+ @cached_property
176
+ def options(self) -> list[Option]:
177
+ res = []
178
+ for opt in self._list_options():
179
+ res.append(Option(self, opt))
180
+ return res
181
+
182
+ def add_options_to_parser(self):
183
+ p = self.parser
184
+ for o in sorted(self.options, key=lambda x: (not x.required, x.name)):
185
+ p.add_argument(*o.names, **o.kwargs)
186
+
187
+ def update_dynamic_defaults(self, args):
188
+ pass
189
+
190
+
191
+ def _is_optional(t):
192
+ if get_origin(t) is Union:
193
+ args = get_args(t)
194
+ if len(args) == 2 and isinstance(None, args[1]):
195
+ return True
196
+ return False
197
+
198
+
199
+ def _argv_to_option_name(arg: str):
200
+ return arg.upper().removeprefix("--").replace("-", "_")
201
+
202
+
203
+ _TRUEISH = {"true", "True", "1", "on", "enabled"}
204
+
205
+
206
+ log = logging.getLogger(__name__)
slidge/util/db.py ADDED
@@ -0,0 +1,229 @@
1
+ """
2
+ This module covers a backend for storing user data persistently and managing a
3
+ pseudo-roster for the gateway component.
4
+ """
5
+
6
+ import dataclasses
7
+ import datetime
8
+ import logging
9
+ import os.path
10
+ import shelve
11
+ from io import BytesIO
12
+ from os import PathLike
13
+ from typing import Iterable, Optional, Union
14
+
15
+ from pickle_secure import Pickler, Unpickler
16
+ from slixmpp import JID, Iq, Message, Presence
17
+
18
+ from .sql import db
19
+
20
+
21
+ # noinspection PyUnresolvedReferences
22
+ class EncryptedShelf(shelve.DbfilenameShelf):
23
+ cache: dict
24
+ dict: dict
25
+ writeback: bool
26
+ keyencoding: str
27
+ _protocol: int
28
+
29
+ def __init__(
30
+ self, filename: PathLike, key: str, flag="c", protocol=None, writeback=False
31
+ ):
32
+ super().__init__(str(filename), flag, protocol, writeback)
33
+ self.secret_key = key
34
+
35
+ def __getitem__(self, key):
36
+ try:
37
+ value = self.cache[key]
38
+ except KeyError:
39
+ f = BytesIO(self.dict[key.encode(self.keyencoding)])
40
+ value = Unpickler(f, key=self.secret_key).load() # type:ignore
41
+ if self.writeback:
42
+ self.cache[key] = value
43
+ return value
44
+
45
+ def __setitem__(self, key, value):
46
+ if self.writeback:
47
+ self.cache[key] = value
48
+ f = BytesIO()
49
+ p = Pickler(f, self._protocol, key=self.secret_key) # type:ignore
50
+ p.dump(value)
51
+ self.dict[key.encode(self.keyencoding)] = f.getvalue()
52
+
53
+
54
+ @dataclasses.dataclass
55
+ class GatewayUser:
56
+ """
57
+ A gateway user
58
+ """
59
+
60
+ bare_jid: str
61
+ """Bare JID of the user"""
62
+ registration_form: dict[str, Optional[str]]
63
+ """Content of the registration form, as a dict"""
64
+ plugin_data: Optional[dict] = None
65
+ registration_date: Optional[datetime.datetime] = None
66
+
67
+ def __hash__(self):
68
+ return hash(self.bare_jid)
69
+
70
+ def __repr__(self):
71
+ return f"<User {self.bare_jid}>"
72
+
73
+ def __post_init__(self):
74
+ if self.registration_date is None:
75
+ self.registration_date = datetime.datetime.now()
76
+
77
+ @property
78
+ def jid(self) -> JID:
79
+ """
80
+ The user's (bare) JID
81
+
82
+ :return:
83
+ """
84
+ return JID(self.bare_jid)
85
+
86
+ def get(self, field: str, default: str = "") -> Optional[str]:
87
+ # """
88
+ # Get fields from the registration form (required to comply with slixmpp backend protocol)
89
+ #
90
+ # :param field: Name of the field
91
+ # :param default: Default value to return if the field is not present
92
+ #
93
+ # :return: Value of the field
94
+ # """
95
+ return self.registration_form.get(field, default)
96
+
97
+ def commit(self):
98
+ db.user_store(self)
99
+ user_store.commit(self)
100
+
101
+
102
+ class UserStore:
103
+ """
104
+ Basic user store implementation using shelve from the python standard library
105
+
106
+ Set_file must be called before it is usable
107
+ """
108
+
109
+ def __init__(self):
110
+ self._users: shelve.Shelf[GatewayUser] = None # type: ignore
111
+
112
+ def set_file(self, filename: PathLike, secret_key: Optional[str] = None):
113
+ """
114
+ Set the file to use to store user data
115
+
116
+ :param filename: Path to the shelf file
117
+ :param secret_key: Secret key to store files encrypted on disk
118
+ """
119
+ if self._users is not None:
120
+ raise RuntimeError("Shelf file already set!")
121
+ if os.path.exists(filename):
122
+ log.info("Using existing slidge DB: %s", filename)
123
+ else:
124
+ log.info("Creating a new slidge DB: %s", filename)
125
+ if secret_key:
126
+ self._users = EncryptedShelf(filename, key=secret_key)
127
+ else:
128
+ self._users = shelve.open(str(filename))
129
+ for user in self._users.values():
130
+ db.user_store(user)
131
+ log.info("Registered users in the DB: %s", list(self._users.keys()))
132
+
133
+ def get_all(self) -> Iterable[GatewayUser]:
134
+ """
135
+ Get all users in the store
136
+
137
+ :return: An iterable of GatewayUsers
138
+ """
139
+ return self._users.values()
140
+
141
+ def add(self, jid: JID, registration_form: dict[str, Optional[str]]):
142
+ """
143
+ Add a user to the store.
144
+
145
+ NB: there is no reason to call this manually, as this should be covered
146
+ by slixmpp XEP-0077 and XEP-0100 plugins
147
+
148
+ :param jid: JID of the gateway user
149
+ :param registration_form: Content of the registration form (:xep:`0077`)
150
+ """
151
+ log.debug("Adding user %s", jid)
152
+ self._users[jid.bare] = user = GatewayUser(
153
+ bare_jid=jid.bare,
154
+ registration_form=registration_form,
155
+ registration_date=datetime.datetime.now(),
156
+ )
157
+ self._users.sync()
158
+ user.commit()
159
+ log.debug("Store: %s", self._users)
160
+
161
+ def commit(self, user: GatewayUser):
162
+ self._users[user.bare_jid] = user
163
+ self._users.sync()
164
+
165
+ def get(self, _gateway_jid, _node, ifrom: JID, iq) -> Optional[GatewayUser]:
166
+ """
167
+ Get a user from the store
168
+
169
+ NB: there is no reason to call this, it is used by SliXMPP internal API
170
+
171
+ :param _gateway_jid:
172
+ :param _node:
173
+ :param ifrom:
174
+ :param iq:
175
+ :return:
176
+ """
177
+ if ifrom is None: # bug in SliXMPP's XEP_0100 plugin
178
+ ifrom = iq["from"]
179
+ log.debug("Getting user %s", ifrom.bare)
180
+ return self._users.get(ifrom.bare)
181
+
182
+ def remove(self, _gateway_jid, _node, ifrom: JID, _iq):
183
+ """
184
+ Remove a user from the store
185
+
186
+ NB: there is no reason to call this, it is used by SliXMPP internal API
187
+ """
188
+ self.remove_by_jid(ifrom)
189
+
190
+ def remove_by_jid(self, jid: JID):
191
+ """
192
+ Remove a user from the store, by JID
193
+ """
194
+ j = jid.bare
195
+ log.debug("Removing user %s", j)
196
+ db.user_del(self._users[j])
197
+ del self._users[j]
198
+ self._users.sync()
199
+
200
+ def get_by_jid(self, jid: JID) -> Optional[GatewayUser]:
201
+ """
202
+ Convenience function to get a user from their JID.
203
+
204
+ :param jid: JID of the gateway user
205
+ :return:
206
+ """
207
+ return self._users.get(jid.bare)
208
+
209
+ def get_by_stanza(self, s: Union[Presence, Message, Iq]) -> Optional[GatewayUser]:
210
+ """
211
+ Convenience function to get a user from a stanza they sent.
212
+
213
+ :param s: A stanza sent by the gateway user
214
+ :return:
215
+ """
216
+ return self.get_by_jid(s.get_from())
217
+
218
+ def close(self):
219
+ self._users.sync()
220
+ self._users.close()
221
+
222
+
223
+ user_store = UserStore()
224
+ """
225
+ A persistent store for slidge users. Not public, but I didn't find how to hide
226
+ it from the docs!
227
+ """
228
+
229
+ log = logging.getLogger(__name__)