Ruby増田ライブラリ ver 0.2 0.3

トラックバックまわりを実装。
パーサまわりを修正。

# 
# = masuda.rb
# 
# Copyright (c) 2007 SUGAWARA Genki
# 
# == Example
# 
#     require 'masuda'
#     
#     diary = Masuda::Diary.new
#     diary.entries.each {|entry| puts entry.content }
#     entry = diary.entry('20070712231804')
#     puts <<EOS
#       #{entry.title}
#       #{entry.content}
#     EOS
#     entry.trackbacks.each {|trackback| puts trackback.snippet }
#     
#     diary.login('my_id', 'my_pass')
#     diary.my_entries.each {|entry| puts entry.content }
#     diary.post('Ruby is…', <<EOS)
#     A dynamic, open source
#     programming language with a
#     ...
#     EOS
# 

require 'base64'
require 'cgi'
require 'digest/md5'
require 'net/http'
require 'stringio'
require 'time'

module Masuda
  VERSION = '0.3'

  class Diary
    @@host = 'anond.hatelabo.jp'  
    @@login_host = 'www.hatelabo.jp'
    @@user_agent = "RubyMasudaLibrary/#{VERSION}"

    def initialize
      @cookies = {}
    end

    def proxy(addr, port)
    end

    def login(user, pass)
      res = request('/login', {:mode => 'enter', :key => user, :password => pass, :autologin => 1}, @@login_host)
      set_cookie(res) if '200' == res.code
      logined? ? (@user = user) : nil
    end

    def logout
      @cookies.clear
    end

    def logined?
      %w(rk b).all? {|k| @cookies.has_key?(k) and @cookies[k].valid? }
    end

    def entry(id)
      res = request("/#{id}/")
      return nil unless ('200' == res.code)
      source = res.body.to_a
      et= to_entry(source)
      load_trackbacks(source, self, et)
      et
    end

    def trackbacks(id)
      entry(id).trackbacks
    end

    def entries(page = nil)
      path = page ? "/?page=#{page}" : '/'
      res = request(path)
       ('200' == res.code) ? to_entries(res.body) : nil
    end

    def my_entries(page = nil)
      return nil unless logined?
      path = page ? "/#{@user}/?page=#{page}" : "/#{@user}/"
      res = request(path)
       ('200' == res.code) ? to_entries(res.body) : nil
    end

    def post(title, content)
      return false unless logined?
      res = request("/#{@user}/edit", {:mode => 'confirm',  :rkm => rkm, :id => '', :title => title, :body => content, :edit => "\343\201\223\343\201\256\345\206\205\345\256\271\343\202\222\347\231\273\351\214\262\343\201\231\343\202\213"})
       ('302' == res.code)
    end

    private
    def request(path, params = {}, host = @@host)
      Net::HTTP.version_1_2

      Net::HTTP.start(host, 80) {|http|
        req = Net::HTTP::Post.new(path)
        req['Host'] = host
        req['User-Agent'] = @@user_agent
        req['Cookie'] = @cookies.values.select {|cookie| cookie.apply?(host, path) }.map {|cookie| cookie.header_string }.join('; ')

        req.body = params.map {|k, v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join('&')
        http.request(req)
      }
    end

    def set_cookie(res)
      if (cookies = res.get_fields('set-cookie'))
        cookies.each {|raw_cookie|
          cookie = Cookie.parse(raw_cookie)
          @cookies[cookie.name] = cookie
        }
      end
    end

    def rkm
      return nil unless @cookies.has_key?('rk')
      rk = @cookies['rk'].value
      Base64.encode64(Digest::MD5.digest(rk)).strip.sub(/=+\Z/, '')
    end

    def to_entry(source, date = '')
      source = source.to_a if source.kind_of?(String)

      while line = source.shift
        if line['<span class="date">']
          date.replace(%r|<span class="date">(.+)</span>|.match(line)[1])
        elsif line['<div class="section">']
          header = source.shift
          line = source.shift # skip
          content = StringIO.new

          until line['<p class="sectionfooter">']
            content << line
            line = source.shift
          end

          footer = line  

          id, title = %r|<h3><a href="/([0-9]+)"><span class="sanchor">.+</span></a>(.*)?\Z|.match(header).captures
          title.gsub!('</h3>', '')
          time = %r|([0-9]{2}:[0-9]{2})|.match(footer)[1]

          return Entry.new(self, id, title, content.string, Time.parse("#{date} #{time}"))
        end
      end

      nil
    end

    def to_entries(source)
      source = source.to_a if source.kind_of?(String)
      ets = []
      date = ''

      until source.empty?
        et = to_entry(source, date)
        ets << et if et
      end

      ets
    end

    def load_trackbacks(source, diary, parent)
      source = source.to_a if source.kind_of?(String)
      tbs = []

      while (line = source.shift) and not line['</ul>']
        if line['<li>']
          header = source.shift
          line = source.shift until source.shift['<div class="box-curve">']
          line = source.shift # skip
          snippet = StringIO.new

          until line['<span class="curve-bottom">']
            snippet << line
            line = source.shift
          end

          id = %r|<a href="http://anond.hatelabo.jp/([0-9]+)"|.match(header)[1]
          tbs << (tb = Trackback.new(diary, parent, id, snippet.string))

          until line['</li>']
            load_trackbacks(source, diary, tb) if line['<ul>']
            line = source.shift
          end
        end
      end

      parent.trackbacks = tbs
    end
  end # Diary

  class Entry
    attr_reader :diary, :id, :title, :content, :time
    attr_writer :trackbacks

    def initialize(diary, id, title, content, time)
      @diary = diary
      @id = id
      @title = title
      @content = content
      @time = time
    end

    def trackbacks
      @trackbacks or @trackbacks = diary.trackbacks(@id)
    end
  end # Entry

  class Trackback
    attr_reader :diary, :parent, :id, :snippet
    attr_accessor :trackbacks

    def initialize(diary, parent, id, snippet)
      @diary = diary
      @parent = parent
      @id = id
      @snippet = snippet
    end

    def entry
      @entry or @entry = @diary.entry(@id)
    end
  end # Trackback

  class Cookie
    attr_reader :name, :value, :expires, :domain, :path, :secure

    def initialize(source = {})
      %w(name value expires domain path secure).each {|k|
        next unless source.has_key?(k)
        instance_variable_set("@#{k}", source[k])
      }
    end

    def valid?(now = Time.now)
      not @expires or (now <= @expires)
    end

    def apply?(domain, path, secure = true, now = Time.now)
      valid?(now) and
       (domain.slice(-@domain.length, @domain.length) == @domain) and
       (path.slice(0, @path.length) == @path) and
       (not @secure or secure)
    end

    def header_string
    "#{CGI::escape(@name)}=#{CGI::escape(@value)}"
    end

    def self.parse(raw_cookie)
      source = {}

      raw_cookie.split(/;\s?/).each {|pair|
        key, value = pair.split('=', 2)
        next unless key and value
        key = CGI::unescape(key)
        value = CGI::unescape(value)

        case key
        when 'expires'
          source['expires'] = Time.parse(value)
        when 'domain'
          source['domain'] = value
        when 'path'
          source['path'] = value
        when 'secure'
          source['secure'] = ('true' == value.downcase)
        else
          unless source.has_key?('name')
            source['name'] = key
            source['value'] = value
          end
        end
      }

      Cookie.new(source)
    end
  end # Cookie
end