はてなダイアリーAtomPubとはてな記法

TODO: ampleeを使ってAPIをたたいてみる

はてなダイアリーAtomPubとは - はてなキーワード

本ドキュメントに関する注意事項

ドキュメントはてなダイアリーにおける Atom Publishing Protocol の仕様を解説するものです。主にはてなスタッフがその作成と更新を行っています。

変更履歴
Atom Publishing Protocol とは

Atom Publishing Protocol(以下 AtomPub) はウェブリソースを公開、編集するためのアプリケーションプロトコル仕様ですはてなダイアリーのAtomPubと通じて、開発者はてなダイアリー日記を参照、投稿、編集削除するようなオリジナルアプリケーションを作成することができます。

AtomPub について詳しくは http://www.ietf.org/rfc/rfc5023.txt (英語)などを参照してください。

はてなダイアリーにおけるAtomPub実装の概要

HTTP の GET/POST/PUT/DELETE を特定のURIに対してリクエストし、そのリクエストに規定のXML文書を加えて送信することでインタフェースが用意している操作を行うことができます。

AtomPubには特定の操作の対象の集合を表す「コレクション」と個々の操作の対象を表す「メンバ」があります。コレクションとメンバはそれぞれにURIを持ち、そのURIに対して操作を行います。例えば、はてなダイアリー日記エントリーのコレクションとメンバのURIは以下のようになります。

一部の操作はそのレスポンスとして規定のXML文書を返却します。また、下書きエントリーの公開操作を実現するためにAtomPubを拡張しています。

現時点でAPIサポートしている操作は以下です。

以下、はてなダイアリーAtomPubの詳細を解説します。

認証

はてなダイアリーAtomPubを利用するために、クライアントはWSSE認証を行う必要があります。

WSSE認証

AtomPubに良く用いられるWSSE認証が利用できます。WSSE認証の詳細に関しては http://www-128.ibm.com/developerworks/webservices/library/ws-secure/ (英語) を参照してください。ここではWSSE認証についての必要事項を簡単に解説します。

WSSE認証はHTTPのX-WSSEヘッダを用いて認証用文字列を送信する認証手段です。WSSE認証用文字列にはユーザー名とパスワードが含まれます。このとき、パスワードSHA1アルゴリズムによって暗号化されたダイジェストとして送信されるため、HTTP基本認証などに比べてセキュアな認証が可能です。

送信するX-WSSEヘッダのサンプルは以下のようになります。

X-WSSE: UsernameToken Username="hatena", PasswordDigest="ZCNaK2jrXr4+zsCaYK/YLUxImZU=", Nonce="Uh95NQlviNpJQR1MmML+zq6pFxE=", Created="2005-01-18T03:20:15Z"
Username
ユーザー名。(はてなアカウントid)
Nonce
HTTPリクエスト毎に生成したセキュリティトーク*1
Created
Nonceが作成された日時をISO-8601表記で記述したもの
PasswordDigest
Nonce, Created, パスワード(はてなアカウントパスワード)を文字列連結しSHA1アルゴリズムダイジェスト化して生成された文字列を、Base64エンコードした文字列

具体的な実装方法については、はてなフォトライフAtomAPIを参照してください。

文字コード

はてなダイアリーAtomPubでは文字コードとしてUTF-8を利用します。リクエストXMLレスポンスXML共にUTF-8として扱ってください。

はてなダイアリーの他の場所では文字コードとしてEUC-JPを利用していることが多いので、まちがえないよう注意してください。

サービス文書

はてなダイアリーAtomPubで操作できるコレクションの一覧を含むサービス文章を取得することができます。

http://d.hatena.ne.jp/はてなID/atom

リクエスト
GET /はてなID/atom
レスポンス
HTTP/1.1 200 OK
Content-Type: application/atomsvc+xml; charset=utf-8

<?xml version="1.0" encoding="utf-8"?>
<service xmlns="http://www.w3.org/2007/app">
<workspace>
<atom:title xmlns:atom="http://www.w3.org/2005/Atom">Hatena::Diary - はてなID</atom:title>
<collection href="http://d.hatena.ne.jp/はてなID/atom/draft">
<atom:title xmlns:atom="http://www.w3.org/2005/Atom">はてなIDさんの下書き</atom:title>
<accept>application/atom+xml;type=entry</accept>
</collection>
<collection href="http://d.hatena.ne.jp/はてなID/atom/blog">
<atom:title xmlns:atom="http://www.w3.org/2005/Atom">はてなIDさんの日記</atom:title>
<accept>application/atom+xml;type=entry</accept>
</collection>
</workspace>
</service>

ブログ コレクション

はてなダイアリー日記エントリーを操作するためのコレクションです。日記エントリーの投稿、取得、編集削除、一覧の取得を行うことができます。コレクションURI、およびメンバURIは以下になります。

日記エントリー一覧の取得

コレクションURIに対してGETをリクエストすることで、日記エントリー一覧を取得することができます。一度に20件のエントリーを取得できます。また、pageパラメータに数値を指定することで、20件目以降のエントリーを取得することもできます。

リクエスト
GET /はてなID/atom/blog
GET /はてなID/atom/blog?page=2
レスポンス

指定ページに対応した Atom Feed 文章が返却されます。ここでは省略します。

日記エントリーの投稿

コレクションURIに対してリクエストXML文章をPOSTすることで、日記エントリーを投稿することができます。

リクエスト

リクエストXML文章に必要なパラメータは以下です。

POST /はてなID/atom/blog

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://purl.org/atom/ns#">
<title>日記エントリータイトル</title>
<content type="text/plain">
日記エントリー本文
- はてな
- 記法
</content>
<updated>2008-01-01T00:00:00+09:00</updated>
</entry>

レスポンス
HTTP/1.1 201 CREATED
Content-Type: application/atom+xmlcharset=type=entry
Location: http://d.hatena.ne.jp/はてなID/atom/blog/20080101/XXXXXXXXXX

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
<id>tag:d.hatena.ne.jp,2008:diary-はてなID-20080101-XXXXXXXXXX</id>
<link rel="edit" href="http://d.hatena.ne.jp/はてなID/atom/blog/20080101/XXXXXXXXXX"/>
<link rel="alternate" type="text/html" href="http://d.hatena.ne.jp/はてなID/20080101/XXXXXXXXXX"/>
<author>
<name>はてなID</name>
</author>
<title>日記エントリータイトル</title>
<updated>2008-01-01T00:00:00+09:00</updated>
<published>2008-01-01T00:00:00+09:00</published>
<app:edited xmlns:app="http://www.w3.org/2007/app">2008-01-01T00:00:00+09:00</app:edited>
<content type="text/html">
<div class="section">
<p>'日記エントリー本文'</p>
<ul>
<li> はてな</li>
<li> 記法</li>
</ul>

</div>
</content>
</entry>

日記エントリーの取得

メンバURIに対してGETをリクエストすることで、日記エントリーを取得することができます。

リクエスト
GET /はてなID/atom/blog/20080101/XXXXXXXXXX
レスポンス
<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
<id>tag:d.hatena.ne.jp,2008:diary-はてなID-20080101-XXXXXXXXXX</id>
<link rel="edit" href="http://d.hatena.ne.jp/はてなID/atom/blog/20080101/XXXXXXXXXX"/>
<link rel="alternate" type="text/html" href="http://d.hatena.ne.jp/はてなID/20080101/XXXXXXXXXX"/>
<author>
<name>はてなID</name>
</author>
<title>日記エントリータイトル</title>
<updated>2008-01-01T00:00:00+09:00</updated>
<published>2008-01-01T00:00:00+09:00</published>
<app:edited xmlns:app="http://www.w3.org/2007/app">2008-01-01T00:00:00+09:00</app:edited>
<content type="text/html">
<div class="section">
<p>'日記エントリー本文'</p>
<ul>
<li> はてな</li>
<li> 記法</li>
</ul>

</div>
</content>
<hatena:syntax xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#">'日記エントリー本文'

</entry>

日記エントリー編集

メンバURIに対してリクエストXML文章をPUTすることで、日記エントリー編集することができます。投稿された日記エントリーの日時は投稿を行った日時になります。

リクエスト

リクエストXML文章に必要なパラメータは以下です。

PUT /はてなID/atom/blog/20080101/XXXXXXXXXX

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://purl.org/atom/ns#">
<title>あたらしい日記エントリータイトル</title>
<content type="text/plain">
あたらしい日記エントリー本文
- はてな
- 記法
</content>
<updated>2008-01-01T00:00:00+09:00</updated>
</entry>

レスポンス
HTTP/1.1 200 OK
Content-Type: application/atom+xmlcharset=type=entry

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
<id>tag:d.hatena.ne.jp,2008:diary-はてなID-20080101-XXXXXXXXXX</id>
<link rel="edit" href="http://d.hatena.ne.jp/はてなID/atom/blog/20080101/XXXXXXXXXX"/>
<link rel="alternate" type="text/html" href="http://d.hatena.ne.jp/はてなID/20080101/XXXXXXXXXX"/>
<author>
<name>はてなID</name>
</author>
<title>あたらしい日記エントリータイトル</title>
<updated>2008-01-01T00:00:00+09:00</updated>
<published>2008-01-01T00:00:00+09:00</published>
<app:edited xmlns:app="http://www.w3.org/2007/app">2008-01-01T00:00:00+09:00</app:edited>
<content type="text/html">
<div class="section">
<p>'あたらしい日記エントリー本文'</p>
<ul>
<li> はてな</li>
<li> 記法</li>
</ul>

</div>
</content>
</entry>

日記エントリー削除

メンバURIに対してDELETEをリクエストすることで、日記エントリー削除することができます。

リクエスト
DELETE /はてなID/atom/blog/20080101/XXXXXXXX
レスポンス
HTTP/1.1 200 OK
下書き コレクション

はてなダイアリーの下書きを操作するためのコレクションです。下書きエントリーの投稿、取得、編集削除、一覧の取得、および日記エントリーとしての公開を行うことができます。コレクションURI、およびメンバURIは以下になります。

下書きコレクションの操作は基本的にブログコレクションと同等なため詳しい説明は省略します。返却されるAtom Entryのcontent要素に含まれるのがはてな記法になっている部分が異なっています。

下書きコレクションでは、AtomPub を拡張し、下書きを公開する操作を提供しています。

下書きエントリー日記への公開

メンバURIに対してX-HATENA-PUBLISHヘッダを付与してPUTすることで、メンバURIで指定した下書きをもとに日記エントリーを投稿することができます。投稿された日記エントリーの日時は投稿を行った日時になります。

リクエスト
  • X-HATENA-PUBLISH ヘッダに1を含めます。
PUT /はてなID/atom/blog/20080101/XXXXXXXX
X-HATENA-PUBLISH: 1
レスポンス

実際のレスポンスの例はブログコレクションの日記エントリーの投稿を参照してください。

はてなダイアリー AtomPub を利用したプログラムの例
Perlを用いた例

CPANモジュールXML::Atom::ClientはAtomPubクライアントを実装するための、WSSE認証やリクエストレスポンスに必要なXML文書の組み立てなどを抽象化したモジュールです。

XML::Atom::Clientを用いて、はてなダイアリー日記を投稿するサンプルコードは以下です。

#!/usr/bin/env perl
use strict;
use warnings;

use XML::Atom::Entry;
use XML::Atom::Client;

my $username = shift or die "need username";
my $password = shift;

my $PostURI = "http://d.hatena.ne.jp/$username/atom/blog";

my $client = XML::Atom::Client->new;
$client->username($username);
$client->password($password);

my $entry = XML::Atom::Entry->new;
$entry->title('テスト日記だよー');
$entry->content(<<'ENDCONTENT');
わーい、はてな記法もかけるぞー

  • こんな
  • ふうに

ENDCONTENT

my $EditURI = $client->createEntry($PostURI, $entry)
or die $client->errstr;

print $EditURI;

XML::Atom::Client はWSSE認証を抽象化しているため、username/passwordメソッドでそれぞれをセットするだけで認証を通過できます。また、XML文書の組み立てはXML::Atom::Entryインスタンスを生成して行い、それを最後にXML::Atom::Clientインスタンスに渡せば完了です。

このスクリプトコマンドラインから、

$ perl atompost.pl はてなID password

とすることで実行できます。

Rubyを用いた例

Ruby から API を利用するには、atomutil ライブラリを利用することによって投稿することが可能です。

まず、atomutil ライブラリ拡張し、X-Hatena-Publish に対応させ、下書きからの投稿を行えるようにします。

require 'rubygems'
require 'atomutil'

module Atompub
class HatenaClient < Client
def publish_entry(uri)
@hatena_publish = true
update_resource(uri, ' ', Atom::MediaType::ENTRY.to_s)
ensure
@hatena_publish = false
end

private
def set_common_info(req)
req['X-Hatena-Publish'] = 1 if @hatena_publish
super(req)
end
end
end

エントリーの新規作成のサンプルコードは以下です。

auth = Atompub::Auth::Wsse.new :username => 'はてなID', :password => 'hatena_password'
client = Atompub::HatenaClient.new :auth => auth
service = client.get_service 'http://d.hatena.ne.jp/%s/atom' % config[:username]
collection_uri = service.workspace.collections[1].href

entry = Atom::Entry.new(
:title => 'My Entry Title',
:updated => Time.now
)

entry.content = <<EOF
エントリー本文だよ
EOF

puts client.create_entry collection_uri, entry

エントリー下書き投稿は、collection_uri を変更するだけです。

collection_uri = service.workspace.collections[0].href
puts client.create_entry collection_uri, entry # 下書き投稿

下書き投稿からパブリッシュするには、HatenaClient#publish_entry を使います

client.publish_entry entry.edit_link

ricollab Web Tech Blog ≫ Blog Archive ≫ はてなダイアリー AtomPub レビュー: その1 実装編


はてなさんがダイアリーAtomPubインターフェースをリリースしていました。

私は AtomPub が大好きなので、少しだけ試してみました。簡単にレビューを書こうと思ってエントリを起したのですが、意外と長くなりそうなので3部構成でお送りします。まずは実装編です。

認証

とりあえず普通のGETリクエストをサービス文書に送ってみます。http://d.hatena.ne.jp/{hatena-id}/atom がサービス文書の URI です。

GET http://d.hatena.ne.jp/yohei/atom HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 401 Unauthorized
Date: Fri, 19 Sep 2008 07:00:10 GMT
Server: Apache/2.2.3 (CentOS)
WWW-Authenticate: WSSE profile="UsernameToken"
Content-Length: 16
Content-Type: text/plain;charset=utf-8
Set-Cookie: b=xxx; path=/; expires=Thu, 14-Sep-28 05:05:06 GMT; domain=.hatena.ne.jp
Vary: Accept-Encoding

401 Unauthorized

401 が返ってきました。WWW-Authenticate ヘッダによれば、WSSE 認証が必要なようです。

レスポンスヘッダで一つ気になるのが、なぜか Set-Cookie ヘッダがあることです。Cookie 認証のなごりでしょうか…。セキュリティの観点からもこのヘッダはなくした方がよさそうです。ちなみに expires の年が2桁で 28 なのもちょっと困りますね。UIの方は普通の Cookie を返すので、何かのバグなんでしょうか。以下の例でも全て Set-Cookie ヘッダが返ってくるのですが、省略しています。

サービス文書

ということで、X-WSSE ヘッダを加えてサービス文書を GET してみます。以下、全てのリクエストで認証が必要ですが、長いので、次以降の例ではこのヘッダは省略します。

GET http://d.hatena.ne.jp/yohei/atom HTTP/1.1
Accept: */*
X-Wsse: UsernameToken Username="yohei", PasswordDigest="foo", Nonce="bar", Created="2008-09-19T22:53:55+09:00"
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 05:05:06 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 676
Content-Type: application/atomsvc+xml;charset=utf-8
Vary: Accept-Encoding

<?xml version="1.0" encoding="utf-8"?>
<service xmlns="http://www.w3.org/2007/app">
<workspace>
<atom:title xmlns:atom="http://www.w3.org/2005/Atom">Hatena::Diary - yohei</atom:title>
<collection href="http://d.hatena.ne.jp/yohei/atom/draft">
<atom:title xmlns:atom="http://www.w3.org/2005/Atom">yoheiさんの下書き</atom:title>
<accept>application/atom+xml;type=entry</accept>
</collection>
<collection href="http://d.hatena.ne.jp/yohei/atom/blog">
<atom:title xmlns:atom="http://www.w3.org/2005/Atom">yoheiさんの日記</atom:title>
<accept>application/atom+xml;type=entry</accept>
</collection>
</workspace>
</service>

サービス文書が取れました。はてなダイアリーAtomPub では二つのコレクションリソースが用意されているのがわかります。draftコレクション(http://d.hatena.ne.jp /{hatena-id}/atom/draft)とblogコレクション(http://d.hatena.ne.jp/{hatena-id} /atom/blog)です。両方とも、Atom エントリ文書が POST できることがわかります。

ちなみに HEAD してみると…

HEAD http://d.hatena.ne.jp/yohei/atom HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 05:07:26 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 676
Content-Type: application/atomsvc+xml;charset=utf-8
Vary: Accept-Encoding

nil

HEAD リクエストへのレスポンスなのに、ボディに “nil” とあるのが残念です。

コレクションリソース

次にコレクションリソースです。まずは GET してみました。

GET http://d.hatena.ne.jp/yohei/atom/blog HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 05:34:08 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 20722
Content-Type: application/atom+xml;charset=type=feed
Vary: Accept-Encoding

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<updated>2006-11-24T00:00:00+09:00</updated>
<id>tag:d.hatena.ne.jp,2006:diary-yohei</id>
<title>傭兵日記</title>
<author>
<name>yohei</name>
</author>
<link rel="self" href="http://d.hatena.ne.jp/yohei/atom/blog"/>
<link rel="next" href="http://d.hatena.ne.jp/yohei/atom/blog?page=2"/>
<entry>
...

プレーンな Atom フィードが返ってきました。rel=”next” なリンクもありますね。これは便利です。

ということで、2ページ目を GET してみました。

GET http://d.hatena.ne.jp/yohei/atom/blog?page=2 HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 07:06:51 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 24964
Content-Type: application/atom+xml;charset=type=feed
Vary: Accept-Encoding
X-Cache: MISS from unknown

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<updated>2006-11-24T00:00:00+09:00</updated>
<id>tag:d.hatena.ne.jp,2006:diary-yohei</id>
<title>傭兵日記</title>
<author>
<name>yohei</name>
</author>
<link rel="self" href="http://d.hatena.ne.jp/yohei/atom/blog?page=2"/>
<link rel="next" href="http://d.hatena.ne.jp/yohei/atom/blog?page=3"/>
<entry>
...

2ページ目のリソースからは3ページ目にリンクしてますね。ここで惜しいのは、rel=”prev” なリンクがないことです。リンクが片道通行になっちゃってます。

もうひとつ気になったのはフィードの id です。tag スキームを使っているのですが、これに含まれる年がフィードに含まれる最新エントリの年になるようなのです。MT 3.x で同様の問題がありましたが、idは一定の値でないとまずいので、たとえば2008をハードコーディングした方がよいのではないかなと思います。

それから、Content-Type が

application/atom+xml;charset=type=feed

なんですが、これは正しくは

application/atom+xml;charset=utf-8;type=feed

ですね。

ちなみに、コレクションでも HEAD リクエストを送ってみました。

HEAD http://d.hatena.ne.jp/yohei/atom/draft HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 405 Method Not Allowed
Date: Fri, 19 Sep 2008 05:16:12 GMT
Server: Apache/2.2.3 (CentOS)
Allow: GET, POST
Content-Length: 22
Content-Type: application/atom+xml;charset=type=entry
Vary: Accept-Encoding
X-Cache: MISS from unknown

nil

おおー、405 が返ってきました。しかもちゃんと Allow ヘッダもあります。ただ、Content-Type のパラメータが charset=type=entry なのはおかしいですね。entry ではないですし、charset=utf-8;type=feed であるべきです。

エントリのPOST

ではエントリを POST してみましょう。まずは draft コレクションに POST してみました。

POST http://d.hatena.ne.jp/yohei/atom/draft HTTP/1.1
Accept: */*
Content-Type: application/atom+xml;type=entry;charset=utf-8
Content-Length: 131
Host: d.hatena.ne.jp

<entry xmlns="http://www.w3.org/2005/Atom">
<title>test</title>
<content type="text">- test

  • てすと</content>

</entry>

HTTP/1.0 201 Created
Date: Fri, 19 Sep 2008 07:45:24 GMT
Server: Apache/2.2.3 (CentOS)
Location: http://d.hatena.ne.jp/yohei/atom/draft/1221810325
Content-Length: 549
Content-Type: application/atom+xml;charset=type=entry
Vary: Accept-Encoding

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
<id>tag:d.hatena.ne.jp,2008:diary-draft-yohei-1221810325</id>
<link rel="edit" href="http://d.hatena.ne.jp/yohei/atom/draft/1221810325"/>
<author>
<name>yohei</name>
</author>
<title>test</title>
<updated>2008-09-19T16:45:25+09:00</updated>
<published>2008-09-19T16:45:25+09:00</published>
<app:edited xmlns:app="http://www.w3.org/2007/app">2008-09-19T16:45:25+09:00</app:edited>
<content type="text/plain">- test

  • てすと</content>

</entry>

無事エントリが作成されます。ちなみに、content 要素の内容は draft コレクションでははてな記法の文字列が text/plain 形式で入り、blog コレクションではhtmlに変換済みの文字列が text/html で入ります。そして、blog コレクションでのはてな記法の文字列は hatena:syntax 要素に入ってくるそうです。これは最初わからず、ちょっとはまりました…

ただ、ちょっと気になるのは content 要素の type 属性の値です。RFC 4287 では text/html/xhtml or MIME タイプを取ることになっており、ここでは text/plain ではなく text が、そして text/html ではなく html が望ましいですね。

その他の操作

とりあえず仕様書どおりに実装すれば PUT/DELETE はできました。blog コレクションでも PUT するときは content 要素にはてな記法を入れるというのにははまりましたが…。これについては設計編で詳しく触れたいと思います。

それから、ちょっとした不正 URI のテスト(全てGET)をしてみましたが、以下のような結果でした。

これらは全て 404 が望ましいと思います。

また、全体的に Content-Type が正しく設定されていない場面がみうけられます。

たとえば DELETE のレスポンスが空なのに text/html;charset=utf-8 だったり、400/403 が返るときにプレーンテキストなのにapplication/atom+xml;type=entry が返ってきたりなどです。

この辺はテストケースの不足だと思いますが、AtomPub には Tim Bray が作った Ape や Joe Gregorio の Atom Publishing Protocol Test Suite など、テストケースが用意されているので、これらを使ってテストするといいと思います。