このライブラリは非公式なNode.js用のHatena::Blog AtomPub APIのラッパーです。
bouzuyaさんのnode-hatena-blog-apiをベースにcoffeeからes6に変換し、大きく修正を加えています。
https://github.com/bouzuya/node-hatena-blog-api
- 不正なポストを行わないようにするため、引数のチェックはできる限り入念に行う。
- ES6(ES2015)の構文をできる限り採用する。
- メソッド名は自明であることを心がける。
$ npm install https://github.com/sfpgmr/node-hatena-blog-api2
サンプルコード
も併せてご確認ください。
本ライブラリはOAuthおよびWSSEの認証方式に対応しております。いずれかの認証方式を選択し、ライブラリを初期化してください。
ライブラリ(Blog)をrequire()
し、WSSEにおける初期化パラメータをオブジェクトに格納し、Blogのインスタンスを生成します。
※WSSE認証については「はてなサービスにおけるWSSE認証」を参照してください。
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'wsse',// 認証方式
username: 'はてなブログユーザー名',
blogId: 'はてなブログID',
apiKey: ' [AtomPub API key](http://blog.hatena.ne.jp/my/config/detail)'
});
- ブログIDはブログのURLです。カスタムドメインの場合はブログURLではないので注意してください。確認方法は「はてなブログの設定」->「詳細設定」の「AtomPub」欄にある「ルートエンドポイント」項目を参照してください。
ルートエンドポイントの内容は通常以下のフォーマットとなっています。
https://blog.hatena.ne.jp/(はてなブログユーザー名)/(はてなブログID)/atom - AtomPubのAPIキーは「はてなブログの設定」->「詳細設定」の「AtomPub」欄にある「APIキー」です。
ライブラリ(Blog)をrequire()
し、OAuthにおける初期化パラメータをオブジェクトに格納し、Blogのインスタンスを生成します。
※アプリケーション・スコープは"read_private"かつ"write_private"である必要があります。
※OAuthについては「はてなサービスにおける OAuth」をご確認ください。
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'oauth',// 認証方式
blogId: 'はてなブログID',
consumerKey: 'コンシューマー・キー',
consumerSecret: 'コンシューマー・シークレット',
accessToken: 'アクセス・トークン',
accessTokenSecret: 'アクセス・トークン・シークレット'
});
// ...
インスタンスを使用して、各種メソッドを呼び出します。
元ライブラリはcallback/Promiseに対応しておりましたが、本ライブラリはPromiseのみとなっています。
const Blog = require('hatena-blog-api').Blog;/* ... */;
var client = new Blog (/* ... */);
var options = {/* ... */};
// 記事の作成
client.post(options)
.then(()=>{
// 作成完了
console.log('uploaded');
})
.catch((err)=>{
// エラー処理
console.error(err);
});
AtomPub API はデータフォーマットとしてXMLを使用しています。本ライブラリでは、APIに渡すデータ・返却されるデータをxml2jsを使用してJSONへ変換しています。変換されるJSONデータはxml2jsの変換方式に準じています。
- XMLタグの親子関係 ... JSONプロパティの階層で対応
- XMLタグの属性 ... プロパティ $ の中に格納
- XMLタグのtextノードの値 ... プロパティ _ に格納
AtomPub APIをメソッドで提供します。
constructor({ type,userName,blogId,apiKey,consumerKey,consumerSecret,accessToken,accessTokenSecret})
optionパラメータオブジェクトを引数に持ちます。
利用者は認証に必要なオプションパラメータを指定してコンストラクタを呼び出します。
- type ... 認証方式を指定します。
設定できる値は'wsse'か'oauth'のいずれかです。
既定値は'wsse'です。
'wsse','oauth'以外の値を指定した場合は例外が発生します。 - userName (必須) ... はてなブログユーザー名を指定します。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。 - blogId (必須) ... はてなブログIDを指定します。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。 - apiKey (必須) ... はてなブログのAPIキーを指定します。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。 - consumerKey ('oauth'のみ必須) ... コンシューマー・キーを指定します。
'wsse'認証では使用しません。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。 - consumerSecret ('oauth'のみ必須) ... コンシューマー・シークレットを指定します。
'wsse'認証では使用しません。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。 - accessToken ('oauth'のみ必須) ... アクセストークンを指定します。
'wsse'認証では使用しません。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。 - accessTokenSecret ('oauth'のみ必須) ... アクセストークンを指定します。
'wsse'認証では使用しません。
''
(空文字列)・null
・未指定(undefined
)の場合は例外が発生します。
const Blog = require('hatena-blog-api2').Blog;
//コンストラクタ
const client = new Blog({
type:'wsse',
userName:'sfpgmr',
blogId:'sfpgmr.hatenablog.jp',
apiKey:'(APIキー)'
});
postEntry({ title = '', content = '', updated = new Date(), categories, draft = true });
記事をポストします。
引数はオブジェクト'{}
'1つのみです。
オブジェクトのプロパティとして以下のパラメータを格納します。
-
title ... 記事タイトルを指定します。既定値は
''
(空白)です。null
は空白に置き換えられます。 -
content ... 記事本文を指定します。既定値は
''
(空白)です。null
は空白に置き換えられます。 -
updated ... 記事の公開日付を指定します。値は
Date
もしくはISO8601形式でミリ秒を省略したものを文字列で指定します。それ以外の値を指定した場合はreject
されます。 -
categories ... カテゴリ文字列を文字列、もしくは文字列の配列で指定します。指定しない場合は省略されます。
文字列もしくは文字列の配列以外を指定した場合、配列中に文字列以外が含まれる場合はreject
されます。 -
draft ... 下書きかどうかを指定します。既定値は
false
(公開)です。
ブール値以外の値を指定するとreject
されます。
Promiseを返します。処理結果はthen()
の引数に指定するfunction
オブジェクトの第一引数として渡されます。
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'wsse',
userName: process.env.HATENA_USERNAME, // 'username'
blogId: process.env.HATENA_BLOG_ID, // 'blog id'
apiKey: process.env.HATENA_APIKEY // 'apikey'
});
// POST CollectionURI (/<username>/<blog_id>/atom/entry)
client.postEntry({
title: 'テストエントリ',
updated:new Date(2010,1,1,10,10),
content: '# テストエントリ\r\nこれはテストです。\r\n\r\n',
})
.then(
// resolve
(res)=>{
console.log('posted\n',JSON.stringify(res,null,1));
},
// reject
console.error
);
上記の実行結果resolveにセットされるjsonデータ例
{
"entry": {
"$": {
"xmlns": "http://www.w3.org/2005/Atom",
"xmlns:app": "http://www.w3.org/2007/app"
},
"id": {
"_": "tag:blog.hatena.ne.jp,2013:blog-sfpgmr-12921228815731439891-10328749687243023284"
},
"link": [
{
"$": {
"rel": "edit",
"href": "https://blog.hatena.ne.jp/sfpgmr/sfpgmr-test.hatenablog.com/atom/entry/10328749687243023284"
}
},
{
"$": {
"rel": "alternate",
"type": "text/html",
"href": "http://sfpgmr-test.hatenablog.com/entry/2010/02/01/%E3%83%86%E3%82%B9%E3%83%88%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA_1"
}
}
],
"author": {
"name": {
"_": "sfpgmr"
}
},
"title": {
"_": "テストエントリ"
},
"updated": {
"_": "2010-02-01T01:10:00+09:00"
},
"published": {
"_": "2017-05-04T15:55:52+09:00"
},
"app:edited": {
"_": "2017-05-04T15:55:52+09:00"
},
"summary": {
"_": "# テストエントリ これはテストです。",
"$": {
"type": "text"
}
},
"content": {
"_": "# テストエントリ\r\nこれはテストです。\r\n\r\n",
"$": {
"type": "text/x-hatena-syntax"
}
},
"hatena:formatted-content": {
"_": "<p># テストエントリ<br />\nこれはテストです。</p>\n",
"$": {
"type": "text/html",
"xmlns:hatena": "http://www.hatena.ne.jp/info/xmlns#"
}
},
"app:control": {
"app:draft": {
"_": "no"
}
}
}
}
ブログ記事を更新します。
updateEntry({id,title,content,updated,categories,draft})
引数はオブジェクト'{}
'1つのみです。
オブジェクトのプロパティとして以下のパラメータを格納します。
- id (必須)... 記事IDを指定します。
id
を指定しない場合はreject
されます。 - title (必須)... 記事タイトルを指定します。既定値は
''
(空白)です。null
は空白に置き換えられます。title
を指定しない場合はreject
されます。 - content (必須)... 記事本文を指定します。既定値は
''
(空白)です。null
は空白に置き換えられます。content
を指定しない場合はreject
されます。 - updated (必須)... 記事の公開日付を指定します。値は
Date
もしくはISO8601形式でミリ秒を省略したものを文字列で指定します。updated
自体を指定しない、もしくはupdated
にDate
もしくはISO8601形式の文字列出ない場合reject
されます。 - categories ... カテゴリ文字列を文字列、もしくは文字列の配列で指定します。指定しない場合は省略されます。
文字列もしくは文字列の配列以外を指定した場合、配列中に文字列以外が含まれる場合はreject
されます。 - draft ... 下書きかどうかを指定します。既定値は
false
(公開)です。
ブール値以外の値を指定するとreject
されます。
Promiseを返します。処理結果はthen()
の引数に指定するfunction
オブジェクトの第一引数として渡されます。
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'wsse',
userName: process.env.HATENA_USERNAME, // 'username'
blogId: process.env.HATENA_BLOG_ID, // 'blog id'
apiKey: process.env.HATENA_APIKEY // 'apikey'
});
//process.on('unhandledRejection', console.dir);
// POST CollectionURI (/<username>/<blog_id>/atom/entry)
client.postEntry({
title: 'テストエントリ',
updated:new Date(2010,1,1,10,10),
content: '# テストエントリ\r\nこれはテストです。\r\n\r\n',
categories:['blog','hatena']
})
.then(
// resolve
res=>{
console.log('#postEntryの結果\n',JSON.stringify(res,null,1));
// idの取り出し
const entryId = client.getEntryID(res.entry);
console.log(entryId);
// ポストしたデータの更新
return client.updateEntry({
id:entryId,
title:res.entry.title._,
content:'修正',
updated:res.entry.updated._
});
}
)
.then(
// resolve
res=>{
console.log('#updateEntryの結果\n',JSON.stringify(res,null,1));
}
)
.catch(console.error);
記事を削除します。
deleteEntry(id);
- id (必須)... 記事IDを指定します。
id
を指定しない場合はreject
されます。
Promiseを返します。処理結果はthen()
の引数に指定するfunction
オブジェクトの第一引数として渡されます。値は正常終了した場合はnull
です。
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'wsse',
userName: process.env.HATENA_USERNAME, // 'username'
blogId: process.env.HATENA_BLOG_ID, // 'blog id'
apiKey: process.env.HATENA_APIKEY // 'apikey'
});
//process.on('unhandledRejection', console.dir);
// POST CollectionURI (/<username>/<blog_id>/atom/entry)
client.postEntry({
title: 'テストエントリ',
updated:new Date(2010,1,1,10,10),
content: '# テストエントリ\r\nこれはテストです。\r\n\r\n',
categories:['blog','hatena']
})
.then(
// resolve
res=>{
console.log('#postEntryの結果\n',JSON.stringify(res,null,1));
// idの取り出し
const entryId = client.getEntryID(res.entry);
console.log(entryId);
// 記事の削除
return client.deleteEntry(entryId);
}
)
.then(
// resolve
res=>{
console.log('#deleteEntryの結果\n',JSON.stringify(res,null,1));
}
)
.catch(console.error);
記事を取得します。
getEntry(id);
- id (必須)... 記事IDを指定します。
id
を指定しない場合はreject
されます。
Promiseを返します。処理結果はthen()
の引数に指定するfunction
オブジェクトの第一引数として渡されます。
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'wsse',
userName: process.env.HATENA_USERNAME, // 'username'
blogId: process.env.HATENA_BLOG_ID, // 'blog id'
apiKey: process.env.HATENA_APIKEY // 'apikey'
});
//process.on('unhandledRejection', console.dir);
// POST CollectionURI (/<username>/<blog_id>/atom/entry)
client.postEntry({
title: 'テストエントリ',
updated:new Date(2010,1,1,10,10),
content: '# テストエントリ\r\nこれはテストです。\r\n\r\n',
categories:['blog','hatena']
})
.then(
// resolve
res=>{
console.log('#postEntryの結果\n',JSON.stringify(res,null,1));
// idの取り出し
const entryId = client.getEntryID(res.entry);
// エントリを取得
return client.getEntry(entryId);
}
)
.then(
// resolve
res=>{
console.log('#getEntryの結果\n',JSON.stringify(res,null,1));
}
)
.catch(console.error);
getEntry
で返却されるJSONデータの例
{
"entry": {
"$": {
"xmlns": "http://www.w3.org/2005/Atom",
"xmlns:app": "http://www.w3.org/2007/app"
},
"id": {
"_": "tag:blog.hatena.ne.jp,2013:blog-sfpgmr-12921228815731439891-10328749687243094177"
},
"link": [
{
"$": {
"rel": "edit",
"href": "https://blog.hatena.ne.jp/sfpgmr/sfpgmr-test.hatenablog.com/atom/entry/10328749687243094177"
}
},
{
"$": {
"rel": "alternate",
"type": "text/html",
"href": "http://sfpgmr-test.hatenablog.com/entry/2010/02/01/%E3%83%86%E3%82%B9%E3%83%88%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA_10"
}
}
],
"author": {
"name": {
"_": "sfpgmr"
}
},
"title": {
"_": "テストエントリ"
},
"updated": {
"_": "2010-02-01T01:10:00+09:00"
},
"published": {
"_": "2017-05-04T21:11:55+09:00"
},
"app:edited": {
"_": "2017-05-04T21:11:55+09:00"
},
"summary": {
"_": "# テストエントリ これはテストです。",
"$": {
"type": "text"
}
},
"content": {
"_": "# テストエントリ\r\nこれはテストです。\r\n\r\n",
"$": {
"type": "text/x-hatena-syntax"
}
},
"hatena:formatted-content": {
"_": "<p># テストエントリ<br />\nこれはテストです。</p>\n",
"$": {
"type": "text/html",
"xmlns:hatena": "http://www.hatena.ne.jp/info/xmlns#"
}
},
"category": [
{
"$": {
"term": "blog"
}
},
{
"$": {
"term": "hatena"
}
}
],
"app:control": {
"app:draft": {
"_": "no"
}
}
}
}
エントリのコレクションを取得します。AtomPub APIの制約により、1回のAPI呼び出しで取得できるエントリは10エントリまでとなっています。
(仕様では7エントリとなっているのですが。。)
getEntries(page);
- page ... pageIDを指定します。このメソッドを呼び出すことで得られるpageIDを取得・設定することで、10エントリ以上の記事を連続して取得することができます。使用例を参照してください。
指定しない場合は最初の10記事と、次のページがある場合はpageIDを返します。
(AtomPub APIでは1回のAPIコールで返ってくるエントリ数は7エントリとなっていますが、実際に試すと10エントリ返ってきます。API仕様が変更されたのかは不明です。)
Promiseを返します。処理結果はthen()
の引数に指定するfunction
オブジェクトの第一引数として渡されます。処理結果は以下のフォーマットです。
{
res:(APIのレスポンス結果のJSONデータ),
nextPageID:(次のページのpageID)
}
"use strict"
// エントリのコレクションをすべて取り出すサンプル
const Blog = require('hatena-blog-api2').Blog;
const client = new Blog({
type: 'wsse',
userName: process.env.HATENA_USERNAME, // 'username'
blogId: process.env.HATENA_BLOG_ID, // 'blog id'
apiKey: process.env.HATENA_APIKEY // 'apikey'
});
let ps = Promise.resolve();
function list(res){
const entry = res.res.feed.entry;
entry.forEach(entry=>{
console.log(entry.title._);
});
if(res.nextPageID){
ps = ps.then(client.getEntries.bind(client,res.nextPageID))
.then(list);
}
}
ps = ps.then(client.getEntries.bind(client))
.then(list);
エントリ情報からEntryIDを取り出します。
getEntryID(entry);
- entry ... エントリーJSONデータを指定します。エントリーデータでない場合は例外が発生します。
Entry IDを返却します。
このライブラリには以下の問題点があります。
- 連続ポストを行う際、記事のポストとポストの間に1000ms程度のウェイトを入れないと、正常にポストされない。
ポストされた情報ははてなブログの管理画面で正常に編集・更新・削除できるものの、実際のブログページには表示されない。
API上では正常にポストされたステータスコードが返ってくるため、なぜこうなるのかは原因不明。
SFPGMR (Satoshi Fujiwara)
- 本ライブラリのソースコード、Webページに書かれている情報は、その内容について保証するものではありません。
- 製作者は本ライブラリおよびソースコードの利用によって生じたいかなる損害にも責任を負いません。
- 本ライブラリの内容や情報は予告なく変更される場合があります。