Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
|
ec5550f3d2 | ||
|
b5e28c8d11 | ||
|
30b3426dc7 | ||
|
6baaa38cba | ||
|
519c277ea6 | ||
|
9e4d6f0d32 | ||
|
09d50b9636 | ||
|
524d4aff07 | ||
|
d03a76db23 | ||
|
799cd80ebe | ||
|
24dbadf7dc | ||
|
77ac4c0ed9 | ||
|
0ab40eedec | ||
|
ea255d84e0 | ||
|
c31278b576 | ||
|
21e3aa34aa |
109
README.md
|
@ -1,24 +1,35 @@
|
||||||
# Blasta - The agreeable and cordial civic bookmarking system.
|
# Blasta - The agreeable and cordial civic annotation system.
|
||||||
|
|
||||||
Blasta is a collaborative bookmarks manager for organizing online content. It
|
Blasta is a collaborative bookmarks manager for organizing online content.
|
||||||
allows you to add links to your personal collection of links, to categorize them
|
|
||||||
with keywords, and to share your collection not only among your own software,
|
|
||||||
devices and machines, but also with others.
|
|
||||||
|
|
||||||
What makes Blasta a collaborative system is its ability to display to you the
|
It allows you to add links to your personal collection of links, to categorize
|
||||||
links that other people have collected, as well as showing you who else has
|
them with keywords, and to share and synchronize your collection among your own
|
||||||
bookmarked a specific link. You can also view the links collected by others, and
|
software, devices, machines, and also with others.
|
||||||
subscribe to the links of people whose lists you deem to be interesting.
|
|
||||||
|
The ability of Blasta to display to you the links that other people have
|
||||||
|
collected and shared, as well as showing you who else has bookmarked a specific
|
||||||
|
link is what makes Blasta a collaborative system.
|
||||||
|
|
||||||
|
You can also view the links collected by others, and subscribe to the links of
|
||||||
|
people whose lists you deem to be interesting.
|
||||||
|
|
||||||
Blasta does not limit you to save links of certain types; you can save links of
|
Blasta does not limit you to save links of certain types; you can save links of
|
||||||
types adc, dweb, ed2k, feed, ftp, gemini, geo, gopher, http, ipfs, irc, magnet,
|
types adc, dweb, ed2k, feed, ftp, gemini, geo, gopher, http, ipfs, irc, magnet,
|
||||||
mailto, monero, mms, news, sip, udp, xmpp and any scheme and type that you
|
mailto, monero, mms, news, sip, udp, xmpp and any scheme and type that you
|
||||||
desire.
|
desire.
|
||||||
|
|
||||||
|
## Instances
|
||||||
|
|
||||||
|
* https://blasta.woodpeckersnest.eu
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
[<img alt="browse view" src="graphic/browse.png" width="200px"/>](screenshot/browse.png)
|
[<img alt="browse view" src="blasta/screenshot/browse.png" width="200px"/>](blasta/screenshot/browse.png)
|
||||||
[<img alt="tags view" src="graphic/tag.png" width="200px"/>](screenshot/tag.png)
|
[<img alt="tags view" src="blasta/screenshot/tag.png" width="200px"/>](blasta/screenshot/tag.png)
|
||||||
|
|
||||||
|
## Videos
|
||||||
|
|
||||||
|
* [Blasta - An XMPP PubSub Annotation Management System](https://video.xmpp-it.net/w/cfozoUeVLFbBFMCCSCJ1Dn) [06:38]
|
||||||
|
|
||||||
## Technicalities
|
## Technicalities
|
||||||
|
|
||||||
|
@ -26,8 +37,8 @@ Blasta is a federated bookmarking system which is based on XMPP and stores
|
||||||
bookmarks on your own XMPP account; to achieve this task, Blasta utilizes the
|
bookmarks on your own XMPP account; to achieve this task, Blasta utilizes the
|
||||||
following XMPP specifications:
|
following XMPP specifications:
|
||||||
|
|
||||||
- [XEP-0163: Personal Eventing Protocol](https://xmpp.org/extensions/xep-0163.html)
|
* [XEP-0163: Personal Eventing Protocol](https://xmpp.org/extensions/xep-0163.html)
|
||||||
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
|
* [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
|
||||||
|
|
||||||
Blasta operates as an XMPP client, and therefore, does not have a bookmarks
|
Blasta operates as an XMPP client, and therefore, does not have a bookmarks
|
||||||
system nor an account system, of its own.
|
system nor an account system, of its own.
|
||||||
|
@ -40,42 +51,76 @@ The connection to the Blasta system is made with XMPP accounts.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Private bookmarks;
|
* Private bookmarks;
|
||||||
- Public bookmarks;
|
* Public bookmarks;
|
||||||
- Read list;
|
* Read list;
|
||||||
- Search;
|
* Search;
|
||||||
- Syndication;
|
* Syndication;
|
||||||
- Tags.
|
* Tags.
|
||||||
|
|
||||||
## Future features
|
## Future features
|
||||||
|
|
||||||
- ActivityPub;
|
* ActivityPub;
|
||||||
- Federation;
|
* Federation;
|
||||||
- Filters;
|
* Filters;
|
||||||
- Pin;
|
* Pin;
|
||||||
- Publish-Subscribe;
|
* Publish-Subscribe;
|
||||||
- Report.
|
* Report.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* Python >= 3.5
|
* Python >= 3.5
|
||||||
* fastapi
|
* fastapi
|
||||||
* lxml
|
* lxml
|
||||||
|
* python-dateutil
|
||||||
|
* python-multipart
|
||||||
* slixmpp
|
* slixmpp
|
||||||
* tomllib (Python <= 3.10)
|
* tomllib (Python <= 3.10)
|
||||||
* uvicorn
|
* uvicorn
|
||||||
|
|
||||||
## Instructions
|
## Installation
|
||||||
|
|
||||||
Use the following commands to start Blasta.
|
It is possible to install Blasta using pip and pipx.
|
||||||
|
|
||||||
```shell
|
#### pip inside venv
|
||||||
$ git clone https://git.xmpp-it.net/sch/Blasta
|
|
||||||
$ cd Blasta/
|
```
|
||||||
$ python -m uvicorn blasta:app --reload
|
$ python3 -m venv .venv
|
||||||
|
$ source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
Open URL http://localhost:8000/ and connect with your Jabber ID.
|
##### Install
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pip install git+https://git.xmpp-it.net/sch/Blasta
|
||||||
|
```
|
||||||
|
|
||||||
|
#### pipx
|
||||||
|
|
||||||
|
##### Install
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pipx install git+https://git.xmpp-it.net/sch/Blasta
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Update
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pipx reinstall blasta
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pipx uninstall blasta
|
||||||
|
$ pipx install git+https://git.xmpp-it.net/sch/Blasta
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start
|
||||||
|
|
||||||
|
```
|
||||||
|
$ blasta
|
||||||
|
```
|
||||||
|
|
||||||
|
Open URL http://localhost:8000
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
3
blasta/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from blasta.version import __version__, __version_info__
|
||||||
|
|
||||||
|
print('Blasta', __version__)
|
95
blasta/__main__.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
* Delete cookie if session does not match
|
||||||
|
|
||||||
|
* Delete entry/tag/jid combination row upon removal of a tag.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from blasta.config import Cache, Settings, Share
|
||||||
|
from blasta.http.instance import HttpInstance
|
||||||
|
from blasta.database.sqlite import DatabaseSQLite
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from os.path import getsize, exists
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
import uvicorn
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
directory_config = Settings.get_directory()
|
||||||
|
sql_filename = os.path.join(directory_config, 'blasta.sql')
|
||||||
|
directory_data = Share.get_directory()
|
||||||
|
dbs_filename = os.path.join(directory_data, 'main.sqlite')
|
||||||
|
if not exists(dbs_filename) or not getsize(dbs_filename):
|
||||||
|
DatabaseSQLite.create_tables(sql_filename, dbs_filename)
|
||||||
|
accounts = {}
|
||||||
|
sessions = {}
|
||||||
|
http_instance = HttpInstance(accounts, sessions)
|
||||||
|
return http_instance.app
|
||||||
|
|
||||||
|
if __name__ == 'blasta.__main__':
|
||||||
|
|
||||||
|
directory = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Copy data files
|
||||||
|
directory_data = Share.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)
|
||||||
|
for subdirectory in ('data', 'export', 'items'):
|
||||||
|
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)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='blasta',
|
||||||
|
description='Blasta - A collaborative annotation system.',
|
||||||
|
usage='%(prog)s [OPTION]...')
|
||||||
|
parser.add_argument('-v', '--version', help='print version',
|
||||||
|
action='version', version='0.1')
|
||||||
|
parser.add_argument('-p', '--port', help='port number', dest='port')
|
||||||
|
parser.add_argument('-o', '--open', help='open an html browser', action='store_const', const=True, dest='open')
|
||||||
|
args = parser.parse_args()
|
||||||
|
port = int(args.port or 8000)
|
||||||
|
|
||||||
|
app = main()
|
||||||
|
uvicorn.run(app, host='localhost', port=port)
|
||||||
|
|
||||||
|
if args.open:
|
||||||
|
# TODO Check first time
|
||||||
|
webbrowser.open('http://localhost:{}/help/about'.format(port))
|
||||||
|
webbrowser.open_new_tab('http://localhost:{}'.format(port))
|
||||||
|
|
BIN
blasta/assets/graphic/blasta.ico
Normal file
After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
19
blasta/assets/graphic/syndicate.svg
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="RSSg" x1="0.085" y1="0.085" x2="0.915" y2="0.915">
|
||||||
|
<stop offset="0" stop-color="#E3702D" />
|
||||||
|
<stop offset="0.1071" stop-color="#EA7D31" />
|
||||||
|
<stop offset="0.3503" stop-color="#F69537" />
|
||||||
|
<stop offset="0.5" stop-color="#FB9E3A" />
|
||||||
|
<stop offset="0.7016" stop-color="#EA7C31" />
|
||||||
|
<stop offset="0.8866" stop-color="#DE642B" />
|
||||||
|
<stop offset="1" stop-color="#D95B29" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
|
||||||
|
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
|
||||||
|
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
|
||||||
|
<circle cx="68" cy="189" r="24" fill="#FFF"/>
|
||||||
|
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
|
||||||
|
<path d="M184 213A140 140 0 0 0 44 73V38a175 175 0 0 1 175 175z" fill="#FFF"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1,016 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
@ -161,9 +161,9 @@ form > * {
|
||||||
|
|
||||||
#related-tags {
|
#related-tags {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
|
/* height: 90vh; */
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
padding: 0 0.5em 1em 1em;
|
padding: 0 0.5em 1em 1em;
|
||||||
height: 90vh;
|
|
||||||
width: 15%;
|
width: 15%;
|
||||||
/* float: right; */
|
/* float: right; */
|
||||||
/* width: 200px; */
|
/* width: 200px; */
|
|
@ -76,7 +76,9 @@
|
||||||
» Information and resources about Blasta, collaborative
|
» Information and resources about Blasta, collaborative
|
||||||
bookmarks with an Irish manner.
|
bookmarks with an Irish manner.
|
||||||
</p>
|
</p>
|
||||||
<h3>About Blasta</h3>
|
<h3>
|
||||||
|
About Blasta
|
||||||
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
Blasta is a collaborative bookmarks manager for organizing
|
Blasta is a collaborative bookmarks manager for organizing
|
||||||
online content. It allows you to add links to your personal
|
online content. It allows you to add links to your personal
|
||||||
|
@ -114,7 +116,12 @@
|
||||||
monero, mms, news, sip, udp, xmpp and any scheme and type
|
monero, mms, news, sip, udp, xmpp and any scheme and type
|
||||||
that you desire.
|
that you desire.
|
||||||
</p>
|
</p>
|
||||||
<h4>Why Blasta?</h4>
|
<p>
|
||||||
|
Blasta was inspired by projects Movim and Rivista.
|
||||||
|
</p>
|
||||||
|
<h4>
|
||||||
|
Why Blasta?
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Corporate search engines are archaic and outdated, and often
|
Corporate search engines are archaic and outdated, and often
|
||||||
prioritize their own interests, leading to censorship and
|
prioritize their own interests, leading to censorship and
|
||||||
|
@ -128,14 +135,18 @@
|
||||||
references and resources that you need in order to be
|
references and resources that you need in order to be
|
||||||
productive and get that you need.
|
productive and get that you need.
|
||||||
</p>
|
</p>
|
||||||
<h4>The things that you can do with Blasta are endless</h4>
|
<h4>
|
||||||
|
The things that you can do with Blasta are endless
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Blasta is an open-ended indexing system, and, as such, it
|
Blasta is an open-ended indexing system, and, as such, it
|
||||||
provides a versatile platform with which you have the
|
provides a versatile platform with which you have the
|
||||||
ability to tailor its usage according to your desired
|
ability to tailor its usage according to your desired
|
||||||
preferences. <a href="/help/about/ideas">Learn more</a>.
|
preferences. <a href="/help/about/ideas">Learn more</a>.
|
||||||
</p>
|
</p>
|
||||||
<h4>The difference from other services</h4>
|
<h4>
|
||||||
|
The difference from other services
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Unlike some so called "social" bookmarking systems, Blasta
|
Unlike some so called "social" bookmarking systems, Blasta
|
||||||
does not own your information; your bookmarks are
|
does not own your information; your bookmarks are
|
||||||
|
@ -151,7 +162,9 @@
|
||||||
your personal XMPP account under PubSub node
|
your personal XMPP account under PubSub node
|
||||||
<code>urn:xmpp:bibliography:0</code>.
|
<code>urn:xmpp:bibliography:0</code>.
|
||||||
</p>
|
</p>
|
||||||
<h4>Information that is stored by Blasta</h4>
|
<h4>
|
||||||
|
Information that is stored by Blasta
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
In order for Blasta to facilitate sharing of information and
|
In order for Blasta to facilitate sharing of information and
|
||||||
accessibility to information, Blasta aggregates your own
|
accessibility to information, Blasta aggregates your own
|
||||||
|
@ -166,14 +179,18 @@
|
||||||
all of their owners as private and no one else has stored
|
all of their owners as private and no one else has stored
|
||||||
them in a public fashion (i.e. not classified private).
|
them in a public fashion (i.e. not classified private).
|
||||||
</p>
|
</p>
|
||||||
<h4>Blasta source code</h4>
|
<h4>
|
||||||
|
Blasta source code
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
The source code of Blasta is available under the terms of
|
The source code of Blasta is available under the terms of
|
||||||
the license <a href="/license/agpl-3.0.txt">AGPL-3.0</a> at
|
the license <a href="/license/agpl-3.0.txt">AGPL-3.0</a> at
|
||||||
<a href="https://git.xmpp-it.net/sch/Blasta">
|
<a href="https://git.xmpp-it.net/sch/Blasta">
|
||||||
git.xmpp-it.net</a>.
|
git.xmpp-it.net</a>.
|
||||||
</p>
|
</p>
|
||||||
<h4>Our motives</h4>
|
<h4>
|
||||||
|
Our motives
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
We are adopting the attitude towards life and towards death,
|
We are adopting the attitude towards life and towards death,
|
||||||
which was implicit in the old Vikings' and in Schopenhauer's
|
which was implicit in the old Vikings' and in Schopenhauer's
|
||||||
|
@ -186,7 +203,9 @@
|
||||||
particular for and through his racial community, which is
|
particular for and through his racial community, which is
|
||||||
eternal.
|
eternal.
|
||||||
</p>
|
</p>
|
||||||
<h4>About us</h4>
|
<h4>
|
||||||
|
About us
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Blasta was proudly made in the Republic of Ireland, by a
|
Blasta was proudly made in the Republic of Ireland, by a
|
||||||
group of bible loving, religious, and stylish Irish men, who
|
group of bible loving, religious, and stylish Irish men, who
|
||||||
|
@ -200,12 +219,16 @@
|
||||||
proceeding year, and he was the one who has initiated the
|
proceeding year, and he was the one who has initiated the
|
||||||
idea of XMPP PubSub bookmarks.
|
idea of XMPP PubSub bookmarks.
|
||||||
</p>
|
</p>
|
||||||
<h4>Conclusion</h4>
|
<h4>
|
||||||
|
Conclusion
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Blasta is for you to enjoy, excite, instigate, investigate,
|
Blasta is for you to enjoy, excite, instigate, investigate,
|
||||||
learn and research.
|
learn and research.
|
||||||
</p>
|
</p>
|
||||||
<p>We hope you would have productive outcomes with Blasta.</p>
|
<p>
|
||||||
|
We hope you would have productive outcomes with Blasta.
|
||||||
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<p class="quote bottom">
|
<p class="quote bottom">
|
||||||
“All you can take with you; is that which you have given
|
“All you can take with you; is that which you have given
|
|
@ -168,7 +168,7 @@
|
||||||
xmpp.org
|
xmpp.org
|
||||||
</a>
|
</a>
|
||||||
​ 
|
​ 
|
||||||
<a href="https://libervia.org/">
|
<a href="https://libervia.org">
|
||||||
libervia.org
|
libervia.org
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -188,13 +188,15 @@
|
||||||
xmpp.org
|
xmpp.org
|
||||||
</a>
|
</a>
|
||||||
​ 
|
​ 
|
||||||
<a href="https://movim.eu/">
|
<a href="https://movim.eu">
|
||||||
movim.eu
|
movim.eu
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h4>Of note</h4>
|
<h4>
|
||||||
|
Of note
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
These type of technologies are public information for over
|
These type of technologies are public information for over
|
||||||
a couple of decades (i.e. more than 20 years); and people
|
a couple of decades (i.e. more than 20 years); and people
|
|
@ -69,7 +69,7 @@
|
||||||
<label for="remember">Remember</label -->
|
<label for="remember">Remember</label -->
|
||||||
</form>
|
</form>
|
||||||
<p>
|
<p>
|
||||||
Log in to Blasta with your XMPP account or
|
Connect to Blasta with your XMPP account or
|
||||||
<a href="/register">register</a> for an account.
|
<a href="/register">register</a> for an account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
|
@ -226,7 +226,9 @@
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<p class="quote bottom">
|
<p class="quote bottom">
|
||||||
Blasta was inspired by Movim and Rivista.
|
“Talent hits a target no one else can hit.
|
||||||
|
Genius hits a target no one else can see.”
|
||||||
|
― Arthur Schopenhauer
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -64,14 +64,20 @@
|
||||||
|
|
||||||
PubSub bookmarks
|
PubSub bookmarks
|
||||||
</h2>
|
</h2>
|
||||||
<p>» Information of your Jabber ID.</p>
|
<p>
|
||||||
<h3>Your Profile</h3>
|
» Information of your Jabber ID.
|
||||||
|
</p>
|
||||||
|
<h3>
|
||||||
|
Your profile
|
||||||
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
This page provides a general survey of your XMPP account and
|
This page provides a general survey of your XMPP account and
|
||||||
stored bookmarks.
|
stored bookmarks.
|
||||||
</p>
|
</p>
|
||||||
<!--
|
<!--
|
||||||
<h4 id="enrollment">Enrollment</h4>
|
<h4 id="enrollment">
|
||||||
|
Enrollment
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Blasta does not automatically include your public bookmarks
|
Blasta does not automatically include your public bookmarks
|
||||||
to its database.
|
to its database.
|
||||||
|
@ -120,7 +126,9 @@
|
||||||
therefore.
|
therefore.
|
||||||
</p>
|
</p>
|
||||||
-->
|
-->
|
||||||
<h4 id="export">Export</h4>
|
<h4 id="export">
|
||||||
|
Export
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Export bookmarks to a file.
|
Export bookmarks to a file.
|
||||||
</p>
|
</p>
|
||||||
|
@ -128,7 +136,9 @@
|
||||||
<!-- TODO Add XBEL, XHTML and XML -->
|
<!-- TODO Add XBEL, XHTML and XML -->
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
<strong>Private</strong>
|
<strong>
|
||||||
|
Private
|
||||||
|
</strong>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a download="{{jabber_id}}_private.json"
|
<a download="{{jabber_id}}_private.json"
|
||||||
|
@ -139,7 +149,9 @@
|
||||||
TOML</a>.
|
TOML</a>.
|
||||||
</dd>
|
</dd>
|
||||||
<dt>
|
<dt>
|
||||||
<strong>Public</strong>
|
<strong>
|
||||||
|
Public
|
||||||
|
</strong>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a download="{{jabber_id}}_public.json"
|
<a download="{{jabber_id}}_public.json"
|
||||||
|
@ -150,7 +162,9 @@
|
||||||
TOML</a>.
|
TOML</a>.
|
||||||
</dd>
|
</dd>
|
||||||
<dt>
|
<dt>
|
||||||
<strong>Read</strong>
|
<strong>
|
||||||
|
Read
|
||||||
|
</strong>
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a download="{{jabber_id}}_read.json"
|
<a download="{{jabber_id}}_read.json"
|
||||||
|
@ -162,7 +176,9 @@
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</p>
|
</p>
|
||||||
<h4 id="import">Import</h4>
|
<h4 id="import">
|
||||||
|
Import
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Import bookmarks from a file, and choose a node to import
|
Import bookmarks from a file, and choose a node to import
|
||||||
your bookmarks to.
|
your bookmarks to.
|
||||||
|
@ -175,7 +191,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<label for="file">File</label>
|
<label for="file">
|
||||||
|
File
|
||||||
|
</label>
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -189,7 +207,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<label for="node">Node</label>
|
<label for="node">
|
||||||
|
Node
|
||||||
|
</label>
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -214,7 +234,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<label for="node">Action</label>
|
<label for="node">
|
||||||
|
Action
|
||||||
|
</label>
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -422,7 +444,9 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
proceeding.
|
proceeding.
|
||||||
</p>
|
</p>
|
||||||
<hr/>
|
<hr/>
|
||||||
<h4 id="termination">Termination</h4>
|
<h4 id="termination">
|
||||||
|
Termination
|
||||||
|
</h4>
|
||||||
<p>
|
<p>
|
||||||
Due to security concerns, Blasta does not have a built-in
|
Due to security concerns, Blasta does not have a built-in
|
||||||
mechanism to delete nodes.
|
mechanism to delete nodes.
|
||||||
|
@ -438,7 +462,9 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
<a href="https://psi-im.org">Psi</a>, or
|
<a href="https://psi-im.org">Psi</a>, or
|
||||||
<a href="https://psi-plus.com">Psi+</a>.
|
<a href="https://psi-plus.com">Psi+</a>.
|
||||||
</p>
|
</p>
|
||||||
<h4>Delete your public bookmarks</h4>
|
<h4>
|
||||||
|
Delete your public bookmarks
|
||||||
|
</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<iq type='set'
|
<iq type='set'
|
||||||
from='{{jabber_id}}'
|
from='{{jabber_id}}'
|
||||||
|
@ -449,7 +475,9 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
</pubsub>
|
</pubsub>
|
||||||
</iq>
|
</iq>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>Delete your private bookmarks</h4>
|
<h4>
|
||||||
|
Delete your private bookmarks
|
||||||
|
</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<iq type='set'
|
<iq type='set'
|
||||||
from='{{jabber_id}}'
|
from='{{jabber_id}}'
|
||||||
|
@ -460,7 +488,9 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
</pubsub>
|
</pubsub>
|
||||||
</iq>
|
</iq>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>Delete your reading list</h4>
|
<h4>
|
||||||
|
Delete your reading list
|
||||||
|
</h4>
|
||||||
<pre>
|
<pre>
|
||||||
<iq type='set'
|
<iq type='set'
|
||||||
from='{{jabber_id}}'
|
from='{{jabber_id}}'
|
|
@ -278,11 +278,19 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br/>
|
<br/>
|
||||||
<p class="quote bottom"
|
<p class="quote bottom">
|
||||||
title="Arthur Schopenhauer speaks about Bob Wyman, Jérôme Poisson, Joe Hildebrand, Peter Saint-Andre, and Timothée Jaussoin.">
|
“Technology is extremely powerful and has the potential to
|
||||||
“Talent hits a target no one else can hit.
|
change the world; however, it cannot realize its full
|
||||||
Genius hits a target no one else can see.”
|
potential unless people feel the need to use it. Some
|
||||||
― Arthur Schopenhauer
|
researchers agree, that to ensure the success of new
|
||||||
|
technology, the focus should be on the people’s perspective
|
||||||
|
rather than on the technology itself. Designing a new
|
||||||
|
experience is a process that facilitates the relationship
|
||||||
|
between technology and people; thus, balanced research
|
||||||
|
should be conducted from both perspectives.”
|
||||||
|
― <a href="https://www.diva.exchange/en/privacy/trust-in-the-cryptocurrency-economy-resolving-the-problem-experience-of-diva-exchange-part-2/">
|
||||||
|
DIVA.EXCHANGE
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -76,8 +76,9 @@
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
As with email, you need an account with a service provider
|
As with email, you need an account with a service provider
|
||||||
to operate Blasta, so if you already have an XMPP account,
|
to utilize Blasta; if you already have an XMPP account, you
|
||||||
you can <a href="/connect">connect</a> and start to Blasta.
|
can <a href="/connect">connect</a> and start to utilize
|
||||||
|
Blasta.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you do not have an XMPP account, yet, you can use a
|
If you do not have an XMPP account, yet, you can use a
|
116
blasta/config.py
Normal 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, 'blasta')
|
||||||
|
|
||||||
|
def get_setting(filename, section):
|
||||||
|
with open(filename, mode="rb") as settings:
|
||||||
|
result = tomllib.load(settings)[section]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Share:
|
||||||
|
|
||||||
|
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('.blasta/data')
|
||||||
|
else:
|
||||||
|
return os.path.abspath('.blasta/data')
|
||||||
|
else:
|
||||||
|
data_home = os.path.join(
|
||||||
|
os.environ.get('HOME'), '.local', 'share'
|
||||||
|
)
|
||||||
|
return os.path.join(data_home, 'blasta')
|
||||||
|
|
||||||
|
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('.blasta/cache')
|
||||||
|
else:
|
||||||
|
return os.path.abspath('.blasta/cache')
|
||||||
|
else:
|
||||||
|
cache_home = os.path.join(
|
||||||
|
os.environ.get('HOME'), '.cache'
|
||||||
|
)
|
||||||
|
return os.path.join(cache_home, 'blasta')
|
310
blasta/configs/blasta.sql
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
-- Blasta SQLite database script.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS main_entries (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
url_hash TEXT NOT NULL UNIQUE,
|
||||||
|
url TEXT NOT NULL UNIQUE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
summary TEXT,
|
||||||
|
jid_id TEXT NOT NULL,
|
||||||
|
date_first TEXT NOT NULL,
|
||||||
|
date_last TEXT NOT NULL,
|
||||||
|
instances INTEGER NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS main_jids (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
jid TEXT NOT NULL UNIQUE,
|
||||||
|
opt_in INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS main_tags (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
tag TEXT NOT NULL UNIQUE,
|
||||||
|
instances INTEGER NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS main_statistics (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
type TEXT NOT NULL UNIQUE,
|
||||||
|
count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT
|
||||||
|
INTO main_statistics(
|
||||||
|
type)
|
||||||
|
VALUES ('entries'),
|
||||||
|
('jids'),
|
||||||
|
('tags');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS combination_entries_tags_jids (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
entry_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
jid_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("entry_id") REFERENCES "main_entries" ("id")
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("tag_id") REFERENCES "main_tags" ("id")
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("jid_id") REFERENCES "main_jids" ("id")
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- NOTE Digit for JID which is authorized;
|
||||||
|
-- Zero (0) for private;
|
||||||
|
-- Empty (no row) for public.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS authorization_entries_jids (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
entry_id INTEGER NOT NULL,
|
||||||
|
jid_id INTEGER NOT NULL,
|
||||||
|
authorization INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("entry_id") REFERENCES "main_entries" ("id")
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("jid_id") REFERENCES "main_jids" ("id")
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS report_entries (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
url_hash_subject TEXT NOT NULL,
|
||||||
|
jid_reporter TEXT NOT NULL,
|
||||||
|
type TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS report_jids (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
jid_subject TEXT NOT NULL,
|
||||||
|
jid_reporter TEXT NOT NULL,
|
||||||
|
type TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER instances_entry_decrease
|
||||||
|
AFTER DELETE ON combination_entries_tags_jids
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_entries
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(DISTINCT jid_id)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE entry_id = OLD.entry_id
|
||||||
|
)
|
||||||
|
WHERE id = OLD.entry_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER instances_entry_increase
|
||||||
|
AFTER INSERT ON combination_entries_tags_jids
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_entries
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(DISTINCT jid_id)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE entry_id = NEW.entry_id
|
||||||
|
)
|
||||||
|
WHERE id = NEW.entry_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER instances_entry_update
|
||||||
|
AFTER UPDATE ON combination_entries_tags_jids
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
-- Decrease instances for the old tag_id
|
||||||
|
UPDATE main_entries
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(DISTINCT jid_id)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE entry_id = OLD.entry_id
|
||||||
|
)
|
||||||
|
WHERE id = OLD.entry_id;
|
||||||
|
|
||||||
|
-- Increase instances for the new tag_id
|
||||||
|
UPDATE main_entries
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(DISTINCT jid_id)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE entry_id = NEW.entry_id
|
||||||
|
)
|
||||||
|
WHERE id = NEW.entry_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TRIGGER instances_tag_decrease
|
||||||
|
AFTER DELETE ON combination_entries_tags_jids
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_tags
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE tag_id = OLD.tag_id
|
||||||
|
)
|
||||||
|
WHERE id = OLD.tag_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER instances_tag_increase
|
||||||
|
AFTER INSERT ON combination_entries_tags_jids
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_tags
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE tag_id = NEW.tag_id
|
||||||
|
)
|
||||||
|
WHERE id = NEW.tag_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER instances_tag_update
|
||||||
|
AFTER UPDATE ON combination_entries_tags_jids
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
-- Decrease instances for the old tag_id
|
||||||
|
UPDATE main_tags
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE tag_id = OLD.tag_id
|
||||||
|
)
|
||||||
|
WHERE id = OLD.tag_id;
|
||||||
|
|
||||||
|
-- Increase instances for the new tag_id
|
||||||
|
UPDATE main_tags
|
||||||
|
SET instances = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM combination_entries_tags_jids
|
||||||
|
WHERE tag_id = NEW.tag_id
|
||||||
|
)
|
||||||
|
WHERE id = NEW.tag_id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER entry_count_increase
|
||||||
|
AFTER INSERT ON main_entries
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_entries
|
||||||
|
)
|
||||||
|
WHERE type = 'entries';
|
||||||
|
END;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TRIGGER entry_count_decrease
|
||||||
|
AFTER DELETE ON main_entries
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_entries
|
||||||
|
)
|
||||||
|
WHERE type = 'entries';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER entry_count_update
|
||||||
|
AFTER UPDATE ON main_entries
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_entries
|
||||||
|
)
|
||||||
|
WHERE type = 'entries';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER entry_remove
|
||||||
|
AFTER UPDATE ON main_entries
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.instances < 1
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM main_entries WHERE id = OLD.id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER jid_count_increase
|
||||||
|
AFTER INSERT ON main_jids
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_jids
|
||||||
|
)
|
||||||
|
WHERE type = 'jids';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER jid_count_decrease
|
||||||
|
AFTER DELETE ON main_jids
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_jids
|
||||||
|
)
|
||||||
|
WHERE type = 'jids';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER jid_count_update
|
||||||
|
AFTER UPDATE ON main_jids
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_jids
|
||||||
|
)
|
||||||
|
WHERE type = 'jids';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER tag_count_increase
|
||||||
|
AFTER INSERT ON main_tags
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_tags
|
||||||
|
)
|
||||||
|
WHERE type = 'tags';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER tag_count_decrease
|
||||||
|
AFTER DELETE ON main_tags
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_tags
|
||||||
|
)
|
||||||
|
WHERE type = 'tags';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER tag_count_update
|
||||||
|
AFTER UPDATE ON main_tags
|
||||||
|
BEGIN
|
||||||
|
UPDATE main_statistics
|
||||||
|
SET count = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM main_tags
|
||||||
|
)
|
||||||
|
WHERE type = 'tags';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER tag_remove
|
||||||
|
AFTER UPDATE ON main_tags
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN NEW.instances < 1
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM main_tags WHERE id = OLD.id;
|
||||||
|
END;
|
|
@ -13,22 +13,22 @@ journal = ""
|
||||||
pubsub = ""
|
pubsub = ""
|
||||||
|
|
||||||
# Bibliography
|
# Bibliography
|
||||||
node_id = "urn:xmpp:bibliography:0"
|
node_id = "blasta:annotation:0"
|
||||||
node_title = "Blasta"
|
node_title = "Blasta"
|
||||||
node_subtitle = "Bibliography"
|
node_subtitle = "Annotation"
|
||||||
|
|
||||||
# Private bibliography
|
# Private bibliography
|
||||||
node_id_private = "xmpp:bibliography:private:0"
|
node_id_private = "blasta:annotation:private:0"
|
||||||
node_title_private = "Blasta (Private)"
|
node_title_private = "Blasta (Private)"
|
||||||
node_subtitle_private = "Private bibliography"
|
node_subtitle_private = "Private annotation"
|
||||||
|
|
||||||
# Reading list
|
# Reading list
|
||||||
node_id_read = "xmpp:bibliography:read:0"
|
node_id_read = "blasta:annotation:read:0"
|
||||||
node_title_read = "Blasta (Read)"
|
node_title_read = "Blasta (Read)"
|
||||||
node_subtitle_read = "Reading list"
|
node_subtitle_read = "Reading list"
|
||||||
|
|
||||||
# Settings node
|
# Settings node
|
||||||
node_settings = "xmpp:blasta:settings:0"
|
node_settings = "blasta:settings:0"
|
||||||
|
|
||||||
# Acceptable protocol types that would be aggregated to the Blasta database
|
# Acceptable protocol types that would be aggregated to the Blasta database
|
||||||
schemes = [
|
schemes = [
|
2041
blasta/database/sqlite.py
Normal file
2306
blasta/http/instance.py
Normal file
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
12
blasta/utilities/cryptography.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
class UtilitiesCryptography:
|
||||||
|
|
||||||
|
def hash_url_to_md5(url):
|
||||||
|
url_encoded = url.encode()
|
||||||
|
url_hashed = hashlib.md5(url_encoded)
|
||||||
|
url_digest = url_hashed.hexdigest()
|
||||||
|
return url_digest
|
256
blasta/utilities/data.py
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from blasta.database.sqlite import DatabaseSQLite
|
||||||
|
from blasta.utilities.cryptography import UtilitiesCryptography
|
||||||
|
from blasta.utilities.syndication import UtilitiesSyndication
|
||||||
|
from blasta.xmpp.pubsub import XmppPubsub
|
||||||
|
import os
|
||||||
|
from slixmpp.stanza.iq import Iq
|
||||||
|
import tomli_w
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
except:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
|
class UtilitiesData:
|
||||||
|
|
||||||
|
def cache_items_and_tags_search(directory_cache, entries, jid, query):
|
||||||
|
"""Create a cache file of node items and tags."""
|
||||||
|
item_ids = []
|
||||||
|
tags = {}
|
||||||
|
for entry in entries:
|
||||||
|
entry_tags = entry['tags']
|
||||||
|
entry_url_hash = entry['url_hash']
|
||||||
|
tags_to_include = []
|
||||||
|
if query in ' '.join([entry['title'], entry['link'], entry['summary'], ' '.join(entry_tags)]):
|
||||||
|
item_ids.append(entry_url_hash)
|
||||||
|
tags_to_include += entry_tags
|
||||||
|
for tag_to_include in tags_to_include:
|
||||||
|
tags[tag_to_include] = tags[tag_to_include]+1 if tag_to_include in tags else 1
|
||||||
|
if tags:
|
||||||
|
tags = dict(sorted(tags.items(), key=lambda item: (-item[1], item[0])))
|
||||||
|
tags = dict(list(tags.items())[:30])
|
||||||
|
if item_ids:
|
||||||
|
filename = os.path.join(directory_cache, 'data', jid + '_query.toml')
|
||||||
|
data = {
|
||||||
|
'item_ids' : item_ids,
|
||||||
|
'tags' : tags}
|
||||||
|
UtilitiesData.save_to_toml(filename, data)
|
||||||
|
|
||||||
|
def cache_items_and_tags_filter(directory_cache, entries, jid, tag):
|
||||||
|
"""Create a cache file of node items and tags."""
|
||||||
|
item_ids = []
|
||||||
|
tags = {}
|
||||||
|
for entry in entries:
|
||||||
|
entry_tags = entry['tags']
|
||||||
|
entry_url_hash = entry['url_hash']
|
||||||
|
tags_to_include = []
|
||||||
|
if tag in entry_tags:
|
||||||
|
item_ids.append(entry_url_hash)
|
||||||
|
tags_to_include += entry_tags
|
||||||
|
for tag_to_include in tags_to_include:
|
||||||
|
tags[tag_to_include] = tags[tag_to_include]+1 if tag_to_include in tags else 1
|
||||||
|
if tags:
|
||||||
|
tags = dict(sorted(tags.items(), key=lambda item: (-item[1], item[0])))
|
||||||
|
tags = dict(list(tags.items())[:30])
|
||||||
|
del tags[tag]
|
||||||
|
if item_ids:
|
||||||
|
directory = os.path.join(directory_cache, 'data', jid)
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
os.mkdir(directory)
|
||||||
|
filename = os.path.join(directory, tag + '.toml')
|
||||||
|
# Add support for search query
|
||||||
|
#filename = 'data/{}/query:{}.toml'.format(jid, query)
|
||||||
|
#filename = 'data/{}/tag:{}.toml'.format(jid, tag)
|
||||||
|
data = {
|
||||||
|
'item_ids' : item_ids,
|
||||||
|
'tags' : tags}
|
||||||
|
UtilitiesData.save_to_toml(filename, data)
|
||||||
|
|
||||||
|
def cache_items_and_tags(directory_cache, entries, jid):
|
||||||
|
"""Create a cache file of node items and tags."""
|
||||||
|
item_ids = []
|
||||||
|
tags = {}
|
||||||
|
for entry in entries:
|
||||||
|
entry_tags = entry['tags']
|
||||||
|
entry_url_hash = entry['url_hash']
|
||||||
|
tags_to_include = []
|
||||||
|
item_ids.append(entry_url_hash)
|
||||||
|
tags_to_include += entry_tags
|
||||||
|
for tag_to_include in tags_to_include:
|
||||||
|
tags[tag_to_include] = tags[tag_to_include]+1 if tag_to_include in tags else 1
|
||||||
|
if tags:
|
||||||
|
tags = dict(sorted(tags.items(), key=lambda item: (-item[1], item[0])))
|
||||||
|
tags = dict(list(tags.items())[:30])
|
||||||
|
if item_ids:
|
||||||
|
filename = os.path.join(directory_cache, 'data', jid + '.toml')
|
||||||
|
data = {
|
||||||
|
'item_ids' : item_ids,
|
||||||
|
'tags' : tags}
|
||||||
|
UtilitiesData.save_to_toml(filename, data)
|
||||||
|
|
||||||
|
def extract_iq_items(iq, jabber_id):
|
||||||
|
iq_items = iq['pubsub']['items']
|
||||||
|
entries = []
|
||||||
|
name = jabber_id.split('@')[0]
|
||||||
|
for iq_item in iq_items:
|
||||||
|
item_payload = iq_item['payload']
|
||||||
|
entry = UtilitiesSyndication.extract_items(item_payload)
|
||||||
|
entries.append(entry)
|
||||||
|
# TODO Handle this with XEP-0059 (reverse: bool), instead of reversing it.
|
||||||
|
entries.reverse()
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def extract_iq_items_extra(db_file, iq, jabber_id, limit=None):
|
||||||
|
iq_items = iq['pubsub']['items']
|
||||||
|
entries = []
|
||||||
|
name = jabber_id.split('@')[0]
|
||||||
|
for iq_item in iq_items:
|
||||||
|
item_payload = iq_item['payload']
|
||||||
|
entry = UtilitiesSyndication.extract_items(item_payload, limit)
|
||||||
|
url_hash = UtilitiesCryptography.hash_url_to_md5(entry['link'])
|
||||||
|
iq_item_id = iq_item['id']
|
||||||
|
if iq_item_id != url_hash:
|
||||||
|
logging.error('Item ID does not match MD5. id: {} hash: {}'.format(iq_item_id, url_hash))
|
||||||
|
logging.warn('Item ID does not match MD5. id: {} hash: {}'.format(iq_item_id, url_hash))
|
||||||
|
instances = DatabaseSQLite.get_entry_instances_by_url_hash(db_file, url_hash)
|
||||||
|
if entry:
|
||||||
|
entry['instances'] = instances or 0
|
||||||
|
entry['jid'] = jabber_id
|
||||||
|
entry['name'] = name
|
||||||
|
entry['url_hash'] = url_hash
|
||||||
|
entries.append(entry)
|
||||||
|
# TODO Handle this with XEP-0059 (reverse: bool), instead of reversing it.
|
||||||
|
entries.reverse()
|
||||||
|
result = entries
|
||||||
|
return result
|
||||||
|
|
||||||
|
def open_file_toml(filename: str) -> dict:
|
||||||
|
with open(filename, mode="rb") as fn:
|
||||||
|
data = tomllib.load(fn)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def organize_tags(tags):
|
||||||
|
tags_organized = []
|
||||||
|
tags = tags.split(',')
|
||||||
|
#tags = sorted(set(tags))
|
||||||
|
for tag in tags:
|
||||||
|
if tag:
|
||||||
|
tag = tag.lower().strip()
|
||||||
|
if tag not in tags_organized:
|
||||||
|
tags_organized.append(tag)
|
||||||
|
return sorted(tags_organized)
|
||||||
|
|
||||||
|
def remove_item_from_cache(directory_cache, jabber_id, node, url_hash):
|
||||||
|
filename_items = os.path.join(directory_cache, 'items', jabber_id + '.toml')
|
||||||
|
entries_cache = UtilitiesData.open_file_toml(filename_items)
|
||||||
|
if node in entries_cache:
|
||||||
|
entries_cache_node = entries_cache[node]
|
||||||
|
for entry_cache in entries_cache_node:
|
||||||
|
if entry_cache['url_hash'] == url_hash:
|
||||||
|
entry_cache_index = entries_cache_node.index(entry_cache)
|
||||||
|
del entries_cache_node[entry_cache_index]
|
||||||
|
break
|
||||||
|
data_items = entries_cache
|
||||||
|
UtilitiesData.save_to_toml(filename_items, data_items)
|
||||||
|
|
||||||
|
def save_to_json(filename: str, data) -> None:
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
async def update_cache_and_database(
|
||||||
|
db_file, directory_cache, xmpp_instance, jabber_id: str, node_type: str, node_id: str):
|
||||||
|
# Download identifiers of node items.
|
||||||
|
iq = await XmppPubsub.get_node_item_ids(xmpp_instance, jabber_id, node_id)
|
||||||
|
if isinstance(iq, Iq):
|
||||||
|
iq_items_remote = iq['disco_items']
|
||||||
|
|
||||||
|
# Cache a list of identifiers of node items to a file.
|
||||||
|
iq_items_remote_name = []
|
||||||
|
for iq_item_remote in iq_items_remote:
|
||||||
|
iq_item_remote_name = iq_item_remote['name']
|
||||||
|
iq_items_remote_name.append(iq_item_remote_name)
|
||||||
|
|
||||||
|
#data_item_ids = {'iq_items' : iq_items_remote_name}
|
||||||
|
#filename_item_ids = 'item_ids/' + jabber_id + '.toml'
|
||||||
|
#Data.save_to_toml(filename_item_ids, data_item_ids)
|
||||||
|
|
||||||
|
filename_items = os.path.join(directory_cache, 'items', jabber_id + '.toml')
|
||||||
|
if not os.path.exists(filename_items) or os.path.getsize(filename_items) in (0, 13):
|
||||||
|
iq = await XmppPubsub.get_node_items(xmpp_instance, jabber_id, node_id)
|
||||||
|
if isinstance(iq, Iq):
|
||||||
|
entries_cache_node = UtilitiesData.extract_iq_items_extra(db_file, iq, jabber_id)
|
||||||
|
data_items = {node_type : entries_cache_node}
|
||||||
|
UtilitiesData.save_to_toml(filename_items, data_items)
|
||||||
|
return ['fine', iq] # TODO Remove this line
|
||||||
|
else:
|
||||||
|
return ['error', iq]
|
||||||
|
else:
|
||||||
|
entries_cache = UtilitiesData.open_file_toml(filename_items)
|
||||||
|
if not node_type in entries_cache: return ['error', 'Directory "{}" is empty'. format(node_type)]
|
||||||
|
entries_cache_node = entries_cache[node_type]
|
||||||
|
|
||||||
|
# Check whether items still exist on node
|
||||||
|
for entry in entries_cache_node:
|
||||||
|
iq_item_remote_exist = False
|
||||||
|
url_hash = None
|
||||||
|
for url_hash in iq_items_remote_name:
|
||||||
|
if url_hash == entry['url_hash']:
|
||||||
|
iq_item_remote_exist = True
|
||||||
|
break
|
||||||
|
if url_hash and not iq_item_remote_exist:
|
||||||
|
await DatabaseSQLite.delete_combination_row_by_jid_and_url_hash(
|
||||||
|
db_file, url_hash, jabber_id)
|
||||||
|
entry_index = entries_cache_node.index(entry)
|
||||||
|
del entries_cache_node[entry_index]
|
||||||
|
|
||||||
|
# Check for new items on node
|
||||||
|
entries_cache_node_new = []
|
||||||
|
for url_hash in iq_items_remote_name:
|
||||||
|
iq_item_local_exist = False
|
||||||
|
for entry in entries_cache_node:
|
||||||
|
if url_hash == entry['url_hash']:
|
||||||
|
iq_item_local_exist = True
|
||||||
|
break
|
||||||
|
if not iq_item_local_exist:
|
||||||
|
iq = await XmppPubsub.get_node_item(
|
||||||
|
xmpp_instance, jabber_id, node_id, url_hash)
|
||||||
|
if isinstance(iq, Iq):
|
||||||
|
entries_iq = UtilitiesData.extract_iq_items_extra(db_file, iq, jabber_id)
|
||||||
|
entries_cache_node_new += entries_iq
|
||||||
|
else:
|
||||||
|
# TODO
|
||||||
|
# Handle this concern in a different fashion,
|
||||||
|
# instead of stopping the whole operation.
|
||||||
|
return ['error', iq]
|
||||||
|
entries_cache_node += entries_cache_node_new
|
||||||
|
|
||||||
|
if node_type == 'public':
|
||||||
|
# Fast (low I/O)
|
||||||
|
if not DatabaseSQLite.get_jid_id_by_jid(db_file, jabber_id):
|
||||||
|
await DatabaseSQLite.set_jid(db_file, jabber_id)
|
||||||
|
#await DatabaseSQLite.add_new_entries(db_file, entries)
|
||||||
|
await DatabaseSQLite.add_tags(db_file, entries_cache_node)
|
||||||
|
# Slow (high I/O)
|
||||||
|
for entry in entries_cache_node:
|
||||||
|
url_hash = entry['url_hash']
|
||||||
|
if not DatabaseSQLite.get_entry_id_by_url_hash(db_file, url_hash):
|
||||||
|
await DatabaseSQLite.add_new_entries(db_file, entries_cache_node)
|
||||||
|
await DatabaseSQLite.associate_entries_tags_jids(db_file, entry)
|
||||||
|
#elif not DatabaseSQLite.is_jid_associated_with_url_hash(db_file, jabber_id, url_hash):
|
||||||
|
# await DatabaseSQLite.associate_entries_tags_jids(db_file, entry)
|
||||||
|
else:
|
||||||
|
await DatabaseSQLite.associate_entries_tags_jids(db_file, entry)
|
||||||
|
|
||||||
|
data_items = entries_cache
|
||||||
|
UtilitiesData.save_to_toml(filename_items, data_items)
|
||||||
|
return ['fine', iq] # TODO Remove this line
|
||||||
|
else:
|
||||||
|
return ['error', iq]
|
11
blasta/utilities/date.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class UtilitiesDate:
|
||||||
|
|
||||||
|
def convert_iso8601_to_readable(timestamp):
|
||||||
|
old_date_format = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
||||||
|
new_date_format = old_date_format.strftime("%B %d, %Y")
|
||||||
|
return new_date_format
|
13
blasta/utilities/http.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
class UtilitiesHttp:
|
||||||
|
|
||||||
|
def is_jid_matches_to_session(accounts, sessions, request):
|
||||||
|
jabber_id = request.cookies.get('jabber_id')
|
||||||
|
session_key = request.cookies.get('session_key')
|
||||||
|
if (jabber_id and
|
||||||
|
jabber_id in accounts and
|
||||||
|
jabber_id in sessions and
|
||||||
|
session_key == sessions[jabber_id]):
|
||||||
|
return jabber_id
|
88
blasta/utilities/syndication.py
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
class UtilitiesSyndication:
|
||||||
|
|
||||||
|
def create_rfc4287_entry(feed_entry):
|
||||||
|
node_entry = ET.Element('entry')
|
||||||
|
node_entry.set('xmlns', 'http://www.w3.org/2005/Atom')
|
||||||
|
# Title
|
||||||
|
title = ET.SubElement(node_entry, 'title')
|
||||||
|
title.set('type', 'text')
|
||||||
|
title.text = feed_entry['title']
|
||||||
|
# Summary
|
||||||
|
summary = ET.SubElement(node_entry, 'summary') # TODO Try 'content'
|
||||||
|
summary.set('type', 'text')
|
||||||
|
#summary.set('lang', feed_entry['summary_lang'])
|
||||||
|
summary.text = feed_entry['summary']
|
||||||
|
# Tags
|
||||||
|
if feed_entry['tags']:
|
||||||
|
for term in feed_entry['tags']:
|
||||||
|
tag = ET.SubElement(node_entry, 'category')
|
||||||
|
tag.set('term', term)
|
||||||
|
# Link
|
||||||
|
link = ET.SubElement(node_entry, "link")
|
||||||
|
link.set('href', feed_entry['link'])
|
||||||
|
# Links
|
||||||
|
# for feed_entry_link in feed_entry['links']:
|
||||||
|
# link = ET.SubElement(node_entry, "link")
|
||||||
|
# link.set('href', feed_entry_link['url'])
|
||||||
|
# link.set('type', feed_entry_link['type'])
|
||||||
|
# link.set('rel', feed_entry_link['rel'])
|
||||||
|
# Date saved
|
||||||
|
if 'published' in feed_entry and feed_entry['published']:
|
||||||
|
published = ET.SubElement(node_entry, 'published')
|
||||||
|
published.text = feed_entry['published']
|
||||||
|
# Date edited
|
||||||
|
if 'updated' in feed_entry and feed_entry['updated']:
|
||||||
|
updated = ET.SubElement(node_entry, 'updated')
|
||||||
|
updated.text = feed_entry['updated']
|
||||||
|
return node_entry
|
||||||
|
|
||||||
|
def extract_items(item_payload, limit=False):
|
||||||
|
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)): return None
|
||||||
|
title_text = '' if title == None else title.text
|
||||||
|
if isinstance(links, ET.Element):
|
||||||
|
for link in item_payload.findall(namespace + 'link'):
|
||||||
|
link_href = link.attrib['href'] if 'href' in link.attrib else ''
|
||||||
|
if link_href: break
|
||||||
|
contents = item_payload.find(namespace + 'summary')
|
||||||
|
summary_text = ''
|
||||||
|
if isinstance(contents, ET.Element):
|
||||||
|
for summary in item_payload.findall(namespace + 'summary'):
|
||||||
|
summary_text = summary.text or ''
|
||||||
|
if summary_text: break
|
||||||
|
published = item_payload.find(namespace + 'published')
|
||||||
|
published_text = '' if published == None else published.text
|
||||||
|
categories = item_payload.find(namespace + 'category')
|
||||||
|
tags = []
|
||||||
|
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']
|
||||||
|
if len(category_term) < 20:
|
||||||
|
tags.append(category_term)
|
||||||
|
elif len(category_term) < 50:
|
||||||
|
tags.append(category_term)
|
||||||
|
if limit and len(tags) > 4: break
|
||||||
|
|
||||||
|
|
||||||
|
identifier = item_payload.find(namespace + 'id')
|
||||||
|
if identifier and identifier.attrib: print(identifier.attrib)
|
||||||
|
identifier_text = '' if identifier == None else identifier.text
|
||||||
|
|
||||||
|
instances = '' # TODO Check the Blasta database for instances.
|
||||||
|
|
||||||
|
entry = {'title' : title_text,
|
||||||
|
'link' : link_href,
|
||||||
|
'summary' : summary_text,
|
||||||
|
'published' : published_text,
|
||||||
|
'updated' : published_text, # TODO "Updated" is missing
|
||||||
|
'tags' : tags}
|
||||||
|
return entry
|
2
blasta/version.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
__version__ = '0.1'
|
||||||
|
__version_info__ = (0, 1)
|
15
blasta/xmpp/form.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
class DataForm:
|
||||||
|
|
||||||
|
def create_setting_entry(xmpp_instance, key : str, value : str):
|
||||||
|
form = xmpp_instance['xep_0004'].make_form('form', 'Settings')
|
||||||
|
form['type'] = 'result'
|
||||||
|
form.add_field(var=key, value=value)
|
||||||
|
return form
|
||||||
|
|
||||||
|
# def create_setting_entry(value : str):
|
||||||
|
# element = ET.Element('value')
|
||||||
|
# element.text = value
|
||||||
|
# return element
|
31
blasta/xmpp/instance.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from slixmpp import ClientXMPP
|
||||||
|
|
||||||
|
class XmppInstance(ClientXMPP):
|
||||||
|
def __init__(self, jid, password):
|
||||||
|
super().__init__(jid, password)
|
||||||
|
#self.add_event_handler("connection_failed", self.on_connection_failed)
|
||||||
|
#self.add_event_handler("failed_auth", self.on_failed_auth)
|
||||||
|
self.add_event_handler("session_start", self.on_session_start)
|
||||||
|
self.register_plugin('xep_0004') # XEP-0004: Data Forms
|
||||||
|
self.register_plugin('xep_0030') # XEP-0030: Service Discovery
|
||||||
|
self.register_plugin('xep_0059') # XEP-0059: Result Set Management
|
||||||
|
self.register_plugin('xep_0060') # XEP-0060: Publish-Subscribe
|
||||||
|
self.register_plugin('xep_0078') # XEP-0078: Non-SASL Authentication
|
||||||
|
self.register_plugin('xep_0163') # XEP-0163: Personal Eventing Protocol
|
||||||
|
self.register_plugin('xep_0223') # XEP-0223: Persistent Storage of Private Data via PubSub
|
||||||
|
self.connect()
|
||||||
|
# self.process(forever=False)
|
||||||
|
|
||||||
|
self.connection_accepted = False
|
||||||
|
|
||||||
|
# def on_connection_failed(self, event):
|
||||||
|
# self.connection_accepted = False
|
||||||
|
|
||||||
|
# def on_failed_auth(self, event):
|
||||||
|
# self.connection_accepted = False
|
||||||
|
|
||||||
|
def on_session_start(self, event):
|
||||||
|
self.connection_accepted = True
|
22
blasta/xmpp/message.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
class XmppMessage:
|
||||||
|
|
||||||
|
def send(self, jid, message_body):
|
||||||
|
jid_from = str(self.boundjid) if self.is_component else None
|
||||||
|
self.send_message(
|
||||||
|
mto=jid,
|
||||||
|
mfrom=jid_from,
|
||||||
|
mbody=message_body,
|
||||||
|
mtype='chat')
|
||||||
|
|
||||||
|
# NOTE It appears to not work.
|
||||||
|
def send_headline(self, jid, message_subject, message_body):
|
||||||
|
jid_from = str(self.boundjid) if self.is_component else None
|
||||||
|
self.send_message(
|
||||||
|
mto=jid,
|
||||||
|
mfrom=jid_from,
|
||||||
|
msubject=message_subject,
|
||||||
|
mbody=message_body,
|
||||||
|
mtype='headline')
|
231
blasta/xmpp/pubsub.py
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import slixmpp
|
||||||
|
from slixmpp.exceptions import IqError, IqTimeout
|
||||||
|
#import slixmpp.plugins.xep_0060.stanza.pubsub as pubsub
|
||||||
|
import slixmpp.plugins.xep_0059.rsm as rsm
|
||||||
|
|
||||||
|
class XmppPubsub:
|
||||||
|
|
||||||
|
# TODO max-items might be limited (CanChat: 255), so iterate from a bigger number to a smaller.
|
||||||
|
# NOTE This function was copied from atomtopubsub
|
||||||
|
def create_node_atom(xmpp_instance, jid, node, title, subtitle, access_model):
|
||||||
|
jid_from = str(xmpp_instance.boundjid) if xmpp_instance.is_component else None
|
||||||
|
iq = xmpp_instance.Iq(stype='set',
|
||||||
|
sto=jid,
|
||||||
|
sfrom=jid_from)
|
||||||
|
iq['pubsub']['create']['node'] = node
|
||||||
|
form = iq['pubsub']['configure']['form']
|
||||||
|
form['type'] = 'submit'
|
||||||
|
form.addField('pubsub#access_model',
|
||||||
|
ftype='list-single',
|
||||||
|
value=access_model)
|
||||||
|
form.addField('pubsub#deliver_payloads',
|
||||||
|
ftype='boolean',
|
||||||
|
value=0)
|
||||||
|
form.addField('pubsub#description',
|
||||||
|
ftype='text-single',
|
||||||
|
value=subtitle)
|
||||||
|
form.addField('pubsub#max_items',
|
||||||
|
ftype='text-single',
|
||||||
|
value='255')
|
||||||
|
form.addField('pubsub#notify_retract',
|
||||||
|
ftype='boolean',
|
||||||
|
value=1)
|
||||||
|
form.addField('pubsub#persist_items',
|
||||||
|
ftype='boolean',
|
||||||
|
value=1)
|
||||||
|
form.addField('pubsub#send_last_published_item',
|
||||||
|
ftype='text-single',
|
||||||
|
value='never')
|
||||||
|
form.addField('pubsub#title',
|
||||||
|
ftype='text-single',
|
||||||
|
value=title)
|
||||||
|
form.addField('pubsub#type',
|
||||||
|
ftype='text-single',
|
||||||
|
value='http://www.w3.org/2005/Atom')
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def create_node_config(xmpp_instance, jid):
|
||||||
|
jid_from = str(xmpp_instance.boundjid) if xmpp_instance.is_component else None
|
||||||
|
iq = xmpp_instance.Iq(stype='set',
|
||||||
|
sto=jid,
|
||||||
|
sfrom=jid_from)
|
||||||
|
iq['pubsub']['create']['node'] = 'xmpp:blasta:configuration:0'
|
||||||
|
form = iq['pubsub']['configure']['form']
|
||||||
|
form['type'] = 'submit'
|
||||||
|
form.addField('pubsub#access_model',
|
||||||
|
ftype='list-single',
|
||||||
|
value='whitelist')
|
||||||
|
form.addField('pubsub#deliver_payloads',
|
||||||
|
ftype='boolean',
|
||||||
|
value=0)
|
||||||
|
form.addField('pubsub#description',
|
||||||
|
ftype='text-single',
|
||||||
|
value='Settings of the Blasta PubSub bookmarks system')
|
||||||
|
form.addField('pubsub#max_items',
|
||||||
|
ftype='text-single',
|
||||||
|
value='30')
|
||||||
|
form.addField('pubsub#notify_retract',
|
||||||
|
ftype='boolean',
|
||||||
|
value=1)
|
||||||
|
form.addField('pubsub#persist_items',
|
||||||
|
ftype='boolean',
|
||||||
|
value=1)
|
||||||
|
form.addField('pubsub#send_last_published_item',
|
||||||
|
ftype='text-single',
|
||||||
|
value='never')
|
||||||
|
form.addField('pubsub#title',
|
||||||
|
ftype='text-single',
|
||||||
|
value='Blasta Settings')
|
||||||
|
form.addField('pubsub#type',
|
||||||
|
ftype='text-single',
|
||||||
|
value='settings')
|
||||||
|
return iq
|
||||||
|
|
||||||
|
async def del_node_item(xmpp_instance, pubsub, node, item_id):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].retract(
|
||||||
|
pubsub, node, item_id, timeout=5, notify=None)
|
||||||
|
result = iq
|
||||||
|
except IqError as e:
|
||||||
|
result = e.iq['error']['text']
|
||||||
|
print(e)
|
||||||
|
except IqTimeout as e:
|
||||||
|
result = 'Timeout'
|
||||||
|
print(e)
|
||||||
|
print(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_iterator(xmpp_instance, pubsub, node, max_items, iterator):
|
||||||
|
iterator = xmpp_instance.plugin['xep_0060'].get_items(
|
||||||
|
pubsub, node, timeout=5, max_items=max_items, iterator=iterator)
|
||||||
|
return iterator
|
||||||
|
|
||||||
|
async def get_node_configuration(xmpp_instance, pubsub, node):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].get_node_config(
|
||||||
|
pubsub, node)
|
||||||
|
return iq
|
||||||
|
except (IqError, IqTimeout) as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
async def get_node_item(xmpp_instance, pubsub, node, item_id):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].get_item(
|
||||||
|
pubsub, node, item_id, timeout=5)
|
||||||
|
result = iq
|
||||||
|
except IqError as e:
|
||||||
|
result = e.iq['error']['text']
|
||||||
|
print(e)
|
||||||
|
except IqTimeout as e:
|
||||||
|
result = 'Timeout'
|
||||||
|
print(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_node_item_ids(xmpp_instance, pubsub, node):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0030'].get_items(
|
||||||
|
pubsub, node)
|
||||||
|
# Broken. See https://codeberg.org/poezio/slixmpp/issues/3548
|
||||||
|
#iq = await xmpp_instance.plugin['xep_0060'].get_item_ids(
|
||||||
|
# pubsub, node, timeout=5)
|
||||||
|
result = iq
|
||||||
|
except IqError as e:
|
||||||
|
if e.iq['error']['text'] == 'Node not found':
|
||||||
|
result = 'Node not found'
|
||||||
|
elif e.iq['error']['condition'] == 'item-not-found':
|
||||||
|
result = 'Item not found'
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
print(e)
|
||||||
|
except IqTimeout as e:
|
||||||
|
result = 'Timeout'
|
||||||
|
print(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_node_item_private(xmpp_instance, node, item_id):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0223'].retrieve(
|
||||||
|
node, item_id, timeout=5)
|
||||||
|
result = iq
|
||||||
|
except IqError as e:
|
||||||
|
result = e.iq['error']['text']
|
||||||
|
print(e)
|
||||||
|
except IqTimeout as e:
|
||||||
|
result = 'Timeout'
|
||||||
|
print(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_node_items(xmpp_instance, pubsub, node, item_ids=None, max_items=None):
|
||||||
|
try:
|
||||||
|
if max_items:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].get_items(
|
||||||
|
pubsub, node, timeout=5)
|
||||||
|
it = xmpp_instance.plugin['xep_0060'].get_items(
|
||||||
|
pubsub, node, timeout=5, max_items=max_items, iterator=True)
|
||||||
|
q = rsm.Iq()
|
||||||
|
q['to'] = pubsub
|
||||||
|
q['disco_items']['node'] = node
|
||||||
|
async for item in rsm.ResultIterator(q, 'disco_items', '10'):
|
||||||
|
print(item['disco_items']['items'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].get_items(
|
||||||
|
pubsub, node, timeout=5, item_ids=item_ids)
|
||||||
|
result = iq
|
||||||
|
except IqError as e:
|
||||||
|
if e.iq['error']['text'] == 'Node not found':
|
||||||
|
result = 'Node not found'
|
||||||
|
elif e.iq['error']['condition'] == 'item-not-found':
|
||||||
|
result = 'Item not found'
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
print(e)
|
||||||
|
except IqTimeout as e:
|
||||||
|
result = 'Timeout'
|
||||||
|
print(e)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def get_nodes(xmpp_instance):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].get_nodes()
|
||||||
|
return iq
|
||||||
|
except (IqError, IqTimeout) as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
async def is_node_exist(xmpp_instance, node_name):
|
||||||
|
iq = await XmppPubsub.get_nodes(xmpp_instance)
|
||||||
|
nodes = iq['disco_items']['items']
|
||||||
|
for node in nodes:
|
||||||
|
if node[1] == node_name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def publish_node_item(xmpp_instance, jid, node, item_id, payload):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0060'].publish(
|
||||||
|
jid, node, id=item_id, payload=payload)
|
||||||
|
print(iq)
|
||||||
|
return iq
|
||||||
|
except (IqError, IqTimeout) as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
async def publish_node_item_private(xmpp_instance, node, item_id, stanza):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0223'].store(
|
||||||
|
stanza, node, item_id)
|
||||||
|
print(iq)
|
||||||
|
return iq
|
||||||
|
except (IqError, IqTimeout) as e:
|
||||||
|
print(e)
|
||||||
|
if e.iq['error']['text'] == 'Field does not match: access_model':
|
||||||
|
return 'Error: Could not set private bookmark due to Access Model mismatch'
|
||||||
|
|
||||||
|
async def set_node_private(xmpp_instance, node):
|
||||||
|
try:
|
||||||
|
iq = await xmpp_instance.plugin['xep_0223'].configure(node)
|
||||||
|
print(iq)
|
||||||
|
return iq
|
||||||
|
except (IqError, IqTimeout) as e:
|
||||||
|
print(e)
|
|
@ -1,3 +1,6 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import webview
|
import webview
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
@ -13,7 +16,7 @@ class HtmlView:
|
||||||
# Open the link using xdg-open
|
# Open the link using xdg-open
|
||||||
subprocess.run(['xdg-open', uri], check=True)
|
subprocess.run(['xdg-open', uri], check=True)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Failed to open URL: {uri}. Error: {e}")
|
print('Failed to open URL: {}. Error: {}'.format(uri, e))
|
||||||
else:
|
else:
|
||||||
# If it is from url_instance, just load it in the webview
|
# If it is from url_instance, just load it in the webview
|
||||||
#webview.load_url(uri)
|
#webview.load_url(uri)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
This directory is meant to store hashes and tags per JID as TOML.
|
|
|
@ -1 +0,0 @@
|
||||||
This directory is contains exported nodes.
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="128px" height="128px" id="RSSicon" viewBox="0 0 256 256">
|
|
||||||
<defs>
|
|
||||||
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
|
|
||||||
<stop offset="0.0" stop-color="#E3702D"/><stop offset="0.1071" stop-color="#EA7D31"/>
|
|
||||||
<stop offset="0.3503" stop-color="#F69537"/><stop offset="0.5" stop-color="#FB9E3A"/>
|
|
||||||
<stop offset="0.7016" stop-color="#EA7C31"/><stop offset="0.8866" stop-color="#DE642B"/>
|
|
||||||
<stop offset="1.0" stop-color="#D95B29"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
|
|
||||||
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
|
|
||||||
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
|
|
||||||
<circle cx="68" cy="189" r="24" fill="#FFF"/>
|
|
||||||
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
|
|
||||||
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1 +0,0 @@
|
||||||
This directory is meant to cache nodes per JID as TOML.
|
|
64
pyproject.toml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.2"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "Blasta"
|
||||||
|
version = "1.0"
|
||||||
|
description = "A collaborative annotation 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 = [
|
||||||
|
"annotation",
|
||||||
|
"atom",
|
||||||
|
"bibliography",
|
||||||
|
"bookmark",
|
||||||
|
"collaboration",
|
||||||
|
"gemini",
|
||||||
|
"index",
|
||||||
|
"jabber",
|
||||||
|
"journal",
|
||||||
|
"news",
|
||||||
|
"social",
|
||||||
|
"syndication",
|
||||||
|
"xml",
|
||||||
|
"xmpp",
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"fastapi",
|
||||||
|
"jinja2",
|
||||||
|
"lxml",
|
||||||
|
"python-dateutil",
|
||||||
|
"python-multipart",
|
||||||
|
"slixmpp",
|
||||||
|
"tomli", # Python 3.10
|
||||||
|
"tomli-w",
|
||||||
|
"uvicorn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://schapps.woodpeckersnest.eu/blasta/"
|
||||||
|
Repository = "https://git.xmpp-it.net/sch/Blasta"
|
||||||
|
Issues = "https://git.xmpp-it.net/sch/Blasta/issues"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
blasta = "blasta.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
platforms = ["any"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"*" = ["*.atom", "*.css", "*.ico", "*.js", "*.sql", "*.svg", "*.toml", "*.xhtml", "*.xsl"]
|