forked from sch/Rivista
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:
parent
a4c7ada540
commit
a1e4cf0f71
6 changed files with 295 additions and 41 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -391,7 +391,7 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
|
|||
</div>
|
||||
<div id='references'>
|
||||
<a href='https://joinjabber.org/'
|
||||
title='An inclusive space on the Jabber network.'>
|
||||
title='An Inclusive Space On The Jabber Network.'>
|
||||
JoinJabber
|
||||
</a>
|
||||
<a href='https://libervia.org/'
|
||||
|
@ -399,15 +399,15 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
|
|||
Libervia
|
||||
</a>
|
||||
<a href='https://join.movim.eu/'
|
||||
title='The social platform shaped for your community.'>
|
||||
title='The Social Platform Shaped For Your Community.'>
|
||||
Movim
|
||||
</a>
|
||||
<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
|
||||
</a>
|
||||
<a href='https://xmpp.org/'
|
||||
title='The universal messaging standard.'>
|
||||
title='The Universal Messaging Standard.'>
|
||||
XMPP
|
||||
</a>
|
||||
<a href='https://xmpp.org/extensions/xep-0060.html'
|
||||
|
@ -418,11 +418,12 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
|
|||
<!-- note -->
|
||||
<p id='note'>
|
||||
<i>
|
||||
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. <span id="selection-link">Click here</span> 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. <span id="selection-link">Click
|
||||
here</span> for a selection of software and pick the
|
||||
ones that would fit you best!
|
||||
</i>
|
||||
</p>
|
||||
</body>
|
||||
|
|
Loading…
Reference in a new issue