7c341299b3d07f120ed3c9b436477dbbd18197f4
[irc-logger.git] / logger.lisp
1 ;;;  -*- Mode: Lisp -*-
2 ;;;; $Id: logger.lisp,v 1.11 2003/12/16 21:19:56 krosenberg Exp $
3 ;;;;
4 ;;;; Purpose: A IRC logging bot 
5 ;;;; Author:  Kevin Rosenberg
6
7 (in-package irc-logger)
8
9 (defvar *daemon-monitor-process* nil "Process of background monitor.")
10
11 (defclass channel ()
12   ((name :initarg :name :reader name
13          :documentation "Name of channel.")
14    (streams :initarg :streams :reader streams
15             :documentation "List of output streams.")
16    (output-root :initarg :output-root :reader output-root)
17    (current-output-names :initarg :current-output-names :accessor current-output-names)))
18
19    
20 (defclass logger ()
21   ((connection :initarg :connection :accessor connection
22                :documentation "IRC connection object.")
23    (handler :initform nil :accessor handler
24             :documentation "Background handler process.")
25    (nick :initarg :nick :reader nickname
26          :documentation "Nickname of the bot.")
27    (password :initarg :password :reader password
28              :documentation "Nickname's nickserver password.")
29    (server :initarg :server :reader server
30            :documentation "Connected IRC server.")
31    (channel-names :initarg :channel-names :accessor channel-names
32                   :documentation "List of channel names.")
33    (realname :initarg :realname :reader realname
34            :documentation "Realname for cl-irc")
35    (username :initarg :username :reader username
36            :documentation "Username for cl-irc")
37    (logging-stream :initarg :logging-stream :reader logging-stream 
38                    :documentation "logging-stream for cl-irc.")
39    (channels :initarg :channels :accessor channels
40              :documentation "List of channels.")
41    (user-output :initarg :user-output :reader user-output
42                 :documentation
43                 "Output parameter from user, maybe stream or pathname.")
44    (unichannel :initarg :unichannel :reader unichannel :type boolean
45                :documentation "T if user-output is directory for individual channel output.")
46    (formats :initarg :formats :reader formats
47                   :documentation
48                   "A list of output formats.")
49    (async :initarg :async :reader async
50                   :documentation
51                   "Whether to use async")
52    (last-pong :initform nil :accessor last-pong
53                   :documentation
54                   "utime of last pong message")
55    (private-log :initarg :private-log :reader private-log
56                 :documentation "Pathname of the private log file for the daemon.")
57    (unknown-log :initarg :unknown-log :reader unknown-log
58                 :documentation "Pathname of the log file for unknown messages.")
59    (private-log-stream :initarg :private-log-stream :reader private-log-stream
60                        :documentation "Stream of the private log file for the daemon.")
61    (unknown-log-stream :initarg :unknown-log-stream :reader unknown-log-stream
62                 :documentation "Stream of the log file for unknown messages.")
63    (monitor-events :initform nil :accessor monitor-events
64                    :documentation "List of events for the monitor to process.")
65    (warning-message-utime :initform nil :accessor warning-message-utime
66                   :documentation
67                   "Time of last, potentially active, warning message.")))
68
69 (defvar *loggers* nil "List of active loggers.")
70
71 (defparameter *user-address-scanner*
72   (create-scanner
73    '(:sequence #\!
74      (:register
75       (:greedy-repetition 1 nil :non-whitespace-char-class)))
76    :case-insensitive-mode t))
77
78 (defun find-logger-with-nick (nick)
79   (find nick (the list *loggers*) :test #'string-equal :key #'nickname))
80
81 (defun find-logger-with-connection (conn)
82   (find conn (the list *loggers*) :test #'eq :key #'connection))
83
84 (defun canonicalize-channel-name (name)
85   (string-left-trim '(#\#) name))
86
87 (defun find-channel-with-name (logger name)
88   (find name (the list (channels logger)) :test #'string-equal :key #'name))
89   
90 (defun make-output-name (name year month day)
91     (format nil "~A-~4,'0D.~2,'0D.~2,'0D" (canonicalize-channel-name name)
92             year month day))
93
94 (defun make-output-name-utime (name utime)
95   (multiple-value-bind
96         (second minute hour day-of-month month year day-of-week daylight-p zone)
97       (decode-universal-time utime)
98     (declare (ignore second minute hour day-of-week daylight-p zone))
99     (make-output-name name year month day-of-month)))
100
101 (defgeneric write-file-header (format channel-name stream))
102
103 (defmethod write-file-header ((format t) channel-name stream)
104   (declare (ignore channel-name stream))
105   )
106
107 (defgeneric write-file-footer (format channel-name stream))
108
109 (defmethod write-file-footer ((format t) channel-name stream)
110   (declare (ignore channel-name stream))
111   )
112                                
113 (defun %log-file-path (output-root channel-name year month day type)
114   (make-pathname
115    :defaults output-root
116    :directory (append (pathname-directory output-root)
117                       (list
118                        (string-left-trim '(#\#) channel-name)
119                        (format nil "~4,'0D-~2,'0D" year month)))
120    :name (make-output-name channel-name year month day)
121    :type type))
122
123 (defgeneric log-file-path (output-root channel-name year month day format))
124
125 (defmethod log-file-path (output-root channel-name year month day
126                           (format (eql :raw)))
127   (%log-file-path output-root channel-name year month day "raw"))
128
129 (defmethod log-file-path (output-root channel-name year month day (format (eql :sexp)))
130   (%log-file-path output-root channel-name year month day "sexp"))
131
132 (defmethod log-file-path (output-root channel-name year month day (format (eql :text)))
133   (%log-file-path output-root channel-name year month day "txt"))
134
135 (defmethod log-file-path (output-root channel-name year month day (format string))
136   (%log-file-path output-root channel-name year month day format))
137
138
139 (defun log-file-path-utime (output-root channel-name format utime)
140   (multiple-value-bind
141         (second minute hour day month year day-of-week daylight-p zone)
142       (decode-universal-time utime)
143     (declare (ignore second minute hour day-of-week daylight-p zone))
144     (log-file-path output-root channel-name year month day format)))
145
146 (defun get-stream (channel istream)
147   (elt (streams channel) istream))
148
149 (defun (setf get-stream) (value channel istream)
150   (setf (elt (streams channel) istream) value))
151
152 (defun get-format (logger istream)
153   (elt (formats logger) istream))
154
155 (defun get-output-name (channel istream)
156   (elt (current-output-names channel) istream))
157
158 (defun (setf get-output-name) (value channel istream)
159   (setf (elt (current-output-names channel) istream) value))
160
161 (defun ensure-output-stream-for-unichannel (utime logger channel istream)
162   (let ((name (make-output-name-utime (name channel) utime)))
163     (unless (string= name (get-output-name channel istream))
164       (when (get-stream channel istream)
165         (write-file-footer (get-format logger istream)
166                            (name channel)
167                            (get-stream channel istream))
168         (close (get-stream channel istream)))
169       (setf (get-output-name channel istream) name)
170       (let ((path (log-file-path-utime (output-root channel) (name channel)
171                                        (get-format logger istream) utime)))
172         (unless (probe-file path)
173           (ensure-directories-exist path)
174           (setf (get-stream channel istream)
175                 (open path :direction :output :if-exists :error
176                       :if-does-not-exist :create))
177           (write-file-header (get-format logger istream)
178                              (name channel)
179                               (get-stream channel istream))
180           (close (get-stream channel istream)))
181         (setf (get-stream channel istream)
182               (open path :direction :output :if-exists :append
183                     :if-does-not-exist :create))))))
184
185 (defun ensure-output-stream (utime logger channel istream)
186   "Ensures that *output-stream* is correct."
187   (cond
188    ((streamp (user-output logger))
189     (unless (get-stream channel istream)
190       (setf (get-stream channel istream) (user-output logger))))
191    ((pathnamep (user-output logger))
192     (cond
193      ((unichannel logger)
194       (ensure-output-stream-for-unichannel utime logger channel istream))
195      (t
196       (setf (get-stream channel istream)
197         (open (user-output logger) :direction :output :if-exists :append
198               :if-does-not-exist :create)))))))
199
200 (defun format-date-time (utime)
201   (multiple-value-bind
202         (second minute hour day-of-month month year day-of-week daylight-p zone)
203       (decode-universal-time utime)
204     (declare (ignore day-of-week daylight-p zone))
205     (format nil "~4,'0D/~2,'0D/~2,'0D ~2,'0D:~2,'0D:~2,'0D" year month day-of-month hour minute second)))
206
207 (defun format-utime (utime)
208   (multiple-value-bind
209         (second minute hour day-of-month month year day-of-week daylight-p zone)
210       (decode-universal-time utime)
211     (declare (ignore day-of-month month year day-of-week daylight-p zone))
212     (format nil "~2,'0D:~2,'0D:~2,'0D" hour minute second)))
213
214 (defun format-utime-short (utime)
215   (multiple-value-bind
216         (second minute hour day-of-month month year day-of-week daylight-p zone)
217       (decode-universal-time utime)
218     (declare (ignore second day-of-month month year day-of-week daylight-p zone))
219     (format nil "~2,'0D:~2,'0D" hour minute)))
220
221 (defun user-address (msg)
222   (let ((split (split *user-address-scanner* (raw-message-string msg)
223                       :with-registers-p t)))
224     (if (second split)
225         (second split)
226         "")))
227
228 (defun need-user-address? (type)
229   (case type
230     ((:action :privmsg :names :rpl_topic)
231      nil)
232     (t
233      t)))
234
235 (defgeneric %output-event (format stream utime type channel source text msg 
236                            unichannel))
237
238 (defmethod %output-event ((format t) stream utime type channel source text
239                           msg unichannel)
240   (%output-event :raw stream utime type channel source text msg unichannel))
241
242 (defmethod %output-event ((format (eql :raw)) stream utime type channel source
243                           text msg unichannel)
244   (declare (ignore utime type channel source text text unichannel))
245   (when msg
246     (format stream "~S~%" 
247             (string-right-trim '(#\return) (raw-message-string msg)))))
248
249 (defun last-sexp-field (type msg)
250   (cond
251    ((null msg)
252     nil)
253    ((eq type :kick)
254     (trailing-argument msg))
255    ((need-user-address? type)
256     (user-address msg))))
257
258 (defmethod %output-event ((format (eql :sexp)) stream utime type channel source text
259                           msg unichannel)
260   (let ((*print-circle* nil)
261         (*print-pretty* nil))
262     (if unichannel
263         (format stream "(~S ~S ~S ~S ~S)~%" utime type source text (last-sexp-field type msg))
264       (format stream "(~S ~S ~S ~S ~S ~S)~%" utime type source channel text
265               (last-sexp-field type msg)))))
266
267 (defmethod %output-event ((format (eql :text)) stream utime type channel
268                           source text msg unichannel)
269   (format stream "~A " (format-utime utime))
270   (when (and (null unichannel) channel)
271     (format stream "[~A] " channel))
272   
273   (let ((user-address (when (and msg (need-user-address? type)) (user-address msg))))
274     (case type
275       (:privmsg
276        (format stream "<~A> ~A" source text))
277       (:action
278        (format stream "*~A* ~A" source text))
279       (:join
280        (format stream "~A [~A] has joined ~A" source user-address channel))
281       (:part
282        (format stream "-!- ~A [~A] has left ~A" source user-address channel))
283       (:nick
284        (format stream "-!- ~A is now known as ~A" source text))
285       (:kick
286        (format stream "-!- ~A [~A] has been kicked from ~A" source user-address channel))
287       (:quit
288        (format stream "-!- ~A [~A] has quit [~A]" source user-address (if text text "")))
289       (:mode
290        (format stream "-!- ~A has set mode ~A"  source text))
291       (:topic
292        (format stream "-!- ~A changed the topic of ~A to: ~A" source channel text))
293       (:notice
294        (format stream "-~A:~A- ~A" source channel text))
295       (:daemon
296        (format stream "-!!!- ~A" text))
297       (:names
298        (format stream "-!- names: ~A" text))
299       (:rpl_topic
300        (format stream "-!- topic: ~A" text))
301       (t
302        (warn "Unhandled msg type ~A." type))))
303   (write-char #\Newline stream))
304
305 (defun output-event-for-a-stream (msg type channel text logger istream)
306   (ensure-output-stream (received-time msg) logger channel istream)
307   (%output-event  (get-format logger istream) (get-stream channel istream)
308                   (received-time msg) type (name channel) (source msg) text msg
309                   (unichannel logger))
310   (force-output (get-stream channel istream)))
311
312 (defun log-daemon-message (logger fmt &rest args)
313   (let ((text (apply #'format nil fmt args))
314         (time (get-universal-time)))
315     (format (get-private-log-stream logger)
316             "~A ~A~%" (format-date-time time) text)
317     (force-output (get-private-log-stream logger))
318     (dolist (channel (channels logger))
319       (dotimes (istream (length (formats logger)))
320         (ensure-output-stream time logger channel istream)
321         (%output-event  (get-format logger istream)
322                         (get-stream channel istream)
323                         time :daemon nil nil text nil
324                         (unichannel logger))
325         (force-output (get-stream channel istream))))))
326
327 (defvar *msg*)
328 (defun output-event (msg type channel-name &optional text)
329   (setq *msg* msg)
330   (dolist (logger *loggers*)
331     (case type
332       ((:error :server :kill)
333        (format (get-private-log-stream logger)
334                "~A ~A~%"
335                (format-date-time (received-time msg))
336                (raw-message-string msg))
337        (force-output (get-private-log-stream logger)))
338       ((:quit :nick)
339        ;; send to all channels that a nickname is joined
340        (let* ((user (find-user (connection logger)
341                                (case type
342                                  (:nick (source msg))
343                                  (:quit (source msg)))))
344               (channels (when user (cl-irc::channels user))))
345          (dolist (channel (mapcar
346                            #'(lambda (name) (find-channel-with-name logger name)) 
347                            (mapcar #'cl-irc::name channels)))
348            (when channel
349              (dotimes (i (length (formats logger)))
350                (output-event-for-a-stream msg type channel text logger i))))))
351       (t
352        ;; msg contains channel name
353        (let* ((channel (find-channel-with-name logger channel-name)))
354          (when channel
355            (dotimes (i (length (formats logger)))
356              (output-event-for-a-stream msg type channel text logger i))))))))
357
358 (defun get-private-log-stream (logger)
359   (or (private-log-stream logger) *standard-output*))
360
361 (defun get-unknown-log-stream (logger)
362   (or (unknown-log-stream logger) *standard-output*))
363
364 (defun privmsg-hook (msg)
365   (let ((logger (find-logger-with-connection (connection msg)))
366         (channel (first (arguments msg))))
367     (cond
368      ((equal channel (nickname logger))
369       (format
370        (get-private-log-stream logger)
371        "~A ~A~%" 
372        (format-date-time (get-universal-time))
373        (raw-message-string msg))
374       (force-output (get-private-log-stream logger)))
375      (t
376       (output-event msg :privmsg channel (trailing-argument msg))))))
377
378 (defun action-hook (msg)
379   (output-event msg :action (first (arguments msg))
380                 (subseq (trailing-argument msg) 8 
381                         (- (length (trailing-argument msg)) 1))))
382
383 (defun nick-hook (msg)
384   (output-event msg :nick nil (trailing-argument msg)))
385
386 (defun part-hook (msg)
387   (output-event msg :part (first (arguments msg))))
388
389 (defun quit-hook (msg)
390   (output-event msg :quit nil (trailing-argument msg)))
391
392 (defun join-hook (msg)
393   (output-event msg :join (trailing-argument msg)))
394
395 (defun kick-hook (msg)
396   (let ((logger (find-logger-with-connection (connection msg)))
397         (channel (first (arguments msg)))
398         (who-kicked (second (arguments msg))))
399     (output-event msg :kick channel who-kicked)
400     (when (string-equal (nickname logger) who-kicked)
401       (format
402        (get-private-log-stream logger)
403        "~A Logging daemon ~A has been kicked from ~A (~A)~%"
404        (format-date-time (received-time msg))
405        (nickname logger)
406        channel
407        (trailing-argument msg))
408       (force-output (get-private-log-stream logger))
409       (daemon-sleep 1)
410       (remove-channel-logger logger channel)
411       (daemon-sleep 1)
412       (add-channel-logger logger channel)
413       (format
414        (get-private-log-stream logger)
415        "~A Rejoined ~A~%"
416        (format-date-time (received-time msg))
417        channel)
418       (force-output (get-private-log-stream logger)))))
419
420 (defun notice-hook (msg)
421   (let ((logger (find-logger-with-connection (connection msg)))
422         (channel (first (arguments msg))))
423     (cond
424       ((and (string-equal (source msg) "NickServ")
425             (string-equal channel (nickname logger))
426             (string-equal "owned by someone else" (trailing-argument msg)))
427        (if logger
428            (privmsg (connection msg) (source msg) (format nil "IDENTIFY ~A" (password logger)))
429          (format
430           (get-private-log-stream logger)
431           "~A NickServ asks for identity with connection not found.~%"
432           (format-date-time (received-time msg)))))
433       ((equal channel (nickname logger))
434        (format
435         (get-private-log-stream logger)
436         "~A ~A~%" 
437         (format-date-time (get-universal-time))
438         (raw-message-string msg))
439        (force-output (get-private-log-stream logger)))
440       (t
441        (output-event msg :notice channel (trailing-argument msg))))))
442
443 (defun ping-hook (msg)
444   (let ((logger (find-logger-with-connection (connection msg))))
445     (pong (connection msg) (server logger))
446     #+debug (format *standard-output* "Sending pong to ~A~%" (server logger))))
447
448 (defun pong-hook (msg)
449   (let ((logger (find-logger-with-connection (connection msg))))
450     (setf (last-pong logger) (received-time msg))))
451         
452 (defun topic-hook (msg)
453   (output-event msg :topic (first (arguments msg)) (trailing-argument msg)))
454
455 (defun mode-hook (msg)
456   (output-event msg :mode (first (arguments msg)) 
457                 (format nil "~{~A~^ ~}" (cdr (arguments msg)))))
458
459 (defun rpl_namreply-hook (msg)
460   (output-event msg :names (third (arguments msg))
461                 (trailing-argument msg)))
462
463 (defun rpl_endofnames-hook (msg)
464   (declare (ignore msg))
465   ;; nothing to do for this message
466   )
467
468 (defun rpl_topic-hook (msg)
469   (output-event msg :rpl_topic (format nil "~{~A~^ ~}" (arguments msg))
470                 (trailing-argument msg)))
471
472 (defun rpl_topicwhotime-hook (msg)
473   (output-event msg :rpl_topicwhotime
474                 (format nil "~{~A~^ ~}" (arguments msg))
475                 (trailing-argument msg)))
476
477
478 (defun invite-hook (msg)
479   (let ((logger (find-logger-with-connection (connection msg))))
480     (format 
481      (get-private-log-stream logger)
482      "~A ~A~%"
483      (format-date-time (get-universal-time))
484      (raw-message-string msg))
485     (force-output (get-private-log-stream logger))))
486
487
488 (defun make-a-channel (name formats output)
489   (make-instance 'channel
490                  :name name
491                  :streams (make-array (length formats) :initial-element nil)
492                  :output-root (when (and (pathnamep output)
493                                          (null (pathname-name output)))
494                                 output)
495                  :current-output-names (make-array (length formats)
496                                                    :initial-element nil)))
497   
498 (defun make-channels (names formats output)
499   (loop for i from 0 to (1- (length names))
500         collect (make-a-channel (elt names i) formats output)))
501
502 (defun is-unichannel-output (user-output)
503   "Returns T if output is setup for a single channel directory structure."
504   (and (pathnamep user-output) (null (pathname-name user-output))))
505
506 (defun do-connect-and-join (nick server username realname logging-stream channels)
507   (let ((conn (connect :nickname nick :server server
508                        :username username :realname realname
509                        :logging-stream logging-stream)))
510     (mapc #'(lambda (channel) (join conn channel)) channels)
511     (add-hook conn 'irc::irc-privmsg-message 'privmsg-hook)
512     (add-hook conn 'irc::ctcp-action-message 'action-hook)
513     (add-hook conn 'irc::irc-nick-message 'nick-hook)
514     (add-hook conn 'irc::irc-part-message 'part-hook)
515     (add-hook conn 'irc::irc-quit-message 'quit-hook)
516     (add-hook conn 'irc::irc-join-message 'join-hook)
517     (add-hook conn 'irc::irc-kick-message 'kick-hook)
518     (add-hook conn 'irc::irc-mode-message 'mode-hook)
519     (add-hook conn 'irc::irc-topic-message 'topic-hook)
520     (add-hook conn 'irc::irc-notice-message 'notice-hook)
521     (add-hook conn 'irc::irc-error-message 'error-hook)
522     (add-hook conn 'irc::irc-ping-message 'ping-hook)
523     (add-hook conn 'irc::irc-pong-message 'pong-hook)
524     (add-hook conn 'irc::irc-kill-message 'kill-hook)
525     (add-hook conn 'irc::irc-invite-message 'invite-hook)
526     (add-hook conn 'irc::irc-rpl_killdone-message 'warning-hook)
527     (add-hook conn 'irc::irc-rpl_closing-message 'warning-hook)
528     (add-hook conn 'irc::irc-rpl_topic-message 'rpl_topic-hook)
529     (add-hook conn 'irc::irc-rpl_namreply-message 'rpl_namreply-hook)
530     (add-hook conn 'irc::irc-rpl_endofnames-message 'rpl_endofnames-hook)
531     (add-hook conn 'irc::irc-rpl_topicwhotime-message 'rpl_topicwhotime-hook)
532     conn))
533
534 (defmethod cl-irc::irc-message-event :around ((msg cl-irc::irc-message))
535   (let ((result (call-next-method msg)))
536     (typecase msg
537       ((or irc::irc-privmsg-message irc::ctcp-action-message irc::irc-nick-message
538         irc::irc-part-message irc::irc-quit-message irc::irc-join-message
539         irc::irc-kick-message irc::irc-mode-message irc::irc-topic-message
540         irc::irc-notice-message irc::irc-error-message irc::irc-ping-message
541         irc::irc-pong-message irc::irc-kill-message irc::irc-invite-message
542         irc::irc-rpl_killdone-message irc::irc-rpl_closing-message
543         irc::irc-rpl_topic-message irc::irc-rpl_namreply-message
544         irc::irc-rpl_endofnames-message irc::irc-rpl_topicwhotime-message
545         irc::irc-rpl_motd irc::irc-rpl_motdstart-message irc::irc-rpl_endofmotd-message)
546        ;; nothing to do
547        )
548       (t
549        (let ((logger (find-logger-with-connection (connection msg))))
550          (format (get-unknown-log-stream
551                   (find-logger-with-connection (connection msg)))
552                  "~A ~A~%"
553                  (format-date-time (received-time msg ))
554                  (raw-message-string msg))
555          (force-output (get-unknown-log-stream logger)))))
556     result))
557
558 (defun create-logger (nick server &key channels output password
559                       realname username async
560                       private-log unknown-log
561                       (logging-stream t) (formats '(:text)))
562   "OUTPUT may be a pathname or a stream"
563   ;; check arguments
564   (assert formats)
565   (if (and channels (atom channels))
566       (setq channels (list channels)))
567   (if (atom formats)
568       (setq formats (list formats)))
569   (if (stringp output)
570       (setq output (parse-namestring output)))
571   (let* ((conn (do-connect-and-join nick server username realname logging-stream channels))
572          (logger (make-instance
573                   'logger
574                   :connection conn
575                   :nick nick
576                   :password password
577                   :server server
578                   :channels (make-channels channels formats output)
579                   :channel-names channels
580                   :username username
581                   :realname realname
582                   :async async
583                   :logging-stream logging-stream
584                   :user-output output
585                   :formats formats
586                   :private-log private-log
587                   :unknown-log unknown-log
588                   :private-log-stream (when private-log
589                                         (open private-log :direction :output
590                                               :if-exists :append
591                                               :if-does-not-exist :create))
592                   :unknown-log-stream (when unknown-log
593                                         (open unknown-log :direction :output
594                                               :if-exists :append
595                                               :if-does-not-exist :create))
596                   :unichannel (is-unichannel-output output))))
597     (unless *daemon-monitor-process*
598       (setq *daemon-monitor-process* (cl-irc::start-process 'daemon-monitor "logger-monitor")))
599     logger))
600
601 (defun start-logger (logger async)
602   (if async
603       (setf (handler logger)
604         (start-background-message-handler (connection logger)))
605       (read-message-loop (connection logger))))
606
607 (defun remove-logger (nick)
608   "Quit the active connection with nick and remove from active list."
609   (let ((logger (find-logger-with-nick nick)))
610     (cond
611       ((null logger)
612        (warn
613         "~A No active connection found with nick ~A [remove-logger].~%"
614         (format-date-time (get-universal-time))
615         nick)
616        nil)
617       (t
618        (ignore-errors (irc:quit (connection logger) ""))
619        (stop-background-message-handler (handler logger))
620        (sleep 1)
621        (dolist (channel (channels logger))
622          (let ((c (connection logger)))
623            (when c
624              (ignore-errors (remove-channel c (find-channel c (name channel)))))))
625        (when (private-log-stream logger)
626          (close (private-log-stream logger)))
627        (when (unknown-log-stream logger)
628          (close (unknown-log-stream logger)))
629
630        (setq *loggers*
631              (delete nick *loggers*  :test #'string-equal :key #'nickname))
632        t))))
633
634 (defun add-logger (nick server &key channels output (password "")
635                                     realname username private-log unknown-log
636                                     (logging-stream t) (async t)
637                                     (formats '(:text)))
638   (when (find-logger-with-nick nick)
639     (format
640      (get-private-log-stream (find-logger-with-nick nick))
641      "~A Closing previously active connection [add-logger].~%"
642      (format-date-time (get-universal-time)))
643     (ignore-errors (remove-logger nick)))
644   (let ((logger (create-logger nick server :channels channels :output output
645                                :logging-stream logging-stream :password password
646                                :realname realname :username username
647                                :private-log private-log 
648                                :unknown-log unknown-log 
649                                :formats formats
650                                :async async)))
651     (push logger *loggers*)
652     (start-logger logger async)
653     logger))
654   
655 (defun add-channel-logger (logger channel-name)
656   (cond
657     ((find-channel-with-name logger channel-name)
658      (format (get-private-log-stream logger)
659              "~A Channel ~A already in logger ~A.~%" 
660              (format-date-time (get-universal-time))
661              channel-name logger)
662      (force-output (get-private-log-stream logger))
663      nil)
664     (t
665      (let ((channel (make-a-channel channel-name (formats logger) (user-output logger))))
666        (join (connection logger) channel-name)
667        (push channel (channels logger))
668        (push channel-name (channel-names logger))))))
669
670 (defun remove-channel-logger (logger channel-name)
671   (let ((channel (find-channel-with-name logger channel-name)))
672     (cond
673       (channel
674        (part (connection logger) channel-name)
675        (dotimes (i (length (streams channel)))
676          (when (streamp (get-stream channel i))
677            (close (get-stream channel i))
678            (setf (get-stream channel i) nil)))
679        (setf (channels logger) (delete channel-name (channels logger)
680                                        :test #'string-equal
681                                        :key #'name))
682        (setf (channel-names logger) (delete channel-name (channel-names logger)
683                                             :test #'string-equal))
684        t)
685       (t
686        (format
687         (get-private-log-stream logger)
688         "~A Channel name ~A not found in logger ~A.~%" 
689         (format-date-time (get-universal-time)) 
690         channel-name logger)
691        nil))))
692
693 (defun add-hook-logger (logger class hook)
694   (add-hook (connection logger) class hook))
695
696 (defun remove-hook-logger (logger class hook)
697   (remove-hook (connection logger) class hook))
698
699 (defvar *warning-message-utime* nil)
700
701 (defun kill-hook (msg)
702   (let ((target (second (arguments msg)))
703         (logger (find-logger-with-connection (connection msg))))
704     (when (and (stringp target)
705                (string-equal target (nickname logger)))
706       (setf (warning-message-utime logger) (received-time msg)))
707     (format (get-private-log-stream logger)
708             "~A Killed by ~A~%"
709             (format-date-time (received-time msg))
710             (source msg))
711     (force-output (get-private-log-stream logger))))
712
713 (defun error-hook (msg)
714   (let ((text (trailing-argument msg))
715         (logger (find-logger-with-connection (connection msg))))
716     (when (and (stringp text) 
717                (zerop (search (format nil "Closing Link: ~A" (nickname logger)) text)))
718       (setf (warning-message-utime logger) (received-time msg)))
719     (output-event msg :error nil (trailing-argument msg))))
720
721 (defun warning-hook (msg)
722   (let ((logger (find-logger-with-connection (connection msg))))
723     (output-event msg :server 
724                   (format nil "~{~A~^ ~} ~A)"
725                           (arguments msg)
726                           (trailing-argument msg)))
727     (when logger
728       (setf (warning-message-utime logger) (get-universal-time)))))
729   
730 (defun daemon-sleep (seconds)
731   #-allegro (sleep seconds)
732   #+allegro (mp:process-sleep seconds))
733
734 (defun log-disconnection (logger)
735   ;; avoid generating too many reconnect messages in the logs
736   (when (or (null (warning-message-utime logger))
737             (< (- (get-universal-time) (warning-message-utime logger)) 300))
738     (log-daemon-message logger "Disconnected. Attempting reconnection at ~A."
739                         (format-date-time (get-universal-time)))))
740
741 (defun log-reconnection (logger)
742   (log-daemon-message
743    logger
744    "Connection restablished at ~A." (format-date-time (get-universal-time))))
745
746 (defun is-connected (logger)
747   (when (ignore-errors (ping (connection logger) (server logger)))
748     (dotimes (i 20)
749       (when (and (last-pong logger)
750                  (< (- (get-universal-time) (last-pong logger)) 21))
751         (return-from is-connected t))
752       (sleep 1))))
753
754
755 (let (*recon-nick* *recon-server* *recon-username* *recon-realname*
756       *recon-user-output* *recon-private-log* *recon-unknown-log*
757       *recon-formats* *recon-async* *recon-logging-stream* *recon-channel-names*)
758   (declare (special *recon-nick* *recon-server* *recon-username* *recon-realname*
759                     *recon-formats* *recon-password* *recon-async* 
760                     *recon-user-output* *recon-private-log* *recon-unknown-log*
761                     *recon-logging-stream* *recon-channel-names*))
762   
763   (defun attempt-reconnection (logger)
764     (when (is-connected logger)
765       (return-from attempt-reconnection nil))
766     
767     (log-disconnection logger)
768     (when (connection logger)
769       (ignore-errors (quit (connection logger) "Client terminated by server"))
770       (setf *recon-nick* (nickname logger)
771             *recon-server* (server logger)
772             *recon-username* (username logger)
773             *recon-realname* (realname logger)
774             *recon-password* (password logger)
775             *recon-async* (async logger)
776             *recon-user-output* (user-output logger)
777             *recon-private-log* (private-log logger)
778             *recon-unknown-log* (unknown-log logger)
779             *recon-formats* (formats logger)
780             *recon-logging-stream* (logging-stream logger)
781             *recon-channel-names* (channel-names logger))
782       (ignore-errors (remove-logger logger)))
783     
784     (let ((new-logger
785            (ignore-errors
786             (add-logger *recon-nick* *recon-server*
787                         :channels *recon-channel-names*
788                         :output *recon-user-output*
789                         :password *recon-password*
790                         :realname *recon-realname*
791                         :username *recon-username*
792                         :logging-stream *recon-logging-stream*
793                         :private-log *recon-private-log*
794                         :unknown-log *recon-unknown-log*
795                         :async *recon-async*
796                         :formats *recon-formats*))))
797       (when new-logger
798         (sleep 5)
799         (when (is-connected new-logger)
800           (log-reconnection new-logger)))))
801       
802   )
803
804 (defun daemon-monitor ()
805   "This function runs in the background and monitors the connection of the logger."
806   ;; run forever
807   (do ()
808       ()
809     (block main-loop
810       (dolist (logger *loggers*)
811         (do ((warning-time (warning-message-utime logger) (warning-message-utime logger)))
812             ((or (is-connected logger) (null warning-time)))
813           (cond
814            ((and warning-time (> (- (get-universal-time) warning-time) 180))
815             ;;give up frequent checking because no disconnection despite waiting
816             (setf (warning-message-utime logger) nil))
817            ((not (is-connected logger))
818             (unless warning-time
819               (setf (warning-message-utime logger) (get-universal-time)))
820             (attempt-reconnection logger)
821             ;;after a succesful reconnection, the value of logger will be invalid 
822             (sleep 30)
823             (return-from main-loop))
824            (t
825             (daemon-sleep 30)))))
826       (do ((i 0 (1+ i)))
827           ((or (>= i 20) (some (lambda (logger) (warning-message-utime logger)) *loggers*))) 
828         (daemon-sleep 15)))))
829   
830
831