From 6dd1089c37a5a0c5b18e262ba78c66bbc9524f38 Mon Sep 17 00:00:00 2001 From: Schimon Jehudah Date: Mon, 4 Mar 2024 10:16:49 +0000 Subject: [PATCH] Measure function elapsed time period and display a warning for a period of over a second. --- slixfeed/action.py | 142 ++- slixfeed/log.py | 10 +- slixfeed/sqlite.py | 209 ++-- slixfeed/version.py | 4 +- slixfeed/xmpp/client.py | 260 +++-- slixfeed/xmpp/command.py | 2091 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 2454 insertions(+), 262 deletions(-) create mode 100644 slixfeed/xmpp/command.py diff --git a/slixfeed/action.py b/slixfeed/action.py index 0f2aca9..46b3b62 100644 --- a/slixfeed/action.py +++ b/slixfeed/action.py @@ -87,8 +87,7 @@ except ImportError: async def export_feeds(self, jid, jid_file, ext): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid)) + logger.debug('{}: jid: {}: jid_file: {}: ext: {}'.format(function_name, jid, jid_file, ext)) cache_dir = config.get_default_cache_directory() if not os.path.isdir(cache_dir): os.mkdir(cache_dir) @@ -119,8 +118,7 @@ async def xmpp_send_status(self, jid): Jabber ID. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid)) + logger.debug('{}: jid: {}'.format(function_name, jid)) status_text = '📜️ Slixfeed RSS News Bot' jid_file = jid.replace('/', '_') db_file = config.get_pathname_to_database(jid_file) @@ -173,8 +171,7 @@ async def xmpp_send_update(self, jid, num=None): Number. The default is None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid)) + logger.debug('{}: jid: {} num: {}'.format(function_name, jid, num)) jid_file = jid.replace('/', '_') db_file = config.get_pathname_to_database(jid_file) enabled = config.get_setting_value(db_file, 'enabled') @@ -274,8 +271,7 @@ async def xmpp_send_update(self, jid, num=None): def manual(filename, section=None, command=None): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, filename)) + logger.debug('{}: filename: {}'.format(function_name, filename)) config_dir = config.get_default_config_directory() with open(config_dir + '/' + filename, mode="rb") as commands: cmds = tomllib.load(commands) @@ -326,8 +322,7 @@ def log_to_markdown(timestamp, filename, jid, message): """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, filename)) + logger.debug('{}: timestamp: {} filename: {} jid: {} message: {}'.format(function_name, timestamp, filename, jid, message)) with open(filename + '.md', 'a') as file: # entry = "{} {}:\n{}\n\n".format(timestamp, jid, message) entry = ( @@ -355,7 +350,7 @@ def is_feed_json(document): True or False. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated'.format(function_name)) + logger.debug('{}'.format(function_name)) value = False try: feed = json.loads(document) @@ -391,7 +386,7 @@ def is_feed(feed): True or False. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated'.format(function_name)) + logger.debug('{}'.format(function_name)) value = False # message = None if not feed.entries: @@ -427,7 +422,8 @@ def is_feed(feed): def list_unread_entries(result, feed_title, jid_file): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated'.format(function_name)) + logger.debug('{}: feed_title: {} jid_file: {}' + .format(function_name, feed_title, jid_file)) # TODO Add filtering # TODO Do this when entry is added to list and mark it as read # DONE! @@ -488,7 +484,7 @@ def list_unread_entries(result, feed_title, jid_file): def list_search_results(query, results): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for query {}.' + logger.debug('{}: query: {}' .format(function_name, query)) message = ("Search results for '{}':\n\n```" .format(query)) @@ -504,8 +500,8 @@ def list_search_results(query, results): def list_feeds_by_query(db_file, query): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for query {}.' - .format(function_name, query)) + logger.debug('{}: db_file: {} query: {}' + .format(function_name, db_file, query)) results = sqlite.search_feeds(db_file, query) message = ('Feeds containing "{}":\n\n```' .format(query)) @@ -536,7 +532,7 @@ async def list_statistics(db_file): Statistics as message. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) entries_unread = await sqlite.get_number_of_entries_unread(db_file) entries = await sqlite.get_number_of_items(db_file, 'entries') @@ -582,8 +578,8 @@ async def list_statistics(db_file): # FIXME Replace counter by len def list_last_entries(results, num): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}: num: {}' + .format(function_name, num)) message = "Recent {} titles:\n\n```".format(num) for result in results: message += ("\n{}\n{}\n" @@ -597,8 +593,8 @@ def list_last_entries(results, num): def pick_a_feed(lang=None): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}: lang: {}' + .format(function_name, lang)) config_dir = config.get_default_config_directory() with open(config_dir + '/' + 'feeds.toml', mode="rb") as feeds: urls = tomllib.load(feeds) @@ -609,8 +605,7 @@ def pick_a_feed(lang=None): def list_feeds(results): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) message = "\nList of subscriptions:\n\n```\n" for result in results: message += ("Name : {}\n" @@ -634,8 +629,7 @@ def list_feeds(results): async def list_bookmarks(self): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) conferences = await XmppBookmark.get(self) message = '\nList of groupchats:\n\n```\n' for conference in conferences: @@ -650,8 +644,8 @@ async def list_bookmarks(self): def export_to_markdown(jid, filename, results): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid)) + logger.debug('{}: jid: {} filename: {}' + .format(function_name, jid, filename)) with open(filename, 'w') as file: file.write('# Subscriptions for {}\n'.format(jid)) file.write('## Set of feeds exported with Slixfeed\n') @@ -665,8 +659,8 @@ def export_to_markdown(jid, filename, results): # TODO Consider adding element jid as a pointer of import def export_to_opml(jid, filename, results): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid)) + logger.debug('{} jid: {} filename: {}' + .format(function_name, jid, filename)) root = ET.Element("opml") root.set("version", "1.0") head = ET.SubElement(root, "head") @@ -691,8 +685,8 @@ def export_to_opml(jid, filename, results): async def import_opml(db_file, url): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, db_file)) + logger.debug('{}: db_file: {} url: {}' + .format(function_name, db_file, url)) result = await fetch.http(url) if not result['error']: document = result['content'] @@ -716,8 +710,8 @@ async def import_opml(db_file, url): async def add_feed(db_file, url): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, db_file)) + logger.debug('{}: db_file: {} url: {}' + .format(function_name, db_file, url)) while True: exist = await sqlite.get_feed_id_and_name(db_file, url) if not exist: @@ -884,8 +878,8 @@ async def scan_json(db_file, url): URL. The default is None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, db_file)) + logger.debug('{}: db_file: {} url: {}' + .format(function_name, db_file, url)) if isinstance(url, tuple): url = url[0] result = await fetch.http(url) if not result['error']: @@ -924,6 +918,8 @@ async def scan_json(db_file, url): return # new_entry = 0 for entry in entries: + logger.debug('{}: entry: {}' + .format(function_name, entry["title"])) if "date_published" in entry.keys(): date = entry["date_published"] date = dt.rfc2822_to_iso8601(date) @@ -993,10 +989,10 @@ async def scan_json(db_file, url): media_link = trim_url(media_link) break except: - logger.info('KeyError: "url"\n' - 'Missing "url" attribute for {}' - .format(url)) - logger.info('Continue scanning for next ' + logger.error('KeyError: "url"\n' + 'Missing "url" attribute for {}' + .format(url)) + logger.error('Continue scanning for next ' 'potential enclosure of {}' .format(link)) entry = { @@ -1021,8 +1017,8 @@ async def scan_json(db_file, url): async def view_feed(url): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for URL {}.' - .format(function_name, url)) + logger.debug('{}: url: {}' + .format(function_name, url)) while True: result = await fetch.http(url) if not result['error']: @@ -1086,8 +1082,8 @@ async def view_feed(url): async def view_entry(url, num): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for URL {}.' - .format(function_name, url)) + logger.debug('{}: url: {} num: {}' + .format(function_name, url, num)) while True: result = await fetch.http(url) if not result['error']: @@ -1166,10 +1162,14 @@ async def scan(db_file, url): URL. The default is None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) if isinstance(url, tuple): url = url[0] result = await fetch.http(url) + feed_id = await sqlite.get_feed_id(db_file, url) + feed_id = feed_id[0] + status_code = result['status_code'] + await sqlite.update_feed_status(db_file, feed_id, status_code) if not result['error']: document = result['content'] status = result['status_code'] @@ -1212,6 +1212,7 @@ async def scan(db_file, url): return # new_entry = 0 for entry in entries: + logger.debug('{}: entry: {}'.format(function_name, entry.title)) if entry.has_key("published"): date = entry.published date = dt.rfc2822_to_iso8601(date) @@ -1276,10 +1277,10 @@ async def scan(db_file, url): media_link = trim_url(media_link) break except: - logger.info('KeyError: "href"\n' - 'Missing "href" attribute for {}' - .format(url)) - logger.info('Continue scanning for next ' + logger.error('KeyError: "href"\n' + 'Missing "href" attribute for {}' + .format(url)) + logger.error('Continue scanning for next ' 'potential enclosure of {}' .format(link)) entry = { @@ -1305,8 +1306,7 @@ async def scan(db_file, url): def get_document_title(data): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) try: document = Document(data) title = document.short_title() @@ -1318,8 +1318,7 @@ def get_document_title(data): def get_document_content(data): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) try: document = Document(data) content = document.summary() @@ -1331,8 +1330,7 @@ def get_document_content(data): def get_document_content_as_text(data): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) try: document = Document(data) content = document.summary() @@ -1345,8 +1343,8 @@ def get_document_content_as_text(data): def generate_document(data, url, ext, filename, readability=False): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' - .format(function_name, filename, url)) + logger.debug('{}: url: {} ext: {} filename: {}' + .format(function_name, url, ext, filename)) error = None if readability: try: @@ -1398,8 +1396,8 @@ def generate_document(data, url, ext, filename, readability=False): async def extract_image_from_feed(db_file, feed_id, url): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' - .format(function_name, db_file, url)) + logger.debug('{}: db_file: {} feed_id: {} url: {}' + .format(function_name, db_file, feed_id, url)) feed_url = sqlite.get_feed_url(db_file, feed_id) feed_url = feed_url[0] result = await fetch.http(feed_url) @@ -1421,8 +1419,7 @@ async def extract_image_from_feed(db_file, feed_id, url): async def extract_image_from_html(url): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for URL {}.' - .format(function_name, url)) + logger.debug('{}: url: {}'.format(function_name, url)) result = await fetch.http(url) if not result['error']: data = result['content'] @@ -1453,8 +1450,7 @@ async def extract_image_from_html(url): def generate_epub(text, pathname): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, pathname)) + logger.debug('{}: text: {} pathname: {}'.format(function_name, text, pathname)) ## create an empty eBook pathname_list = pathname.split("/") filename = pathname_list.pop() @@ -1483,16 +1479,14 @@ def generate_epub(text, pathname): def generate_html(text, filename): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, filename)) + logger.debug('{}: text: {} filename: {}'.format(function_name, text, filename)) with open(filename, 'w') as file: file.write(text) def generate_markdown(text, filename): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, filename)) + logger.debug('{}: text: {} filename: {}'.format(function_name, text, filename)) h2m = html2text.HTML2Text() # Convert HTML to Markdown markdown = h2m.handle(text) @@ -1502,8 +1496,7 @@ def generate_markdown(text, filename): def generate_pdf(text, filename): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, filename)) + logger.debug('{}: text: {} filename: {}'.format(function_name, text, filename)) try: pdfkit.from_string(text, filename) except IOError as error: @@ -1514,16 +1507,14 @@ def generate_pdf(text, filename): def generate_txt(text, filename): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, filename)) + logger.debug('{}: text: {} filename: {}'.format(function_name, text, filename)) text = remove_html_tags(text) with open(filename, 'w') as file: file.write(text) def remove_html_tags(data): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) data = BeautifulSoup(data, "lxml").text data = data.replace("\n\n", "\n") return data @@ -1531,8 +1522,7 @@ def remove_html_tags(data): # TODO Add support for eDonkey, Gnutella, Soulseek async def get_magnet(link): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for URL {}.' - .format(function_name, link)) + logger.debug('{}: {}'.format(function_name, link)) parted_link = urlsplit(link) queries = parse_qs(parted_link.query) query_xt = queries["xt"][0] @@ -1574,7 +1564,7 @@ async def remove_nonexistent_entries(db_file, url, feed): Parsed feed document. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] @@ -1683,7 +1673,7 @@ async def remove_nonexistent_entries_json(db_file, url, feed): Parsed feed document. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {}: url: {}' .format(function_name, db_file, url)) feed_id = await sqlite.get_feed_id(db_file, url) feed_id = feed_id[0] diff --git a/slixfeed/log.py b/slixfeed/log.py index 6b3ffe2..12d201b 100644 --- a/slixfeed/log.py +++ b/slixfeed/log.py @@ -17,12 +17,12 @@ class Logger: def __init__(self, name): self.logger = logging.getLogger(name) - self.logger.setLevel(logging.DEBUG) + self.logger.setLevel(logging.WARNING) ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) + ch.setLevel(logging.WARNING) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(name)s: %(message)s') ch.setFormatter(formatter) self.logger.addHandler(ch) @@ -41,3 +41,7 @@ class Logger: def warning(self, message): self.logger.warning(message) + + # def check_difference(function_name, difference): + # if difference > 1: + # Logger.warning(message) diff --git a/slixfeed/sqlite.py b/slixfeed/sqlite.py index 1cb3ecc..3e5e9f3 100644 --- a/slixfeed/sqlite.py +++ b/slixfeed/sqlite.py @@ -54,16 +54,21 @@ def create_connection(db_file): conn : object Connection object or None. """ + time_begin = time.time() function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' - .format(function_name, db_file)) + message_log = '{}' + logger.debug(message_log.format(function_name)) conn = None try: conn = connect(db_file) conn.execute("PRAGMA foreign_keys = ON") - return conn + # return conn except Error as e: print(e) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) return conn @@ -77,7 +82,7 @@ def create_tables(db_file): Path to database file. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: archive_table_sql = ( @@ -306,7 +311,7 @@ def get_cursor(db_file): Cursor. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) if db_file in CURSORS: return CURSORS[db_file] @@ -329,12 +334,14 @@ async def import_feeds(db_file, feeds): Set of feeds (Title and URL). """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) async with DBLOCK: with create_connection(db_file) as conn: cur = conn.cursor() for feed in feeds: + logger.debug('{}: feed: {}' + .format(function_name, feed)) url = feed[0] title = feed[1] sql = ( @@ -366,7 +373,7 @@ async def add_metadata(db_file): Path to database file. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) async with DBLOCK: with create_connection(db_file) as conn: @@ -395,7 +402,7 @@ def insert_feed_status(cur, feed_id): Cursor object. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for feed_id {}.' + logger.debug('{}: feed_id: {}' .format(function_name, feed_id)) sql = ( """ @@ -425,7 +432,7 @@ def insert_feed_properties(cur, feed_id): Cursor object. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for feed_id {}.' + logger.debug('{}: feed_id: {}' .format(function_name, feed_id)) sql = ( """ @@ -473,7 +480,7 @@ async def insert_feed(db_file, url, title=None, entries=None, version=None, Date feed was last updated. The default is None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) async with DBLOCK: with create_connection(db_file) as conn: @@ -560,7 +567,7 @@ async def insert_feed_(db_file, url, title=None, entries=None, version=None, Date feed was last updated. The default is None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) async with DBLOCK: with create_connection(db_file) as conn: @@ -606,7 +613,7 @@ async def remove_feed_by_url(db_file, url): URL of feed. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) with create_connection(db_file) as conn: async with DBLOCK: @@ -634,7 +641,7 @@ async def remove_feed_by_index(db_file, ix): Index of feed. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: async with DBLOCK: @@ -682,7 +689,7 @@ def get_feeds_by_tag_id(db_file, tag_id): List of tags. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Tag ID {}.' + logger.debug('{}: db_file: {} tag_id: {}' .format(function_name, db_file, tag_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -717,7 +724,7 @@ def get_tags_by_feed_id(db_file, feed_id): List of tags. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{}: db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -749,7 +756,7 @@ async def set_feed_id_and_tag_id(db_file, feed_id, tag_id): Tag ID """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Tag ID {}.' + logger.debug('{}: db_file: {} feed_id: {} tag_id: {}' .format(function_name, db_file, feed_id, tag_id)) async with DBLOCK: with create_connection(db_file) as conn: @@ -787,7 +794,7 @@ def get_tag_id(db_file, tag_name): Tag ID. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Tag {}.' + logger.debug('{}: db_file: {} tag_name: {}' .format(function_name, db_file, tag_name)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -820,7 +827,7 @@ def get_tag_name(db_file, ix): Tag name. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -853,7 +860,7 @@ def is_tag_id_associated(db_file, tag_id): Tag ID. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Tag ID {}.' + logger.debug('{}: db_file: {} tag_id: {}' .format(function_name, db_file, tag_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -873,7 +880,7 @@ def is_tag_id_associated(db_file, tag_id): async def delete_tag_by_index(db_file, ix): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) async with DBLOCK: with create_connection(db_file) as conn: @@ -910,7 +917,7 @@ def is_tag_id_of_feed_id(db_file, tag_id, feed_id): Tag ID. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Tag ID {}.' + logger.debug('{}: db_file: {} tag_id: {} feed_id: {}' .format(function_name, db_file, feed_id, tag_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -931,7 +938,7 @@ def is_tag_id_of_feed_id(db_file, tag_id, feed_id): async def delete_feed_id_tag_id(db_file, feed_id, tag_id): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Tag ID {}.' + logger.debug('{}: db_file: {} tag_id: {} feed_id: {}' .format(function_name, db_file, feed_id, tag_id)) async with DBLOCK: with create_connection(db_file) as conn: @@ -962,7 +969,7 @@ async def set_new_tag(db_file, tag): Tag """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Tag {}.' + logger.debug('{}: db_file: {} tag: {}' .format(function_name, db_file, tag)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1001,7 +1008,7 @@ async def get_feed_id_and_name(db_file, url): List of ID and Name of feed. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1034,7 +1041,7 @@ async def get_number_of_items(db_file, table): Number of rows. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Table {}.' + logger.debug('{}: db_file: {} table: {}' .format(function_name, db_file, table)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1063,7 +1070,7 @@ async def get_number_of_feeds_active(db_file): Number of rows. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1093,7 +1100,7 @@ async def get_number_of_entries_unread(db_file): Number of rows. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1132,7 +1139,7 @@ async def get_unread_entries(db_file, num): News items. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Num {}.' + logger.debug('{}: db_file: {} num: {}' .format(function_name, db_file, num)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1170,7 +1177,7 @@ def get_feed_id_by_entry_index(db_file, ix): Feed index. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1203,7 +1210,7 @@ async def get_feed_id(db_file, url): Feed index. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and URL {}.' + logger.debug('{}: db_file: {} url: {}' .format(function_name, db_file, url)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1231,8 +1238,8 @@ async def mark_entry_as_read(cur, ix): Index of entry. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' - .format(function_name, db_file, ix)) + logger.debug('{}: ix: {}' + .format(function_name, ix)) sql = ( """ UPDATE entries @@ -1256,7 +1263,7 @@ def get_number_of_unread_entries_by_feed(db_file, feed_id): Feed Id. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{}: db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1284,7 +1291,7 @@ async def mark_feed_as_read(db_file, feed_id): Feed Id. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{}: db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1312,7 +1319,7 @@ async def delete_entry_by_id(db_file, ix): Index. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1340,7 +1347,7 @@ async def archive_entry(db_file, ix): Index. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1379,7 +1386,7 @@ async def archive_entry(db_file, ix): def get_feed_title(db_file, ix): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1409,7 +1416,7 @@ async def set_feed_title(db_file, feed_id, name): New name. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Name {}.' + logger.debug('{}: db_file: {} feed_id: {} name: {}' .format(function_name, db_file, feed_id, name)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1430,7 +1437,7 @@ async def set_feed_title(db_file, feed_id, name): def get_entry_title(db_file, ix): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1448,7 +1455,7 @@ def get_entry_title(db_file, ix): def get_entry_url(db_file, ix): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1466,7 +1473,7 @@ def get_entry_url(db_file, ix): def get_feed_url(db_file, ix): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1484,7 +1491,7 @@ def get_feed_url(db_file, ix): async def mark_as_read(db_file, ix): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Index {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file, ix)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1509,7 +1516,7 @@ async def mark_all_as_read(db_file): Path to database file. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {} ix: {}' .format(function_name, db_file)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1542,7 +1549,7 @@ async def delete_archived_entry(cur, ix): Index of entry. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for Index {}.' + logger.debug('{}: ix: {}' .format(function_name, ix)) sql = ( """ @@ -1565,8 +1572,7 @@ async def update_statistics(cur): Cursor object. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.' - .format(function_name)) + logger.debug('{}'.format(function_name)) stat_dict = {} stat_dict["feeds"] = await get_number_of_items(cur, 'feeds') stat_dict["entries"] = await get_number_of_items(cur, 'entries') @@ -1619,7 +1625,7 @@ async def set_enabled_status(db_file, feed_id, status): 0 or 1. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Status {}.' + logger.debug('{}: db_file: {} feed_id: {} status: {}' .format(function_name, db_file, feed_id, status)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1672,8 +1678,8 @@ async def add_entry(db_file, title, link, entry_id, feed_id, date, 0 or 1. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' - .format(function_name, db_file, feed_id)) + logger.debug('{}: db_file: {} title: {} link: {} entry_id: {} feed_id: {} date: {} read_status: {}' + .format(function_name, db_file, title, link, entry_id, feed_id, date, read_status)) async with DBLOCK: with create_connection(db_file) as conn: cur = conn.cursor() @@ -1724,12 +1730,14 @@ async def add_entries_and_update_timestamp(db_file, feed_id, new_entries): Set of entries as dict. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{}: db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) async with DBLOCK: with create_connection(db_file) as conn: cur = conn.cursor() for entry in new_entries: + logger.debug('{}: db_file: {} feed_id: {} entry: {}' + .format(function_name, db_file, feed_id, entry["title"])) sql = ( """ INSERT @@ -1776,7 +1784,7 @@ async def set_date(db_file, feed_id): Feed Id. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{}: db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1810,7 +1818,7 @@ async def update_feed_status(db_file, feed_id, status_code): Status ID or message. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Status Code {}.' + print('{}: db_file: {} feed_id: {} status_code: {}' .format(function_name, db_file, feed_id, status_code)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1844,7 +1852,7 @@ async def update_feed_validity(db_file, feed_id, valid): 0 or 1. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Feed ID {} and Validity {}.' + logger.debug('{}: db_file: {} feed_id: {} valid: {}' .format(function_name, db_file, feed_id, valid)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1879,8 +1887,8 @@ async def update_feed_properties(db_file, feed_id, entries, updated): Date feed was last updated. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' - .format(function_name, db_file, feed_id)) + logger.debug('{}: db_file: {} feed_id: {} entries: {} updated: {}' + .format(function_name, db_file, feed_id, entries, updated)) async with DBLOCK: with create_connection(db_file) as conn: cur = conn.cursor() @@ -1910,7 +1918,7 @@ async def maintain_archive(db_file, limit): Number of maximum entries to store. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Limit {}.' + logger.debug('{}: db_file: {} limit: {}' .format(function_name, db_file, limit)) async with DBLOCK: with create_connection(db_file) as conn: @@ -1955,9 +1963,7 @@ async def maintain_archive(db_file, limit): # table archive are not marked as read. async def get_entries_of_feed(db_file, feed_id): """ - Remove entries that don't exist in a given parsed feed. - Check the entries returned from feed and delete read non - existing entries, otherwise move to table archive, if unread. + Get entries of given feed. Parameters ---------- @@ -1967,7 +1973,7 @@ async def get_entries_of_feed(db_file, feed_id): Feed Id. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{} db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -1976,6 +1982,7 @@ async def get_entries_of_feed(db_file, feed_id): SELECT id, title, link, entry_id, timestamp, read FROM entries WHERE feed_id = ? + ORDER BY timestamp DESC """ ) par = (feed_id,) @@ -2024,7 +2031,7 @@ async def get_feeds_url(db_file): URLs of active feeds. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{} db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2055,7 +2062,7 @@ def get_feeds_by_enabled_state(db_file, enabled_state): List of URLs. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and State {}.' + logger.debug('{}: db_file: {} enabled_state: {}' .format(function_name, db_file, enabled_state)) if enabled_state: enabled_state = 1 @@ -2091,7 +2098,7 @@ async def get_active_feeds_url(db_file): URLs of active feeds. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2122,7 +2129,7 @@ def get_tags(db_file): List of tags. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2151,7 +2158,7 @@ async def get_feeds(db_file): URLs of feeds. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) # TODO # 1) Select id from table feeds @@ -2187,7 +2194,7 @@ async def last_entries(db_file, num): List of recent N entries as message. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Num {}.' + logger.debug('{}: db_file: {} num: {}' .format(function_name, db_file, num)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2232,7 +2239,7 @@ def search_feeds(db_file, query): Feeds of specified keywords as message. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Query {}.' + logger.debug('{}: db_file: {} query: {}' .format(function_name, db_file, query)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2267,7 +2274,7 @@ async def search_entries(db_file, query): Entries of specified keywords as message. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Query {}.' + logger.debug('{}: db_file: {} query: {}' .format(function_name, db_file, query)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2339,7 +2346,7 @@ def check_entry_exist(db_file, feed_id, entry_id=None, title=None, link=None, True or None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Feed ID {}.' + logger.debug('{}: db_file: {} feed_id: {}' .format(function_name, db_file, feed_id)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2416,18 +2423,18 @@ async def set_setting_value(db_file, key_value): Numeric value. """ key = key_value[0] - value = key_value[1] + val = key_value[1] function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Key {} and Value {}.' - .format(function_name, db_file, key, value)) + logger.debug('{}: db_file: {} key: {} val: {}' + .format(function_name, db_file, key, val)) - if not value: + if not val: match key: case 'interval': - value = 90 + val = 90 case 'quantum': - value = 3 + val = 3 async with DBLOCK: with create_connection(db_file) as conn: @@ -2438,12 +2445,12 @@ async def set_setting_value(db_file, key_value): INTO settings( key, value) VALUES( - :key, :value) + :key, :val) """ ) par = { "key": key, - "value": value + "val": val } cur.execute(sql, par) @@ -2472,11 +2479,11 @@ async def update_setting_value(db_file, key_value): # key = "enabled" # val = 0 key = key_value[0] - value = key_value[1] + val = key_value[1] function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Key {} and Value {}.' - .format(function_name, db_file, key, value)) + logger.debug('{}: db_file: {} key: {} val: {}' + .format(function_name, db_file, key, val)) async with DBLOCK: with create_connection(db_file) as conn: @@ -2484,13 +2491,13 @@ async def update_setting_value(db_file, key_value): sql = ( """ UPDATE settings - SET value = :value + SET value = :val WHERE key = :key """ ) par = { "key": key, - "value": value + "val": val } cur.execute(sql, par) # except: @@ -2501,7 +2508,7 @@ async def update_setting_value(db_file, key_value): async def delete_filter(db_file, key): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Key {}.' + logger.debug('{}: db_file: {} key: {}' .format(function_name, db_file, key)) async with DBLOCK: with create_connection(db_file) as conn: @@ -2519,7 +2526,7 @@ async def delete_filter(db_file, key): async def delete_setting(db_file, key): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Key {}.' + logger.debug('{}: db_file: {} key: {}' .format(function_name, db_file, key)) async with DBLOCK: with create_connection(db_file) as conn: @@ -2537,7 +2544,7 @@ async def delete_setting(db_file, key): async def delete_settings(db_file): function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) async with DBLOCK: with create_connection(db_file) as conn: @@ -2569,7 +2576,7 @@ def get_setting_value(db_file, key): Numeric value. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Key {}.' + logger.debug('{}: db_file: {} key: {}' .format(function_name, db_file, key)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2602,7 +2609,7 @@ def is_setting_key(db_file, key): Key. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Key {}.' + logger.debug('{}: db_file: {} key: {}' .format(function_name, db_file, key)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2636,7 +2643,7 @@ async def set_filter_value(db_file, key_value): val = key_value[1] function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Key {} and Value {}.' + logger.debug('{}: db_file: {} key: {} val: {}' .format(function_name, db_file, key, val)) async with DBLOCK: @@ -2648,12 +2655,12 @@ async def set_filter_value(db_file, key_value): INTO filters( key, value) VALUES( - :key, :value) + :key, :val) """ ) par = { "key": key, - "value": val + "val": val } cur.execute(sql, par) @@ -2685,7 +2692,7 @@ async def update_filter_value(db_file, key_value): val = key_value[1] function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Key {} and Value {}.' + logger.debug('{}: db_file: {} key: {} val: {}' .format(function_name, db_file, key, val)) async with DBLOCK: @@ -2722,7 +2729,7 @@ def is_filter_key(db_file, key): Key. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Key {}.' + logger.debug('{}: db_file: {} key: {}' .format(function_name, db_file, key)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2755,7 +2762,7 @@ def get_filter_value(db_file, key): List of strings. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}, Key {}.' + logger.debug('{}: db_file: {} key: {}' .format(function_name, db_file, key)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2785,7 +2792,7 @@ async def set_last_update_time(db_file): None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2820,7 +2827,7 @@ async def get_last_update_time(db_file): Time. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2855,7 +2862,7 @@ async def update_last_update_time(db_file): None. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2892,7 +2899,7 @@ def get_categories(db_file): List of categories. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2922,7 +2929,7 @@ def get_locales(db_file): List of locales. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -2952,7 +2959,7 @@ def get_nations(db_file): List of nations. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -3009,7 +3016,7 @@ def get_titles_tags_urls(db_file): List of titles and urls. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {}.' + logger.debug('{}: db_file: {}' .format(function_name, db_file)) with create_connection(db_file) as conn: cur = conn.cursor() @@ -3040,7 +3047,7 @@ def get_titles_tags_urls_by_category(db_file, category): List of titles and urls. """ function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for filename {} and Category {}.' + logger.debug('{}: db_file: {} category: {}' .format(function_name, db_file, category)) with create_connection(db_file) as conn: cur = conn.cursor() diff --git a/slixfeed/version.py b/slixfeed/version.py index fadb6f2..ee29bcd 100644 --- a/slixfeed/version.py +++ b/slixfeed/version.py @@ -1,2 +1,2 @@ -__version__ = '0.1.26' -__version_info__ = (0, 1, 26) +__version__ = '0.1.27' +__version_info__ = (0, 1, 27) diff --git a/slixfeed/xmpp/client.py b/slixfeed/xmpp/client.py index af7d4fc..11eab1d 100644 --- a/slixfeed/xmpp/client.py +++ b/slixfeed/xmpp/client.py @@ -56,6 +56,7 @@ from slixfeed.xmpp.roster import XmppRoster from slixfeed.xmpp.presence import XmppPresence from slixfeed.xmpp.utility import get_chat_type import sys +import time import asyncio from datetime import datetime @@ -181,10 +182,11 @@ class Slixfeed(slixmpp.ClientXMPP): # TODO Test async def on_groupchat_invite(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) inviter = message['from'].bare muc_jid = message['groupchat_invite']['jid'] await XmppBookmark.add(self, muc_jid) @@ -195,14 +197,19 @@ class Slixfeed(slixmpp.ClientXMPP): 'You may always reach me via xmpp:{}?message' .format(self.alias, self.boundjid.bare)) XmppMessage.send(self, muc_jid, message_body, 'groupchat') + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) # NOTE Tested with Gajim and Psi async def on_groupchat_direct_invite(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) inviter = message['from'].bare muc_jid = message['groupchat_invite']['jid'] await XmppBookmark.add(self, muc_jid) @@ -213,6 +220,10 @@ class Slixfeed(slixmpp.ClientXMPP): 'You may always reach me via xmpp:{}?message' .format(self.alias, self.boundjid.bare)) XmppMessage.send(self, muc_jid, message_body, 'groupchat') + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_session_end(self, event): @@ -221,14 +232,23 @@ class Slixfeed(slixmpp.ClientXMPP): async def on_connection_failed(self, event): - logger.info('Event connection_failed has been initiated.') + time_begin = time.time() + function_name = sys._getframe().f_code.co_name + message_log = '{}' + logger.debug(message_log.format(function_name)) message = 'Connection has failed. Reason: {}'.format(event) XmppConnect.recover(self, message) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_session_start(self, event): + time_begin = time.time() function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.'.format(function_name)) + message_log = '{}' + logger.debug(message_log.format(function_name)) # self.send_presence() profile.set_identity(self, 'client') # XmppCommand.adhoc_commands(self) @@ -244,32 +264,48 @@ class Slixfeed(slixmpp.ClientXMPP): jid_op = config.get_value('accounts', 'XMPP', 'operator') status_message = 'Slixfeed version {}'.format(__version__) XmppPresence.send(self, jid_op, status_message) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) def on_session_resumed(self, event): + time_begin = time.time() function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated.'.format(function_name)) + message_log = '{}' + logger.debug(message_log.format(function_name)) # self.send_presence() profile.set_identity(self, 'client') self['xep_0115'].update_caps() XmppGroupchat.autojoin(self) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_disco_info(self, DiscoInfo): + time_begin = time.time() jid_full = str(DiscoInfo['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) # self.service_reactions() # self.send_presence(pto=jid) await self['xep_0115'].update_caps(jid=jid_full) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_message(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = message['from'].bare if jid_bare == self.boundjid.bare: status_type = 'dnd' @@ -296,13 +332,18 @@ class Slixfeed(slixmpp.ClientXMPP): # chat_type = message["type"] # message_body = message["body"] # message_reply = message.reply + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_changed_status(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) # await task.check_readiness(self, presence) jid_bare = presence['from'].bare if jid_bare in self.boundjid.bare: @@ -312,13 +353,18 @@ class Slixfeed(slixmpp.ClientXMPP): task.clean_tasks_xmpp(self, jid_bare, key_list) key_list = ['status', 'check'] await task.start_tasks_xmpp(self, jid_bare, key_list) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_presence_subscribe(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = presence['from'].bare if not self.client_roster[jid_bare]['to']: # XmppPresence.subscription(self, jid, 'subscribe') @@ -330,13 +376,18 @@ class Slixfeed(slixmpp.ClientXMPP): message_body = 'Share online status to receive updates.' XmppMessage.send_headline(self, jid_bare, message_subject, message_body, 'chat') + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) def on_presence_subscribed(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) message_subject = 'RSS News Bot' message_body = ('Greetings! I am {}, the news anchor.\n' 'My job is to bring you the latest ' @@ -347,13 +398,18 @@ class Slixfeed(slixmpp.ClientXMPP): # XmppPresence.subscription(self, jid, 'subscribed') XmppMessage.send_headline(self, jid_bare, message_subject, message_body, 'chat') + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_presence_available(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) # TODO Add function to check whether task is already running or not # await task.start_tasks(self, presence) # NOTE Already done inside the start-task function @@ -366,13 +422,19 @@ class Slixfeed(slixmpp.ClientXMPP): await task.start_tasks_xmpp(self, jid_bare) self.add_event_handler("presence_unavailable", self.on_presence_unavailable) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{}: jid_full: {} (time: {})' + .format(function_name, jid_full, + difference)) def on_presence_unsubscribed(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = presence['from'].bare message_body = 'You have been unsubscribed.' # status_message = '🖋️ Subscribe to receive updates' @@ -382,16 +444,19 @@ class Slixfeed(slixmpp.ClientXMPP): # XmppPresence.send(self, jid, status_message, # presence_type='unsubscribed') XmppRoster.remove(self, jid_bare) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) def on_presence_unavailable(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = presence['from'].bare - logger.info('Event presence_unavailable has been initiated for JID {}.' - .format(jid_bare)) # await task.stop_tasks(self, jid) task.clean_tasks_xmpp(self, jid_bare) @@ -402,6 +467,10 @@ class Slixfeed(slixmpp.ClientXMPP): presence_type='unavailable') self.del_event_handler("presence_unavailable", self.on_presence_unavailable) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) # TODO @@ -410,12 +479,17 @@ class Slixfeed(slixmpp.ClientXMPP): # If roster, remove contact JID into file # If bookmarks, remove groupchat JID into file def on_presence_error(self, presence): + time_begin = time.time() jid_full = str(presence['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = presence["from"].bare task.clean_tasks_xmpp(self, jid_bare) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) def on_reactions(self, message): @@ -424,10 +498,11 @@ class Slixfeed(slixmpp.ClientXMPP): async def on_chatstate_active(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = message['from'].bare if jid_bare in self.boundjid.bare: return @@ -439,13 +514,18 @@ class Slixfeed(slixmpp.ClientXMPP): await asyncio.sleep(5) key_list = ['status'] await task.start_tasks_xmpp(self, jid_bare, key_list) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_chatstate_composing(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) if message['type'] in ('chat', 'normal'): jid_bare = message['from'].bare # NOTE: Required for Cheogram @@ -456,13 +536,18 @@ class Slixfeed(slixmpp.ClientXMPP): status_message = ('💡 Send "help" for manual, or "info" for ' 'information.') XmppPresence.send(self, jid_bare, status_message) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_chatstate_gone(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = message['from'].bare if jid_bare in self.boundjid.bare: return @@ -470,13 +555,18 @@ class Slixfeed(slixmpp.ClientXMPP): # task.clean_tasks_xmpp(self, jid, ['status']) key_list = ['status'] await task.start_tasks_xmpp(self, jid_bare, key_list) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_chatstate_inactive(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = message['from'].bare if jid_bare in self.boundjid.bare: return @@ -484,13 +574,18 @@ class Slixfeed(slixmpp.ClientXMPP): # task.clean_tasks_xmpp(self, jid, ['status']) key_list = ['status'] await task.start_tasks_xmpp(self, jid_bare, key_list) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) async def on_chatstate_paused(self, message): + time_begin = time.time() jid_full = str(message['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' - .format(function_name, jid_full)) + message_log = '{}: jid_full: {}' + logger.debug(message_log.format(function_name, jid_full)) jid_bare = message['from'].bare if jid_bare in self.boundjid.bare: return @@ -498,6 +593,10 @@ class Slixfeed(slixmpp.ClientXMPP): # task.clean_tasks_xmpp(self, jid, ['status']) key_list = ['status'] await task.start_tasks_xmpp(self, jid_bare, key_list) + time_end = time.time() + difference = time_end - time_begin + if difference > 1: logger.warning('{} (time: {})'.format(function_name, + difference)) @@ -546,7 +645,8 @@ class Slixfeed(slixmpp.ClientXMPP): def adhoc_commands(self): - logger.info('Function adhoc_commands has been initiated.') + function_name = sys._getframe().f_code.co_name + logger.debug('{}'.format(function_name)) # self["xep_0050"].add_command( # node="updates_enable", # name="Enable/Disable News Updates", @@ -598,7 +698,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_profile(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -693,7 +793,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_filters(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare chat_type = await get_chat_type(self, jid_bare) @@ -746,7 +846,7 @@ class Slixfeed(slixmpp.ClientXMPP): """ jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) # Text is not displayed; only labels form = payload @@ -784,7 +884,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_add(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare chat_type = await get_chat_type(self, jid_bare) @@ -825,7 +925,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_recent(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = self['xep_0004'].make_form('form', 'Updates') form['instructions'] = 'Browse and read news' @@ -850,7 +950,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_recent_result(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -858,9 +958,9 @@ class Slixfeed(slixmpp.ClientXMPP): num = 100 match payload['values']['action']: case 'all': - results = await sqlite.get_entries(db_file, num) + results = await sqlite.get_entries(db_file, num) # FIXME case 'rejected': - results = await sqlite.get_entries_rejected(db_file, num) + results = await sqlite.get_entries_rejected(db_file, num) # FIXME case 'unread': results = await sqlite.get_unread_entries(db_file, num) if results: @@ -889,7 +989,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_recent_select(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) values = payload['values'] ix = values['update'] @@ -956,7 +1056,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_recent_action(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) ext = payload['values']['filetype'] url = payload['values']['url'][0] @@ -1010,7 +1110,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_new(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -1137,7 +1237,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_enable(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = payload jid_bare = session['from'].bare @@ -1171,7 +1271,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_disable(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = payload jid_bare = session['from'].bare @@ -1205,7 +1305,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_del_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = payload jid_bare = session['from'].bare @@ -1239,7 +1339,7 @@ class Slixfeed(slixmpp.ClientXMPP): def _handle_cancel(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) text_note = ('Operation has been cancelled.' '\n' @@ -1252,7 +1352,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_discover(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare chat_type = await get_chat_type(self, jid_bare) @@ -1284,7 +1384,7 @@ class Slixfeed(slixmpp.ClientXMPP): def _handle_discover_type(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) values = payload['values'] search_type = values['search_type'] @@ -1340,7 +1440,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_discover_category(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) values = payload['values'] category = values['category'] @@ -1370,7 +1470,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscriptions(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare chat_type = await get_chat_type(self, jid_bare) @@ -1403,7 +1503,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscriptions_result(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -1507,7 +1607,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_tag(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -1541,7 +1641,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare form = self['xep_0004'].make_form('form', 'Subscription editor') @@ -1588,7 +1688,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_editor(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -1684,7 +1784,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare values = payload['values'] @@ -1749,7 +1849,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscription_selector(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare form = self['xep_0004'].make_form('form', 'Add Subscription') @@ -1791,7 +1891,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_advanced(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare chat_type = await get_chat_type(self, jid_bare) @@ -1826,7 +1926,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_advanced_result(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) match payload['values']['option']: case 'activity': @@ -1931,7 +2031,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_about(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = self['xep_0004'].make_form('form', 'About') form['instructions'] = 'Information about Slixfeed and related projects' @@ -1960,7 +2060,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_about_result(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) match payload['values']['option']: case 'about': @@ -2043,7 +2143,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_motd(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) # TODO add functionality to attach image. # Here you can add groupchat rules,post schedule, tasks or @@ -2056,7 +2156,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_help(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) import tomllib @@ -2098,7 +2198,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_import_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = payload url = payload['values']['url'] @@ -2135,7 +2235,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_export_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) form = payload jid_bare = session['from'].bare @@ -2168,7 +2268,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_promoted(self, iq, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_full = str(session['from']) @@ -2235,7 +2335,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_admin_action(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare jid_file = jid_bare @@ -2319,7 +2419,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_subscribers_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) values = payload['values'] jid_bare = values['jid'] @@ -2367,7 +2467,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_contact_action(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = payload['values']['jid'] form = self['xep_0004'].make_form('form', 'Contacts') @@ -2434,7 +2534,7 @@ class Slixfeed(slixmpp.ClientXMPP): def _handle_contacts_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) values = payload['values'] jid_bare = values['jid'] @@ -2458,7 +2558,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_bookmarks_editor(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = payload['values']['jid'] properties = await XmppBookmark.properties(self, jid_bare) @@ -2523,7 +2623,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_bookmarks_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) # form = self['xep_0004'].make_form('result', 'Done') # form['instructions'] = ('✅️ Bookmark has been saved') @@ -2558,7 +2658,7 @@ class Slixfeed(slixmpp.ClientXMPP): """ jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare chat_type = await get_chat_type(self, jid_bare) @@ -2671,7 +2771,7 @@ class Slixfeed(slixmpp.ClientXMPP): async def _handle_settings_complete(self, payload, session): jid_full = str(session['from']) function_name = sys._getframe().f_code.co_name - logger.info('Function {} has been initiated for JID {}.' + logger.debug('{}: jid_full: {}' .format(function_name, jid_full)) jid_bare = session['from'].bare form = payload diff --git a/slixfeed/xmpp/command.py b/slixfeed/xmpp/command.py new file mode 100644 index 0000000..5cef9de --- /dev/null +++ b/slixfeed/xmpp/command.py @@ -0,0 +1,2091 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +from datetime import datetime +import logging +import os +import slixfeed.action as action +import slixfeed.config as config +import slixfeed.crawl as crawl +import slixfeed.dt as dt +import slixfeed.fetch as fetch +import slixfeed.url as uri +import slixfeed.sqlite as sqlite +import slixfeed.task as task +from slixfeed.version import __version__ +from slixfeed.xmpp.bookmark import XmppBookmark +from slixfeed.xmpp.message import XmppMessage +from slixfeed.xmpp.presence import XmppPresence +from slixfeed.xmpp.roster import XmppRoster +from slixfeed.xmpp.upload import XmppUpload +from slixfeed.xmpp.utility import get_chat_type, is_moderator + +class XmppCommand: + +# TODO Move class Command to a separate file +# class Command(Slixfeed): +# def __init__(self): +# super().__init__() + + + def adhoc_commands(self): + # self["xep_0050"].add_command( + # node="updates_enable", + # name="Enable/Disable News Updates", + # handler=option_enable_updates, + # ) + + # NOTE https://codeberg.org/poezio/slixmpp/issues/3515 + # if jid == config.get_value('accounts', 'XMPP', 'operator'): + self['xep_0050'].add_command(node='recent', + name='📰️ Browse', + handler=self._handle_recent) + self['xep_0050'].add_command(node='subscription', + name='🪶️ Subscribe', + handler=self._handle_subscription_add) + self['xep_0050'].add_command(node='subscriptions', + name='🎫️ Subscriptions', + handler=self._handle_subscriptions) + self['xep_0050'].add_command(node='promoted', + name='🔮️ Featured', + handler=self._handle_promoted) + self['xep_0050'].add_command(node='discover', + name='🔍️ Discover', + handler=self._handle_discover) + self['xep_0050'].add_command(node='settings', + name='📮️ Settings', + handler=self._handle_settings) + self['xep_0050'].add_command(node='filters', + name='🛡️ Filters', + handler=self._handle_filters) + self['xep_0050'].add_command(node='help', + name='📔️ Manual', + handler=self._handle_help) + self['xep_0050'].add_command(node='advanced', + name='📓 Advanced', + handler=self._handle_advanced) + self['xep_0050'].add_command(node='profile', + name='💼️ Profile', + handler=self._handle_profile) + self['xep_0050'].add_command(node='about', + name='📜️ About', + handler=self._handle_about) + # self['xep_0050'].add_command(node='search', + # name='Search', + # handler=self._handle_search) + + # Special interface + # http://jabber.org/protocol/commands#actions + + async def _handle_profile(self, iq, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + form = self['xep_0004'].make_form('form', 'Profile') + form['instructions'] = ('Displaying information\nJabber ID {}' + .format(jid)) + form.add_field(ftype='fixed', + value='News') + feeds_all = str(await sqlite.get_number_of_items(db_file, 'feeds')) + form.add_field(label='Subscriptions', + ftype='text-single', + value=feeds_all) + feeds_act = str(await sqlite.get_number_of_feeds_active(db_file)) + form.add_field(label='Active', + ftype='text-single', + value=feeds_act) + entries = await sqlite.get_number_of_items(db_file, 'entries') + archive = await sqlite.get_number_of_items(db_file, 'archive') + entries_all = str(entries + archive) + form.add_field(label='Items', + ftype='text-single', + value=entries_all) + unread = str(await sqlite.get_number_of_entries_unread(db_file)) + form.add_field(label='Unread', + ftype='text-single', + value=unread) + form.add_field(ftype='fixed', + value='Options') + key_archive = str(config.get_setting_value(db_file, 'archive')) + form.add_field(label='Archive', + ftype='text-single', + value=key_archive) + key_enabled = str(config.get_setting_value(db_file, 'enabled')) + form.add_field(label='Enabled', + ftype='text-single', + value=key_enabled) + key_interval = str(config.get_setting_value(db_file, 'interval')) + form.add_field(label='Interval', + ftype='text-single', + value=key_interval) + key_length = str(config.get_setting_value(db_file, 'length')) + form.add_field(label='Length', + ftype='text-single', + value=key_length) + key_media = str(config.get_setting_value(db_file, 'media')) + form.add_field(label='Media', + ftype='text-single', + value=key_media) + key_old = str(config.get_setting_value(db_file, 'old')) + form.add_field(label='Old', + ftype='text-single', + value=key_old) + key_quantum = str(config.get_setting_value(db_file, 'quantum')) + form.add_field(label='Quantum', + ftype='text-single', + value=key_quantum) + update_interval = config.get_setting_value(db_file, 'interval') + update_interval = 60 * int(update_interval) + last_update_time = await sqlite.get_last_update_time(db_file) + if last_update_time: + last_update_time = float(last_update_time) + dt_object = datetime.fromtimestamp(last_update_time) + last_update = dt_object.strftime('%H:%M:%S') + if int(key_enabled): + next_update_time = last_update_time + update_interval + dt_object = datetime.fromtimestamp(next_update_time) + next_update = dt_object.strftime('%H:%M:%S') + else: + next_update = 'n/a' + else: + last_update_time = 'n/a' + next_update = 'n/a' + form.add_field(ftype='fixed', + value='Schedule') + form.add_field(label='Last update', + ftype='text-single', + value=last_update) + form.add_field(label='Next update', + ftype='text-single', + value=next_update) + session['payload'] = form + # text_note = ('Jabber ID: {}' + # '\n' + # 'Last update: {}' + # '\n' + # 'Next update: {}' + # ''.format(jid, last_update, next_update)) + # session['notes'] = [['info', text_note]] + return session + + async def _handle_filters(self, iq, session): + jid = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + form = self['xep_0004'].make_form('form', 'Filters') + form['instructions'] = 'Editing filters' # 🪄️ 🛡️ + value = sqlite.get_filter_value(db_file, 'allow') + if value: value = str(value[0]) + form.add_field(var='allow', + ftype='text-single', + label='Allow list', + value=value, + desc='Keywords to allow (comma-separated keywords).') + value = sqlite.get_filter_value(db_file, 'deny') + if value: value = str(value[0]) + form.add_field(var='deny', + ftype='text-single', + label='Deny list', + value=value, + desc='Keywords to deny (comma-separated keywords).') + session['allow_complete'] = True + session['has_next'] = False + session['next'] = self._handle_filters_complete + session['payload'] = form + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + async def _handle_filters_complete(self, payload, session): + """ + Process a command result from the user. + + Arguments: + payload -- Either a single item, such as a form, or a list + of items or forms if more than one form was + provided to the user. The payload may be any + stanza, such as jabber:x:oob for out of band + data, or jabber:x:data for typical data forms. + session -- A dictionary of data relevant to the command + session. Additional, custom data may be saved + here to persist across handler callbacks. + """ + # Text is not displayed; only labels + form = payload + + jid = session['from'].bare + # form = self['xep_0004'].make_form('result', 'Done') + # form['instructions'] = ('✅️ Filters have been updated') + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + # In this case (as is typical), the payload is a form + values = payload['values'] + for value in values: + key = value + val = values[value] + # NOTE We might want to add new keywords from + # an empty form instead of editing a form. + # keywords = sqlite.get_filter_value(db_file, key) + keywords = '' + val = await config.add_to_list(val, keywords) if val else '' + if sqlite.is_filter_key(db_file, key): + await sqlite.update_filter_value(db_file, [key, val]) + elif val: + await sqlite.set_filter_value(db_file, [key, val]) + # form.add_field(var=key.capitalize() + ' list', + # ftype='text-single', + # value=val) + form['title'] = 'Done' + form['instructions'] = 'has been completed!' + # session["has_next"] = False + session['next'] = None + session['payload'] = form + return session + + + async def _handle_subscription_add(self, iq, session): + jid = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + form = self['xep_0004'].make_form('form', 'Subscription') + form['instructions'] = 'Adding subscription' + form.add_field(var='subscription', + # TODO Make it possible to add several subscriptions at once; + # Similarly to BitTorrent trackers list + # ftype='text-multi', + # label='Subscription URLs', + # desc=('Add subscriptions one time per ' + # 'subscription.'), + ftype='text-single', + label='URL', + desc='Enter subscription URL.', + value='http://', + required=True) + # form.add_field(var='scan', + # ftype='boolean', + # label='Scan', + # desc='Scan URL for validity (recommended).', + # value=True) + session['allow_prev'] = False + session['has_next'] = True + session['next'] = self._handle_subscription_new + session['prev'] = None + session['payload'] = form + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + async def _handle_recent(self, iq, session): + form = self['xep_0004'].make_form('form', 'Updates') + form['instructions'] = 'Browse and read news' + options = form.add_field(var='action', + ftype='list-single', + label='Read', + desc=('What would you want to read?'), + required=True) + options.addOption('All news', 'all') + # options.addOption('News by subscription', 'feed') + # options.addOption('News by tag', 'tag') + options.addOption('Rejected news', 'rejected') + options.addOption('Unread news', 'unread') + session['allow_prev'] = False # Cheogram changes style if that button - which should not be on this form - is present + session['has_next'] = True + session['next'] = self._handle_recent_result + session['payload'] = form + session['prev'] = None # Cheogram works as expected with 'allow_prev' set to False Just in case + return session + + + async def _handle_recent_result(self, payload, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + num = 100 + match payload['values']['action']: + case 'all': + results = await sqlite.get_entries(db_file, num) + case 'rejected': + results = await sqlite.get_entries_rejected(db_file, num) + case 'unread': + results = await sqlite.get_unread_entries(db_file, num) + if results: + form = self['xep_0004'].make_form('form', 'Updates') + form['instructions'] = 'Recent {} updates'.format(num) + options = form.add_field(var='update', + ftype='list-single', + label='News', + desc=('Select a news item to read.'), + required=True) + for result in results: + title = result[1] + ix = str(result[0]) + options.addOption(title, ix) + session['allow_prev'] = False # Cheogram changes style if that button - which should not be on this form - is present + session['has_next'] = True + session['next'] = self._handle_recent_select + session['payload'] = form + session['prev'] = None # Cheogram works as expected with 'allow_prev' set to False Just in case + else: + text_info = 'There are no unread news.' + session['notes'] = [['info', text_info]] + return session + + + async def _handle_recent_select(self, payload, session): + values = payload['values'] + ix = values['update'] + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + title = sqlite.get_entry_title(db_file, ix) + title = title[0] if title else 'Untitled' + form = self['xep_0004'].make_form('form', 'Article') + url = sqlite.get_entry_url(db_file, ix) + url = url[0] + logging.info('Original URL: {}'.format(url)) + url = uri.remove_tracking_parameters(url) + logging.info('Processed URL (tracker removal): {}'.format(url)) + url = (uri.replace_hostname(url, 'link')) or url + logging.info('Processed URL (replace hostname): {}'.format(url)) + result = await fetch.http(url) + if 'content' in result: + data = result['content'] + summary = action.get_document_content_as_text(data) + else: + summary = 'No content to show.' + form.add_field(ftype="text-multi", + label=title, + value=summary) + field_url = form.add_field(var='url', + ftype='hidden', + value=url) + field_url = form.add_field(var='url_link', + label='Link', + ftype='text-single', + value=url) + field_url['validate']['datatype'] = 'xs:anyURI' + feed_id = sqlite.get_feed_id_by_entry_index(db_file, ix) + feed_id = feed_id[0] + feed_url = sqlite.get_feed_url(db_file, feed_id) + feed_url = feed_url[0] + field_feed = form.add_field(var='url_feed', + label='Source', + ftype='text-single', + value=feed_url) + field_feed['validate']['datatype'] = 'xs:anyURI' + options = form.add_field(var='filetype', + ftype='list-single', + label='Save as', + desc=('Select file type.'), + value='pdf', + required=True) + options.addOption('ePUB', 'epub') + options.addOption('HTML', 'html') + options.addOption('Markdown', 'md') + options.addOption('PDF', 'pdf') + options.addOption('Plain Text', 'txt') + form['instructions'] = 'Proceed to download article.' + session['allow_complete'] = False + session['allow_prev'] = True + session['has_next'] = True + session['next'] = self._handle_recent_action + session['payload'] = form + session['prev'] = self._handle_recent + return session + + + async def _handle_recent_action(self, payload, session): + ext = payload['values']['filetype'] + url = payload['values']['url'][0] + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + cache_dir = config.get_default_cache_directory() + if not os.path.isdir(cache_dir): + os.mkdir(cache_dir) + if not os.path.isdir(cache_dir + '/readability'): + os.mkdir(cache_dir + '/readability') + url = uri.remove_tracking_parameters(url) + url = (uri.replace_hostname(url, 'link')) or url + result = await fetch.http(url) + if not result['error']: + data = result['content'] + code = result['status_code'] + title = action.get_document_title(data) + title = title.strip().lower() + for i in (' ', '-'): + title = title.replace(i, '_') + for i in ('?', '"', '\'', '!'): + title = title.replace(i, '') + filename = os.path.join( + cache_dir, 'readability', + title + '_' + dt.timestamp() + '.' + ext) + error = action.generate_document(data, url, ext, filename, + readability=True) + if error: + text_error = ('Failed to export {} fot {}' + '\n\n' + 'Reason: {}'.format(ext.upper(), url, error)) + session['notes'] = [['error', text_error]] + else: + url = await XmppUpload.start(self, jid, filename) + form = self['xep_0004'].make_form('result', 'Download') + form['instructions'] = ('Download {} document.' + .format(ext.upper())) + field_url = form.add_field(var='url', + label='Link', + ftype='text-single', + value=url) + field_url['validate']['datatype'] = 'xs:anyURI' + session['payload'] = form + session['allow_complete'] = True + session['next'] = None + session['prev'] = None + return session + + + async def _handle_subscription_new(self, payload, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + # scan = payload['values']['scan'] + url = payload['values']['subscription'] + if isinstance(url, list) and len(url) > 1: + url_count = len(url) + urls = url + agree_count = 0 + error_count = 0 + exist_count = 0 + for url in urls: + result = await action.add_feed(db_file, url) + if result['error']: + error_count += 1 + elif result['exist']: + exist_count += 1 + else: + agree_count += 1 + form = self['xep_0004'].make_form('form', 'Subscription') + if agree_count: + response = ('Added {} new subscription(s) out of {}' + .format(agree_count, url_count)) + session['notes'] = [['info', response]] + else: + response = ('No new subscription was added. ' + 'Exist: {} Error: {}.' + .format(exist_count, error_count)) + session['notes'] = [['error', response]] + session['allow_prev'] = True + session['next'] = None + session['payload'] = None + session['prev'] = self._handle_subscription_add + else: + if isinstance(url, list): + url = url[0] + result = await action.add_feed(db_file, url) + if isinstance(result, list): + results = result + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = ('Discovered {} subscriptions for {}' + .format(len(results), url)) + options = form.add_field(var='subscription', + ftype='list-multi', + label='Subscribe', + desc=('Select subscriptions to add.'), + required=True) + for result in results: + options.addOption(result['name'], result['link']) + # NOTE Disabling "allow_prev" until Cheogram would allow to display + # items of list-single as buttons when button "back" is enabled. + # session['allow_prev'] = True + session['has_next'] = True + session['next'] = self._handle_subscription_new + session['payload'] = form + # session['prev'] = self._handle_subscription_add + elif result['error']: + response = ('Failed to load URL <{}> Reason: {}' + .format(url, result['code'])) + session['allow_prev'] = True + session['next'] = None + session['notes'] = [['error', response]] + session['payload'] = None + session['prev'] = self._handle_subscription_add + elif result['exist']: + # response = ('News source "{}" is already listed ' + # 'in the subscription list at index ' + # '{}.\n{}'.format(result['name'], result['index'], + # result['link'])) + # session['notes'] = [['warn', response]] # Not supported by Gajim + # session['notes'] = [['info', response]] + form = self['xep_0004'].make_form('form', 'Subscription') + form['instructions'] = ('Subscription is already assigned at index {}.' + '\n' + '{}' + .format(result['index'], result['name'])) + form.add_field(ftype='boolean', + var='edit', + label='Would you want to edit this subscription?') + form.add_field(var='subscription', + ftype='hidden', + value=result['link']) + # NOTE Should we allow "Complete"? + # Do all clients provide button "Cancel". + session['allow_complete'] = False + session['has_next'] = True + session['next'] = self._handle_subscription_editor + session['payload'] = form + # session['has_next'] = False + else: + # response = ('News source "{}" has been ' + # 'added to subscription list.\n{}' + # .format(result['name'], result['link'])) + # session['notes'] = [['info', response]] + form = self['xep_0004'].make_form('form', 'Subscription') + # form['instructions'] = ('✅️ News source "{}" has been added to ' + # 'subscription list as index {}' + # '\n\n' + # 'Choose next to continue to subscription ' + # 'editor.' + # .format(result['name'], result['index'])) + form['instructions'] = ('New subscription' + '\n' + '"{}"' + .format(result['name'])) + form.add_field(ftype='boolean', + var='edit', + label='Continue to edit subscription?') + form.add_field(var='subscription', + ftype='hidden', + value=result['link']) + session['allow_complete'] = False + session['has_next'] = True + # session['allow_prev'] = False + # Gajim: Will offer next dialog but as a result, not as form. + # session['has_next'] = False + session['next'] = self._handle_subscription_editor + session['payload'] = form + # session['prev'] = None + return session + + + async def _handle_subscription_enable(self, payload, session): + form = payload + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + ixs = payload['values']['subscriptions'] + form.add_field(ftype='fixed', + value='Modified subscriptions') + for ix in ixs: + name = sqlite.get_feed_title(db_file, ix) + url = sqlite.get_feed_url(db_file, ix) + await sqlite.set_enabled_status(db_file, ix, 1) + # text = (ix,) + name + url + # text = ' - '.join(text) + name = name[0] if name else 'Subscription #{}'.format(ix) + url = url[0] + text = '{} <{}>'.format(name, url) + form.add_field(var=ix, + ftype='text-single', + label=url, + value=text) + form['type'] = 'result' + form['title'] = 'Done' + form['instructions'] = ('Completed successfully!') + session["has_next"] = False + session['next'] = None + session['payload'] = form + return session + + + async def _handle_subscription_disable(self, payload, session): + form = payload + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + ixs = payload['values']['subscriptions'] + form.add_field(ftype='fixed', + value='Modified subscriptions') + for ix in ixs: + name = sqlite.get_feed_title(db_file, ix) + url = sqlite.get_feed_url(db_file, ix) + await sqlite.set_enabled_status(db_file, ix, 0) + # text = (ix,) + name + url + # text = ' - '.join(text) + name = name[0] if name else 'Subscription #{}'.format(ix) + url = url[0] + text = '{} <{}>'.format(name, url) + form.add_field(var=ix, + ftype='text-single', + label=url, + value=text) + form['type'] = 'result' + form['title'] = 'Done' + form['instructions'] = ('Completed successfully!') + session["has_next"] = False + session['next'] = None + session['payload'] = form + return session + + + async def _handle_subscription_del_complete(self, payload, session): + form = payload + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + ixs = payload['values']['subscriptions'] + form.add_field(ftype='fixed', + value='Deleted subscriptions') + for ix in ixs: + name = sqlite.get_feed_title(db_file, ix) + url = sqlite.get_feed_url(db_file, ix) + await sqlite.remove_feed_by_index(db_file, ix) + # text = (ix,) + name + url + # text = ' - '.join(text) + name = name[0] if name else 'Subscription #{}'.format(ix) + url = url[0] + text = '{} <{}>'.format(name, url) + form.add_field(var=ix, + ftype='text-single', + label=url, + value=text) + form['type'] = 'result' + form['title'] = 'Done' + form['instructions'] = ('Completed successfully!') + session["has_next"] = False + session['next'] = None + session['payload'] = form + return session + + + def _handle_cancel(self, payload, session): + text_note = ('Operation has been cancelled.' + '\n' + '\n' + 'No action was taken.') + session['notes'] = [['info', text_note]] + return session + + + async def _handle_discover(self, iq, session): + jid = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + form = self['xep_0004'].make_form('form', 'Discover & Search') + form['instructions'] = 'Discover news subscriptions of all kinds' + options = form.add_field(var='search_type', + ftype='list-single', + label='Browse', + desc=('Select type of search.'), + required=True) + options.addOption('All', 'all') + options.addOption('Categories', 'cat') # Should we write this in a singular form + # options.addOption('Tags', 'tag') + session['allow_prev'] = False + session['has_next'] = True + session['next'] = self._handle_discover_type + session['payload'] = form + session['prev'] = None + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + def _handle_discover_type(self, payload, session): + values = payload['values'] + search_type = values['search_type'] + config_dir = config.get_default_config_directory() + db_file = config_dir + '/feeds.sqlite' + if os.path.isfile(db_file): + form = self['xep_0004'].make_form('form', 'Discover & Search') + match search_type: + case 'all': + form['instructions'] = 'Browsing subscriptions' + options = form.add_field(var='subscription', + # ftype='list-multi', # TODO To be added soon + ftype='list-single', + label='Subscription', + desc=('Select a subscription to add.'), + required=True) + results = sqlite.get_titles_tags_urls(db_file) + for result in results: + title = result[0] + tag = result[1] + url = result[2] + text = '{} ({})'.format(title, tag) + options.addOption(text, url) + # session['allow_complete'] = True + session['next'] = self._handle_subscription_new + case 'cat': + form['instructions'] = 'Browsing categories' + session['next'] = self._handle_discover_category + options = form.add_field(var='category', + ftype='list-single', + label='Categories', + desc=('Select a category to browse.'), + required=True) # NOTE Uncategories or no option for entries without category + categories = sqlite.get_categories(db_file) + for category in categories: + category = category[0] + options.addOption(category, category) + # case 'tag': + session['allow_prev'] = True + session['has_next'] = True + session['payload'] = form + session['prev'] = self._handle_discover + else: + text_note = ('Database is missing.' + '\n' + 'Contact operator.') + session['next'] = None + session['notes'] = [['info', text_note]] + session['payload'] = None + return session + + + async def _handle_discover_category(self, payload, session): + values = payload['values'] + category = values['category'] + config_dir = config.get_default_config_directory() + db_file = config_dir + '/feeds.sqlite' + form = self['xep_0004'].make_form('form', 'Discover & Search') + form['instructions'] = 'Browsing category "{}"'.format(category) + options = form.add_field(var='subscription', + # ftype='list-multi', # TODO To be added soon + ftype='list-single', + label='Subscription', + desc=('Select a subscription to add.'), + required=True) + results = sqlite.get_titles_tags_urls_by_category(db_file, category) + for result in results: + title = result[0] + tag = result[1] + url = result[2] + text = '{} ({})'.format(title, tag) + options.addOption(text, url) + # session['allow_complete'] = True + session['next'] = self._handle_subscription_new + session['payload'] = form + return session + + + async def _handle_subscriptions(self, iq, session): + jid = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = 'Managing subscriptions' + options = form.add_field(var='action', + ftype='list-single', + label='Action', + desc='Select action type.', + required=True, + value='browse') + options.addOption('Browse subscriptions', 'browse') + options.addOption('Browse subscriptions by tag', 'tag') + options.addOption('Enable subscriptions', 'enable') + options.addOption('Disable subscriptions', 'disable') + options.addOption('Remove subscriptions', 'delete') + session['payload'] = form + session['next'] = self._handle_subscriptions_result + session['has_next'] = True + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + async def _handle_subscriptions_result(self, payload, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + form = self['xep_0004'].make_form('form', 'Subscriptions') + match payload['values']['action']: + case 'browse': + form['instructions'] = 'Editing subscriptions' + options = form.add_field(var='subscriptions', + # ftype='list-multi', # TODO To be added soon + ftype='list-single', + label='Subscription', + desc=('Select a subscription to edit.'), + required=True) + subscriptions = await sqlite.get_feeds(db_file) + subscriptions = sorted(subscriptions, key=lambda x: x[0]) + for subscription in subscriptions: + title = subscription[0] + url = subscription[1] + options.addOption(title, url) + session['has_next'] = True + session['next'] = self._handle_subscription_editor + session['allow_complete'] = False + case 'delete': + form['instructions'] = 'Removing subscriptions' + # form.addField(var='interval', + # ftype='text-single', + # label='Interval period') + options = form.add_field(var='subscriptions', + ftype='list-multi', + label='Subscriptions', + desc=('Select subscriptions to remove.'), + required=True) + subscriptions = await sqlite.get_feeds(db_file) + subscriptions = sorted(subscriptions, key=lambda x: x[0]) + for subscription in subscriptions: + title = subscription[0] + ix = str(subscription[2]) + options.addOption(title, ix) + session['cancel'] = self._handle_cancel + session['has_next'] = False + # TODO Refer to confirmation dialog which would display feeds selected + session['next'] = self._handle_subscription_del_complete + session['allow_complete'] = True + case 'disable': + form['instructions'] = 'Disabling subscriptions' + options = form.add_field(var='subscriptions', + ftype='list-multi', + label='Subscriptions', + desc=('Select subscriptions to disable.'), + required=True) + subscriptions = sqlite.get_feeds_by_enabled_state(db_file, True) + subscriptions = sorted(subscriptions, key=lambda x: x[1]) + for subscription in subscriptions: + title = subscription[1] + ix = str(subscription[0]) + options.addOption(title, ix) + session['cancel'] = self._handle_cancel + session['has_next'] = False + session['next'] = self._handle_subscription_disable + session['allow_complete'] = True + case 'enable': + form['instructions'] = 'Enabling subscriptions' + options = form.add_field(var='subscriptions', + ftype='list-multi', + label='Subscriptions', + desc=('Select subscriptions to enable.'), + required=True) + subscriptions = sqlite.get_feeds_by_enabled_state(db_file, False) + subscriptions = sorted(subscriptions, key=lambda x: x[1]) + for subscription in subscriptions: + title = subscription[1] + ix = str(subscription[0]) + options.addOption(title, ix) + session['cancel'] = self._handle_cancel + session['has_next'] = False + session['next'] = self._handle_subscription_enable + session['allow_complete'] = True + case 'tag': + form['instructions'] = 'Browsing tags' + options = form.add_field(var='tag', + ftype='list-single', + label='Tag', + desc=('Select a tag to browse.'), + required=True) + tags = sqlite.get_tags(db_file) + tags = sorted(tags, key=lambda x: x[0]) + for tag in tags: + name = tag[0] + ix = str(tag[1]) + options.addOption(name, ix) + session['allow_complete'] = False + session['next'] = self._handle_subscription_tag + session['has_next'] = True + session['allow_prev'] = True + session['payload'] = form + session['prev'] = self._handle_subscriptions + return session + + + async def _handle_subscription_tag(self, payload, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + tag_id = payload['values']['tag'] + tag_name = sqlite.get_tag_name(db_file, tag_id)[0] + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = 'Subscriptions tagged with "{}"'.format(tag_name) + options = form.add_field(var='subscriptions', + # ftype='list-multi', # TODO To be added soon + ftype='list-single', + label='Subscription', + desc=('Select a subscription to edit.'), + required=True) + subscriptions = sqlite.get_feeds_by_tag_id(db_file, tag_id) + subscriptions = sorted(subscriptions, key=lambda x: x[1]) + for subscription in subscriptions: + title = subscription[1] + url = subscription[2] + options.addOption(title, url) + session['allow_complete'] = False + session['allow_prev'] = True + session['has_next'] = True + session['next'] = self._handle_subscription_editor + session['payload'] = form + session['prev'] = self._handle_subscriptions + return session + + + # FIXME There are feeds that are missing (possibly because of sortings) + async def _handle_subscription(self, iq, session): + jid = session['from'].bare + form = self['xep_0004'].make_form('form', 'Subscription editor') + form['instructions'] = '📰️ Edit subscription preferences and properties' + # form.addField(var='interval', + # ftype='text-single', + # label='Interval period') + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + subscriptions = await sqlite.get_feeds(db_file) + # subscriptions = set(subscriptions) + categorized_subscriptions = {} + for subscription in subscriptions: + title = subscription[0] + url = subscription[1] + try: + letter = title[0].capitalize() + if letter not in categorized_subscriptions: + categorized_subscriptions[letter] = [subscription] + # title[0].capitalize()] = [subscription] + else: + categorized_subscriptions[letter].append(subscription) + # title[0].capitalize()].append(subscription) + except Exception as e: + logging.warning('Title might be empty:', str(e)) + for category in sorted(categorized_subscriptions): + options = form.add_field(var=category, + ftype='list-single', + label=category.capitalize(), + desc='Select a subscription to view.') + subscriptions_ = categorized_subscriptions[category] + subscriptions_ = sorted(subscriptions_, key=lambda x: x[0]) + for subscription_ in subscriptions_: + # for subscription in categorized_subscriptions[category]: + title = subscription_[0] + url = subscription_[1] + options.addOption(title, url) + session['payload'] = form + session['next'] = self._handle_subscription_editor + session['has_next'] = True + return session + + + async def _handle_subscription_editor(self, payload, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + if 'edit' in payload['values'] and not payload['values']['edit']: + session['payload'] = None + session['next'] = None + return session + if 'subscription' in payload['values']: + urls = payload['values']['subscription'] + elif 'subscriptions' in payload['values']: + urls = payload['values']['subscriptions'] + url_count = len(urls) + form = self['xep_0004'].make_form('form', 'Subscription') + if isinstance(urls, list) and url_count > 1: + form['instructions'] = 'Editing {} subscriptions'.format(url_count) + else: + if isinstance(urls, list): + url = urls[0] + # elif isinstance(urls, str): + else: + url = urls + feed_id = await sqlite.get_feed_id(db_file, url) + if feed_id: + feed_id = feed_id[0] + title = sqlite.get_feed_title(db_file, feed_id) + title = title[0] + tags_result = sqlite.get_tags_by_feed_id(db_file, feed_id) + tags_sorted = sorted(x[0] for x in tags_result) + tags = ', '.join(tags_sorted) + form['instructions'] = 'Editing subscription #{}'.format(feed_id) + else: + form['instructions'] = 'Adding subscription' + title = '' + tags = '' # TODO Suggest tags by element "categories" + form.add_field(ftype='fixed', + value='Properties') + form.add_field(var='name', + ftype='text-single', + label='Name', + value=title, + required=True) + # NOTE This does not look good in Gajim + # url = form.add_field(ftype='fixed', + # value=url) + #url['validate']['datatype'] = 'xs:anyURI' + options = form.add_field(var='url', + ftype='list-single', + label='URL', + value=url) + options.addOption(url, url) + feed_id_str = str(feed_id) + options = form.add_field(var='id', + ftype='list-single', + label='ID #', + value=feed_id_str) + options.addOption(feed_id_str, feed_id_str) + form.add_field(var='tags_new', + ftype='text-single', + label='Tags', + desc='Comma-separated tags.', + value=tags) + form.add_field(var='tags_old', + ftype='hidden', + value=tags) + form.add_field(ftype='fixed', + value='Options') + options = form.add_field(var='priority', + ftype='list-single', + label='Priority', + value='0') + options['validate']['datatype'] = 'xs:integer' + options['validate']['range'] = { 'minimum': 1, 'maximum': 5 } + i = 0 + while i <= 5: + num = str(i) + options.addOption(num, num) + i += 1 + form.add_field(var='enabled', + ftype='boolean', + label='Enabled', + value=True) + session['allow_complete'] = True + # session['allow_prev'] = True + session['cancel'] = self._handle_cancel + session['has_next'] = False + session['next'] = self._handle_subscription_complete + session['payload'] = form + return session + + + # TODO Create a new form. Do not "recycle" the last form. + async def _handle_subscription_complete(self, payload, session): + jid = session['from'].bare + values = payload['values'] + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + # url = values['url'] + # feed_id = await sqlite.get_feed_id(db_file, url) + # feed_id = feed_id[0] + # if feed_id: feed_id = feed_id[0] + feed_id = values['id'] + enabled = values['enabled'] + # if enabled: + # enabled_status = 1 + # else: + # enabled_status = 0 + # await sqlite.mark_feed_as_read(db_file, feed_id) + enabled_status = 1 if enabled else 0 + if not enabled_status: await sqlite.mark_feed_as_read(db_file, feed_id) + await sqlite.set_enabled_status(db_file, feed_id, enabled_status) + name = values['name'] + await sqlite.set_feed_title(db_file, feed_id, name) + priority = values['priority'] + tags_new = values['tags_new'] + tags_old = values['tags_old'] + # Add new tags + for tag in tags_new.split(','): + tag = tag.strip() + if not tag: + continue + tag = tag.lower() + tag_id = sqlite.get_tag_id(db_file, tag) + if not tag_id: + await sqlite.set_new_tag(db_file, tag) + tag_id = sqlite.get_tag_id(db_file, tag) + tag_id = tag_id[0] + if not sqlite.is_tag_id_of_feed_id(db_file, tag_id, feed_id): + await sqlite.set_feed_id_and_tag_id(db_file, feed_id, tag_id) + # Remove tags that were not submitted + for tag in tags_old[0].split(','): + tag = tag.strip() + if not tag: + continue + if tag not in tags_new: + tag_id = sqlite.get_tag_id(db_file, tag) + tag_id = tag_id[0] + await sqlite.delete_feed_id_tag_id(db_file, feed_id, tag_id) + sqlite.is_tag_id_associated(db_file, tag_id) + await sqlite.delete_tag_by_index(db_file, tag_id) + # form = self['xep_0004'].make_form('form', 'Subscription') + # form['instructions'] = ('📁️ Subscription #{} has been {}' + # .format(feed_id, action)) + form = payload + form['title'] = 'Done' + form['instructions'] = ('has been completed!') + # session["has_next"] = False + session['next'] = None + session['payload'] = form + # session['type'] = 'submit' + return session + + + async def _handle_subscription_selector(self, payload, session): + jid = session['from'].bare + form = self['xep_0004'].make_form('form', 'Add Subscription') + form['instructions'] = ('📰️ Select a subscription to add\n' + 'Subsciptions discovered for {}' + .format(url)) + # form.addField(var='interval', + # ftype='text-single', + # label='Interval period') + options = form.add_field(var='subscriptions', + ftype='list-multi', + label='Subscriptions', + desc=('Select subscriptions to perform ' + 'actions upon.'), + required=True) + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + subscriptions = await sqlite.get_feeds(db_file) + subscriptions = sorted(subscriptions, key=lambda x: x[0]) + for subscription in subscriptions: + title = subscription[0] + url = subscription[1] + options.addOption(title, url) + # options = form.add_field(var='action', + # ftype='list-single', + # label='Action', + # value='none') + # options.addOption('None', 'none') + # options.addOption('Reset', 'reset') + # options.addOption('Enable', 'enable') + # options.addOption('Disable', 'disable') + # options.addOption('Delete', 'delete') + session['payload'] = form + session['next'] = self._handle_subscription_editor + session['has_next'] = True + return session + + + async def _handle_advanced(self, iq, session): + jid = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + form = self['xep_0004'].make_form('form', 'Advanced') + form['instructions'] = 'Extended options' + options = form.add_field(var='option', + ftype='list-single', + label='Choose', + required=True) + # options.addOption('Activity', 'activity') + # options.addOption('Filters', 'filter') + # options.addOption('Statistics', 'statistics') + # options.addOption('Scheduler', 'scheduler') + options.addOption('Import', 'import') + options.addOption('Export', 'export') + jid = session['from'].bare + if jid == config.get_value('accounts', 'XMPP', 'operator'): + options.addOption('Administration', 'admin') + session['payload'] = form + session['next'] = self._handle_advanced_result + session['has_next'] = True + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + async def _handle_advanced_result(self, payload, session): + match payload['values']['option']: + case 'activity': + # TODO dialog for JID and special dialog for operator + # Here you can monitor activity + text_note = 'This feature is not yet available.' + session['notes'] = [['info', text_note]] + case 'admin': + # NOTE Even though this check is already conducted on previous + # form, this check is being done just in case. + jid = session['from'].bare + if jid == config.get_value('accounts', 'XMPP', 'operator'): + if self.is_component: + # NOTE This will be changed with XEP-0222 XEP-0223 + text_info = ('Subscriber management options are ' + 'currently not available for Slixfeed ' + 'running as component. Once support for ' + 'XEP-0222 and XEP-0223 be added, this ' + 'panel will be usable for components as ' + 'well.') + session['has_next'] = False + session['next'] = None + session['notes'] = [['info', text_info]] + else: + form = self['xep_0004'].make_form('form', 'Admin Panel') + form['instructions'] = 'Administration actions' + options = form.add_field(var='action', + ftype='list-single', + label='Manage', + desc='Select action type.', + value='subscribers', + required=True) + options.addOption('Bookmarks', 'bookmarks') + options.addOption('Contacts', 'roster') + options.addOption('Subscribers', 'subscribers') + session['payload'] = form + session['next'] = self._handle_admin_action + session['has_next'] = True + else: + logging.warning('An unauthorized attempt to access bookmarks has ' + 'been detected!\n' + 'Details:\n' + ' Jabber ID: {}\n' + ' Timestamp: {}\n' + .format(jid, dt.timestamp())) + text_warn = 'This resource is restricted.' + session['notes'] = [['warn', text_warn]] + session['has_next'] = False + session['next'] = None + # case 'filters': + # TODO Ability to edit lists.toml or filters.toml + case 'scheduler': + # TODO Set days and hours to receive news + text_note = 'This feature is not yet available.' + session['notes'] = [['info', text_note]] + session['has_next'] = False + session['next'] = None + case 'statistics': + # TODO Here you can monitor statistics + text_note = 'This feature is not yet available.' + session['notes'] = [['info', text_note]] + session['has_next'] = False + session['next'] = None + case 'import': + form = self['xep_0004'].make_form('form', 'Import') + form['instructions'] = 'Importing feeds' + url = form.add_field(var='url', + ftype='text-single', + label='URL', + desc='Enter URL to an OPML file.', + required=True) + url['validate']['datatype'] = 'xs:anyURI' + session['allow_complete'] = True + session['has_next'] = False + session['next'] = self._handle_import_complete + session['payload'] = form + case 'export': + form = self['xep_0004'].make_form('form', 'Export') + form['instructions'] = ('To easily import subscriptions from ' + 'one News Reader to another, then it ' + 'is always recommended to export ' + 'subscriptions into OPML file. See ' + 'About -> Software for a list of ' + 'News Readers offered for desktop and ' + 'mobile devices.') + options = form.add_field(var='filetype', + ftype='list-multi', + label='Format', + desc='Choose export format.', + value='opml', + required=True) + options.addOption('Markdown', 'md') + options.addOption('OPML', 'opml') + # options.addOption('HTML', 'html') + # options.addOption('XBEL', 'xbel') + session['allow_complete'] = True + session['has_next'] = False + session['next'] = self._handle_export_complete + session['payload'] = form + session['allow_prev'] = True + session['prev'] = self._handle_advanced + return session + + + async def _handle_about(self, iq, session): + form = self['xep_0004'].make_form('form', 'About') + form['instructions'] = 'Information about Slixfeed and related projects' + options = form.add_field(var='option', + ftype='list-single', + label='About', + required=True) + options.addOption('Slixfeed', 'about') + options.addOption('RSS Task Force', 'rtf') + # options.addOption('Manual', 'manual') + options.addOption('Tips', 'tips') + options.addOption('Services for syndication', 'services') + options.addOption('Software for syndication', 'software') + options.addOption('Terms and conditions', 'terms') + options.addOption('Privacy policy', 'policy') + options.addOption('License', 'license') + options.addOption('Authors', 'author') + options.addOption('Translators', 'translators') + options.addOption('Thanks', 'thanks') + session['payload'] = form + session['next'] = self._handle_about_result + session['has_next'] = True + return session + + + async def _handle_about_result(self, payload, session): + match payload['values']['option']: + case 'about': + title = 'About' + subtitle = 'Slixfeed {}\n\n'.format(__version__) + content = action.manual('information.toml', 'about') + content += ['\nslixmpp\n'] + content += action.manual('information.toml', 'slixmpp') + content += ['\nSleekXMPP\n'] + content += action.manual('information.toml', 'sleekxmpp') + content += ['\nXMPP\n'] + content += action.manual('information.toml', 'xmpp') + case 'rtf': + title = 'About' + subtitle = 'RSS Task Force' + content = action.manual('information.toml', 'rtf') + case 'author': + title = 'Authors' + subtitle = 'The People Who Made This To Happen' + content = action.manual('information.toml', 'authors') + # case 'manual': + # title = 'Manual' + # subtitle = 'Slixfeed Manual' + # content = action.manual('information.toml', 'manual') + case 'license': + title = 'License' + subtitle = 'Slixfeed Software License' + content = action.manual('information.toml', 'license') + case 'policy': + title = 'Policies' + subtitle = 'Privacy Policy' + content = action.manual('information.toml', 'privacy') + case 'services': + title = 'Services' + subtitle = ('Below are online services that extend the ' + 'syndication experience by means of bookmarking ' + 'and multimedia, and also enhance it by restoring ' + 'access to news web feeds.') + content = action.manual('information.toml', 'services') + case 'software': + title = 'Software' + subtitle = ('Take back control of your news. With free, high-' + 'quality, software for your desktop, home and ' + 'mobile devices.') + content = action.manual('information.toml', 'software') + case 'terms': + title = 'Policies' + subtitle = 'Terms and Conditions' + content = action.manual('information.toml', 'terms') + case 'thanks': + title = 'Thanks' + subtitle = 'We are XMPP' + content = action.manual('information.toml', 'thanks') + case 'tips': + # Tips and tricks you might have not known about Slixfeed and XMPP! + title = 'Help' + subtitle = 'Tips & Tricks' + content = 'This page is not yet available.' + case 'translators': + title = 'Translators' + subtitle = 'From all across the world' + content = action.manual('information.toml', 'translators') + form = self['xep_0004'].make_form('result', title) + form['instructions'] = subtitle + form.add_field(ftype="text-multi", + value=content) + # Gajim displays all form['instructions'] on top + # Psi ignore the latter form['instructions'] + # form['instructions'] = 'YOU!\n🫵️\n- Join us -' + session['payload'] = form + session['allow_complete'] = True + session['allow_prev'] = True + session["has_next"] = False + session['next'] = None + # session['payload'] = None # Crash Cheogram + session['prev'] = self._handle_about + return session + + + async def _handle_motd(self, iq, session): + # TODO add functionality to attach image. + # Here you can add groupchat rules,post schedule, tasks or + # anything elaborated you might deem fit. Good luck! + text_note = 'This feature is not yet available.' + session['notes'] = [['info', text_note]] + return session + + + async def _handle_help(self, iq, session): + import tomllib + config_dir = config.get_default_config_directory() + with open(config_dir + '/' + 'commands.toml', mode="rb") as commands: + cmds = tomllib.load(commands) + + form = self['xep_0004'].make_form('result', 'Manual') + form['instructions'] = '🛟️ Help manual for interactive chat' + + # text = '🛟️ Help and Information about Slixfeed\n\n' + # for cmd in cmds: + # name = cmd.capitalize() + # elements = cmds[cmd] + # text += name + '\n' + # for key, value in elements.items(): + # text += " " + key.capitalize() + '\n' + # for line in value.split("\n"): + # text += " " + line + '\n' + # form['instructions'] = text + + for cmd in cmds: + name = cmd.capitalize() + form.add_field(var='title', + ftype='fixed', + value=name) + elements = cmds[cmd] + for key, value in elements.items(): + key = key.replace('_', ' ') + key = key.capitalize() + form.add_field(var='title', + ftype='text-multi', + label=key, + value=value) + session['payload'] = form + return session + + + async def _handle_import_complete(self, payload, session): + form = payload + url = payload['values']['url'] + if url.startswith('http') and url.endswith('.opml'): + jid = session['from'].bare + jid_file = jid.replace('/', '_') + db_file = config.get_pathname_to_database(jid_file) + count = await action.import_opml(db_file, url) + try: + int(count) + # form = self['xep_0004'].make_form('result', 'Done') + # form['instructions'] = ('✅️ Feeds have been imported') + form['title'] = 'Done' + form['instructions'] = ('has been completed!') + message = '{} feeds have been imported.'.format(count) + form.add_field(var='Message', + ftype='text-single', + value=message) + session['payload'] = form + except: + session['payload'] = None + text_error = ('Import failed. Filetype does not appear to be ' + 'an OPML file.') + session['notes'] = [['error', text_error]] + else: + session['payload'] = None + text_error = 'Import aborted. Send URL of OPML file.' + session['notes'] = [['error', text_error]] + session["has_next"] = False + session['next'] = None + return session + + + async def _handle_export_complete(self, payload, session): + form = payload + jid = session['from'].bare + jid_file = jid.replace('/', '_') + # form = self['xep_0004'].make_form('result', 'Done') + # form['instructions'] = ('✅️ Feeds have been exported') + exts = payload['values']['filetype'] + for ext in exts: + filename = await action.export_feeds(self, jid, jid_file, ext) + url = await XmppUpload.start(self, jid, filename) + url_field = form.add_field(var=ext.upper(), + ftype='text-single', + label=ext, + value=url) + url_field['validate']['datatype'] = 'xs:anyURI' + chat_type = await get_chat_type(self, jid) + XmppMessage.send_oob(self, jid, url, chat_type) + form['type'] = 'result' + form['title'] = 'Done' + form['instructions'] = ('Completed successfully!') + session["has_next"] = False + session['next'] = None + session['payload'] = form + return session + + + # TODO Exclude feeds that are already in database or requester. + # 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 = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + form = self['xep_0004'].make_form('form', 'Subscribe') + # NOTE Refresh button would be of use + form['instructions'] = 'Featured subscriptions' + url = action.pick_a_feed() + # options = form.add_field(var='choice', + # ftype="boolean", + # label='Subscribe to {}?'.format(url['name']), + # desc='Click to subscribe.') + # form.add_field(var='subscription', + # ftype='hidden', + # value=url['link']) + options = form.add_field(var='subscription', + ftype="list-single", + label='Subscribe', + desc='Click to subscribe.') + for i in range(10): + url = action.pick_a_feed() + options.addOption(url['name'], url['link']) + jid = session['from'].bare + if '@' in jid: + hostname = jid.split('@')[1] + url = 'http://' + hostname + result = await crawl.probe_page(url) + if not result: + url = {'url' : url, + 'index' : None, + 'name' : None, + 'code' : None, + 'error' : True, + 'exist' : False} + elif isinstance(result, list): + for url in result: + if url['link']: options.addOption('{}\n{}'.format(url['name'], url['link']), url['link']) + else: + url = result + # Automatically set priority to 5 (highest) + if url['link']: options.addOption(url['name'], url['link']) + session['allow_complete'] = False + session['allow_prev'] = True + # singpolyma: Don't use complete action if there may be more steps + # https://gitgud.io/sjehuda/slixfeed/-/merge_requests/13 + # Gajim: On next form Gajim offers no button other than "Commands". + # Psi: Psi closes the dialog. + # Conclusion, change session['has_next'] from False to True + # session['has_next'] = False + session['has_next'] = True + session['next'] = self._handle_subscription_new + session['payload'] = form + session['prev'] = self._handle_promoted + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + async def _handle_admin_action(self, payload, session): + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + subscriptions = await sqlite.get_feeds(db_file) + subscriptions = sorted(subscriptions, key=lambda x: x[0]) + form = self['xep_0004'].make_form('form', 'Subscriptions') + match payload['values']['action']: + case 'bookmarks': + form = self['xep_0004'].make_form('form', 'Bookmarks') + form['instructions'] = 'Bookmarks' + options = form.add_field(var='jid', + ftype='list-single', + label='Jabber ID', + desc='Select a bookmark to edit.', + required=True) + conferences = await XmppBookmark.get(self) + for conference in conferences: + options.addOption(conference['name'], conference['jid']) + session['has_next'] = True + session['next'] = self._handle_bookmarks_editor + case 'roster': + form = self['xep_0004'].make_form('form', 'Contacts') + form['instructions'] = 'Organize contacts' + options = form.add_field(var='jid', + ftype='list-single', + label='Contact', + desc='Select a contact.', + required=True) + contacts = await XmppRoster.get_contacts(self) + for contact in contacts: + contact_name = contacts[contact]['name'] + contact_name = contact_name if contact_name else contact + options.addOption(contact_name, contact) + options = form.add_field(var='action', + ftype='list-single', + label='Action', + required=True) + options.addOption('Display', 'view') + options.addOption('Edit', 'edit') + session['has_next'] = True + session['next'] = self._handle_contact_action + case 'subscribers': + form = self['xep_0004'].make_form('form', 'Subscribers') + form['instructions'] = 'Committing subscriber action' + options = form.add_field(var='action', + ftype='list-single', + label='Action', + value='message', + required=True) + options.addOption('Request authorization From', 'from') + options.addOption('Resend authorization To', 'to') + options.addOption('Send message', 'message') + options.addOption('Remove', 'remove') + options = form.add_field(var='jid', + ftype='list-single', + label='Jabber ID', + desc='Select a contact.', + required=True) + contacts = await XmppRoster.get_contacts(self) + for contact in contacts: + contact_name = contacts[contact]['name'] + contact_name = contact_name if contact_name else contact + options.addOption(contact_name, contact) + form.add_field(var='subject', + ftype='text-single', + label='Subject') + form.add_field(var='message', + ftype='text-multi', + label='Message', + desc='Add a descriptive message.') + session['allow_complete'] = True + session['has_next'] = False + session['next'] = self._handle_subscribers_complete + session['allow_prev'] = True + session['payload'] = form + session['prev'] = self._handle_advanced + return session + + + async def _handle_subscribers_complete(self, payload, session): + values = payload['values'] + jid = values['jid'] + value_subject = values['subject'] + message_subject = value_subject if value_subject else None + value_message = values['message'] + message_body = value_message if value_message else None + match values['action']: + case 'from': + XmppPresence.subscription(self, jid, 'subscribe') + if not message_subject: + message_subject = 'System Message' + if not message_body: + message_body = ('This user wants to subscribe to your presence' + '. Click the button labelled "Add/Auth" to ' + 'authorize the subscription. This will also ' + 'add the person to your contact list if it is ' + 'not already there.') + case 'remove': + XmppRoster.remove(self, jid) + if not message_subject: + message_subject = 'System Message' + if not message_body: + message_body = 'Your authorization has been removed!' + case 'to': + XmppPresence.subscription(self, jid, 'subscribed') + if not message_subject: + message_subject = 'System Message' + if not message_body: + message_body = 'Your authorization has been approved!' + if message_subject: + XmppMessage.send_headline(self, jid, message_subject, message_body, + 'chat') + elif message_body: + XmppMessage.send(self, jid, message_body, 'chat') + form = payload + form['title'] = 'Done' + form['instructions'] = ('has been completed!') + # session["has_next"] = False + session['next'] = None + session['payload'] = form + return session + + + async def _handle_contact_action(self, payload, session): + jid = payload['values']['jid'] + form = self['xep_0004'].make_form('form', 'Contacts') + session['allow_complete'] = True + roster = await XmppRoster.get_contacts(self) + properties = roster[jid] + match payload['values']['action']: + case 'edit': + form['instructions'] = 'Editing contact' + options = form.add_field(var='jid', + ftype='list-single', + label='Jabber ID', + value=jid) + options.addOption(jid, jid) + form.add_field(var='name', + ftype='text-single', + label='Name', + value=properties['name']) + session['allow_complete'] = True + session['next'] = self._handle_contacts_complete + case 'view': + form['instructions'] = 'Viewing contact' + contact_name = properties['name'] + contact_name = contact_name if contact_name else jid + form.add_field(var='name', + ftype='text-single', + label='Name', + value=properties['name']) + form.add_field(var='from', + ftype='boolean', + label='From', + value=properties['from']) + form.add_field(var='to', + ftype='boolean', + label='To', + value=properties['to']) + form.add_field(var='pending_in', + ftype='boolean', + label='Pending in', + value=properties['pending_in']) + form.add_field(var='pending_out', + ftype='boolean', + label='Pending out', + value=properties['pending_out']) + form.add_field(var='whitelisted', + ftype='boolean', + label='Whitelisted', + value=properties['whitelisted']) + form.add_field(var='subscription', + ftype='fixed', + label='Subscription', + value=properties['subscription']) + session['allow_complete'] = None + session['next'] = None + # session['allow_complete'] = True + session['allow_prev'] = True + session['has_next'] = False + # session['next'] = None + session['payload'] = form + session['prev'] = self._handle_contacts_complete + return session + + + def _handle_contacts_complete(self, payload, session): + values = payload['values'] + jid = values['jid'] + name = values['name'] + name_old = XmppRoster.get_contact_name(self, jid) + if name == name_old: + message = ('No action has been taken. Reason: New ' + 'name is identical to the current one.') + session['payload'] = None + session['notes'] = [['info', message]] + else: + XmppRoster.set_contact_name(self, jid, name) + form = payload + form['title'] = 'Done' + form['instructions'] = ('has been completed!') + session['payload'] = form + session['next'] = None + return session + + + async def _handle_bookmarks_editor(self, payload, session): + jid = payload['values']['jid'] + properties = await XmppBookmark.properties(self, jid) + form = self['xep_0004'].make_form('form', 'Bookmarks') + form['instructions'] = 'Editing bookmark' + jid_split = properties['jid'].split('@') + room = jid_split[0] + host = jid_split[1] + options = form.addField(var='jid', + ftype='list-single', + label='Jabber ID', + value=jid) + options.addOption(jid, jid) + form.addField(var='name', + ftype='text-single', + label='Name', + value=properties['name'], + required=True) + form.addField(var='room', + ftype='text-single', + label='Room', + value=room, + required=True) + form.addField(var='host', + ftype='text-single', + label='Host', + value=host, + required=True) + form.addField(var='alias', + ftype='text-single', + label='Alias', + value=properties['nick'], + required=True) + form.addField(var='password', + ftype='text-private', + label='Password', + value=properties['password']) + form.addField(var='language', + ftype='text-single', + label='Language', + value=properties['lang']) + form.add_field(var='autojoin', + ftype='boolean', + label='Auto-join', + value=properties['autojoin']) + # options = form.add_field(var='action', + # ftype='list-single', + # label='Action', + # value='join') + # options.addOption('Add', 'add') + # options.addOption('Join', 'join') + # options.addOption('Remove', 'remove') + session['allow_complete'] = True + session['allow_prev'] = True + session['has_next'] = False + session['next'] = self._handle_bookmarks_complete + session['payload'] = form + session['prev'] = self._handle_admin_action + return session + + + async def _handle_bookmarks_complete(self, payload, session): + # form = self['xep_0004'].make_form('result', 'Done') + # form['instructions'] = ('✅️ Bookmark has been saved') + # # In this case (as is typical), the payload is a form + values = payload['values'] + await XmppBookmark.add(self, properties=values) + # for value in values: + # key = str(value) + # val = str(values[value]) + # if not val: val = 'None' # '(empty)' + # form.add_field(var=key, + # ftype='text-single', + # label=key.capitalize(), + # value=val) + form = payload + form['title'] = 'Done' + form['instructions'] = 'has been completed!' + session['next'] = None + session['payload'] = form + return session + + + async def _handle_settings(self, iq, session): + """ + Respond to the initial request for a command. + + Arguments: + iq -- The iq stanza containing the command request. + session -- A dictionary of data relevant to the command + session. Additional, custom data may be saved + here to persist across handler callbacks. + """ + jid = session['from'].bare + jid_full = str(session['from']) + chat_type = await get_chat_type(self, jid) + if chat_type == 'groupchat': + moderator = is_moderator(self, jid, jid_full) + if chat_type == 'chat' or moderator: + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + form = self['xep_0004'].make_form('form', 'Settings') + form['instructions'] = 'Editing settings' + + value = config.get_setting_value(db_file, 'enabled') + value = int(value) + if value: + value = True + else: + value = False + form.add_field(var='enabled', + ftype='boolean', + label='Enabled', + desc='Enable news updates.', + value=value) + + value = config.get_setting_value(db_file, 'media') + value = int(value) + if value: + value = True + else: + value = False + form.add_field(var='media', + ftype='boolean', + desc='Send audio, images or videos if found.', + label='Display media', + value=value) + + value = config.get_setting_value(db_file, 'old') + value = int(value) + if value: + value = True + else: + value = False + form.add_field(var='old', + ftype='boolean', + desc='Treat all items of newly added subscriptions as new.', + # label='Send only new items', + label='Include old news', + value=value) + + value = config.get_setting_value(db_file, 'interval') + value = int(value) + value = value/60 + value = int(value) + value = str(value) + options = form.add_field(var='interval', + ftype='list-single', + label='Interval', + desc='Interval update (in hours).', + value=value) + options['validate']['datatype'] = 'xs:integer' + options['validate']['range'] = { 'minimum': 1, 'maximum': 48 } + i = 1 + while i <= 48: + x = str(i) + options.addOption(x, x) + if i >= 12: + i += 6 + else: + i += 1 + + value = config.get_setting_value(db_file, 'quantum') + value = str(value) + options = form.add_field(var='quantum', + ftype='list-single', + label='Amount', + desc='Amount of items per update.', + value=value) + options['validate']['datatype'] = 'xs:integer' + options['validate']['range'] = { 'minimum': 1, 'maximum': 5 } + i = 1 + while i <= 5: + x = str(i) + options.addOption(x, x) + i += 1 + + value = config.get_setting_value(db_file, 'archive') + value = str(value) + options = form.add_field(var='archive', + ftype='list-single', + label='Archive', + desc='Number of news items to archive.', + value=value) + options['validate']['datatype'] = 'xs:integer' + options['validate']['range'] = { 'minimum': 0, 'maximum': 500 } + i = 0 + while i <= 500: + x = str(i) + options.addOption(x, x) + i += 50 + session['allow_complete'] = True + # session['has_next'] = True + session['next'] = self._handle_settings_complete + session['payload'] = form + else: + text_warn = ('This resource is restricted to moderators of {}.' + .format(jid)) + session['notes'] = [['warn', text_warn]] + return session + + + async def _handle_settings_complete(self, payload, session): + jid = session['from'].bare + form = payload + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + # In this case (as is typical), the payload is a form + values = payload['values'] + for value in values: + key = value + val = values[value] + + if key == 'interval': + val = int(val) + if val < 1: val = 1 + val = val * 60 + + if (key == 'enabled' and val == 1 and + config.get_setting_value(db_file, 'enabled') == 0): + logging.info('Slixfeed has been enabled for {}'.format(jid)) + status_type = 'available' + status_message = '📫️ Welcome back!' + XmppPresence.send(self, jid, status_message, + status_type=status_type) + await asyncio.sleep(5) + await task.start_tasks_xmpp(self, jid, ['check', 'status', + 'interval']) + + if (key == 'enabled' and val == 0 and + config.get_setting_value(db_file, 'enabled') == 1): + logging.info('Slixfeed has been disabled for {}'.format(jid)) + task.clean_tasks_xmpp(self, jid, ['interval', 'status']) + status_type = 'xa' + status_message = '📪️ Send "Start" to receive updates' + XmppPresence.send(self, jid, status_message, + status_type=status_type) + + await config.set_setting_value(db_file, key, val) + val = sqlite.get_setting_value(db_file, key) + val = val[0] + + # if key == 'enabled': + # if config.get_setting_value(db_file, 'enabled') == 0: + # status_type = 'available' + # status_message = '📫️ Welcome back!' + # XmppPresence.send(self, jid, status_message, + # status_type=status_type) + # await asyncio.sleep(5) + # await task.start_tasks_xmpp(self, jid, ['check', 'status', + # 'interval']) + # else: + # task.clean_tasks_xmpp(self, jid, ['interval', 'status']) + # status_type = 'xa' + # status_message = '📪️ Send "Start" to receive Jabber updates' + # XmppPresence.send(self, jid, status_message, + # status_type=status_type) + + if key in ('enabled', 'media', 'old'): + if val == '1': + val = 'Yes' + elif val == '0': + val = 'No' + + if key == 'interval': + val = int(val) + val = val/60 + val = int(val) + val = str(val) + + # match value: + # case 'enabled': + # pass + # case 'interval': + # pass + + # result = '{}: {}'.format(key.capitalize(), val) + + # form.add_field(var=key, + # ftype='fixed', + # value=result) + form['title'] = 'Done' + form['instructions'] = 'has been completed!' + # session['allow_complete'] = True + # session['has_next'] = False + # session['next'] = self._handle_profile + session['payload'] = form + return session