Compare commits

..

16 commits

Author SHA1 Message Date
Schimon Jehudah, Adv.
ec5550f3d2 Add files that should have been added to previous commit. 2024-11-20 18:42:52 +02:00
Schimon Jehudah, Adv.
b5e28c8d11 Automate installation process;
Update document README;
Thank you to LeXofLeviafan from the project jarun/buku.
2024-11-20 18:38:06 +02:00
Schimon Jehudah, Adv.
30b3426dc7 Fix urllib error upon an attempt to delete a bookmark. 2024-11-17 19:38:46 +02:00
Schimon Jehudah, Adv.
6baaa38cba Fix more errors. (Thank you. roughnecks). 2024-11-17 19:16:45 +02:00
Schimon Jehudah, Adv.
519c277ea6 Fix further errors. 2024-11-17 18:56:05 +02:00
Schimon Jehudah, Adv.
9e4d6f0d32 Fix missing argument error for database file. (Thank you. roughnecks). 2024-11-17 18:30:29 +02:00
Schimon Jehudah, Adv.
09d50b9636 Fix argument to modify port. (Thank you. roughnecks) 2024-11-17 18:12:01 +02:00
Schimon Jehudah, Adv.
524d4aff07 Update instructions for pipx and sql. 2024-11-17 16:30:14 +02:00
Schimon Jehudah, Adv.
d03a76db23 SQLite: Add an SQL script to generate the SQLite database;
CSS   : Remove property height from element ID 'related-tags'.
2024-11-17 16:24:16 +02:00
Schimon Jehudah, Adv.
799cd80ebe Fix aggregation of annoations of a connecting Jabber ID. 2024-11-17 14:19:13 +02:00
Schimon Jehudah, Adv.
24dbadf7dc Fix various of errors, consequent to the restructuring of the code;
Add missing dependencies and files;
Thank you to LeXofLeviafan and roughnecks.
2024-11-17 14:00:31 +02:00
Schimon Jehudah, Adv.
77ac4c0ed9 Modularize code;
Update document README.
2024-11-13 16:41:33 +02:00
Schimon Jehudah, Adv.
0ab40eedec Python : Utilize XEP-0004: Data Forms to store configurations;
Python : Fix error in blasta_client.py (Thank you. yvo.);
XMPP   : Replace node xmpp:blasta:settings:0 by xmpp:blasta:configuration:0;
2024-09-11 07:44:18 +03:00
Schimon Jehudah, Adv.
ea255d84e0 Fix error (500) upon first connection of an account. 2024-09-05 16:27:53 +03:00
Schimon Jehudah, Adv.
c31278b576 Add functionality to search own directory;
Fix file check (Thank you roughnecks).
2024-09-05 14:58:14 +03:00
Schimon Jehudah, Adv.
21e3aa34aa Add HTTP error codes;
Correct count for filtering by tag.
2024-09-05 13:20:21 +03:00
67 changed files with 5826 additions and 5474 deletions

109
README.md
View file

@ -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
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.
Blasta is a collaborative bookmarks manager for organizing online content.
What makes Blasta a collaborative system is its ability to display to you the
links that other people have collected, as well as showing you who else has
bookmarked a specific link. You can also view the links collected by others, and
subscribe to the links of people whose lists you deem to be interesting.
It allows you to add links to your personal collection of links, to categorize
them with keywords, and to share and synchronize your collection among your own
software, devices, machines, and also with others.
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
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
desire.
## Instances
* https://blasta.woodpeckersnest.eu
## Screenshots
[<img alt="browse view" src="graphic/browse.png" width="200px"/>](screenshot/browse.png)
[<img alt="tags view" src="graphic/tag.png" width="200px"/>](screenshot/tag.png)
[<img alt="browse view" src="blasta/screenshot/browse.png" width="200px"/>](blasta/screenshot/browse.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
@ -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
following XMPP specifications:
- [XEP-0163: Personal Eventing Protocol](https://xmpp.org/extensions/xep-0163.html)
- [XEP-0060: Publish-Subscribe](https://xmpp.org/extensions/xep-0060.html)
* [XEP-0163: Personal Eventing Protocol](https://xmpp.org/extensions/xep-0163.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
system nor an account system, of its own.
@ -40,42 +51,76 @@ The connection to the Blasta system is made with XMPP accounts.
## Features
- Private bookmarks;
- Public bookmarks;
- Read list;
- Search;
- Syndication;
- Tags.
* Private bookmarks;
* Public bookmarks;
* Read list;
* Search;
* Syndication;
* Tags.
## Future features
- ActivityPub;
- Federation;
- Filters;
- Pin;
- Publish-Subscribe;
- Report.
* ActivityPub;
* Federation;
* Filters;
* Pin;
* Publish-Subscribe;
* Report.
## Requirements
* Python >= 3.5
* fastapi
* lxml
* python-dateutil
* python-multipart
* slixmpp
* tomllib (Python <= 3.10)
* uvicorn
## Instructions
## Installation
Use the following commands to start Blasta.
It is possible to install Blasta using pip and pipx.
```shell
$ git clone https://git.xmpp-it.net/sch/Blasta
$ cd Blasta/
$ python -m uvicorn blasta:app --reload
#### pip inside venv
```
$ 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

5330
blasta.py

File diff suppressed because it is too large Load diff

3
blasta/__init__.py Normal file
View file

@ -0,0 +1,3 @@
from blasta.version import __version__, __version_info__
print('Blasta', __version__)

95
blasta/__main__.py Normal file
View 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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View file

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 316 B

View file

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 316 B

View 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

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -161,9 +161,9 @@ form > * {
#related-tags {
background-color: #eee;
/* height: 90vh; */
min-width: 200px;
padding: 0 0.5em 1em 1em;
height: 90vh;
width: 15%;
/* float: right; */
/* width: 200px; */

View file

@ -76,7 +76,9 @@
» Information and resources about Blasta, collaborative
bookmarks with an Irish manner.
</p>
<h3>About Blasta</h3>
<h3>
About Blasta
</h3>
<p>
Blasta is a collaborative bookmarks manager for organizing
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
that you desire.
</p>
<h4>Why Blasta?</h4>
<p>
Blasta was inspired by projects Movim and Rivista.
</p>
<h4>
Why Blasta?
</h4>
<p>
Corporate search engines are archaic and outdated, and often
prioritize their own interests, leading to censorship and
@ -128,14 +135,18 @@
references and resources that you need in order to be
productive and get that you need.
</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>
Blasta is an open-ended indexing system, and, as such, it
provides a versatile platform with which you have the
ability to tailor its usage according to your desired
preferences. <a href="/help/about/ideas">Learn more</a>.
</p>
<h4>The difference from other services</h4>
<h4>
The difference from other services
</h4>
<p>
Unlike some so called "social" bookmarking systems, Blasta
does not own your information; your bookmarks are
@ -151,7 +162,9 @@
your personal XMPP account under PubSub node
<code>urn:xmpp:bibliography:0</code>.
</p>
<h4>Information that is stored by Blasta</h4>
<h4>
Information that is stored by Blasta
</h4>
<p>
In order for Blasta to facilitate sharing of information and
accessibility to information, Blasta aggregates your own
@ -166,14 +179,18 @@
all of their owners as private and no one else has stored
them in a public fashion (i.e. not classified private).
</p>
<h4>Blasta source code</h4>
<h4>
Blasta source code
</h4>
<p>
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
<a href="https://git.xmpp-it.net/sch/Blasta">
git.xmpp-it.net</a>.
</p>
<h4>Our motives</h4>
<h4>
Our motives
</h4>
<p>
We are adopting the attitude towards life and towards death,
which was implicit in the old Vikings' and in Schopenhauer's
@ -186,7 +203,9 @@
particular for and through his racial community, which is
eternal.
</p>
<h4>About us</h4>
<h4>
About us
</h4>
<p>
Blasta was proudly made in the Republic of Ireland, by a
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
idea of XMPP PubSub bookmarks.
</p>
<h4>Conclusion</h4>
<h4>
Conclusion
</h4>
<p>
Blasta is for you to enjoy, excite, instigate, investigate,
learn and research.
</p>
<p>We hope you would have productive outcomes with Blasta.</p>
<p>
We hope you would have productive outcomes with Blasta.
</p>
<br/>
<p class="quote bottom">
“All you can take with you; is that which you have given

View file

@ -168,7 +168,7 @@
xmpp.org
</a>
&#8203;&#8202;
<a href="https://libervia.org/">
<a href="https://libervia.org">
libervia.org
</a>
</p>
@ -188,13 +188,15 @@
xmpp.org
</a>
&#8203;&#8202;
<a href="https://movim.eu/">
<a href="https://movim.eu">
movim.eu
</a>
</p>
</li>
</ul>
<h4>Of note</h4>
<h4>
Of note
</h4>
<p>
These type of technologies are public information for over
a couple of decades (i.e. more than 20 years); and people

View file

@ -69,7 +69,7 @@
<label for="remember">Remember</label -->
</form>
<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.
</p>
</div>

View file

@ -226,7 +226,9 @@
</p>
<br/>
<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>
</div>
</div>

View file

@ -64,14 +64,20 @@
&nbsp;
PubSub bookmarks
</h2>
<p>» Information of your Jabber ID.</p>
<h3>Your Profile</h3>
<p>
» Information of your Jabber ID.
</p>
<h3>
Your profile
</h3>
<p>
This page provides a general survey of your XMPP account and
stored bookmarks.
</p>
<!--
<h4 id="enrollment">Enrollment</h4>
<h4 id="enrollment">
Enrollment
</h4>
<p>
Blasta does not automatically include your public bookmarks
to its database.
@ -120,7 +126,9 @@
therefore.
</p>
-->
<h4 id="export">Export</h4>
<h4 id="export">
Export
</h4>
<p>
Export bookmarks to a file.
</p>
@ -128,7 +136,9 @@
<!-- TODO Add XBEL, XHTML and XML -->
<dl>
<dt>
<strong>Private</strong>
<strong>
Private
</strong>
</dt>
<dd>
<a download="{{jabber_id}}_private.json"
@ -139,7 +149,9 @@
TOML</a>.
</dd>
<dt>
<strong>Public</strong>
<strong>
Public
</strong>
</dt>
<dd>
<a download="{{jabber_id}}_public.json"
@ -150,7 +162,9 @@
TOML</a>.
</dd>
<dt>
<strong>Read</strong>
<strong>
Read
</strong>
</dt>
<dd>
<a download="{{jabber_id}}_read.json"
@ -162,7 +176,9 @@
</dd>
</dl>
</p>
<h4 id="import">Import</h4>
<h4 id="import">
Import
</h4>
<p>
Import bookmarks from a file, and choose a node to import
your bookmarks to.
@ -175,7 +191,9 @@
<tr>
<td>
<strong>
<label for="file">File</label>
<label for="file">
File
</label>
</strong>
</td>
<td>
@ -189,7 +207,9 @@
<tr>
<td>
<strong>
<label for="node">Node</label>
<label for="node">
Node
</label>
</strong>
</td>
<td>
@ -214,7 +234,9 @@
<tr>
<td>
<strong>
<label for="node">Action</label>
<label for="node">
Action
</label>
</strong>
</td>
<td>
@ -422,7 +444,9 @@ retrieve items only if on a whitelist managed by the node owner.">
proceeding.
</p>
<hr/>
<h4 id="termination">Termination</h4>
<h4 id="termination">
Termination
</h4>
<p>
Due to security concerns, Blasta does not have a built-in
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-plus.com">Psi+</a>.
</p>
<h4>Delete your public bookmarks</h4>
<h4>
Delete your public bookmarks
</h4>
<pre>
&lt;iq type='set'
from='{{jabber_id}}'
@ -449,7 +475,9 @@ retrieve items only if on a whitelist managed by the node owner.">
&lt;/pubsub&gt;
&lt;/iq&gt;
</pre>
<h4>Delete your private bookmarks</h4>
<h4>
Delete your private bookmarks
</h4>
<pre>
&lt;iq type='set'
from='{{jabber_id}}'
@ -460,7 +488,9 @@ retrieve items only if on a whitelist managed by the node owner.">
&lt;/pubsub&gt;
&lt;/iq&gt;
</pre>
<h4>Delete your reading list</h4>
<h4>
Delete your reading list
</h4>
<pre>
&lt;iq type='set'
from='{{jabber_id}}'

View file

@ -278,11 +278,19 @@
</li>
</ul>
<br/>
<p class="quote bottom"
title="Arthur Schopenhauer speaks about Bob Wyman, Jérôme Poisson, Joe Hildebrand, Peter Saint-Andre, and Timothée Jaussoin.">
“Talent hits a target no one else can hit.
Genius hits a target no one else can see.”
― Arthur Schopenhauer
<p class="quote bottom">
“Technology is extremely powerful and has the potential to
change the world; however, it cannot realize its full
potential unless people feel the need to use it. Some
researchers agree, that to ensure the success of new
technology, the focus should be on the peoples 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>
</div>
</div>

View file

@ -76,8 +76,9 @@
</h3>
<p>
As with email, you need an account with a service provider
to operate Blasta, so if you already have an XMPP account,
you can <a href="/connect">connect</a> and start to Blasta.
to utilize Blasta; if you already have an XMPP account, you
can <a href="/connect">connect</a> and start to utilize
Blasta.
</p>
<p>
If you do not have an XMPP account, yet, you can use a

116
blasta/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, '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
View 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;

View file

@ -13,22 +13,22 @@ journal = ""
pubsub = ""
# Bibliography
node_id = "urn:xmpp:bibliography:0"
node_id = "blasta:annotation:0"
node_title = "Blasta"
node_subtitle = "Bibliography"
node_subtitle = "Annotation"
# Private bibliography
node_id_private = "xmpp:bibliography:private:0"
node_id_private = "blasta:annotation:private:0"
node_title_private = "Blasta (Private)"
node_subtitle_private = "Private bibliography"
node_subtitle_private = "Private annotation"
# Reading list
node_id_read = "xmpp:bibliography:read:0"
node_id_read = "blasta:annotation:read:0"
node_title_read = "Blasta (Read)"
node_subtitle_read = "Reading list"
# Settings node
node_settings = "xmpp:blasta:settings:0"
node_settings = "blasta:settings:0"
# Acceptable protocol types that would be aggregated to the Blasta database
schemes = [

2041
blasta/database/sqlite.py Normal file

File diff suppressed because it is too large Load diff

2306
blasta/http/instance.py Normal file

File diff suppressed because it is too large Load diff

View file

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 262 KiB

View file

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

View 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
View 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
View 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
View 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

View 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
View file

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

15
blasta/xmpp/form.py Normal file
View 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
View 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
View 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
View 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)

View file

@ -1,3 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import webview
import subprocess
import sys
@ -13,7 +16,7 @@ class HtmlView:
# Open the link using xdg-open
subprocess.run(['xdg-open', uri], check=True)
except subprocess.CalledProcessError as e:
print(f"Failed to open URL: {uri}. Error: {e}")
print('Failed to open URL: {}. Error: {}'.format(uri, e))
else:
# If it is from url_instance, just load it in the webview
#webview.load_url(uri)

View file

@ -1 +0,0 @@
This directory is meant to store hashes and tags per JID as TOML.

View file

@ -1 +0,0 @@
This directory is contains exported nodes.

View file

@ -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

View file

@ -1 +0,0 @@
This directory is meant to cache nodes per JID as TOML.

View file

@ -1,30 +0,0 @@
map $http_upgrade $connection_upgrade { default upgrade; '' close; } server {
server_name blasta.example.tld;
access_log /var/log/nginx/blasta/access.log;
error_log /var/log/nginx/blasta/error.log;
location / {
proxy_pass http://127.0.0.1:11112;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600;
}
listen [::]:443 ssl http2;
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/blasta.example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blasta.example.tld/privkey.pem;
}
server {
listen [::]:80;
listen 80;
server_name blasta.example.tld;
location / {
return 301 https://$host$request_uri;
}
}

64
pyproject.toml Normal file
View 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"]

View file

@ -1,16 +0,0 @@
# /etc/systemd/system/blasta.service
[Unit]
Description=Blasta Bookmarks Service
After=network.target
Requisite=network-online.target
[Service]
ExecStart=/usr/bin/uvicorn blasta:app --port 11112
User=blasta
Restart=always
RestartSec=20s
Type=simple
WorkingDirectory=/home/blasta/Blasta
[Install]
WantedBy=multi-user.target