I felt like my previous script was suboptimal as a keylogger because:
it could only be parsed by regular expressions
it generated a pretty verbose log with every keystroke, which seemed like an easy way to rapidly create a multi-megabyte log file.
I’ve updated the keylogger so that:
the log file is a
JSON Lines document, which is very easy to parse (just split it into lines and use your favorite JSON parser to parse each line)
the date is only logged when the date changes or when the logger first starts up
The current filename is only logged when the file is saved or opened.
key logs have the format {"+": <text added>, "t": hours:minutes:seconds, "pos": position}, with "-": <text removed> for text removal operations.
While the user must later re-associate filenames and dates with each modification log, this is a very simple task.
Below is an example log:
{"date":"2023-06-07"}
{"fileOpened":"Path\\To\\AppData\\Roaming\\Notepad++\\plugins\\config\\PythonScript\\scripts\\keylogger.py","t":"14:38:16"}
{"fileOpened":"Path\\To\\Python311\\example_silly.txt","t":"14:38:16"}
{"+":"a","pos":0,"t":"14:38:18"}
{"+":"s","pos":1,"t":"14:38:18"}
{"+":"d","pos":2,"t":"14:38:18"}
{"+":"f","pos":3,"t":"14:38:18"}
{"+":"\r\n","pos":4,"t":"14:38:19"}
{"-":"asdf\r\n","pos":0,"t":"14:38:20"}
{"fileSaved":"Path\\To\\Python311\\example_silly.txt","t":"14:38:21"}
And here’s the new script:
from Npp import *
from datetime import datetime
import json
from pathlib import Path
# create a subdirectory of the PythonScript scripts directory called mod_logger
LOGDIR = Path(__file__).parent / 'mod_logger'
LOGFILE = LOGDIR / 'mod.jsonl' # create a JSON Lines document in that directory
def compact_json_dump(obj):
return json.dumps(obj, separators=(',', ':'))
def date_to_int(date):
return (date.year << 16) | (date.month << 8) | date.day
class ModLogger:
def __init__(self):
self.FNAME = notepad.getCurrentFilename()
self.date_int = 0
self.log_date_change()
def log(self, obj):
with LOGFILE.open('a') as f:
f.write(compact_json_dump(obj))
f.write('\n')
def log_date_change(self):
now = datetime.now()
dint = date_to_int(now)
if dint > self.date_int:
self.date_int = dint
self.log({'date': '%d-%02d-%02d' % (now.year, now.month, now.day)})
return now
def on_bufferactivate(self, notif):
self.FNAME = notepad.getCurrentFilename()
now = self.log_date_change()
self.log({'fileOpened': self.FNAME, 't': '%02d:%02d:%02d' % (now.hour, now.minute, now.second)})
def on_filesaved(self, notif):
now = self.log_date_change()
self.log({'fileSaved': self.FNAME, 't': '%02d:%02d:%02d' % (now.hour, now.minute, now.second)})
def on_modification(self, notif):
# see https://www.scintilla.org/ScintillaDoc.html#SCN_MODIFIED
mod_type = notif['modificationType']
keylog = {}
if mod_type & 0x01 != 0:
keylog['+'] = notif['text']
elif mod_type & 0x02 != 0:
keylog['-'] = notif['text']
else:
return
keylog['pos'] = notif['position']
now = self.log_date_change()
keylog['t'] = '%02d:%02d:%02d' % (now.hour, now.minute, now.second)
self.log(keylog)
if __name__ == '__main__':
try:
MOLO
except NameError:
MOLO = ModLogger()
if not LOGDIR.exists():
LOGDIR.mkdir()
if not LOGFILE.exists():
with LOGFILE.open('w') as f:
pass
# I *WANT* to use SCINTILLANOTIFICATION.KEY, which would *actually*
# log keypresses,
# but for some reason that notification never fires,
# and SCINTILLANOTIFICATION.MODIFIED is the closest we've got
editor.callback(MOLO.on_modification, [SCINTILLANOTIFICATION.MODIFIED])
notepad.callback(MOLO.on_bufferactivate, [NOTIFICATION.BUFFERACTIVATED])
notepad.callback(MOLO.on_filesaved, [NOTIFICATION.FILEBEFORESAVE])