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)).
|
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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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', '')
|
||||||
if pubsub and node and item_id:
|
settings = get_configuration('settings')
|
||||||
iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id)
|
result = None
|
||||||
link = 'xmpp:{pubsub}?;node={node};item={item}'.format(
|
if settings['service']:
|
||||||
pubsub=pubsub, node=node, item=item_id)
|
if settings['include'] in pubsub or not settings['include']:
|
||||||
xml_atom = pubsub_to_atom(iq, link)
|
if pubsub and node and item_id:
|
||||||
result = append_stylesheet(xml_atom)
|
iq = await xmpp.plugin['xep_0060'].get_item(pubsub, node, item_id)
|
||||||
elif pubsub and node:
|
link = 'xmpp:{pubsub}?;node={node};item={item}'.format(
|
||||||
iq = await xmpp.plugin['xep_0060'].get_items(pubsub, node)
|
pubsub=pubsub, node=node, item=item_id)
|
||||||
link = 'xmpp:{pubsub}?;node={node}'.format(pubsub=pubsub, node=node)
|
xml_atom = generate_rfc_4287(iq, link)
|
||||||
xml_atom = pubsub_to_atom(iq, link)
|
result = append_stylesheet(xml_atom)
|
||||||
result = append_stylesheet(xml_atom)
|
iq = await get_node_items(pubsub, node)
|
||||||
elif pubsub:
|
if iq:
|
||||||
iq = await xmpp.plugin['xep_0060'].get_nodes(pubsub)
|
generate_json(iq, node)
|
||||||
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
|
else:
|
||||||
result = pubsub_to_opml(iq)
|
operator = get_configuration('settings')['operator']
|
||||||
elif node:
|
json_data = [{'title' : 'Timeout Error: Press here to contact the operator.',
|
||||||
result = 'PubSub parameter is missing.'
|
'link' : 'xmpp:{}?message'.format(operator)}]
|
||||||
else:
|
filename = 'data/{}.json'.format(node)
|
||||||
result = ('Mandatory parameter PubSub and '
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
'optional parameter Node are missing.')
|
json.dump(json_data, f, ensure_ascii=False, indent=4)
|
||||||
return Response(content=result, media_type="application/xml")
|
# 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."""
|
"""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)
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue