Refactor file structure.

This commit is contained in:
Schimon Jehudah, Adv. 2024-05-31 12:18:59 +03:00
parent 163975f837
commit 6aa270c274
16 changed files with 1131 additions and 18 deletions

View file

@ -25,12 +25,12 @@ BukuBot is primarily designed for XMPP (aka Jabber), yet it is built to be exten
BukuBot as appears with Cheogram. BukuBot as appears with Cheogram.
<img alt="Chat: Search" src="documentation/screenshots/chat_search.jpg" width="200px"/> <img alt="Chat: Search" src="bukubot/documentation/screenshots/chat_search.jpg" width="200px"/>
<img alt="Ad-Hoc: Commands" src="documentation/screenshots/adhoc_commands.jpg" width="200px"/> <img alt="Ad-Hoc: Commands" src="bukubot/documentation/screenshots/adhoc_commands.jpg" width="200px"/>
<img alt="Ad-Hoc: Browse" src="documentation/screenshots/adhoc_browse.jpg" width="200px"/> <img alt="Ad-Hoc: Browse" src="bukubot/documentation/screenshots/adhoc_browse.jpg" width="200px"/>
<img alt="Ad-Hoc: Add" src="documentation/screenshots/adhoc_add.jpg" width="200px"/> <img alt="Ad-Hoc: Add" src="bukubot/documentation/screenshots/adhoc_add.jpg" width="200px"/>
<img alt="Ad-Hoc: Edit bookmark" src="documentation/screenshots/adhoc_edit.jpg" width="200px"/> <img alt="Ad-Hoc: Edit bookmark" src="bukubot/documentation/screenshots/adhoc_edit.jpg" width="200px"/>
<img alt="Ad-Hoc: Seach" width="200px" src="documentation/screenshots/adhoc_search.jpg"/> <img alt="Ad-Hoc: Seach" width="200px" src="bukubot/documentation/screenshots/adhoc_search.jpg"/>
## Getting Started ## Getting Started

3
bukubot/__init__.py Normal file
View file

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

66
bukubot/__main__.py Normal file
View file

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

99
bukubot/about.py Normal file
View file

@ -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 <ID> <TEXT>"
"\n"
" Modify bookmark title."
"\n"
"mod note <ID> <TEXT>"
"\n"
" Modify bookmark description."
"\n"
"tag [+|-] <ID> [tag1,tag2,tag3,...]"
"\n"
" Modify bookmark tags. Appends or deletes tags, if flag tag "
"is preceded by \'+\' or \'-\' respectively."
"\n"
"del <ID> or <URL>"
"\n"
" Delete a bookmark by ID or URL."
"\n"
"\n"
"id <ID>"
"\n"
" Print a bookmark by ID."
"\n"
"last"
"\n"
" Print most recently bookmarked item."
"\n"
"tag <TEXT>"
"\n"
" Search bookmarks of given tag."
"\n"
"search <TEXT>"
"\n"
" Search bookmarks by a given search query."
"\n"
"search any <TEXT>"
"\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.')

27
bukubot/config.py Normal file
View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

47
bukubot/log.py Normal file
View file

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

2
bukubot/version.py Normal file
View file

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

182
bukubot/xmpp/chat.py Normal file
View file

@ -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) <ID> or <URL>; '
'(3) <TEXT>.')
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) <ID> or <URL>; '
'(3) <TAGS>.')
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

698
bukubot/xmpp/client.py Normal file
View file

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

View file

@ -46,18 +46,6 @@ Homepage = "http://bukubot.i2p/"
Repository = "https://git.xmpp-it.net/sch/BukuBot" Repository = "https://git.xmpp-it.net/sch/BukuBot"
Issues = "https://codeberg.org/sch/BukuBot/issues" 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] # [project.readme]
# text = "BukuBot is a bookmark manager bot using buku. This program is primarily designed for XMPP" # 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] [tool.setuptools]
platforms = ["any"] platforms = ["any"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"*" = ["*.ini", "*.csv", "*.toml", "*.svg"] "*" = ["*.ini", "*.csv", "*.toml", "*.svg"]