r8496: make last field for rpl_topicwhotime a utime
[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 (uffi:def-foreign-type time-t :unsigned-long)
250 (uffi:def-function ("ctime" c-ctime) 
251     ((time (* time-t)))
252   :returning :cstring)
253
254 (defconstant +posix-epoch+
255   (encode-universal-time 0 0 0 1 1 1970 0))
256
257 (defun posix-time-to-utime (time)
258   (+ time +posix-epoch+))
259
260 (defun last-sexp-field (type msg)
261   (cond
262    ((null msg)
263     nil)
264    ((eq type :kick)
265     (trailing-argument msg))
266    ((eq type :rpl_topicwhotime)
267     (when (stringp (car (last (arguments msg))))
268       (let ((secs (parse-integer (car (last (arguments msg))) :junk-allowed t)))
269         (when secs
270           (posix-time-to-utime secs)))))
271    ((need-user-address? type)
272     (user-address msg))))
273
274 (defmethod %output-event ((format (eql :sexp)) stream utime type channel source text
275                           msg unichannel)
276   (let ((*print-circle* nil)
277         (*print-pretty* nil))
278     (if unichannel
279         (format stream "(~S ~S ~S ~S ~S)~%" utime type source text (last-sexp-field type msg))
280       (format stream "(~S ~S ~S ~S ~S ~S)~%" utime type source channel text
281               (last-sexp-field type msg)))))
282
283 (defmethod %output-event ((format (eql :text)) stream utime type channel
284                           source text msg unichannel)
285   (format stream "~A " (format-utime utime))
286   (when (and (null unichannel) channel)
287     (format stream "[~A] " channel))
288   
289   (let ((user-address (when (and msg (need-user-address? type)) (user-address msg))))
290     (case type
291       (:privmsg
292        (format stream "<~A> ~A" source text))
293       (:action
294        (format stream "*~A* ~A" source text))
295       (:join
296        (format stream "~A [~A] has joined ~A" source user-address channel))
297       (:part
298        (format stream "-!- ~A [~A] has left ~A" source user-address channel))
299       (:nick
300        (format stream "-!- ~A is now known as ~A" source text))
301       (:kick
302        (format stream "-!- ~A [~A] has been kicked from ~A" source user-address channel))
303       (:quit
304        (format stream "-!- ~A [~A] has quit [~A]" source user-address (if text text "")))
305       (:mode
306        (format stream "-!- ~A has set mode ~A"  source text))
307       (:topic
308        (format stream "-!- ~A changed the topic of ~A to: ~A" source channel text))
309       (:notice
310        (format stream "-~A:~A- ~A" source channel text))
311       (:daemon
312        (format stream "-!!!- ~A" text))
313       (:names
314        (format stream "-!- names: ~A" text))
315       (:rpl_topic
316        (format stream "-!- topic: ~A" text))
317       (t
318        (warn "Unhandled msg type ~A." type))))
319   (write-char #\Newline stream))
320
321 (defun output-event-for-a-stream (msg type channel text logger istream)
322   (ensure-output-stream (received-time msg) logger channel istream)
323   (%output-event  (get-format logger istream) (get-stream channel istream)
324                   (received-time msg) type (name channel) (source msg) text msg
325                   (unichannel logger))
326   (force-output (get-stream channel istream)))
327
328 (defun log-daemon-message (logger fmt &rest args)
329   (let ((text (apply #'format nil fmt args))
330         (time (get-universal-time)))
331     (format (get-private-log-stream logger)
332             "~A ~A~%" (format-date-time time) text)
333     (force-output (get-private-log-stream logger))
334     (dolist (channel (channels logger))
335       (dotimes (istream (length (formats logger)))
336         (ensure-output-stream time logger channel istream)
337         (%output-event  (get-format logger istream)
338                         (get-stream channel istream)
339                         time :daemon nil nil text nil
340                         (unichannel logger))
341         (force-output (get-stream channel istream))))))
342
343 (defvar *msg*)
344 (defun output-event (msg type channel-name &optional text)
345   (setq *msg* msg)
346   (dolist (logger *loggers*)
347     (case type
348       ((:error :server :kill)
349        (format (get-private-log-stream logger)
350                "~A ~A~%"
351                (format-date-time (received-time msg))
352                (raw-message-string msg))
353        (force-output (get-private-log-stream logger)))
354       ((:quit :nick)
355        ;; send to all channels that a nickname is joined
356        (let* ((user (find-user (connection logger)
357                                (case type
358                                  (:nick (source msg))
359                                  (:quit (source msg)))))
360               (channels (when user (cl-irc::channels user))))
361          (dolist (channel (mapcar
362                            #'(lambda (name) (find-channel-with-name logger name)) 
363                            (mapcar #'cl-irc::name channels)))
364            (when channel
365              (dotimes (i (length (formats logger)))
366                (output-event-for-a-stream msg type channel text logger i))))))
367       (t
368        ;; msg contains channel name
369        (let* ((channel (find-channel-with-name logger channel-name)))
370          (when channel
371            (dotimes (i (length (formats logger)))
372              (output-event-for-a-stream msg type channel text logger i))))))))
373
374 (defun get-private-log-stream (logger)
375   (or (private-log-stream logger) *standard-output*))
376
377 (defun get-unknown-log-stream (logger)
378   (or (unknown-log-stream logger) *standard-output*))
379
380 (defun privmsg-hook (msg)
381   (let ((logger (find-logger-with-connection (connection msg)))
382         (channel (first (arguments msg))))
383     (cond
384      ((equal channel (nickname logger))
385       (format
386        (get-private-log-stream logger)
387        "~A ~A~%" 
388        (format-date-time (get-universal-time))
389        (raw-message-string msg))
390       (force-output (get-private-log-stream logger)))
391      (t
392       (output-event msg :privmsg channel (trailing-argument msg))))))
393
394 (defun action-hook (msg)
395   (output-event msg :action (first (arguments msg))
396                 (subseq (trailing-argument msg) 8 
397                         (- (length (trailing-argument msg)) 1))))
398
399 (defun nick-hook (msg)
400   (output-event msg :nick nil (trailing-argument msg)))
401
402 (defun part-hook (msg)
403   (output-event msg :part (first (arguments msg))))
404
405 (defun quit-hook (msg)
406   (output-event msg :quit nil (trailing-argument msg)))
407
408 (defun join-hook (msg)
409   (output-event msg :join (trailing-argument msg)))
410
411 (defun kick-hook (msg)
412   (let ((logger (find-logger-with-connection (connection msg)))
413         (channel (first (arguments msg)))
414         (who-kicked (second (arguments msg))))
415     (output-event msg :kick channel who-kicked)
416     (when (string-equal (nickname logger) who-kicked)
417       (format
418        (get-private-log-stream logger)
419        "~A Logging daemon ~A has been kicked from ~A (~A)~%"
420        (format-date-time (received-time msg))
421        (nickname logger)
422        channel
423        (trailing-argument msg))
424       (force-output (get-private-log-stream logger))
425       (daemon-sleep 1)
426       (remove-channel-logger logger channel)
427       (daemon-sleep 1)
428       (add-channel-logger logger channel)
429       (format
430        (get-private-log-stream logger)
431        "~A Rejoined ~A~%"
432        (format-date-time (received-time msg))
433        channel)
434       (force-output (get-private-log-stream logger)))))
435
436 (defun notice-hook (msg)
437   (let ((logger (find-logger-with-connection (connection msg)))
438         (channel (first (arguments msg))))
439     (cond
440       ((and (string-equal (source msg) "NickServ")
441             (string-equal channel (nickname logger))
442             (string-equal "owned by someone else" (trailing-argument msg)))
443        (if logger
444            (privmsg (connection msg) (source msg) (format nil "IDENTIFY ~A" (password logger)))
445          (format
446           (get-private-log-stream logger)
447           "~A NickServ asks for identity with connection not found.~%"
448           (format-date-time (received-time msg)))))
449       ((equal channel (nickname logger))
450        (format
451         (get-private-log-stream logger)
452         "~A ~A~%" 
453         (format-date-time (get-universal-time))
454         (raw-message-string msg))
455        (force-output (get-private-log-stream logger)))
456       (t
457        (output-event msg :notice channel (trailing-argument msg))))))
458
459 (defun ping-hook (msg)
460   (let ((logger (find-logger-with-connection (connection msg))))
461     (pong (connection msg) (server logger))
462     #+debug (format *standard-output* "Sending pong to ~A~%" (server logger))))
463
464 (defun pong-hook (msg)
465   (let ((logger (find-logger-with-connection (connection msg))))
466     (setf (last-pong logger) (received-time msg))))
467         
468 (defun topic-hook (msg)
469   (output-event msg :topic (first (arguments msg)) (trailing-argument msg)))
470
471 (defun mode-hook (msg)
472   (output-event msg :mode (first (arguments msg)) 
473                 (format nil "~{~A~^ ~}" (cdr (arguments msg)))))
474
475 (defun rpl_namreply-hook (msg)
476   (output-event msg :names (third (arguments msg))
477                 (trailing-argument msg)))
478
479 (defun rpl_endofnames-hook (msg)
480   (declare (ignore msg))
481   ;; nothing to do for this message
482   )
483
484 (defun rpl_topic-hook (msg)
485   (output-event msg :rpl_topic (format nil "~{~A~^ ~}" (arguments msg))
486                 (trailing-argument msg)))
487
488 (defun rpl_topicwhotime-hook (msg)
489   (output-event msg :rpl_topicwhotime
490                 (second (arguments msg))
491                 (third (arguments msg))))
492
493
494 (defun invite-hook (msg)
495   (let ((logger (find-logger-with-connection (connection msg))))
496     (format 
497      (get-private-log-stream logger)
498      "~A ~A~%"
499      (format-date-time (get-universal-time))
500      (raw-message-string msg))
501     (force-output (get-private-log-stream logger))))
502
503
504 (defun make-a-channel (name formats output)
505   (make-instance 'channel
506                  :name name
507                  :streams (make-array (length formats) :initial-element nil)
508                  :output-root (when (and (pathnamep output)
509                                          (null (pathname-name output)))
510                                 output)
511                  :current-output-names (make-array (length formats)
512                                                    :initial-element nil)))
513   
514 (defun make-channels (names formats output)
515   (loop for i from 0 to (1- (length names))
516         collect (make-a-channel (elt names i) formats output)))
517
518 (defun is-unichannel-output (user-output)
519   "Returns T if output is setup for a single channel directory structure."
520   (and (pathnamep user-output) (null (pathname-name user-output))))
521
522 (defun do-connect-and-join (nick server username realname logging-stream channels)
523   (let ((conn (connect :nickname nick :server server
524                        :username username :realname realname
525                        :logging-stream logging-stream)))
526     (mapc #'(lambda (channel) (join conn channel)) channels)
527     (add-hook conn 'irc::irc-privmsg-message 'privmsg-hook)
528     (add-hook conn 'irc::ctcp-action-message 'action-hook)
529     (add-hook conn 'irc::irc-nick-message 'nick-hook)
530     (add-hook conn 'irc::irc-part-message 'part-hook)
531     (add-hook conn 'irc::irc-quit-message 'quit-hook)
532     (add-hook conn 'irc::irc-join-message 'join-hook)
533     (add-hook conn 'irc::irc-kick-message 'kick-hook)
534     (add-hook conn 'irc::irc-mode-message 'mode-hook)
535     (add-hook conn 'irc::irc-topic-message 'topic-hook)
536     (add-hook conn 'irc::irc-notice-message 'notice-hook)
537     (add-hook conn 'irc::irc-error-message 'error-hook)
538     (add-hook conn 'irc::irc-ping-message 'ping-hook)
539     (add-hook conn 'irc::irc-pong-message 'pong-hook)
540     (add-hook conn 'irc::irc-kill-message 'kill-hook)
541     (add-hook conn 'irc::irc-invite-message 'invite-hook)
542     (add-hook conn 'irc::irc-rpl_killdone-message 'warning-hook)
543     (add-hook conn 'irc::irc-rpl_closing-message 'warning-hook)
544     (add-hook conn 'irc::irc-rpl_topic-message 'rpl_topic-hook)
545     (add-hook conn 'irc::irc-rpl_namreply-message 'rpl_namreply-hook)
546     (add-hook conn 'irc::irc-rpl_endofnames-message 'rpl_endofnames-hook)
547     (add-hook conn 'irc::irc-rpl_topicwhotime-message 'rpl_topicwhotime-hook)
548     conn))
549
550 (defmethod cl-irc::irc-message-event :around ((msg cl-irc::irc-message))
551   (let ((result (call-next-method msg)))
552     (typecase msg
553       ((or irc::irc-privmsg-message irc::ctcp-action-message irc::irc-nick-message
554         irc::irc-part-message irc::irc-quit-message irc::irc-join-message
555         irc::irc-kick-message irc::irc-mode-message irc::irc-topic-message
556         irc::irc-notice-message irc::irc-error-message irc::irc-ping-message
557         irc::irc-pong-message irc::irc-kill-message irc::irc-invite-message
558         irc::irc-rpl_killdone-message irc::irc-rpl_closing-message
559         irc::irc-rpl_topic-message irc::irc-rpl_namreply-message
560         irc::irc-rpl_endofnames-message irc::irc-rpl_topicwhotime-message
561         irc::irc-rpl_motd-message irc::irc-rpl_motdstart-message
562         irc::irc-rpl_endofmotd-message)
563        ;; nothing to do
564        )
565       (t
566        (let ((logger (find-logger-with-connection (connection msg))))
567          (format (get-unknown-log-stream
568                   (find-logger-with-connection (connection msg)))
569                  "~A ~A~%"
570                  (format-date-time (received-time msg ))
571                  (raw-message-string msg))
572          (force-output (get-unknown-log-stream logger)))))
573     result))
574
575 (defun create-logger (nick server &key channels output password
576                       realname username async
577                       private-log unknown-log
578                       (logging-stream t) (formats '(:text)))
579   "OUTPUT may be a pathname or a stream"
580   ;; check arguments
581   (assert formats)
582   (if (and channels (atom channels))
583       (setq channels (list channels)))
584   (if (atom formats)
585       (setq formats (list formats)))
586   (if (stringp output)
587       (setq output (parse-namestring output)))
588   (let* ((conn (do-connect-and-join nick server username realname logging-stream channels))
589          (logger (make-instance
590                   'logger
591                   :connection conn
592                   :nick nick
593                   :password password
594                   :server server
595                   :channels (make-channels channels formats output)
596                   :channel-names channels
597                   :username username
598                   :realname realname
599                   :async async
600                   :logging-stream logging-stream
601                   :user-output output
602                   :formats formats
603                   :private-log private-log
604                   :unknown-log unknown-log
605                   :private-log-stream (when private-log
606                                         (open private-log :direction :output
607                                               :if-exists :append
608                                               :if-does-not-exist :create))
609                   :unknown-log-stream (when unknown-log
610                                         (open unknown-log :direction :output
611                                               :if-exists :append
612                                               :if-does-not-exist :create))
613                   :unichannel (is-unichannel-output output))))
614     (unless *daemon-monitor-process*
615       (setq *daemon-monitor-process* (cl-irc::start-process 'daemon-monitor "logger-monitor")))
616     logger))
617
618 (defun start-logger (logger async)
619   (if async
620       (setf (handler logger)
621         (start-background-message-handler (connection logger)))
622       (read-message-loop (connection logger))))
623
624 (defun remove-logger (nick)
625   "Quit the active connection with nick and remove from active list."
626   (let ((logger (find-logger-with-nick nick)))
627     (cond
628       ((null logger)
629        (warn
630         "~A No active connection found with nick ~A [remove-logger].~%"
631         (format-date-time (get-universal-time))
632         nick)
633        nil)
634       (t
635        (ignore-errors (irc:quit (connection logger) ""))
636        (stop-background-message-handler (handler logger))
637        (sleep 1)
638        (dolist (channel (channels logger))
639          (let ((c (connection logger)))
640            (when c
641              (ignore-errors (remove-channel c (find-channel c (name channel)))))))
642        (when (private-log-stream logger)
643          (close (private-log-stream logger)))
644        (when (unknown-log-stream logger)
645          (close (unknown-log-stream logger)))
646
647        (setq *loggers*
648              (delete nick *loggers*  :test #'string-equal :key #'nickname))
649        t))))
650
651 (defun add-logger (nick server &key channels output (password "")
652                                     realname username private-log unknown-log
653                                     (logging-stream t) (async t)
654                                     (formats '(:sexp)))
655   (when (find-logger-with-nick nick)
656     (format
657      (get-private-log-stream (find-logger-with-nick nick))
658      "~A Closing previously active connection [add-logger].~%"
659      (format-date-time (get-universal-time)))
660     (ignore-errors (remove-logger nick)))
661   (let ((logger (create-logger nick server :channels channels :output output
662                                :logging-stream logging-stream :password password
663                                :realname realname :username username
664                                :private-log private-log 
665                                :unknown-log unknown-log 
666                                :formats formats
667                                :async async)))
668     (push logger *loggers*)
669     (start-logger logger async)
670     logger))
671   
672 (defun add-channel-logger (logger channel-name)
673   (cond
674     ((find-channel-with-name logger channel-name)
675      (format (get-private-log-stream logger)
676              "~A Channel ~A already in logger ~A.~%" 
677              (format-date-time (get-universal-time))
678              channel-name logger)
679      (force-output (get-private-log-stream logger))
680      nil)
681     (t
682      (let ((channel (make-a-channel channel-name (formats logger) (user-output logger))))
683        (join (connection logger) channel-name)
684        (push channel (channels logger))
685        (push channel-name (channel-names logger))))))
686
687 (defun remove-channel-logger (logger channel-name)
688   (let ((channel (find-channel-with-name logger channel-name)))
689     (cond
690       (channel
691        (part (connection logger) channel-name)
692        (dotimes (i (length (streams channel)))
693          (when (streamp (get-stream channel i))
694            (close (get-stream channel i))
695            (setf (get-stream channel i) nil)))
696        (setf (channels logger) (delete channel-name (channels logger)
697                                        :test #'string-equal
698                                        :key #'name))
699        (setf (channel-names logger) (delete channel-name (channel-names logger)
700                                             :test #'string-equal))
701        t)
702       (t
703        (format
704         (get-private-log-stream logger)
705         "~A Channel name ~A not found in logger ~A.~%" 
706         (format-date-time (get-universal-time)) 
707         channel-name logger)
708        nil))))
709
710 (defun add-hook-logger (logger class hook)
711   (add-hook (connection logger) class hook))
712
713 (defun remove-hook-logger (logger class hook)
714   (remove-hook (connection logger) class hook))
715
716 (defvar *warning-message-utime* nil)
717
718 (defun kill-hook (msg)
719   (let ((target (second (arguments msg)))
720         (logger (find-logger-with-connection (connection msg))))
721     (when (and (stringp target)
722                (string-equal target (nickname logger)))
723       (setf (warning-message-utime logger) (received-time msg)))
724     (format (get-private-log-stream logger)
725             "~A Killed by ~A~%"
726             (format-date-time (received-time msg))
727             (source msg))
728     (force-output (get-private-log-stream logger))))
729
730 (defun error-hook (msg)
731   (let ((text (trailing-argument msg))
732         (logger (find-logger-with-connection (connection msg))))
733     (when (and (stringp text) 
734                (zerop (search (format nil "Closing Link: ~A" (nickname logger)) text)))
735       (setf (warning-message-utime logger) (received-time msg)))
736     (output-event msg :error nil (trailing-argument msg))))
737
738 (defun warning-hook (msg)
739   (let ((logger (find-logger-with-connection (connection msg))))
740     (output-event msg :server 
741                   (format nil "~{~A~^ ~} ~A)"
742                           (arguments msg)
743                           (trailing-argument msg)))
744     (when logger
745       (setf (warning-message-utime logger) (get-universal-time)))))
746   
747 (defun daemon-sleep (seconds)
748   #-allegro (sleep seconds)
749   #+allegro (mp:process-sleep seconds))
750
751 (defun log-disconnection (logger)
752   ;; avoid generating too many reconnect messages in the logs
753   (when (or (null (warning-message-utime logger))
754             (< (- (get-universal-time) (warning-message-utime logger)) 300))
755     (log-daemon-message logger "Disconnected. Attempting reconnection at ~A."
756                         (format-date-time (get-universal-time)))))
757
758 (defun log-reconnection (logger)
759   (log-daemon-message
760    logger
761    "Connection restablished at ~A." (format-date-time (get-universal-time))))
762
763 (defun is-connected (logger)
764   (when (ignore-errors (ping (connection logger) (server logger)))
765     (dotimes (i 20)
766       (when (and (last-pong logger)
767                  (< (- (get-universal-time) (last-pong logger)) 21))
768         (return-from is-connected t))
769       (sleep 1))))
770
771
772 (let (*recon-nick* *recon-server* *recon-username* *recon-realname*
773       *recon-user-output* *recon-private-log* *recon-unknown-log*
774       *recon-formats* *recon-async* *recon-logging-stream* *recon-channel-names*)
775   (declare (special *recon-nick* *recon-server* *recon-username* *recon-realname*
776                     *recon-formats* *recon-password* *recon-async* 
777                     *recon-user-output* *recon-private-log* *recon-unknown-log*
778                     *recon-logging-stream* *recon-channel-names*))
779   
780   (defun attempt-reconnection (logger)
781     (when (is-connected logger)
782       (return-from attempt-reconnection nil))
783     
784     (log-disconnection logger)
785     (when (connection logger)
786       (ignore-errors (quit (connection logger) "Client terminated by server"))
787       (setf *recon-nick* (nickname logger)
788             *recon-server* (server logger)
789             *recon-username* (username logger)
790             *recon-realname* (realname logger)
791             *recon-password* (password logger)
792             *recon-async* (async logger)
793             *recon-user-output* (user-output logger)
794             *recon-private-log* (private-log logger)
795             *recon-unknown-log* (unknown-log logger)
796             *recon-formats* (formats logger)
797             *recon-logging-stream* (logging-stream logger)
798             *recon-channel-names* (channel-names logger))
799       (ignore-errors (remove-logger logger)))
800     
801     (let ((new-logger
802            (ignore-errors
803             (add-logger *recon-nick* *recon-server*
804                         :channels *recon-channel-names*
805                         :output *recon-user-output*
806                         :password *recon-password*
807                         :realname *recon-realname*
808                         :username *recon-username*
809                         :logging-stream *recon-logging-stream*
810                         :private-log *recon-private-log*
811                         :unknown-log *recon-unknown-log*
812                         :async *recon-async*
813                         :formats *recon-formats*))))
814       (when new-logger
815         (sleep 5)
816         (when (is-connected new-logger)
817           (log-reconnection new-logger)))))
818       
819   )
820
821 (defun daemon-monitor ()
822   "This function runs in the background and monitors the connection of the logger."
823   ;; run forever
824   (do ()
825       ()
826     (block main-loop
827       (dolist (logger *loggers*)
828         (do ((warning-time (warning-message-utime logger) (warning-message-utime logger)))
829             ((or (is-connected logger) (null warning-time)))
830           (cond
831            ((and warning-time (> (- (get-universal-time) warning-time) 180))
832             ;;give up frequent checking because no disconnection despite waiting
833             (setf (warning-message-utime logger) nil))
834            ((not (is-connected logger))
835             (unless warning-time
836               (setf (warning-message-utime logger) (get-universal-time)))
837             (attempt-reconnection logger)
838             ;;after a succesful reconnection, the value of logger will be invalid 
839             (sleep 30)
840             (return-from main-loop))
841            (t
842             (daemon-sleep 30)))))
843       (do ((i 0 (1+ i)))
844           ((or (>= i 20) (some (lambda (logger) (warning-message-utime logger)) *loggers*))) 
845         (daemon-sleep 15)))))
846   
847
848