「ただダラ」Ruby版

一応出来たので貼っておきます。Ruby1.8以降専用になっちゃいましたが…。こんなもんでどうすかねぇ?

id:jounoさんのコメントを見て、そのようにも使えるように機能追加も行ってみました。ありがとうございます。

追加機能の詳しい内容は、あちらの日記を参照してください。

はてダラ」から、こんなところまで来ちゃいましたねぇ…。

#!/usr/bin/env ruby
=begin
hw.rb - tDiary Writer.

Copyright (C) 2004 by Hahahaha.
<rin_ne@big.or.jp>
http://www20.big.or.jp/~rin_ne/

This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
=end

=begin
#
# Configure file example.
id:yourid
password:yourpassword
cgi_url:http://example.com/tdiary/update.rb
# txt_dir:/usr/yourid/diary
# touch:/usr/yourid/diary/hw.touch
# proxy:http://www.example.com:8080/
# g:yourgroup
# client_encoding:Shift_JIS
# server_encoding:UTF-8
## for Unix, if Encode module is not available.
# filter:iconv -f euc-jp -t utf-8 %s
=end

require 'optparse'
require 'uri'
require 'iconv'
require 'stringio'
require 'tempfile'
require 'net/http'

Net::HTTP.version_1_2

Version      = '0.1.0'
ProgramName  = 'tDiary Writer'

Banner       = "#{ProgramName} Version #{Version}\nCopyright (c) 2004 by Hahahaha.\n"

DefaultAgent = "#{ProgramName.tr(' ', '')}/#{Version}"

DATE_FORMAT  = Regexp.new('(\d{4})-(\d\d)-(\d\d)')
CONF_FORMAT  = Regexp.new('(.*?):(.*)$')
  

module MessagePrint
  @@debug       = false
  
  ## Debug flag on.
  def debug_on
    @@debug = true
  end
  
  def debug
    return @@debug
  end
  
  ## Debug print.
  def print_debug(str)
    $stdout.puts("DEBUG: #{str}") if @@debug
  end
  
  ## Error Exit.
  def error_exit(str)
    $stderr.puts("Error: #{str}")
    exit(1)
  end
  
  ## Print message.
  def print_message(str)
    $stdout.puts(str)
  end
end


# Configureuration store class.
#
class Configure
  include MessagePrint
  
  attr_accessor :user             # tDiary admin user id
  attr_accessor :password         # tDiary admin password
  attr_accessor :filter_command   # Filter command.
  
  attr_accessor :config_file      # Configure file.
  attr_accessor :touch_file       # Touch file.
  attr_accessor :target_files     # Target files.
  attr_accessor :target_file      # Target file.
  attr_accessor :target_date      # Target date.
  
  attr_accessor :client_encoding  # Client encoding.
  attr_accessor :server_encoding  # Server encoding.
  
  attr_accessor :agent            # Agent name.
  attr_accessor :timeout          # Timeout.
  
  attr          :txt_dir          # Directory for "YYYY-MM-DD.txt".
  attr          :proxy            # Proxy location.
  attr          :location         # Your tDiary update CGI URL.
  
  attr          :delay            # Delay localtime.
  
  attr_accessor :force_update     # Force update flag.
  attr_accessor :mode             # Action mode.
  
  def initialize
    @user       = nil
    @password     = nil
    @config_file    = 'config.txt'
    @touch_file     = 'touch.txt'
    @target_files   = []
    @target_file    = nil
    @target_date    = nil
    
    @client_encoding  = 'EUC-JP'
    @server_encoding  = 'EUC-JP'
    
    @agent        = "#{ProgramName.tr(' ', '')}/#{Version}"
    @timeout      = 180
    
    @txt_dir      = '.'
    @proxy        = nil
    @location     = nil
    
    @filter_command   = nil
    
    @delay        = 0
    
    @force_update   = false
    @mode       = 'replace'
  end
  
  # Load configuration file.
  #
  def load
    print_debug("Loading config file (#{@config_file}).")
    begin
      File.open(@config_file) {|f|
        f.each {|line|
          next if /^#/.match(line)
          
          m = CONF_FORMAT.match(line)
          next unless m[1].strip
          
          opt = m[1].strip.downcase
          val = m[2].strip
          
          val = nil if val.empty?
          
          case opt
          when "id"
            @user = val unless @user
            print_debug("Configure#load: id:#{@user}")
            
          when "password"
            @password = val unless @password
            print_debug("Configure#load: password:#{@password}")
            
          when "client_encoding"
            @client_encoding = val
            print_debug("Configure#load: client_encoding:#{@client_encoding}")
            
          when "server_encoding"
            @server_encoding = val
            print_debug("Configure#load: server_encoding:#{@server_encoding}")
            
          when "filter"
            @filter_command = val
            print_debug("Configure#load: filter:#{@filter_command}")
            
          when "touch"
            @touch_file = val
            print_debug("Configure#load: touch:#{@touch}")
            
          when "cgi_url"
            @location = URI.parse(val).normalize
            if ((not @location.host) or (not @location.port))
              raise("cgi_url: Invalid URI: '#{val}'")
            end
            print_debug("Configure#load: cgi_url:#{val}")
            
          when "proxy"
            @proxy = URI.parse(val).normalize
            if ((not @proxy.host) or (not @proxy.port))
              raise("proxy: Invalid URI: '#{val}'")
            end
            print_debug("Configure#load: proxy:#{val}")
            
          when "txt_dir"
            unless File.directory?(val)
              raise("txt_dir: No such directory: '#{val}'")
            end
            @txt_dir = val
            print_debug("Configure#load: txt_dir:#{@txt_dir}")
            
          when "delay"
            @delay = val.to_i * 3600
            print_debug("Configure#load: delay:#{val}")
            
          else
            raise("#{opt}: No such option.")
          end
        }
      }
      
      @proxy = URI.parse('') unless @proxy
      
      raise 'cgi_url not found' unless @location
    rescue
      error_exit("[Configure#load] #$! in '#{@config_file}'.")
    end
  end
end

# tDiary Access class.
#
class TDiaryServer
  include MessagePrint
  
  attr  :response
  
  def initialize(config)
    raise(ArgumentError, config.to_s) if config.class != Configure
    
    @request  = nil
    @http     = nil
    @response = nil
  
    @conf = config
    create_http
  end
  
  # Open server.
  #
  def open(method = 'get')
    return if @request
    
    case method.strip.downcase
    when 'get'
      @request = Net::HTTP::Get.new(@conf.location.path)
    when 'post'
      @request = Net::HTTP::Post.new(@conf.location.path)
    when 'head'
      @request = Net::HTTP::Head.new(@conf.location.path)
    when 'put'
      @request = Net::HTTP::Put.new(@conf.location.path)
    end
    
    @request['User-Agent'] = @conf.agent
    
    user = @conf.user
    pass = @conf.password
    
    unless (user)
      print "Username: "
      user = $stdin.gets.chomp
    end
    
    unless (pass)
      print "Password: "
      pass = $stdin.gets.chomp
    end
    
    @request.basic_auth(user, pass)
  end
  
  # Close server.
  #
  def close
    @request = nil
  end
  
  # Send request.
  #
  def request(body = nil)
    open unless @request
    
    if (body.class == Hash)
      body_ary = []
      body.each_pair {|name, val|
        body_ary.push( [name, val].join('=') )
      }
      body = body_ary.join(';')
    end
    
    @request['Content-Type'] = 'application/x-www-form-urlencoded'
    
    2.times {|i|
      begin
        @http.start {|http|
            @response = http.request(@request, body)
        }
      rescue
        if (@response)
          case @response.code
          when 401
            error_exit('[TDiaryServer#request]: Check username/password.')
            
          when 407
            error_exit('[TDiaryServer#request]: Check username/password.')
            
          else
            print_debug('[TDiaryServer#request]: RETRY.')
            print_message('Retry.')
            next
          end
        end
        raise('[TDiaryServer#request]: Network error. Check cgi_url, proxy, timeout and so on.')
      end
      break
    }
    
    if (@response.code.to_i != 200)
      error_exit("[TDiaryServer#request]: ErrorCode = #{@response.code}.")
    end
  end
  
  def send_entry( title, body )
    open unless @request
    
    year, month, day = DATE_FORMAT.match(@conf.target_date).captures
    
    self.request({
      @conf.mode  => @conf.mode,
      'old'   => year + month + day,
      'year'    => year.to_i.to_s,
      'month'   => month.to_i.to_s,
      'day'   => day.to_i.to_s,
      'title'   => URI.escape(title, /./),
      'body'    => URI.escape(body, /./)
    })
  end
  
  # Create HTTP instance.
  #
  def create_http
    p_user, p_pass = @conf.proxy.userinfo.split(':') if @conf.proxy.userinfo
    
    print_debug("use proxy: #{@conf.proxy.normalize}") if @conf.proxy.host
    
    @http = Net::HTTP.new(@conf.location.host, @conf.location.port,
               @conf.proxy.host, @conf.proxy.port,
               p_user, p_pass)
    @http.read_timeout = @conf.timeout
  end
  private :create_http
end


# Main routine.
#
class MainRoutine
  include MessagePrint
  
  def initialize
    @conf   = nil
    @server = nil
  end
  
  def run
    @conf = Configure.new
    
    # Parse ARGV.
    ARGV.options {|opt|
      begin
        opt.banner = Banner + "\n" + "#{opt.banner}"
        opt.summary_width = 16
        
        opt.on( '-d',
            "Debug. Use this switch for verbose log.") {
          unless debug
            debug_on
            print_debug("Debug flag on.")
            print_message(Banner)
          end
        }
        opt.on( '-u username',
            "Username. Specify username.") {|arg|
          @conf.user = arg
        }
        opt.on( '-p password',
            "Password. Specify password.") {|arg|
          @conf.password = arg
        }
        opt.on( '-a agent',
            "User agent. Default value is '#{DefaultAgent}'.") {|arg|
          @conf.agent = arg
        }
        opt.on( '-T seconds',
            "Timeout. Default value is #{@conf.timeout}.") {|arg|
          raise("-T #{arg}: not integer.") if /\d+/ !~ arg
          @conf.timeout = arg.to_i
        }
        opt.on( '-f filename',
            "File. Send only this file without checking timestamp.") {|arg|
          if (DATE_FORMAT !~ File.basename(arg, ".txt"))
            raise("-f #{arg}: Invalid filename format.") 
          end
          raise("-f #{arg}: No such file.") unless File.file?(arg)
          @conf.target_files.push(arg)
          @conf.force_update = true
        }
        opt.on( '-F filename',
            "Nearly -f option, but only one file with arbitrary filename.") {|arg|
          raise("-F #{arg}: No such file.") unless File.file?(arg)
          @conf.target_file = arg
          @conf.force_update = true
        }
        opt.on( '-s [yyyy-mm-dd]',
            "STDIN or -F filename is treated as 'yyyy-mm-dd' or current date.") {|arg|
          if arg
            raise("-s #{arg}: Invalid date format.") if DATE_FORMAT !~ arg
            @conf.target_date = arg
          else
            @conf.target_date = Time.now.localtime
            @conf.mode = 'append'
          end
          @conf.force_update = true
        }
        opt.on( '-n config_file',
            "Config file. Default value is '#{@conf.config_file}'.") {|arg|
          raise("-n #{arg}: No such file.") unless FileTest.file?(arg)
          @conf.config_file = arg
        }
        
        opt.parse!
      rescue
        error_exit($!)
      end
    }
    
    if (@conf.target_file)
      error_exit('Set -s option when -F set.') unless @conf.target_date
    end
    
    @conf.load
    if (@conf.target_date)
      @conf.target_date = (@conf.target_date - @conf.delay).strftime('%Y-%m-%d')
    end
    
    @server = TDiaryServer.new(@conf)
    
    count = 0
    files = setup_file_list
    
    if (files.empty? and @conf.target_date)
      title, body = read_title_body
      
      @server.open('post')
      print_message("Post #{mode}:#{@conf.target_date}")
      @server.send_entry( title, body )
      print_message('Post OK.')
      
      count = 1
    else
      files.each {|file|
        @conf.target_date = File.basename(file.downcase, '.txt') unless @conf.target_date
        
        title, body = read_title_body(file)
        
        @server.open('post')
        print_message("Post replace:#{@conf.target_date}")
        @server.send_entry( title, body )
        print_message('Post OK.')
        
        sleep(1)
        count += 1
      }
    end
    
    @server.close
    
    if (count==0)
      print_message('No files are posted.')
    else
      unless (@conf.force_update)
        begin
          File.open(@conf.touch_file, 'w') {|file|
            file.puts(get_timestamp)
          }
        rescue
          error_exit("[MainRoutine#run] #$!")
        end
      end
    end
  end
  
    # Setup file list.
  def setup_file_list
    files = []
    
    if (@conf.target_file)
      files.push(@conf.target_file)
      print_debug("MainRoutine#setup_file_list: option -F: #{files.join(', ')}")
    elsif (not @conf.target_files.empty?)
      files.concat(@conf.target_files)
      print_debug("MainRoutine#setup_file_list: option -f: #{files.join(', ')}")
    end
    
    if (files.empty?)
      Dir.glob("#{@conf.txt_dir}/*.txt") {|file|
        next if ( File.file?(@conf.touch_file) and
              (File.mtime(file) < File.mtime(@conf.touch_file)) )
        next if DATE_FORMAT !~ File.basename(file.downcase, ".txt")
        files.push(file)
      }
      print_debug("MainRoutine#setup_file_list: " +
            "current dir (#{@conf.txt_dir}) : #{files.join(', ')}")
    end
    
    return files
  end
  
  # Read title and body.
  def read_title_body(file = nil)
    input = nil
  
    begin
      # Read file.
      if (file)
        input = StringIO.new(File.open(file).read)
        print_debug("read_title_body: input: #{file}.");
      else
        input = StringIO.new($stdin.read)
        print_debug("read_title_body: input: STDIN.");
      end
      
      # Execute filter command, if any.
      if (@conf.filter_command)
        tmpfile = Tempfile.open($0)
        tmpfile.write(input.string)
        tmpfile.close
        
        input = StringIO.new(`#{sprintf(@conf.filter_command, tmpfile.path)}`)
        raise('cannot execute filter command.') if ($?.to_i != 0)
        print_debug("read_title_body: execute filter.");
      end
      
      title = convert_to_server(input.gets.chomp)
      body  = convert_to_server(input.read)
      
      return([title, body]);
    rescue
      error_exit("read_title_body: #$!")
    end
  end
  
  # Convert from client to server.
  def convert_to_server(str)
    return '' unless str
    return str if @conf.server_encoding == @conf.client_encoding
    return Iconv.conv(@conf.server_encoding, @conf.client_encoding, str)
  end
  
  # Convert from server to client.
  def convert_to_client(str)
    return '' unless str
    return str if @conf.server_encoding == @conf.client_encoding
    return Iconv.conv(@conf.client_encoding, @conf.server_encoding, str)
  end
  
  # Get "YYYYMMDDhhmmss" for now.
  def get_timestamp
    return Time.now.localtime.strftime('%Y%m%d%H%M%S')
  end
end

begin
  routine = MainRoutine.new
  routine.run
end