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 :
today [ < content > . . . ]
View or update today ' s daily note.
daily [ - - date < date > ] [ < content > . . . ]
View or update a daily note for the specified date ( default is today ) .
general < title > [ - - 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__ )
2025-02-18 14:48:02 +00:00
@command ( aliases = [ ' notes ' ] , public = True )
2025-02-18 01:10:25 +00:00
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 :
2025-02-18 14:48:02 +00:00
if not args :
# Display the help message if no arguments are provided
help_msg = ircstyle . style ( " Usage: !note <subcommand> [options] \n " , fg = " red " , bold = True )
help_msg + = " Subcommands: \n "
help_msg + = ircstyle . style ( " today " , fg = " yellow " ) + " - View or update today ' s daily note. \n "
help_msg + = ircstyle . style ( " daily [--date <date>] " , fg = " yellow " ) + " - View or update a daily note for a specific date (default is today). \n "
help_msg + = ircstyle . style ( " general <title> [--tags <tags>] " , fg = " yellow " ) + " - View or update a general note with the given title. Optionally, specify tags. \n "
help_msg + = ircstyle . style ( " list [daily|general] " , fg = " yellow " ) + " - List all notes of a specific type (either ' daily ' or ' general ' ). \n "
help_msg + = ircstyle . style ( " search <type> <keyword> " , fg = " yellow " ) + " - Search for a keyword in notes of a specific type. \n "
help_msg + = ircstyle . style ( " summary " , fg = " yellow " ) + " - Display a summary of note counts. \n "
self . bot . privmsg ( target , help_msg )
elif 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 )
else :
2025-02-18 14:48:02 +00:00
help_msg = ircstyle . style ( " Invalid subcommand! Use !note for a list of available commands. \n " , fg = " red " , bold = True )
2025-02-18 01:10:25 +00:00
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