From 51943b5b0c9c20bc5c58676027d844b7e5c530cb Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Sun, 7 Jul 2024 11:16:00 +0300 Subject: [PATCH] Add chat command for omemo; Fix NameError: name 'XmppChat' is not defined of function XmppOmemo.decrypt. --- slixfeed/fetch.py | 4 ++ slixfeed/utilities.py | 1 + slixfeed/version.py | 4 +- slixfeed/xmpp/chat.py | 102 +++++++++++++++++++++++++++--------- slixfeed/xmpp/client.py | 15 ++++-- slixfeed/xmpp/commands.py | 18 +++++-- slixfeed/xmpp/encryption.py | 13 +++-- 7 files changed, 119 insertions(+), 38 deletions(-) diff --git a/slixfeed/fetch.py b/slixfeed/fetch.py index 0a11583..10c6a41 100644 --- a/slixfeed/fetch.py +++ b/slixfeed/fetch.py @@ -36,6 +36,7 @@ NOTE """ +import aiofiles from aiohttp import ClientError, ClientSession, ClientTimeout from asyncio import TimeoutError # from asyncio.exceptions import IncompleteReadError @@ -135,6 +136,9 @@ class Http: ) as response: status = response.status if status in (200, 201): + f = await aiofiles.open(pathname, mode='wb') + await f.write(await response.read()) + await f.close() try: result = {'charset': response.charset, 'content_length': response.content_length, diff --git a/slixfeed/utilities.py b/slixfeed/utilities.py index 0504231..4934782 100644 --- a/slixfeed/utilities.py +++ b/slixfeed/utilities.py @@ -327,6 +327,7 @@ class Html: '//img[not(' 'contains(@src, "avatar") or ' 'contains(@src, "cc-by-sa") or ' + 'contains(@src, "data:image/") or ' 'contains(@src, "emoji") or ' 'contains(@src, "icon") or ' 'contains(@src, "logo") or ' diff --git a/slixfeed/version.py b/slixfeed/version.py index c4774b7..9074bf5 100644 --- a/slixfeed/version.py +++ b/slixfeed/version.py @@ -1,2 +1,2 @@ -__version__ = '0.1.87' -__version_info__ = (0, 1, 87) +__version__ = '0.1.88' +__version_info__ = (0, 1, 88) diff --git a/slixfeed/xmpp/chat.py b/slixfeed/xmpp/chat.py index 84728c3..3c6bd4d 100644 --- a/slixfeed/xmpp/chat.py +++ b/slixfeed/xmpp/chat.py @@ -46,6 +46,7 @@ from slixmpp import JID from slixmpp.stanza import Message import sys import time +from typing import Optional logger = Logger(__name__) @@ -150,8 +151,11 @@ class XmppChat: # await compose.message(self, jid_bare, message) if self['xep_0384'].is_encrypted(message): - command, omemo_decrypted = await XmppOmemo.decrypt( + 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) else: omemo_decrypted = None @@ -353,7 +357,12 @@ class XmppChat: # XmppMessage.send_oob_reply_message(message, url, response) if url: chat_type = await XmppUtilities.get_chat_type(self, jid_bare) - XmppMessage.send_oob(self, jid_bare, url, chat_type) + if encrypted: + url_encrypted, omemo_encrypted = await XmppOmemo.encrypt( + self, JID(jid_bare), url) + XmppMessage.send_omemo_oob(self, JID(jid_bare), url_encrypted, chat_type) + else: + XmppMessage.send_oob(self, jid_bare, url, chat_type) else: response = 'OPML file export has been failed.' del self.pending_tasks[jid_bare][pending_tasks_num] @@ -473,6 +482,13 @@ class XmppChat: self, jid_bare, db_file) case _ if command_lowercase.startswith('next'): num = command[5:] + if num: + try: + int(num) + except: + # NOTE Show this text as a status message + # response = 'Argument for command "next" must be an integer.' + num = None await XmppChatAction.send_unread_items(self, jid_bare, num) XmppStatusTask.restart_task(self, jid_bare) case _ if command_lowercase.startswith('node delete'): @@ -494,6 +510,12 @@ class XmppChat: case 'old': response = await XmppCommands.set_old_on( self, jid_bare, db_file) + case 'omemo off': + response = await XmppCommands.set_omemo_off( + self, jid_bare, db_file) + case 'omemo on': + response = await XmppCommands.set_omemo_on( + self, jid_bare, db_file) case 'options': response = 'Options:\n```' response += XmppCommands.print_options(self, jid_bare) @@ -572,9 +594,9 @@ class XmppChat: XmppPresence.send(self, jid_bare, status_message, status_type=status_type) await asyncio.sleep(5) - tasks = (FeedTask, XmppChatTask, XmppStatusTask) + callbacks = (FeedTask, XmppChatTask, XmppStatusTask) response = await XmppCommands.scheduler_start( - self, db_file, jid_bare, tasks) + self, db_file, jid_bare, callbacks) case 'stats': response = XmppCommands.print_statistics(db_file) case 'stop': @@ -639,7 +661,7 @@ class XmppChat: class XmppChatAction: - async def send_unread_items(self, jid_bare, num=None): + async def send_unread_items(self, jid_bare, num: Optional[int] = None): """ Send news items as messages. @@ -693,6 +715,17 @@ class XmppChatAction: media_url = enclosure else: media_url = await Html.extract_image_from_html(url) + try: + http_headers = await Http.fetch_headers(media_url) + if ('Content-Length' in http_headers): + if int(http_headers['Content-Length']) < 100000: + media_url = None + else: + media_url = None + except Exception as e: + print(media_url) + logger.error(e) + media_url = None if media_url and news_digest: if encrypt_omemo: @@ -707,34 +740,55 @@ class XmppChatAction: # Send media if encrypt_omemo: cache_dir = config.get_default_cache_directory() + # if not media_url.startswith('data:'): filename = media_url.split('/').pop().split('?')[0] + if not filename: breakpoint() pathname = os.path.join(cache_dir, filename) # http_response = await Http.response(media_url) - - # http_headers = await Http.fetch_headers(media_url) - # breakpoint() - # status = Http.fetch_media(media_url, pathname) - # if status: + http_headers = await Http.fetch_headers(media_url) + if ('Content-Length' in http_headers and + int(http_headers['Content-Length']) < 3000000): + status = await Http.fetch_media(media_url, pathname) + if status: + filesize = os.path.getsize(pathname) + media_url_new = await XmppUpload.start( + self, jid_bare, Path(pathname), filesize, encrypted=encrypted) + else: + media_url_new = media_url + else: + media_url_new = media_url + # else: + # import io, base64 + # from PIL import Image + # file_content = media_url.split(',').pop() + # file_extension = media_url.split(';')[0].split(':').pop().split('/').pop() + # img = Image.open(io.BytesIO(base64.decodebytes(bytes(file_content, "utf-8")))) + # filename = 'image.' + file_extension + # pathname = os.path.join(cache_dir, filename) + # img.save(pathname) # filesize = os.path.getsize(pathname) # media_url_new = await XmppUpload.start( # self, jid_bare, Path(pathname), filesize, encrypted=encrypted) - # else: - # media_url_new = media_url - - media_url_new = media_url media_url_new_encrypted, omemo_encrypted = await XmppOmemo.encrypt( self, jid, media_url_new) - - # NOTE Temporary line! - XmppMessage.send_omemo_oob(self, jid_bare, media_url_new_encrypted, chat_type) - - - # if media_url_new_encrypted and omemo_encrypted: - # XmppMessage.send_omemo_oob(self, jid, media_url_new_encrypted, chat_type) - # elif media_url: - # XmppMessage.send_oob(self, jid_bare, media_url_new_encrypted, chat_type) + if media_url_new_encrypted and omemo_encrypted: + # NOTE Tested against Gajim. + # FIXME This only works with aesgcm URLs, and it does + # not work with http URLs. + # url = saxutils.escape(url) + # AttributeError: 'Encrypted' object has no attribute 'replace' + XmppMessage.send_omemo_oob(self, jid, media_url_new_encrypted, chat_type) else: - XmppMessage.send_oob(self, jid_bare, media_url, chat_type) + # NOTE Tested against Gajim. + # FIXME Jandle data: URIs. + if not media_url.startswith('data:'): + http_headers = await Http.fetch_headers(media_url) + if ('Content-Length' in http_headers and + int(http_headers['Content-Length']) > 100000): + print(http_headers['Content-Length']) + XmppMessage.send_oob(self, jid_bare, media_url, chat_type) + else: + XmppMessage.send_oob(self, jid_bare, media_url, chat_type) media_url = None if news_digest: diff --git a/slixfeed/xmpp/client.py b/slixfeed/xmpp/client.py index 7563502..14a8dd3 100644 --- a/slixfeed/xmpp/client.py +++ b/slixfeed/xmpp/client.py @@ -43,7 +43,6 @@ import slixmpp # import xml.etree.ElementTree as ET # from lxml import etree -from omemo.exceptions import MissingBundleException import slixfeed.config as config from slixfeed.config import Config, Data import slixfeed.fetch as fetch @@ -55,11 +54,12 @@ from slixfeed.version import __version__ from slixfeed.xmpp.bookmark import XmppBookmark from slixfeed.xmpp.chat import XmppChat, XmppChatTask from slixfeed.xmpp.connect import XmppConnect, XmppConnectTask +from slixfeed.xmpp.encryption import XmppOmemo +from slixfeed.xmpp.groupchat import XmppGroupchat from slixfeed.xmpp.ipc import XmppIpcServer from slixfeed.xmpp.iq import XmppIQ from slixfeed.xmpp.message import XmppMessage from slixfeed.xmpp.muc import XmppMuc -from slixfeed.xmpp.groupchat import XmppGroupchat from slixfeed.xmpp.presence import XmppPresence import slixfeed.xmpp.profile as profile from slixfeed.xmpp.publish import XmppPubsub, XmppPubsubAction, XmppPubsubTask @@ -68,9 +68,9 @@ from slixfeed.xmpp.roster import XmppRoster from slixfeed.xmpp.status import XmppStatusTask from slixfeed.xmpp.upload import XmppUpload from slixfeed.xmpp.utilities import XmppUtilities +from slixmpp import JID import slixmpp_omemo -from slixmpp_omemo import PluginCouldNotLoad, MissingOwnKey, EncryptionPrepareException -from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession +from slixmpp_omemo import PluginCouldNotLoad import sys import time @@ -2906,7 +2906,12 @@ class XmppClient(slixmpp.ClientXMPP): if url: form['instructions'] = 'Export has been completed successfully!' chat_type = await XmppUtilities.get_chat_type(self, jid_bare) - XmppMessage.send_oob(self, jid_bare, url, chat_type) + if encrypted: + url_encrypted, omemo_encrypted = await XmppOmemo.encrypt( + self, JID(jid_bare), url) + XmppMessage.send_omemo_oob(self, JID(jid_bare), url_encrypted, chat_type) + else: + XmppMessage.send_oob(self, jid_bare, url, chat_type) url_field = form.add_field(var=ext.upper(), ftype='text-single', label=ext, diff --git a/slixfeed/xmpp/commands.py b/slixfeed/xmpp/commands.py index b577ec0..e2f2c56 100644 --- a/slixfeed/xmpp/commands.py +++ b/slixfeed/xmpp/commands.py @@ -648,6 +648,18 @@ class XmppCommands: return message + async def set_omemo_off(self, jid_bare, db_file): + await Config.set_setting_value(self.settings, jid_bare, db_file, 'omemo', 0) + message = 'OMEMO is disabled.' + return message + + + async def set_omemo_on(self, jid_bare, db_file): + await Config.set_setting_value(self.settings, jid_bare, db_file, 'omemo', 1) + message = 'OMEMO is enabled.' + return message + + def node_delete(self, info): info = info.split(' ') if len(info) > 2: @@ -958,10 +970,10 @@ class XmppCommands: # Tasks are classes which are passed to this function # On an occasion in which they would have returned, variable "tasks" might be called "callback" - async def scheduler_start(self, db_file, jid_bare, tasks): + async def scheduler_start(self, db_file, jid_bare, callbacks): await Config.set_setting_value(self.settings, jid_bare, db_file, 'enabled', 1) - for task in tasks: - task.restart_task(self, jid_bare) + for callback in callbacks: + callback.restart_task(self, jid_bare) message = 'Updates are enabled.' return message diff --git a/slixfeed/xmpp/encryption.py b/slixfeed/xmpp/encryption.py index 6c8d06a..bf59acf 100644 --- a/slixfeed/xmpp/encryption.py +++ b/slixfeed/xmpp/encryption.py @@ -23,13 +23,13 @@ TODO """ +from omemo.exceptions import MissingBundleException from slixfeed.log import Logger from slixmpp import JID from slixmpp.exceptions import IqTimeout, IqError from slixmpp.stanza import Message -from slixmpp_omemo import PluginCouldNotLoad, MissingOwnKey, EncryptionPrepareException +from slixmpp_omemo import MissingOwnKey, EncryptionPrepareException from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession -from omemo.exceptions import MissingBundleException logger = Logger(__name__) @@ -66,6 +66,7 @@ class XmppOmemo: response = ('Error: Your message has not been encrypted for ' 'Slixfeed (MissingOwnKey).') omemo_decrypted = False + retry = False logger.error(exn) except (NoAvailableSession,) as exn: # We received a message from that contained a session that we @@ -77,6 +78,7 @@ class XmppOmemo: response = ('Error: Your message has not been encrypted for ' 'Slixfeed (NoAvailableSession).') omemo_decrypted = False + retry = False logger.error(exn) except (UndecidedException, UntrustedException) as exn: # We received a message from an untrusted device. We can @@ -90,9 +92,10 @@ class XmppOmemo: response = (f'Error: Device "{exn.device}" is not present in the ' 'trusted devices of Slixfeed.') omemo_decrypted = False + retry = True logger.error(exn) # We resend, setting the `allow_untrusted` parameter to True. - await XmppChat.process_message(self, message, allow_untrusted=True) + # await XmppChat.process_message(self, message, allow_untrusted=True) except (EncryptionPrepareException,) as exn: # Slixmpp tried its best, but there were errors it couldn't # resolve. At this point you should have seen other exceptions @@ -100,15 +103,17 @@ class XmppOmemo: response = ('Error: Your message has not been encrypted for ' 'Slixfeed (EncryptionPrepareException).') omemo_decrypted = False + retry = False logger.error(exn) except (Exception,) as exn: response = ('Error: Your message has not been encrypted for ' 'Slixfeed (Unknown).') omemo_decrypted = False + retry = False logger.error(exn) raise - return response, omemo_decrypted + return response, omemo_decrypted, retry async def encrypt(self, jid: JID, message_body):