2018年以降の記事はGitHub Pagesに移行しました

EvernoteのAPIをRubyから叩きたい

はじめに

この記事はRuby Advent Calendar jp: 2011 : ATNDの27日目の記事です。
26日目はid:tackunさんのSinatraを使って、RESTFulなWeb-APIを作ってみたい - tackun noteでした。
28日目はr7kamuraさんのRubyでGPUを使おう - ✘╹◡╹✘です。


今日はターミナル(or コマンドプロンプト)経由でEvernoteAPIを叩き、自分のノートブックを取得してみようとしてみます。ソースを書く前にAPIの申請からやっていきます。

APIの申請…の前に

developer用サイトからAPI SDKを入手。


上記のリンクを叩くとzipがDLできるので、これを解凍しruby/lib以下のファイルを用意しておく。

APIの申請

同じページを下へスクロールしていくとRequest an API Keyという箇所があるので以下の項目を入力する。

Evernote username Evernoteのユーザ名
Your Name 氏名
Your Email メールアドレス
Organization 組織や団体、だが、個人で使う場合は氏名でよい
Web or Client 今回は自分で使う用なのでClient Keysを選択(既に持っているので下の画像ではWebを選択している)
Application Details どうやってAPIを使いたいか

つたない英語でDetailsを書く。以前は審査に時間がかかったらしいが今はSUBMITすればすぐKeyが発行されるみたい。

WebApplicationを選んだ場合。

Clientを選んだ場合。

これでConsumer KeyとConsumer Secretをもらえた。ただし、この状態では砂場(sandbox.evernote.com)でしか使えない。

上記画像のNext Steps 2.によるとCreate an Evernote Accountで砂場用アカウントを作成し、砂場で開発をしてくださいという事でいきなり本家(www.evernote.com)で動かせないようになっている。

Activate

今回のエントリではやらないが、本家で動かす時はActivateサイト(Next Steps 3.のlet us knowリンクから行ける)でActivateしてもらう必要がある。(ここは昔の事なので少し曖昧)

Email メールアドレス
API Consumer Key 登録時にもらったConsumer Key
Additional Info その他何かあれば?(何か書いたっけ…?)

ソース

GitHubに。 Page not found · GitHub
ここから実際に取得していきます。

パス追加

プロジェクト内にあるライブラリ(さっき落としたAPI SDK)を読み込む。

dir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.push("#{dir}/lib/ruby")
$LOAD_PATH.push("#{dir}/lib/ruby/Evernote/EDAM")

require 'thrift/types'
(略)
require 'Evernote/EDAM/user_store'
(略)

File.expand_path(File.dirname(__FILE__))は出力するとそれぞれこのようになる。

> puts __FILE__
#=> ./myEvernote.rb
> puts File.dirname(__FILE__)
#=> .
> puts File.expand_path(File.dirname(__FILE__))
#=> C:/project/evernotexxx

プロジェクトディレクトリを指定できる。そして、

$LOAD_PATH.push("#{dir}/lib/ruby")

で組み込み変数$LOAD_PATH*1にプロジェクト内のlib/rubyを含めている。これで読み込める。

初期化

流れ
  1. パスワードなどの情報を取得する
  2. UserStoreを作成し、認証する
  3. NoteStoreを作成し、Evernoteのノートをいじれるようにする
パスワードなどの情報を取得する
  def initialize()
    @core = Pit.get("evernote", :require => {
      "userName" => "your evernote userName.",
      "password" => "your evernote password.",
      "consumerKey" => "your evernote consumerKey.",
      "consumerSecret" => "your evernote consumerSecret.",
    })
    
    host = "sandbox.evernote.com"
    userStoreUrl = "https://#{host}/edam/user"
    userStoreTransport = Thrift::HTTPClientTransport.new(userStoreUrl)
    userStoreProtocol = Thrift::BinaryProtocol.new(userStoreTransport)
    userStore = Evernote::EDAM::UserStore::UserStore::Client.new(userStoreProtocol)

初期化時にまず

  • userName(登録時の)
  • password(登録時の)
  • consumer_key(Evernoteからもらったもの)
  • consumer_secret(Evernoteからもらったもの)

を取得する。ソースにべた書きしてもいいのだが、パスワードとかはGitHubにあげる時に消しておきたいのでpit
を使って情報を外出しする事に。

pitを使う事で%USERPROFILE%\.pit\下にyaml形式で情報を保存しておくことができる。

UserStore作成

次にUserStoreを作成する。UserStoreはとりあえず、ユーザの情報をほげほげするものという認識。hostはテスト環境なのでsandbox.evernote.comを指定している。

認証
  def auth(userStore)
    # バージョンチェック
    versionOK = userStore.checkVersion("MyEvernote",
      Evernote::EDAM::UserStore::EDAM_VERSION_MAJOR,
      Evernote::EDAM::UserStore::EDAM_VERSION_MINOR)
#      puts "Is my EDAM protocol version up to date? #{versionOK}"
    if (!versionOK) then
      exit(1)
    end
    begin
      auth = userStore.authenticate(
        @core["userName"],
        @core["password"],
        @core["consumerKey"],
        @core["consumerSecret"])
#        puts "Auth Success: #{auth.user.username}"
      return auth
    rescue Evernote::EDAM::Error::EDAMUserException => ex
      parameter = ex.parameter
      errorCode = ex.errorCode
      errorText = Evernote::EDAM::Error::EDAMErrorCode::VALUE_MAP[errorCode]
      puts "Auth Error: #{errorText}(ErrorCode: #{errorCode}), Parameter: #{parameter}"
      exit
    end
  end

まずは、checkVersionメソッド(後述)の呼び出し。*2

bool UserStore.checkVersion(clientname, edamVersionMajor, edamVersionMinor)

チェックが終わったら次はauthenticateでユーザの認証を行う。

AuthenticationResult UserStore.authenticate(userName, password, consumer_key, consumer_secret)

認証が成功したらAuthenticationResultオブジェクトが返ってくる。ユーザ情報なんかも返ってくるが、この中のauthenticationTokenが、この先ノートを新規作成したり更新したりする時に必ず必要になるので控えておく。Tokenを使っている例は下記の通り。

# ノートブック作成。第一引数がauthenticationToken。
Types.Notebook createNotebook(string authenticationToken, Types.Notebook notebook)
# ノートブック更新。第一引数が以下同文。
i32 updateNotebook(string authenticationToken, Types.Notebook notebook)
# ノートブック取得。略。
Types.Notebook getNotebook(string authenticationToken, Types.Guid guid)
NoteStore作成
    noteStoreUrlBase = "https://#{host}/edam/note/"
    noteStoreUrl = noteStoreUrlBase + @authentication.user.shardId
    noteStoreTransport = Thrift::HTTPClientTransport.new(noteStoreUrl)
    noteStoreProtocol = Thrift::BinaryProtocol.new(noteStoreTransport)
    @noteStore = Evernote::EDAM::NoteStore::NoteStore::Client.new(noteStoreProtocol)

で、最後にNoteStoreを作成する。shardIdを指定する必要がある。shardIdは何かっていうと…。

The name of the virtual server that manages the state of this user. This value is used internally to determine which system should service requests about this user's data. It is also used to construct the appropriate URL to make requests from the NoteStore.

ユーザ状態を管理する仮想サーバ名?

ノートブックを取得していく。

準備はできたのでノートブックを取得していく。今のEvernote(砂場)の構造はこう。

それに対して全ノートを取得してくるとこんな感じ。

@e = MyEvernote.new()
# 全ノートブックを取得
pp @e.getNotebooks()
  # 全ノートブックを取得する
  def getNotebooks()
    @noteStore.listNotebooks(@token)
  end

全ノートブックはNoteStore.listNotebooks(authenticationToken)で取得する事ができる。

[<Evernote::EDAM::Type::Notebook guid:"2d8ec8b5-5706-434d-a1dc-4ea0c6ba1993", name:"Wait", updateSequenceNum:745, defaultNotebook:false, serviceCreated:1312818684000, serviceUpdated:1324051378000>,
 <Evernote::EDAM::Type::Notebook guid:"33880e53-4c9f-4104-a6e6-777ed1e3cef2", name:"Sandbox", updateSequenceNum:871, defaultNotebook:true, serviceCreated:1309536176000, serviceUpdated:1324051378000>,
 <Evernote::EDAM::Type::Notebook guid:"71cdd6f9-5070-4508-bc80-a3f835a61a55", name:"UpDeleteNotebook", updateSequenceNum:1468, defaultNotebook:false, serviceCreated:1324099155000, serviceUpdated:1324099155000>,
 <Evernote::EDAM::Type::Notebook guid:"450b52e6-2daa-4b04-9012-4623a8e12ef5", name:"TestNotebook", updateSequenceNum:1469, defaultNotebook:false, serviceCreated:1324099138000, serviceUpdated:1324099166000>]

おお、取れた!

nameは文字通りノートブック名。defaultNoteBookはデフォルトの設定true|false。以下略。guid(globally unique identifier)は、ノートブック、ノート、タグなどに対して振られる一意のID。フォーマットはこんな感じ。*3

^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$

GUIDがわかれば、特定のノートブックを引っ張ってくる事もできる。例えばGUID: 450b52e6-2daa-4b04-9012-4623a8e12ef5がTestNotebookなので…。

    @e = MyEvernote.new()
    @NotebookGuid = '450b52e6-2daa-4b04-9012-4623a8e12ef5'
    # 特定のノートブックを取得
    pp @e.getNotebook(@NotebookGuid).name
    pp @e.getNotebook(@NotebookGuid)
  def getNotebook(key)
    if isGuid(key) then
      # GUIDから検索
      return @noteStore.getNotebook(@token, key)
    end
  end
"TestNotebook"
<Evernote::EDAM::Type::Notebook guid:"450b52e6-2daa-4b04-9012-4623a8e12ef5", name:"TestNotebook", updateSequenceNum:1469, defaultNotebook:false, serviceCreated:1324099138000, serviceUpdated:1324099166000>

TestNotebookが取れた!

さらにfilterをかける事でノートブック内から特定のノートを取得できる。

@e = MyEvernote.new()
@NotebookGuid = '450b52e6-2daa-4b04-9012-4623a8e12ef5'
# ノートブック内のノートを取得
pp @e.getNote(@NotebookGuid)
  def getNote(notebookGuid, count=100)
    filter = Evernote::EDAM::NoteStore::NoteFilter.new
    filter.notebookGuid = notebookGuid
    @noteStore.findNotes(@token, filter, 0, count)
  end
<Evernote::EDAM::NoteStore::NoteList startIndex:0, totalNotes:2, 
  notes:[
    <Evernote::EDAM::Type::Note guid:"fa05f909-97d3-4504-8015-125fb026c8be", 
      title:"Images", contentHash:6dd62d0a9b187cbea4350b1ada8fda74, contentLength:1200,
      created:1324099541000, updated:1324099550000, active:true, updateSequenceNum:1473,
      notebookGuid:"450b52e6-2daa-4b04-9012-4623a8e12ef5",
      resources:[<Evernote::EDAM::Type::Resource guid:"536abe91-3ad3-48f6-8add-9fbcad7b95ab", 
      noteGuid:"fa05f909-97d3-4504-8015-125fb026c8be", 
      data:<Evernote::EDAM::Type::Data bodyHash:203b77ab9cb177e5406340445b64e78c, size:2044>, 
      mime:"image/jpeg", width:64, height:64, active:true, 
      recognition:<Evernote::EDAM::Type::Data bodyHash:68b3bd35f22b01ac57e55e7a7974ed8d, size:533>, 
      attributes:<Evernote::EDAM::Type::ResourceAttributes >, updateSequenceNum:1476>, 
      <Evernote::EDAM::Type::Resource guid:"8c329667-4c20-4e0a-a291-e437baf99f2d", 
      noteGuid:"fa05f909-97d3-4504-8015-125fb026c8be", 
      data:<Evernote::EDAM::Type::Data bodyHash:b1bdcdc515d937fb3e16a1e654abfffb, size:453>,
      mime:"image/png", width:14, height:13, active:true, 
      attributes:<Evernote::EDAM::Type::ResourceAttributes >, updateSequenceNum:1474>], 
      attributes:<Evernote::EDAM::Type::NoteAttributes sourceURL:"http://google.com">>,
    <Evernote::EDAM::Type::Note guid:"19c20dac-6d90-4e8c-bbf9-f11450416c48", title:"TestNote", 
      contentHash:401df83bfdb6fd1f152c4d226fa1d0b6, contentLength:189, created:1324099587000, 
      updated:1324099587000, active:true, updateSequenceNum:1477, 
      notebookGuid:"450b52e6-2daa-4b04-9012-4623a8e12ef5", 
      attributes:<Evernote::EDAM::Type::NoteAttributes >>], updateCount:1477>

終わりに

という事で、EvernoteAPI登録からノート取得までをする事ができました。もっと画像をアップしたりタグ埋め込んだりもできるのですが、それはこれから学んでいくという事で。

参考サイト、兼自分で調べる用

UserStoreとは

The UserStore service is primarily used by EDAM clients to establish authentication via username and password over a trusted connection (e.g. SSL). A client's first call to this interface should be checkVersion() to ensure that the client's software is up to date.

ユーザ名やパスワードを信頼された接続上で認証するためEDAMクライアントで利用される。はじめにcheckVersion()メソッドを呼び、クライアントのソフトのバージョンが最新である事を保障すべき。

NoteStoreとは

The NoteStore service is used by EDAM clients to exchange information about the collection of notes in an account. This is primarily used for synchronization, but could also be used by a "thin" client without a full local cache.

All functions take an "authenticationToken" parameter, which is the value returned by the UserStore which permits access to the account. This parameter is mandatory for all functions.

アカウントのノート収集情報を交換、同期するためEDAMクライアントで利用される。(ローカルキャッシュのない新クライアントでも使われた)全ての機能はauthenticationTokenのパラメータを使用する。これはアクセス認証が通ったアカウントのUserStoreの値を使用する。

*1:requireがファイルを読み込むときに検索するディレクトリの名前を含む配列 - たのしいRuby 第三版 P373

*2:キャメルケースを多用してる…? と思ったけど、xxx_yyyZzzzみたいなメソッドもあるなあ。どっちにあわせれば…

*3:Evernote::EDAM::Limits::EDAM_GUID_REGEX