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.
This commit is contained in:
Schimon Jehudah, Adv. 2024-07-11 17:43:28 +03:00
parent a4c7ada540
commit a1e4cf0f71
6 changed files with 295 additions and 41 deletions

View file

@ -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)). 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 ## Preview

View file

@ -1,3 +1,15 @@
# An account to connect PubSubToAtom to the XMPP network.
[account] [account]
xmpp = "" xmpp = "" # Jabber ID.
pass = "" 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.

View file

@ -51,6 +51,27 @@ h1#title, h2#subtitle, #actions, #references {
border-top: 1px solid #eee; 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 { #articles > ul > li > div > p.content {
font-size: 120%; font-size: 120%;
@ -115,3 +136,17 @@ h1#title, h2#subtitle, #actions, #references {
cursor: pointer; cursor: pointer;
text-decoration: underline; 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;
}
}

View file

@ -6,7 +6,9 @@ from fastapi import FastAPI, Request, Response
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import feedgenerator import feedgenerator
import json
from slixmpp import ClientXMPP from slixmpp import ClientXMPP
from slixmpp.exceptions import IqError, IqTimeout
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
#import importlib.resources #import importlib.resources
@ -28,6 +30,7 @@ xmpp = None
# Mount static graphic, script and stylesheet directories # Mount static graphic, script and stylesheet directories
app.mount("/css", StaticFiles(directory="css"), name="css") 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("/graphic", StaticFiles(directory="graphic"), name="graphic")
app.mount("/script", StaticFiles(directory="script"), name="script") app.mount("/script", StaticFiles(directory="script"), name="script")
app.mount("/xsl", StaticFiles(directory="xsl"), name="xsl") app.mount("/xsl", StaticFiles(directory="xsl"), name="xsl")
@ -40,40 +43,136 @@ async def favicon():
async def view_pubsub(request: Request): async def view_pubsub(request: Request):
global xmpp global xmpp
if not xmpp: if not xmpp:
with open('configuration.toml', mode="rb") as configuration: credentials = get_configuration('account')
credentials = tomllib.load(configuration)['account']
xmpp = XmppInstance(credentials['xmpp'], credentials['pass']) xmpp = XmppInstance(credentials['xmpp'], credentials['pass'])
# xmpp.connect() # xmpp.connect()
pubsub = request.query_params.get('pubsub', '') pubsub = request.query_params.get('pubsub', '')
node = request.query_params.get('node', '') node = request.query_params.get('node', '')
item_id = request.query_params.get('item', '') item_id = request.query_params.get('item', '')
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: if pubsub and node and item_id:
iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id) iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id)
link = 'xmpp:{pubsub}?;node={node};item={item}'.format( link = 'xmpp:{pubsub}?;node={node};item={item}'.format(
pubsub=pubsub, node=node, item=item_id) pubsub=pubsub, node=node, item=item_id)
xml_atom = pubsub_to_atom(iq, link) xml_atom = generate_rfc_4287(iq, link)
result = append_stylesheet(xml_atom) 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: elif pubsub and node:
iq = await xmpp.plugin['xep_0060'].get_items(pubsub, node) iq = await get_node_items(pubsub, node)
link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node) link = form_a_link(pubsub, node)
xml_atom = pubsub_to_atom(iq, link) xml_atom = generate_rfc_4287(iq, link)
result = append_stylesheet(xml_atom) result = append_stylesheet(xml_atom)
elif pubsub: elif pubsub:
iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub) iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub)
link = 'xmpp:{pubsub}'.format(pubsub=pubsub) link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
result = pubsub_to_opml(iq) result = pubsub_to_opml(iq)
elif node: elif node:
result = 'PubSub parameter is missing.' 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: else:
result = ('Mandatory parameter PubSub and ' text = 'The given domain {} is not allowed.'.format(pubsub)
'optional parameter Node are missing.') xml_atom = error_message(text)
return Response(content=result, media_type="application/xml") 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.""" """Convert XEP-0060: Publish-Subscribe to RFC 4287: The Atom Syndication Format."""
feed = feedgenerator.Atom1Feed( 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 ' 'Atom, which conveys XEP-0060: Publish-Subscribe nodes '
'to standard RFC 4287: The Atom Syndication Format.'), 'to standard RFC 4287: The Atom Syndication Format.'),
language = iq['pubsub']['items']['lang'], language = iq['pubsub']['items']['lang'],
@ -87,7 +186,6 @@ def pubsub_to_atom(iq, link):
namespace = '{http://www.w3.org/2005/Atom}' namespace = '{http://www.w3.org/2005/Atom}'
title = item.find(namespace + 'title') title = item.find(namespace + 'title')
title = None if title == None else title.text title = None if title == None else title.text
feed_url = 'gemini://schimon.i2p/'
updated = item.find(namespace + 'updated') updated = item.find(namespace + 'updated')
updated = None if updated == None else updated.text updated = None if updated == None else updated.text
# if updated: updated = datetime.datetime.fromisoformat(updated) # if updated: updated = datetime.datetime.fromisoformat(updated)
@ -113,6 +211,28 @@ def pubsub_to_atom(iq, link):
'XPTA: XMPP PubSub To Atom') 'XPTA: XMPP PubSub To Atom')
return xml_atom_extended 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""" """Patch function to append elements which are not provided by feedgenerator"""
def append_element(xml_data, element, text): def append_element(xml_data, element, text):
root = ET.fromstring(xml_data) root = ET.fromstring(xml_data)

View file

@ -1,13 +1,17 @@
window.onload = function(){ window.onload = async function(){
// Fix button follow // Fix button follow
let follow = document.querySelector('#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.href = feedUrl;
follow.addEventListener ('click', function() { follow.addEventListener ('click', function() {
window.open(feedUrl, "_self"); window.open(feedUrl, "_self");
}); });
// Fix button subtome // 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 // 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, #feed > #header > h2#subtitle.date')) {
let timeStamp = new Date(element.textContent); let timeStamp = new Date(element.textContent);
@ -18,6 +22,63 @@ window.onload = function(){
let markDown = element.textContent let markDown = element.textContent
element.innerHTML = marked.parse(markDown); 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. // Display a selection of suggested software.
const selection = { const selection = {
'akregator' : { 'akregator' : {
@ -99,3 +160,26 @@ window.onload = function(){
document.body.appendChild(elementDiv); 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 };
}

View file

@ -391,7 +391,7 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
</div> </div>
<div id='references'> <div id='references'>
<a href='https://joinjabber.org/' <a href='https://joinjabber.org/'
title='An inclusive space on the Jabber network.'> title='An Inclusive Space On The Jabber Network.'>
JoinJabber JoinJabber
</a> </a>
<a href='https://libervia.org/' <a href='https://libervia.org/'
@ -399,15 +399,15 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
Libervia Libervia
</a> </a>
<a href='https://join.movim.eu/' <a href='https://join.movim.eu/'
title='The social platform shaped for your community.'> title='The Social Platform Shaped For Your Community.'>
Movim Movim
</a> </a>
<a href='https://modernxmpp.org/' <a href='https://modernxmpp.org/'
title='A project to improve the quality of user-to-user messaging applications that use XMPP.'> title='A Project To Improve The Quality Of User-To-User Messaging Applications That Use Xmpp.'>
Modern Modern
</a> </a>
<a href='https://xmpp.org/' <a href='https://xmpp.org/'
title='The universal messaging standard.'> title='The Universal Messaging Standard.'>
XMPP XMPP
</a> </a>
<a href='https://xmpp.org/extensions/xep-0060.html' <a href='https://xmpp.org/extensions/xep-0060.html'
@ -418,11 +418,12 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
<!-- note --> <!-- note -->
<p id='note'> <p id='note'>
<i> <i>
This is an XMPP news feed which is conveyed as HTML, This is an XMPP news feed which is conveyed as an HTML
and it can even be viewed by a syndication feed reader document, and it can even be viewed by a syndication
which provides automated notifications on desktop and feed reader which provides automated notifications on
mobile. <span id="selection-link">Click here</span> for desktop and mobile. <span id="selection-link">Click
a selection of software that would fit you best! here</span> for a selection of software and pick the
ones that would fit you best!
</i> </i>
</p> </p>
</body> </body>