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:
parent
6eac59dc2d
commit
75ae8160ae
27 changed files with 2202 additions and 1414 deletions
3
pylintrc
3
pylintrc
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
5
setup.py
5
setup.py
|
|
@ -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',
|
||||
|
|
|
|||
0
stubs/twisted/__init__.pyi
Normal file
0
stubs/twisted/__init__.pyi
Normal file
0
stubs/twisted/words/__init__.pyi
Normal file
0
stubs/twisted/words/__init__.pyi
Normal file
0
stubs/twisted/words/protocols/__init__.pyi
Normal file
0
stubs/twisted/words/protocols/__init__.pyi
Normal file
0
stubs/twisted/words/protocols/jabber/__init__.pyi
Normal file
0
stubs/twisted/words/protocols/jabber/__init__.pyi
Normal file
14
stubs/twisted/words/protocols/jabber/error.pyi
Normal file
14
stubs/twisted/words/protocols/jabber/error.pyi
Normal 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):
|
||||
...
|
||||
30
stubs/twisted/words/protocols/jabber/jid.pyi
Normal file
30
stubs/twisted/words/protocols/jabber/jid.pyi
Normal 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:
|
||||
...
|
||||
0
stubs/twisted/words/xish/__init__.pyi
Normal file
0
stubs/twisted/words/xish/__init__.pyi
Normal file
80
stubs/twisted/words/xish/domish.pyi
Normal file
80
stubs/twisted/words/xish/domish.pyi
Normal 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:
|
||||
...
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue