require 'net/http' require 'open-uri' require 'uri' require 'rubygems' require 'json' # # = hatena/api/haiku.rb # # Copyright; 2008 ODA Kaname [trashsuite@gmail.com] # See also ; http://d.hatena.ne.jp/trashsuite/ # # Hatena Haiku API の Ruby バインディング # module Hatena module API class Haiku class UserAgent NAME = 'sakura' VERSION = '0.2.7' end # haiku = Hatena::API::Haiku.new(username, password) def initialize(username, password) @username, @password = username, password end attr_reader :username # haiku.public_timeline def public_timeline Statuses.new.public_timeline end # haiku.friends_timeline # haiku.friends_timeline(:id => 'trashsuite') def friends_timeline(options = {}) Statuses.new.other_timeline( Statuses::API_URI[:friends_timeline], options.update({:username => @username, :password => @password}) ) end # haiku.user_timeline # haiku.user_timeline(:id => 'trashsuite') def user_timeline(options = {}) Statuses.new.other_timeline( Statuses::API_URI[:user_timeline], options.update({:username => @username, :password => @password}) ) end # haiku.keyword_timeline(:keyword => 'ひとりごと') def keyword_timeline(options = {}) options[:id] = URI.encode(options[:keyword]) Statuses.new.other_timeline(Statuses::API_URI[:keyword_timeline], options) end # haiku.friends # haiku.friends(:id => 'trashsuite') def friends(options = {}) Statuses.new.other_timeline( Statuses::API_URI[:friends], options.update({:username => @username, :password => @password}) ) end # haiku.followers # haiku.followers(:id => 'trashsuite') def followers(options = {}) Statuses.new.other_timeline( Statuses::API_URI[:followers], options.update({:username => @username, :password => @password}) ) end # haiku.show_entry(:entry_id => '9245604209777696502') def show_entry(options = {}) Statuses.new.show(options) end # haiku.post_entry( # :body => 'エントリ本文', # 必須 # :keyword => 'ひとりごと', # :file => 'ファイル名', # :reply_id => '9245604209777696502' # ) def post_entry(options = {}) Statuses.new.update( options.update({:username => @username, :password => @password}) ) end # haiku.remove_entry(:entry_id => '9245604209777696502') def remove_entry(options = {}) show_entry options Statuses.new.destroy( options.update({:username => @username, :password => @password}) ) end # haiku.add_star(:entry_id => '9245604209777696502') def add_star(options = {}) show_entry options Favorites.new.create( options.update({:username => @username, :password => @password}) ) end # haiku.remove_star(:entry_id => '9245604209777696502') def remove_star(options = {}) show_entry options Favorites.new.destroy( options.update({:username => @username, :password => @password}) ) end # こいつがここに居るのは構成的におかしいので,あとで修正する def self.build_params(options = {}) URI.encode options.map{|k,v|"#{k}=#{v}"}.join('&') end # コマンドラインインターフェイス # '@キーワード名' で,投稿先キーワード切り替え def command require 'readline' agent ||= Hatena::API::Haiku::UserAgent::NAME version ||= Hatena::API::Haiku::UserAgent::VERSION keyword ||= "id:#{username}" prompt ||= '% ' puts "Hatena::API::Haiku(#{agent} v#{version})" unless @interrupt print keyword while line = ::Readline.readline(prompt, true) # 特殊コマンド line.match(/^(exit|quit)|@(.*)|(edit|editor)|(update)$/) case when $1 exit # chkeyword when $2 keyword = $2 line = '' # editor when $3 filename = "/tmp/#{Time.now.to_i}" system "#{ENV['EDITOR']} #{filename}" line = File.exist?(filename) ? File.open(filename).read.strip : '' File.unlink filename if File.exist?(filename) # update when $4 entry = post_entry(:keyword => "id:#{username}", :body => 'tmp') remove_entry :entry_id => entry['id'] line = '' end options = {:keyword => keyword} # リプライ・ファイル添付 line.gsub!(%r=^>http://h.hatena.ne.jp/[^/]*/([0-9]*)$|file:(.*)=) do case when $1 options[:reply_id] = $1 when $2 options[:file] = $2 end nil end print keyword next if line.empty? and !options.has_key?(:file) post_entry options.update({:body => line.strip}) end rescue Interrupt @interrupt = true puts retry end class Statuses API_ROOT = 'http://h.hatena.ne.jp/api/statuses/' API_URI = { :public_timeline => File.join(API_ROOT, 'public_timeline.json'), :friends_timeline => File.join(API_ROOT, 'friends_timeline'), :user_timeline => File.join(API_ROOT, 'user_timeline'), :keyword_timeline => File.join(API_ROOT, 'keyword_timeline'), :friends => File.join(API_ROOT, 'friends'), :followers => File.join(API_ROOT, 'followers'), :show => File.join(API_ROOT, 'show'), :update => File.join(API_ROOT, 'update.json'), :destroy => File.join(API_ROOT, 'destroy'), } def public_timeline JSON.parse open(API_URI[:public_timeline]).read end def other_timeline(uri, options = {}) params = ::Hatena::API::Haiku.build_params({ :since => options[:since], :page => options[:page], :count => options[:count] }) JSON.parse case options[:id].nil? when true open(uri + ".json?#{params}", :http_basic_authentication => [options[:username], options[:password]]).read when false open(uri + "/#{options[:id]}.json?#{params}").read end end def show(options = {}) uri = API_URI[:show] + "/#{options[:entry_id]}.json" JSON.parse open(uri).read rescue Exception => e raise ::Hatena::API::Haiku::EntryGetError, e end def update(options = {}) ::Hatena::API::Haiku::Request.new.post( API_URI[:update], options.update({ :params => { :source => ::Hatena::API::Haiku::UserAgent::NAME, :status => options[:body], :keyword => options[:keyword], :file => options[:file], :in_reply_to_status_id => options[:reply_id] } }) ) end def destroy(options = {}) uri = API_URI[:destroy] + "/#{options[:entry_id]}.json" ::Hatena::API::Haiku::Request.new.post uri, options end end # Statuses class Favorites API_ROOT = 'http://h.hatena.ne.jp/api/favorites/' API_URI = { :create => File.join(API_ROOT, 'create'), :destroy => File.join(API_ROOT, 'destroy') } def create(options = {}) uri = API_URI[:create] + "/#{options[:entry_id]}.json" ::Hatena::API::Haiku::Request.new.post uri, options end def destroy(options = {}) uri = API_URI[:destroy] + "/#{options[:entry_id]}.json" ::Hatena::API::Haiku::Request.new.post uri, options end end class Request def post(uri, options = {}) uri = URI.parse(uri) boundary = "--------------------" + Time.now.to_f.to_s.tr('.', '') header = { 'Content-Type' => "multipart/form-data; boundary=#{boundary}" } Net::HTTP.start(uri.host) do |http| post = Net::HTTP::Post.new(uri.path, header) post.basic_auth options[:username], options[:password] post.body = build_form_data( boundary, options[:params] || {} ) res = http.request(post) raise ::Hatena::API::Haiku::EntryGetError if res.instance_of? Net::HTTPNotFound JSON.parse res.body end end private def build_form_data(boundary, params) file_type = {'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', 'bmp' => 'image/bmp'} enter = "\r\n" data = '' boundary = '--' + boundary params.each do |k,v| next if v.nil? data << boundary + enter data << %Q[Content-Disposition: form-data; name="#{k.to_s}"] if k.to_s == 'file' filename = v.split('/').last mimetype = file_type[v.split('.').last] data << %Q[; filename="#{filename}"] + enter data << %Q[Content-Type: #{mimetype}] + enter + enter data << File.read(v) + enter else data << enter + enter data << v + enter end end data << boundary + '--' end end # Request class EntryGetError < StandardError; end end # Haiku end # API end # Hatena # vim: set expandtab tabstop=2 encoding=utf-8 :