Kitzel mich (Linux-Magazin, April 2012)

Aus den Augen, aus dem Sinn: Um vorläufig eingefrorene Projekte regelmäßig zu reanimieren, sortieren Anhänger der "Getting-Things-Done"-Methode Zettel in Hängeregistern nach Datum und sehen diese "Tickler-Files" regelmäßig durch. Perl und Evernote hingegen kitzeln den User automatisch mit Erinnerungsmeldungen in dessen Inbox wach.

"Mind like Water", die geistige Ruhe und extreme Flexibilität eines Karatekämpfers beim Bewältigen des Alltags, das verspricht die Produktivitätsmethode "Getting-Things-Done" (GTD) von Erfolgsautor David Allen ([3]). Grundregel: Man belastet sich nicht mehr mit Aufgaben, bei denen man den nächsten Schritt momentan eh nicht erledigen kann, sondern legt sie in einem Ordnungssystem (Abbildung 1) ab.

Abbildung 1: Ein Tickler-System für Papierzettel (Flickr-Foto: [http://www.flickr.com/photos/benchristen/3902110], können wir leider nicht nehmen, hat keine kommerzielle Lizenz, vielleicht könnt ihr ja ein lizensiertes auftreiben? Außerdem habe ich mal im Kommentarteil des Fotos angefragt, vielleicht erbarmt sich der User ja.)

Wer Papier mag ...

Die Hängeordner tragen Etiketten für die Tage des Monats und die Monate des Jahres. Hat ein Kollege zum Beispiel für den 14. eines Monats ein Ergebnis angekündigt, schreibt der GTD-Jünger einen Zettel mit den Eckdaten und wirft ihn in den Order mit der Nummer 14. Und sollte man im Januar mit der Urlaubsplanung für den Sommer beginnen, weil es dann günstige Flüge gibt, landet ein Zettel mit dem URL des Online-Buchungssystems im Order mit dem Reiter "Januar". Am Abend des 13. des Monats oder Ende Februar fällt dann beim regelmäßigen Prüfen der Ordner auf, dass für den nächsten Tag oder den kommenden Monat bestimmte Aufgaben anstehen. Die packt man dann zum Erstaunen seiner unorganisierten Mitwelt tatsächlich pünktlich an und prüft zuverlässig nach, ob Terminversprechen tatsächlich eingehalten wurden.

Automatisch mit Evernote

Wie schon im vorletzten Snapshot ([2]) erwähnt, kommt der in der Grundversion kostenlose Service "Evernote" ([4]) wie gerufen, um Alltagsaufgaben gemäß GTD-Tipps zu optimieren. Der User definiert sich ein Eingangsfach (als "00-Inbox", damit Evernote den Ordner ganz nach oben sortiert), in dem alle Anfragen eingehen, für die der User dann den nächsten Bearbeitungsschritt bestimmt und diesen entweder sofort anpackt oder die Notiz im richtigen Projektordner ablegt.

Abbildung 2: Die Inbox des Users zeigt zunächst nur einen lesenswerten Artikel ...

Ähnlich wie mit Papierzetteln in Hängeregistern lässt sich mit Evernote ein Tickler-System aufsetzen: Das Notebook "01-Tickler" enthält Einzeleinträge, die in der Betreffzeile ihr Aktionsdatum im Format YYYY-MM-DD führen. Mittels der Evernote-API öffnet dann ein einmal täglich ablaufender Cronjob das Tickler-Notebook, wandert durch alle Einträge und prüft, ob ein datierter Eintrag am kommenden Tag fällig ist. Trifft dies zu, schiebt das Skript die Notiz in die Inbox des Users, der erfreut zur Kenntnis nimmt, dass er nun den nächsten Bearbeitungsschritt eines Miniprojekts in Angriff nehmen kann.

Abbildung 3: Die Tickler-Datei zeigt für morgen einen Zahnarzttermin an.

Planen mit Tickler

Der Eintrag "2013-01 Sommerferien planen" erinnert so daran, bereits im Januar 2013 einen Flug für den Urlaub im August zu buchen, und der Cronjob wird ihn am 31.12.2012 aus dem Tickler-Notebook ziehen und in die Inbox stellen, damit der User ein neues Projekt "Ferienplanung" in Angriff nimmt und die nächste Aktion ("Lufthansa-Angebote studieren") startet. Und der Tickler-Eintag "2012-04-14: Müller hat Linux-Version fertig" wandert am Abend des 13.04. automatisch in die Inbox des Anwenders, der seinen aus allen Wolken fallenden Kollegen am nächsten Tag an dessen vor Wochen gegebenes Terminversprechen erinnert.

Abbildung 4: ... doch nach dem Lauf des Tickler-Cronjobs landet der morgige Zahnarzttermin in der Inbox.

Mit Evernotes Web API ist die Implementierung des Ticklers ein Kinderspiel. Der vorletzte Snapshot ([2]) hat bereits ausführlich dokumentiert, wie das verwendete Thrift-Protokoll mit Perl funktioniert, und wie Applikationsschreiber einen Application-Key von der Evernote-Webseite holen, um auf der Evernote-Sandbox zunächst etwaige Bugs auszubügeln und dann Zugriff auf die Produktions-Server zu beantragen.

Listing 1 wechselt anfangs im BEGIN-Block in das Verzeichnis $Bin, in dem das Skript liegt, um sicherzustellen, dass es die später eingeholten auto-generierten Thrift-Module im Unterverzeichnis gen-perl auch dann findet, falls es als Cronjob startet. Das CPAN-Modul local::lib sorgt dafür, dass es auch im Homeverzeichnis des Users installierte CPAN-Module findet. Zeile 26 initialisiert Log4perl, das mit DEBUG-Anweisungen in einer Logdatei festschreibt, was das Skript so treibt. Besonders bei einem per Cronjob gestarteten Skript ist dies hilfreich, um die Ursachen etwaiger Fehlfunktionen aufzuspüren und Bugs auszumerzen. Neben dem Log-Level $DEBUG legt es die Kategorie "main" fest, damit nicht gleich alle verwendeten CPAN-Module mit eingebautem Log4perl-Support zu loggen anfangen, sondern nur das Hauptprogramm. Abbildung 4 zeigt die Logdaten eines erfolgreichen Skriptlaufs.

Operation am offenen Gehirn

Zeile 52 authentisiert den User auf dem Evernote-Webserver. Stimmt das Password und der Consumer-Key, erlaubt dieser dem Skript uneingeschränkten Lese- und Schreibzugriff. Da es sich um sensitive Daten handelt, die niemand gern verlieren möchte, ist entsprechende Vorsicht beim Programmieren angebracht. Außerdem sollte der User sicherstellen, dass das Skript nur auf einem gesicherten System hinter einer Firewall läuft, um Missbrauch, zum Beispiel auf geknackten Webservern, vorzubeugen.

Um nun die Einträge des Notebooks "01-Tickler" aufzuspüren, benötigt das Skript dessen GUID. Zeile 79 iteriert deshalb über alle Notebooks des Accounts und prüft, ob das gerade bearbeitete den gesuchten Namen trägt. Das Gleiche gilt für die Inbox "00-Inbox", und die GUIDs beider Notebooks legt evernote-tickler in den Vairablen $tickler_guid bzw. $inbox_guid, sowie in der Logdatei ab, falls es sie findet. Falls nicht, brechen die Zeilen 91 und 95 das Programm mit einem Fehler ab, denn eine Verarbeitung in einem Account ohne entsprechend angelegte Ordner wäre sinnlos.

Listing 1: evernote-tickler

    001 #!/usr/local/bin/perl -w
    002 use strict;
    003 
    004 BEGIN {
    005     use FindBin qw($Bin);
    006     chdir $Bin;
    007 }
    008 
    009 use local::lib;
    010 use Thrift;
    011 use Thrift::HttpClient;
    012 use Thrift::BinaryProtocol;
    013 
    014 use lib 'gen-perl';
    015 use EDAMUserStore::Constants;
    016 use EDAMUserStore::UserStore;
    017 use EDAMNoteStore::NoteStore;
    018 use EDAMNoteStore::Types;
    019 use EDAMErrors::Types;
    020 use EDAMTypes::Types;
    021 use DateTime;
    022 use Log::Log4perl qw(:easy);
    023 
    024 my( $home ) = glob "~";
    025 
    026 Log::Log4perl->easy_init( { 
    027     level => $DEBUG, category => "main",
    028     file => 
    029    ">>$home/data/evernote-tickler.log" } );
    030 
    031 my $username        = "my-user";
    032 my $password        = "my-passwd";
    033 my $consumer_key    = "perlsnapshot";
    034 my $consumer_secret = "my-consumer-secret";
    035 
    036 my $evernote_host = "evernote.com";
    037 my $user_store_uri =
    038     "https://$evernote_host/edam/user";
    039 my $note_store_uri_base =
    040     "https://$evernote_host/edam/note/";
    041 
    042 my $http_client =
    043   Thrift::HttpClient->new($user_store_uri);
    044 my $protocol = Thrift::BinaryProtocol->new(
    045   $http_client);
    046 
    047 my $client =
    048   EDAMUserStore::UserStoreClient->new(
    049   $protocol);
    050 
    051 my $result =
    052   $client->authenticate( $username,
    053   $password, $consumer_key,
    054   $consumer_secret );
    055 
    056 my $user = $result->user();
    057 
    058 my $note_store_uri =
    059   $note_store_uri_base . $user->shardId();
    060 
    061 my $note_store_client =
    062   Thrift::HttpClient->new($note_store_uri);
    063 
    064 my $note_store_protocol =
    065   Thrift::BinaryProtocol->new(
    066     $note_store_client);
    067 
    068 my $note_store =
    069   EDAMNoteStore::NoteStoreClient->new(
    070     $note_store_protocol);
    071 
    072 my $notebooks =
    073   $note_store->listNotebooks(
    074     $result->authenticationToken() );
    075 
    076 my $tickler_guid;
    077 my $inbox_guid;
    078 
    079 for my $notebook (@$notebooks) {
    080   if ( $notebook->name() eq "01-Tickler" ){
    081     $tickler_guid = $notebook->guid();
    082     DEBUG "Found Tickler notebook";
    083   }
    084   if ( $notebook->name() eq "00-Inbox" ) {
    085     $inbox_guid = $notebook->guid();
    086     DEBUG "Found Inbox notebook";
    087   }
    088 }
    089 
    090 if ( !defined $tickler_guid ) {
    091   die "No Tickler notebook found";
    092 }
    093 
    094 if ( !defined $inbox_guid ) {
    095   die "No Inbox notebook found";
    096 }
    097 
    098 my $filter = 
    099     EDAMNoteStore::NoteFilter->new();
    100 $filter->notebookGuid( $tickler_guid );
    101 
    102 my $note_list = $note_store->findNotes( 
    103     $result->authenticationToken(), 
    104         $filter, 0, 1000 );
    105 
    106 my $tomorrow = DateTime->today(
    107   time_zone => "local" )->add( days => 1 );
    108 my $tomorrow_date_match = $tomorrow->ymd();
    109 
    110 for my $note ( 
    111     @{ $note_list->{ notes } } ) {
    112   my $title = $note->title();
    113 
    114   my( $date_in_title ) = 
    115       ( $title =~ /^(\S+)/ );
    116 
    117   DEBUG "Check if $tomorrow_date_match ",
    118         "matches '$date_in_title'";
    119 
    120   if( $tomorrow_date_match =~ 
    121       /^$date_in_title/ ) {
    122 
    123     DEBUG "$title matches. Move to Inbox.";
    124 
    125     my $worked = $note_store->copyNote(
    126         $result->authenticationToken(),
    127         $note->guid(), $inbox_guid );
    128 
    129     die "copy note failed ($!)" if 
    130       !defined $worked;
    131 
    132     DEBUG "Deleting note in Tickler file";
    133 
    134     $note_store->deleteNote( 
    135         $result->authenticationToken(),
    136         $note->guid() );
    137     }
    138 }

Die Evernote-API bietet nun keine Verzeichnisfunktion eines vorgegebenen Notebooks, sondern besteht auf einer Methode findNotes() die in allen Notebooks nach Notes sucht. Ein Filter vom Typ EDAMNoteStore::NoteFilter mit dem Parameter notebookGuid beschränkt die Suche allerdings auf ein Notebook mit der angegebenen GUID.

Der zweite Parameter für findNotes() gibt einen Offset an, mit dem sich ein Paging von gefundenen Notes einrichten lässt. Im vorliegenden Fall wünscht das Skript allerdings die vollständige Ergebnisliste und beschränkt diese mit dem dritten Parameter lediglich auf 1.000, was selbst für längere Ticklerlisten ausreichen dürfte.

Zeile 106 berechnet mit dem CPAN-Modul DateTime das morgige Datum, in dem es zum heutigen Datum (today()) einen einzelnen Tag hinzuaddiert. Die Methode ymd() wandelt das resultierende DateTime-Objekt anschließend in einen String im Format "YYYY-MM-DD" um. Der reguläre Ausdruck in Zeile 115 schneidet aus der Betreffzeile (title()) der Note das Datum aus und legt es in der Variablen $date_in_title ab.

Die if-Bedingung in Zeile 120 prüft, ob das Betreffs-Datum ganz oder teilweise mit dem morgigen Datum übereinstimmt. Sowohl eine Monatsangabe (YYYY-MM) als auch ein Tagesdatum (YYYY-MM-DD) fördern so Treffer zutage. Evernotes Web-API bietet keinen move-Befehl, also kopiert Zeile 125 die Tickler-Notiz in die Inbox des Users, falls das Datum stimmt. Die im Tickler-Notebook verbliebene Kopie löscht die Methode deleteNote() anschließend in Zeile 134.

Abbildung 5: Der Cronjob hat einen Tickler-Eintrag für den nächsten Tag gefunden und schiebt ihn in die Inbox des Users.

Zuverlässiger Cron

Ein Eintrag in der Cron-Datei im Format

    00 16 * * * /pfad/evernote-tickler

sorgt dafür, dass der Tickler jeden Tag um vier Uhr nachmittags startet und der User für morgen anberaumte Aufgaben in seiner Inbox sieht. Ist die Zeit für ein Projekt trotzdem noch nicht reif, korrigiert der User schlicht das Datum und schiebt die Notiz zurück in das Tickler-Notebook. Handelt es sich um zeitkritische Angelegenheiten wie ein Meeting, sucht der User dafür nun einen Termin in einer Kalenderapplikation. Kann er sogar den nächsten Schritt zur Bewältigung der Aufgabe erledigen, tut er dies nach GTD entweder sofort, falls es weniger als 2 Minuten dauert, oder erzeugt eine neue Notiz in einem Notebook, das alle brandheißen Projekte führt, und von denen der kampfbereite Alltagskrieger je nach Stimmung, Energielevel oder Kontext eines zur sofortigen Bearbeitung auswählen kann.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2012/04/Perl

[2]

Michael Schilli, "Zettels Trauma", Linux-Magazin 01/2012, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2012/01/Perl-Snapshot

[3]

David Allen, "Getting Things Done: The Art of Stress-Free Productivity", http://www.amazon.com/dp/0142000280

[4]

"Evernote", evernote.com, Web-Applikation und Apps für Mac/Windows, sowie Android/iPad/iPhone.

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.