#!/usr/bin/env python3 # -*- coding: utf-8 -*- import slixmpp from bukuxmpp.xmpp.chat import Chat from bukuxmpp.config import Configuration from bukuxmpp.about import Documentation from slixfeed.log import Logger from slixfeed.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): """ bukuxmpp - Bookmark manager bot for Jabber/XMPP. bukuxmpp 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