Fix presence and subscription handling.

Segregate and atomize into classes.
This commit is contained in:
Schimon Jehudah 2024-02-07 00:26:42 +00:00
parent 422e0669f1
commit 00a8ed180a
18 changed files with 734 additions and 782 deletions

View file

@ -24,6 +24,7 @@ TODO
"""
import asyncio
from asyncio.exceptions import IncompleteReadError
from bs4 import BeautifulSoup
from feedparser import parse
@ -34,13 +35,11 @@ from lxml import html
import os
import slixfeed.config as config
import slixfeed.crawl as crawl
from slixfeed.dt import (
current_date, current_time, now,
convert_struct_time_to_iso8601,
rfc2822_to_iso8601
)
import slixfeed.dt as dt
import slixfeed.fetch as fetch
import slixfeed.sqlite as sqlite
import slixfeed.url as uri
from slixfeed.url import (
complete_url,
join_url,
@ -51,7 +50,9 @@ from slixfeed.url import (
import slixfeed.task as task
from slixfeed.xmpp.bookmark import XmppBookmark
from slixfeed.xmpp.message import XmppMessage
from slixfeed.xmpp.status import XmppStatus
from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.upload import XmppUpload
from slixfeed.xmpp.utility import get_chat_type
import tomllib
from urllib import error
from urllib.parse import parse_qs, urlsplit
@ -122,8 +123,7 @@ async def xmpp_change_interval(self, key, val, jid, jid_file, message=None, sess
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)
await task.refresh_task(self, jid, task.send_update, key, val)
response = ('Updates will be sent every {} minutes.'
.format(val))
else:
@ -131,7 +131,24 @@ async def xmpp_change_interval(self, key, val, jid, jid_file, message=None, sess
if message:
XmppMessage.send_reply(self, message, response)
if session:
await XmppMessage.send(self, jid, response, chat_type='chat')
XmppMessage.send(self, jid, response, chat_type='chat')
async def xmpp_start_updates(self, message, jid, jid_file):
key = 'enabled'
val = 1
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])
status_type = 'available'
status_message = '💡️ Welcome back!'
XmppPresence.send(self, jid, status_message, status_type=status_type)
message_body = 'Updates are enabled.'
XmppMessage.send_reply(self, message, message_body)
await asyncio.sleep(5)
await task.start_tasks_xmpp(self, jid, ['check', 'status', 'interval'])
async def xmpp_stop_updates(self, message, jid, jid_file):
@ -143,11 +160,12 @@ async def xmpp_stop_updates(self, message, jid, jid_file):
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)
message_body = 'Updates are disabled.'
XmppMessage.send_reply(self, message, message_body)
status_type = 'xa'
status_message = '💡️ Send "Start" to receive Jabber updates'
await XmppStatus.send(self, jid, status_message, status_type)
XmppPresence.send(self, jid, status_message, status_type=status_type)
def log_to_markdown(timestamp, filename, jid, message):
"""
@ -473,7 +491,7 @@ def export_to_markdown(jid, filename, results):
file.write(
'\n\n* * *\n\nThis list was saved on {} from xmpp:{} using '
'[Slixfeed](https://gitgud.io/sjehuda/slixfeed)\n'.format(
current_date(), jid))
dt.current_date(), jid))
# TODO Consider adding element jid as a pointer of import
@ -487,7 +505,7 @@ def export_to_opml(jid, filename, results):
ET.SubElement(head, "generator").text = "Slixfeed"
ET.SubElement(head, "urlPublic").text = (
"https://gitgud.io/sjehuda/slixfeed")
time_stamp = current_time()
time_stamp = dt.current_time()
ET.SubElement(head, "dateCreated").text = time_stamp
ET.SubElement(head, "dateModified").text = time_stamp
body = ET.SubElement(root, "body")
@ -548,7 +566,7 @@ async def add_feed(db_file, url):
if "updated_parsed" in feed["feed"].keys():
updated = feed["feed"]["updated_parsed"]
try:
updated = convert_struct_time_to_iso8601(updated)
updated = dt.convert_struct_time_to_iso8601(updated)
except:
updated = ''
else:
@ -594,7 +612,7 @@ async def add_feed(db_file, url):
if "date_published" in feed.keys():
updated = feed["date_published"]
try:
updated = convert_struct_time_to_iso8601(updated)
updated = dt.convert_struct_time_to_iso8601(updated)
except:
updated = ''
else:
@ -676,7 +694,7 @@ async def scan_json(db_file, url):
if "date_published" in feed.keys():
updated = feed["date_published"]
try:
updated = convert_struct_time_to_iso8601(updated)
updated = dt.convert_struct_time_to_iso8601(updated)
except:
updated = ''
else:
@ -697,12 +715,12 @@ async def scan_json(db_file, url):
for entry in entries:
if "date_published" in entry.keys():
date = entry["date_published"]
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
elif "date_modified" in entry.keys():
date = entry["date_modified"]
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
else:
date = now()
date = dt.now()
if "url" in entry.keys():
# link = complete_url(source, entry.link)
link = join_url(url, entry["url"])
@ -820,10 +838,10 @@ async def view_feed(url):
link = "*** No link ***"
if entry.has_key("published"):
date = entry.published
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
else:
date = "*** No date ***"
response += (
@ -877,10 +895,10 @@ async def view_entry(url, num):
title = "*** No title ***"
if entry.has_key("published"):
date = entry.published
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
else:
date = "*** No date ***"
if entry.has_key("summary"):
@ -965,7 +983,7 @@ async def scan(db_file, url):
if "updated_parsed" in feed["feed"].keys():
updated = feed["feed"]["updated_parsed"]
try:
updated = convert_struct_time_to_iso8601(updated)
updated = dt.convert_struct_time_to_iso8601(updated)
except:
updated = ''
else:
@ -986,12 +1004,12 @@ async def scan(db_file, url):
for entry in entries:
if entry.has_key("published"):
date = entry.published
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = rfc2822_to_iso8601(date)
date = dt.rfc2822_to_iso8601(date)
else:
date = now()
date = dt.now()
if entry.has_key("link"):
# link = complete_url(source, entry.link)
link = join_url(url, entry.link)
@ -1073,6 +1091,89 @@ async def scan(db_file, url):
db_file, feed_id, new_entries)
async def download_document(self, message, jid, jid_file, message_text, ix_url,
readability):
ext = ' '.join(message_text.split(' ')[1:])
ext = ext if ext else 'pdf'
url = None
error = None
response = None
if ext in ('epub', 'html', 'markdown',
'md', 'pdf', 'text', 'txt'):
match ext:
case 'markdown':
ext = 'md'
case 'text':
ext = 'txt'
status_type = 'dnd'
status_message = ('📃️ Procesing request to produce {} document...'
.format(ext.upper()))
XmppPresence.send(self, jid, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
cache_dir = config.get_default_cache_directory()
if ix_url:
if not os.path.isdir(cache_dir):
os.mkdir(cache_dir)
if not os.path.isdir(cache_dir + '/readability'):
os.mkdir(cache_dir + '/readability')
try:
ix = int(ix_url)
try:
url = sqlite.get_entry_url(db_file, ix)
except:
response = 'No entry with index {}'.format(ix)
except:
url = ix_url
if url:
logging.info('Original URL: {}'
.format(url))
url = uri.remove_tracking_parameters(url)
logging.info('Processed URL (tracker removal): {}'
.format(url))
url = (uri.replace_hostname(url, 'link')) or url
logging.info('Processed URL (replace hostname): {}'
.format(url))
result = await fetch.http(url)
data = result[0]
code = result[1]
if data:
title = get_document_title(data)
title = title.strip().lower()
for i in (' ', '-'):
title = title.replace(i, '_')
for i in ('?', '"', '\'', '!'):
title = title.replace(i, '')
filename = os.path.join(
cache_dir, 'readability',
title + '_' + dt.timestamp() + '.' + ext)
error = generate_document(data, url, ext, filename,
readability)
if error:
response = ('> {}\n'
'Failed to export {}. Reason: {}'
.format(url, ext.upper(), error))
else:
url = await XmppUpload.start(self, jid,
filename)
chat_type = await get_chat_type(self, jid)
XmppMessage.send_oob(self, jid, url, chat_type)
else:
response = ('> {}\n'
'Failed to fetch URL. Reason: {}'
.format(url, code))
await task.start_tasks_xmpp(self, jid, ['status'])
else:
response = 'Missing entry index number.'
else:
response = ('Unsupported filetype.\n'
'Try: epub, html, md (markdown), '
'pdf, or txt (text)')
if response:
logging.warning('Error for URL {}: {}'.format(url, error))
XmppMessage.send_reply(self, message, response)
def get_document_title(data):
try:
document = Document(data)
@ -1083,14 +1184,17 @@ def get_document_title(data):
return title
def generate_document(data, url, ext, filename):
def generate_document(data, url, ext, filename, readability=False):
error = None
try:
document = Document(data)
content = document.summary()
except:
if readability:
try:
document = Document(data)
content = document.summary()
except:
content = data
logging.warning('Check that package readability is installed.')
else:
content = data
logging.warning('Check that package readability is installed.')
match ext:
case "epub":
error = generate_epub(content, filename)
@ -1319,7 +1423,7 @@ async def remove_nonexistent_entries(db_file, url, feed):
# print("compare11:", title, link, time)
# print("compare22:", entry_title, entry_link, timestamp)
# print("============")
time = rfc2822_to_iso8601(entry.published)
time = dt.rfc2822_to_iso8601(entry.published)
if (entry_title == title and
entry_link == link and
timestamp == time):
@ -1423,7 +1527,7 @@ async def remove_nonexistent_entries_json(db_file, url, feed):
link = url
# "date_published" "date_modified"
if entry.has_key("date_published") and timestamp:
time = rfc2822_to_iso8601(entry["date_published"])
time = dt.rfc2822_to_iso8601(entry["date_published"])
if (entry_title == title and
entry_link == link and
timestamp == time):

View file

@ -52,9 +52,14 @@ Send all items of newly added feeds.
"""
[document]
get = """
get <id> <type>
Send an article as file. Specify <id> and <type>.
content = """
content <id>/<url> <type>
Send a readability (arc90) version of an article as file. Specify <id> or <url> and <type>.
Supported types are ePUB, HTML, MD and PDF (default).
"""
page = """
page <id>/<url> <type>
Send an article as file. Specify <id> or <url> and <type>.
Supported types are ePUB, HTML, MD and PDF (default).
"""

View file

@ -116,6 +116,7 @@ Kevin Smith <isode.com> (Swift IM, Wales), \
Luis Henrique Mello (SalixOS, Brazil), \
magicfelix, \
Markus Muttilainen (SalixOS), \
Martin <debacle@debian.org> (Debian, Germany), \
Mathieu Pasquet (slixmpp, France), \
Maxime Buquet (slixmpp, France), \
Phillip Watkins (United Kingdom, SalixOS), \

View file

@ -47,9 +47,10 @@ import slixfeed.config as config
# from slixfeed.dt import current_time
import slixfeed.sqlite as sqlite
# from xmpp import Slixfeed
import slixfeed.xmpp.client as xmpp
import slixfeed.xmpp.connect as connect
import slixfeed.xmpp.utility as utility
from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.message import XmppMessage
from slixfeed.xmpp.connect import XmppConnect
from slixfeed.xmpp.utility import get_chat_type
import time
main_task = []
@ -70,12 +71,12 @@ loop = asyncio.get_event_loop()
def ping_task(self):
global ping_task
global ping_task_instance
try:
ping_task.cancel()
ping_task_instance.cancel()
except:
logging.info('No ping task to cancel.')
ping_task = asyncio.create_task(connect.ping(self))
ping_task_instance = asyncio.create_task(XmppConnect.ping(self))
"""
@ -102,7 +103,7 @@ async def start_tasks_xmpp(self, jid, tasks=None):
logging.debug('KeyError:', str(e))
logging.info('Creating new task manager for JID {}'.format(jid))
if not tasks:
tasks = ['interval', 'status', 'check']
tasks = ['status', 'check', 'interval']
logging.info('Stopping tasks {} for JID {}'.format(tasks, jid))
for task in tasks:
# if task_manager[jid][task]:
@ -201,7 +202,7 @@ async def send_update(self, jid, num=None):
results = await sqlite.get_unread_entries(db_file, num)
news_digest = ''
media = None
chat_type = await utility.get_chat_type(self, jid)
chat_type = await get_chat_type(self, jid)
for result in results:
ix = result[0]
title_e = result[1]
@ -230,24 +231,10 @@ async def send_update(self, jid, num=None):
if media and news_digest:
# Send textual message
xmpp.Slixfeed.send_message(
self,
mto=jid,
mfrom=self.boundjid.bare,
mbody=news_digest,
mtype=chat_type
)
XmppMessage.send(self, jid, news_digest, chat_type)
news_digest = ''
# Send media
message = xmpp.Slixfeed.make_message(
self,
mto=jid,
mfrom=self.boundjid.bare,
mbody=media,
mtype=chat_type
)
message['oob']['url'] = media
message.send()
XmppMessage.send_oob(self, jid, media, chat_type)
media = None
if news_digest:
@ -256,13 +243,8 @@ async def send_update(self, jid, num=None):
# NOTE Do we need "if statement"? See NOTE at is_muc.
if chat_type in ('chat', 'groupchat'):
# TODO Provide a choice (with or without images)
xmpp.Slixfeed.send_message(
self,
mto=jid,
mfrom=self.boundjid.bare,
mbody=news_digest,
mtype=chat_type
)
XmppMessage.send(self, jid, news_digest, chat_type)
# See XEP-0367
# if media:
# # message = xmpp.Slixfeed.make_message(
# # self, mto=jid, mbody=new, mtype=chat_type)
@ -340,13 +322,7 @@ async def send_status(self, jid):
# breakpoint()
# print(await current_time(), status_text, "for", jid)
xmpp.Slixfeed.send_presence(
self,
pto=jid,
pfrom=self.boundjid.bare,
pshow=status_mode,
pstatus=status_text
)
XmppPresence.send(self, jid, status_text, status_type=status_mode)
# await asyncio.sleep(60 * 20)
await refresh_task(self, jid, send_status, 'status', '90')
# loop.call_at(

View file

@ -77,13 +77,8 @@ def replace_hostname(url, url_type):
parted_proxy_url = urlsplit(proxy_url)
protocol_new = parted_proxy_url.scheme
hostname_new = parted_proxy_url.netloc
url_new = urlunsplit([
protocol_new,
hostname_new,
pathname,
queries,
fragment
])
url_new = urlunsplit([protocol_new, hostname_new,
pathname, queries, fragment])
response = fetch.http_response(url_new)
if (response and
response.status_code == 200 and
@ -96,8 +91,11 @@ def replace_hostname(url, url_type):
proxies_file = config_dir + '/proxies.toml'
if not os.path.isfile(proxies_obsolete_file):
config.create_skeleton(proxies_file)
config.backup_obsolete(proxies_obsolete_file, proxy_name, proxy_type, proxy_url)
config.update_proxies(proxies_file, proxy_name, proxy_type, proxy_url)
config.backup_obsolete(proxies_obsolete_file,
proxy_name, proxy_type,
proxy_url)
config.update_proxies(proxies_file, proxy_name,
proxy_type, proxy_url)
url_new = None
else:
logging.warning(
@ -132,13 +130,7 @@ def remove_tracking_parameters(url):
for tracker in trackers:
if tracker in queries: del queries[tracker]
queries_new = urlencode(queries, doseq=True)
url = urlunsplit([
protocol,
hostname,
pathname,
queries_new,
fragment
])
url = urlunsplit([protocol, hostname, pathname, queries_new, fragment])
return url
@ -157,13 +149,8 @@ def feed_to_http(url):
URL.
"""
par_url = urlsplit(url)
new_url = urlunsplit([
'http',
par_url.netloc,
par_url.path,
par_url.query,
par_url.fragment
])
new_url = urlunsplit(['http', par_url.netloc, par_url.path, par_url.query,
par_url.fragment])
return new_url
@ -215,21 +202,13 @@ def complete_url(source, link):
return link
if link.startswith('//'):
if parted_link.netloc and parted_link.path:
new_link = urlunsplit([
parted_feed.scheme,
parted_link.netloc,
parted_link.path,
parted_link.query,
parted_link.fragment
])
new_link = urlunsplit([parted_feed.scheme, parted_link.netloc,
parted_link.path, parted_link.query,
parted_link.fragment])
elif link.startswith('/'):
new_link = urlunsplit([
parted_feed.scheme,
parted_feed.netloc,
parted_link.path,
parted_link.query,
parted_link.fragment
])
new_link = urlunsplit([parted_feed.scheme, parted_feed.netloc,
parted_link.path, parted_link.query,
parted_link.fragment])
elif link.startswith('../'):
pathlink = parted_link.path.split('/')
pathfeed = parted_feed.path.split('/')
@ -246,13 +225,9 @@ def complete_url(source, link):
break
pathlink = '/'.join(pathlink)
pathfeed.extend([pathlink])
new_link = urlunsplit([
parted_feed.scheme,
parted_feed.netloc,
'/'.join(pathfeed),
parted_link.query,
parted_link.fragment
])
new_link = urlunsplit([parted_feed.scheme, parted_feed.netloc,
'/'.join(pathfeed), parted_link.query,
parted_link.fragment])
else:
pathlink = parted_link.path.split('/')
pathfeed = parted_feed.path.split('/')
@ -262,13 +237,9 @@ def complete_url(source, link):
pathfeed.pop()
pathlink = '/'.join(pathlink)
pathfeed.extend([pathlink])
new_link = urlunsplit([
parted_feed.scheme,
parted_feed.netloc,
'/'.join(pathfeed),
parted_link.query,
parted_link.fragment
])
new_link = urlunsplit([parted_feed.scheme, parted_feed.netloc,
'/'.join(pathfeed), parted_link.query,
parted_link.fragment])
return new_link
@ -333,13 +304,7 @@ def trim_url(url):
fragment = parted_url.fragment
while '//' in pathname:
pathname = pathname.replace('//', '/')
url = urlunsplit([
protocol,
hostname,
pathname,
queries,
fragment
])
url = urlunsplit([protocol, hostname, pathname, queries, fragment])
return url

View file

@ -34,11 +34,9 @@ class XmppBookmark:
bookmarks = Bookmarks()
mucs.extend([muc_jid])
for muc in mucs:
bookmarks.add_conference(
muc,
self.alias,
autojoin=True
)
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)
@ -61,9 +59,7 @@ class XmppBookmark:
bookmarks = Bookmarks()
mucs.remove(muc_jid)
for muc in mucs:
bookmarks.add_conference(
muc,
self.alias,
autojoin=True
)
bookmarks.add_conference(muc,
self.alias,
autojoin=True)
await self.plugin['xep_0048'].set_bookmarks(bookmarks)

View file

@ -63,15 +63,15 @@ from slixmpp.plugins.xep_0048.stanza import Bookmarks
import slixfeed.config as config
import slixfeed.sqlite as sqlite
from slixfeed.xmpp.bookmark import XmppBookmark
import slixfeed.xmpp.connect as connect
import slixfeed.xmpp.muc as muc
from slixfeed.xmpp.connect import XmppConnect
from slixfeed.xmpp.message import XmppMessage
from slixfeed.xmpp.muc import XmppGroupchat
import slixfeed.xmpp.process as process
import slixfeed.xmpp.profile as profile
import slixfeed.xmpp.roster as roster
from slixfeed.xmpp.roster import XmppRoster
# import slixfeed.xmpp.service as service
import slixfeed.xmpp.state as state
import slixfeed.xmpp.status as status
import slixfeed.xmpp.utility as utility
from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.utility import get_chat_type
main_task = []
jid_tasker = {}
@ -146,8 +146,8 @@ class Slixfeed(slixmpp.ClientXMPP):
self.add_event_handler("reactions",
self.on_reactions)
self.add_event_handler("presence_error",
self.on_presence_error)
# self.add_event_handler("presence_error",
# self.on_presence_error)
self.add_event_handler("presence_subscribe",
self.on_presence_subscribe)
self.add_event_handler("presence_subscribed",
@ -161,43 +161,59 @@ class Slixfeed(slixmpp.ClientXMPP):
# handlers for connection events
self.connection_attempts = 0
self.max_connection_attempts = 10
self.add_event_handler("connection_failed", self.on_connection_failed)
self.add_event_handler("session_end", self.on_session_end)
self.add_event_handler('connection_failed',
self.on_connection_failed)
self.add_event_handler('session_end',
self.on_session_end)
# TODO Test
async def on_groupchat_invite(self, message):
logging.warning("on_groupchat_invite")
inviter = message["from"].bare
inviter = message['from'].bare
muc_jid = message['groupchat_invite']['jid']
await muc.join(self, inviter, muc_jid)
await XmppBookmark.add(self, muc_jid)
await XmppGroupchat.join(self, inviter, muc_jid)
message_body = ('Greetings!\n'
'I am {}, the news anchor.\n'
'My job is to bring you the latest '
'news from sources you provide me with.\n'
'You may always reach me via xmpp:{}?message'
.format(self.alias, self.boundjid.bare))
XmppMessage.send(self, muc_jid, message_body, 'groupchat')
# NOTE Tested with Gajim and Psi
async def on_groupchat_direct_invite(self, message):
inviter = message["from"].bare
inviter = message['from'].bare
muc_jid = message['groupchat_invite']['jid']
await muc.join(self, inviter, muc_jid)
await XmppBookmark.add(self, muc_jid)
await XmppGroupchat.join(self, inviter, muc_jid)
message_body = ('Greetings!\n'
'I am {}, the news anchor.\n'
'My job is to bring you the latest '
'news from sources you provide me with.\n'
'You may always reach me via xmpp:{}?message'
.format(self.alias, self.boundjid.bare))
XmppMessage.send(self, muc_jid, message_body, 'groupchat')
async def on_session_end(self, event):
message = "Session has ended."
await connect.recover_connection(self, message)
message = 'Session has ended.'
await XmppConnect.recover(self, message)
async def on_connection_failed(self, event):
message = "Connection has failed. Reason: {}".format(event)
await connect.recover_connection(self, message)
message = 'Connection has failed. Reason: {}'.format(event)
await XmppConnect.recover(self, message)
async def on_session_start(self, event):
self.send_presence()
await self["xep_0115"].update_caps()
await self['xep_0115'].update_caps()
await self.get_roster()
await muc.autojoin(self)
profile.set_identity(self, "client")
await XmppGroupchat.autojoin(self)
profile.set_identity(self, 'client')
await profile.update(self)
task.ping_task(self)
@ -210,9 +226,9 @@ class Slixfeed(slixmpp.ClientXMPP):
async def on_session_resumed(self, event):
self.send_presence()
self["xep_0115"].update_caps()
await muc.autojoin(self)
profile.set_identity(self, "client")
self['xep_0115'].update_caps()
await XmppGroupchat.autojoin(self)
profile.set_identity(self, 'client')
# Service.commands(self)
# Service.reactions(self)
@ -224,9 +240,16 @@ class Slixfeed(slixmpp.ClientXMPP):
# TODO Request for subscription
async def on_message(self, message):
jid = message["from"].bare
if "chat" == await utility.get_chat_type(self, jid):
await roster.add(self, jid)
await state.request(self, jid)
if (await get_chat_type(self, jid) == 'chat' and
not self.client_roster[jid]['to']):
XmppPresence.subscribe(self, jid)
await XmppRoster.add(self, jid)
status_message = '✒️ Share online status to receive updates'
XmppPresence.send(self, jid, status_message)
message_subject = 'RSS News Bot'
message_body = 'Share online status to receive updates.'
XmppMessage.send_headline(self, jid, message_subject, message_body,
'chat')
await process.message(self, message)
# chat_type = message["type"]
# message_body = message["body"]
@ -242,31 +265,54 @@ class Slixfeed(slixmpp.ClientXMPP):
async def on_presence_subscribe(self, presence):
jid = presence["from"].bare
await state.request(self, jid)
jid = presence['from'].bare
if not self.client_roster[jid]['to']:
XmppPresence.subscribe(self, jid)
await XmppRoster.add(self, jid)
status_message = '✒️ Share online status to receive updates'
XmppPresence.send(self, jid, status_message)
message_subject = 'RSS News Bot'
message_body = 'Share online status to receive updates.'
XmppMessage.send_headline(self, jid, message_subject, message_body,
'chat')
async def on_presence_subscribed(self, presence):
jid = presence["from"].bare
process.greet(self, jid)
jid = presence['from'].bare
message_subject = 'RSS News Bot'
message_body = ('Greetings!\n'
'I am {}, the news anchor.\n'
'My job is to bring you the latest '
'news from sources you provide me with.\n'
'You may always reach me via xmpp:{}?message'
.format(self.alias, self.boundjid.bare))
XmppMessage.send_headline(self, jid, message_subject, message_body,
'chat')
async def on_presence_available(self, presence):
# TODO Add function to check whether task is already running or not
# await task.start_tasks(self, presence)
# NOTE Already done inside the start-task function
jid = presence["from"].bare
jid = presence['from'].bare
# FIXME TODO Find out what is the source responsible for a couple presences with empty message
# NOTE This is a temporary solution
await asyncio.sleep(10)
await task.start_tasks_xmpp(self, jid)
async def on_presence_unsubscribed(self, presence):
await state.unsubscribed(self, presence)
jid = presence["from"].bare
await roster.remove(self, jid)
jid = presence['from'].bare
message_body = 'You have been unsubscribed.'
status_message = '🖋️ Subscribe to receive updates'
XmppMessage.send(self, jid, message_body, 'chat')
XmppPresence.send(self, jid, status_message,
presence_type='unsubscribed')
await XmppRoster.remove(self, jid)
async def on_presence_unavailable(self, presence):
jid = presence["from"].bare
jid = presence['from'].bare
# await task.stop_tasks(self, jid)
await task.clean_tasks_xmpp(jid)
@ -299,8 +345,8 @@ class Slixfeed(slixmpp.ClientXMPP):
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)
status_message='Press "help" for manual, or "info" for information.'
XmppPresence.send(self, jid, status_message)
async def on_chatstate_gone(self, message):
@ -324,6 +370,25 @@ class Slixfeed(slixmpp.ClientXMPP):
await task.start_tasks_xmpp(self, jid, ['status'])
# NOTE Failed attempt
# Need to use Super or Inheritance or both
# 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)
# async def _handle_settings(self, iq, session):
# await XmppCommand._handle_settings(self, iq, session)
# async def _handle_subscriptions(self, iq, session):
# await XmppCommand._handle_subscriptions(self, iq, session)
# TODO Move class Service to a separate file
# class Service(Slixfeed):
# def __init__(self):
@ -534,7 +599,8 @@ class Slixfeed(slixmpp.ClientXMPP):
value = True
form.add_field(var='old',
ftype='boolean',
desc='Mark items of newly added subscriptions as read.',
desc='Do not mark items of newly added subscriptions '
'as read.',
# label='Send only new items',
label='Include old news',
value=value)

View file

@ -66,13 +66,16 @@ from slixfeed.xmpp.bookmark import XmppBookmark
import slixfeed.xmpp.connect as connect
# NOTE MUC is possible for component
# import slixfeed.xmpp.muc as muc
from slixfeed.xmpp.message import XmppMessage
import slixfeed.xmpp.process as process
import slixfeed.xmpp.profile as profile
# import slixfeed.xmpp.roster as roster
# import slixfeed.xmpp.service as service
import slixfeed.xmpp.state as state
import slixfeed.xmpp.status as status
import slixfeed.xmpp.utility as utility
from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.utility import XmppUtility
from slixmpp.xmlstream import ET
# from slixmpp.xmlstream.handler import Callback
# from slixmpp.xmlstream.matcher import MatchXPath
main_task = []
jid_tasker = {}
@ -154,13 +157,15 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# handlers for connection events
self.connection_attempts = 0
self.max_connection_attempts = 10
self.add_event_handler("connection_failed", self.on_connection_failed)
self.add_event_handler("session_end", self.on_session_end)
self.add_event_handler('connection_failed',
self.on_connection_failed)
self.add_event_handler('session_end',
self.on_session_end)
# async def on_groupchat_invite(self, message):
# logging.warning("on_groupchat_invite")
# inviter = message["from"].bare
# inviter = message['from'].bare
# muc_jid = message['groupchat_invite']['jid']
# await muc.join(self, inviter, muc_jid)
# await bookmark.add(self, muc_jid)
@ -168,27 +173,27 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# NOTE Tested with Gajim and Psi
# async def on_groupchat_direct_invite(self, message):
# inviter = message["from"].bare
# inviter = message['from'].bare
# muc_jid = message['groupchat_invite']['jid']
# await muc.join(self, inviter, muc_jid)
# await bookmark.add(self, muc_jid)
async def on_session_end(self, event):
message = "Session has ended."
message = 'Session has ended.'
await connect.recover_connection(self, message)
async def on_connection_failed(self, event):
message = "Connection has failed. Reason: {}".format(event)
message = 'Connection has failed. Reason: {}'.format(event)
await connect.recover_connection(self, message)
async def on_session_start(self, event):
self.send_presence()
await self["xep_0115"].update_caps()
await self['xep_0115'].update_caps()
# await muc.autojoin(self)
profile.set_identity(self, "service")
profile.set_identity(self, 'service')
await profile.update(self)
task.ping_task(self)
@ -201,9 +206,9 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_session_resumed(self, event):
self.send_presence()
self["xep_0115"].update_caps()
self['xep_0115'].update_caps()
# await muc.autojoin(self)
profile.set_identity(self, "service")
profile.set_identity(self, 'service')
# Service.commands(self)
# Service.reactions(self)
@ -214,13 +219,21 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# TODO Request for subscription
async def on_message(self, message):
# jid = message["from"].bare
# if "chat" == await utility.get_chat_type(self, jid):
# await roster.add(self, jid)
# await state.request(self, jid)
# jid = message['from'].bare
# if "chat" == await XmppUtility.get_chat_type(self, jid):
# presence_probe = ET.Element('presence')
# presence_probe.attrib['type'] = 'probe'
# presence_probe.attrib['to'] = jid
# print('presence_probe')
# print(presence_probe)
# self.send_raw(str(presence_probe))
# presence_probe.send()
await process.message(self, message)
# chat_type = message["type"]
# message_body = message["body"]
# chat_type = message['type']
# message_body = message['body']
# message_reply = message.reply
@ -233,31 +246,40 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
async def on_presence_subscribe(self, presence):
jid = presence["from"].bare
# await state.request(self, jid)
self.send_presence_subscription(
pto=jid,
pfrom=self.boundjid.bare,
ptype="subscribe",
pnick=self.alias
)
jid = presence['from'].bare
# XmppPresence.request(self, jid)
XmppPresence.subscribe(self, jid)
async def on_presence_subscribed(self, presence):
jid = presence["from"].bare
process.greet(self, jid)
jid = presence['from'].bare
if await XmppUtility.get_chat_type(self, jid) == 'chat':
message_subject = 'RSS News Bot'
message_body = ('Greetings!\n'
'I am {}, the news anchor.\n'
'My job is to bring you the latest '
'news from sources you provide me with.\n'
'You may always reach me via xmpp:{}?message'
.format(self.alias, self.boundjid.bare))
XmppMessage.send_headline(self, jid, message_subject, message_body,
'chat')
async def on_presence_available(self, presence):
# TODO Add function to check whether task is already running or not
# await task.start_tasks(self, presence)
# NOTE Already done inside the start-task function
jid = presence["from"].bare
jid = presence['from'].bare
await task.start_tasks_xmpp(self, jid)
async def on_presence_unsubscribed(self, presence):
await state.unsubscribed(self, presence)
jid = presence['from'].bare
message = 'You have been unsubscribed.'
status_message = '🖋️ Subscribe to receive updates'
chat_type = await XmppUtility.get_chat_type(self, jid)
XmppMessage.send(self, jid, message, chat_type)
XmppPresence.send(self, jid, status_message, chat_type)
async def on_presence_unavailable(self, presence):
@ -294,8 +316,9 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
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)
status_message='Press "help" for manual, or "info" for information.'
chat_type = await XmppUtility.get_chat_type(self, jid)
XmppPresence.send(self, jid, status_message, chat_type)
async def on_chatstate_gone(self, message):

View file

@ -21,63 +21,65 @@ from time import sleep
import logging
async def ping(self, jid=None):
"""
Check for ping and disconnect if no ping has been received.
class XmppConnect:
Parameters
----------
jid : str, optional
Jabber ID. The default is None.
Returns
-------
None.
async def ping(self, jid=None):
"""
Check for ping and disconnect if no ping has been received.
"""
if not jid:
jid = self.boundjid.bare
while True:
rtt = None
Parameters
----------
jid : str, optional
Jabber ID. The default is None.
Returns
-------
None.
"""
if not jid:
jid = self.boundjid.bare
while True:
rtt = None
try:
rtt = await self['xep_0199'].ping(jid, timeout=10)
logging.info("Success! RTT: %s", rtt)
except IqError as e:
logging.info("Error pinging %s: %s",
jid,
e.iq['error']['condition'])
except IqTimeout:
logging.info("No response from %s", jid)
if not rtt:
self.disconnect()
await asyncio.sleep(60 * 1)
async def recover(self, message):
logging.warning(message)
print(current_time(), message, "Attempting to reconnect.")
self.connection_attempts += 1
# if self.connection_attempts <= self.max_connection_attempts:
# self.reconnect(wait=5.0) # wait a bit before attempting to reconnect
# else:
# print(current_time(),"Maximum connection attempts exceeded.")
# logging.error("Maximum connection attempts exceeded.")
print(current_time(), "Attempt number", self.connection_attempts)
seconds = (get_value("accounts", "XMPP", "reconnect_timeout")) or 30
seconds = int(seconds)
print(current_time(), "Next attempt within", seconds, "seconds")
# NOTE asyncio.sleep doesn't interval as expected
# await asyncio.sleep(seconds)
sleep(seconds)
self.reconnect(wait=5.0)
async def inspect(self):
print('Disconnected\n'
'Reconnecting...')
try:
rtt = await self['xep_0199'].ping(jid, timeout=10)
logging.info("Success! RTT: %s", rtt)
except IqError as e:
logging.info("Error pinging %s: %s",
jid,
e.iq['error']['condition'])
except IqTimeout:
logging.info("No response from %s", jid)
if not rtt:
self.reconnect
except:
self.disconnect()
await asyncio.sleep(60 * 1)
async def recover_connection(self, message):
logging.warning(message)
print(current_time(), message, "Attempting to reconnect.")
self.connection_attempts += 1
# if self.connection_attempts <= self.max_connection_attempts:
# self.reconnect(wait=5.0) # wait a bit before attempting to reconnect
# else:
# print(current_time(),"Maximum connection attempts exceeded.")
# logging.error("Maximum connection attempts exceeded.")
print(current_time(), "Attempt number", self.connection_attempts)
seconds = (get_value(
"accounts", "XMPP", "reconnect_timeout")) or 30
seconds = int(seconds)
print(current_time(), "Next attempt within", seconds, "seconds")
# NOTE asyncio.sleep doesn't interval as expected
# await asyncio.sleep(seconds)
sleep(seconds)
self.reconnect(wait=5.0)
async def inspect_connection(self, event):
print("Disconnected\nReconnecting...")
print(event)
try:
self.reconnect
except:
self.disconnect()
print("Problem reconnecting")
print('Problem reconnecting')

View file

@ -1,37 +1,77 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import os
# import slixfeed.action as action
import slixfeed.config as config
from slixfeed.dt import current_time, timestamp
import slixfeed.fetch as fetch
import slixfeed.sqlite as sqlite
import slixfeed.task as task
import slixfeed.url as uri
from slixfeed.xmpp.bookmark import XmppBookmark
# from slixfeed.xmpp.muc import XmppGroupchat
# from slixfeed.xmpp.message import XmppMessage
from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.upload import XmppUpload
from slixfeed.xmpp.utility import get_chat_type
import time
"""
NOTE
See XEP-0367: Message Attaching
"""
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
)
# def process():
async def send_oob(self, jid, url):
chat_type = await get_chat_type(self, jid)
def send(self, jid, message_body, chat_type):
self.send_message(mto=jid,
mfrom=self.boundjid.bare,
mbody=message_body,
mtype=chat_type)
def send_headline(self, jid, message_subject, message_body, chat_type):
self.send_message(mto=jid,
mfrom=self.boundjid.bare,
# mtype='headline',
msubject=message_subject,
mbody=message_body,
mtype=chat_type,
mnick=self.alias)
def send_oob(self, jid, url, chat_type):
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 = self.make_message(mto=jid,
mfrom=self.boundjid.bare,
mbody=url,
mhtml=html,
mtype=chat_type)
message['oob']['url'] = url
message.send()
# FIXME Solve this function
def send_oob_reply_message(message, url, response):
reply = message.reply(response)
reply['oob']['url'] = url
reply.send()
# def send_reply(self, message, message_body):
# message.reply(message_body).send()
def send_reply(self, message, response):
message.reply(response).send()
message.reply(response).send()

View file

@ -17,102 +17,90 @@ FIXME
"""
import logging
import slixfeed.xmpp.process as process
from slixfeed.dt import current_time
from slixfeed.xmpp.message import XmppMessage
# 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 accept_invitation(self, message):
# operator muc_chat
inviter = message["from"].bare
muc_jid = message['groupchat_invite']['jid']
await join(self, inviter, muc_jid)
class XmppGroupchat:
# 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 accept_invitation(self, message):
# operator muc_chat
inviter = message["from"].bare
muc_jid = message['groupchat_invite']['jid']
await self.join(self, inviter, muc_jid)
async def autojoin(self):
result = await self.plugin['xep_0048'].get_bookmarks()
bookmarks = result["private"]["bookmarks"]
conferences = bookmarks["conferences"]
for conference in conferences:
if conference["autojoin"]:
muc_jid = conference["jid"]
logging.info(
'Autojoin groupchat\n'
'Name : {}\n'
'JID : {}\n'
'Alias : {}\n'
.format(
conference["name"],
muc_jid,
conference["nick"]
))
self.plugin['xep_0045'].join_muc(
muc_jid,
conference["nick"],
# If a room password is needed, use:
# password=the_room_password,
)
async def join(self, inviter, muc_jid):
# token = await initdb(
# muc_jid,
# get_settings_value,
# "token"
# )
# if token != "accepted":
# token = randrange(10000, 99999)
# await initdb(
# muc_jid,
# update_settings_value,
# ["token", token]
# )
# self.send_message(
# mto=inviter,
# mfrom=self.boundjid.bare,
# mbody=(
# "Send activation token {} to groupchat xmpp:{}?join."
# ).format(token, muc_jid)
# )
logging.info(
'Joining groupchat\n'
'JID : {}\n'
'Inviter : {}\n'
.format(
muc_jid,
inviter
))
self.plugin['xep_0045'].join_muc(
muc_jid,
self.alias,
# If a room password is needed, use:
# password=the_room_password,
)
process.greet(self, muc_jid, chat_type="groupchat")
async def autojoin(self):
result = await self.plugin['xep_0048'].get_bookmarks()
bookmarks = result["private"]["bookmarks"]
conferences = bookmarks["conferences"]
for conference in conferences:
if conference["autojoin"]:
muc_jid = conference["jid"]
self.plugin['xep_0045'].join_muc(muc_jid,
conference["nick"],
# If a room password is needed, use:
# password=the_room_password,
)
logging.info('Autojoin groupchat\n'
'Name : {}\n'
'JID : {}\n'
'Alias : {}\n'
.format(conference["name"],
muc_jid,
conference["nick"]))
async def leave(self, muc_jid):
messages = [
"Whenever you need an RSS service again, "
"please dont hesitate to contact me.",
"My personal contact is xmpp:{}?message".format(self.boundjid.bare),
"Farewell, and take care."
]
for message in messages:
self.send_message(
mto=muc_jid,
mfrom=self.boundjid.bare,
mbody=message,
mtype="groupchat"
)
self.plugin['xep_0045'].leave_muc(
muc_jid,
self.alias,
"Goodbye!",
self.boundjid.bare
)
async def join(self, inviter, muc_jid):
# token = await initdb(
# muc_jid,
# get_settings_value,
# "token"
# )
# if token != "accepted":
# token = randrange(10000, 99999)
# await initdb(
# muc_jid,
# update_settings_value,
# ["token", token]
# )
# self.send_message(
# mto=inviter,
# mfrom=self.boundjid.bare,
# mbody=(
# "Send activation token {} to groupchat xmpp:{}?join."
# ).format(token, muc_jid)
# )
logging.info('Joining groupchat\n'
'JID : {}\n'
'Inviter : {}\n'
.format(muc_jid, inviter))
self.plugin['xep_0045'].join_muc(muc_jid,
self.alias,
# If a room password is needed, use:
# password=the_room_password,
)
async def leave(self, muc_jid):
messages = [
"Whenever you need an RSS service again, "
"please dont hesitate to contact me.",
"My JID is xmpp:{}?message".format(self.boundjid.bare),
"Farewell."
]
for message in messages:
self.send_message(mto=muc_jid,
mfrom=self.boundjid.bare,
mbody=message,
mtype='groupchat')
self.plugin['xep_0045'].leave_muc(muc_jid,
self.alias,
'Goodbye!',
self.boundjid.bare)

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

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
NOTE
Accept symbols 🉑 👍
"""
class XmppPresence:
def send(self, jid, status_message, presence_type=None, status_type=None):
self.send_presence(pto=jid,
pfrom=self.boundjid.bare,
pshow=status_type,
pstatus=status_message,
ptype=presence_type)
def subscribe(self, jid):
self.send_presence_subscription(pto=jid,
pfrom=self.boundjid.bare,
ptype='subscribe',
pnick=self.alias)
def remove(self):
"""
Remove subscription from JID that do not (stopped) share presence.
Returns
-------
None.
"""

View file

@ -9,7 +9,7 @@ TODO
Slixfeed: Do you still want to add this URL to subscription list?
See: case _ if message_lowercase.startswith("add"):
2) If subscription is inadequate (see state.request), send a message that says so.
2) If subscription is inadequate (see XmppPresence.request), send a message that says so.
elif not self.client_roster[jid]["to"]:
breakpoint()
@ -28,9 +28,10 @@ import slixfeed.sqlite as sqlite
import slixfeed.task as task
import slixfeed.url as uri
from slixfeed.xmpp.bookmark import XmppBookmark
import slixfeed.xmpp.muc as groupchat
from slixfeed.xmpp.status import XmppStatus
import slixfeed.xmpp.upload as upload
from slixfeed.xmpp.muc import XmppGroupchat
from slixfeed.xmpp.message import XmppMessage
from slixfeed.xmpp.presence import XmppPresence
from slixfeed.xmpp.upload import XmppUpload
from slixfeed.xmpp.utility import get_chat_type
import time
@ -88,7 +89,8 @@ async def message(self, message):
await task.clean_tasks_xmpp(jid, ['status'])
status_type = 'dnd'
status_message = '📥️ Procesing request to import feeds...'
await XmppStatus.send(self, jid, status_message, status_type)
XmppPresence.send(self, jid, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
count = await action.import_opml(db_file, url)
if count:
@ -97,7 +99,7 @@ async def message(self, message):
response = 'OPML file was not imported.'
# await task.clean_tasks_xmpp(jid, ['status'])
await task.start_tasks_xmpp(self, jid, ['status'])
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
return
@ -190,7 +192,7 @@ async def message(self, message):
# 'This action is restricted. '
# 'Type: breakpoint.'
# )
# send_reply_message(self, message, response)
# XmppMessage.send_reply(self, message, response)
case 'help':
command_list = ' '.join(action.manual('commands.toml'))
response = ('Available command keys:\n'
@ -198,7 +200,7 @@ async def message(self, message):
'Usage: `help <key>`'
.format(command_list))
print(response)
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('help'):
command = message_text[5:]
command = command.split(' ')
@ -228,14 +230,14 @@ async def message(self, message):
else:
response = ('Invalid. Enter command key '
'or command key & name')
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'info':
command_list = ' '.join(action.manual('information.toml'))
response = ('Available command options:\n'
'```\n{}\n```\n'
'Usage: `info <option>`'
.format(command_list))
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('info'):
command = message_text[5:]
command_list = action.manual('information.toml', command)
@ -245,7 +247,7 @@ async def message(self, message):
else:
response = ('KeyError for {}'
.format(command))
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase in ['greetings', 'hallo', 'hello',
'hey', 'hi', 'hola', 'holla',
'hollo']:
@ -253,7 +255,7 @@ async def message(self, message):
'I am an RSS News Bot.\n'
'Send "help" for further instructions.\n'
.format(self.alias))
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
# case _ if message_lowercase.startswith('activate'):
# if message['type'] == 'groupchat':
@ -315,7 +317,7 @@ async def message(self, message):
.format(url, name, ix))
else:
response = 'Missing URL.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('allow +'):
key = 'filter-' + message_text[:5]
val = message_text[7:]
@ -333,7 +335,7 @@ async def message(self, message):
.format(val))
else:
response = 'Missing keywords.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('allow -'):
key = 'filter-' + message_text[:5]
val = message_text[7:]
@ -351,7 +353,7 @@ async def message(self, message):
.format(val))
else:
response = 'Missing keywords.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('archive'):
key = message_text[:7]
val = message_text[8:]
@ -375,7 +377,7 @@ async def message(self, message):
response = 'Enter a numeric value only.'
else:
response = 'Missing value.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('bookmark -'):
if jid == config.get_value('accounts', 'XMPP', 'operator'):
muc_jid = message_text[11:]
@ -386,14 +388,14 @@ async def message(self, message):
else:
response = ('This action is restricted. '
'Type: removing bookmarks.')
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'bookmarks':
if jid == config.get_value('accounts', 'XMPP', 'operator'):
response = await action.list_bookmarks(self)
else:
response = ('This action is restricted. '
'Type: viewing bookmarks.')
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('deny +'):
key = 'filter-' + message_text[:4]
val = message_text[6:]
@ -411,7 +413,7 @@ async def message(self, message):
.format(val))
else:
response = 'Missing keywords.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('deny -'):
key = 'filter-' + message_text[:4]
val = message_text[6:]
@ -429,7 +431,7 @@ async def message(self, message):
.format(val))
else:
response = 'Missing keywords.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('export'):
ex = message_text[7:]
if ex in ('opml', 'html', 'md', 'xbel'):
@ -437,8 +439,8 @@ async def message(self, message):
status_message = ('📤️ Procesing request to '
'export feeds into {}...'
.format(ex))
await XmppStatus.send(self, jid, status_message,
status_type)
XmppPresence.send(self, jid, status_message,
status_type=status_type)
cache_dir = config.get_default_cache_directory()
if not os.path.isdir(cache_dir):
os.mkdir(cache_dir)
@ -457,105 +459,33 @@ async def message(self, message):
action.export_to_opml(jid, filename, results)
case 'xbel':
response = 'Not yet implemented.'
url = await upload.start(self, jid, filename)
url = await XmppUpload.start(self, jid, filename)
# response = (
# 'Feeds exported successfully to {}.\n{}'
# ).format(ex, url)
# send_oob_reply_message(message, url, response)
await send_oob_message(self, jid, url)
# XmppMessage.send_oob_reply_message(message, url, response)
chat_type = await get_chat_type(self, jid)
XmppMessage.send_oob(self, jid, url, chat_type)
await task.start_tasks_xmpp(self, jid, ['status'])
else:
response = ('Unsupported filetype.\n'
'Try: html, md, opml, or xbel')
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if (message_lowercase.startswith('gemini:') or
message_lowercase.startswith('gopher:')):
response = 'Gemini and Gopher are not supported yet.'
send_reply_message(self, message, response)
# TODO xHTML, HTMLZ, Markdown, MHTML, PDF, TXT
case _ if (message_lowercase.startswith('get')):
message_text = message_text[4:]
XmppMessage.send_reply(self, message, response)
# TODO xHTML, HTMLZ, MHTML
case _ if (message_lowercase.startswith('page')):
message_text = message_text[5:]
ix_url = message_text.split(' ')[0]
ext = ' '.join(message_text.split(' ')[1:])
ext = ext if ext else 'pdf'
url = None
error = None
if ext in ('epub', 'html', 'markdown',
'md', 'pdf', 'text', 'txt'):
match ext:
case 'markdown':
ext = 'md'
case 'text':
ext = 'txt'
status_type = 'dnd'
status_message = ('📃️ Procesing request to '
'produce {} document...'
.format(ext.upper()))
await XmppStatus.send(self, jid, status_message,
status_type)
db_file = config.get_pathname_to_database(jid_file)
cache_dir = config.get_default_cache_directory()
if ix_url:
if not os.path.isdir(cache_dir):
os.mkdir(cache_dir)
if not os.path.isdir(cache_dir + '/readability'):
os.mkdir(cache_dir + '/readability')
try:
ix = int(ix_url)
try:
url = sqlite.get_entry_url(db_file, ix)
except:
response = 'No entry with index {}'.format(ix)
except:
url = ix_url
if url:
logging.info('Original URL: {}'
.format(url))
url = uri.remove_tracking_parameters(url)
logging.info('Processed URL (tracker removal): {}'
.format(url))
url = (uri.replace_hostname(url, 'link')) or url
logging.info('Processed URL (replace hostname): {}'
.format(url))
result = await fetch.http(url)
data = result[0]
code = result[1]
if data:
title = action.get_document_title(data)
title = title.strip().lower()
for i in (' ', '-'):
title = title.replace(i, '_')
for i in ('?', '"', '\'', '!'):
title = title.replace(i, '')
filename = os.path.join(
cache_dir, 'readability',
title + '_' + timestamp() + '.' + ext)
error = action.generate_document(data, url,
ext, filename)
if error:
response = ('> {}\n'
'Failed to export {}. '
'Reason: {}'
.format(url, ext.upper(),
error))
else:
url = await upload.start(self, jid,
filename)
await send_oob_message(self, jid, url)
else:
response = ('> {}\n'
'Failed to fetch URL. Reason: {}'
.format(url, code))
await task.start_tasks_xmpp(self, jid, ['status'])
else:
response = 'Missing entry index number.'
else:
response = ('Unsupported filetype.\n'
'Try: epub, html, md (markdown), '
'pdf, or txt (text)')
if response:
logging.warning('Error for URL {}: {}'.format(url, error))
send_reply_message(self, message, response)
await action.download_document(self, message, jid, jid_file,
message_text, ix_url, False)
case _ if (message_lowercase.startswith('content')):
message_text = message_text[8:]
ix_url = message_text.split(' ')[0]
await action.download_document(self, message, jid, jid_file,
message_text, ix_url, True)
# case _ if (message_lowercase.startswith('http')) and(
# message_lowercase.endswith('.opml')):
# url = message_text
@ -565,8 +495,8 @@ async def message(self, message):
# status_message = (
# '📥️ Procesing request to import feeds...'
# )
# await XmppStatus.send(
# self, jid, status_message, status_type)
# XmppPresence.send(
# self, jid, status_message, status_type=status_type)
# db_file = config.get_pathname_to_database(jid_file)
# count = await action.import_opml(db_file, url)
# if count:
@ -581,7 +511,7 @@ async def message(self, message):
# jid, ['status'])
# await task.start_tasks_xmpp(
# self, jid, ['status'])
# send_reply_message(self, message, response)
# XmppMessage.send_reply(self, message, response)
case _ if (message_lowercase.startswith('http') or
message_lowercase.startswith('feed:')):
url = message_text
@ -590,7 +520,8 @@ async def message(self, message):
status_message = ('📫️ Processing request '
'to fetch data from {}'
.format(url))
await XmppStatus.send(self, jid, status_message, status_type)
XmppPresence.send(self, jid, status_message,
status_type=status_type)
if url.startswith('feed:'):
url = uri.feed_to_http(url)
url = (uri.replace_hostname(url, 'feed')) or url
@ -605,7 +536,7 @@ async def message(self, message):
# 'of being added to the subscription '
# 'list.'.format(url)
# )
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('feeds'):
query = message_text[6:]
if query:
@ -619,14 +550,14 @@ async def message(self, message):
db_file = config.get_pathname_to_database(jid_file)
result = await sqlite.get_feeds(db_file)
response = action.list_feeds(result)
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'goodbye':
if message['type'] == 'groupchat':
await groupchat.leave(self, jid)
await XmppGroupchat.leave(self, jid)
await XmppBookmark.remove(self, muc_jid)
else:
response = 'This command is valid in groupchat only.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('interval'):
key = message_text[:8]
val = message_text[9:]
@ -636,14 +567,14 @@ async def message(self, message):
muc_jid = uri.check_xmpp_uri(message_text[5:])
if muc_jid:
# TODO probe JID and confirm it's a groupchat
await groupchat.join(self, jid, muc_jid)
await XmppGroupchat.join(self, jid, muc_jid)
response = ('Joined groupchat {}'
.format(message_text))
else:
response = ('> {}\n'
'XMPP URI is not valid.'
.format(message_text))
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('length'):
key = message_text[:6]
val = message_text[7:]
@ -692,7 +623,7 @@ async def message(self, message):
# ).format(val)
# else:
# response = 'Missing value.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'new':
db_file = config.get_pathname_to_database(jid_file)
key = 'old'
@ -702,7 +633,7 @@ async def message(self, message):
else:
await sqlite.set_settings_value(db_file, [key, val])
response = 'Only new items of newly added feeds will be sent.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
# TODO Will you add support for number of messages?
case 'next':
# num = message_text[5:]
@ -739,7 +670,7 @@ async def message(self, message):
else:
await sqlite.set_settings_value(db_file, [key, val])
response = 'All items of newly added feeds will be sent.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('quantum'):
key = message_text[:7]
val = message_text[8:]
@ -762,12 +693,12 @@ async def message(self, message):
response = 'Enter a numeric value only.'
else:
response = 'Missing value.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'random':
# TODO /questions/2279706/select-random-row-from-a-sqlite-table
# NOTE sqlitehandler.get_entry_unread
response = 'Updates will be sent by random order.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('read'):
data = message_text[5:]
data = data.split()
@ -776,7 +707,8 @@ async def message(self, message):
status_type = 'dnd'
status_message = ('📫️ Processing request to fetch data from {}'
.format(url))
await XmppStatus.send(self, jid, status_message, status_type)
XmppPresence.send(self, jid, status_message,
status_type=status_type)
if url.startswith('feed:'):
url = uri.feed_to_http(url)
url = (uri.replace_hostname(url, 'feed')) or url
@ -796,7 +728,7 @@ async def message(self, message):
response = ('Enter command as follows:\n'
'`read <url>` or `read <url> <number>`\n'
'URL must not contain white space.')
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
await task.start_tasks_xmpp(self, jid, ['status'])
case _ if message_lowercase.startswith('recent'):
num = message_text[7:]
@ -813,7 +745,7 @@ async def message(self, message):
response = 'Enter a numeric value only.'
else:
response = 'Missing value.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('remove'):
ix_url = message_text[7:]
if ix_url:
@ -859,14 +791,15 @@ async def message(self, message):
await task.start_tasks_xmpp(self, jid, ['status'])
else:
response = 'Missing feed URL or index number.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('reset'):
# TODO Reset also by ID
ix_url = message_text[6:]
await task.clean_tasks_xmpp(jid, ['status'])
status_type = 'dnd'
status_message = '📫️ Marking entries as read...'
await XmppStatus.send(self, jid, status_message, status_type)
XmppPresence.send(self, jid, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
if ix_url:
db_file = config.get_pathname_to_database(jid_file)
@ -905,7 +838,7 @@ async def message(self, message):
else:
await sqlite.mark_all_as_read(db_file)
response = 'All entries have been marked as read.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
await task.start_tasks_xmpp(self, jid, ['status'])
case _ if message_lowercase.startswith('search'):
query = message_text[7:]
@ -918,25 +851,13 @@ async def message(self, message):
response = 'Enter at least 2 characters to search'
else:
response = 'Missing search query.'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'start':
# response = 'Updates are enabled.'
key = 'enabled'
val = 1
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.start_tasks_xmpp(self, jid)
response = 'Updates are enabled.'
# print(current_time(), 'task_manager[jid]')
# print(task_manager[jid])
send_reply_message(self, message, response)
await action.xmpp_start_updates(self, message, jid, jid_file)
case 'stats':
db_file = config.get_pathname_to_database(jid_file)
response = await action.list_statistics(db_file)
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('disable '):
ix = message_text[8:]
db_file = config.get_pathname_to_database(jid_file)
@ -946,7 +867,7 @@ async def message(self, message):
.format(ix))
except:
response = 'No news source with index {}.'.format(ix)
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('enable'):
ix = message_text[7:]
db_file = config.get_pathname_to_database(jid_file)
@ -956,29 +877,29 @@ async def message(self, message):
.format(ix))
except:
response = 'No news source with index {}.'.format(ix)
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case 'stop':
await action.xmpp_stop_updates(self, message, jid, jid_file)
case 'support':
# TODO Send an invitation.
response = 'Join xmpp:slixfeed@chat.woodpeckersnest.space?join'
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _ if message_lowercase.startswith('xmpp:'):
muc_jid = uri.check_xmpp_uri(message_text)
if muc_jid:
# TODO probe JID and confirm it's a groupchat
await groupchat.join(self, jid, muc_jid)
await XmppGroupchat.join(self, jid, muc_jid)
response = ('Joined groupchat {}'
.format(message_text))
else:
response = ('> {}\n'
'XMPP URI is not valid.'
.format(message_text))
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
case _:
response = ('Unknown command. '
'Press "help" for list of commands')
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
# TODO Use message correction here
# NOTE This might not be a good idea if
# commands are sent one close to the next
@ -988,7 +909,7 @@ async def message(self, message):
command_time_total = command_time_finish - command_time_start
command_time_total = round(command_time_total, 3)
response = 'Finished. Total time: {}s'.format(command_time_total)
send_reply_message(self, message, response)
XmppMessage.send_reply(self, message, response)
if not response: response = 'EMPTY MESSAGE - ACTION ONLY'
data_dir = config.get_default_data_directory()
@ -1010,67 +931,3 @@ async def message(self, message):
'{}\n'
.format(message_text, jid, jid_file, response)
)
def send_reply_message(self, message, response):
message.reply(response).send()
# TODO Solve this function
def send_oob_reply_message(message, url, response):
reply = message.reply(response)
reply['oob']['url'] = url
reply.send()
async def send_oob_message(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 greet(self, jid, chat_type='chat'):
# messages = [
# 'Greetings!',
# 'I'm {}, the news anchor.'.format(self.nick),
# 'My job is to bring you the latest news '
# 'from sources you provide me with.',
# 'You may always reach me via '
# 'xmpp:{}?message'.format(self.boundjid.bare)
# ]
# for message in messages:
# self.send_message(
# mto=jid,
# mfrom=self.boundjid.bare,
# mbody=message,
# mtype=chat_type
# )
def greet(self, jid, chat_type='chat'):
message = (
'Greetings!\n'
'I am {}, the news anchor.\n'
'My job is to bring you the latest '
'news from sources you provide me with.\n'
'You may always reach me via xmpp:{}?message').format(
self.alias,
self.boundjid.bare
)
self.send_message(
mto=jid,
mfrom=self.boundjid.bare,
mbody=message,
mtype=chat_type
)

View file

@ -10,65 +10,40 @@ TODO
"""
import slixfeed.xmpp.utility as utility
class XmppRoster:
async def remove(self, jid):
"""
Remove JID to roster.
async def remove(self, jid):
"""
Remove JID to roster.
Parameters
----------
jid : str
Jabber ID.
Parameters
----------
jid : str
Jabber ID.
Returns
-------
None.
"""
self.update_roster(
jid,
subscription="remove"
)
Returns
-------
None.
"""
self.update_roster(jid, subscription="remove")
async def add(self, jid):
"""
Add JID to roster.
async def add(self, jid):
"""
Add JID to roster.
Parameters
----------
jid : str
Jabber ID.
Add JID to roster if it is not already in roster.
Returns
-------
None.
"""
if await utility.get_chat_type(self, jid) == "groupchat":
# Check whether JID is in bookmarks; otherwise, add it.
print(jid, "is muc")
return
else:
Parameters
----------
jid : str
Jabber ID.
Returns
-------
None.
"""
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,
pfrom=self.boundjid.bare,
ptype="subscribe",
pnick=self.alias
)
self.update_roster(
jid,
subscription="both"
)
self.update_roster(jid, subscription="both")
def remove_subscription(self):
"""
Remove subscription from contacts that don't share their presence.
Returns
-------
None.
"""

View file

@ -1,71 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from slixmpp.xmlstream.matcher import MatchXPath
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream import ET
async def request(self, jid):
"""
Ask contant to settle subscription.
Parameters
----------
jid : str
Jabber ID.
Returns
-------
None.
"""
# Check whether JID is subscribed; otherwise, ask for presence.
if self.is_component:
presence_probe = ET.Element('presence')
presence_probe.attrib['type'] = 'probe'
presence_probe.attrib['to'] = jid
print(presence_probe)
breakpoint()
self.send_raw(str(presence_probe))
presence_probe.send()
elif not self.client_roster[jid]["to"]:
self.send_presence_subscription(
pto=jid,
pfrom=self.boundjid.bare,
ptype="subscribe",
pnick=self.alias
)
self.send_message(
mto=jid,
mfrom=self.boundjid.bare,
# mtype="headline",
msubject="RSS News Bot",
mbody=(
"Share online status to receive updates."
),
mnick=self.alias
)
self.send_presence(
pto=jid,
pfrom=self.boundjid.bare,
# Accept symbol 🉑️ 👍️ ✍
pstatus=(
"✒️ Share online status to receive updates."
),
# ptype="subscribe",
pnick=self.alias
)
async def unsubscribed(self, presence):
jid = presence["from"].bare
self.send_message(
mto=jid,
mfrom=self.boundjid.bare,
mbody="You have been unsubscribed."
)
self.send_presence(
pto=jid,
pfrom=self.boundjid.bare,
pstatus="🖋️ Subscribe to receive updates",
pnick=self.alias
)

View file

@ -1,19 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from slixfeed.xmpp.utility import get_chat_type
class XmppStatus:
async def send(self, jid, status_message, status_type=None, chat_type=None):
if not chat_type:
chat_type = await get_chat_type(self, jid)
self.send_presence(
pto=jid,
pfrom=self.boundjid.bare,
pshow=status_type,
pstatus=status_message,
ptype=chat_type
)

View file

@ -11,38 +11,39 @@ from slixmpp.exceptions import IqTimeout, IqError
from slixmpp.plugins.xep_0363.http_upload import HTTPError
# import sys
class XmppUpload:
async def start(self, jid, filename, domain=None):
logging.info('Uploading file %s...', filename)
try:
upload_file = self['xep_0363'].upload_file
# if self.encrypted and not self['xep_0454']:
# print(
# 'The xep_0454 module isn\'t available. '
# 'Ensure you have \'cryptography\' '
# 'from extras_require installed.',
# file=sys.stderr,
# )
# return
# elif self.encrypted:
# upload_file = self['xep_0454'].upload_file
async def start(self, jid, filename, domain=None):
logging.info('Uploading file %s...', filename)
try:
url = await upload_file(
filename, domain, timeout=10,
)
logging.info('Upload successful!')
logging.info('Sending file to %s', jid)
except HTTPError:
url = (
"Error: It appears that this server doesn't support "
"HTTP File Upload."
)
logging.error(
"It appears that this server doesn't support HTTP File Upload."
)
# raise HTTPError(
# "This server doesn't appear to support HTTP File Upload"
upload_file = self['xep_0363'].upload_file
# if self.encrypted and not self['xep_0454']:
# print(
# 'The xep_0454 module isn\'t available. '
# 'Ensure you have \'cryptography\' '
# 'from extras_require installed.',
# file=sys.stderr,
# )
except IqTimeout:
raise TimeoutError('Could not send message in time')
return url
# return
# elif self.encrypted:
# upload_file = self['xep_0454'].upload_file
try:
url = await upload_file(
filename, domain, timeout=10,
)
logging.info('Upload successful!')
logging.info('Sending file to %s', jid)
except HTTPError:
url = (
"Error: It appears that this server doesn't support "
"HTTP File Upload."
)
logging.error(
"It appears that this server doesn't support HTTP File Upload."
)
# raise HTTPError(
# "This server doesn't appear to support HTTP File Upload"
# )
except IqTimeout:
raise TimeoutError('Could not send message in time')
return url

View file

@ -1,9 +1,11 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from slixmpp.exceptions import IqTimeout
from slixmpp.exceptions import IqError, IqTimeout
import logging
# class XmppChat
# class XmppUtility:
async def get_chat_type(self, jid):
"""
@ -43,11 +45,15 @@ async def get_chat_type(self, jid):
'Chat Type: {}'.format(jid, chat_type))
return chat_type
# TODO Test whether this exception is realized
except IqError as e:
message = ('IQ Error\n'
'IQ Stanza: {}'
'Jabber ID: {}'
.format(e, jid))
logging.error(message)
except IqTimeout as e:
messages = [
("Timeout IQ"),
("IQ Stanza:", e),
("Jabber ID:", jid)
]
for message in messages:
logging.error(message)
message = ('IQ Timeout\n'
'IQ Stanza: {}'
'Jabber ID: {}'
.format(e, jid))
logging.error(message)