g1mp/plugins/notes.py

671 lines
27 KiB
Python
Raw Permalink Normal View History

2025-02-18 01:10:25 +00:00
#!/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:
2025-02-19 03:51:12 +00:00
today [--add|--replace] [<content>...]
View or update today's daily note. If content is provided and a note for
today already exists, a warning is shown unless you specify --add (to append)
or --replace (to replace).
2025-02-18 01:10:25 +00:00
2025-02-19 03:51:12 +00:00
daily [--date <date>] [--add|--replace] [<content>...]
View or update a daily note for the specified date (default is today). If
content is provided and a note already exists, a warning is shown unless you
specify --add (to append) or --replace (to replace).
2025-02-18 01:10:25 +00:00
general <title> [--tags <tags>] [<content>...]
View or update a general note with the given title.
Optionally specify tags.
list <type>
2025-02-19 03:51:12 +00:00
List all notes of the specified type (either 'daily' or 'general'). The
content of each note is displayed truncated.
2025-02-18 01:10:25 +00:00
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
2025-02-19 03:51:12 +00:00
Version: 1.3
Date: 2025-02-18
2025-02-18 01:10:25 +00:00
"""
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}
2025-02-19 03:51:12 +00:00
{tag_line} Created on: {now_str()}
2025-02-18 01:10:25 +00:00
"""
2025-02-19 03:51:12 +00:00
def truncate(text, length=50):
"""
Truncate the text to a specified length, appending "..." if truncated.
Args:
text (str): The text to truncate.
length (int): Maximum length of the returned text.
Returns:
str: The truncated text.
"""
if not isinstance(text, str):
text = str(text)
return text if len(text) <= length else text[:length] + "..."
2025-02-18 01:10:25 +00:00
# -----------------------------------------------------------------------------
# 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 = [
2025-02-19 08:11:44 +00:00
'plugins.users', # Ensure userlist plugin is loaded, if using user-specific notes
2025-02-18 01:10:25 +00:00
]
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__)
2025-02-19 03:14:09 +00:00
@command(aliases=['notes'], public=True, options_first=False, use_shlex=True)
2025-02-18 01:10:25 +00:00
async def note(self, mask, target, args):
"""
2025-02-19 03:51:12 +00:00
%%note today [--add|--replace] [<content>...]
%%note daily [--date=<date>] [--add|--replace] [<content>...]
%%note general <title> [--tags=<tags>] [<content>...]
%%note list [<type>]
2025-02-18 01:10:25 +00:00
%%note search <type> <keyword>
%%note summary
2025-02-19 03:51:12 +00:00
%%note
2025-02-18 01:10:25 +00:00
"""
2025-02-19 03:14:09 +00:00
# If no subcommand is provided, show the help message.
if not any(args.get(key) for key in ('today', 'daily', 'general', 'list', 'search', 'summary')):
2025-02-19 03:51:12 +00:00
# If there's content provided without a subcommand, treat it as a daily note update.
if args.get('<content>'):
await self._note_today(mask, target, args)
else:
self._show_help(target)
2025-02-19 03:14:09 +00:00
return
2025-02-18 01:10:25 +00:00
try:
2025-02-19 03:14:09 +00:00
if args.get('today'):
2025-02-18 01:10:25 +00:00
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)
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)
)
2025-02-19 03:14:09 +00:00
def _show_help(self, target):
help_lines = [
ircstyle.style("Usage: !note <subcommand> [options]", fg="red", bold=True),
"Subcommands:",
2025-02-19 03:51:12 +00:00
f"{ircstyle.style(' today [--add|--replace] [<content>...]', fg='yellow')} - View or update today's daily note.",
f"{ircstyle.style(' daily [--date <date>] [--add|--replace] [<content>...]', fg='yellow')} - View or update a daily note for a specific date (default is today).",
f"{ircstyle.style(' general <title> [--tags <tags>] [<content>...]', fg='yellow')} - View or update a general note with the given title. Optionally, specify tags.",
f"{ircstyle.style(' list [daily|general]', fg='yellow')} - List all notes of a specific type with truncated content.",
2025-02-19 03:14:09 +00:00
f"{ircstyle.style(' search <type> <keyword>', fg='yellow')} - Search for a keyword in notes of a specific type.",
f"{ircstyle.style(' summary', fg='yellow')} - Display a summary of note counts.",
]
for line in help_lines:
self.bot.privmsg(target, line)
2025-02-18 01:10:25 +00:00
async def _note_today(self, mask, target, args):
"""
Handle the 'today' subcommand for daily notes.
2025-02-19 03:51:12 +00:00
If content is provided, update today's note.
If a note already exists and content is provided, warn the user unless
--add or --replace is specified.
2025-02-18 01:10:25 +00:00
"""
day = today_str()
content_list = args.get('<content>')
try:
note = self._get_daily_note(day)
if content_list:
new_content = " ".join(content_list)
2025-02-19 03:51:12 +00:00
# Check if both flags are provided
if args.get('--add') and args.get('--replace'):
self.bot.privmsg(
target,
ircstyle.style("❌ Error: Cannot use both --add and --replace simultaneously. Please choose one.", fg="red", bold=True)
2025-02-18 01:10:25 +00:00
)
2025-02-19 03:51:12 +00:00
return
if note:
# If note exists but neither flag is provided, warn the user.
if not (args.get('--add') or args.get('--replace')):
warning = ircstyle.style(
f"⚠️ Warning: Daily note for {day} already exists. Use --add to append to it or --replace to overwrite it.", fg="red", bold=True
)
self.bot.privmsg(target, warning)
return
if args.get('--add'):
appended_content = note.get('content') + "\n" + new_content
self.daily_table.update(
{'content': appended_content, 'updated': now_str()},
self.User.date == day
)
msg = ircstyle.style("", fg="green") + ircstyle.style(
f"Daily note for {day} updated (content added).", fg="blue", bold=True
)
elif args.get('--replace'):
self.daily_table.update(
{'content': new_content, 'updated': now_str()},
self.User.date == day
)
msg = ircstyle.style("", fg="green") + ircstyle.style(
f"Daily note for {day} replaced.", fg="blue", bold=True
)
2025-02-18 01:10:25 +00:00
else:
2025-02-19 03:51:12 +00:00
# No existing note; create a new one.
2025-02-18 01:10:25 +00:00
self.daily_table.insert({
'date': day,
'content': new_content,
'created': now_str(),
'updated': now_str()
})
2025-02-19 03:51:12 +00:00
msg = ircstyle.style("", fg="green") + ircstyle.style(
f"Daily note for {day} created.", fg="blue", bold=True
)
2025-02-18 01:10:25 +00:00
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.
2025-02-19 03:51:12 +00:00
If content is provided, update the note.
If a note already exists and content is provided, warn the user unless
--add or --replace is specified.
2025-02-18 01:10:25 +00:00
"""
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>')
2025-02-19 03:14:09 +00:00
2025-02-18 01:10:25 +00:00
try:
note = self._get_daily_note(day)
if content_list:
new_content = " ".join(content_list)
2025-02-19 03:51:12 +00:00
# Check if both flags are provided
if args.get('--add') and args.get('--replace'):
self.bot.privmsg(
target,
ircstyle.style("❌ Error: Cannot use both --add and --replace simultaneously. Please choose one.", fg="red", bold=True)
2025-02-18 01:10:25 +00:00
)
2025-02-19 03:51:12 +00:00
return
if note:
if not (args.get('--add') or args.get('--replace')):
self.bot.privmsg(
target,
ircstyle.style(f"⚠️ Warning: Daily note for {day} already exists. Use --add to append or --replace to overwrite.", fg="red", bold=True)
)
return
if args.get('--add'):
appended_content = note.get('content') + "\n" + new_content
self.daily_table.update(
{'content': appended_content, 'updated': now_str()},
self.User.date == day
)
msg = ircstyle.style("", fg="green") + ircstyle.style(
f"Daily note for {day} updated (content added).", fg="blue", bold=True
)
elif args.get('--replace'):
self.daily_table.update(
{'content': new_content, 'updated': now_str()},
self.User.date == day
)
msg = ircstyle.style("", fg="green") + ircstyle.style(
f"Daily note for {day} replaced.", fg="blue", bold=True
)
2025-02-18 01:10:25 +00:00
else:
2025-02-19 03:51:12 +00:00
# Create new note if none exists.
2025-02-18 01:10:25 +00:00
self.daily_table.insert({
'date': day,
'content': new_content,
'created': now_str(),
'updated': now_str()
})
2025-02-19 03:51:12 +00:00
msg = ircstyle.style("", fg="green") + ircstyle.style(
f"Daily note for {day} created.", fg="blue", bold=True
)
2025-02-18 01:10:25 +00:00
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):
"""
2025-02-19 03:51:12 +00:00
Handle the 'list' subcommand to list notes with truncated content.
2025-02-18 01:10:25 +00:00
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:
2025-02-19 03:51:12 +00:00
# List both daily and general notes with content truncated.
2025-02-18 01:10:25 +00:00
daily_notes = self.daily_table.all()
general_notes = self.general_table.all()
2025-02-19 03:51:12 +00:00
if not daily_notes and not general_notes:
2025-02-18 01:10:25 +00:00
msg = ircstyle.style("No notes found.", fg="red", bold=True)
else:
2025-02-19 03:51:12 +00:00
msg_lines = [ircstyle.style("📝 All Notes:", fg="blue", bold=True)]
if daily_notes:
msg_lines.append(ircstyle.style("📅 Daily Notes:", fg="yellow", bold=True))
# Sort daily notes by date.
daily_sorted = sorted(
[n for n in daily_notes if isinstance(n.get('date'), str)],
key=lambda x: x['date']
)
for note in daily_sorted:
date_str = note.get('date')
snippet = truncate(note.get('content', ''))
msg_lines.append(f"{ircstyle.style(date_str, fg='green')}: {ircstyle.style(snippet, fg='white')}")
if general_notes:
msg_lines.append(ircstyle.style("📋 General Notes:", fg="cyan", bold=True))
# Sort general notes by title.
general_sorted = sorted(
[n for n in general_notes if isinstance(n.get('title'), str)],
key=lambda x: x['title']
)
for note in general_sorted:
title = note.get('title')
snippet = truncate(note.get('content', ''))
msg_lines.append(f"{ircstyle.style(title, fg='green')}: {ircstyle.style(snippet, fg='white')}")
msg = "\n".join(msg_lines)
2025-02-18 01:10:25 +00:00
elif note_type == 'daily':
notes = self.daily_table.all()
2025-02-19 03:51:12 +00:00
valid_notes = [n for n in notes if isinstance(n.get('date'), str)]
if not valid_notes:
2025-02-18 01:10:25 +00:00
msg = ircstyle.style("No daily notes found.", fg="red", bold=True)
else:
2025-02-19 03:51:12 +00:00
msg_lines = [ircstyle.style("📅 Daily Notes:", fg="blue", bold=True)]
daily_sorted = sorted(valid_notes, key=lambda x: x['date'])
for note in daily_sorted:
date_str = note.get('date')
snippet = truncate(note.get('content', ''))
msg_lines.append(f"{ircstyle.style(date_str, fg='green')}: {ircstyle.style(snippet, fg='white')}")
msg = "\n".join(msg_lines)
2025-02-18 01:10:25 +00:00
elif note_type == 'general':
notes = self.general_table.all()
2025-02-19 03:51:12 +00:00
valid_notes = [n for n in notes if isinstance(n.get('title'), str)]
if not valid_notes:
2025-02-18 01:10:25 +00:00
msg = ircstyle.style("No general notes found.", fg="red", bold=True)
else:
2025-02-19 03:51:12 +00:00
msg_lines = [ircstyle.style("📋 General Notes:", fg="blue", bold=True)]
general_sorted = sorted(valid_notes, key=lambda x: x['title'])
for note in general_sorted:
title = note.get('title')
snippet = truncate(note.get('content', ''))
msg_lines.append(f"{ircstyle.style(title, fg='green')}: {ircstyle.style(snippet, fg='white')}")
msg = "\n".join(msg_lines)
2025-02-18 01:10:25 +00:00
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