plugin XEP-0384: Fully reworked to adjust to the reworked python-omemo:

- support for both (modern) OMEMO under the `urn:xmpp:omemo:2` namespace and (legacy) OMEMO under the `eu.siacs.conversations.axolotl` namespace
- maintains one identity across both versions of OMEMO
- migrates data from the old plugin
- includes more features for protocol stability
- uses SCE for modern OMEMO
- fully type-checked, linted and format-checked
- added type hints to various pieces of backend code used by the plugin
- added stubs for some Twisted APIs used by the plugin under stubs/ (use `export MYPYPATH=stubs/` before running mypy)
- core (xmpp): enabled `send` trigger and made it an asyncPoint

fix 375
This commit is contained in:
Syndace 2022-08-23 21:06:24 +02:00
commit 75ae8160ae
27 changed files with 2202 additions and 1414 deletions

View file

@ -63,7 +63,8 @@ confidence=
disable=missing-module-docstring,
duplicate-code,
fixme,
logging-fstring-interpolation
logging-fstring-interpolation,
broad-except
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

View file

@ -13,7 +13,7 @@ cryptography==3.4.8
cssselect2==0.6.0
dbus-python==1.2.18
defusedxml==0.7.1
DoubleRatchet==0.7.0
DoubleRatchet @ git+https://github.com/Syndace/python-doubleratchet.git@stable
greenlet==1.1.2
html2text==2020.1.16
hyperlink==21.0.0
@ -29,8 +29,8 @@ miniupnpc==2.0.2
mutagen==1.45.1
netifaces==0.11.0
numpy==1.22.3
OMEMO==0.12.0
omemo-backend-signal==0.2.7
Oldmemo @ git+https://github.com/Syndace/python-oldmemo.git
OMEMO @ git+https://github.com/Syndace/python-omemo.git@stable
packaging==21.3
Pillow==9.1.0
progressbar2==3.53.3
@ -61,14 +61,16 @@ tinycss2==1.1.1
tomli==2.0.1
treq==21.5.0
Twisted==21.2.0
Twomemo @ git+https://github.com/Syndace/python-twomemo.git
txdbus==1.1.2
typing_extensions==4.2.0
typing_extensions==4.3.0
urllib3==1.26.9
urwid==2.1.2
webencodings==0.5.1
wokkel==18.0.0
X3DH==0.5.9
XEdDSA==0.4.7
X3DH @ git+https://github.com/Syndace/python-x3dh.git@stable
XEdDSA @ git+https://github.com/Syndace/python-xeddsa.git@stable
xmlschema==2.0.4
zope.interface==5.4.0
sat-tmp @ hg+https://repos.goffi.org/sat_tmp

View file

@ -22,6 +22,7 @@ try:
except ImportError:
BaseDirectory = None
from os.path import dirname
from typing_extensions import Final
import sat
@ -126,7 +127,7 @@ class Const(object):
EXTRA_INFO_ENCR_ERR = "ENCRYPTION_ERROR"
# encryption is a key for plugins
MESS_KEY_ENCRYPTION = "ENCRYPTION"
MESS_KEY_ENCRYPTION: Final = "ENCRYPTION"
# encrypted is a key for frontends
MESS_KEY_ENCRYPTED = "encrypted"
MESS_KEY_TRUSTED = "trusted"
@ -431,7 +432,7 @@ class Const(object):
return value.lower() in (cls.BOOL_TRUE, "1", "yes", "on")
@classmethod
def boolConst(cls, value):
def boolConst(cls, value: bool) -> str:
"""@return (str): constant associated to bool value"""
assert isinstance(value, bool)
return cls.BOOL_TRUE if value else cls.BOOL_FALSE

View file

@ -16,10 +16,47 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import namedtuple
from typing import Dict
from typing_extensions import TypedDict
from twisted.words.protocols.jabber import jid as t_jid
from twisted.words.xish import domish
class SatXMPPEntity:
jid: t_jid.JID
is_component: bool
EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance",
"name",
"namespace",
"priority",
"directed"))
class EncryptionSession(TypedDict):
plugin: EncryptionPlugin
# Incomplete types built through observation rather than code inspection.
MessageDataExtra = TypedDict(
"MessageDataExtra",
{ "encrypted": bool, "origin_id": str },
total=False
)
MessageData = TypedDict("MessageData", {
"from": t_jid.JID,
"to": t_jid.JID,
"uid": str,
"message": Dict[str, str],
"subject": Dict[str, str],
"type": str,
"timestamp": float,
"extra": MessageDataExtra,
"ENCRYPTION": EncryptionSession,
"xml": domish.Element
}, total=False)

View file

@ -21,6 +21,14 @@
# XXX: this module use standard logging module when possible, but as SàT can work in different cases where logging is not the best choice (twisted, pyjamas, etc), it is necessary to have a dedicated module. Additional feature like environment variables and colors are also managed.
# TODO: change formatting from "%s" style to "{}" when moved to Python 3
from typing import TYPE_CHECKING, Any, Optional
from typing_extensions import TypedDict
if TYPE_CHECKING:
from logging import _ExcInfoType
else:
_ExcInfoType = Any
from sat.core.constants import Const as C
from sat.tools.common.ansi import ANSI as A
from sat.core import exceptions
@ -37,6 +45,10 @@ class Filtered(Exception):
pass
class KWArgs(TypedDict):
exc_info: _ExcInfoType
class Logger:
"""High level logging class"""
fmt = None # format option as given by user (e.g. SAT_LOG_LOGGER)
@ -60,7 +72,7 @@ class Logger:
tb = traceback.format_exc()
return message + "\n==== traceback ====\n" + tb
def out(self, message, level=None, **kwargs):
def out(self, message: object, level: Optional[str] = None, **kwargs: KWArgs) -> None:
"""Actually log the message
@param message: formatted message
@ -69,7 +81,7 @@ class Logger:
message = self.addTraceback(message)
print(message)
def log(self, level, message, **kwargs):
def log(self, level: str, message: object, **kwargs: KWArgs) -> None:
"""Print message
@param level: one of C.LOG_LEVELS
@ -84,7 +96,7 @@ class Logger:
except Filtered:
pass
def format(self, level, message):
def format(self, level: str, message: object) -> object:
"""Format message according to Logger.fmt
@param level: one of C.LOG_LEVELS
@ -117,19 +129,19 @@ class Logger:
else:
raise e
def debug(self, msg, **kwargs):
def debug(self, msg: object, **kwargs: KWArgs) -> None:
self.log(C.LOG_LVL_DEBUG, msg, **kwargs)
def info(self, msg, **kwargs):
def info(self, msg: object, **kwargs: KWArgs) -> None:
self.log(C.LOG_LVL_INFO, msg, **kwargs)
def warning(self, msg, **kwargs):
def warning(self, msg: object, **kwargs: KWArgs) -> None:
self.log(C.LOG_LVL_WARNING, msg, **kwargs)
def error(self, msg, **kwargs):
def error(self, msg: object, **kwargs: KWArgs) -> None:
self.log(C.LOG_LVL_ERROR, msg, **kwargs)
def critical(self, msg, **kwargs):
def critical(self, msg: object, **kwargs: KWArgs) -> None:
self.log(C.LOG_LVL_CRITICAL, msg, **kwargs)

View file

@ -634,10 +634,10 @@ class SAT(service.Service):
return (None, None)
return (self.profiles[profile].jid, self.profiles[profile].xmlstream)
def getClient(self, profile_key):
def getClient(self, profile_key: str) -> xmpp.SatXMPPClient:
"""Convenient method to get client from profile key
@return: client or None if it doesn't exist
@return: the client
@raise exceptions.ProfileKeyUnknown: the profile or profile key doesn't exist
@raise exceptions.NotFound: client is not available
This happen if profile has not been used yet

View file

@ -550,10 +550,14 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
).toResponse(iq_elt)
self.xmlstream.send(iq_error_elt)
def generateMessageXML(self, data, post_xml_treatments=None):
def generateMessageXML(
self,
data: core_types.MessageData,
post_xml_treatments: Optional[defer.Deferred] = None
) -> core_types.MessageData:
"""Generate <message/> stanza from message data
@param data(dict): message data
@param data: message data
domish element will be put in data['xml']
following keys are needed:
- from
@ -563,9 +567,9 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
- type
- subject
- extra
@param post_xml_treatments(Deferred): a Deferred which will be called with data
once XML is generated
@return (dict) message data
@param post_xml_treatments: a Deferred which will be called with data once XML is
generated
@return: message data
"""
data["xml"] = message_elt = domish.Element((None, "message"))
message_elt["to"] = data["to"].full()
@ -612,7 +616,7 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
"""
raise NotImplementedError
def send(self, obj):
async def send(self, obj):
# original send method accept string
# but we restrict to domish.Element to make trigger treatments easier
assert isinstance(obj, domish.Element)
@ -622,14 +626,11 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
# (out of band transmission for instance).
# e2e should have a priority of 0 here, and out of band transmission
# a lower priority
#  FIXME: trigger not used yet, can be uncommented when e2e full stanza
# encryption is implemented
#  if not self.host_app.trigger.point("send", self, obj):
#   return
if not (await self.host_app.trigger.asyncPoint("send", self, obj)):
return
super().send(obj)
@defer.inlineCallbacks
def sendMessageData(self, mess_data):
async def sendMessageData(self, mess_data):
"""Convenient method to send message data to stream
This method will send mess_data[u'xml'] to stream, but a trigger is there
@ -644,10 +645,10 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
# This is intented for e2e encryption which doesn't do full stanza
# encryption (e.g. OTR)
# This trigger point can't cancel the method
yield self.host_app.trigger.asyncPoint("sendMessageData", self, mess_data,
await self.host_app.trigger.asyncPoint("sendMessageData", self, mess_data,
triggers_no_cancel=True)
self.send(mess_data["xml"])
defer.returnValue(mess_data)
await self.send(mess_data["xml"])
return mess_data
def sendMessage(
self, to_jid, message, subject=None, mess_type="auto", extra=None, uid=None,
@ -824,6 +825,9 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
return ClientPluginWrapper(self, plugin_name, missing)
ExtraDict = dict # TODO
@implementer(iwokkel.IDisco)
class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
trigger_suffix = ""
@ -930,15 +934,20 @@ class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
)
post_xml_treatments.addCallback(self.messageSendToBridge)
def feedback(self, to_jid, message, extra=None):
def feedback(
self,
to_jid: jid.JID,
message: str,
extra: Optional[ExtraDict] = None
) -> None:
"""Send message to frontends
This message will be an info message, not recorded in history.
It can be used to give feedback of a command
@param to_jid(jid.JID): destinee jid
@param message(unicode): message to send to frontends
@param extra(None, dict): extra data to use
in particular, info subtype can be specified with MESS_EXTRA_INFO
@param to_jid: destinee jid
@param message: message to send to frontends
@param extra: extra data to use in particular, info subtype can be specified with
MESS_EXTRA_INFO
"""
if extra is None:
extra = {}

View file

@ -19,10 +19,11 @@
import copy
from functools import partial
from collections import namedtuple
from typing import Optional
from twisted.words.protocols.jabber import jid
from twisted.internet import defer
from twisted.python import failure
from sat.core.core_types import EncryptionPlugin, EncryptionSession, MessageData
from sat.core.i18n import D_, _
from sat.core.constants import Const as C
from sat.core import exceptions
@ -34,12 +35,6 @@ from sat.memory import persistent
log = getLogger(__name__)
EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance",
"name",
"namespace",
"priority",
"directed"))
class EncryptionHandler:
"""Class to handle encryption sessions for a client"""
@ -339,7 +334,7 @@ class EncryptionHandler:
self.client.feedback(entity, msg)
def getSession(self, entity):
def getSession(self, entity: jid.JID) -> Optional[EncryptionSession]:
"""Get encryption session for this contact
@param entity(jid.JID): get the session for this entity
@ -476,13 +471,17 @@ class EncryptionHandler:
return mess_data
def isEncryptionRequested(self, mess_data, namespace=None):
def isEncryptionRequested(
self,
mess_data: MessageData,
namespace: Optional[str] = None
) -> bool:
"""Helper method to check if encryption is requested in an outgoind message
@param mess_data(dict): message data for outgoing message
@param namespace(str, None): if set, check if encryption is requested for the
algorithm specified
@return (bool): True if the encryption flag is present
@param mess_data: message data for outgoing message
@param namespace: if set, check if encryption is requested for the algorithm
specified
@return: True if the encryption flag is present
"""
encryption = mess_data.get(C.MESS_KEY_ENCRYPTION)
if encryption is None:

View file

@ -26,6 +26,7 @@ from twisted.words.protocols.jabber import error as xmpp_error
from twisted.python import failure
from sat.core import exceptions
from sat.core.xmpp import SatXMPPClient
from sat.memory import memory
import time
@ -204,10 +205,10 @@ class XEP_0045(object):
return False
return True
def getRoom(self, client, room_jid):
def getRoom(self, client: SatXMPPClient, room_jid: jid.JID) -> muc.Room:
"""Retrieve Room instance from its jid
@param room_jid(jid.JID): jid of the room
@param room_jid: jid of the room
@raise exceptions.NotFound: the room has not been joined
"""
try:
@ -223,10 +224,10 @@ class XEP_0045(object):
if room_jid not in client._muc_client.joined_rooms:
raise exceptions.NotFound(_("This room has not been joined"))
def isJoinedRoom(self, client, room_jid):
def isJoinedRoom(self, client: SatXMPPClient, room_jid: jid.JID) -> bool:
"""Tell if a jid is a known and joined room
@room_jid(jid.JID): jid of the room
@room_jid: jid of the room
"""
try:
self.checkRoomJoined(client, room_jid)

View file

@ -19,7 +19,7 @@
from collections import namedtuple
from functools import reduce
from typing import Any, Dict, List, Optional, Tuple, Union, Callable
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, Callable
import urllib.error
import urllib.parse
import urllib.request
@ -39,6 +39,7 @@ from sat.core.constants import Const as C
from sat.core.core_types import SatXMPPEntity
from sat.core.i18n import _
from sat.core.log import getLogger
from sat.core.xmpp import SatXMPPClient
from sat.tools import utils
from sat.tools import sat_defer
from sat.tools import xml_tools
@ -538,17 +539,22 @@ class XEP_0060(object):
))
async def sendItem(
self, client, service, nodeIdentifier, payload, item_id=None, extra=None
):
self,
client: SatXMPPClient,
service: Union[jid.JID, None],
nodeIdentifier: str,
payload: domish.Element,
item_id: Optional[str] = None,
extra: Optional[Dict[str, Any]] = None
) -> Optional[str]:
"""High level method to send one item
@param service(jid.JID, None): service to send the item to
None to use PEP
@param NodeIdentifier(unicode): PubSub node to use
@param payload(domish.Element): payload of the item to send
@param item_id(unicode, None): id to use or None to create one
@param extra(dict, None): extra option, not used yet
@return (unicode, None): id of the created item
@param service: service to send the item to None to use PEP
@param NodeIdentifier: PubSub node to use
@param payload: payload of the item to send
@param item_id: id to use or None to create one
@param extra: extra options
@return: id of the created item
"""
assert isinstance(payload, domish.Element)
item_elt = domish.Element((pubsub.NS_PUBSUB, 'item'))
@ -1090,7 +1096,12 @@ class XEP_0060(object):
client, jid.JID(service_s) if service_s else None, nodeIdentifier
)
def deleteNode(self, client, service, nodeIdentifier):
def deleteNode(
self,
client: SatXMPPClient,
service: jid.JID,
nodeIdentifier: str
) -> defer.Deferred:
return client.pubsub_client.deleteNode(service, nodeIdentifier)
def _addWatch(self, service_s, node, profile_key):
@ -1132,12 +1143,12 @@ class XEP_0060(object):
def retractItems(
self,
client,
service,
nodeIdentifier,
itemIdentifiers,
notify=True,
):
client: SatXMPPClient,
service: jid.JID,
nodeIdentifier: str,
itemIdentifiers: Iterable[str],
notify: bool = True,
) -> defer.Deferred:
return client.pubsub_client.retractItems(
service, nodeIdentifier, itemIdentifiers, notify=notify
)

View file

@ -17,9 +17,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Type-check with `mypy --strict`
# Lint with `pylint`
from sat.core.constants import Const as C
from sat.core.i18n import D_
from sat.core.sat_main import SAT

View file

@ -18,6 +18,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Iterable
from sat.core.i18n import _, D_
from sat.core.log import getLogger
@ -29,6 +30,7 @@ from sat.tools.common import data_format
from wokkel import disco, iwokkel
from twisted.words.protocols.jabber import xmlstream
from twisted.words.xish import domish
from zope.interface import implementer
from textwrap import dedent
@ -83,11 +85,11 @@ class XEP_0334(object):
else:
log.error("Unknown hint: {}".format(hint))
def addHintElements(self, message_elt, hints):
def addHintElements(self, message_elt: domish.Element, hints: Iterable[str]) -> None:
"""Add hints elements to message stanza
@param message_elt(domish.Element): stanza where hints must be added
@param hints(iterable(unicode)): hints to add
@param message_elt: stanza where hints must be added
@param hints: hints to add
"""
for hint in hints:
message_elt.addElement((NS_HINTS, hint))

View file

@ -18,6 +18,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Optional
import uuid
from zope.interface import implementer
from twisted.words.protocols.jabber import xmlstream
@ -26,6 +27,7 @@ from sat.core.constants import Const as C
from sat.core import exceptions
from sat.core.i18n import _
from sat.core.log import getLogger
from twisted.words.xish import domish
log = getLogger(__name__)
@ -101,11 +103,11 @@ class XEP_0359(object):
sid_elt["by"] = client.jid.userhost() if by is None else by.userhost()
sid_elt["id"] = stanza_id
def getOriginId(self, element):
def getOriginId(self, element: domish.Element) -> Optional[str]:
"""Return origin-id if found in element
@param element(domish.Element): element to parse
@return (unicode, None): origin-id if found
@param element: element to parse
@return: origin-id if found
"""
try:
origin_elt = next(element.elements(NS_SID, "origin-id"))

File diff suppressed because it is too large Load diff

View file

@ -16,15 +16,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Type-check with `mypy --strict --disable-error-code no-untyped-call`
# Lint with `pylint`
from abc import ABC, abstractmethod
from datetime import datetime
import enum
import secrets
import string
from typing import Dict, Iterator, List, NamedTuple, Optional, Set, Tuple, Union, cast
from typing import Dict, NamedTuple, Optional, Set, Tuple, cast
from lxml import etree
@ -55,7 +52,7 @@ __all__ = [ # pylint: disable=unused-variable
]
log = cast(Logger, getLogger(__name__))
log = cast(Logger, getLogger(__name__)) # type: ignore[no-untyped-call]
PLUGIN_INFO = {
@ -291,19 +288,15 @@ class XEP_0420: # pylint: disable=invalid-name
# Note the serialized byte size of the content element before adding any children
empty_content_byte_size = len(content.toXml().encode("utf-8"))
# Just for type safety
stanza_children = cast(List[Union[domish.Element, str]], stanza.children)
content_children = cast(List[Union[domish.Element, str]], content.children)
# Move elements that are not explicitly forbidden from being encrypted from the
# stanza to the content element.
for child in list(cast(Iterator[domish.Element], stanza.elements())):
for child in list(stanza.elements()):
if (
child.uri not in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES
and (child.uri, child.name) not in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS
):
# Remove the child from the stanza
stanza_children.remove(child)
stanza.children.remove(child)
# A namespace of ``None`` can be used on domish elements to inherit the
# namespace from the parent. When moving elements from the stanza root to
@ -316,7 +309,7 @@ class XEP_0420: # pylint: disable=invalid-name
child.defaultUri = C.NS_CLIENT
# Add the child with corrected namespaces to the content element
content_children.append(child)
content.addChild(child)
# Add the affixes requested by the profile
if profile.rpad_policy is not SCEAffixPolicy.NOT_NEEDED:
@ -345,7 +338,7 @@ class XEP_0420: # pylint: disable=invalid-name
time_element["stamp"] = XEP_0082.format_datetime()
if profile.to_policy is not SCEAffixPolicy.NOT_NEEDED:
recipient = cast(Optional[str], stanza.getAttribute("to", None))
recipient = stanza.getAttribute("to", None)
if recipient is None:
raise ValueError(
"<to/> affix requested, but stanza doesn't have the 'to' attribute"
@ -356,7 +349,7 @@ class XEP_0420: # pylint: disable=invalid-name
to_element["jid"] = jid.JID(recipient).userhost()
if profile.from_policy is not SCEAffixPolicy.NOT_NEEDED:
sender = cast(Optional[str], stanza.getAttribute("from", None))
sender = stanza.getAttribute("from", None)
if sender is None:
raise ValueError(
"<from/> affix requested, but stanza doesn't have the 'from'"
@ -370,7 +363,7 @@ class XEP_0420: # pylint: disable=invalid-name
if policy is not SCEAffixPolicy.NOT_NEEDED:
envelope.addChild(affix.create(stanza))
return cast(str, envelope.toXml()).encode("utf-8")
return envelope.toXml().encode("utf-8")
@staticmethod
def unpack_stanza(
@ -431,7 +424,7 @@ class XEP_0420: # pylint: disable=invalid-name
# Prepare the envelope and content elements
envelope = cast(domish.Element, ElementParser()(envelope_serialized_string))
content = cast(domish.Element, next(envelope.elements(NS_SCE, "content")))
content = next(envelope.elements(NS_SCE, "content"))
# Verify the affixes
rpad_element = cast(
@ -468,7 +461,7 @@ class XEP_0420: # pylint: disable=invalid-name
if to_element is not None:
recipient_value = jid.JID(to_element["jid"])
recipient_actual = cast(Optional[str], stanza.getAttribute("to", None))
recipient_actual = stanza.getAttribute("to", None)
if recipient_actual is None:
raise AffixVerificationFailed(
"'To' affix is included in the envelope, but the stanza is lacking a"
@ -491,7 +484,7 @@ class XEP_0420: # pylint: disable=invalid-name
if from_element is not None:
sender_value = jid.JID(from_element["jid"])
sender_actual = cast(Optional[str], stanza.getAttribute("from", None))
sender_actual = stanza.getAttribute("from", None)
if sender_actual is None:
raise AffixVerificationFailed(
"'From' affix is included in the envelope, but the stanza is lacking"
@ -551,13 +544,9 @@ class XEP_0420: # pylint: disable=invalid-name
+ custom_missing_string
)
# Just for type safety
content_children = cast(List[Union[domish.Element, str]], content.children)
stanza_children = cast(List[Union[domish.Element, str]], stanza.children)
# Move elements that are not explicitly forbidden from being encrypted from the
# content element to the stanza.
for child in list(cast(Iterator[domish.Element], content.elements())):
for child in list(content.elements()):
if (
child.uri in XEP_0420.MUST_BE_PLAINTEXT_NAMESPACES
or (child.uri, child.name) in XEP_0420.MUST_BE_PLAINTEXT_ELEMENTS
@ -568,10 +557,10 @@ class XEP_0420: # pylint: disable=invalid-name
)
else:
# Remove the child from the content element
content_children.remove(child)
content.children.remove(child)
# Add the child to the stanza
stanza_children.append(child)
stanza.addChild(child)
return SCEAffixValues(
rpad_value,

View file

@ -16,14 +16,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Type-check with `mypy --strict`
# Lint with `pylint`
from sat.core import exceptions
from datetime import date, datetime, time, timezone
import re
from typing import Optional, Tuple
from sat.core import exceptions
__all__ = [ # pylint: disable=unused-variable
"format_date",

View file

@ -51,8 +51,9 @@ install_requires = [
'urwid >= 1.2.0, < 3',
'urwid-satext == 0.9.*',
'wokkel >= 18.0.0, < 19.0.0',
'omemo >= 0.11.0, < 0.13.0',
'omemo-backend-signal < 0.3',
'omemo >= 1.0.0, < 2',
'twomemo >= 1.0.0, < 2',
'oldmemo >= 1.0.0, < 2',
'pyyaml < 7.0.0',
'sqlalchemy >= 1.4',
'alembic',

View file

View file

View file

@ -0,0 +1,14 @@
from typing import Optional
from twisted.words.xish import domish
class BaseError(Exception):
namespace: str
condition: str
text: Optional[str]
textLang: Optional[str]
appCondition: Optional[domish.Element]
class StanzaError(BaseError):
...

View file

@ -0,0 +1,30 @@
from typing import Tuple, overload
User = str
Host = str
Resource = str
# Note: these typings are incomplete and evolve as needed.
class JID:
user: User
host: Host
resource: Resource
@overload
def __init__(self, str: str, tuple: None = None) -> None:
...
@overload
def __init__(self, str: None, tuple: Tuple[User, Host, Resource]) -> None:
...
def userhost(self) -> str:
...
def userhostJID(self) -> "JID":
...
def full(self) -> str:
...

View file

View file

@ -0,0 +1,80 @@
from typing import (
Dict, Iterator, List, Literal, Optional, Tuple, TypeVar, Union, overload
)
URI = str
Name = str
QName = Tuple[Optional[URI], Name]
AttributeKey = Union[QName, Name]
Attributes = Dict[AttributeKey, str]
Prefix = str
D = TypeVar("D")
# Note: these typings are incomplete and evolve as needed.
class Element:
uri: Optional[URI]
name: Name
defaultUri: Optional[URI]
children: List[Union["Element", str]]
attributes: Attributes
def __init__(
self,
qname: QName,
defaultUri: Optional[URI] = None,
attribs: Optional[Attributes] = None,
localPrefixes: Optional[Dict[URI, Prefix]] = None
) -> None:
...
def __getitem__(self, key: AttributeKey) -> str:
...
def __setitem__(self, key: AttributeKey, value: str) -> None:
...
@overload
def getAttribute(
self,
attribname: AttributeKey,
default: None = None
) -> Union[str, None]:
...
@overload
def getAttribute(self, attribname: AttributeKey, default: D) -> Union[str, D]:
...
def addChild(self, node: "Element") -> "Element":
...
def addContent(self, text: str) -> str:
...
def addElement(
self,
name: Union[QName, Name],
defaultUri: Optional[URI] = None,
content: Optional[str] = None
) -> "Element":
...
def elements(
self,
uri: Optional[URI] = None,
name: Optional[Name] = None
) -> Iterator["Element"]:
...
def toXml(
self,
prefixes: Optional[Dict[URI, Prefix]] = None,
closeElement: Literal[0, 1] = 1,
defaultUri: str = "",
prefixesInScope: Optional[List[Prefix]] = None
) -> str:
...

View file

@ -16,9 +16,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Type-check with `mypy --strict`
# Lint with `pylint`
from datetime import date, datetime, time, timezone
import pytest

View file

@ -16,11 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Type-check with `mypy --strict --disable-error-code no-untyped-call`
# Lint with `pylint`
from datetime import datetime, timezone
from typing import Callable, Iterator, Optional, cast
from typing import Callable, cast
import pytest
@ -77,8 +74,8 @@ class CustomAffixImpl(SCECustomAffix):
</xs:element>"""
def create(self, stanza: domish.Element) -> domish.Element:
recipient = cast(Optional[str], stanza.getAttribute("to", None))
sender = cast(Optional[str], stanza.getAttribute("from", None))
recipient = stanza.getAttribute("to", None)
sender = stanza.getAttribute("from", None)
if recipient is None or sender is None:
raise ValueError(
@ -487,21 +484,15 @@ def test_namespace_conversion() -> None: # pylint: disable=missing-function-doc
envelope_serialized = XEP_0420.pack_stanza(profile, stanza)
envelope = string_to_domish(envelope_serialized.decode("utf-8"))
content = next(cast(Iterator[domish.Element], envelope.elements(NS_SCE, "content")))
content = next(envelope.elements(NS_SCE, "content"))
# The body should have been assigned ``jabber:client`` as its namespace
assert next(
cast(Iterator[domish.Element], content.elements("jabber:client", "body")),
None
) is not None
assert next(content.elements("jabber:client", "body"), None) is not None
XEP_0420.unpack_stanza(profile, stanza, envelope_serialized)
# The body should still have ``jabber:client`` after unpacking
assert next(
cast(Iterator[domish.Element], stanza.elements("jabber:client", "body")),
None
) is not None
assert next(stanza.elements("jabber:client", "body"), None) is not None
def test_non_encryptable_elements() -> None: # pylint: disable=missing-function-docstring
@ -520,18 +511,11 @@ def test_non_encryptable_elements() -> None: # pylint: disable=missing-function
envelope_serialized = XEP_0420.pack_stanza(profile, stanza)
envelope = string_to_domish(envelope_serialized.decode("utf-8"))
content = next(cast(Iterator[domish.Element], envelope.elements(NS_SCE, "content")))
content = next(envelope.elements(NS_SCE, "content"))
# The store hint must not have been moved to the content element
assert next(
cast(Iterator[domish.Element], stanza.elements(NS_HINTS, "store")),
None
) is not None
assert next(
cast(Iterator[domish.Element], content.elements(NS_HINTS, "store")),
None
) is None
assert next(stanza.elements(NS_HINTS, "store"), None) is not None
assert next(content.elements(NS_HINTS, "store"), None) is None
stanza = string_to_domish(
"""<message from="foo@example.com" to="bar@example.com"></message>"""
@ -548,10 +532,7 @@ def test_non_encryptable_elements() -> None: # pylint: disable=missing-function
XEP_0420.unpack_stanza(profile, stanza, envelope_serialized)
assert next(
cast(Iterator[domish.Element], stanza.elements(NS_HINTS, "store")),
None
) is None
assert next(stanza.elements(NS_HINTS, "store"), None) is None
def test_schema_validation() -> None: # pylint: disable=missing-function-docstring