#このファイルはGPLで配付しています.
#このプログラムは無保証です.
# connect2ch.rb  
CONNECT2CH_VERSION = 0.15
# 春山 征吾 haruyama@unixuser.org
# $Id: connect2ch.rb,v 1.54 2002/07/01 12:53:06 haruyama Exp $

# EUC版
# まだ適当

#require 'kconv'
require 'nkf'
require 'net/http'
require 'timeout'
begin
  require 'zlib'
  $connect_2ch_gzip = true
rescue LoadError
  #
  $connect_2ch_gzip = false
end

$connect2ch_config={
  'get_proxy_addr' => nil,
  'get_proxy_port' => nil,
  'post_proxy_addr' => nil,
  'post_proxy_port' => nil,
  'http_get_timeout' => 60,
  'user_agent' => "connect2ch.rb #{CONNECT2CH_VERSION}",
  'x_2ch_ua' => "connect2ch.rb #{CONNECT2CH_VERSION}",
}

class CookieExcepton < StandardError

end

class AboneExcepton < StandardError

end


def parse_http_url(url)
  # httpsやftp は別に扱うか
  if %r!^h?ttps?://(\S*?)(?::(\d+))?(/\S*)?$! =~  url
    host = $1
    if $2
      port = Integer($2)
    else
      port = 80
    end
    path = $3
    return host,port,path
  else
    return nil,nil,nil
  end

end


module PostMessage2ch

  def http_encode(str)
    return str.gsub(/[^a-zA-Z0-9_.-]/n){sprintf('%%%02X',$&.unpack('C')[0]) }
  end

  def quote(str)
#まだなし
    return str
  end

  def convert_and_encode(str)
#    str = Kconv::kconv(str,Kconv::SJIS,Kconv::EUC)
    str=NKF::nkf('-sxE',str)
    return http_encode(str)
  
  end


  def post_message(thread,name,mailto,message)
    host = thread.get_host
    port = thread.get_port
    path = thread.get_base_path + 'test/bbs.cgi'

    bbs = thread.get_bbs
    key = thread.get_key
    if port == 80
      tmp  = "http://#{host}/"
    else
      tmp  = "http://#{host}:#{port}/"
    end
    referer = tmp + "#{bbs}/index2.html"
    return post_message_(host,port,path,referer,bbs,key,name,mailto,message)
  end

  def post_message_(host,port,path,referer,bbs,key,name,mailto,message)
    kakikomu='書きこむ'
    kakikomu = convert_and_encode(kakikomu)
    name = convert_and_encode(name)
    mailto = convert_and_encode(mailto)
    message = convert_and_encode(message)

    num_of_trial = 0 
#    time
    time = Time.now.to_i.to_s

    
    header = {}
    header['User-Agent']= $connect2ch_config['user_agent']
    header['X-2ch-UA']= $connect2ch_config['x_2ch_ua']
    header['Referer']= referer
    header['Content-Type']= 'application/x-www-form-urlencoded'
    header['Cookie']= "NAME=#{name};MAIL=#{mailto}"
      
#    Net::HTTP.start(host,port ) {|http|
      # Thanks: >>159
      Net::HTTP.start(host,port,$connect2ch_config['post_proxy_addr'],$connect2ch_config['post_proxy_port']){ |http|
      begin
	
	response , = http.post( path,
			       "submit=#{kakikomu}&FROM=#{name}&mail=#{mailto}&MESSAGE=#{message}&bbs=#{bbs}&key=#{key}&time=#{time}",
			       header)


	# なにか失敗があれば return false

	body = NKF::nkf('-exS',response.body)
	if response.code == '200'
	  # navi2ch 1.5.1 より
	  if /書きこみました。/ =~ body
	    return true, '書き込み成功(1)'
	  elsif /ＥＲＲＯＲ：([^<]+)/ =~ body
	    STDERR.print "書きこみエラー: #{$1}\n"
	      return false,"書きこみ失敗: #{$1}"
# Thanks: 222さん
	  elsif /クッキーがないか期限切れです/ =~ body
	    STDERR.print body
	    STDERR.print "\n\n"
	    STDERR.print response['Set-Cookie']
	    STDERR.print "\n\n"
	    
	    num_of_trial += 1 
	    if num_of_trial < 2
	      header['Cookie'] += ';' + response['Set-Cookie']
	      STDERR.print "クッキーを設定して再書きこみしてみます.\n"
	      raise CookieExcepton
	    end
	    return false,'書きこみエラー: クッキーがないか期限切れです'
	  else
	    STDERR.print "原因不明の書きこみエラー(1)\n"
	    STDERR.print body
	    return false,'原因不明の書きこみエラー(1)'
	  end
	else
	  #	  STDERR.print Kconv::kconv(response.body,Kconv::EUC,Kconv::SJIS)
	  STDERR.print "原因不明の書きこみエラー(2)\n"
	  STDERR.print body
	  return false,'原因不明の書きこみエラー(2)'
	end
      rescue Net::ProtoRetriableError
      # リダイレクトページだと出るようだ
=begin
	 if m = %r<http://([^/]+)>.match( err.response['location'] ) then
	   host = m[1].strip
	   path = m.post_match
	   retry
	 else
	   print "リダイレクトページ。書きこまれたかどうかわかりません。\n"
	   return false
	 end
=end
      # きちんと書きこまれたからリダイレクトページにいったんだと
      # 解釈
      # 上のを処理したら失敗になるだろうし…
	return true, 'リダイレクトページ--おそらく書きこみ成功--要確認(2)'
      #	return true
      rescue CookieExcepton
	STDERR.print "CookieExceptonをrescue\n"
	retry
      rescue
	print "例外発生。書きこまれたかどうかわかりません。\n"
	return false,'例外発生。書きこまれたかどうかわかりません'
      end
    }
  end


end


module HTTPGetGzip

  def http_get_file_with_info(host,port,path,filename,info_filename)
#,gzip=true)
#    p host,port,path
    header = {}
=begin
    if gzip
      gzip =  $connect_2ch_gzip
    end

    if gzip
      header['Accept-Encoding']='gzip'
    end
=end
    header['User-Agent']= $connect2ch_config['user_agent']
    header['X-2ch-UA']= $connect2ch_config['x_2ch_ua']

    last_modifiled = ''
    content_length = 0

    begin
      info_file = open( info_filename, 'r' )
    rescue 
#      STDERR.print "#{info_filename}が開けませんでした(1)。\n"    
#      return nil,"#{info_filename}が開けませんでした"
    end
    if info_file
      info = info_file.read
      if /Last-Modified:(.*)/ =~ info
	last_modifiled = $1.strip
      end
      info_file.close
    end
    
    if File.exist?(filename)
      content_length = File.stat(filename).size
    end

    if content_length > 0 && last_modifiled != ''
      header['Range'] = "bytes=#{content_length-1}-"
      header['If-Modified-Since'] = last_modifiled
#      header['If-Range'] = last_modifiled
    end
#    p header['Range']
#    p last_modifiled

    file = nil
    begin
      timeout($connect2ch_config['http_get_timeout']){
	Net::HTTP.start(host,port,$connect2ch_config['get_proxy_addr'],$connect2ch_config['get_proxy_port']){ |http|
	  response , = http.get(path,header)


#	  p response.code 
	  case response.code 
	  when '304' #Not Modified)
	    begin
	      file = open( filename, 'r' )
	    rescue 
	      STDERR.print "#{filename}が開けませんでした。\n"    
	      return nil,"#{filename}が開けませんでした"
	    end

	    return file,nil
	  when '206' # partial get
#	    p response.body.size
#	    p response.body
#	    p  response.body[0]
	    if response.body[0] == 10
	      begin
		file = open( filename, 'a+' )
	      rescue 
		STDERR.print "#{filename}が開けませんでした。\n"    
		return nil,"#{filename}が開けませんでした"
	      end
#	      file.write response.body[1,response.body.size-1]
	      tmp  = response.body[1,response.body.size-1]
	      if tmp
		file.write tmp
	      end
	    else
	      # あぼーん有りの疑い
	      raise AboneExcepton
	    end
	  when '200'
	    begin
	      file = open( filename, 'w+' )
	    rescue 
	      STDERR.print "#{filename}が開けませんでした。\n"    
	      return nil,"#{filename}が開けませんでした"
	    end
	    file.write response.body
	  else
	    STDERR.print "HTTPのエラー #{response.code}\n"    
	    return nil,"HTTPのエラー #{response.code}\n"
	  end
#	  file.rewind
	  if response.header
	    begin
	      info_file = open( info_filename, 'w+' )
	    rescue 
	      STDERR.print "#{info_filename}が開けませんでした(2)。\n"    
	      return nil,"#{info_filename}が開けませんでした(1)"
	    end
#	    info_file.truncate(0)
	    if response.header['Last-Modified']
#	      p response.header['Last-Modified'] 
	      info_file.write 'Last-Modified: '
	      info_file.write response.header['Last-Modified'] 
	      info_file.write "\n"
	    end
	  end
	  info_file.close
=begin
	  if  gzip == true && response['Content-Encoding'] == 'gzip'
	    begin 
	      file=Zlib::GzipReader.new(file)
	    rescue
	    end
	  end
=end
	  file.rewind
	  return file,nil	    
	}
      }
    rescue AboneExcepton
      header.delete('Range')
      header.delete('If-Modified-Since')
      header.delete('If-Range')
      retry
    rescue Net::ProtoRetriableError => err
      if m = %r<http://([^/]+)>.match( err.response['location'] ) then
	# おそらく意味ないが一応
#	host = m[1].strip
#	path = m.post_match
#	p host 
#	p path
#	retry
	return nil,'ファイルの取得に失敗(3,転送URL(threadならdat落ちか?))'
      end
      return nil,nil
    rescue Errno::ECONNREFUSED
      STDERR.print "Errno::ECONNREFUSED が起りました。\n"    
      STDERR.print "サーバないしProxyが動いていないのかもしれません。\n"    
      STDERR.print "ファイルの取得に失敗(1)。\n"    
      return nil,'ファイルの取得に失敗(1)'
    rescue  TimeoutError
      STDERR.print "タイムアウトしました\n"
      return nil,'タイムアウトしました'
      
    rescue => evar
      p evar
      STDERR.print "ファイルの取得に失敗(2)。\n"    
      return nil,'ファイルの取得に失敗(2)'
    end

    #    STDERR.print "タイムアウトしました\n"
    return nil,nil
  end

  def http_get_file(host,port,path,filename,gzip=true)

    header = {}
    if gzip
      gzip =  $connect_2ch_gzip
    end

    header['User-Agent']= $connect2ch_config['user_agent']
    header['X-2ch-UA']= $connect2ch_config['x_2ch_ua']
    
    if gzip
      header['Accept-Encoding']='gzip'
    end
# Thanks >>7
    file = nil
    begin
      timeout($connect2ch_config['http_get_timeout']){
	Net::HTTP.start(host,port,$connect2ch_config['get_proxy_addr'],$connect2ch_config['get_proxy_port']){ |http|
	  response , = http.get(path,header)
	  begin
	    file = open( filename, 'w+' )
	  rescue 
	    STDERR.print "#{filename}が開けませんでした。\n"    
	    return nil,"#{filename}が開けませんでした"
	  end
	  file.write response.body
	  file.rewind
	  if  gzip == true && response['Content-Encoding'] == 'gzip'
	    begin 
	      file=Zlib::GzipReader.new(file)
	    rescue
	    end
	  end
	  return file,nil	    
	}
      }
    rescue Net::ProtoRetriableError => err
#      p err
      if m = %r<http://([^/]+)>.match( err.response['location'] ) then
	# おそらく意味ないが一応
#	host = m[1].strip
#	path = m.post_match
#	retry
	return nil,'ファイルの取得に失敗(3,転送URL(threadならdat落ちか?))'
      end
      return nil,nil
    rescue Errno::ECONNREFUSED
      STDERR.print "Errno::ECONNREFUSED が起りました。\n"    
      STDERR.print "サーバないしProxyが動いていないのかもしれません。\n"    
      STDERR.print "ファイルの取得に失敗。\n"    
      return nil,'ファイルの取得に失敗(1)'
    rescue  TimeoutError
      STDERR.print "タイムアウトしました\n"
      return nil,'タイムアウトしました'
    rescue => evar
      p evar
      STDERR.print "ファイルの取得に失敗。\n"    
      return nil,'ファイルの取得に失敗(2)'
    end
  end
end


module GetThread2ch
  include HTTPGetGzip

  def file_change(from,to)
    File.rename(from,to)

    begin
      file =  open(to,'r')
    rescue
      STDERR.print "#{to}が開けませんでした。\n"    
      return nil,'#{to}が開けませんでした'
    end


    begin
      file = Zlib::GzipReader.new(file)
    rescue
    end
    return file,nil
  end

  def get_thread_file(thread,size=0,num_of_statements=0)
    host = thread.get_host
    port = thread.get_port
    path = thread.get_path
    filename = thread.get_filename
    file = thread.get_file    
    dat=  thread.get_dat
    return get_thread_file_(host,port,path,filename,file,dat,size,num_of_statements)
  end


  def get_thread_file_(host,port,path,filename,file,dat=nil,size=0,num_of_statements=0)

    if file 
      file.rewind
    else
      if File.exist?(filename)
	begin
	  file = open( filename, 'r' )
	rescue
	  STDERR.print "#{filename}が開けませんでした。\n"   
	  return nil,nil
	end
	begin
	  file = Zlib::GzipReader.new(file)
	rescue  
	  # gzipでない場合
	end
      end
    end


    #差分読みこみ用準備 差分の処理はかなりいいかげん

    if size <=0 && num_of_statements <=0
      size =0
      num_of_statements=0
      if file 
	file.rewind
	begin
	  file.each{ |line|
#	    line=Kconv::kconv(line,Kconv::EUC,Kconv::SJIS)
	    line=NKF::nkf('-exS',line)
	    if /^(?:\+OK|-INCR)\s+(\d+)/ =~ line
	      size+=Integer($1)
	    else
	      num_of_statements+=1
	    end

	  }
	rescue Zlib::StreamError
	  STDERR.print "Zlib::StreamError です。読みこみを中止します\n"
#	  return nil
	  size=0
	  num_of_statements=0
	rescue
	  STDERR.print "予期しないエラーです。読みこみを中止します\n"
#	  return nil
	  size=0
	  num_of_statements=0
	end

      end

    end



    if dat
      # dat直読み
      file,error = get_thread_file_via_dat(host,port,path,filename)
      return file,num_of_statements,error
    else
      return get_thread_file_via_read_cgi(host,port,path,filename,file,size,num_of_statements)
    end

  end

#未テスト
  def get_thread_file_via_dat(host,port,path,filename)
    
#    file,error = http_get_file(host,port,path,filename)
#    file,error = http_get_file(host,port,path,filename,filename + "_info")
    file,error = http_get_file_with_info(host,port,path,filename,filename + "_info")
    if file == nil 
      if error
	STDERR.print error
	STDERR.print "\ndatファイルの取得に失敗しました。\n"
      end
      if File.exist?(filename)
	if error
	  STDERR.print "キャッシュを利用します。\n"
	  error += ' キャッシュを利用します。'
	end
	begin
	  file = open( filename, 'r' )
	rescue
	  STDERR.print "#{filename}が開けませんでした。\n"   
	  return nil,"#{filename}が開けませんでした"
	end
	begin
	  file = Zlib::GzipReader.new(file)
	rescue  
	  # gzipでない場合
	end
      else
	STDERR.print "キャッシュもありません。\n"
	error += 'キャッシュもありません。'
	return nil,error
      end

    end
    return file,error
    
  end

  def get_thread_file_via_read_cgi(host,port,path,filename,file,size=0,num_of_statements=0)


    if path =~ /\?/
      path += "&raw=#{num_of_statements}.#{size}"
    else
      path += "?raw=#{num_of_statements}.#{size}"
    end

    tmp_filename = filename+"_tmp"
    tmp_file,error= http_get_file(host,port,path,tmp_filename)

    if tmp_file ==nil
      STDERR.print "thread情報ファイルの取得に失敗しました。\n"
      if file
	STDERR.print "キャッシュを利用します.\n"
	return file,nil,'thread情報ファイルの取得に失敗しました。キャッシュを利用します'
      else
	STDERR.print "キャッシュもありません.\n"
	return nil,nil,'thread情報ファイルの取得に失敗しました。キャッシュもありません'
      end
    end
    

    begin

      line= tmp_file.gets
      #    line=Kconv::kconv(line,Kconv::EUC,Kconv::SJIS)
      line=NKF::nkf('-exS',line)
      #TODO: サイズが0のときは追加しないようにする

      if /^\+OK\s+(\d+)/ =~ line
	if $1 == '0'
	  tmp_file.close
	  File.unlink(tmp_filename)
	  return file,num_of_statements,nil
	end

	tmp_file.rewind
	if file 
	  begin
	    file.rewind
	    contents = file.read
	  rescue 
	    # Zlib::BufError が出たことが
	    file.close
	    File.link(filename)
	    tmp_file.close	  
	    File.unlink(tmp_filename)
	    STDERR.print "#{filename}の読みこみに失敗したのでファイルを消しました。再試行すれば直っているかもしれません\n"
	    return nil,nil,"#{filename}の読みこみに失敗したのでファイルを消しました。再試行すれば直っているかもしれません"
	  end
	  file.close
	  # fileがかならずしもgzipで圧縮されているとはかぎらないので
	  # 面倒なことをする
	  # また GzipReaderにはrewindはあってもseekはないし
	  # (gzipのヘッダ フッタのためだろう)
	  begin
	    file = open(filename,'w')
	  rescue
	    STDERR.print "#{filename}が開けませんでした(1)。\n"    
	    return nil,nil,"#{filename}が開けませんでした(1)"
	  end

	  file = Zlib::GzipWriter.new(file)
	  
	  file.write  contents
	  file.write  tmp_file.read
	  
	  file.close
	  tmp_file.close

	  File.unlink(tmp_filename)

	  begin
	    file =  open(filename,'r')
	  rescue 
	    STDERR.print "#{filename}が開けませんでした(2)。\n"    
	    return nil,nil,"#{filename}が開けませんでした(2)"
	  end
	  

	  file = Zlib::GzipReader.new(file)
	else
	  tmp_file.close
	  file,error = file_change(tmp_filename,filename)
	  unless file 
	    return nil,nil,error
	  end
	end
      elsif  /^\-INCR/ =~ line
	if file
	  file.close
	end
	tmp_file.close

	file,error = file_change(tmp_filename,filename)
	unless file 
	  return nil,nil,error
	end

      else
	# Thanks: 136さん
	STDERR.print "thread情報ファイルに異常があります。dat落ちしている可能性が高いです。\n"
	tmp_file.close
	File.unlink(tmp_filename)
	return file,nil,'thread情報ファイルに異常があります。dat落ちしている可能性が高いです'
      end
    rescue
      STDERR.print "原因不明の例外発生。手でthreadファイル #{filename} を削除する必要があるかもしれません。\n"
      return nil,nil,'原因不明の例外発生。手でthreadファイル #{filename} を削除する必要があるかもしれません'      
    end
    return file,num_of_statements,nil
  end


end

module  GetBoardSubject2ch
  include HTTPGetGzip


  def get_board_subject_file(board)
    host = board.get_host
    port = board.get_port
    path = board.get_path
    filename = board.get_filename

    if board.get_dat
      return get_board_subject_file_dat(host,port,path,filename)
    else
      return get_board_subject_file_read_cgi(host,port,path,filename)
    end
  end

  def get_board_subject_file_read_cgi(host,port,path,filename,ls=nil)
    # lsは未テスト
    if %r!^(.*)(/[^/]+/)$! =~ path
      path2 = $1 + '/test/read.cgi' + $2 + '?raw=0.0'
      if ls 
	path2 += "&ls=#{ls}"
      end
    else
      return get_board_subject_file_(host,port,path,filename,nil)
    end
    file,error=http_get_file(host,port,path2,filename)
    return get_board_subject_file_(host,port,path,filename,file)
  end

  def get_board_subject_file_dat(host,port,path,filename)
    
    path2 = path + 'subject.txt'
#    p path2
    file,error=http_get_file(host,port,path2,filename)
    return get_board_subject_file_(host,port,path,filename,file)
  end

  def get_board_subject_file_(host,port,path,filename,file)
    # Thanks >>8:
    error = nil
    if file == nil
      STDERR.print "subjectファイルの取得に失敗しました。\n"

      error = 'subjectファイルの取得に失敗しました。'
      if File.exist?(filename)
	STDERR.print "キャッシュを利用します.\n"
	begin
	  file = open( filename, 'r' )
	rescue
	  STDERR.print "#{filename}が開けませんでした。\n"   
	  return nil,error + " キャッシュファイル #{filename} が開けませんでした"   
	end
	begin
	  file = Zlib::GzipReader.new(file)
	rescue  
         # gzipでない場合
	end
	return file,error +  ' キャッシュを利用します'
      else
	STDERR.print "キャッシュもありません.\n"
      end

      return nil, error + ' キャッシュもありません'
    end

    

    return file,nil

  end

end


module  GetBoardInfo2ch
  include HTTPGetGzip

  def get_board_info(host,port,path)
#     Net::HTTP.start(host,port,$proxy_addr,$proxy_port){ |http|
    Net::HTTP.start(host,port,$connect2ch_config['get_proxy_addr'],$connect2ch_config['get_proxy_port']){ |http|
      response,= http.get(path,nil)

      if response.code == '200'
	return response.body
      else
	return nil
      end
    }
  end

  def parse_board_info_html(body)

    flag = 0
    info = []

    body=NKF::nkf('-ex',body)
    body.each { |line|
      while true
#	line=Kconv::toeuc(line)
#	line=NKF::nkf('-ex',line)
#	p line
	line.strip!
	if /^<B>(.*?)<\/B><BR>(.*)$/i =~  line
	  tmp = 'C ' + $1 +"\n"
	  if 'チャット' == $1
	    return info
	  elsif 'まちＢＢＳ' == $1
	    flag=0
	  else
	    flag=1
	    info << tmp
	  end
	  line = $2
	elsif /^<A HREF=([^> ]*).*?>(.*?)<\/A>(.*)$/i =~ line
	  url = $1
	  board = $2
	  line = $3

	  if url != 'http://www.machibbs.com/' && url != 'http://dempa.gozans.com/' && url != 'http://2ch.tanteifile.com'
#	    if flag ==1
#	      tmp = 'B '
#	    elsif flag ==2
#	      tmp = 'A ' 
#	    end
	    tmp = 'A ' 
	    if flag > 0 
#	      tmp1 = $1
#	      tmp2 = $2
#	      tmp2.gsub!(/<br>/i,' ')
#	      board.gsub!(/<br>/i,' ')
	      tmp += url + ' ' + board.gsub(/<br>/i,' ')  + "\n"
	      info << tmp
	    end
	  end
	elsif /^.+?((<B>|<A).*)$/i =~ line
	  line = $1
	else
	  break
	end
      end
    }
    return info
  end

end
