mixi api 使ってみたよ

今更ですか!?という感じもしますが、mixi for iPhoneから発掘されたmixi日記投稿用API « kuで紹介されていたAPI を使ってmixi に日記を投稿するruby スクリプトを書きました。

使い方

  • 環境
  • API の認証にWSSE を使っているので、WSSE 認証に使うgem を入れる

$ sudo gem install wsse

  • 使う
mixi = Mixi.new('hoge@address.com', 'password').authenticate
mixi.post_diary('タイトル', '内容')

ソース

参考


wig.rb は、id:cho45 さん作のnet-irc モジュールの中に含まれているやつです。WassrIrcGateway#api メソッド周りの実装に感動しました。凄い勉強になります。

# -*- coding: utf-8 -*-
require 'net/http'
require 'nkf'
require 'rexml/document'
require 'rubygems'
require 'uri'
require 'wsse'

Net::HTTP.version_1_2

class Mixi
  def self.base_uri
    URI('http://mixi.jp/')
  end
  
  attr_reader :member_id, :nickname, :mailaddress, :password
  
  def initialize(mailaddress, password)
    @mailaddress = mailaddress
    @password = password
  end
  
  def authenticate
    begin
      api('updates', Net::HTTPOK) do |res|
        doc = REXML::Document.new(res.body)

        @nickname = 
          doc.elements['/service/workspace/atom:author/atom:name'].text      

        @member_id = 
          doc.elements['/service/workspace/atom:author/atom:uri'].text.match(/id=(\d+)/).to_a[1]    
      end
      
      self
    rescue ApiFailure 
      false
    end
  end

  def post_diary(title, content)
    body = <<-XML
<?xml version='1.0' encoding='utf-8'?>
  <entry xmlns='http://purl.org/atom/ns#'>
    <title>#{NKF.nkf('-w', title)}</title>
    <summary>#{NKF.nkf('-w', content)}</summary>
  </entry>
XML

    begin 
      api("diary/member_id=#{@member_id}", Net::HTTPCreated, body) do
        notify('日記を投稿しました')
      end
    rescue ApiFailure
      notify('日記の投稿ができませんでした')
    end
  end

  private
  
  def require_post?(path)
    !!Regexp.union(%r|/atom/diary/member_id=\d+|).match(path)
  end
  
  def api(point, success_code, query = nil)
    uri = self.class.base_uri
    uri.path = "/atom/#{point}"
    
    if require_post?(uri.path)
      req = Net::HTTP::Post.new(uri.path)
      req.body = query
    else
      if query
        uri.query = query.map{|k, v| "#{k}=#{URI.encode(v)}"}.join('&')
      end
      
      req = Net::HTTP::Get.new(uri.request_uri)
    end
    
    req['X-WSSE'] = WSSE::header(@mailaddress, @password)
        
    Net::HTTP.start(uri.host) do |http|
      res = http.request(req)
      
      case res
      when success_code
        yield(res)
      else
        raise ApiFailure, "#{res.code}: #{res.message}"
      end
    end
  end
  
  def notify(msg)
    puts msg
  end
  
  class ApiFailure < StandardError; end
end

メモ

Atom Publishing Protocol
WSSE 認証
  • Atom API の認証に使われる
  • Basic 認証よりセキュア
  • X-WSSE ヘッダとして送信する
  • 構成要素
    • Username
      • ユーザー名。今回の場合はmixi に登録してあるメールアドレス
    • Nonce
      • HTTPリクエスト毎に生成したランダムな文字列をBase64 エンコードしたもの
      • クライアント側で生成する
      • Created との関係か、現在の時間を使って文字列を生成する例が多いが、通常ランダムな16進数を使うらしい。ランダムな文字列ならなんでもよいっぽい
    • Created
      • Nonceが作成された日時をISO-8601表記で記述したもの
    • PasswordDigest
  • 参考
  • ruby での実装例(wsse のgem を参考)
def wsse(username, password)
  #セキュリティトークンの例として、ランダムな16進数のバイナリ表現
  nonce = Array.new(10){rand(0x100000000)}.pack('I*')
  created = Time.now.utc.iso8601
  pass_digest = 
    [Digest::SHA1.digest(nonce + created + password)].pack('m').chomp

  %W(UsernameToken\ Username="#{username}"
     PasswordDigest="#{pass_digest}"
     Nonce="#{[nonce].pack('m').chomp}"
     Created="#{created}").join(', ')
  end
end
wig.rb
  • base_uri を文字列でなく、URI のオブジェクトとして定義してある。これによって、path とかquery をそこに追加していくことができる。追加していく様子がかっこいい
  • post とget はNet::HTTP::(Get|Post) のオブジェクトを生成することで分けている。リクエスト自体は、Net::HTTP#request メソッドで一元化している。Net::HTTP#(get|post) でわけてないので統一されて見える。メソッドで分けると引数の形式異なるしね
  • post が必要なAPI はrequire_post? メソッドにエンドポイントのpath を送って判定
  • ここかっこいい。仕様にきちんとそうようにメソッドを使い分けている
if require_post?(path)
  req = Net::HTTP::Post.new(uri.path)
  req.body = uri.query
else
  req = Net::HTTP::Get.new(uri.request_uri)
end
    • post の場合は、クエリがURL に含まれることはないのd、uri.path を使っている
      • データはbody に入れる
    • get の場合はクエリがURL に含まれるのでrequest_uri を使っている
  • API のエラーは「raise ApiFailed, "#{ret.code}: #{ret.message}"」のようにして処理