Python : Add support for newer OMEMO (Thank you. Syndance);

Python : Fix Ad-Hoc Commands;
SQLite : Fix tagging mechanism.
This commit is contained in:
Schimon Jehudah, Adv. 2024-09-12 15:20:14 +03:00
parent 178f49cb86
commit c050c765dd
8 changed files with 216 additions and 60 deletions

View file

@ -47,7 +47,6 @@ dependencies = [
"feedparser",
"lxml",
"python-dateutil",
"requests",
"slixmpp",
"tomli", # Python 3.10
"tomli_w",
@ -59,14 +58,7 @@ Repository = "https://git.xmpp-it.net/sch/Slixfeed"
Issues = "https://gitgud.io/sjehuda/slixfeed/issues"
[project.optional-dependencies]
omemo = [
"DoubleRatchet>=0.7.0,<0.8",
"OMEMO>=0.13.0,<0.15",
"protobuf==3.20.3",
"slixmpp-omemo",
"X3DH>=0.5.9,<0.6",
"XEdDSA<0.5,>=0.4.7",
]
omemo = ["slixmpp-omemo"]
proxy = ["pysocks"]
# [project.readme]

View file

@ -366,7 +366,7 @@ def create_tables(db_file):
id INTEGER NOT NULL,
feed_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
FOREIGN KEY ("feed_id") REFERENCES "feeds" ("id")
FOREIGN KEY ("feed_id") REFERENCES "feeds_properties" ("id")
ON UPDATE CASCADE
ON DELETE CASCADE,
FOREIGN KEY ("tag_id") REFERENCES "tags" ("id")

View file

@ -1,2 +1,2 @@
__version__ = '0.1.95'
__version_info__ = (0, 1, 95)
__version__ = '0.1.96'
__version_info__ = (0, 1, 96)

View file

@ -154,18 +154,23 @@ class XmppChat:
# await compose.message(self, jid_bare, message)
if self.omemo_present and self['xep_0384'].is_encrypted(message):
allow_untrusted=True # Temporary fix. This should be handled by "retry""
command, omemo_decrypted, retry = await XmppOmemo.decrypt(
self, message, allow_untrusted)
if retry:
command, omemo_decrypted, retry = await XmppOmemo.decrypt(
self, message, allow_untrusted=True)
command, omemo_decrypted = await XmppOmemo.decrypt(
self, message)
else:
omemo_decrypted = None
if message_type == 'groupchat':
command = command[1:]
command_lowercase = command.lower()
if isinstance(command, str):
command_lowercase = command.lower()
elif isinstance(command, Message):
command_lowercase = command['body'].lower()
# This is a work-around to empty messages that are caused by function
# self.register_handler(CoroutineCallback( of module client.py.
# The code was taken from the cho bot xample of slixmpp-omemo.
#if not command_lowercase: return
logger.debug([message_from.full, ':', command])
@ -363,7 +368,7 @@ class XmppChat:
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
if self.omemo_present and encrypted:
url_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, message_from, url)
self, message_from, 'chat', url)
XmppMessage.send_omemo_oob(self, message_from, url_encrypted, chat_type)
else:
XmppMessage.send_oob(self, jid_bare, url, chat_type)
@ -632,7 +637,7 @@ class XmppChat:
encrypted = True if encrypt_omemo else False
if self.omemo_present and encrypted and self['xep_0384'].is_encrypted(message):
response_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, message_from, response)
self, message_from, 'chat', response)
if omemo_decrypted and omemo_encrypted:
# message_from = message['from']
# message_type = message['type']
@ -737,7 +742,7 @@ class XmppChatAction:
if media_url and news_digest:
if self.omemo_present and encrypt_omemo:
news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, jid, news_digest)
self, jid, 'chat', news_digest)
if self.omemo_present and encrypt_omemo and omemo_encrypted:
XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted)
else:
@ -777,7 +782,7 @@ class XmppChatAction:
# media_url_new = await XmppUpload.start(
# self, jid_bare, Path(pathname), filesize, encrypted=encrypted)
media_url_new_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, jid, media_url_new)
self, jid, 'chat', media_url_new)
if media_url_new_encrypted and omemo_encrypted:
# NOTE Tested against Gajim.
# FIXME This only works with aesgcm URLs, and it does
@ -801,7 +806,7 @@ class XmppChatAction:
if news_digest:
if self.omemo_present and encrypt_omemo:
news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, jid, news_digest)
self, jid, 'chat', news_digest)
if self.omemo_present and encrypt_omemo and omemo_encrypted:
XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted)
else:

View file

@ -145,22 +145,40 @@ class XmppClient(slixmpp.ClientXMPP):
self.register_plugin('xep_0115') # Entity Capabilities
self.register_plugin('xep_0122') # Data Forms Validation
self.register_plugin('xep_0153') # vCard-Based Avatars
self.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping
self.register_plugin('xep_0199', # XMPP Ping
{'keepalive': True})
self.register_plugin('xep_0203') # Delayed Delivery
self.register_plugin('xep_0249') # Direct MUC Invitations
self.register_plugin('xep_0363') # HTTP File Upload
self.register_plugin('xep_0380') # Explicit Message Encryption
self.register_plugin('xep_0402') # PEP Native Bookmarks
self.register_plugin('xep_0444') # Message Reactions
try:
from slixfeed.xmpp.encryption import XmppOmemo
import slixmpp_omemo
from slixmpp_omemo import PluginCouldNotLoad
self.omemo_present = True
except Exception as e:
print('Encryption of type OMEMO is not enabled. Reason: ' + str(e))
self.omemo_present = False
if self.omemo_present:
#from slixmpp.xmlstream.handler import CoroutineCallback
#from slixmpp.xmlstream.matcher import MatchXPath
#self.register_handler(CoroutineCallback(
# 'Messages',
# MatchXPath(f'{{{self.default_ns}}}message'),
# self.on_message # type: ignore[arg-type]
#))
from slixfeed.xmpp.encryption import XEP_0384Impl
from slixfeed.xmpp.encryption import XmppOmemo
import slixfeed.xmpp.encryption as slixfeed_xmpp_encryption
from slixmpp.plugins import register_plugin
register_plugin(XEP_0384Impl)
self.register_plugin('xep_0384', # OMEMO Encryption
module=XEP_0384Impl)
"""
if self.omemo_present:
try:
self.register_plugin(
@ -177,7 +195,7 @@ class XmppClient(slixmpp.ClientXMPP):
except slixmpp.plugins.base.PluginNotFound:
logger.error('Could not load xep_0454. Ensure you have '
'\'cryptography\' from extras_require installed.')
"""
# proxy_enabled = config.get_value('accounts', 'XMPP', 'proxy_enabled')
# if proxy_enabled == '1':
# values = config.get_value('accounts', 'XMPP', [
@ -870,6 +888,7 @@ class XmppClient(slixmpp.ClientXMPP):
# http://jabber.org/protocol/commands#actions
async def _handle_publish(self, iq, session):
jid = session['from']
jid_full = session['from'].full
function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}'
@ -911,6 +930,7 @@ class XmppClient(slixmpp.ClientXMPP):
return session
async def _handle_publish_action(self, payload, session):
jid = session['from']
jid_full = session['from'].full
function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}'
@ -1340,7 +1360,7 @@ class XmppClient(slixmpp.ClientXMPP):
form.add_field(label='Active',
ftype='text-single',
value=feeds_act)
entries = sqlite.get_number_of_items(db_file, 'entries_properties')
entries = str(sqlite.get_number_of_items(db_file, 'entries_properties'))
form.add_field(label='Items',
ftype='text-single',
value=entries)
@ -1350,44 +1370,35 @@ class XmppClient(slixmpp.ClientXMPP):
value=unread)
form.add_field(ftype='fixed',
label='Options')
key_archive = Config.get_setting_value(self, jid_bare, 'archive')
key_archive = str(key_archive)
key_archive = str(Config.get_setting_value(self, jid_bare, 'archive'))
form.add_field(label='Archive',
ftype='text-single',
value=key_archive)
key_enabled = Config.get_setting_value(self, jid_bare, 'enabled')
key_enabled = str(key_enabled)
key_enabled = str(Config.get_setting_value(self, jid_bare, 'enabled'))
form.add_field(label='Enabled',
ftype='text-single',
value=key_enabled)
key_interval = Config.get_setting_value(self, jid_bare, 'interval')
key_interval = str(key_interval)
key_interval = str(Config.get_setting_value(self, jid_bare, 'interval'))
form.add_field(label='Interval',
ftype='text-single',
value=key_interval)
key_length = Config.get_setting_value(self, jid_bare, 'length')
key_length = str(key_length)
key_length = str(Config.get_setting_value(self, jid_bare, 'length'))
form.add_field(label='Length',
ftype='text-single',
value=key_length)
key_media = Config.get_setting_value(self, jid_bare, 'media')
key_media = str(key_media)
key_media = str(Config.get_setting_value(self, jid_bare, 'media'))
form.add_field(label='Media',
ftype='text-single',
value=key_media)
key_old = Config.get_setting_value(self, jid_bare, 'old')
key_old = str(key_old)
key_old = str(Config.get_setting_value(self, jid_bare, 'old'))
form.add_field(label='Old',
ftype='text-single',
value=key_old)
key_quantum = Config.get_setting_value(self, jid_bare, 'quantum')
key_quantum = str(key_quantum)
key_quantum = str(Config.get_setting_value(self, jid_bare, 'quantum'))
form.add_field(label='Quantum',
ftype='text-single',
value=key_quantum)
update_interval = Config.get_setting_value(self, jid_bare, 'interval')
update_interval = str(update_interval)
update_interval = 60 * int(update_interval)
update_interval = 60 * Config.get_setting_value(self, jid_bare, 'interval')
last_update_time = sqlite.get_last_update_time(db_file)
if last_update_time:
last_update_time = float(last_update_time)
@ -2930,7 +2941,7 @@ class XmppClient(slixmpp.ClientXMPP):
chat_type = await XmppUtilities.get_chat_type(self, jid_bare)
if encrypted:
url_encrypted, omemo_encrypted = await XmppOmemo.encrypt(
self, JID(jid_bare), url)
self, JID(jid_bare), 'chat', url)
XmppMessage.send_omemo_oob(self, JID(jid_bare), url_encrypted, chat_type)
else:
XmppMessage.send_oob(self, jid_bare, url, chat_type)

View file

@ -23,18 +23,19 @@ TODO
"""
from omemo.exceptions import MissingBundleException
import json
from omemo.storage import Just, Maybe, Nothing, Storage
from omemo.types import DeviceInformation, JSONType
from slixfeed.log import Logger
from slixmpp import JID
from slixmpp.exceptions import IqTimeout, IqError
#from slixmpp.plugins import register_plugin
from slixmpp.stanza import Message
from slixmpp_omemo import MissingOwnKey, EncryptionPrepareException
from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession
from slixmpp_omemo import TrustLevel, XEP_0384
from typing import Any, Dict, FrozenSet, Literal, Optional, Union
logger = Logger(__name__)
# for task in main_task:
# task.cancel()
@ -46,7 +47,59 @@ logger = Logger(__name__)
class XmppOmemo:
async def decrypt(self, message: Message, allow_untrusted: bool = False):
async def decrypt(self, stanza: Message):
omemo_decrypted = None
mto = stanza["from"]
mtype = stanza["type"]
namespace = self['xep_0384'].is_encrypted(stanza)
if namespace is None:
omemo_decrypted = False
response = f"Unencrypted message or unsupported message encryption: {stanza['body']}"
else:
print(f'Message in namespace {namespace} received: {stanza}')
try:
response, device_information = await self['xep_0384'].decrypt_message(stanza)
print(f'Information about sender: {device_information}')
omemo_decrypted = True
except Exception as e: # pylint: disable=broad-exception-caught
response = f'Error {type(e).__name__}: {e}'
return response, omemo_decrypted
async def encrypt(
self,
mto: JID,
mtype: Literal['chat', 'normal'],
mbody: str
) -> None:
if isinstance(mbody, str):
reply = self.make_message(mto=mto, mtype=mtype)
reply['body'] = mbody
reply.set_to(mto)
reply.set_from(self.boundjid)
# It might be a good idea to strip everything except for the body from the stanza,
# since some things might break when echoed.
message, encryption_errors = await self['xep_0384'].encrypt_message(reply, mto)
if len(encryption_errors) > 0:
print(f'There were non-critical errors during encryption: {encryption_errors}')
# log.info(f'There were non-critical errors during encryption: {encryption_errors}')
# for namespace, message in messages.items():
# message['eme']['namespace'] = namespace
# message['eme']['name'] = self['xep_0380'].mechanisms[namespace]
return message, True
async def _decrypt(self, message: Message, allow_untrusted: bool = False):
jid = message['from']
try:
print('XmppOmemo.decrypt')
@ -124,7 +177,7 @@ class XmppOmemo:
return response, omemo_decrypted, retry
async def encrypt(self, jid: JID, message_body):
async def _encrypt(self, jid: JID, message_body):
print(jid)
print(message_body)
expect_problems = {} # type: Optional[Dict[JID, List[int]]]
@ -192,3 +245,95 @@ class XmppOmemo:
raise
return message_body, omemo_encrypted
class StorageImpl(Storage):
"""
Example storage implementation that stores all data in a single JSON file.
"""
JSON_FILE = "/home/admin/omemo-echo-client.json"
def __init__(self) -> None:
super().__init__()
self.__data: Dict[str, JSONType] = {}
try:
with open(self.JSON_FILE, encoding="utf8") as f:
self.__data = json.load(f)
except Exception: # pylint: disable=broad-exception-caught
pass
async def _load(self, key: str) -> Maybe[JSONType]:
if key in self.__data:
return Just(self.__data[key])
return Nothing()
async def _store(self, key: str, value: JSONType) -> None:
self.__data[key] = value
with open(self.JSON_FILE, "w", encoding="utf8") as f:
json.dump(self.__data, f)
async def _delete(self, key: str) -> None:
self.__data.pop(key, None)
with open(self.JSON_FILE, "w", encoding="utf8") as f:
json.dump(self.__data, f)
class XEP_0384Impl(XEP_0384): # pylint: disable=invalid-name
"""
Example implementation of the OMEMO plugin for Slixmpp.
"""
def __init__(self, *args: Any, **kwargs: Any) -> None: # pylint: disable=redefined-outer-name
super().__init__(*args, **kwargs)
# Just the type definition here
self.__storage: Storage
def plugin_init(self) -> None:
self.__storage = StorageImpl()
super().plugin_init()
@property
def storage(self) -> Storage:
return self.__storage
@property
def _btbv_enabled(self) -> bool:
return True
async def _devices_blindly_trusted(
self,
blindly_trusted: FrozenSet[DeviceInformation],
identifier: Optional[str]
) -> None:
log.info(f"[{identifier}] Devices trusted blindly: {blindly_trusted}")
async def _prompt_manual_trust(
self,
manually_trusted: FrozenSet[DeviceInformation],
identifier: Optional[str]
) -> None:
# Since BTBV is enabled and we don't do any manual trust adjustments in the example, this method
# should never be called. All devices should be automatically trusted blindly by BTBV.
# To show how a full implementation could look like, the following code will prompt for a trust
# decision using `input`:
session_mananger = await self.get_session_manager()
for device in manually_trusted:
while True:
answer = input(f"[{identifier}] Trust the following device? (yes/no) {device}")
if answer in { "yes", "no" }:
await session_mananger.set_trust(
device.bare_jid,
device.identity_key,
TrustLevel.TRUSTED.value if answer == "yes" else TrustLevel.DISTRUSTED.value
)
break
print("Please answer yes or no.")
#register_plugin(XEP_0384Impl)

View file

@ -41,14 +41,17 @@ class XmppMessage:
def send_omemo(self, jid: JID, chat_type, response_encrypted):
jid_from = str(self.boundjid) if self.is_component else None
message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type)
eme_ns = 'eu.siacs.conversations.axolotl'
# jid_from = str(self.boundjid) if self.is_component else None
# message = self.make_message(mto=jid, mfrom=jid_from, mtype=chat_type)
# eme_ns = 'eu.siacs.conversations.axolotl'
# message['eme']['namespace'] = eme_ns
# message['eme']['name'] = self['xep_0380'].mechanisms[eme_ns]
message['eme'] = {'namespace': eme_ns}
# message['eme'] = {'name': self['xep_0380'].mechanisms[eme_ns]}
message.append(response_encrypted)
# message['eme'] = {'namespace': eme_ns}
# message.append(response_encrypted)
for namespace, message in response_encrypted.items():
message['eme']['namespace'] = namespace
message['eme']['name'] = self['xep_0380'].mechanisms[namespace]
message.send()

View file

@ -66,7 +66,7 @@ class XmppUtilities:
access = True if XmppUtilities.is_moderator(self, room, alias) else False
if access: print('Access granted to groupchat moderator ' + alias)
else:
print('Access granted to chat ' + jid_bare)
print('Access granted to chat jid ' + jid_bare)
access = True
return access