4 # :title: rbot IRC logging facilities
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 class IrcLogModule < CoreBotModule
10 Config.register Config::IntegerValue.new('irclog.max_open_files',
11 :default => 20, :validate => Proc.new { |v| v > 0 },
12 :desc => "Maximum number of irclog files to keep open at any one time.")
13 Config.register Config::ArrayValue.new('irclog.no_log',
14 :default => [], :on_change => Proc.new { |bot, v|
15 bot.plugins.delegate 'event_irclog_list_changed', v, bot.config['irclog.do_log']
17 :desc => "List of channels and nicks for which logging is disabled. IRC patterns can be used too.")
18 Config.register Config::ArrayValue.new('irclog.do_log',
19 :default => [], :on_change => Proc.new { |bot, v|
20 bot.plugins.delegate 'event_irclog_list_changed', bot.config['irclog.no_log'], v
22 :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")
23 Config.register Config::StringValue.new('irclog.filename_format',
24 :default => '%{where}', :requires_rescan => true,
25 :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)")
27 attr :nolog_rx, :dolog_rx
31 @thread = Thread.new { loggers_thread }
33 Dir.mkdir("#{@bot.botclass}/logs") unless File.exist?("#{@bot.botclass}/logs")
34 event_irclog_list_changed(@bot.config['irclog.no_log'], @bot.config['irclog.do_log'])
35 @fn_format = @bot.config['irclog.filename_format']
39 return true if @dolog_rx and where.match @dolog_rx
40 return false if @nolog_rx and where.match @nolog_rx
44 def event_irclog_list_changed(nolist, dolist)
45 @nolog_rx = nolist.empty? ? nil : Regexp.union(*(nolist.map { |r| r.to_irc_regexp }))
46 debug "no log: #{@nolog_rx}"
47 @dolog_rx = dolist.empty? ? nil : Regexp.union(*(dolist.map { |r| r.to_irc_regexp }))
48 debug "do log: #{@dolog_rx}"
49 @logs.inject([]) { |ar, kv|
50 ar << kv.first unless can_log_on(kv.first)
52 }.each { |w| logfile_close(w, 'logging disabled here') }
55 def logfile_close(where_str, reason = 'unknown reason')
56 f = @logs.delete(where_str) or return
57 stamp = Time.now.strftime '%Y/%m/%d %H:%M:%S'
58 f[1].puts "[#{stamp}] @ Log closed by #{@bot.myself.nick} (#{reason})"
62 # log IRC-related message +message+ to a file determined by +where+.
63 # +where+ can be a channel name, or a nick for private message logging
64 def irclog(message, where="server")
65 @queue.push [message, where]
77 irclog "-#{m.source}- #{m.message}", m.target
79 logtarget = who = m.target
83 irclog "* #{m.source} #{m.logmessage}", logtarget
85 irclog "@ #{m.source} asked #{who} about version info", logtarget
87 irclog "@ #{m.source} asked #{who} about source info", logtarget
89 irclog "@ #{m.source} pinged #{who}", logtarget
91 irclog "@ #{m.source} asked #{who} what time it is", logtarget
93 irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget
96 irclog "<#{m.source}> #{m.logmessage}", logtarget
100 irclog "@ quit (#{m.message})", ch
106 irclog "joined server #{m.server} as #{m.target}", "server"
112 method = 'log_message'
114 method = 'log_' + m.class.name.downcase.match(/^irc::(\w+)message$/).captures.first
116 if self.respond_to?(method)
117 self.__send__(method, m)
119 warning "unhandled logging for #{m.pretty_inspect} (no such method #{method})"
126 who = m.private? ? "me" : m.target
127 logtarget = m.private? ? m.source : m.target
131 irclog "* #{m.source} #{m.logmessage}", m.target
133 irclog "* #{m.source}(#{m.sourceaddress}) #{m.logmessage}", m.source
136 irclog "@ #{m.source} asked #{who} about version info", logtarget
138 irclog "@ #{m.source} asked #{who} about source info", logtarget
140 irclog "@ #{m.source} pinged #{who}", logtarget
142 irclog "@ #{m.source} asked #{who} what time it is", logtarget
144 irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget
148 irclog "<#{m.source}> #{m.logmessage}", m.target
150 irclog "<#{m.source}(#{m.sourceaddress})> #{m.logmessage}", m.source
157 irclog "-#{m.source}(#{m.sourceaddress})- #{m.logmessage}", m.source
159 irclog "-#{m.source}- #{m.logmessage}", m.target
164 m.message.each_line { |line|
165 irclog "MOTD: #{line}", "server"
171 irclog "@ #{m.oldnick} is now known as #{m.newnick}", ch
177 irclog "@ Quit: #{m.source}: #{m.logmessage}", ch
182 irclog "@ Mode #{m.logmessage} by #{m.source}", m.target
187 debug "joined channel #{m.channel}"
188 irclog "@ Joined channel #{m.channel}", m.channel
190 irclog "@ #{m.source} joined channel #{m.channel}", m.channel
196 debug "left channel #{m.channel}"
197 irclog "@ Left channel #{m.channel} (#{m.logmessage})", m.channel
199 irclog "@ #{m.source} left channel #{m.channel} (#{m.logmessage})", m.channel
205 debug "kicked from channel #{m.channel}"
206 irclog "@ You have been kicked from #{m.channel} by #{m.source} (#{m.logmessage})", m.channel
208 irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.source} (#{m.logmessage})", m.channel
219 if m.source == @bot.myself
220 irclog "@ I set topic \"#{m.topic}\"", m.channel
222 irclog "@ #{m.source} set topic \"#{m.topic}\"", m.channel
225 topic = m.channel.topic
226 irclog "@ Topic is \"#{m.topic}\"", m.channel
227 irclog "@ Topic set by #{topic.set_by} on #{topic.set_on}", m.channel
235 def unknown_message(m)
236 irclog m.logmessage, ".unknown"
239 def logfilepath(where_str, now)
240 File.join(@bot.botclass, 'logs', now.strftime(@fn_format) % { :where => where_str })
246 debug 'loggers_thread starting'
247 while ls = @queue.pop
249 message = message.chomp
251 stamp = now.strftime("%Y/%m/%d %H:%M:%S")
252 if where.class <= Server
255 where_str = where.downcase.gsub(/[:!?$*()\/\\<>|"']/, "_")
257 return unless can_log_on(where_str)
259 # close the previous logfile if we're rotating
260 if @logs.has_key? where_str
261 fp = logfilepath(where_str, now)
262 logfile_close(where_str, 'log rotation') if fp != @logs[where_str][1].path
265 # (re)open the logfile if necessary
266 unless @logs.has_key? where_str
267 if @logs.size > @bot.config['irclog.max_open_files']
268 @logs.keys.sort do |a, b|
269 @logs[a][0] <=> @logs[b][0]
270 end.slice(0, @logs.size - @bot.config['irclog.max_open_files']).each do |w|
271 logfile_close w, "idle since #{@logs[w][0]}"
274 fp = logfilepath(where_str, now)
275 FileUtils.mkdir_p File.dirname(fp)
276 f = File.new(fp, "a")
278 f.puts "[#{stamp}] @ Log started by #{@bot.myself.nick}"
279 @logs[where_str] = [now, f]
281 @logs[where_str][1].puts "[#{stamp}] #{message}"
282 @logs[where_str][0] = now
283 #debug "[#{stamp}] <#{where}> #{message}"
285 @logs.keys.each { |w| logfile_close(w, 'rescan or shutdown') }
286 debug 'loggers_thread terminating'
290 ilm = IrcLogModule.new