#!/usr/bin/ruby # Copyright (c) 2005, 2006, 2007, 2008 Peter Palfrader # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'socket' require 'openssl' require 'yaml' require 'monitor' default_irc = { 'server' => 'irc.oftc.net', 'port' => 6667, 'username' => 'unknown_nsa', 'nick' => 'unknown_nsa', 'realname' => 'Unknown NSA instance' } CONFIG = YAML::load( File.open( 'config' ) ) default_irc.each_pair do |k,v| CONFIG['irc'][k] = v unless CONFIG['irc'][k] end CONFIG['mailin'] = '/home/commit/Maildir' unless CONFIG['mailin']; Log = Object.new class << Log def init @ignore_list = [] @ignore_list << "dispatch thread" @ignore_list << "connection" @ignore_list << "waituntilonline - waiter" @ignore_list << "waituntilonline - runthread" end def log section, message puts message unless @ignore_list.include?(section) end end class Connection def initialize @inQueue = [] @inQueue.extend(MonitorMixin) @inEmpty = @inQueue.new_cond @outQueue = [] @outQueue.extend(MonitorMixin) @outEmpty = @outQueue.new_cond tcpsock = TCPSocket.new(CONFIG['irc']['server'], CONFIG['irc']['port']); if CONFIG['irc']['ssl'] ctx = OpenSSL::SSL::SSLContext.new @sock = OpenSSL::SSL::SSLSocket.new(tcpsock, ctx) @sock.connect else @sock = tcpsock end puts "Connected!" createInThread createOutThread end def print line @outQueue.synchronize do @outQueue << line @outEmpty.signal end end def getline line = "" @inQueue.synchronize do @inEmpty.wait_while { @inQueue.empty? } line = @inQueue.shift end return line end private def createInThread @inThread = Thread.new { begin while true line = @sock.readline Log.log "connection", "[connection] <<< " + line line.chop! @inQueue.synchronize do @inQueue << line @inEmpty.signal end end rescue => e puts e.class.to_s+": "+e.message puts e.backtrace end Thread.main.exit } end def createOutThread @outThread = Thread.new { begin while true @outQueue.synchronize do @outEmpty.wait_while { @outQueue.empty? } line = @outQueue.shift @sock.puts line Log.log "connection", "[connection] >>> " + line end end rescue => e puts e.class.to_s+": "+e.message puts e.backtrace end Thread.main.exit } end end IrcHandleDevNull = Object.new class << IrcHandleDevNull def handle(irc, parsed) end end IrcHandlePing = Object.new class << IrcHandlePing def handle(irc, parsed) irc.print "PONG "+irc.getNick end end IrcHandle001 = Object.new class << IrcHandle001 def handle(irc, parsed) irc.weAreOnline end end IrcHandlePickAnotherNick = Object.new class << IrcHandlePickAnotherNick def handle(irc, parsed) irc.useADifferentNick end end IrcHandleMode = Object.new class << IrcHandleMode def handle(irc, parsed) irc.modeUpdate(parsed) end end class Irc IRC_NOT_CONNECTED = 0 IRC_CONNECTED = 1 IRC_SENT_NICK = 2 IRC_PICK_NEW_NICK = 3 IRC_ONLINE = 4 def initialize @handler = {} @nick = "#{CONFIG['irc']['nick']}" @onlineMonitor = Monitor.new; @onlineCond = @onlineMonitor.new_cond @handler['PING'] = IrcHandlePing # PING @handler['NOTICE'] = IrcHandleDevNull # NOTICE @handler['MODE'] = IrcHandleMode # NOTICE @handler['001'] = IrcHandle001 # w :Welcome to the OFTC Internet @handler['002'] = IrcHandleDevNull # w :Your host is neutron.oftc.net.. @handler['003'] = IrcHandleDevNull # w :This server was created Fri .... @handler['004'] = IrcHandleDevNull # w neutron.oftc.net hybrid-7.1+oftc1.... @handler['005'] = IrcHandleDevNull # w WALLCHOPS KNOCK EXCEPTS INVEX.... @handler['375'] = IrcHandleDevNull # RPL_MOTDSTART @handler['372'] = IrcHandleDevNull # RPL_MOTD @handler['376'] = IrcHandleDevNull # RPL_ENDOFMOTD @handler['250'] = IrcHandleDevNull # Highest connection count: 2 (2 clients) (7 connections received) @handler['251'] = IrcHandleDevNull # RPL_LUSERCLIENT @handler['252'] = IrcHandleDevNull # RPL_LUSEROP @handler['253'] = IrcHandleDevNull # 1 :unknown connection(s) @handler['254'] = IrcHandleDevNull # 575 :channels formed @handler['255'] = IrcHandleDevNull # RPL_LUSERME @handler['265'] = IrcHandleDevNull # Current local users: 2 Max: 2 @handler['266'] = IrcHandleDevNull # Current global users: 2 Max: 2 @handler['353'] = IrcHandleDevNull # RAB = #rab :RAB @weasel· @handler['366'] = IrcHandleDevNull # RAB #rab :End of /NAMES list. @handler['433'] = IrcHandlePickAnotherNick # * oftc-bot :Nickname is already in use. dispatchToHandlers run end def print line @connection.print line end def getNick @nick end def weAreOnline throw "Current run_state is @{run_state}. that's unexpected" unless @run_state == IRC_SENT_NICK @run_state = IRC_ONLINE @run_thread.wakeup end def useADifferentNick throw "Current run_state is @{run_state}. that's unexpected" unless @run_state == IRC_SENT_NICK @run_state = IRC_PICK_NEW_NICK @run_thread.wakeup end def modeUpdate(message) puts "[irc] Received mode update: " + message.to_yaml.gsub("\n","\n ") end def waitUntilOnline Log.log "waituntilonline - waiter", "[waituntilonline] entering monitor" @onlineMonitor.synchronize do Log.log "waituntilonline - waiter", "[waituntilonline] entered monitor" @onlineCond.wait_while { @run_state != IRC_ONLINE } Log.log "waituntilonline - waiter", "[waituntilonline] waiting done" @onlineCond.signal Log.log "waituntilonline - waiter", "[waituntilonline] woke up the rest" end Log.log "waituntilonline - waiter", "[waituntilonline] left monitor" end private def run @run_thread = Thread.new { @run_state = IRC_NOT_CONNECTED while true puts "[run thread] state '#{@run_state}'" case @run_state when IRC_NOT_CONNECTED @connection = Connection.new @dispatch_thread.wakeup @run_state = IRC_CONNECTED when IRC_CONNECTED @connection.print "USER #{CONFIG['irc']['username']} . . :#{CONFIG['irc']['realname']}" issueNick @run_state = IRC_SENT_NICK when IRC_SENT_NICK sleep when IRC_PICK_NEW_NICK @nick.succ! issueNick @run_state = IRC_SENT_NICK when IRC_ONLINE @connection.print "MODE #{@nick} +w" Log.log "waituntilonline - runthread", "[run thread] entering monitor onlineMonitor" @onlineMonitor.synchronize do Log.log "waituntilonline - runthread", "[run thread] entered monitor onlineMonitor" @onlineCond.signal Log.log "waituntilonline - runthread", "[run thread] sent signal on onlineCond" end Log.log "waituntilonline - runthread", "[run thread] left monitor onlineMonitor" sleep end end } end def dispatchToHandlers @dispatch_thread = Thread.new { while true while not @connection Log.log "dispatch thread", "[dispatch thread] waiting for connection" sleep Log.log "dispatch thread", "[dispatch thread] waiting for connection done" end Log.log "dispatch thread", "[dispatch thread] waiting for line" line = @connection.getline Log.log "dispatch thread", "[dispatch thread] waiting for line done" parsed = parseLine line if @handler.has_key?(parsed['command']) Log.log "dispatch thread", "[dispatch thread] dispatching #{parsed['command']}: " + parsed['params'].join(' ') @handler[parsed['command']].handle( self, parsed ) else Log.log "dispatch thread - unhandled", "[dispatch thread] Unhandled: #{line}" end end } end def issueNick @connection.print "NICK #{@nick}" end def parseLine line source = nil (source, line) = line.split(' ', 2) if line[0,1] == ':' source = source[1,source.length-1] if source (command, line) = line.split(' ', 2) params = [] while line and line[0,1] != ':' (middle, line) = line.split(' ', 2) params << middle end params << line[1,line.length-1] if line and line[0,1] == ':' throw "hmmmm. line is '#{line}'." if line and line[0,1] != ':' return { 'source' => source, 'command' => command, 'params' => params } end end class Counter include MonitorMixin def initialize super @count = 5 @ready = new_cond Thread.new { begin while true synchronize do if @count < 8 @count = @count + 1 @ready.signal end end sleep 1 end rescue => e puts e.class.to_s+": "+e.message puts e.backtrace end Thread.main.exit } end def pop synchronize do @ready.wait_until{ @count > 0 } @count = @count - 1 end end end Thread.abort_on_exception = true Dir.chdir( CONFIG['mailin'] + "/new" ) Log.init counter = Counter.new bot = Irc.new bot.waitUntilOnline sleep 1 if CONFIG['irc']['nickserv_is_smart'] bot.print "NICKSERV :identify #{CONFIG['irc']['nickservpassword']} #{CONFIG['irc']['nick']}" else bot.print "NICKSERV :identify #{CONFIG['irc']['nickservpassword']}" end sleep 5 channels = {} CONFIG['projects'].each_value do |cl| cl.each do |c| m = /^NOTICE:(.*)/.match c if m then c = m[1] end channels[c] = true end end channels.each_key do |c| if CONFIG['channelkeys'] and CONFIG['channelkeys'].has_key?(c) bot.print "JOIN #{c} #{CONFIG['channelkeys'][c]}" else bot.print "JOIN #{c}" end end while (1) do oldcommitmsg = nil Dir.foreach('.') { |filename| next if filename == "." next if filename == ".." in_headers = true fh = File.open(filename, "r") project = nil lines = [] commitmsg = "" transfer_encoding = "plain" fh.readlines.each { |line| line.chomp! in_headers = false if line == "" if (in_headers and not line =~ /^\s/) (header, content) = line.split(':', 2); content.strip! if header.upcase == "SUBJECT" m = /Announce\s+([A-Za-z0-9_-]+)/.match(content) if m project = m[1]; end elsif header.upcase == "CONTENT-TRANSFER-ENCODING" transfer_encoding = content end elsif (not in_headers) lines.push line commitmsg = commitmsg + line end } fh.close File.unlink(filename) if transfer_encoding == "base64" require 'base64' lines = Base64.decode64(lines.join()).split("\n") end if project.nil? puts "Ignoring invalid mail without project" next end puts "Project "+project puts "commitmsg "+commitmsg if project and commitmsg != oldcommitmsg pr = "%c"%(002) + project + "%c: "%(002) lines.each{ |line| if (line != '') line = pr + line channellist = CONFIG['projects'].has_key?(project) ? CONFIG['projects'][project] : CONFIG['projects']['*'] channellist.each do |c| counter.pop m = /^NOTICE:(.*)/.match c if m bot.print "NOTICE #{m[1]} :#{line}" else bot.print "PRIVMSG #{c} :#{line}" end end end } end oldcommitmsg = commitmsg } sleep 5 end Thread.stop