Mit Evernotes Web-API ist die Implementierung des Ticklers fast ein Kinderspiel. Der Snapshot in 1/2012 [3] 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 Produktionsserver zu beantragen.
Listing 1 wechselt anfangs im »BEGIN« -Block in das Verzeichnis »$Bin« , in dem das Skript liegt. Dies stellt sicher, dass es die später eingeholten auto-generierten Thrift-Module im Unterverzeichnis »gen-perl« auch dann noch findet, falls es als Cronjob startet. Das CPAN-Modul »local::lib« sorgt dafür, dass es auch im Homeverzeichnis des Users installierte CPAN-Module aufspüren kann. 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 diese Methode sehr hilfreich, um die Ursachen etwaiger Fehlfunktionen aufzuspüren und Bugs auszumerzen. Neben dem Loglevel »$DEBUG« legt es zusätzlich die Kategorie »”main”« fest, die dafür sorgt, dass 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.
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 }

Abbildung 4: Der täglich startende Cronjob hat einen Tickler-Eintrag für den nächsten Tag gefunden und schiebt ihn pflichtgemäß in die Inbox des Users.
Operation am offenen Herzen
Zeile 52 authentisiert den User auf dem Evernote-Webserver. Stimmen das Passwort und der zugehörige Consumer-Key, erlaubt dieser Server 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 tunlichst 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« . Zudem legt »evernote-tickler« die GUIDs beider Notebooks in den Variablen »$tickler_guid« beziehungsweise »$inbox_guid« sowie in der Logdatei ab – falls es sie findet. Falls es nicht fündig wird, 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.
Filterung
Das Evernote-API bietet 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 1000, was aber selbst für die längeren Ticklerlisten sehr beschäftigter Nutzer noch ausreichen dürfte.
Zeile 106 berechnet mit dem CPAN-Modul »DateTime« das morgige Datum, indem es zum heutigen Datum (»today()« ) einfach einen einzelnen Tag hinzuaddiert. Die Methode »ymd()« wandelt das daraus resultierende »DateTime« -Objekt anschließend in einen String im Format YYYY-MM-DD um. Der reguläre Ausdruck in der 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 Betreff-Datum ganz oder teilweise mit dem morgigen Datum übereinstimmt. Sowohl eine Monatsangabe (YYYY-MM) als auch ein Tagesdatum (YYYY-MM-DD) fördert so Treffer zutage. Evernotes Web-API bietet von sich aus keinen Move-Befehl an, also kopiert Zeile 125 die Tickler-Notiz in die Inbox des Users, falls das Datum stimmt. Die Methode »deleteNote()« in Zeile 134 löscht anschließend die im Tickler-Notebook verbliebene Kopie.




