Skip to content

Latest commit

 

History

History
660 lines (528 loc) · 21.1 KB

README.md

File metadata and controls

660 lines (528 loc) · 21.1 KB

hatena-blog-api2

このライブラリは非公式な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の認証方式に対応しております。いずれかの認証方式を選択し、ライブラリを初期化してください。

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)'
});

OAuthの設定

ライブラリ(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ノードの値 ... プロパティ _ に格納

API ドキュメント

Blog Class

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キー)'
});

Blog.postEntry()

書式
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"
   }
  }
 }
}

Blog.updateEntry()

ブログ記事を更新します。

書式
  updateEntry({id,title,content,updated,categories,draft})
引数

引数はオブジェクト'{}'1つのみです。
オブジェクトのプロパティとして以下のパラメータを格納します。

  • id (必須)... 記事IDを指定します。idを指定しない場合はrejectされます。
  • title (必須)... 記事タイトルを指定します。既定値は''(空白)です。nullは空白に置き換えられます。titleを指定しない場合はrejectされます。
  • content (必須)... 記事本文を指定します。既定値は''(空白)です。nullは空白に置き換えられます。contentを指定しない場合はrejectされます。
  • updated (必須)... 記事の公開日付を指定します。値はDateもしくはISO8601形式でミリ秒を省略したものを文字列で指定します。updated自体を指定しない、もしくはupdatedDateもしくは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);

Blog.deleteEntry()

記事を削除します。

書式
  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);

Blog.getEntry()

記事を取得します。

書式
 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"
   }
  }
 }
}

Blog.getEntries()

エントリのコレクションを取得します。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);

Blog.getEntryID()

エントリ情報からEntryIDを取り出します。

書式
getEntryID(entry);
引数
  • entry ... エントリーJSONデータを指定します。エントリーデータでない場合は例外が発生します。
戻り値

Entry IDを返却します。

このライブラリの問題点

このライブラリには以下の問題点があります。

  • 連続ポストを行う際、記事のポストとポストの間に1000ms程度のウェイトを入れないと、正常にポストされない。
    ポストされた情報ははてなブログの管理画面で正常に編集・更新・削除できるものの、実際のブログページには表示されない。
    API上では正常にポストされたステータスコードが返ってくるため、なぜこうなるのかは原因不明。

License

MIT

製作者

SFPGMR (Satoshi Fujiwara)

免責事項

  • 本ライブラリのソースコード、Webページに書かれている情報は、その内容について保証するものではありません。
  • 製作者は本ライブラリおよびソースコードの利用によって生じたいかなる損害にも責任を負いません。
  • 本ライブラリの内容や情報は予告なく変更される場合があります。