#!/usr/bin/env python # -*- coding: utf-8 -*- """ IRC Bot Plugin for Note Taking using TinyDB. This plugin provides a single !note command with subcommands to manage both daily and general notes. It supports the following subcommands: today [...] View or update today's daily note. daily [--date ] [...] View or update a daily note for the specified date (default is today). general [--tags <tags>] [<content>...] View or update a general note with the given title. Optionally specify tags. list <type> List all notes of the specified type (either 'daily' or 'general'). search <type> <keyword> Search for a keyword in notes of the specified type. summary Display a summary of note counts. Notes are stored persistently in the TinyDB database file "notes.json". Daily notes are keyed by date (YYYY-MM-DD) and general notes by a slugified title. Dependencies: - irc3 - ircstyle - tinydb Author: Your Name Version: 1.1 Date: 2025-02-17 """ import re from datetime import datetime, date from tinydb import TinyDB, Query import irc3 from irc3.plugins.command import command import ircstyle import logging # ----------------------------------------------------------------------------- # Helper Functions # ----------------------------------------------------------------------------- def slugify(text): """ Convert text to a slug (lowercase, underscores, alphanumerics only). Args: text (str): The text to slugify. Returns: str: A slugified version of the text. """ text = text.lower() text = re.sub(r'\s+', '_', text) text = re.sub(r'[^a-z0-9_-]', '', text) return text def now_str(): """ Return the current datetime as an ISO formatted string. Returns: str: The current datetime. """ return datetime.now().isoformat() def today_str(): """ Return today's date in ISO format (YYYY-MM-DD). Returns: str: Today's date. """ return date.today().isoformat() def default_daily_template(day): """ Generate a default daily note template. Args: day (str): The date string (YYYY-MM-DD). Returns: str: Default content for a daily note. """ return f"""# Daily Note for {day} ## To do: ## Tomorrow: ## Reminder: ## Journal: """ def default_general_template(title, tags): """ Generate a default general note template. Args: title (str): The note title. tags (str): Tags for the note. Returns: str: Default content for a general note. """ tag_line = f"Tags: {tags}" if tags else "" return f"""# General Note: {title} {tag_line} Created on: {now_str()} """ # ----------------------------------------------------------------------------- # NoteTaking Plugin # ----------------------------------------------------------------------------- @irc3.plugin class NoteTaking: """ IRC3 plugin for unified note taking. Provides a single !note command with subcommands to create, update, view, list, and search both daily and general notes stored in TinyDB. """ requires = [ 'irc3.plugins.userlist', # Ensure userlist plugin is loaded, if using user-specific notes ] def __init__(self, bot): """ Initialize the NoteTaking plugin. Args: bot (irc3.IrcBot): The IRC bot instance. """ self.bot = bot # Configuration options with defaults self.db_filename = self.bot.config.get('note_db_filename', 'notes.json') self.daily_template = self.bot.config.get('daily_template', default_daily_template) self.general_template = self.bot.config.get('general_template', default_general_template) try: self.db = TinyDB(self.db_filename) self.daily_table = self.db.table('daily') self.general_table = self.db.table('general') except Exception as e: self.bot.log.error(f"Error initializing TinyDB: {e}") raise # Re-raise to prevent the plugin from loading self.User = Query() self.log = logging.getLogger(__name__) @command async def note(self, mask, target, args): """ Unified note command. %%note today [<content>...] %%note daily [--date <date>] [<content>...] %%note general <title> [--tags <tags>] [<content>...] %%note list [<type>] %%note search <type> <keyword> %%note summary Options: --date <date> Date in YYYY-MM-DD format. --tags <tags> Tags for a general note. <type> Either 'daily' or 'general'. <title> Title for a general note. <content> Content to update the note. <keyword> Keyword to search in notes. """ try: if args.get('today'): await self._note_today(mask, target, args) elif args.get('daily'): await self._note_daily(mask, target, args) elif args.get('general'): await self._note_general(mask, target, args) elif args.get('list'): await self._note_list(target, args) elif args.get('search'): await self._note_search(target, args) elif args.get('summary'): await self._note_summary(target) else: help_msg = ircstyle.style("Usage: !note <subcommand> [options]\n", fg="red", bold=True) help_msg += "Subcommands: today, daily, general, list, search, summary" self.bot.privmsg(target, help_msg) except Exception as e: self.bot.log.error(f"Error processing note command: {e}", exc_info=True) self.bot.privmsg( target, ircstyle.style(f"❌ Error processing command: {e}", fg="red", bold=True) ) async def _note_today(self, mask, target, args): """ Handle the 'today' subcommand for daily notes. If content is provided, update today's note; otherwise, display it. """ day = today_str() content_list = args.get('<content>') try: note = self._get_daily_note(day) if content_list: new_content = " ".join(content_list) if note: self.daily_table.update( {'content': new_content, 'updated': now_str()}, self.User.date == day ) else: self.daily_table.insert({ 'date': day, 'content': new_content, 'created': now_str(), 'updated': now_str() }) msg = ircstyle.style("✅ ", fg="green") + ircstyle.style( f"Daily note for {day} updated.", fg="blue", bold=True ) else: if not note: default_content = self.daily_template(day) self.daily_table.insert({ 'date': day, 'content': default_content, 'created': now_str(), 'updated': now_str() }) note = self._get_daily_note(day) msg = ircstyle.style( f"📝 Daily Note for {day}:\n", fg="yellow", bold=True ) + note.get('content') self.bot.privmsg(target, msg) except Exception as e: self.bot.log.error(f"Error in _note_today: {e}", exc_info=True) self.bot.privmsg( target, ircstyle.style(f"❌ Error: {e}", fg="red", bold=True) ) async def _note_daily(self, mask, target, args): """ Handle the 'daily' subcommand for daily notes with an optional date. If content is provided, update the note; otherwise, display it. """ day = args.get('--date') or today_str() try: datetime.strptime(day, '%Y-%m-%d') # Validate date format except ValueError: self.bot.privmsg( target, ircstyle.style("❌ Invalid date format. Use YYYY-MM-DD.", fg="red", bold=True) ) return content_list = args.get('<content>') try: note = self._get_daily_note(day) if content_list: new_content = " ".join(content_list) if note: self.daily_table.update( {'content': new_content, 'updated': now_str()}, self.User.date == day ) else: self.daily_table.insert({ 'date': day, 'content': new_content, 'created': now_str(), 'updated': now_str() }) msg = ircstyle.style("✅ ", fg="green") + ircstyle.style( f"Daily note for {day} updated.", fg="blue", bold=True ) else: if not note: default_content = self.daily_template(day) self.daily_table.insert({ 'date': day, 'content': default_content, 'created': now_str(), 'updated': now_str() }) note = self._get_daily_note(day) msg = ircstyle.style( f"📝 Daily Note for {day}:\n", fg="yellow", bold=True ) + note.get('content') self.bot.privmsg(target, msg) except Exception as e: self.bot.log.error(f"Error in _note_daily: {e}", exc_info=True) self.bot.privmsg( target, ircstyle.style(f"❌ Error: {e}", fg="red", bold=True) ) async def _note_general(self, mask, target, args): """ Handle the 'general' subcommand for general notes. If content is provided, update the note; otherwise, display it. """ nick = mask.nick # Get the user's nickname # Retrieve <title> as a string (not a list) title = args.get('<title>') if not title: self.bot.privmsg( target, ircstyle.style("❌ ", fg="red") + ircstyle.style("Title is required.", fg="red", bold=True) ) return slug = slugify(title) tags = args.get('--tags') or "" content_list = args.get('<content>') try: # Now filter by owner (nick) so users only access their own general notes note = self._get_general_note(slug, nick) if content_list: new_content = " ".join(content_list) update_fields = {'content': new_content, 'updated': now_str()} if tags: update_fields['tags'] = tags if note: self.general_table.update( update_fields, (self.User.slug == slug) & (self.User.owner == nick) ) msg = ircstyle.style("✅ ", fg="green") + ircstyle.style( f"General note '{title}' updated.", fg="blue", bold=True ) else: self.general_table.insert({ 'title': title, 'slug': slug, 'tags': tags, 'content': new_content, 'created': now_str(), 'updated': now_str(), 'owner': nick # Store the owner of the note }) msg = ircstyle.style("✅ ", fg="green") + ircstyle.style( f"General note '{title}' created.", fg="blue", bold=True ) else: if not note: default_content = self.general_template(title, tags) self.general_table.insert({ 'title': title, 'slug': slug, 'tags': tags, 'content': default_content, 'created': now_str(), 'updated': now_str(), 'owner': nick # Store the owner of the note }) note = self._get_general_note(slug, nick) header = ircstyle.style( f"📝 General Note: {note.get('title')}\n", fg="yellow", bold=True ) tag_line = ircstyle.style( f"Tags: {note.get('tags')}\n", fg="cyan" ) msg = header + tag_line + note.get('content') self.bot.privmsg(target, msg) except Exception as e: self.bot.log.error(f"Error in _note_general: {e}", exc_info=True) self.bot.privmsg( target, ircstyle.style(f"❌ Error: {e}", fg="red", bold=True) ) async def _note_list(self, target, args): """ Handle the 'list' subcommand to list notes. Usage: %%note list [<type>] where <type> is optional and can be 'daily' or 'general'. If <type> is not provided, both daily and general notes are listed. """ note_type = args.get('<type>') try: if not note_type: # List both daily and general notes daily_notes = self.daily_table.all() general_notes = self.general_table.all() # Filter valid daily notes valid_daily_dates = [n.get('date') for n in daily_notes if isinstance(n.get('date'), str)] # Filter valid general notes valid_general_titles = [n.get('title') for n in general_notes if isinstance(n.get('title'), str)] if not valid_daily_dates and not valid_general_titles: msg = ircstyle.style("No notes found.", fg="red", bold=True) else: msg = ircstyle.style("📝 All Notes:", fg="blue", bold=True) + "\n" if valid_daily_dates: msg += ircstyle.style("📅 Daily Notes:", fg="yellow", bold=True) + "\n" msg += "\n".join([f"• {date}" for date in sorted(valid_daily_dates)]) + "\n" if valid_general_titles: msg += ircstyle.style("📋 General Notes:", fg="cyan", bold=True) + "\n" msg += "\n".join([f"• {title}" for title in sorted(valid_general_titles)]) elif note_type == 'daily': notes = self.daily_table.all() # Filter only notes with a valid date string valid_dates = [n.get('date') for n in notes if isinstance(n.get('date'), str)] if not valid_dates: msg = ircstyle.style("No daily notes found.", fg="red", bold=True) else: dates = sorted(valid_dates) msg = ircstyle.style("📅 Daily Notes:", fg="blue", bold=True) + "\n" msg += "\n".join([f"• {date}" for date in dates]) elif note_type == 'general': notes = self.general_table.all() if not notes: msg = ircstyle.style("No general notes found.", fg="red", bold=True) else: titles = sorted(n['title'] for n in notes if isinstance(n.get('title'), str)) msg = ircstyle.style("📋 General Notes:", fg="blue", bold=True) + "\n" msg += "\n".join([f"• {title}" for title in titles]) else: msg = ircstyle.style("Usage: !note list [daily|general]", fg="red", bold=True) for line in msg.split('\n'): self.bot.privmsg(target, line) except Exception as e: self.bot.log.error(f"Error in _note_list: {e}", exc_info=True) self.bot.privmsg(target, ircstyle.style(f"❌ Error: {e}", fg="red", bold=True)) async def _note_search(self, target, args): """ Handle the 'search' subcommand to search notes by keyword. Usage: %%note search <type> <keyword> """ note_type = args.get('<type>') keyword = args.get('<keyword>') if note_type not in ('daily', 'general') or not keyword: msg = ircstyle.style("Usage: !note search <daily|general> <keyword>", fg="red", bold=True) self.bot.privmsg(target, msg) return try: if note_type == 'daily': notes = self.daily_table.search(self.User.content.search(keyword, flags=re.IGNORECASE)) header = "Daily" else: notes = self.general_table.search(self.User.content.search(keyword, flags=re.IGNORECASE)) header = "General" if not notes: msg = ircstyle.style(f"No {note_type} notes found with '{keyword}'.", fg="red", bold=True) else: results = [] for n in notes: identifier = n.get('date') if note_type == 'daily' else n.get('title') content_snippet = n.get('content')[:50] + "..." # Show a snippet of the content results.append(f"{identifier} ({content_snippet})") msg = ircstyle.style(f"{header} Notes matching '{keyword}':\n", fg="blue", bold=True) + "\n".join(results) for line in msg.split('\n'): self.bot.privmsg(target, line) except Exception as e: self.bot.log.error(f"Error in _note_search: {e}", exc_info=True) self.bot.privmsg( target, ircstyle.style(f"❌ Error: {e}", fg="red", bold=True) ) async def _note_summary(self, target): """ Handle the 'summary' subcommand to display note counts. """ try: daily_count = len(self.daily_table.all()) general_count = len(self.general_table.all()) lines = [ ircstyle.style("===== NOTE SUMMARY =====", fg="teal", bold=True), ircstyle.style(f"Daily notes: {daily_count}", fg="yellow"), ircstyle.style(f"General notes: {general_count}", fg="cyan"), ircstyle.style("===== END SUMMARY =====", fg="teal", bold=True) ] for line in lines: self.bot.privmsg(target, line) except Exception as e: self.bot.log.error(f"Error in _note_summary: {e}", exc_info=True) self.bot.privmsg( target, ircstyle.style(f"❌ Error: {e}", fg="red", bold=True) ) def _get_daily_note(self, day): """ Retrieve a daily note for the given day. Args: day (str): The date (YYYY-MM-DD). Returns: dict or None: The note if found, else None. """ try: results = self.daily_table.search(self.User.date == day) return results[0] if results else None except Exception as e: self.bot.log.error(f"Error in _get_daily_note: {e}", exc_info=True) return None def _get_general_note(self, slug, owner=None): """ Retrieve a general note by its slug and (optionally) owner. Args: slug (str): The slugified title. owner (str, optional): The owner of the note. Returns: dict or None: The note if found, else None. """ try: if owner: results = self.general_table.search((self.User.slug == slug) & (self.User.owner == owner)) else: results = self.general_table.search(self.User.slug == slug) return results[0] if results else None except Exception as e: self.bot.log.error(f"Error in _get_general_note: {e}", exc_info=True) return None