Add support for groupchat and activation feature

This commit is contained in:
Schimon Jehudah 2023-11-26 15:23:52 +00:00
parent 31baf96430
commit 071bf78e1d
7 changed files with 296 additions and 131 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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(",")

View file

@ -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.
"""

View file

@ -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.

View file

@ -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']="<h1>Parse me!</h1>"
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,14 +505,26 @@ 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"
@ -451,12 +532,9 @@ class Slixfeed(slixmpp.ClientXMPP):
# 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"