]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/core/irclog.rb
fix: webservice message user type
[user/henk/code/ruby/rbot.git] / lib / rbot / core / irclog.rb
index 64469f78568f4936f1b93c7fc17c374f85e49553..3b3134c205ebe67ce7e0022a839fc4489cd4332b 100644 (file)
 # :title: rbot IRC logging facilities
 #
 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
-# Copyright:: (C) 2008 Giuseppe Bilotta
-# License:: GPL v2
 
 class IrcLogModule < CoreBotModule
-  
+
+  Config.register Config::IntegerValue.new('irclog.max_open_files',
+    :default => 20, :validate => Proc.new { |v| v > 0 },
+    :desc => "Maximum number of irclog files to keep open at any one time.")
+  Config.register Config::ArrayValue.new('irclog.no_log',
+    :default => [], :on_change => Proc.new { |bot, v|
+      bot.plugins.delegate 'event_irclog_list_changed', v, bot.config['irclog.do_log']
+    },
+    :desc => "List of channels and nicks for which logging is disabled. IRC patterns can be used too.")
+  Config.register Config::ArrayValue.new('irclog.do_log',
+    :default => [], :on_change => Proc.new { |bot, v|
+      bot.plugins.delegate 'event_irclog_list_changed', bot.config['irclog.no_log'], v
+    },
+    :desc => "List of channels and nicks for which logging is enabled. IRC patterns can be used too. This can be used to override wide patters in irclog.no_log")
+  Config.register Config::StringValue.new('irclog.filename_format',
+    :default => '%%{where}', :requires_rescan => true,
+    :desc => "filename pattern for the IRC log. You can put typical strftime keys such as %Y for year and %m for month, plus the special %%{where} key for location (channel name or user nick)")
+  Config.register Config::StringValue.new('irclog.timestamp_format',
+    :default => '[%Y/%m/%d %H:%M:%S]', :requires_rescan => true,
+    :desc => "timestamp pattern for the IRC log, using typical strftime keys")
+
+  attr :nolog_rx, :dolog_rx
   def initialize
     super
+    @queue = Queue.new
+    @thread = Thread.new { loggers_thread }
     @logs = Hash.new
-    Dir.mkdir("#{@bot.botclass}/logs") unless File.exist?("#{@bot.botclass}/logs")
+    logdir = @bot.path 'logs'
+    Dir.mkdir(logdir) unless File.exist?(logdir)
+    # TODO what shall we do if the logdir couldn't be created? (e.g. it existed as a file)
+    event_irclog_list_changed(@bot.config['irclog.no_log'], @bot.config['irclog.do_log'])
+    @fn_format = @bot.config['irclog.filename_format']
+  end
+
+  def can_log_on(where)
+    return true if @dolog_rx and where.match @dolog_rx
+    return false if @nolog_rx and where.match @nolog_rx
+    return true
+  end
+
+  def timestamp(time)
+    return time.strftime(@bot.config['irclog.timestamp_format'])
+  end
+
+  def event_irclog_list_changed(nolist, dolist)
+    @nolog_rx = nolist.empty? ? nil : Regexp.union(*(nolist.map { |r| r.to_irc_regexp }))
+    debug "no log: #{@nolog_rx}"
+    @dolog_rx = dolist.empty? ? nil : Regexp.union(*(dolist.map { |r| r.to_irc_regexp }))
+    debug "do log: #{@dolog_rx}"
+    @logs.inject([]) { |ar, kv|
+      ar << kv.first unless can_log_on(kv.first)
+      ar
+    }.each { |w| logfile_close(w, 'logging disabled here') }
+  end
+
+  def logfile_close(where_str, reason = 'unknown reason')
+    f = @logs.delete(where_str) or return
+    stamp = timestamp(Time.now)
+    f[1].puts "#{stamp} @ Log closed by #{@bot.myself.nick} (#{reason})"
+    f[1].close
   end
 
   # log IRC-related message +message+ to a file determined by +where+.
   # +where+ can be a channel name, or a nick for private message logging
   def irclog(message, where="server")
-    message = message.chomp
-    stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
-    if where.class <= Server
-      where_str = "server"
-    else
-      where_str = where.downcase.gsub(/[:!?$*()\/\\<>|"']/, "_")
-    end
-    unless(@logs.has_key?(where_str))
-      @logs[where_str] = File.new("#{@bot.botclass}/logs/#{where_str}", "a")
-      @logs[where_str].sync = true
-    end
-    @logs[where_str].puts "[#{stamp}] #{message}"
-    #debug "[#{stamp}] <#{where}> #{message}"
+    @queue.push [message, where]
+  end
+
+  def cleanup
+    @queue << nil
+    @thread.join
+    @thread = nil
   end
 
   def sent(m)
     case m
     when NoticeMessage
-      if m.public?
-        irclog "-=#{m.source}=- #{m.message}", m.target
-      else
-        irclog "[-=#{m.source}=-] #{m.message}", m.target
-      end
+      irclog "-#{m.source}- #{m.message}", m.target
     when PrivMessage
-      if m.public?
-        irclog "<#{m.source}> #{m.message}", m.target
+      logtarget = who = m.target
+      if m.ctcp
+        case m.ctcp.intern
+        when :ACTION
+          irclog "* #{m.source} #{m.logmessage}", logtarget
+        when :VERSION
+          irclog "@ #{m.source} asked #{who} about version info", logtarget
+        when :SOURCE
+          irclog "@ #{m.source} asked #{who} about source info", logtarget
+        when :PING
+          irclog "@ #{m.source} pinged #{who}", logtarget
+        when :TIME
+          irclog "@ #{m.source} asked #{who} what time it is", logtarget
+        else
+          irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget
+        end
       else
-        irclog "[msg(#{m.target})] #{m.message}", m.target
+        irclog "<#{m.source}> #{m.logmessage}", logtarget
       end
     when QuitMessage
       m.was_on.each { |ch|
@@ -82,7 +139,7 @@ class IrcLogModule < CoreBotModule
         if m.public?
           irclog "* #{m.source} #{m.logmessage}", m.target
         else
-          irclog "* [#{m.source}(#{m.sourceaddress})] #{m.logmessage}", m.source
+          irclog "* #{m.source}(#{m.sourceaddress}) #{m.logmessage}", m.source
         end
       when :VERSION
         irclog "@ #{m.source} asked #{who} about version info", logtarget
@@ -96,19 +153,19 @@ class IrcLogModule < CoreBotModule
         irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget
       end
     else
-      if m.public? 
+      if m.public?
         irclog "<#{m.source}> #{m.logmessage}", m.target
       else
-        irclog "[#{m.source}(#{m.sourceaddress})] #{m.logmessage}", m.source
+        irclog "<#{m.source}(#{m.sourceaddress})> #{m.logmessage}", m.source
       end
     end
   end
 
   def log_notice(m)
     if m.private?
-      irclog "-#{m.source}- #{m.message}", m.source
+      irclog "-#{m.source}(#{m.sourceaddress})- #{m.logmessage}", m.source
     else
-      irclog "-#{m.source}- #{m.message}", m.target
+      irclog "-#{m.source}- #{m.logmessage}", m.target
     end
   end
 
@@ -119,19 +176,19 @@ class IrcLogModule < CoreBotModule
   end
 
   def log_nick(m)
-    m.is_on.each { |ch|
+    (m.is_on & @bot.myself.channels).each { |ch|
       irclog "@ #{m.oldnick} is now known as #{m.newnick}", ch
     }
   end
 
   def log_quit(m)
-    m.was_on.each { |ch|
-      irclog "@ Quit: #{m.source}: #{m.message}", ch
+    (m.was_on & @bot.myself.channels).each { |ch|
+      irclog "@ Quit: #{m.source}: #{m.logmessage}", ch
     }
   end
 
   def modechange(m)
-    irclog "@ Mode #{m.message} by #{m.source}", m.target
+    irclog "@ Mode #{m.logmessage} by #{m.source}", m.target
   end
 
   def log_join(m)
@@ -185,7 +242,86 @@ class IrcLogModule < CoreBotModule
   # end
 
   def unknown_message(m)
-    irclog m.message, ".unknown"
+    irclog m.logmessage, ".unknown"
+  end
+
+  def logfilepath(where_str, now)
+    @bot.path('logs', now.strftime(@fn_format) % { :where => where_str })
+  end
+
+  protected
+  def loggers_thread
+    ls = nil
+    debug 'loggers_thread starting'
+    while ls = @queue.pop
+      message, where = ls
+      message = message.chomp
+      now = Time.now
+      stamp = timestamp(now)
+      if where.class <= Server
+        where_str = "server"
+      else
+        where_str = where.downcase.gsub(/[:!?$*()\/\\<>|"']/, "_")
+      end
+      next unless can_log_on(where_str)
+
+      # close the previous logfile if we're rotating
+      if @logs.has_key? where_str
+        fp = logfilepath(where_str, now)
+        logfile_close(where_str, 'log rotation') if fp != @logs[where_str][1].path
+      end
+
+      # (re)open the logfile if necessary
+      unless @logs.has_key? where_str
+        if @logs.size > @bot.config['irclog.max_open_files']
+          @logs.keys.sort do |a, b|
+            @logs[a][0] <=> @logs[b][0]
+          end.slice(0, @logs.size - @bot.config['irclog.max_open_files']).each do |w|
+            logfile_close w, "idle since #{@logs[w][0]}"
+          end
+        end
+        fp = logfilepath(where_str, now)
+        begin
+          dir = File.dirname(fp)
+          # first of all, we check we're not trying to build a directory structure
+          # where one of the components exists already as a file, so we
+          # backtrack along dir until we come across the topmost existing name.
+          # If it's a file, we rename to filename.old.filedate
+          up = dir.dup
+          until File.exist? up
+            up.replace(File.dirname(up))
+          end
+          unless File.directory? up
+            backup = up.dup
+            backup << ".old." << File.atime(up).strftime('%Y%m%d%H%M%S')
+            debug "#{up} is not a directory! renaming to #{backup}"
+            File.rename(up, backup)
+          end
+          FileUtils.mkdir_p(dir)
+          # conversely, it may happen that fp exists and is a directory, in
+          # which case we rename the directory instead
+          if File.directory? fp
+            backup = fp.dup
+            backup << ".old." << File.atime(fp).strftime('%Y%m%d%H%M%S')
+            debug "#{fp} is not a file! renaming to #{backup}"
+            File.rename(fp, backup)
+          end
+          # it should be fine to create the file now
+          f = File.new(fp, "a")
+          f.sync = true
+          f.puts "#{stamp} @ Log started by #{@bot.myself.nick}"
+        rescue Exception => e
+          error e
+          next
+        end
+        @logs[where_str] = [now, f]
+      end
+      @logs[where_str][1].puts "#{stamp} #{message}"
+      @logs[where_str][0] = now
+      #debug "#{stamp} <#{where}> #{message}"
+    end
+    @logs.keys.each { |w| logfile_close(w, 'rescan or shutdown') }
+    debug 'loggers_thread terminating'
   end
 end