ruby/xmpp/examples/advanced/fileserve.rb
#!/usr/bin/env ruby

require 'yaml'
require 'xmpp4r'
require 'xmpp4r/bytestreams'
require 'xmpp4r/roster'
require 'xmpp4r/version'

Jabber::debug = true

def human_readable(num)
  unit = ''
  units = %w(K M G T P E)
  while num > 10240 and units.size > 0
    num /= 1024
    unit = units.shift
  end
  "#{num} #{unit}B"
end

class Transfer
  attr_reader :peer, :filename, :bytes, :filesize

  def initialize(filetransfer, peer, filename, filesize, msgblock)
    @filetransfer = filetransfer
    @peer = peer
    @filename = filename
    @filesize = filesize
    @msgblock = msgblock

    @done = false
    @bytes = 0
    @stats = [0]
    @stats_lock = Mutex.new
    @stats_thread = Thread.new { stats_loop }


    @streamhost_cb = lambda { |streamhost,state,e|
      case state
        when :connecting
          say "Connecting to #{streamhost.jid} (#{streamhost.host}:#{streamhost.port})"
        when :success
          say "Successfully using #{streamhost.jid} (#{streamhost.host}:#{streamhost.port})"
        when :failure
          say "Error using #{streamhost.jid} (#{streamhost.host}:#{streamhost.port}): #{e}"
      end
    }
  end

  def done?
    @stats_thread.kill if @done
    @done
  end

  def stats_loop
    loop {
      sleep 1
      @stats_lock.synchronize {
        @stats.push @bytes
        @stats.shift if @stats.size > 5
      }
    }
  end

  def stats
    @stats_lock.synchronize {
      if @stats.first >= @stats.last
        0
      else
        (@stats.last - @stats.first) / (@stats.size - 1)
      end
    }
  end

  def say(text)
    @msgblock.call(text)
  end

  def transfer(from, to)
    while buf = from.read
      @bytes += to.write buf
    end
  end
end

class Upload < Transfer
  def initialize(filetransfer, iq, filename, filesize, can_range, msgblock)
    super(filetransfer, iq.from, filename, filesize, msgblock)

    if filename.size < 1
      say "What is this file for?"
      @done = true
      return
    end

    offset = nil
    if File::exist?(filename)
      if File::size(filename) < filesize and can_range
        @bytes = offset = File::size(filename)
        say "Will retrieve #{filename} starting at #{offset}"
      else
        say "#{filename} already exists"
        filetransfer.decline(iq)
        @done = true
        return
      end
    end

    Thread.new {
      begin
        stream = filetransfer.accept(iq, offset)
        if stream.kind_of?(Jabber::Bytestreams::SOCKS5Bytestreams)
          stream.connect_timeout = 5
          stream.add_streamhost_callback(nil, nil, &@streamhost_cb)
        end

        if stream.accept
          outfile = File.new(filename, (offset ? 'a' : 'w'))

          transfer(stream, outfile)

          outfile.close
          stream.close
          @done = true
        else
          say "Stream failed"
          @done = true
        end
      rescue
        puts $!.backtrace.first
        say "Error: #{$!}"
        @done = true
      end
    }
  end
end

class Download < Transfer
  def initialize(filetransfer, peer, filename, msgblock, socksconf)
    begin
      filesize = File.size(filename)
    rescue
      filesize = 0
    end

    super(filetransfer, peer, filename, filesize, msgblock)

    Thread.new {
      begin
        raise "No regular file" unless File.file?(filename)

        source = Jabber::FileTransfer::FileSource.new(filename)
        stream = filetransfer.offer(peer, source)
        unless stream
          raise "Well, you should accept what you request..."
          @done = true
        end

        if stream.kind_of?(Jabber::Bytestreams::SOCKS5Bytestreams)
          socksconf.call(stream)
          stream.add_streamhost_callback(nil, nil, &@streamhost_cb)
        end

        stream.open
        transfer(source, stream)
        stream.close
        @done = true
      rescue
        say "Error: #{$!}"
        @done = true
      end
    }
  end
end

class FileServe
  def initialize(conf)
    @uploads = 0
    @downloads = 0

    @transfers = []
    @transfers_lock = Mutex.new

    @client = Jabber::Client.new Jabber::JID.new(conf['jabber']['jid'])
    @client.connect
    @client.auth(conf['jabber']['password'])

    @ft = Jabber::FileTransfer::Helper.new(@client)
    Jabber::Version::SimpleResponder.new(@client,
                                       "XMPP4R FileServe example",
                                       Jabber::XMPP4R_VERSION,
                                       IO.popen('uname -sr').readlines.to_s.strip)
    register_handlers

    @directory = conf['directory']
    @directory.gsub!(/\/+$/, '')

    @socksserver = Jabber::Bytestreams::SOCKS5BytestreamsServer.new(conf['socks']['port'])

    conf['socks']['addresses'].each { |addr| @socksserver.add_address(addr) }

    @proxies = []
    conf['socks']['proxies'].collect { |jid|
      puts "Querying proxy #{jid}..."
      begin
        @proxies.push(Jabber::Bytestreams::SOCKS5Bytestreams::query_streamhost(@client, jid))
      rescue
        puts "Error: #{$!}"
      end
    }

    Thread.new { presence }
    Thread.new { cleaner }

    # Panic reboot ;-)
    @client.on_exception { initialize(conf) }
  end

  def presence
    rate_upload = 0
    rate_download = 0

    old_status = nil
    loop {
      # Update the @rate_* variables
      @transfers_lock.synchronize {
        @transfers.each { |t|
          if t.kind_of?(Upload) and t.stats > rate_upload
            rate_upload = t.stats
          elsif t.kind_of?(Download) and t.stats > rate_download
            rate_download = t.stats
          end
        }
      }
      status = "Attempted #{@downloads} downloads (max. #{human_readable rate_download}/s) and #{@uploads} uploads (max. #{human_readable rate_upload}/s)"

      @client.send(Jabber::Presence.new(:chat, status)) if status != old_status
      old_status = status

      sleep 1

    }
  end

  def register_handlers
    @client.add_message_callback { |msg|
      if msg.type == :chat and msg.body and msg.from != 'pentabarf@pentabarf.org/rails'
        puts "<#{msg.from}> #{msg.body.strip}"
        cmd, arg = msg.body.split(/ /, 2)

        command(msg.from, cmd, arg)
      end
    }

    @ft.add_incoming_callback { |iq,file|

      say = lambda { |text|
        say(iq.from, text)
      }

      puts "Incoming file transfer from #{iq.from}: #{file.fname} (#{file.size / 1024} KB)"
      filename = file.fname.split(/\//).last
      filename.gsub!(/^\.+/, '')

      puts "Range: #{file.range != nil}"
      transfer = Upload.new(@ft, iq, "#{@directory}/#{filename}", file.size, file.range != nil, say)
      @uploads += 1

      @transfers_lock.synchronize {
        @transfers.push(transfer)
      }

    }

    roster = Jabber::Roster::Helper.new(@client)

    roster.add_subscription_request_callback { |item,presence|
      roster.accept_subscription(presence.from)
    }

  end

  def command(from, cmd, arg)
    say = lambda { |text|
      say(from, text)
    }
    socksconf = lambda { |stream|
      stream.add_streamhost(@socksserver)
      @proxies.each { |sh|
        stream.add_streamhost(sh)
      }
    }

    case cmd
      when 'get'
        arg.gsub!(/\//, '')
        arg.gsub!(/^\.+/, '')
        transfer = Download.new(@ft, from, "#{@directory}/#{arg}", say, socksconf)
        @downloads += 1
        @transfers_lock.synchronize {
          @transfers.push(transfer)
        }
      when 'ls'
        text = ""
        Dir.foreach(@directory) { |file|
          next if file =~ /^\./
          path = "#{@directory}/#{file}"
          text += "#{file} (#{human_readable File.size(path)})\n" if File.file?(path)
        }
        say.call(text.strip)
      when 'stat'
        @transfers_lock.synchronize {
          text = "#{@transfers.size} transfers:\n"
          @transfers.each { |t|
            text += "#{t.filename} (#{t.peer}): #{(t.bytes * 100) / t.filesize}% (#{human_readable t.stats}/s)\n"
          }
        }
        say.call(text.strip)
      when 'help'
        say.call "Download a file: get <filename>\nList directory contents: ls\nLook who is currently wasting bandwidth: stat\nUpload a file, simply send this file"
      else
        say.call "Unknown command: #{cmd}, try help"
    end
  end

  def say(to, text)
    puts ">#{to}< #{text.strip}"
    @client.send(Jabber::Message.new(to, text).set_type(:chat))
  end

  def cleaner
    loop {

      @transfers_lock.synchronize {
        @transfers.delete_if { |t| t.done? }
      }

      sleep 1
    }
  end
end


FileServe.new(YAML::load(File.new('fileserve.conf')))
puts "Up and running!"
Thread.stop
