From 071bf78e1d68741f8d4fd21e9b13aa89994237d9 Mon Sep 17 00:00:00 2001 From: Schimon Jehudah Date: Sun, 26 Nov 2023 15:23:52 +0000 Subject: [PATCH] Add support for groupchat and activation feature --- slixfeed/__main__.py | 1 + slixfeed/confighandler.py | 7 +- slixfeed/datahandler.py | 71 ++-- slixfeed/{filterhandler.py => listhandler.py} | 11 +- slixfeed/sqlitehandler.py | 4 +- slixfeed/taskhandler.py | 6 +- slixfeed/xmpphandler.py | 327 +++++++++++++----- 7 files changed, 296 insertions(+), 131 deletions(-) rename slixfeed/{filterhandler.py => listhandler.py} (94%) diff --git a/slixfeed/__main__.py b/slixfeed/__main__.py index 9590dbd..6802792 100644 --- a/slixfeed/__main__.py +++ b/slixfeed/__main__.py @@ -113,6 +113,7 @@ if __name__ == '__main__': xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0045') # Multi-User Chat + xmpp.register_plugin('xep_0048') # Bookmarks xmpp.register_plugin('xep_0060') # PubSub xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0249') # Multi-User Chat diff --git a/slixfeed/confighandler.py b/slixfeed/confighandler.py index 8e1e954..6727277 100644 --- a/slixfeed/confighandler.py +++ b/slixfeed/confighandler.py @@ -12,6 +12,7 @@ TODO import os import filehandler +from random import randrange async def get_value_default(key): @@ -42,8 +43,10 @@ async def get_value_default(key): result = 3 case "random": result = 0 - case "read": - result = "https://www.blacklistednews.com/rss.php" + case "masters": + result = randrange(100000, 999999) + case "token": + result = "none" return result diff --git a/slixfeed/datahandler.py b/slixfeed/datahandler.py index 234cdd7..b6b98b2 100644 --- a/slixfeed/datahandler.py +++ b/slixfeed/datahandler.py @@ -21,7 +21,7 @@ import feedparser import sqlitehandler import confighandler import datetimehandler -import filterhandler +import listhandler from asyncio.exceptions import IncompleteReadError from http.client import IncompleteRead @@ -168,15 +168,15 @@ async def download_updates(db_file, url=None): summary, pathname ) - allow_list = await filterhandler.is_listed( + allow_list = await listhandler.is_listed( db_file, - "allow", + "filter-allow", string ) if not allow_list: - reject_list = await filterhandler.is_listed( + reject_list = await listhandler.is_listed( db_file, - "deny", + "filter-deny", string ) if reject_list: @@ -208,7 +208,7 @@ async def download_updates(db_file, url=None): # NOTE Why (if result[0]) and (if result[1] == 200)? -async def view_feed(db_file, url): +async def view_feed(url): """ Check feeds for new entries. @@ -235,8 +235,7 @@ async def view_feed(db_file, url): # "For more information, visit " # "https://pythonhosted.org/feedparser/bozo.html" # ).format(url) - # msg = await probe_page(view_feed, url, result[0]) - msg = await probe_page(view_feed, url, result[0], db_file) + msg = await probe_page(view_feed, url, result[0]) return msg except ( IncompleteReadError, @@ -253,10 +252,7 @@ async def view_feed(db_file, url): if result[1] == 200: title = await get_title(url, result[0]) entries = feed.entries - msg = "Extracted {} entries from {}:\n```\n".format( - len(entries), - title - ) + msg = "Preview of {}:\n```\n".format(title) count = 0 for entry in entries: count += 1 @@ -290,13 +286,11 @@ async def view_feed(db_file, url): link, count ) + if count > 4: + break msg += ( - "```\n" - "Source: {}\n" - "Enter a number from 1 - {} using command `select` " - "to view a specific item from the list." - ).format(url, count) - await sqlitehandler.set_settings_value(db_file, ["read", url]) + "```\nSource: {}" + ).format(url) else: msg = ( ">{}\nFailed to load URL. Reason: {}" @@ -304,14 +298,38 @@ async def view_feed(db_file, url): return msg -async def view_entry(db_file, num): - num = int(num) - 1 - url = await sqlitehandler.get_settings_value(db_file, "read") +# NOTE Why (if result[0]) and (if result[1] == 200)? +async def view_entry(url, num): result = await download_feed(url) + if result[0]: + try: + feed = feedparser.parse(result[0]) + if feed.bozo: + # msg = ( + # ">{}\n" + # "WARNING: Bozo detected!\n" + # "For more information, visit " + # "https://pythonhosted.org/feedparser/bozo.html" + # ).format(url) + msg = await probe_page(view_entry, url, result[0], num) + return msg + except ( + IncompleteReadError, + IncompleteRead, + error.URLError + ) as e: + # print(e) + # TODO Print error to log + msg = ( + "> {}\n" + "Error: {}" + ).format(url, e) + breakpoint() if result[1] == 200: feed = feedparser.parse(result[0]) title = await get_title(url, result[0]) entries = feed.entries + num = int(num) - 1 entry = entries[num] if entry.has_key("title"): title = entry.title @@ -328,9 +346,9 @@ async def view_entry(db_file, num): if entry.has_key("summary"): summary = entry.summary # Remove HTML tags - # summary = BeautifulSoup(summary, "lxml").text + summary = BeautifulSoup(summary, "lxml").text # TODO Limit text length - # summary = summary.replace("\n\n", "\n") + summary = summary.replace("\n\n\n", "\n\n") else: summary = "*** No summary ***" if entry.has_key("link"): @@ -346,11 +364,8 @@ async def view_entry(db_file, num): "\n" "{}\n" "\n" - "{}\n" - "\n" ).format( title, - date, summary, link ) @@ -453,7 +468,7 @@ async def add_feed(db_file, url): # TODO callback for use with add_feed and view_feed -async def probe_page(callback, url, doc, db_file=None): +async def probe_page(callback, url, doc, num=None, db_file=None): msg = None try: # tree = etree.fromstring(res[0]) # etree is for xml @@ -483,6 +498,8 @@ async def probe_page(callback, url, doc, db_file=None): url = msg[0] if db_file: return await callback(db_file, url) + elif num: + return await callback(url, num) else: return await callback(url) diff --git a/slixfeed/filterhandler.py b/slixfeed/listhandler.py similarity index 94% rename from slixfeed/filterhandler.py rename to slixfeed/listhandler.py index ad68762..e5063c8 100644 --- a/slixfeed/filterhandler.py +++ b/slixfeed/listhandler.py @@ -17,9 +17,10 @@ TODO import sqlitehandler -async def set_filter(newwords, keywords): + +async def set_list(newwords, keywords): """ - Append new keywords to filter. + Append new keywords to list. Parameters ---------- @@ -46,7 +47,8 @@ async def set_filter(newwords, keywords): val = ",".join(keywords) return val -async def is_listed(db_file, type, string): + +async def is_listed(db_file, key, string): """ Check keyword match. @@ -66,10 +68,9 @@ async def is_listed(db_file, type, string): """ # async def reject(db_file, string): # async def is_blacklisted(db_file, string): - filter_type = "filter-" + type list = await sqlitehandler.get_settings_value( db_file, - filter_type + key ) if list: list = list.split(",") diff --git a/slixfeed/sqlitehandler.py b/slixfeed/sqlitehandler.py index fe54a2e..b766eda 100644 --- a/slixfeed/sqlitehandler.py +++ b/slixfeed/sqlitehandler.py @@ -1059,7 +1059,7 @@ async def list_feeds(db_file): "FROM feeds" ) results = cur.execute(sql) - feeds_list = "\nList of subscriptions:\n```" + feeds_list = "\nList of subscriptions:\n```\n" counter = 0 for result in results: counter += 1 @@ -1329,7 +1329,7 @@ async def set_settings_value(db_file, key_value): key_value : list key : str enabled, filter-allow, filter-deny, - interval, master, quantum, random. + interval, masters, quantum, random. value : int Numeric value. """ diff --git a/slixfeed/taskhandler.py b/slixfeed/taskhandler.py index 0527b4b..4adda25 100644 --- a/slixfeed/taskhandler.py +++ b/slixfeed/taskhandler.py @@ -198,11 +198,15 @@ async def send_update(self, jid, num=None): if new: # TODO Add while loop to assure delivery. # print(await datetimehandler.current_time(), ">>> ACT send_message",jid) + if await xmpphandler.Slixfeed.is_muc(self, jid): + chat_type = "groupchat" + else: + chat_type = "chat" xmpphandler.Slixfeed.send_message( self, mto=jid, mbody=new, - mtype="chat" + mtype=chat_type ) # TODO Do not refresh task before # verifying that it was completed. diff --git a/slixfeed/xmpphandler.py b/slixfeed/xmpphandler.py index 2e34fa2..3e70915 100644 --- a/slixfeed/xmpphandler.py +++ b/slixfeed/xmpphandler.py @@ -23,6 +23,15 @@ TODO 4) Do not send updates when busy or away. See https://slixmpp.readthedocs.io/en/latest/event_index.html#term-changed_status +5) XHTTML-IM + case _ if message_lowercase.startswith("html"): + message['html']="

Parse me!

" + self.send_message( + mto=jid, + mfrom=self.boundjid.bare, + mhtml=message + ) + NOTE 1) Self presence @@ -51,7 +60,7 @@ from slixmpp.plugins.xep_0363.http_upload import FileTooBig, HTTPError, UploadSe import datahandler import datetimehandler import filehandler -import filterhandler +import listhandler import sqlitehandler import taskhandler @@ -105,8 +114,8 @@ class Slixfeed(slixmpp.ClientXMPP): self.add_event_handler("message", self.message) self.add_event_handler("message", self.settle) - self.add_event_handler("groupchat_invite", self.accept_muc_invite) - self.add_event_handler("groupchat_direct_invite", self.accept_muc_invite) + self.add_event_handler("groupchat_invite", self.process_muc_invite) # XEP_0045 + self.add_event_handler("groupchat_direct_invite", self.process_muc_invite) # XEP_0249 # self.add_event_handler("groupchat_message", self.message) # self.add_event_handler("disconnected", self.reconnect) @@ -190,27 +199,58 @@ class Slixfeed(slixmpp.ClientXMPP): print("reactions") print(message) - async def accept_muc_invite(self, message): - ctr = message["from"].bare - jid = message['groupchat_invite']['jid'] - tkn = randrange(10000, 99999) + # async def accept_muc_invite(self, message, ctr=None): + # # if isinstance(message, str): + # if not ctr: + # ctr = message["from"].bare + # jid = message['groupchat_invite']['jid'] + # else: + # jid = message + async def process_muc_invite(self, message): + # operator muc_chat + inviter = message["from"].bare + muc_jid = message['groupchat_invite']['jid'] + await self.join_muc(inviter, muc_jid) + + + async def join_muc(self, inviter, muc_jid): + token = await filehandler.initdb( + muc_jid, + sqlitehandler.get_settings_value, + "token" + ) + if token != "accepted": + token = randrange(10000, 99999) + await filehandler.initdb( + muc_jid, + sqlitehandler.set_settings_value, + ["token", token] + ) + self.send_message( + mto=inviter, + mbody=( + "Send activation token {} to groupchat xmpp:{}?join." + ).format(token, muc_jid) + ) self.plugin['xep_0045'].join_muc( - jid, + muc_jid, "Slixfeed (RSS News Bot)", # If a room password is needed, use: # password=the_room_password, ) - self.send_message( - mto=ctr, - mbody=( - "Send activation token {} to groupchat xmpp:{}?join." - ).format(tkn, jid) - ) # self.add_event_handler( # "muc::[room]::message", # self.message # ) + # await self.get_bookmarks() + # bookmark = self.plugin['xep_0048'].instantiate_pep() + # print(bookmark) + # nick = "Slixfeed (RSS News Bot)" + # bookmark.add_bookmark(muc_jid, nick=nick) + # await self['xep_0048'].set_bookmarks(bookmark) + # print(bookmark) + async def on_session_end(self, event): print(await datetimehandler.current_time(), "Session ended. Attempting to reconnect.") @@ -341,6 +381,31 @@ class Slixfeed(slixmpp.ClientXMPP): # await taskhandler.select_file() + async def is_muc(self, jid): + """ + Check whether a JID is of MUC. + + Parameters + ---------- + jid : str + Jabber ID. + + Returns + ------- + boolean + True or False. + """ + iqresult = await self["xep_0030"].get_info(jid=jid) + features = iqresult["disco_info"]["features"] + # identity = iqresult['disco_info']['identities'] + # if 'account' in indentity: + # if 'conference' in indentity: + if 'http://jabber.org/protocol/muc' in features: + return True + else: + return False + + async def settle(self, msg): """ Add JID to roster and settle subscription. @@ -355,42 +420,46 @@ class Slixfeed(slixmpp.ClientXMPP): None. """ jid = msg["from"].bare - await self.get_roster() - # Check whether JID is in roster; otherwise, add it. - if jid not in self.client_roster.keys(): - self.send_presence_subscription( - pto=jid, - ptype="subscribe", - pnick="Slixfeed RSS News Bot" - ) - self.update_roster( - jid, - subscription="both" - ) - # Check whether JID is subscribed; otherwise, ask for presence. - if not self.client_roster[jid]["to"]: - self.send_presence_subscription( - pto=jid, - pfrom=self.boundjid.bare, - ptype="subscribe", - pnick="Slixfeed RSS News Bot" - ) - self.send_message( - mto=jid, - mtype="headline", - msubject="RSS News Bot", - mbody=("Accept subscription request to receive updates."), - mfrom=self.boundjid.bare, - mnick="Slixfeed RSS News Bot" - ) - self.send_presence( - pto=jid, - pfrom=self.boundjid.bare, - # Accept symbol 🉑️ 👍️ ✍ - pstatus="✒️ Accept subscription request to receive updates", - # ptype="subscribe", - pnick="Slixfeed RSS News Bot" - ) + if await self.is_muc(jid): + # Check whether JID is in bookmarks; otherwise, add it. + print(jid, "is muc") + else: + await self.get_roster() + # Check whether JID is in roster; otherwise, add it. + if jid not in self.client_roster.keys(): + self.send_presence_subscription( + pto=jid, + ptype="subscribe", + pnick="Slixfeed RSS News Bot" + ) + self.update_roster( + jid, + subscription="both" + ) + # Check whether JID is subscribed; otherwise, ask for presence. + if not self.client_roster[jid]["to"]: + self.send_presence_subscription( + pto=jid, + pfrom=self.boundjid.bare, + ptype="subscribe", + pnick="Slixfeed RSS News Bot" + ) + self.send_message( + mto=jid, + # mtype="headline", + msubject="RSS News Bot", + mbody="Accept subscription request to receive updates.", + mfrom=self.boundjid.bare, + mnick="Slixfeed RSS News Bot" + ) + self.send_presence( + pto=jid, + pfrom=self.boundjid.bare, + # Accept symbol 🉑️ 👍️ ✍ + pstatus="✒️ Accept subscription request to receive updates", + # ptype="subscribe", + pnick="Slixfeed RSS News Bot" + ) async def presence_unsubscribe(self, presence): @@ -436,27 +505,36 @@ class Slixfeed(slixmpp.ClientXMPP): action = 0 jid = msg["from"].bare if msg["type"] == "groupchat": - ctr = await filehandler.initdb( + # nick = msg["from"][msg["from"].index("/")+1:] + nick = str(msg["from"]) + nick = nick[nick.index("/")+1:] + if (msg['muc']['nick'] == "Slixfeed (RSS News Bot)" or + not msg["body"].startswith("!")): + return + token = await filehandler.initdb( jid, sqlitehandler.get_settings_value, - "masters" + "token" ) - if (msg["from"][msg["from"].index("/")+1:] not in ctr - or not msg["body"].startswith("!")): - return - + if token == "accepted": + operator = await filehandler.initdb( + jid, + sqlitehandler.get_settings_value, + "masters" + ) + if operator: + if nick not in operator: + return + # # Begin processing new JID # # Deprecated in favour of event "presence_available" # db_dir = filehandler.get_default_dbdir() # os.chdir(db_dir) # if jid + ".db" not in os.listdir(): # await taskhandler.task_jid(jid) - print(msg["body"]) - print(msg["body"].split()) message = " ".join(msg["body"].split()) if msg["type"] == "groupchat": message = message[1:] - print(message) message_lowercase = message.lower() print(await datetimehandler.current_time(), "ACCOUNT: " + str(msg["from"])) @@ -482,6 +560,33 @@ class Slixfeed(slixmpp.ClientXMPP): print(self.client_roster) print("roster 2") print(self.client_roster.keys()) + print("jid") + print(jid) + + case _ if message_lowercase.startswith("activate"): + if msg["type"] == "groupchat": + acode = message[9:] + token = await filehandler.initdb( + jid, + sqlitehandler.get_settings_value, + "token" + ) + if int(acode) == token: + await filehandler.initdb( + jid, + sqlitehandler.set_settings_value, + ["masters", nick] + ) + await filehandler.initdb( + jid, + sqlitehandler.set_settings_value, + ["token", "accepted"] + ) + action = "{}, your are in command.".format(nick) + else: + action = "Activation code is not valid." + else: + action = "This command is valid for groupchat only." case _ if message_lowercase.startswith("add"): message = message[4:] url = message.split(" ")[0] @@ -510,7 +615,7 @@ class Slixfeed(slixmpp.ClientXMPP): sqlitehandler.get_settings_value, key ) - val = await filterhandler.set_filter( + val = await listhandler.set_list( val, keywords ) @@ -534,7 +639,7 @@ class Slixfeed(slixmpp.ClientXMPP): sqlitehandler.get_settings_value, key ) - val = await filterhandler.set_filter( + val = await listhandler.set_list( val, keywords ) @@ -629,6 +734,33 @@ class Slixfeed(slixmpp.ClientXMPP): ).format(val) else: action = "Missing value." + case _ if message_lowercase.startswith("join"): + muc = message[5:] + await self.join_muc(jid, muc) + case _ if message_lowercase.startswith("mastership"): + key = message[:7] + val = message[11:] + if val: + names = await filehandler.initdb( + jid, + sqlitehandler.get_settings_value, + key + ) + val = await listhandler.set_list( + val, + names + ) + await filehandler.initdb( + jid, + sqlitehandler.set_settings_value, + [key, val] + ) + action = ( + "Operators\n" + "```\n{}\n```" + ).format(val) + else: + action = "Missing value." case _ if message_lowercase.startswith("next"): num = message[5:] await taskhandler.clean_tasks_xmpp( @@ -675,16 +807,29 @@ class Slixfeed(slixmpp.ClientXMPP): case "random": action = "Updates will be sent randomly." case _ if message_lowercase.startswith("read"): - url = message[5:] - if url.startswith("http"): - # action = await datahandler.view_feed(url) - action = await filehandler.initdb( - jid, - datahandler.view_feed, - url - ) - else: - action = "Missing URL." + data = message[5:] + data = data.split() + url = data[0] + if url.startswith("feed:"): + url = await datahandler.feed_to_http(url) + match len(data): + case 1: + if url.startswith("http"): + action = await datahandler.view_feed(url) + else: + action = "Missing URL." + case 2: + num = data[1] + if url.startswith("http"): + action = await datahandler.view_entry(url, num) + else: + action = "Missing URL." + case _: + action = ( + "Enter command as follows:\n" + "`read URL` or `read URL NUMBER`\n" + "URL must not contain white space." + ) case _ if message_lowercase.startswith("recent"): num = message[7:] if num: @@ -759,16 +904,6 @@ class Slixfeed(slixmpp.ClientXMPP): jid, sqlitehandler.statistics ) - case _ if message_lowercase.startswith("select"): - num = message[7:] - if num: - action = await filehandler.initdb( - jid, - datahandler.view_entry, - num - ) - else: - action = "Missing number." case _ if message_lowercase.startswith("status "): ix = message[7:] action = await filehandler.initdb( @@ -878,8 +1013,6 @@ def print_info(): " GNU General Public License for more details.\n" "\n" "NOTE\n" - " Make Slixfeed your own.\n" - "\n" " You can run Slixfeed on your own computer, server, and\n" " even on a Linux phone (i.e. Droidian, Mobian NixOS,\n" " postmarketOS). You can also use Termux.\n" @@ -919,30 +1052,34 @@ def print_help(): " For more information, visit https://xmpp.org/software/\n" "\n" "BASIC USAGE\n" - " start\n" - " Enable bot and send updates.\n" - " stop\n" - " Disable bot and stop updates.\n" " URL\n" " Add URL to subscription list.\n" " add URL TITLE\n" " Add URL to subscription list (without validity check).\n" - " feeds\n" - " List subscriptions.\n" + " join MUC\n" + " Join specified groupchat.\n" + " read URL\n" + " Display most recent 20 titles of given URL.\n" + " read URL N\n" + " Display specified entry number from given URL.\n" + "\n" + "MESSAGE OPTIONS\n" + " start\n" + " Enable bot and send updates.\n" + " stop\n" + " Disable bot and stop updates.\n" " interval N\n" " Set interval update to every N minutes.\n" " next N\n" " Send N next updates.\n" " quantum N\n" - " Set amount of updates for each interval.\n" - " read URL\n" - " Display most recent 20 titles of given URL.\n" - " read URL NUM\n" - " Display specified entry from given URL.\n" + " Set N amount of updates per interval.\n" "\n" "GROUPCHAT OPTIONS\n" " ! (command initiation)\n" " Use exclamation mark to initiate an actionable command.\n" + " activate CODE\n" + " Activate and command bot.\n" " demaster NICKNAME\n" " Remove master privilege.\n" " mastership NICKNAME\n" @@ -967,6 +1104,8 @@ def print_help(): " Toggle update status of feed.\n" "\n" "SEARCH OPTIONS\n" + " feeds\n" + " List all subscriptions.\n" " feeds TEXT\n" " Search subscriptions by given keywords.\n" " search TEXT\n"