From a1e4cf0f71daca1de36d73bac5883e14e0ebe1dc Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Thu, 11 Jul 2024 17:43:28 +0300 Subject: [PATCH] Add a journal list to pages with a single item; Add an option to enable PubSubToAtom as a service; Add an option to confine queries to a specified hostname. --- README.md | 4 +- configuration.toml | 16 +++- css/stylesheet.css | 35 +++++++++ pubsub_to_atom.py | 172 +++++++++++++++++++++++++++++++++++------- script/postprocess.js | 90 +++++++++++++++++++++- xsl/atom_as_xhtml.xsl | 19 ++--- 6 files changed, 295 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 0f88b1b..e42c59b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ XMPP PubSub To Atom ("XPTA") is a simple Python script that parses XMPP Pubsub N XPTA generates Atom syndication feeds ([RFC 4287](https://www.rfc-editor.org/rfc/rfc4287)) from XMPP PubSub nodes ([XEP-0060](http://xmpp.org/extensions/xep-0060.html)). -This software was inspired from Tigase and was motivated by Movim. +XPTA includes [XSLT ](https://www.w3.org/TR/xslt/) stylesheets that transforms PubSub nodes into static XHTML journal sites. + +XPTA was inspired from Tigase and was motivated by Movim. ## Preview diff --git a/configuration.toml b/configuration.toml index ed6eb52..df55891 100644 --- a/configuration.toml +++ b/configuration.toml @@ -1,3 +1,15 @@ +# An account to connect PubSubToAtom to the XMPP network. [account] -xmpp = "" -pass = "" +xmpp = "" # Jabber ID. +pass = "" # Password. + +# A default node, when no arguments are set. +[default] +pubsub = "blog.jmp.chat" # Jabber ID. +nodeid = "urn:xmpp:microblog:0" # Node ID. + +# Settings +[settings] +service = 1 # Enable server as a service. +include = "" # Limit service to a given domain. +operator = "" # A Jabber ID to contact with, in case of an error. diff --git a/css/stylesheet.css b/css/stylesheet.css index 76256b8..91a2cad 100644 --- a/css/stylesheet.css +++ b/css/stylesheet.css @@ -51,6 +51,27 @@ h1#title, h2#subtitle, #actions, #references { border-top: 1px solid #eee; } +#articles { + display: flex; +} + +#articles #journal { + margin-left: 2%; + margin-right: 2%; + min-width: 350px; + padding-bottom: 50px; + width: 20%; +} + +#articles #journal ul { + /* height: 500px; */ + line-height: 160%; + overflow: auto; +} + +#articles #journal > a { + font-weight: bold; +} #articles > ul > li > div > p.content { font-size: 120%; @@ -115,3 +136,17 @@ h1#title, h2#subtitle, #actions, #references { cursor: pointer; text-decoration: underline; } + +@media (max-width: 950px) { + #articles { + display: unset; + } + + #articles #journal { + margin-left: unset; + margin-right: unset; + padding-bottom: 50px; + min-width: unset; + width: unset; + } +} diff --git a/pubsub_to_atom.py b/pubsub_to_atom.py index 79f394e..c15d958 100644 --- a/pubsub_to_atom.py +++ b/pubsub_to_atom.py @@ -6,7 +6,9 @@ from fastapi import FastAPI, Request, Response from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles import feedgenerator +import json from slixmpp import ClientXMPP +from slixmpp.exceptions import IqError, IqTimeout import xml.etree.ElementTree as ET #import importlib.resources @@ -28,6 +30,7 @@ xmpp = None # Mount static graphic, script and stylesheet directories app.mount("/css", StaticFiles(directory="css"), name="css") +app.mount("/data", StaticFiles(directory="data"), name="data") app.mount("/graphic", StaticFiles(directory="graphic"), name="graphic") app.mount("/script", StaticFiles(directory="script"), name="script") app.mount("/xsl", StaticFiles(directory="xsl"), name="xsl") @@ -40,40 +43,136 @@ async def favicon(): async def view_pubsub(request: Request): global xmpp if not xmpp: - with open('configuration.toml', mode="rb") as configuration: - credentials = tomllib.load(configuration)['account'] + credentials = get_configuration('account') xmpp = XmppInstance(credentials['xmpp'], credentials['pass']) # xmpp.connect() pubsub = request.query_params.get('pubsub', '') node = request.query_params.get('node', '') item_id = request.query_params.get('item', '') - if pubsub and node and item_id: - iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id) - link = 'xmpp:{pubsub}?;node={node};item={item}'.format( - pubsub=pubsub, node=node, item=item_id) - xml_atom = pubsub_to_atom(iq, link) - result = append_stylesheet(xml_atom) - elif pubsub and node: - iq = await xmpp.plugin['xep_0060'].get_items(pubsub, node) - link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node) - xml_atom = pubsub_to_atom(iq, link) - result = append_stylesheet(xml_atom) - elif pubsub: - iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub) - link = 'xmpp:{pubsub}'.format(pubsub=pubsub) - result = pubsub_to_opml(iq) - elif node: - result = 'PubSub parameter is missing.' - else: - result = ('Mandatory parameter PubSub and ' - 'optional parameter Node are missing.') - return Response(content=result, media_type="application/xml") + settings = get_configuration('settings') + result = None + if settings['service']: + if settings['include'] in pubsub or not settings['include']: + if pubsub and node and item_id: + iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id) + link = 'xmpp:{pubsub}?;node={node};item={item}'.format( + pubsub=pubsub, node=node, item=item_id) + xml_atom = generate_rfc_4287(iq, link) + result = append_stylesheet(xml_atom) + iq = await get_node_items(pubsub, node) + if iq: + generate_json(iq, node) + else: + operator = get_configuration('settings')['operator'] + json_data = [{'title' : 'Timeout Error: Press here to contact the operator.', + 'link' : 'xmpp:{}?message'.format(operator)}] + filename = 'data/{}.json'.format(node) + with open(filename, 'w', encoding='utf-8') as f: + json.dump(json_data, f, ensure_ascii=False, indent=4) + # try: + # iq = await get_node_items(pubsub, node) + # generate_json(iq, node) + # except: + # operator = get_configuration('settings')['operator'] + # json_data = [{'title' : 'Timeout retrieving node items from {}'.format(node), + # 'link' : 'xmpp:{}?message'.format(operator)}] + # filename = 'data/{}.json'.format(node) + # with open(filename, 'w', encoding='utf-8') as f: + # json.dump(json_data, f, ensure_ascii=False, indent=4) + elif pubsub and node: + iq = await get_node_items(pubsub, node) + link = form_a_link(pubsub, node) + xml_atom = generate_rfc_4287(iq, link) + result = append_stylesheet(xml_atom) + elif pubsub: + iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub) + link = 'xmpp:{pubsub}'.format(pubsub=pubsub) + result = pubsub_to_opml(iq) + elif node: + text = 'PubSub parameter is missing.' + xml_atom = error_message(text) + result = append_stylesheet(xml_atom) + # else: + # result = ('Mandatory parameter PubSub and ' + # 'optional parameter Node are missing.') + else: + text = 'The given domain {} is not allowed.'.format(pubsub) + xml_atom = error_message(text) + result = append_stylesheet(xml_atom) + default = get_configuration('default') + if not result: + if default['pubsub'] and default['nodeid']: + if not pubsub and not node: + pubsub = default['pubsub'] + node = default['nodeid'] + iq = await get_node_items(pubsub, node) + link = form_a_link(pubsub, node) + xml_atom = generate_rfc_4287(iq, link) + result = append_stylesheet(xml_atom) + elif not settings['service']: + pubsub = default['pubsub'] + node = default['nodeid'] + iq = await get_node_items(pubsub, node) + link = form_a_link(pubsub, node) + xml_atom = generate_rfc_4287(iq, link) + result = append_stylesheet(xml_atom) + else: + text = 'Please contact the administrator and ask him to set default PubSub and Node ID.' + xml_atom = error_message(text) + result = append_stylesheet(xml_atom) + response = Response(content=result, media_type="application/xml") + return response -def pubsub_to_atom(iq, link): +def get_configuration(section): + with open('configuration.toml', mode="rb") as configuration: + result = tomllib.load(configuration)[section] + return result + +#@timeout(5) +async def get_node_items(pubsub, node): + try: + iq = await xmpp.plugin['xep_0060'].get_items(pubsub, node, timeout=5) + return iq + except IqTimeout as e: + print(e) + +def form_a_link(pubsub, node): + link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node) + return link + +def error_message(text): + """Error message in RFC 4287: The Atom Syndication Format.""" + feed = feedgenerator.Atom1Feed( + description = ('This is a syndication feed generated with PubSub To ' + 'Atom, which conveys XEP-0060: Publish-Subscribe nodes ' + 'to standard RFC 4287: The Atom Syndication Format.'), + language = 'en', + link = '', + subtitle = 'XMPP PubSub To Atom', + title = 'StreamBurner') + namespace = '{http://www.w3.org/2005/Atom}' + feed_url = 'gemini://schimon.i2p/' + # create entry + feed.add_item( + description = text, + # enclosure = feedgenerator.Enclosure(enclosure, enclosure_size, enclosure_type) if args.entry_enclosure else None, + link = '', + # pubdate = updated, + title = 'Error', + # unique_id = '' + ) + xml_atom = feed.writeString('utf-8') + xml_atom_extended = append_element( + xml_atom, + 'generator', + 'XPTA: XMPP PubSub To Atom') + return xml_atom_extended + +def generate_rfc_4287(iq, link): """Convert XEP-0060: Publish-Subscribe to RFC 4287: The Atom Syndication Format.""" feed = feedgenerator.Atom1Feed( - description = ('This is a syndication feed generated with PubSub to ' + description = ('This is a syndication feed generated with PubSub To ' 'Atom, which conveys XEP-0060: Publish-Subscribe nodes ' 'to standard RFC 4287: The Atom Syndication Format.'), language = iq['pubsub']['items']['lang'], @@ -87,7 +186,6 @@ def pubsub_to_atom(iq, link): namespace = '{http://www.w3.org/2005/Atom}' title = item.find(namespace + 'title') title = None if title == None else title.text - feed_url = 'gemini://schimon.i2p/' updated = item.find(namespace + 'updated') updated = None if updated == None else updated.text # if updated: updated = datetime.datetime.fromisoformat(updated) @@ -113,6 +211,28 @@ def pubsub_to_atom(iq, link): 'XPTA: XMPP PubSub To Atom') return xml_atom_extended +def generate_json(iq, node): + """Create a JSON file from node items.""" + json_data = [] + entries = iq['pubsub']['items'] + for entry in entries: + item = entry['payload'] + namespace = '{http://www.w3.org/2005/Atom}' + title = item.find(namespace + 'title') + title = None if title == None else title.text + # updated = item.find(namespace + 'updated') + # updated = None if updated == None else updated.text + # if updated: updated = datetime.datetime.fromisoformat(updated) + link = item.find(namespace + 'link') + link = '' if link == None else link.attrib['href'] + json_data_entry = {'title' : title, + 'link' : link} + json_data.append(json_data_entry) + if len(json_data) > 6: break + filename = 'data/{}.json'.format(node) + with open(filename, 'w', encoding='utf-8') as f: + json.dump(json_data, f, ensure_ascii=False, indent=4) + """Patch function to append elements which are not provided by feedgenerator""" def append_element(xml_data, element, text): root = ET.fromstring(xml_data) diff --git a/script/postprocess.js b/script/postprocess.js index ed316a8..9ec4ba9 100644 --- a/script/postprocess.js +++ b/script/postprocess.js @@ -1,13 +1,17 @@ -window.onload = function(){ +window.onload = async function(){ // Fix button follow let follow = document.querySelector('#follow'); - feedUrl = location.href.replace(/^https?:/, 'feed:'); + //let feedUrl = location.href.replace(/^https?:/, 'feed:'); + let locationHref = new URL(location.href); + let node = locationHref.searchParams.get('node') + let pubsub = locationHref.searchParams.get('pubsub') + let feedUrl = `feed://${location.host}/atom?pubsub=${pubsub}&node=${node}`; follow.href = feedUrl; follow.addEventListener ('click', function() { window.open(feedUrl, "_self"); }); // Fix button subtome - document.querySelector('#subtome').href='https://www.subtome.com/#/subscribe?feeds='+location.href; + document.querySelector('#subtome').href='https://www.subtome.com/#/subscribe?feeds=' + feedUrl; // Convert ISO8601 To UTC for (let element of document.querySelectorAll('#articles > ul > li > div > h4, #feed > #header > h2#subtitle.date')) { let timeStamp = new Date(element.textContent); @@ -18,6 +22,63 @@ window.onload = function(){ let markDown = element.textContent element.innerHTML = marked.parse(markDown); } + // Build a journal list + if (locationHref.searchParams.get('item')) { + node = locationHref.searchParams.get('node') + pubsub = locationHref.searchParams.get('pubsub') + itemsList = await openJson(node) + let elementDiv = document.createElement('div'); + elementDiv.id = 'journal'; + let elementH3 = document.createElement('h3'); + elementH3.textContent = 'Journal'; + elementDiv.appendChild(elementH3); + let elementH4 = document.createElement('h4'); + elementH4.textContent = node; + elementDiv.appendChild(elementH4); + let elementUl = document.createElement('ul'); + elementDiv.appendChild(elementUl); + for (let item of itemsList) { + let elementLi = document.createElement('li'); + let elementA = document.createElement('a'); + elementA.textContent = item.title; + elementA.href = item.link; + elementLi.appendChild(elementA); + elementUl.appendChild(elementLi); + } + let elementB = document.createElement('b'); + elementB.textContent = 'Actions'; + elementDiv.appendChild(elementB); + let elementUl2 = document.createElement('ul'); + elementDiv.appendChild(elementUl2); + links = [ + {'text' : 'Subscribe from an XMPP client...', + 'href' : `xmpp:${pubsub}?pubsub;action=subscribe;node=${node}`}, + {'text' : 'Subscribe with a News Reader...', + 'href' : `feed://${location.host}/atom?pubsub=${pubsub}&node=${node}`}, + {'text' : 'Browse the journal...', + 'href' : `atom?pubsub=${pubsub}&node=${node}`} + ] + for (let link of links) { + let elementLi = document.createElement('li'); + let elementA = document.createElement('a'); + elementA.textContent = link.text; + elementA.href = link.href; + elementLi.appendChild(elementA); + elementUl2.appendChild(elementLi); + } + elementDiv.appendChild(elementUl2); + // document.querySelector('#feed').appendChild(elementDiv); // This would result in a combination of Title, Article, and Journal + document.querySelector('#articles').appendChild(elementDiv); + } + // Convert URI xmpp: to URI http: links. + for (let xmppLink of document.querySelectorAll('a[href^="xmpp:"]')) { + xmppUri = new URL(xmppLink); + let parameters = xmppUri.search.split(';'); + let node = parameters.find(parameter => parameter.startsWith('node=')).split('=')[1]; + let item = parameters.find(parameter => parameter.startsWith('item=')).split('=')[1]; + let pubsub = xmppUri.pathname; + xmppLink.href = `atom?pubsub=${pubsub}&node=${node}&item=${item}` + } // Display a selection of suggested software. const selection = { 'akregator' : { @@ -99,3 +160,26 @@ window.onload = function(){ document.body.appendChild(elementDiv); }); } + +async function openJson(nodeId) { + return fetch(`/data/${nodeId}.json`) + .then(response => { + if (!response.ok) { + throw new Error('HTTP Error: ' + response.status); + } + return response.json(); + }) + .then(json => { + return json; + }) + .catch(err => { + throw new Error('Error: ' + err); + }) +} + +function parseXmppPubsubLink(link) { + const parts = link.split(';'); + const node = parts.find(part => part.startsWith('node=')).split('=')[1]; + const item = parts.find(part => part.startsWith('item=')).split('=')[1]; + return { node, item }; +} diff --git a/xsl/atom_as_xhtml.xsl b/xsl/atom_as_xhtml.xsl index 02ee0d5..725c3b7 100644 --- a/xsl/atom_as_xhtml.xsl +++ b/xsl/atom_as_xhtml.xsl @@ -391,7 +391,7 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
+ title='An Inclusive Space On The Jabber Network.'> JoinJabber Libervia + title='The Social Platform Shaped For Your Community.'> Movim + title='A Project To Improve The Quality Of User-To-User Messaging Applications That Use Xmpp.'> Modern + title='The Universal Messaging Standard.'> XMPP

- This is an XMPP news feed which is conveyed as HTML, - and it can even be viewed by a syndication feed reader - which provides automated notifications on desktop and - mobile. Click here for - a selection of software that would fit you best! + This is an XMPP news feed which is conveyed as an HTML + document, and it can even be viewed by a syndication + feed reader which provides automated notifications on + desktop and mobile. Click + here for a selection of software and pick the + ones that would fit you best!