Update of /project/net-nittin-irc/cvsroot/net-nittin-irc/example In directory common-lisp.net:/tmp/cvs-serv3113
Modified Files: logger.lisp Added Files: irclogs.css Log Message: add multiple,simultaneous loggers, channels, and formats. add html and sexp formats. Date: Sun Dec 14 05:40:09 2003 Author: krosenberg
Index: net-nittin-irc/example/logger.lisp diff -u net-nittin-irc/example/logger.lisp:1.2 net-nittin-irc/example/logger.lisp:1.3 --- net-nittin-irc/example/logger.lisp:1.2 Sat Dec 13 23:22:24 2003 +++ net-nittin-irc/example/logger.lisp Sun Dec 14 05:40:08 2003 @@ -1,54 +1,132 @@ ;;;; -*- Mode: Lisp -*- -;;;; $Id: logger.lisp,v 1.2 2003/12/14 04:22:24 krosenberg Exp $ +;;;; $Id: logger.lisp,v 1.3 2003/12/14 10:40:08 krosenberg Exp $ ;;;; ;;;; Purpose: A logging bot ;;;; Author: Kevin Rosenberg ;;;; License: net-nittin-irc license
;;; Quickstart: -;;; - load net-nittin-irc asdf system +;;; - have net-nittin-irc, cl-ppcre paths on your asdf:*central-registry* ;;; - load this file: logger.lisp ;;; - (logger:start-logger-bot <nickname> <server> &keys channels output)
+(unless (find-package 'net-nittin-irc) + (asdf:operate 'asdf:load-op 'net-nittin-irc)) +(unless (find-package 'cl-ppcre) + (asdf:operate 'asdf:load-op 'cl-ppcre)) +#+ignore +(unless (find-package 'puri) + (asdf:operate 'asdf:load-op 'puri)) + (in-package cl-user) (defpackage logger - (:use :common-lisp :irc) + (:use :common-lisp :irc :cl-ppcre) (:export #:start-logger-bot)) (in-package logger)
-(defvar *bot-nickname* nil) -(defvar *connection* nil) -(defvar *output* nil "User output parametet to start-logger-bot.") -(defvar *current-output-name* "Name of current output file.") -(defvar *base-name* nil "Base name for output files.") -(defvar *output-stream* nil "Current output stream.") +(defclass channel () + ((name :initarg :name :reader name + :documentation "Name of channel.") + (streams :initarg :streams :reader streams + :documentation "List of output streams.") + (base-name :initarg :base-name :reader base-name + :documentation "Base file name for channel") + (current-output-names :initarg :current-output-names :accessor current-output-names))) + + +(defclass logger () + ((connection :initarg :connection :reader connection + :documentation "IRC connection object.") + (nick :initarg :nick :reader nickname + :documentation "Nickname of the bot.") + (server :initarg :server :reader server + :documentation "Connected IRC server.") + (channels :initarg :channels :reader channels + :documentation "List of channels.") + (user-output :initarg :user-output :reader user-output + :documentation + "Output parameter from user, maybe stream or pathname.") + (base-name :initarg :base-name :reader base-name + :documentation "Base name of log files.") + (formats :initarg :formats :reader formats + :documentation + "A list of output formats."))) + +(defvar *loggers* nil "List of active loggers.") +(defparameter *uri-scanner* + (create-scanner + '(:register + (:alternation + (:sequence :word-boundary "http://" + (:char-class (:range #\a #\z) (:range #\a #\z) (:range #\0 #\9)) + (:greedy-repetition 1 nil :non-whitespace-char-class)) + (:sequence :word-boundary "ftp://" + (:char-class (:range #\a #\z) (:range #\a #\z) (:range #\0 #\9)) + (:greedy-repetition 1 nil :non-whitespace-char-class)) + (:sequence :word-boundary "mailto:" + (:char-class (:range #\a #\z) (:range #\a #\z) (:range #\0 #\9)) + (:greedy-repetition 1 nil :non-whitespace-char-class)))) + :case-insensitive-mode t)) + +(defun find-logger-with-nick (nick) + (find nick (the list *loggers*) :test #'string-equal :key #'nickname))
-(defun make-output-name (utime) +(defun make-output-name (base-name utime) (multiple-value-bind (second minute hour day-of-month month year day-of-week daylight-p zone) (decode-universal-time utime) (declare (ignore second minute hour day-of-week daylight-p zone)) - (format nil "~A~4,'0D-~2,'0D-~2,'0D" *base-name* year month day-of-month))) + (format nil "~A~4,'0D-~2,'0D-~2,'0D" base-name year month day-of-month))) + +(defun output-file-header (logger channel istream) + (case (elt (formats logger) istream) + (:html + (format (elt (streams channel) istream) + "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">~%") + (format (elt (streams channel) istream) "<html><head><link rel="stylesheet" href="irclogs.css" type="text/css" /></head><body>~%")))) + +(defun output-file-footer (logger channel istream) + (case (elt (formats logger) istream) + (:html + (format (elt (streams channel) istream) "</body></html>~%")))) + +(defun ensure-output-stream-for-user-directory (utime logger channel istream) + (let ((name (make-output-name (base-name channel) utime))) + (unless (string= name (elt (current-output-names channel) istream)) + (when (elt (streams channel) istream) + (output-file-footer logger channel istream) + (close (elt (streams channel) istream))) + (setf (elt (current-output-names channel) istream) name) + (let ((path (make-pathname :defaults (user-output logger) :name name + :type (case (elt (formats logger) istream) + (:html "html") + (:sexp "sexp") + (t "txt"))))) + (unless (probe-file path) + (setf (elt (streams channel) istream) + (open path :direction :output :if-exists :error + :if-does-not-exist :create)) + (output-file-header logger channel istream) + (close (elt (streams channel) istream))) + (setf (elt (streams channel) istream) + (open path :direction :output :if-exists :append + :if-does-not-exist :error))))))
-(defun ensure-output-stream (utime) +(defun ensure-output-stream (utime logger channel istream) "Ensures that *output-stream* is correct." (cond - ((streamp *output*) - (unless *output-stream* - (setq *output-stream* *output*))) - ((pathnamep *output*) - (let ((name (make-output-name utime))) - (unless (string= name *current-output-name*) - (when *output-stream* - (close *output-stream*)) - (setq *current-output-name* name) - (setq *output-stream* - (open (make-pathname :defaults *output* :name name - :type "txt") - :direction :output - :if-exists :append - :if-does-not-exist :create))))))) - + ((streamp (user-output logger)) + (unless (elt (streams channel) istream) + (setf (elt (streams channel) istream) (user-output logger)))) + ((pathnamep (user-output logger)) + (cond + ((pathname-name (user-output logger)) + ;; a file is named for output + (setf (elt (streams channel) istream) + (open (user-output logger) :direction :output :if-exists :append))) + (t + (ensure-output-stream-for-user-directory utime logger channel istream)))))) + (defun format-time (utime) (multiple-value-bind (second minute hour day-of-month month year day-of-week daylight-p zone) @@ -56,94 +134,198 @@ (declare (ignore second day-of-month month year day-of-week daylight-p zone)) (format nil "~2,'0D:~2,'0D" hour minute)))
-(defun output-event (msg text) - (ensure-output-stream (received-time msg)) - (assert (streamp *output-stream*)) - (format *output-stream* "~A ~A~%" - (format-time (received-time msg)) - text) - (force-output *output-stream*)) - -(defmethod irc::irc-message-event ((msg irc::irc-privmsg-message)) - (output-event msg - (format nil "<~A> ~A" - (source msg) - (trailing-argument msg)))) - - -(defmethod irc::irc-message-event ((msg irc::irc-nick-message)) - (output-event msg - (format nil "[info] ~A is now known as ~A" - (source msg) - (trailing-argument msg)))) - -(defmethod irc::irc-message-event ((msg irc::irc-part-message)) - (output-event msg - (format nil "[info] ~A has left ~A" - (source msg) - (first (arguments msg))))) - -(defmethod irc::irc-message-event ((msg irc::irc-quit-message)) - (output-event msg - (format nil "[info] ~A has quit ~A" - (source msg) - (trailing-argument msg)))) - -(defmethod irc::irc-message-event ((msg irc::irc-join-message)) - (output-event msg - (format nil "[info] ~A has joined ~A" - (source msg) - (trailing-argument msg)))) - -(defmethod irc::irc-message-event ((msg irc::irc-kick-message)) - (output-event msg - (format nil "[info] ~A has been kicked from ~A" - (source msg) - (first (arguments msg))))) - -(defmethod irc::irc-message-event ((msg irc::ctcp-action-message)) - (output-event msg - (format nil "*~A* ~A" - (source msg) - (subseq (trailing-argument msg) - 8 - (- (length (trailing-argument msg)) 1))))) - - -(defun start-logger-bot (nick server &key channels output - (base-name "log-") - (logging-stream t) - (async t)) + +(defun activate-uris (str) + "Find any URI's in a string and make them HTML clickable." + (let ((split (split *uri-scanner* str :with-registers-p t))) + (if (= 1 (length split)) + str + (with-output-to-string (stream) + (dolist (item split) + (if (and (> (length item) 6) + (or + (string-equal "http://" (subseq item 0 7)) + (string-equal "ftp://" (subseq item 0 6)) + (string-equal "mailto:" (subseq item 0 7))) + ;; (ignore-errors (puri:parse-uri item)) + t) + (format stream "<a href='~A'>~A</a>" item item) + (write-string item stream))))))) + +(defun output-event-for-a-stream (msg type text object logger channel istream) + (ensure-output-stream (received-time msg) logger channel istream) + (let ((source (source msg)) + (stream (elt (streams channel) istream))) + (assert (streamp stream)) + (case (elt (formats logger) istream) + (:html + (format stream + "<div class='~A'><span class='time'>" + (case type + (:privmsg "privmsg") + (:action "action") + (t "info"))) + (write-string (format-time (received-time msg)) stream) + (format stream "</span> ") + (case type + (:privmsg + (format stream "<span class='brack'><</span><span class='subject'>~A</span><span class='brack'>></span> <span class='msg'>~A</span>" + source (activate-uris text))) + (:action + (format stream + "<span class='action-brack'>*</span><span class='action-name'>~A</span><span class='action-brack'>*</span> <span class='action-msg'>~A</span>" + source (activate-uris text))) + (t + (format stream "<span class='subject'>~A</span> <span class='info-msg'>~A</span>" + source text) + (when object + (format stream " <span class='object'>~A</span>" object)))) + (format stream "</div>~%")) + (:sexp + (format stream "(~W ~W ~W ~W ~W)~%" (received-time msg) + type source text object)) + (t + (format stream "~A " (format-time (received-time msg))) + (case type + (:privmsg + (format stream "<~A> ~A" source text)) + (:action + (format stream "*~A* ~A" source text)) + (t + (format stream "[info] ~A ~A" source text) + (when object + (format stream " ~A" object)))) + (write-char #\Newline stream))) + (force-output stream)))/ + +(defun output-event (msg type text &optional object) + (dolist (logger *loggers*) + (let* ((channel-name (car (last (arguments msg)))) + (channel (find channel-name (the list (channels logger)) + :test #'string-equal :key #'name))) + (print channel-name) + (print channel) + (when channel + (dotimes (i (length (formats logger))) + (output-event-for-a-stream msg type text object logger channel i)))))) + +(defun privmsg-hook (msg) + (output-event msg :privmsg (trailing-argument msg))) + +(defun action-hook (msg) + (output-event msg :action + (subseq (trailing-argument msg) + 8 + (- (length (trailing-argument msg)) 1)))) + +(defun nick-hook (msg) + (output-event msg :nick "is now known as" + (trailing-argument msg))) + +(defun part-hook (msg) + (output-event msg :part "has left" + (first (arguments msg)))) + +(defun quit-hook (msg) + (output-event msg :quit "has quit" + (concatenate 'string "[" (trailing-argument msg) "]"))) + +(defun join-hook (msg) + (output-event msg :join "has joined" + (trailing-argument msg))) + +(defun kick-hook (msg) + (output-event msg :kick "has been kicked from" + (first (arguments msg)))) + +(defun create-logger (nick server &key channels output + (base-name "log-") + (logging-stream t) + (async t) + (formats '(:text))) "OUTPUT may be a pathname or a stream" ;; check arguments (assert channels) + (assert formats) (assert (stringp base-name)) - (if (stringp channels) + (if (atom channels) (setq channels (list channels))) + (if (atom formats) + (setq formats (list formats))) (if (stringp output) (setq output (parse-namestring output))) - (setq *bot-nickname* nick) - (setq *base-name* base-name) - (setq *output* output) - (when *connection* - (warn "Closing open logger connection.") - (quit *connection*) - (sleep 2)) ;; give the server a chance to close out connection - - (setq *connection* - (connect :nickname *bot-nickname* :server server - :logging-stream logging-stream)) - (mapc #'(lambda (channel) (join *connection* channel)) channels) - - (reset-hooks) - (cond - (async - #+sbcl (add-asynchronous-message-handler *connection*) - #-sbcl (read-message-loop *connection*)) - (t - (read-message-loop *connection*)))) - - -(defun reset-hooks () - (irc::remove-all-hooks *connection*)) + (let* ((conn (connect :nickname nick :server server + :logging-stream logging-stream)) + (logger (make-instance + 'logger + :connection conn + :nick nick + :server server + :channels + (loop for i from 0 to (1- (length channels)) + collect (make-instance 'channel + :name (nth i channels) + :streams (make-list (length formats)) + :base-name + (concatenate 'string + base-name + (string-left-trim + '(##) + (nth i channels)) + "-") + :current-output-names + (make-list (length formats)))) + :user-output output + :base-name base-name + :formats formats))) + (mapc #'(lambda (channel) (join conn channel)) channels) + + (add-hook conn 'irc::irc-privmsg-message 'privmsg-hook) + (add-hook conn 'irc::ctcp-action-message 'action-hook) + (add-hook conn 'irc::irc-nick-message 'nick-hook) + (add-hook conn 'irc::irc-part-message 'part-hook) + (add-hook conn 'irc::irc-quit-message 'quit-hook) + (add-hook conn 'irc::irc-join-message 'join-hook) + (add-hook conn 'irc::irc-kick-message 'kick-hook) + (cond + (async + #+sbcl (add-asynchronous-message-handler conn) + #-sbcl (read-message-loop conn)) + (t + (read-message-loop conn))) + logger)) + +(defun quit-logger (nick) + "Quit the active connection with nick and remove from active list." + (let ((logger (find-logger-with-nick nick))) + (cond + ((null logger) + (warn "No active connection found with nick ~A." nick) + nil) + (t + (irc:quit (connection logger)) + (sleep 1) + (dolist (channel (channels logger)) + (dotimes (i (length (streams channel))) + (when (streamp (elt (streams channel) i)) + (close (elt (streams channel) i)) + (setf (elt (streams channel) i) nil)))) + (setq *loggers* (delete nick *loggers* :test #'string-equal :key #'nickname)) + t)))) + +(defun add-logger (nick server &key channels output + (base-name "log-") + (logging-stream t) + (async t) + (formats '(:text))) + (when (find-logger-with-nick nick) + (warn "Closing previously active connection.") + (quit-logger nick)) + (let ((logger + (create-logger nick server :channels channels :output output + :base-name base-name :logging-stream logging-stream + :async async :formats formats))) + (push logger *loggers*) + logger)) +
net-nittin-irc-cvs@common-lisp.net