はてなブログAtomPub APIをOAuth認証経由で叩く

はてなブログAtomPub - Hatena Developer Center を使ってはてなブログの情報を取得したり投稿したりしてみる。

OAuth認証

はてなブログAtomPub を利用するために、クライアントは OAuth 認証、WSSE認証、Basic認証のいずれかを行う必要があります。

http://developer.hatena.ne.jp/ja/documents/blog/apis/atom?kid=238#auth

とのこと。ここではOAuth認証を使ってみることにする。

はてな OAuth - Hatena Developer Center を読みつつ、まずはアプリケーションを登録して、Consumer KeyとConsumer Secretを取得。

Consumer key を取得して OAuth 開発をはじめよう - Hatena Developer Center に、PerlMojolicious::Liteを使ってWebアプリでAccess Tokenを取得する例が載っているけど、ここでは"oob"を使ってWebアプリを立ち上げずにターミナル上で完結する方式で取得してみる。

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

use Config::Pit;
use OAuth::Lite::Consumer;

my $config = pit_get('hatena.ne.jp', require => {
    consumer_key    => '<oauth consumer key>',
    consumer_secret => '<oauth consumer secret>',
});
my $consumer = OAuth::Lite::Consumer->new(
    consumer_key       => $config->{consumer_key},
    consumer_secret    => $config->{consumer_secret},
    site               => 'https://www.hatena.com',
    request_token_path => '/oauth/initiate',
    access_token_path  => '/oauth/token',
    authorize_path     => 'https://www.hatena.com/oauth/authorize',
);
my $request_token = $consumer->get_request_token(
    callback_url => 'oob',
    scope        => 'read_private,write_private',
);
my $url = $consumer->url_to_authorize(token => $request_token);
print "open ${url} and authorize.", "\n";

print "enter verification code: ";
my $verifier = <STDIN>;
chomp $verifier;
my $access_token = $consumer->get_access_token(
    token    => $request_token,
    verifier => $verifier,
);

print "access_token: ", $access_token->token, "\n";
print "access_token_secret: ", $access_token->secret, "\n";

こんなかんじでcallback_urloobを指定して認証用のURLを発行して開くと、認証した先でコードが表示されるので、この値をコピペして入力することでAccess Tokenが得られる。
はてなブログAtomPubでは"read_private"と"write_private"の権限が必要らしいのでscopeにはその2つを指定。

エントリの取得

上記でAccess TokenとAccess Token Secretを取得しておけば、それらを使ってはてなブログのエントリ取得/投稿などが出来るようになるらしい。

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

use Config::Pit;
use OAuth::Lite::Consumer;
use XML::Atom::Feed;

my $hatena_id = shift or die;
my $blog_id   = shift or die;

my $config = pit_get('hatena.ne.jp', require => {
    consumer_key    => '<oauth consumer key>',
    consumer_secret => '<oauth consumer secret>',
    access_token        => '<your access token>',
    access_token_secret => '<your access token secret>',
});
my $consumer = OAuth::Lite::Consumer->new(
    consumer_key       => $config->{consumer_key},
    consumer_secret    => $config->{consumer_secret},
);
$consumer->access_token(
    OAuth::Lite::Token->new(
        token  => $config->{access_token},
        secret => $config->{access_token_secret},
    ),
);

my $res = $consumer->get("https://blog.hatena.ne.jp/${hatena_id}/${blog_id}/atom/entry");
$res->is_success or die $res->code;
my $xml  = $res->decoded_content;
my $feed = XML::Atom::Feed->new(\$xml);
for my $entry ($feed->entries) {
    printf "%s - %s\n", $entry->published, $entry->title;
}

普通に[]https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom[]にGETリクエストを送るだけでXMLが返ってくるので、これらを適当にparseすればエントリの内容を取得したりできる。

次のページへのリンクが

<link rel="next" href="https://blog.hatena.ne.jp/sugyan/sugyan.hatenablog.com/atom/entry?page=1352341991" />

のような形で返ってくるけど、OAuth::Liteの場合クエリパラメータを含むURLではなく"params"として別オプションで渡さないといけないようだ。

# これはダメ
# my $res = $consumer->get('https://blog.hatena.ne.jp/sugyan/sugyan.hatenablog.com/atom/entry?page=1352341991');
# これならOK
my $res = $consumer->get('https://blog.hatena.ne.jp/sugyan/sugyan.hatenablog.com/atom/entry', +{ params => +{ page => 1352341991 } });

エントリの投稿

上記と同じURLに対し、POSTで所定のXMLを送信することでエントリの投稿ができる。

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

use Config::Pit;
use OAuth::Lite::Consumer;
use XML::Atom::Feed;

my $hatena_id = shift or die;
my $blog_id   = shift or die;

my $config = pit_get('hatena.ne.jp', require => {
    consumer_key    => '<oauth consumer key>',
    consumer_secret => '<oauth consumer secret>',
    access_token        => '<your access token>',
    access_token_secret => '<your access token secret>',
});
my $consumer = OAuth::Lite::Consumer->new(
    consumer_key       => $config->{consumer_key},
    consumer_secret    => $config->{consumer_secret},
);
$consumer->access_token(
    OAuth::Lite::Token->new(
        token  => $config->{access_token},
        secret => $config->{access_token_secret},
    ),
);

my $entry = XML::Atom::Entry->new(Namespace => 'http://www.w3.org/2005/Atom');
$entry->title('投稿テスト');
$entry->add('', 'content', <<'CONTENT', +{ type => 'text/plain' });
てすと

- hoge
- fuga
- piyo
CONTENT

my $res = $consumer->post("https://blog.hatena.ne.jp/${hatena_id}/${blog_id}/atom/entry", $entry->as_xml, +{ headers => [ 'Content-Type' => 'application/xml' ] });
$res->is_success or die $res->code;
my $xml = $res->decoded_content;
print $xml;

リクエストヘッダの"Content-Type"を指定していないとinvalid signatureになってしまっていてハマった…


下書きで投稿したい場合は"app:control/app:draft"要素を指定すれば良いらしい。

use XML::Atom::Util qw(create_element);
...

my $entry = XML::Atom::Entry->new(Namespace => 'http://www.w3.org/2005/Atom');
$entry->set_attr('xmlns:app', 'http://www.w3.org/2007/app');
$entry->title('投稿テスト');
$entry->add('', 'content', '内容', +{ type => 'text/plain' });
my $draft = create_element('', 'app:draft');
$draft->appendText('yes');
my $control = create_element('', 'app:control');
$control->appendChild($draft);
$entry->set('', '', $control);

こんなカンジでできた