| 1 | Index: cl-irc.asd |
|---|
| 2 | =================================================================== |
|---|
| 3 | RCS file: /project/cl-irc/cvsroot/cl-irc/cl-irc.asd,v |
|---|
| 4 | retrieving revision 1.2 |
|---|
| 5 | diff -u -r1.2 cl-irc.asd |
|---|
| 6 | --- cl-irc.asd 29 Mar 2004 19:07:54 -0000 1.2 |
|---|
| 7 | +++ cl-irc.asd 8 Jan 2006 22:25:28 -0000 |
|---|
| 8 | @@ -39,4 +39,6 @@ |
|---|
| 9 | (:file "command" |
|---|
| 10 | :depends-on ("protocol")) |
|---|
| 11 | (:file "event" |
|---|
| 12 | + :depends-on ("command")) |
|---|
| 13 | + (:file "track" |
|---|
| 14 | :depends-on ("command")))) |
|---|
| 15 | Index: package.lisp |
|---|
| 16 | =================================================================== |
|---|
| 17 | RCS file: /project/cl-irc/cvsroot/cl-irc/package.lisp,v |
|---|
| 18 | retrieving revision 1.8 |
|---|
| 19 | diff -u -r1.8 package.lisp |
|---|
| 20 | --- package.lisp 15 Apr 2005 16:01:22 -0000 1.8 |
|---|
| 21 | +++ package.lisp 8 Jan 2006 22:25:28 -0000 |
|---|
| 22 | @@ -129,5 +129,11 @@ |
|---|
| 23 | :users- |
|---|
| 24 | :wallops |
|---|
| 25 | :userhost |
|---|
| 26 | - :ison))) |
|---|
| 27 | + :ison |
|---|
| 28 | + ;;; user activity tracking |
|---|
| 29 | + :record-events |
|---|
| 30 | + :recorded-event |
|---|
| 31 | + :seen |
|---|
| 32 | + :spoke |
|---|
| 33 | + ))) |
|---|
| 34 | |
|---|
| 35 | Index: protocol.lisp |
|---|
| 36 | =================================================================== |
|---|
| 37 | RCS file: /project/cl-irc/cvsroot/cl-irc/protocol.lisp,v |
|---|
| 38 | retrieving revision 1.25 |
|---|
| 39 | diff -u -r1.25 protocol.lisp |
|---|
| 40 | --- protocol.lisp 25 Sep 2005 14:55:02 -0000 1.25 |
|---|
| 41 | +++ protocol.lisp 8 Jan 2006 22:25:28 -0000 |
|---|
| 42 | @@ -159,7 +159,8 @@ |
|---|
| 43 | (users |
|---|
| 44 | :initarg :users |
|---|
| 45 | :accessor users |
|---|
| 46 | - :initform (make-hash-table :test #'equal)))) |
|---|
| 47 | + :initform (make-hash-table :test #'equal)) |
|---|
| 48 | + (track-db))) |
|---|
| 49 | |
|---|
| 50 | (defmethod print-object ((object connection) stream) |
|---|
| 51 | "Print the object for the Lisp reader." |
|---|
| 52 | Index: track.lisp |
|---|
| 53 | =================================================================== |
|---|
| 54 | RCS file: track.lisp |
|---|
| 55 | diff -N track.lisp |
|---|
| 56 | --- /dev/null 1 Jan 1970 00:00:00 -0000 |
|---|
| 57 | +++ track.lisp 8 Jan 2006 22:25:28 -0000 |
|---|
| 58 | @@ -0,0 +1,288 @@ |
|---|
| 59 | +;;; |
|---|
| 60 | +;;; track.lisp |
|---|
| 61 | +;;; |
|---|
| 62 | +;;; Heavily based on seen.lisp |
|---|
| 63 | +;;; as created on 2004-01-08 by Zach Beane <xach@gwi.net> |
|---|
| 64 | +;;; |
|---|
| 65 | +;;; Adapted to be part of cl-irc by Erik Huelsmann <e.huelsmann@gmx.net> |
|---|
| 66 | +;;; |
|---|
| 67 | +;;; |
|---|
| 68 | + |
|---|
| 69 | +(in-package :irc) |
|---|
| 70 | + |
|---|
| 71 | + |
|---|
| 72 | +(defclass msg-db () |
|---|
| 73 | + ((table :reader msg-db-table |
|---|
| 74 | + :initform (make-hash-table :test 'equalp) |
|---|
| 75 | + :documentation "A hash table mapping IRC nicknames to |
|---|
| 76 | + their seen data.") |
|---|
| 77 | + (file :reader msg-db-file |
|---|
| 78 | + :initarg :file |
|---|
| 79 | + :documentation "The file to which seen data will be |
|---|
| 80 | + saved and updated.") |
|---|
| 81 | + (compact-threshold :accessor msg-db-compact-threshold |
|---|
| 82 | + :initarg :compact-threshold |
|---|
| 83 | + :initform 10000 |
|---|
| 84 | + :documentation "How many updates to write |
|---|
| 85 | + to the data file before compacting it.") |
|---|
| 86 | + (update-count :accessor msg-db-update-count |
|---|
| 87 | + :initform 0))) |
|---|
| 88 | + |
|---|
| 89 | +(defun %file-update (stream key &rest args) |
|---|
| 90 | + (let ((*print-pretty* nil) |
|---|
| 91 | + (*print-readably* t)) |
|---|
| 92 | + (print (cons key args) stream))) |
|---|
| 93 | + |
|---|
| 94 | +(defun %table-update (table key &rest args) |
|---|
| 95 | + (setf (gethash key table) args)) |
|---|
| 96 | + |
|---|
| 97 | +(defmethod update-db ((db msg-db) |
|---|
| 98 | + nick msg-type time &rest rest) |
|---|
| 99 | + (let ((key (list nick msg-type))) |
|---|
| 100 | + (with-open-file (out (msg-db-file db) |
|---|
| 101 | + :direction :output |
|---|
| 102 | + :if-exists :append |
|---|
| 103 | + :if-does-not-exist :create) |
|---|
| 104 | + (apply #'%file-update (append (list out key time) rest))) |
|---|
| 105 | + (incf (msg-db-update-count db)) |
|---|
| 106 | + (maybe-compact-db db) |
|---|
| 107 | + (apply #'%table-update (append (list (msg-db-table db) key time) rest)))) |
|---|
| 108 | + |
|---|
| 109 | +(defmethod save-db ((db msg-db)) |
|---|
| 110 | + (with-open-file (out (msg-db-file db) |
|---|
| 111 | + :direction :output |
|---|
| 112 | + :if-exists :supersede |
|---|
| 113 | + :if-does-not-exist :create) |
|---|
| 114 | + (maphash #'(lambda (key val) |
|---|
| 115 | + (apply #'%file-update out key val)) |
|---|
| 116 | + (msg-db-table db)))) |
|---|
| 117 | + |
|---|
| 118 | +(defmethod compact-db ((db msg-db)) |
|---|
| 119 | + (save-db db) |
|---|
| 120 | + (setf (msg-db-update-count db) 0)) |
|---|
| 121 | + |
|---|
| 122 | +(defmethod maybe-compact-db ((db msg-db)) |
|---|
| 123 | + (when (> (msg-db-update-count db) |
|---|
| 124 | + (msg-db-compact-threshold db)) |
|---|
| 125 | + (compact-db db))) |
|---|
| 126 | + |
|---|
| 127 | +(defmethod load-db ((db msg-db)) |
|---|
| 128 | + (with-open-file (in (msg-db-file db) |
|---|
| 129 | + :direction :input |
|---|
| 130 | + :if-does-not-exist :create) |
|---|
| 131 | + (loop for item = (read in nil nil) |
|---|
| 132 | + while item |
|---|
| 133 | + do |
|---|
| 134 | + (destructuring-bind |
|---|
| 135 | + (nick &rest rest) |
|---|
| 136 | + item |
|---|
| 137 | + (apply #'%table-update (append (list (msg-db-table db) nick) |
|---|
| 138 | + rest)))))) |
|---|
| 139 | + |
|---|
| 140 | + |
|---|
| 141 | +(defun make-msg-db (&key file (compact-threshold 10000)) |
|---|
| 142 | + (let ((db (make-instance 'msg-db |
|---|
| 143 | + :file file |
|---|
| 144 | + :compact-threshold compact-threshold))) |
|---|
| 145 | + (load-db db) |
|---|
| 146 | + (compact-db db) |
|---|
| 147 | + db)) |
|---|
| 148 | + |
|---|
| 149 | + |
|---|
| 150 | +;;; Producing pretty messages for various events |
|---|
| 151 | + |
|---|
| 152 | +(defmacro with-slots-and-args (slots args message &body body) |
|---|
| 153 | + "Evaluate BODY with the slots of the MESSAGE bound to the |
|---|
| 154 | + symbols of SLOTS, and the arguments of the message |
|---|
| 155 | + destructuring-bind'ed to ARGS." |
|---|
| 156 | + (let ((irc-message (gensym))) |
|---|
| 157 | + `(let ((,irc-message ,message)) |
|---|
| 158 | + (with-slots ,slots ,irc-message |
|---|
| 159 | + (destructuring-bind ,args (arguments ,irc-message) |
|---|
| 160 | + ,@body))))) |
|---|
| 161 | + |
|---|
| 162 | + |
|---|
| 163 | +;; track-data returns (values): |
|---|
| 164 | + |
|---|
| 165 | +;; * a keyword designating the registered event, one of: |
|---|
| 166 | +;; :joined |
|---|
| 167 | +;; :left |
|---|
| 168 | +;; :quit |
|---|
| 169 | +;; :kicked |
|---|
| 170 | +;; :nick-changed |
|---|
| 171 | +;; :unknown |
|---|
| 172 | +;; * a list of arguments for the given event |
|---|
| 173 | + |
|---|
| 174 | + |
|---|
| 175 | +;; Default events to be recorded |
|---|
| 176 | + |
|---|
| 177 | +(defmethod track-data ((message irc-message)) |
|---|
| 178 | + (values :unknown (list command message))) |
|---|
| 179 | + |
|---|
| 180 | +(defmethod track-data ((message irc-kick-message)) |
|---|
| 181 | + (with-slots-and-args (source trailing-argument) (channel target) |
|---|
| 182 | + message |
|---|
| 183 | + (declare (ignore target)) |
|---|
| 184 | + (values :kicked (list channel source trailing-argument)))) |
|---|
| 185 | + |
|---|
| 186 | +(defmethod track-data ((message irc-join-message)) |
|---|
| 187 | + (values :joined (list (trailing-argument message)))) |
|---|
| 188 | + |
|---|
| 189 | +(defmethod track-data ((message irc-quit-message)) |
|---|
| 190 | + (values :quit (list (trailing-argument message)))) |
|---|
| 191 | + |
|---|
| 192 | +(defmethod track-data ((message irc-part-message)) |
|---|
| 193 | + (with-slots-and-args (trailing-argument) (channel) |
|---|
| 194 | + message |
|---|
| 195 | + (values :left (list channel trailing-argument)))) |
|---|
| 196 | + |
|---|
| 197 | +(defmethod track-data ((message irc-nick-message)) |
|---|
| 198 | + (with-slots-and-args (source) (new-nick) |
|---|
| 199 | + message |
|---|
| 200 | + (values :nick-changed (list source new-nick)))) |
|---|
| 201 | + |
|---|
| 202 | +(defmethod track-data ((message irc-privmsg-message)) |
|---|
| 203 | + (with-slots-and-args (received-time trailing-argument arguments) |
|---|
| 204 | + (channel) |
|---|
| 205 | + message |
|---|
| 206 | + (values :privmsg (list trailing-argument channel)))) |
|---|
| 207 | + |
|---|
| 208 | +(defmethod track-data ((message ctcp-action-message)) |
|---|
| 209 | + (with-slots-and-args (received-time trailing-argument arguments) |
|---|
| 210 | + (channel) |
|---|
| 211 | + message |
|---|
| 212 | + (values :action (list trailing-argument channel)))) |
|---|
| 213 | + |
|---|
| 214 | +;; Most messages have the the source as track-related nick, |
|---|
| 215 | +;; but there are exceptions. |
|---|
| 216 | + |
|---|
| 217 | +(defmethod track-nicks ((message irc-message)) |
|---|
| 218 | + (list (source message))) |
|---|
| 219 | + |
|---|
| 220 | +(defmethod track-nicks ((message irc-kick-message)) |
|---|
| 221 | + (destructuring-bind (channel target) |
|---|
| 222 | + (arguments message) |
|---|
| 223 | + (declare (ignore channel)) |
|---|
| 224 | + (list target (source message)))) |
|---|
| 225 | + |
|---|
| 226 | + |
|---|
| 227 | + |
|---|
| 228 | +;; Hook setup |
|---|
| 229 | + |
|---|
| 230 | +(defun make-track-hook (connection db data-callback nicks-callback) |
|---|
| 231 | + (lambda (message) |
|---|
| 232 | + (let ((time (received-time message)) |
|---|
| 233 | + (data (multiple-value-list (funcall data-callback message)))) |
|---|
| 234 | + (dolist (nick (mapcar #'(lambda (x) |
|---|
| 235 | + (normalize-nickname connection x)) |
|---|
| 236 | + (funcall nicks-callback message))) |
|---|
| 237 | + (apply #'update-db |
|---|
| 238 | + (append (list db nick (type-of message) time) data)))))) |
|---|
| 239 | + |
|---|
| 240 | + |
|---|
| 241 | +(defvar special-message-names |
|---|
| 242 | + '((:seen irc-kick-message |
|---|
| 243 | + irc-quit-message |
|---|
| 244 | + irc-part-message |
|---|
| 245 | + irc-join-message |
|---|
| 246 | + irc-nick-message) |
|---|
| 247 | + (:spoke irc-privmsg-message |
|---|
| 248 | + ctcp-action-message))) |
|---|
| 249 | + |
|---|
| 250 | +(defun map-special-messages (messages) |
|---|
| 251 | + (mapcan #'(lambda (x) (if (keywordp x) |
|---|
| 252 | + ;; copy-list is here because |
|---|
| 253 | + ;; mapcan modifies its lists |
|---|
| 254 | + (copy-list (cdr (assoc x special-message-names))) |
|---|
| 255 | + (list x))) |
|---|
| 256 | + messages)) |
|---|
| 257 | + |
|---|
| 258 | +(defun record-events (connection file |
|---|
| 259 | + &key (data #'track-data) |
|---|
| 260 | + (nicks #'track-nicks) |
|---|
| 261 | + (messages '(:seen :spoke))) |
|---|
| 262 | + "Add hooks to CONNECTION necessary to track event information. |
|---|
| 263 | +Uses FILE as the event database. |
|---|
| 264 | + |
|---|
| 265 | +Returns the database-object used to record events." |
|---|
| 266 | + (let* ((db (make-msg-db :file file)) |
|---|
| 267 | + (fun (make-track-hook connection db data nicks))) |
|---|
| 268 | + (setf (slot-value connection 'track-db) db) |
|---|
| 269 | + (dolist (class (map-special-messages messages)) |
|---|
| 270 | + (add-hook connection class fun)) |
|---|
| 271 | + db)) |
|---|
| 272 | + |
|---|
| 273 | +(defmethod recorded-event ((connection connection) nick |
|---|
| 274 | + &optional (messages '(:seen :spoke))) |
|---|
| 275 | + (values-list |
|---|
| 276 | + (reduce #'(lambda (&optional x y) |
|---|
| 277 | + (cond |
|---|
| 278 | + ((and x y) |
|---|
| 279 | + ;; select latest (newest) event |
|---|
| 280 | + (if (> (first x) (first y)) x y)) |
|---|
| 281 | + (x x) |
|---|
| 282 | + (y y))) |
|---|
| 283 | + (mapcar #'(lambda (x) |
|---|
| 284 | + (gethash (list nick x) |
|---|
| 285 | + (msg-db-table |
|---|
| 286 | + (slot-value connection 'track-db)))) |
|---|
| 287 | + (map-special-messages messages))))) |
|---|
| 288 | + |
|---|
| 289 | +(defmethod seen ((connection connection) nick) |
|---|
| 290 | + "Returns (values time event-type msg-data ...)" |
|---|
| 291 | + (recorded-event connection nick '(:seen))) |
|---|
| 292 | + |
|---|
| 293 | +(defmethod spoke ((connection connection) nick) |
|---|
| 294 | + "Returns (values time event-type msg-data ...)" |
|---|
| 295 | + (recorded-event connection nick '(:spoke))) |
|---|
| 296 | + |
|---|
| 297 | + |
|---|
| 298 | +;; Formatting of default recorded events |
|---|
| 299 | + |
|---|
| 300 | +(defmethod fmt-track (stream (action (eql :kicked)) arguments) |
|---|
| 301 | + (declare (ignore action)) |
|---|
| 302 | + (destructuring-bind |
|---|
| 303 | + (channel oper text) |
|---|
| 304 | + arguments |
|---|
| 305 | + (format stream "being kicked out of ~A by ~A~@[ (~A)~]" |
|---|
| 306 | + channel oper text))) |
|---|
| 307 | + |
|---|
| 308 | +(defmethod fmt-track (stream (action (eql :joined)) arguments) |
|---|
| 309 | + (declare (ignore action)) |
|---|
| 310 | + (destructuring-bind |
|---|
| 311 | + (channel) |
|---|
| 312 | + arguments |
|---|
| 313 | + (format stream "joining ~A" channel))) |
|---|
| 314 | + |
|---|
| 315 | +(defmethod fmt-track (stream (action (eql :left)) arguments) |
|---|
| 316 | + (declare (ignore action)) |
|---|
| 317 | + (destructuring-bind |
|---|
| 318 | + (channel text) |
|---|
| 319 | + arguments |
|---|
| 320 | + (format stream "leaving ~A~@[ (~A)~]" channel text))) |
|---|
| 321 | + |
|---|
| 322 | +(defmethod fmt-track (stream (action (eql :quit)) arguments) |
|---|
| 323 | + (declare (ignore action)) |
|---|
| 324 | + (destructuring-bind |
|---|
| 325 | + (text) |
|---|
| 326 | + arguments |
|---|
| 327 | + (format stream "leaving irc~@[ (~A)~]" text))) |
|---|
| 328 | + |
|---|
| 329 | +(defmethod fmt-track (stream (action (eql :nick-changed)) arguments) |
|---|
| 330 | + (declare (ignore action)) |
|---|
| 331 | + (destructuring-bind |
|---|
| 332 | + (from-nick to-nick) |
|---|
| 333 | + arguments |
|---|
| 334 | + (format stream "changing his nick from ~A to ~A" from-nick to-nick))) |
|---|
| 335 | + |
|---|
| 336 | +(defmethod fmt-track (stream (action (eql :privmsg)) arguments) |
|---|
| 337 | + (declare (ignore action)) |
|---|
| 338 | + (destructuring-bind |
|---|
| 339 | + (text channel) |
|---|
| 340 | + arguments |
|---|
| 341 | + (format stream "'~A' in ~A" text channel))) |
|---|
| 342 | + |
|---|
| 343 | + |
|---|
| 344 | +(defun format-track-data (stream action arguments) |
|---|
| 345 | + (fmt-track stream action arguments)) |
|---|
| 346 | + |
|---|