Add ad-hoc commands

This commit is contained in:
Schimon Jehudah 2024-02-06 03:04:43 +00:00
parent 56aaccce68
commit 422e0669f1
12 changed files with 964 additions and 247 deletions

View file

@ -129,7 +129,7 @@ class JabberComponent:
xmpp.register_plugin('xep_0066') # Out of Band Data xmpp.register_plugin('xep_0066') # Out of Band Data
xmpp.register_plugin('xep_0071') # XHTML-IM xmpp.register_plugin('xep_0071') # XHTML-IM
xmpp.register_plugin('xep_0084') # User Avatar 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_0115') # Entity Capabilities
xmpp.register_plugin('xep_0153') # vCard-Based Avatars xmpp.register_plugin('xep_0153') # vCard-Based Avatars
xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping 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_0066') # Out of Band Data
xmpp.register_plugin('xep_0071') # XHTML-IM xmpp.register_plugin('xep_0071') # XHTML-IM
xmpp.register_plugin('xep_0084') # User Avatar 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_0115') # Entity Capabilities
xmpp.register_plugin('xep_0153') # vCard-Based Avatars xmpp.register_plugin('xep_0153') # vCard-Based Avatars
xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping xmpp.register_plugin('xep_0199', {'keepalive': True}) # XMPP Ping

View file

@ -48,7 +48,10 @@ from slixfeed.url import (
replace_hostname, replace_hostname,
trim_url 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 import tomllib
from urllib import error from urllib import error
from urllib.parse import parse_qs, urlsplit from urllib.parse import parse_qs, urlsplit
@ -56,28 +59,28 @@ import xml.etree.ElementTree as ET
try: try:
import xml2epub import xml2epub
except: except ImportError:
logging.info( logging.info(
"Package xml2epub was not found.\n" "Package xml2epub was not found.\n"
"ePUB support is disabled.") "ePUB support is disabled.")
try: try:
import html2text import html2text
except: except ImportError:
logging.info( logging.info(
"Package html2text was not found.\n" "Package html2text was not found.\n"
"Markdown support is disabled.") "Markdown support is disabled.")
try: try:
import pdfkit import pdfkit
except: except ImportError:
logging.info( logging.info(
"Package pdfkit was not found.\n" "Package pdfkit was not found.\n"
"PDF support is disabled.") "PDF support is disabled.")
try: try:
from readability import Document from readability import Document
except: except ImportError:
logging.info( logging.info(
"Package readability was not found.\n" "Package readability was not found.\n"
"Arc90 Lab algorithm is disabled.") "Arc90 Lab algorithm is disabled.")
@ -107,6 +110,45 @@ def manual(filename, section=None, command=None):
return cmd_list 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): def log_to_markdown(timestamp, filename, jid, message):
""" """
Log message to file. Log message to file.
@ -404,7 +446,7 @@ def list_feeds(results):
async def list_bookmarks(self): async def list_bookmarks(self):
conferences = await bookmark.get(self) conferences = await XmppBookmark.get(self)
message = "\nList of groupchats:\n\n```\n" message = "\nList of groupchats:\n\n```\n"
for conference in conferences: for conference in conferences:
message += ( message += (

View file

@ -1,7 +1,7 @@
[action] [action]
url = """ url = """
<url> <url>
Add given <url> to subscription list. Add given <url> to subscription list (prefix http. Default).
""" """
add = """ add = """
add <url> <title> add <url> <title>
@ -30,6 +30,17 @@ backup news text
Send a Plain Text file of your news items. 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] [custom]
new = """ new = """
new new
@ -70,10 +81,18 @@ Reset deny list.
""" """
[groupchat] [groupchat]
uri = """
<muc>
Join groupchat by given <muc> (prefix xmpp).
"""
join = """ join = """
join <muc> join <muc>
Join groupchat by given <muc>. Join groupchat by given <muc>.
""" """
leave = """
goodbye
Leave groupchat and delete it from bookmarks.
"""
[manual] [manual]
help = """ help = """
@ -122,10 +141,18 @@ Set maximum length of news item description. (0 for no limit)
""" """
quantum = """ quantum = """
quantum <number> 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] [modification]
archive = """
archive <number>
Number of news items to archive (maximum value 500).
"""
remove = """ remove = """
remove <id> remove <id>
Remove feed of from subscription list by given <id>. Remove feed of from subscription list by given <id>.
@ -158,7 +185,7 @@ Disable bot and stop updates.
[preview] [preview]
read = """ read = """
read <url> read <url>
Display most recent 20 titles of given <url>. Display most recent 5 titles of given <url>.
""" """
read_num = """ read_num = """
read <url> <index> read <url> <index>
@ -180,10 +207,14 @@ Search news items by given <text>.
""" """
recent = """ recent = """
recent <number> recent <number>
List recent <number> news items (up to 50 items). List recent <number> news items (max. 50).
""" """
[statistics] [statistics]
stats = """
stats
Show general statistics.
"""
analyses = """ analyses = """
analyses analyses
Show report and statistics of feeds. Show report and statistics of feeds.

View file

@ -61,7 +61,7 @@ No operator was specified for this instance.
platforms = """ platforms = """
Supported platforms: XMPP 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. For ideal experience, we recommend using XMPP.
""" """
@ -97,27 +97,36 @@ https://gitgud.io/sjehuda/slixfeed
""" """
thanks = """ thanks = """
Alixander Court (alixandercourt.com, Utah), \ Alixander Court <alixandercourt.com> (Utah), \
Chriss Farrell (SalixOS, Oregon), \
Christian Dersch (SalixOS), \ Christian Dersch (SalixOS), \
Cyrille Pontvieux (SalixOS, France), \ Cyrille Pontvieux <enialis.net> (SalixOS, France), \
Denis Fomin (Gajim, Russia), \ Denis Fomin (Gajim, Russia), \
Dimitris Tzemos (SalixOS, Greece), \ Dimitris Tzemos (SalixOS, Greece), \
Emmanuel Gil Peyrot (Poezio, France), \ Emmanuel Gil Peyrot (Poezio, France), \
Florent Le Coz (Poezio, France), \ Florent Le Coz (Poezio, France), \
George Vlahavas (SalixOS, Greece), \ George Vlahavas <vlahavas.com> (SalixOS, Greece), \
Guus der Kinderen from IgniteRealtime.org (Openfire, Netherlands), \ Guus der Kinderen <igniterealtime.org> (Openfire, Netherlands), \
habnabit_ from #python on irc.libera.chat, \ habnabit_ from #python on irc.libera.chat, \
Imar van Erven Dorens <simplicit.nl> (SalixOS, Netherlands), \
imattau (atomtopubsub), \ imattau (atomtopubsub), \
Jaussoin Timothée (Movim, France), \ Jaussoin Timothée <mov.im> (Movim, France), \
Kevin Smith from Isode (Swift, Wales), \ Justin Karneges <jblog.andbit.net> (Psi, California), \
Kevin Smith <isode.com> (Swift IM, Wales), \
Luis Henrique Mello (SalixOS, Brazil), \
magicfelix, \ magicfelix, \
Markus Muttilainen (SalixOS), \
Mathieu Pasquet (slixmpp, France), \ Mathieu Pasquet (slixmpp, France), \
Maxime Buquet (slixmpp, France), \ Maxime Buquet (slixmpp, France), \
Phillip Watkins (United Kingdom, SalixOS), \
Pierrick Le Brun (SalixOS, France), \ Pierrick Le Brun (SalixOS, France), \
Raphael Groner (Fedora, Germany), \ Raphael Groner (Fedora, Germany), \
Remko Tronçon (Swift, Germany), \ Remko Tronçon <mko.re> (Psi , Belgium), \
Simone "roughnecks" Canaletti (woodpeckersnest.space, Italy), \ Simone "roughnecks" Canaletti <woodpeckersnest.space> (Italy), \
Richard Lapointe (SalixOS, Connecticut), \
Strix from Loqi, \ Strix from Loqi, \
Thibaud Guerin (SalixOS), \
Tim Beech (SalixOS, Brazil), \
Thorsten Mühlfelder (SalixOS, Germany), \ Thorsten Mühlfelder (SalixOS, Germany), \
Yann Leboulanger (Gajim, France). Yann Leboulanger (Gajim, France).
""" """

View file

@ -12,55 +12,58 @@ TODO
from slixmpp.plugins.xep_0048.stanza import Bookmarks from slixmpp.plugins.xep_0048.stanza import Bookmarks
async def get(self): class XmppBookmark:
result = await self.plugin['xep_0048'].get_bookmarks()
bookmarks = result['private']['bookmarks']
conferences = bookmarks['conferences']
return conferences
async def add(self, muc_jid): async def get(self):
result = await self.plugin['xep_0048'].get_bookmarks() result = await self.plugin['xep_0048'].get_bookmarks()
bookmarks = result['private']['bookmarks'] bookmarks = result['private']['bookmarks']
conferences = bookmarks['conferences'] conferences = bookmarks['conferences']
mucs = [] return conferences
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): async def add(self, muc_jid):
result = await self.plugin['xep_0048'].get_bookmarks() result = await self.plugin['xep_0048'].get_bookmarks()
bookmarks = result['private']['bookmarks'] bookmarks = result['private']['bookmarks']
conferences = bookmarks['conferences'] conferences = bookmarks['conferences']
mucs = [] mucs = []
for conference in conferences: for conference in conferences:
jid = conference['jid'] jid = conference['jid']
mucs.extend([jid]) mucs.extend([jid])
if muc_jid in mucs: if muc_jid not in mucs:
bookmarks = Bookmarks() bookmarks = Bookmarks()
mucs.remove(muc_jid) mucs.extend([muc_jid])
for muc in mucs: for muc in mucs:
bookmarks.add_conference( bookmarks.add_conference(
muc, muc,
self.alias, self.alias,
autojoin=True autojoin=True
) )
await self.plugin['xep_0048'].set_bookmarks(bookmarks) 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)

View file

@ -60,7 +60,9 @@ from slixmpp.plugins.xep_0048.stanza import Bookmarks
# import xml.etree.ElementTree as ET # import xml.etree.ElementTree as ET
# from lxml import etree # 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.connect as connect
import slixfeed.xmpp.muc as muc import slixfeed.xmpp.muc as muc
import slixfeed.xmpp.process as process import slixfeed.xmpp.process as process
@ -169,7 +171,7 @@ class Slixfeed(slixmpp.ClientXMPP):
inviter = message["from"].bare inviter = message["from"].bare
muc_jid = message['groupchat_invite']['jid'] muc_jid = message['groupchat_invite']['jid']
await muc.join(self, inviter, muc_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 # NOTE Tested with Gajim and Psi
@ -177,7 +179,7 @@ class Slixfeed(slixmpp.ClientXMPP):
inviter = message["from"].bare inviter = message["from"].bare
muc_jid = message['groupchat_invite']['jid'] muc_jid = message['groupchat_invite']['jid']
await muc.join(self, inviter, muc_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): async def on_session_end(self, event):
@ -191,17 +193,32 @@ class Slixfeed(slixmpp.ClientXMPP):
async def on_session_start(self, event): 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) await muc.autojoin(self)
profile.set_identity(self, "client") profile.set_identity(self, "client")
await profile.update(self) await profile.update(self)
task.ping_task(self) task.ping_task(self)
# Service.commands(self)
# Service.reactions(self)
self.service_commands()
self.service_reactions()
async def on_session_resumed(self, event): async def on_session_resumed(self, event):
await process.event(self) self.send_presence()
self["xep_0115"].update_caps()
await muc.autojoin(self) await muc.autojoin(self)
profile.set_identity(self, "client") profile.set_identity(self, "client")
# Service.commands(self)
# Service.reactions(self)
self.service_commands()
self.service_reactions()
# TODO Request for subscription # TODO Request for subscription
@ -305,3 +322,301 @@ class Slixfeed(slixmpp.ClientXMPP):
jid = message['from'].bare 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']) 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

View file

@ -60,7 +60,9 @@ from slixmpp.plugins.xep_0048.stanza import Bookmarks
# import xml.etree.ElementTree as ET # import xml.etree.ElementTree as ET
# from lxml import etree # 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.connect as connect
# NOTE MUC is possible for component # NOTE MUC is possible for component
# import slixfeed.xmpp.muc as muc # 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 # and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize # listen for this event so that we we can initialize
# our roster. # our roster.
self.add_event_handler("session_start", self.on_session_start) self.add_event_handler("session_start",
self.add_event_handler("session_resumed", self.on_session_resumed) 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_offline", print("got_offline"))
# self.add_event_handler("got_online", self.check_readiness) # self.add_event_handler("got_online", self.check_readiness)
self.add_event_handler("changed_status", self.on_changed_status) self.add_event_handler("changed_status",
self.add_event_handler("presence_available", self.on_presence_available) self.on_changed_status)
self.add_event_handler("presence_unavailable", self.on_presence_unavailable) self.add_event_handler("presence_available",
self.add_event_handler("chatstate_active", self.on_chatstate_active) self.on_presence_available)
self.add_event_handler("chatstate_gone", self.on_chatstate_gone) self.add_event_handler("presence_unavailable",
self.add_event_handler("chatstate_composing", self.check_chatstate_composing) self.on_presence_unavailable)
self.add_event_handler("chatstate_paused", self.check_chatstate_paused) 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 # The message event is triggered whenever a message
# stanza is received. Be aware that that includes # stanza is received. Be aware that that includes
# MUC messages and error messages. # 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_invite",
self.add_event_handler("groupchat_direct_invite", self.on_groupchat_direct_invite) # XEP_0249 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("groupchat_message", self.message)
# self.add_event_handler("disconnected", self.reconnect) # self.add_event_handler("disconnected", self.reconnect)
# self.add_event_handler("disconnected", self.inspect_connection) # self.add_event_handler("disconnected", self.inspect_connection)
self.add_event_handler("reactions", self.on_reactions) self.add_event_handler("reactions",
self.add_event_handler("presence_error", self.on_presence_error) self.on_reactions)
self.add_event_handler("presence_subscribe", self.on_presence_subscribe) self.add_event_handler("presence_error",
self.add_event_handler("presence_subscribed", self.on_presence_subscribed) self.on_presence_error)
self.add_event_handler("presence_unsubscribed", self.on_presence_unsubscribed) 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 # Initialize event loop
# self.loop = asyncio.get_event_loop() # self.loop = asyncio.get_event_loop()
@ -159,37 +180,34 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_connection_failed(self, event): 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) await connect.recover_connection(self, message)
async def on_session_start(self, event): async def on_session_start(self, event):
self.send_presence() self.send_presence()
await process.event_component(self) await self["xep_0115"].update_caps()
# await muc.autojoin(self) # await muc.autojoin(self)
profile.set_identity(self, "service") profile.set_identity(self, "service")
await profile.update(self) await profile.update(self)
connect.ping_task(self) task.ping_task(self)
# await Service.capabilities(self)
# Service.commands(self) # Service.commands(self)
# Service.reactions(self) # Service.reactions(self)
await self.service_capabilities()
self.service_commands() self.service_commands()
self.service_reactions() self.service_reactions()
async def on_session_resumed(self, event): async def on_session_resumed(self, event):
await process.event_component(self) self.send_presence()
self["xep_0115"].update_caps()
# await muc.autojoin(self) # await muc.autojoin(self)
profile.set_identity(self, "service") profile.set_identity(self, "service")
# await Service.capabilities(self)
# Service.commands(self) # Service.commands(self)
# Service.reactions(self) # Service.reactions(self)
await self.service_capabilities()
self.service_commands() self.service_commands()
self.service_reactions() self.service_reactions()
@ -207,7 +225,11 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_changed_status(self, presence): 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): async def on_presence_subscribe(self, presence):
@ -228,7 +250,10 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_presence_available(self, presence): async def on_presence_available(self, presence):
# TODO Add function to check whether task is already running or not # 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): async def on_presence_unsubscribed(self, presence):
@ -237,12 +262,20 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_presence_unavailable(self, presence): async def on_presence_unavailable(self, presence):
jid = presence["from"].bare 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): async def on_presence_error(self, presence):
print("on_presence_error") print("on_presence_error")
print(presence) print(presence)
jid = presence["from"].bare
await task.clean_tasks_xmpp(jid)
async def on_reactions(self, message): async def on_reactions(self, message):
@ -253,28 +286,332 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_chatstate_active(self, message): async def on_chatstate_active(self, message):
if message['type'] in ('chat', 'normal'): if message['type'] in ('chat', 'normal'):
jid = message['from'].bare 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']) 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): async def on_chatstate_gone(self, message):
if message['type'] in ('chat', 'normal'): if message['type'] in ('chat', 'normal'):
jid = message['from'].bare 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']) 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'): if message['type'] in ('chat', 'normal'):
jid = message['from'].bare jid = message['from'].bare
await task.clean_tasks_xmpp(jid, ['status']) # 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.start_tasks_xmpp(self, 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

37
slixfeed/xmpp/message.py Normal file
View file

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

View file

@ -17,7 +17,6 @@ FIXME
""" """
import logging import logging
import slixfeed.xmpp.bookmark as bookmark
import slixfeed.xmpp.process as process import slixfeed.xmpp.process as process
from slixfeed.dt import current_time from slixfeed.dt import current_time

View file

@ -21,32 +21,19 @@ TODO
import logging import logging
import os import os
import slixfeed.action as action import slixfeed.action as action
from slixfeed.config import ( import slixfeed.config as config
add_to_list,
get_default_cache_directory,
get_default_data_directory,
get_value,
get_pathname_to_database,
remove_from_list)
from slixfeed.dt import current_time, timestamp from slixfeed.dt import current_time, timestamp
import slixfeed.fetch as fetch import slixfeed.fetch as fetch
import slixfeed.sqlite as sqlite import slixfeed.sqlite as sqlite
import slixfeed.task as task import slixfeed.task as task
import slixfeed.url as uri 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.muc as groupchat
import slixfeed.xmpp.status as status from slixfeed.xmpp.status import XmppStatus
import slixfeed.xmpp.upload as upload import slixfeed.xmpp.upload as upload
from slixfeed.xmpp.utility import get_chat_type from slixfeed.xmpp.utility import get_chat_type
import time 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: # for task in main_task:
# task.cancel() # task.cancel()
@ -101,9 +88,8 @@ async def message(self, message):
await task.clean_tasks_xmpp(jid, ['status']) await task.clean_tasks_xmpp(jid, ['status'])
status_type = 'dnd' status_type = 'dnd'
status_message = '📥️ Procesing request to import feeds...' status_message = '📥️ Procesing request to import feeds...'
status.send( await XmppStatus.send(self, jid, status_message, status_type)
self, jid, status_message, status_type) db_file = config.get_pathname_to_database(jid_file)
db_file = get_pathname_to_database(jid_file)
count = await action.import_opml(db_file, url) count = await action.import_opml(db_file, url)
if count: if count:
response = 'Successfully imported {} feeds.'.format(count) response = 'Successfully imported {} feeds.'.format(count)
@ -163,7 +149,7 @@ async def message(self, message):
# # Begin processing new JID # # Begin processing new JID
# # Deprecated in favour of event 'presence_available' # # Deprecated in favour of event 'presence_available'
# db_dir = get_default_data_directory() # db_dir = config.get_default_data_directory()
# os.chdir(db_dir) # os.chdir(db_dir)
# if jid + '.db' not in os.listdir(): # if jid + '.db' not in os.listdir():
# await task_jid(jid) # await task_jid(jid)
@ -301,7 +287,7 @@ async def message(self, message):
if not title: if not title:
title = uri.get_hostname(url) title = uri.get_hostname(url)
if url.startswith('http'): 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) exist = await sqlite.get_feed_id_and_name(db_file, url)
if not exist: if not exist:
await sqlite.insert_feed(db_file, url, title) await sqlite.insert_feed(db_file, url, title)
@ -334,9 +320,9 @@ async def message(self, message):
key = 'filter-' + message_text[:5] key = 'filter-' + message_text[:5]
val = message_text[7:] val = message_text[7:]
if val: 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) 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): if await sqlite.get_filters_value(db_file, key):
await sqlite.update_filters_value(db_file, await sqlite.update_filters_value(db_file,
[key, val]) [key, val])
@ -352,9 +338,9 @@ async def message(self, message):
key = 'filter-' + message_text[:5] key = 'filter-' + message_text[:5]
val = message_text[7:] val = message_text[7:]
if val: 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) 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): if await sqlite.get_filters_value(db_file, key):
await sqlite.update_filters_value(db_file, await sqlite.update_filters_value(db_file,
[key, val]) [key, val])
@ -374,7 +360,7 @@ async def message(self, message):
if int(val) > 500: if int(val) > 500:
response = 'Value may not be greater than 500.' response = 'Value may not be greater than 500.'
else: 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, if await sqlite.get_settings_value(db_file,
[key, val]): [key, val]):
await sqlite.update_settings_value(db_file, await sqlite.update_settings_value(db_file,
@ -391,9 +377,9 @@ async def message(self, message):
response = 'Missing value.' response = 'Missing value.'
send_reply_message(self, message, response) send_reply_message(self, message, response)
case _ if message_lowercase.startswith('bookmark -'): 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:] muc_jid = message_text[11:]
await bookmark.remove(self, muc_jid) await XmppBookmark.remove(self, muc_jid)
response = ('Groupchat {} has been removed ' response = ('Groupchat {} has been removed '
'from bookmarks.' 'from bookmarks.'
.format(muc_jid)) .format(muc_jid))
@ -402,7 +388,7 @@ async def message(self, message):
'Type: removing bookmarks.') 'Type: removing bookmarks.')
send_reply_message(self, message, response) send_reply_message(self, message, response)
case 'bookmarks': case 'bookmarks':
if jid == get_value('accounts', 'XMPP', 'operator'): if jid == config.get_value('accounts', 'XMPP', 'operator'):
response = await action.list_bookmarks(self) response = await action.list_bookmarks(self)
else: else:
response = ('This action is restricted. ' response = ('This action is restricted. '
@ -412,9 +398,9 @@ async def message(self, message):
key = 'filter-' + message_text[:4] key = 'filter-' + message_text[:4]
val = message_text[6:] val = message_text[6:]
if val: 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) 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): if await sqlite.get_filters_value(db_file, key):
await sqlite.update_filters_value(db_file, await sqlite.update_filters_value(db_file,
[key, val]) [key, val])
@ -430,9 +416,9 @@ async def message(self, message):
key = 'filter-' + message_text[:4] key = 'filter-' + message_text[:4]
val = message_text[6:] val = message_text[6:]
if val: 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) 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): if await sqlite.get_filters_value(db_file, key):
await sqlite.update_filters_value(db_file, await sqlite.update_filters_value(db_file,
[key, val]) [key, val])
@ -451,16 +437,16 @@ async def message(self, message):
status_message = ('📤️ Procesing request to ' status_message = ('📤️ Procesing request to '
'export feeds into {}...' 'export feeds into {}...'
.format(ex)) .format(ex))
status.send( await XmppStatus.send(self, jid, status_message,
self, jid, status_message, status_type) status_type)
cache_dir = get_default_cache_directory() cache_dir = config.get_default_cache_directory()
if not os.path.isdir(cache_dir): if not os.path.isdir(cache_dir):
os.mkdir(cache_dir) os.mkdir(cache_dir)
if not os.path.isdir(cache_dir + '/' + ex): if not os.path.isdir(cache_dir + '/' + ex):
os.mkdir(cache_dir + '/' + ex) os.mkdir(cache_dir + '/' + ex)
filename = os.path.join( filename = os.path.join(
cache_dir, ex, 'slixfeed_' + timestamp() + '.' + ex) 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) results = await sqlite.get_feeds(db_file)
match ex: match ex:
case 'html': case 'html':
@ -505,9 +491,10 @@ async def message(self, message):
status_message = ('📃️ Procesing request to ' status_message = ('📃️ Procesing request to '
'produce {} document...' 'produce {} document...'
.format(ext.upper())) .format(ext.upper()))
status.send(self, jid, status_message, status_type) await XmppStatus.send(self, jid, status_message,
db_file = get_pathname_to_database(jid_file) status_type)
cache_dir = get_default_cache_directory() db_file = config.get_pathname_to_database(jid_file)
cache_dir = config.get_default_cache_directory()
if ix_url: if ix_url:
if not os.path.isdir(cache_dir): if not os.path.isdir(cache_dir):
os.mkdir(cache_dir) os.mkdir(cache_dir)
@ -578,9 +565,9 @@ async def message(self, message):
# status_message = ( # status_message = (
# '📥️ Procesing request to import feeds...' # '📥️ Procesing request to import feeds...'
# ) # )
# status.send( # await XmppStatus.send(
# self, jid, status_message, status_type) # 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) # count = await action.import_opml(db_file, url)
# if count: # if count:
# response = ( # response = (
@ -603,11 +590,11 @@ async def message(self, message):
status_message = ('📫️ Processing request ' status_message = ('📫️ Processing request '
'to fetch data from {}' 'to fetch data from {}'
.format(url)) .format(url))
status.send(self, jid, status_message, status_type) await XmppStatus.send(self, jid, status_message, status_type)
if url.startswith('feed:'): if url.startswith('feed:'):
url = uri.feed_to_http(url) url = uri.feed_to_http(url)
url = (uri.replace_hostname(url, 'feed')) or 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: # try:
response = await action.add_feed(db_file, url) response = await action.add_feed(db_file, url)
# await task.clean_tasks_xmpp(jid, ['status']) # await task.clean_tasks_xmpp(jid, ['status'])
@ -623,49 +610,28 @@ async def message(self, message):
query = message_text[6:] query = message_text[6:]
if query: if query:
if len(query) > 3: 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) result = await sqlite.search_feeds(db_file, query)
response = action.list_feeds_by_query(query, result) response = action.list_feeds_by_query(query, result)
else: else:
response = 'Enter at least 4 characters to search' response = 'Enter at least 4 characters to search'
else: 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) result = await sqlite.get_feeds(db_file)
response = action.list_feeds(result) response = action.list_feeds(result)
send_reply_message(self, message, response) send_reply_message(self, message, response)
case 'goodbye': case 'goodbye':
if message['type'] == 'groupchat': if message['type'] == 'groupchat':
await groupchat.leave(self, jid) await groupchat.leave(self, jid)
await bookmark.remove(self, muc_jid) await XmppBookmark.remove(self, muc_jid)
else: else:
response = 'This command is valid in groupchat only.' response = 'This command is valid in groupchat only.'
send_reply_message(self, message, response) send_reply_message(self, message, response)
case _ if message_lowercase.startswith('interval'): 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] key = message_text[:8]
val = message_text[9:] val = message_text[9:]
if val: await action.xmpp_change_interval(
# response = ( self, key, val, jid, jid_file, message=message)
# '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)
case _ if message_lowercase.startswith('join'): case _ if message_lowercase.startswith('join'):
muc_jid = uri.check_xmpp_uri(message_text[5:]) muc_jid = uri.check_xmpp_uri(message_text[5:])
if muc_jid: if muc_jid:
@ -684,7 +650,7 @@ async def message(self, message):
if val: if val:
try: try:
val = int(val) 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, if await sqlite.get_settings_value(db_file,
[key, val]): [key, val]):
await sqlite.update_settings_value(db_file, await sqlite.update_settings_value(db_file,
@ -711,7 +677,7 @@ async def message(self, message):
# get_settings_value, # get_settings_value,
# key # key
# ) # )
# val = await add_to_list( # val = await config.add_to_list(
# val, # val,
# names # names
# ) # )
@ -728,7 +694,7 @@ async def message(self, message):
# response = 'Missing value.' # response = 'Missing value.'
send_reply_message(self, message, response) send_reply_message(self, message, response)
case 'new': case 'new':
db_file = get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
key = 'old' key = 'old'
val = 0 val = 0
if await sqlite.get_settings_value(db_file, key): if await sqlite.get_settings_value(db_file, key):
@ -765,7 +731,7 @@ async def message(self, message):
# ) # )
# await refresh_task(jid, key, val) # await refresh_task(jid, key, val)
case 'old': case 'old':
db_file = get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
key = 'old' key = 'old'
val = 1 val = 1
if await sqlite.get_settings_value(db_file, key): if await sqlite.get_settings_value(db_file, key):
@ -783,11 +749,13 @@ async def message(self, message):
# response = ( # response = (
# 'Every update will contain {} news items.' # 'Every update will contain {} news items.'
# ).format(response) # ).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): 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: 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.' response = ('Next update will contain {} news items.'
.format(val)) .format(val))
except: except:
@ -808,8 +776,7 @@ async def message(self, message):
status_type = 'dnd' status_type = 'dnd'
status_message = ('📫️ Processing request to fetch data from {}' status_message = ('📫️ Processing request to fetch data from {}'
.format(url)) .format(url))
status.send( await XmppStatus.send(self, jid, status_message, status_type)
self, jid, status_message, status_type)
if url.startswith('feed:'): if url.startswith('feed:'):
url = uri.feed_to_http(url) url = uri.feed_to_http(url)
url = (uri.replace_hostname(url, 'feed')) or url url = (uri.replace_hostname(url, 'feed')) or url
@ -839,7 +806,7 @@ async def message(self, message):
if num < 1 or num > 50: if num < 1 or num > 50:
response = 'Value must be ranged from 1 to 50.' response = 'Value must be ranged from 1 to 50.'
else: 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) result = await sqlite.last_entries(db_file, num)
response = action.list_last_entries(result, num) response = action.list_last_entries(result, num)
except: except:
@ -850,7 +817,7 @@ async def message(self, message):
case _ if message_lowercase.startswith('remove'): case _ if message_lowercase.startswith('remove'):
ix_url = message_text[7:] ix_url = message_text[7:]
if ix_url: if ix_url:
db_file = get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
try: try:
ix = int(ix_url) ix = int(ix_url)
url = sqlite.get_feed_url(db_file, ix) url = sqlite.get_feed_url(db_file, ix)
@ -899,10 +866,10 @@ async def message(self, message):
await task.clean_tasks_xmpp(jid, ['status']) await task.clean_tasks_xmpp(jid, ['status'])
status_type = 'dnd' status_type = 'dnd'
status_message = '📫️ Marking entries as read...' status_message = '📫️ Marking entries as read...'
status.send(self, jid, status_message, status_type) 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)
if ix_url: if ix_url:
db_file = get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
try: try:
ix = int(ix_url) ix = int(ix_url)
url = sqlite.get_feed_url(db_file, ix) url = sqlite.get_feed_url(db_file, ix)
@ -944,7 +911,7 @@ async def message(self, message):
query = message_text[7:] query = message_text[7:]
if query: if query:
if len(query) > 1: 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) results = await sqlite.search_entries(db_file, query)
response = action.list_search_results(query, results) response = action.list_search_results(query, results)
else: else:
@ -956,7 +923,7 @@ async def message(self, message):
# response = 'Updates are enabled.' # response = 'Updates are enabled.'
key = 'enabled' key = 'enabled'
val = 1 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): 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: else:
@ -967,12 +934,12 @@ async def message(self, message):
# print(task_manager[jid]) # print(task_manager[jid])
send_reply_message(self, message, response) send_reply_message(self, message, response)
case 'stats': 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) response = await action.list_statistics(db_file)
send_reply_message(self, message, response) send_reply_message(self, message, response)
case _ if message_lowercase.startswith('disable '): case _ if message_lowercase.startswith('disable '):
ix = message_text[8:] ix = message_text[8:]
db_file = get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
try: try:
await sqlite.set_enabled_status(db_file, ix, 0) await sqlite.set_enabled_status(db_file, ix, 0)
response = ('Updates are now disabled for news source {}.' response = ('Updates are now disabled for news source {}.'
@ -982,7 +949,7 @@ async def message(self, message):
send_reply_message(self, message, response) send_reply_message(self, message, response)
case _ if message_lowercase.startswith('enable'): case _ if message_lowercase.startswith('enable'):
ix = message_text[7:] ix = message_text[7:]
db_file = get_pathname_to_database(jid_file) db_file = config.get_pathname_to_database(jid_file)
try: try:
await sqlite.set_enabled_status(db_file, ix, 1) await sqlite.set_enabled_status(db_file, ix, 1)
response = ('Updates are now enabled for news source {}.' 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) response = 'No news source with index {}.'.format(ix)
send_reply_message(self, message, response) send_reply_message(self, message, response)
case 'stop': case 'stop':
# FIXME await action.xmpp_stop_updates(self, message, jid, jid_file)
# 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)
case 'support': case 'support':
# TODO Send an invitation. # TODO Send an invitation.
response = 'Join xmpp:slixfeed@chat.woodpeckersnest.space?join' response = 'Join xmpp:slixfeed@chat.woodpeckersnest.space?join'
@ -1057,7 +991,7 @@ async def message(self, message):
send_reply_message(self, message, response) send_reply_message(self, message, response)
if not response: response = 'EMPTY MESSAGE - ACTION ONLY' 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): if not os.path.isdir(data_dir):
os.mkdir(data_dir) os.mkdir(data_dir)
if not os.path.isdir(data_dir + '/logs/'): if not os.path.isdir(data_dir + '/logs/'):

View file

@ -31,6 +31,7 @@ from slixfeed.config import get_value, get_default_config_directory
# import logging # import logging
import os import os
# class XmppProfile:
async def update(self): async def update(self):
""" """

View file

@ -1,10 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
def send(self, jid, status_message, status_type=None): from slixfeed.xmpp.utility import get_chat_type
self.send_presence(
pshow=status_type,
pstatus=status_message, class XmppStatus:
pfrom=self.boundjid.bare,
pto=jid
) 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
)