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.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- 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,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)
|
slidge/util/__init__.py
ADDED
@@ -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__)
|