From 04cc68a29ab9a91da2461fdd900e0a4cafd7492a Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 18:37:28 +0200 Subject: [PATCH 1/7] External editor can be used for composing by specifying the editor config option --- jrnl.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/jrnl.py b/jrnl.py index 015dc0d7..cda589f5 100755 --- a/jrnl.py +++ b/jrnl.py @@ -1,20 +1,24 @@ -#!/usr/bin/python +#!/usr/bin/env python # encoding: utf-8 - +import os +import tempfile import parsedatetime.parsedatetime as pdt import parsedatetime.parsedatetime_consts as pdc +import subprocess import re import argparse from datetime import datetime import time import json +import sys config = { - 'journal': "/home/manuel/Dropbox/Notes/journal.txt", + 'journal': "/Users/dedan/Dropbox/Notes/journal_neu.txt", + 'editor': "subl -w", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", - 'tagsymbols': '#@' + 'tagsymbols': '@' } class Entry: @@ -37,7 +41,7 @@ class Entry: space = "\n" return "%(date)s %(title)s %(body)s %(space)s" % { - 'date': date_str, + 'date': date_str, 'title': self.title, 'body': body, 'space': space @@ -133,13 +137,13 @@ class Journal: def filter(self, tags=[], start_date=None, end_date=None, strict=False): """Removes all entries from the journal that don't match the filter. - + tags is a list of tags, each being a string that starts with one of the tag symbols defined in the config, e.g. ["@John", "#WorldDomination"]. start_date and end_date define a timespan by which to filter. - If strict is True, all tags must be present in an entry. If false, the + If strict is True, all tags must be present in an entry. If false, the entry is kept if any tag is present.""" search_tags = set(tags) end_date = self.parse_date(end_date) @@ -162,7 +166,7 @@ class Journal: return date date, flag = self.dateparse.parse(date) - + if not flag: # Oops, unparsable. return None @@ -198,6 +202,7 @@ class Journal: self.sort() if __name__ == "__main__": + print sys.argv parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') @@ -210,6 +215,7 @@ if __name__ == "__main__": reading.add_argument('-n', dest='limit', default=None, metavar="N", help='Shows the last n entries matching the filter', nargs="?", type=int) reading.add_argument('-json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal') args = parser.parse_args() + print args # open journal journal = Journal(config=config) @@ -225,7 +231,15 @@ if __name__ == "__main__": # No text? Query if compose and not args.text: - raw = raw_input("Compose Entry: ") + if config['editor']: + tmpfile = os.path.join(tempfile.gettempdir(), "jrnl") + subprocess.call(config['editor'].split() + [tmpfile]) + with open(tmpfile) as f: + raw = f.read() + os.remove(tmpfile) + + else: + raw = raw_input("Compose Entry: ") if raw: args.text = [raw] else: @@ -233,7 +247,7 @@ if __name__ == "__main__": # Writing mode if compose: - raw = " ".join(args.text).strip() + raw = " ".join(args.text).strip() journal.new_entry(raw, args.date) print journal journal.write() From 1569f9c59cf179fa604255e80ee0469e48be239d Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 19:28:11 +0200 Subject: [PATCH 2/7] create config file and ask for journal file name with autocomplete !!! --- jrnl.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/jrnl.py b/jrnl.py index cda589f5..7c355355 100755 --- a/jrnl.py +++ b/jrnl.py @@ -11,10 +11,12 @@ from datetime import datetime import time import json import sys +import readline, glob -config = { - 'journal': "/Users/dedan/Dropbox/Notes/journal_neu.txt", - 'editor': "subl -w", + +default_config = { + 'journal': os.path.expanduser("~/journal.txt"), + 'editor': "", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -202,7 +204,26 @@ class Journal: self.sort() if __name__ == "__main__": - print sys.argv + config_path = os.path.expanduser('~/.jrnl_config') + if not os.path.exists(config_path): + def autocomplete(text, state): + expansions = glob.glob(os.path.expanduser(text)+'*') + expansions = [e+"/" if os.path.isdir(e) else e for e in expansions] + expansions.append(None) + return expansions[state] + readline.set_completer_delims(' \t\n;') + readline.parse_and_bind("tab: complete") + readline.set_completer(autocomplete) + + path_query = 'Path to your journal file (leave blank for ~/journal.txt): ' + journal_path = raw_input(path_query).strip() or os.path.expanduser('~/journal.txt') + default_config['journal'] = os.path.expanduser(journal_path) + open(default_config['journal'], 'a').close() # Touch to make sure it's there + with open(config_path, 'w') as f: + json.dump(default_config, f) + with open(config_path) as f: + config = json.load(f) + parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') @@ -215,7 +236,6 @@ if __name__ == "__main__": reading.add_argument('-n', dest='limit', default=None, metavar="N", help='Shows the last n entries matching the filter', nargs="?", type=int) reading.add_argument('-json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal') args = parser.parse_args() - print args # open journal journal = Journal(config=config) From c6b86991c448c6641ced307ee2d62f23ff235ad9 Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 20:04:10 +0200 Subject: [PATCH 3/7] Buffer journal file into string before reading or writing --- jrnl.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jrnl.py b/jrnl.py index 7c355355..f6b91112 100755 --- a/jrnl.py +++ b/jrnl.py @@ -17,6 +17,7 @@ import readline, glob default_config = { 'journal': os.path.expanduser("~/journal.txt"), 'editor': "", + 'encrypt': True, 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -86,8 +87,9 @@ class Journal: entries = [] current_entry = None - journal_file = open(filename) - for line in journal_file.readlines(): + with open(filename) as f: + journal_plain = f.read() + for line in journal_plain.split(os.linesep): if line: try: new_date = datetime.fromtimestamp(time.mktime(time.strptime(line[:date_length], config['timeformat']))) @@ -103,7 +105,6 @@ class Journal: # Append last entry if current_entry: entries.append(current_entry) - journal_file.close() for entry in entries: entry.parse_tags() return entries @@ -123,10 +124,9 @@ class Journal: def write(self, filename = None): """Dumps the journal into the config file, overwriting it""" filename = filename or self.config['journal'] - journal_file = open(filename, 'w') - for entry in self.entries: - journal_file.write(str(entry)+"\n") - journal_file.close() + journal_plain = os.linesep.join([str(e) for e in self.entries]) + with open(filename, 'w') as journal_file: + journal_file.write(journal_plain) def sort(self): """Sorts the Journal's entries by date""" From 9c18fdc9b46046edc4b8b6d1c0ba5fbd29f54978 Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 20:20:15 +0200 Subject: [PATCH 4/7] Encryption / decryption --- jrnl.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/jrnl.py b/jrnl.py index f6b91112..a7b53f96 100755 --- a/jrnl.py +++ b/jrnl.py @@ -12,12 +12,14 @@ import time import json import sys import readline, glob - +from Crypto.Cipher import AES +import getpass default_config = { 'journal': os.path.expanduser("~/journal.txt"), 'editor': "", 'encrypt': True, + 'key': "", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -74,6 +76,10 @@ class Journal: self.entries = self.open() self.sort() + def _block_tail(self, s, b=16): + """Appends spaces to a string until length is a multiple of b""" + return s + " "*(b - len(s) % b) + def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" @@ -88,7 +94,15 @@ class Journal: current_entry = None with open(filename) as f: - journal_plain = f.read() + if config['encrypt']: + journal_encrypted = f.read() + key = config['key'] or getpass.getpass() + key = self._block_tail(key) + self.crypto = AES.new(key, AES.MODE_ECB) + journal_plain = self.crypto.decrypt(journal_encrypted) + else: + journal_plain = f.read() + for line in journal_plain.split(os.linesep): if line: try: @@ -126,7 +140,11 @@ class Journal: filename = filename or self.config['journal'] journal_plain = os.linesep.join([str(e) for e in self.entries]) with open(filename, 'w') as journal_file: - journal_file.write(journal_plain) + if self.crypto: + journal_padded = self._block_tail(journal_plain) + journal_file.write(self.crypto.encrypt(journal_padded)) + else: + journal_file.write(journal_plain) def sort(self): """Sorts the Journal's entries by date""" @@ -237,9 +255,6 @@ if __name__ == "__main__": reading.add_argument('-json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal') args = parser.parse_args() - # open journal - journal = Journal(config=config) - # Guess mode compose = True if args.start_date or args.end_date or args.limit or args.json or args.strict: @@ -265,6 +280,9 @@ if __name__ == "__main__": else: compose = False + # open journal + journal = Journal(config=config) + # Writing mode if compose: raw = " ".join(args.text).strip() From acb7fa2ed46fc57b24dfb6d031cb0828bf224de3 Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 20:36:02 +0200 Subject: [PATCH 5/7] saving forces blanks to be appended --- jrnl.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/jrnl.py b/jrnl.py index a7b53f96..01d958e8 100755 --- a/jrnl.py +++ b/jrnl.py @@ -14,6 +14,7 @@ import sys import readline, glob from Crypto.Cipher import AES import getpass +import mimetypes default_config = { 'journal': os.path.expanduser("~/journal.txt"), @@ -76,8 +77,10 @@ class Journal: self.entries = self.open() self.sort() - def _block_tail(self, s, b=16): + def _block_tail(self, s, b=16, force=False): """Appends spaces to a string until length is a multiple of b""" + if force and len(s) % 16 == 0: + return s + " "*16 return s + " "*(b - len(s) % b) def open(self, filename=None): @@ -100,6 +103,10 @@ class Journal: key = self._block_tail(key) self.crypto = AES.new(key, AES.MODE_ECB) journal_plain = self.crypto.decrypt(journal_encrypted) + + print len(journal_plain) + print journal_plain[-16:] + print 'xxxxxxxxxx' else: journal_plain = f.read() @@ -141,7 +148,7 @@ class Journal: journal_plain = os.linesep.join([str(e) for e in self.entries]) with open(filename, 'w') as journal_file: if self.crypto: - journal_padded = self._block_tail(journal_plain) + journal_padded = self._block_tail(journal_plain, force=True) journal_file.write(self.crypto.encrypt(journal_padded)) else: journal_file.write(journal_plain) @@ -238,7 +245,7 @@ if __name__ == "__main__": default_config['journal'] = os.path.expanduser(journal_path) open(default_config['journal'], 'a').close() # Touch to make sure it's there with open(config_path, 'w') as f: - json.dump(default_config, f) + json.dump(default_config, f, indent=2) with open(config_path) as f: config = json.load(f) From 196531aafc6f2c0652e31669f9d0e1d6ca310c6c Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 20:38:36 +0200 Subject: [PATCH 6/7] Checks if password is wrong --- jrnl.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jrnl.py b/jrnl.py index 01d958e8..362fa230 100755 --- a/jrnl.py +++ b/jrnl.py @@ -103,10 +103,12 @@ class Journal: key = self._block_tail(key) self.crypto = AES.new(key, AES.MODE_ECB) journal_plain = self.crypto.decrypt(journal_encrypted) - - print len(journal_plain) - print journal_plain[-16:] - print 'xxxxxxxxxx' + # encrypted files should end with spaces. No spaces, no luck. + while journal_plain[-1] != " ": + key = getpass.getpass('Wrong password. Try again: ') + key = self._block_tail(key) + self.crypto = AES.new(key, AES.MODE_ECB) + journal_plain = self.crypto.decrypt(journal_encrypted) else: journal_plain = f.read() From 651f9e5213aa76354389b4e44601432b9965baea Mon Sep 17 00:00:00 2001 From: Stephan Gabler Date: Fri, 13 Apr 2012 20:58:22 +0200 Subject: [PATCH 7/7] Asks for password at creation of .jrnl_config --- jrnl.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/jrnl.py b/jrnl.py index 362fa230..2f7a9fbc 100755 --- a/jrnl.py +++ b/jrnl.py @@ -19,7 +19,7 @@ import mimetypes default_config = { 'journal': os.path.expanduser("~/journal.txt"), 'editor': "", - 'encrypt': True, + 'encrypt': False, 'key': "", 'default_hour': 9, 'default_minute': 0, @@ -73,6 +73,7 @@ class Journal: consts = pdc.Constants() consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday self.dateparse = pdt.Calendar(consts) + self.crypto = None self.entries = self.open() self.sort() @@ -104,11 +105,16 @@ class Journal: self.crypto = AES.new(key, AES.MODE_ECB) journal_plain = self.crypto.decrypt(journal_encrypted) # encrypted files should end with spaces. No spaces, no luck. - while journal_plain[-1] != " ": + attempts = 1 + while journal_plain and attempts < 3 and journal_plain[-1] != " ": + attempts += 1 key = getpass.getpass('Wrong password. Try again: ') key = self._block_tail(key) self.crypto = AES.new(key, AES.MODE_ECB) journal_plain = self.crypto.decrypt(journal_encrypted) + if attempts >= 3: + print("Extremely wrong password.") + sys.exit(-1) else: journal_plain = f.read() @@ -245,11 +251,21 @@ if __name__ == "__main__": path_query = 'Path to your journal file (leave blank for ~/journal.txt): ' journal_path = raw_input(path_query).strip() or os.path.expanduser('~/journal.txt') default_config['journal'] = os.path.expanduser(journal_path) + + key = getpass.getpass("Enter password for journal (leave blank for no encryption): ") + if key: + default_config['encrypt'] = True + print("Journal will be encrypted.") + print("If you want to, you can store your password in .jrnl_config and will never be bothered about it again.") open(default_config['journal'], 'a').close() # Touch to make sure it's there with open(config_path, 'w') as f: json.dump(default_config, f, indent=2) - with open(config_path) as f: - config = json.load(f) + config = default_config + if key: + config['key'] = key + else: + with open(config_path) as f: + config = json.load(f) parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments')