diff --git a/slixfeed/__main__.py b/slixfeed/__main__.py index f9ac5c7..51ab358 100644 --- a/slixfeed/__main__.py +++ b/slixfeed/__main__.py @@ -129,7 +129,7 @@ class JabberComponent: xmpp.register_plugin('xep_0066') # Out of Band Data xmpp.register_plugin('xep_0071') # XHTML-IM xmpp.register_plugin('xep_0084') # User Avatar - xmpp.register_plugin('xep_0085') # Chat State Notifications + # xmpp.register_plugin('xep_0085') # Chat State Notifications xmpp.register_plugin('xep_0115') # Entity Capabilities xmpp.register_plugin('xep_0153') # vCard-Based Avatars xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping @@ -155,7 +155,7 @@ class JabberClient: xmpp.register_plugin('xep_0066') # Out of Band Data xmpp.register_plugin('xep_0071') # XHTML-IM xmpp.register_plugin('xep_0084') # User Avatar - xmpp.register_plugin('xep_0085') # Chat State Notifications + # xmpp.register_plugin('xep_0085') # Chat State Notifications xmpp.register_plugin('xep_0115') # Entity Capabilities xmpp.register_plugin('xep_0153') # vCard-Based Avatars xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping diff --git a/slixfeed/action.py b/slixfeed/action.py index 5349ab7..594c71d 100644 --- a/slixfeed/action.py +++ b/slixfeed/action.py @@ -48,7 +48,10 @@ from slixfeed.url import ( replace_hostname, trim_url ) -import slixfeed.xmpp.bookmark as bookmark +import slixfeed.task as task +from slixfeed.xmpp.bookmark import XmppBookmark +from slixfeed.xmpp.message import XmppMessage +from slixfeed.xmpp.status import XmppStatus import tomllib from urllib import error from urllib.parse import parse_qs, urlsplit @@ -56,28 +59,28 @@ import xml.etree.ElementTree as ET try: import xml2epub -except: +except ImportError: logging.info( "Package xml2epub was not found.\n" "ePUB support is disabled.") try: import html2text -except: +except ImportError: logging.info( "Package html2text was not found.\n" "Markdown support is disabled.") try: import pdfkit -except: +except ImportError: logging.info( "Package pdfkit was not found.\n" "PDF support is disabled.") try: from readability import Document -except: +except ImportError: logging.info( "Package readability was not found.\n" "Arc90 Lab algorithm is disabled.") @@ -107,6 +110,45 @@ def manual(filename, section=None, command=None): return cmd_list +async def xmpp_change_interval(self, key, val, jid, jid_file, message=None, session=None): + if val: + # response = ( + # 'Updates will be sent every {} minutes.' + # ).format(response) + db_file = config.get_pathname_to_database(jid_file) + if await sqlite.get_settings_value(db_file, key): + await sqlite.update_settings_value(db_file, [key, val]) + else: + await sqlite.set_settings_value(db_file, [key, val]) + # NOTE Perhaps this should be replaced + # by functions clean and start + await task.refresh_task(self, jid, task.send_update, + key, val) + response = ('Updates will be sent every {} minutes.' + .format(val)) + else: + response = 'Missing value.' + if message: + XmppMessage.send_reply(self, message, response) + if session: + await XmppMessage.send(self, jid, response, chat_type='chat') + + +async def xmpp_stop_updates(self, message, jid, jid_file): + key = 'enabled' + val = 0 + db_file = config.get_pathname_to_database(jid_file) + if await sqlite.get_settings_value(db_file, key): + await sqlite.update_settings_value(db_file, [key, val]) + else: + await sqlite.set_settings_value(db_file, [key, val]) + await task.clean_tasks_xmpp(jid, ['interval', 'status']) + response = 'Updates are disabled.' + XmppMessage.send_reply(self, message, response) + status_type = 'xa' + status_message = '💡️ Send "Start" to receive Jabber updates' + await XmppStatus.send(self, jid, status_message, status_type) + def log_to_markdown(timestamp, filename, jid, message): """ Log message to file. @@ -404,7 +446,7 @@ def list_feeds(results): async def list_bookmarks(self): - conferences = await bookmark.get(self) + conferences = await XmppBookmark.get(self) message = "\nList of groupchats:\n\n```\n" for conference in conferences: message += ( diff --git a/slixfeed/assets/commands.toml b/slixfeed/assets/commands.toml index d4eef47..3245d2a 100644 --- a/slixfeed/assets/commands.toml +++ b/slixfeed/assets/commands.toml @@ -1,7 +1,7 @@ [action] url = """ -Add given to subscription list. +Add given to subscription list (prefix http. Default). """ add = """ add @@ -30,6 +30,17 @@ backup news text Send a Plain Text file of your news items. """ +[bookmarks] +bookmark = """ +bookmark [+|-] <muc> +Groupchat to add or remove. +'+' appends to, '-' removes from. +""" +bookmarks = """ +bookmarks +List bookmarked groupchats. +""" + [custom] new = """ new @@ -70,10 +81,18 @@ Reset deny list. """ [groupchat] +uri = """ +<muc> +Join groupchat by given <muc> (prefix xmpp). +""" join = """ join <muc> Join groupchat by given <muc>. """ +leave = """ +goodbye +Leave groupchat and delete it from bookmarks. +""" [manual] help = """ @@ -122,10 +141,18 @@ Set maximum length of news item description. (0 for no limit) """ quantum = """ quantum <number> -Set amount of updates per interval by given <number>. +Set amount of updates per message by given <number>. +""" +random = """ +random +Send messages by random order instead of date. """ [modification] +archive = """ +archive <number> +Number of news items to archive (maximum value 500). +""" remove = """ remove <id> Remove feed of from subscription list by given <id>. @@ -158,7 +185,7 @@ Disable bot and stop updates. [preview] read = """ read <url> -Display most recent 20 titles of given <url>. +Display most recent 5 titles of given <url>. """ read_num = """ read <url> <index> @@ -180,10 +207,14 @@ Search news items by given <text>. """ recent = """ recent <number> -List recent <number> news items (up to 50 items). +List recent <number> news items (max. 50). """ [statistics] +stats = """ +stats +Show general statistics. +""" analyses = """ analyses Show report and statistics of feeds. diff --git a/slixfeed/assets/information.toml b/slixfeed/assets/information.toml index 79537f3..0f6498a 100644 --- a/slixfeed/assets/information.toml +++ b/slixfeed/assets/information.toml @@ -61,7 +61,7 @@ No operator was specified for this instance. platforms = """ Supported platforms: XMPP -Platforms to be added in future: Briar, Email, IRC, Matrix, MQTT, Tox. +Platforms to be added in future: Briar, Email, IRC, Matrix, MQTT, Nostr, Tox. For ideal experience, we recommend using XMPP. """ @@ -97,27 +97,36 @@ https://gitgud.io/sjehuda/slixfeed """ thanks = """ -Alixander Court (alixandercourt.com, Utah), \ +Alixander Court <alixandercourt.com> (Utah), \ +Chriss Farrell (SalixOS, Oregon), \ Christian Dersch (SalixOS), \ -Cyrille Pontvieux (SalixOS, France), \ +Cyrille Pontvieux <enialis.net> (SalixOS, France), \ Denis Fomin (Gajim, Russia), \ Dimitris Tzemos (SalixOS, Greece), \ Emmanuel Gil Peyrot (Poezio, France), \ Florent Le Coz (Poezio, France), \ -George Vlahavas (SalixOS, Greece), \ -Guus der Kinderen from IgniteRealtime.org (Openfire, Netherlands), \ +George Vlahavas <vlahavas.com> (SalixOS, Greece), \ +Guus der Kinderen <igniterealtime.org> (Openfire, Netherlands), \ habnabit_ from #python on irc.libera.chat, \ +Imar van Erven Dorens <simplicit.nl> (SalixOS, Netherlands), \ imattau (atomtopubsub), \ -Jaussoin Timothée (Movim, France), \ -Kevin Smith from Isode (Swift, Wales), \ +Jaussoin Timothée <mov.im> (Movim, France), \ +Justin Karneges <jblog.andbit.net> (Psi, California), \ +Kevin Smith <isode.com> (Swift IM, Wales), \ +Luis Henrique Mello (SalixOS, Brazil), \ magicfelix, \ +Markus Muttilainen (SalixOS), \ Mathieu Pasquet (slixmpp, France), \ Maxime Buquet (slixmpp, France), \ +Phillip Watkins (United Kingdom, SalixOS), \ Pierrick Le Brun (SalixOS, France), \ Raphael Groner (Fedora, Germany), \ -Remko Tronçon (Swift, Germany), \ -Simone "roughnecks" Canaletti (woodpeckersnest.space, Italy), \ +Remko Tronçon <mko.re> (Psi , Belgium), \ +Simone "roughnecks" Canaletti <woodpeckersnest.space> (Italy), \ +Richard Lapointe (SalixOS, Connecticut), \ Strix from Loqi, \ +Thibaud Guerin (SalixOS), \ +Tim Beech (SalixOS, Brazil), \ Thorsten Mühlfelder (SalixOS, Germany), \ Yann Leboulanger (Gajim, France). """ diff --git a/slixfeed/xmpp/bookmark.py b/slixfeed/xmpp/bookmark.py index 32e8549..ddc123a 100644 --- a/slixfeed/xmpp/bookmark.py +++ b/slixfeed/xmpp/bookmark.py @@ -12,55 +12,58 @@ TODO from slixmpp.plugins.xep_0048.stanza import Bookmarks -async def get(self): - result = await self.plugin['xep_0048'].get_bookmarks() - bookmarks = result['private']['bookmarks'] - conferences = bookmarks['conferences'] - return conferences +class XmppBookmark: -async def add(self, muc_jid): - result = await self.plugin['xep_0048'].get_bookmarks() - bookmarks = result['private']['bookmarks'] - conferences = bookmarks['conferences'] - mucs = [] - for conference in conferences: - jid = conference['jid'] - mucs.extend([jid]) - if muc_jid not in mucs: - bookmarks = Bookmarks() - mucs.extend([muc_jid]) - for muc in mucs: - bookmarks.add_conference( - muc, - self.alias, - autojoin=True - ) - await self.plugin['xep_0048'].set_bookmarks(bookmarks) - # bookmarks = Bookmarks() - # await self.plugin['xep_0048'].set_bookmarks(bookmarks) - # print(await self.plugin['xep_0048'].get_bookmarks()) - - # bm = BookmarkStorage() - # bm.conferences.append(Conference(muc_jid, autojoin=True, nick=self.alias)) - # await self['xep_0402'].publish(bm) + async def get(self): + result = await self.plugin['xep_0048'].get_bookmarks() + bookmarks = result['private']['bookmarks'] + conferences = bookmarks['conferences'] + return conferences -async def remove(self, muc_jid): - result = await self.plugin['xep_0048'].get_bookmarks() - bookmarks = result['private']['bookmarks'] - conferences = bookmarks['conferences'] - mucs = [] - for conference in conferences: - jid = conference['jid'] - mucs.extend([jid]) - if muc_jid in mucs: - bookmarks = Bookmarks() - mucs.remove(muc_jid) - for muc in mucs: - bookmarks.add_conference( - muc, - self.alias, - autojoin=True - ) - await self.plugin['xep_0048'].set_bookmarks(bookmarks) + async def add(self, muc_jid): + result = await self.plugin['xep_0048'].get_bookmarks() + bookmarks = result['private']['bookmarks'] + conferences = bookmarks['conferences'] + mucs = [] + for conference in conferences: + jid = conference['jid'] + mucs.extend([jid]) + if muc_jid not in mucs: + bookmarks = Bookmarks() + mucs.extend([muc_jid]) + for muc in mucs: + bookmarks.add_conference( + muc, + self.alias, + autojoin=True + ) + await self.plugin['xep_0048'].set_bookmarks(bookmarks) + # bookmarks = Bookmarks() + # await self.plugin['xep_0048'].set_bookmarks(bookmarks) + # print(await self.plugin['xep_0048'].get_bookmarks()) + + # bm = BookmarkStorage() + # bm.conferences.append(Conference(muc_jid, autojoin=True, nick=self.alias)) + # await self['xep_0402'].publish(bm) + + + async def remove(self, muc_jid): + result = await self.plugin['xep_0048'].get_bookmarks() + bookmarks = result['private']['bookmarks'] + conferences = bookmarks['conferences'] + mucs = [] + for conference in conferences: + jid = conference['jid'] + mucs.extend([jid]) + if muc_jid in mucs: + bookmarks = Bookmarks() + mucs.remove(muc_jid) + for muc in mucs: + bookmarks.add_conference( + muc, + self.alias, + autojoin=True + ) + await self.plugin['xep_0048'].set_bookmarks(bookmarks) diff --git a/slixfeed/xmpp/client.py b/slixfeed/xmpp/client.py index e9ae151..8740245 100644 --- a/slixfeed/xmpp/client.py +++ b/slixfeed/xmpp/client.py @@ -60,7 +60,9 @@ from slixmpp.plugins.xep_0048.stanza import Bookmarks # import xml.etree.ElementTree as ET # from lxml import etree -import slixfeed.xmpp.bookmark as bookmark +import slixfeed.config as config +import slixfeed.sqlite as sqlite +from slixfeed.xmpp.bookmark import XmppBookmark import slixfeed.xmpp.connect as connect import slixfeed.xmpp.muc as muc import slixfeed.xmpp.process as process @@ -169,7 +171,7 @@ class Slixfeed(slixmpp.ClientXMPP): inviter = message["from"].bare muc_jid = message['groupchat_invite']['jid'] await muc.join(self, inviter, muc_jid) - await bookmark.add(self, muc_jid) + await XmppBookmark.add(self, muc_jid) # NOTE Tested with Gajim and Psi @@ -177,7 +179,7 @@ class Slixfeed(slixmpp.ClientXMPP): inviter = message["from"].bare muc_jid = message['groupchat_invite']['jid'] await muc.join(self, inviter, muc_jid) - await bookmark.add(self, muc_jid) + await XmppBookmark.add(self, muc_jid) async def on_session_end(self, event): @@ -191,17 +193,32 @@ class Slixfeed(slixmpp.ClientXMPP): async def on_session_start(self, event): - await process.event(self) + self.send_presence() + await self["xep_0115"].update_caps() + await self.get_roster() await muc.autojoin(self) profile.set_identity(self, "client") await profile.update(self) task.ping_task(self) + + # Service.commands(self) + # Service.reactions(self) + + self.service_commands() + self.service_reactions() async def on_session_resumed(self, event): - await process.event(self) + self.send_presence() + self["xep_0115"].update_caps() await muc.autojoin(self) profile.set_identity(self, "client") + + # Service.commands(self) + # Service.reactions(self) + + self.service_commands() + self.service_reactions() # TODO Request for subscription @@ -305,3 +322,301 @@ class Slixfeed(slixmpp.ClientXMPP): jid = message['from'].bare # await task.clean_tasks_xmpp(jid, ['status']) await task.start_tasks_xmpp(self, jid, ['status']) + + +# TODO Move class Service to a separate file +# class Service(Slixfeed): +# def __init__(self): +# super().__init__() + +# TODO https://xmpp.org/extensions/xep-0115.html +# https://xmpp.org/extensions/xep-0444.html#disco + + + # TODO https://xmpp.org/extensions/xep-0444.html#disco-restricted + def service_reactions(self): + """ + Publish allow list of reactions. + + Parameters + ---------- + None. + + Returns + ------- + None. + + """ + form = self['xep_0004'].make_form( + 'form', 'Reactions Information' + ) + + +# TODO Move class Command to a separate file +# class Command(Slixfeed): +# def __init__(self): +# super().__init__() + + + def service_commands(self): + # self["xep_0050"].add_command( + # node="updates_enable", + # name="Enable/Disable News Updates", + # handler=option_enable_updates, + # ) + + # if jid == config.get_value('accounts', 'XMPP', 'operator'): + # self['xep_0050'].add_command(node='bookmarks', + # name='Bookmarks', + # handler=self._handle_bookmarks) + # self['xep_0050'].add_command(node='roster', + # name='Roster', + # handler=self._handle_roster) + self['xep_0050'].add_command(node='settings', + name='Settings', + handler=self._handle_settings) + self['xep_0050'].add_command(node='subscriptions', + name='Subscriptions', + handler=self._handle_subscriptions) + # self['xep_0050'].add_command(node='search', + # name='Search', + # handler=self._handle_search) + # self['xep_0050'].add_command(node='filters', + # name='Filters', + # handler=self._handle_filters) + + + async def _handle_subscriptions(self, iq, session): + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = '📰️ Manage subscriptions.' + # form.addField(var='interval', + # ftype='text-single', + # label='Interval period') + options = form.add_field(var='subscriptions', + ftype='list-multi', + label='Select subscriptions', + desc='Select subscription(s) to edit.') + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + subscriptions = await sqlite.get_feeds(db_file) + for subscription in subscriptions: + title = subscription[0] + url = subscription[1] + options.addOption(title, url) + session['payload'] = form + session['next'] = self._handle_subscription_editor + session['has_next'] = True + # Other useful session values: + # session['to'] -- The JID that received the + # command request. + # session['from'] -- The JID that sent the + # command request. + # session['has_next'] = True -- There are more steps to complete + # session['allow_complete'] = True -- Allow user to finish immediately + # and possibly skip steps + # session['cancel'] = handler -- Assign a handler for if the user + # cancels the command. + # session['notes'] = [ -- Add informative notes about the + # ('info', 'Info message'), command's results. + # ('warning', 'Warning message'), + # ('error', 'Error message')] + return session + + + # TODO Make form for a single subscription and several subscriptions + # single: Delete, Disable, Reset and Rename + # several: Delete, Disable, Reset + async def _handle_subscription_editor(self, iq, session): + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = '🗞️ Edit subscriptions.' + options = form.add_field(var='enable', + ftype='boolean', + label='Enable', + value=True) + options = form.add_field(var='action', + ftype='list-single', + label='Action', + value='reset') + options.addOption('Delete', 'delete') + options.addOption('Reset', 'reset') + session['payload'] = form + session['next'] = None + session['has_next'] = False + return session + + + async def _handle_bookmarks(self, iq, session): + form = self['xep_0004'].make_form('form', 'Bookmarks') + form['instructions'] = '📑️ Organize bookmarks.' + options = form.add_field(var='bookmarks', + # ftype='list-multi' + ftype='list-single', + label='Select a bookmark', + desc='Select a bookmark to edit.') + conferences = await XmppBookmark.get(self) + for conference in conferences: + options.addOption(conference['jid'], conference['jid']) + session['payload'] = form + session['next'] = self._handle_command_complete + session['has_next'] = False + return session + + + async def _handle_bookmarks_editor(self, iq, session): + form = self['xep_0004'].make_form('form', 'Bookmarks') + form['instructions'] = '📝️ Edit bookmarks.' + form.addField(var='name', + ftype='text-single', + label='Name') + form.addField(var='host', + ftype='text-single', + label='Host', + required=True) + form.addField(var='room', + ftype='text-single', + label='Room', + required=True) + form.addField(var='alias', + ftype='text-single', + label='Alias') + form.addField(var='password', + ftype='text-private', + label='Password') + form.add_field(var='autojoin', + ftype='boolean', + label='Auto-join', + value=True) + 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['payload'] = form + session['next'] = None + session['has_next'] = False + 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. + """ + form = self['xep_0004'].make_form('form', 'Settings') + form['instructions'] = ('📮️ Customize news updates.') + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + value = await 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='Enable', + desc='Enable news updates.', + value=value) + value = await config.get_setting_value(db_file, 'old') + value = int(value) + if value: + value = False + else: + value = True + form.add_field(var='old', + ftype='boolean', + desc='Mark items of newly added subscriptions as read.', + # label='Send only new items', + label='Include old news', + value=value) + value = await config.get_setting_value(db_file, 'interval') + value = str(int(value/60)) + options = form.add_field(var='interval', + ftype='list-single', + label='Interval', + desc='Set interval update (in hours).', + value=value) + i = 60 + while i <= 2880: + var = str(i) + lab = str(int(i/60)) + options.addOption(lab, var) + i += 60 + value = await 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) + i = 0 + while i <= 500: + x = str(i) + options.addOption(x, x) + i += 1 + value = await config.get_setting_value(db_file, 'quantum') + value = str(value) + options = form.add_field(var='quantum', + ftype='list-single', + label='Amount', + desc='Set amount of updates per update.', + value='3') + i = 1 + while i <= 10: + x = str(i) + options.addOption(x, x) + i += 1 + session['payload'] = form + session['next'] = self._handle_settings_complete + session['has_next'] = False + return session + + + async def _handle_settings_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. + """ + + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + # In this case (as is typical), the payload is a form + form = payload + values = form['values'] + for value in values: + key = value + val = values[value] + if await sqlite.get_settings_value(db_file, key): + await sqlite.update_settings_value(db_file, [key, val]) + else: + await sqlite.set_settings_value(db_file, [key, val]) + match value: + case 'enabled': + pass + case 'interval': + pass + # Having no return statement is the same as unsetting the 'payload' + # and 'next' session values and returning the session. + # Unless it is the final step, always return the session dictionary. + session['payload'] = None + session['next'] = None + return session diff --git a/slixfeed/xmpp/component.py b/slixfeed/xmpp/component.py index 89cd8eb..7f02afa 100644 --- a/slixfeed/xmpp/component.py +++ b/slixfeed/xmpp/component.py @@ -60,7 +60,9 @@ from slixmpp.plugins.xep_0048.stanza import Bookmarks # import xml.etree.ElementTree as ET # from lxml import etree -# import slixfeed.xmpp.bookmark as bookmark +import slixfeed.config as config +import slixfeed.sqlite as sqlite +from slixfeed.xmpp.bookmark import XmppBookmark import slixfeed.xmpp.connect as connect # NOTE MUC is possible for component # import slixfeed.xmpp.muc as muc @@ -97,35 +99,54 @@ class SlixfeedComponent(slixmpp.ComponentXMPP): # and the XML streams are ready for use. We want to # listen for this event so that we we can initialize # our roster. - self.add_event_handler("session_start", self.on_session_start) - self.add_event_handler("session_resumed", self.on_session_resumed) + self.add_event_handler("session_start", + self.on_session_start) + self.add_event_handler("session_resumed", + self.on_session_resumed) self.add_event_handler("got_offline", print("got_offline")) # self.add_event_handler("got_online", self.check_readiness) - self.add_event_handler("changed_status", self.on_changed_status) - self.add_event_handler("presence_available", self.on_presence_available) - self.add_event_handler("presence_unavailable", self.on_presence_unavailable) - self.add_event_handler("chatstate_active", self.on_chatstate_active) - self.add_event_handler("chatstate_gone", self.on_chatstate_gone) - self.add_event_handler("chatstate_composing", self.check_chatstate_composing) - self.add_event_handler("chatstate_paused", self.check_chatstate_paused) + self.add_event_handler("changed_status", + self.on_changed_status) + self.add_event_handler("presence_available", + self.on_presence_available) + self.add_event_handler("presence_unavailable", + self.on_presence_unavailable) + self.add_event_handler("chatstate_active", + self.on_chatstate_active) + self.add_event_handler("chatstate_composing", + self.on_chatstate_composing) + self.add_event_handler("chatstate_gone", + self.on_chatstate_gone) + self.add_event_handler("chatstate_inactive", + self.on_chatstate_inactive) + self.add_event_handler("chatstate_paused", + self.on_chatstate_paused) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. - self.add_event_handler("message", self.on_message) + self.add_event_handler("message", + self.on_message) - self.add_event_handler("groupchat_invite", self.on_groupchat_invite) # XEP_0045 - self.add_event_handler("groupchat_direct_invite", self.on_groupchat_direct_invite) # XEP_0249 + self.add_event_handler("groupchat_invite", + self.on_groupchat_invite) # XEP_0045 + self.add_event_handler("groupchat_direct_invite", + self.on_groupchat_direct_invite) # XEP_0249 # self.add_event_handler("groupchat_message", self.message) # self.add_event_handler("disconnected", self.reconnect) # self.add_event_handler("disconnected", self.inspect_connection) - self.add_event_handler("reactions", self.on_reactions) - self.add_event_handler("presence_error", self.on_presence_error) - self.add_event_handler("presence_subscribe", self.on_presence_subscribe) - self.add_event_handler("presence_subscribed", self.on_presence_subscribed) - self.add_event_handler("presence_unsubscribed", self.on_presence_unsubscribed) + self.add_event_handler("reactions", + self.on_reactions) + self.add_event_handler("presence_error", + self.on_presence_error) + self.add_event_handler("presence_subscribe", + self.on_presence_subscribe) + self.add_event_handler("presence_subscribed", + self.on_presence_subscribed) + self.add_event_handler("presence_unsubscribed", + self.on_presence_unsubscribed) # Initialize event loop # self.loop = asyncio.get_event_loop() @@ -159,37 +180,34 @@ class SlixfeedComponent(slixmpp.ComponentXMPP): async def on_connection_failed(self, event): - message = "Connection has failed. Reason: {}".format(event) + message = "Connection has failed. Reason: {}".format(event) await connect.recover_connection(self, message) async def on_session_start(self, event): self.send_presence() - await process.event_component(self) + await self["xep_0115"].update_caps() # await muc.autojoin(self) profile.set_identity(self, "service") await profile.update(self) - connect.ping_task(self) + task.ping_task(self) - # await Service.capabilities(self) # Service.commands(self) # Service.reactions(self) - await self.service_capabilities() self.service_commands() self.service_reactions() async def on_session_resumed(self, event): - await process.event_component(self) + self.send_presence() + self["xep_0115"].update_caps() # await muc.autojoin(self) profile.set_identity(self, "service") - # await Service.capabilities(self) # Service.commands(self) # Service.reactions(self) - await self.service_capabilities() self.service_commands() self.service_reactions() @@ -207,7 +225,11 @@ class SlixfeedComponent(slixmpp.ComponentXMPP): async def on_changed_status(self, presence): - await task.check_readiness(self, presence) + # await task.check_readiness(self, presence) + jid = presence['from'].bare + if presence['show'] in ('away', 'dnd', 'xa'): + await task.clean_tasks_xmpp(jid, ['interval']) + await task.start_tasks_xmpp(self, jid, ['status', 'check']) async def on_presence_subscribe(self, presence): @@ -228,7 +250,10 @@ class SlixfeedComponent(slixmpp.ComponentXMPP): async def on_presence_available(self, presence): # TODO Add function to check whether task is already running or not - await task.start_tasks(self, presence) + # await task.start_tasks(self, presence) + # NOTE Already done inside the start-task function + jid = presence["from"].bare + await task.start_tasks_xmpp(self, jid) async def on_presence_unsubscribed(self, presence): @@ -237,12 +262,20 @@ class SlixfeedComponent(slixmpp.ComponentXMPP): async def on_presence_unavailable(self, presence): jid = presence["from"].bare - await task.stop_tasks(self, jid) + # await task.stop_tasks(self, jid) + await task.clean_tasks_xmpp(jid) + # TODO + # Send message that database will be deleted within 30 days + # Check whether JID is in bookmarks or roster + # If roster, remove contact JID into file + # If bookmarks, remove groupchat JID into file async def on_presence_error(self, presence): print("on_presence_error") print(presence) + jid = presence["from"].bare + await task.clean_tasks_xmpp(jid) async def on_reactions(self, message): @@ -253,28 +286,332 @@ class SlixfeedComponent(slixmpp.ComponentXMPP): async def on_chatstate_active(self, message): if message['type'] in ('chat', 'normal'): jid = message['from'].bare - await task.clean_tasks_xmpp(jid, ['status']) + # await task.clean_tasks_xmpp(jid, ['status']) await task.start_tasks_xmpp(self, jid, ['status']) + async def on_chatstate_composing(self, message): + if message['type'] in ('chat', 'normal'): + jid = message['from'].bare + # await task.clean_tasks_xmpp(jid, ['status']) + status_text='Press "help" for manual, or "info" for information.' + status.send(self, jid, status_text) + + async def on_chatstate_gone(self, message): if message['type'] in ('chat', 'normal'): jid = message['from'].bare - await task.clean_tasks_xmpp(jid, ['status']) + # await task.clean_tasks_xmpp(jid, ['status']) await task.start_tasks_xmpp(self, jid, ['status']) - async def check_chatstate_composing(self, message): + async def on_chatstate_inactive(self, message): if message['type'] in ('chat', 'normal'): jid = message['from'].bare - await task.clean_tasks_xmpp(jid, ['status']) - status_text='Press "help" for manual, or "info" for information.' - status.send(self, jid, status_text) - - - async def check_chatstate_paused(self, message): - if message['type'] in ('chat', 'normal'): - jid = message['from'].bare - await task.clean_tasks_xmpp(jid, ['status']) + # await task.clean_tasks_xmpp(jid, ['status']) await task.start_tasks_xmpp(self, jid, ['status']) + + async def on_chatstate_paused(self, message): + if message['type'] in ('chat', 'normal'): + jid = message['from'].bare + # await task.clean_tasks_xmpp(jid, ['status']) + await task.start_tasks_xmpp(self, jid, ['status']) + + +# TODO Move class Service to a separate file +# class Service(Slixfeed): +# def __init__(self): +# super().__init__() + +# TODO https://xmpp.org/extensions/xep-0115.html +# https://xmpp.org/extensions/xep-0444.html#disco + + + # TODO https://xmpp.org/extensions/xep-0444.html#disco-restricted + def service_reactions(self): + """ + Publish allow list of reactions. + + Parameters + ---------- + None. + + Returns + ------- + None. + + """ + form = self['xep_0004'].make_form( + 'form', 'Reactions Information' + ) + + +# TODO Move class Command to a separate file +# class Command(Slixfeed): +# def __init__(self): +# super().__init__() + + + def service_commands(self): + # self["xep_0050"].add_command( + # node="updates_enable", + # name="Enable/Disable News Updates", + # handler=option_enable_updates, + # ) + + # if jid == config.get_value('accounts', 'XMPP', 'operator'): + # self['xep_0050'].add_command(node='bookmarks', + # name='Bookmarks', + # handler=self._handle_bookmarks) + # self['xep_0050'].add_command(node='roster', + # name='Roster', + # handler=self._handle_roster) + self['xep_0050'].add_command(node='settings', + name='Settings', + handler=self._handle_settings) + self['xep_0050'].add_command(node='subscriptions', + name='Subscriptions', + handler=self._handle_subscriptions) + # self['xep_0050'].add_command(node='search', + # name='Search', + # handler=self._handle_search) + # self['xep_0050'].add_command(node='filters', + # name='Filters', + # handler=self._handle_filters) + + + async def _handle_subscriptions(self, iq, session): + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = '📰️ Manage subscriptions.' + # form.addField(var='interval', + # ftype='text-single', + # label='Interval period') + options = form.add_field(var='subscriptions', + ftype='list-multi', + label='Select subscriptions', + desc='Select subscription(s) to edit.') + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + subscriptions = await sqlite.get_feeds(db_file) + for subscription in subscriptions: + title = subscription[0] + url = subscription[1] + options.addOption(title, url) + session['payload'] = form + session['next'] = self._handle_subscription_editor + session['has_next'] = True + # Other useful session values: + # session['to'] -- The JID that received the + # command request. + # session['from'] -- The JID that sent the + # command request. + # session['has_next'] = True -- There are more steps to complete + # session['allow_complete'] = True -- Allow user to finish immediately + # and possibly skip steps + # session['cancel'] = handler -- Assign a handler for if the user + # cancels the command. + # session['notes'] = [ -- Add informative notes about the + # ('info', 'Info message'), command's results. + # ('warning', 'Warning message'), + # ('error', 'Error message')] + return session + + + # TODO Make form for a single subscription and several subscriptions + # single: Delete, Disable, Reset and Rename + # several: Delete, Disable, Reset + async def _handle_subscription_editor(self, iq, session): + form = self['xep_0004'].make_form('form', 'Subscriptions') + form['instructions'] = '🗞️ Edit subscriptions.' + options = form.add_field(var='enable', + ftype='boolean', + label='Enable', + value=True) + options = form.add_field(var='action', + ftype='list-single', + label='Action', + value='reset') + options.addOption('Delete', 'delete') + options.addOption('Reset', 'reset') + session['payload'] = form + session['next'] = None + session['has_next'] = False + return session + + + async def _handle_bookmarks(self, iq, session): + form = self['xep_0004'].make_form('form', 'Bookmarks') + form['instructions'] = '📑️ Organize bookmarks.' + options = form.add_field(var='bookmarks', + # ftype='list-multi' + ftype='list-single', + label='Select a bookmark', + desc='Select a bookmark to edit.') + conferences = await XmppBookmark.get(self) + for conference in conferences: + options.addOption(conference['jid'], conference['jid']) + session['payload'] = form + session['next'] = self._handle_command_complete + session['has_next'] = False + return session + + + async def _handle_bookmarks_editor(self, iq, session): + form = self['xep_0004'].make_form('form', 'Bookmarks') + form['instructions'] = '📝️ Edit bookmarks.' + form.addField(var='name', + ftype='text-single', + label='Name') + form.addField(var='host', + ftype='text-single', + label='Host', + required=True) + form.addField(var='room', + ftype='text-single', + label='Room', + required=True) + form.addField(var='alias', + ftype='text-single', + label='Alias') + form.addField(var='password', + ftype='text-private', + label='Password') + form.add_field(var='autojoin', + ftype='boolean', + label='Auto-join', + value=True) + 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['payload'] = form + session['next'] = None + session['has_next'] = False + 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. + """ + form = self['xep_0004'].make_form('form', 'Settings') + form['instructions'] = ('📮️ Customize news updates.') + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + value = await 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='Enable', + desc='Enable news updates.', + value=value) + value = await config.get_setting_value(db_file, 'old') + value = int(value) + if value: + value = False + else: + value = True + form.add_field(var='old', + ftype='boolean', + desc='Mark items of newly added subscriptions as read.', + # label='Send only new items', + label='Include old news', + value=value) + value = await config.get_setting_value(db_file, 'interval') + value = str(int(value/60)) + options = form.add_field(var='interval', + ftype='list-single', + label='Interval', + desc='Set interval update (in hours).', + value=value) + i = 60 + while i <= 2880: + var = str(i) + lab = str(int(i/60)) + options.addOption(lab, var) + i += 60 + value = await 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) + i = 0 + while i <= 500: + x = str(i) + options.addOption(x, x) + i += 1 + value = await config.get_setting_value(db_file, 'quantum') + value = str(value) + options = form.add_field(var='quantum', + ftype='list-single', + label='Amount', + desc='Set amount of updates per update.', + value='3') + i = 1 + while i <= 10: + x = str(i) + options.addOption(x, x) + i += 1 + session['payload'] = form + session['next'] = self._handle_settings_complete + session['has_next'] = False + return session + + + async def _handle_settings_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. + """ + + jid = session['from'].bare + jid_file = jid + db_file = config.get_pathname_to_database(jid_file) + # In this case (as is typical), the payload is a form + form = payload + values = form['values'] + for value in values: + key = value + val = values[value] + if await sqlite.get_settings_value(db_file, key): + await sqlite.update_settings_value(db_file, [key, val]) + else: + await sqlite.set_settings_value(db_file, [key, val]) + match value: + case 'enabled': + pass + case 'interval': + pass + # Having no return statement is the same as unsetting the 'payload' + # and 'next' session values and returning the session. + # Unless it is the final step, always return the session dictionary. + session['payload'] = None + session['next'] = None + return session diff --git a/slixfeed/xmpp/message.py b/slixfeed/xmpp/message.py new file mode 100644 index 0000000..378d67c --- /dev/null +++ b/slixfeed/xmpp/message.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from slixfeed.xmpp.utility import get_chat_type + + +class XmppMessage: + + async def send(self, jid, message, chat_type=None): + if not chat_type: + chat_type = await get_chat_type(self, jid) + self.send_message( + mto=jid, + mfrom=self.boundjid.bare, + mbody=message, + mtype=chat_type + ) + + + async def send_oob(self, jid, url): + chat_type = await get_chat_type(self, jid) + html = ( + f'<body xmlns="http://www.w3.org/1999/xhtml">' + f'<a href="{url}">{url}</a></body>') + message = self.make_message( + mto=jid, + mfrom=self.boundjid.bare, + mbody=url, + mhtml=html, + mtype=chat_type + ) + message['oob']['url'] = url + message.send() + + + def send_reply(self, message, response): + message.reply(response).send() \ No newline at end of file diff --git a/slixfeed/xmpp/muc.py b/slixfeed/xmpp/muc.py index b5536b4..f699eeb 100644 --- a/slixfeed/xmpp/muc.py +++ b/slixfeed/xmpp/muc.py @@ -17,7 +17,6 @@ FIXME """ import logging -import slixfeed.xmpp.bookmark as bookmark import slixfeed.xmpp.process as process from slixfeed.dt import current_time diff --git a/slixfeed/xmpp/process.py b/slixfeed/xmpp/process.py index 7ca57e8..a822323 100644 --- a/slixfeed/xmpp/process.py +++ b/slixfeed/xmpp/process.py @@ -21,32 +21,19 @@ TODO import logging import os import slixfeed.action as action -from slixfeed.config import ( - add_to_list, - get_default_cache_directory, - get_default_data_directory, - get_value, - get_pathname_to_database, - remove_from_list) +import slixfeed.config as config from slixfeed.dt import current_time, timestamp import slixfeed.fetch as fetch import slixfeed.sqlite as sqlite import slixfeed.task as task import slixfeed.url as uri -import slixfeed.xmpp.bookmark as bookmark +from slixfeed.xmpp.bookmark import XmppBookmark import slixfeed.xmpp.muc as groupchat -import slixfeed.xmpp.status as status +from slixfeed.xmpp.status import XmppStatus import slixfeed.xmpp.upload as upload from slixfeed.xmpp.utility import get_chat_type import time -async def event_component(self): - self.send_presence() - - -async def event(self): - self.send_presence() - await self.get_roster() # for task in main_task: # task.cancel() @@ -101,9 +88,8 @@ async def message(self, message): await task.clean_tasks_xmpp(jid, ['status']) status_type = 'dnd' status_message = '📥️ Procesing request to import feeds...' - status.send( - self, jid, status_message, status_type) - db_file = get_pathname_to_database(jid_file) + await XmppStatus.send(self, jid, status_message, status_type) + db_file = config.get_pathname_to_database(jid_file) count = await action.import_opml(db_file, url) if count: response = 'Successfully imported {} feeds.'.format(count) @@ -163,7 +149,7 @@ async def message(self, message): # # Begin processing new JID # # Deprecated in favour of event 'presence_available' - # db_dir = get_default_data_directory() + # db_dir = config.get_default_data_directory() # os.chdir(db_dir) # if jid + '.db' not in os.listdir(): # await task_jid(jid) @@ -301,7 +287,7 @@ async def message(self, message): if not title: title = uri.get_hostname(url) if url.startswith('http'): - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) exist = await sqlite.get_feed_id_and_name(db_file, url) if not exist: await sqlite.insert_feed(db_file, url, title) @@ -334,9 +320,9 @@ async def message(self, message): key = 'filter-' + message_text[:5] val = message_text[7:] if val: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) keywords = await sqlite.get_filters_value(db_file, key) - val = await add_to_list(val, keywords) + val = await config.add_to_list(val, keywords) if await sqlite.get_filters_value(db_file, key): await sqlite.update_filters_value(db_file, [key, val]) @@ -352,9 +338,9 @@ async def message(self, message): key = 'filter-' + message_text[:5] val = message_text[7:] if val: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) keywords = await sqlite.get_filters_value(db_file, key) - val = await remove_from_list(val, keywords) + val = await config.remove_from_list(val, keywords) if await sqlite.get_filters_value(db_file, key): await sqlite.update_filters_value(db_file, [key, val]) @@ -374,7 +360,7 @@ async def message(self, message): if int(val) > 500: response = 'Value may not be greater than 500.' else: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) if await sqlite.get_settings_value(db_file, [key, val]): await sqlite.update_settings_value(db_file, @@ -391,9 +377,9 @@ async def message(self, message): response = 'Missing value.' send_reply_message(self, message, response) case _ if message_lowercase.startswith('bookmark -'): - if jid == get_value('accounts', 'XMPP', 'operator'): + if jid == config.get_value('accounts', 'XMPP', 'operator'): muc_jid = message_text[11:] - await bookmark.remove(self, muc_jid) + await XmppBookmark.remove(self, muc_jid) response = ('Groupchat {} has been removed ' 'from bookmarks.' .format(muc_jid)) @@ -402,7 +388,7 @@ async def message(self, message): 'Type: removing bookmarks.') send_reply_message(self, message, response) case 'bookmarks': - if jid == get_value('accounts', 'XMPP', 'operator'): + if jid == config.get_value('accounts', 'XMPP', 'operator'): response = await action.list_bookmarks(self) else: response = ('This action is restricted. ' @@ -412,9 +398,9 @@ async def message(self, message): key = 'filter-' + message_text[:4] val = message_text[6:] if val: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) keywords = await sqlite.get_filters_value(db_file, key) - val = await add_to_list(val, keywords) + val = await config.add_to_list(val, keywords) if await sqlite.get_filters_value(db_file, key): await sqlite.update_filters_value(db_file, [key, val]) @@ -430,9 +416,9 @@ async def message(self, message): key = 'filter-' + message_text[:4] val = message_text[6:] if val: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) keywords = await sqlite.get_filters_value(db_file, key) - val = await remove_from_list(val, keywords) + val = await config.remove_from_list(val, keywords) if await sqlite.get_filters_value(db_file, key): await sqlite.update_filters_value(db_file, [key, val]) @@ -451,16 +437,16 @@ async def message(self, message): status_message = ('📤️ Procesing request to ' 'export feeds into {}...' .format(ex)) - status.send( - self, jid, status_message, status_type) - cache_dir = get_default_cache_directory() + await XmppStatus.send(self, jid, status_message, + status_type) + 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 + '/' + ex): os.mkdir(cache_dir + '/' + ex) filename = os.path.join( cache_dir, ex, 'slixfeed_' + timestamp() + '.' + ex) - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) results = await sqlite.get_feeds(db_file) match ex: case 'html': @@ -505,9 +491,10 @@ async def message(self, message): status_message = ('📃️ Procesing request to ' 'produce {} document...' .format(ext.upper())) - status.send(self, jid, status_message, status_type) - db_file = get_pathname_to_database(jid_file) - cache_dir = get_default_cache_directory() + await XmppStatus.send(self, jid, status_message, + status_type) + db_file = config.get_pathname_to_database(jid_file) + cache_dir = config.get_default_cache_directory() if ix_url: if not os.path.isdir(cache_dir): os.mkdir(cache_dir) @@ -578,9 +565,9 @@ async def message(self, message): # status_message = ( # '📥️ Procesing request to import feeds...' # ) - # status.send( + # await XmppStatus.send( # self, jid, status_message, status_type) - # db_file = get_pathname_to_database(jid_file) + # db_file = config.get_pathname_to_database(jid_file) # count = await action.import_opml(db_file, url) # if count: # response = ( @@ -603,11 +590,11 @@ async def message(self, message): status_message = ('📫️ Processing request ' 'to fetch data from {}' .format(url)) - status.send(self, jid, status_message, status_type) + await XmppStatus.send(self, jid, status_message, status_type) if url.startswith('feed:'): url = uri.feed_to_http(url) url = (uri.replace_hostname(url, 'feed')) or url - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) # try: response = await action.add_feed(db_file, url) # await task.clean_tasks_xmpp(jid, ['status']) @@ -623,49 +610,28 @@ async def message(self, message): query = message_text[6:] if query: if len(query) > 3: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) result = await sqlite.search_feeds(db_file, query) response = action.list_feeds_by_query(query, result) else: response = 'Enter at least 4 characters to search' else: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) result = await sqlite.get_feeds(db_file) response = action.list_feeds(result) send_reply_message(self, message, response) case 'goodbye': if message['type'] == 'groupchat': await groupchat.leave(self, jid) - await bookmark.remove(self, muc_jid) + await XmppBookmark.remove(self, muc_jid) else: response = 'This command is valid in groupchat only.' send_reply_message(self, message, response) case _ if message_lowercase.startswith('interval'): - # FIXME - # The following error occurs only upon first attempt to set interval. - # /usr/lib/python3.11/asyncio/events.py:73: RuntimeWarning: coroutine 'Slixfeed.send_update' was never awaited - # self._args = None - # RuntimeWarning: Enable tracemalloc to get the object allocation traceback key = message_text[:8] val = message_text[9:] - if val: - # response = ( - # 'Updates will be sent every {} minutes.' - # ).format(response) - db_file = get_pathname_to_database(jid_file) - if await sqlite.get_settings_value(db_file, key): - await sqlite.update_settings_value(db_file, [key, val]) - else: - await sqlite.set_settings_value(db_file, [key, val]) - # NOTE Perhaps this should be replaced - # by functions clean and start - await task.refresh_task(self, jid, task.send_update, - key, val) - response = ('Updates will be sent every {} minutes.' - .format(val)) - else: - response = 'Missing value.' - send_reply_message(self, message, response) + await action.xmpp_change_interval( + self, key, val, jid, jid_file, message=message) case _ if message_lowercase.startswith('join'): muc_jid = uri.check_xmpp_uri(message_text[5:]) if muc_jid: @@ -684,7 +650,7 @@ async def message(self, message): if val: try: val = int(val) - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) if await sqlite.get_settings_value(db_file, [key, val]): await sqlite.update_settings_value(db_file, @@ -711,7 +677,7 @@ async def message(self, message): # get_settings_value, # key # ) - # val = await add_to_list( + # val = await config.add_to_list( # val, # names # ) @@ -728,7 +694,7 @@ async def message(self, message): # response = 'Missing value.' send_reply_message(self, message, response) case 'new': - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) key = 'old' val = 0 if await sqlite.get_settings_value(db_file, key): @@ -765,7 +731,7 @@ async def message(self, message): # ) # await refresh_task(jid, key, val) case 'old': - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) key = 'old' val = 1 if await sqlite.get_settings_value(db_file, key): @@ -783,11 +749,13 @@ async def message(self, message): # response = ( # 'Every update will contain {} news items.' # ).format(response) - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) if await sqlite.get_settings_value(db_file, key): - await sqlite.update_settings_value(db_file, [key, val]) + await sqlite.update_settings_value(db_file, + [key, val]) else: - await sqlite.set_settings_value(db_file, [key, val]) + await sqlite.set_settings_value(db_file, + [key, val]) response = ('Next update will contain {} news items.' .format(val)) except: @@ -808,8 +776,7 @@ async def message(self, message): status_type = 'dnd' status_message = ('📫️ Processing request to fetch data from {}' .format(url)) - status.send( - self, jid, status_message, status_type) + await XmppStatus.send(self, jid, status_message, status_type) if url.startswith('feed:'): url = uri.feed_to_http(url) url = (uri.replace_hostname(url, 'feed')) or url @@ -839,7 +806,7 @@ async def message(self, message): if num < 1 or num > 50: response = 'Value must be ranged from 1 to 50.' else: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) result = await sqlite.last_entries(db_file, num) response = action.list_last_entries(result, num) except: @@ -850,7 +817,7 @@ async def message(self, message): case _ if message_lowercase.startswith('remove'): ix_url = message_text[7:] if ix_url: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) try: ix = int(ix_url) url = sqlite.get_feed_url(db_file, ix) @@ -899,10 +866,10 @@ async def message(self, message): await task.clean_tasks_xmpp(jid, ['status']) status_type = 'dnd' status_message = '📫️ Marking entries as read...' - status.send(self, jid, status_message, status_type) - db_file = get_pathname_to_database(jid_file) + await XmppStatus.send(self, jid, status_message, status_type) + db_file = config.get_pathname_to_database(jid_file) if ix_url: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) try: ix = int(ix_url) url = sqlite.get_feed_url(db_file, ix) @@ -944,7 +911,7 @@ async def message(self, message): query = message_text[7:] if query: if len(query) > 1: - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) results = await sqlite.search_entries(db_file, query) response = action.list_search_results(query, results) else: @@ -956,7 +923,7 @@ async def message(self, message): # response = 'Updates are enabled.' key = 'enabled' val = 1 - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) if await sqlite.get_settings_value(db_file, key): await sqlite.update_settings_value(db_file, [key, val]) else: @@ -967,12 +934,12 @@ async def message(self, message): # print(task_manager[jid]) send_reply_message(self, message, response) case 'stats': - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) response = await action.list_statistics(db_file) send_reply_message(self, message, response) case _ if message_lowercase.startswith('disable '): ix = message_text[8:] - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) try: await sqlite.set_enabled_status(db_file, ix, 0) response = ('Updates are now disabled for news source {}.' @@ -982,7 +949,7 @@ async def message(self, message): send_reply_message(self, message, response) case _ if message_lowercase.startswith('enable'): ix = message_text[7:] - db_file = get_pathname_to_database(jid_file) + db_file = config.get_pathname_to_database(jid_file) try: await sqlite.set_enabled_status(db_file, ix, 1) response = ('Updates are now enabled for news source {}.' @@ -991,40 +958,7 @@ async def message(self, message): response = 'No news source with index {}.'.format(ix) send_reply_message(self, message, response) case 'stop': - # FIXME - # The following error occurs only upon first attempt to stop. - # /usr/lib/python3.11/asyncio/events.py:73: RuntimeWarning: coroutine 'Slixfeed.send_update' was never awaited - # self._args = None - # RuntimeWarning: Enable tracemalloc to get the object allocation traceback - # response = 'Updates are disabled.' - # try: - # # task_manager[jid]['check'].cancel() - # # task_manager[jid]['status'].cancel() - # task_manager[jid]['interval'].cancel() - # key = 'enabled' - # val = 0 - # response = await initdb( - # jid, - # update_settings_value, - # [key, val] - # ) - # except: - # response = 'Updates are already disabled.' - # # print('Updates are already disabled. Nothing to do.') - # # await send_status(jid) - key = 'enabled' - val = 0 - db_file = get_pathname_to_database(jid_file) - if await sqlite.get_settings_value(db_file, key): - await sqlite.update_settings_value(db_file, [key, val]) - else: - await sqlite.set_settings_value(db_file, [key, val]) - await task.clean_tasks_xmpp(jid, ['interval', 'status']) - response = 'Updates are disabled.' - send_reply_message(self, message, response) - status_type = 'xa' - status_message = '💡️ Send "Start" to receive Jabber updates' - status.send(self, jid, status_message, status_type) + await action.xmpp_stop_updates(self, message, jid, jid_file) case 'support': # TODO Send an invitation. response = 'Join xmpp:slixfeed@chat.woodpeckersnest.space?join' @@ -1057,7 +991,7 @@ async def message(self, message): send_reply_message(self, message, response) if not response: response = 'EMPTY MESSAGE - ACTION ONLY' - data_dir = get_default_data_directory() + data_dir = config.get_default_data_directory() if not os.path.isdir(data_dir): os.mkdir(data_dir) if not os.path.isdir(data_dir + '/logs/'): diff --git a/slixfeed/xmpp/profile.py b/slixfeed/xmpp/profile.py index ce13689..c4ade62 100644 --- a/slixfeed/xmpp/profile.py +++ b/slixfeed/xmpp/profile.py @@ -31,6 +31,7 @@ from slixfeed.config import get_value, get_default_config_directory # import logging import os +# class XmppProfile: async def update(self): """ diff --git a/slixfeed/xmpp/status.py b/slixfeed/xmpp/status.py index d77a381..990db52 100644 --- a/slixfeed/xmpp/status.py +++ b/slixfeed/xmpp/status.py @@ -1,10 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -def send(self, jid, status_message, status_type=None): - self.send_presence( - pshow=status_type, - pstatus=status_message, - pfrom=self.boundjid.bare, - pto=jid - ) +from slixfeed.xmpp.utility import get_chat_type + + +class XmppStatus: + + + async def send(self, jid, status_message, status_type=None, chat_type=None): + if not chat_type: + chat_type = await get_chat_type(self, jid) + self.send_presence( + pto=jid, + pfrom=self.boundjid.bare, + pshow=status_type, + pstatus=status_message, + ptype=chat_type + )