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 %} + + {% 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 %} +
+
+

+ + {{entry['title']}} +

+ + + {% 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 %} + + {% endif %} + {% endif %} +
+

+ + + PubSub + + and + + + RSS + + + feeds for this page are available. +

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

  PubSub Bookmarks

+

» Connecting with XMPP.

+

Connect to XMPP

+
+ + + + + + +
+

+ Log in to Blasta with your XMPP account or + register for an account. +

+
+
+ +
+ + 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. +

+

+ Contact +

+

+ 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 confirm or edit %} + +

{{url}}

+ {% else %} + + {% endif %} +
+ + + {% if confirm %} + +

{{title}}

+ {% else %} + + {% endif %} +
+ + + {% if confirm %} + +

{{summary}}

+ {% else %} + + {% endif %} +
+ + + {% if confirm %} + +

{{tags}}

+ {% else %} + + {% endif %} +
  + {% if confirm %} + + {% else %} + + {% endif %} + {% if confirm %} + + [ {{node}} ] + {% else %} + + + + + + + ( + ? + ) + {% endif %} +
  +

+ + Fields that are marked with an asterisk + (*) are required. + +

+

+ + Press key F5 or reload page to reset + form values. + + +

+
+ {% 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: +

    +
  1. + Peer to peer subscription which allows you to + subscribe directly to a Blasta node of a contact. +
  2. +
  3. + Peer to server subscription that is exclusive to + Blasta which offers its special nodes, such as + "popular", "recent", "tag" and "url" variants. +
  4. +
+

+

+ 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. +

+
+
+

About

+

Blasta

+

Ideas

+

Philosophy

+

Projects

+

Software

+

Thanks

+
+
+

Help

+

Feeds

+

Questions

+

Syndication

+

Utilities

+
+
+

Personal

+

Contact

+

Now

+
+
+

Policy

+

Copy

+

Privacy

+

Terms

+
+ {% if jabber_id %} +
+

Profile

+

About

+

Enrollment

+

Export

+

Import

+

Permissions

+

Routine

+

Termination

+
+ {% endif %} +
+

XMPP

+

Atomsub

+

Libervia

+

Movim

+

PubSub

+

XMPP

+
+
+
+
+ +
+ + 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 : +
      +
    1. + The freedom to run the program for any + purpose. +
    2. +
    3. + The freedom to study how the program works, + and change it to make it do what you wish. +
    4. +
    5. + The freedom to redistribute copies so you + can help your neighbor. +
    6. +
    7. + 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. +
    8. +
    +
  • +
  • + 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: +
      +
    1. + we will help the people, whatever their + computer literacy is, to the extent of what we can +
    2. +
    3. + we will as well commit ourselves to help the + accessibility to "Libervia" for all +
    4. +
    5. + "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. +
    6. +
    +
  • +
+

+

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. +

+
+ + + + + + + + + + + + + + + + + +
+ + Private + + + + + + + +
+ + Public + + + + + + + + + + +
+ + Read + + + + + + + +
+ + +
+

Routine

+

+ Choose a routine (i.e. default) directory (i.e. node). +

+
+ + + + + + +
+ +
+

+ 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 - Personal mini-portal in text. + +

+

+ 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 - A webpage bookmarking and snapshotting service. + +

+

+ 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. +

+

+ + Shaarli - The personal, minimalist, super fast, + database-free, bookmarking service. + +

+

+ 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. +

+

+ + SearxNG - Search without being tracked. + +

+

+ 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 - A collaborative repository of torrents. + +

+

+ 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 - A free software for your own search engine. + +

+

+ 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. +

+

+ + The Pirate Bay - The most resilient BitTorrent site + +

+

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.

+

What platform would you choose if not 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 + + + + + +
+ +
+
+

  PubSub Bookmarks

+

+ » Register an XMPP account. It is free of charge. +

+

+ Registering with XMPP +

+

+ As with email, you need an account with a service provider + to operate Blasta, so if you already have an XMPP account, + you can connect and start to Blasta. +

+

+ If you do not have an XMPP account, yet, you can use a + public provider from + the curated list of XMPP Providers or + be your own provider by + hosting a server yourself. +

+
+
+ +
+ + 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}}

+
+ + + +
+ {% 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 - An HTTP and XHTML framework. + +

+

+ FastAPI is a fast (high-performance), framework for building + APIs with Python based on standard Python type hints. +

+

+ + lxml - XML and HTML with Python. + +

+

+ 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 - A computer language. + +

+

+ Python is a computer language that lets you work quickly and + integrate systems more effectively. +

+

+ + Slixmpp - A modern python XMPP library using asyncio. + +

+

+ 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 - Small. Fast. Reliable. + +

+

+ 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. +

+

+ + Uvicorn - An ASGI server, for Python. + +

+

+ 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. +

+

+ + XML | Extensible Markup Language. + +

+

+ 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 | The universal messaging standard. + +

+

+ 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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeFunctionURL
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
PopularA list of popular bookmarks by popularity.{{origin}}/popular?mode=feed
RecentA 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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeFunctionURI
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 +
PopularA list of popular bookmarks by popularity. + xmpp:{{pubsub_jid}}?pubsub;action=subscribe;node=popular +
RecentA 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"). +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DesktopAboutPlatforms
+ + 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
MobileAboutPlatforms
+ + 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
OnlineAboutPlatforms
+ 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
BrowserAboutPlatforms
+ + 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. +

+

+ Bookmarklet and home buttons +

+

+ Blasta offers a bookmarklet to facilitate the task of saving + HTTP links to your collection. Pin this + + Blasta + + 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 + + Blasta + and + + Blasta (Private) + + Private + and + + Blasta (Read) + + 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.” +

+
+
+ +
+ +