diff --git a/README.md b/README.md
index 1b1da43..2a976dd 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,77 @@
-# Blasta
+# Blasta - The agreeable and cordial civic bookmarking system.
-A federated PubSub bookmarking system for XMPP.
\ No newline at end of file
+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.
+
+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.
+
+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.
+
+# Technicalities
+
+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)
+
+Blasta operates as an XMPP client, and therefore, does not have a bookmarks
+system nor an account system, of its own.
+
+Blasta has a database which is compiled by aggregating the bookmarks of people
+who are participating in the Blasta system, and that database is utilized to
+relate accounts and shared links.
+
+The connection to the Blasta system is made with XMPP accounts.
+
+## Features
+
+- Private bookmarks;
+- Public bookmarks;
+- Read list;
+- Search;
+- Syndication;
+- Tags.
+
+## Future features
+
+- ActivityPub;
+- Atom Syndication Format;
+- Federation;
+- Filters;
+- Pin directory;
+- Publish-Subscribe.
+
+## Requirements
+
+* Python >= 3.5
+* fastapi
+* lxml
+* slixmpp
+* tomllib (Python <= 3.10)
+* uvicorn
+
+## Instructions
+
+Use the following commands to start Blasta.
+
+```shell
+$ git clone https://git.xmpp-it.net/sch/Blasta
+$ cd Blasta/
+python -m uvicorn blasta:app --reload
+```
+
+Open URL http://localhost:8000/ and connect with your Jabber ID.
+
+## License
+
+AGPL-3.0 only.
diff --git a/blasta.py b/blasta.py
new file mode 100644
index 0000000..1201ffa
--- /dev/null
+++ b/blasta.py
@@ -0,0 +1,5051 @@
+#!/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 asyncio
+from asyncio import Lock
+from datetime import datetime
+from fastapi import Cookie, FastAPI, File, Form, HTTPException, Request, Response, UploadFile
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import FileResponse, HTMLResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+import hashlib
+import json
+import logging
+from os import mkdir
+from os.path import getsize, exists
+import random
+import slixmpp
+from slixmpp import ClientXMPP
+from slixmpp.exceptions import IqError, IqTimeout
+import slixmpp.plugins.xep_0060.stanza.pubsub as pubsub
+import slixmpp.plugins.xep_0059.rsm as rsm
+from sqlite3 import connect, Error, IntegrityError
+from starlette.responses import RedirectResponse
+import sys
+import time
+import tomli_w
+from typing import Optional
+import urllib.parse
+import uvicorn
+import xml.etree.ElementTree as ET
+
+
+try:
+ import tomllib
+except:
+ import tomli as tomllib
+
+DBLOCK = Lock()
+
+class Data:
+
+ def cache_items_and_tags(entries, jid, tag=None):
+ """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:
+ 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
+ else:
+ 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 tag: del tags[tag]
+ if item_ids:
+ directory = 'data/{}/'.format(jid)
+ if not exists(directory):
+ mkdir(directory)
+ if tag:
+ filename = 'data/{}/{}.toml'.format(jid, tag)
+ # Add support for search query
+ #if tag:
+ # filename = 'data/{}/query:{}.toml'.format(jid, query)
+ #if tag:
+ # filename = 'data/{}/tag:{}.toml'.format(jid, tag)
+ else:
+ filename = 'data/{}.toml'.format(jid)
+ data = {
+ 'item_ids' : item_ids,
+ 'tags' : tags}
+ Data.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 = Syndication.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(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 = Syndication.extract_items(item_payload, limit)
+ url_hash = Utilities.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))
+ db_file = 'main.sqlite'
+ instances = SQLite.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(jabber_id, node, url_hash):
+ filename_items = 'items/' + jabber_id + '.toml'
+ entries_cache = Data.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
+ Data.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(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, slixmpp.stanza.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 = 'items/' + jabber_id + '.toml'
+ if not exists(filename_items) or getsize(filename_items) == 13:
+ iq = await XmppPubsub.get_node_items(xmpp_instance, jabber_id, node_id)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ entries_cache_node = Data.extract_iq_items_extra(iq, jabber_id)
+ data_items = {node_type : entries_cache_node}
+ Data.save_to_toml(filename_items, data_items)
+ else:
+ print('iq problem')
+ breakpoint()
+ print('iq problem')
+ else:
+ entries_cache = Data.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]
+ db_file = 'main.sqlite'
+
+ # 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 SQLite.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, slixmpp.stanza.iq.Iq):
+ entries_iq = Data.extract_iq_items_extra(iq, jabber_id)
+ entries_cache_node_new += entries_iq
+ else:
+ print('iq problem')
+ breakpoint()
+ print('iq problem')
+
+ entries_cache_node += entries_cache_node_new
+
+ if node_type == 'public':
+ # Fast (low I/O)
+ if not SQLite.get_jid_id_by_jid(db_file, jabber_id):
+ await SQLite.set_jid(db_file, jabber_id)
+ #await SQLite.add_new_entries(db_file, entries)
+ await SQLite.add_tags(db_file, entries_cache_node)
+ # Slow (high I/O)
+ for entry in entries_cache_node:
+ url_hash = entry['url_hash']
+ if not SQLite.get_entry_id_by_url_hash(db_file, url_hash):
+ await SQLite.add_new_entries(db_file, entries_cache_node)
+ await SQLite.associate_entries_tags_jids(db_file, entry)
+ #elif not SQLite.is_jid_associated_with_url_hash(db_file, jabber_id, url_hash):
+ # await SQLite.associate_entries_tags_jids(db_file, entry)
+ else:
+ await SQLite.associate_entries_tags_jids(db_file, entry)
+
+ data_items = entries_cache
+ Data.save_to_toml(filename_items, data_items)
+ return ['fine', iq] # TODO Remove this line
+ else:
+ return ['error', iq]
+
+class HttpInstance:
+ def __init__(self, accounts, sessions):
+
+ self.app = FastAPI()
+ #templates = Jinja2Templates(directory='xhtml/template')
+ templates = Jinja2Templates(directory='xhtml')
+
+ self.app.mount('/data', StaticFiles(directory='data'), name='data')
+ self.app.mount('/export', StaticFiles(directory='export'), name='export')
+ self.app.mount('/graphic', StaticFiles(directory='graphic'), name='graphic')
+ self.app.mount('/stylesheet', StaticFiles(directory='stylesheet'), name='stylesheet')
+ #self.app.mount('/policy', StaticFiles(directory='xhtml/policy'), name='policy')
+ #self.app.mount('/xhtml', StaticFiles(directory='xhtml'), name='xhtml')
+
+ filename_configuration = 'configuration.toml'
+ data = Data.open_file_toml(filename_configuration)
+
+ contacts = data['contacts']
+ contact_email = contacts['email']
+ contact_irc_channel = contacts['irc_channel']
+ contact_irc_server = contacts['irc_server']
+ contact_mix = contacts['mix']
+ contact_muc = contacts['muc']
+ contact_xmpp = contacts['xmpp']
+
+ settings = data['settings']
+
+ jabber_id_pubsub = settings['pubsub']
+ journal = settings['journal']
+
+ node_id_public = settings['node_id']
+ node_title_public = settings['node_title']
+ node_subtitle_public = settings['node_subtitle']
+
+ node_id_private = settings['node_id_private']
+ node_title_private = settings['node_title_private']
+ node_subtitle_private = settings['node_subtitle_private']
+
+ node_id_read = settings['node_id_read']
+ node_title_read = settings['node_title_read']
+ node_subtitle_read = settings['node_subtitle_read']
+
+ nodes = {
+ 'public' : {
+ 'name' : node_id_public,
+ 'title' : node_title_public,
+ 'subtitle' : node_subtitle_public,
+ 'access_model' : 'presence'},
+ 'private' : {
+ 'name' : node_id_private,
+ 'title' : node_title_private,
+ 'subtitle' : node_subtitle_private,
+ 'access_model' : 'whitelist'},
+ 'read' : {
+ 'name' : node_id_read,
+ 'title' : node_title_read,
+ 'subtitle' : node_subtitle_read,
+ 'access_model' : 'whitelist'}
+ }
+
+ origins = [
+ "http://localhost",
+ "http://localhost:8080",
+ "http://127.0.0.1",
+ "http://127.0.0.1:8080",
+ ]
+
+ self.app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ # This is workaround for setting a "cookie" issue
+ # It appears that there is a problem to set or send a "cookie" when a template is returned.
+ @self.app.middleware('http')
+ async def middleware_handler(request: Request, call_next):
+
+ # Handle URL query
+ if request.url.path != '/save':
+ param_url = request.query_params.get('url', '') or None
+ param_hash = request.query_params.get('hash', '') or None
+ if param_hash:
+ return RedirectResponse(url='/url/' + param_hash)
+ if param_url:
+ url_hash = Utilities.hash_url_to_md5(param_url)
+ return RedirectResponse(url='/url/' + url_hash)
+
+ response = await call_next(request)
+ jabber_id = session_key = None
+
+ infoo = {
+ 'accounts' : accounts,
+ 'sessions' : sessions
+ }
+ print(infoo)
+
+ # Handle credentials (i.e. so called "cookies")
+ if request.url.path == '/disconnect':
+ jid = request.cookies.get('jabber_id')
+ if jid in accounts: del accounts[jid]
+ if jid in sessions: del sessions[jid]
+ response.delete_cookie('session_key')
+ response.delete_cookie('jabber_id')
+ else:
+ try:
+ # Access the variable from the request state
+ jabber_id = request.app.state.jabber_id
+ except Exception as e:
+ print(request.cookies.get('jabber_id'))
+ print(e)
+ pass
+ try:
+ # Access the variable from the request state
+ session_key = request.app.state.session_key
+ except Exception as e:
+ print(request.cookies.get('session_key'))
+ print(e)
+ pass
+ if jabber_id and session_key:
+ print(['Establishing a sessiong for:', jabber_id, session_key])
+ response.set_cookie(key='jabber_id', value=jabber_id)
+ response.set_cookie(key='session_key', value=session_key)
+# del request.app.state.jabber_id
+# del request.app.state.session_key
+ request.app.state.jabber_id = request.app.state.session_key = None
+ return response
+
+# response.set_cookie(key='session', value=str(jid) + '/' + str(session_key))
+# response.set_cookie(key='session',
+# value=jid + '/' + session_key,
+# expires=datetime.now().replace(tzinfo=timezone.utc) + timedelta(days=30),
+# max_age=3600,
+# domain='localhost',
+# path='/',
+# secure=True,
+# httponly=False, # True
+# samesite='lax')
+
+ @self.app.exception_handler(404)
+ def not_found_exception_handler(request: Request, exc: HTTPException):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ message = 'Blasta system message » Not Found.'
+ description = 'Not found (404)'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.exception_handler(405)
+ def not_allowed_exception_handler(request: Request, exc: HTTPException):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ message = 'Blasta system message » Method Not Allowed.'
+ description = 'Not allowed (405)'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.exception_handler(500)
+ def internal_error__exception_handler(request: Request, exc: HTTPException):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ message = 'Blasta system message » Internal Server Error.'
+ description = 'Internal error (500)'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/connect')
+ def connect_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ response = RedirectResponse(url='/jid/' + jabber_id)
+ else:
+ template_file = 'connect.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/contact')
+ def contact_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'contact.xhtml'
+ template_dict = {
+ 'contact_email' : contact_email,
+ 'contact_irc_channel' : contact_irc_channel,
+ 'contact_irc_server' : contact_irc_server,
+ 'contact_mix' : contact_mix,
+ 'contact_muc' : contact_muc,
+ 'contact_xmpp' : contact_xmpp,
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/disconnect')
+ def disconnect_get(request: Request,
+ response: Response,
+ jabber_id: str = Cookie(None),
+ session_key: str = Cookie(None)):
+# response.set_cookie(max_age=0, value='', key='jabber_id')
+# response.set_cookie(max_age=0, value='', key='session_key')
+ response = RedirectResponse(url='/')
+ response.delete_cookie('session_key')
+ response.delete_cookie('jabber_id')
+ return response
+
+ @self.app.get('/favicon.ico', include_in_schema=False)
+ def favicon_get():
+ return FileResponse('graphic/blasta.ico')
+
+ @self.app.get('/help')
+ def help_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'help.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about')
+ def help_about_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'about.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/ideas')
+ def help_about_ideas_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ protocol = request.url.scheme
+ hostname = request.url.hostname + ':' + str(request.url.port)
+ origin = protocol + '://' + hostname
+ template_file = 'ideas.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'origin' : origin}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/philosophy')
+ def help_about_philosophy_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'philosophy.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/projects')
+ def help_about_projects_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'projects.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/software')
+ def help_about_software_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'software.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/thanks')
+ def help_about_thanks_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'thanks.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/xmpp')
+ def help_about_xmpp_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'xmpp.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/xmpp/atomsub')
+ def help_about_xmpp_atomsub_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'atomsub.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/xmpp/libervia')
+ def help_about_xmpp_libervia_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'libervia.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/xmpp/movim')
+ def help_about_xmpp_movim_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'movim.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/about/xmpp/pubsub')
+ def help_about_xmpp_pubsub_get(request: Request):
+ date_now_iso = datetime.now().isoformat()
+ date_now_readable = Utilities.convert_iso8601_to_readable(date_now_iso)
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'pubsub.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'date_now_iso' : date_now_iso,
+ 'date_now_readable' : date_now_readable,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/feeds')
+ def help_about_feeds_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'feeds.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/questions')
+ def help_questions_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'questions.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/syndication')
+ def help_syndication_get(request: Request):
+ hostname = request.url.hostname + ':' + str(request.url.port)
+ protocol = request.url.scheme
+ origin = protocol + '://' + hostname
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'syndication.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'origin' : origin,
+ 'pubsub_jid' : jabber_id_pubsub}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/help/utilities')
+ def help_utilities_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ hostname = request.url.hostname
+ protocol = request.url.scheme
+ hostname = request.url.hostname + ':' + str(request.url.port)
+ origin = protocol + '://' + hostname
+ bookmarklet = 'location.href=`' + origin + '/save?url=${encodeURIComponent(window.location.href)}&title=${encodeURIComponent(document.title)}`;'
+ template_file = 'utilities.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'bookmarklet' : bookmarklet,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/jid', response_class=HTMLResponse)
+ @self.app.post('/jid')
+ async def jid_get(request: Request, response : Response):
+ node_type = 'public'
+ path = 'jid'
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ node_id = nodes[node_type]['name']
+ result, reason = await Data.update_cache_and_database(xmpp_instance, jabber_id, node_type, node_id)
+ if result == 'error':
+ message = 'XMPP system message » {}.'.format(reason)
+ description = 'IQ Error'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ response = await jid_main_get(request, node_type, path, jid=jabber_id)
+ return response
+ else:
+ description = 'An XMPP account is required'
+ message = 'Blasta system message » Please connect with your XMPP account to view this directory.'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/jid/{jid}')
+ @self.app.post('/jid/{jid}')
+ async def jid_jid_get(request: Request, response : Response, jid):
+ response = await jid_main_get(request, node_type='public', path='jid', jid=jid)
+ return response
+
+ async def jid_main_get(request: Request, node_type=None, path=None, jid=None):
+ ask = invite = name = origin = start = ''
+# pubsub_jid = syndicate = jid
+# message = 'Find and share bookmarks with family and friends!'
+# description = 'Bookmarks of {}'.format(jid)
+ max_count = 10
+ entries = None
+ related_tags = None
+ tags_dict = None
+ param_filetype = request.query_params.get('filetype', '') or None
+ param_page = request.query_params.get('page', '') or None
+ param_protocol = request.query_params.get('protocol', '') or None
+ param_query = request.query_params.get('q', '') or None
+ if param_query: param_query = param_query.strip()
+ param_tags = request.query_params.get('tags', '') or None
+ param_tld = request.query_params.get('tld', '') or None
+ if param_page:
+ try:
+ page = int(param_page)
+ page_next = page + 1
+ page_prev = page - 1
+ except:
+ page = 1
+ page_next = 2
+ else:
+ page = 1
+ page_next = 2
+ page_prev = page - 1
+ index_first = (page - 1)*10
+ index_last = index_first+10
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id == jid or node_type in ('private', 'read'):
+ xmpp_instance = accounts[jabber_id]
+ # NOTE You need something other than an iterator (XEP-0059).
+ # You need a PubSub key that would hold tags.
+ filename_items = 'items/' + jabber_id + '.toml'
+ # NOTE Does it work?
+ # It does not seem to actually filter tags.
+ # NOTE Yes. It does work.
+ # See function "cache_items_and_tags".
+ if param_tags or param_tld or param_filetype or param_protocol:
+ tags_list = param_tags.split('+')
+ if len(tags_list) == 1:
+ tag = param_tags
+ entries_cache = Data.open_file_toml(filename_items)
+ entries_cache_node = entries_cache[node_type]
+ filename_cache = 'data/{}/{}.toml'.format(jid, tag)
+ Data.cache_items_and_tags(entries_cache_node, jid, tag)
+ if exists(filename_cache) or getsize(filename_cache):
+ data = Data.open_file_toml(filename_cache)
+ item_ids_all = data['item_ids']
+ related_tags = data['tags']
+ if len(item_ids_all) <= index_last:
+ index_last = len(item_ids_all)
+ page_next = None
+ item_ids_selection = []
+ for item_id in item_ids_all[index_first:index_last]:
+ item_ids_selection.append(item_id)
+ entries = []
+ for entry in entries_cache_node:
+ for item_id in item_ids_selection:
+ if entry['url_hash'] == item_id:
+ entries.append(entry)
+ for entry in entries:
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(entry['published'])
+ entry['tags'] = entry['tags'][:5]
+ description = 'Your {} bookmarks tagged with "{}"'.format(node_type, tag)
+ message = 'Listing {} bookmarks {} - {} out of {}.'.format(node_type, index_first+1, index_last, len(item_ids_all))
+ #item_id_next = entries[len(entries)-1]
+ else:
+ description = 'No {} bookmarks tagged with "{}" were found for {}'.format(node_type, tag, jid)
+ message = 'Blasta system message » No entries.'
+ page_next = None
+ page_prev = None
+ elif len(tag_list) > 1:
+ pass #TODO Support multiple tags
+# if not param_tags and not param_tld and not param_filetype and not param_protocol and not param_url and not param_hash:
+ else:
+ name = jabber_id.split('@')[0]
+ entries_cache = Data.open_file_toml(filename_items)
+ entries_cache_node = entries_cache[node_type]
+ filename_cache = 'data/{}.toml'.format(jid)
+ #if len(entries_cache_node) and not exists(filename_cache):
+ Data.cache_items_and_tags(entries_cache_node, jid)
+ if exists(filename_cache) or getsize(filename_cache):
+ data = Data.open_file_toml(filename_cache)
+ item_ids_all = data['item_ids']
+ related_tags = data['tags']
+ if len(item_ids_all) <= index_last:
+ index_last = len(item_ids_all)
+ page_next = None
+ item_ids_selection = []
+ for item_id in item_ids_all[index_first:index_last]:
+ item_ids_selection.append(item_id)
+ entries = []
+ for entry in entries_cache_node:
+ for item_id in item_ids_selection:
+ if entry['url_hash'] == item_id:
+ entries.append(entry)
+ for entry in entries:
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(entry['published'])
+ entry['tags'] = entry['tags'][:5]
+ description = 'Your {} bookmarks'.format(node_type)
+ message = 'Listing {} bookmarks {} - {} out of {}.'.format(node_type, index_first+1, index_last, len(item_ids_all))
+ #item_id_next = entries[len(entries)-1]
+ else:
+ description = 'Your bookmarks directory appears to be empty'
+ message = 'Blasta system message » Zero count.'
+ start = True
+ elif jabber_id in accounts:
+ # NOTE Keep this IQ function call as an exception.
+ # If one wants to see contents of someone else, an
+ # authorization is required.
+ # NOTE It might be wiser to use cached items or item identifiers
+ # provided that the viewer is authorized to view items.
+ xmpp_instance = accounts[jabber_id]
+ db_file = 'main.sqlite'
+ tags_dict = {}
+ if param_query:
+ description = 'Bookmarks from {} with "{}"'.format(jid, param_query)
+ entries_database = SQLite.get_entries_by_jid_and_query(db_file, jid, param_query, index_first)
+ entries_count = SQLite.get_entries_count_by_jid_and_query(db_file, jid, param_query)
+ for tag, instances in SQLite.get_30_tags_by_jid_and_query(db_file, jid, param_query, index_first):
+ tags_dict[tag] = instances
+ elif param_tags:
+ description = 'Bookmarks from {} tagged with "{}"'.format(jid, param_tags)
+ entries_database = SQLite.get_entries_by_jid_and_tag(db_file, jid, param_tags, index_first)
+ entries_count = SQLite.get_entries_count_by_jid_and_tag(db_file, jid, param_tags)
+ for tag, instances in SQLite.get_30_tags_by_jid_and_tag(db_file, jid, param_tags, index_first):
+ tags_dict[tag] = instances
+ else:
+ description = 'Bookmarks from {}'.format(jid)
+ entries_database = SQLite.get_entries_by_jid(db_file, jid, index_first)
+ entries_count = SQLite.get_entries_count_by_jid(db_file, jid)
+ for tag, instances in SQLite.get_30_tags_by_jid(db_file, jid, index_first):
+ tags_dict[tag] = instances
+ if entries_count:
+ entries = []
+ for entry in entries_database:
+ tags_sorted = []
+ for tag in SQLite.get_tags_by_entry_id(db_file, entry[0]):
+ tags_sorted.append(tag[0])
+ entry_jid = SQLite.get_jid_by_jid_id(db_file, entry[5])
+ entries.append(
+ {'title' : entry[3],
+ 'link' : entry[2],
+ 'summary' : entry[4],
+ 'published' : entry[6],
+ 'updated' : entry[7],
+ 'tags' : tags_sorted,
+ 'url_hash' : Utilities.hash_url_to_md5(entry[2]), #entry[1]
+ 'jid' : entry_jid,
+ 'name' : entry_jid, # jid.split('@')[0] if '@' in jid else jid,
+ 'instances' : entry[8]})
+ for entry in entries:
+ try:
+ date_iso = entry['published']
+ date_wrt = Utilities.convert_iso8601_to_readable(date_iso)
+ entry['published_mod'] = date_wrt
+ except:
+ print('ERROR: Probably due to an attempt to convert a non ISO 8601.')
+ print(entry['published'])
+ print(entry['published_mod'])
+ print(entry)
+ index_last = index_first+len(entries_database)
+ if entries_count <= index_last:
+ index_last = entries_count
+ page_next = None
+ message = 'Listing bookmarks {} - {} out of {}.'.format(index_first+1, index_last, entries_count)
+ else:
+ # TODO Check permission, so there is no unintended continuing to cached data which is not authorized for.
+ iq = await XmppPubsub.get_node_item_ids(xmpp_instance, jid, node_id_public)
+ if isinstance(iq, slixmpp.stanza.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/' + jid + '.toml'
+ #Data.save_to_toml(filename_item_ids, data_item_ids)
+
+ item_ids_all = iq_items_remote_name
+ #item_ids_all = data['item_ids']
+ #related_tags = data['tags']
+ if len(item_ids_all) <= index_last:
+ page_next = None
+ index_last = len(item_ids_all)
+ item_ids_selection = []
+ for item_id in item_ids_all[index_first:index_last]:
+ item_ids_selection.append(item_id)
+
+ iq = await XmppPubsub.get_node_items(xmpp_instance, jid, node_id_public, item_ids_selection)
+ entries = Data.extract_iq_items_extra(iq, jid)
+ if entries:
+ for entry in entries:
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(entry['published'])
+ message = 'Listing bookmarks {} - {} out of {}.'.format(index_first+1, index_last, len(item_ids_all))
+ description = 'Bookmarks from {}'.format(jid)
+ else:
+ message = 'Blasta system message » Zero count.'
+ description = 'Bookmarks directory appears to be empty'
+ invite = True
+ else:
+ message = 'XMPP system message » {}.'.format(iq)
+ name = jid.split('@')[0]
+ path = 'error'
+ if not iq:
+ message = 'XMPP system message » Empty.'
+ description = 'An unknown error has occurred'
+ invite = True
+ elif iq == 'Item not found':
+ description = 'Bookmarks directory appears to be empty'
+ invite = True
+ elif iq == 'forbidden':
+ description = 'Access forbidden'
+ elif iq == 'item-not-found':
+ description = 'Jabber ID does not appear to be exist'
+ elif iq == 'not-authorized':
+ description = 'You have no authorization to view ' + name + '\'s bookmarks.'
+
+ ask = True
+ elif iq == 'Node not found':
+ description = name + '\'s bookmarks directory appears to be empty.'
+ invite = True
+ elif 'DNS lookup failed' in iq:
+ domain = jid.split('@')[1] if '@' in jid else jid
+ description = 'Blasta could not connect to server {}'.format(domain)
+ elif iq == 'Connection failed: connection refused':
+ description = 'Connection with ' + name + ' has been refused'
+ elif 'Timeout' in iq or 'timeout' in iq:
+ description = 'Connection with ' + name + ' has been timed out'
+ else:
+ breakpoint()
+ description = 'An unknown error has occurred'
+ if invite:
+ hostname = request.url.hostname + ':' + str(request.url.port)
+ protocol = request.url.scheme
+ origin = protocol + '://' + hostname
+ template_file = 'ask.xhtml'
+ template_dict = {
+ 'request': request,
+ 'ask' : ask,
+ 'alias' : jabber_id.split('@')[0],
+ 'description': description,
+ 'invite' : invite,
+ 'jabber_id': jabber_id,
+ 'jid': jid,
+ 'journal': journal,
+ 'message': message,
+ 'name': name,
+ 'origin': origin,
+ 'path': path}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+ else:
+ description = 'An XMPP account is required'
+ message = 'Blasta system message » Please connect with your XMPP account to view this directory.'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ template_file = 'browse.xhtml'
+ template_dict = {
+ 'request': request,
+ 'description': description,
+ 'entries': entries,
+ 'jabber_id': jabber_id,
+ 'jid': jid,
+ 'journal': journal,
+ 'message': message,
+ 'page_next': page_next,
+ 'page_prev': page_prev,
+ 'pager' : True,
+ 'param_query' : param_query,
+ 'param_tags': param_tags,
+ 'path': path,
+ 'pubsub_jid': jid,
+ 'node_id': nodes[node_type]['name'],
+ 'start': start,
+ 'syndicate': jid,
+ 'tags' : tags_dict or related_tags or ''}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/blasta.svg')
+ def logo_get():
+ return FileResponse('graphic/blasta.svg')
+
+ @self.app.get('/', response_class=HTMLResponse)
+ @self.app.get('/new', response_class=HTMLResponse)
+ async def root_get_new(request: Request, response : Response):
+ response = await root_main_get(request, response, page_type='new')
+ return response
+
+ @self.app.get('/popular', response_class=HTMLResponse)
+ async def root_get_popular(request: Request, response : Response):
+ response = await root_main_get(request, response, page_type='popular')
+ return response
+
+ @self.app.get('/query', response_class=HTMLResponse)
+ async def root_get_query(request: Request, response : Response):
+ response = await root_main_get(request, response, page_type='query')
+ return response
+
+ @self.app.get('/recent', response_class=HTMLResponse)
+ async def root_get_recent(request: Request, response : Response):
+ response = await root_main_get(request, response, page_type='recent')
+ return response
+
+ async def root_main_get(request: Request, response : Response, page_type=None):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ node_id = path = syndicate = page_type
+ param_query = request.query_params.get('q', '') or None
+ if param_query: param_query = param_query.strip()
+ param_page = request.query_params.get('page', '') or None
+ param_tags = request.query_params.get('tags', '') or None
+ param_tld = request.query_params.get('tld', '') or None
+ param_filetype = request.query_params.get('filetype', '') or None
+ param_protocol = request.query_params.get('protocol', '') or None
+ if param_page:
+ try:
+ page = int(param_page)
+ page_next = page + 1
+ page_prev = page - 1
+ except:
+ page = 1
+ page_next = 2
+ else:
+ page = 1
+ page_next = 2
+ page_prev = page - 1
+ index_first = (page - 1)*10
+ db_file = 'main.sqlite'
+ if param_tags or param_tld or param_filetype or param_protocol:
+ entries_count = SQLite.get_entries_count_by_tag(db_file, param_tags)
+ match page_type:
+ case 'new':
+ description = 'New bookmarks tagged with "{}"'.format(param_tags)
+ entries_database = SQLite.get_entries_new_by_tag(db_file, param_tags, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_new_by_tag(db_file, param_tags, index_first)
+ case 'popular':
+ description = 'Popular bookmarks tagged with "{}"'.format(param_tags) # 'Most popular'
+ entries_database = SQLite.get_entries_popular_by_tag(db_file, param_tags, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_popular_by_tag(db_file, param_tags, index_first)
+ case 'recent':
+ description = 'Recent bookmarks tagged with "{}"'.format(param_tags)
+ entries_database = SQLite.get_entries_recent_by_tag(db_file, param_tags, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_recent_by_tag(db_file, param_tags, index_first)
+ # TODO case 'query':
+ else:
+ match page_type:
+ case 'new':
+ description = 'New bookmarks'
+ entries_database = SQLite.get_entries_new(db_file, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_new(db_file, index_first)
+ entries_count = SQLite.get_entries_count(db_file)
+ case 'popular':
+ description = 'Popular bookmarks' # 'Most popular'
+ entries_database = SQLite.get_entries_popular(db_file, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_popular(db_file, index_first)
+ entries_count = SQLite.get_entries_count(db_file)
+ case 'query':
+ node_id = syndicate = 'new'
+ description = 'Posted bookmarks with "{}"'.format(param_query)
+ entries_database = SQLite.get_entries_by_query(db_file, param_query, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_by_query_recent(db_file, param_query, index_first)
+ entries_count = SQLite.get_entries_count_by_query(db_file, param_query)
+ case 'recent':
+ description = 'Recent bookmarks'
+ entries_database = SQLite.get_entries_recent(db_file, index_first)
+ tags_of_entries = SQLite.get_30_tags_by_entries_recent(db_file, index_first)
+ entries_count = SQLite.get_entries_count(db_file)
+ tags_dict = {}
+ #for tag, instances in SQLite.get_tags_30(db_file):
+ for tag, instances in tags_of_entries:
+ tags_dict[tag] = instances
+ entries = []
+ for entry in entries_database:
+ tags_sorted = []
+ for tag in SQLite.get_tags_by_entry_id(db_file, entry[0]):
+ tags_sorted.append(tag[0])
+ jid = SQLite.get_jid_by_jid_id(db_file, entry[5])
+ entries.append(
+ {'title' : entry[3],
+ 'link' : entry[2],
+ 'summary' : entry[4],
+ 'published' : entry[6],
+ 'updated' : entry[7],
+ 'tags' : tags_sorted,
+ 'url_hash' : Utilities.hash_url_to_md5(entry[2]), #entry[1]
+ 'jid' : jid,
+ 'name' : jid, # jid.split('@')[0] if '@' in jid else jid,
+ 'instances' : entry[8]})
+ for entry in entries:
+ try:
+ date_iso = entry['published']
+ date_wrt = Utilities.convert_iso8601_to_readable(date_iso)
+ entry['published_mod'] = date_wrt
+ except:
+ print('ERROR: Probably due to an attempt to convert a non ISO 8601.')
+ print(entry['published'])
+ print(entry['published_mod'])
+ print(entry)
+ index_last = index_first+len(entries_database)
+ if entries_count <= index_last:
+ # NOTE Did you forget to modify index_last?
+ # NOTE No. It appears that it probably not needed index_last = entries_count
+ page_next = None
+ #if page_type != 'new' or page_prev or param_tags or param_tld or param_filetype or param_protocol:
+ if request.url.path != '/' or request.url.query:
+ message = 'Listing bookmarks {} - {} out of {}.'.format(index_first+1, index_last, entries_count)
+ message_link = None
+ else:
+ message = ('Welcome to Blasta, an XMPP PubSub oriented social '
+ 'bookmarks manager for organizing online content.')
+ message_link = {'href' : '/help/about', 'text' : 'Learn more'}
+ template_file = 'browse.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'entries' : entries,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'message_link' : message_link,
+ 'node_id' : node_id,
+ 'page_next' : page_next,
+ 'page_prev' : page_prev,
+ 'pager' : True,
+ 'param_query' : param_query,
+ 'param_tags' : param_tags,
+ 'path' : path,
+ 'pubsub_jid' : jabber_id_pubsub,
+ 'syndicate' : syndicate,
+ 'tags' : tags_dict}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ """
+ # TODO Return to code /tag and / (root) once SQLite database is ready.
+ @self.app.get('/tag/{tag}')
+ async def tag_tag_get(request: Request, tag):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ node_id = 'tag:{}'.format(tag)
+ syndicate = '?tag={}'.format(tag)
+ path = 'tag'
+ # NOTE Perhaps it would be beneficial to retrieve "published" and
+ # tags ("category") of viewer to override the tags of Blasta
+ # TODO If URL exist in visitor's bookmarks, display its properties
+ # (summary, tags title etc.) before data of others.
+# if Utilities.is_jid_matches_to_session(accounts, sessions, request):
+ page = request.query_params.get('page', '') or None
+ if page:
+ try:
+ page = int(page)
+ page_next = page + 1
+ page_prev = page - 1
+ except:
+ page = 1
+ page_next = 2
+ else:
+ page = 1
+ page_next = 2
+ page_prev = page - 1
+ index_first = (page - 1)*10
+ index_last = index_first+10
+ tags_dict = {}
+ for entry in entries_database:
+ for entry_tag in entry['tags']:
+ if entry_tag in tags_dict:
+ tags_dict[entry_tag] = tags_dict[entry_tag]+1
+ else:
+ tags_dict[entry_tag] = 1
+ tags_dict = dict(sorted(tags_dict.items(), key=lambda item: (-item[1], item[0])))
+ tags_dict = dict(list(tags_dict.items())[:30])
+ #tags_dict = dict(sorted(tags_dict.items(), key=lambda item: (-item[1], item[0]))[:30])
+ print(tags_dict)
+ entries = []
+ for entry in entries_database:
+ if tag in entry['tags']:
+ entries.append(entry)
+ for entry in entries:
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+ """
+
+ @self.app.post('/', response_class=HTMLResponse)
+ async def root_post(request: Request,
+ response: Response,
+ jabber_id: str = Form(...),
+ password: str = Form(...)):
+ if not Utilities.is_jid_matches_to_session(accounts, sessions, request):
+ # Store a variable in the request's state
+ request.app.state.jabber_id = jabber_id
+ session_key = str(random.random())
+ request.app.state.session_key = session_key
+ accounts[jabber_id] = XmppInstance(jabber_id + '/blasta', password)
+ # accounts[jabber_id].authenticated
+ # dir(accounts[jabber_id])
+ # accounts[jabber_id].failed_auth
+ # accounts[jabber_id].event_when_connected
+ sessions[jabber_id] = session_key
+ # Check if the user and password are present and valid
+ # If not valid, return "Could not connect to JID"
+
+ # FIXME Instead of an arbitrary number (i.e. 5 seconds), write a
+ # while loop with a timeout of 10 seconds.
+
+ # Check whether an account is connected.
+ # Wait for 5 seconds to connect.
+ await asyncio.sleep(5)
+ #if jabber_id in accounts and accounts[jabber_id].connection_accepted:
+
+ if jabber_id in accounts:
+ xmpp_instance = accounts[jabber_id]
+ #await xmpp_instance.plugin['xep_0060'].delete_node(jabber_id, node_id_public)
+
+ for node_properties in nodes:
+ properties = nodes[node_properties]
+ if not await XmppPubsub.is_node_exist(xmpp_instance, properties['name']):
+ iq = XmppPubsub.create_node_atom(
+ xmpp_instance, jabber_id, properties['name'],
+ properties['title'], properties['subtitle'],
+ properties['access_model'])
+ await iq.send(timeout=15)
+
+ #await XmppPubsub.set_node_private(xmpp_instance, node_id_private)
+ #await XmppPubsub.set_node_private(xmpp_instance, node_id_read)
+ #configuration_form = await xmpp_instance['xep_0060'].get_node_config(jabber_id, properties['name'])
+ #print(configuration_form)
+ node_id = nodes['public']['name']
+ result, reason = await Data.update_cache_and_database(xmpp_instance, jabber_id, 'public', node_id)
+ if result == 'error':
+ message = 'XMPP system message » {}.'.format(reason)
+ description = 'IQ Error'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, 'xmpp:blasta:settings:0', 'routine')
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ payload = iq['pubsub']['items']['item']['payload']
+ routine = payload.text if payload else None
+ else:
+ routine = None
+ match routine:
+ case 'private':
+ response = RedirectResponse(url='/private')
+ case 'read':
+ response = RedirectResponse(url='/read')
+ case _:
+ response = RedirectResponse(url='/jid/' + jabber_id)
+
+ else:
+ #del accounts[jabber_id]
+ #del sessions[jabber_id]
+ message = 'Blasta system message » Authorization has failed.'
+ description = 'Connection has failed'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.post('/message')
+ async def message_post(request: Request,
+ jid: str = Form(...),
+ body: str = Form(...)):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ #headline = 'This is a message from Blasta'
+ xmpp_instance = accounts[jabber_id]
+ XmppMessage.send(xmpp_instance, jid, body)
+ alias = jid.split('@')[0]
+ message = 'Your message has been sent to {}.'.format(alias)
+ description = 'Message has been sent'
+ path = 'message'
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/now')
+ def now_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'now.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/private', response_class=HTMLResponse)
+ @self.app.post('/private')
+ async def private_get(request: Request, response : Response):
+ node_type = 'private'
+ path = 'private'
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ node_id = nodes[node_type]['name']
+ result, reason = await Data.update_cache_and_database(xmpp_instance, jabber_id, node_type, node_id)
+ if result == 'error':
+ message = 'Blasta system message » {}.'.format(reason)
+ description = 'Directory "private" appears to be empty'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ response = await jid_main_get(request, node_type, path)
+ return response
+ else:
+ description = 'An XMPP account is required'
+ message = 'Blasta system message » Please connect with your XMPP account to view this directory.'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/profile')
+ async def profile_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ if not await XmppPubsub.is_node_exist(xmpp_instance, 'xmpp:blasta:settings:0'):
+ iq = XmppPubsub.create_node_config(xmpp_instance, jabber_id)
+ await iq.send(timeout=15)
+ access_models = {}
+ for node_type in nodes:
+ node_id = nodes[node_type]['name']
+ iq = await XmppPubsub.get_node_configuration(xmpp_instance, jabber_id, node_id)
+ access_model = iq['pubsub_owner']['configure']['form']['values']['pubsub#access_model']
+ access_models[node_type] = access_model
+ settings = {}
+ for setting in ['enrollment', 'routine']:
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, 'xmpp:blasta:settings:0', setting)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ payload = iq['pubsub']['items']['item']['payload']
+ if payload: settings[setting] = payload.text
+ template_file = 'profile.xhtml'
+ template_dict = {
+ 'access_models' : access_models,
+ 'enroll' : settings['enrollment'] if 'enrollment' in settings else None,
+ 'request' : request,
+ 'routine' : settings['routine'] if 'routine' in settings else None,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.post('/profile')
+ async def profile_post(request: Request,
+ routine: str = Form(None),
+ enroll: str = Form(None)):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ if routine:
+ message = 'The routine directory has been set to {}'.format(routine)
+ payload = Xml.create_setting_entry(routine)
+ iq = await XmppPubsub.publish_node_item(
+ xmpp_instance, jabber_id, 'xmpp:blasta:settings:0', 'routine', payload)
+ if enroll:
+ if enroll == '1': message = 'Your database is shared with the Blasta system'
+ else: message = 'Your database is excluded from the Blasta system'
+ payload = Xml.create_setting_entry(enroll)
+ iq = await XmppPubsub.publish_node_item(
+ xmpp_instance, jabber_id, 'xmpp:blasta:settings:0', 'enrollment', payload)
+ description = 'Setting has been saved'
+ template_file = 'result.xhtml'
+ template_dict = {
+ 'description' : description,
+ 'enroll' : enroll,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'request' : request,
+ 'routine' : routine}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.get('/profile/export/{node_type}/{filetype}')
+ async def profile_export_get(request: Request, node_type, filetype):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ node_id = nodes[node_type]['name']
+ iq = await XmppPubsub.get_node_items(xmpp_instance, jabber_id, node_id)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ entries = Data.extract_iq_items(iq, jabber_id)
+ # TODO Append a bookmark or bookmarks of Blasta
+ if entries:
+ filename = 'export/' + jabber_id + '_' + node_type + '.' + filetype
+ #filename = 'export/{}_{}.{}'.format(jabber_id, node_type, filetype)
+ #filename = 'export_' + node_type + '/' + jabber_id + '_' + '.' + filetype
+ #filename = 'export_{}/{}.{}'.format(node_type, jabber_id, filetype)
+ match filetype:
+ case 'json':
+ Data.save_to_json(filename, entries)
+ case 'toml':
+ # NOTE Should the dict be named with 'entries' or 'private'/'public'/'read'?
+ data = {'entries' : entries}
+ Data.save_to_toml(filename, data)
+ response = FileResponse(filename)
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.post('/profile/import')
+# def profile_import_post(file: UploadFile = File(...)):
+ async def profile_import_post(request: Request,
+ file: UploadFile | None = None,
+ merge: str = Form(None),
+ node: str = Form(...),
+ override: str = Form(None)):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ if file:
+
+ # TODO If node does not exist, redirect to result page with
+ # a message that bookmarks are empty.
+ # NOTE No.
+
+ # TODO match/case public, private, read
+ node_id = node[node]['name']
+ node_title = node[node]['title']
+ node_subtitle = node[node]['subtitle']
+ node_access_model = node[node]['access_model']
+ if not await XmppPubsub.is_node_exist(xmpp_instance, node_id):
+ iq = XmppPubsub.create_node_atom(
+ xmpp_instance, jabber_id, node_id, node_title,
+ node_subtitle, node_access_model)
+ await iq.send(timeout=15)
+
+ #return {"filename": file.filename}
+ content = file.file.read().decode()
+ entries = tomllib.loads(content)
+ # entries_node = entries[node]
+
+ name = jabber_id.split('@')[0]
+ # timestamp = datetime.now().isoformat()
+ db_file = 'main.sqlite'
+ for entry in entries:
+ url_hash = item_id = Utilities.hash_url_to_md5(entry['link'])
+ instances = SQLite.get_entry_instances_by_url_hash(db_file, url_hash)
+ entry = {'title' : entry['title'],
+ 'link' : entry['link'],
+ 'summary' : entry['summary'],
+ 'published' : entry['published'],
+ 'updated' : entry['published'],
+ #'updated' : entry['updated'],
+ 'tags' : entry['tags'],
+ 'url_hash' : url_hash,
+ 'jid' : jabber_id,
+ 'name' : name,
+ 'instances' : instances}
+ #message = 'Discover new links and see who shares them'
+ xmpp_instance = accounts[jabber_id]
+ payload = Syndication.create_rfc4287_entry(entry)
+ iq = await XmppPubsub.publish_node_item(
+ xmpp_instance, jabber_id, node_id, item_id, payload)
+ #await iq.send(timeout=15)
+ message = 'Blasta system message » Imported {} items.'.format(len(entries))
+ description = 'Import successful'
+ path = 'profile'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ message = 'Blasta system message » Error: No upload file sent.'
+ description = 'Import error'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/save')
+ async def save_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ param_url = request.query_params.get('url', '')
+ url_hash = Utilities.hash_url_to_md5(param_url)
+ for node_type in nodes:
+ node_id = nodes[node_type]['name']
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, node_id, url_hash)
+ #if len(iq['pubsub']['items']):
+ if (isinstance(iq, slixmpp.stanza.iq.Iq) and
+ url_hash == iq['pubsub']['items']['item']['id']):
+ return RedirectResponse(url='/url/' + url_hash + '/edit')
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, 'xmpp:blasta:settings:0', 'routine')
+ routine = iq['pubsub']['items']['item']['payload'].text
+ # NOTE Is "message" missing?
+ description = 'Add a new bookmark' # 'Enter properties for a bookmark'
+ param_title = request.query_params.get('title', '')
+ param_tags = request.query_params.get('tags', '')
+ param_summary = request.query_params.get('summary', '')
+ path = 'save'
+ template_file = 'edit.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'path' : path,
+ 'routine' : routine,
+ 'summary' : param_summary,
+ 'tags' : param_tags,
+ 'title' : param_title,
+ 'url' : param_url}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.post('/save')
+ async def save_post(request: Request,
+ node: str = Form(...),
+ summary: str = Form(''),
+ tags: str = Form(''),
+ title: str = Form(...),
+ url: str = Form(...)):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ url_hash = Utilities.hash_url_to_md5(url)
+ for node_type in nodes:
+ node_id = nodes[node_type]['name']
+ iq = await XmppPubsub.get_node_item(
+ xmpp_instance, jabber_id, node_id, url_hash)
+ if (isinstance(iq, slixmpp.stanza.iq.Iq) and
+ url_hash == iq['pubsub']['items']['item']['id']):
+ return RedirectResponse(url='/url/' + url_hash + '/edit')
+ description = 'Confirm properties of a bookmark'
+ path = 'save'
+ published = datetime.now().isoformat()
+ template_file = 'edit.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'confirm' : True,
+ 'description' : description,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'node' : node,
+ 'path' : path,
+ 'published' : published,
+ 'summary' : summary,
+ 'tags' : tags,
+ 'title' : title,
+ 'url' : url,
+ 'url_hash' : url_hash}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/read', response_class=HTMLResponse)
+ @self.app.post('/read')
+ async def read_get(request: Request, response : Response):
+ node_type = 'read'
+ path = 'read'
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ node_id = nodes[node_type]['name']
+ result, reason = await Data.update_cache_and_database(xmpp_instance, jabber_id, node_type, node_id)
+ if result == 'error':
+ message = 'Blasta system message » {}.'.format(reason)
+ description = 'Directory "read" appears to be empty'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ response = await jid_main_get(request, node_type, path)
+ return response
+ else:
+ description = 'An XMPP account is required'
+ message = 'Blasta system message » Please connect with your XMPP account to view this directory.'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ def result_post(request: Request, jabber_id: str, description: str, message: str, path: str, http_code=None):
+ template_file = 'result.xhtml'
+ template_dict = {
+ 'description' : description,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'path' : path,
+ 'request' : request}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/register')
+ def register_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ template_file = 'register.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/rss')
+ def rss(request: Request):
+ return RedirectResponse(url='/help/syndication')
+
+ @self.app.get('/search')
+ async def search_get(request: Request):
+ response = RedirectResponse(url='/search/all')
+ return response
+
+ @self.app.get('/search/all')
+ async def search_all_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ description = 'Search for public bookmarks'
+ form_action = '/query'
+ input_id = input_name = label_for = 'q'
+ input_placeholder = 'Enter a search query.'
+ input_type = 'search'
+ label = 'Search'
+ message = 'Search for bookmarks in the Blasta system.'
+ path = 'all'
+ template_file = 'search.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'form_action' : form_action,
+ 'input_id' : input_id,
+ 'input_name' : input_name,
+ 'input_placeholder' : input_placeholder,
+ 'input_type' : input_type,
+ 'label' : label,
+ 'label_for' : label_for,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'path' : path}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/search/jid/{jid}')
+ async def search_jid_get(request: Request, jid):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ if jabber_id == jid:
+ description = 'Search your own bookmarks'
+ message = 'Search for bookmarks from your own directory.'
+ else:
+ description = 'Search bookmarks of {}'.format(jid)
+ message = 'Search for bookmarks of a given Jabber ID.'
+ form_action = '/jid/' + jid
+ input_id = input_name = label_for = 'q'
+ input_placeholder = 'Enter a search query.'
+ input_type = 'search'
+ label = 'Search'
+ path = 'jid'
+ template_file = 'search.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'form_action' : form_action,
+ 'input_id' : input_id,
+ 'input_name' : input_name,
+ 'input_placeholder' : input_placeholder,
+ 'input_type' : input_type,
+ 'jabber_id' : jabber_id,
+ 'jid' : jid,
+ 'label' : label,
+ 'label_for' : label_for,
+ 'journal' : journal,
+ 'message' : message,
+ 'path' : path}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ response = RedirectResponse(url='/search/all')
+ return response
+
+ @self.app.get('/search/url')
+ async def search_url_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ description = 'Search for a bookmark'
+ form_action = None # This is not relevant due to function middleware. Maybe / or /url.
+ input_id = input_name = label_for = 'url'
+ input_placeholder = 'Enter a URL.'
+ input_type = 'url'
+ label = 'URL'
+ message = 'Search for a bookmark by a URL.'
+ path = 'url'
+ template_file = 'search.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+# 'form_action' : form_action,
+ 'input_id' : input_id,
+ 'input_name' : input_name,
+ 'input_placeholder' : input_placeholder,
+ 'input_type' : input_type,
+ 'label' : label,
+ 'label_for' : label_for,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'path' : path}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/tag')
+ def tag_get(request: Request):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ db_file = 'main.sqlite'
+ tag_list = SQLite.get_tags_500(db_file)
+ message = 'Common 500 tags sorted by name and sized by commonality.'
+ description = 'Common tags'
+ template_file = 'tag.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'tag_list' : tag_list}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/tag/{jid}')
+ def tag_get_jid(request: Request, jid):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ # NOTE Consider retrieval of tags from cache file.
+ # This is relevant to private and read nodes.
+ #if jabber_id == jid or node_type in ('private', 'read'):
+ db_file = 'main.sqlite'
+ tag_list = SQLite.get_500_tags_by_jid_sorted_by_name(db_file, jid)
+ message = 'Common 500 tags sorted by name and sized by commonality.'
+ description = 'Common tags of {}'.format(jid)
+ template_file = 'tag.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'jabber_id' : jabber_id,
+ 'jid' : jid,
+ 'journal' : journal,
+ 'message' : message,
+ 'tag_list' : tag_list}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+
+ @self.app.get('/url')
+ async def url_get(request: Request):
+ response = RedirectResponse(url='/search/url')
+ return response
+
+ @self.app.get('/url/{url_hash}')
+ async def url_hash_get(request: Request, url_hash):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ node_id = 'hash:{}'.format(url_hash)
+ param_hash = url_hash
+ syndicate = path = 'url'
+ db_file = 'main.sqlite'
+ entries = []
+ exist = False
+ if len(url_hash) == 32:
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ for node in nodes:
+ node_id = nodes[node]['name']
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, node_id, url_hash)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ # TODO If URL exist in visitor's bookmarks, display its properties (summary, tags title etc.) before data of others.
+ iq_item = iq['pubsub']['items']['item']
+ item_payload = iq_item['payload']
+ if item_payload:
+ exist = True
+ break
+ else:
+ print('IQ ISSUE?')
+ breakpoint()
+ print('IQ ISSUE?')
+
+ if exist:
+ # TODO Perhaps adding a paragraph with "your tags" and "who else has tagged this link"
+ # and keep the (5 item) limit.
+ #entry = Syndication.extract_items(item_payload, limit=True)
+ entry = Syndication.extract_items(item_payload)
+ if entry:
+ #url_hash = iq_item['id']
+ url_hash = Utilities.hash_url_to_md5(entry['link'])
+ # TODO Add a check: if iq_item['id'] == url_hash:
+ instances = SQLite.get_entry_instances_by_url_hash(db_file, url_hash)
+ entry['instances'] = instances
+ entry['jid'] = jabber_id
+ name = jabber_id.split('@')[0]
+ entry['name'] = name
+ entry['url_hash'] = url_hash
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(entry['published'])
+ entries.append(entry)
+ tags_list = {}
+ tags_and_instances = SQLite.get_tags_and_instances_by_url_hash(db_file, url_hash)
+ for tag, instances in tags_and_instances: tags_list[tag] = instances
+ else:
+ # https://fastapi.tiangolo.com/tutorial/handling-errors/
+ #raise HTTPException(status_code=404, detail="Item not found")
+ message = 'Blasta system message » Error: Not found.'
+ description = 'The requested bookmark does not exist'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+ else:
+ entry = SQLite.get_entry_by_url_hash(db_file, url_hash)
+ tags_sorted = []
+ if entry:
+ for tag in SQLite.get_tags_by_entry_id(db_file, entry[0]):
+ tags_sorted.append(tag[0])
+ tags_list = {}
+ tags_and_instances = SQLite.get_tags_and_instances_by_entry_id(db_file, entry[0])
+ for tag, instances in tags_and_instances: tags_list[tag] = instances
+ jid = SQLite.get_jid_by_jid_id(db_file, entry[5])
+ entries.append(
+ {'title' : entry[3],
+ 'link' : entry[2],
+ 'summary' : entry[4],
+ 'published' : entry[6],
+ 'published_mod' : Utilities.convert_iso8601_to_readable(entry[6]),
+ 'updated' : entry[7],
+ 'tags' : tags_sorted,
+ 'url_hash' : entry[1], # Utilities.hash_url_to_md5(entry[2])
+ 'jid' : jid,
+ 'name' : jid, # jid.split('@')[0] if '@' in jid else jid,
+ 'instances' : entry[8]})
+ # message = 'XMPP system message » {}.'.format(iq)
+ # if iq == 'Node not found':
+ # description = 'An error has occurred'
+ # else:
+ # description = 'An unknown error has occurred'
+ else:
+ # https://fastapi.tiangolo.com/tutorial/handling-errors/
+ #raise HTTPException(status_code=404, detail="Item not found")
+ message = 'Blasta system message » Error: Not found.'
+ description = 'The requested bookmark does not exist'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+ else:
+ entry = SQLite.get_entry_by_url_hash(db_file, url_hash)
+ if entry:
+ tags_sorted = []
+ for tag in SQLite.get_tags_by_entry_id(db_file, entry[0]):
+ tags_sorted.append(tag[0])
+ tags_list = {}
+ tags_and_instances = SQLite.get_tags_and_instances_by_entry_id(db_file, entry[0])
+ for tag, instances in tags_and_instances: tags_list[tag] = instances
+ jid = SQLite.get_jid_by_jid_id(db_file, entry[5])
+ entries.append(
+ {'title' : entry[3],
+ 'link' : entry[2],
+ 'summary' : entry[4],
+ 'published' : entry[6],
+ 'published_mod' : Utilities.convert_iso8601_to_readable(entry[6]),
+ 'updated' : entry[7],
+ 'tags' : tags_sorted,
+ 'url_hash' : entry[1], # Utilities.hash_url_to_md5(entry[2])
+ 'jid' : jid,
+ 'name' : jid, # jid.split('@')[0] if '@' in jid else jid,
+ 'instances' : entry[8]})
+ else:
+ # https://fastapi.tiangolo.com/tutorial/handling-errors/
+ #raise HTTPException(status_code=404, detail="Item not found")
+ message = 'Blasta system message » Error: Not found.'
+ description = 'The requested bookmark does not exist'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+ message = 'Information for URL {}'.format(entries[0]['link']) # entry[2]
+ description = 'Discover new links and see who shares them'
+ template_file = 'browse.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'entries' : entries,
+ 'exist' : exist,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'node_id' : node_id,
+ 'param_hash' : param_hash,
+ 'path' : path,
+ 'pubsub_jid' : jabber_id_pubsub,
+ 'syndicate' : syndicate,
+ 'tags' : tags_list}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ message = 'Blasta system message » Error: MD5 message-digest algorithm.'
+ description = 'The argument for URL does not appear to be a valid MD5 Checksum'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.post('/url/{url_hash}')
+ async def url_hash_post(request: Request,
+ url_hash,
+ node: str = Form(...),
+ published: str = Form(...),
+ summary: str = Form(''),
+ tags: str = Form(''),
+ #tags_old: str = Form(...),
+ tags_old: str = Form(''),
+ title: str = Form(...),
+ url: str = Form(...)):
+ node_id = 'hash:{}'.format(url_hash)
+ param_hash = url_hash
+ syndicate = path = 'url'
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ if jabber_id:
+ name = jabber_id.split('@')[0]
+ db_file = 'main.sqlite'
+ instances = SQLite.get_entry_instances_by_url_hash(db_file, url_hash)
+ timestamp = datetime.now().isoformat()
+ entry = {'title' : title.strip(),
+ 'link' : url.strip(),
+ 'summary' : summary.strip() if summary else '',
+ 'published' : published,
+ 'updated' : timestamp,
+ 'tags' : Data.organize_tags(tags) if tags else '',
+ 'url_hash' : url_hash,
+ 'jid' : jabber_id,
+ 'name' : name,
+ 'instances' : instances or 1}
+ message = 'Information for URL {}'.format(url)
+ description = 'Bookmark properties'
+ xmpp_instance = accounts[jabber_id]
+ payload = Syndication.create_rfc4287_entry(entry)
+ # TODO Add try/except for IQ
+ print('Publish item')
+ # TODO Check.
+ # NOTE You might not need to append to an open node before appending to a whitelist node.
+ node_id = nodes[node]['name']
+ iq = await XmppPubsub.publish_node_item(
+ xmpp_instance, jabber_id, node_id, url_hash, payload)
+ match node:
+ case 'private':
+ print('Set item as private (XEP-0223)')
+ #iq = await XmppPubsub.publish_node_item_private(
+ # xmpp_instance, node_id_private, url_hash, iq)
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id_public, url_hash)
+ Data.remove_item_from_cache(jabber_id, 'public', url_hash)
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id_read, url_hash)
+ Data.remove_item_from_cache(jabber_id, 'read', url_hash)
+ case 'public':
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id_private, url_hash)
+ Data.remove_item_from_cache(jabber_id, 'private', url_hash)
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id_read, url_hash)
+ Data.remove_item_from_cache(jabber_id, 'read', url_hash)
+ case 'read':
+ #iq = await XmppPubsub.publish_node_item_private(
+ # xmpp_instance, node_id_read, url_hash, iq)
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id_public, url_hash)
+ Data.remove_item_from_cache(jabber_id, 'public', url_hash)
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id_private, url_hash)
+ Data.remove_item_from_cache(jabber_id, 'private', url_hash)
+ if isinstance(iq, str):
+ description = 'Could not save bookmark'
+ message = 'XMPP system message » {}.'.format(iq)
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ #await iq.send(timeout=15)
+ # Save changes to cache file
+ entries_cache_filename = 'items/' + jabber_id + '.toml'
+ entries_cache = Data.open_file_toml(entries_cache_filename)
+ entries_cache_node = entries_cache[node] if node in entries_cache else []
+ entries_cache_mod = []
+ #for entry_cache in entries_cache_node:
+ # if entry_cache['url_hash'] == url_hash:
+ # entry_cache = entry
+ # break
+ is_entry_modified = False
+ # You already have this code in the HTML form, which indicates that this is an edit of an existing item
+ #
+ for entry_cache in entries_cache_node:
+ if entry_cache['url_hash'] == url_hash:
+ is_entry_modified = True
+ entries_cache_mod.append(entry)
+ else:
+ entries_cache_mod.append(entry_cache)
+ if not is_entry_modified: entries_cache_mod.append(entry)
+ entries_cache[node] = entries_cache_mod
+ entries_cache_data = entries_cache
+ Data.save_to_toml(entries_cache_filename, entries_cache_data)
+ # Save changes to database
+ if node == 'public':
+ tags_valid = []
+ tags_invalid = []
+ tags_list = tags.split(',')
+ tags_old_list = tags_old.split(',')
+ for tag in tags_old_list:
+ if tag not in tags_list:
+ tags_invalid.append(tag)
+ for tag in tags_list:
+ if tag not in tags_old_list:
+ tags_valid.append(tag)
+ await SQLite.delete_combination_row_by_entry_id_and_tag_id_and_jid_id(db_file, url_hash, tags_invalid, jabber_id)
+ await SQLite.add_tags(db_file, [entry])
+ # Slow (high I/O)
+ if not SQLite.get_entry_id_by_url_hash(db_file, url_hash):
+ await SQLite.add_new_entries(db_file, [entry]) # Is this line needed?
+ await SQLite.associate_entries_tags_jids(db_file, entry)
+ #elif not SQLite.is_jid_associated_with_url_hash(db_file, jabber_id, url_hash):
+ # await SQLite.associate_entries_tags_jids(db_file, entry)
+ else:
+ await SQLite.associate_entries_tags_jids(db_file, entry)
+ # Entry for HTML
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(published)
+ entry['updated_mod'] = Utilities.convert_iso8601_to_readable(timestamp)
+ entries = [entry]
+ template_file = 'browse.xhtml'
+ template_dict = {
+ 'request': request,
+ 'description': description,
+ 'entries': entries,
+ 'jabber_id': jabber_id,
+ 'journal': journal,
+ 'message': message,
+ 'node_id': node_id,
+ 'param_hash': param_hash,
+ 'path': path,
+ 'pubsub_jid': jabber_id_pubsub,
+ 'syndicate': syndicate}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ return response
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ @self.app.get('/url/{url_hash}/confirm')
+ async def url_hash_confirm_get(request: Request, url_hash):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ node_id = 'hash:{}'.format(url_hash)
+ param_hash = url_hash
+ syndicate = path = 'url'
+ if len(url_hash) == 32:
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ exist = False
+ for node in nodes:
+ node_id = nodes[node]['name']
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, node_id, url_hash)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ # TODO If URL exist in visitor's bookmarks, display its properties (summary, tags title etc.) before data of others.
+ iq_item = iq['pubsub']['items']['item']
+ item_payload = iq_item['payload']
+ if item_payload:
+ exist = True
+ break
+ else:
+ message = 'XMPP system message » {}.'.format(iq)
+ if iq == 'Node not found':
+ description = 'An error has occurred'
+ else:
+ description = 'An unknown error has occurred'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ if exist:
+ # TODO Add a check: if iq_item['id'] == url_hash:
+ entries = []
+ entry = Syndication.extract_items(item_payload)
+ db_file = 'main.sqlite'
+ instances = SQLite.get_entry_instances_by_url_hash(db_file, url_hash)
+ entry['instances'] = instances
+ entry['jid'] = jabber_id
+ name = jabber_id.split('@')[0]
+ entry['name'] = name
+ entry['url_hash'] = url_hash
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(entry['published'])
+ entries.append(entry)
+ description = 'Confirm deletion of a bookmark'
+ message = 'Details for bookmark {}'.format(entries[0]['link'])
+ template_file = 'browse.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'delete' : True,
+ 'description' : description,
+ 'entries' : entries,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'message' : message,
+ 'node_id' : node_id,
+ 'param_hash' : param_hash,
+ 'path' : path,
+ 'pubsub_jid' : jabber_id_pubsub,
+ 'syndicate' : syndicate}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ response = RedirectResponse(url='/jid/' + jabber_id)
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ message = 'Blasta system message » Error: MD5 message-digest algorithm.'
+ description = 'The argument for URL does not appear to be a valid MD5 Checksum'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.get('/url/{url_hash}/delete')
+ async def url_hash_delete_get(request: Request, url_hash):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+ node_id = 'hash:{}'.format(url_hash)
+ param_hash = url_hash
+ syndicate = path = 'url'
+ if len(url_hash) == 32:
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ exist = False
+ for node_type in nodes:
+ node_id = nodes[node_type]['name']
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, node_id, url_hash)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ # TODO If URL exist in visitor's bookmarks, display its properties (summary, tags title etc.) before data of others.
+ iq_item = iq['pubsub']['items']['item']
+ item_payload = iq_item['payload']
+ if item_payload:
+ exist = True
+ break
+ else:
+ message = 'XMPP system message » {}.'.format(iq)
+ if iq == 'Node not found':
+ description = 'An error has occurred'
+ else:
+ description = 'An unknown error has occurred'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ if exist:
+ # TODO Add a check: if iq_item['id'] == url_hash:
+ entries = []
+ entry = Syndication.extract_items(item_payload)
+ db_file = 'main.sqlite'
+ instances = SQLite.get_entry_instances_by_url_hash(db_file, url_hash)
+ entry['instances'] = instances
+ entry['jid'] = jabber_id
+ name = jabber_id.split('@')[0]
+ entry['name'] = name
+ entry['url_hash'] = url_hash
+ entry['published_mod'] = Utilities.convert_iso8601_to_readable(entry['published'])
+ entries.append(entry)
+
+ # Set a title
+ description = 'A bookmark has been deleted'
+ # Set a message
+ message = 'Details for bookmark {}'.format(entry['link'])
+
+ # Create a link to restore bookmark
+ link_save = ('/save?url=' + urllib.parse.quote(entry['link']) +
+ '&title=' + urllib.parse.quote(entry['title']) +
+ '&summary=' + urllib.parse.quote(entry['summary']) +
+ '&tags=' + urllib.parse.quote(','.join(entry['tags'])))
+
+ # Remove the item from node
+ xmpp_instance = accounts[jabber_id]
+ await XmppPubsub.del_node_item(xmpp_instance, jabber_id, node_id, url_hash)
+
+ # Remove the item association from database
+ await SQLite.delete_combination_row_by_jid_and_url_hash(db_file, url_hash, jabber_id)
+ #await SQLite.delete_combination_row_by_entry_id_and_tag_id_and_jid_id(db_file, url_hash, entry['tags'], jabber_id)
+
+ # Remove the item from cache
+ Data.remove_item_from_cache(jabber_id, node_type, url_hash)
+
+ template_file = 'browse.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'entries' : entries,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'link_save' : link_save,
+ 'message' : message,
+ 'node_id' : node_id,
+ 'param_hash' : param_hash,
+ 'path' : path,
+ 'pubsub_jid' : jabber_id_pubsub,
+ 'restore' : True,
+ 'syndicate' : syndicate}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ response = RedirectResponse(url='/jid/' + jabber_id)
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ message = 'Blasta system message » Error: MD5 message-digest algorithm.'
+ description = 'The argument for URL does not appear to be a valid MD5 Checksum'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+ @self.app.get('/url/{url_hash}/edit')
+ @self.app.post('/url/{url_hash}/edit')
+ async def url_hash_edit_get(request: Request, url_hash):
+ jabber_id = Utilities.is_jid_matches_to_session(accounts, sessions, request)
+# node_id = 'hash:{}'.format(url_hash)
+ if len(url_hash) == 32:
+ if jabber_id:
+ xmpp_instance = accounts[jabber_id]
+ exist = False
+ for node in nodes:
+ node_id = nodes[node]['name']
+ iq = await XmppPubsub.get_node_item(xmpp_instance, jabber_id, node_id, url_hash)
+ if isinstance(iq, slixmpp.stanza.iq.Iq):
+ name = jabber_id.split('@')[0]
+ iq_item = iq['pubsub']['items']['item']
+ # TODO Add a check: if iq_item['id'] == url_hash:
+ # Is this valid entry['url_hash'] = iq['id'] or should it be iq_item['id']
+ db_file = 'main.sqlite'
+ entry = None
+ item_payload = iq_item['payload']
+ if item_payload:
+ exist = True
+ break
+ else:
+ message = 'XMPP system message » {}.'.format(iq)
+ if iq == 'Node not found':
+ description = 'An error has occurred'
+ else:
+ description = 'An unknown error has occurred'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+
+ if exist:
+ path = 'edit'
+ description = 'Edit an existing bookmark'
+ entry = Syndication.extract_items(item_payload)
+ entry['instances'] = SQLite.get_entry_instances_by_url_hash(db_file, url_hash)
+ else:
+ # TODO Consider redirect to path /save (function save_get)
+ # NOTE This seems to be the best to do, albeit, perhaps the pathname should be /save instead of /url/hash/edit.
+ path = 'save' # 'add'
+ description = 'Add a new bookmark'
+ result = SQLite.get_entry_by_url_hash(db_file, url_hash)
+ tags_sorted = []
+ if result:
+ for tag in SQLite.get_tags_by_entry_id(db_file, result[0]):
+ tags_sorted.append(tag[0])
+ entry = {'title' : result[3],
+ 'link' : result[2],
+ 'summary' : result[4],
+ 'published' : result[6],
+ 'updated' : result[7],
+ 'tags' : tags_sorted,
+ 'instances' : result[8]}
+ #'jid' = jabber_id,
+ #'name' : name,
+ #'url_hash' : url_hash
+ if entry:
+ entry['jid'] = jabber_id
+ entry['name'] = name
+ entry['url_hash'] = url_hash
+ template_file = 'edit.xhtml'
+ template_dict = {
+ 'request' : request,
+ 'description' : description,
+ 'edit' : True,
+ 'jabber_id' : jabber_id,
+ 'journal' : journal,
+ 'node' : node,
+ 'path' : path,
+ 'published' : entry['published'],
+ 'summary' : entry['summary'],
+ 'tags' : ', '.join(entry['tags']),
+ 'title' : entry['title'],
+ 'url' : entry['link'],
+ 'url_hash' : url_hash}
+ response = templates.TemplateResponse(template_file, template_dict)
+ response.headers["Content-Type"] = "application/xhtml+xml"
+ else:
+ message = 'Blasta system message » Error: No active session.'
+ description = 'You are not connected'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ else:
+ message = 'Blasta system message » Error: MD5 message-digest algorithm.'
+ description = 'The argument for URL does not appear to be a valid MD5 Checksum'
+ path = 'error'
+ return result_post(request, jabber_id, description, message, path)
+ return response
+
+class SQLite:
+
+ #from slixfeed.log import Logger
+ #from slixfeed.utilities import DateAndTime, Url
+
+ # DBLOCK = Lock()
+
+ #logger = Logger(__name__)
+
+ def create_connection(db_file):
+ """
+ Create a database connection to the SQLite database
+ specified by db_file.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+
+ Returns
+ -------
+ conn : object
+ Connection object or None.
+ """
+ time_begin = time.time()
+ function_name = sys._getframe().f_code.co_name
+# message_log = '{}'
+# logger.debug(message_log.format(function_name))
+ conn = None
+ try:
+ conn = connect(db_file)
+ conn.execute("PRAGMA foreign_keys = ON")
+ # return conn
+ except Error as e:
+ print(e)
+# logger.warning('Error creating a connection to database {}.'.format(db_file))
+# logger.error(e)
+ time_end = time.time()
+ difference = time_end - time_begin
+ if difference > 1: logger.warning('{} (time: {})'.format(function_name,
+ difference))
+ return conn
+
+
+ def create_tables(db_file):
+ """
+ Create SQLite tables.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ with SQLite.create_connection(db_file) as conn:
+ sql_table_main_entries = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_table_main_jids = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_table_main_tags = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_table_main_statistics = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_table_combination_entries_tags_jids = (
+ """
+ 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.
+ sql_table_authorization_entries_jids = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_table_report_entries = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_table_report_jids = (
+ """
+ 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")
+ );
+ """
+ )
+ sql_trigger_instances_tag_decrease = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_instances_tag_increase = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_instances_tag_update = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_entry_count_increase = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_entry_count_decrease = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_entry_count_update = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_jid_count_increase = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_jid_count_decrease = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_jid_count_update = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_tag_count_increase = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_tag_count_decrease = (
+ """
+ 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;
+ """
+ )
+ sql_trigger_tag_count_update = (
+ """
+ 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;
+ """
+ )
+ cur = conn.cursor()
+ cur.execute(sql_table_main_entries)
+ cur.execute(sql_table_main_jids)
+ cur.execute(sql_table_main_tags)
+ cur.execute(sql_table_main_statistics)
+ cur.execute(sql_table_combination_entries_tags_jids)
+ cur.execute(sql_table_authorization_entries_jids)
+ cur.execute(sql_table_report_entries)
+ cur.execute(sql_table_report_jids)
+ cur.execute(sql_trigger_instances_tag_decrease)
+ cur.execute(sql_trigger_instances_tag_increase)
+ cur.execute(sql_trigger_instances_tag_update)
+ cur.execute(sql_trigger_entry_count_increase)
+ cur.execute(sql_trigger_entry_count_decrease)
+ cur.execute(sql_trigger_entry_count_update)
+ cur.execute(sql_trigger_jid_count_increase)
+ cur.execute(sql_trigger_jid_count_decrease)
+ cur.execute(sql_trigger_jid_count_update)
+ cur.execute(sql_trigger_tag_count_increase)
+ cur.execute(sql_trigger_tag_count_decrease)
+ cur.execute(sql_trigger_tag_count_update)
+
+ def add_statistics(db_file):
+ """
+ Batch insertion of tags.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ entries : list
+ Set of entries.
+
+ Returns
+ -------
+ None.
+
+ Note
+ ----
+ This function is executed immediately after the creation of the database
+ and, therefore, the directive "async with DBLOCK:" is not necessary.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ sql = (
+ """
+ INSERT
+ INTO main_statistics(
+ type)
+ VALUES ('entries'),
+ ('jids'),
+ ('tags');
+ """
+ )
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ try:
+ cur.execute(sql)
+ except IntegrityError as e:
+ print(e)
+
+ async def associate_entries_tags_jids(db_file, entry):
+ async with DBLOCK:
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ jid = entry['jid']
+ url_hash = entry['url_hash']
+ entry_id = SQLite.get_entry_id_by_url_hash(db_file, url_hash)
+ jid_id = SQLite.get_jid_id_by_jid(db_file, jid)
+ if entry_id:
+ for tag in entry['tags']:
+ tag_id = SQLite.get_tag_id_by_tag(db_file, tag)
+ cet_id = SQLite.get_combination_id_by_entry_id_tag_id_jid_id(db_file, entry_id, tag_id, jid_id)
+ if not cet_id:
+ sql = (
+ """
+ INSERT
+ INTO combination_entries_tags_jids (
+ entry_id, tag_id, jid_id)
+ VALUES (
+ ?, ?, ?);
+ """
+ )
+ par = (entry_id, tag_id, jid_id)
+ try:
+ cur.execute(sql, par)
+ except IntegrityError as e:
+ print('associate_entries_tags_jids')
+ print(e)
+
+ async def add_tags(db_file, entries):
+ """
+ Batch insertion of tags.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ entries : list
+ Set of entries.
+
+ Returns
+ -------
+ None.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ async with DBLOCK:
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ for entry in entries:
+ tags = entry['tags']
+ for tag in tags:
+# sql = (
+# """
+# INSERT OR IGNORE INTO main_tags(tag) VALUES (?);
+# """
+# )
+ if not SQLite.get_tag_id_by_tag(db_file, tag):
+ sql = (
+ """
+ INSERT INTO main_tags(tag) VALUES(?);
+ """
+ )
+ par = (tag,)
+ try:
+ cur.execute(sql, par)
+ except IntegrityError as e:
+ print(e)
+
+ async def add_new_entries(db_file, entries):
+ """
+ Batch insert of new entries into table entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ entries : list
+ Set of entries.
+
+ Returns
+ -------
+ None.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ async with DBLOCK:
+
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+
+ for entry in entries:
+ url_hash = entry['url_hash']
+ url = entry['link']
+ title = entry['title']
+ summary = entry['summary']
+ jid = entry['jid']
+ date_first = entry['published']
+ date_last = entry['published']
+ # instances = entry['instances']
+
+ # Import entries
+ jid_id = SQLite.get_jid_id_by_jid(db_file, jid)
+ sql = (
+ """
+ INSERT
+ INTO main_entries(
+ url_hash, url, title, summary, jid_id, date_first, date_last)
+ VALUES(
+ ?, ?, ?, ?, ?, ?, ?);
+ """
+ )
+ par = (url_hash, url, title, summary, jid_id, date_first, date_last)
+
+ try:
+ cur.execute(sql, par)
+ except IntegrityError as e:
+ print(e)
+ print(jid_id)
+ print(entry)
+# logger.warning("Skipping: " + str(url))
+# logger.error(e)
+
+ # TODO An additional function to ssociate jid_id (jid) with entry_id (hash_url)
+ async def set_jid(db_file, jid):
+ """
+ Add a JID to database.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ A Jabber ID.
+
+ Returns
+ -------
+ None.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid: {}'
+# .format(function_name, db_file, jid))
+ sql = (
+ """
+ INSERT
+ INTO main_jids(
+ jid)
+ VALUES(
+ ?);
+ """
+ )
+ par = (jid, )
+ async with DBLOCK:
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ try:
+ cur.execute(sql, par)
+ except IntegrityError as e:
+ print(e)
+# logger.warning("Skipping: " + str(url))
+# logger.error(e)
+
+ def get_entries_count(db_file):
+ """
+ Get entries count.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+
+ Returns
+ -------
+ result : tuple
+ Number.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ sql = (
+ """
+ SELECT count
+ FROM main_statistics
+ WHERE type = "entries";
+ """
+ )
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_combination_id_by_entry_id_tag_id_jid_id(db_file, entry_id, tag_id, jid_id):
+ """
+ Get ID by a given Entry ID and a given Tag ID and a given Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ entry_id : str
+ Entry ID.
+ tag_id : str
+ Tag ID.
+ jid_id : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ ID.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} entry_id: {} tag_id: {} jid_id: {}'
+# .format(function_name, db_file, entry_id, tag_id, jid_id))
+ sql = (
+ """
+ SELECT id
+ FROM combination_entries_tags_jids
+ WHERE entry_id = :entry_id AND tag_id = :tag_id AND jid_id = :jid_id;
+ """
+ )
+ par = {
+ "entry_id": entry_id,
+ "tag_id": tag_id,
+ "jid_id": jid_id
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ async def delete_combination_row_by_entry_id_and_tag_id_and_jid_id(db_file, url_hash, tags, jid):
+ """
+ Delete a row by a given entry ID and a given Jabber ID and given tags.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ url_hash : str
+ URL hash.
+ tags : list
+ Tags.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ None.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} entry_id: {} tag_id: {} jid_id: {}'
+# .format(function_name, db_file, entry_id, tag_id, jid_id))
+ sql = (
+ """
+ DELETE
+ FROM combination_entries_tags_jids
+ WHERE
+ entry_id = (SELECT id FROM main_entries WHERE url_hash = :url_hash) AND
+ tag_id = (SELECT id FROM main_tags WHERE tag = :tag) AND
+ jid_id = (SELECT id FROM main_jids WHERE jid = :jid);
+ """
+ )
+ async with DBLOCK:
+ with SQLite.create_connection(db_file) as conn:
+ for tag in tags:
+ par = {
+ "url_hash": url_hash,
+ "tag": tag,
+ "jid": jid
+ }
+ cur = conn.cursor()
+ cur.execute(sql, par)
+
+ def get_tag_id_and_instances_by_tag(db_file, tag):
+ """
+ Get a tag ID and instances by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ Tag.
+
+ Returns
+ -------
+ result : tuple
+ Tag ID.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {}'
+# .format(function_name, db_file, tag))
+ sql = (
+ """
+ SELECT id, instances
+ FROM main_tags
+ WHERE tag = ?;
+ """
+ )
+ par = (tag,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+# return result[0] if result else None, None
+ if not result: result = None, None
+ return result
+
+ def get_tags_and_instances_by_url_hash(db_file, url_hash):
+ """
+ Get tags and instances by a given URL hash.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ url_hash : str
+ A hash of a URL.
+
+ Returns
+ -------
+ result : tuple
+ Tags and instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT mt.tag, mt.instances
+ FROM main_tags AS mt
+ INNER JOIN combination_entries_tags_jids AS co ON mt.id = co.tag_id
+ INNER JOIN main_entries AS me ON me.id = co.entry_id
+ WHERE me.url_hash = ?
+ ORDER BY mt.instances DESC;
+ """
+ )
+ par = (url_hash,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_tags_and_instances_by_entry_id(db_file, entry_id):
+ """
+ Get tags and instances by a given ID entry.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ entry_id : str
+ An ID of an entry.
+
+ Returns
+ -------
+ result : tuple
+ Tags and instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT main_tags.tag, main_tags.instances
+ FROM main_tags
+ INNER JOIN combination_entries_tags_jids ON main_tags.id = combination_entries_tags_jids.tag_id
+ WHERE combination_entries_tags_jids.entry_id = ?
+ ORDER BY main_tags.instances DESC;
+ """
+ )
+ par = (entry_id,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_tag_id_by_tag(db_file, tag):
+ """
+ Get a tag ID by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ Tag.
+
+ Returns
+ -------
+ result : tuple
+ Tag ID.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {}'
+# .format(function_name, db_file, tag))
+ sql = (
+ """
+ SELECT id
+ FROM main_tags
+ WHERE tag = ?;
+ """
+ )
+ par = (tag,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entry_id_by_url_hash(db_file, url_hash):
+ """
+ Get an entry ID by a given URL hash.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ url_hash : str
+ MD5 hash of URL.
+
+ Returns
+ -------
+ result : tuple
+ Entry ID.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} url_hash: {}'
+# .format(function_name, db_file, url_hash))
+ sql = (
+ """
+ SELECT id
+ FROM main_entries
+ WHERE url_hash = ?;
+ """
+ )
+ par = (url_hash,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entry_instances_by_url_hash(db_file, url_hash):
+ """
+ Get value of entry instances by a given URL hash.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ url_hash : str
+ MD5 hash of URL.
+
+ Returns
+ -------
+ result : tuple
+ Value of entry instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} url_hash: {}'
+# .format(function_name, db_file, url_hash))
+ sql = (
+ """
+ SELECT instances
+ FROM main_entries
+ WHERE url_hash = ?;
+ """
+ )
+ par = (url_hash,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entry_by_url_hash(db_file, url_hash):
+ """
+ Get entry of a given URL hash.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ url_hash : str
+ MD5 hash of URL.
+
+ Returns
+ -------
+ result : tuple
+ Entry properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} url_hash: {}'
+# .format(function_name, db_file, url_hash))
+ sql = (
+ """
+ SELECT *
+ FROM main_entries
+ WHERE url_hash = ?;
+ """
+ )
+ par = (url_hash,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entries_new(db_file, index_first):
+ """
+ Get new entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT *
+ FROM main_entries
+ ORDER BY date_first DESC
+ LIMIT 10
+ OFFSET ?;
+ """
+ )
+ par = (index_first,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_popular(db_file, index_first):
+ """
+ Get popular entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT *
+ FROM main_entries
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET ?;
+ """
+ )
+ par = (index_first,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_recent(db_file, index_first):
+ """
+ Get recent entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT *
+ FROM main_entries
+ ORDER BY date_last DESC
+ LIMIT 10
+ OFFSET ?;
+ """
+ )
+ par = (index_first,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_by_query(db_file, query, index_first):
+ """
+ Get entries by a query.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ query : str
+ Search query.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT *
+ FROM main_entries
+ WHERE title LIKE :query OR url LIKE :query OR summary LIKE :query
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "query": f'%{query}%',
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_count_by_query(db_file, query):
+ """
+ Get entries count by a query.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ query : str
+ Search query.
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT COUNT(id)
+ FROM main_entries
+ WHERE title LIKE :query OR url LIKE :query OR summary LIKE :query
+ ORDER BY instances DESC;
+ """
+ )
+ par = {
+ "query": f'%{query}%',
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entries_by_jid_and_tag(db_file, jid, tag, index_first):
+ """
+ Get entries by a tag and a Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ Tag.
+ jid : str
+ Jabber ID.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {} jid: {} index_first: {}'
+# .format(function_name, db_file, tag, jid, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT DISTINCT me.*
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE mj.jid = :jid AND mt.tag = :tag
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "jid": jid,
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_count_by_jid_and_tag(db_file, jid, tag):
+ """
+ Get entries count by a tag and a Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ Tag.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {} jid: {}'
+# .format(function_name, db_file, tag, jid))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT COUNT(DISTINCT me.id)
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE mj.jid = :jid AND mt.tag = :tag;
+ """
+ )
+ par = {
+ "jid": jid,
+ "tag": tag
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entries_by_jid_and_query(db_file, jid, query, index_first):
+ """
+ Get entries by a query and a Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ query : str
+ Search query.
+ jid : str
+ Jabber ID.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} query: {} jid: {} index_first: {}'
+# .format(function_name, db_file, query, jid, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT DISTINCT me.*
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ WHERE mj.jid = :jid AND (title LIKE :query OR url LIKE :query OR summary LIKE :query)
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "jid": jid,
+ "query": f'%{query}%',
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_count_by_jid_and_query(db_file, jid, query):
+ """
+ Get entries count by a query and a Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ query : str
+ Search query.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} query: {} jid: {}'
+# .format(function_name, db_file, query, jid))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT COUNT(DISTINCT me.id)
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ WHERE mj.jid = :jid AND (title LIKE :query OR url LIKE :query OR summary LIKE :query);
+ """
+ )
+ par = {
+ "jid": jid,
+ "query": f'%{query}%'
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entries_by_jid(db_file, jid, index_first):
+ """
+ Get entries by a Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid: {} index_first: {}'
+# .format(function_name, db_file, jid, index_first))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT DISTINCT me.*
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ WHERE mj.jid = :jid
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "jid": jid,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_count_by_jid(db_file, jid):
+ """
+ Get entries count by a Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ Entries properties.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid: {}'
+# .format(function_name, db_file, jid))
+ # NOTE Consider date_first
+ sql = (
+ """
+ SELECT COUNT(DISTINCT me.id)
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ WHERE mj.jid = :jid;
+ """
+ )
+ par = {
+ "jid": jid
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entries_count_by_tag(db_file, tag):
+ """
+ Get entries count by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ A tag.
+
+ Returns
+ -------
+ result : tuple
+ Entries.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {}'
+# .format(function_name, db_file, tag))
+ sql = (
+ """
+ SELECT COUNT(entries.id)
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag;
+ """
+ )
+ par = (tag,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_entries_popular_by_tag(db_file, tag, index_first):
+ """
+ Get popular entries by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ A tag.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {}'
+# .format(function_name, db_file, tag))
+ sql = (
+ """
+ SELECT DISTINCT entries.*
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag
+ ORDER BY entries.instances DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_recent_by_tag(db_file, tag, index_first):
+ """
+ Get recent entries by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ A tag.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {}'
+# .format(function_name, db_file, tag))
+ sql = (
+ """
+ SELECT DISTINCT entries.*
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag
+ ORDER BY date_last DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_entries_new_by_tag(db_file, tag, index_first):
+ """
+ Get new entries by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ tag : str
+ A tag.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Entries.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} tag: {}'
+# .format(function_name, db_file, tag))
+ sql = (
+ """
+ SELECT DISTINCT entries.*
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag
+ ORDER BY date_first DESC
+ LIMIT 10
+ OFFSET :index_first;
+ """
+ )
+ par = {
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_tags_30(db_file):
+ """
+ Get 30 tags.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT tag, instances
+ FROM main_tags
+ ORDER BY instances DESC
+ LIMIT 30;
+ """
+ )
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql).fetchall()
+ return result
+
+ def get_30_tags_by_entries_popular(db_file, index_first):
+ """
+ Get 30 tags by currently viewed popular entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT id
+ FROM main_entries
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET ?
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = (index_first,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_entries_new_by_tag(db_file, tag, index_first):
+ """
+ Get 30 tags by currently viewed new entries by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT DISTINCT entries.id
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag
+ ORDER BY date_first DESC
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_entries_popular_by_tag(db_file, tag, index_first):
+ """
+ Get 30 tags by currently viewed popular entries by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT DISTINCT entries.id
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag
+ ORDER BY entries.instances DESC
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_entries_recent_by_tag(db_file, tag, index_first):
+ """
+ Get 30 tags by currently viewed recent entries by a given tag.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT DISTINCT entries.id
+ FROM main_entries AS entries
+ INNER JOIN combination_entries_tags_jids AS co ON entries.id = co.entry_id
+ INNER JOIN main_tags AS tags ON tags.id = co.tag_id
+ WHERE tags.tag = :tag
+ ORDER BY date_last DESC
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_entries_new(db_file, index_first):
+ """
+ Get 30 tags by currently viewed new entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT id
+ FROM main_entries
+ ORDER BY date_first DESC
+ LIMIT 10
+ OFFSET ?
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = (index_first,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_entries_recent(db_file, index_first):
+ """
+ Get 30 tags by currently viewed recent entries.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT id
+ FROM main_entries
+ ORDER BY date_last DESC
+ LIMIT 10
+ OFFSET ?
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = (index_first,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_entries_by_query_recent(db_file, query, index_first):
+ """
+ Get 30 tags by currently viewed entries by query.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ query : str
+ A search query.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT id
+ FROM main_entries
+ WHERE title LIKE :query OR url LIKE :query OR summary LIKE :query
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "query": f'%{query}%',
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_jid_and_tag(db_file, jid, tag, index_first):
+ """
+ Get 30 tags by Jabber ID and tags.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+ tag : str
+ A tag.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT co.entry_id
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE mj.jid = :jid AND mt.tag = :tag
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "jid": jid,
+ "tag": tag,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_jid_and_query(db_file, jid, query, index_first):
+ """
+ Get 30 tags by Jabber ID and query.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+ query : str
+ A search query.
+ index_first : str
+ .
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT co.entry_id
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE mj.jid = :jid AND (title LIKE :query OR url LIKE :query OR summary LIKE :query)
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "jid": jid,
+ "query": f'%{query}%',
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_30_tags_by_jid(db_file, jid, index_first):
+ """
+ Get 30 tags by Jabber ID.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT DISTINCT mt.tag, mt.instances
+ FROM combination_entries_tags_jids AS co
+ INNER JOIN main_tags AS mt ON mt.id = co.tag_id
+ WHERE co.entry_id IN (
+ SELECT DISTINCT me.id
+ FROM main_entries AS me
+ INNER JOIN combination_entries_tags_jids AS co ON co.entry_id = me.id
+ INNER JOIN main_jids AS mj ON mj.id = co.jid_id
+ WHERE mj.jid = :jid
+ ORDER BY instances DESC
+ LIMIT 10
+ OFFSET :index_first
+ )
+ ORDER BY mt.instances DESC
+ LIMIT 30;
+ """
+ )
+ par = {
+ "jid": jid,
+ "index_first": index_first
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_tags_500(db_file):
+ """
+ Get 500 tags.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {}'
+# .format(function_name, db_file))
+ sql = (
+ """
+ WITH Common500Tags AS (
+ SELECT tag, instances
+ FROM main_tags
+ ORDER BY instances DESC
+ LIMIT 500
+ )
+ SELECT tag, instances
+ FROM Common500Tags
+ ORDER BY tag ASC;
+ """
+ )
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql).fetchall()
+ return result
+
+ def get_500_tags_by_jid_sorted_by_name(db_file, jid):
+ """
+ Get 500 tags by Jabber ID, sorted by name.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT mt.tag, COUNT(*) AS instances
+ FROM main_tags mt
+ JOIN combination_entries_tags_jids combination ON mt.id = combination.tag_id
+ JOIN main_jids mj ON combination.jid_id = mj.id
+ WHERE mj.jid = :jid
+ GROUP BY mt.tag
+ LIMIT 500;
+ """
+ )
+ par = {
+ "jid": jid
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_500_tags_by_jid_sorted_by_instance(db_file, jid):
+ """
+ Get 500 tags by Jabber ID, sorted by instance.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ Tags and number of instances.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT mt.tag, COUNT(*) AS instances
+ FROM main_tags mt
+ JOIN combination_entries_tags_jids combination ON mt.id = combination.tag_id
+ JOIN main_jids mj ON combination.jid_id = mj.id
+ WHERE mj.jid = :jid
+ GROUP BY mt.tag
+ ORDER BY instances DESC
+ LIMIT 500;
+ """
+ )
+ par = {
+ "jid": jid
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ # FIXME It appear that the wrong table is fetched
+ # The table to be fetched is combination_entries_tags_jids
+ def is_jid_associated_with_url_hash(db_file, jid, url_hash):
+ """
+ Check whether a given Jabber ID is associated with a given URL hash.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ A Jabber ID.
+ url_hash : str
+ An MD5 checksuum of a URL.
+
+ Returns
+ -------
+ result : tuple
+ Tags.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid: {} url_hash: {}'
+# .format(function_name, db_file, jid, url_hash))
+ sql = (
+ """
+ SELECT mj.jid, me.url_hash
+ FROM main_jids AS mj
+ INNER JOIN combination_entries_tags_jids AS co ON mj.id = co.jid_id
+ INNER JOIN main_entries AS me ON me.id = co.entry_id
+ WHERE mj.jid = :jid AND me.url_hash = :url_hash;
+ """
+ )
+ par = {
+ "jid": jid,
+ "url_hash": url_hash
+ }
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ #deassociate_entry_from_jid
+ async def delete_combination_row_by_jid_and_url_hash(db_file, url_hash, jid):
+ """
+ Remove association of a given Jabber ID and a given URL hash.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ A Jabber ID.
+ url_hash : str
+ An MD5 checksuum of a URL.
+
+ Returns
+ -------
+ None.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid: {} url_hash: {}'
+# .format(function_name, db_file, jid, url_hash))
+ sql = (
+ """
+ DELETE FROM combination_entries_tags_jids
+ WHERE id IN (
+ SELECT co.id
+ FROM combination_entries_tags_jids co
+ JOIN main_entries me ON co.entry_id = me.id
+ JOIN main_jids mj ON co.jid_id = mj.id
+ WHERE me.url_hash = :url_hash AND mj.jid = :jid
+ );
+ """
+ )
+ par = {
+ "jid": jid,
+ "url_hash": url_hash
+ }
+ async with DBLOCK:
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ cur.execute(sql, par)
+
+ def get_tags_by_entry_id(db_file, entry_id):
+ """
+ Get tags by an ID entry.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ entry_id : str
+ An ID of an entry.
+
+ Returns
+ -------
+ result : tuple
+ Tags.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} index_first: {}'
+# .format(function_name, db_file, index_first))
+ sql = (
+ """
+ SELECT main_tags.tag
+ FROM main_tags
+ INNER JOIN combination_entries_tags_jids ON main_tags.id = combination_entries_tags_jids.tag_id
+ WHERE combination_entries_tags_jids.entry_id = ?
+ ORDER BY main_tags.instances DESC
+ LIMIT 5;
+ """
+ )
+ par = (entry_id,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchall()
+ return result
+
+ def get_jid_id_by_jid(db_file, jid):
+ """
+ Get id of a given jid.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid : str
+ Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ ID.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid: {}'
+# .format(function_name, db_file, jid))
+ sql = (
+ """
+ SELECT id
+ FROM main_jids
+ WHERE jid = ?;
+ """
+ )
+ par = (jid,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+ def get_jid_by_jid_id(db_file, jid_id):
+ """
+ Get jid of a given jid_id.
+
+ Parameters
+ ----------
+ db_file : str
+ Path to database file.
+ jid_id : str
+ ID of Jabber ID.
+
+ Returns
+ -------
+ result : tuple
+ ID.
+ """
+ function_name = sys._getframe().f_code.co_name
+# logger.debug('{}: db_file: {} jid_id: {}'
+# .format(function_name, db_file, jid_id))
+ sql = (
+ """
+ SELECT jid
+ FROM main_jids
+ WHERE id = ?;
+ """
+ )
+ par = (jid_id,)
+ with SQLite.create_connection(db_file) as conn:
+ cur = conn.cursor()
+ result = cur.execute(sql, par).fetchone()
+ return result[0] if result and len(result) == 1 else result
+
+class Syndication:
+
+ 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
+
+class Utilities:
+
+ 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
+
+ def hash_url_to_md5(url):
+ url_encoded = url.encode()
+ url_hashed = hashlib.md5(url_encoded)
+ url_digest = url_hashed.hexdigest()
+ return url_digest
+
+ 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
+
+class Xml:
+
+ def create_setting_entry(value : str):
+ element = ET.Element('value')
+ element.text = value
+ return element
+
+class Configuration:
+
+ def instantiate_database(db_file):
+# db_dir = get_default_data_directory()
+# if not os.path.isdir(db_dir):
+# os.mkdir(db_dir)
+# if not os.path.isdir(db_dir + "/sqlite"):
+# os.mkdir(db_dir + "/sqlite")
+# db_file = os.path.join(db_dir, "sqlite", r"{}.db".format(jid_file))
+ SQLite.create_tables(db_file)
+ SQLite.add_statistics(db_file)
+ return db_file
+
+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
+
+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')
+
+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:settings: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)
+
+def main():
+ if not exists('main.sqlite') or not getsize('main.sqlite'):
+ Configuration.instantiate_database('main.sqlite')
+ accounts = {}
+ sessions = {}
+ http_instance = HttpInstance(accounts, sessions)
+ return http_instance.app
+
+app = main()
+
+# FIXME
+if __name__ == '__main__':
+ uvicorn.run(app, host='127.0.0.1', port=8000, reload=True)
diff --git a/configuration.toml b/configuration.toml
new file mode 100644
index 0000000..b0cf4a8
--- /dev/null
+++ b/configuration.toml
@@ -0,0 +1,61 @@
+# Contact
+[contacts]
+email = ""
+xmpp = ""
+mix = ""
+muc = ""
+irc_channel = "#blasta"
+irc_server = ""
+
+# Settings
+[settings]
+journal = ""
+pubsub = ""
+
+# Bibliography
+node_id = "urn:xmpp:bibliography:0"
+node_title = "Blasta"
+node_subtitle = "Bibliography"
+
+# Private bibliography
+node_id_private = "xmpp:bibliography:private:0"
+node_title_private = "Blasta (Private)"
+node_subtitle_private = "Private bibliography"
+
+# Reading list
+node_id_read = "xmpp:bibliography:read:0"
+node_title_read = "Blasta (Read)"
+node_subtitle_read = "Reading list"
+
+# Settings node
+node_settings = "xmpp:blasta:settings:0"
+
+# Acceptable protocol types that would be aggregated to the Blasta database
+schemes = [
+ "adc",
+ "bitcoin",
+ "dweb",
+ "ed2k",
+ "ethereum",
+ "feed",
+ "ftp",
+ "ftps",
+ "gemini",
+ "geo",
+ "gopher",
+ "http",
+ "https",
+ "ipfs",
+ "ipns",
+ "irc",
+ "litecoin",
+ "magnet",
+ "mailto",
+ "monero",
+ "mms",
+ "news",
+ "sip",
+ "sms",
+ "tel",
+ "udp",
+ "xmpp"]
diff --git a/data/README.txt b/data/README.txt
new file mode 100644
index 0000000..1946376
--- /dev/null
+++ b/data/README.txt
@@ -0,0 +1 @@
+This directory is meant to store hashes and tags per JID as TOML.
diff --git a/export/README.txt b/export/README.txt
new file mode 100644
index 0000000..14ac63a
--- /dev/null
+++ b/export/README.txt
@@ -0,0 +1 @@
+This directory is contains exported nodes.
diff --git a/graphic/blasta.svg b/graphic/blasta.svg
new file mode 100644
index 0000000..e6095b3
--- /dev/null
+++ b/graphic/blasta.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/graphic/syndicate.svg b/graphic/syndicate.svg
new file mode 100644
index 0000000..b325149
--- /dev/null
+++ b/graphic/syndicate.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/graphic/xmpp.svg b/graphic/xmpp.svg
new file mode 100644
index 0000000..f0332bc
--- /dev/null
+++ b/graphic/xmpp.svg
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/items/README.txt b/items/README.txt
new file mode 100644
index 0000000..f706a25
--- /dev/null
+++ b/items/README.txt
@@ -0,0 +1 @@
+This directory is meant to cache nodes per JID as TOML.
diff --git a/stylesheet/stylesheet.css b/stylesheet/stylesheet.css
new file mode 100644
index 0000000..5bdb553
--- /dev/null
+++ b/stylesheet/stylesheet.css
@@ -0,0 +1,462 @@
+a {
+ color: #439639;
+ text-decoration: none;
+}
+
+a:visited {
+ color: #d9541e;
+}
+
+a:hover {
+ text-decoration: underline
+}
+
+body {
+ background-color:#f5f5f5;
+}
+
+a, div, h1, h2, h3, h4, h5, p, span {
+ font-family: system-ui;
+}
+
+body, h1, h2, html,
+#footer dd,
+#footer dl,
+#posts > dd,
+#navigation dd,
+dl#navigation,
+#related-tags dd {
+ margin: 0;
+ padding: 0
+}
+
+body, html {
+ height: 100%;
+}
+
+dl, form, p, .details {
+ line-height: 150%;
+ margin: 8px;
+ margin-left: 8px;
+}
+
+input.submit {
+ margin-top: 1em;
+}
+
+h1 {
+ display: inline-block;
+ font-size: 150%;
+ font-weight: bolder;
+ margin:0;
+ padding:0 0 0 4px;
+}
+
+h2 {
+ background-color: #eee;
+ font-size: initial;
+ font-weight: normal;
+ padding: 0.5ex 0.5em 0.5pc 0.5em;
+}
+
+h3, h4, h5 {
+ margin: 1em;
+ margin-block-end: 1em;
+ margin-left: 8px;
+}
+
+#container {
+ display: flex;
+ flex-flow: column;
+ height: 100%;
+}
+
+#container #header.row {
+ flex: 0 1 auto;
+ /* The above is shorthand for:
+ flex-grow: 0,
+ flex-shrink: 1,
+ flex-basis: auto
+ */
+}
+
+#container #main.row {
+ flex: 1 1 auto;
+}
+
+#container #footer.row {
+ flex: 0 1 20px;
+}
+
+#header {
+ background-color: #ddd;
+ padding: 0.5ex 0 0.5pc 0.5em;
+}
+
+#header h1 {
+ margin-bottom: 0.5em;
+}
+
+#header dl {
+ float: right;
+}
+
+#header dd:last-child {
+ border-right: unset;
+}
+
+#profile-link {
+ color: unset;
+}
+
+#header dd {
+ border-right: 1px solid #444;
+ display: inline;
+ padding: 0 0.4em;
+}
+
+.title:first-child {
+ margin-block-start: 2.22em;
+}
+
+.title {
+ margin-block-start: 1.33em;
+}
+
+.title > h4 {
+ display: inline;
+}
+/*
+
+form > * {
+ line-height: 120%;
+ margin: 2px;
+}
+*/
+
+#main {
+ display: flex;
+ /* justify-content: space-between; */
+}
+
+#related-tags {
+ background-color: #eee;
+ min-width: 200px;
+ padding: 0 0.5em 1em 1em;
+ height: 90vh;
+ width: 15%;
+ /* float: right; */
+ /* width: 200px; */
+ overflow-wrap: break-word;
+}
+
+#content {
+ /* min-width: 85%; */
+ /* display: inline-block; */
+ flex-grow: 1;
+}
+/*
+#posts {
+ line-height: 120%;
+ margin: 8px;
+}
+
+#posts h4 {
+ margin-bottom: 1px;
+}
+*/
+form#editor tr > td:first-child {
+ text-align: right;
+ padding: 1em;
+}
+
+form#editor th {
+ text-align: right;
+ padding: 1em;
+}
+
+/*
+form#editor th,
+form#editor td {
+ vertical-align: top;
+}
+*/
+
+form#editor #description,
+form#editor #title,
+form#editor #summary,
+form#editor #tags,
+form#editor #url {
+ width: 150%;
+}
+
+form#editor input[type="radio"]:checked:disabled + label {
+ /* font-weight: bold; */
+ text-decoration: underline;
+}
+
+/*
+input[type="radio"]:disabled:not(:checked),
+input[type="radio"]:disabled:not(:checked) + label {
+ display: none;
+}
+*/
+
+pre {
+ white-space: pre-wrap;
+}
+
+p.summary {
+ white-space: break-spaces;
+}
+
+table.pattern,
+table#software,
+table.atom-over-xmpp {
+ margin: 8px;
+}
+
+table.pattern th,
+table#software th {
+ padding-top: 22px;
+ text-align: left;
+}
+
+table.pattern td,
+table#software td {
+ line-height: 120%;
+ overflow-wrap: anywhere;
+ padding: 8px;
+ vertical-align: text-top;
+}
+
+#bookmarklet {
+ background: #eee;
+ border: 1px solid #bbb;
+ padding-left: 3px;
+ padding-right: 3px;
+}
+
+#subscribe {
+ /* font-size: 75%; */
+ padding-top: 1em;
+ /* text-align: center; */
+}
+
+#footer {
+ border-top: 1px solid #eee;
+ font-size: 80%;
+ margin-top: 3em;
+ /* text-align: center; end left start */
+}
+
+#tags-sized {
+ line-height: 2em;
+ margin: 1em;
+ text-align: justify;
+}
+
+.tag-degree-first {
+ font-size: 100%;
+ font-weight: 300;
+ margin: 1em;
+}
+
+.tag-degree-first:first-child {
+ margin: unset;
+}
+
+.tag-degree-second {
+ font-size: 125%;
+ font-weight: 400;
+ margin: 1.25em;
+}
+
+.tag-degree-second:first-child {
+ margin: unset;
+}
+
+.tag-degree-third {
+ font-size: 150%;
+ font-weight: 500;
+ margin: 1.5em;
+}
+
+.tag-degree-third:first-child {
+ margin: unset;
+}
+
+.tag-degree-fourth {
+ font-size: 175%;
+ font-weight: 600;
+ margin: 1.75em;
+}
+
+.tag-degree-fourth:first-child {
+ margin: unset;
+}
+
+.tag-degree-fifth {
+ font-size: 200%;
+ font-weight: 700;
+ margin: 2em;
+}
+
+.tag-degree-fifth:first-child {
+ margin: unset;
+}
+
+.instances-degree-first {
+ color: unset;
+ background: #cbecef;
+}
+
+.instances-degree-second {
+ color: unset;
+ background: #b6ffd6; /* #b6ffeb */
+}
+
+.instances-degree-third {
+ color: unset;
+ background: #ebefcb;
+}
+
+.instances-degree-fourth {
+ color: unset;
+ background: #f4fbb8;
+}
+
+.instances-degree-fifth {
+ color: unset;
+ background: #efd8cb;
+}
+
+.quote {
+ font-family: serif;
+ font-style: italic;
+ line-height: 2.5em;
+ margin-left: 1.5em;
+ width: 50%;
+}
+
+.warning {
+ color: #822493;
+ font-weight: bold;
+}
+
+.bottom {
+ font-size: 80%;
+}
+
+#navigate-top {
+ margin: 8px;
+ float: inline-end;
+}
+
+#navigate-bottom {
+ margin: 8px;
+ padding-top: 2em;
+ /* text-align: center; */
+}
+
+.inactive {
+ color: #818181;
+}
+
+#footer :first-child {
+ border-left: unset;
+}
+
+#footer dd {
+ display: inline;
+ border-left: 1px solid #444;
+ padding: 0 0.5em;
+}
+
+div#table {
+ display: flex;
+}
+
+.enlarge {
+ font-size: 24px;
+}
+
+#navigation dd:first-child {
+ display: none;
+}
+
+#header img {
+ height: 20px;
+ width: 20px;
+}
+
+#bookmarklet img {
+ height: 12px;
+ width: 12px;
+}
+
+#footer img {
+ height: 10px;
+ width: 10px;
+}
+
+#navigation img {
+ height: 18px;
+ width: 18px;
+}
+
+@media (max-width: 1150px) {
+ #header > h1 {
+ font-size: 100%;
+ }
+
+ #header img {
+ height: 16px;
+ width: 16px;
+ }
+}
+
+@media (max-width: 1000px) {
+ #related-tags,
+ #header > h1 {
+ display: none;
+ }
+
+ #header dl {
+ float: none;
+ }
+
+ #navigation {
+ text-align: center;
+ }
+
+ #navigation dd:first-child {
+ display: unset;
+ }
+}
+
+@media (max-width: 725px) {
+ #profile-link > b {
+ display: none;
+ }
+
+ #profile-link:before {
+ content: 'Home';
+ }
+}
+
+/*
+.strip {
+ background: #c4e17f;
+ border-radius: 5px;
+ border-top: 0;
+ display: block;
+ height: 3px;
+ max-width: 220px;
+ background-image: linear-gradient(to right, #c4e17f, #c4e17f 12.5%, #f7fdca 12.5%, #f7fdca 25%, #fecf71 25%, #fecf71 37.5%, #f0776c 37.5%, #f0776c 50%, #db9dbe 50%, #db9dbe 62.5%, #c49cde 62.5%, #c49cde 75%, #669ae1 75%, #669ae1 87.5%, #62c2e4 87.5%, #62c2e4);
+ background-image: linear-gradient(to right, #fff 25%, #d9541e 25%, #439639 25%, #d3d2d2 25%);
+ background-image: linear-gradient(to right, #fff, #d9541e, #439639, #d3d2d2);
+ background-image: linear-gradient(to right, #439639, #439639 25%, #fff 25%, #fff 75%, #d9541e 75%, #d9541e);
+ background-image: linear-gradient(to right, #fff, #fff 25%, #d9541e 25%, #d9541e 50%, #439639 50%, #439639 75%, #d3d2d2 75%, #d3d2d2);
+}
+*/
diff --git a/xhtml/about.xhtml b/xhtml/about.xhtml
new file mode 100644
index 0000000..fbbfbbd
--- /dev/null
+++ b/xhtml/about.xhtml
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » Information and resources about Blasta, collaborative
+ bookmarks with an Irish manner.
+
+
About Blasta
+
+ 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.
+
+
+ Once you are connected to the service, you add a simple
+ bookmarklet to your
+ browser. When you find a page of your interest, you can
+ use the Blasta bookmarklet, and enter information about the
+ given page. You can add descriptive terms, group similar
+ links together and add notes for yourself or for others.
+
+
+ You can access your list of links from any browser. Your
+ links are shown to you with those you have added most
+ recently at the top. In addition to viewing by date, you can
+ also view all links with a specific keyword you define, or
+ search your links for potential keywords.
+
+
+ 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.
+
+
+ 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.
+
+
Why Blasta?
+
+ Corporate search engines are archaic and outdated, and often
+ prioritize their own interests, leading to censorship and
+ distortion of important and vital information. This results
+ in unproductive and low-quality search outcomes.
+
+
+ With the collaborative indexing system which Blasta offers,
+ you can be rest assured that you will find the references
+ and resources that you need in order to be productive and
+ get that you need.
+
+
The things that you can do with Blasta are endless
+
+ 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. Learn more .
+
+
The difference from other services
+
+ Unlike some so called "social" bookmarking systems, Blasta
+ does not own your information; your bookmarks are
+ categorically owned and controlled by you; your bookmarks
+ are stored as PubSub
+ items within your own XMPP account; you are the soveriegn
+ of your information , and it is always under your
+ control .
+
+
+ This means, that if Blasta be offline tomorrow, your
+ personal bookmarks, private and public, remain intact inside
+ your personal XMPP account under PubSub node
+ urn:xmpp:bibliography:0
.
+
+
Information that is stored by Blasta
+
+ In order for Blasta to facilitate sharing of information and
+ accessibility to information, Blasta aggregates your own
+ public bookmarks and the public bookmarks of other people,
+ in order to create a database of its own, with which Blasta
+ manages and rates references to build recommendations and
+ gather statistics of resources that are both valid and
+ bookmarked more often than other resources.
+
+
+ Blasta does not store links of bookmarks that are marked by
+ all of their owners as private and no one else has stored
+ them in a public fashion (i.e. not classified private).
+
+
Blasta source code
+
+ The source code of Blasta is available under the terms of
+ the license AGPL-3.0 at
+
+ git.xmpp-it.net .
+
+
Our motives
+
+ We are adopting the attitude towards life and towards death,
+ which was implicit in the old Vikings' and in Schopenhauer's
+ statements:
+ Living for the sake of eternity .
+ Living with eternity always in mind, instead of living only
+ for the moment.
+ The attitude that the individual is not an in and of
+ himself, but lives for and through something greater, in
+ particular for and through his racial community, which is
+ eternal.
+
+
About us
+
+ Blasta was proudly made in the Republic of Ireland, by a
+ group of bible loving, religious, and stylish Irish men, who
+ have met each other, for the first time, at the Irish Red
+ Head Convention, and who dearly enjoy tea, and who love
+ their wives, children and their ancestors.
+
+
+ It is important to mention that we have met Mr. Schimon
+ Zachary, at the consequent Irish Red Head Convention of the
+ proceeding year, and he was the one who has initiated the
+ idea of XMPP PubSub bookmarks.
+
+
Conclusion
+
+ Blasta is for you to enjoy, excite, instigate, investigate,
+ learn and research.
+
+
We hope you would have productive outcomes with Blasta.
+
+
+ “All you can take with you; is that which you have given
+ away.”
+ ― It's a Wonderful Life (1946)
+
+
+
+
+
+
+
diff --git a/xhtml/ask.xhtml b/xhtml/ask.xhtml
new file mode 100644
index 0000000..a9b8301
--- /dev/null
+++ b/xhtml/ask.xhtml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » {{message}}
+
+
+ {{description}}
+
+
+ Send a message to {{name}}
+
+
+
+ You can also communicate with {{name}}, directly from your
+ desktop or mobile client, by clicking on the Jabber ID
+ ({{jid}}) of {{name}}.
+
+
+ Click the Jabber ID of {{name}} to
+ {% if ask %}
+ ask
+
+ {{jid}}
+ to view this directory.
+ {% elif invite %}
+ invite
+
+ {{jid}}
+ to Blasta.
+ {% endif %}
+
+
+
+
+
+
+
diff --git a/xhtml/atomsub.xhtml b/xhtml/atomsub.xhtml
new file mode 100644
index 0000000..4f73b83
--- /dev/null
+++ b/xhtml/atomsub.xhtml
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» The master standard for HTML.
+
Atomsub
+
+ Atomsub, or Atom Over XMPP, is a simple and yet ingenious
+ idea which was realized by the gentlemen: Peter Saint-Andre;
+ Joe Hildebrand; and Bob Wyman.
+
+
+ A memo which was published on May 7, 2008 and is titled
+ "Atomsub: Transporting Atom Notifications over the
+ Publish-Subscribe Extension to the Extensible Messaging and
+ Presence Protocol (XMPP)"
+ (codename: draft-saintandre-atompub-notify-07) describes a
+ method for notifying interested parties about changes in
+ syndicated information encapsulated in the Atom feed format,
+ where such notifications are delivered via an extension to
+ the Extensible Messaging and Presence Protocol (XMPP) for
+ publish-subscribe functionality.
+
+
References
+
Herein resources concerning to Atomsub.
+
+
+ Atom Over XMPP: Presentation
+ Peter Saint-Andre - Jabber Software Foundation - IETF 66
+ draft-saintandre-atompub-notify
+
+
+ datatracker.ietf.org
+ (PDF)
+
+
+
+ Atomsub
+
+ Transporting Atom Notifications over the
+ Publish-Subscribe Extension to the Extensible
+ Messaging and Presence Protocol (XMPP)
+
+ draft-saintandre-atompub-notify-07
+
+
+ datatracker.ietf.org
+
+
+
+
+
+ ATOM over XMPP: PubSub message types
+ One of different PubSub message types.
+
+
+ wiki.xmpp.org
+
+
+
+
+ XEP-0060: Publish-Subscribe
+ This is the general specification for PubSub.
+
+
+ xmpp.org
+
+
+
+
+ XEP-0277: Microblogging over XMPP
+
+ This specification defines a method for
+ microblogging over XMPP.
+
+
+ This specification is utilized by the publishing
+ platform Libervia.
+
+
+
+ xmpp.org
+
+
+ libervia.org
+
+
+
+
+ XEP-0472: Pubsub Social Feed
+
+ This specification defines a way of publishing
+ social content over XMPP.
+
+
+ This specification is utilized by the publishing
+ platform Movim.
+
+
+
+ xmpp.org
+
+
+ movim.eu
+
+
+
+
+
Of note
+
+ These type of technologies are public information for over
+ a couple of decades (i.e. more than 20 years); and people
+ from corporations and governments with special interests,
+ and who think that they have nothing better to do with their
+ lives, have been attempting to suppress these technologies,
+ and doing so is absolutely against your best interest.
+
+
+
+ “People demand freedom of speech as a compensation for the
+ freedom of thought which they seldom use.”
+ ― Sören Kierkegaard
+
+
+
+
+
+
+
diff --git a/xhtml/browse.xhtml b/xhtml/browse.xhtml
new file mode 100644
index 0000000..3dd4920
--- /dev/null
+++ b/xhtml/browse.xhtml
@@ -0,0 +1,265 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+ {% if pager %}
+ {% if page_next or page_prev %}
+
+ {% if page_prev %}
+ «
+
+ retract
+ {% else %}
+
« retract
+ {% endif %}
+ or
+ {% if page_next %}
+
+ proceed
+ »
+ {% else %}
+
proceed »
+ {% endif %}
+
+ {% endif %}
+ {% endif %}
+ {% if message %}
+
+ » {{message}}
+ {% if message_link %}
+
+ {{message_link['text']}} .
+ {% endif %}
+
+ {% endif %}
+
{{description}}
+ {% if start %}
+
+ Start
+ your PubSub bookmarks directory with Blasta!
+
+ {% endif %}
+
+ {% if entries %}
+ {% for entry in entries %}
+
+
+
+
+
+ {% if jabber_id %}
+ {% if restore %}
+ restore
+ {% elif delete %}
+ confirm deletion
+ {% elif jabber_id == jid or exist or path in ('private', 'read') %}
+
+ edit
+ /
+
+ delete
+ {% else %}
+
+ add
+ {% endif %}
+ {% endif %}
+ {{entry['summary']}}
+
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
+
+ {% if pager %}
+ {% if page_next or page_prev %}
+
+ {% if page_prev %}
+ «
retract
+ {% else %}
+
« retract
+ {% endif %}
+ or
+ {% if page_next %}
+
proceed »
+ {% else %}
+
proceed »
+ {% endif %}
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
diff --git a/xhtml/connect.xhtml b/xhtml/connect.xhtml
new file mode 100644
index 0000000..a4328b2
--- /dev/null
+++ b/xhtml/connect.xhtml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
diff --git a/xhtml/contact.xhtml b/xhtml/contact.xhtml
new file mode 100644
index 0000000..99a93de
--- /dev/null
+++ b/xhtml/contact.xhtml
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » Herein contant information.
+
+
+
+ You can reach us by the following means.
+
+ {% if contact_mix or contact_muc %}
+
+
+ Blasta Community
+
+ {% if contact_mix %}
+
+ MIX Groupchat
+
+
+ {{contact_mix}}
+
+ {% endif %}
+ {% if contact_muc %}
+
+ MUC Groupchat
+
+
+ {{contact_muc}}
+
+ {% endif %}
+ {% if contact_irc_channel and contact_irc_server %}
+
+ IRC Channel
+
+
+ {{contact_irc_channel}}
+ at {{contact_irc_server}}
+
+ {% endif %}
+
+ {% endif %}
+ {% if contact_xmpp or contact_email %}
+
+
+ Blasta Contact
+
+ {% if contact_xmpp %}
+
+ Send a Jabber message
+
+
+ {{contact_xmpp}}
+
+ {% endif %}
+ {% if contact_email %}
+
+ Send an Email message
+
+
+ {{contact_email}}
+
+ {% endif %}
+
+ {% endif %}
+
+
+ “Be seeing you.”
+ ― Blasta
+
+
+
+
+
+
+
diff --git a/xhtml/edit.xhtml b/xhtml/edit.xhtml
new file mode 100644
index 0000000..996962f
--- /dev/null
+++ b/xhtml/edit.xhtml
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » Bookmark a link to your personal PubSub node.
+
+
+ {{description}}
+
+
+
+
+ {% if published %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
diff --git a/xhtml/feeds.xhtml b/xhtml/feeds.xhtml
new file mode 100644
index 0000000..cfbbd8e
--- /dev/null
+++ b/xhtml/feeds.xhtml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» This is a help guide for feeds of Blasta.
+
An introduction to feeds
+
+ A feed, or "Atom", or "Syndication", or "RSS" (Really Simple
+ Syndication or Rich Site Summary) is a mean that allows you
+ to receive news articles and other updates from sites
+ quickly and conveniently through the installation of a Feed
+ Reader (also News Reader or RSS Reader) similar to e-mail.
+
+
+ Through RSS, you can easily receive news updates on your PC,
+ mobile and tablet without visiting the site yourself, which
+ spares the need for you to manually check the site for new
+ content.
+
+
Atom Over HTTP
+
+ Blasta offers Atom feeds for you to subscribe with your News
+ Reader.
+
+
+ The subscriptions to Atom Syndication Feeds are centralized
+ to the Blasta server.
+
+
+ If you do not have a News Reader yet, please refer to our
+
+ selection of News Readers .
+
+
Atom Over XMPP
+
+ Since Blasta is based upon XMPP, Blasta offers
+ PubSub feeds for you
+ to subscribe with XMPP clients such as
+ LeechCraft ,
+ Libervia ,
+ Movim and
+ Reeder .
+
+
+ Generally, there are two types of PubSub subscriptions:
+
+
+ Peer to peer subscription which allows you to
+ subscribe directly to a Blasta node of a contact.
+
+
+ Peer to server subscription that is exclusive to
+ Blasta which offers its special nodes, such as
+ "popular", "recent", "tag" and "url" variants.
+
+
+
+
+ Technically, both PubSub subscriptions are the same, yet the
+ "peer to peer" type of subscription is entirely independent
+ from Blasta.
+
+
Which is better, HTTP or XMPP?
+
It depends on your use case.
+
+ If you want to receive immediate updates and save bandwidth
+ then XMPP would be a good choice for you.
+
+
+ If you rather want to be anonymous to the Blasta system or
+ to a peer, and you do not care so much for bandwidth and,
+ perhaps, you also want to be behind a mixnet or proxy such
+ as I2P or Tor, then HTTP would be a good choice for you.
+
+
+ Please note, that most of the XMPP desktop clients do not
+ yet provide graphical interfaces for PubSub, so you might
+ have to resort to the HTTP method, until XMPP desktop
+ clients would provide graphical interfaces for PubSub.
+
+
+ For more information about Blasta feeds, please refer to the
+ feeds legend .
+
+
Conclusion
+
+ The syndication feeds of Blasta are available in two forms
+ (HTTP and XMPP) and can be utilized via any mean you think
+ is fit to you, be it a Feed Reader or an XMPP Client. You
+ can decide by your own personal preference.
+
+
+
+ “Syndication is, in fact, the technology that the
+ Fortune 500, so called, do not want you to know about”
+ ― Alex James Anderson
+
+
+
+
+
+
+
diff --git a/xhtml/help.xhtml b/xhtml/help.xhtml
new file mode 100644
index 0000000..0d4cd7f
--- /dev/null
+++ b/xhtml/help.xhtml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» This is a help guide of Blasta.
+
Contents
+
+ The following references provide references to features that
+ are available on XMPP and to features are offered by Blasta,
+ and also disclose the mechanisms which Blasta utilizes.
+
+
+
+
+
+
+ {% if jabber_id %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
diff --git a/xhtml/ideas.xhtml b/xhtml/ideas.xhtml
new file mode 100644
index 0000000..4657503
--- /dev/null
+++ b/xhtml/ideas.xhtml
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» Some ideas which illustrate potential uses for Blasta.
+
The things that you can do with Blasta are endless
+
+ 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. Here are some illustrative scenarios:
+
+
Research
+
+ Whether you are composing an article, delving into an
+ industry, or diligently working on your dissertation,
+ leverage Blasta to organize and store your online source
+ materials and insights.
+
+
Podcast
+
+ Explore a plethora of captivating podcasts by visiting
+
+ /tag/podcast?filetype=spx
+ or
+
+ /tag/podcast?filetype=ogg
+ and enjoy enriching content.
+
+
+ Additionally, Blasta offers
+ Atom and PubSub
+ feeds that seamlessly integrate with
+ Feed Readers . If
+ you are a podcaster, upload your spx, ogg or mp3 files to
+ Blasta to generate a dedicated feed for your audience.
+
+
News
+
+ If you are intending to start a site of news flash, then
+ Blasta can be an adequate solution for you.
+
+
+ Additionally, you can also use Blasta to manage your notes.
+
+
Travel
+
+ Facilitate trip planning by saving links related to
+ accommodations, activities, and transportation on Blasta
+ using tags such as "leisure", "travel", "vacation", and
+ "tovisit" (to visit). Engage in collaborative trip planning
+ with friends and family members by utilizing the tag
+ "for:{% if jabber_id %}{{jabber_id}}{% else %}jid{% endif %}".
+
+
Employment
+
+ Stay updated on job openings and industry trends by
+ following specific tags related to your field of interest on
+ Blasta. For instance, you can subscribe to tags like
+ "directory:job", "directory:vacancy", "news:career", or
+ "news:industry" to access relevant articles and resources.
+
+
+ Additionally, use Blasta to bookmark useful resources for
+ resume building, interview tips, and
+ professional development.
+
+
File sharing
+
+ Share you favourite contents that you share via BitTorrent,
+ eD2k or IPFS, whether publicly by
+
+ /tag/?filetype=torrent
+ and
+
+ /tag/?protocol=magnet
+ or whether privately by utilizing the tag
+ "for:{% if jabber_id %}{{jabber_id}}{% else %}jid{% endif %}".
+
+
+ Additionally, you might also want to research for new
+ content on the BitTorrent network by following tags
+ "brand:milw0rm", "brand:roflcopter2110", "brand:redice", or
+ "brand:red-ice"; or any other brand which is associated with
+ contents that you desire.
+
+
Tracker
+
+ You can run your own instance of Blatsa and determine to
+ index only magnet links, be it of BitTorrent or eD2k links.
+ And with the correct set of plugins you can even turn it
+ into a BitTorrent tracker.
+
+
Sell
+
+ If you are selling items online, you can use Blasta to
+ create a categorized list of your products for easy sharing
+ with potential buyers. Tag your items with descriptive
+ keywords such as "sale:furniture", "sale:electronics", or
+ "sale:vintage" and "sale:clothing" to attract the right
+ audience. You can also share your Blasta profile link on
+ other sites or in online marketplaces to showcase your
+ items.
+
+
Trade
+
+ Engage in trading communities by tagging items you are
+ willing to trade with specific tags such as "trading",
+ "barter", or "swap". You can also join trading groups on
+ Blasta to connect with like-minded individuals interested in
+ exchanging goods or services.
+
+
+ Additionally, use Blasta to bookmark resources related to
+ trading strategies, bartering tips, and best practices for
+ successful exchanges.
+
+
Wishlist
+
+ While browsing through various of online shopping platforms,
+ bookmark items that you are interested at and categorize
+ them under "wishlist". Share your curated list with others
+ by directing them to
+ {% if jabber_id %}
+
+ /{{jabber_id}}/wishlist
+
+ {% else %}
+ {{origin}}/YOUR_JID/wishlist
+ {% endif %}
+
+
More ideas
+
+ Codex, Contacts, Geographic Location, Notes manager, and even more.
+
+
+
+ “Think, while it is still legal.”
+
+
+
+
+
+
+
diff --git a/xhtml/libervia.xhtml b/xhtml/libervia.xhtml
new file mode 100644
index 0000000..2e7b521
--- /dev/null
+++ b/xhtml/libervia.xhtml
@@ -0,0 +1,368 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » The Universal Communication Ecosystem.
+
+
About Libervia
+
+ Libervia is a all-in-one tool to manage all your
+ communications needs: instant messaging, (micro)blogging,
+ file sharing, photo albums, events, forums, tasks, etc.
+
+
What is Libervia?
+
+ Libervia (formerly "Salut à Toi") is a Libre communication
+ ecosystem based on XMPP standard. It allows you to do many
+ things such as:
+
+ instant messaging;
+ (micro)blogging;
+ file sharing;
+ managing photo albums;
+ organizing/managing events;
+ handling tasks;
+ etc.
+
+ It features many interfaces (desktop, mobile, web, command
+ line, console), and is multi-platforms.
+
+
Motives
+
+ The project "Libervia" was born from a need to protect our
+ liberties, our privacy and our independence. It is intended
+ to protect the rights and liberties people have regarding
+ their own private and numeric data, their acquaintance's,
+ and the data they handle; it is also intended to be a human
+ contact point, not substituting itself to physical
+ encounters, but rather facilitating them. Libervia will
+ always fight against all forms of technology control by
+ private interests. The network must belong to everybody, and
+ be a force of expression and freedom for all Humanity.
+
+
Social Contract
+
+ Towards this end, "Libervia" and those who participate in
+ the project operate on a Social Contract, a commitment to
+ those who use it. This Contract involves the following
+ points:
+
+
+ We put the freedom at the top of our priorities:
+ freedom of the people, freedom with her data. To
+ achieve this, "Libervia" is a Libre Software - an
+ essential condition - and its infrastructure also
+ relies on Libre Software, meaning softwares that
+ respect the 4 fundamental rules :
+
+
+ The freedom to run the program for any
+ purpose.
+
+
+ The freedom to study how the program works,
+ and change it to make it do what you wish.
+
+
+ The freedom to redistribute copies so you
+ can help your neighbor.
+
+
+ The freedom to improve the program, and
+ release your improvements (and modified
+ versions in general) to the public, so that
+ the whole community benefits. You have the
+ full possibility to install your own version
+ of "Libervia" on your own machine, to verify
+ - and understand - how it works, adapt it to
+ your needs, and share the knowledge with
+ your friends.
+
+
+
+
+ The information regarding the people belong to them,
+ and we will never have the pretention - and
+ indecency ! - to consider the content that they
+ produce or relay via "Libervia" as our property. As
+ well, we commit ourselves to never make profit from
+ selling any of her personal information.
+
+
+ We greatly encourage a general decentralisation.
+ "Libervia" being based on a decentralised protocol
+ (XMPP), it is by nature decentralised. This is
+ essential for a better protection of your
+ information, a better resistance to censorship and
+ hardware or software failures, and to alleviate
+ authoritarian tendencies.
+
+
+ By fighting against the attempts at private control
+ and commercial abuses of the network, and trying to
+ remain independent, we are absolutely opposed to any
+ form of advertisement: you will never see any
+ advertisement coming from us
+
+
+ The people Equality is essential for us, we refuse
+ any kind of discrimination, being based on
+ geographical location, population category, or any
+ other ground.
+
+
+ We will do whatever is possible to fight against any
+ kind of censorship including protecting the speech
+ of victims of harassment, hate speech, threats,
+ humiliation and anything that could lead to self
+ censorship. The network must be a means of
+ expression for everyone.
+
+
+ We refuse the mere idea of an absolute authority
+ regarding the decisions taken for "Libervia" and how
+ it works, and the choice of decentralisation and the
+ use of Libre Software allows to reject all hierarchy.
+
+
+ The idea of Fraternity is essential. This is why:
+
+
+ we will help the people, whatever their
+ computer literacy is, to the extent of what we can
+
+
+ we will as well commit ourselves to help the
+ accessibility to "Libervia" for all
+
+
+ "Libervia" , XMPP, and the technologies used
+ help facilitate the electronic exchanges,
+ but we strive to focus on real and human
+ exchanges : we will always favor Real on
+ Virtual.
+
+
+
+
+
+
Features
+
+
+
+ Chat with your friends, family or coworkers;
+
+
+ Encrypt conversations to protect your privacy;
+
+
+ Blog publicly or only with a group of contacts;
+
+
+ Share files directly (peer to peer) or store them on
+ your server and access them from anywhere;
+
+
+ Share private photos albums with your family;
+
+
+ Create and manage events;
+
+
+ Organise your day to day life or work with lists.
+
+
+
+
Interfaces
+
+
+
+ Works natively on desktop;
+
+
+ Works on the web;
+
+
+ Works natively on mobile devices;
+
+
+ Cross-platform;
+
+
+ Powerful command-line interface;
+
+
+ Highly modular and customisable;
+
+
+ Lot of powerful tools and features accompany the
+ project, please check the
+
+ documentation .
+
+
+
+
+ With its easy invitation system, you can smoothly meet your
+ family or friends. It is a perfect fit to share with your
+ loved ones.
+
+
+ Libervia is a Libre software, based on well established
+ standards (XMPP), decentralised and federating. It is
+ developed around strong ethical values. Check our
+ social
+ contract .
+
+
Press
+
+
+
+
Resources
+
+
+
+
+
+ "Salut a Toi" is the name of the association which manages
+ Libervia.
+
+
+
+
+
+
+
diff --git a/xhtml/movim.xhtml b/xhtml/movim.xhtml
new file mode 100644
index 0000000..381ea38
--- /dev/null
+++ b/xhtml/movim.xhtml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » The social platform shaped for your community.
+
+
About Movim
+
+ Movim is an abbreviation of "My Open Virtual Identity
+ Manager" and is a distributed social network which, just
+ like Blasta, is built on top of XMPP.
+
+
+ Movim allows people to communicate easily with their
+ friends, family, and work colleagues using chatroom, video
+ conferences, and private messaging. Movim works as a
+ front-end for XMPP network.
+
+
+ Movim can act as your decentralized messaging app or a
+ full-fledged social media platform giving you an all-in-one
+ experience without relying on a centralized network.
+
+
+ Movim addresses the privacy concerns related to centralized
+ social networks by allowing people to set up their own
+ server (or "pod") to host content; pods can then interact to
+ share status updates, photographs, and other social data.
+
+
+ People can export their data to other pods or offline
+ allowing for greater flexibility.
+
+
+ Movim allows to host data with a traditional internet host,
+ a VPS-based host, an ISP, or a friend.
+
+
+ The framework, which is built upon PHP, is a free software
+ and can be experimented with by external developers.
+
+
What is Movim?
+
+ Movim is a social and chat platform that acts as a frontend
+ for the XMPP network.
+
+
+ Once deployed Movim offers a complete social and chat
+ experience for the decentralized XMPP network. It can easily
+ connect to several XMPP servers at the same time.
+
+
+ With a simple configuration it can also be restricted to one
+ XMPP server and will then act as a powerful frontend for it.
+ Movim is fully compatible with the most used XMPP servers
+ such as ejabberd or Prosody.
+
+
Blogs & Communities
+
+ Movim simplifies the management of your publications and
+ news articles. So forget the ads, forget the superfluous. We
+ designed an interface that really focuses on your content.
+
+
+ In Communities you can publish and subscribe to various
+ nodes on different topics. Just click on the Communities
+ link in your menu and start exploring!
+
+
Chats & Chatrooms
+
+ Start a conversation with a friend or join a multi-user
+ chatroom in one click.
+
+
+ Movim chats are coming with many features to give you the
+ best chat experience across all your devices
+
+
Let's get started!
+
+
+ Create an XMPP account
+ and join one of our public instances on
+
+ join.movim.eu
+ (If you already have a XMPP account, you can directly join).
+
+
Press
+
+
+
+
Resources
+
+
+
+
+
+ Blasta was inspired by Movim and Rivista.
+
+
+
+
+
+
+
diff --git a/xhtml/now.xhtml b/xhtml/now.xhtml
new file mode 100644
index 0000000..97b7647
--- /dev/null
+++ b/xhtml/now.xhtml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» The /now page movement.
+
Now page
+
This is the /now page of Blasta.
+
+ Blasta is dancing happily and is also recommending you to do
+ the same, especially when you wake up after a good night ;-)
+
+
+ And in case you want to know what Blasta is doing today,
+ please read this list.
+
+
Eternity
+
+ Blasta is adopting the attitude of "living for the sake of
+ eternity", the attitude of living with eternity always in
+ mind, instead of living only for the moment.
+
+
Empowerment
+
+ Blasta is empowering people by making the access to
+ information open, impartial and unbiased.
+
+
Sovereignty
+
+ Blasta is respecting the privacy of people and consequently
+ preserving their sovereignty and their precious information.
+
+
Knowledge
+
+ Blasta is intending to enhance the access to valuable
+ knowledge and information.
+
+
Features
+
+ Blasta is working on new enhancements and features, namely
+ to make its database federated and shared between multiple
+ instances, by adding support for the protocol DHT, either by
+ BitTorrent or IPFS or even both; and
+
+
+ Blasta is also working on adding support for Gemini and
+ Gopher; and
+
+
+ Blasta is further working on new extensions and userscripts.
+
+
Dance
+
+ Blasta just loves to dance and be happy!
+
+
+
+ Visit nownownow.com ,
+ for more /now pages.
+
+
+
+
+
+
+
diff --git a/xhtml/philosophy.xhtml b/xhtml/philosophy.xhtml
new file mode 100644
index 0000000..114c57c
--- /dev/null
+++ b/xhtml/philosophy.xhtml
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» The principles of Blasta.
+
Philosophy
+
+ This document explains the fundamental principles of Blasta
+ and sharing of referential resources (also known as
+ "bookmarks").
+
+
+ Some ideas that are conveyed in this article were taken
+ from the video
+
+ Trusted Computing .
+
+
Visit AgainstTCPA.com
+ to learn more about the subject of computing and trust.
+
+
Control
+
+ You can control your bookmarks by your own personal
+ conviction (minute 00:01:30 to the video), and the most
+ that Blasta can ever do, is to not accept links into its
+ system, and that is it. Blasta will never be able to remove
+ information from your system without you doing so by
+ yourself.
+
+
Think
+
+ The Blasta system is constructed and designed in a fashion
+ which would encourage, inspire and exert your mind to incite
+ yourself to research, investigate, discover, and, most
+ importantly, to own and to think.
+
+
Trust
+
+ TRUST | confidence.
+
+
+ Trust is the personal believe in correctness of something.
+
+
+ It is the deep conviction of truth and rightness, and can
+ not be enforced.
+
+
+ If you gain someone's trust, you have established an
+ interpersonal relationship, based on communication, shared
+ values and experiences.
+
+
+ TRUST always depends on mutuality.
+
+
Conclusion
+
+ Blasta is designed for organizing, sharing and spreading of
+ information in a collaborative fashion, be it in an
+ exclusive manner, by the use of selective authorizations or
+ inclusive manner, or both.
+
+
+ Blasta is designed for people to control their information,
+ or more precisely, not to disrupt nor sabotage freedoms.
+
+
+ By these, to a great degree, you are able to make use of
+ Blasta in any shape that you desire, and consequently form
+ your bookmarks in your own image.
+
+
+
+ So God created man in his own image, in the image of God
+ created he him; male and female created he them.
+ ― Genesis 1:27 (KJV)
+
+
+
+
+
+
+
diff --git a/xhtml/profile.xhtml b/xhtml/profile.xhtml
new file mode 100644
index 0000000..70f2a34
--- /dev/null
+++ b/xhtml/profile.xhtml
@@ -0,0 +1,490 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» Information of your Jabber ID.
+
Your Profile
+
+ This page provides a general survey of your XMPP account and
+ stored bookmarks.
+
+
+
Export
+
+ Export bookmarks to a file.
+
+
+
+
+
+ Private
+
+
+
+ JSON ,
+
+ TOML .
+
+
+ Public
+
+
+
+ JSON ,
+
+ TOML .
+
+
+ Read
+
+
+
+ JSON ,
+
+ TOML .
+
+
+
+
Import
+
+ Import bookmarks from a file, and choose a node to import
+ your bookmarks to.
+
+
+
+
+
+
Permissions
+
+ Choose the desired
+
+ access models
+ of your directories.
+
+
+
+
+
+
+
Routine
+
+ Choose a routine (i.e. default) directory (i.e. node).
+
+
+
+ Private
+
+ Public
+
+ Read
+
+
+
+
+ Please export your bookmarks before
+ proceeding.
+
+
+
Termination
+
+ Due to security concerns, Blasta does not have a built-in
+ mechanism to delete nodes.
+
+
+ Warning! The following actions are irreversible and are
+ committed at your own risk!
+
+
+ If you want to delete your Blasta nodes from your XMPP
+ account, you can execute the following commands, using the
+ XML Console of Gajim ,
+ Psi , or
+ Psi+ .
+
+
Delete your public bookmarks
+
+ <iq type='set'
+ from='{{jabber_id}}'
+ to='{{jabber_id}}'
+ id='delete1'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
+ <delete node='urn:xmpp:bibliography:0'/>
+ </pubsub>
+ </iq>
+
+
Delete your private bookmarks
+
+ <iq type='set'
+ from='{{jabber_id}}'
+ to='{{jabber_id}}'
+ id='delete2'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
+ <delete node='xmpp:bibliography:private:0'/>
+ </pubsub>
+ </iq>
+
+
Delete your reading list
+
+ <iq type='set'
+ from='{{jabber_id}}'
+ to='{{jabber_id}}'
+ id='delete3'>
+ <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
+ <delete node='xmpp:bibliography:read:0'/>
+ </pubsub>
+ </iq>
+
+
+
+ “Blasta bookmarks are stored on your own XMPP account.”
+
+
+
+
+
+
+
diff --git a/xhtml/projects.xhtml b/xhtml/projects.xhtml
new file mode 100644
index 0000000..5a8e201
--- /dev/null
+++ b/xhtml/projects.xhtml
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» Information about projects that are similar to Blasta.
+
Similar projects
+
+ Same as with search engines, so called, Blasta is a
+ referential index (or, if you would, a bibliographical
+ index) of any
+ URI
+ sequence that you desire, from Gemini and HTTP to Geo and
+ XMPP, and so we deem it relevant to list projects with
+ similar aims and objectives, and which do not hinder your
+ freedom.
+
+
+
+ buku is a powerful bookmark manager and a personal textual
+ portal.
+
+
+ buku can import bookmarks from browsers or fetch the
+ title, tags and description of a URL from a given page. Use
+ your favourite editor to add, compose and update bookmarks.
+ Search bookmarks instantly with multiple search options,
+ including regex and a deep scan mode (handy with URLs).
+
+
+ There's no tracking, hidden history, obsolete records, usage
+ analytics or homing.
+
+
+ buku is a library too! There are several related projects,
+ including a browser plug-in.
+
+
+
+ Omnom is a bookmarking and snapshotting service, and
+ consists of two parts; a multi-user HTML application that
+ accepts bookmarks and snapshots and a browser extension
+ responsible for bookmark and snapshot creation.
+
+
+
+ Do you want to share the links you discover?
+
+
+ Shaarli is a minimalist bookmark manager and link sharing
+ service that you can install on your own server. It is
+ designed to be personal (single-user), fast and handy.
+
+
+
+ A free metasearch engine which aggregates results from
+ search services and databases.
+
+
+ SearXNG is a free internet metasearch engine which
+ aggregates results from more than 70 search services. People
+ are neither tracked nor profiled. Additionally, SearXNG can
+ be used over I2P and Tor for online anonymity.
+
+
+
+ Torrents.csv is a collaborative repository of torrents,
+ consisting of a searchable torrents.csv file. It aims to be
+ a universal file system for popular data.
+
+
+ Torrents.csv is a collaborative git repository of torrents,
+ consisting of a single, searchable torrents.csv file. Its
+ initially populated with a January 2017 backup of the pirate
+ bay, and new torrents are periodically added from various
+ torrents sites. It comes with a self-hostable webserver, a
+ command line search, and a folder scanner to add torrents.
+
+
+
+ YaCy is a distributed Search Engine, based on a peer-to-peer
+ network.
+
+
+ Imagine if, rather than relying on the proprietary software
+ of a large professional search engine operator, your search
+ engine was run across many private devices, not under the
+ control of any one company or individual. Well, that is what
+ YaCy does!
+
+
+ YaCy was initially started as a bookmarking system.
+
+
+
+ It Is “All Of Us For All Of Us” Or We Are On Our Own.
+ ―
+ Federati Nu
+
+
+
+
+
+
+
diff --git a/xhtml/pubsub.xhtml b/xhtml/pubsub.xhtml
new file mode 100644
index 0000000..79439ca
--- /dev/null
+++ b/xhtml/pubsub.xhtml
@@ -0,0 +1,251 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» This is a help guide for XMPP PubSub of Blasta.
+
PubSub
+
+ Blasta manages and posts information on PubSub nodes of your
+ own XMPP account.
+
+
Synopsis
+
+ The technologies (i.e. standards and specifications) that
+ are being served for this task are Atom Syndication Format
+ (ASF) and Publish-Subscribe.
+
+
+
Atom Syndication Format (RFC 4287)
+
+ The Atom Syndication Format or ASF is a standard for
+ syndication which was yielded from RSS. The ASF was
+ approved by the Internet Engineering Task Force (IETF)
+ as
+ RFC 4287 in december 2005.
+
+
+ This standard is an XML-based document format that describes
+ lists of related information known as "feeds". Feeds are
+ composed of a number of items, known as "entries", each with
+ an extensible set of attached metadata. For example, each
+ entry has a title.
+
+
+ The primary use case that Atom addresses is the syndication
+ of content such as journals and news headlines to sites as
+ well as directly to user agents.
+
+
Publish-Subscribe (XEP-0060)
+
+ Publish-Subscribe or PubSub is an XEP specification for
+ XMPP which was approved by the XMPP Standard Foundation (XSF)
+ as
+ XEP-0060 in november 2002.
+
+
+ This specification defines an XMPP protocol extension for
+ generic publish-subscribe functionality. The protocol
+ enables XMPP entities to create nodes (topics) at a pubsub
+ service and publish information at those nodes; an event
+ notification (with or without payload) is then broadcasted
+ to all entities that have subscribed to the node. Pubsub
+ therefore adheres to the classic Observer design pattern and
+ can serve as the foundation for a wide variety of applications,
+ including news feeds, content syndication, rich presence,
+ geolocation, workflow systems, network management systems,
+ and any other application that requires event notifications.
+
+
Atomsub
+
+ Atomsub, or Atom Over XMPP, is the combination of PubSub and
+ Atom. Learn more .
+
+
Technicalities
+
+ The location at which Blasta stores your information is inside
+ node urn:xmpp:bibliography:0
of
+ {% if jabber_id %}{{jabber_id}}{% else %}your@jabber.id{% endif %}
+ which interpretes to
+ xmpp:{% if jabber_id %}{{jabber_id}}{% else %}your@jabber.id{% endif %}?;node=urn:xmpp:bibliography:0
+ and is where you can find all of your information, even if
+ this Blasta instance be offline.
+
+
PubSub
+
+ This is an illustrated example of a node item which stores
+ as an Atom entry.
+
+
+ JID : {% if jabber_id %}{{jabber_id}}{% else %}your@jabber.id{% endif %}
+ Node : urn:xmpp:bibliography:0
+ Item : 3d0db4c019a01ebbbab1cf0723ed7ddd
+
+
+ The item ID is a an MD5 string which was yielded by a URL
+ which was hashed by the MD5 algorithm, and is used as a
+ representation of a URL ID for referential purposes in the
+ Blasta system.
+
+
Atom
+
+ This is an illustrated example of an Atom Syndication Format
+ entry which is stored in item
+ 3d0db4c019a01ebbbab1cf0723ed7ddd
.
+
+
+ Title : The Pirate Bay - The most resilient BitTorrent site
+ Summary : Download music, movies, games, software!
+ Category : brand:the-pirate-bay
+ Category : directory:torrent
+ Category : service:search
+ Link : https://tpb.party/
+ Published : {{date_now_iso}}
+ Updated : {{date_now_iso}}
+
+
This is the data as retrieved by an XMPP IQ Stanza.
+
+ <item xmlns="http://jabber.org/protocol/pubsub" id="3d0db4c019a01ebbbab1cf0723ed7ddd">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title type="text">The Pirate Bay - The most resilient BitTorrent site</title>
+ <summary type="text">Download music, movies, games, software!</summary>
+ <category term="brand:the-pirate-bay" />
+ <category term="directory:torrent" />
+ <category term="service:search" />
+ <link href="https://tpb.party/" />
+ <published>{{date_now_iso}}</published>
+ <updated>{{date_now_iso}}</updated>
+ </entry>
+ </item>
+
+
+ Which be realized in Blasta as follows.
+
+
+
Download music, movies, games, software!
+
+
Conclusion
+
+ This was an illustrative representation of how your data is
+ stored by Blasta as Atom feed entries on Publish-Subscribe
+ node urn:xmpp:bibliography:0
of your XMPP
+ account.
+
+
+
+ “Talent hits a target no one else can hit.
+ Genius hits a target no one else can see.”
+ ― Arthur Schopenhauer
+
+
+
+
+
+
+
diff --git a/xhtml/questions.xhtml b/xhtml/questions.xhtml
new file mode 100644
index 0000000..9c397df
--- /dev/null
+++ b/xhtml/questions.xhtml
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» Queries about Blasta.
+
Questions
+
+ This document enumerates questions that were referred to the
+ project Blasta.
+
+
What is JID and what does it mean?
+
+ JID is an abbreviation of Jabber ID.
+
+
+ A Jabber ID is the address of your XMPP account with which
+ Blasta identifies you.
+
+
+ What is the instance:keyword method?
+
+
+ The method instance:keyword is a practice which improves
+ bookmarks organizing, archiving, filtering and searching, as
+ it narrows the context of a given bookmark.
+
+
+ Suppose you have two bookmarks; one is related to
+ maintenance and removal of rust, while the other is related
+ to the computer language which is called rust.
+
+
+ To make the tagging more efficient for you and others, the
+ bookmark which is related to maintenance be tagged with
+ "rust" and "tutorial:maintenance", while the bookmark which
+ is related to computer language rust be tagged with
+ "code:rust".
+
+
Why did you create Blasta?
+
+ We have created Blasta out of necessity.
+
+
+ We have felt that there are no viable solutions that would
+ provide a sensible management of URis, and also a mean to
+ share URIs.
+
+
+ We are well aware of project YaCy which indeed provides
+ exactly what we desire, yet, the interface that is provided
+ by YaCy does not fulfill the interface which we have
+ determined to be fit for the cause, and which would be fit
+ with certain functionalities that we deemed to be necessary.
+
+
+ We also wanted a system that would be independent and in
+ current sync, and we have decided that XMPP is the perfect
+ platform for this task.
+
+
+ Hence, we have created Blasta, which is a combination of
+ YaCy and Shaarli, and is built on top of XMPP.
+
+
Why do you provide Blasta for free?
+
+ While we want to make profit, and feed our wives and
+ children, we are aware that we must be providing the Blasta
+ system for free, because this is a matter of urgency; and
+
+
+ Because some people do bad things, such as attempting to
+ centralize the internet, and revoke privacy, which is
+ crucial; and
+
+
We can not afford to have another centralized service which
+ later becomes clandestinely subdued to thugs from government
+ or intelligence, which is the worst of all circumstances.
+
+
What is your perspective?
+
+ Our perspective is that people should be living for the sake
+ of eternity, which is what we are doing by creating and
+ distributing Blasta.
+
+
+ As conveyed in verse 77 of Hávamál:
+
+
+ “Kinsmen die, and cattle die and so must one die oneself;
+
+ But there is one thing I know which never dies,
+
+ and that is the fame of a dead man deeds.”
+
+ ― Hávamál: Sayings of the High One
+
+
+ The German philosopher Arthur Schopenhauer has expressed
+ since the same idea when he said:
+
+
+ “The most any man can hope for:
+
+ Is a heroic passage through life.”
+
+ ― Arthur Schopenhauer
+
+
+ To explain it in other words:
+
+
+ Greatness, rather than happiness, is
+
+ a mark of a good life .
+
+
+ Live for the sake of eternity, for the past, and towards the
+ future.
+
+
Why did you choose XMPP as a platform?
+
+ The XMPP platform (which is also known as Jabber), is a
+ reputable and a strong telecommunication platform which
+ almost every intelligence and militant agency on the world
+ is making use of as a premier telecommunication mean.
+
+
+ Because we did not want to use databases, which sometimes
+ are too complicated for most of the people who are
+ interested in sharing links publicly and privately, we have
+ determined that XMPP would be the best choice for this task.
+
+
+ XMPP was chosen as a platform, because it is a standard and
+ it is extensible; it has all the features that are necessary
+ to integrate other standardised mechanisms to store data
+ which does not necessarily has to require a database (e.g.
+ SQLite); XMPP provides exactly that, whether we want to make
+ use of Atom Syndication Format and even JSON, if at all
+ needed.
+
+
+ So we chose the specification XEP-0060: Publish-Subscribe
+ (PubSub) as a base mechanism for a data storage platform.
+
+
+ In addition to it, XMPP offers a robust and stable mechanism
+ of storing of contents, on both XMPP and HTTP, and more, or
+ even most, importantly, the PubSub mechanism of XMPP is
+ truely private and is truely owned by the account owner,
+ which is also a major advantage to Blasta, not only for the
+ sake of privacy and data integrity, but also for an
+ immediate recoverying of the bookmarks sharing system, if
+ the Blasta database would be to diappear or should be
+ restructured.
+
+
Use XMPP. Spread XMPP. Be XMPP.
+
+
+ If we were to choose another platfom, it probably would have
+ been DeltaChat (SMTP and IMAP), Nostr or Session.
+
+
Why did you decide to make Blasta open source?
+
+ One of the main reasons is for freedom to thrive.
+
+
+ We are worried due to various of PsyOps that are conducted
+ by governments, military and intelligence agencies, and we
+ realize that most of their success relies on the
+ centralization of information hubs; and
+
+
+ We are further worried of the awful measures that are being
+ dictated by HTML browser vendors, who distort the original
+ intentions and expectations for the internet, that were
+ presented to the public during the 90's, and instead these
+ vendors are
+
+ conspiring to make people to forget about the existance
+ of crucial technologies such as
+
+ syndication and XSLT, and they are also committing
+ crimes against people, and the worst of all crimes is of
+ attempting to revoke the privacy of people.
+
+
+ Hence, we have decided that it is crucial to make a resource
+ sharing platform to utilize standards that can be utilized
+ outside of browsers, and
+
+
+ We have further decided to make Blasta available in an open
+ source form, that would allow public examining and auditing
+ of the mechanism that Blasta offers, including
+ gaining the trust of
+ people by showcasing that the data is truely private,
+ and that the data is truely owned by the people, and that
+ the data is backed by true standards.
+
+
+ “No reason to explain this. It is absolutely insane to
+ use a software to browse the internet of which source
+ code is not publicly auditable.”
+ ―
+
+ Luke Smith
+
+
+
+ Does Blasta deploy ECMAScript (JavaScript)
+
+
+ Blasta does not make any use of ECMAScript.
+
+
+ We have decided not to use unecessary components nor "toxic"
+ features that would compromise the intended experience that
+ Blasta attempts to offer, neither waste power resources on
+ your end for no just reason, nor to compromise your privacy.
+
+
+ To compensate this, Blasta is designed in a fashion that
+ would make it easier to customize appearance and
+ functionality with
+ userscripts and userstyles .
+
+
Conclusion
+
+ Please contact us and ask us
+ anything. We will be happy to read from you!
+
+
+
+ “Trust is the basis for social value and self-organisation,
+ but technical assurances are what help make it trustworthy.”
+ ―
+
+ NGI Assure
+
+
+
+
+
+
+
+
diff --git a/xhtml/register.xhtml b/xhtml/register.xhtml
new file mode 100644
index 0000000..06fd722
--- /dev/null
+++ b/xhtml/register.xhtml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
diff --git a/xhtml/result.xhtml b/xhtml/result.xhtml
new file mode 100644
index 0000000..1afa598
--- /dev/null
+++ b/xhtml/result.xhtml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » {{message}}
+
+
+ {{description}}
+
+
+ You can return to the previous page or browse
+ {% if jabber_id %}
+ your bookmarks .
+ {% else %}
+ the portal .
+ {% endif %}
+
+
+
+
+
+
+
diff --git a/xhtml/search.xhtml b/xhtml/search.xhtml
new file mode 100644
index 0000000..83e7f1e
--- /dev/null
+++ b/xhtml/search.xhtml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» {{message}}
+
{{description}}
+
+ {{label}}:
+
+
+
+ {% if path == 'all' %}
+
+ Search public bookmarks. Or seek by a
+ URL {% if jabber_id %}, or
+ lookup your own
+ bookmarks{% endif %}.
+
+ {% elif path == 'url' %}
+
+ Search a bookmark by a URL. Or seek for public
+ bookmarks {% if jabber_id %},
+ or lookup your
+ own bookmarks{% endif %}.
+
+ {% else %}
+
+ Search bookmarks by Jabber ID. Or seek for public
+ bookmarks , or lookup by a
+ URL .
+
+ {% endif %}
+
+
+
+
+
+
diff --git a/xhtml/software.xhtml b/xhtml/software.xhtml
new file mode 100644
index 0000000..e924a7a
--- /dev/null
+++ b/xhtml/software.xhtml
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» Information about projects that Blasta is based upon.
+
Software
+
+ Blasta was made possible thanks to the following projects.
+
+
+
+ FastAPI is a fast (high-performance), framework for building
+ APIs with Python based on standard Python type hints.
+
+
+
+ The lxml XML toolkit is a Pythonic binding for the C
+ libraries libxml2 and libxslt. It is unique in that it
+ combines the speed and XML feature completeness of these
+ libraries with the simplicity of a native Python API, mostly
+ compatible but superior to the well-known ElementTree API.
+ The latest release works with all CPython versions from 3.6
+ to 3.12. See the introduction for more information about
+ background and goals of the lxml project.
+
+
+
+ Python is a computer language that lets you work quickly and
+ integrate systems more effectively.
+
+
+
+ Slixmpp is an MIT licensed XMPP library for Python 3.7+.
+ It is a fork of SleekXMPP.
+ Slixmpp's goal is to only rewrite the core of the library
+ (the low level socket handling, the timers, the events
+ dispatching) in order to remove all threads.
+
+
+
+ SQLite is a C-language library that implements a small,
+ fast, self-contained, high-reliability, full-featured, SQL
+ database engine. SQLite is the most used database engine in
+ the world. SQLite is built into all mobile phones and most
+ computers and comes bundled inside countless other
+ applications that people use every day.
+
+
+
+ A low-level server/application interface for async
+ frameworks, using the ASGI specification to make it possible
+ to start building a common set of tooling usable across all
+ async frameworks.
+
+
+
+ The Extensible Markup Language (XML) is a markup language
+ that provides rules to define any data. Unlike other
+ programming languages, XML cannot perform computing
+ operations by itself. Instead, any programming language or
+ software can be implemented for structured data management.
+
+
+
+ XMPP is the Extensible Messaging and Presence Protocol, a
+ set of open technologies for instant messaging, presence,
+ multi-party chat, voice and video calls, collaboration,
+ lightweight middleware, content syndication, and generalized
+ routing of XML data.
+
+
+
+ Have you joined to XMPP, yet?
+
+
+
+
+
+
+
diff --git a/xhtml/syndication.xhtml b/xhtml/syndication.xhtml
new file mode 100644
index 0000000..c8374b3
--- /dev/null
+++ b/xhtml/syndication.xhtml
@@ -0,0 +1,1004 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» This is a help guide for syndication of Blasta.
+
Feeds
+
+ This page illustrates the available functionalities that
+ Blasta provides for syndication
+ feeds .
+
+
+
Icon
+
+ Blasta offers Atom feeds on pages of bookmarks and of
+ pages that list bookmarks.
+
+
+ An atom ⚛ or an orange
+
+ icon at the bottom of a subject page would indicate of
+ availability of an Atom Syndication Feed for use with
+ Feed Reader.
+
+
+ A bulb 💡 or an XMPP
+
+ icon at the bottom of a subject page would indicate of
+ availability of a PubSub subscription for use with an
+ XMPP client.
+
+
+
Patterns
+
+ The set of Atom URIs (XMPP) and URLs (HTTP) available by
+ Blasta is as follows:
+
+
HTTP Pattern
+
+
+ Type
+ Function
+ URL
+
+
+ JID
+
+ A list of bookmarks of a given JID by recency.
+
+ {{origin}}/jid/fionn@blasta.i2p?mode=feed
+
+
+ JID+Tag
+
+ A list of bookmarks of a given tag of a given
+ JID by recency.
+
+ {{origin}}/jid/fionn@blasta.i2p?mode=feed&tags=jabber
+
+
+ JID+Tags
+
+ A list of bookmarks of given tags of a given JID
+ by recency.
+
+
+ {{origin}}/jid/fionn@blasta.i2p?mode=feed&tags=jabber+xmpp
+
+
+
+ Tag*
+
+ A list of bookmarks of a given tag by recency.
+
+ {{origin}}/?mode=feed&tags=jabber
+
+
+ Tags*
+
+ A list of bookmarks of given tags by recency.
+
+ {{origin}}/?mode=feed&tags=jabber+xmpp
+
+
+ URL
+
+ A list of people who have bookmarked a given URL
+ by recency.
+
+ {{origin}}/?mode=feed&url=https://xmpp.org
+
+
+ Hash
+
+ A list of people who have bookmarked a given
+ Hash by recency.
+
+ {{origin}}/?mode=feed&hash=md5sum
+
+
+ Filetype*
+
+ A list of bookmarks of given filetypes by
+ recency.
+
+
+ {{origin}}/?mode=feed&filetype=meta4+metalink+torrent
+
+
+
+ Protocol*
+
+ A list of bookmarks of given prootcols by
+ recency.
+
+
+ {{origin}}/?mode=feed&protocol=gemini+gopher+ipfs+magnet
+
+
+
+ Top-level domain*
+
+ A list of bookmarks of given top-level domains
+ by recency.
+
+ {{origin}}/?mode=feed&tld=bit+i2p
+
+
+ Popular
+ A list of popular bookmarks by popularity.
+ {{origin}}/popular?mode=feed
+
+
+ Recent
+ A list of popular bookmarks by recency.
+ {{origin}}/recent?mode=feed
+
+
+
+ * This type does not work when is combined with types
+ "Hash" nor "URL".
+
+
XMPP Pattern
+
+
+ Type
+ Function
+ URI
+
+
+ JID*
+
+ A list of bookmarks of a given JID by recency.
+
+
+ xmpp:fionn@blasta.i2p?pubsub;action=subscribe;node=urn:xmpp:bibliography:0
+
+
+
+ JID+Tag**
+
+ A list of bookmarks of a given tag of a given
+ JID by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=fionn@blasta.i2p:tags:jabber
+
+
+
+ JID+Tags**
+
+ A list of bookmarks of given tags of a given JID
+ by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=fionn@blasta.i2p:tags:jabber+xmpp
+
+
+
+ Tag
+
+ A list of bookmarks of a given tag by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=tags:jabber
+
+
+
+ Tags**
+
+ A list of bookmarks of given tags by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=tags:jabber+xmpp
+
+
+
+ URL
+
+ A list of people who have bookmarked a given URL
+ by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node==url:https://xmpp.org
+
+
+
+ Hash
+
+ A list of people who have bookmarked a given
+ Hash by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=hash:md5sum
+
+
+
+ Filetype**
+
+ A list of bookmarks of given filetypes by
+ recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=filetype:meta4+metalink+torrent
+
+
+
+ Protocol**
+
+ A list of bookmarks of given prootcols by
+ recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=protocol:gemini+gopher+ipfs+magnet
+
+
+
+ Top-level domain**
+
+ A list of bookmarks of given top-level domains
+ by recency.
+
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=tld:bit+i2p
+
+
+
+ Popular
+ A list of popular bookmarks by popularity.
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=popular
+
+
+
+ Recent
+ A list of popular bookmarks by recency.
+
+ xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=recent
+
+
+
+
+ * This type of feed is entirely independent from Blasta,
+ and is available only via XMPP.
+
+
** This type of feed is not yet available for XMPP.
+
Notes
+
+ Blasta feeds are updated only once in every two hours.
+
+
+ Do not attempt to query any feeds more often than every
+ 120 minutes.
+
+
+ If you attempt to query a feed more frequently, you will
+ receive an error code 429.
+
+
+
Software
+
+ Feed Readers are offered for Desktop and Handset devices,
+ including as HTML (somtimes referred to as "HTML-based" or
+ "Online").
+
+
+
+ Desktop
+ About
+ Platforms
+
+
+
+
+ Akregator
+
+
+
+ Akregator is a news feed reader.
+ It enables you to follow news sites, journals and
+ other Atom/RSS-enabled sites without the need to
+ manually check for updates using a browser.
+ Akregator is designed to be both easy to use and to
+ be powerful enough to read hundreds of news sources
+ conveniently.
+ It comes with a fast search, advanced archiving
+ functionality and an internal browser for easy news
+ reading.
+
+ BSD, Linux
+
+
+
+
+ Feed The Monkey
+
+
+
+ FeedTheMonkey is a desktop client for TinyTinyRSS.
+ That means that it doesn't work as a standalone feed
+ reader but only as a client for the TinyTinyRSS API
+ which it uses to get the normalized feeds and to
+ synchronize the "article read" marks.
+
+ BSD, Linux
+
+
+ Fraidycat
+
+ Fraidycat is a desktop app or browser extension. Use
+ it to follow people (hundreds) on whatever platform
+ they choose - Movim, Libervia, Pleroma, a journal,
+ even on a public PeerTube.
+
+ BSD, Linux, Mac, ReactOS
+
+
+
+ Leech Craft
+
+
+ LeechCraft is a free open source cross-platform
+ modular live environment. It is a modular system,
+ and by installing different modules you can
+ customize the feature set, keeping off the things
+ you do not need and getting a decent IM client,
+ media player, BitTorrent client, or a feed reader,
+ for example.
+
+ BSD, Linux, Mac, ReactOS
+
+
+ Liferea
+
+ Liferea is a feed reader/news aggregator that brings
+ together all of the content from your favorite
+ subscriptions into a simple interface that makes it
+ easy to organize and browse feeds. Its graphical
+ interface is similar to a desktop mail/news client,
+ with an embedded browser.
+
+ BSD, Linux
+
+
+
+ Net News Wire
+
+
+ It is similar to podcasts — but for reading.
+ NetNewsWire shows you articles from your favorite
+ journals and news sites and keeps track of what you
+ have read. This means you can stop going from page
+ to page in your browser looking for new articles to
+ read. Do it the easy way instead: let NetNewsWire
+ bring you the news. And, if you have been getting
+ your news via data-mining (so called, "social")
+ networks — with their ads, algorithms, user
+ tracking, outrage, and misinformation — you can
+ switch to NetNewsWire to get news directly and more
+ reliably from the sites you trust. Take back control
+ of your news. With free, high-quality, native apps
+ for Mac and iOS.
+
+ Mac
+
+
+ TICKR
+
+ TICKR is a Free Open Source, GTK-based RSS READER
+ application which displays RSS FEEDS in a TICKER bar
+ on your desktop. With a single click, you get the
+ latest headlines scrolling in a thin window on your
+ desktop, as what can be seen on Cable News TV
+ channels.
+
+ BSD, Linux
+
+
+
+
+ Otter Browser
+
+
+
+ Controlled by the people, not vice versa. Otter
+ Browser aims to recreate the best aspects of the
+ classic Opera (12.x) interface using Qt. Otter
+ Browser has a built-in Feed Reader, as true internet
+ browsers should.
+
+ BSD, Linux, Mac, ReactOS
+
+
+
+ Quite RSS
+
+
+ QuiteRSS is a open-source cross-platform Atom/RSS
+ news feeds reader which is quite fast and
+ comfortable to use.
+
+ BSD, Linux, Mac, ReactOS
+
+
+
+ Raven Reader
+
+
+ Raven Reader is an open source desktop news reader
+ with flexible settings to optimize your experience.
+ No login is required, and no personal data is
+ collected. Just select the sites you want to curate
+ articles from and enjoy!
+
+ BSD, Linux, Mac, ReactOS
+
+
+ RSS Bandit
+
+ RSS Bandit is a free application that allows you to
+ read news feeds (both RSS and Atom feeds) and
+ download podcasts from your desktop. It supports
+ NNTP, RSS 0.91, 0.92, 1.0, 2.0 and ATOM. Browsing
+ news without a browser.
+
+ ReactOS
+
+
+
+
+ RSS Guard
+
+
+ RSS Guard is an simple cross-platform multi-protocol
+ desktop feed reader and podcast player. It is able
+ to fetch feeds in RSS/RDF/ATOM/JSON formats and
+ connect to multiple online feed services. It is also
+ able to play most audio/video formats.
+
+ BSD, Linux, Mac, ReactOS
+
+
+
+ RSS Owl and
+
+ RSS Owl nix
+
+
+
+ RSSOwl is a powerful application to organize, search
+ and read your news feeds in a comfortable way. Some
+ of the unique highlights are tabbed reading,
+ powerful searches that can be saved, news filters
+ with automated actions, embedded browser and
+ newspaper layout, tray notifications, clean-up
+ wizard and powerful interface customization.
+
+ BSD, Linux, Mac, ReactOS
+
+
+
+
+ Spot-On
+
+
+
+ Spot-On is an Echo Communications Software which
+ provides a general communication tool, including
+ e-mail.
+
+ BSD, Linux, Mac, ReactOS
+
+
+ Vienna RSS
+
+ ViennaRSS is an Atom/RSS reader for macOS, packed
+ with powerful features that help you make sense of
+ the flood of information that is distributed via
+ these formats today. The Vienna Project is
+ continuously being improved and updated.
+
+ Mac
+
+
+ Mobile
+ About
+ Platforms
+
+
+
+
+ Alligator
+
+
+
+ Alligator is a convergent Atom/RSS feed reader for
+ KDE.
+
+ BSD, Linux
+
+
+
+
+ Feeder
+
+
+
+ Feeder is an open source feed reader
+ (Atom/JSONFeed/RSS) for DivestOS created in 2014.
+ With Feeder you can read the latest news and posts
+ from your favorite sites. Feeder does NOT sync with
+ usual remote backends so no account registration of
+ any kind is necessary. Feeder is free to use and
+ runs locally on your device. Your data is 100%
+ private.
+
+ DivestOS
+
+
+
+
+ feedolin
+
+
+
+ Read your favorit Atom/RSS feeds and stream your
+ podcasts.
+
+ GerdaOS, KaiOS
+
+
+ Feeds
+
+ Feeds is an Atom/RSS news reader for GNOME, with a
+ simple graphical interface and which provides you
+ the ability to organize your feeds with tags, and
+ filter by either single feed or by tag. Only read
+ what you want to read.
+
+ BSD, Linux
+
+
+
+
+ Flym DecSync
+
+
+
+ Flym DecSync is a fork of Flym which adds
+ synchronization using DecSync. To start
+ synchronizing, all you have to do is synchronize the
+ selected DecSync directory, using for example
+ Syncthing.
+
+ DivestOS
+
+
+
+ Net News Wire
+
+
+ NetNewsWire is a free and open source RSS reader. It
+ is fast, stable, and accessible. NetNewsWire shows
+ you articles from your favorite journals and news
+ sites — and keeps track of what you have read. If
+ you have been going from page to page in your
+ browser looking for new articles to read, let
+ NetNewsWire bring them to you instead.
+
+ iOS
+
+
+
+
+ News
+
+
+
+ Subscribe to your favorite RSS and Atom feeds; Sync
+ with your personal Miniflux or Nextcloud server;
+ Smooth and snappy experience even on the older and
+ cheaper devices; Built-in podcast fetcher which can
+ be used with external players; Enhance your news
+ feed with high-resolution preview images; Enjoy
+ modern and minimalistic Material Design; Both light
+ and dark themes are supported; This is an open
+ source app which respects your privacy.
+
+ DivestOS
+
+
+
+
+ Nunti
+
+
+
+ Finally a smart RSS reader which doesn't suck ass or
+ your data.
+
+ DivestOS
+
+
+
+
+ Read You
+
+
+
+ Read You A modern and elegant RSS reader with
+ "Material You" Design.
+
+ DivestOS
+
+
+
+
+ RSS Reader
+
+
+
+ RSS Reader is designed to work with small screens.
+
+ GerdaOS, KaiOS
+
+
+
+
+ Thud
+
+
+
+ If you are tired of having to check a dozen sites
+ and RSS feeds for your daily dose of news, then Thud
+ is the app for you. Using a sleek, mosaic-like
+ interface, Thud organizes all your news and feeds in
+ one place so you can easily stay informed. Plus,
+ with no filtration algorithms, you see the content
+ you want in the most efficient way possible. Thud
+ was created because we love reading the news but did
+ not love all the different sites and apps we had to
+ use to get it. So we made Thud - a sleek,
+ easy-to-use app that gathers all your news and feeds
+ in one place. We want you to have an enjoyable,
+ clutter-free reading experience. With Thud, you can
+ quickly scan through all your favorite news sources
+ without having to jump between different sites and
+ apps.
+
+ DivestOS
+
+
+ Online
+ About
+ Platforms
+
+
+
+ Comma Feed
+
+
+ "Reader" inspired self-hosted and bloat-free feed
+ reader, based on Dropwizard and React/TypeScript.
+
+ Java
+
+
+ feed on feeds
+
+ Your server side, multi-user Atom and RSS
+ aggregator.
+
+ PHP
+
+
+ Feed bin
+
+ A nice place to read on the internet.
+ Follow your passions with Atom, RSS, email
+ newsletters, podcasts, and vocasts. Feedbin is a
+ simple, fast and nice looking RSS reader.
+
+ Ruby
+
+
+ Feed HQ
+
+ FeedHQ is a simple, lightweight HTML-based feed
+ reader.
+
+ Python
+
+
+ Fresh RSS
+
+ FreshRSS is a self-hosted RSS feed aggregator. It is
+ lightweight, easy to work with, powerful, and
+ customizable.
+
+ PHP
+
+
+ Miniflux
+
+ Miniflux is a minimalist and opinionated feed
+ reader.
+
+ Go
+
+
+
+ rawdog
+
+
+ rawdog is an RSS Aggregator Without Delusions Of
+ Grandeur. It is a "river of news"-style aggregator:
+ it uses feedparser to download feeds in RSS, Atom
+ and a variety of other formats, and (by default)
+ produces static HTML pages containing the newest
+ articles in date order.
+
+ Python
+
+
+
+ RRSS
+
+
+ RRSS - Ruby RSS feed reader.
+
+ Ruby
+
+
+ selfoss
+
+ The ultimate multi-purpose RSS reader, data stream,
+ mash-up, aggregation HTML application.
+
+ PHP
+
+
+
+
+ Reader
+
+ and
+
+ Feeds
+
+
+
+ Reader is an HTML app for article saving/scraping;
+ and Feeds is a minimal RSS feed reader.
+
+ PHP, Python
+
+
+ Tiny Tiny RSS
+
+ Tiny Tiny RSS is a free and open source HTML-based
+ news feed (Atom/RSS) reader and aggregator.
+
+ PHP
+
+
+
+ yarr
+
+
+ yarr (yet another rss reader) is an HTML-based feed
+ aggregator which can be used both as a desktop
+ application and a personal self-hosted server.
+
+ Go
+
+
+
+
+ Yarrharr
+
+
+
+ Yarrharr is a simple HTML-based feed reader intended
+ for self-hosting.
+
+ Python
+
+
+ Browser
+ About
+ Platforms
+
+
+
+
+ Feed Preview
+
+
+
+ Feed Preview is an add-on which indicates the
+ availability of Atom or RSS feeds in the URL bar and
+ renders a previews of feeds.
+
+ LibreWolf
+
+
+
+ Feedbro
+
+
+ Advanced Feed Reader with built-in Rule Engine. Read
+ news and journals or any Atom/RDF/RSS source.
+
+ Extension
+
+
+ Fraidycat
+
+ Fraidycat is a desktop app or browser extension. Use
+ it to follow people (hundreds) on whatever platform
+ they choose - Movim, Libervia, Pleroma, a journal,
+ even on a public PeerTube.
+
+ Extension
+
+
+
+
+ Livemarks
+
+
+
+ Get auto-updated RSS feed bookmark folders.
+
+ LibreWolf
+
+
+
+ mPage
+
+
+ Simple dashboard-like RSS feed reader.
+
+ LibreWolf
+
+
+
+
+ Newspaper
+
+
+
+ View feeds as HTML inside your browser. Newspaper
+ renders feeds of types ActivityStreams, Atom, JSON
+ Feed, OPML, RDF, RSS, RSS-in-JSON and SMF.
+
+ Userscript
+
+
+
+
+ Otter Browser
+
+
+
+ Controlled by the people, not vice versa. Otter
+ Browser aims to recreate the best aspects of the
+ classic Opera (12.x) interface using Qt. Otter
+ Browser has a built-in Feed Reader, as true internet
+ browsers should.
+
+ Qt
+
+
+
+
+ Smart RSS Reader
+
+
+
+ RSS Reader which is made to be simple and effective.
+
+ Extension
+
+
+
+
+ “You deserve to have a first-class RSS experience baked in to your browser so well your grandmother could use it.”
+ ― Kroc
+
+
+
+
+
+
+
diff --git a/xhtml/tag.xhtml b/xhtml/tag.xhtml
new file mode 100644
index 0000000..bd2d1a5
--- /dev/null
+++ b/xhtml/tag.xhtml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» {{message}}
+
{{description}}
+
+ {% for tag, instance in tag_list %}
+ {% if instance > 5000 %}
+
+ {% elif instance > 500 %}
+
+ {% elif instance > 50 %}
+
+ {% elif instance > 5 %}
+
+ {% else %}
+
+ {% endif %}
+
+ {{tag}}
+
+ {% endfor %}
+
+
+
+
+
+
+
diff --git a/xhtml/thanks.xhtml b/xhtml/thanks.xhtml
new file mode 100644
index 0000000..f8dc388
--- /dev/null
+++ b/xhtml/thanks.xhtml
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » Gratitude and appreciation from the world over.
+
+
Thanks
+
+ We would want to express our gratitude and appreciation to
+ special men and women from Argentina, Canada, France
+ Germany, Ireland, Italy, Palestine, Russia and The
+ Netherlands.
+
+
Mr. Damian Sartori
+
+ We would want to thank to Mr. Damian Sartori (a.k.a.:
+ TheCoffeMaker) from
+ Cyberdelia for providing instructions for present and
+ future database management.
+
+
Mr. Guus der Kinderen
+
+ We would want to thank to Mr. Guus der Kinderen of
+ Ignite Realtime who
+ has instantly provided us with relevant references from
+
+ XEP-0060: Publish-Subscribe and who has generously
+ provided us with Goodbytes
+ Openfire servers for testing Blasta and related projects.
+
+
Mr. Jérôme Poisson
+
+ We would want to thank to Mr. Jérôme Poisson of project
+ Libervia who has
+ instructed us in coordinating the Blasta system with the
+ Libervia system.
+
+
Mrs. Laura Lapina
+
+ We would want to thank to Mrs. Laura Lapina of 404 City and
+ Loqi who has contributed her advises to select the proper
+ technologies, and plan and engineer the master database
+ operation workflow.
+
+
Mr. Schimon Jehudah Zachary
+
+ We would want to thank to Mr. Schimon Jehudah Zachary of
+ project
+ Rivista who has provided us with concise, yet essential
+ information and presentation about the value and worthiness
+ of making use of XMPP as a platform for sharing of
+ information, including elaborative SQLite queries that have
+ accelerated and saved us precious development time.
+
+
Mr. Simone Canaletti
+
+ We would want to thank to Mr. Simone Canaletti of project
+ WPN who has helped
+ in deploying and testing our applications, including
+ benchmarking for mass deployment, in addition to providing
+ us with the adequate means for hosting the development
+ ground of the Blasta platform and related projects.
+
+
Mr. Stephen Paul Weber
+
+ We would want to thank to Mr. Stephen Paul Weber of project
+ Soprani.ca who has
+ explained to us about various of the XMPP specifications.
+
+
Mr. Timothée Jaussoin
+
+ We would want to thank to Mr. Timothée Jaussoin of project
+ Movim who has contributed
+ from his own time to personally convey and explain to us the
+ XMPP specification
+
+ XEP-0060: Publish-Subscribe , and he has done so on
+ various of occasions, including answering to questions that
+ were already asked and answered more than once.
+
+
XMPP Berlin in association with XMPP Italia
+
+ Finally, we would want to thank to the fine gentlemen
+ Lorenzo, Mario, Martin, Roberto, Schimon, Simone, and others
+ for their efforts of producing the premiere public
+ presention of the Blasta system.
+
+
+
+ Berlin XMPP Meetup
+
+
+
+ XMPP-IT
+
+
+
+ “Some men are heroes by nature in that they will give all
+ that is in them without regard to the effort or to the
+ personal returns.”
+ ―
+ Carson McCullers
+
+
+
+
+
+
+
diff --git a/xhtml/utilities.xhtml b/xhtml/utilities.xhtml
new file mode 100644
index 0000000..ded36ab
--- /dev/null
+++ b/xhtml/utilities.xhtml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
+ » This is a referential guide for features of Blasta.
+
+
+ Synopsis
+
+
+ Blasta offers utilities such as bookmarklet buttons,
+ extensions and even userscripts and userstyles that would
+ extend it features with extra functionalities, and change
+ its appearance.
+
+
+
+ Blasta offers a bookmarklet to facilitate the task of saving
+ HTTP links to your collection. Pin this
+
+
+
+ Blastize!
+ bookmarklet to your bookmarks bar, and click on it
+ when you want to save a link of a given page to your
+ collection.
+
+
+ You can also pin these links to your bookmarks bar, in order
+ to swiftly navigate to your bookmarks with a single click.
+
+
+
+ Blasta
+ and
+
+
+
+ Private
+ and
+
+
+
+ Reading List .
+
+
+ Software
+
+
+ Blasta works together with other XMPP clients; currently,
+ there are two clients, and these are:
+ Libervia and
+ Movim .
+
+
+ Falkon extension
+
+
+ Blasta offers a simple extension for the
+ Falkon browser, that would
+ allow you to save links from your browser, without the need
+ to open the page. This is useful in case you want to save a
+ link which is not intended to be open in a browser, for
+ instance: irc
, magnet
,
+ mailto
, monero
, tel
,
+ xmpp
etc.
+
+
+ Greasemonkey userscripts and userstyles
+
+
+ Blasta offers userscripts to extend its set of features, and
+ userstyles to custom its appearance, layout
+ and its colors.
+ The features offered are: "favicon", "preview", "themes" to
+ name just a few. You are not bound to the userscripts and
+ userstyles that are offered by Blasta, and you are
+ encouraged to install, write and share your own scripts!
+
+
+ References
+
+
+ Herein links to repositories that offer userscripts and
+ userstyles.
+
+
+
+ Greasy Fork
+
+
+
+
+ OpenUserJS
+
+
+
+
+ UserScripts Archive
+
+
+
+
+
+
+
+
diff --git a/xhtml/xmpp.xhtml b/xhtml/xmpp.xhtml
new file mode 100644
index 0000000..b853716
--- /dev/null
+++ b/xhtml/xmpp.xhtml
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+ Blasta
+
+
+
+
+
+
+
+
+
+
PubSub Bookmarks
+
» The universal messaging standard; Tried and tested. Independent. Privacy-focused.
+
An Overview of XMPP
+
+ XMPP is the Extensible Messaging and Presence Protocol, a
+ set of open technologies for instant messaging, presence,
+ multi-party chat, voice and video calls, collaboration,
+ lightweight middleware, content syndication, and generalized
+ routing of XML data.
+
+
+ XMPP was originally developed in the Jabber open-source
+ community to provide an open, decentralized alternative to
+ the closed instant messaging services at that time. XMPP
+ offers several key advantages over such services:
+
+
Open
+
+ The XMPP protocols are free, open, public, and easily
+ understandable; in addition, multiple implementations exist
+ in the form of clients, servers, server components, and code
+ libraries.
+
+
Standard
+
+ The Internet Engineering Task
+ Force (IETF) has formalized the core XML streaming
+ protocols as an approved instant messaging and presence
+ technology. The XMPP specifications were published as
+ RFC 3920 and
+ RFC 3921 in 2004,
+ and the XMPP Standards Foundation continues to publish many
+ XMPP Extension
+ Protocols . In 2011 the core RFCs were revised, resulting
+ in the most up-to-date specifications (
+ RFC 6120 ,
+ RFC 6121 , and
+ RFC 7622 ).
+
+
Proven
+
+ The first Jabber/XMPP technologies were developed by Jeremie
+ Miller in 1998 and are now quite stable; hundreds of
+ developers are working on these technologies, there are tens
+ of thousands of XMPP servers running on the Internet today,
+ and millions of people use XMPP for instant messaging
+ through various public services and XMPP deployments at
+ organizations worldwide.
+
+
Decentralized
+
+ The architecture of the XMPP network is similar to email; as
+ a result, anyone can run their own XMPP server, enabling
+ individuals and organizations to take control of their
+ communications experience.
+
+
Secure
+
+ Any XMPP server may be isolated from the public network
+ (e.g., on a company intranet) and robust security using SASL
+ and TLS has been built into the core
+ XMPP specifications . In
+ addition, the XMPP developer community is actively working
+ on end-to-end encryption to raise the security bar even
+ further.
+
+
Extensible
+
+ Using the power of XML, anyone can build custom
+ functionality on top of the core protocols; to maintain
+ interoperability, common extensions are published in the
+ XEP series , but
+ such publication is not required and organizations can
+ maintain their own private extensions if so desired.
+
+
Flexible
+
+ XMPP applications beyond IM include network management,
+ content syndication, collaboration tools, file sharing,
+ gaming, remote systems monitoring, internet services,
+ lightweight middleware, cloud computing, and much more.
+
+
Diverse
+
+ A wide range of companies and open-source projects use XMPP
+ to build and deploy real-time applications and services; you
+ will never get “locked in” when you use XMPP technologies.
+
+
+ Visit
+ this page which provides an introduction to various XMPP
+ technologies, including links to specifications,
+ implementations, tutorials, and special-purpose discussion
+ venues.
+
+
Conclusion
+
+ XMPP in general is an open and standardized protocol for
+ real time communication.
+
+
+ Anyone can host their own server and communicate freely with
+ each other, just like with email and just like email the
+ used addresses are of the form “name@domain.tld”.
+
+
+ People can use different apps and services, such as Monal,
+ from a single but also multiple accounts. This serves a
+ decentral and sovereign infrastructure and digital
+ communication on the internet but also offers many potential
+ for innovation.
+
+
+ Visit xmpp.org to learn more.
+
+
+
+
+ “The open standard for messaging and presence.”
+
+
+
+
+
+
+