1 | ? dist.sh |
---|
2 | ? msg |
---|
3 | ? tracking.patch |
---|
4 | ? tracking.patch2 |
---|
5 | ? working-track.lisp |
---|
6 | ? doc/tracking-considerations |
---|
7 | Index: TODO |
---|
8 | =================================================================== |
---|
9 | RCS file: /project/cl-irc/cvsroot/cl-irc/TODO,v |
---|
10 | retrieving revision 1.2 |
---|
11 | diff -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 |
---|
23 | Index: cl-irc.asd |
---|
24 | =================================================================== |
---|
25 | RCS file: /project/cl-irc/cvsroot/cl-irc/cl-irc.asd,v |
---|
26 | retrieving revision 1.2 |
---|
27 | diff -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")))) |
---|
37 | Index: package.lisp |
---|
38 | =================================================================== |
---|
39 | RCS file: /project/cl-irc/cvsroot/cl-irc/package.lisp,v |
---|
40 | retrieving revision 1.8 |
---|
41 | diff -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 | |
---|
57 | Index: protocol.lisp |
---|
58 | =================================================================== |
---|
59 | RCS file: /project/cl-irc/cvsroot/cl-irc/protocol.lisp,v |
---|
60 | retrieving revision 1.25 |
---|
61 | diff -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." |
---|
75 | Index: track.lisp |
---|
76 | =================================================================== |
---|
77 | RCS file: track.lisp |
---|
78 | diff -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))))) |
---|