diff --git a/README.md b/README.md
index 2d8e77d..70221df 100644
--- a/README.md
+++ b/README.md
@@ -25,12 +25,12 @@ BukuBot is primarily designed for XMPP (aka Jabber), yet it is built to be exten
BukuBot as appears with Cheogram.
-
-
-
-
-
-
+
+
+
+
+
+
## Getting Started
diff --git a/bukubot/__init__.py b/bukubot/__init__.py
new file mode 100644
index 0000000..4fdd866
--- /dev/null
+++ b/bukubot/__init__.py
@@ -0,0 +1,3 @@
+from bukubot.version import __version__, __version_info__
+
+print('BukuBot', __version__)
diff --git a/bukubot/__main__.py b/bukubot/__main__.py
new file mode 100644
index 0000000..238772b
--- /dev/null
+++ b/bukubot/__main__.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+# BukuBot: Chat Bot Bookmark Manager for XMPP
+# Copyright (C) 2024 Schimon Zackary
+# This file is part of BukuBot.
+# See the file LICENSE for copying permission.
+
+import buku
+from bukubot.about import Documentation
+from bukubot.config import Configuration
+from bukubot.xmpp.chat import Chat
+from bukubot.xmpp.client import Client
+from getpass import getpass
+from argparse import ArgumentParser
+import logging
+import os
+import slixmpp
+import sys
+
+# bookmarks_db = buku.BukuDb(dbfile='temp.db')
+# bookmarks_db.get_tag_all
+# bookmarks_db.search_keywords_and_filter_by_tags
+# bookmarks_db.exclude_results_from_search
+
+
+
+
+def main():
+ # Setup the command line arguments.
+ parser = ArgumentParser(description=Client.__doc__)
+
+ # Output verbosity options.
+ parser.add_argument("-q", "--quiet", help="set logging to ERROR",
+ action="store_const", dest="loglevel",
+ const=logging.ERROR, default=logging.INFO)
+ parser.add_argument("-d", "--debug", help="set logging to DEBUG",
+ action="store_const", dest="loglevel",
+ const=logging.DEBUG, default=logging.INFO)
+
+ # JID and password options.
+ parser.add_argument("-j", "--jid", dest="jid",
+ help="JID to use")
+ parser.add_argument("-p", "--password", dest="password",
+ help="password to use")
+
+ args = parser.parse_args()
+
+ # Setup logging.
+ logging.basicConfig(level=args.loglevel,
+ format='%(levelname)-8s %(message)s')
+
+ if args.jid is None:
+ args.jid = input("Username: ")
+ if args.password is None:
+ args.password = getpass("Password: ")
+
+ # Setup the bot and register plugins. Note that while plugins may
+ # have interdependencies, the order in which you register them does
+ # not matter.
+ xmpp = Client(args.jid, args.password)
+ xmpp.connect()
+ xmpp.process()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/bukubot/about.py b/bukubot/about.py
new file mode 100644
index 0000000..d696806
--- /dev/null
+++ b/bukubot/about.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+class Documentation:
+
+ def about():
+ return ('BukuBot'
+ '\n'
+ 'Jabber/XMPP Bookmark Manager'
+ '\n\n'
+ 'BukuBot is an XMPP bot that facilitates accessing and '
+ 'managing bookmarks remotely.'
+ '\n\n'
+ 'BukuBot is written in Python'
+ '\n\n'
+ 'It is utilizing buku to handle bookmarks, and slixmpp to '
+ 'communicate in the XMPP telecommunication network.'
+ '\n\n'
+ 'https://git.xmpp-it.net/sch/BukuBot'
+ '\n\n'
+ 'Copyright 2024 Schimon Jehudah Zackary'
+ '\n\n'
+ 'Made in Switzerland'
+ '\n\n'
+ 'π¨ποΈ')
+
+ def commands():
+ return ("add URL [tag1,tag2,tag3,...]"
+ "\n"
+ " Bookmark URL along with comma-separated tags."
+ "\n\n"
+ "mod name "
+ "\n"
+ " Modify bookmark title."
+ "\n"
+ "mod note "
+ "\n"
+ " Modify bookmark description."
+ "\n"
+ "tag [+|-] [tag1,tag2,tag3,...]"
+ "\n"
+ " Modify bookmark tags. Appends or deletes tags, if flag tag "
+ "is preceded by \'+\' or \'-\' respectively."
+ "\n"
+ "del or "
+ "\n"
+ " Delete a bookmark by ID or URL."
+ "\n"
+ "\n"
+ "id "
+ "\n"
+ " Print a bookmark by ID."
+ "\n"
+ "last"
+ "\n"
+ " Print most recently bookmarked item."
+ "\n"
+ "tag "
+ "\n"
+ " Search bookmarks of given tag."
+ "\n"
+ "search "
+ "\n"
+ " Search bookmarks by a given search query."
+ "\n"
+ "search any "
+ "\n"
+ " Search bookmarks by a any given keyword."
+ # "\n"
+ # "regex"
+ # "\n"
+ # " Search bookmarks using Regular Expression."
+ "\n")
+
+ def notice():
+ return ('Copyright 2024 Schimon Jehudah Zackary'
+ '\n\n'
+ 'Permission is hereby granted, free of charge, to any person '
+ 'obtaining a copy of this software and associated '
+ 'documentation files (the βSoftwareβ), to deal in the '
+ 'Software without restriction, including without limitation '
+ 'the rights to use, copy, modify, merge, publish, distribute, '
+ 'sublicense, and/or sell copies of the Software, and to '
+ 'permit persons to whom the Software is furnished to do so, '
+ 'subject to the following conditions:'
+ '\n\n'
+ 'The above copyright notice and this permission notice shall '
+ 'be included in all copies or substantial portions of the '
+ 'Software.'
+ '\n\n'
+ 'THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY '
+ 'KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE '
+ 'WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR '
+ 'PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR '
+ 'COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER '
+ 'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR '
+ 'OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE '
+ 'SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.')
+
diff --git a/bukubot/config.py b/bukubot/config.py
new file mode 100644
index 0000000..7366f61
--- /dev/null
+++ b/bukubot/config.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import buku
+import os
+import sys
+
+class Configuration:
+
+ def init_db(jid_bare):
+ filename = jid_bare + '.db'
+ data_dir = Configuration.get_db_directory()
+ pathname = data_dir + '/' + filename
+ bookmarks_db = buku.BukuDb(dbfile=pathname)
+ return bookmarks_db
+
+ def get_db_directory():
+ if os.environ.get('HOME'):
+ data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
+ return os.path.join(data_home, 'bukubot')
+ elif sys.platform == 'win32':
+ data_home = os.environ.get('APPDATA')
+ if data_home is None:
+ return os.path.join(
+ os.path.dirname(__file__) + '/bukubot_data')
+ else:
+ return os.path.join(os.path.dirname(__file__) + '/bukubot_data')
diff --git a/bukubot/documentation/screenshots/adhoc_add.jpg b/bukubot/documentation/screenshots/adhoc_add.jpg
new file mode 100644
index 0000000..08e95fa
Binary files /dev/null and b/bukubot/documentation/screenshots/adhoc_add.jpg differ
diff --git a/bukubot/documentation/screenshots/adhoc_browse.jpg b/bukubot/documentation/screenshots/adhoc_browse.jpg
new file mode 100644
index 0000000..36abaf7
Binary files /dev/null and b/bukubot/documentation/screenshots/adhoc_browse.jpg differ
diff --git a/bukubot/documentation/screenshots/adhoc_commands.jpg b/bukubot/documentation/screenshots/adhoc_commands.jpg
new file mode 100644
index 0000000..2135385
Binary files /dev/null and b/bukubot/documentation/screenshots/adhoc_commands.jpg differ
diff --git a/bukubot/documentation/screenshots/adhoc_edit.jpg b/bukubot/documentation/screenshots/adhoc_edit.jpg
new file mode 100644
index 0000000..0d68830
Binary files /dev/null and b/bukubot/documentation/screenshots/adhoc_edit.jpg differ
diff --git a/bukubot/documentation/screenshots/adhoc_search.jpg b/bukubot/documentation/screenshots/adhoc_search.jpg
new file mode 100644
index 0000000..d4bcf03
Binary files /dev/null and b/bukubot/documentation/screenshots/adhoc_search.jpg differ
diff --git a/bukubot/documentation/screenshots/chat_search.jpg b/bukubot/documentation/screenshots/chat_search.jpg
new file mode 100644
index 0000000..f8300fa
Binary files /dev/null and b/bukubot/documentation/screenshots/chat_search.jpg differ
diff --git a/bukubot/log.py b/bukubot/log.py
new file mode 100644
index 0000000..12d201b
--- /dev/null
+++ b/bukubot/log.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+
+To use this class, first, instantiate Logger with the name of your module
+or class, then call the appropriate logging methods on that instance.
+
+logger = Logger(__name__)
+logger.debug('This is a debug message')
+
+"""
+
+import logging
+
+class Logger:
+
+ def __init__(self, name):
+ self.logger = logging.getLogger(name)
+ self.logger.setLevel(logging.WARNING)
+
+ ch = logging.StreamHandler()
+ ch.setLevel(logging.WARNING)
+
+ formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(name)s: %(message)s')
+ ch.setFormatter(formatter)
+
+ self.logger.addHandler(ch)
+
+ def critical(self, message):
+ self.logger.critical(message)
+
+ def debug(self, message):
+ self.logger.debug(message)
+
+ def error(self, message):
+ self.logger.error(message)
+
+ def info(self, message):
+ self.logger.info(message)
+
+ def warning(self, message):
+ self.logger.warning(message)
+
+ # def check_difference(function_name, difference):
+ # if difference > 1:
+ # Logger.warning(message)
diff --git a/bukubot/version.py b/bukubot/version.py
new file mode 100644
index 0000000..05585a8
--- /dev/null
+++ b/bukubot/version.py
@@ -0,0 +1,2 @@
+__version__ = '0.0.3'
+__version_info__ = (0, 0, 3)
diff --git a/bukubot/xmpp/chat.py b/bukubot/xmpp/chat.py
new file mode 100644
index 0000000..3884be3
--- /dev/null
+++ b/bukubot/xmpp/chat.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from bukubot.about import Documentation
+from bukubot.config import Configuration
+
+try:
+ import tomllib
+except:
+ import tomli as tomllib
+
+
+class Chat:
+
+ def action(self, message):
+ """
+ Process incoming message stanzas. Be aware that this also
+ includes MUC messages and error messages. It is usually
+ a good idea to check the messages's type before processing
+ or sending replies.
+
+ Arguments:
+ message -- The received message stanza. See the documentation
+ for stanza objects and the Message stanza to see
+ how it may be used.
+ """
+ jid_bare = message['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ if message['type'] in ('chat', 'normal'):
+ message_text = " ".join(message['body'].split())
+ message_lowercase = message_text.lower()
+ match message_lowercase:
+ case 'help':
+ message_body = '```\n' + Documentation.commands() + '\n```'
+ case _ if message_lowercase.startswith('add '):
+ message_lowercase_split = message_lowercase[4:].split(' ')
+ link = message_lowercase_split[0]
+ tags = ' '.join(message_lowercase_split[1:])
+ tags = tags.replace(' ,', ',')
+ tags = tags.replace(', ', ',')
+ idx = bookmarks_db.get_rec_id(link)
+ if idx:
+ message_body = '*URL already exists.*'
+ else:
+ idx = bookmarks_db.add_rec(url=link, tags_in=tags)
+ message_body = ('*New bookmark has been added as {}.*'
+ .format(idx))
+ case _ if message_lowercase.startswith('id'):
+ idx = message_lowercase[2:]
+ result = bookmarks_db.get_rec_by_id(idx)
+ message_body = Chat.format_message(result, extended=True)
+ case 'last':
+ idx = bookmarks_db.get_max_id()
+ result = bookmarks_db.get_rec_by_id(idx)
+ message_body = Chat.format_message(result)
+ case _ if message_lowercase.startswith('search any '):
+ query = message_lowercase[11:]
+ query = query.split(' ')
+ results = bookmarks_db.searchdb(query,
+ all_keywords=False,
+ deep=True,
+ regex=False)
+ message_body = '*Results for query: {}*\n\n'.format(query)
+ for result in results:
+ message_body += Chat.format_message(result) + '\n\n'
+ message_body += '*Total of {} results*'.format(len(results))
+ case _ if message_lowercase.startswith('search '):
+ query = message_lowercase[7:]
+ query = query.split(' ')
+ results = bookmarks_db.searchdb(query,
+ all_keywords=True,
+ deep=True,
+ regex=False)
+ message_body = '*Results for query: {}*\n\n'.format(query)
+ for result in results:
+ message_body += Chat.format_message(result) + '\n\n'
+ message_body += '*Total of {} results*\n\n'.format(len(results))
+ # elif message.startswith('regex'):
+ # message_body = bookmark_regexp(message[7:])
+ case _ if message_lowercase.startswith('del '):
+ val = message_lowercase[4:]
+ try:
+ idx = int(val)
+ except:
+ idx = bookmarks_db.get_rec_id(val)
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ message_body = Chat.format_message(bookmark, extended=True) if bookmark else ''
+ result = bookmarks_db.delete_rec(idx)
+ if result:
+ message_body += '\n*Bookmark has been deleted.*'
+ else:
+ message_body += '\n*No action has been taken for index {}*'.format(idx)
+ case _ if message_lowercase.startswith('mod '):
+ message_lowercase_split = message_lowercase[4:].split(' ')
+ if len(message_lowercase_split) > 2:
+ arg = message_lowercase_split[0]
+ val = message_lowercase_split[1]
+ new = ' '.join(message_lowercase_split[2:])
+ message_body = ''
+ try:
+ idx = int(val)
+ except:
+ idx = bookmarks_db.get_rec_id(val)
+ match arg:
+ case 'name':
+ result = bookmarks_db.update_rec(idx, title_in=new)
+ case 'note':
+ result = bookmarks_db.update_rec(idx, desc=new)
+ case _:
+ result = None
+ message_body = ('*Invalid argument. '
+ 'Must be "name" or "note".*\n')
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ message_body += Chat.format_message(bookmark, extended=True) if bookmark else ''
+ if result:
+ message_body += '\n*Bookmark has been deleted.*'
+ else:
+ message_body += '\n*No action has been taken for index {}*'.format(idx)
+ else:
+ message_body = ('Missing argument. '
+ 'Require three arguments: '
+ '(1) "name" or "note"; '
+ '(2) or ; '
+ '(3) .')
+ case _ if (message_lowercase.startswith('tag +') or
+ message_lowercase.startswith('tag -')):
+ message_lowercase_split = message_lowercase[4:].split(' ')
+ if len(message_lowercase_split) > 2:
+ arg = message_lowercase_split[0]
+ val = message_lowercase_split[1]
+ try:
+ idx = int(val)
+ except:
+ idx = bookmarks_db.get_rec_id(val)
+ # tag = ',' + ' '.join(message_lowercase_split[2:]) + ','
+ # tag = ' '.join(message_lowercase_split[2:])
+ tag = arg + ',' + ' '.join(message_lowercase_split[2:])
+ tag = tag.replace(' ,', ',')
+ tag = tag.replace(', ', ',')
+ result = bookmarks_db.update_rec(idx, tags_in=tag)
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ if result:
+ message_body = Chat.format_message(bookmark, extended=True) if bookmark else ''
+ message_body += '\n*Bookmark has been updated.*'
+ else:
+ message_body = '\n*No action has been taken for index {}*'.format(idx)
+ else:
+ message_body = ('Missing argument. '
+ 'Require three arguments: '
+ '(1) + or - sign; '
+ '(2) or ; '
+ '(3) .')
+ case _ if message_lowercase.startswith('tag'):
+ tag = message_lowercase[4:]
+ results = bookmarks_db.search_by_tag(tag)
+ message_body = '*Results for tag: {}*\n\n'.format(tag)
+ for result in results:
+ message_body += Chat.format_message(result) + '\n\n'
+ message_body += '*Total of {} results*'.format(len(results))
+ case _:
+ message_body = ('Unknown command. Send "help" for list '
+ 'of commands.')
+ message.reply(message_body).send()
+ #message.reply("Thanks for sending\n%(body)s" % message).send()
+
+ def format_message(bookmark, extended=False):
+ # idx = bookmark.id
+ # url = bookmark.url
+ # name = bookmark.title if bookmark.title else 'Untitled'
+ # desc = bookmark.desc if bookmark.desc else 'No comment'
+ idx = bookmark[0]
+ url = bookmark[1]
+ name = bookmark[2] if bookmark[2] else 'Untitled'
+ desc = bookmark[4] if bookmark[4] else None
+ # rec = '\n ποΈ {} [{}]\n ποΈ {}\n π·οΈ {}'.format(title, index, link, tags)
+ if extended:
+ tags = '' if bookmark.tags_raw == ',' else ", ".join(bookmark.tags_raw.split(","))[2:-2]
+ tags = tags if tags else 'No tags'
+ message_body = ('{}. {}\n{}\n{}\n{}'.format(idx, name, url, desc, tags))
+ else:
+ message_body = ('{}. {}\n{}'.format(idx, name, url))
+ return message_body
diff --git a/bukubot/xmpp/client.py b/bukubot/xmpp/client.py
new file mode 100644
index 0000000..f704dbb
--- /dev/null
+++ b/bukubot/xmpp/client.py
@@ -0,0 +1,698 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import slixmpp
+from bukubot.xmpp.chat import Chat
+from bukubot.config import Configuration
+from bukubot.about import Documentation
+
+from bukubot.log import Logger
+from bukubot.version import __version__
+
+try:
+ import tomllib
+except:
+ import tomli as tomllib
+
+
+# time_now = datetime.now()
+# time_now = time_now.strftime("%H:%M:%S")
+
+# def print_time():
+# # return datetime.now().strftime("%H:%M:%S")
+# now = datetime.now()
+# current_time = now.strftime("%H:%M:%S")
+# return current_time
+
+logger = Logger(__name__)
+
+class Client(slixmpp.ClientXMPP):
+
+ """
+ bukubot - Bookmark manager chat bot for Jabber/XMPP.
+ bukubot is a bookmark manager bot based on buku and slixmpp.
+ """
+
+ def __init__(self, jid, password):
+ slixmpp.ClientXMPP.__init__(self, jid, password)
+
+ print('client')
+
+ self.register_plugin('xep_0030') # Service Discovery
+ self.register_plugin('xep_0004') # Data Forms
+ self.register_plugin('xep_0060') # Publish-Subscribe
+ self.register_plugin('xep_0050') # Ad-Hoc Commands
+ self.register_plugin('xep_0115') # Entity Capabilities
+ self.register_plugin('xep_0122') # Data Forms Validation
+ self.register_plugin('xep_0199') # XMPP Ping
+
+ # Connect to the XMPP server and start processing XMPP stanzas.
+ # self.connect()
+ # self.process()
+
+ # The session_start event will be triggered when
+ # the bot establishes its connection with the server
+ # and the XML streams are ready for use. We want to
+ # listen for this event so that we we can initialize
+ # our roster.
+ self.add_event_handler("session_start", self.process_session_start)
+
+ # The message event is triggered whenever a message
+ # stanza is received. Be aware that that includes
+ # MUC messages and error messages.
+ self.add_event_handler("message", self.process_message)
+ self.add_event_handler("disco_info", self.process_disco_info)
+
+ async def process_session_start(self, event):
+ """
+ Process the session_start event.
+
+ Typical actions for the session_start event are
+ requesting the roster and broadcasting an initial
+ presence stanza.
+
+ Arguments:
+ event -- An empty dictionary. The session_start
+ event does not provide any additional
+ data.
+ """
+ self.command_list()
+ self.send_presence()
+ # await self.get_roster()
+ await self['xep_0115'].update_caps()
+
+ async def process_disco_info(self, DiscoInfo):
+ jid = DiscoInfo['from']
+ await self['xep_0115'].update_caps(jid=jid)
+ # jid_bare = DiscoInfo['from'].bare
+ # self.bookmarks_db = buku.BukuDb(dbfile=jid_bare + '.db')
+
+ def process_message(self, message):
+ Chat.action(self, message)
+
+ def command_list(self):
+ self['xep_0050'].add_command(node='add',
+ name='ποΈ Add',
+ handler=self._handle_add)
+ self['xep_0050'].add_command(node='random',
+ name='π²οΈ Random',
+ handler=self._handle_random)
+ self['xep_0050'].add_command(node='modify',
+ name='ποΈ Browse',
+ handler=self._handle_browse)
+ self['xep_0050'].add_command(node='search',
+ name='ποΈ Search',
+ handler=self._handle_search)
+ # self['xep_0050'].add_command(node='tag',
+ # name='π·οΈ Tags',
+ # handler=self._handle_tag)
+ # self['xep_0050'].add_command(node='statistics',
+ # name='ποΈ Statistics',
+ # handler=self._handle_stats)
+ self['xep_0050'].add_command(node='help',
+ name='ποΈ Help',
+ handler=self._handle_help)
+ self['xep_0050'].add_command(node='license',
+ name='βοΈ License',
+ handler=self._handle_license)
+ self['xep_0050'].add_command(node='about',
+ name='ποΈ About',
+ handler=self._handle_about)
+
+ def _handle_add(self, iq, session):
+ # jid = session['from'].bare
+ # jid_file = jid
+ # db_file = config.get_pathname_to_database(jid_file)
+ form = self['xep_0004'].make_form('form', 'Add')
+ form['instructions'] = 'Add a new bookmark'
+ form.add_field(desc='URL to bookmark.',
+ ftype='text-single',
+ label='URL',
+ required=True,
+ var='url')
+ form.add_field(desc='Title to add manually.',
+ ftype='text-single',
+ label='Title',
+ var='title')
+ form.add_field(desc='Description of the bookmark.',
+ ftype='text-multi',
+ label='Note',
+ var='note')
+ form.add_field(desc='Comma-separated tags.',
+ ftype='text-single',
+ label='Tags',
+ var='tag')
+ form.add_field(desc='Check to disable automatic title fetch.',
+ ftype='boolean',
+ label='Immutable',
+ var='immutable')
+ session['has_next'] = True
+ session['next'] = self._handle_edit_single
+ session['payload'] = form
+ return session
+
+ def _handle_edit_single(self, payload, session):
+ jid_bare = session['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ form = self['xep_0004'].make_form('form', 'Edit')
+ form['instructions'] = 'Edit bookmark'
+ url = payload['values']['url']
+ idx = bookmarks_db.get_rec_id(url)
+ if not idx:
+ immu = payload['values']['immutable']
+ desc = payload['values']['note']
+ tags = payload['values']['tag']
+ name = payload['values']['title']
+ fetch = True if not name else False
+ idx = bookmarks_db.add_rec(desc=desc,
+ fetch=fetch,
+ immutable=immu,
+ tags_in=tags,
+ url=url)
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ form.add_field(ftype='text-single',
+ label='ID #',
+ value=str(idx))
+ form.add_field(ftype='text-single',
+ label='Title',
+ value=bookmark[2])
+ form.add_field(ftype='text-single',
+ label='URL',
+ value=bookmark[1])
+ form.add_field(ftype='text-multi',
+ label='Note',
+ value=bookmark[4])
+ options = form.add_field(ftype='list-multi',
+ label='Tags')
+ for tag in bookmark[3].split(','):
+ options.addOption(tag, tag)
+ form.add_field(ftype='boolean',
+ label='Immutable',
+ value=str(bookmark[5]))
+ session['allow_complete'] = True
+ session['has_next'] = False
+ session['next'] = None
+ session['payload'] = form
+ return session
+
+ def _handle_browse(self, iq, session):
+ form = self['xep_0004'].make_form('form', 'Browse')
+ form['instructions'] = 'Browse bookmark'
+ options = form.add_field(desc='Items per page.',
+ label='Items',
+ ftype='list-single',
+ value='20',
+ var='limit')
+ i = 10
+ while i <= 50:
+ x = str(i)
+ options.addOption(x, x)
+ i += 10
+ # options['validate']['datatype'] = 'xs:integer'
+ # options['validate']['range'] = { 'minimum': 10, 'maximum': 50 }
+ form.add_field(ftype='hidden',
+ value='0',
+ var='count')
+ session['has_next'] = True
+ session['next'] = self._handle_browse_all
+ session['payload'] = form
+ return session
+
+ def _handle_tag(self, iq, session):
+ jid_bare = session['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ form = self['xep_0004'].make_form('form', 'Tags')
+ form['instructions'] = ('Select tags to browse')
+ options = form.add_field(desc='Select tag to list its items.',
+ ftype='list-single',
+ label='Tags',
+ var='tag')
+ tags = bookmarks_db.get_tag_all()
+ counter = 0
+ for tag in tags[0]:
+ if counter == 100: break
+ options.addOption(tag, tag)
+ counter += 1
+ session['has_next'] = True
+ session['next'] = self._handle_browse_all
+ session['payload'] = form
+ return session
+
+ def _handle_browse_all(self, payload, session):
+ jid_bare = session['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ vals = payload['values']
+ if 'url' in vals and vals['url']:
+ act = vals['action']
+ url = vals['url']
+ form = self['xep_0004'].make_form('form', 'Edit')
+ match act:
+ case 'edit':
+ if len(url) > 1:
+ form['instructions'] = 'Modify bookmarks'
+ idxs = ''
+ tags = ''
+ for i in url:
+ idx = bookmarks_db.get_rec_id(i)
+ idxs += ',' + str(idx)
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ tags += bookmark[3]
+ form.add_field(desc='Comma-separated tags.',
+ ftype='text-single',
+ label='Tags',
+ value=bookmark[3],
+ var='tags_new')
+ form.add_field(desc='Check to disable automatic title fetch.',
+ ftype='boolean',
+ label='Immutable',
+ value=True,
+ var='immutable')
+ form.add_field(ftype='hidden',
+ value=idxs,
+ var='ids')
+ # session['next'] = self._handle_edit_single(payload, session)
+ else:
+ form['instructions'] = 'Modify bookmark'
+ idx = bookmarks_db.get_rec_id(url[0])
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ form.add_field(ftype='fixed',
+ label='ID #',
+ value=str(idx),
+ var='id')
+ form.add_field(ftype='text-single',
+ label='Title',
+ value=bookmark[2],
+ var='title')
+ form.add_field(ftype='text-single',
+ label='URL',
+ value=bookmark[1],
+ var='url')
+ form.add_field(ftype='text-multi',
+ label='Note',
+ value=bookmark[4],
+ var='description')
+ form.add_field(ftype='hidden',
+ value=bookmark[3],
+ var='tags_old')
+ form.add_field(desc='Comma-separated tags.',
+ ftype='text-single',
+ label='Tags',
+ value=bookmark[3],
+ var='tags_new')
+ form.add_field(desc='Check to disable automatic title fetch.',
+ ftype='boolean',
+ label='Immutable',
+ value=str(bookmark[5]),
+ var='immutable')
+ case 'remove':
+ form['instructions'] = ('The following items were deleted from '
+ 'bookmarks.\nProceed to finish or '
+ 'select items to restore.')
+ options = form.add_field(desc='Select items to restore',
+ ftype='list-multi',
+ label='Deleted items',
+ var='url')
+ for i in url:
+ idx = bookmarks_db.get_rec_id(i)
+ rec = bookmarks_db.get_rec_by_id(idx)
+ bookmarks_db.delete_rec(idx)
+ options.addOption(rec.title, i)
+ session['cancel'] = self._handle_cancel
+ # session['allow_complete'] = True
+ session['has_next'] = True
+ session['next'] = self._handle_action_result
+ session['payload'] = form
+ else:
+ limit = vals['limit'] if 'limit' in vals else 0
+ if isinstance(limit, list): limit = limit[0]
+ count = vals['count'] if 'count' in vals else 0
+ if isinstance(count, list): count = count[0]
+ form = self['xep_0004'].make_form('form', 'Browse')
+ form['instructions'] = ('Select bookmarks to modify')
+ options = form.add_field(desc='Select an action',
+ ftype='list-single',
+ label='Action',
+ required=True,
+ value='edit',
+ var='action')
+ options.addOption('Edit', 'edit')
+ options.addOption('Remove', 'remove')
+ options = form.add_field(desc='Selection of several bookmarks will '
+ 'only allow to modify tags.',
+ ftype='list-multi',
+ label='Bookmark',
+ var='url')
+ bookmarks = bookmarks_db.get_rec_all()
+ # bookmarks = sorted(bookmarks, key=lambda x: x.title)
+ counter = int(count)
+ limiter = counter + int(limit)
+ for bookmark in bookmarks[counter:limiter]:
+ if counter == limiter: break
+ options.addOption(bookmark[2], bookmark[1])
+ counter += 1
+ form.add_field(ftype='hidden',
+ value=str(counter),
+ var='count')
+ form.add_field(ftype='hidden',
+ value=str(limit),
+ var='limit')
+ session['has_next'] = True
+ session['next'] = self._handle_browse_all
+ session['payload'] = form
+ return session
+
+ def _handle_action_result(self, payload, session):
+ jid_bare = session['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ vals = payload['values']
+ if 'id' in vals:
+ idx = vals['id']
+ idx = int(idx)
+ tags_new = vals['tags_new']
+ tags_new = tags_new.replace(' ,', ',')
+ tags_new = tags_new.replace(', ', ',')
+ tags_old = vals['tags_old'][0]
+ tags = tags_old.split(',')
+ tags_to_remove = '-,'
+ for tag in tags:
+ if tag not in tags_new:
+ tags_to_remove += ',' + tag
+ bookmarks_db.update_rec(idx,
+ url=vals['url'],
+ title_in=vals['title'],
+ tags_in='+,' + tags_new,
+ desc=vals['description'],
+ immutable=vals['immutable'])
+ bookmarks_db.update_rec(idx,
+ tags_in=tags_to_remove)
+ form = self['xep_0004'].make_form('result', 'Done')
+ rec = bookmarks_db.get_rec_by_id(idx)
+ form.add_field(ftype='text-single',
+ label='Title',
+ value=rec.title)
+ form.add_field(ftype='text-single',
+ label='URL',
+ value=rec.url)
+ form.add_field(ftype='text-single',
+ label='Note',
+ value=rec.desc)
+ form.add_field(ftype='text-single',
+ label='Tags',
+ value=rec.tags_raw)
+ form.add_field(ftype='text-single',
+ label='Immutable',
+ value=str(rec.flags))
+ elif 'ids' in vals:
+ immutable = vals['immutable']
+ tags_new = vals['tags_new']
+ tags_new = tags_new.replace(' ,', ',')
+ tags_new = tags_new.replace(', ', ',')
+ idxs = vals['ids'].split(',')
+ for idx in idxs:
+ bookmarks_db.update_rec(idx,
+ tags_in='+,' + tags_new,
+ immutable=immutable)
+ form = self['xep_0004'].make_form('result', 'Done')
+ form.add_field(ftype='text-single',
+ label='Tags',
+ value=tags_new)
+ form.add_field(ftype='text-single',
+ label='Immutable',
+ value=immutable)
+ elif 'url' in vals:
+ url = vals['url']
+ form = self['xep_0004'].make_form('result', 'Add')
+ form['instructions'] = ('The following items have been added to '
+ 'the bookmarks\nNote: You will have to '
+ 'manually tag these items, if you would.')
+ for i in url:
+ idx = bookmarks_db.add_rec(url=i)
+ rec = bookmarks_db.get_rec_by_id(idx)
+ form.add_field(ftype='fixed',
+ value=str(idx))
+ form.add_field(ftype='text-single',
+ value=rec.title)
+ form.add_field(ftype='text-single',
+ value=rec.url)
+ session['allow_complete'] = True
+ session['has_next'] = False
+ session['next'] = None
+ session['payload'] = form
+ return session
+
+ def _handle_random(self, iq, session):
+ jid_bare = session['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ bookmarks = bookmarks_db.get_rec_all()
+ if bookmarks:
+ import random
+ bookmark = random.choice(bookmarks)
+ form = self['xep_0004'].make_form('form', 'Random')
+ form['instructions'] = 'Bookmark #{}'.format(bookmark[0])
+ form.add_field(ftype='fixed',
+ # ftype='text-single',
+ label='URL',
+ # required=True,
+ value=bookmark[1],
+ var='url')
+ form.add_field(ftype='text-single',
+ label='Title',
+ value=bookmark[2],
+ var='title')
+ form.add_field(ftype='text-multi',
+ label='Note',
+ value=bookmark[4],
+ var='note')
+ form.add_field(desc='Comma-separated tags.',
+ ftype='text-single',
+ label='Tags',
+ value=bookmark[3],
+ var='tag')
+ form.add_field(desc='Check to disable automatic title fetch.',
+ ftype='boolean',
+ label='Immutable',
+ value=str(bookmark[5]),
+ var='immutable')
+ session['has_next'] = True
+ session['next'] = self._handle_edit_single
+ session['payload'] = form
+ else:
+ text_note = ('There are no bookmarks, yet.')
+ session['notes'] = [['info', text_note]]
+ return session
+
+ def _handle_search(self, iq, session):
+ form = self['xep_0004'].make_form('form', 'Search')
+ form['instructions'] = 'Search for bookmarks'
+ form.add_field(desc='Enter a search term to query.',
+ ftype='text-single',
+ label='Search',
+ var='query')
+ options = form.add_field(desc='Select type of search.',
+ ftype='list-single',
+ label='Search by',
+ value='any',
+ var='type')
+ options.addOption('All keywords', 'all')
+ options.addOption('Any keyword', 'any')
+ options.addOption('Tag', 'tag')
+ form.add_field(desc='Search for matching substrings.',
+ ftype='boolean',
+ label='Deep',
+ value=True,
+ var='deep')
+ form.add_field(desc='Match a regular expression.',
+ ftype='boolean',
+ label='Regular Expression',
+ var='regex')
+ session['allow_prev'] = False
+ session['has_next'] = True
+ session['next'] = self._handle_search_result
+ session['payload'] = form
+ session['prev'] = None
+ return session
+
+ def _handle_search_result(self, payload, session):
+ jid_bare = session['from'].bare
+ bookmarks_db = Configuration.init_db(jid_bare)
+ vals = payload['values']
+ if 'url' in vals and vals['url']:
+ act = vals['action']
+ url = vals['url']
+ form = self['xep_0004'].make_form('form', 'Edit')
+ match act:
+ case 'edit':
+ if len(url) > 1:
+ form['instructions'] = 'Modify bookmarks'
+ idxs = ''
+ tags = ''
+ for i in url:
+ idx = bookmarks_db.get_rec_id(i)
+ idxs += ',' + str(idx)
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ tags += bookmark[3]
+ form.add_field(desc='Comma-separated tags.',
+ ftype='text-single',
+ label='Tags',
+ value=bookmark[3],
+ var='tags_new')
+ form.add_field(desc='Check to disable automatic title fetch.',
+ ftype='boolean',
+ label='Immutable',
+ value=True,
+ var='immutable')
+ form.add_field(ftype='hidden',
+ value=idxs,
+ var='ids')
+ # session['next'] = self._handle_edit_single(payload, session)
+ else:
+ form['instructions'] = 'Modify bookmark'
+ idx = bookmarks_db.get_rec_id(url[0])
+ bookmark = bookmarks_db.get_rec_by_id(idx)
+ form.add_field(ftype='fixed',
+ label='ID #',
+ value=str(idx),
+ var='id')
+ form.add_field(ftype='text-single',
+ label='Title',
+ value=bookmark[2],
+ var='title')
+ form.add_field(ftype='text-single',
+ label='URL',
+ value=bookmark[1],
+ var='url')
+ form.add_field(ftype='text-multi',
+ label='Note',
+ value=bookmark[4],
+ var='description')
+ form.add_field(ftype='hidden',
+ value=bookmark[3],
+ var='tags_old')
+ form.add_field(desc='Comma-separated tags.',
+ ftype='text-single',
+ label='Tags',
+ value=bookmark[3],
+ var='tags_new')
+ form.add_field(desc='Check to disable automatic title fetch.',
+ ftype='boolean',
+ label='Immutable',
+ value=str(bookmark[5]),
+ var='immutable')
+ case 'remove':
+ form['instructions'] = ('The following items were deleted from '
+ 'bookmarks.\nProceed to finish or '
+ 'select items to restore.')
+ options = form.add_field(desc='Select items to restore',
+ ftype='list-multi',
+ label='Deleted items',
+ var='url')
+ for i in url:
+ idx = bookmarks_db.get_rec_id(i)
+ rec = bookmarks_db.get_rec_by_id(idx)
+ bookmarks_db.delete_rec(idx)
+ options.addOption(rec.title, i)
+ session['cancel'] = self._handle_cancel
+ # session['allow_complete'] = True
+ session['has_next'] = True
+ session['next'] = self._handle_action_result
+ session['payload'] = form
+ else:
+ count = vals['count'] if 'count' in vals else 0
+ if isinstance(count, list): count = count[0]
+ # count = count if count else 0
+ query = vals['query']
+ if isinstance(query, list): query = query[0]
+ stype = vals['type']
+ if isinstance(stype, list): stype = stype[0]
+ deep = vals['deep']
+ if isinstance(deep, list): deep = deep[0]
+ deep = True if '1' else False
+ regex = vals['regex']
+ if isinstance(regex, list): regex = regex[0]
+ regex = True if '1' else False
+ match stype:
+ case 'all':
+ bookmarks = bookmarks_db.searchdb(query,
+ all_keywords=True,
+ deep=deep,
+ regex=regex)
+ case 'any':
+ bookmarks = bookmarks_db.searchdb(query,
+ all_keywords=False,
+ deep=deep,
+ regex=regex)
+ case 'tag':
+ bookmarks = bookmarks_db.search_by_tag(query)
+ # bookmarks = sorted(bookmarks, key=lambda x: x.title)
+ if bookmarks:
+ form = self['xep_0004'].make_form('form', 'Browse')
+ form['instructions'] = ('Select bookmarks to modify')
+ options = form.add_field(desc='Select an action',
+ ftype='list-single',
+ label='Action',
+ required=True,
+ value='edit',
+ var='action')
+ options.addOption('Edit', 'edit')
+ options.addOption('Remove', 'remove')
+ options = form.add_field(desc='Selection of several bookmarks '
+ 'will only allow to modify tags.',
+ ftype='list-multi',
+ label='Bookmark',
+ var='url')
+ counter = int(count)
+ limiter = counter + 10
+ for bookmark in bookmarks[counter:limiter]:
+ if counter == limiter: break
+ options.addOption(bookmark[2], bookmark[1])
+ counter += 1
+ form.add_field(ftype='hidden',
+ value=str(counter),
+ var='count')
+ form.add_field(ftype='hidden',
+ value=query,
+ var='query')
+ form.add_field(ftype='hidden',
+ value=stype,
+ var='type')
+ deep = '1'if deep else ''
+ form.add_field(ftype='hidden',
+ value=deep,
+ var='deep')
+ regex = '1'if regex else ''
+ form.add_field(ftype='hidden',
+ value=regex,
+ var='regex')
+ session['has_next'] = True
+ session['next'] = self._handle_search_result
+ session['payload'] = form
+ else:
+ text_note = 'No results were yielded for: {}'.format(query)
+ session['notes'] = [['info', text_note]]
+ session['allow_prev'] = True
+ session['prev'] = self._handle_search
+ return session
+
+ def _handle_cancel(self, payload, session):
+ text_note = ('Operation has been cancelled.'
+ '\n\n'
+ 'No action was taken.')
+ session['notes'] = [['info', text_note]]
+ return session
+
+ def _handle_help(self, iq, session):
+ text_note = Documentation.commands()
+ session['notes'] = [['info', text_note]]
+ return session
+
+ def _handle_license(self, iq, session):
+ text_note = Documentation.notice()
+ session['notes'] = [['info', text_note]]
+ return session
+
+ def _handle_about(self, iq, session):
+ text_note = Documentation.about()
+ session['notes'] = [['info', text_note]]
+ return session
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 8522b7c..a2e5010 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,18 +46,6 @@ Homepage = "http://bukubot.i2p/"
Repository = "https://git.xmpp-it.net/sch/BukuBot"
Issues = "https://codeberg.org/sch/BukuBot/issues"
-
-[project.optional-dependencies]
-file-export = ["html2text", "pdfkit", "xml2epub"]
-proxy = ["pysocks"]
-readability = ["readability-lxml"]
-
-# This section returns pep508-identifier error
-# [project.optional-dependencies]
-# "export as markdown" = ["html2text"]
-# "export as pdf" = ["pdfkit"]
-# "readable html" = ["readability-lxml"]
-
# [project.readme]
# text = "BukuBot is a bookmark manager bot using buku. This program is primarily designed for XMPP"
@@ -66,5 +54,6 @@ bukubot = "bukubot.__main__:main"
[tool.setuptools]
platforms = ["any"]
+
[tool.setuptools.package-data]
"*" = ["*.ini", "*.csv", "*.toml", "*.svg"]