Growl-Based Bonjour Chat with Channels in 150 Lines

Posted by kev Sun, 06 May 2007 10:58:00 GMT

One of the many awesome things about working at Powerset is the guys I get to hack with. Tonight, my buddies Tom Preston-Werner, Chris Van Pelt, and I were feeling whimsical. Full source, with quicksilver hook and startup scripts can be found here, but this is the meat:

growl_handler.rb
require 'rubygems'
require 'ruby-growl'

module Jakl
  class GrowlHandler
    def initialize
      if `which growlnotify` =~ /^no .+ in/
        @strategy = :ruby
        @growl = Growl.new("localhost", "jakl", ["jakl_message"])
      else
        @strategy = :command
      end
    end

    def notify(group, name, message)
      case @strategy
        when :command
          img_path = File.join(File.dirname(__FILE__), '../../assets/jakl.png')
          `growlnotify -n jakl --image #{img_path} -m '#{message}' '#{name} (#{group})'`
        when :ruby
          @growl.notify("jakl_message", "#{name} (#{group})", message)
        else
          raise StandardError.new('Invalid strategy')
      end
    end
  end
end
client.rb
require 'rubygems'
require 'net/dns/mdns-sd'
require 'base64'

module Jakl
  class Client
    DNSSD = Net::DNS::MDNSSD

    @@debug = false

    def self.debug=(value)
      @@debug = value
    end

    def self.debug
      @@debug
    end

    def initialize(options={})
      default_options = {
        :default_recv => "jakl", 
        :timeout => 2, 
        :login => ENV['USER']
      }
      @options = default_options.merge(options)
    end

    def send(message, recv=nil)
      recv ||= @options[:default_recv]
      recv = recv.split(',').collect {|g| g.strip }
      puts "Sending: '#{message}' to '#{recv.join(',')}'" if @@debug

      find_recipients = DNSSD.resolve('jakl', '_jakl._tcp') do |r|
        puts "Found jakl service at #{r.target}" if @@debug
        recvs = r.text_record['recvs'].split(',').collect {|g| g.strip }
        puts "  responds to: #{recvs.join(', ')}"  if @@debug

        if (succ_recvs = recvs & recv).any?
          puts "Sending to: #{r.target}:#{r.port}" if @@debug

          # B64 Encoded NAME;GROUP;MESSAGE
          data = [@options[:login], succ_recvs.first, message].map do |s| 
            Base64.encode64(s)
          end.join(';')
          TCPSocket.new(r.target, r.port).send(data, 0)
        end
      end

      sleep @options[:timeout]
      find_recipients.stop
    end
  end
end
server.rb
require 'rubygems'
require 'eventmachine'
require 'net/dns/mdns-sd'
require 'base64'

$:.unshift File.dirname(__FILE__)
require 'growl_handler'

module JaklEventServer
  def receive_data(data)
    # B64 Encoded NAME;GROUP;MESSAGE
    name, recv, message = data.split(';').map {|s| Base64.decode64(s) }
    Jakl::GrowlHandler.new.notify(recv, name, message)
    $stderr.puts "Name: #{name}, Recipient: #{recv}, Message: #{message}" if Jakl::Server.debug
  end
end

module Jakl
  class Server
    DNSSD = Net::DNS::MDNSSD

    @@debug = false

    def self.debug=(value)
      @@debug = value
    end

    def self.debug
      @@debug
    end

    def initialize(options={})
      default_options = {
        :recvs => "jakl", 
        :timeout => 5, 
        :login => ENV['USER']
      }
      @options = default_options.merge(options)

      validate_login!
    end

    def start
      DNSSD.register('jakl', '_jakl._tcp', 'local', 4180, 
                             {'recvs' => @options[:recvs], 'login' => @options[:login]})

      EventMachine::run {
        EventMachine::start_server "0.0.0.0", 4180, JaklEventServer
        puts "Listening for howls on 4180"
      }
    end

    def validate_login!
      # Yeah, I know there's a race condition here. So it goes.
      name_validation = DNSSD.resolve('jakl', '_jakl._tcp') do |r|
        if [r.text_record['login'], 
            r.text_record['recvs'].split(',')].flatten.include? @options[:login]
          puts "The name #{@options[:login]} is already taken. Sorry :\\"
          exit 1
        end
      end

      # Add username to recvs
      @options[:recvs] = [@options[:recvs].split(',') << @options[:login]].join(',')

      # Wait for resolv thread to come back
      sleep 3
      name_validation.stop
    end
  end
end

Posted in ,  | no comments

Comments

Comments are disabled