これは圏です(はてな使ったら負けだとおもっていた)

きっと何者にもなれないつぎの読者につづく。

はてブのRubyラッパをつくった

……と書いたところで間違えて確定を押してしまった(爆


えーと、LeopardからのRubyCocoa環境がズバ抜けていいので、試しにソーシャルブックマークサービス用のツールをなんか一つでっち上げちまおうと思いたったわけです。……その前にやることやれよorz

最新のRSSParserが必要なので注意してください。


ダウンロード
動かすと勝手にはてブにwww.example.orgを追加すると云う酷い仕様なので気をつけてください。

(if $0 == __FILE__ 〜 end を書き換えれば大丈夫)


既にありそうな気がしないでもないですが、まあ気にしない気にしない。
ドキュメントとかは気が向いたら。


次の目標は del.icio.us 用のラッパを作ること。それから、重複部分を一つに纏めるところ。
ヒント:仕事が沢山


以下、ソース。



#!/usr/bin/env ruby -Ku
require 'time'
require 'digest/sha1'
require 'base64'
require 'net/http'
require 'rss'
require 'net/http'
require 'uri'


USER = "USER"
PASS = "PASS"


class Object
  def self.protected_attr(*attrs)
    attrs.each{|att|
      define_method(att){
        if @_updating
          instance_variable_get("@#{att}")
        else
          data = instance_variable_get("@#{att}").clone
          if Array === data
            data.map!{|i| i.clone}
          end
          data
        end
      }
      
      define_method("#{att}="){|other|
        if @_updating
          instance_variable_set("@#{att}", other)
        else
          raise "Not in update"
        end
      }
    }
  end
end


class Hatebu
  attr_reader :user, :pass
  include Digest

  def initialize(user, pass)
    @user = user
    @pass = pass
    
    res = get_data("/atom")
    body = res.body
    atom = RSS::Parser.parse(body, false)
    atom.links.each{|item| 
      if item.rel=~ /^service\.(.+)$/
        service = $1
        path = URI.split(item.href)[5]
        instance_variable_set("@#{service}", path)
      end
    }
  end

  def delete(item)
    unless Entry === item
      item = get_entry(item)
    end
    res = send_data(:delete, item.path)
    
    if res.code == "200"
      true
    else
      raise "Couldn't delete entry: #{item.eid}"
    end
  end

  def get_entry(eid)
    Entry.new(self, get_data("/atom/edit/#{eid}").body)
  end

  def make_wsse
    nonce = ''
    20.times{ nonce << rand(256) }
    
    now = Time.now.utc.iso8601
    
    digest = Base64.encode64(SHA1.digest(nonce+now+@pass)).chomp
    return %Q(UsernameToken Username="#{@user}", PasswordDigest="#{digest}", Nonce="#{Base64.encode64(nonce).chomp}", Created="#{now}")
  end

  def send_data(mtd, path, *data)
    data << {"X-WSSE" => make_wsse}
    Net::HTTP.start("b.hatena.ne.jp"){|http|
      http.__send__(mtd, path, *data)
    }
  end

  def get_data(path)
    send_data(:get, path)
  end

  def put_data(path, data)
    send_data(:put, path, data)
  end

  def post_data(path, data)
    send_data(:post, path, data)
  end

  def edit(ident)
    get_data("/atom/edit/#{ident}")
  end

  def post(url, comment="")
    body = <<-EOS
      <entry xmlns="http://purl.org/atom/ns#">
        <title>dummy</title>
        <link rel="related" type="text/html" href="http://www.example.com/" />
        <summary type="text/plain">#{comment}</summary>
      </entry>
    EOS
    res = post_data(@post, body)
    if res.code == "201"
      Entry.new(self, res.body)
    else
      raise "Couldn't post new entry for `#{url}'"
    end
  end
  alias create post
  alias add post

  def entry_from_word(*word)
    entries_from_word(*word)[0]
  end

  def entries_from_word(*word)
    atom = get_data("/#@user/atomfeed?word=#{word.map{|t| URI.escape(t)}.join(';word=')}").body
    feed = RSS::Parser.parse(atom, false)
    feed.entries.map{|ent| Entry.new(self, ent)}
  end

  def entry_from_tag(*tag)
    entries_from_tag(*tag)[0]
  end

  def entries_from_tag(*tag)
    atom = get_data("/#@user/atomfeed?tag=#{tag.map{|t| URI.escape(t)}.join(';tag=')}").body
    feed = RSS::Parser.parse(atom, false)
    feed.entries.map{|ent| Entry.new(self, ent)}
  end

  def entry_from_url(url)
    entries_from_url(url)[0]
  end

  def entries_from_url(url)
    atom = get_data("/#@user/atomfeed?url=#{URI.escape url}").body
    feed = RSS::Parser.parse(atom, false)
    feed.entries.map{|ent| Entry.new(self, ent)}
  end


  def entries
    rss = RSS::Parser.parse(get_data("/#@user/atomfeed").body, false)
    rss.entries.map{|ent| Entry.new(self, ent)}
  end

  class Entry
    attr_reader :issued, :address, :eid, :path, :comment, :url
    protected_attr :title, :tags, :summary

    def initialize(parent, atom)
      @parent = parent
      if String === atom
        entry = RSS::Parser.parse(atom, false)
      else
        entry = atom
      end
      @url = entry.links.find{|i| i.rel=="related"}.href
      @title = entry.title.content
      @summary = entry.summary.content
      @path = URI.split(entry.links.find{|l| l.rel=="service.edit"}.href)[5]
      @eid = @path.split("/")[-1].to_i
      @tags = entry.dc_subjects.map{|i| i.content}
      @_updating = false
    end

    def update(&block)
      if block
        @_updating = true
        block.call(self)
        @_updating = false
      end
      @parent.put_data(@path, <<-EOS)
        <entry xmlns="http://purl.org/atom/ns#">
          <title>#{title}</title>
          <summary type="text/plain">#{tags.map{|t|"["+t+"]"}.join("")}#{summary}</summary>
        </entry>
      EOS
    end
    alias edit update

  end

end

if $0 == __FILE__
  hat = Hatebu.new(USER, PASS)
  p hat.entries
  ent = hat.create("http://www.example.com/", "[test] 試験")
  p ent.tags, ent.title, ent.url, ent.summary
  ent.update{|e|
    e.tags << "web"
    e.summary = "ふかをかけてごめんなさい"
  }
  p hat.entries
  hat.delete ent
end