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 | + |
---|