Ticket #13: tracking.patch3

File tracking.patch3, 11.3 KB (added by Erik Huelsmann, 18 years ago)

Third iteration patch

Line 
1Index: cl-irc.asd
2===================================================================
3RCS file: /project/cl-irc/cvsroot/cl-irc/cl-irc.asd,v
4retrieving revision 1.2
5diff -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"))))
15Index: package.lisp
16===================================================================
17RCS file: /project/cl-irc/cvsroot/cl-irc/package.lisp,v
18retrieving revision 1.8
19diff -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 
35Index: protocol.lisp
36===================================================================
37RCS file: /project/cl-irc/cvsroot/cl-irc/protocol.lisp,v
38retrieving revision 1.25
39diff -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."
52Index: track.lisp
53===================================================================
54RCS file: track.lisp
55diff -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+