Ticket #13: tracking.patch2

File tracking.patch2, 11.0 KB (added by Erik Huelsmann, 18 years ago)

Second iteration patch to make message tracking work in cl-irc

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