HatenaHaikuAPI for Groovy
ふもぼ(h:id:fumobot)が使っている
はてなハイクAPIをGroovyから操るAPIをせっかくなので公開してみます。
#ただし、自分用に書いているので、使いにくいかもしれません。エッセンスだけでもどうぞ。
HatenaHaikuAPI.groovy
import java.text.* class HatenaHaikuAPI { String username = null String password = null String client = 'Web' /** * ステータスを取得しマップして返却 * マップに指定できる値 * ・id エントリID * ・keyword キーワード * ・user ユーザID * ・friends フレンドタイムラインを探すユーザID * ・page 指定のページを探す * ・count 取得件数(最大200件, 省略時20件) * ・sinceDate 指定の日付より新しい投稿のみ探す * ・sinceId 指定のエントリIDの日付より新しい投稿のみ探す */ def getStatus(Map param) { def reqParams = [:] if (param?.sinceDate) { reqParams['since'] = toDateTimeTZ(param.sinceDate) } else if (param?.sinceId) { def idStatus = getStatus(id: param.sinceId) if (idStatus) reqParams['since'] = toDateTimeTZ(idStatus.first().createdAt) } if (param?.page) reqParams['page'] = param.page if (param?.count) reqParams['count'] = param.count def paramStr = reqParams.collect{ "${it.key}=${URLEncoder.encode(it.value.toString(), 'UTF-8')}" }.join('&') switch(param) { // id指定ありの場合 case {param?.containsKey('id')} : def id = URLEncoder.encode(param.id, 'UTF-8') def url = "http://h.hatena.ne.jp/api/statuses/show/${id}.xml" println "GET ${url}" def result = [] try { def status = new XmlParser().parse(url) if (status) { result = toStatusMap(status) } else { String msg = "指定したID ${param.id} が不正。" LogUtil.error(msg) } } catch(FileNotFoundException fnfe) { String msg = "指定したID ${param.id} が不正。" LogUtil.error(msg) } return result // keyword指定ありの場合 case {param?.containsKey('keyword')} : def keyword = URLEncoder.encode(param.keyword, 'UTF-8') def url = "http://h.hatena.ne.jp/api/statuses/keyword_timeline/${keyword}.xml?${paramStr}" println "GET ${url}" def statuses = new XmlParser().parse(url) def result = [] statuses.each { result << toStatusMap(it) } return result // user指定ありの場合 case {param?.containsKey('user')} : def user = URLEncoder.encode(param.user, 'UTF-8') def url = "http://h.hatena.ne.jp/api/statuses/user_timeline/${user}.xml?${paramStr}" println "GET ${url}" def statuses = new XmlParser().parse(url) def result = [] statuses.each { result << toStatusMap(it) } return result // friends指定ありの場合 case {param?.containsKey('friends')} : def friends = URLEncoder.encode(param.friends, 'UTF-8') def url = "http://h.hatena.ne.jp/api/statuses/friends_timeline/${friends}.xml?${paramStr}" println "GET ${url}" def statuses = new XmlParser().parse(url) def result = [] statuses.each { result << toStatusMap(it) } return result // パブリックタイムラインから取得 default : def url = "http://h.hatena.ne.jp/api/statuses/public_timeline.xml?${paramStr}" println "GET ${url}" def statuses = new XmlParser().parse(url) def result = [] statuses.each { result << toStatusMap(it) } return result } } /** * Hot Keywordsのリストを取得する * @param depth related_ */ def showHotKeywords(depth = 1) { int count = depth - 1 def list = [] def relatedList = [] // Hot全体を取ってくる def url = 'http://h.hatena.ne.jp/api/keywords/hot.xml' println "GET ${url}" def keywords = new XmlParser().parse(url) list.addAll(keywords.keyword*.title*.text()) relatedList.addAll(keywords.'**'.related_keywords*.text()) relatedList = relatedList.unique() // depthが2以上なら、relatedListの内容を追加 if(count >= 1){ list.addAll(relatedList) count-- } // depthが3以上ならなくなるまで繰り返し探す while(count) { list.addAll(relatedList) def nextRelatedList = [] def size = relatedList.size() relatedList.eachWithIndex{ item, idx -> print "${idx+1}/${size} " def kw = this.showKeyword(item) if(kw) nextRelatedList.addAll(kw.relatedKeywords) } nextRelatedList = nextRelatedList.unique() list.addAll(nextRelatedList) relatedList = nextRelatedList count-- } return list.unique() - [null] } /** キーワードリストを取得 */ def showKeywordsList(Map param) { def reqParams = [:] if (param?.page) reqParams['page'] = param.page if (param?.word) reqParams['word'] = param.word def paramStr = reqParams.collect{ "${it.key}=${URLEncoder.encode(it.value.toString(), 'UTF-8')}" }.join('&') try { def url = "http://h.hatena.ne.jp/api/keywords/list.xml?${paramStr}" println "GET ${url}" def list = new XmlParser().parse(url) list ? list.keyword.collect{ toKeywordMap(it) } : [] } catch(Exception e) { println "取得失敗 ${e}" [] } } /** * keywordノードを取得してくる。 */ def showKeyword(searchKeyword) { def url = "http://h.hatena.ne.jp/api/keywords/show/${URLEncoder.encode(searchKeyword, 'UTF-8')}.xml" println "GET ${url}" try { return toKeywordMap(new XmlParser().parse(url)) } catch(Exception e) { return [:] } } /** * エントリする。 * @param message 投稿するメッセージ [必須] * @param keyword キーワード [省略された場合idページへ投稿] */ def entry(message, keyword = null) { post(message, keyword) } /** * 特定のエントリに返信する。 * @param message 投稿するメッセージ [必須] * @param replyId 返信先メッセージID [必須] */ def reply(message, replyId) { post(message, null, replyId) } /** * ポストする。 * @param message 投稿するメッセージ [必須] * @param keyword 投稿するメッセージ [省略された場合idページへ投稿] * @param replyId 投稿するメッセージ [省略された場合通常エントリ] */ def post(message, keyword = null, replyId = null) { // コネクション接続 URL url = new URL('http://h.hatena.ne.jp/api/statuses/update.xml') def authenticator = new HttpAuthenticator('username':username, 'password':password) Authenticator.default = authenticator def urlconn = url.openConnection() as HttpURLConnection urlconn.requestMethod = 'POST' urlconn.doOutput = true // ポスト内容の表示 println "keyword=[${keyword}]" println "message=[${message}]" println "client =[${client}]" println "replyId=[${replyId}]" // ポストする文字 def postStr = [ 'source' : client, // クライアント名 'status' : message, // 本文 ] if (keyword) postStr['keyword'] = keyword // キーワード if (replyId) postStr['in_reply_to_status_id'] = replyId // 返信先ID try { def writer = new PrintWriter(urlconn.outputStream) writer.write(postStr.collect{ "${it.key}=${URLEncoder.encode(it.value, 'UTF-8')}" }.join('&')) writer.flush() writer.close() // レスポンス Map headers = urlconn.headerFields headers.each { println " ${it.key}: ${it.value}" } // ボディ String responseBody = new BufferedReader(new InputStreamReader(urlconn.inputStream, 'UTF-8')).text println """\ ---------------- レスポンスコード [${urlconn.responseCode}] レスポンスメッセージ[${urlconn.responseMessage}] プロンプト(realm) [${authenticator.myGetRequestingPrompt()}] ----------------\ """ println '●●●●● ポスト成功 ●●●●●' // レスポンスボディをStatusMapにして返却 return toStatusMap(new XmlParser().parseText(responseBody)) } catch( IOException e ) { LogUtil.error(e) println '●●●●● ポスト失敗 ●●●●●' return null } finally { // コネクション切断 urlconn.disconnect() } } /** スターを消す */ def addStar(id) { _updateStar(id, true) } /** スターを消す */ def deleteStar(id) { _updateStar(id, false) } /** * スターを操作 * @param id スター操作対象のステータスID * @param isAdd true:スターを付ける, false:スターを消す */ def _updateStar(id, isAdd) { // コネクション接続 URL url = isAdd ? new URL("http://h.hatena.ne.jp/api/favorites/create/${id}.xml") : new URL("http://h.hatena.ne.jp/api/favorites/destroy/${id}.xml") def authenticator = new HttpAuthenticator('username':username, 'password':password) Authenticator.default = authenticator def urlconn = url.openConnection() as HttpURLConnection urlconn.requestMethod = 'POST' urlconn.doOutput = true // ポスト内容の表示 println "id=[${id}]" try { // レスポンス Map headers = urlconn.headerFields headers.each { println " ${it.key}: ${it.value}" } println """\ ---------------- レスポンスコード [${urlconn.responseCode}] レスポンスメッセージ[${urlconn.responseMessage}] プロンプト(realm) [${authenticator.myGetRequestingPrompt()}] ---- ボディ ---- ${new BufferedReader(new InputStreamReader(urlconn.inputStream, 'UTF-8')).text} ----------------\ """ println "●●●●● スター${isAdd ? '付加' : '削除'}成功 ●●●●●" } catch( IOException e ) { LogUtil.error(e) println "●●●●● スター${isAdd ? '付加' : '削除'}失敗 ●●●●●" } finally { // コネクション切断 urlconn.disconnect() } } /** ステータスノードをマップして返却 */ def toStatusMap(status) { def text = (status.keyword.text() ==~ /^${Util.ID_PATTERN}$/) ? status.text.text() : status.text.text().split('=').toList().tail().join() [ 'id' : status.id.text(), 'createdAt' : toDefaultDate(status.created_at.text()), 'star' : status.favorited.text().toInteger(), 'replyToStatusId' : status.in_reply_to_status_id.text(), 'replyToUserId' : status.in_reply_to_user_id.text(), 'keyword' : status.keyword.text(), 'link' : status.link.text(), 'source' : status.source.text(), 'text' : text, 'userId' : status.user.id.text(), 'userName' : status.user.name.text(), ] } /** キーワードノートをマップして返却 */ def toKeywordMap(keyword) { [ 'entries' : keyword.entry_count.text(), 'fans' : keyword.followers_count.text(), 'entriesAndFans' : "${NumberFormat.instance.format(keyword.entry_count.text().toInteger())} entries /${NumberFormat.instance.format(keyword.followers_count.text().toInteger())} fans", 'relatedKeywords' : keyword.related_keywords*.text(), 'title' : keyword.title.text(), ] } /** * datetimeTZ形式からDateに変換 * * @param datetimeTZ datetimeTZ形式の文字列 * @return 日本標準時のDate */ Date toDefaultDate(String datetimeTZ) { DateFormat fm = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss') fm.timeZone = TimeZone.getTimeZone('GMT+0') fm.parse(datetimeTZ.replace('T', ' ').replace('Z', '')) } /** * DateをdatetimeTZ形式に変換 * * @param defaultDate 日本標準時のDate * @return datetimeTZ形式の文字列 */ String toDateTimeTZ(Date defaultDate) { DateFormat fmYMD = new SimpleDateFormat('yyyy-MM-dd') DateFormat fmHHMMSS = new SimpleDateFormat('HH:mm:ss') fmYMD.timeZone = TimeZone.getTimeZone('GMT+0') fmHHMMSS.timeZone = TimeZone.getTimeZone('GMT+0') "${fmYMD.format(defaultDate)}T${fmHHMMSS.format(defaultDate)}Z" } } /** * Basic認証のためのクラス */ class HttpAuthenticator extends Authenticator { def username def password PasswordAuthentication getPasswordAuthentication() { new PasswordAuthentication(this.username, this.password as char[]) } String myGetRequestingPrompt() { super.getRequestingPrompt() } }