diff --git a/README.md b/README.md index f2dfc22..d1a5b7e 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,15 @@ Slixfeed is primarily designed for XMPP (aka Jabber), yet it is built to be exte ## Features -- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands, - **Ease** - Slixfeed automatically scans (i.e. crawls) for syndication feeds of given URL. +- **Encryption** - Messages are encrypted with the OMEMO standard. - **Export** - Download articles as ePUB, HTML, Markdown and PDF. - **Filtering** - Filter news items using lists of allow and deny. - **Multimedia** - Display audios pictures and videos inline. - **Privacy** - Redirect to alternative back-ends, such as Invidious, Librarian, Nitter, for increased privacy, productivity and security. - **Portable** - Export and import feeds with a standard OPML file. - **Simultaneous** - Slixfeed is designed to handle multiple contacts, including groupchats, Simultaneously. +- **Visual interface** - Interactive interface for XMPP using Ad-Hoc Commands, ## Preview diff --git a/slixfeed/assets/settings.toml b/slixfeed/assets/settings.toml index 0f32f53..f6072cf 100644 --- a/slixfeed/assets/settings.toml +++ b/slixfeed/assets/settings.toml @@ -11,6 +11,7 @@ interval = 300 # Update interval (Minimum value 10) length = 300 # Maximum length of summary (Value 0 to disable) media = 0 # Display media (audio, image, video) when available old = 0 # Mark entries of newly added entries as unread +omemo = 1 # Encrypt messages with OMEMO quantum = 3 # Amount of entries per update random = 0 # Pick random item from database diff --git a/slixfeed/config.py b/slixfeed/config.py index 51cad6d..f9d05c9 100644 --- a/slixfeed/config.py +++ b/slixfeed/config.py @@ -105,6 +105,71 @@ class ConfigJabberID: settings[jid_bare][key] = value +class Data: + + + def get_default_data_directory(): + """ + Determine the directory path where dbfile will be stored. + + * If $XDG_DATA_HOME is defined, use it; + * else if $HOME exists, use it; + * else if the platform is Windows, use %APPDATA%; + * else use the current directory. + + Returns + ------- + str + Path to database file. + + Note + ---- + This function was taken from project buku. + + See https://github.com/jarun/buku + + * Arun Prakash Jana (jarun) + * Dmitry Marakasov (AMDmi3) + """ + # data_home = xdg.BaseDirectory.xdg_data_home + data_home = os.environ.get('XDG_DATA_HOME') + if data_home is None: + if os.environ.get('HOME') is None: + if sys.platform == 'win32': + data_home = os.environ.get('APPDATA') + if data_home is None: + return os.path.abspath('.slixfeed/data') + else: + return os.path.abspath('.slixfeed/data') + else: + data_home = os.path.join( + os.environ.get('HOME'), '.local', 'share' + ) + return os.path.join(data_home, 'slixfeed') + + + def get_pathname_to_omemo_directory(): + """ + Get OMEMO directory. + + Parameters + ---------- + None + + Returns + ------- + object + Coroutine object. + """ + db_dir = get_default_data_directory() + if not os.path.isdir(db_dir): + os.mkdir(db_dir) + if not os.path.isdir(db_dir + "/omemo"): + os.mkdir(db_dir + "/omemo") + omemo_dir = os.path.join(db_dir, "omemo") + return omemo_dir + + def get_values(filename, key=None): config_dir = get_default_config_directory() if not os.path.isdir(config_dir): diff --git a/slixfeed/fetch.py b/slixfeed/fetch.py index 57aca50..0a11583 100644 --- a/slixfeed/fetch.py +++ b/slixfeed/fetch.py @@ -45,6 +45,8 @@ from asyncio import TimeoutError import requests import slixfeed.config as config from slixfeed.log import Logger +# import urllib.request +# from urllib.error import HTTPError logger = Logger(__name__) @@ -55,7 +57,6 @@ except: "Package magnet2torrent was not found.\n" "BitTorrent is disabled.") - # class Dat: # async def dat(): @@ -68,52 +69,151 @@ except: # class Gopher: # async def gopher(): -# class Http: -# async def http(): - # class Ipfs: # async def ipfs(): -def http_response(url): - """ - Download response headers. +class Http: - Parameters - ---------- - url : str - URL. - Returns - ------- - response: requests.models.Response - HTTP Header Response. + # def fetch_media(url, pathname): + # try: + # urllib.request.urlretrieve(url, pathname) + # status = 1 + # except HTTPError as e: + # logger.error(e) + # status = 0 + # return status - Result would contain these: - response.encoding - response.headers - response.history - response.reason - response.status_code - response.url - """ - user_agent = ( - config.get_value( - "settings", "Network", "user_agent") - ) or 'Slixfeed/0.1' - headers = { - "User-Agent": user_agent - } - try: - # Do not use HEAD request because it appears that too many sites would - # deny it. - # response = requests.head(url, headers=headers, allow_redirects=True) - response = requests.get(url, headers=headers, allow_redirects=True) - except Exception as e: - logger.warning('Error in HTTP response') - logger.error(e) - response = None - return response + + async def fetch_headers(url): + network_settings = config.get_values('settings.toml', 'network') + user_agent = (network_settings['user_agent'] or 'Slixfeed/0.1') + headers = {'User-Agent': user_agent} + proxy = (network_settings['http_proxy'] or None) + timeout = ClientTimeout(total=10) + async with ClientSession(headers=headers) as session: + async with session.get(url, proxy=proxy, + # proxy_auth=(proxy_username, proxy_password), + timeout=timeout + ) as response: + headers = response.headers + return headers + # print("Headers for URL:", url) + # for header_name, header_value in headers.items(): + # print(f"{header_name}: {header_value}") + + + # TODO Write file to disk. Consider aiofiles + async def fetch_media(url, pathname): + """ + Download media content of given URL. + + Parameters + ---------- + url : str + URL. + pathname : list + Pathname (including filename) to save content to. + + Returns + ------- + msg: list or str + Document or error message. + """ + network_settings = config.get_values('settings.toml', 'network') + user_agent = (network_settings['user_agent'] or 'Slixfeed/0.1') + headers = {'User-Agent': user_agent} + proxy = (network_settings['http_proxy'] or None) + timeout = ClientTimeout(total=10) + async with ClientSession(headers=headers) as session: + # async with ClientSession(trust_env=True) as session: + try: + async with session.get(url, proxy=proxy, + # proxy_auth=(proxy_username, proxy_password), + timeout=timeout + ) as response: + status = response.status + if status in (200, 201): + try: + result = {'charset': response.charset, + 'content_length': response.content_length, + 'content_type': response.content_type, + 'error': False, + 'message': None, + 'original_url': url, + 'status_code': status, + 'response_url': response.url} + except: + result = {'error': True, + 'message': 'Could not get document.', + 'original_url': url, + 'status_code': status, + 'response_url': response.url} + else: + result = {'error': True, + 'message': 'HTTP Error:' + str(status), + 'original_url': url, + 'status_code': status, + 'response_url': response.url} + except ClientError as e: + result = {'error': True, + 'message': 'Error:' + str(e) if e else 'ClientError', + 'original_url': url, + 'status_code': None} + except TimeoutError as e: + result = {'error': True, + 'message': 'Timeout:' + str(e) if e else 'TimeoutError', + 'original_url': url, + 'status_code': None} + except Exception as e: + logger.error(e) + result = {'error': True, + 'message': 'Error:' + str(e) if e else 'Error', + 'original_url': url, + 'status_code': None} + return result + + + def http_response(url): + """ + Download response headers. + + Parameters + ---------- + url : str + URL. + + Returns + ------- + response: requests.models.Response + HTTP Header Response. + + Result would contain these: + response.encoding + response.headers + response.history + response.reason + response.status_code + response.url + """ + user_agent = ( + config.get_value( + "settings", "Network", "user_agent") + ) or 'Slixfeed/0.1' + headers = { + "User-Agent": user_agent + } + try: + # Do not use HEAD request because it appears that too many sites would + # deny it. + # response = requests.head(url, headers=headers, allow_redirects=True) + response = requests.get(url, headers=headers, allow_redirects=True) + except Exception as e: + logger.warning('Error in HTTP response') + logger.error(e) + response = None + return response async def http(url): diff --git a/slixfeed/version.py b/slixfeed/version.py index a0c6369..c4774b7 100644 --- a/slixfeed/version.py +++ b/slixfeed/version.py @@ -1,2 +1,2 @@ -__version__ = '0.1.86' -__version_info__ = (0, 1, 86) +__version__ = '0.1.87' +__version_info__ = (0, 1, 87) diff --git a/slixfeed/xmpp/chat.py b/slixfeed/xmpp/chat.py index 51abb55..84728c3 100644 --- a/slixfeed/xmpp/chat.py +++ b/slixfeed/xmpp/chat.py @@ -24,19 +24,26 @@ TODO """ import asyncio +import os +from pathlib import Path from random import randrange # pending_tasks: Use a list and read the first index (i.e. index 0). import slixfeed.config as config from slixfeed.config import Config +import slixfeed.fetch as fetch +from slixfeed.fetch import Http from slixfeed.log import Logger import slixfeed.sqlite as sqlite from slixfeed.syndication import FeedTask from slixfeed.utilities import Documentation, Html, MD, Task, Url from slixfeed.xmpp.commands import XmppCommands +from slixfeed.xmpp.encryption import XmppOmemo from slixfeed.xmpp.message import XmppMessage from slixfeed.xmpp.presence import XmppPresence from slixfeed.xmpp.status import XmppStatusTask from slixfeed.xmpp.upload import XmppUpload from slixfeed.xmpp.utilities import XmppUtilities +from slixmpp import JID +from slixmpp.stanza import Message import sys import time @@ -55,7 +62,7 @@ logger = Logger(__name__) class XmppChat: - async def process_message(self, message): + async def process_message(self, message: Message, allow_untrusted: bool = False) -> None: """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually @@ -69,8 +76,10 @@ class XmppChat: for stanza objects and the Message stanza to see how it may be used. """ - if message['type'] in ('chat', 'groupchat', 'normal'): - jid_bare = message['from'].bare + message_from = message['from'] + message_type = message['type'] + if message_type in ('chat', 'groupchat', 'normal'): + jid_bare = message_from.bare command = ' '.join(message['body'].split()) command_time_start = time.time() @@ -80,14 +89,14 @@ class XmppChat: # FIXME Code repetition. See below. # TODO Check alias by nickname associated with conference - if message['type'] == 'groupchat': + if message_type == 'groupchat': if (message['muc']['nick'] == self.alias): return - jid_full = str(message['from']) + jid_full = message_from.full if not XmppUtilities.is_moderator(self, jid_bare, jid_full): return - if message['type'] == 'groupchat': + if message_type == 'groupchat': # nick = message['from'][message['from'].index('/')+1:] # nick = str(message['from']) # nick = nick[nick.index('/')+1:] @@ -109,7 +118,7 @@ class XmppChat: # if nick not in operator: # return # approved = False - jid_full = str(message['from']) + jid_full = message_from.full if not XmppUtilities.is_moderator(self, jid_bare, jid_full): return # if role == 'moderator': @@ -140,17 +149,23 @@ class XmppChat: # await compose.message(self, jid_bare, message) + if self['xep_0384'].is_encrypted(message): + command, omemo_decrypted = await XmppOmemo.decrypt( + self, message, allow_untrusted) + else: + omemo_decrypted = None + if message['type'] == 'groupchat': command = command[1:] command_lowercase = command.lower() - logger.debug([str(message['from']), ':', command]) + logger.debug([message_from.full, ':', command]) # Support private message via groupchat # See https://codeberg.org/poezio/slixmpp/issues/3506 if message['type'] == 'chat' and message.get_plugin('muc', check=True): - # jid_bare = message['from'].bare - jid_full = str(message['from']) + # jid_bare = message_from.bare + jid_full = message_from.full if (jid_bare == jid_full[:jid_full.index('/')]): # TODO Count and alert of MUC-PM attempts return @@ -217,7 +232,7 @@ class XmppChat: command = command[4:] url = command.split(' ')[0] title = ' '.join(command.split(' ')[1:]) - response = XmppCommands.feed_add( + response = await XmppCommands.feed_add( url, db_file, jid_bare, title) case _ if command_lowercase.startswith('allow +'): val = command[7:] @@ -327,15 +342,20 @@ class XmppChat: # self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message XmppPresence.send(self, jid_bare, status_message, status_type=status_type) - filename, response = XmppCommands.export_feeds( + pathname, response = XmppCommands.export_feeds( jid_bare, ext) - url = await XmppUpload.start(self, jid_bare, filename) + encrypt_omemo = Config.get_setting_value(self.settings, jid_bare, 'omemo') + encrypted = True if encrypt_omemo else False + url = await XmppUpload.start(self, jid_bare, Path(pathname), encrypted=encrypted) # response = ( # 'Feeds exported successfully to {}.\n{}' # ).format(ex, url) # XmppMessage.send_oob_reply_message(message, url, response) - chat_type = await XmppUtilities.get_chat_type(self, jid_bare) - XmppMessage.send_oob(self, jid_bare, url, chat_type) + if url: + chat_type = await XmppUtilities.get_chat_type(self, jid_bare) + 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] # del self.pending_tasks[jid_bare][self.pending_tasks_counter] XmppStatusTask.restart_task(self, jid_bare) @@ -378,18 +398,18 @@ class XmppChat: # del self.pending_tasks[jid_bare][self.pending_tasks_counter] XmppStatusTask.restart_task(self, jid_bare) case _ if command_lowercase.startswith('pubsub list'): - jid = command[12:] - response = 'List of nodes for {}:\n```\n'.format(jid) - response = await XmppCommands.pubsub_list(self, jid) + jid_full_pubsub = command[12:] + response = 'List of nodes for {}:\n```\n'.format(jid_full_pubsub) + response = await XmppCommands.pubsub_list(self, jid_full_pubsub) response += '```' case _ if command_lowercase.startswith('pubsub send'): if XmppUtilities.is_operator(self, jid_bare): info = command[12:] info = info.split(' ') - jid = info[0] + jid_full_pubsub = info[0] # num = int(info[1]) - if jid: - response = XmppCommands.pubsub_send(self, info, jid) + if jid_full_pubsub: + response = XmppCommands.pubsub_send(self, info, jid_full_pubsub) else: response = ('This action is restricted. ' 'Type: sending news to PubSub.') @@ -581,7 +601,16 @@ class XmppChat: command_time_finish = time.time() command_time_total = command_time_finish - command_time_start command_time_total = round(command_time_total, 3) - if response: XmppMessage.send_reply(self, message, response) + if response: + response_encrypted, omemo_encrypted = await XmppOmemo.encrypt( + self, message_from, response) + if omemo_encrypted and omemo_decrypted: + message_from = message['from'] + message_type = message['type'] + XmppMessage.send_omemo(self, message_from, message_type, response_encrypted) + # XmppMessage.send_omemo_reply(self, message, response_encrypted) + else: + XmppMessage.send_reply(self, message, response) if Config.get_setting_value(self.settings, jid_bare, 'finished'): response_finished = 'Finished. Total time: {}s'.format(command_time_total) XmppMessage.send_reply(self, message, response_finished) @@ -616,7 +645,7 @@ class XmppChatAction: Parameters ---------- - jid : str + jid_bare : str Jabber ID. num : str, optional Number. The default is None. @@ -624,6 +653,9 @@ class XmppChatAction: function_name = sys._getframe().f_code.co_name logger.debug('{}: jid: {} num: {}'.format(function_name, jid_bare, num)) db_file = config.get_pathname_to_database(jid_bare) + encrypt_omemo = Config.get_setting_value(self.settings, jid_bare, 'omemo') + encrypted = True if encrypt_omemo else False + jid = JID(jid_bare) show_media = Config.get_setting_value(self.settings, jid_bare, 'media') if not num: num = Config.get_setting_value(self.settings, jid_bare, 'quantum') @@ -631,7 +663,7 @@ class XmppChatAction: num = int(num) results = sqlite.get_unread_entries(db_file, num) news_digest = '' - media = None + media_url = None chat_type = await XmppUtilities.get_chat_type(self, jid_bare) for result in results: ix = result[0] @@ -658,20 +690,60 @@ class XmppChatAction: # elif enclosure: if show_media: if enclosure: - media = enclosure + media_url = enclosure else: - media = await Html.extract_image_from_html(url) - - if media and news_digest: - # Send textual message - XmppMessage.send(self, jid_bare, news_digest, chat_type) + media_url = await Html.extract_image_from_html(url) + + if media_url and news_digest: + if encrypt_omemo: + news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt( + self, jid, news_digest) + if encrypt_omemo and omemo_encrypted: + XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted) + else: + # Send textual message + XmppMessage.send(self, jid_bare, news_digest, chat_type) news_digest = '' # Send media - XmppMessage.send_oob(self, jid_bare, media, chat_type) - media = None - + if encrypt_omemo: + cache_dir = config.get_default_cache_directory() + filename = media_url.split('/').pop().split('?')[0] + 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: + # 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) + else: + XmppMessage.send_oob(self, jid_bare, media_url, chat_type) + media_url = None + if news_digest: - XmppMessage.send(self, jid_bare, news_digest, chat_type) + if encrypt_omemo: news_digest_encrypted, omemo_encrypted = await XmppOmemo.encrypt( + self, jid, news_digest) + if encrypt_omemo and omemo_encrypted: + XmppMessage.send_omemo(self, jid, chat_type, news_digest_encrypted) + else: + XmppMessage.send(self, jid_bare, news_digest, chat_type) # TODO Add while loop to assure delivery. # print(await current_time(), ">>> ACT send_message",jid) # NOTE Do we need "if statement"? See NOTE at is_muc. diff --git a/slixfeed/xmpp/client.py b/slixfeed/xmpp/client.py index 915d82e..7563502 100644 --- a/slixfeed/xmpp/client.py +++ b/slixfeed/xmpp/client.py @@ -31,8 +31,9 @@ NOTE import asyncio from datetime import datetime -import os from feedparser import parse +import os +from pathlib import Path import slixmpp # from slixmpp.plugins.xep_0363.http_upload import FileTooBig, HTTPError, UploadServiceNotFound # from slixmpp.plugins.xep_0402 import BookmarkStorage, Conference @@ -42,8 +43,9 @@ 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 +from slixfeed.config import Config, Data import slixfeed.fetch as fetch from slixfeed.log import Logger import slixfeed.sqlite as sqlite @@ -66,6 +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 +import slixmpp_omemo +from slixmpp_omemo import PluginCouldNotLoad, MissingOwnKey, EncryptionPrepareException +from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession import sys import time @@ -147,6 +152,21 @@ class XmppClient(slixmpp.ClientXMPP): self.register_plugin('xep_0363') # HTTP File Upload self.register_plugin('xep_0402') # PEP Native Bookmarks self.register_plugin('xep_0444') # Message Reactions + try: + self.register_plugin( + 'xep_0384', + { + 'data_dir': Data.get_pathname_to_omemo_directory(), + }, + module=slixmpp_omemo,) # OMEMO Encryption + except (PluginCouldNotLoad,): + logger.error('An error has occured when loading the OMEMO plugin.') + sys.exit(1) + try: + self.register_plugin('xep_0454') + 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': @@ -230,7 +250,7 @@ class XmppClient(slixmpp.ClientXMPP): # TODO Test async def on_groupchat_invite(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -268,7 +288,7 @@ class XmppClient(slixmpp.ClientXMPP): # NOTE Tested with Gajim and Psi async def on_groupchat_direct_invite(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -366,7 +386,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_disco_info(self, DiscoInfo): time_begin = time.time() - jid_full = str(DiscoInfo['from']) + jid_full = DiscoInfo['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -381,7 +401,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_message(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -427,7 +447,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_changed_status(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -452,7 +472,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_presence_subscribe(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -475,7 +495,7 @@ class XmppClient(slixmpp.ClientXMPP): def on_presence_subscribed(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -497,7 +517,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_presence_available(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -521,7 +541,7 @@ class XmppClient(slixmpp.ClientXMPP): def on_presence_unsubscribed(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -542,7 +562,7 @@ class XmppClient(slixmpp.ClientXMPP): def on_presence_unavailable(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -570,7 +590,7 @@ class XmppClient(slixmpp.ClientXMPP): # If bookmarks, remove groupchat JID into file def on_presence_error(self, presence): time_begin = time.time() - jid_full = str(presence['from']) + jid_full = presence['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -590,7 +610,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_chatstate_active(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -613,7 +633,7 @@ class XmppClient(slixmpp.ClientXMPP): async def on_chatstate_composing(self, message): # print('on_chatstate_composing START') time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -636,7 +656,7 @@ class XmppClient(slixmpp.ClientXMPP): def on_chatstate_gone(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -653,7 +673,7 @@ class XmppClient(slixmpp.ClientXMPP): def on_chatstate_inactive(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -670,7 +690,7 @@ class XmppClient(slixmpp.ClientXMPP): def on_chatstate_paused(self, message): time_begin = time.time() - jid_full = str(message['from']) + jid_full = message['from'].full function_name = sys._getframe().f_code.co_name message_log = '{}: jid_full: {}' logger.debug(message_log.format(function_name, jid_full)) @@ -832,7 +852,7 @@ class XmppClient(slixmpp.ClientXMPP): # http://jabber.org/protocol/commands#actions async def _handle_publish(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -873,7 +893,7 @@ class XmppClient(slixmpp.ClientXMPP): return session async def _handle_publish_action(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -981,7 +1001,7 @@ class XmppClient(slixmpp.ClientXMPP): return session async def _handle_publish_db_preview(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1089,7 +1109,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_publish_url_preview(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1281,7 +1301,7 @@ class XmppClient(slixmpp.ClientXMPP): return session async def _handle_profile(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1383,7 +1403,7 @@ class XmppClient(slixmpp.ClientXMPP): return session async def _handle_filters(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1456,7 +1476,7 @@ class XmppClient(slixmpp.ClientXMPP): session. Additional, custom data may be saved here to persist across handler callbacks. """ - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1492,7 +1512,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscription_add(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1555,7 +1575,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_recent(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1606,7 +1626,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_recent_result(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1663,7 +1683,7 @@ class XmppClient(slixmpp.ClientXMPP): # FIXME async def _handle_recent_select(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1736,7 +1756,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscription_new(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1940,7 +1960,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscription_toggle(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1965,7 +1985,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscription_del_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -1993,7 +2013,7 @@ class XmppClient(slixmpp.ClientXMPP): def _handle_cancel(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2006,7 +2026,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_discover(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2044,7 +2064,7 @@ class XmppClient(slixmpp.ClientXMPP): def _handle_discover_type(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2100,7 +2120,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_discover_category(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2130,7 +2150,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscriptions(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2195,7 +2215,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscriptions_result(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2289,7 +2309,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscription_tag(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2327,7 +2347,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscription_edit(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2424,7 +2444,7 @@ class XmppClient(slixmpp.ClientXMPP): # TODO Create a new form. Do not "recycle" the last form. async def _handle_subscription_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2490,7 +2510,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_advanced(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2532,7 +2552,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_advanced_result(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2679,7 +2699,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_about(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2704,7 +2724,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_about_result(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2768,7 +2788,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_motd(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2781,7 +2801,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_help(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2822,7 +2842,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_import_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -2863,12 +2883,11 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_export_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = self['xep_0004'].make_form('result', 'Done') - form['instructions'] = 'Export has been completed successfully!' # form['type'] = 'result' values = payload['values'] jid_bare = session['from'].bare @@ -2880,17 +2899,25 @@ class XmppClient(slixmpp.ClientXMPP): exts = values['filetype'] for ext in exts: filename = Feed.export_feeds(jid_bare, ext) - url = await XmppUpload.start(self, jid_bare, filename) - chat_type = await XmppUtilities.get_chat_type(self, jid_bare) - XmppMessage.send_oob(self, jid_bare, url, chat_type) - url_field = form.add_field(var=ext.upper(), - ftype='text-single', - label=ext, - value=url) - url_field['validate']['datatype'] = 'xs:anyURI' - session["has_next"] = False - session['next'] = None - session['payload'] = form + encrypt_omemo = Config.get_setting_value(self.settings, jid_bare, 'omemo') + encrypted = True if encrypt_omemo else False + url = await XmppUpload.start( + self, jid_bare, Path(filename), encrypted=encrypted) + 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) + url_field = form.add_field(var=ext.upper(), + ftype='text-single', + label=ext, + value=url) + url_field['validate']['datatype'] = 'xs:anyURI' + session["has_next"] = False + session['next'] = None + session['payload'] = form + else: + text_warn = 'OPML file export has been failed.' + session['notes'] = [['warn', text_warn]] return session @@ -2898,12 +2925,12 @@ class XmppClient(slixmpp.ClientXMPP): # TODO Attempt to look up for feeds of hostname of JID (i.e. scan # jabber.de for feeds for juliet@jabber.de) async def _handle_promoted(self, iq, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare - jid_full = str(session['from']) + jid_full = session['from'].full chat_type = await XmppUtilities.get_chat_type(self, jid_bare) if XmppUtilities.is_access(self, jid_bare, jid_full, chat_type): form = self['xep_0004'].make_form('form', 'Subscribe') @@ -2971,7 +2998,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_admin_action(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3096,7 +3123,7 @@ class XmppClient(slixmpp.ClientXMPP): def _handle_nodes(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3124,7 +3151,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_nodes_action(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3183,7 +3210,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_node_browse(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3214,7 +3241,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_item_view(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3250,7 +3277,7 @@ class XmppClient(slixmpp.ClientXMPP): # FIXME Undefined name 'jid_bare' async def _handle_node_edit(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3303,7 +3330,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_nodes_purge(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3321,7 +3348,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_nodes_delete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3339,7 +3366,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_pubsub_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3364,7 +3391,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_subscribers_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3412,7 +3439,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_contact_action(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3480,7 +3507,7 @@ class XmppClient(slixmpp.ClientXMPP): def _handle_contacts_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3504,7 +3531,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_bookmarks_edit(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3569,7 +3596,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_bookmarks_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3604,7 +3631,7 @@ class XmppClient(slixmpp.ClientXMPP): session. Additional, custom data may be saved here to persist across handler callbacks. """ - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) @@ -3723,7 +3750,7 @@ class XmppClient(slixmpp.ClientXMPP): async def _handle_settings_complete(self, payload, session): - jid_full = str(session['from']) + jid_full = session['from'].full function_name = sys._getframe().f_code.co_name logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) diff --git a/slixfeed/xmpp/commands.py b/slixfeed/xmpp/commands.py index d8f6faf..b577ec0 100644 --- a/slixfeed/xmpp/commands.py +++ b/slixfeed/xmpp/commands.py @@ -334,9 +334,9 @@ class XmppCommands: def export_feeds(jid_bare, ext): - filename = Feed.export_feeds(jid_bare, ext) + pathname = Feed.export_feeds(jid_bare, ext) message = 'Feeds successfuly exported to {}.'.format(ext) - return filename, message + return pathname, message def fetch_gemini(): diff --git a/slixfeed/xmpp/encryption.py b/slixfeed/xmpp/encryption.py new file mode 100644 index 0000000..6c8d06a --- /dev/null +++ b/slixfeed/xmpp/encryption.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + +TODO + +1) Deprecate "add" (see above) and make it interactive. + Slixfeed: Do you still want to add this URL to subscription list? + See: case _ if command_lowercase.startswith("add"): + +2) If subscription is inadequate (see XmppPresence.request), send a message that says so. + + elif not self.client_roster[jid]["to"]: + breakpoint() + message.reply("Share online status to activate bot.").send() + return + +3) Set timeout for moderator interaction. + If moderator interaction has been made, and moderator approves the bot, then + the bot will add the given groupchat to bookmarks; otherwise, the bot will + send a message that it was not approved and therefore leaves the groupchat. + +""" + +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 UndecidedException, UntrustedException, NoAvailableSession +from omemo.exceptions import MissingBundleException + + +logger = Logger(__name__) + + + # for task in main_task: + # task.cancel() + + # Deprecated in favour of event "presence_available" + # if not main_task: + # await select_file() + + +class XmppOmemo: + + + async def decrypt(self, message: Message, allow_untrusted: bool = False): + jid = message['from'] + try: + message_omemo_encrypted = message['omemo_encrypted'] + message_body = await self['xep_0384'].decrypt_message( + message_omemo_encrypted, jid, allow_untrusted) + # decrypt_message returns Optional[str]. It is possible to get + # body-less OMEMO message (see KeyTransportMessages), currently + # used for example to send heartbeats to other devices. + if message_body is not None: + response = message_body.decode('utf8') + omemo_decrypted = True + else: + response = omemo_decrypted = None + except (MissingOwnKey,) as exn: + # The message is missing our own key, it was not encrypted for + # us, and we can't decrypt it. + response = ('Error: Your message has not been encrypted for ' + 'Slixfeed (MissingOwnKey).') + omemo_decrypted = False + logger.error(exn) + except (NoAvailableSession,) as exn: + # We received a message from that contained a session that we + # don't know about (deleted session storage, etc.). We can't + # decrypt the message, and it's going to be lost. + # Here, as we need to initiate a new encrypted session, it is + # best if we send an encrypted message directly. XXX: Is it + # where we talk about self-healing messages? + response = ('Error: Your message has not been encrypted for ' + 'Slixfeed (NoAvailableSession).') + omemo_decrypted = False + logger.error(exn) + except (UndecidedException, UntrustedException) as exn: + # We received a message from an untrusted device. We can + # choose to decrypt the message nonetheless, with the + # `allow_untrusted` flag on the `decrypt_message` call, which + # we will do here. This is only possible for decryption, + # encryption will require us to decide if we trust the device + # or not. Clients _should_ indicate that the message was not + # trusted, or in undecided state, if they decide to decrypt it + # anyway. + response = (f'Error: Device "{exn.device}" is not present in the ' + 'trusted devices of Slixfeed.') + omemo_decrypted = False + logger.error(exn) + # We resend, setting the `allow_untrusted` parameter to 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 + # and given a chance to resolve them already. + response = ('Error: Your message has not been encrypted for ' + 'Slixfeed (EncryptionPrepareException).') + omemo_decrypted = False + logger.error(exn) + except (Exception,) as exn: + response = ('Error: Your message has not been encrypted for ' + 'Slixfeed (Unknown).') + omemo_decrypted = False + logger.error(exn) + raise + + return response, omemo_decrypted + + + async def encrypt(self, jid: JID, message_body): + expect_problems = {} # type: Optional[Dict[JID, List[int]]] + while True: + try: + # `encrypt_message` excepts the plaintext to be sent, a list of + # bare JIDs to encrypt to, and optionally a dict of problems to + # expect per bare JID. + # + # Note that this function returns an `` object, + # and not a full Message stanza. This combined with the + # `recipients` parameter that requires for a list of JIDs, + # allows you to encrypt for 1:1 as well as groupchats (MUC). + # + # `expect_problems`: See EncryptionPrepareException handling. + recipients = [jid] + message_body = await self['xep_0384'].encrypt_message( + message_body, recipients, expect_problems) + omemo_encrypted = True + break + except UndecidedException as exn: + # The library prevents us from sending a message to an + # untrusted/undecided barejid, so we need to make a decision here. + # This is where you prompt your user to ask what to do. In + # this bot we will automatically trust undecided recipients. + await self['xep_0384'].trust(exn.bare_jid, exn.device, exn.ik) + omemo_encrypted = False + # TODO: catch NoEligibleDevicesException + except EncryptionPrepareException as exn: + # This exception is being raised when the library has tried + # all it could and doesn't know what to do anymore. It + # contains a list of exceptions that the user must resolve, or + # explicitely ignore via `expect_problems`. + # TODO: We might need to bail out here if errors are the same? + for error in exn.errors: + if isinstance(error, MissingBundleException): + # We choose to ignore MissingBundleException. It seems + # to be somewhat accepted that it's better not to + # encrypt for a device if it has problems and encrypt + # for the rest, rather than error out. The "faulty" + # device won't be able to decrypt and should display a + # generic message. The receiving end-user at this + # point can bring up the issue if it happens. + message_body = (f'Could not find keys for device ' + '"{error.device}"' + f' of recipient "{error.bare_jid}". ' + 'Skipping.') + omemo_encrypted = False + jid = JID(error.bare_jid) + device_list = expect_problems.setdefault(jid, []) + device_list.append(error.device) + except (IqError, IqTimeout) as exn: + message_body = ('An error occured while fetching information ' + 'on a recipient.\n%r' % exn) + omemo_encrypted = False + except Exception as exn: + message_body = ('An error occured while attempting to encrypt' + '.\n%r' % exn) + omemo_encrypted = False + raise + + return message_body, omemo_encrypted diff --git a/slixfeed/xmpp/groupchat.py b/slixfeed/xmpp/groupchat.py index 180c862..13c1f73 100644 --- a/slixfeed/xmpp/groupchat.py +++ b/slixfeed/xmpp/groupchat.py @@ -34,21 +34,31 @@ class XmppGroupchat: 'bookmark {}'.format(bookmark['name'])) alias = bookmark["nick"] muc_jid = bookmark["jid"] - Message.printer('Joining to MUC {} ...'.format(muc_jid)) + # Message.printer('Joining to MUC {} ...'.format(muc_jid)) + print('Joining to MUC {} ...'.format(muc_jid)) result = await XmppMuc.join(self, muc_jid, alias) - if result == 'ban': - await XmppBookmark.remove(self, muc_jid) - logger.warning('{} is banned from {}'.format(self.alias, muc_jid)) - logger.warning('Groupchat {} has been removed from bookmarks' - .format(muc_jid)) - else: - logger.info('Autojoin groupchat\n' - 'Name : {}\n' - 'JID : {}\n' - 'Alias : {}\n' - .format(bookmark["name"], - bookmark["jid"], - bookmark["nick"])) + match result: + case 'ban': + await XmppBookmark.remove(self, muc_jid) + logger.warning('{} is banned from {}'.format(self.alias, muc_jid)) + logger.warning('Groupchat {} has been removed from bookmarks' + .format(muc_jid)) + case 'error': + logger.warning('An error has occured while attempting ' + 'to join to groupchat {}' + .format(muc_jid)) + case 'timeout': + logger.warning('Timeout has reached while attempting ' + 'to join to groupchat {}' + .format(muc_jid)) + case _: + logger.info('Autojoin groupchat\n' + 'Name : {}\n' + 'JID : {}\n' + 'Alias : {}\n' + .format(bookmark["name"], + bookmark["jid"], + bookmark["nick"])) elif not bookmark["jid"]: logger.error('JID is missing for bookmark {}' .format(bookmark['name'])) diff --git a/slixfeed/xmpp/message.py b/slixfeed/xmpp/message.py index b5ae0b0..9773ee0 100644 --- a/slixfeed/xmpp/message.py +++ b/slixfeed/xmpp/message.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- from slixfeed.log import Logger +from slixmpp import JID import xml.sax.saxutils as saxutils logger = Logger(__name__) @@ -39,6 +40,43 @@ class XmppMessage: mnick=self.alias) + 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' + # 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.send() + + + def send_omemo_oob(self, jid: JID, url_encrypted, chat_type, aesgcm=False): + jid_from = str(self.boundjid) if self.is_component else None + # if not aesgcm: url_encrypted = saxutils.escape(url_encrypted) + 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['oob']['url'] = url_encrypted + message.append(url_encrypted) + message.send() + + + # FIXME Solve this function + def send_omemo_reply(self, message, response_encrypted): + 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.reply(message['body']).send() + + # NOTE We might want to add more characters # def escape_to_xml(raw_string): # escape_map = { diff --git a/slixfeed/xmpp/status.py b/slixfeed/xmpp/status.py index d4d0561..497d736 100644 --- a/slixfeed/xmpp/status.py +++ b/slixfeed/xmpp/status.py @@ -50,7 +50,7 @@ class XmppStatus: status_text = '📬️ There are {} news items'.format(str(unread)) else: # print('status no news for ' + jid_bare) - status_mode = 'available' + status_mode = 'away' status_text = '📭️ No news' else: # print('status disabled for ' + jid_bare) @@ -91,4 +91,4 @@ class XmppStatusTask: self.task_manager[jid_bare]['status'].cancel() else: logger.debug('No task "status" for JID {}' - .format(jid_bare)) \ No newline at end of file + .format(jid_bare)) diff --git a/slixfeed/xmpp/upload.py b/slixfeed/xmpp/upload.py index bb5638d..11f166f 100644 --- a/slixfeed/xmpp/upload.py +++ b/slixfeed/xmpp/upload.py @@ -6,47 +6,51 @@ Based on http_upload.py example from project slixmpp https://codeberg.org/poezio/slixmpp/src/branch/master/examples/http_upload.py """ +from pathlib import Path from slixfeed.log import Logger +from slixmpp import JID from slixmpp.exceptions import IqTimeout, IqError from slixmpp.plugins.xep_0363.http_upload import HTTPError +import sys +from typing import Optional logger = Logger(__name__) # import sys class XmppUpload: - async def start(self, jid, filename, domain=None): + async def start(self, jid, filename: Path, size: Optional[int] = None, + encrypted: bool = False, domain: Optional[JID] = None): logger.info(['Uploading file %s...', filename]) try: upload_file = self['xep_0363'].upload_file - # if self.encrypted and not self['xep_0454']: - # print( - # 'The xep_0454 module isn\'t available. ' - # 'Ensure you have \'cryptography\' ' - # 'from extras_require installed.', - # file=sys.stderr, - # ) - # return - # elif self.encrypted: - # upload_file = self['xep_0454'].upload_file - try: - url = await upload_file( - filename, domain, timeout=10, + if encrypted and not self['xep_0454']: + print( + 'The xep_0454 module isn\'t available. ' + 'Ensure you have \'cryptography\' ' + 'from extras_require installed.', + file=sys.stderr, ) + url = None + elif encrypted: + upload_file = self['xep_0454'].upload_file + try: + url = await upload_file(filename, size, domain, timeout=10,) logger.info('Upload successful!') logger.info(['Sending file to %s', jid]) except HTTPError: - url = ('Error: It appears that this server does not support ' - 'HTTP File Upload.') + url = None logger.error('It appears that this server does not support ' 'HTTP File Upload.') # raise HTTPError( # "This server doesn't appear to support HTTP File Upload" # ) except IqError as e: + url = None logger.error('Could not send message') logger.error(e) except IqTimeout as e: + url = None # raise TimeoutError('Could not send message in time') logger.error('Could not send message in time') logger.error(e)