はてなはあまり利用してないのだけど(人気エントリは昼休みに見てる)、ヘルプをさまよっていたら、はてなブックマークAtomAPI というのがあったのでVB.NETで実装してみたよ! 2005年からあったのかー。
まず、つまづいた点から。ブックマークを追加する際にPOST投稿する必要があるのだけど、HttpWebRequestクラスを使って投稿した場合、サーバーから 417 Expectation Failed というエラーが返ってきていた。期待されるものがおかしいのか?? と思ってリクエスト内容がおかしいのかなーと調べてたけど、このエラーコードは、Expect リクエストヘッダフィールドに関するものらしい。.NET で通常 POSTするとこの Expect リクエストヘッダフィールドに「100 (Continue) ステータス」が指定されるようだ。これを指定するとPOSTデータを受け取る前に、サーバが受付拒否ができ、トラフィックを有効に利用できる感じだ。はてなサーバーはこのExpect リクエストを理解できねーとエラーを返してたのですな。
.NETのコードで、どうやって解決するかというと、検索したらありました。
ServicePointManager.Expect100Continue = False
Sharedなプロパティです。これで一律 Expect を使用しない送信方法になるっぽい。Sharedなので使い方が微妙。作ったコードでは処理前に元の値を覚えておいて、処理後に元の値に戻しているけどマルチスレッドで破綻 オワタ。
さて認証は、WSSE認証というものを使うらしい。仕様書のリンクはきれてるし詳細がよくわからないのだけど、とりあえず動いたのでOKでしょう。クライアントソフトはユーザのパスワードを記憶しておかないといけないので、少しめんどいですね。
というわけで、説明もほどほどに(してないけど)コードはこちら。
Public Class HatenaBookmarkApi
Private Shared _servicePostUri As String = "http://b.hatena.ne.jp/atom/post"
Private Shared Function createWsseHeaderValue(ByVal username As String, ByVal password As String) As String
Dim sha1 = New System.Security.Cryptography.SHA1CryptoServiceProvider
Dim created = Now.ToString("s")
Dim nonce = Guid.NewGuid.ToString
Dim passwordDigest = Convert.ToBase64String( _
sha1.ComputeHash(System.Text.Encoding.ASCII.GetBytes(nonce & created & password)))
Return String.Format("UsernameToken Username=""{0}"", PasswordDigest=""{1}"", Nonce=""{2}"", Created=""{3}""", _
username, passwordDigest, Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(nonce)), created)
End Function
Public Shared Function PostUri(ByVal username As String, ByVal password As String, ByVal uri As String, ByVal comment As String) As WebExceptionStatus
Dim except100Continue = ServicePointManager.Expect100Continue
ServicePointManager.Expect100Continue = False
Dim request = CType(WebRequest.Create(_servicePostUri), HttpWebRequest)
request.Method = "POST"
request.Headers.Add("Authorization", "WSSE profile=""UsernameToken""")
request.Headers.Add("X-WSSE", createWsseHeaderValue(username, password))
request.Accept = "application/x.atom+xml, application/xml, text/xml, */*"
request.ContentType = "application/x.atom+xml"
Dim entry = <entry xmlns="http://purl.org/atom/ns#">
<title>dummy</title>
<link rel="related" type="text/html" href=<%= uri %>/>
<summary type="text/plain"><%= comment %></summary>
</entry>
' こっちでもまったく問題なし
'Dim entry = "<entry xmlns=""http://purl.org/atom/ns#""><title>dummy</title><link rel=""related"" type=""text/html"" href=""" & uri & """ /><summary type=""text/plain"">" & comment & "</summary></entry>"
Dim buf = System.Text.Encoding.UTF8.GetBytes(entry.ToString)
request.ContentLength = buf.Length
Try
Dim stream = request.GetRequestStream
stream.Write(buf, 0, buf.Length)
stream.Close()
request.GetResponse.Close()
Return WebExceptionStatus.Success
Catch ex As WebException
Return ex.Status
Finally
ServicePointManager.Expect100Continue = except100Continue
End Try
End Function
End Class
使い方はこちら。戻り値が、WebExceptionStatusなんだよねー。そういう使い方普通するんだろうか。でもBooleanはどうかと思う。
HatenaBookmarkApi.PostUri(username, password, "http://yahoo.co.jp", "[tag1][tag2][tag3]")
参考になったサイト
作ってから知ったのだけど、こちらのサイトが参考になりそうですね。
もっと調べて(検索して)から作れば良かった。というか、これ作るつもりではてなのヘルプをさまよってなかったからなぁ。