# Author: Lurker_pas # Simple console based IRC Client written in Ruby # # see www.lurkersburrow.wordpress.com # # Supports: # * basic chat (channel/private) # * basic dcc send (with resume and queueing ability) # * non-private chat recording # # Based on: simple IRC bot from http://snippets.dzone.com/posts/show/1785 # Created 2007 10 10 # Update 2007 14 10 - Added basic DCC SEND support # Update 2007 15 10 - Added queue and record ability require "socket" require "monitor" class IRCDCC attr_writer :requestresume attr_writer :queuecooldown attr_reader :queuecooldown def initialize() @allowed = Hash.new @blocks = Hash.new @blocks.extend(MonitorMixin) @blockschanged = @blocks.new_cond @requestresume = false @queue = Array.new @queue.extend(MonitorMixin) @queueready = @queue.new_cond @bot = Hash.new @bot.extend(MonitorMixin) @botdone = @bot.new_cond @queuecooldown = 3 @currentqueuebot = "" end def initsocket(socket) @ircsocket = socket end def addallowed(name) @allowed[name] = true end def removeallowed(name) @allowed[name] = false end def isallowed(name) result = false if (@allowed[name]==true) result = true end result end def min(x,y) if (x < y) x else y end end def addtoqueue(bot,pack) @queue.synchronize do @queue.push([bot,pack]) @queueready.signal end end def cancelcurrentbot @bot.synchronize do @bot[@currentqueuebot] = true @botdone.signal end end def clearqueue() @queue.synchronize do @queue.clear end end def queuethread() botname = "" packnumber = "" while true puts "Queue: waiting for items" @queue.synchronize do @queueready.wait_while {@queue.length < 1} botname = @queue[0][0] packnumber = @queue[0][1] @queue.shift end @currentqueuebot = botname.dup puts "Queue: #{botname} > #{packnumber}" #request pack s = "PRIVMSG #{botname} :xdcc send ##{packnumber}" puts "--> #{s}" @ircsocket.print "#{s}\r\n" #wait until everything is downloaded puts "Queue: downloading..." @bot.synchronize do @bot[botname] = false @botdone.wait_while { @bot[botname]==false} end puts "Queue: cooling down" sleep(@queuecooldown) end end def negotiateresume(bot,name,port,existingfilesize) puts "Waiting for resume on #{name}" s = "PRIVMSG #{bot} :\001DCC RESUME #{name} #{port} #{existingfilesize}\001" puts "--> #{s}" @ircsocket.print "#{s}\r\n" @blocks.synchronize do @blocks[name] = false @blockschanged.wait_while {@blocks[name] == false} end puts "Resume acceptance on #{name} acknowledged" existingfilesize end def downloadthread(bot,name,address,port,filesize) puts "Downloading #{name} (#{filesize} bytes) from #{bot}@#{address}:#{port}" packetlength = 1024 bytesleft = filesize fname = name.gsub(/"|\s|\\/,"") existingfilesize = 0 if (File.exists?(fname) && @requestresume) then existingfilesize = File.size?(fname) negotiateresume(bot,name,port,existingfilesize) file = File.new(fname,"a") else file = File.new(fname,"w") end bytesleft = filesize-existingfilesize i = 0 if (!file) then puts "File IO Error" return end socket = TCPSocket.open(address, port) if (!socket) then puts "Socket IO Error" return end puts "Downloading #{name}: #{bytesleft} bytes left..." while bytesleft > 0 chunk = min(packetlength,bytesleft) packet = socket.recv(chunk) file.write(packet) bytesleft = bytesleft - packet.length i = i +1 if (i > 10000) i = 0 puts "Downloading #{name}: #{bytesleft} bytes left..." end end socket.close file.close puts "Downloading #{name} done" @bot.synchronize do @bot[bot] = true @botdone.signal end end def extractaddress(intaddress) #Translate address binaddr = intaddress ip0 = binaddr%256 binaddr = binaddr >> 8 ip1 = binaddr%256 binaddr = binaddr >> 8 ip2 = binaddr%256 binaddr = binaddr >> 8 ip3 = binaddr%256 ipaddress = "#{ip3}.#{ip2}.#{ip1}.#{ip0}" return ipaddress end def downloadblocking(bot,name,address,port,filesize) ipaddress = extractaddress(address) #create downloading thread downloadthread(bot,name,ipaddress,port,filesize) end def download(bot,name,address,port,filesize) ipaddress = extractaddress(address) #create downloading thread dt = Thread.new do downloadthread(bot,name,ipaddress,port,filesize) end end def processdccmsg(name,msg) message = msg.strip if message =~ /DCC ACCEPT (\".*\"|\S*) (\d*) (\d*)/ puts "Resume of #{$1} accepted" @blocks.synchronize do @blocks[$1] = true @blockschanged.signal end return end if message =~ /DCC SEND (\".*\"|\S*) (\d*) (\d*) (\d*)/ print "DCC Send received from #{name} - " if isallowed(name) puts " accepting" download(name,$1,($2).to_i,($3).to_i,($4).to_i) else puts " refusing" end return end puts "Unknown dcc message" end end class IRCClient def initialize(server, port, nick) @server = server @port = port @nick = nick @channel = "" @dead = false @raw = false @quitmessage = "Disconnecting..." @dcc = IRCDCC.new() @recording = false end def initializerecording(filename) @recording = true @recordingfile = File.new(filename,"w") end def shutdownrecording() if (@recording) then @recording = false @recordingfile.close end end def print_help puts "IRC Client by Lurker_pas" puts "see www.lurkersburrow.wordpress.com" puts "Available commands:" puts ">> help - shows this help" puts ">> say *message* - sends specified message to channel" puts ">> psay *nick* *message* - sends specified message to specified client" puts ">> quit - quits" puts ">> mode *modechange* - sets user mode ([+|-][i|w|s|o])" puts ">> join *channel* - join specified channel" puts ">> leave *channel* - leave specified channel" puts ">> context *channel* - set the channel you want to talk to" puts ">> /*command* - direct IRC protocol command (without validation)" puts ">> raw - start showing all unprocessed messages from server" puts ">> unraw - stop showing all unprocessed messages from server" puts ">> record *filename* - start recording channel messages to the specified file" puts ">> dontrecord - stop recording" puts ">> dccaccept *name* - accept all dcc sends from *name*" puts ">> dccdeny *name* - deny all dcc sends from *name*" puts ">> dccrequestresume - request resume if file sent by dcc send exists" puts ">> dccdontrequestresume - dont request resume" puts ">> dcclist *name* - send xdcc list command to *name*" puts ">> dccinfo *name* *pack* - request info from *name* on pack number *pack*" puts ">> dccget *name* *pack* - request pack number *pack* from *name*" puts ">> dccqueue *name* *pack* - add pack *pack* from *name* to the auto-download queue" puts ">> dccqueueclear - clear the auto-download queue" puts ">> dccqueuecancel - cancel the current download by the auto-download queue" puts ">> dccqueuecooldown *seconds* - set the 'after-download' cooldown for *seconds* seconds" puts ">> NOTE : *record* records only channel messages from subscribed channels - no private chats are recorded" puts ">> NOTE : *dccaccept name* must be invoked >>before<< the actual download occurs (before dccget or dccqueue...)" puts ">> NOTE : queue cooldown is given in (integer) seconds " puts "Enjoy" end def send(s) puts "--> #{s}" @irc.print "#{s}\r\n" end def connect() puts "Connecting to server #{@server} at port #(@port)..." @irc = TCPSocket.open(@server, @port) @dcc.initsocket(@irc) puts "Setting Nick to #{@nick} and User to #{@nick} user..." send "USER #{@nick} server #{@server} :#{@nick} user" send "Nick #{@nick}" end def handleservermessage(s) if @raw then puts s end case s.strip when /^PING :(.+)$/i puts "[ Server ping ]" send "PONG :#{$1}" when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s.+\s:[\001]PING (.+)[\001]$/i puts "[ CTCP PING from #{$1}!#{$2}@#{$3} ]" send "NOTICE #{$1} :\001PING #{$4}\001" when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s.+\s:[\001]VERSION[\001]$/i puts "[ CTCP VERSION from #{$1}!#{$2}@#{$3} ]" send "NOTICE #{$1} :\001VERSION Ruby-IRCClient v0.001\001" when /^:(\S*)!(.)* PRIVMSG #{@nick} :(.*)$/ puts $1+" >>*>> "+$3 sendername = $1.strip sendermsg = $3.strip if (sendermsg =~ /DCC (.*)/) puts "DCC message received from #{sendername}" @dcc.processdccmsg(sendername,sendermsg) end when /^:(\S*)!(.)* PRIVMSG (\S+) :(.*)$/ if @recording then @recordingfile.write("#{$1}@#{$3} >> #{$4}\n") end puts $1+"@"+$3+" >> "+$4 when /^:(\S*)!(.)* NOTICE (.*)$/ puts "#{$1} NOTICE >> #{$3}" when /^:(\S*)!(.*) JOIN (\S+)/ puts $1+" (#{$2}) joined "+$3 when /^:(\S*)!(.*) PART (\S*)/ puts "#{$1} (#{$2}) left #{$3}" when /^:(\S*)!(.*) QUIT(.*)/ user = $1 address = $2 tail = $3 if (tail =~ /:(.*)/) puts "#{user} (#{address}) quit - #{$1}" else puts "#{user} (#{address}) quit" end end end def user_handler(lock) while true s = gets if s=~ /^say / then if @recording then @recordingfile.write("#{@nick}@#{@channel} >> #{s.gsub(/^say /,"")}\n") end lock.synchronize do send "PRIVMSG #{@channel} :"+s.gsub(/^say /,"") end next end if s=~ /^psay (\S*) (.*)/ then lock.synchronize do send "PRIVMSG "+$1+" :"+$2 end next end if s=~ /^context (.+)/ then @channel = $1 if !(@channel =~ /^#.*/) then @channel = "#"+@channel end next end if s=~ /^join (.+)/ then @channel = $1 if !(@channel =~ /^#.*/) then @channel = "#"+@channel end lock.synchronize do send "JOIN #{@channel}" end next end if s=~ /^leave (.+)/ then channelname = $1 if !(channelname =~ /^#.*/) then channelname = "#"+channelname end lock.synchronize do send "PART #{channelname}" end next end if s=~ /^raw/ then @raw = true puts "RAW mode ON" next end if s=~ /^unraw/ then @raw = false puts "RAW mode OFF" next end if s=~ /^mode ([+-][iwso]+)/ then lock.synchronize do send "MODE #{@nick} :"+$1 end next end if s=~ /^quit/ then lock.synchronize do send "QUIT :"+@quitmessage end @dead = true break end if s=~ /^\/(.*)/ then lock.synchronize do send $1.chomp next end end if s=~ /^help/ then print_help next end if s =~ /^record (\S*)/ then initializerecording($1) puts "Recording ON (#{$1})" next end if s =~ /^dontrecord/ then shutdownrecording() puts "Recording OFF" next end if s=~ /^dccaccept (\S*)/ then @dcc.addallowed($1) puts "Added #{$1} to allowed list" next end if s=~ /^dccdeny (\S*)/ then @dcc.removeallowed($1) puts "Removed #{$1} from allowed list" next end if s=~ /^dccrequestresume/ then @dcc.requestresume = true puts "DCC Resume Request ON" next end if s=~ /^dccdontrequestresume/ then @dcc.requestresume = false puts "DCC Resume Request OFF" next end if s=~ /^dcclist (\S*)/ lock.synchronize do send "PRIVMSG #{$1} :xdcc list" end next end if s=~ /^dccinfo (\S*) (\d*)/ lock.synchronize do send "PRIVMSG #{$1} :xdcc info ##{$2}" end next end if s=~ /^dccget (\S*) (\d*)/ lock.synchronize do send "PRIVMSG #{$1} :xdcc send ##{$2}" end next end if s=~ /^dccqueue (\S*) (\d*)/ @dcc.addtoqueue($1,($2).to_i) puts "Added #{$1} > pack #{$2} to queue" next end if s=~ /^dccqueueclear/ @dcc.clearqueue() puts "Queue cleared" next end if s=~ /^dccqueuecooldown (\d*)/ @dcc.queuecooldown = ($1).to_i puts "Queue cooldown set to #{@dcc.queuecooldown}" next end if s=~ /^dccqueuecancel/ @dcc.cancelcurrentbot puts "Cancelling current download" next end puts "Invalid command" end end def main_loop() lock = Monitor.new input = Thread.new do user_handler(lock) end downloadqueue = Thread.new do @dcc.queuethread() end while true ready = select([@irc],nil, nil,1) break if @dead next if !ready for rs in ready[0] if rs == @irc then return if @irc.eof lock.synchronize do s = @irc.readline.chomp handleservermessage(s) end end end end input.join shutdownrecording() @irc.close end end puts "Enter server name:" server_name = gets.chomp puts "Enter nick:" nick = gets.chomp begin irc = IRCClient.new(server_name, 6667,nick) irc.connect() irc.main_loop() rescue Exception => e puts "Exception while executing"+e end