slidge 0.1.0__py3-none-any.whl

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