Compare commits

..

38 commits
main ... main

Author SHA1 Message Date
Schimon Jehudah, Adv.
68db51ea94 Correct path of JSON files in which indices are stored. 2024-11-20 17:42:56 +02:00
Schimon Jehudah, Adv.
5fd6a6c710 Automate installation process;
Add command line prompt to set configurations;
Update document README.
2024-11-20 17:21:26 +02:00
Schimon Jehudah, Adv.
2cdba527f6 Update readme, information and license. 2024-11-12 15:43:43 +02:00
sch
3724eaa0a8 Update README.md
Update paths to screenshots.
2024-11-12 14:37:40 +01:00
Schimon Jehudah, Adv.
3ac005708d Add installation instruction file;
Order directories.
2024-11-12 15:31:05 +02:00
Schimon Jehudah, Adv.
766e51af4c Modularize code;
Add packaging instructions;
Add modules to handle Gemini file type (no Gemini server yet);
Improve handling of configuration.
2024-11-12 15:25:05 +02:00
Schimon Jehudah, Adv.
84e54085b5 Fix connectivity issue, by starting and ending an XMPP session upon every HTTP request;
Improve code structure;
Segregate functions to extract data and create feed;
Add a function to create an XHTML file (not utilized yet).
2024-11-11 00:17:36 +02:00
Schimon Jehudah, Adv.
74b0f50c61 Fix errors of type UnboundLocalError (Thank you. roughnecks). 2024-09-16 16:10:46 +03:00
Schimon Jehudah, Adv.
028fdacc39 Improve display of comments of discussion nodes;
Add a list of instances (Thank you roughnecks).
2024-09-05 11:08:26 +03:00
Schimon Jehudah, Adv.
b26958acae Update ECMAScript accordingly. 2024-09-04 15:50:11 +03:00
Schimon Jehudah, Adv.
b5762e90ef Improve cache handling;
Order items descendingly.
2024-09-04 15:11:45 +03:00
Schimon Jehudah, Adv.
ad34af72ff Add support for contents of Libervia (XEP-0277);
Various of visual improvements.
2024-07-31 19:21:33 +03:00
Schimon Jehudah, Adv.
44e7778578 Improve the presentation of related links. 2024-07-14 14:14:34 +03:00
Schimon Jehudah, Adv.
a9c7832da1 Python: Fix an error upon iterating for elements of type category (Thank you roughnecks). 2024-07-14 12:49:56 +03:00
Schimon Jehudah, Adv.
8ab7396a36 CSS: Ad support for element category;
XSLT: Add a reference to script postprocess.js to file opml_as_xhtml.xsl;
XSLT: Add support for element category;
Python: Add support for element category.
2024-07-14 12:35:59 +03:00
Schimon Jehudah, Adv.
cb4507bb78 Rename project to Rivista;
CSS: Improve position of elements;
JS: Improve handling of content in erroneous cases;
JS: Render post as HTML when XSLT does not render correctly;
JS: Handle posts which were tagged as Text instead of HTML and vice versa;
XSLT: Adapt to be viewed on browsers with a lack of support;
XSLT: Add Support for link elements of attribute "rel";
Python: Add elements author and link of attribute "rel".
2024-07-13 22:16:25 +03:00
Schimon Jehudah, Adv.
040d532fb9 CSS: Add padding to title links. 2024-07-12 18:54:50 +03:00
Schimon Jehudah, Adv.
a3c989cec9 Correct pathname from /atom to /opml 2024-07-12 18:33:51 +03:00
Schimon Jehudah, Adv.
1138faceda Add more examples to README and update information. 2024-07-12 18:27:21 +03:00
Schimon Jehudah, Adv.
eb280b5aab Default to content type HTML;
Correct name to XHTML;
Improve CSS.
2024-07-12 18:05:33 +03:00
Schimon Jehudah, Adv.
694c8bb489 Set pathname /opml as a pathname for OPML;
Add a message to be displayed upon empty content;
Add more error handlings;
Fix error related to Markdown parsing;
Fix error related to Atom Syndication Format;
Thank you roughnecks.
2024-07-12 16:50:59 +03:00
Schimon Jehudah, Adv.
d1f1edbaca Add OPML support;
Set a new default node (Thank you roughnecks);
Improve CSS, JS, XSLT;
Neglect external libraries to produce syndications.
2024-07-12 15:39:17 +03:00
Schimon Jehudah, Adv.
e07ff6e838 Rename project to XMPP Journal Publisher;
Retrieve dates of PubSub node items;
Improve CSS stylesheet;
Fix JS error.
2024-07-11 20:56:20 +03:00
Schimon Jehudah, Adv.
16bd475be2 Improve error handling. 2024-07-11 19:01:45 +03:00
sch
ed33aca596 Add a dummy file
Add a dummy file in order to create a directory.
2024-07-11 16:53:42 +02:00
Schimon Jehudah, Adv.
a1e4cf0f71 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.
2024-07-11 17:43:28 +03:00
sch
a4c7ada540 Add a list of supported XEPs 2024-07-10 05:27:05 +02:00
sch
af7f31e19b Improve wording 2024-07-10 05:10:55 +02:00
sch
d0ded117e2 Thank you to stpeter, Wojtek and edhelas
Thank you for Atom Over XMPP, Sure.IM and Movim.
2024-07-10 04:51:09 +02:00
sch
7f6be68df1 Add a paragraph to section "Motivation" 2024-07-10 04:29:56 +02:00
sch
45e484a409 Improve a paragraph of section "Motivation" 2024-07-10 04:14:20 +02:00
sch
7f26996860 Add a new section: Motivation
An explanation of this project.
2024-07-10 04:11:18 +02:00
sch
ea51e8198e Thank you roughnecks
Your instructions, corrections and advises have accelerated the development of PubSubToAtom, and consequently saved me many hours.
2024-07-09 19:55:57 +02:00
sch
2a42e7b202 Merge pull request 'Update README.md with Debian based distro instructions' (#1) from roughnecks/PubSubToAtom:apt-instructions-for-readme into main
Reviewed-on: sch/PubSubToAtom#1
2024-07-09 19:32:55 +02:00
Schimon Jehudah, Adv.
11ee189748 Improve software reference page. 2024-07-09 19:37:33 +03:00
Schimon Jehudah, Adv.
7712b7d9b8 JS: Fix an unintended usage of event click. 2024-07-09 19:24:23 +03:00
Schimon Jehudah, Adv.
0785b167ad Improve the handling of subscription hyperlinks. 2024-07-09 19:17:40 +03:00
5306ee9b12 Update README.md
Added instructions to install requirements in Debian based distro.
2024-07-09 18:13:32 +02:00
54 changed files with 3530 additions and 404 deletions

165
README.md
View file

@ -1,76 +1,136 @@
# XMPP PubSub To Atom # Rivista XJP
A little client that parses XMPP Pubsub Nodes and sends them as Atom Syndication Format or OPML over HTTP. Rivista XJP ("XMPP Journal Publisher"); previously XMPP PubSub To Atom ("XPTA").
## About Rivista is a software that parses XMPP Pubsub Nodes and sends them as Atom
Syndication Format or OPML over HTTP.
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. Rivista 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)). Rivista includes [XSLT ](https://www.w3.org/TR/xslt/) stylesheets that transform
PubSub nodes into static XHTML journal sites.
This software was inspired from Tigase and was motivated by Movim. Rivista was inspired from Tigase and was motivated by Movim.
## Home site
- gemini://woodpeckersnest.space/~schapps/rivista.gmi
- https://schapps.woodpeckersnest.eu/rivista/
## Instances
* https://rivista.woodpeckersnest.eu
## Preview ## Preview
[<img alt="berlin-xmpp-meetup" src="screenshot/berlin-xmpp-meetup.png" width="200px"/>](screenshot/berlin-xmpp-meetup.png) [<img alt="berlin-xmpp-meetup" src="rivista/screenshot/berlin-xmpp-meetup.png" width="200px"/>](rivista/screenshot/berlin-xmpp-meetup.png)
[<img alt="let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh" src="screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png" width="200px"/>](screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png) [<img alt="let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh" src="rivista/screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png" width="200px"/>](rivista/screenshot/let-s-talk-about-xmpp-tricks-with-conversations-author-daniel-gultsch-OqjaQh.png)
[<img alt="59d860ab-d7c8-477c-bb4b-86924485cbbb" src="screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png" width="200px"/>](screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png) [<img alt="59d860ab-d7c8-477c-bb4b-86924485cbbb" src="rivista/screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png" width="200px"/>](rivista/screenshot/59d860ab-d7c8-477c-bb4b-86924485cbbb.png)
[<img alt="selection" src="screenshot/selection.png" width="200px"/>](screenshot/selection.png) [<img alt="selection" src="rivista/screenshot/selection.png" width="200px"/>](rivista/screenshot/selection.png)
## Motivation
Rivista 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 Rivista, 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 Rivista 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 Rivista would save bandwidth and system overhead, which
includes CPU, I/O and RAM usage.
## Requirements ## Requirements
* Python >= 3.5 * Python >= 3.5
* beautifulsoup4
* fastapi * fastapi
* feedgenerator
* lxml * lxml
* markdown
* slixmpp * slixmpp
* tomllib * tomllib (Python <= 3.10)
* uvicorn * uvicorn
## Installation ## Installation
### Download It is possible to install Rivista using pip and pipx.
Extract the source package to a directory that you have permission to run #### pip inside venv
software.
```shell ```
$ git clone https://git.xmpp-it.net/sch/PubSubToAtom $ python3 -m venv .venv
$ cd PubSubToAtom/ $ source .venv/bin/activate
``` ```
### Configure ##### Install
Add account credentials to file `configuration.toml`. ```
$ pip install git+https://git.xmpp-it.net/sch/Rivista
```
#### pipx
##### Install
```
$ pipx install git+https://git.xmpp-it.net/sch/Rivista
```
##### Update
```
$ pipx uninstall rivista
$ pipx install git+https://git.xmpp-it.net/sch/Rivista
```
### Start ### Start
Execute PubSubToAtom with one of the following commands:
```shell
$ python -m uvicorn pubsub_to_atom:app --reload
$ python -m uvicorn pubsub_to_atom:app --reload --host 127.0.0.1 --port 8000
$ uvicorn pubsub_to_atom:app --host 127.0.0.1 --port 8000
$ fastapi dev pubsub_to_atom.py
``` ```
$ rivista
```
Open URL http://localhost:8000
## Usage ## Usage
It is possible to view a complete node and even a single item, which means, that it is possible to save bandwidth and it further means that a considered and carefully earnest use of this software would saves system overhead, which includes CPU, I/O and RAM usage. It is possible to view a complete node and even a single item, which means, that
it is possible to save bandwidth and it further means that a considered and
carefully earnest use of this software would save system overhead, which
includes CPU, I/O and RAM usage.
### Viewing PubSub ### Viewing PubSub
Suppose you have the following nodes and items. Suppose you have the following PubSub nodes and items.
|PubSub |Node |Item | |Jabber ID |Node |Item |
|--- |--- |--- | |--- |--- |--- |
|blog.jmp.chat |urn:xmpp:microblog:0|launch-2023 | |blog.jmp.chat |urn:xmpp:microblog:0 |launch-2023 |
|edhelas%40movim.eu|urn:xmpp:microblog:0|working-on-launching-the-movim-network-qPBzwc | |edhelas%40movim.eu |urn:xmpp:microblog:0 |working-on-launching-the-movim-network-qPBzwc |
|goffi%40goffi.org |urn:xmpp:microblog:0|libervia-v0-8-la-cecilia-BdQ4 | |goffi%40goffi.org |urn:xmpp:microblog:0 |libervia-v0-8-la-cecilia-BdQ4 |
|news.movim.eu |Phoronix | | |news.movim.eu |Phoronix | |
|pubsub.movim.eu |berlin-xmpp-meetup |7363a41d-1146-40b3-ac0f-8ee2559591a3 | |pubsub.movim.eu |berlin-xmpp-meetup |7363a41d-1146-40b3-ac0f-8ee2559591a3 |
|pubsub.movim.eu |berlin-xmpp-meetup |let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4| |pubsub.movim.eu |berlin-xmpp-meetup |let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4 |
|pubsub.movim.eu |jesus-christ-son-of-god|the-passion-of-christ-redemption-and-salvation-for-all-who-believe-moSqXO|
|pubsub.woodpeckersnest.space|PlanetJabber | |
|pubsub.woodpeckersnest.space|xmpp-it | |
|pubsub%40sure.im |news | |
#### To view pubsub nodes
```
http://127.0.0.1:8000/opml?pubsub=news.movim.eu
http://127.0.0.1:8000/opml?pubsub=pubsub.woodpeckersnest.space
```
#### To view node items #### To view node items
@ -78,6 +138,10 @@ Suppose you have the following nodes and items.
http://127.0.0.1:8000/atom?pubsub=edhelas%40movim.eu&node=urn%3Axmpp%3Amicroblog%3A0 http://127.0.0.1:8000/atom?pubsub=edhelas%40movim.eu&node=urn%3Axmpp%3Amicroblog%3A0
http://127.0.0.1:8000/atom?pubsub=news.movim.eu&node=Phoronix http://127.0.0.1:8000/atom?pubsub=news.movim.eu&node=Phoronix
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=jesus-christ-son-of-god
http://127.0.0.1:8000/atom?pubsub=pubsub.woodpeckersnest.space&node=PlanetJabber
http://127.0.0.1:8000/atom?pubsub=pubsub.woodpeckersnest.space&node=xmpp-it
http://127.0.0.1:8000/atom?pubsub=pubsub%40sure.im&node=news
``` ```
#### To view a node item #### To view a node item
@ -88,8 +152,15 @@ http://127.0.0.1:8000/atom?pubsub=edhelas%40movim.eu&node=urn%3Axmpp%3Amicroblog
http://127.0.0.1:8000/atom?pubsub=goffi%40goffi.org&node=urn%3Axmpp%3Amicroblog%3A0&item=libervia-v0-8-la-cecilia-BdQ4 http://127.0.0.1:8000/atom?pubsub=goffi%40goffi.org&node=urn%3Axmpp%3Amicroblog%3A0&item=libervia-v0-8-la-cecilia-BdQ4
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup&item=7363a41d-1146-40b3-ac0f-8ee2559591a3 http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup&item=7363a41d-1146-40b3-ac0f-8ee2559591a3
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup&item=let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4 http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=berlin-xmpp-meetup&item=let-s-talk-about-the-xsf-and-possibly-other-things-6A8eV4
http://127.0.0.1:8000/atom?pubsub=pubsub.movim.eu&node=jesus-christ-son-of-god&item=the-passion-of-christ-redemption-and-salvation-for-all-who-believe-moSqXO
``` ```
## Supported XEPs
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
- [XEP-0277: Microblogging over XMPP](https://xmpp.org/extensions/xep-0277.html)
- [XEP-0472: Pubsub Social Feed](https://xmpp.org/extensions/xep-0472.html)
## Author ## Author
Schimon Jehudah Zackary Schimon Jehudah Zackary
@ -104,10 +175,30 @@ Python code is licensed under the license AGPL-3.0 only.
## Acknowledgement ## Acknowledgement
Special thanks to "d3x" and "cchianel" from IRC channel #python on irc.libera.chat Thank you to Mr. Peter Saint-Andre ([stpeter](https://stpeter.im/journal/731.html))
for manifesting [Atom Over XMPP](https://www.ietf.org/archive/id/draft-saintandre-atompub-notify-07.html).
Thank you to Mr. Wojtek and [Tigase](https://tigase.org/) for publicly exposing
an implementation of PubSub as Syndication at [Sure.IM](https://sure.im/) as
[feeds.tigase.im](http://feeds.tigase.im/atom?server=pubsub@sure.im&node=news).
Thank you to to Mr. Timothée Jaussoin ([edhelas](https://edhelas.movim.eu/)) who
consistently and earnestly showcases the potential of PubSub as a publication
platform with project [Movim](https://movim.eu/).
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
Rivista into production.
## Similar Projects ## Similar Projects
* [AtomEntry](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/AtomEntry.java) and [PubSubPublishViewImpl](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/PubSubPublishViewImpl.java) - Convert XMPP Pubsub Nodes to Atom Syndication Format and convey them over HTTP. * [AtomEntry](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/AtomEntry.java)
and [PubSubPublishViewImpl](https://github.com/tigase/sureim/blob/master/site/src/main/java/tigase/sure/web/site/client/pubsub/PubSubPublishViewImpl.java) -
Convert XMPP Pubsub Nodes to Atom Syndication Format and convey them over HTTP.
* [AtomToPubsub](https://github.com/edhelas/atomtopubsub) - A little client that parses Atom + RSS feeds and send them on XMPP Pubsub Nodes. * [AtomToPubsub](https://github.com/edhelas/atomtopubsub) - A little client that
parses Atom + RSS feeds and send them on XMPP Pubsub Nodes.

View file

@ -1,3 +0,0 @@
[account]
xmpp = ""
pass = ""

View file

@ -1,117 +0,0 @@
* {
color: #eee;
max-width: 100%;
}
body {
background: #000;
}
h1#title, h2#subtitle, #actions, #references {
text-align: center;
text-transform: uppercase;
line-height: 140%;
}
#actions, #references {
border-bottom: 1px solid #eee;
line-height: 150%;
padding: 10px;
user-select: none;
}
#actions > *, #references * {
letter-spacing: 5px;
margin: 5px;
text-decoration: none;
}
#header, #menu {
line-height: 120%;
padding-bottom: 20px;
}
#menu > h3 {
padding-left: 2%;
}
#menu > ul > li {
padding: 5px;
}
#note {
line-height: 30px;
margin: auto;
max-width: 70%;
padding: 10px;
text-align: center;
}
#references {
border-top: 1px solid #eee;
}
#articles > ul > li > div > p.content {
font-size: 120%;
line-height: 30px;
margin: auto;
padding-left: 2%;
padding-right: 10%;
/* text-align: justify; */
}
#articles > ul > li > div.entry {
padding-bottom: 50px;
}
#articles > ul > li > div.entry h1 {
font-size: 2vw;
}
#articles > ul > li > div.entry h2 {
font-size: 1.5vw;
}
#selection-page {
background: #000;
bottom: 0;
left: 0;
line-height: 200%;
overflow: overlay;
position: fixed;
right: 0;
text-align: center;
top: 0;
}
#selection-page h3 {
margin-left: 10%;
margin-right: 10%;
}
#selection-page span {
display: inline-grid;
margin: 2%
}
#selection-page img {
height: 128px;
margin-bottom: 20%;
margin-top: 20%;
width: 128px;
}
#selection-page #selection {
margin-bottom: 5%;
}
#selection-page #return {
font-style: italic;
margin: auto;
}
#selection-link, #selection-page #return {
cursor: pointer;
text-decoration: underline;
}

View file

@ -1,143 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#import datetime
from fastapi import FastAPI, Request, Response
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import feedgenerator
from slixmpp import ClientXMPP
import xml.etree.ElementTree as ET
#import importlib.resources
try:
import tomllib
except:
import tomli as tomllib
app = FastAPI()
class XmppInstance(ClientXMPP):
def __init__(self, jid, password):
super().__init__(jid, password)
self.register_plugin('xep_0060')
self.connect()
# self.process(forever=False)
xmpp = None
# Mount static graphic, script and stylesheet directories
app.mount("/css", StaticFiles(directory="css"), name="css")
app.mount("/graphic", StaticFiles(directory="graphic"), name="graphic")
app.mount("/script", StaticFiles(directory="script"), name="script")
app.mount("/xsl", StaticFiles(directory="xsl"), name="xsl")
@app.get('/favicon.ico', include_in_schema=False)
async def favicon():
return FileResponse('favicon.ico')
@app.get('/atom')
async def view_pubsub(request: Request):
global xmpp
if not xmpp:
with open('configuration.toml', mode="rb") as configuration:
credentials = tomllib.load(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")
def pubsub_to_atom(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']
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)
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')
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 = 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')
return xml_atom_extended
"""Patch function to append elements which are not provided by feedgenerator"""
def append_element(xml_data, element, text):
root = ET.fromstring(xml_data)
# Create the generator element
generator_element = ET.Element(element)
generator_element.text = text
# Append the generator element to the root
root.append(generator_element)
# Return the modified XML as a string
return ET.tostring(root, encoding='unicode')
"""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):
# Register namespace in order to avoide ns0:
ET.register_namespace("", "http://www.w3.org/2005/Atom")
# 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')
# Add XML declaration and stylesheet
xml_data_declaration = ('<?xml version="1.0" encoding="utf-8"?>'
'<?xml-stylesheet type="text/xsl" href="xsl/stylesheet.xsl"?>' +
xml_data_no_declaration)
return xml_data_declaration

63
pyproject.toml Normal file
View file

@ -0,0 +1,63 @@
[build-system]
requires = ["setuptools>=61.2"]
build-backend = "setuptools.build_meta"
[project]
name = "Rivista"
version = "1.0"
description = "A private journal publication and a content management system for XMPP"
authors = [{name = "Schimon Zachary", email = "sch@fedora.email"}]
license = {text = "AGPL-3.0"}
classifiers = [
"Framework :: slixmpp",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: AGPL-3.0 License",
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 3.10",
"Topic :: Internet :: Extensible Messaging and Presence Protocol (XMPP)",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary",
"Topic :: Internet :: XMPP",
"Topic :: Office/Business :: News/Diary",
]
keywords = [
"atom",
"blog",
"cms",
"gemini",
"jabber",
"journal",
"news",
"ssg",
"syndication",
"xml",
"xmpp",
]
dependencies = [
"beautifulsoup4",
"fastapi",
"gmcapsule",
"lxml",
"markdown",
# "markdown-text-clean",
# "md2gemini",
"python-dateutil",
"slixmpp",
"tomli", # Python 3.10
"uvicorn",
]
[project.urls]
Homepage = "https://schapps.woodpeckersnest.eu/rivista/"
Repository = "https://git.xmpp-it.net/sch/Rivista"
Issues = "https://git.xmpp-it.net/sch/Rivista/issues"
[project.scripts]
rivista = "rivista.__main__:main"
[tool.setuptools]
platforms = ["any"]
[tool.setuptools.package-data]
"*" = ["*.css", "*.js", "*.ico", "*.png", "*.svg", "*.toml", "*.xsl"]

3
rivista/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from rivista.version import __version__, __version_info__
print('Rivista XJP', __version__)

114
rivista/__main__.py Normal file
View file

@ -0,0 +1,114 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#import importlib.resources
import os
from rivista.config import Cache, Data, Settings
from rivista.utilities import Toml
from rivista.http.instance import HttpInstance
import shutil
import uvicorn
def main():
http_instance = HttpInstance()
return http_instance.app
if __name__ == 'rivista.__main__':
directory = os.path.dirname(__file__)
# Copy data files
# FIXME File settins.toml is copied too.
directory_data = Data.get_directory()
if not os.path.exists(directory_data):
directory_assets = os.path.join(directory, 'assets')
directory_assets_new = shutil.copytree(directory_assets, directory_data)
print(f'Data directory {directory_assets_new} has been created and populated.')
# Copy settings files
directory_settings = Settings.get_directory()
if not os.path.exists(directory_settings):
directory_configs = os.path.join(directory, 'configs')
directory_settings_new = shutil.copytree(directory_configs, directory_settings)
print(f'Settings directory {directory_settings_new} has been created and populated.')
# Create cache directories
directory_cache = Cache.get_directory()
if not os.path.exists(directory_cache):
print(f'Creating a cache directory at {directory_cache}.')
os.mkdir(directory_cache)
# TODO Add more cache directories (see JabberCard).
# TODO Remove json (indices is the new name)
for subdirectory in ('json', 'indices', 'pubsub'):
subdirectory_cache = os.path.join(directory_cache, subdirectory)
if not os.path.exists(subdirectory_cache):
print(f'Creating a cache subdirectory at {subdirectory_cache}.')
os.mkdir(subdirectory_cache)
# Configure settings file
file_settings = os.path.join(directory_settings, 'settings.toml')
if not os.path.exists(file_settings):
directory_configs = os.path.join(directory, 'configs')
file_settings_empty = os.path.join(directory_configs, 'settings.toml')
shutil.copyfile(file_settings_empty, file_settings)
data_settings = Toml.open_file_toml(file_settings)
# Configure account
data_settings_account = data_settings['account']
settings_account = {
'xmpp': 'Set a Jabber ID',
'pass': 'Input Password'
}
for key in settings_account:
data_settings_account_value = data_settings_account[key]
if not data_settings_account_value:
settings_account_message = settings_account[key]
while not data_settings_account_value:
data_settings_account_value = input(f'{settings_account_message}: ')
data_settings_account[key] = data_settings_account_value
# Configure default PubSub Service and Node Name
data_settings_default = data_settings['default']
settings_default_default = {
'pubsub': 'pubsub.woodpeckersnest.space',
'nodeid': 'PlanetJabber'
}
for key in settings_default_default:
data_settings_default_value = data_settings_default[key]
if not data_settings_default_value:
settings_default_default_value = settings_default_default[key]
data_settings_default_value = input(f'Set brand {key} (default: "{settings_default_default_value}"): ')
if not data_settings_default_value: data_settings_default_value = settings_default_default_value
data_settings_default[key] = data_settings_default_value
# Configure Settings
data_settings_settings = data_settings['settings']
data_settings_settings_service = data_settings_settings['service']
if data_settings_settings_service is '':
while data_settings_settings_service not in ('n', 'N', 'no', 'No', 'y', 'Y', 'yes', 'Yes'):
data_settings_settings_service = input(f'Run as service? [Y/n] ')
if data_settings_settings_service in ('y', 'Y', 'yes', 'Yes'):
data_settings_settings['service'] = 1
else:
data_settings_settings['service'] = 0
data_settings_settings_include = data_settings_settings['include']
if data_settings_settings_include is '':
data_settings_settings_include = input(f'Limit service to included domains? (comma separated. remain empty for no limit) ')
data_settings_settings['include'] = data_settings_settings_include if data_settings_settings_include else 0
data_settings_settings_operator = data_settings_settings['operator']
if data_settings_settings_operator is '':
data_settings_settings['operator'] = input(f'Set a Jabber ID of an operator to contact with: ')
Toml.save_to_toml(file_settings, data_settings)
# Start JabberCard
app = main()
uvicorn.run(app, host='localhost', port=8000, reload=False)

View file

@ -0,0 +1,303 @@
/*
TODO
pubsub: news.movim.eu
node: fake-news
item: fdef84f5-e3e1-41ea-9b68-af4bb9130f77
title: #14October2023EpochEclipse
date: Fri, 15 Jan 2021 20:24:46
*/
* {
color: #eee;
max-width: 100%;
}
/*
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
*/
body {
background: #000;
}
code, pre {
overflow: auto;
max-height: 100%;
max-width: 100%; }
img, svg, video {
display: block;
max-height: 500px;
width: auto;
height: auto;
}
h1#title, h2#subtitle, #actions, #references {
text-align: center;
text-transform: uppercase;
line-height: 140%;
}
h5.related > a {
margin-right: 5px;
text-decoration: none;
user-select: none;
}
h3.title > a {
display: block;
padding-top: 50px;
}
#actions, #references {
border-bottom: 1px solid #eee;
line-height: 150%;
padding: 10px;
user-select: none;
}
#actions > *, #references * {
letter-spacing: 5px;
margin: 5px;
text-decoration: none;
}
#header, #menu {
line-height: 120%;
padding-bottom: 20px;
}
#menu > h3 {
padding-left: 2%;
}
#menu > ol > li {
padding: 5px;
}
#references {
margin-top: 5em;
border-top: 1px solid #eee;
}
#articles {
min-height: 80vh;
display: flex;
}
#articles #journal {
margin-left: 2%;
margin-right: 2%;
margin-top: 2%;
min-width: 350px;
padding-bottom: 0.67em;
width: 20%;
}
#journal {
margin: auto;
padding-top: 50px;
}
#articles > ul {
margin: auto;
margin-left: 2%;
margin-top: 2%;
}
#articles #journal ol,
#articles #journal ul {
/* height: 500px; */
line-height: 160%;
overflow: auto;
word-wrap: break-word;
}
#articles div.content {
font-size: 120%;
line-height: 200%;
margin: auto;
padding-left: 2%;
padding-right: 10%;
/* text-align: justify; */
word-wrap: break-word;
}
#articles div.entry {
padding-bottom: 0.67em;
}
#articles div.entry h1 {
font-size: 115%; /* 2vw */
}
#articles div.entry h2 {
font-size: 1.5vw;
}
#selection-page {
background: #000;
bottom: 0;
left: 0;
line-height: 200%;
overflow: overlay;
position: fixed;
right: 0;
text-align: center;
top: 0;
}
#selection-page p {
margin-left: 10%;
margin-right: 10%;
}
#selection-page span {
display: inline-grid;
margin: 2%
}
#selection-page img {
height: 128px;
margin-bottom: 20%;
margin-top: 20%;
width: 128px;
}
#selection-page #selection {
margin-bottom: 2%;
}
#selection-page #return {
/* font-style: italic; */
margin: auto;
}
#selection-link, #selection-page #return {
cursor: pointer;
text-decoration: underline;
}
.content[type='text'] {
white-space: pre-wrap;
}
#articles div.entry span.tags {
display: inline-flex;
/* display: ruby; */
flex-wrap: wrap;
}
#articles div.entry span.tags > div {
margin: 5px;
}
.enclosures {
cursor: help;
direction: ltr;
margin: 5px auto 15px 1%;
padding: 15px;
padding: 1em;
/* background: #222;
border: 1px solid GrayText;
border-radius: 4px;
border-radius: .5em;
color: #525c66;
border-left: double;
max-width: 40%; */
}
.enclosure a {
margin: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.enclosure > span[icon='atom']:after,
.enclosure > span[icon='html5']:after,
.enclosure > span[icon='rss']:after {
content: '📰';
margin:3px;
}
.enclosure > span.audio:after {
content: ' (Audio file) ';
}
.enclosure > span[icon='audio']:after{
content: '🎼️';
margin: 3px;
}
.enclosure > span:after {
content: ' (Document file) ';
}
.enclosure > span[icon]:after {
content: '📄️';
margin: 3px;
}
.enclosure > span.executable:after{
content: ' (Executable file) ';
}
.enclosure > span[icon='executable']:after {
content: '📦️';
margin: 3px;
}
.enclosure > span.image:after {
content: ' (Image file) ';
}
.enclosure > span[icon='image']:after {
content: '🖼️';
margin: 3px;
}
.enclosure > span.video:after {
content: ' (Video file) ';
}
.enclosure > span[icon='video']:after {
content: '📽️';
margin:3px;
}
#note, #small {
line-height: 30px;
margin: auto;
margin-top: 0.67em;
max-width: 80%;
padding: 10px;
text-align: center;
user-select: none;
}
#small {
font-size: 80%;
}
@media (max-width: 1550px) {
#articles {
display: unset;
}
#articles #journal {
margin-right: unset;
min-width: unset;
width: unset;
}
}

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -0,0 +1,724 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
width="96"
height="96"
id="svg2408"
inkscape:version="0.48.1 r9760"
sodipodi:docname="main.svg"
inkscape:export-filename="/home/buckstabu/Dropbox/Public/lcicons/lcproto2.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview68"
showgrid="false"
inkscape:zoom="6.6666666"
inkscape:cx="53.537878"
inkscape:cy="36.716849"
inkscape:window-x="-8"
inkscape:window-y="-4"
inkscape:window-maximized="1"
inkscape:current-layer="svg2408" />
<defs
id="defs2410">
<linearGradient
id="linearGradient4017">
<stop
style="stop-color:#ff6600;stop-opacity:1;"
offset="0"
id="stop4019" />
<stop
style="stop-color:#ff1e00;stop-opacity:1;"
offset="1"
id="stop4021" />
</linearGradient>
<linearGradient
id="linearGradient4145">
<stop
style="stop-color:#ff5d00;stop-opacity:1;"
offset="0"
id="stop4147" />
<stop
style="stop-color:#00b1ff;stop-opacity:0;"
offset="1"
id="stop4149" />
</linearGradient>
<linearGradient
id="linearGradient3224">
<stop
id="stop3226"
style="stop-color:#42394b;stop-opacity:1"
offset="0" />
<stop
id="stop3228"
style="stop-color:#000000;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3465">
<stop
id="stop3467"
style="stop-color:#919191;stop-opacity:1"
offset="0" />
<stop
id="stop3469"
style="stop-color:#fdfdfd;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3187">
<stop
id="stop3189"
style="stop-color:#b4b4b4;stop-opacity:1"
offset="0" />
<stop
id="stop3191"
style="stop-color:#e6e6e6;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="45.447727"
y1="92.539597"
x2="45.447727"
y2="7.0165396"
id="ButtonShadow"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(1.0058652,0.994169)">
<stop
id="stop3750"
style="stop-color:#000000;stop-opacity:1"
offset="0" />
<stop
id="stop3752"
style="stop-color:#000000;stop-opacity:0.58823532"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3737">
<stop
id="stop3739"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop3741"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<filter
color-interpolation-filters="sRGB"
id="filter3174">
<feGaussianBlur
id="feGaussianBlur3176"
stdDeviation="1.71" />
</filter>
<linearGradient
x1="36.357143"
y1="6"
x2="36.357143"
y2="63.893143"
id="linearGradient3188"
xlink:href="#linearGradient3737"
gradientUnits="userSpaceOnUse" />
<linearGradient
x1="48"
y1="90"
x2="48"
y2="5.9877172"
id="linearGradient3427"
xlink:href="#linearGradient3187"
gradientUnits="userSpaceOnUse" />
<radialGradient
cx="47.098362"
cy="78.286942"
r="38.957481"
fx="47.098362"
fy="78.286942"
id="radialGradient3205"
xlink:href="#linearGradient3224"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-18.038924,-24.218731)" />
<linearGradient
x1="43.17857"
y1="13"
x2="43.17857"
y2="83.294571"
id="linearGradient3279"
xlink:href="#linearGradient3465"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0294118,0,0,1.0294118,-1.4117647,-1.411765)" />
<linearGradient
id="linearGradient3737-7">
<stop
id="stop3739-2"
style="stop-color:#ffffff;stop-opacity:1;"
offset="0" />
<stop
id="stop3741-7"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<radialGradient
cx="48"
cy="90.171875"
r="42"
fx="48"
fy="90.171875"
id="radialGradient2874"
xlink:href="#linearGradient3737-7"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.551021,0.1971319)" />
<linearGradient
id="linearGradient3737-4">
<stop
id="stop3739-8"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop3741-71"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<clipPath
id="clipPath3613">
<rect
width="84"
height="84"
rx="6"
ry="6"
x="6"
y="6"
id="rect3615"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</clipPath>
<filter
x="-0.192"
y="-0.192"
width="1.3839999"
height="1.3839999"
color-interpolation-filters="sRGB"
id="filter3794">
<feGaussianBlur
id="feGaussianBlur3796"
stdDeviation="5.28" />
</filter>
<linearGradient
x1="48"
y1="20.220806"
x2="48"
y2="138.66119"
id="linearGradient2908"
xlink:href="#linearGradient3737-4"
gradientUnits="userSpaceOnUse" />
<linearGradient
x1="45.447727"
y1="92.539597"
x2="45.447727"
y2="7.0165396"
id="ButtonShadow-0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0058652,0,0,0.994169,100,0)">
<stop
id="stop3750-8"
style="stop-color:#000000;stop-opacity:1"
offset="0" />
<stop
id="stop3752-5"
style="stop-color:#000000;stop-opacity:0.58823532"
offset="1" />
</linearGradient>
<linearGradient
x1="32.251034"
y1="6.1317081"
x2="32.251034"
y2="90.238609"
id="linearGradient3780"
xlink:href="#ButtonShadow-0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
<linearGradient
x1="32.251034"
y1="6.1317081"
x2="32.251034"
y2="90.238609"
id="linearGradient3772"
xlink:href="#ButtonShadow-0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
<linearGradient
x1="32.251034"
y1="6.1317081"
x2="32.251034"
y2="90.238609"
id="linearGradient3725"
xlink:href="#ButtonShadow-0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
<linearGradient
x1="32.251034"
y1="6.1317081"
x2="32.251034"
y2="90.238609"
id="linearGradient3721"
xlink:href="#ButtonShadow-0"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(0,-97)" />
<linearGradient
x1="32.251034"
y1="6.1317081"
x2="32.251034"
y2="90.238609"
id="linearGradient3224-3"
xlink:href="#ButtonShadow-0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0238095,0,0,1.0119048,-1.1428571,-98.071429)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3737-4"
id="linearGradient3053"
gradientUnits="userSpaceOnUse"
x1="48"
y1="20.220806"
x2="48"
y2="138.66119" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3224"
id="radialGradient3071"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-17.140614,-23.729195)"
cx="47.098362"
cy="78.286942"
fx="47.098362"
fy="78.286942"
r="38.957481" />
<linearGradient
id="linearGradient3304">
<stop
id="stop3306"
style="stop-color:#ff8313;stop-opacity:1"
offset="0" />
<stop
id="stop3308"
style="stop-color:#ffe400;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3304"
id="linearGradient3023"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.2146893,0,0,1.2146893,-90.000003,-246.69493)"
x1="143.75"
y1="334.5"
x2="143.75"
y2="276" />
<linearGradient
id="linearGradient3045">
<stop
id="stop3047"
style="stop-color:#ff8313;stop-opacity:1"
offset="0" />
<stop
id="stop3049"
style="stop-color:#ffe400;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3737"
id="linearGradient3894"
x1="49.010517"
y1="48.650372"
x2="73.444344"
y2="48.650372"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(2.1,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3737"
id="linearGradient3896"
x1="23.037859"
y1="48.650372"
x2="47.471485"
y2="48.650372"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(2.1,0)" />
<radialGradient
inkscape:collect="always"
xlink:href="#ButtonShadow-0"
id="radialGradient3908"
cx="61.227432"
cy="48.650372"
fx="61.227432"
fy="48.650372"
r="12.216913"
gradientTransform="translate(2.1,7.9644297e-6)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#ButtonShadow-0"
id="radialGradient3910"
cx="35.254673"
cy="48.650372"
fx="35.254673"
fy="48.650372"
r="12.216814"
gradientTransform="translate(2.1,-5.9733707e-6)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3224"
id="radialGradient3955"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.4908884,-1.1072317e-8,0,1.4039702,-22.221992,-28.734351)"
cx="47.098362"
cy="78.286942"
fx="47.098362"
fy="78.286942"
r="38.957481" />
<radialGradient
cx="47.098362"
cy="78.286942"
r="38.957481"
fx="47.098362"
fy="78.286942"
id="radialGradient3205-1"
xlink:href="#linearGradient3224-6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-18.038924,-24.218731)" />
<linearGradient
id="linearGradient3224-6">
<stop
id="stop3226-2"
style="stop-color:#42394b;stop-opacity:1"
offset="0" />
<stop
id="stop3228-1"
style="stop-color:#000000;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="48"
y1="90"
x2="48"
y2="5.9877172"
id="linearGradient3427-0"
xlink:href="#linearGradient3187-5"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3187-5">
<stop
id="stop3189-3"
style="stop-color:#b4b4b4;stop-opacity:1"
offset="0" />
<stop
id="stop3191-2"
style="stop-color:#e6e6e6;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
x1="36.357143"
y1="6"
x2="36.357143"
y2="63.893143"
id="linearGradient3188-1"
xlink:href="#linearGradient3737-9"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3737-9">
<stop
id="stop3739-7"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop3741-6"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
x1="43.17857"
y1="13"
x2="43.17857"
y2="83.294571"
id="linearGradient3279-8"
xlink:href="#linearGradient3465-2"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0294118,0,0,1.0294118,-1.4117647,-1.411765)" />
<linearGradient
id="linearGradient3465-2">
<stop
id="stop3467-0"
style="stop-color:#919191;stop-opacity:1"
offset="0" />
<stop
id="stop3469-6"
style="stop-color:#fdfdfd;stop-opacity:1"
offset="1" />
</linearGradient>
<radialGradient
cx="48"
cy="90.171875"
r="42"
fx="48"
fy="90.171875"
id="radialGradient2874-4"
xlink:href="#linearGradient3737-7-3"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.551021,0.1971319)" />
<linearGradient
id="linearGradient3737-7-3">
<stop
id="stop3739-2-3"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop3741-7-6"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
x1="48"
y1="20.220806"
x2="48"
y2="138.66119"
id="linearGradient2908-9"
xlink:href="#linearGradient3737-4-0"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3737-4-0">
<stop
id="stop3739-8-4"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop3741-71-8"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<clipPath
id="clipPath3613-5">
<rect
width="84"
height="84"
rx="6"
ry="6"
x="6"
y="6"
id="rect3615-1"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</clipPath>
<filter
x="-0.192"
y="-0.192"
width="1.3839999"
height="1.3839999"
color-interpolation-filters="sRGB"
id="filter3794-2">
<feGaussianBlur
id="feGaussianBlur3796-9"
stdDeviation="5.28" />
</filter>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3187-5"
id="linearGradient4094"
gradientUnits="userSpaceOnUse"
x1="48"
y1="90"
x2="48"
y2="5.9877172" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3737-9"
id="linearGradient4096"
gradientUnits="userSpaceOnUse"
x1="36.357143"
y1="6"
x2="36.357143"
y2="63.893143" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3465-2"
id="linearGradient4098"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0294118,0,0,1.0294118,-1.4117647,-1.411765)"
x1="43.17857"
y1="13"
x2="43.17857"
y2="83.294571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3737-7-3"
id="radialGradient4100"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1573129,0,0,0.99590774,-7.551021,0.1971319)"
cx="48"
cy="90.171875"
fx="48"
fy="90.171875"
r="42" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3737-4-0"
id="linearGradient4102"
gradientUnits="userSpaceOnUse"
x1="48"
y1="20.220806"
x2="48"
y2="138.66119" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3224-6"
id="radialGradient4104"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-18.038924,-24.218731)"
cx="47.098362"
cy="78.286942"
fx="47.098362"
fy="78.286942"
r="38.957481" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3224-6"
id="radialGradient4113"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3830053,-1.0350102e-8,0,1.3123934,-17.140614,-23.729195)"
cx="47.098362"
cy="78.286942"
fx="47.098362"
fy="78.286942"
r="38.957481" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4017"
id="linearGradient4023"
x1="48.732876"
y1="37.021988"
x2="48.609169"
y2="60.526169"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata2413">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="opacity:0.6;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3977-9"
inkscape:connector-curvature="0"
d="m 17.89831,14.489536 c -1.662,0 -3,1.338 -3,3 l 0,62 c 0,1.662 1.338,3 3,3 l 62,0 c 1.662,0 3,-1.338 3,-3 l 0,-62 c 0,-1.662 -1.338,-3 -3,-3 l -62,0 z m 0,0.96875 62,0 c 1.140081,0 2.03125,0.891169 2.03125,2.03125 l 0,62 c 0,1.140081 -0.891169,2.03125 -2.03125,2.03125 l -62,0 c -1.140081,0 -2.03125,-0.891169 -2.03125,-2.03125 l 0,-62 c 0,-1.140081 0.891169,-2.03125 2.03125,-2.03125 z" />
<g
id="layer2"
style="display:none">
<rect
width="86"
height="85"
rx="6"
ry="6"
x="5"
y="7"
id="rect3745"
style="opacity:0.9;fill:url(#ButtonShadow);fill-opacity:1;fill-rule:nonzero;stroke:none;filter:url(#filter3174)" />
</g>
<rect
style="fill:url(#radialGradient4113);fill-opacity:1;stroke:none"
id="rect3461-7"
y="8.743969"
x="9.0391903"
ry="6.1732869"
rx="5.8527675"
height="78.516495"
width="77.914963" />
<path
style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;opacity:0.15;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Abandoned Bitplane;-inkscape-font-specification:Abandoned Bitplane"
id="rect3222-3-8"
inkscape:connector-curvature="0"
d="m 19.89831,13.489536 c -3.289406,0 -6,2.710594 -6,6 l 0,58 c 0,3.289406 2.710594,6 6,6 l 58,0 c 3.289406,0 6,-2.710594 6,-6 l 0,-58 c 0,-3.289406 -2.710594,-6 -6,-6 l -58,0 z m 0,4 58,0 c 1.142594,0 2,0.857406 2,2 l 0,58 c 0,1.142594 -0.857406,2 -2,2 l -58,0 c -1.142594,0 -2,-0.857406 -2,-2 l 0,-58 c 0,-1.142594 0.857406,-2 2,-2 z" />
<path
style="opacity:0.3;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect3981-0"
inkscape:connector-curvature="0"
d="m 17.89831,14.489536 c -1.662,0 -3,1.338 -3,3 l 0,62 c 0,1.662 1.338,3 3,3 l 62,0 c 1.662,0 3,-1.338 3,-3 l 0,-62 c 0,-1.662 -1.338,-3 -3,-3 l -62,0 z m 0,1.9375 62,0 c 0.618163,0 1.0625,0.444337 1.0625,1.0625 l 0,62 c 0,0.618163 -0.444337,1.0625 -1.0625,1.0625 l -62,0 c -0.618163,0 -1.0625,-0.444337 -1.0625,-1.0625 l 0,-62 c 0,-0.618163 0.444337,-1.0625 1.0625,-1.0625 z" />
<g
id="g3912"
style="fill:url(#linearGradient4023);fill-opacity:1.0;stroke:#000000;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
transform="matrix(1.2125503,0,0,1.2125503,-13.04106,-10.991024)">
<path
d="m 26.099813,37.395511 0,22.509723 22.50972,0 0,-7.118459 -15.198872,0 0,-15.391264 -7.310848,0 z"
id="rect3294"
style="fill:url(#linearGradient4023);fill-opacity:1.0;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" />
<path
d="m 52.072568,37.395511 0,22.509723 22.509727,0 0,-7.310849 -15.583656,0 0,-7.888025 15.583656,0 0,-7.310849 -22.509727,0 z"
id="rect3298"
style="fill:url(#linearGradient4023);fill-opacity:1.0;stroke:#000000;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
inkscape:connector-curvature="0" />
</g>
<path
style="opacity:0.07999998;fill:#d9c6ed;fill-opacity:1;stroke:none"
id="rect3256-3"
inkscape:connector-curvature="0"
d="m 14.89831,8.7395363 c -3.242433,0 -5.8437503,2.7674997 -5.8437503,6.1874997 l 0,49.0625 C 27.878614,50.130638 54.866224,40.195879 86.96081,42.208286 l 0,-27.28125 c 0,-3.420001 -2.63257,-6.1874997 -5.875,-6.1874997 l -66.1875,0 z" />
<g
id="layer1"
style="display:inline">
<path
d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,72 c 0,3.324 2.676,6 6,6 l 72,0 c 3.324,0 6,-2.676 6,-6 L 90,12 C 90,8.676 87.324,6 84,6 L 12,6 z m 5,7 62,0 c 2.216001,0 4,1.784 4,4 l 0,62 c 10e-7,2.216001 -1.784,4 -4,4 l -62,0 c -2.215999,10e-7 -4,-1.784 -4,-4 l 0,-62 c 0,-2.215999 1.784,-4 4,-4 z"
inkscape:connector-curvature="0"
id="rect2419"
style="fill:url(#linearGradient4094);fill-opacity:1;fill-rule:nonzero;stroke:none" />
<path
d="M 12,6 C 8.676,6 6,8.676 6,12 l 0,2 0,68 0,2 c 0,0.334721 0.04135,0.6507 0.09375,0.96875 0.0487,0.295596 0.09704,0.596915 0.1875,0.875 0.00988,0.03038 0.020892,0.0636 0.03125,0.09375 0.098865,0.287771 0.2348802,0.547452 0.375,0.8125 0.1445918,0.273507 0.3156161,0.535615 0.5,0.78125 0.1843839,0.245635 0.3737765,0.473472 0.59375,0.6875 0.439947,0.428056 0.94291,0.814526 1.5,1.09375 0.278545,0.139612 0.5734731,0.246947 0.875,0.34375 -0.2562018,-0.100222 -0.4867109,-0.236272 -0.71875,-0.375 -0.00741,-0.0044 -0.023866,0.0045 -0.03125,0 -0.031933,-0.0193 -0.062293,-0.04251 -0.09375,-0.0625 -0.120395,-0.0767 -0.2310226,-0.163513 -0.34375,-0.25 -0.1061728,-0.0808 -0.2132809,-0.161112 -0.3125,-0.25 C 8.4783201,88.557317 8.3087904,88.373362 8.15625,88.1875 8.0486711,88.057245 7.9378561,87.922215 7.84375,87.78125 7.818661,87.74287 7.805304,87.69538 7.78125,87.65625 7.716487,87.553218 7.6510225,87.451733 7.59375,87.34375 7.4927417,87.149044 7.3880752,86.928049 7.3125,86.71875 7.30454,86.69694 7.288911,86.6782 7.28125,86.65625 7.2494249,86.5643 7.2454455,86.469419 7.21875,86.375 7.1884177,86.268382 7.1483606,86.171969 7.125,86.0625 7.0521214,85.720988 7,85.364295 7,85 L 7,83 7,15 7,13 C 7,10.218152 9.2181517,8 12,8 l 2,0 68,0 2,0 c 2.781848,0 5,2.218152 5,5 l 0,2 0,68 0,2 c 0,0.364295 -0.05212,0.720988 -0.125,1.0625 -0.04415,0.206893 -0.08838,0.397658 -0.15625,0.59375 -0.0077,0.02195 -0.0233,0.04069 -0.03125,0.0625 -0.06274,0.173739 -0.138383,0.367449 -0.21875,0.53125 -0.04158,0.0828 -0.07904,0.169954 -0.125,0.25 -0.0546,0.09721 -0.126774,0.18835 -0.1875,0.28125 -0.09411,0.140965 -0.204921,0.275995 -0.3125,0.40625 -0.143174,0.17445 -0.303141,0.346998 -0.46875,0.5 -0.01117,0.0102 -0.01998,0.02115 -0.03125,0.03125 -0.138386,0.125556 -0.285091,0.234436 -0.4375,0.34375 -0.102571,0.07315 -0.204318,0.153364 -0.3125,0.21875 -0.0074,0.0045 -0.02384,-0.0044 -0.03125,0 -0.232039,0.138728 -0.462548,0.274778 -0.71875,0.375 0.301527,-0.0968 0.596455,-0.204138 0.875,-0.34375 0.55709,-0.279224 1.060053,-0.665694 1.5,-1.09375 0.219973,-0.214028 0.409366,-0.441865 0.59375,-0.6875 0.184384,-0.245635 0.355408,-0.507743 0.5,-0.78125 0.14012,-0.265048 0.276135,-0.524729 0.375,-0.8125 0.01041,-0.03078 0.02133,-0.06274 0.03125,-0.09375 0.09046,-0.278085 0.1388,-0.579404 0.1875,-0.875 C 89.95865,84.6507 90,84.334721 90,84 l 0,-2 0,-68 0,-2 C 90,8.676 87.324,6 84,6 L 12,6 z"
inkscape:connector-curvature="0"
id="rect3728"
style="opacity:0.5;fill:url(#linearGradient4096);fill-opacity:1;fill-rule:nonzero;stroke:none" />
<path
d="m 17,12 c -2.752703,0 -5,2.247297 -5,5 l 0,62 c 0,2.752703 2.247298,5 5,5 l 62,0 c 2.752703,0 5.000001,-2.247297 5,-5 l 0,-62 c 0,-2.752703 -2.247297,-5 -5,-5 l -62,0 z m 0,2 62,0 c 1.679298,0 3,1.320703 3,3 l 0,62 c 10e-7,1.679297 -1.320703,3 -3,3 l -62,0 c -1.679296,0 -3,-1.320703 -3,-3 l 0,-62 c 0,-1.679297 1.320703,-3 3,-3 z"
inkscape:connector-curvature="0"
id="rect3425"
style="fill:url(#linearGradient4098);fill-opacity:1;fill-rule:nonzero;stroke:none" />
<path
d="M 12,90 C 8.676,90 6,87.324 6,84 L 6,82 6,14 6,12 c 0,-0.334721 0.04135,-0.6507 0.09375,-0.96875 0.0487,-0.295596 0.09704,-0.596915 0.1875,-0.875 C 6.29113,10.12587 6.302142,10.09265 6.3125,10.0625 6.411365,9.774729 6.5473802,9.515048 6.6875,9.25 6.8320918,8.976493 7.0031161,8.714385 7.1875,8.46875 7.3718839,8.223115 7.5612765,7.995278 7.78125,7.78125 8.221197,7.353194 8.72416,6.966724 9.28125,6.6875 9.559795,6.547888 9.8547231,6.440553 10.15625,6.34375 9.9000482,6.443972 9.6695391,6.580022 9.4375,6.71875 c -0.00741,0.0044 -0.023866,-0.0045 -0.03125,0 -0.031933,0.0193 -0.062293,0.04251 -0.09375,0.0625 -0.120395,0.0767 -0.2310226,0.163513 -0.34375,0.25 -0.1061728,0.0808 -0.2132809,0.161112 -0.3125,0.25 C 8.4783201,7.442683 8.3087904,7.626638 8.15625,7.8125 8.0486711,7.942755 7.9378561,8.077785 7.84375,8.21875 7.818661,8.25713 7.805304,8.30462 7.78125,8.34375 7.716487,8.446782 7.6510225,8.548267 7.59375,8.65625 7.4927417,8.850956 7.3880752,9.071951 7.3125,9.28125 7.30454,9.30306 7.288911,9.3218 7.28125,9.34375 7.2494249,9.4357 7.2454455,9.530581 7.21875,9.625 7.1884177,9.731618 7.1483606,9.828031 7.125,9.9375 7.0521214,10.279012 7,10.635705 7,11 l 0,2 0,68 0,2 c 0,2.781848 2.2181517,5 5,5 l 2,0 68,0 2,0 c 2.781848,0 5,-2.218152 5,-5 l 0,-2 0,-68 0,-2 C 89,10.635705 88.94788,10.279012 88.875,9.9375 88.83085,9.730607 88.78662,9.539842 88.71875,9.34375 88.71105,9.3218 88.69545,9.30306 88.6875,9.28125 88.62476,9.107511 88.549117,8.913801 88.46875,8.75 88.42717,8.6672 88.38971,8.580046 88.34375,8.5 88.28915,8.40279 88.216976,8.31165 88.15625,8.21875 88.06214,8.077785 87.951329,7.942755 87.84375,7.8125 87.700576,7.63805 87.540609,7.465502 87.375,7.3125 87.36383,7.3023 87.35502,7.29135 87.34375,7.28125 87.205364,7.155694 87.058659,7.046814 86.90625,6.9375 86.803679,6.86435 86.701932,6.784136 86.59375,6.71875 c -0.0074,-0.0045 -0.02384,0.0044 -0.03125,0 -0.232039,-0.138728 -0.462548,-0.274778 -0.71875,-0.375 0.301527,0.0968 0.596455,0.204138 0.875,0.34375 0.55709,0.279224 1.060053,0.665694 1.5,1.09375 0.219973,0.214028 0.409366,0.441865 0.59375,0.6875 0.184384,0.245635 0.355408,0.507743 0.5,0.78125 0.14012,0.265048 0.276135,0.524729 0.375,0.8125 0.01041,0.03078 0.02133,0.06274 0.03125,0.09375 0.09046,0.278085 0.1388,0.579404 0.1875,0.875 C 89.95865,11.3493 90,11.665279 90,12 l 0,2 0,68 0,2 c 0,3.324 -2.676,6 -6,6 l -72,0 z"
inkscape:connector-curvature="0"
id="path3615"
style="opacity:0.4;fill:url(#radialGradient4100);fill-opacity:1;fill-rule:nonzero;stroke:none" />
<rect
width="66"
height="66"
rx="12"
ry="12"
x="15"
y="15"
clip-path="url(#clipPath3613-5)"
id="rect3171"
style="opacity:0.1;fill:url(#linearGradient4102);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;filter:url(#filter3794-2)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 28 KiB

View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View file

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View file

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,8 @@
// Convert ISO8601 To UTC
window.onload = function(){
for (element of document.querySelectorAll('#articles > ul > li > div > h4')) {
timestamp = new Date(element.textContent);
element.textContent = timestamp.toUTCString();
}
}

View file

@ -0,0 +1,7 @@
// Parse Markdown
window.onload = function(){
for (element of document.querySelectorAll('#articles > ul > li > div > p')) {
element.innerHTML = marked.parse(element.textContent);
}
}

View file

@ -0,0 +1,265 @@
window.onload = async function(){
let locationHref = new URL(location.href);
let node = locationHref.searchParams.get('node')
let pubsub = locationHref.searchParams.get('pubsub')
// Set 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.type = `application/atom+xml`;
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.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 div[type="text"]')) {
element.innerHTML = marked.parse(element.textContent);
}
// NOTE Report this issue to Movim. See node "deltachat" of pubsub "news.movim.eu".
for (let element of document.querySelectorAll('#articles div[type="html"]')) {
if (!element.children.length) {
element.innerHTML = marked.parse(element.textContent);
}
}
/*
NOTE
The reason for the following code to parse HTML inside an software which
already parses HTML, is that some people who influence the so called Gecko
HTML Engine product (mostly people from the advertisement industry with whom
I shamefully used to work with) have intentions to eliminate standards and
technologies such as Syndication (Atom/RDF/RSS), XSLT and many other
technologies that actually empower people and their privacy.
Recently, some changes were made to the XSLT parser of Gecko which result in
noncompliance with the XSLT standard.
The XSLT feature that was removed is "disable-output-escaping" which upon the
value "yes" the XSLT engine should transform and treat a subject string into
HTML, yet the Gecko HTML Engine ignores this XSLT direction.
This change was probably made in order to:
* Frustrate new XSLT developers; or
* Influence new XSLT developers to think that XSLT has a limited set of
features and believing of some sort of inability to parse HTML from a
retrieved HTML string; and
* Consequently cause developers to abstain from using the XSLT technology.
Do not use HTML browsers or use Ladybird, Pale Moon or browsers that are
powered by KHTML (WebKit) instead of anti-privacy software such as Chromium
and Gecko.
*/
// Parse HTML
//if (navigator.userAgent.includes(') Gecko/')) {
// for (let element of document.querySelectorAll('#articles div[type="html"]')) {
// element.innerHTML = element.textContent;
// }
//}
for (let element of document.querySelectorAll('#articles div[type="html"]')) {
if (!element.children.length) {
element.innerHTML = element.textContent;
}
}
// Build a journal list
if (locationHref.pathname.startsWith('/atom') && pubsub && node && !node.includes('/')) {
itemsList = await openJson(pubsub, node)
if (itemsList && locationHref.searchParams.get('item')) {
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.reverse()) {
let elementLi = document.createElement('li');
let elementA = document.createElement('a');
elementA.textContent = item.title;
elementA.href = item.link;
elementLi.appendChild(elementA);
elementUl.appendChild(elementLi);
console.log(elementLi.length)
if (elementUl.children.length > 9) {break};
}
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}`,
'type' : 'x-scheme-handler/xmpp'},
{'text' : 'Subscribe with a News Reader.',
'href' : `feed://${location.host}/atom?pubsub=${pubsub}&node=${node}`,
'type' : 'application/atom+xml'},
{'text' : 'Browse the journal.',
'href' : `atom?pubsub=${pubsub}&node=${node}`,
'type' : 'application/atom+xml'},
{'text' : 'Browse the portal.',
'href' : `opml?pubsub=${pubsub}`,
'type' : 'text/x-opml'}
]
for (let link of links) {
let elementLi = document.createElement('li');
let elementA = document.createElement('a');
elementA.textContent = link.text;
elementA.href = link.href;
elementA.type = link.type;
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(
'#articles h3 > a[href^="xmpp:"][id^="rivista-"],' +
'#articles h5.related > a[class^="rivista-"],' +
'#journal > ol > li > a[href^="xmpp:"]')) {
xmppUri = new URL(xmppLink);
let parameters = xmppUri.search.split(';');
try {
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)
}
}
// Display a selection of suggested software.
const selection = {
'akregator' : {
'name' : 'Akregator',
'image' : 'akregator.svg',
'url' : 'https://apps.kde.org/akregator/'
},
'leechcraft' : {
'name' : 'LeechCraft',
'image' : 'leechcraft.png',
'url' : 'https://leechcraft.org/'
},
'liferea' : {
'name' : 'Liferea',
'image' : 'liferea.svg',
'url' : 'https://lzone.de/liferea/'
},
'raven' : {
'name' : 'Raven Reader',
'image' : 'raven.svg',
'url' : 'https://ravenreader.app/'
},
'rssguard' : {
'name' : 'RSS Guard',
'image' : 'rssguard.png',
'url' : 'https://github.com/martinrotter/rssguard'
},
'rssowl' : {
'name' : 'RSSOwl',
'image' : 'rssowl.svg',
'url' : 'http://www.rssowl.org/'
},
'tickr' : {
'name' : 'TICKR',
'image' : 'tickr.png',
'url' : 'https://www.open-tickr.net/'
}
}
let selectionLink = document.querySelector('#selection-link');
selectionLink.addEventListener ('click', function() {
let elementDiv = document.createElement('div');
elementDiv.id = 'selection-page';
let elementH1 = document.createElement('h1');
elementH1.textContent = 'Select A News Reader';
elementDiv.appendChild(elementH1);
let elementH2 = document.createElement('h2');
elementH2.textContent = 'Install A Feed Reader For Desktop And Mobile';
elementDiv.appendChild(elementH2);
const brands = Object.keys(selection);
let elementDivSel = document.createElement('div');
elementDivSel.id = 'selection';
for (let i = 0; i < brands.length; i++) {
let brand = brands[i];
let elementSpan = document.createElement('span');
let elementA = document.createElement('a');
elementA.href = selection[brand].url;
elementA.textContent = selection[brand].name;
let elementImg = document.createElement('img');
elementImg.src = 'graphic/' + selection[brand].image;
elementSpan.appendChild(elementImg);
elementSpan.appendChild(elementA);
elementDivSel.appendChild(elementSpan);
elementDiv.appendChild(elementDivSel);
}
let elementP1 = document.createElement('p');
elementP1.textContent = '' +
'This is a selection of desktop, mobile and HTML (sometimes referred to ' +
'as "online") News Readers for you to choose from.';
elementDiv.appendChild(elementP1);
let elementP2 = document.createElement('p');
elementP2.textContent = '' +
'This selection includes: Podcast Managers, Torrent ' +
'Clients, Chat Bots, HTML Browsers and Plugins which support ' +
'syndication feeds.';
elementDiv.appendChild(elementP2);
let elementSpan = document.createElement('span');
elementSpan.id = 'return';
elementSpan.textContent = 'Continue Reading';
elementSpan.addEventListener ('click', function() {
document.querySelector('#selection-page').remove();
});
elementDiv.appendChild(elementSpan);
document.body.appendChild(elementDiv);
});
}
async function openJson(pubsubJid, nodeId) {
return fetch(`/data/${pubsubJid}/${nodeId}.json`)
.then(response => {
if (!response.ok) {
throw new Error('HTTP Error: ' + response.status);
}
return response.json();
})
.then(json => {
return json;
})
.catch(err => {
console.warn(err);
})
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
include https://schimon.i2p/ in attribute name='generator' of
element <meta/> inside of html element </head>
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding = 'UTF-8'
indent = 'yes'
media-type = 'application/atom+xml'
method = 'html'
version = '4.01' />
<!-- Atom Syndication Format 1.0 -->
<xsl:include href='atom_as_xhtml.xsl'/>
<!-- extract filename from given url string -->
<xsl:include href='extract-filename.xsl'/>
<!-- set page metadata -->
<xsl:include href='metadata.xsl'/>
<!-- transform filesize from given length string -->
<xsl:include href='transform-filesize.xsl'/>
</xsl:stylesheet>

View file

@ -0,0 +1,531 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
include https://schimon.i2p/ in attribute name='generator' of
element <meta/> inside of html element </head>
-->
<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:xml='http://www.w3.org/XML/1998/namespace'
xmlns:media='http://search.yahoo.com/mrss/'
xmlns:georss='http://www.georss.org/georss'
xmlns:geo='http://www.w3.org/2003/01/geo/wgs84_pos#'
xmlns:atom10='http://www.w3.org/2005/Atom'
xmlns:atom='http://www.w3.org/2005/Atom'>
<xsl:template match='/atom:feed'>
<!-- index right-to-left language codes -->
<!-- TODO http://www.w3.org/TR/xpath/#function-lang -->
<xsl:variable name='rtl'
select='@xml:lang[
contains(self::node(),"ar") or
contains(self::node(),"fa") or
contains(self::node(),"he") or
contains(self::node(),"ji") or
contains(self::node(),"ku") or
contains(self::node(),"ur") or
contains(self::node(),"yi")]'/>
<html>
<head>
<xsl:call-template name='metadata'>
<xsl:with-param name='name' select='"description"' />
<xsl:with-param name='content' select='atom:subtitle' />
</xsl:call-template>
<xsl:call-template name='metadata'>
<xsl:with-param name='name' select='"generator"' />
<xsl:with-param name='content' select='Rivista' />
</xsl:call-template>
<xsl:call-template name='metadata'>
<xsl:with-param name='name' select='"mimetype"' />
<xsl:with-param name='content' select='"application/atom+xml"' />
</xsl:call-template>
<title>
<xsl:choose>
<xsl:when test='atom:title and not(atom:title="") and count(atom:entry) &gt; 1'>
<xsl:value-of select='atom:title'/>
</xsl:when>
<xsl:when test='atom:entry'>
<xsl:value-of select='atom:entry/atom:title'/>
</xsl:when>
<xsl:otherwise>Rivista</xsl:otherwise>
</xsl:choose>
</title>
<!-- TODO media='print' -->
<link rel='stylesheet' type='text/css' media='screen' href='/css/stylesheet.css'/>
<link rel='icon' type='image/svg+xml' href='/graphic/xmpp.svg'/>
<!-- whether language code is of direction right-to-left -->
<xsl:if test='$rtl'>
<link id='semitic' href='/css/stylesheet-rtl.css' rel='stylesheet' type='text/css' />
</xsl:if>
<script type='text/javascript' src='/script/marked.min.js'/>
<script type='text/javascript' src='/script/postprocess.js'/>
</head>
<body>
<div id='actions'>
<a id='follow' title='Subscribe the latest updates and news.'
onclick='window.open(location.href.replace(/^https?:/, "feed:"), "_self")'>
<!-- xsl:attribute name="href">
feed:<xsl:value-of select="atom:link[@rel='self']/@href" />
</xsl:attribute -->
Follow
</a>
<a id='subtome' title='Subscribe via SubToMe.'>
<xsl:attribute name='href'>
javascript:location.href='https://www.subtome.com/#/subscribe?feeds='+location.href;
</xsl:attribute>
<xsl:attribute name='onclick'>
(
function(btn){
var z=document.createElement('script');
document.subtomeBtn=btn;
z.src='https://www.subtome.com/load.js';document.body.appendChild(z);
}
)(this);
return false;
</xsl:attribute>
SubToMe
</a>
<a href='https://xmpp.org/about/technology-overview/'
title='Of the benefits of XMPP.'>
XMPP
</a>
<a href='https://join.jabber.network/#syndication@conference.movim.eu?join'
title='Syndictaion and PubSub.'>
Groupchat
</a>
<a href='https://aboutfeeds.com/'
title='Of the benefits of syndication feed.'
id='aboutfeeds'>
Help
</a>
</div>
<div id='feed'>
<div id='header'>
<!-- feed title -->
<h1 id='title'>
<xsl:choose>
<xsl:when test='atom:title and not(atom:title="") and count(atom:entry) &gt; 1'>
<xsl:value-of select='atom:title'/>
</xsl:when>
<xsl:when test='atom:entry'>
<xsl:value-of select='atom:entry/atom:title'/>
</xsl:when>
<xsl:otherwise>
No title
</xsl:otherwise>
</xsl:choose>
</h1>
<!-- feed subtitle -->
<h2 id='subtitle'>
<xsl:choose>
<xsl:when test='atom:title and not(atom:title="") and count(atom:entry) &gt; 1'>
<xsl:value-of select='atom:subtitle'/>
</xsl:when>
<xsl:when test='atom:entry'>
<xsl:attribute name='class'><xsl:text>date</xsl:text></xsl:attribute>
<xsl:value-of select='atom:entry/atom:updated'/>
</xsl:when>
<xsl:when test='atom:entry'>
<xsl:attribute name='class'><xsl:text>date</xsl:text></xsl:attribute>
<xsl:value-of select='atom:entry/atom:published'/>
</xsl:when>
<xsl:otherwise>
Rivista XMPP Journal Publisher
</xsl:otherwise>
</xsl:choose>
</h2>
</div>
<xsl:if test='count(atom:entry) &gt; 1'>
<div id='menu'>
<h3>Latest Posts</h3>
<!-- xsl:for-each select='atom:entry[position() &lt;21]' -->
<ol>
<xsl:for-each select='atom:entry[not(position() &gt; 20)]'>
<li>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:text>#rivista-</xsl:text>
<xsl:value-of select='position()'/>
</xsl:attribute>
<xsl:choose>
<xsl:when test='string-length(atom:title) &gt; 0'>
<xsl:value-of select='atom:title'/>
</xsl:when>
<xsl:otherwise>
*** No Title ***
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</li>
</xsl:for-each>
</ol>
</div>
</xsl:if>
<div id='articles'>
<!-- feed entry -->
<xsl:choose>
<xsl:when test='atom:entry'>
<ul>
<xsl:for-each select='atom:entry[not(position() >20)]'>
<li>
<div class='entry'>
<!-- entry title -->
<h3 class='title'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:choose>
<xsl:when test='atom:link[@rel="self"]'>
<xsl:value-of select='atom:link[@rel="self"]/@href'/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select='atom:link/@href'/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:attribute name='id'>
<xsl:text>rivista-</xsl:text>
<xsl:value-of select='position()'/>
</xsl:attribute>
<xsl:choose>
<xsl:when test='string-length(atom:title) &gt; 0'>
<xsl:value-of select='atom:title'/>
</xsl:when>
<xsl:otherwise>
*** No Title ***
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</h3>
<!-- geographic location -->
<xsl:choose>
<xsl:when test='geo:lat and geo:long'>
<xsl:variable name='lat' select='geo:lat'/>
<xsl:variable name='lng' select='geo:long'/>
<span class='geolocation'>
<a href='geo:{$lat},{$lng}'>📍</a>
</span>
</xsl:when>
<xsl:when test='geo:Point'>
<xsl:variable name='lat' select='geo:Point/geo:lat'/>
<xsl:variable name='lng' select='geo:Point/geo:long'/>
<span class='geolocation'>
<a href='geo:{$lat},{$lng}'>📍</a>
</span>
</xsl:when>
<xsl:when test='georss:point'>
<xsl:variable name='lat' select='substring-before(georss:point, " ")'/>
<xsl:variable name='lng' select='substring-after(georss:point, " ")'/>
<xsl:variable name='name' select='georss:featurename'/>
<span class='geolocation'>
<a href='geo:{$lat},{$lng}' title='{$name}'>📍</a>
</span>
</xsl:when>
</xsl:choose>
<!-- entry date -->
<xsl:element name='h4'>
<xsl:choose>
<xsl:when test='atom:updated'>
<xsl:attribute name='class'>
<xsl:text>updated</xsl:text>
</xsl:attribute>
<xsl:value-of select='atom:updated'/>
</xsl:when>
<xsl:when test='atom:published'>
<xsl:attribute name='class'>
<xsl:text>published</xsl:text>
</xsl:attribute>
<xsl:value-of select='atom:published'/>
</xsl:when>
<xsl:otherwise>
<h4 class='warning atom1 published'></h4>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
<!-- entry author -->
<xsl:if test='atom:author'>
<h4 class='author'>
<xsl:text>By </xsl:text>
<xsl:choose>
<xsl:when test='atom:author/atom:email'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:text>mailto:</xsl:text>
<xsl:value-of select='atom:author/atom:email'/>
</xsl:attribute>
<xsl:attribute name='title'>
<xsl:text>Send an Email to </xsl:text>
<xsl:value-of select='atom:author/atom:email'/>
</xsl:attribute>
<xsl:value-of select='atom:author/atom:name'/>
</xsl:element>
</xsl:when>
<xsl:when test='atom:author/atom:uri'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:author/atom:uri'/>
</xsl:attribute>
<xsl:attribute name='title'>
<xsl:value-of select='atom:author/atom:summary'/>
</xsl:attribute>
<xsl:value-of select='atom:author/atom:name'/>
</xsl:element>
</xsl:when>
<xsl:when test='atom:author/atom:name'>
<xsl:value-of select='atom:author/atom:name'/>
</xsl:when>
<xsl:when test='atom:uri'>
<xsl:value-of select='atom:uri'/>
</xsl:when>
</xsl:choose>
</h4>
</xsl:if>
<h5 class='related'>
<xsl:if test='atom:link[@rel="alternate" and @type="x-scheme-handler/xmpp"]'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="alternate" and @type="x-scheme-handler/xmpp"]/@href'/>
</xsl:attribute>
<xsl:attribute name='class'>
<xsl:text>rivista-jabber</xsl:text>
</xsl:attribute>
💡️ Source
</xsl:element>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="alternate" and @type="x-scheme-handler/xmpp"]/@href'/>
</xsl:attribute>
(XMPP)
</xsl:element>
</xsl:if>
<xsl:if test='atom:link[@rel="contact"]'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="contact"]/@href'/>
</xsl:attribute>
<xsl:attribute name='class'>
<xsl:text>contact-uri</xsl:text>
</xsl:attribute>
🪪️ Contact
</xsl:element>
</xsl:if>
<xsl:if test='atom:link[@rel="replies"]'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="replies"]/@href'/>
</xsl:attribute>
<xsl:attribute name='class'>
<xsl:text>rivista-replies</xsl:text>
</xsl:attribute>
💬 Discussion
</xsl:element>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="replies"]/@href'/>
</xsl:attribute>
(XMPP)
</xsl:element>
</xsl:if>
<xsl:if test='atom:link[@rel="alternate" and @type="text/html"]'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="alternate" and @type="text/html"]/@href'/>
</xsl:attribute>
📜 HTML
</xsl:element>
</xsl:if>
<xsl:if test='atom:link[@rel="related" and @type="text/html"]'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='atom:link[@rel="related" and @type="text/html"]/@href'/>
</xsl:attribute>
📜 HTML (Related)
</xsl:element>
</xsl:if>
</h5>
<!-- entry content -->
<xsl:if test='atom:content or atom:summary'>
<div class='content'>
<xsl:choose>
<xsl:when test='atom:summary[contains(@type,"text")]'>
<xsl:attribute name='type'>
<xsl:value-of select='atom:summary/@type'/>
</xsl:attribute>
<xsl:value-of select='atom:summary'/>
</xsl:when>
<xsl:when test='atom:summary[contains(@type,"html")]'>
<xsl:attribute name='type'>
<xsl:value-of select='atom:summary/@type'/>
</xsl:attribute>
<xsl:value-of select='atom:summary' disable-output-escaping='yes'/>
</xsl:when>
<xsl:when test='atom:summary[contains(@type,"base64")]'>
<!-- TODO add xsl:template to handle inline media -->
</xsl:when>
<xsl:when test='atom:content[contains(@type,"text")]'>
<xsl:attribute name='type'>
<xsl:value-of select='atom:content/@type'/>
</xsl:attribute>
<xsl:value-of select='atom:content'/>
</xsl:when>
<xsl:when test='atom:content[contains(@type,"html")]'>
<xsl:attribute name='type'>
<xsl:value-of select='atom:content/@type'/>
</xsl:attribute>
<xsl:value-of select='atom:content' disable-output-escaping='yes'/>
</xsl:when>
<xsl:when test='atom:content[contains(@type,"base64")]'>
<!-- TODO add xsl:template to handle inline media -->
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test='atom:summary and not(atom:summary="")'>
<xsl:value-of select='atom:summary' disable-output-escaping='yes'/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select='atom:content' disable-output-escaping='yes'/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:if>
<!-- entry tags -->
<xsl:if test='atom:category/@term'>
<h4>Tags</h4>
<span class='tags'>
<xsl:for-each select='atom:category'>
<xsl:element name='div'>
<xsl:value-of select='@term'/>
</xsl:element>
</xsl:for-each>
</span>
</xsl:if>
<!-- entry enclosure -->
<xsl:if test='atom:link[@rel="enclosure"]'>
<h4>Media</h4>
<span class='enclosures' title='Right-click and Save link as…'>
<xsl:for-each select='atom:link[@rel="enclosure"]'>
<div class='enclosure' title='Right-click and Save link as…'>
<xsl:element name='span'>
<xsl:attribute name='icon'>
<xsl:value-of select='substring-before(@type,"/")'/>
</xsl:attribute>
</xsl:element>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='@href'/>
</xsl:attribute>
<xsl:attribute name='download'/>
<xsl:call-template name='extract-filename'>
<xsl:with-param name='url' select='@href' />
</xsl:call-template>
</xsl:element>
<xsl:element name='span'>
<xsl:attribute name='class'>
<xsl:value-of select='substring-before(@type,"/")'/>
</xsl:attribute>
</xsl:element>
<xsl:if test='@length &gt; 0'>
<xsl:call-template name='transform-filesize'>
<xsl:with-param name='length' select='@length' />
</xsl:call-template>
</xsl:if>
<br/>
</div>
</xsl:for-each>
</span>
</xsl:if>
</div>
<!-- entry id -->
<xsl:if test='not(atom:id)'>
<div class='warning atom1 id'>No entry ID</div>
</xsl:if>
</li>
</xsl:for-each>
</ul>
</xsl:when>
<xsl:otherwise>
<ul>
<li>
<div class='entry'>
<h3 class='title'>
<a href='javascript:alert("Please check that the mother PubSub node is populated with content.")'>
No content
</a>
</h3>
<h4>This entry is empty</h4>
<div class='content'>Please check that the mother PubSub node is populated with content.</div>
</div>
</li>
</ul>
</xsl:otherwise>
</xsl:choose>
</div>
</div>
<div id='references'>
<a href='https://joinjabber.org/'
title='An Inclusive Space On The Jabber Network.'>
JoinJabber
</a>
<a href='https://libervia.org/'
title='The Universal Communication Ecosystem.'>
Libervia
</a>
<a href='https://join.movim.eu/'
title='The Social Platform Shaped For Your Community.'>
Movim
</a>
<a href='https://git.xmpp-it.net/sch/Rivista'
title='A Journal Publisher And Browser For XMPP.'>
Rivista
</a>
<a href='https://git.xmpp-it.net/sch/Blasta'
title='A Social Bookmark Manager For XMPP.'>
Blasta
</a>
<a href='https://github.com/SeveFP/Reeder'
title='An XMPP-Based Feed Reader.'>
Reeder
</a>
<a href='https://modernxmpp.org/'
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.'>
XMPP
</a>
<a href='https://xmpp.org/extensions/xep-0060.html'
title='XEP-0060: Publish-Subscribe.'>
PubSub
</a>
</div>
<!-- note -->
<p id='note'>
This an Atom document which can also be viewed with
a Syndication Feed Reader (also referred to as News Reader
or RSS Reader) which provides automated news updates and
notifications on desktop and mobile.
<span id="selection-link">Click here</span> for a
selection of software and pick the ones that would fit
to you best!
</p>
<p id='small'>
<i>
This ASF (Atom Syndication Format)
document is conveyed as an XHTML document.
This document was produced by an
XSLT <a href="xsl/atom.xsl">stylesheet</a>.
XSLT is a powerful technology which transforms XML
documents into HTML, JSON, PDF, Text XHTML, and
(modified) XML documents;
<a href="https://www.w3.org/Style/XSL/">Learn more</a>
about The Extensible Stylesheet Language Family (XSL).
</i>
</p>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
include https://schimon.i2p/ in attribute name='generator' of
element <meta/> inside of html element </head>
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding = 'UTF-8'
indent = 'yes'
media-type = 'text/x-opml'
method = 'html'
version = '4.01' />
<!-- Outline Processor Markup Language 1.0 -->
<xsl:include href='opml_as_xhtml.xsl'/>
<!-- set page metadata -->
<xsl:include href='metadata.xsl'/>
</xsl:stylesheet>

View file

@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (C) 2016 - 2024 Schimon Jehuda. Released under MIT license
Feeds rendered using this XSLT stylesheet, or it's derivatives, must
include https://schimon.i2p/ in attribute name='generator' of
element <meta/> inside of html element </head>
-->
<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:xml='http://www.w3.org/XML/1998/namespace'>
<xsl:template match='/opml'>
<!-- index right-to-left language codes -->
<!-- TODO http://www.w3.org/TR/xpath/#function-lang -->
<xsl:variable name='rtl'
select='lang[
contains(self::node(),"ar") or
contains(self::node(),"fa") or
contains(self::node(),"he") or
contains(self::node(),"ji") or
contains(self::node(),"ku") or
contains(self::node(),"ur") or
contains(self::node(),"yi")]'/>
<html>
<head>
<xsl:call-template name='metadata'>
<xsl:with-param name='name' select='"description"' />
<xsl:with-param name='content' select='subtitle' />
</xsl:call-template>
<xsl:call-template name='metadata'>
<xsl:with-param name='name' select='"generator"' />
<xsl:with-param name='content' select='Rivista' />
</xsl:call-template>
<xsl:call-template name='metadata'>
<xsl:with-param name='name' select='"mimetype"' />
<xsl:with-param name='content' select='"text/x-opml"' />
</xsl:call-template>
<title>
<xsl:choose>
<xsl:when test='//head/title and not(//head/title="")'>
<xsl:value-of select='//head/title'/>
</xsl:when>
<xsl:otherwise>Rivista OPML</xsl:otherwise>
</xsl:choose>
</title>
<!-- TODO media='print' -->
<link rel='stylesheet' type='text/css' media='screen' href='/css/stylesheet.css'/>
<link rel='icon' type='image/svg+xml' href='/graphic/xmpp.svg'/>
<!-- whether language code is of direction right-to-left -->
<xsl:if test='$rtl'>
<link id='semitic' href='/css/stylesheet-rtl.css' rel='stylesheet' type='text/css' />
</xsl:if>
<script type='text/javascript' src='/script/postprocess.js'/>
</head>
<body>
<div id='actions'>
<a href='https://xmpp.org/about/technology-overview/'
title='Of the benefits of XMPP.'>
XMPP
</a>
<a href='https://join.jabber.network/#syndication@conference.movim.eu?join'
title='Syndictaion and PubSub.'>
Groupchat
</a>
<a href='https://aboutfeeds.com/'
title='Of the benefits of syndication feed.'
id='aboutfeeds'>
Help
</a>
</div>
<div id='feed'>
<div id='header'>
<!-- feed title -->
<h1 id='title'>
<xsl:choose>
<xsl:when test='//head/title and not(//head/title="") and count(//outline) &gt; 1'>
<xsl:value-of select='//head/title'/>
</xsl:when>
<xsl:otherwise>
Rivista OPML Collection
</xsl:otherwise>
</xsl:choose>
</h1>
<!-- feed subtitle -->
<h2 id='subtitle'>
<xsl:choose>
<xsl:when test='//head/description and not(//head/description="") and count(//outline) &gt; 1'>
<xsl:value-of select='//head/description'/>
</xsl:when>
<xsl:otherwise>
Outline Processor Markup Language
</xsl:otherwise>
</xsl:choose>
</h2>
</div>
<xsl:if test='count(//outline) &gt; 1'>
<div id='menu'>
<h3>Subscriptions</h3>
<!-- xsl:for-each select='outline[position() &lt;21]' -->
<ol>
<xsl:for-each select='//outline[not(position() &gt; 20)]'>
<li>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:text>#rivista-</xsl:text>
<xsl:value-of select='position()'/>
</xsl:attribute>
<xsl:choose>
<xsl:when test='string-length(@text) &gt; 0'>
<xsl:value-of select='@text'/>
</xsl:when>
<xsl:otherwise>
*** No Title ***
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</li>
</xsl:for-each>
</ol>
</div>
</xsl:if>
<div id='articles'>
<!-- opml outline -->
<xsl:choose>
<xsl:when test='//outline'>
<ul>
<xsl:for-each select='//outline[not(position() &gt; 20)]'>
<li>
<div class='entry'>
<!-- outline title -->
<h3 class='title'>
<xsl:element name='a'>
<xsl:attribute name='href'>
<xsl:value-of select='@xmlUrl'/>
</xsl:attribute>
<xsl:attribute name='id'>
<xsl:text>rivista-</xsl:text>
<xsl:value-of select='position()'/>
</xsl:attribute>
<xsl:choose>
<xsl:when test='string-length(@text) &gt; 0'>
<xsl:value-of select='@text'/>
</xsl:when>
<xsl:otherwise>
*** No Title ***
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</h3>
<!-- entry content -->
<h4>
<xsl:value-of select='@text'/>
</h4>
<p class='content'>
<xsl:value-of select='@xmlUrl'/>
</p>
</div>
</li>
</xsl:for-each>
</ul>
</xsl:when>
<xsl:otherwise>
<div class='notice no-entry'></div>
</xsl:otherwise>
</xsl:choose>
</div>
</div>
<div id='references'>
<a href='https://joinjabber.org/'
title='An Inclusive Space On The Jabber Network.'>
JoinJabber
</a>
<a href='https://libervia.org/'
title='The Universal Communication Ecosystem.'>
Libervia
</a>
<a href='https://join.movim.eu/'
title='The Social Platform Shaped For Your Community.'>
Movim
</a>
<a href='https://git.xmpp-it.net/sch/Rivista'
title='A Journal Publisher And Browser For XMPP.'>
Rivista
</a>
<a href='https://git.xmpp-it.net/sch/Blasta'
title='A Social Bookmark Manager For XMPP.'>
Blasta
</a>
<a href='https://github.com/SeveFP/Reeder'
title='An XMPP-Based Feed Reader.'>
Reeder
</a>
<a href='https://modernxmpp.org/'
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.'>
XMPP
</a>
<a href='https://xmpp.org/extensions/xep-0060.html'
title='XEP-0060: Publish-Subscribe.'>
PubSub
</a>
</div>
<!-- note -->
<p id='note'>
This is an OPML document which includes a collection of
subscriptions, and it can be imported to
a Syndication Feed Reader (also referred to as News Reader
or RSS Reader) which provides automated news updates and
notifications on desktop and mobile.
<span id="selection-link">Click here</span> for a
selection of software and pick the ones that would fit
to you best!
</p>
<p id='small'>
<i>
This OPML (Outline Processor Markup Language)
document is conveyed as an XHTML document.
This document was produced by an
XSLT <a href="xsl/opml.xsl">stylesheet</a>.
XSLT is a powerful technology which transforms XML
documents into HTML, JSON, PDF, Text XHTML, and
(modified) XML documents;
<a href="https://www.w3.org/Style/XSL/">Learn more</a>
about The Extensible Stylesheet Language Family (XSL).
</i>
</p>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

View file

@ -14,7 +14,7 @@ element <meta/> inside of html element </head>
indent = 'yes' indent = 'yes'
omit-xml-decleration='no' /> omit-xml-decleration='no' />
<!-- Atom 1.0 Syndication Format --> <!-- Atom Syndication Format 1.0 -->
<xsl:include href='atom_as_xhtml.xsl'/> <xsl:include href='atom_as_xhtml.xsl'/>
<!-- extract filename from given url string --> <!-- extract filename from given url string -->

View file

@ -15,9 +15,13 @@ xmlns:georss='http://www.georss.org/georss'
xmlns:geo='http://www.w3.org/2003/01/geo/wgs84_pos#' xmlns:geo='http://www.w3.org/2003/01/geo/wgs84_pos#'
xmlns:atom10='http://www.w3.org/2005/Atom' xmlns:atom10='http://www.w3.org/2005/Atom'
xmlns:atom='http://www.w3.org/2005/Atom'> xmlns:atom='http://www.w3.org/2005/Atom'>
<!-- Atom 1.0 Syndication Format -->
<xsl:output <xsl:output
media-type='application/atom+xml' /> method = 'html'
indent = 'yes'
omit-xml-decleration='no' />
<!-- Atom 1.0 Syndication Format -->
<xsl:template match='/atom:feed'> <xsl:template match='/atom:feed'>
<!-- index right-to-left language codes --> <!-- index right-to-left language codes -->
<!-- TODO http://www.w3.org/TR/xpath/#function-lang --> <!-- TODO http://www.w3.org/TR/xpath/#function-lang -->
@ -64,18 +68,18 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
</head> </head>
<body> <body>
<div id='actions'> <div id='actions'>
<a title='Subscribe the latest updates and news.' <a id='follow' title='Subscribe the latest updates and news.'
onclick='window.open("feed:" + location.href, "_self")'> onclick='window.open(location.href.replace(/^https?:/, "feed:"), "_self")'>
<!-- xsl:attribute name="href"> <!-- xsl:attribute name="href">
feed:<xsl:value-of select="atom:link[@rel='self']/@href" /> feed:<xsl:value-of select="atom:link[@rel='self']/@href" />
</xsl:attribute --> </xsl:attribute -->
Follow Follow
</a> </a>
<a title='Subscribe via SubToMe.'> <a id='subtome' title='Subscribe via SubToMe.'>
<xsl:attribute name="href"> <xsl:attribute name='href'>
https://www.subtome.com/#/subscribe?feeds=<xsl:value-of select="atom:link[@rel='self']/@href" /> javascript:location.href='https://www.subtome.com/#/subscribe?feeds='+location.href;
</xsl:attribute> </xsl:attribute>
<xsl:attribute name="onclick"> <xsl:attribute name='onclick'>
( (
function(btn){ function(btn){
var z=document.createElement('script'); var z=document.createElement('script');
@ -428,4 +432,70 @@ xmlns:atom='http://www.w3.org/2005/Atom'>
</body> </body>
</html> </html>
</xsl:template> </xsl:template>
<!-- extract filename from given url string -->
<!-- extract filename from given url string -->
<xsl:template name='extract-filename'>
<xsl:param name='url'/>
<xsl:choose>
<xsl:when test='contains($url,"/")'>
<xsl:call-template name='extract-filename'>
<xsl:with-param name='url' select='substring-after($url,"/")'/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select='$url'/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- set page metadata -->
<!-- set page metadata -->
<xsl:template name='metadata'>
<xsl:param name='name'/>
<xsl:param name='content'/>
<xsl:if test='$content and not($content="")'>
<xsl:element name='meta'>
<xsl:attribute name='name'>
<xsl:value-of select='$name'/>
</xsl:attribute>
<xsl:attribute name='content'>
<xsl:value-of select='$content'/>
</xsl:attribute>
</xsl:element>
</xsl:if>
</xsl:template>
<!-- transform filesize from given length string -->
<!-- transform filesize from given length string -->
<xsl:template name='transform-filesize'>
<xsl:param name='length'/>
<!-- TODO consider xsl:decimal-format and xsl:number -->
<xsl:choose>
<!-- TODO consider removal of Byte -->
<xsl:when test='$length &lt; 2'>
<xsl:value-of select='$length'/>
Byte
</xsl:when>
<xsl:when test='floor($length div 1024) &lt; 1'>
<xsl:value-of select='$length'/>
Bytes
</xsl:when>
<xsl:when test='floor($length div (1024 * 1024)) &lt; 1'>
<xsl:value-of select='floor($length div 1024)'/>.<xsl:value-of select='substring($length mod 1024,0,2)'/>
KiB
</xsl:when>
<xsl:when test='floor($length div (1024 * 1024 * 1024)) &lt; 1'>
<xsl:value-of select='floor($length div (1024 * 1024))'/>.<xsl:value-of select='substring($length mod (1024 * 1024),0,2)'/>
MiB
</xsl:when>
<xsl:otherwise>
<!-- P2P links -->
<xsl:value-of select='floor($length div (1024 * 1024 * 1024))'/>.<xsl:value-of select='substring($length mod (1024 * 1024 * 1024),0,2)'/>
GiB
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet> </xsl:stylesheet>

116
rivista/config.py Normal file
View file

@ -0,0 +1,116 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Functions get_directory() were taken from project jarun/buku.
By Arun Prakash Jana (jarun) and Dmitry Marakasov (AMDmi3).
"""
import os
import sys
try:
import tomllib
except:
import tomli as tomllib
class Settings:
def get_directory():
"""
Determine the directory path where setting files be stored.
* If $XDG_CONFIG_HOME is defined, use it;
* else if $HOME exists, use it;
* else if the platform is Windows, use %APPDATA%;
* else use the current directory.
Returns
-------
str
Path to configuration directory.
"""
# config_home = xdg.BaseDirectory.xdg_config_home
config_home = os.environ.get('XDG_CONFIG_HOME')
if config_home is None:
if os.environ.get('HOME') is None:
if sys.platform == 'win32':
config_home = os.environ.get('APPDATA')
if config_home is None:
return os.path.abspath('.')
else:
return os.path.abspath('.')
else:
config_home = os.path.join(
os.environ.get('HOME'), '.config'
)
return os.path.join(config_home, 'rivista')
def get_setting(filename, section):
with open(filename, mode="rb") as settings:
result = tomllib.load(settings)[section]
return result
class Data:
def get_directory():
"""
Determine the directory path where data files be stored.
* If $XDG_DATA_HOME is defined, use it;
* else if $HOME exists, use it;
* else if the platform is Windows, use %APPDATA%;
* else use the current directory.
Returns
-------
str
Path to database file.
"""
# data_home = xdg.BaseDirectory.xdg_data_home
data_home = os.environ.get('XDG_DATA_HOME')
if data_home is None:
if os.environ.get('HOME') is None:
if sys.platform == 'win32':
data_home = os.environ.get('APPDATA')
if data_home is None:
return os.path.abspath('.rivista/data')
else:
return os.path.abspath('.rivista/data')
else:
data_home = os.path.join(
os.environ.get('HOME'), '.local', 'share'
)
return os.path.join(data_home, 'rivista')
class Cache:
def get_directory():
"""
Determine the directory path where cache files be stored.
* If $XDG_CACHE_HOME is defined, use it;
* else if $HOME exists, use it;
* else if the platform is Windows, use %APPDATA%;
* else use the current directory.
Returns
-------
str
Path to cache directory.
"""
# cache_home = xdg.BaseDirectory.xdg_cache_home
cache_home = os.environ.get('XDG_CACHE_HOME')
if cache_home is None:
if os.environ.get('HOME') is None:
if sys.platform == 'win32':
cache_home = os.environ.get('APPDATA')
if cache_home is None:
return os.path.abspath('.rivista/cache')
else:
return os.path.abspath('.rivista/cache')
else:
cache_home = os.path.join(
os.environ.get('HOME'), '.cache'
)
return os.path.join(cache_home, 'rivista')

View file

@ -0,0 +1,15 @@
# An account to connect XMPP Journal Publisher to the XMPP network.
[account]
xmpp = "" # Jabber ID.
pass = "" # Password.
# A default node, when no arguments are set.
[default]
pubsub = "" # Jabber ID.
nodeid = "" # Node ID.
# Settings
[settings]
service = "" # Enable server as a service.
include = "" # Limit service to a given domain.
operator = "" # A Jabber ID to contact with, in case of an error.

4
rivista/gmi/index.py Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
class GmiIndex:

52
rivista/gmi/markdown.py Normal file
View file

@ -0,0 +1,52 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
class GmiMarkdown:
def convert_to_gmi(markdown_text):
lines = markdown_text.splitlines()
footnotes = []
footnote_counter = 1
output_lines = []
for line in lines:
# Process links in the format [text](url)
while True:
start_bracket = line.find('[')
if start_bracket == -1:
break # No more links in this line
end_bracket = line.find(']', start_bracket)
if end_bracket == -1:
break # Malformed link; exit loop
start_parenthesis = line.find('(', end_bracket)
if start_parenthesis == -1:
break # Malformed link; exit loop
end_parenthesis = line.find(')', start_parenthesis)
if end_parenthesis == -1:
break # Malformed link; exit loop
link_text = line[start_bracket + 1:end_bracket]
url = line[start_parenthesis + 1:end_parenthesis]
# Add footnote
footnotes.append(f" [{footnote_counter}]: {link_text}{url}")
footnote_marker = f"{link_text}[{footnote_counter}]"
footnote_counter += 1
# Replace link with footnote marker
line = line[:start_bracket] + footnote_marker + line[end_parenthesis + 1:]
# Remove Markdown header markers
if line.startswith('#'):
line = line.lstrip('# ').strip()
output_lines.append(line.strip())
# Combine output lines and footnotes
output_text = "\n".join(output_lines).strip()
footnotes_text = "\n".join(footnotes)
return f"{output_text}\n\n{footnotes_text}" if footnotes else output_text

79
rivista/gmi/post.py Normal file
View file

@ -0,0 +1,79 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#from markdown_text_clean import clean_text
#from md2gemini import md2gemini
#from rivista.gmi.markdown import GmiMarkdown
from rivista.html.gmi import HtmlGmi
from rivista.markdown.html import MarkdownHtml
class GmiPost:
def generate_gmi(atom: dict):
"""Generate an Gemini document."""
atom_title = atom['title']
atom_subtitle = atom['subtitle']
atom_header = f'# {atom_title}\n### {atom_subtitle}\n\n'
atom_items = []
for item in atom['items']:
item_title = item['title']
item_published = item['published']
item_updated = item['updated']
item_contents = ''
for content in item['contents']:
match content['type']:
case 'text':
content_html = MarkdownHtml.convert_to_html(content['text'])
content_text = HtmlGmi.convert_to_gmi(content_html)
#content_text = GmiMarkdown.convert_to_gmi(content['text'])
#content_text = md2gemini(content['text'], links="at-end")
#content_text = clean_text(content['text'])
item_contents += f'\n{content_text}\n'
case _ if content['type'] in ('html', 'xhtml'):
content_text = HtmlGmi.convert_to_gmi(content['text'])
item_contents += f'\n{content_text}\n'
case _:
content_text = content['text']
item_contents += f'\n```\n{content_text}\n```\n'
links = item['links'] if 'links' in item else None
item_links = ''
if links:
item_links = '\n### Related resources\n\n'
for link in links:
link_href = link['href']
link_rel = link['rel']
link_type = link['type']
if link_type:
item_links += f'=> {link_href} {link_rel} ({link_type})\n'
else:
item_links += f'=> {link_href} {link_rel}\n'
categories = item['categories'] if 'categories' in item else None
item_categories = ''
if categories:
item_categories = '\n### Categories\n\n'
for category in categories:
item_categories += f'{category}, '
item_categories = item_categories[0:len(item_categories)-2] + '.\n'
authors = item['authors'] if 'authors' in item else None
item_authors = ''
if authors:
item_authors = '\n### Authors\n\n'
for author in authors:
author_text = author['name'] or author['uri'] or author['email']
if 'email' in author and author['email']:
item_author_email = 'mailto:' + author['email']
item_authors += f'=> {author_text} {item_author_email}\n'
elif 'uri' in author and author['uri']:
item_author_uri = author['uri']
item_authors += f'=> {author_text} {item_author_uri}\n'
else:
item_authors += f'{author_text}\n'
atom_items. append(f'\n## {item_title}\n\n' +
f'Published: {item_published}\n' +
f'Updated: {item_updated}\n' +
item_contents +
item_links +
item_categories +
item_authors)
gmi_text = atom_header + '\n'.join(atom_items)
return gmi_text

51
rivista/html/gmi.py Normal file
View file

@ -0,0 +1,51 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
class HtmlGmi:
def convert_to_gmi(html_content):
soup = BeautifulSoup(html_content, 'html.parser')
footnotes = ['### References\n']
footnote_counter = 1
# Extract text and links
for a in soup.find_all('a'):
link_text = a.get_text()
url = a['href']
if not url.startswith('#') and not url.startswith('/'):
url_modified = url.replace(' ', '%20')
url_modified = url_modified.replace('(', '%28')
url_modified = url_modified.replace(')', '%29')
#footnotes.append(f"=> {url_modified} [{footnote_counter}]: {link_text}")
footnotes.append(f"=> {url_modified} [{footnote_counter}]: {url}")
footnote_marker = f"{link_text}[{footnote_counter}]"
a.replace_with(footnote_marker)
footnote_counter += 1
# Handle <code> tags
for code in soup.find_all('code'):
if code.string:
original_text = code.get_text()
modified_text = '`' + original_text + '`'
code.string.replace_with(modified_text)
# Handle <pre> tags
for pre in soup.find_all('pre'):
pre.insert_before('\n```\n') # Add Markdown code block start
pre.insert_after('\n```\n') # Add Markdown code block end
# Convert <ul> and <li> to Markdown bullet points
for ul in soup.find_all('ul'):
for li in ul.find_all('li'):
if li.string:
original_text = li.get_text()
modified_text = '- ' + original_text
li.string.replace_with(modified_text)
# Get the text without HTML tags
text = soup.get_text().strip().replace('#', '')
# Combine text with footnotes
return f"{text}\n\n" + "\n".join(footnotes) if footnotes else text

215
rivista/http/instance.py Normal file
View file

@ -0,0 +1,215 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from fastapi import FastAPI, Request, Response
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import os
from rivista.config import Cache, Data, Settings
from rivista.gmi.post import GmiPost
from rivista.json.index import JsonIndex
from rivista.xml.atom import XmlAtom
from rivista.xml.opml import XmlOpml
from rivista.xml.xhtml import XmlXhtml
from rivista.xml.xslt import XmlXslt
from rivista.xmpp.instance import XmppInstance
from rivista.xmpp.utilities import XmppUtilities
from rivista.xmpp.xep_0060 import XmppXep0060
class HttpInstance:
def __init__(self):
directory_settings = Settings.get_directory()
filename_settings = os.path.join(directory_settings, 'settings.toml')
credentials = Settings.get_setting(filename_settings, 'account')
settings = Settings.get_setting(filename_settings, 'settings')
default = Settings.get_setting(filename_settings, 'default')
operator = Settings.get_setting(filename_settings, 'settings')['operator']
directory_data = Data.get_directory()
directory_data_css = os.path.join(directory_data, 'css')
directory_data_graphic = os.path.join(directory_data, 'graphic')
directory_data_script = os.path.join(directory_data, 'script')
directory_data_xsl = os.path.join(directory_data, 'xsl')
filename_favicon = os.path.join(directory_data, 'img', 'favicon.ico')
directory_cache = Cache.get_directory()
directory_cache_json = os.path.join(directory_cache, 'json')
self.app = FastAPI()
# Mount static graphic, script and stylesheet directories
self.app.mount("/css", StaticFiles(directory=directory_data_css), name="css")
self.app.mount("/data", StaticFiles(directory=directory_cache_json), name="data")
self.app.mount("/graphic", StaticFiles(directory=directory_data_graphic), name="graphic")
self.app.mount("/script", StaticFiles(directory=directory_data_script), name="script")
self.app.mount("/xsl", StaticFiles(directory=directory_data_xsl), name="xsl")
@self.app.get('/favicon.ico', include_in_schema=False)
async def favicon():
return FileResponse(filename_favicon)
@self.app.route('/')
@self.app.get('/opml')
async def view_pubsub_nodes(request: Request):
xmpp = XmppInstance(credentials['xmpp'], credentials['pass'])
# xmpp.connect()
pubsub = request.query_params.get('pubsub', '')
result = None
if settings['service']:
if not settings['include'] or settings['include'] in pubsub:
if pubsub:
iq = await XmppXep0060.get_nodes(xmpp, pubsub)
if iq:
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
xml_opml = XmlOpml.generate_opml(iq)
result = XmlXslt.append_stylesheet(xml_opml, 'opml')
else:
text = 'Please ensure that PubSub "{}" (Jabber ID) is valid and accessible.'.format(pubsub)
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
else:
text = 'The given domain {} is not allowed.'.format(pubsub)
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
if not result:
if default['pubsub']:
if not pubsub:
pubsub = default['pubsub']
iq = await XmppXep0060.get_nodes(xmpp, pubsub)
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
xml_opml = XmlOpml.generate_opml(iq)
result = XmlXslt.append_stylesheet(xml_opml, 'opml')
elif not settings['service']:
pubsub = default['pubsub']
link = 'xmpp:{pubsub}'.format(pubsub=pubsub)
xml_opml = XmlOpml.generate_opml(iq)
result = XmlXslt.append_stylesheet(xml_opml, 'opml')
else:
text = 'Please contact the administrator and ask him to set default PubSub and Node ID.'
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
xmpp.disconnect()
response = Response(content=result, media_type="application/xml")
return response
@self.app.get('/atom')
async def view_node_items(request: Request):
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', '')
result = None
if settings['service']:
if not settings['include'] or settings['include'] in pubsub:
if pubsub and node and item_id:
iq = await XmppXep0060.get_node_item(xmpp, pubsub, node, item_id)
if iq:
link = XmppUtilities.form_an_item_link(pubsub, node, item_id)
if 'urn:xmpp:microblog:0:comments/' in node:
atom = XmlAtom.extract_atom(iq)
xml_atom = XmlAtom.generate_atom_comment(atom, pubsub, node, link)
else:
atom = XmlAtom.extract_atom(iq)
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
gmi_text = GmiPost.generate_gmi(atom)
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
if not '/' in node:
if iq:
JsonIndex.generate_json(iq, directory_cache_json)
else:
json_data = [{'title' : 'Error retrieving node items.',
'link' : ('javascript:alert("Rivista 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=Rivista;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 = os.path.join(directory_cache_json, 'json', f'{node}.json')
with open(filename, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)
else:
text = 'Please ensure that PubSub node "{}" and item "{}" are valid and accessible.'.format(node, item_id)
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
# try:
# iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
# JsonIndex.generate_json(iq, node)
# except:
# 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 XmppXep0060.get_node_items(xmpp, pubsub, node)
if iq:
link = XmppUtilities.form_a_node_link(pubsub, node)
if 'urn:xmpp:microblog:0:comments/' in node:
atom = XmlAtom.extract_atom(iq)
xml_atom = XmlAtom.generate_atom_comment(atom, pubsub, node, link)
else:
atom = XmlAtom.extract_atom(iq)
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
gmi_text = GmiPost.generate_gmi(atom)
else:
text = 'Please ensure that PubSub node "{}" is valid and accessible.'.format(node)
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
elif pubsub:
text = 'Node parameter is missing.'
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
elif node:
text = 'PubSub parameter is missing.'
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
# else:
# text = ('Mandatory parameter PubSub and '
# 'optional parameter Node are missing.')
# xml_atom = XmlAtom.error_message(text)
# result = XmlXslt.append_stylesheet(xml_atom, 'atom')
else:
text = 'The given domain {} is not allowed.'.format(pubsub)
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
if not result:
if default['pubsub'] and default['nodeid']:
if not pubsub and not node:
pubsub = default['pubsub']
node = default['nodeid']
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
if iq:
link = XmppUtilities.form_a_node_link(pubsub, node)
atom = XmlAtom.extract_atom(iq)
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
gmi_text = GmiPost.generate_gmi(atom)
else:
text = 'Please ensure that PubSub node "{}" is valid and accessible.'.format(node)
xml_atom = XmlAtom.error_message(text)
elif not settings['service']:
pubsub = default['pubsub']
node = default['nodeid']
iq = await XmppXep0060.get_node_items(xmpp, pubsub, node)
if iq:
link = XmppUtilities.form_a_node_link(pubsub, node)
atom = XmlAtom.extract_atom(iq)
xml_atom = XmlAtom.generate_atom_post(atom, pubsub, node, link)
gmi_text = GmiPost.generate_gmi(atom)
else:
text = 'Please ensure that PubSub node "{}" is valid and accessible.'.format(node)
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
else:
text = 'Please contact the administrator and ask him to set default PubSub and Node ID.'
xml_atom = XmlAtom.error_message(text)
result = XmlXslt.append_stylesheet(xml_atom, 'atom')
xmpp.disconnect()
response = Response(content=result, media_type="application/xml")
return response

38
rivista/json/index.py Normal file
View file

@ -0,0 +1,38 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import json
import os
from rivista.xmpp.utilities import XmppUtilities
class JsonIndex:
def generate_json(iq, directory_cache_json):
"""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_id = entry['id']
item_payload = entry['payload']
namespace = '{http://www.w3.org/2005/Atom}'
title = item_payload.find(namespace + 'title')
title_text = '*** No Title ***' 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_href = XmppUtilities.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
directory = os.path.join(directory_cache_json, pubsub)
if not os.path.exists(directory):
os.mkdir(directory)
filename = os.path.join(directory_cache_json, pubsub, f'{node}.json')
with open(filename, 'w', encoding='utf-8') as f:
json.dump(json_data, f, ensure_ascii=False, indent=4)

11
rivista/markdown/html.py Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import markdown
class MarkdownHtml:
def convert_to_html(markdown_content):
# Convert Markdown to HTML
html_content = markdown.markdown(markdown_content)
return html_content

View file

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 599 KiB

View file

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View file

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

22
rivista/utilities.py Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import tomli_w
try:
import tomllib
except:
import tomli as tomllib
class Toml:
def open_file_toml(filename: str) -> dict:
with open(filename, mode="rb") as fn:
data = tomllib.load(fn)
return data
def save_to_toml(filename: str, data: dict) -> None:
with open(filename, 'w') as fn:
data_as_string = tomli_w.dumps(data)
fn.write(data_as_string)

2
rivista/version.py Normal file
View file

@ -0,0 +1,2 @@
__version__ = '0.1'
__version_info__ = (0, 1)

251
rivista/xml/atom.py Normal file
View file

@ -0,0 +1,251 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import datetime
from rivista.xmpp.utilities import XmppUtilities
from slixmpp.stanza.iq import Iq
import xml.etree.ElementTree as ET
class XmlAtom:
def error_message(text):
"""Error message in RFC 4287: The Atom Syndication Format."""
title = 'Rivista'
subtitle = 'XMPP Journal Publisher'
description = ('This is a syndication feed generated with Rivista, an 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':'Rivista','email':'rivista@schimon.i2p'})
ET.SubElement(feed, 'generator', {
'uri': 'https://git.xmpp-it.net/sch/Rivista',
'version': '0.1'}).text = 'Rivista 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 = 'rivista-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 extract_atom(iq: Iq):
"""Extract data from an Atom Syndication Format (RFC 4287) of a Publish-Subscribe (XEP-0060) node item."""
jid = iq['from'].bare
node = iq['pubsub']['items']['node']
atom = {}
atom['title'] = jid
atom['subtitle'] = node
atom['language'] = iq['pubsub']['items']['lang']
atom['items'] = []
items = iq['pubsub']['items']
for item in list(items)[::-1]:
atom_item = {}
item_payload = item['payload']
namespace = '{http://www.w3.org/2005/Atom}'
title = item_payload.find(namespace + 'title')
links = item_payload.find(namespace + 'link')
if (not isinstance(title, ET.Element) and
not isinstance(links, ET.Element)): continue
title_text = 'No title' if title == None else title.text
atom_item['title'] = title_text
if isinstance(links, ET.Element):
atom_item['links'] = []
for link in item_payload.findall(namespace + 'link'):
link_href = link.attrib['href'] if 'href' in link.attrib else ''
link_type = link.attrib['type'] if 'type' in link.attrib else ''
link_rel = link.attrib['rel'] if 'rel' in link.attrib else ''
atom_item['links'].append({'href': link_href,
'rel': link_rel,
'type': link_type})
contents = item_payload.find(namespace + 'content')
atom_item['contents'] = []
if isinstance(contents, ET.Element):
for content in item_payload.findall(namespace + 'content'):
if not content.text: continue
content_text = content.text
content_type = content.attrib['type'] if 'type' in content.attrib else 'html'
content_type_text = 'html' if 'html' in content_type else 'text'
atom_item['contents'].append({'text' : content_text,
'type' : content_type_text})
else:
summary = item_payload.find(namespace + 'summary')
summary_text = summary.text if summary else None
if summary_text:
summary_type = summary.attrib['type'] if 'type' in summary.attrib else 'html'
summary_type_text = 'html' if 'html' in summary_type else 'text'
atom_item['contents'].append(summary_text)
# else:
# atom_item['contents'].append('No content.')
published = item_payload.find(namespace + 'published')
published_text = '' if published == None else published.text
atom_item['published'] = published_text
updated = item_payload.find(namespace + 'updated')
updated_text = '' if updated == None else updated.text
atom_item['updated'] = updated_text
atom_item['authors'] = []
authors = item_payload.find(namespace + 'author')
if isinstance(authors, ET.Element):
for author in item_payload.findall(namespace + 'author'):
atom_item_author = {}
author_email = author.find(namespace + 'email')
if author_email is not None:
author_email_text = author_email.text
if author_email_text:
atom_item_author['email'] = author_email_text
else:
author_email_text = None
author_uri = author.find(namespace + 'uri')
if author_uri is not None:
author_uri_text = author_uri.text
if author_uri_text:
atom_item_author['uri'] = author_uri_text
else:
author_uri_text = None
author_name = author.find(namespace + 'name')
if author_name is not None and author_name.text:
author_name_text = author_name.text
else:
author_name_text = author_uri_text or author_email_text
atom_item_author['name'] = author_name_text
atom_item['authors'].append(atom_item_author)
categories = item_payload.find(namespace + 'category')
atom_item['categories'] = []
if isinstance(categories, ET.Element):
for category in item_payload.findall(namespace + 'category'):
if 'term' in category.attrib and category.attrib['term']:
category_term = category.attrib['term']
atom_item['categories'].append(category_term)
identifier = item_payload.find(namespace + 'id')
if identifier is not None and identifier.attrib: print(identifier.attrib)
identifier_text = item['id'] if identifier == None else identifier.text
atom_item['id'] = identifier_text
#atom_item['id'] = item['id']
atom['items'].append(atom_item)
return atom
# generate_rfc_4287
def generate_atom_post(atom: dict, pubsub: str, node: str, link: str):
"""Generate an Atom Syndication Format (RFC 4287) from a Publish-Subscribe (XEP-0060) node items."""
# link = XmppUtilities.form_a_node_link(pubsub, node)
# subtitle = 'XMPP PubSub Syndication Feed'
description = ('This is a syndication feed generated with Rivista, an XMPP '
'Journal Publisher, which conveys XEP-0060: Publish-'
'Subscribe nodes to standard RFC 4287: The Atom Syndication '
'Format.')
e_feed = ET.Element("feed")
e_feed.set('xmlns', 'http://www.w3.org/2005/Atom')
ET.SubElement(e_feed, 'title', {'type': 'text'}).text = atom['title']
ET.SubElement(e_feed, 'subtitle', {'type': 'text'}).text = atom['subtitle']
ET.SubElement(e_feed, 'link', {'rel': 'self', 'href': link})
ET.SubElement(e_feed, 'generator', {
'uri': 'https://git.xmpp-it.net/sch/Rivista',
'version': '0.1'}).text = 'Rivista XJP'
ET.SubElement(e_feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat()
for item in atom['items']:
e_entry = ET.SubElement(e_feed, 'entry')
ET.SubElement(e_entry, 'title').text = item['title']
links = item['links'] if 'links' in item else None
if links:
for link in links:
ET.SubElement(e_entry, 'link', {'href': link['href'],
'rel': link['rel'],
'type': link['type']})
else:
# NOTE What does this instruction line do?
ET.SubElement(e_entry, 'content', {'href': ''})
link_xmpp = XmppUtilities.form_an_item_link(pubsub, node, item['id'])
ET.SubElement(e_entry, 'link', {'href': link_xmpp,
'rel': 'alternate',
'type': 'x-scheme-handler/xmpp'})
contents = item['contents'] if 'contents' in item else None
if contents:
for content in contents:
ET.SubElement(e_entry, 'content', {'type': content['type']}).text = content['text']
else:
ET.SubElement(e_entry, 'content').text = 'No content.'
ET.SubElement(e_entry, 'published').text = item['published']
ET.SubElement(e_entry, 'updated').text = item['updated']
authors = item['authors'] if 'authors' in item else None
if authors:
for author in authors:
e_author = ET.SubElement(e_entry, 'author')
if 'email' in author and author['email']:
ET.SubElement(e_author, 'email').text = author['email']
if 'uri' in author and author['uri']:
ET.SubElement(e_entry, 'uri').text = author['uri']
ET.SubElement(e_author, 'uri').text = author['uri']
ET.SubElement(e_author, 'name').text = author['name'] or author['uri'] or author['email']
categories = item['categories']
if categories:
for category in categories:
ET.SubElement(e_entry, 'category', {'term': category})
ET.SubElement(e_entry, 'id').text = item['id']
return ET.tostring(e_feed, encoding='unicode')
# generate_rfc_4287
def generate_atom_comment(atom: dict, pubsub: str, node: str, link: str):
"""Generate an Atom Syndication Format (RFC 4287) from a Publish-Subscribe (XEP-0060) node items."""
# link = XmppUtilities.form_a_node_link(pubsub, node)
# subtitle = 'XMPP PubSub Syndication Feed'
description = ('This is a syndication feed generated with Rivista, an XMPP '
'Journal Publisher, which conveys XEP-0060: Publish-'
'Subscribe nodes to standard RFC 4287: The Atom Syndication '
'Format.')
e_feed = ET.Element("feed")
e_feed.set('xmlns', 'http://www.w3.org/2005/Atom')
ET.SubElement(e_feed, 'title', {'type': 'text'}).text = atom['title']
ET.SubElement(e_feed, 'subtitle', {'type': 'text'}).text = atom['subtitle']
ET.SubElement(e_feed, 'link', {'rel': 'self', 'href': link})
ET.SubElement(e_feed, 'generator', {
'uri': 'https://git.xmpp-it.net/sch/Rivista',
'version': '0.1'}).text = 'Rivista XJP'
ET.SubElement(e_feed, 'updated').text = datetime.datetime.now(datetime.UTC).isoformat()
for item in atom['items']:
e_entry = ET.SubElement(e_feed, 'entry')
ET.SubElement(e_entry, 'title').text = item['authors'][0]['name']
links = item['links'] if 'links' in item else None
if links:
for link in links:
ET.SubElement(e_entry, 'link', {'href': link['href'],
'rel': link['rel'],
'type': link['type']})
else:
# NOTE What does this instruction line do?
ET.SubElement(e_entry, 'content', {'href': ''})
link_xmpp = XmppUtilities.form_an_item_link(pubsub, node, item['id'])
ET.SubElement(e_entry, 'link', {'href': link_xmpp,
'rel': 'alternate',
'type': 'x-scheme-handler/xmpp'})
contents = item['contents'] if 'contents' in item else None
if contents:
for content in contents:
ET.SubElement(e_entry, 'content', {'type': content['type']}).text = content['text']
else:
ET.SubElement(e_entry, 'content').text = 'No content.'
ET.SubElement(e_entry, 'published').text = item['published']
ET.SubElement(e_entry, 'updated').text = item['updated']
authors = item['authors'] if 'authors' in item else None
if authors:
for author in authors:
e_author = ET.SubElement(e_entry, 'author')
if 'email' in author and author['email']:
ET.SubElement(e_author, 'email').text = author['email']
if 'uri' in author and author['uri']:
ET.SubElement(e_entry, 'uri').text = author['uri']
ET.SubElement(e_author, 'uri').text = author['uri']
ET.SubElement(e_author, 'name').text = author['name'] or author['uri'] or author['email']
categories = item['categories']
if categories:
for category in categories:
ET.SubElement(e_entry, 'category', {'term': category})
ET.SubElement(e_entry, 'id').text = item['id']
return ET.tostring(e_feed, encoding='unicode')

32
rivista/xml/opml.py Normal file
View file

@ -0,0 +1,32 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import datetime
from rivista.xmpp.utilities import XmppUtilities
import xml.etree.ElementTree as ET
class XmlOpml:
def generate_opml(iq):
"""Generate an OPML Collection document."""
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 = 'An OPML of ' + pubsub
ET.SubElement(head, "description").text = (
"PubSub Nodes of {}").format(pubsub)
ET.SubElement(head, "generator").text = 'Rivista'
ET.SubElement(head, "urlPublic").text = 'https://git.xmpp-it.net/sch/Rivista'
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 = XmppUtilities.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')

62
rivista/xml/xhtml.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import datetime
import xml.etree.ElementTree as ET
class XmlXhtml:
def generate_xhtml(atom: dict):
"""Generate an XHTML document."""
e_html = ET.Element('html')
e_html.set('xmlns', 'http://www.w3.org/1999/xhtml')
e_head = ET.SubElement(e_html, 'head')
ET.SubElement(e_head, 'title').text = atom['title']
ET.SubElement(e_head, 'link', {'rel': 'stylesheet',
'href': 'pubsub.css'})
e_body = ET.SubElement(e_html, "body")
ET.SubElement(e_body, "h1").text = atom['title']
ET.SubElement(e_body, "h2").text = atom['subtitle']
for item in atom['items']:
item_id = item['id']
title = item['title']
links = item['links']
e_article = ET.SubElement(e_body, 'article')
e_title = ET.SubElement(e_article, 'h3')
e_title.text = item['title']
e_title.set('id', item['id'])
e_date = ET.SubElement(e_article, 'h4')
e_date.text = item['published']
e_date.set('title', 'Updated: ' + item['updated'])
authors = item['authors']
if authors:
e_authors = ET.SubElement(e_article, "dl")
ET.SubElement(e_authors, "dt").text = 'Authors'
for author in authors:
e_dd = ET.SubElement(e_authors, 'dd')
e_author = ET.SubElement(e_dd, 'a')
e_author.text = author['name'] or author['uri'] or author['email']
if 'email' in author and author['email']:
e_author.set('href', 'mailto:' + author['email'])
elif 'uri' in author and author['uri']:
e_author.set('href', author['uri'])
for content in item['contents']:
ET.SubElement(e_article, 'p', {'type': content['type']}).text = content['text']
if links:
e_links = ET.SubElement(e_article, "dl")
e_links.set('class', 'links')
ET.SubElement(e_links, "dt").text = 'Links'
for link in links:
e_dd = ET.SubElement(e_links, 'dd')
e_link = ET.SubElement(e_dd, 'a')
e_link.set('href', link['href'])
e_link.text = link['rel']
if link['type']: ET.SubElement(e_dd, 'span').text = link['type']
categories = item['categories']
if categories:
e_categories = ET.SubElement(e_article, "dl")
e_categories.set('class', 'categories')
ET.SubElement(e_categories, "dt").text = 'Categories'
for category in categories:
ET.SubElement(e_categories, 'dd').text = category
return ET.tostring(e_html, encoding='unicode')

22
rivista/xml/xslt.py Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
class XmlXslt:
"""This is a patch function to append XSLT reference to XML."""
"""Why is there no built-in function of ElementTree or LXML?"""
def append_stylesheet(xml_data, type):
# Register namespace in order to avoide ns0:
if type == 'atom': ET.register_namespace('', 'http://www.w3.org/2005/Atom')
# Load XML from string
tree = ET.fromstring(xml_data)
# The following direction removes the XML declaration
xml_data_without_a_declaration = ET.tostring(tree, encoding='unicode')
# Add XML declaration and stylesheet
xml_data_declaration = (
'<?xml version="1.0" encoding="utf-8"?>'
'<?xml-stylesheet type="text/xsl" href="xsl/{}.xsl"?>'.format(type) +
xml_data_without_a_declaration)
return xml_data_declaration

11
rivista/xmpp/instance.py Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from slixmpp import ClientXMPP
class XmppInstance(ClientXMPP):
def __init__(self, jid, password):
super().__init__(jid, password)
self.register_plugin('xep_0060')
self.connect()
# self.process(forever=False)

14
rivista/xmpp/utilities.py Normal file
View file

@ -0,0 +1,14 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
class XmppUtilities:
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

27
rivista/xmpp/xep_0060.py Normal file
View file

@ -0,0 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from slixmpp.exceptions import IqError, IqTimeout
class XmppXep0060:
async def get_node_item(self, pubsub, node, item_id):
try:
iq = await self.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(self, pubsub, node):
try:
iq = await self.plugin['xep_0060'].get_items(pubsub, node, timeout=5)
return iq
except (IqError, IqTimeout) as e:
print(e)
async def get_nodes(self, pubsub):
try:
iq = await self.plugin['xep_0060'].get_nodes(pubsub, timeout=5)
return iq
except (IqError, IqTimeout) as e:
print(e)

View file

@ -1,92 +0,0 @@
window.onload = function(){
// 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);
element.textContent = timeStamp.toUTCString();
}
// Parse Markdown/
for (let element of document.querySelectorAll('#articles > ul > li > div > p')) {
let markDown = element.textContent
element.innerHTML = marked.parse(markDown);
}
// Display a selection of suggested software.
const selection = {
'akregator' : {
'name' : 'Akregator',
'image' : 'akregator.svg',
'url' : 'https://apps.kde.org/akregator/'
},
'leechcraft' : {
'name' : 'LeechCraft',
'image' : 'leechcraft.png',
'url' : 'https://leechcraft.org/'
},
'liferea' : {
'name' : 'Liferea',
'image' : 'liferea.svg',
'url' : 'https://lzone.de/liferea/'
},
'raven' : {
'name' : 'Raven Reader',
'image' : 'raven.svg',
'url' : 'https://ravenreader.app/'
},
'rssguard' : {
'name' : 'RSS Guard',
'image' : 'rssguard.png',
'url' : 'https://github.com/martinrotter/rssguard'
},
'rssowl' : {
'name' : 'RSSOwl',
'image' : 'rssowl.svg',
'url' : 'http://www.rssowl.org/'
},
'tickr' : {
'name' : 'TICKR',
'image' : 'tickr.png',
'url' : 'https://www.open-tickr.net/'
}
}
let selectionLink = document.querySelector('#selection-link');
selectionLink.addEventListener ('click', function() {
let elementDiv = document.createElement('div');
elementDiv.id = 'selection-page';
let elementH1 = document.createElement('h1');
elementH1.textContent = 'Get A News Reader';
elementDiv.appendChild(elementH1);
let elementH2 = document.createElement('h2');
elementH2.textContent = 'Install Feed Reader Apps For Desktop And Mobile';
elementDiv.appendChild(elementH2);
let elementH3 = document.createElement('h3');
elementH3.textContent = '' +
'This is a selection of desktop applications, mobile apps and online ' +
'services for you to choose from. This selection includes news ' +
'readers, podcast managers, torrent clients, chat bots, HTML browsers ' +
'and plugins which support syndication feeds.';
elementDiv.appendChild(elementH3);
const brands = Object.keys(selection);
let elementDivSel = document.createElement('div');
elementDivSel.id = 'selection';
for (let i = 0; i < brands.length; i++) {
let brand = brands[i];
elementSpan = document.createElement('span');
let elementA = document.createElement('a');
elementA.href = selection[brand].url;
elementA.textContent = selection[brand].name;
let elementImg = document.createElement('img');
elementImg.src = 'graphic/' + selection[brand].image;
elementSpan.appendChild(elementImg);
elementSpan.appendChild(elementA);
elementDivSel.appendChild(elementSpan);
elementDiv.appendChild(elementDivSel);
}
let elementDivReturn = document.createElement('div');
elementDivReturn.id = 'return';
elementDivReturn.textContent = 'Return To PubSub...';
elementDivReturn.addEventListener ('click', function() {
document.querySelector('#selection-page').remove();
});
elementDiv.appendChild(elementDivReturn);
document.body.appendChild(elementDiv);
});
}