]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/core/irclog.rb
irclog core module: skip, don't die when unable to open logfile
[user/henk/code/ruby/rbot.git] / lib / rbot / core / irclog.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: rbot IRC logging facilities
5 #
6 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
7
8 class IrcLogModule < CoreBotModule
9
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']
16     },
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
21     },
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)")
26
27   attr :nolog_rx, :dolog_rx
28   def initialize
29     super
30     @queue = Queue.new
31     @thread = Thread.new { loggers_thread }
32     @logs = Hash.new
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']
36   end
37
38   def can_log_on(where)
39     return true if @dolog_rx and where.match @dolog_rx
40     return false if @nolog_rx and where.match @nolog_rx
41     return true
42   end
43
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)
51       ar
52     }.each { |w| logfile_close(w, 'logging disabled here') }
53   end
54
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})"
59     f[1].close
60   end
61
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]
66   end
67
68   def cleanup
69     @queue << nil
70     @thread.join
71     @thread = nil
72   end
73
74   def sent(m)
75     case m
76     when NoticeMessage
77       irclog "-#{m.source}- #{m.message}", m.target
78     when PrivMessage
79       logtarget = who = m.target
80       if m.ctcp
81         case m.ctcp.intern
82         when :ACTION
83           irclog "* #{m.source} #{m.logmessage}", logtarget
84         when :VERSION
85           irclog "@ #{m.source} asked #{who} about version info", logtarget
86         when :SOURCE
87           irclog "@ #{m.source} asked #{who} about source info", logtarget
88         when :PING
89           irclog "@ #{m.source} pinged #{who}", logtarget
90         when :TIME
91           irclog "@ #{m.source} asked #{who} what time it is", logtarget
92         else
93           irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget
94         end
95       else
96         irclog "<#{m.source}> #{m.logmessage}", logtarget
97       end
98     when QuitMessage
99       m.was_on.each { |ch|
100         irclog "@ quit (#{m.message})", ch
101       }
102     end
103   end
104
105   def welcome(m)
106     irclog "joined server #{m.server} as #{m.target}", "server"
107   end
108
109   def listen(m)
110     case m
111     when PrivMessage
112       method = 'log_message'
113     else
114       method = 'log_' + m.class.name.downcase.match(/^irc::(\w+)message$/).captures.first
115     end
116     if self.respond_to?(method)
117       self.__send__(method, m)
118     else
119       warning "unhandled logging for #{m.pretty_inspect} (no such method #{method})"
120       unknown_message(m)
121     end
122   end
123
124   def log_message(m)
125     if m.ctcp
126       who = m.private? ? "me" : m.target
127       logtarget = m.private? ? m.source : m.target
128       case m.ctcp.intern
129       when :ACTION
130         if m.public?
131           irclog "* #{m.source} #{m.logmessage}", m.target
132         else
133           irclog "* #{m.source}(#{m.sourceaddress}) #{m.logmessage}", m.source
134         end
135       when :VERSION
136         irclog "@ #{m.source} asked #{who} about version info", logtarget
137       when :SOURCE
138         irclog "@ #{m.source} asked #{who} about source info", logtarget
139       when :PING
140         irclog "@ #{m.source} pinged #{who}", logtarget
141       when :TIME
142         irclog "@ #{m.source} asked #{who} what time it is", logtarget
143       else
144         irclog "@ #{m.source} asked #{who} about #{[m.ctcp, m.message].join(' ')}", logtarget
145       end
146     else
147       if m.public? 
148         irclog "<#{m.source}> #{m.logmessage}", m.target
149       else
150         irclog "<#{m.source}(#{m.sourceaddress})> #{m.logmessage}", m.source
151       end
152     end
153   end
154
155   def log_notice(m)
156     if m.private?
157       irclog "-#{m.source}(#{m.sourceaddress})- #{m.logmessage}", m.source
158     else
159       irclog "-#{m.source}- #{m.logmessage}", m.target
160     end
161   end
162
163   def motd(m)
164     m.message.each_line { |line|
165       irclog "MOTD: #{line}", "server"
166     }
167   end
168
169   def log_nick(m)
170     m.is_on.each { |ch|
171       irclog "@ #{m.oldnick} is now known as #{m.newnick}", ch
172     }
173   end
174
175   def log_quit(m)
176     m.was_on.each { |ch|
177       irclog "@ Quit: #{m.source}: #{m.logmessage}", ch
178     }
179   end
180
181   def modechange(m)
182     irclog "@ Mode #{m.logmessage} by #{m.source}", m.target
183   end
184
185   def log_join(m)
186     if m.address?
187       debug "joined channel #{m.channel}"
188       irclog "@ Joined channel #{m.channel}", m.channel
189     else
190       irclog "@ #{m.source} joined channel #{m.channel}", m.channel
191     end
192   end
193
194   def log_part(m)
195     if(m.address?)
196       debug "left channel #{m.channel}"
197       irclog "@ Left channel #{m.channel} (#{m.logmessage})", m.channel
198     else
199       irclog "@ #{m.source} left channel #{m.channel} (#{m.logmessage})", m.channel
200     end
201   end
202
203   def log_kick(m)
204     if(m.address?)
205       debug "kicked from channel #{m.channel}"
206       irclog "@ You have been kicked from #{m.channel} by #{m.source} (#{m.logmessage})", m.channel
207     else
208       irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.source} (#{m.logmessage})", m.channel
209     end
210   end
211
212   # def log_invite(m)
213   #   # TODO
214   # end
215
216   def log_topic(m)
217     case m.info_or_set
218     when :set
219       if m.source == @bot.myself
220         irclog "@ I set topic \"#{m.topic}\"", m.channel
221       else
222         irclog "@ #{m.source} set topic \"#{m.topic}\"", m.channel
223       end
224     when :info
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
228     end
229   end
230
231   # def names(m)
232   #   # TODO
233   # end
234
235   def unknown_message(m)
236     irclog m.logmessage, ".unknown"
237   end
238
239   def logfilepath(where_str, now)
240     File.join(@bot.botclass, 'logs', now.strftime(@fn_format) % { :where => where_str })
241   end
242
243   protected
244   def loggers_thread
245     ls = nil
246     debug 'loggers_thread starting'
247     while ls = @queue.pop
248       message, where = ls
249       message = message.chomp
250       now = Time.now
251       stamp = now.strftime("%Y/%m/%d %H:%M:%S")
252       if where.class <= Server
253         where_str = "server"
254       else
255         where_str = where.downcase.gsub(/[:!?$*()\/\\<>|"']/, "_")
256       end
257       return unless can_log_on(where_str)
258
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
263       end
264
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]}"
272           end
273         end
274         fp = logfilepath(where_str, now)
275         begin
276           FileUtils.mkdir_p File.dirname(fp)
277           f = File.new(fp, "a")
278           f.sync = true
279           f.puts "[#{stamp}] @ Log started by #{@bot.myself.nick}"
280         rescue Exception => e
281           error e
282           next
283         end
284         @logs[where_str] = [now, f]
285       end
286       @logs[where_str][1].puts "[#{stamp}] #{message}"
287       @logs[where_str][0] = now
288       #debug "[#{stamp}] <#{where}> #{message}"
289     end
290     @logs.keys.each { |w| logfile_close(w, 'rescan or shutdown') }
291     debug 'loggers_thread terminating'
292   end
293 end
294
295 ilm = IrcLogModule.new
296 ilm.priority = -1
297