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:
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
endrequire '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
endrequire '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
