From 1b63d032292f2daffd749949fefb4feb2c40d5f6 Mon Sep 17 00:00:00 2001 From: sch Date: Thu, 11 Jul 2024 16:54:19 +0200 Subject: [PATCH 01/25] Delete data/README --- data/README | 1 - 1 file changed, 1 deletion(-) delete mode 100644 data/README diff --git a/data/README b/data/README deleted file mode 100644 index 67b9b1a..0000000 --- a/data/README +++ /dev/null @@ -1 +0,0 @@ -This directory caches lists of PubSub nodes. \ No newline at end of file -- 2.47.2 From 16bd475be27062556faaf3a168c12f9417939e19 Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Thu, 11 Jul 2024 19:01:45 +0300 Subject: [PATCH 02/25] Improve error handling. --- pubsub_to_atom.py | 66 ++++++++++++++++++++++++++++++------------- script/postprocess.js | 25 ++++++++-------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/pubsub_to_atom.py b/pubsub_to_atom.py index c15d958..a664497 100644 --- a/pubsub_to_atom.py +++ b/pubsub_to_atom.py @@ -55,21 +55,26 @@ async def view_pubsub(request: Request): 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) + iq = await get_node_item(pubsub, node, item_id) if iq: - generate_json(iq, node) + link = 'xmpp:{pubsub}?;node={node};item={item}'.format( + pubsub=pubsub, node=node, item=item_id) + xml_atom = generate_rfc_4287(iq, link) + 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) 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) + text = 'Please check that PubSub node and item are valid and accessible.' + xml_atom = error_message(text) + result = append_stylesheet(xml_atom) + # try: # iq = await get_node_items(pubsub, node) # generate_json(iq, node) @@ -82,13 +87,22 @@ async def view_pubsub(request: Request): # 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) + if iq: + link = form_a_link(pubsub, node) + xml_atom = generate_rfc_4287(iq, link) + else: + text = 'Please check that PubSub node is valid and accessible.' + xml_atom = error_message(text) 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) + iq = await get_nodes(pubsub) + if iq: + link = 'xmpp:{pubsub}'.format(pubsub=pubsub) + result = pubsub_to_opml(iq) + else: + text = 'Please check that PubSub Jabber ID is valid and accessible.' + xml_atom = error_message(text) + result = append_stylesheet(xml_atom) elif node: text = 'PubSub parameter is missing.' xml_atom = error_message(text) @@ -130,11 +144,25 @@ def get_configuration(section): return result #@timeout(5) +async def get_node_item(pubsub, node, item_id): + try: + iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id, timeout=5) + return iq + except (IqError, IqTimeout) as e: + print(e) + 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: + except (IqError, IqTimeout) as e: + print(e) + +async def get_nodes(pubsub): + try: + await xmpp.plugin['xep_0060'].get_nodes(pubsub, timeout=5) + return iq + except (IqError, IqTimeout) as e: print(e) def form_a_link(pubsub, node): diff --git a/script/postprocess.js b/script/postprocess.js index 9ec4ba9..b7522bb 100644 --- a/script/postprocess.js +++ b/script/postprocess.js @@ -23,10 +23,10 @@ window.onload = async function(){ element.innerHTML = marked.parse(markDown); } // Build a journal list - if (locationHref.searchParams.get('item')) { + itemsList = await openJson(node) + if (itemsList && 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'); @@ -74,10 +74,14 @@ window.onload = async function(){ 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}` + try { + 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}` + } catch (err) { + console.warn(err) + } } // Display a selection of suggested software. const selection = { @@ -173,13 +177,6 @@ async function openJson(nodeId) { return json; }) .catch(err => { - throw new Error('Error: ' + err); + console.warn(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 }; -} -- 2.47.2 From e07ff6e838cb39a18570bf6d680e4461cfe5ca04 Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Thu, 11 Jul 2024 20:56:20 +0300 Subject: [PATCH 03/25] Rename project to XMPP Journal Publisher; Retrieve dates of PubSub node items; Improve CSS stylesheet; Fix JS error. --- README.md | 26 ++++++++++++++------------ configuration.toml | 2 +- css/stylesheet.css | 6 +++--- pubsub_to_atom.py | 31 ++++++++++++++++++++----------- script/postprocess.js | 6 +++--- 5 files changed, 41 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e42c59b..d4d59f2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -# XMPP PubSub To Atom +# XMPP Journal Publisher -XMPP PubSub To Atom ("XPTA") is a simple Python script that parses XMPP Pubsub Nodes and sends them as Atom Syndication Format or OPML over HTTP. +Previously, XMPP PubSub To Atom ("XPTA"). -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)). +XMPP Journal Publisher ("XJP") is a software that parses XMPP Pubsub Nodes and sends them as Atom Syndication Format or OPML over HTTP. -XPTA includes [XSLT ](https://www.w3.org/TR/xslt/) stylesheets that transforms PubSub nodes into static XHTML journal sites. +XJP 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)). -XPTA was inspired from Tigase and was motivated by Movim. +XJP includes [XSLT ](https://www.w3.org/TR/xslt/) stylesheets that transforms PubSub nodes into static XHTML journal sites. + +XJP was inspired from Tigase and was motivated by Movim. ## Preview @@ -17,14 +19,14 @@ XPTA was inspired from Tigase and was motivated by Movim. ## Motivation -PubSub To Atom is a syndication project which makes journals and publications that are hosted on XMPP PubSub nodes, available +XMPP Journal Publisher is a syndication project which makes journals and publications that are hosted on XMPP PubSub nodes, available from HTTP to both, XML news readers and even HTML browsers. -This means that instead of hosting a journal or publication site in the old fashion (i.e. HTML documents hosted on an HTTP server), one only has to have an HTTP server to operate PubSub To Atom, and the rest of the content is delivered from an XMPP server (i.e. PubSub nodes). +This means that instead of hosting a journal or publication site in the old fashion (i.e. HTML documents hosted on an HTTP server), one only has to have an HTTP server to operate XMPP Journal Publisher, and the rest of the content is delivered from an XMPP server (i.e. PubSub nodes). The project also showcases the non-necessity of HTML, as it automatically generates valid XHTML pages by HTML browsers (client-side) from XSLT stylesheets. -Because PubSub To Atom reads XMPP PubSub nodes, it is possible to view a complete set of node items, and even a single node item, which means, that a considered and carefully earnest use of PubSub To Atom would save bandwidth and system overhead, which includes CPU, I/O and RAM usage. +Because XMPP Journal Publisher reads XMPP PubSub nodes, it is possible to view a complete set of node items, and even a single node item, which means, that a considered and carefully earnest use of XMPP Journal Publisher would save bandwidth and system overhead, which includes CPU, I/O and RAM usage. ## Requirements @@ -49,8 +51,8 @@ Because PubSub To Atom reads XMPP PubSub nodes, it is possible to view a complet Extract the source package to a directory that you have permission to run software. ```shell -$ git clone https://git.xmpp-it.net/sch/PubSubToAtom -$ cd PubSubToAtom/ +$ git clone https://git.xmpp-it.net/sch/XJP +$ cd XJP/ ``` ### Configure @@ -59,7 +61,7 @@ Add account credentials to file `configuration.toml`. ### Start -Execute PubSub To Atom with one of the following commands: +Execute XMPP Journal Publisher with one of the following commands: ```shell $ python -m uvicorn pubsub_to_atom:app --reload @@ -132,7 +134,7 @@ Thank you to to Mr. Timothée Jaussoin ([edhelas](https://edhelas.movim.eu/)) wh A special thank you to the gentlemen "d3x" and "cchianel" from IRC channel #python on irc.libera.chat for initial references concerning code, servers and FastAPI. -And an important thank you to Mr. Simone Canaletti ([roughnecks](https://blog.woodpeckersnest.space/)) for testing and deploying PubSub To Atom into production. +And an important thank you to Mr. Simone Canaletti ([roughnecks](https://blog.woodpeckersnest.space/)) for testing and deploying XMPP Journal Publisher into production. ## Similar Projects diff --git a/configuration.toml b/configuration.toml index df55891..06f20bb 100644 --- a/configuration.toml +++ b/configuration.toml @@ -1,4 +1,4 @@ -# An account to connect PubSubToAtom to the XMPP network. +# An account to connect XMPP Journal Publisher to the XMPP network. [account] xmpp = "" # Jabber ID. pass = "" # Password. diff --git a/css/stylesheet.css b/css/stylesheet.css index 91a2cad..a919cd0 100644 --- a/css/stylesheet.css +++ b/css/stylesheet.css @@ -42,6 +42,7 @@ h1#title, h2#subtitle, #actions, #references { #note { line-height: 30px; margin: auto; + margin-top: 0.67em; max-width: 70%; padding: 10px; text-align: center; @@ -59,7 +60,7 @@ h1#title, h2#subtitle, #actions, #references { margin-left: 2%; margin-right: 2%; min-width: 350px; - padding-bottom: 50px; + padding-bottom: 0.67em; width: 20%; } @@ -83,7 +84,7 @@ h1#title, h2#subtitle, #actions, #references { } #articles > ul > li > div.entry { - padding-bottom: 50px; + padding-bottom: 0.67em; } #articles > ul > li > div.entry h1 { @@ -145,7 +146,6 @@ h1#title, h2#subtitle, #actions, #references { #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 a664497..ccb4e3c 100644 --- a/pubsub_to_atom.py +++ b/pubsub_to_atom.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -#import datetime +import datetime from fastapi import FastAPI, Request, Response from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles @@ -65,8 +65,14 @@ async def view_pubsub(request: Request): 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)}] + json_data = [{'title' : 'Error retrieving items list.', + 'link' : ('javascript:alert("XJP: XMPP Journal Publisher has experienced an error ' + 'while attempting to retrieve the list of items for Node {} of PubSub {}.")') + .format(node, pubsub)}, + {'title' : 'Contact the operator.', + 'link' : ('xmpp:{}?message;subject=XJP: XMPP Journal Publisher;body=Greetings! ' + 'I am contacting you to inform you that there is an error listing ' + 'node items for Node {} on PubSub {}.').format(operator, node, pubsub)}] filename = 'data/{}.json'.format(node) with open(filename, 'w', encoding='utf-8') as f: json.dump(json_data, f, ensure_ascii=False, indent=4) @@ -172,12 +178,13 @@ def form_a_link(pubsub, node): 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.'), + description = ('This is a syndication feed generated with XMPP Journal ' + 'Publisher (XJP), which conveys XEP-0060: Publish-' + 'Subscribe nodes to standard RFC 4287: The Atom ' + 'Syndication Format.'), language = 'en', link = '', - subtitle = 'XMPP PubSub To Atom', + subtitle = 'XMPP Journal Publisher', title = 'StreamBurner') namespace = '{http://www.w3.org/2005/Atom}' feed_url = 'gemini://schimon.i2p/' @@ -194,7 +201,7 @@ def error_message(text): xml_atom_extended = append_element( xml_atom, 'generator', - 'XPTA: XMPP PubSub To Atom') + 'XMPP Journal Publisher (XJP)') return xml_atom_extended def generate_rfc_4287(iq, link): @@ -216,7 +223,9 @@ def generate_rfc_4287(iq, link): 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) + published = item.find(namespace + 'published') + published = None if published == None else published.text + if not updated and not published: updated = datetime.datetime.utcnow().isoformat() content = item.find(namespace + 'content') content = 'No content' if content == None else content.text link = item.find(namespace + 'link') @@ -229,14 +238,14 @@ def generate_rfc_4287(iq, link): description = content, # enclosure = feedgenerator.Enclosure(enclosure, enclosure_size, enclosure_type) if args.entry_enclosure else None, link = link, - # pubdate = updated, + pubdate = published or updated, title = title, unique_id = link) xml_atom = feed.writeString('utf-8') xml_atom_extended = append_element( xml_atom, 'generator', - 'XPTA: XMPP PubSub To Atom') + 'XMPP Journal Publisher (XJP)') return xml_atom_extended def generate_json(iq, node): diff --git a/script/postprocess.js b/script/postprocess.js index b7522bb..342b44c 100644 --- a/script/postprocess.js +++ b/script/postprocess.js @@ -51,11 +51,11 @@ window.onload = async function(){ let elementUl2 = document.createElement('ul'); elementDiv.appendChild(elementUl2); links = [ - {'text' : 'Subscribe from an XMPP client...', + {'text' : 'Subscribe from an XMPP client.', 'href' : `xmpp:${pubsub}?pubsub;action=subscribe;node=${node}`}, - {'text' : 'Subscribe with a News Reader...', + {'text' : 'Subscribe with a News Reader.', 'href' : `feed://${location.host}/atom?pubsub=${pubsub}&node=${node}`}, - {'text' : 'Browse the journal...', + {'text' : 'Browse the journal.', 'href' : `atom?pubsub=${pubsub}&node=${node}`} ] for (let link of links) { -- 2.47.2 From d1f1edbaca09b80b18571c0acd67305ca87544e7 Mon Sep 17 00:00:00 2001 From: "Schimon Jehudah, Adv." Date: Fri, 12 Jul 2024 15:39:17 +0300 Subject: [PATCH 04/25] Add OPML support; Set a new default node (Thank you roughnecks); Improve CSS, JS, XSLT; Neglect external libraries to produce syndications. --- configuration.toml | 4 +- css/stylesheet.css | 25 ++- pubsub_to_atom.py | 256 +++++++++++++++++++------------ script/postprocess.js | 139 +++++++++-------- xsl/{stylesheet.xsl => atom.xsl} | 13 +- xsl/atom_as_xhtml.xsl | 23 ++- xsl/opml.xsl | 18 +++ xsl/opml_as_xhtml.xsl | 226 +++++++++++++++++++++++++++ 8 files changed, 519 insertions(+), 185 deletions(-) rename xsl/{stylesheet.xsl => atom.xsl} (86%) create mode 100644 xsl/opml.xsl create mode 100644 xsl/opml_as_xhtml.xsl diff --git a/configuration.toml b/configuration.toml index 06f20bb..e99e40a 100644 --- a/configuration.toml +++ b/configuration.toml @@ -5,8 +5,8 @@ pass = "" # Password. # A default node, when no arguments are set. [default] -pubsub = "blog.jmp.chat" # Jabber ID. -nodeid = "urn:xmpp:microblog:0" # Node ID. +pubsub = "pubsub.woodpeckersnest.space" # Jabber ID. +nodeid = "PlanetJabber" # Node ID. # Settings [settings] diff --git a/css/stylesheet.css b/css/stylesheet.css index a919cd0..e8a8ad2 100644 --- a/css/stylesheet.css +++ b/css/stylesheet.css @@ -7,6 +7,17 @@ body { background: #000; } +code, pre { + overflow: auto; + display: inline-block; + max-height: 500px; + max-width: 100%; } + +img, video { + width: auto; + height: auto; +} + h1#title, h2#subtitle, #actions, #references { text-align: center; text-transform: uppercase; @@ -64,6 +75,7 @@ h1#title, h2#subtitle, #actions, #references { width: 20%; } +#articles #journal ol, #articles #journal ul { /* height: 500px; */ line-height: 160%; @@ -74,24 +86,25 @@ h1#title, h2#subtitle, #actions, #references { font-weight: bold; } -#articles > ul > li > div > p.content { +#articles div.content { font-size: 120%; - line-height: 30px; + line-height: 200%; margin: auto; padding-left: 2%; padding-right: 10%; /* text-align: justify; */ + word-wrap: break-word; } -#articles > ul > li > div.entry { +#articles div.entry { padding-bottom: 0.67em; } -#articles > ul > li > div.entry h1 { +#articles div.entry h1 { font-size: 2vw; } -#articles > ul > li > div.entry h2 { +#articles div.entry h2 { font-size: 1.5vw; } @@ -138,7 +151,7 @@ h1#title, h2#subtitle, #actions, #references { text-decoration: underline; } -@media (max-width: 950px) { +@media (max-width: 1525px) { #articles { display: unset; } diff --git a/pubsub_to_atom.py b/pubsub_to_atom.py index ccb4e3c..475eea2 100644 --- a/pubsub_to_atom.py +++ b/pubsub_to_atom.py @@ -2,10 +2,10 @@ # -*- coding: utf-8 -*- import datetime +from dateutil import parser 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 @@ -57,12 +57,11 @@ async def view_pubsub(request: Request): if pubsub and node and item_id: iq = await get_node_item(pubsub, node, item_id) if iq: - link = 'xmpp:{pubsub}?;node={node};item={item}'.format( - pubsub=pubsub, node=node, item=item_id) - xml_atom = generate_rfc_4287(iq, link) + link = form_an_item_link(pubsub, node, item_id) + xml_atom = generate_atom(iq, link) iq = await get_node_items(pubsub, node) if iq: - generate_json(iq, node) + generate_json(iq) else: operator = get_configuration('settings')['operator'] json_data = [{'title' : 'Error retrieving items list.', @@ -79,7 +78,8 @@ async def view_pubsub(request: Request): else: text = 'Please check that PubSub node and item are valid and accessible.' xml_atom = error_message(text) - result = append_stylesheet(xml_atom) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/Atom') # try: # iq = await get_node_items(pubsub, node) @@ -94,32 +94,37 @@ async def view_pubsub(request: Request): elif pubsub and node: iq = await get_node_items(pubsub, node) if iq: - link = form_a_link(pubsub, node) - xml_atom = generate_rfc_4287(iq, link) + link = form_a_node_link(pubsub, node) + xml_atom = generate_atom(iq, link) else: text = 'Please check that PubSub node is valid and accessible.' xml_atom = error_message(text) - result = append_stylesheet(xml_atom) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/Atom') elif pubsub: iq = await get_nodes(pubsub) if iq: link = 'xmpp:{pubsub}'.format(pubsub=pubsub) - result = pubsub_to_opml(iq) + xml_opml = generate_opml(iq) + result = append_stylesheet(xml_opml, 'opml.xsl') else: text = 'Please check that PubSub Jabber ID is valid and accessible.' xml_atom = error_message(text) - result = append_stylesheet(xml_atom) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/Atom') elif node: text = 'PubSub parameter is missing.' xml_atom = error_message(text) - result = append_stylesheet(xml_atom) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/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) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/Atom') default = get_configuration('default') if not result: if default['pubsub'] and default['nodeid']: @@ -127,20 +132,23 @@ async def view_pubsub(request: Request): pubsub = default['pubsub'] node = default['nodeid'] iq = await get_node_items(pubsub, node) - link = form_a_link(pubsub, node) + link = form_a_node_link(pubsub, node) xml_atom = generate_rfc_4287(iq, link) - result = append_stylesheet(xml_atom) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/Atom') elif not settings['service']: pubsub = default['pubsub'] node = default['nodeid'] iq = await get_node_items(pubsub, node) - link = form_a_link(pubsub, node) + link = form_a_node_link(pubsub, node) xml_atom = generate_rfc_4287(iq, link) - result = append_stylesheet(xml_atom) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/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) + result = append_stylesheet( + xml_atom, 'atom.xsl', namespace='http://www.w3.org/2005/Atom') response = Response(content=result, media_type="application/xml") return response @@ -166,104 +174,125 @@ async def get_node_items(pubsub, node): async def get_nodes(pubsub): try: - await xmpp.plugin['xep_0060'].get_nodes(pubsub, timeout=5) + iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub, timeout=5) return iq except (IqError, IqTimeout) as e: print(e) -def form_a_link(pubsub, node): +def form_a_node_link(pubsub, node): link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node) return link +def form_an_item_link(pubsub, node, item_id): + link = 'xmpp:{pubsub}?;node={node};item={item}'.format( + pubsub=pubsub, node=node, item=item_id) + 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 XMPP Journal ' - 'Publisher (XJP), which conveys XEP-0060: Publish-' - 'Subscribe nodes to standard RFC 4287: The Atom ' - 'Syndication Format.'), - language = 'en', - link = '', - subtitle = 'XMPP Journal Publisher', - 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', - 'XMPP Journal Publisher (XJP)') - return xml_atom_extended + title = 'StreamBurner' + subtitle = 'XMPP Journal Publisher' + description = ('This is a syndication feed generated with XMPP Journal ' + 'Publisher, which conveys XEP-0060: Publish-Subscribe ' + 'nodes to standard RFC 4287: The Atom Syndication Format.') + language = 'en' + feed = ET.Element("feed") + feed.set('xmlns', 'http://www.w3.org/2005/Atom') + ET.SubElement(feed, 'title', {'type': 'text'}).text = title + ET.SubElement(feed, 'subtitle', {'type': 'text'}).text = subtitle + ET.SubElement(feed, 'author', {'name':'XMPP Journal Publisher','email':'xjp@schimon.i2p'}) + ET.SubElement(feed, 'generator', { + 'uri': 'https://git.xmpp-it.net/sch/XMPPJournalPublisher', + 'version': '0.1'}).text = 'XMPP Journal Publisher (XJP)' + ET.SubElement(feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat() + entry = ET.SubElement(feed, 'entry') + ET.SubElement(entry, 'title').text = 'Error' + ET.SubElement(entry, 'id').text = 'xjp-error' + ET.SubElement(entry, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat() + ET.SubElement(entry, 'published').text = datetime.datetime.now(datetime.UTC).isoformat() + # ET.SubElement(entry, 'summary', {'type': summary_type_text}).text = summary_text + ET.SubElement(entry, 'content', {'type': 'text'}).text = text + return ET.tostring(feed, encoding='unicode') -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 ' - 'Atom, which conveys XEP-0060: Publish-Subscribe nodes ' - 'to standard RFC 4287: The Atom Syndication Format.'), - language = iq['pubsub']['items']['lang'], - link = link, - subtitle = 'XMPP PubSub Syndication Feed', - title = iq['pubsub']['items']['node']) - # See also iq['pubsub']['items']['substanzas'] - entries = iq['pubsub']['items'] - for entry in entries: - item = entry['payload'] +# generate_rfc_4287 +def generate_atom(iq, link): + """Generate an Atom Syndication Format (RFC 4287) from a Publish-Subscribe (XEP-0060) node items.""" + pubsub = iq['from'].bare + node = iq['pubsub']['items']['node'] + title = node + link = link + # link = form_a_node_link(pubsub, node) + subtitle = 'XMPP PubSub Syndication Feed' + description = ('This is a syndication feed generated with XMPP Journal ' + 'Publisher, which conveys XEP-0060: Publish-Subscribe ' + 'nodes to standard RFC 4287: The Atom Syndication Format.') + language = iq['pubsub']['items']['lang'] + items = iq['pubsub']['items'] + feed = ET.Element("feed") + feed.set('xmlns', 'http://www.w3.org/2005/Atom') + ET.SubElement(feed, 'title', {'type': 'text'}).text = title + ET.SubElement(feed, 'subtitle', {'type': 'text'}).text = subtitle + ET.SubElement(feed, 'link', {'rel': 'self', 'href': link}) + ET.SubElement(feed, 'generator', { + 'uri': 'https://git.xmpp-it.net/sch/XMPPJournalPublisher', + 'version': '0.1'}).text = 'XMPP Journal Publisher (XJP)' + ET.SubElement(feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat() + for item in items: + item_id = item['id'] + item_payload = item['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 - published = item.find(namespace + 'published') - published = None if published == None else published.text - if not updated and not published: updated = datetime.datetime.utcnow().isoformat() - content = item.find(namespace + 'content') - content = 'No content' if content == None else content.text - link = item.find(namespace + 'link') - link = '' if link == None else link.attrib['href'] - author = item.find(namespace + 'author') + title = item_payload.find(namespace + 'title') + title_text = None if title == None else title.text + # link = item_payload.find(namespace + 'link') + # link_href = '' if link == None else link.attrib['href'] + link_href = form_an_item_link(pubsub, node, item_id) + if not title_text or not link_href: continue + content = item_payload.find(namespace + 'content') + content_text = 'No content' if content == None else content.text + if content.attrib: + content_type = content.attrib['type'] if 'type' in content.attrib else 'text' + content_type_text = 'html' if 'html' in content_type else 'text' + published = item_payload.find(namespace + 'published') + published_text = None if published == None else published.text + if published: published_dt = parser.parse(published_text) + updated = item_payload.find(namespace + 'updated') + updated_text = None if updated == None else updated.text + author = item_payload.find(namespace + 'author') if author and author.attrib: print(author.attrib) - author = 'None' if author == None else author.text - # create entry - feed.add_item( - description = content, - # enclosure = feedgenerator.Enclosure(enclosure, enclosure_size, enclosure_type) if args.entry_enclosure else None, - link = link, - pubdate = published or updated, - title = title, - unique_id = link) - xml_atom = feed.writeString('utf-8') - xml_atom_extended = append_element( - xml_atom, - 'generator', - 'XMPP Journal Publisher (XJP)') - return xml_atom_extended + author_text = 'None' if author == None else author.text + identifier = item_payload.find(namespace + 'id') + if identifier and identifier.attrib: print(identifier.attrib) + identifier_text = 'None' if identifier == None else identifier.text + entry = ET.SubElement(feed, 'entry') + ET.SubElement(entry, 'title').text = title_text + ET.SubElement(entry, 'link', {'href': link_href}) + ET.SubElement(entry, 'id').text = identifier_text + ET.SubElement(entry, 'updated').text = updated_text + ET.SubElement(entry, 'published').text = published_text + # ET.SubElement(entry, 'summary', {'type': summary_type_text}).text = summary_text + ET.SubElement(entry, 'content', {'type': content_type_text}).text = content_text + return ET.tostring(feed, encoding='unicode') -def generate_json(iq, node): +def generate_json(iq): """Create a JSON file from node items.""" json_data = [] + pubsub = iq['from'].bare + node = iq['pubsub']['items']['node'] entries = iq['pubsub']['items'] for entry in entries: - item = entry['payload'] + item_id = entry['id'] + item_payload = entry['payload'] namespace = '{http://www.w3.org/2005/Atom}' - title = item.find(namespace + 'title') - title = None if title == None else title.text + title = item_payload.find(namespace + 'title') + title_text = 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} + link_href = form_an_item_link(pubsub, node, item_id) + # link = item.find(namespace + 'link') + # link_href = '' if link == None else link.attrib['href'] + json_data_entry = {'title' : title_text, + 'link' : link_href} json_data.append(json_data_entry) if len(json_data) > 6: break filename = 'data/{}.json'.format(node) @@ -286,15 +315,40 @@ def append_element(xml_data, element, text): """Patch function to append XSLT reference to XML""" """Why is not this a built-in function of ElementTree or LXML""" -def append_stylesheet(xml_data): +def append_stylesheet(xml_data, filename, namespace=None): # Register namespace in order to avoide ns0: - ET.register_namespace("", "http://www.w3.org/2005/Atom") + if namespace: ET.register_namespace("", namespace) # Load XML from string tree = ET.fromstring(xml_data) # The following direction removes the XML declaration - xml_data_no_declaration = ET.tostring(tree, encoding='unicode') + xml_data_without_a_declaration = ET.tostring(tree, encoding='unicode') # Add XML declaration and stylesheet - xml_data_declaration = ('' - '' + - xml_data_no_declaration) + xml_data_declaration = ( + '' + ''.format(filename) + + xml_data_without_a_declaration) return xml_data_declaration + +def generate_opml(iq): + pubsub = iq['from'].bare + items = iq['disco_items']['items'] + opml = ET.Element("opml") + opml.set("version", "1.0") + head = ET.SubElement(opml, "head") + ET.SubElement(head, "title").text = pubsub + ET.SubElement(head, "description").text = ( + "PubSub Nodes of {}").format(pubsub) + ET.SubElement(head, "generator").text = "XMPP Journal Publisher (XJP)" + ET.SubElement(head, "urlPublic").text = ( + "https://git.xmpp-it.net/sch/XMPPJournalPublisher") + time_stamp = datetime.datetime.now(datetime.UTC).isoformat() + ET.SubElement(head, "dateCreated").text = time_stamp + ET.SubElement(head, "dateModified").text = time_stamp + body = ET.SubElement(opml, "body") + for item in items: + pubsub, node, title = item + uri = form_a_node_link(pubsub, node) + outline = ET.SubElement(body, "outline") + outline.set("text", title or node) + outline.set("xmlUrl", uri) + return ET.tostring(opml, encoding='unicode') diff --git a/script/postprocess.js b/script/postprocess.js index 342b44c..c04ca0d 100644 --- a/script/postprocess.js +++ b/script/postprocess.js @@ -1,84 +1,99 @@ window.onload = async function(){ - // Fix button follow - let follow = document.querySelector('#follow'); - //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=' + feedUrl; + // Fix button follow + let follow = document.querySelector('#follow'); + if (follow) { + //let feedUrl = location.href.replace(/^https?:/, 'feed:'); + 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=' + feedUrl; + } // Convert ISO8601 To UTC - for (let element of document.querySelectorAll('#articles > ul > li > div > h4, #feed > #header > h2#subtitle.date')) { + for (let element of document.querySelectorAll( + '#articles > ul > li > div > h4.published,' + + '#articles > ul > li > div > h4.updated, ' + + '#feed > #header > h2#subtitle.date')) { let timeStamp = new Date(element.textContent); element.textContent = timeStamp.toUTCString(); } // Parse Markdown - for (let element of document.querySelectorAll('#articles > ul > li > div > p')) { + for (let element of document.querySelectorAll('#articles > ul > li > div > p[type="text"]')) { let markDown = element.textContent element.innerHTML = marked.parse(markDown); } // Build a journal list - itemsList = await openJson(node) - if (itemsList && locationHref.searchParams.get('item')) { - node = locationHref.searchParams.get('node') - pubsub = locationHref.searchParams.get('pubsub') - 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); + if (node) { + itemsList = await openJson(node) + if (itemsList && locationHref.searchParams.get('item')) { + node = locationHref.searchParams.get('node') + pubsub = locationHref.searchParams.get('pubsub') + 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('ol'); + 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}`}, + {'text' : 'Browse the portal.', + 'href' : `atom?pubsub=${pubsub}`} + ] + 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); } - 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:"]')) { + for (let xmppLink of document.querySelectorAll('#articles > ul a[href^="xmpp:"], ol a[href^="xmpp:"]')) { xmppUri = new URL(xmppLink); let parameters = xmppUri.search.split(';'); try { - 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}` + try { + 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}` + } catch { + let node = parameters.find(parameter => parameter.startsWith('node=')).split('=')[1]; + let pubsub = xmppUri.pathname; + xmppLink.href = `atom?pubsub=${pubsub}&node=${node}` + } } catch (err) { console.warn(err) } diff --git a/xsl/stylesheet.xsl b/xsl/atom.xsl similarity index 86% rename from xsl/stylesheet.xsl rename to xsl/atom.xsl index 6b8c5b6..72c1dd2 100644 --- a/xsl/stylesheet.xsl +++ b/xsl/atom.xsl @@ -8,22 +8,15 @@ element inside of html element --> - - - + - - - - diff --git a/xsl/atom_as_xhtml.xsl b/xsl/atom_as_xhtml.xsl index 725c3b7..6d8ba89 100644 --- a/xsl/atom_as_xhtml.xsl +++ b/xsl/atom_as_xhtml.xsl @@ -1,7 +1,7 @@ -

+

@@ -290,6 +293,12 @@ xmlns:atom='http://www.w3.org/2005/Atom'> + + + + + + @@ -299,6 +308,12 @@ xmlns:atom='http://www.w3.org/2005/Atom'> + + + + + + @@ -313,7 +328,7 @@ xmlns:atom='http://www.w3.org/2005/Atom'> -

+
diff --git a/xsl/opml.xsl b/xsl/opml.xsl new file mode 100644 index 0000000..9c57a4f --- /dev/null +++ b/xsl/opml.xsl @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/xsl/opml_as_xhtml.xsl b/xsl/opml_as_xhtml.xsl new file mode 100644 index 0000000..6304ef6 --- /dev/null +++ b/xsl/opml_as_xhtml.xsl @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + <xsl:choose> + <xsl:when test='title and not(title="")'> + <xsl:value-of select='title'/> + </xsl:when> + <xsl:otherwise>StreamBurner</xsl:otherwise> + </xsl:choose> + + + + + + + + +