Compare commits
No commits in common. "main" and "main" have entirely different histories.
109
README.md
|
@ -1,35 +1,24 @@
|
||||||
# Blasta - The agreeable and cordial civic annotation system.
|
# Blasta - The agreeable and cordial civic bookmarking system.
|
||||||
|
|
||||||
Blasta is a collaborative bookmarks manager for organizing online content.
|
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.
|
||||||
|
|
||||||
It allows you to add links to your personal collection of links, to categorize
|
What makes Blasta a collaborative system is its ability to display to you the
|
||||||
them with keywords, and to share and synchronize your collection among your own
|
links that other people have collected, as well as showing you who else has
|
||||||
software, devices, machines, and also with others.
|
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.
|
||||||
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="blasta/screenshot/browse.png" width="200px"/>](blasta/screenshot/browse.png)
|
[<img alt="browse view" src="graphic/browse.png" width="200px"/>](screenshot/browse.png)
|
||||||
[<img alt="tags view" src="blasta/screenshot/tag.png" width="200px"/>](blasta/screenshot/tag.png)
|
[<img alt="tags view" src="graphic/tag.png" width="200px"/>](screenshot/tag.png)
|
||||||
|
|
||||||
## Videos
|
|
||||||
|
|
||||||
* [Blasta - An XMPP PubSub Annotation Management System](https://video.xmpp-it.net/w/cfozoUeVLFbBFMCCSCJ1Dn) [06:38]
|
|
||||||
|
|
||||||
## Technicalities
|
## Technicalities
|
||||||
|
|
||||||
|
@ -37,8 +26,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.
|
||||||
|
@ -51,76 +40,42 @@ 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
|
||||||
|
|
||||||
## Installation
|
## Instructions
|
||||||
|
|
||||||
It is possible to install Blasta using pip and pipx.
|
Use the following commands to start Blasta.
|
||||||
|
|
||||||
#### pip inside venv
|
```shell
|
||||||
|
$ git clone https://git.xmpp-it.net/sch/Blasta
|
||||||
```
|
$ cd Blasta/
|
||||||
$ python3 -m venv .venv
|
$ python -m uvicorn blasta:app --reload
|
||||||
$ source .venv/bin/activate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Install
|
Open URL http://localhost:8000/ and connect with your Jabber ID.
|
||||||
|
|
||||||
```
|
|
||||||
$ 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
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
from blasta.version import __version__, __version_info__
|
|
||||||
|
|
||||||
print('Blasta', __version__)
|
|
|
@ -1,95 +0,0 @@
|
||||||
#!/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))
|
|
||||||
|
|
Before Width: | Height: | Size: 318 B |
|
@ -1,19 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 1,016 B |
116
blasta/config.py
|
@ -1,116 +0,0 @@
|
||||||
#!/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')
|
|
|
@ -1,310 +0,0 @@
|
||||||
-- 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;
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,256 +0,0 @@
|
||||||
#!/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]
|
|
|
@ -1,11 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,13 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,88 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,2 +0,0 @@
|
||||||
__version__ = '0.1'
|
|
||||||
__version_info__ = (0, 1)
|
|
|
@ -1,15 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,31 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,22 +0,0 @@
|
||||||
#!/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')
|
|
|
@ -1,231 +0,0 @@
|
||||||
#!/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,6 +1,3 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import webview
|
import webview
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
@ -16,7 +13,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('Failed to open URL: {}. Error: {}'.format(uri, e))
|
print(f"Failed to open URL: {uri}. Error: {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)
|
||||||
|
|
|
@ -13,22 +13,22 @@ journal = ""
|
||||||
pubsub = ""
|
pubsub = ""
|
||||||
|
|
||||||
# Bibliography
|
# Bibliography
|
||||||
node_id = "blasta:annotation:0"
|
node_id = "urn:xmpp:bibliography:0"
|
||||||
node_title = "Blasta"
|
node_title = "Blasta"
|
||||||
node_subtitle = "Annotation"
|
node_subtitle = "Bibliography"
|
||||||
|
|
||||||
# Private bibliography
|
# Private bibliography
|
||||||
node_id_private = "blasta:annotation:private:0"
|
node_id_private = "xmpp:bibliography:private:0"
|
||||||
node_title_private = "Blasta (Private)"
|
node_title_private = "Blasta (Private)"
|
||||||
node_subtitle_private = "Private annotation"
|
node_subtitle_private = "Private bibliography"
|
||||||
|
|
||||||
# Reading list
|
# Reading list
|
||||||
node_id_read = "blasta:annotation:read:0"
|
node_id_read = "xmpp:bibliography: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 = "blasta:settings:0"
|
node_settings = "xmpp: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 = [
|
1
data/README.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This directory is meant to store hashes and tags per JID as TOML.
|
1
export/README.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This directory is contains exported nodes.
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 316 B |
18
graphic/syndicate.svg
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?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>
|
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
1
items/README.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This directory is meant to cache nodes per JID as TOML.
|
|
@ -1,64 +0,0 @@
|
||||||
[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"]
|
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 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,9 +76,7 @@
|
||||||
» 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>
|
<h3>About Blasta</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
|
||||||
|
@ -116,12 +114,7 @@
|
||||||
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>
|
||||||
<p>
|
<h4>Why Blasta?</h4>
|
||||||
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
|
||||||
|
@ -135,18 +128,14 @@
|
||||||
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>
|
<h4>The things that you can do with Blasta are endless</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>
|
<h4>The difference from other services</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
|
||||||
|
@ -162,9 +151,7 @@
|
||||||
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>
|
<h4>Information that is stored by Blasta</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
|
||||||
|
@ -179,18 +166,14 @@
|
||||||
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>
|
<h4>Blasta source code</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>
|
<h4>Our motives</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
|
||||||
|
@ -203,9 +186,7 @@
|
||||||
particular for and through his racial community, which is
|
particular for and through his racial community, which is
|
||||||
eternal.
|
eternal.
|
||||||
</p>
|
</p>
|
||||||
<h4>
|
<h4>About us</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
|
||||||
|
@ -219,16 +200,12 @@
|
||||||
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>
|
<h4>Conclusion</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>
|
<p>We hope you would have productive outcomes with Blasta.</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,15 +188,13 @@
|
||||||
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>
|
<h4>Of note</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>
|
||||||
Connect to Blasta with your XMPP account or
|
Log in 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,9 +226,7 @@
|
||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
<p class="quote bottom">
|
<p class="quote bottom">
|
||||||
“Talent hits a target no one else can hit.
|
Blasta was inspired by Movim and Rivista.
|
||||||
Genius hits a target no one else can see.”
|
|
||||||
― Arthur Schopenhauer
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -64,20 +64,14 @@
|
||||||
|
|
||||||
PubSub bookmarks
|
PubSub bookmarks
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>» Information of your Jabber ID.</p>
|
||||||
» Information of your Jabber ID.
|
<h3>Your Profile</h3>
|
||||||
</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">
|
<h4 id="enrollment">Enrollment</h4>
|
||||||
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.
|
||||||
|
@ -126,9 +120,7 @@
|
||||||
therefore.
|
therefore.
|
||||||
</p>
|
</p>
|
||||||
-->
|
-->
|
||||||
<h4 id="export">
|
<h4 id="export">Export</h4>
|
||||||
Export
|
|
||||||
</h4>
|
|
||||||
<p>
|
<p>
|
||||||
Export bookmarks to a file.
|
Export bookmarks to a file.
|
||||||
</p>
|
</p>
|
||||||
|
@ -136,9 +128,7 @@
|
||||||
<!-- TODO Add XBEL, XHTML and XML -->
|
<!-- TODO Add XBEL, XHTML and XML -->
|
||||||
<dl>
|
<dl>
|
||||||
<dt>
|
<dt>
|
||||||
<strong>
|
<strong>Private</strong>
|
||||||
Private
|
|
||||||
</strong>
|
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a download="{{jabber_id}}_private.json"
|
<a download="{{jabber_id}}_private.json"
|
||||||
|
@ -149,9 +139,7 @@
|
||||||
TOML</a>.
|
TOML</a>.
|
||||||
</dd>
|
</dd>
|
||||||
<dt>
|
<dt>
|
||||||
<strong>
|
<strong>Public</strong>
|
||||||
Public
|
|
||||||
</strong>
|
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a download="{{jabber_id}}_public.json"
|
<a download="{{jabber_id}}_public.json"
|
||||||
|
@ -162,9 +150,7 @@
|
||||||
TOML</a>.
|
TOML</a>.
|
||||||
</dd>
|
</dd>
|
||||||
<dt>
|
<dt>
|
||||||
<strong>
|
<strong>Read</strong>
|
||||||
Read
|
|
||||||
</strong>
|
|
||||||
</dt>
|
</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a download="{{jabber_id}}_read.json"
|
<a download="{{jabber_id}}_read.json"
|
||||||
|
@ -176,9 +162,7 @@
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</p>
|
</p>
|
||||||
<h4 id="import">
|
<h4 id="import">Import</h4>
|
||||||
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.
|
||||||
|
@ -191,9 +175,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<label for="file">
|
<label for="file">File</label>
|
||||||
File
|
|
||||||
</label>
|
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -207,9 +189,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<label for="node">
|
<label for="node">Node</label>
|
||||||
Node
|
|
||||||
</label>
|
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -234,9 +214,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<label for="node">
|
<label for="node">Action</label>
|
||||||
Action
|
|
||||||
</label>
|
|
||||||
</strong>
|
</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -444,9 +422,7 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
proceeding.
|
proceeding.
|
||||||
</p>
|
</p>
|
||||||
<hr/>
|
<hr/>
|
||||||
<h4 id="termination">
|
<h4 id="termination">Termination</h4>
|
||||||
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.
|
||||||
|
@ -462,9 +438,7 @@ 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>
|
<h4>Delete your public bookmarks</h4>
|
||||||
Delete your public bookmarks
|
|
||||||
</h4>
|
|
||||||
<pre>
|
<pre>
|
||||||
<iq type='set'
|
<iq type='set'
|
||||||
from='{{jabber_id}}'
|
from='{{jabber_id}}'
|
||||||
|
@ -475,9 +449,7 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
</pubsub>
|
</pubsub>
|
||||||
</iq>
|
</iq>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>
|
<h4>Delete your private bookmarks</h4>
|
||||||
Delete your private bookmarks
|
|
||||||
</h4>
|
|
||||||
<pre>
|
<pre>
|
||||||
<iq type='set'
|
<iq type='set'
|
||||||
from='{{jabber_id}}'
|
from='{{jabber_id}}'
|
||||||
|
@ -488,9 +460,7 @@ retrieve items only if on a whitelist managed by the node owner.">
|
||||||
</pubsub>
|
</pubsub>
|
||||||
</iq>
|
</iq>
|
||||||
</pre>
|
</pre>
|
||||||
<h4>
|
<h4>Delete your reading list</h4>
|
||||||
Delete your reading list
|
|
||||||
</h4>
|
|
||||||
<pre>
|
<pre>
|
||||||
<iq type='set'
|
<iq type='set'
|
||||||
from='{{jabber_id}}'
|
from='{{jabber_id}}'
|
|
@ -278,19 +278,11 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br/>
|
<br/>
|
||||||
<p class="quote bottom">
|
<p class="quote bottom"
|
||||||
“Technology is extremely powerful and has the potential to
|
title="Arthur Schopenhauer speaks about Bob Wyman, Jérôme Poisson, Joe Hildebrand, Peter Saint-Andre, and Timothée Jaussoin.">
|
||||||
change the world; however, it cannot realize its full
|
“Talent hits a target no one else can hit.
|
||||||
potential unless people feel the need to use it. Some
|
Genius hits a target no one else can see.”
|
||||||
researchers agree, that to ensure the success of new
|
― Arthur Schopenhauer
|
||||||
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,9 +76,8 @@
|
||||||
</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 utilize Blasta; if you already have an XMPP account, you
|
to operate Blasta, so if you already have an XMPP account,
|
||||||
can <a href="/connect">connect</a> and start to utilize
|
you can <a href="/connect">connect</a> and start to Blasta.
|
||||||
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
|