Add PubSub functionality.

This commit is contained in:
Schimon Jehudah 2024-03-24 08:14:20 +00:00
parent aed43f34dd
commit 35beab7802
7 changed files with 900 additions and 214 deletions

View file

@ -745,11 +745,10 @@ def export_to_opml(jid, filename, results):
tree.write(filename)
async def import_opml(db_file, url):
async def import_opml(db_file, result):
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} url: {}'
.format(function_name, db_file, url))
result = await fetch.http(url)
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
if not result['error']:
document = result['content']
root = ET.fromstring(document)
@ -1070,137 +1069,97 @@ async def scan_json(self, jid_bare, db_file, url):
new_entries)
async def view_feed(url):
def view_feed(url, feed):
function_name = sys._getframe().f_code.co_name
logger.debug('{}: url: {}'
.format(function_name, url))
while True:
result = await fetch.http(url)
if not result['error']:
document = result['content']
status = result['status_code']
feed = parse(document)
# if is_feed(url, feed):
if is_feed(feed):
if "title" in feed["feed"].keys():
title = feed["feed"]["title"]
else:
title = urlsplit(url).netloc
entries = feed.entries
response = "Preview of {}:\n\n```\n".format(title)
counter = 0
for entry in entries:
counter += 1
if entry.has_key("title"):
title = entry.title
else:
title = "*** No title ***"
if entry.has_key("link"):
# link = complete_url(source, entry.link)
link = join_url(url, entry.link)
link = trim_url(link)
else:
link = "*** No link ***"
if entry.has_key("published"):
date = entry.published
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = dt.rfc2822_to_iso8601(date)
else:
date = "*** No date ***"
response += ("Title : {}\n"
"Date : {}\n"
"Link : {}\n"
"Count : {}\n"
"\n"
.format(title, date, link, counter))
if counter > 4:
break
response += (
"```\nSource: {}"
).format(url)
break
else:
result = await crawl.probe_page(url, document)
if isinstance(result, str):
response = result
break
else:
url = result[0]
if "title" in feed["feed"].keys():
title = feed["feed"]["title"]
else:
title = urlsplit(url).netloc
entries = feed.entries
response = "Preview of {}:\n\n```\n".format(title)
counter = 0
for entry in entries:
counter += 1
if entry.has_key("title"):
title = entry.title
else:
response = ('> {}\nFailed to load URL. Reason: {}'
.format(url, status))
title = "*** No title ***"
if entry.has_key("link"):
# link = complete_url(source, entry.link)
link = join_url(url, entry.link)
link = trim_url(link)
else:
link = "*** No link ***"
if entry.has_key("published"):
date = entry.published
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = dt.rfc2822_to_iso8601(date)
else:
date = "*** No date ***"
response += ("Title : {}\n"
"Date : {}\n"
"Link : {}\n"
"Count : {}\n"
"\n"
.format(title, date, link, counter))
if counter > 4:
break
response += (
"```\nSource: {}"
).format(url)
return response
async def view_entry(url, num):
def view_entry(url, feed, num):
function_name = sys._getframe().f_code.co_name
logger.debug('{}: url: {} num: {}'
.format(function_name, url, num))
while True:
result = await fetch.http(url)
if not result['error']:
document = result['content']
status = result['status_code']
feed = parse(document)
# if is_feed(url, feed):
if is_feed(feed):
if "title" in feed["feed"].keys():
title = feed["feed"]["title"]
else:
title = urlsplit(url).netloc
entries = feed.entries
num = int(num) - 1
entry = entries[num]
response = "Preview of {}:\n\n```\n".format(title)
if entry.has_key("title"):
title = entry.title
else:
title = "*** No title ***"
if entry.has_key("published"):
date = entry.published
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = dt.rfc2822_to_iso8601(date)
else:
date = "*** No date ***"
if entry.has_key("summary"):
summary = entry.summary
# Remove HTML tags
summary = BeautifulSoup(summary, "lxml").text
# TODO Limit text length
summary = summary.replace("\n\n\n", "\n\n")
else:
summary = "*** No summary ***"
if entry.has_key("link"):
# link = complete_url(source, entry.link)
link = join_url(url, entry.link)
link = trim_url(link)
else:
link = "*** No link ***"
response = ("{}\n"
"\n"
# "> {}\n"
"{}\n"
"\n"
"{}\n"
"\n"
.format(title, summary, link))
break
else:
result = await crawl.probe_page(url, document)
if isinstance(result, str):
response = result
break
else:
url = result[0]
else:
response = ('> {}\nFailed to load URL. Reason: {}'
.format(url, status))
break
if "title" in feed["feed"].keys():
title = feed["feed"]["title"]
else:
title = urlsplit(url).netloc
entries = feed.entries
num = int(num) - 1
entry = entries[num]
response = "Preview of {}:\n\n```\n".format(title)
if entry.has_key("title"):
title = entry.title
else:
title = "*** No title ***"
if entry.has_key("published"):
date = entry.published
date = dt.rfc2822_to_iso8601(date)
elif entry.has_key("updated"):
date = entry.updated
date = dt.rfc2822_to_iso8601(date)
else:
date = "*** No date ***"
if entry.has_key("summary"):
summary = entry.summary
# Remove HTML tags
summary = BeautifulSoup(summary, "lxml").text
# TODO Limit text length
summary = summary.replace("\n\n\n", "\n\n")
else:
summary = "*** No summary ***"
if entry.has_key("link"):
# link = complete_url(source, entry.link)
link = join_url(url, entry.link)
link = trim_url(link)
else:
link = "*** No link ***"
response = ("{}\n"
"\n"
# "> {}\n"
"{}\n"
"\n"
"{}\n"
"\n"
.format(title, summary, link))
return response

View file

@ -1,2 +1,2 @@
__version__ = '0.1.46'
__version_info__ = (0, 1, 46)
__version__ = '0.1.47'
__version_info__ = (0, 1, 47)

View file

@ -30,10 +30,12 @@ NOTE
"""
import asyncio
from feedparser import parse
import slixmpp
import slixfeed.task as task
from slixfeed.url import join_url, trim_url
# from slixmpp.plugins.xep_0363.http_upload import FileTooBig, HTTPError, UploadServiceNotFound
# from slixmpp.plugins.xep_0402 import BookmarkStorage, Conference
# from slixmpp.plugins.xep_0048.stanza import Bookmarks
@ -49,9 +51,11 @@ from slixfeed.version import __version__
from slixfeed.xmpp.bookmark import XmppBookmark
from slixfeed.xmpp.connect import XmppConnect
from slixfeed.xmpp.muc import XmppGroupchat
from slixfeed.xmpp.iq import XmppIQ
from slixfeed.xmpp.message import XmppMessage
import slixfeed.xmpp.process as process
import slixfeed.xmpp.profile as profile
from slixfeed.xmpp.publish import XmppPubsub
from slixfeed.xmpp.roster import XmppRoster
# import slixfeed.xmpp.service as service
from slixfeed.xmpp.presence import XmppPresence
@ -123,6 +127,9 @@ class Slixfeed(slixmpp.ClientXMPP):
# Handler for task messages
self.pending_tasks = {}
# Handler for task messages counter
# self.pending_tasks_counter = 0
# Handler for ping
self.task_ping_instance = {}
@ -684,15 +691,18 @@ class Slixfeed(slixmpp.ClientXMPP):
# NOTE https://codeberg.org/poezio/slixmpp/issues/3515
# if is_operator(self, jid_bare):
self['xep_0050'].add_command(node='recent',
name='📰️ Browse',
handler=self._handle_recent)
self['xep_0050'].add_command(node='subscription',
name='🪶️ Subscribe',
handler=self._handle_subscription_add)
self['xep_0050'].add_command(node='publish',
name='📣️ Publish',
handler=self._handle_publish)
name='📻 Publish',
handler=self._handle_pubsub_add)
self['xep_0050'].add_command(node='post',
name='📣️ Post',
handler=self._handle_post)
self['xep_0050'].add_command(node='recent',
name='📰️ Browse',
handler=self._handle_recent)
self['xep_0050'].add_command(node='subscriptions',
name='🎫️ Subscriptions',
handler=self._handle_subscriptions)
@ -727,33 +737,56 @@ class Slixfeed(slixmpp.ClientXMPP):
# Special interface
# http://jabber.org/protocol/commands#actions
async def _handle_publish(self, iq, session):
form = self['xep_0004'].make_form('form', 'Publish')
form['instructions'] = ('In order to publish via PubSub, you will '
'have to choose a PubSub hostname and '
'have a privilege to publish into it.')
# TODO Select from list-multi
async def _handle_post(self, iq, session):
jid_bare = session['from'].bare
if not is_operator(self, jid_bare):
text_warn = 'PubSub is not available.'
session['notes'] = [['warn', text_warn]]
return session
form = self['xep_0004'].make_form('form', 'Post')
form['instructions'] = ('In order to post to PubSub, you will have to '
'choose a PubSub Jabber ID (e.g. {}) and '
'Slixfeed has to be allowed to publish into '
'it.'
.format(self.boundjid.bare))
form.add_field(var='url',
ftype='text-single',
label='URL',
desc='Enter subscription URL.',
desc='Enter a subscription URL.',
value='http://',
required=True)
form.add_field(var='pubsub',
# options = form.add_field(var='jid',
# ftype='list-single',
# label='PubSub',
# desc='Select a PubSub Jabber ID.',
# value=self.boundjid.bare,
# required=True)
# iq = await self['xep_0030'].get_items(jid=self.boundjid.domain)
# for item in iq['disco_items']['items']:
# jid = item[0]
# if item[1]: name = item[1]
# elif item[2]: name = item[2]
# else: name = jid
# options.addOption(jid, name)
form.add_field(var='jid',
ftype='text-single',
label='PubSub',
desc='Enter a PubSub URL.',
value='pubsub.' + self.boundjid.host,
desc='Enter a PubSub Jabber ID.',
value=self.boundjid.bare,
# value='pubsub.' + self.boundjid.host,
required=True)
form.add_field(var='node',
ftype='text-single',
label='Node',
desc='Node to publish at.',
desc=('Enter a node to publish to (The default value '
'is "urn:xmpp:microblog:0" which is allocated to '
'each Jabber ID in the Movim system).'),
value='urn:xmpp:microblog:0',
required=False)
options = form.add_field(var='xep',
ftype='list-single',
label='Type',
desc='Select XEP.',
label='Protocol',
desc='Select XMPP Extension Protocol.',
value='0060',
required=True)
options.addOption('XEP-0060: Publish-Subscribe', '0060')
@ -766,14 +799,176 @@ class Slixfeed(slixmpp.ClientXMPP):
session['payload'] = form
return session
def _handle_preview(self, payload, session):
async def _handle_preview(self, payload, session):
jid_full = str(session['from'])
function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}'
.format(function_name, jid_full))
text_note = ('PubSub support will be available soon.')
session['notes'] = [['info', text_note]]
session['payload'] = None
values = payload['values']
jid = values['jid']
jid_bare = session['from'].bare
if jid != jid_bare and not is_operator(self, jid_bare):
text_warn = ('Posting to {} is restricted to operators only.'
.format(jid_bare))
session['allow_prev'] = False
session['has_next'] = False
session['next'] = None
session['notes'] = [['warn', text_warn]]
session['prev'] = None
session['payload'] = None
return session
node = values['node']
url = values['url']
xep = values['xep']
form = self['xep_0004'].make_form('form', 'Publish')
while True:
result = await fetch.http(url)
status = result['status_code']
if not result['error']:
document = result['content']
feed = parse(document)
# if is_feed(url, feed):
if action.is_feed(feed):
form['instructions'] = 'Select entries to publish.'
options = form.add_field(var='entries',
ftype='list-multi',
label='Titles',
desc='Select entries to post.',
required=True)
if "title" in feed["feed"].keys():
title = feed["feed"]["title"]
else:
title = url
entries = feed.entries
entry_ix = 0
for entry in entries:
if entry.has_key("title"):
title = entry.title
else:
if entry.has_key("published"):
title = entry.published
title = dt.rfc2822_to_iso8601(title)
elif entry.has_key("updated"):
title = entry.updated
title = dt.rfc2822_to_iso8601(title)
else:
title = "*** No title ***"
options.addOption(title, str(entry_ix))
entry_ix += 1
if entry_ix > 9:
break
session['allow_prev'] = True
session['has_next'] = True
session['next'] = self._handle_post_complete
session['prev'] = self._handle_post
session['payload'] = form
break
else:
result = await crawl.probe_page(url, document)
if isinstance(result, list):
form['instructions'] = ('Discovered {} subscriptions '
'from the given URL. Please '
'choose a subscription.'
.format(len(result)))
options = form.add_field(var='url',
ftype='list-single',
label='Feeds',
desc='Select a feed.',
required=True)
results = result
for result in results:
title = result['name']
url = result['link']
title = title if title else url
options.addOption(title, url)
session['allow_prev'] = True
session['has_next'] = True
session['next'] = self._handle_preview
session['prev'] = self._handle_post
session['payload'] = form
break
else:
url = result['link']
else:
text_error = ('Failed to load URL {}'
'\n\n'
'Reason: {}'
.format(url, status))
session['notes'] = [['error', text_error]]
session['payload'] = None
break
form.add_field(var='node',
ftype='hidden',
value=node)
form.add_field(var='jid',
ftype='hidden',
value=jid)
# It is essential to place URL at the end, because it might mutate
# For example http://blacklistednews.com would change
# to https://www.blacklistednews.com/rss.php
form.add_field(var='url',
ftype='hidden',
value=url)
form.add_field(var='xep',
ftype='hidden',
value=xep)
return session
async def _handle_post_complete(self, payload, session):
values = payload['values']
entries = values['entries']
# It might not be good to pass feed object as its size might be too big
# Consider a handler self.feeds[url][feed] or self.feeds[jid][url][feed]
# It is not possible to assign non-str to transfer.
# feed = values['feed']
node = values['node'][0]
jid = values['jid'][0]
url = values['url'][0]
xep = values['xep'][0]
result = await fetch.http(url)
if 'content' in result:
document = result['content']
feed = parse(document)
if feed.feed.has_key('title'):
feed_title = feed.feed.title
if feed.feed.has_key('description'):
feed_summary = feed.feed.description
elif feed.feed.has_key('subtitle'):
feed_summary = feed.feed.subtitle
else:
feed_summary = None
iq_create_node = XmppPubsub.create_node(
self, jid, node, xep, feed_title, feed_summary)
await XmppIQ.send(self, iq_create_node)
feed_version = feed.version
for entry in entries:
entry = int(entry)
feed_entry = feed.entries[entry]
# if feed.entries[entry].has_key("title"):
# title = feed.entries[entry].title
# else:
# if feed.entries[entry].has_key("published"):
# title = feed.entries[entry].published
# title = dt.rfc2822_to_iso8601(title)
# elif feed.entries[entry].has_key("updated"):
# title = feed.entries[entry].updated
# title = dt.rfc2822_to_iso8601(title)
# else:
# title = "*** No title ***"
# if feed.entries[entry].has_key("summary"):
# summary = feed.entries[entry].summary
iq_create_entry = XmppPubsub.create_entry(
self, jid, node, feed_entry, feed_version)
await XmppIQ.send(self, iq_create_entry)
text_info = 'Posted {} entries.'.format(len(entries))
session['allow_prev'] = False
session['has_next'] = False
session['next'] = None
session['notes'] = [['info', text_info]]
session['prev'] = None
session['payload'] = None
else:
session['payload'] = payload
return session
async def _handle_profile(self, iq, session):
@ -950,9 +1145,8 @@ class Slixfeed(slixmpp.ClientXMPP):
db_file = config.get_pathname_to_database(jid_file)
# In this case (as is typical), the payload is a form
values = payload['values']
for value in values:
key = value
val = values[value]
for key in values:
val = values[key]
# NOTE We might want to add new keywords from
# an empty form instead of editing a form.
# keywords = sqlite.get_filter_value(db_file, key)
@ -973,6 +1167,80 @@ class Slixfeed(slixmpp.ClientXMPP):
return session
async def _handle_pubsub_add(self, iq, session):
jid_full = str(session['from'])
function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}'
.format(function_name, jid_full))
jid_bare = session['from'].bare
if not is_operator(self, jid_bare):
text_warn = 'PubSub is not available.'
session['notes'] = [['warn', text_warn]]
return session
chat_type = await get_chat_type(self, jid_bare)
moderator = None
if chat_type == 'groupchat':
moderator = is_moderator(self, jid_bare, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Publish')
form['instructions'] = ('In order to publish via PubSub, you will '
'have to choose a PubSub Jabber ID (e.g. '
'{}) and Slixfeed has to be allowed to '
'publish into it.'
.format('pubsub.' + self.boundjid.host))
form.add_field(var='subscription',
# TODO Make it possible to add several subscriptions at once;
# Similarly to BitTorrent trackers list
# ftype='text-multi',
# label='Subscription URLs',
# desc=('Add subscriptions one time per '
# 'subscription.'),
ftype='text-single',
label='URL',
desc='Enter a subscription URL.',
value='http://',
required=True)
form.add_field(var='jid',
ftype='text-single',
label='PubSub',
desc='Enter a PubSub Jabber ID.',
value=self.boundjid.bare,
required=True)
form.add_field(var='node',
ftype='text-single',
label='Node',
desc=('Enter a node to publish to (The default '
'value is "urn:xmpp:microblog:0" which '
'is allocated to each Jabber ID in the '
'Movim system).'),
value='urn:xmpp:microblog:0',
required=False)
options = form.add_field(var='xep',
ftype='list-single',
label='Protocol',
desc='Select XMPP Extension Protocol.',
value='0060',
required=True)
options.addOption('XEP-0060: Publish-Subscribe', '0060')
options.addOption('XEP-0277: Microblogging over XMPP', '0277')
options.addOption('XEP-0472: Pubsub Social Feed', '0472')
# form.add_field(var='scan',
# ftype='boolean',
# label='Scan',
# desc='Scan URL for validity (recommended).',
# value=True)
session['allow_prev'] = False
session['has_next'] = True
session['next'] = self._handle_subscription_new
session['prev'] = None
session['payload'] = form
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid_bare))
session['notes'] = [['warn', text_warn]]
return session
async def _handle_subscription_add(self, iq, session):
jid_full = str(session['from'])
function_name = sys._getframe().f_code.co_name
@ -995,9 +1263,18 @@ class Slixfeed(slixmpp.ClientXMPP):
# 'subscription.'),
ftype='text-single',
label='URL',
desc='Enter subscription URL.',
desc='Enter a subscription URL.',
value='http://',
required=True)
if is_operator(self, jid_bare):
form.add_field(ftype='fixed',
label='Subscriber')
form.add_field(var='jid',
ftype='text-single',
label='Jabber ID',
desc=('Enter a Jabber ID to add the '
'subscription to (The default Jabber ID '
'is your own).'))
# form.add_field(var='scan',
# ftype='boolean',
# label='Scan',
@ -2016,7 +2293,7 @@ class Slixfeed(slixmpp.ClientXMPP):
ftype='list-single',
label='Choose',
required=True,
value='admin')
value='export')
jid = session['from'].bare
if is_operator(self, jid):
options.addOption('Administration', 'admin')
@ -2291,7 +2568,8 @@ class Slixfeed(slixmpp.ClientXMPP):
jid_bare = session['from'].bare
jid_file = jid_bare.replace('/', '_')
db_file = config.get_pathname_to_database(jid_file)
count = await action.import_opml(db_file, url)
result = await fetch.http(url)
count = await action.import_opml(db_file, result)
try:
int(count)
# form = self['xep_0004'].make_form('result', 'Done')
@ -2868,9 +3146,8 @@ class Slixfeed(slixmpp.ClientXMPP):
Config.add_settings_jid(self.settings, jid_bare, db_file)
# In this case (as is typical), the payload is a form
values = payload['values']
for value in values:
key = value
val = values[value]
for key in values:
val = values[key]
if key in ('enabled', 'media', 'old'):
if val == True:

View file

@ -37,9 +37,11 @@ from slixfeed.version import __version__
from slixfeed.xmpp.connect import XmppConnect
# NOTE MUC is possible for component
from slixfeed.xmpp.muc import XmppGroupchat
from slixfeed.xmpp.iq import XmppIQ
from slixfeed.xmpp.message import XmppMessage
import slixfeed.xmpp.process as process
import slixfeed.xmpp.profile as profile
from slixfeed.xmpp.publish import XmppPubsub
# from slixfeed.xmpp.roster import XmppRoster
# import slixfeed.xmpp.service as service
from slixfeed.xmpp.presence import XmppPresence
@ -114,6 +116,9 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# Handler for task messages
self.pending_tasks = {}
# Handler for task messages counter
# self.pending_tasks_counter = 0
# Handler for ping
self.task_ping_instance = {}
@ -643,15 +648,18 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# NOTE https://codeberg.org/poezio/slixmpp/issues/3515
# if is_operator(self, jid_bare):
self['xep_0050'].add_command(node='recent',
name='📰️ Browse',
handler=self._handle_recent)
self['xep_0050'].add_command(node='subscription',
name='🪶️ Subscribe',
handler=self._handle_subscription_add)
self['xep_0050'].add_command(node='publish',
name='📣️ Publish',
handler=self._handle_publish)
name='📻 Publish',
handler=self._handle_pubsub_add)
self['xep_0050'].add_command(node='post',
name='📣️ Post',
handler=self._handle_post)
self['xep_0050'].add_command(node='recent',
name='📰️ Browse',
handler=self._handle_recent)
self['xep_0050'].add_command(node='subscriptions',
name='🎫️ Subscriptions',
handler=self._handle_subscriptions)
@ -686,33 +694,56 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# Special interface
# http://jabber.org/protocol/commands#actions
async def _handle_publish(self, iq, session):
form = self['xep_0004'].make_form('form', 'Publish')
form['instructions'] = ('In order to publish via PubSub, you will '
'have to choose a PubSub hostname and '
'have a privilege to publish into it.')
# TODO Select from list-multi
async def _handle_post(self, iq, session):
jid_bare = session['from'].bare
if not is_operator(self, jid_bare):
text_warn = 'PubSub is not available.'
session['notes'] = [['warn', text_warn]]
return session
form = self['xep_0004'].make_form('form', 'Post')
form['instructions'] = ('In order to post to PubSub, you will have to '
'choose a PubSub Jabber ID (e.g. {}) and '
'Slixfeed has to be allowed to publish into '
'it.'
.format(self.boundjid.bare))
form.add_field(var='url',
ftype='text-single',
label='URL',
desc='Enter subscription URL.',
desc='Enter a subscription URL.',
value='http://',
required=True)
form.add_field(var='pubsub',
# options = form.add_field(var='jid',
# ftype='list-single',
# label='PubSub',
# desc='Select a PubSub Jabber ID.',
# value=self.boundjid.bare,
# required=True)
# iq = await self['xep_0030'].get_items(jid=self.boundjid.domain)
# for item in iq['disco_items']['items']:
# jid = item[0]
# if item[1]: name = item[1]
# elif item[2]: name = item[2]
# else: name = jid
# options.addOption(jid, name)
form.add_field(var='jid',
ftype='text-single',
label='PubSub',
desc='Enter a PubSub URL.',
value='pubsub.' + self.boundjid.host,
desc='Enter a PubSub Jabber ID.',
value=self.boundjid.bare,
# value='pubsub.' + self.boundjid.host,
required=True)
form.add_field(var='node',
ftype='text-single',
label='Node',
desc='Node to publish at.',
desc=('Enter a node to publish to (The default value '
'is "urn:xmpp:microblog:0" which is allocated to '
'each Jabber ID in the Movim system).'),
value='urn:xmpp:microblog:0',
required=False)
options = form.add_field(var='xep',
ftype='list-single',
label='Type',
desc='Select XEP.',
label='Protocol',
desc='Select XMPP Extension Protocol.',
value='0060',
required=True)
options.addOption('XEP-0060: Publish-Subscribe', '0060')
@ -725,14 +756,176 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
session['payload'] = form
return session
def _handle_preview(self, payload, session):
async def _handle_preview(self, payload, session):
jid_full = str(session['from'])
function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}'
.format(function_name, jid_full))
text_note = ('PubSub support will be available soon.')
session['notes'] = [['info', text_note]]
session['payload'] = None
values = payload['values']
jid = values['jid']
jid_bare = session['from'].bare
if jid != jid_bare and not is_operator(self, jid_bare):
text_warn = ('Posting to {} is restricted to operators only.'
.format(jid_bare))
session['allow_prev'] = False
session['has_next'] = False
session['next'] = None
session['notes'] = [['warn', text_warn]]
session['prev'] = None
session['payload'] = None
return session
node = values['node']
url = values['url']
xep = values['xep']
form = self['xep_0004'].make_form('form', 'Publish')
while True:
result = await fetch.http(url)
status = result['status_code']
if not result['error']:
document = result['content']
feed = parse(document)
# if is_feed(url, feed):
if action.is_feed(feed):
form['instructions'] = 'Select entries to publish.'
options = form.add_field(var='entries',
ftype='list-multi',
label='Titles',
desc='Select entries to post.',
required=True)
if "title" in feed["feed"].keys():
title = feed["feed"]["title"]
else:
title = url
entries = feed.entries
entry_ix = 0
for entry in entries:
if entry.has_key("title"):
title = entry.title
else:
if entry.has_key("published"):
title = entry.published
title = dt.rfc2822_to_iso8601(title)
elif entry.has_key("updated"):
title = entry.updated
title = dt.rfc2822_to_iso8601(title)
else:
title = "*** No title ***"
options.addOption(title, str(entry_ix))
entry_ix += 1
if entry_ix > 9:
break
session['allow_prev'] = True
session['has_next'] = True
session['next'] = self._handle_post_complete
session['prev'] = self._handle_post
session['payload'] = form
break
else:
result = await crawl.probe_page(url, document)
if isinstance(result, list):
form['instructions'] = ('Discovered {} subscriptions '
'from the given URL. Please '
'choose a subscription.'
.format(len(result)))
options = form.add_field(var='url',
ftype='list-single',
label='Feeds',
desc='Select a feed.',
required=True)
results = result
for result in results:
title = result['name']
url = result['link']
title = title if title else url
options.addOption(title, url)
session['allow_prev'] = True
session['has_next'] = True
session['next'] = self._handle_preview
session['prev'] = self._handle_post
session['payload'] = form
break
else:
url = result['link']
else:
text_error = ('Failed to load URL {}'
'\n\n'
'Reason: {}'
.format(url, status))
session['notes'] = [['error', text_error]]
session['payload'] = None
break
form.add_field(var='node',
ftype='hidden',
value=node)
form.add_field(var='jid',
ftype='hidden',
value=jid)
# It is essential to place URL at the end, because it might mutate
# For example http://blacklistednews.com would change
# to https://www.blacklistednews.com/rss.php
form.add_field(var='url',
ftype='hidden',
value=url)
form.add_field(var='xep',
ftype='hidden',
value=xep)
return session
async def _handle_post_complete(self, payload, session):
values = payload['values']
entries = values['entries']
# It might not be good to pass feed object as its size might be too big
# Consider a handler self.feeds[url][feed] or self.feeds[jid][url][feed]
# It is not possible to assign non-str to transfer.
# feed = values['feed']
node = values['node'][0]
jid = values['jid'][0]
url = values['url'][0]
xep = values['xep'][0]
result = await fetch.http(url)
if 'content' in result:
document = result['content']
feed = parse(document)
if feed.feed.has_key('title'):
feed_title = feed.feed.title
if feed.feed.has_key('description'):
feed_summary = feed.feed.description
elif feed.feed.has_key('subtitle'):
feed_summary = feed.feed.subtitle
else:
feed_summary = None
iq_create_node = XmppPubsub.create_node(
self, jid, node, xep, feed_title, feed_summary)
await XmppIQ.send(self, iq_create_node)
feed_version = feed.version
for entry in entries:
entry = int(entry)
feed_entry = feed.entries[entry]
# if feed.entries[entry].has_key("title"):
# title = feed.entries[entry].title
# else:
# if feed.entries[entry].has_key("published"):
# title = feed.entries[entry].published
# title = dt.rfc2822_to_iso8601(title)
# elif feed.entries[entry].has_key("updated"):
# title = feed.entries[entry].updated
# title = dt.rfc2822_to_iso8601(title)
# else:
# title = "*** No title ***"
# if feed.entries[entry].has_key("summary"):
# summary = feed.entries[entry].summary
iq_create_entry = XmppPubsub.create_entry(
self, jid, node, feed_entry, feed_version)
await XmppIQ.send(self, iq_create_entry)
text_info = 'Posted {} entries.'.format(len(entries))
session['allow_prev'] = False
session['has_next'] = False
session['next'] = None
session['notes'] = [['info', text_info]]
session['prev'] = None
session['payload'] = None
else:
session['payload'] = payload
return session
async def _handle_profile(self, iq, session):
@ -909,9 +1102,8 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
db_file = config.get_pathname_to_database(jid_file)
# In this case (as is typical), the payload is a form
values = payload['values']
for value in values:
key = value
val = values[value]
for key in values:
val = values[key]
# NOTE We might want to add new keywords from
# an empty form instead of editing a form.
# keywords = sqlite.get_filter_value(db_file, key)
@ -932,6 +1124,80 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
return session
async def _handle_pubsub_add(self, iq, session):
jid_full = str(session['from'])
function_name = sys._getframe().f_code.co_name
logger.debug('{}: jid_full: {}'
.format(function_name, jid_full))
jid_bare = session['from'].bare
if not is_operator(self, jid_bare):
text_warn = 'PubSub is not available.'
session['notes'] = [['warn', text_warn]]
return session
chat_type = await get_chat_type(self, jid_bare)
moderator = None
if chat_type == 'groupchat':
moderator = is_moderator(self, jid_bare, jid_full)
if chat_type == 'chat' or moderator:
form = self['xep_0004'].make_form('form', 'Publish')
form['instructions'] = ('In order to publish via PubSub, you will '
'have to choose a PubSub Jabber ID (e.g. '
'{}) and Slixfeed has to be allowed to '
'publish into it.'
.format('pubsub.' + self.boundjid.host))
form.add_field(var='subscription',
# TODO Make it possible to add several subscriptions at once;
# Similarly to BitTorrent trackers list
# ftype='text-multi',
# label='Subscription URLs',
# desc=('Add subscriptions one time per '
# 'subscription.'),
ftype='text-single',
label='URL',
desc='Enter a subscription URL.',
value='http://',
required=True)
form.add_field(var='jid',
ftype='text-single',
label='PubSub',
desc='Enter a PubSub Jabber ID.',
value=self.boundjid.bare,
required=True)
form.add_field(var='node',
ftype='text-single',
label='Node',
desc=('Enter a node to publish to (The default '
'value is "urn:xmpp:microblog:0" which '
'is allocated to each Jabber ID in the '
'Movim system).'),
value='urn:xmpp:microblog:0',
required=False)
options = form.add_field(var='xep',
ftype='list-single',
label='Protocol',
desc='Select XMPP Extension Protocol.',
value='0060',
required=True)
options.addOption('XEP-0060: Publish-Subscribe', '0060')
options.addOption('XEP-0277: Microblogging over XMPP', '0277')
options.addOption('XEP-0472: Pubsub Social Feed', '0472')
# form.add_field(var='scan',
# ftype='boolean',
# label='Scan',
# desc='Scan URL for validity (recommended).',
# value=True)
session['allow_prev'] = False
session['has_next'] = True
session['next'] = self._handle_subscription_new
session['prev'] = None
session['payload'] = form
else:
text_warn = ('This resource is restricted to moderators of {}.'
.format(jid_bare))
session['notes'] = [['warn', text_warn]]
return session
async def _handle_subscription_add(self, iq, session):
jid_full = str(session['from'])
function_name = sys._getframe().f_code.co_name
@ -954,9 +1220,18 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
# 'subscription.'),
ftype='text-single',
label='URL',
desc='Enter subscription URL.',
desc='Enter a subscription URL.',
value='http://',
required=True)
if is_operator(self, jid_bare):
form.add_field(ftype='fixed',
label='Subscriber')
form.add_field(var='jid',
ftype='text-single',
label='Jabber ID',
desc=('Enter a Jabber ID to add the '
'subscription to (The default Jabber ID '
'is your own).'))
# form.add_field(var='scan',
# ftype='boolean',
# label='Scan',
@ -1975,7 +2250,7 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
ftype='list-single',
label='Choose',
required=True,
value='admin')
value='export')
jid = session['from'].bare
if is_operator(self, jid):
options.addOption('Administration', 'admin')
@ -2250,7 +2525,8 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
jid_bare = session['from'].bare
jid_file = jid_bare.replace('/', '_')
db_file = config.get_pathname_to_database(jid_file)
count = await action.import_opml(db_file, url)
result = await fetch.http(url)
count = await action.import_opml(db_file, result)
try:
int(count)
# form = self['xep_0004'].make_form('result', 'Done')
@ -2827,9 +3103,8 @@ class SlixfeedComponent(slixmpp.ComponentXMPP):
Config.add_settings_jid(self.settings, jid_bare, db_file)
# In this case (as is typical), the payload is a form
values = payload['values']
for value in values:
key = value
val = values[value]
for key in values:
val = values[key]
if key in ('enabled', 'media', 'old'):
if val == True:

View file

@ -1,14 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from slixmpp.exceptions import IqError
import logging
from slixmpp.exceptions import IqError, IqTimeout
class XmppIQ:
async def send(self, iq):
try:
await iq.send(timeout=5)
except IqError as e:
if e.etype == 'cancel' and e.condition == 'conflict':
return
raise
except (IqError, IqTimeout) as e:
logging.error('Error XmppIQ')
logging.error(str(e))

View file

@ -23,14 +23,13 @@ TODO
"""
from slixfeed.xmpp.publish import XmppPubsub
from slixfeed.xmpp.iq import XmppIQ
import asyncio
from feedparser import parse
import logging
import os
import slixfeed.action as action
import slixfeed.config as config
import slixfeed.crawl as crawl
from slixfeed.config import Config
import slixfeed.dt as dt
import slixfeed.fetch as fetch
@ -109,15 +108,19 @@ async def message(self, message):
# pending_tasks_num = len(self.pending_tasks[jid_bare])
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
# self.pending_tasks_counter += 1
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
count = await action.import_opml(db_file, url)
result = await fetch.http(url)
count = await action.import_opml(db_file, result)
if count:
response = 'Successfully imported {} feeds.'.format(count)
else:
response = 'OPML file was not imported.'
del self.pending_tasks[jid_bare][pending_tasks_num]
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
key_list = ['status']
await task.start_tasks_xmpp(self, jid_bare, key_list)
XmppMessage.send_reply(self, message, response)
@ -286,13 +289,6 @@ async def message(self, message):
'Send "help" for further instructions.\n'
.format(self.alias))
XmppMessage.send_reply(self, message, response)
title = message_lowercase
summary = message_text
# from random import randrange
node = str(randrange(10000, 99999))
iq = XmppPubsub.create(self, title, summary, node)
await XmppIQ.send(self, iq)
# case _ if message_lowercase.startswith('activate'):
# if message['type'] == 'groupchat':
@ -529,6 +525,8 @@ async def message(self, message):
# pending_tasks_num = len(self.pending_tasks[jid_bare])
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
# self.pending_tasks_counter += 1
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
filename = await action.export_feeds(self, jid_bare,
@ -541,6 +539,7 @@ async def message(self, message):
chat_type = await get_chat_type(self, jid_bare)
XmppMessage.send_oob(self, jid_bare, url, chat_type)
del self.pending_tasks[jid_bare][pending_tasks_num]
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
key_list = ['status']
await task.start_tasks_xmpp(self, jid_bare, key_list)
else:
@ -579,6 +578,8 @@ async def message(self, message):
# pending_tasks_num = len(self.pending_tasks[jid_bare])
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
# self.pending_tasks_counter += 1
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
@ -642,6 +643,7 @@ async def message(self, message):
'Try: epub, html, md (markdown), '
'pdf, or txt (text)')
del self.pending_tasks[jid_bare][pending_tasks_num]
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
key_list = ['status']
await task.start_tasks_xmpp(self, jid_bare, key_list)
if response:
@ -657,16 +659,20 @@ async def message(self, message):
# pending_tasks_num = len(self.pending_tasks[jid_bare])
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
# self.pending_tasks_counter += 1
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
count = await action.import_opml(db_file, url)
result = await fetch.http(url)
count = await action.import_opml(db_file, result)
if count:
response = ('Successfully imported {} feeds.'
.format(count))
else:
response = 'OPML file was not imported.'
del self.pending_tasks[jid_bare][pending_tasks_num]
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
key_list = ['status']
await task.start_tasks_xmpp(self, jid_bare, key_list)
XmppMessage.send_reply(self, message, response)
@ -680,7 +686,8 @@ async def message(self, message):
# pending_tasks_num = len(self.pending_tasks[jid_bare])
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
print(self.pending_tasks)
# self.pending_tasks_counter += 1
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
if url.startswith('feed:'):
@ -716,8 +723,8 @@ async def message(self, message):
'added to subscription list.'
.format(result['link'], result['name']))
# task.clean_tasks_xmpp(self, jid_bare, ['status'])
print(self.pending_tasks)
del self.pending_tasks[jid_bare][pending_tasks_num]
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
print(self.pending_tasks)
key_list = ['status']
await task.start_tasks_xmpp(self, jid_bare, key_list)
@ -929,7 +936,7 @@ async def message(self, message):
status_type = 'dnd'
status_message = ('📫️ Processing request to fetch data '
'from {}'.format(url))
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
@ -939,7 +946,36 @@ async def message(self, message):
match len(data):
case 1:
if url.startswith('http'):
response = await action.view_feed(url)
while True:
result = await fetch.http(url)
if not result['error']:
document = result['content']
status = result['status_code']
feed = parse(document)
# if is_feed(url, feed):
if action.is_feed(feed):
response = action.view_feed(url, feed)
break
else:
result = await crawl.probe_page(url, document)
if isinstance(result, list):
results = result
response = ("Web feeds found for {}\n\n```\n"
.format(url))
for result in results:
response += ("Title : {}\n"
"Link : {}\n"
"\n"
.format(result['name'], result['link']))
response += ('```\nTotal of {} feeds.'
.format(len(results)))
break
else:
url = result[0]
else:
response = ('> {}\nFailed to load URL. Reason: {}'
.format(url, status))
break
else:
response = ('No action has been taken.'
'\n'
@ -947,7 +983,36 @@ async def message(self, message):
case 2:
num = data[1]
if url.startswith('http'):
response = await action.view_entry(url, num)
while True:
result = await fetch.http(url)
if not result['error']:
document = result['content']
status = result['status_code']
feed = parse(document)
# if is_feed(url, feed):
if action.is_feed(feed):
response = action.view_entry(url, feed, num)
break
else:
result = await crawl.probe_page(url, document)
if isinstance(result, list):
results = result
response = ("Web feeds found for {}\n\n```\n"
.format(url))
for result in results:
response += ("Title : {}\n"
"Link : {}\n"
"\n"
.format(result['name'], result['link']))
response += ('```\nTotal of {} feeds.'
.format(len(results)))
break
else:
url = result[0]
else:
response = ('> {}\nFailed to load URL. Reason: {}'
.format(url, status))
break
else:
response = ('No action has been taken.'
'\n'
@ -1041,6 +1106,8 @@ async def message(self, message):
# pending_tasks_num = len(self.pending_tasks[jid_bare])
pending_tasks_num = randrange(10000, 99999)
self.pending_tasks[jid_bare][pending_tasks_num] = status_message
# self.pending_tasks_counter += 1
# self.pending_tasks[jid_bare][self.pending_tasks_counter] = status_message
XmppPresence.send(self, jid_bare, status_message,
status_type=status_type)
db_file = config.get_pathname_to_database(jid_file)
@ -1083,6 +1150,7 @@ async def message(self, message):
response = 'All entries have been marked as read.'
XmppMessage.send_reply(self, message, response)
del self.pending_tasks[jid_bare][pending_tasks_num]
# del self.pending_tasks[jid_bare][self.pending_tasks_counter]
key_list = ['status']
await task.start_tasks_xmpp(self, jid_bare, key_list)
case _ if message_lowercase.startswith('search '):

View file

@ -3,12 +3,18 @@
# TODO Implement XEP-0472: Pubsub Social Feed
import hashlib
import slixmpp.plugins.xep_0060.stanza.pubsub as pubsub
from slixmpp.xmlstream import ET
class XmppPubsub:
def create(self, title, summary, node, server):
# TODO Make use of var "xep" with match/case
def create_node(self, jid, node, xep ,title, summary=None):
jid_from = str(self.boundjid) if self.is_component else None
iq = self.Iq(stype="set",
sto=server,
iq = self.Iq(stype='set',
sto=jid,
sfrom=jid_from)
iq['pubsub']['create']['node'] = node
form = iq['pubsub']['configure']['form']
@ -42,3 +48,104 @@ class XmppPubsub:
value='http://www.w3.org/2005/Atom')
return iq
def create_entry(self, jid, node, entry, version):
iq = self.Iq(stype="set", sto=jid)
iq['pubsub']['publish']['node'] = node
item = pubsub.Item()
# From atomtopubsub:
# character / is causing a bug in movim. replacing : and , with - in id.
# It provides nicer urls.
# Respond to atomtopubsub:
# I think it would be a good idea to use md5 checksum of Url as Id for
# cross reference, and namely - in another project to utilize PubSub as
# links sharing system (see del.icio.us) - to share node entries.
# NOTE Warning: Entry might not have a link
# TODO Handle situation error
url_encoded = entry.link.encode()
url_hashed = hashlib.md5(url_encoded)
url_digest = url_hashed.hexdigest()
item['id'] = url_digest
node_entry = ET.Element("entry")
node_entry.set('xmlns', 'http://www.w3.org/2005/Atom')
title = ET.SubElement(node_entry, "title")
title.text = entry.title
updated = ET.SubElement(node_entry, "updated")
updated.text = entry.updated
# Content
if version == 'atom3':
if hasattr(entry.content[0], 'type'):
content = ET.SubElement(node_entry, "content")
content.set('type', entry.content[0].type)
content.text = entry.content[0].value
elif version =='rss20' or 'rss10' or 'atom10':
if hasattr(entry, "content"):
content = ET.SubElement(node_entry, "content")
content.set('type', 'text/html')
content.text = entry.content[0].value
elif hasattr(entry, "description"):
content = ET.SubElement(node_entry,"content")
content.set('type', 'text/html')
content.text = entry.description
print('In Description - PublishX')
# Links
if hasattr(entry, 'links'):
for l in entry.links:
link = ET.SubElement(node_entry, "link")
if hasattr(l, 'href'):
link.set('href', l['href'])
link.set('type', l['type'])
link.set('rel', l['rel'])
elif hasattr(entry, 'link'):
link.set('href', entry['link'])
# Tags
if hasattr(entry, 'tags'):
for t in entry.tags:
tag = ET.SubElement(node_entry, "category")
tag.set('term', t.term)
# Categories
if hasattr(entry,'category'):
for c in entry["category"]:
cat = ET.SubElement(node_entry, "category")
cat.set('category', entry.category[0])
# Authors
if version == 'atom03':
if hasattr(entry, 'authors'):
author = ET.SubElement(node_entry, "author")
name = ET.SubElement(author, "name")
name.text = entry.authors[0].name
if hasattr(entry.authors[0], 'href'):
uri = ET.SubElement(author, "uri")
uri.text = entry.authors[0].href
elif version == 'rss20' or 'rss10' or 'atom10':
if hasattr(entry, 'author'):
author = ET.SubElement(node_entry, "author")
name = ET.SubElement(node_entry, "author")
name.text = entry.author
if hasattr(entry.author, 'href'):
uri = ET.SubElement(author, "uri")
uri.text = entry.authors[0].href
item['payload'] = node_entry
iq['pubsub']['publish'].append(item)
return iq