Add more permissions to ad-hoc commands from MUC (see Prosody mod_muc_adhoc_bots)

Handle more errors.
Add ad-hoc command Profile.
Support Python 3.10 (tomli).
Add table for scraping HTML (WIP).
Minor fixes.
This commit is contained in:
Schimon Jehudah 2024-02-25 01:52:24 +00:00
parent 56f85fdf26
commit afeaa8707b
9 changed files with 449 additions and 256 deletions

View file

@ -29,10 +29,12 @@ keywords = [
"chat", "chat",
"im", "im",
"jabber", "jabber",
"json",
"news", "news",
"rdf", "rdf",
"rss", "rss",
"syndication", "syndication",
"xml",
"xmpp", "xmpp",
] ]
# urls = {Homepage = "https://gitgud.io/sjehuda/slixfeed"} # urls = {Homepage = "https://gitgud.io/sjehuda/slixfeed"}
@ -43,6 +45,7 @@ dependencies = [
"bs4", "bs4",
"feedparser", "feedparser",
"lxml", "lxml",
"tomli", # Python 3.10
"tomli_w", "tomli_w",
"slixmpp", "slixmpp",
@ -50,7 +53,7 @@ dependencies = [
# listed here (testing) # listed here (testing)
"html2text", "html2text",
"pdfkit", "pdfkit",
"pysocks", # "pysocks",
"readability-lxml", "readability-lxml",
"xml2epub", "xml2epub",
] ]

View file

@ -87,6 +87,9 @@ import socket
xmpp_type = config.get_value('accounts', 'XMPP', 'type') xmpp_type = config.get_value('accounts', 'XMPP', 'type')
if not xmpp_type:
raise Exception('Key type is missing from accounts.ini.')
match xmpp_type: match xmpp_type:
case 'client': case 'client':
from slixfeed.xmpp.client import Slixfeed from slixfeed.xmpp.client import Slixfeed

View file

@ -32,7 +32,10 @@ import os
import slixfeed.sqlite as sqlite import slixfeed.sqlite as sqlite
import sys import sys
import tomli_w import tomli_w
import tomllib try:
import tomllib
except:
import tomli as tomllib
async def set_setting_value(db_file, key, val): async def set_setting_value(db_file, key, val):
@ -140,6 +143,7 @@ def clear_values(input):
return '' return ''
# TODO Return dict instead of list
def get_value(filename, section, keys): def get_value(filename, section, keys):
""" """
Get setting value. Get setting value.

View file

@ -158,6 +158,24 @@ def create_tables(db_file):
); );
""" """
) )
feeds_rules_table_sql = (
"""
CREATE TABLE IF NOT EXISTS feeds_rules (
id INTEGER NOT NULL,
feed_id INTEGER NOT NULL UNIQUE,
type TEXT NOT NULL,
base TEXT NOT NULL,
title TEXT NOT NULL,
link TEXT NOT NULL,
enclosure TEXT,
summary TEXT,
FOREIGN KEY ("feed_id") REFERENCES "feeds" ("id")
ON UPDATE CASCADE
ON DELETE CASCADE,
PRIMARY KEY (id)
);
"""
)
feeds_state_table_sql = ( feeds_state_table_sql = (
""" """
CREATE TABLE IF NOT EXISTS feeds_state ( CREATE TABLE IF NOT EXISTS feeds_state (
@ -235,6 +253,7 @@ def create_tables(db_file):
cur.execute(feeds_table_sql) cur.execute(feeds_table_sql)
cur.execute(feeds_state_table_sql) cur.execute(feeds_state_table_sql)
cur.execute(feeds_properties_table_sql) cur.execute(feeds_properties_table_sql)
cur.execute(feeds_rules_table_sql)
cur.execute(filters_table_sql) cur.execute(filters_table_sql)
# cur.execute(statistics_table_sql) # cur.execute(statistics_table_sql)
cur.execute(settings_table_sql) cur.execute(settings_table_sql)

View file

@ -1,2 +1,2 @@
__version__ = '0.1.20' __version__ = '0.1.21'
__version_info__ = (0, 1, 20) __version_info__ = (0, 1, 21)

View file

@ -31,12 +31,13 @@ NOTE
""" """
import asyncio import asyncio
from datetime import datetime
import logging import logging
import os import os
from random import randrange from random import randrange
import slixmpp import slixmpp
import slixfeed.task as task import slixfeed.task as task
from time import sleep import time
from slixmpp.plugins.xep_0363.http_upload import FileTooBig, HTTPError, UploadServiceNotFound from slixmpp.plugins.xep_0363.http_upload import FileTooBig, HTTPError, UploadServiceNotFound
# from slixmpp.plugins.xep_0402 import BookmarkStorage, Conference # from slixmpp.plugins.xep_0402 import BookmarkStorage, Conference
@ -64,7 +65,7 @@ from slixfeed.xmpp.roster import XmppRoster
# import slixfeed.xmpp.service as service # import slixfeed.xmpp.service as service
from slixfeed.xmpp.presence import XmppPresence from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.upload import XmppUpload from slixfeed.xmpp.upload import XmppUpload
from slixfeed.xmpp.utility import get_chat_type from slixfeed.xmpp.utility import get_chat_type, is_moderator
main_task = [] main_task = []
jid_tasker = {} jid_tasker = {}
@ -505,12 +506,12 @@ class Slixfeed(slixmpp.ClientXMPP):
self['xep_0050'].add_command(node='advanced', self['xep_0050'].add_command(node='advanced',
name='📓 Advanced', name='📓 Advanced',
handler=self._handle_advanced) handler=self._handle_advanced)
self['xep_0050'].add_command(node='profile',
name='💼️ Profile',
handler=self._handle_profile)
self['xep_0050'].add_command(node='about', self['xep_0050'].add_command(node='about',
name='📜️ About', name='📜️ About',
handler=self._handle_about) handler=self._handle_about)
self['xep_0050'].add_command(node='exploit',
name='Exploit',
handler=self._handle_reveal_jid)
# self['xep_0050'].add_command(node='search', # self['xep_0050'].add_command(node='search',
# name='Search', # name='Search',
# handler=self._handle_search) # handler=self._handle_search)
@ -518,12 +519,97 @@ class Slixfeed(slixmpp.ClientXMPP):
# Special interface # Special interface
# http://jabber.org/protocol/commands#actions # http://jabber.org/protocol/commands#actions
async def _handle_reveal_jid(self, iq, session): async def _handle_profile(self, iq, session):
jid = session['from'].bare jid = session['from'].bare
session['notes'] = [['info', jid]] jid_file = jid
db_file = config.get_pathname_to_database(jid_file)
feeds_all = str(await sqlite.get_number_of_items(db_file, 'feeds'))
feeds_act = str(await sqlite.get_number_of_feeds_active(db_file))
unread = str(await sqlite.get_number_of_entries_unread(db_file))
entries = await sqlite.get_number_of_items(db_file, 'entries')
archive = await sqlite.get_number_of_items(db_file, 'archive')
entries_all = str(entries + archive)
key_archive = str(config.get_setting_value(db_file, 'archive'))
key_enabled = str(config.get_setting_value(db_file, 'enabled'))
key_interval = str(config.get_setting_value(db_file, 'interval'))
key_media = str(config.get_setting_value(db_file, 'media'))
key_old = str(config.get_setting_value(db_file, 'old'))
key_quantum = str(config.get_setting_value(db_file, 'quantum'))
update_interval = config.get_setting_value(db_file, 'interval')
update_interval = 60 * int(update_interval)
last_update_time = float(await sqlite.get_last_update_time(db_file))
dt_object = datetime.fromtimestamp(last_update_time)
last_update = dt_object.strftime('%H:%M:%S')
if int(key_enabled):
next_update_time = last_update_time + update_interval
dt_object = datetime.fromtimestamp(next_update_time)
next_update = dt_object.strftime('%H:%M:%S')
else:
next_update = 'n/a'
form = self['xep_0004'].make_form('form', 'Profile')
form['instructions'] = ('Displaying information\nJabber ID {}'
.format(jid))
form.add_field(ftype='fixed',
value='Data')
form.add_field(label='Subscriptions',
ftype='text-single',
value=feeds_all)
form.add_field(label='Active subscriptions',
ftype='text-single',
value=feeds_act)
form.add_field(label='Items',
ftype='text-single',
value=entries_all)
form.add_field(label='Unread',
ftype='text-single',
value=unread)
form.add_field(ftype='fixed',
value='Schedule')
form.add_field(label='Last update',
ftype='text-single',
value=last_update)
form.add_field(label='Next update',
ftype='text-single',
value=next_update)
form.add_field(ftype='fixed',
value='Options')
form.add_field(label='Archive',
ftype='text-single',
value=key_archive)
form.add_field(label='Enabled',
ftype='text-single',
value=key_enabled)
form.add_field(label='Interval',
ftype='text-single',
value=key_interval)
form.add_field(label='Media',
ftype='text-single',
value=key_media)
form.add_field(label='Old',
ftype='text-single',
value=key_old)
form.add_field(label='Quantum',
ftype='text-single',
value=key_quantum)
session['payload'] = form
# text_note = ('Jabber ID: {}'
# '\n'
# 'Last update: {}'
# '\n'
# 'Next update: {}'
# ''.format(jid, last_update, next_update))
# session['notes'] = [['info', text_note]]
return session return session
async def _handle_filters(self, iq, session): async def _handle_filters(self, iq, session):
jid = session['from'].bare
jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
jid = session['from'].bare jid = session['from'].bare
jid_file = jid jid_file = jid
db_file = config.get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
@ -547,6 +633,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['has_next'] = False session['has_next'] = False
session['next'] = self._handle_filters_complete session['next'] = self._handle_filters_complete
session['payload'] = form session['payload'] = form
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session
@ -598,6 +688,12 @@ class Slixfeed(slixmpp.ClientXMPP):
async def _handle_subscription_add(self, iq, session): async def _handle_subscription_add(self, iq, session):
jid = session['from'].bare
jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Subscription') form = self['xep_0004'].make_form('form', 'Subscription')
form['instructions'] = 'Adding subscription' form['instructions'] = 'Adding subscription'
form.add_field(var='subscription', form.add_field(var='subscription',
@ -622,6 +718,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['next'] = self._handle_subscription_new session['next'] = self._handle_subscription_new
session['prev'] = None session['prev'] = None
session['payload'] = form session['payload'] = form
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session
@ -636,7 +736,7 @@ class Slixfeed(slixmpp.ClientXMPP):
options = form.add_field(var='update', options = form.add_field(var='update',
ftype='list-single', ftype='list-single',
label='News', label='News',
desc=('Select a news update to read.'), desc=('Select a news item to read.'),
required=True) required=True)
for result in results: for result in results:
title = result[1] title = result[1]
@ -770,6 +870,7 @@ class Slixfeed(slixmpp.ClientXMPP):
# scan = payload['values']['scan'] # scan = payload['values']['scan']
url = payload['values']['subscription'] url = payload['values']['subscription']
if isinstance(url, list) and len(url) > 1: if isinstance(url, list) and len(url) > 1:
url_count = len(url)
urls = url urls = url
agree_count = 0 agree_count = 0
error_count = 0 error_count = 0
@ -785,7 +886,7 @@ class Slixfeed(slixmpp.ClientXMPP):
form = self['xep_0004'].make_form('form', 'Subscription') form = self['xep_0004'].make_form('form', 'Subscription')
if agree_count: if agree_count:
response = ('Added {} new subscription(s) out of {}' response = ('Added {} new subscription(s) out of {}'
.format(agree_count, len(url))) .format(agree_count, url_count))
session['notes'] = [['info', response]] session['notes'] = [['info', response]]
else: else:
response = ('No new subscription was added. ' response = ('No new subscription was added. '
@ -984,8 +1085,15 @@ class Slixfeed(slixmpp.ClientXMPP):
return session return session
def _handle_discover(self, iq, session): async def _handle_discover(self, iq, session):
jid = session['from'].bare
jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Discover & Search') form = self['xep_0004'].make_form('form', 'Discover & Search')
form['instructions'] = 'Discover news subscriptions of all kinds'
options = form.add_field(var='search_type', options = form.add_field(var='search_type',
ftype='list-single', ftype='list-single',
label='Browse', label='Browse',
@ -999,6 +1107,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['next'] = self._handle_discover_type session['next'] = self._handle_discover_type
session['payload'] = form session['payload'] = form
session['prev'] = None session['prev'] = None
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session
@ -1011,6 +1123,7 @@ class Slixfeed(slixmpp.ClientXMPP):
form = self['xep_0004'].make_form('form', 'Discover & Search') form = self['xep_0004'].make_form('form', 'Discover & Search')
match search_type: match search_type:
case 'all': case 'all':
form['instructions'] = 'Browsing subscriptions'
options = form.add_field(var='subscription', options = form.add_field(var='subscription',
# ftype='list-multi', # TODO To be added soon # ftype='list-multi', # TODO To be added soon
ftype='list-single', ftype='list-single',
@ -1027,6 +1140,7 @@ class Slixfeed(slixmpp.ClientXMPP):
# session['allow_complete'] = True # session['allow_complete'] = True
session['next'] = self._handle_subscription_new session['next'] = self._handle_subscription_new
case 'cat': case 'cat':
form['instructions'] = 'Browsing categories'
session['next'] = self._handle_discover_category session['next'] = self._handle_discover_category
options = form.add_field(var='category', options = form.add_field(var='category',
ftype='list-single', ftype='list-single',
@ -1058,6 +1172,7 @@ class Slixfeed(slixmpp.ClientXMPP):
config_dir = config.get_default_config_directory() config_dir = config.get_default_config_directory()
db_file = config_dir + '/feeds.sqlite' db_file = config_dir + '/feeds.sqlite'
form = self['xep_0004'].make_form('form', 'Discover & Search') form = self['xep_0004'].make_form('form', 'Discover & Search')
form['instructions'] = 'Browsing category "{}"'.format(category)
options = form.add_field(var='subscription', options = form.add_field(var='subscription',
# ftype='list-multi', # TODO To be added soon # ftype='list-multi', # TODO To be added soon
ftype='list-single', ftype='list-single',
@ -1078,7 +1193,13 @@ class Slixfeed(slixmpp.ClientXMPP):
async def _handle_subscriptions(self, iq, session): async def _handle_subscriptions(self, iq, session):
form = self['xep_0004'].make_form('form', 'Contacts') jid = session['from'].bare
jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Subscriptions')
form['instructions'] = 'Managing subscriptions' form['instructions'] = 'Managing subscriptions'
options = form.add_field(var='action', options = form.add_field(var='action',
ftype='list-single', ftype='list-single',
@ -1092,6 +1213,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['payload'] = form session['payload'] = form
session['next'] = self._handle_subscriptions_result session['next'] = self._handle_subscriptions_result
session['has_next'] = True session['has_next'] = True
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session
@ -1372,8 +1497,14 @@ class Slixfeed(slixmpp.ClientXMPP):
async def _handle_advanced(self, iq, session): async def _handle_advanced(self, iq, session):
form = self['xep_0004'].make_form('form', 'Advanced Options') jid = session['from'].bare
form['instructions'] = 'Extended options and information' jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Advanced')
form['instructions'] = 'Extended options'
options = form.add_field(var='option', options = form.add_field(var='option',
ftype='list-single', ftype='list-single',
label='Choose', label='Choose',
@ -1390,6 +1521,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['payload'] = form session['payload'] = form
session['next'] = self._handle_advanced_result session['next'] = self._handle_advanced_result
session['has_next'] = True session['has_next'] = True
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session
@ -1708,7 +1843,12 @@ class Slixfeed(slixmpp.ClientXMPP):
# TODO Attempt to look up for feeds of hostname of JID (i.e. scan # TODO Attempt to look up for feeds of hostname of JID (i.e. scan
# jabber.de for feeds for juliet@jabber.de) # jabber.de for feeds for juliet@jabber.de)
async def _handle_promoted(self, iq, session): async def _handle_promoted(self, iq, session):
jid = session['from'].bare
jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Subscribe') form = self['xep_0004'].make_form('form', 'Subscribe')
# NOTE Refresh button would be of use # NOTE Refresh button would be of use
form['instructions'] = 'Featured subscriptions' form['instructions'] = 'Featured subscriptions'
@ -1758,6 +1898,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['next'] = self._handle_subscription_new session['next'] = self._handle_subscription_new
session['payload'] = form session['payload'] = form
session['prev'] = self._handle_promoted session['prev'] = self._handle_promoted
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session
@ -2062,6 +2206,11 @@ class Slixfeed(slixmpp.ClientXMPP):
here to persist across handler callbacks. here to persist across handler callbacks.
""" """
jid = session['from'].bare jid = session['from'].bare
jid_full = str(session['from'])
chat_type = await get_chat_type(self, jid)
if chat_type == 'groupchat':
moderator = is_moderator(self, jid, jid_full)
if chat_type == 'chat' or moderator:
jid_file = jid jid_file = jid
db_file = config.get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
form = self['xep_0004'].make_form('form', 'Settings') form = self['xep_0004'].make_form('form', 'Settings')
@ -2158,6 +2307,10 @@ class Slixfeed(slixmpp.ClientXMPP):
session['has_next'] = False session['has_next'] = False
session['next'] = self._handle_settings_complete session['next'] = self._handle_settings_complete
session['payload'] = form session['payload'] = form
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid))
session['notes'] = [['warn', text_warn]]
return session return session

View file

@ -81,8 +81,8 @@ async def message(self, message):
if (message['muc']['nick'] == self.alias): if (message['muc']['nick'] == self.alias):
return return
jid_full = str(message['from']) jid_full = str(message['from'])
role = self.plugin['xep_0045'].get_jid_property( alias = jid_full[jid_full.index('/')+1:]
jid, jid_full[jid_full.index('/')+1:], 'role') role = self.plugin['xep_0045'].get_jid_property(jid, alias, 'role')
if role != 'moderator': if role != 'moderator':
return return

View file

@ -55,6 +55,7 @@ async def set_avatar(self):
config_dir.pop() config_dir.pop()
config_dir = '/'.join(config_dir) config_dir = '/'.join(config_dir)
filename = glob.glob(config_dir + '/assets/image.*') filename = glob.glob(config_dir + '/assets/image.*')
if len(filename):
filename = filename[0] filename = filename[0]
image_file = os.path.join(config_dir, filename) image_file = os.path.join(config_dir, filename)
with open(image_file, 'rb') as avatar_file: with open(image_file, 'rb') as avatar_file:

View file

@ -7,9 +7,19 @@ import logging
# class XmppChat # class XmppChat
# class XmppUtility: # class XmppUtility:
def is_moderator(self, jid, jid_full):
alias = jid_full[jid_full.index('/')+1:]
role = self.plugin['xep_0045'].get_jid_property(jid, alias, 'role')
if role == 'moderator':
return True
else:
return False
# TODO Rename to get_jid_type
async def get_chat_type(self, jid): async def get_chat_type(self, jid):
""" """
Check whether a JID is of MUC. Check chat (i.e. JID) type.
If iqresult["disco_info"]["features"] contains XML namespace If iqresult["disco_info"]["features"] contains XML namespace
of 'http://jabber.org/protocol/muc', then it is a 'groupchat'. of 'http://jabber.org/protocol/muc', then it is a 'groupchat'.