From 245cd9832aa8c2ec77d6766ed239263cb28681de Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Mon, 10 Jun 2024 18:54:27 +0300 Subject: [PATCH] [WIP] Add an IPC interface of type Unix domain socket (Berkeley sockets). Thank you Laura and TheCoffeMaker. --- slixfeed/__main__.py | 4 +- slixfeed/action.py | 101 +-- slixfeed/cli.py | 69 ++ slixfeed/sqlite.py | 22 +- slixfeed/version.py | 4 +- slixfeed/xmpp/chat.py | 1312 +++++++------------------------------ slixfeed/xmpp/client.py | 89 ++- slixfeed/xmpp/commands.py | 1103 +++++++++++++++++++++++++++++++ slixfeed/xmpp/ipc.py | 347 ++++++++++ 9 files changed, 1820 insertions(+), 1231 deletions(-) create mode 100644 slixfeed/cli.py create mode 100644 slixfeed/xmpp/commands.py create mode 100644 slixfeed/xmpp/ipc.py diff --git a/slixfeed/__main__.py b/slixfeed/__main__.py index f204794..1ea1703 100644 --- a/slixfeed/__main__.py +++ b/slixfeed/__main__.py @@ -84,8 +84,6 @@ from slixfeed.version import __version__ # import socks # import socket -account_xmpp = config.get_values('accounts.toml', 'xmpp') - # account = ConfigAccount() # TODO ~Delete~ Clear as soon as posible after is no longer needed def main(): @@ -176,6 +174,8 @@ def main(): # if not alias: # alias = (input('Alias: ')) or 'Slixfeed' + account_xmpp = config.get_values('accounts.toml', 'xmpp') + # Try configuration file if 'client' in account_xmpp: from slixfeed.xmpp.client import XmppClient diff --git a/slixfeed/action.py b/slixfeed/action.py index f6ddaf7..ddc072c 100644 --- a/slixfeed/action.py +++ b/slixfeed/action.py @@ -827,23 +827,6 @@ def list_search_results(query, results): return message -def list_feeds_by_query(query, results): - function_name = sys._getframe().f_code.co_name - logger.debug('{}'.format(function_name)) - message = ('Feeds containing "{}":\n\n```' - .format(query)) - for result in results: - message += ('\nName : {} [{}]' - '\nURL : {}' - '\n' - .format(str(result[0]), str(result[1]), str(result[2]))) - if len(results): - message += "\n```\nTotal of {} feeds".format(len(results)) - else: - message = "No feeds were found for: {}".format(query) - return message - - async def list_options(self, jid_bare): """ Print options. @@ -890,67 +873,6 @@ async def list_options(self, jid_bare): return message -async def list_statistics(db_file): - """ - Print statistics. - - Parameters - ---------- - db_file : str - Path to database file. - - Returns - ------- - msg : str - Statistics as message. - """ - function_name = sys._getframe().f_code.co_name - logger.debug('{}: db_file: {}' - .format(function_name, db_file)) - entries_unread = sqlite.get_number_of_entries_unread(db_file) - entries = sqlite.get_number_of_items(db_file, 'entries_properties') - feeds_active = sqlite.get_number_of_feeds_active(db_file) - feeds_all = sqlite.get_number_of_items(db_file, 'feeds_properties') - - # msg = """You have {} unread news items out of {} from {} news sources. - # """.format(unread_entries, entries, feeds) - - # try: - # value = cur.execute(sql, par).fetchone()[0] - # except: - # print("Error for key:", key) - # value = "Default" - # values.extend([value]) - - message = ("Statistics:" - "\n" - "```" - "\n" - "News items : {}/{}\n" - "News sources : {}/{}\n" - "```").format(entries_unread, - entries, - feeds_active, - feeds_all) - return message - - -# FIXME Replace counter by len -def list_last_entries(results, num): - function_name = sys._getframe().f_code.co_name - logger.debug('{}: num: {}' - .format(function_name, num)) - message = "Recent {} titles:\n\n```".format(num) - for result in results: - message += ("\n{}\n{}\n" - .format(str(result[0]), str(result[1]))) - if len(results): - message += "```\n" - else: - message = "There are no news at the moment." - return message - - def pick_a_feed(lang=None): function_name = sys._getframe().f_code.co_name logger.debug('{}: lang: {}' @@ -963,27 +885,6 @@ def pick_a_feed(lang=None): return url -def list_feeds(results): - function_name = sys._getframe().f_code.co_name - logger.debug('{}'.format(function_name)) - message = "\nList of subscriptions:\n\n```\n" - for result in results: - message += ("{} [{}]\n" - "{}\n" - "\n\n" - .format(str(result[1]), str(result[0]), str(result[2]))) - if len(results): - message += ('```\nTotal of {} subscriptions.\n' - .format(len(results))) - else: - url = pick_a_feed() - message = ('List of subscriptions is empty. To add a feed, send a URL.' - '\n' - 'Featured news: *{}*\n{}' - .format(url['name'], url['link'])) - return message - - def list_bookmarks(self, conferences): function_name = sys._getframe().f_code.co_name logger.debug('{}'.format(function_name)) @@ -1014,7 +915,7 @@ def export_to_markdown(jid, filename, results): # TODO Consider adding element jid as a pointer of import def export_to_opml(jid, filename, results): - print(jid, filename, results) + # print(jid, filename, results) function_name = sys._getframe().f_code.co_name logger.debug('{} jid: {} filename: {}' .format(function_name, jid, filename)) diff --git a/slixfeed/cli.py b/slixfeed/cli.py new file mode 100644 index 0000000..71081cd --- /dev/null +++ b/slixfeed/cli.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +# from slixfeed.log import Logger +import socket +import sys + +# logger = Logger(__name__) + +# IPC parameters +ipc_socket_filename = '/tmp/slixfeed_xmpp.socket' + +# Init socket object +if not os.path.exists(ipc_socket_filename): + print(f"File {ipc_socket_filename} doesn't exists") + sys.exit(-1) + +s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +s.connect(ipc_socket_filename) + +# def get_identifier(): +# data = 'identifier' +# # Send request +# s.sendall(data.encode('utf-8')) +# # Wait for response +# datastream = s.recv(1024) +# return datastream.decode('utf-8') + +def send_command(cmd, jid=None): + data = jid + '~' + cmd if jid else cmd + # Send request + s.sendall(data.encode('utf-8')) + # Wait for response + datastream = s.recv(1024) + return datastream.decode('utf-8') + +# identifier = get_identifier() +# print('You are logged in as client #{}.format(identifier)') +print('Type a Jabber ID to commit an action upon.') +jid = input('slixfeed > ') +if not jid: jid = 'admin' + +# TODO if not argument, enter loop. +try: + while True: + # print('Enter an action to act upon Jabber ID {}'.format(jid)) + # print('Enter command:') + # cmd = input('slixfeed #{} ({}) > '.format(identifier, jid)) + cmd = input('slixfeed ({}) > '.format(jid)) + if cmd != '': + match cmd: + case 'switch': + print('Type a Jabber ID to commit an action upon.') + jid = input('slixfeed > ') + if not jid: jid = 'admin' + cmd = '' + case 'exit': + send_command(cmd, jid) + break + case _: + result = send_command(cmd, jid) + print(result) +except KeyboardInterrupt as e: + print(str(e)) + # logger.error(str(e)) + +print('Disconnecting from IPC interface.') +s.close() diff --git a/slixfeed/sqlite.py b/slixfeed/sqlite.py index 813da36..fe99629 100644 --- a/slixfeed/sqlite.py +++ b/slixfeed/sqlite.py @@ -1887,6 +1887,24 @@ def get_entry_url(db_file, ix): return url +def get_entry_summary(db_file, ix): + function_name = sys._getframe().f_code.co_name + logger.debug('{}: db_file: {} ix: {}' + .format(function_name, db_file, ix)) + with create_connection(db_file) as conn: + cur = conn.cursor() + sql = ( + """ + SELECT summary_text + FROM entries_properties + WHERE id = :ix + """ + ) + par = (ix,) + summary = cur.execute(sql, par).fetchone() + return summary + + def get_feed_url(db_file, feed_id): function_name = sys._getframe().f_code.co_name logger.debug('{}: db_file: {} feed_id: {}' @@ -2948,7 +2966,7 @@ def search_feeds(db_file, query): cur = conn.cursor() sql = ( """ - SELECT title, id, url + SELECT id, title, url FROM feeds_properties WHERE title LIKE ? OR url LIKE ? @@ -2960,7 +2978,7 @@ def search_feeds(db_file, query): return result -async def search_entries(db_file, query): +def search_entries(db_file, query): """ Query entries. diff --git a/slixfeed/version.py b/slixfeed/version.py index 80840ab..42422f1 100644 --- a/slixfeed/version.py +++ b/slixfeed/version.py @@ -1,2 +1,2 @@ -__version__ = '0.1.72' -__version_info__ = (0, 1, 72) +__version__ = '0.1.73' +__version_info__ = (0, 1, 73) diff --git a/slixfeed/xmpp/chat.py b/slixfeed/xmpp/chat.py index e049df6..f2bc384 100644 --- a/slixfeed/xmpp/chat.py +++ b/slixfeed/xmpp/chat.py @@ -7,7 +7,7 @@ TODO 1) Deprecate "add" (see above) and make it interactive. Slixfeed: Do you still want to add this URL to subscription list? - See: case _ if message_lowercase.startswith("add"): + See: case _ if command_lowercase.startswith("add"): 2) If subscription is inadequate (see XmppPresence.request), send a message that says so. @@ -38,9 +38,11 @@ import slixfeed.task as task import slixfeed.url as uri from slixfeed.version import __version__ from slixfeed.xmpp.bookmark import XmppBookmark +from slixfeed.xmpp.commands import XmppCommands from slixfeed.xmpp.muc import XmppGroupchat from slixfeed.xmpp.message import XmppMessage from slixfeed.xmpp.presence import XmppPresence +from slixfeed.xmpp.publish import XmppPubsub from slixfeed.xmpp.upload import XmppUpload from slixfeed.xmpp.privilege import is_moderator, is_operator, is_access from slixfeed.xmpp.utility import get_chat_type @@ -69,7 +71,7 @@ class Chat: includes MUC messages and error messages. It is usually a good practice to check the messages's type before processing or sending replies. - + Parameters ---------- message : str @@ -79,13 +81,13 @@ class Chat: """ if message['type'] in ('chat', 'groupchat', 'normal'): jid_bare = message['from'].bare - message_text = ' '.join(message['body'].split()) + command = ' '.join(message['body'].split()) command_time_start = time.time() - + # if (message['type'] == 'groupchat' and # message['muc']['nick'] == self.alias): # return - + # FIXME Code repetition. See below. # TODO Check alias by nickname associated with conference if message['type'] == 'groupchat': @@ -94,40 +96,7 @@ class Chat: jid_full = str(message['from']) if not is_moderator(self, jid_bare, jid_full): return - - # NOTE This is an exceptional case in which we treat - # type groupchat the same as type chat in a way that - # doesn't require an exclamation mark for actionable - # command. - if (message_text.lower().startswith('http') and - message_text.lower().endswith('.opml')): - url = message_text - key_list = ['status'] - task.clean_tasks_xmpp_chat(self, jid_bare, key_list) - status_type = 'dnd' - status_message = '📥️ Procesing request to import feeds...' - # pending_tasks_num = len(self.pending_tasks[jid_bare]) - pending_tasks_num = randrange(10000, 99999) - self.pending_tasks[jid_bare][pending_tasks_num] = status_message - # self.pending_tasks_counter += 1 - # self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message - XmppPresence.send(self, jid_bare, status_message, - status_type=status_type) - db_file = config.get_pathname_to_database(jid_bare) - result = await fetch.http(url) - count = await action.import_opml(db_file, result) - if count: - response = 'Successfully imported {} feeds.'.format(count) - else: - response = 'OPML file was not imported.' - del self.pending_tasks[jid_bare][pending_tasks_num] - # del self.pending_tasks[jid_bare][self.pending_tasks_counter] - key_list = ['status'] - await task.start_tasks_xmpp_chat(self, jid_bare, key_list) - XmppMessage.send_reply(self, message, response) - return - - + if message['type'] == 'groupchat': # nick = message['from'][message['from'].index('/')+1:] # nick = str(message['from']) @@ -178,15 +147,15 @@ class Chat: # os.chdir(db_dir) # if jid + '.db' not in os.listdir(): # await task_jid(jid) - + # await compose.message(self, jid_bare, message) - + if message['type'] == 'groupchat': - message_text = message_text[1:] - message_lowercase = message_text.lower() - - logging.debug([str(message['from']), ':', message_text]) - + command = command[1:] + command_lowercase = command.lower() + + logging.debug([str(message['from']), ':', command]) + # Support private message via groupchat # See https://codeberg.org/poezio/slixmpp/issues/3506 if message['type'] == 'chat' and message.get_plugin('muc', check=True): @@ -197,42 +166,21 @@ class Chat: return response = None - match message_lowercase: - # case 'breakpoint': - # if is_operator(self, jid_bare): - # breakpoint() - # print('task_manager[jid]') - # print(task_manager[jid]) - # await self.get_roster() - # print('roster 1') - # print(self.client_roster) - # print('roster 2') - # print(self.client_roster.keys()) - # print('jid') - # print(jid) - # else: - # response = ( - # 'This action is restricted. ' - # 'Type: breakpoint.' - # ) - # XmppMessage.send_reply(self, message, response) + db_file = config.get_pathname_to_database(jid_bare) + match command_lowercase: case 'help': - command_list = ' '.join(action.manual('commands.toml')) + command_list = XmppCommands.print_help() response = ('Available command keys:\n' '```\n{}\n```\n' 'Usage: `help `' .format(command_list)) - print(response) - XmppMessage.send_reply(self, message, response) case 'help all': command_list = action.manual('commands.toml', section='all') response = ('Complete list of commands:\n' '```\n{}\n```' .format(command_list)) - print(response) - XmppMessage.send_reply(self, message, response) - case _ if message_lowercase.startswith('help'): - command = message_text[5:].lower() + case _ if command_lowercase.startswith('help'): + command = command[5:].lower() command = command.split(' ') if len(command) == 2: command_root = command[0] @@ -260,161 +208,33 @@ class Chat: else: response = ('Invalid. Enter command key ' 'or command key & name') - XmppMessage.send_reply(self, message, response) case 'info': - config_dir = config.get_default_config_directory() - with open(config_dir + '/' + 'information.toml', mode="rb") as information: - entries = tomllib.load(information) + entries = XmppCommands.print_info_list() response = ('Available command options:\n' '```\n{}\n```\n' 'Usage: `info