いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

C#でEverNote APIを触ってみた (EverNote, 1.17, Note, UserStore, NoteStore, Thrift, AuthenticationResult, AuthenticationToken)


EverNoteからAPIが提供されているのを最近知りまして早速使ってみました。
(実際は大分前から提供されていたみたいですねw)


まずは準備。
EverNote APIは利用する際に、まず申請が必要となります。
以下のページから申請します。


質問の内容のところに、どんな目的でAPIを使いたいか記述して送信します。
早かったら、1営業日くらいでキーとパスワードが届きます。


APIキーが届いたら、https://sandbox.evernote.com/ で新たにアカウントを
作成します。正規のEverNoteの方で利用しているアカウント名と同じ名前でも
作成できます。


後は、http://www.evernote.com/about/developer/api/ からAPI本体をダウンロードします。
APIの中には、各言語用のサンプルとライブラリが入っています。
API自体は、Apache Thriftを用いて作成されています。


Javaとかの場合は、最初からjarファイルとかが用意されているのですが
C#の場合は、dllを自分でコンパイルしないといけません・・・。
APIのフォルダの中の

src/csharp/Thrift/

の中にVisual Studioソリューションファイルが存在します。


これでさっさとコンパイルしようと思ったら、開けませんでした・・・・。
「有効なソリューションファイルではありません」ってエラーが出ます。


しかたないので、自分でVSソリューションを作って中のソースを
放り込んでコンパイル。これでやっとDLLが手に入りました。
(いくつか警告がでるのでちょこちょこ修正)



サンプルを見ながら、テスト処理を記述。
C#のサンプルはMonoで作成しているっぽいです。
C#のサンプルソースのヘッダ部分にもgmcsでコンパイルする例が記述されてあります。


で、やっと使い方ですが利用するにはあまり難しくないです。
以下、要点部分を貼り付けていきます。


まずは、バージョンチェック部分。
利用しているAPIのバージョンをチェックします。

var userName            = "xxxxx";
var password            = "xxxxx";
var consumerKey         = "xxxxx";
var consumerSecret      = "xxxxx";
var defaultNotebookName = "xxxxx";

var host         = @"https://sandbox.evernote.com";
var userStoreUrl = string.Format("{0}/edam/user", host);

TTransport       userStoreTransport = new THttpClient(userStoreUrl);
TProtocol        userStoreProtocol  = new TBinaryProtocol(userStoreTransport);
UserStore.Client userStore          = new UserStore.Client(userStoreProtocol);

bool versionOK = 
        userStore.checkVersion(
            "EverNote Api Version Check", 
            Evernote.EDAM.UserStore.Constants.EDAM_VERSION_MAJOR, 
            Evernote.EDAM.UserStore.Constants.EDAM_VERSION_MINOR
        );

Assert.IsTrue(versionOK);

API全体でよくあるパターンとして

Transportを取得して、次にProtocolを取得、最後に目的の情報を持つxxStoreを取得

するという流れになっています。
ノートの場合は、最終的にNoteStoreを取得してノード情報を処理します。



次に、接続(認証)します。
認証が失敗する場合は例外が発生します。

AuthenticationResult authResult = null;
try
{
    authResult = userStore.authenticate(userName, password, consumerKey, consumerSecret);
}
catch (EDAMUserException ex)
{
    Console.WriteLine("{0}, {1}", ex.ErrorCode, ex.Parameter);
    Assert.Fail();
}


認証が成功したら、後は必要な情報を集めて
ノートを操ります。まずはユーザ情報を取得。

User user = authResult.User;
Assert.AreEqual<string>(userName, user.Username);

String authToken    = authResult.AuthenticationToken;

上記で取得している認証トークン(AuthenticationToken)がすごく重要です。
何かの情報を取得する際に、これが必要となります。


いよいよノートの取得.

String noteStoreUrl = string.Format("{0}/edam/note/{1}", host, user.ShardId);

TTransport       noteStoreTransport = new THttpClient(noteStoreUrl);
TProtocol        noteStoreProtocol  = new TBinaryProtocol(noteStoreTransport);
NoteStore.Client noteStore          = new NoteStore.Client(noteStoreProtocol);

List<Notebook> notebooks = noteStore.listNotebooks(authToken);

user.ShardIdは、user.SharedIdじゃないのかと思いつつ・・・。


EverNoteには、必ず一つはノートブックが存在します。(Default Notebook)
何も設定していない場合でも、notebooksのカウントは1となります。


実際にノートを登録してみます。
サンプル丸ぱくりなので、画像ファイル付きでノートを新規登録します。

byte[] image   = (byte[]) new ImageConverter().ConvertTo(Properties.Resources.enlogo, typeof(byte[]));
byte[] hash    = new MD5CryptoServiceProvider().ComputeHash(image);
string hashHex = BitConverter.ToString(hash).Replace("-", "").ToLower();

Data data     = new Data();
data.Size     = image.Length;
data.BodyHash = hash;
data.Body     = image;

Resource resource = new Resource();
resource.Mime     = "image/png";
resource.Data     = data;

Note note         = new Note();
note.NotebookGuid = defaultNotebook.Guid;
note.Title        = "Test note from EverNoteApiTest";
note.Content      = GetContent(hashHex);
note.Created      = (long) (DateTime.Now - new DateTime(1970, 1, 1)).TotalMilliseconds;
note.Updated      = note.Created;
note.Active       = true;
note.Resources    = new List<Resource>();
note.Resources.Add(resource);

Note createdNote = noteStore.createNote(authToken, note);



private static string GetContent(string hashHex)
{
    return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                    "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml.dtd\">" +
                    "<en-note>Here's the Evernote logo:<br/>" +
                    "<en-media type=\"image/png\" hash=\"" + hashHex + "\"/>" +
                    "</en-note>";
}

流れとしては、

  1. 添付ファイルがある場合
    1. Dataインスタンスを作成
    2. 属性を設定 (データはバイト配列で設定)
    3. Resourceインスタンスを作成してDataを設定
  2. Noteインスタンスを作成
    1. 各属性を設定
    2. 本文はXML形式で設定する。
    3. 添付ファイルは、Resourcesプロパティにリストで設定する。
  3. 最後にnoteStore.createNoteで登録

となります。


次は更新。

Note n       = noteStore.getNote(authToken, createdNote.Guid, true, true, true, true);
n.Title      = "Update From EverNoteApiTest";
note.Updated = (long)(DateTime.Now - new DateTime(1970, 1, 1)).TotalMilliseconds;

noteStore.updateNote(authToken, n);

同じような感じです。ノートを取得して属性を変更。
ノートを取得する際には、トークンとノートのGUIDが必要になります。
最後にupdateNoteで更新します。



次に削除。

noteStore.deleteNote(authToken, createdNote.Guid);

削除されたノートは、Activeプロパティがfalseと設定されます。
またこの場合の削除は、物理削除ではなくてEverNote上の「ごみ箱」に移動されます。


で、最後に検索です。
検索は、NoteFilterを作成して検索します。

NoteFilter filter   = new NoteFilter();
filter.Words        = "EverNoteApiTest";
filter.NotebookGuid = defaultNotebook.Guid;
filter.Inactive     = false;

NoteCollectionCounts findNoteCount = noteStore.findNoteCounts(authToken, filter, false);
NoteList             findNotes     = noteStore.findNotes(authToken, filter, 0, 1000);

findNoteCountsで、合致したノートの数。
findNotesで、合致したノートを取得できます。


でも、私の環境で試してみたところ
NoteFilterのInactiveに明示的にfalse (削除されたノートを検索対象としない)と設定しているのに
検索すると削除されたノートまで含まれてきます。これバグなのかなぁ・・・使い方がわるいのか。
知っている方いらっしゃったら教えてください。 m(_ _)m
(とりあえず問い合わせてみます。)



最後に上記のソースの全体版です。

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Security.Cryptography;
using Evernote.EDAM.Error;
using Evernote.EDAM.NoteStore;
using Evernote.EDAM.Type;
using Evernote.EDAM.UserStore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Thrift.Protocol;
using Thrift.Transport;

namespace EverNoteApiTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            var userName            = "xxxxx";
            var password            = "xxxxx";
            var consumerKey         = "xxxxx";
            var consumerSecret      = "xxxxx";
            var defaultNotebookName = "xxxxx";

            var host         = @"https://sandbox.evernote.com";
            var userStoreUrl = string.Format("{0}/edam/user", host);

            TTransport       userStoreTransport = new THttpClient(userStoreUrl);
            TProtocol        userStoreProtocol  = new TBinaryProtocol(userStoreTransport);
            UserStore.Client userStore          = new UserStore.Client(userStoreProtocol);

            bool versionOK = 
                    userStore.checkVersion(
                        "EverNote Api Version Check", 
                        Evernote.EDAM.UserStore.Constants.EDAM_VERSION_MAJOR, 
                        Evernote.EDAM.UserStore.Constants.EDAM_VERSION_MINOR
                    );

            Assert.IsTrue(versionOK);

            AuthenticationResult authResult = null;
            try
            {
                authResult = userStore.authenticate(userName, password, consumerKey, consumerSecret);
            }
            catch (EDAMUserException ex)
            {
                Console.WriteLine("{0}, {1}", ex.ErrorCode, ex.Parameter);
                Assert.Fail();
            }

            User user = authResult.User;
            Assert.AreEqual<string>(userName, user.Username);

            String authToken    = authResult.AuthenticationToken;          
            String noteStoreUrl = string.Format("{0}/edam/note/{1}", host, user.ShardId);

            TTransport       noteStoreTransport = new THttpClient(noteStoreUrl);
            TProtocol        noteStoreProtocol  = new TBinaryProtocol(noteStoreTransport);
            NoteStore.Client noteStore          = new NoteStore.Client(noteStoreProtocol);

            List<Notebook> notebooks = noteStore.listNotebooks(authToken);
            Assert.AreEqual<int>(1, notebooks.Count);

            Notebook defaultNotebook = null;
            foreach (Notebook notebook in notebooks)
            {
                if (notebook.DefaultNotebook)
                {
                    defaultNotebook = notebook;
                }
            }

            Assert.IsNotNull(defaultNotebook);
            Assert.AreEqual<string>(defaultNotebookName, defaultNotebook.Name);

            byte[] image   = (byte[]) new ImageConverter().ConvertTo(Properties.Resources.enlogo, typeof(byte[]));
            byte[] hash    = new MD5CryptoServiceProvider().ComputeHash(image);
            string hashHex = BitConverter.ToString(hash).Replace("-", "").ToLower();

            Data data     = new Data();
            data.Size     = image.Length;
            data.BodyHash = hash;
            data.Body     = image;

            Resource resource = new Resource();
            resource.Mime     = "image/png";
            resource.Data     = data;

            Note note         = new Note();
            note.NotebookGuid = defaultNotebook.Guid;
            note.Title        = "Test note from EverNoteApiTest";
            note.Content      = GetContent(hashHex);
            note.Created      = (long) (DateTime.Now - new DateTime(1970, 1, 1)).TotalMilliseconds;
            note.Updated      = note.Created;
            note.Active       = true;
            note.Resources    = new List<Resource>();
            note.Resources.Add(resource);

            Note createdNote = noteStore.createNote(authToken, note);

            Assert.AreEqual<string>("Test note from EverNoteApiTest", createdNote.Title);
            Assert.IsTrue(createdNote.Active);

            var bodyHash = createdNote.Resources.First().Data.BodyHash;
            for (int i = 0; i < bodyHash.Length; i++)
            {
                Assert.AreEqual<byte>(hash[i], bodyHash[i]);
            }

            Note n       = noteStore.getNote(authToken, createdNote.Guid, true, true, true, true);
            n.Title      = "Update From EverNoteApiTest";
            note.Updated = (long)(DateTime.Now - new DateTime(1970, 1, 1)).TotalMilliseconds;

            noteStore.updateNote(authToken, n);

            n = noteStore.getNote(authToken, createdNote.Guid, true, true, true, true);
            Assert.AreEqual<string>("Update From EverNoteApiTest", n.Title);

            //NoteFilter filter   = new NoteFilter();
            //filter.Words        = "EverNoteApiTest";
            //filter.NotebookGuid = defaultNotebook.Guid;
            //filter.Inactive     = false;

            //NoteCollectionCounts findNoteCount = noteStore.findNoteCounts(authToken, filter, false);
            //Assert.AreEqual<int>(1, findNoteCount.NotebookCounts[defaultNotebook.Guid]);

            //NoteList findNotes = noteStore.findNotes(authToken, filter, 0, 1000);
            //Assert.AreEqual<int>(1, findNotes.Notes.Count);
            //Assert.AreEqual<string>(n.Guid, findNotes.Notes.First().Guid);

            noteStore.deleteNote(authToken, createdNote.Guid);

            n = noteStore.getNote(authToken, createdNote.Guid, true, true, true, true);
            Assert.IsFalse(n.Active);
        }        

        private static string GetContent(string hashHex)
        {
            return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                            "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml.dtd\">" +
                            "<en-note>Here's the Evernote logo:<br/>" +
                            "<en-media type=\"image/png\" hash=\"" + hashHex + "\"/>" +
                            "</en-note>";
        }
    }
}


こんな感じです。
思ってたよりも使いやすいAPIでした。まだまだ試していないAPIはいっぱいあるので
ちょこちょこ勉強してみます。(データの同期とか)


折角環境構築したので、設定したVisualStudioプロジェクトと
ビルドしたDLLを以下からダウンロードできるようにしています。
てっとり早く試してみたい方はどうぞ。
(プロジェクトはVisualStudio 2010形式ですが、ビルドターゲットは3.5にしてあります。)

================================
過去の記事については、以下のページからご参照下さい。