*** empty log message ***
[gnus] / lisp / gnus-gl.el
1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2 ;; GroupLens software and documentation is copyright (c) 1995 by Paul
3 ;; Resnick (Massachusetts Institute of Technology); Brad Miller, John
4 ;; Riedl, Jon Herlocker, and Joseph Konstan (University of Minnesota),
5 ;; and David Maltz (Carnegie-Mellon University).
6 ;;
7 ;; Permission to use, copy, modify, and distribute this documentation
8 ;; for non-commercial and commercial purposes without fee is hereby
9 ;; granted provided that this copyright notice and permission notice
10 ;; appears in all copies and that the names of the individuals and
11 ;; institutions holding this copyright are not used in advertising or
12 ;; publicity pertaining to this software without specific, written
13 ;; prior permission.  The copyright holders make no representations
14 ;; about the suitability of this software and documentation for any
15 ;; purpose.  It is provided ``as is'' without express or implied
16 ;; warranty.
17 ;;
18 ;; The copyright holders request that they be notified of
19 ;; modifications of this code.  Please send electronic mail to
20 ;; grouplens@cs.umn.edu for more information or to announce derived
21 ;; works.  
22 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
23 ;; Author: Brad Miller
24 ;;
25 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
26 ;;
27 ;; User Documentation:
28 ;; To use GroupLens you must load this file.
29 ;; You must also register a pseudonym with the Better Bit Bureau.
30 ;; http://www.cs.umn.edu/Research/GroupLens
31 ;;
32 ;;    ---------------- For your .emacs or .gnus file ----------------
33 ;;
34 ;; As of version 2.5, grouplens now works as a minor mode of 
35 ;; gnus-summary-mode.  To get make that work you just need a couple of
36 ;; hooks.
37 ;; In addition, there are a few gnus-*-hooks that need to be set:
38 ;; (add-hook 'gnus-startup-hook 'bbb-login)
39 ;; (add-hook 'gnus-summary-mode-hook 'gnus-grouplens-mode)
40 ;;
41 ;; If you want to see grouplens scores using our format you might want to
42 ;; add a %uG to the gnus-summary-line-format.  For example, I use:
43 ;; (setq gnus-summary-line-format "%U%R%uG%I%(%[%4L: %-20,20uB%]%) %s\n")
44 ;; The above format also assumes that you are using gnus-bbdb  You can
45 ;; just as easily ad %uG to whatever format string you use.  Or add
46 ;; a %i to just see a simple numeric version of the predictions that
47 ;; uses less space on the summary line.  If you use %uG you have several
48 ;; choices for how things look.  See the doc string for the
49 ;; grouplens-prediction-display variable.
50 ;; (setq grouplens-prediction-display 'prediction-bar)
51 ;;
52 ;; If you use %uI on your group-line-format you will get (GroupLens Enhanced)
53 ;; after the names of newsgroups supported by GroupLens.
54 ;; (setq gnus-group-line-format "%M%S%p%5y: %(%g%) %uI\n")
55 ;;
56 ;; (setq gnus-summary-default-score 0)
57 ;;
58 ;; In addition there are some GroupLens user variables to set
59 ;; (setq grouplens-pseudonym "foobar")
60 ;; If you are using a bbb other than twain.cs.umn.edu you will need to
61 ;; set the grouplens-bbb-host variable, and possibly the
62 ;; grouplens-bbb-port variable. 
63 ;;
64 ;;(setq grouplens-newsgroups '("comp.lang.c++" "rec.humor" "rec.food.recipes"))
65 ;; This sets up the groups for which you will get predictions and ratings.
66 ;;
67 ;;                              USING GROUPLENS
68 ;; How do I Rate an article??
69 ;;   Before you type n to go to the next article, hit a number from 1-5
70 ;;   Type V r in the summary buffer and you will be prompted.
71 ;;
72 ;; What if, Gasp, I find a bug???
73 ;; Please type M-x gnus-gl-submit-bug-report.  This will set up a
74 ;; mail buffer with the  state of variables and buffers that will help
75 ;; me debug the problem.  A short description up front would help too!
76 ;; 
77 ;; How do I display the prediction for an aritcle:
78 ;;  If you set the gnus-summary-line-format as shown above, the score
79 ;;  (prediction) will be shown automatically.
80 ;;
81 ;; 
82 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
83 ;; Programmer  Notes 
84 ;; 10/9/95
85 ;; gnus-scores-articles contains the articles
86 ;; When scoring is done, the call tree looks something like:
87 ;; gnus-possibly-score-headers
88 ;;  ==> gnus-score-headers
89 ;;      ==> gnus-score-load-file
90 ;;          ==> get-all-mids  (from the eval form)
91 ;;
92 ;; it would be nice to have one that gets called after all the other
93 ;; headers have been scored.
94 ;; we may want a variable gnus-grouplens-scale-factor
95 ;; and gnus-grouplens-offset  this would probably be either -3 or 0
96 ;; to make the scores centered around zero or not.
97 ;; Notes 10/12/95
98 ;; According to Lars, Norse god of gnus, the simple way to insert a
99 ;; call to an external function is to have a function added to the
100 ;; variable gnus-score-find-files-function  This new function
101 ;; gnus-grouplens-score-alist will return a core alist that
102 ;; has (("message-id" ("<message-id-xxxx>" score) ("<message-id-xxxy>" score))
103 ;; This seems like it would be pretty inefficient, though workable.
104 ;;
105 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
106 ;;  TODO
107 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
108 ;;
109 ;; 3. Add some more ways to rate messages
110 ;; 4. Better error handling for token timeouts.
111 ;;
112 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
113 ;; bugs
114 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
115 ;; 
116
117 (require 'gnus-score)
118 (eval-and-compile (require 'cl))
119
120 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
121 ;;;; User variables
122 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
123
124 (defvar gnus-summary-grouplens-line-format
125   "%U%R%z%l%I%(%[%4L: %-20,20n%]%) %s\n"
126   "*The line format spec in summary GroupLens mode buffers.")
127
128 (defvar grouplens-pseudonym ""
129   "User's pseudonym.  This pseudonym is obtained during the registration 
130 process")
131
132 (defvar grouplens-bbb-host "grouplens.cs.umn.edu"
133   "Host where the bbbd is running" )
134
135 (defvar grouplens-bbb-port 9000 
136   "Port where the bbbd is listening" )
137
138 (defvar grouplens-newsgroups 
139   '("comp.lang.c++" "rec.humor" "rec.food.recipes" "comp.groupware"
140     "mn.general" "rec.arts.movies" "rec.arts.movies.current-films"
141     "comp.lang.java" "comp.os.linux.announce" "comp.os.linux.misc"
142     "comp.os.linux.development.apps" "comp.os.linux.development.system")
143   "*Groups that are part of the GroupLens experiment.")
144
145 (defvar grouplens-prediction-display 'prediction-spot
146   "valid values are: 
147       prediction-spot -- an * corresponding to the prediction between 1 and 5, 
148       confidence-interval -- a numeric confidence interval
149       prediction-bar --  |#####     | the longer the bar, the better the article,
150       confidence-bar --  |  -----   } the prediction is in the middle of the bar,
151       confidence-spot -- )  *       | the spot gets bigger with more confidence,
152       prediction-num  --   plain-old numeric value,
153       confidence-plus-minus  -- prediction +/i confidence")
154
155 (defvar grouplens-score-offset 0
156   "Offset the prediction by this value.  
157 Setting this variable to -2 would have the following effect on grouplens 
158 scores:
159    1   -->   -2
160    2   -->   -1
161    3   -->    0
162    4   -->    1
163    5   -->    2
164    
165 the reason a user might want to do this is to combine grouplens 
166 predictions with scores calculated by other score methods")
167
168 (defvar grouplens-score-scale-factor 1
169   "This variable allow sthe user to magify the effect of grouplens scores. 
170 The scale factor is applied after the offset.")
171
172 (defvar gnus-grouplens-override-scoring t
173   "Tell Grouplens to override the normal Gnus scoring mechanism.  If
174   this variable is non-nill than Grouplens will completely override
175   the normal scoring mechanism of Gnus.  When nil, Grouplens will not
176   override the normal scoring mechanism so both can be used at once.")
177  
178
179 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
180 ;;;; Program global variables
181 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
182 (defvar grouplens-bbb-token "0"
183   "Current session token number")
184
185 (defvar grouplens-bbb-process nil
186   "Process Id of current bbbd network stream process")
187
188 (defvar grouplens-bbb-buffer nil
189   "Buffer associated with the BBBD process")
190
191 (defvar grouplens-rating-alist nil
192   "Current set of  message-id rating pairs")
193
194 (defvar grouplens-current-hashtable (make-hash-table :test 'equal :size 100))
195 ;; this seems like a pretty ugly way to get around the problem, but If 
196 ;; I don't do this, then the compiler complains when I call gethash
197 ;;
198 (eval-when-compile (setq grouplens-current-hashtable 
199                          (make-hash-table :test 'equal :size 100)))
200
201 (defvar grouplens-current-group nil)
202
203 (defvar bbb-mid-list nil)
204
205 (defvar bbb-alist nil)
206
207 (defvar bbb-timeout-secs 10
208   "Number of seconds to wait for some response from the BBB before
209     we give up and assume that something has died..." )
210
211 (defvar grouplens-previous-article nil
212   "message-id of the last article read")
213
214 (defvar bbb-read-point)
215 (defvar bbb-response-point)
216
217 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
218 ;;;;  Utility Functions
219 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
220 (defun bbb-connect-to-bbbd (host port)
221   (unless grouplens-bbb-buffer 
222     (setq grouplens-bbb-buffer 
223           (get-buffer-create (format " *BBBD trace: %s*" host)))
224     (save-excursion
225       (set-buffer grouplens-bbb-buffer)
226       (make-local-variable 'bbb-read-point)
227       (setq bbb-read-point (point-min))))
228   ;; clear the trace buffer of old output
229   (save-excursion
230     (set-buffer grouplens-bbb-buffer)
231     (erase-buffer))
232   ;; open the connection to the server
233   (setq grouplens-bbb-process nil)
234   (catch 'done
235     (condition-case error
236         (setq grouplens-bbb-process 
237               (open-network-stream "BBBD" grouplens-bbb-buffer host port))
238       (error (gnus-message 3 "Error: Failed to connect to BBB")
239              nil))
240     (and (null grouplens-bbb-process) 
241          (throw 'done nil))
242     ;; (set-process-filter grouplens-bbb-process 'bbb-process-filter)
243     (save-excursion
244       (set-buffer grouplens-bbb-buffer)
245       (setq bbb-read-point (point-min))
246       (or (bbb-read-response grouplens-bbb-process)
247           (throw 'done nil))))
248   grouplens-bbb-process)
249
250 ;; (defun bbb-process-filter (process output)
251 ;;   (save-excursion
252 ;;     (set-buffer (bbb-process-buffer process))
253 ;;     (goto-char (point-max))
254 ;;     (insert output)))
255
256 (defun bbb-send-command (process command)
257   (goto-char (point-max))
258   (insert command) 
259   (insert "\r\n")
260   (setq bbb-read-point (point))
261   (setq bbb-response-point (point))
262   (set-marker (process-mark process) (point)) ; process output also comes here
263   (process-send-string process command)
264   (process-send-string process "\r\n"))
265
266 (defun bbb-read-response (process) ; &optional return-response-string)
267   "This function eats the initial response of OK or ERROR from the BBB."
268   (let ((case-fold-search nil)
269          match-end)
270     (goto-char bbb-read-point)
271     (while (and (not (search-forward "\r\n" nil t))
272                 (accept-process-output process bbb-timeout-secs))
273       (goto-char bbb-read-point))
274     (setq match-end (point))
275     (goto-char bbb-read-point)
276     (setq bbb-read-point match-end)
277     (looking-at "OK")))
278
279 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
280 ;;;;       Login Functions
281 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
282 (defun bbb-login ()
283   "return the token number if login is successful, otherwise return nil"
284   (interactive)
285   (setq grouplens-bbb-token nil)
286   (if (not (equal grouplens-pseudonym ""))
287       (let ((bbb-process 
288              (bbb-connect-to-bbbd grouplens-bbb-host grouplens-bbb-port)))
289         (if bbb-process
290             (save-excursion 
291               (set-buffer (process-buffer bbb-process))
292               (bbb-send-command bbb-process 
293                                 (concat "login " grouplens-pseudonym))
294               (if (bbb-read-response bbb-process)
295                   (setq grouplens-bbb-token (extract-token-number))
296                 (gnus-message 3 "Error: Grouplens login failed")))))
297     (gnus-message 3 "Error: you must set a pseudonym"))
298   grouplens-bbb-token)
299
300 (defun extract-token-number ()
301   (let ((token-pos (search-forward "token=" nil t) ))
302     (if (looking-at "[0-9]+")
303         (buffer-substring token-pos (match-end 0)))))
304
305 (defun bbb-logout ()
306   "logout of bbb session"
307   (let ((bbb-process 
308          (bbb-connect-to-bbbd grouplens-bbb-host grouplens-bbb-port)))
309     (if bbb-process
310         (save-excursion 
311           (set-buffer (process-buffer bbb-process))
312           (bbb-send-command bbb-process (concat "logout " grouplens-bbb-token))
313           (bbb-read-response bbb-process))
314       nil)))
315
316 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
317 ;;;;       Get Predictions
318 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
319
320 (defun bbb-build-mid-scores-alist (groupname)
321   "this function can be called as part of the function to return the 
322 list of score files to use. See the gnus variable 
323 gnus-score-find-score-files-function.  
324
325 *Note:*  If you want to use grouplens scores along with calculated scores, 
326 you should see the offset and scale variables.  At this point, I don't 
327 recommend using both scores and grouplens predictions together."
328   (setq grouplens-current-group groupname)
329   (if (member groupname grouplens-newsgroups)
330       (let* ((mid-list (get-all-mids))
331              (predict-list (bbb-get-predictions mid-list groupname)))
332         (setq grouplens-previous-article nil)
333         ;; scores-alist should be a list of lists:
334         ;;  ((("message-id" ("<mid1>" score1 nil s) ("<mid2> score2 nil s))))
335         ;;`((("message-id" . ,predict-list))) ; Yes, this is the return value
336         (list (list (list (append (list "message-id") predict-list)))))
337     nil))
338
339 (defun bbb-get-predictions (midlist groupname)
340   "Ask the bbb for predictions, and build up the score alist."
341   (if (or (null grouplens-bbb-token)
342           (equal grouplens-bbb-token "0"))
343       (gnus-message 3 "Error: You are not logged in to a BBB")
344     (gnus-message 5 "Fetching Predictions...")
345     (let (predict-list
346           (predict-command (build-predict-command midlist groupname 
347                                                   grouplens-bbb-token))
348           (bbb-process (bbb-connect-to-bbbd grouplens-bbb-host 
349                                             grouplens-bbb-port)))
350       (if bbb-process
351           (save-excursion 
352             (set-buffer (process-buffer bbb-process))
353             (bbb-send-command bbb-process predict-command)
354             (if (bbb-read-response bbb-process)
355                 (setq predict-list (bbb-get-prediction-response bbb-process))
356               (gnus-message 1 "Invalid Token, login and try again")
357               (ding))))
358       (setq bbb-alist predict-list))))
359
360 (defun get-all-mids ()
361   (let ((index (nth 1 (assoc "message-id" gnus-header-index)))
362         (articles gnus-newsgroup-headers)
363         art this)
364     (setq bbb-mid-list nil)
365     (while articles
366       (progn (setq art (car articles)
367                    this (aref art index)
368                    articles (cdr articles))
369              (setq bbb-mid-list (cons this bbb-mid-list))))
370     bbb-mid-list))
371
372 (defun build-predict-command (mlist grpname token)
373   (let ((cmd (concat "getpredictions " token " " grpname "\r\n"))
374         art)
375     (while mlist
376       (setq art (car mlist)
377             cmd (concat cmd art "\r\n")
378             mlist (cdr mlist)))
379     (setq cmd (concat cmd ".\r\n"))
380   cmd))
381
382 (defun bbb-get-prediction-response (process)
383   (let ((case-fold-search nil)
384         match-end)
385     (goto-char bbb-read-point)
386     (while (and (not (search-forward ".\r\n" nil t))
387                 (accept-process-output process bbb-timeout-secs))
388       (goto-char bbb-read-point))
389     (setq match-end (point))
390     (goto-char (+ bbb-response-point 4))  ;; we ought to be right before OK
391     (build-response-alist)))
392
393 ;; build-response-alist assumes that the cursor has been positioned at
394 ;; the first line of the list of mid/rating pairs.  For now we will
395 ;; use a prediction of 99 to signify no prediction.  Ultimately, we
396 ;; should just ignore messages with no predictions.
397 (defun build-response-alist ()
398   (let ((resp nil)
399         (match-end (point)))
400     (setq grouplens-current-hashtable (make-hash-table :test 'equal :size 100))
401     (while
402         (cond ((looking-at "\\(<.*>\\) :nopred=")
403                (push `(,(get-mid) ,gnus-summary-default-score nil s) resp)
404                (forward-line 1)
405                t)
406               ((looking-at "\\(<.*>\\) :pred=\\([0-9]\.[0-9][0-9]\\) :conflow=\\([0-9]\.[0-9][0-9]\\) :confhigh=\\([0-9]\.[0-9][0-9]\\)")
407                (push `(,(get-mid) ,(get-pred) nil s) resp)
408                (cl-puthash (get-mid)
409                            (list (get-pred) (get-confl) (get-confh))
410                            grouplens-current-hashtable)
411                (forward-line 1)
412                t)
413               ((looking-at "\\(<.*>\\) :pred=\\([0-9]\.[0-9][0-9]\\)")
414                (push `(,(get-mid) ,(get-pred) nil s) resp)
415                (cl-puthash (get-mid)
416                            (list (get-pred) 0 0)
417                            grouplens-current-hashtable)
418                (forward-line 1)
419                t)
420               (t nil)))
421     resp))
422
423 ;; these two functions assume that there is an active match lying
424 ;; around.  Where the first parenthesized expression is the
425 ;; message-id, and the second is the prediction.  Since gnus assumes
426 ;; that scores are integer values?? we round the prediction.
427 (defun get-mid ()
428   (buffer-substring (match-beginning 1) (match-end 1)))
429
430 (defun get-pred ()
431   (let ((tpred (round (string-to-int (buffer-substring  
432                                       (match-beginning 2) 
433                                       (match-end 2))))))
434     (if (> tpred 0)
435         (* grouplens-score-scale-factor (+ grouplens-score-offset  tpred))
436       1)))
437
438 (defun get-confl ()
439   (string-to-number (buffer-substring (match-beginning 3) (match-end 3))))
440
441 (defun get-confh ()
442   (string-to-number (buffer-substring (match-beginning 4) (match-end 4))))
443
444 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
445 ;;;;      Prediction Display
446 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
447 (defconst rating-range 4.0)
448 (defconst grplens-maxrating 5)
449 (defconst grplens-minrating 1)
450 (defconst grplens-predstringsize 12)
451
452 (defalias 'bbb-grouplens-score 'gnus-user-format-function-G)
453
454 (defvar gnus-tmp-score)
455 (defun gnus-user-format-function-G (header)
456   (let* ((rate-string (make-string 12 ? ))
457          (mid (aref header (nth 1 (assoc "message-id" gnus-header-index))))
458          (hashent (gethash mid grouplens-current-hashtable))
459          (iscore (if (string-match "September" gnus-version) 
460                      gnus-tmp-score
461                    score))
462          (low (car (cdr hashent)))
463          (high (car (cdr (cdr hashent)))))
464     (aset rate-string 0 ?|) 
465     (aset rate-string 11 ?|)
466     (unless (member grouplens-current-group grouplens-newsgroups)
467       (unless (equal grouplens-prediction-display 'prediction-num)
468         (cond ((< iscore 0)
469                (setq iscore 1))
470               ((> iscore 5)
471                (setq iscore 5))))
472       (setq low 0) 
473       (setq high 0))
474     (if (and (grouplens-valid-score iscore) 
475              (not (null mid)))
476         (cond 
477          ;; prediction-spot
478          ((equal grouplens-prediction-display 'prediction-spot)
479           (setq rate-string (fmt-prediction-spot rate-string iscore)))
480          ;; confidence-interval
481          ((equal grouplens-prediction-display 'confidence-interval)
482           (setq rate-string (fmt-confidence-interval iscore low high)))
483          ;; prediction-bar
484          ((equal grouplens-prediction-display 'prediction-bar)
485           (setq rate-string (fmt-prediction-bar rate-string iscore)))
486          ;; confidence-bar
487          ((equal grouplens-prediction-display 'confidence-bar)
488           (setq rate-string (format "|   %4.2f   |" iscore)))
489          ;; confidence-spot
490          ((equal grouplens-prediction-display 'confidence-spot)
491           (setq rate-string (format "|   %4.2f   |" iscore)))
492          ;; prediction-num
493          ((equal grouplens-prediction-display 'prediction-num)
494           (setq rate-string (fmt-prediction-num iscore)))
495          ;; confidence-plus-minus
496          ((equal grouplens-prediction-display 'confidence-plus-minus)
497                (setq rate-string (fmt-confidence-plus-minus iscore low high))
498                )
499          (t (gnus-message 3 "Invalid prediction display type")))
500       (aset rate-string 5 ?N) (aset rate-string 6 ?A))
501     rate-string))
502
503 ;;
504 ;; Gnus user format function that doesn't depend on
505 ;; bbb-build-mid-scores-alist being used as the score function, but is
506 ;; instead called from gnus-select-group-hook. -- LAB
507 (defun gnus-user-format-function-L (header)
508   (if (not (member grouplens-current-group grouplens-newsgroups))
509       ;; Return an empty string
510       ""
511     (let* ((rate-string (make-string 12 ? ))
512            (mid (aref header (nth 1 (assoc "message-id" gnus-header-index))))
513            (hashent (gethash mid grouplens-current-hashtable))
514            (pred (nth 0 hashent))
515            (low (nth 1 hashent))
516            (high (nth 2 hashent)))
517       (gnus-message 5 (concat "mid = " mid))
518       ;; Init rate-string
519       (aset rate-string 0 ?|) 
520       (aset rate-string 11 ?|)
521       
522       ;; If no entry in BBB hash mark rate string as NA and return
523       (cond 
524        ((null hashent) 
525         (aset rate-string 5 ?N) 
526         (aset rate-string 6 ?A)
527         rate-string)
528
529        ((equal grouplens-prediction-display 'prediction-spot)
530         (fmt-prediction-spot rate-string pred))
531        
532        ((equal grouplens-prediction-display 'confidence-interval)
533         (fmt-confidence-interval pred low high))
534        
535        ((equal grouplens-prediction-display 'prediction-bar)
536         (fmt-prediction-bar rate-string pred))
537
538        ((equal grouplens-prediction-display 'confidence-bar)
539         (format "|   %4.2f   |" pred))
540
541        ((equal grouplens-prediction-display 'confidence-spot)
542         (format "|   %4.2f   |" pred))
543        
544        ((equal grouplens-prediction-display 'prediction-num)
545         (fmt-prediction-num pred))
546        
547        ((equal grouplens-prediction-display 'confidence-plus-minus)
548         (fmt-confidence-plus-minus pred low high))
549        
550        (t 
551         (gnus-message 3 "Invalid prediction display type")
552         (aset rate-string 0 ?|) 
553         (aset rate-string 11 ?|)
554         rate-string)))))
555
556 (defun grouplens-valid-score (score)
557   (or (equal grouplens-prediction-display 'prediction-num)
558       (and (>= score grplens-minrating)
559            (<= score grplens-maxrating))))
560
561 (defun requires-confidence (format-type)
562   (or (equal format-type 'confidence-plus-minus)
563       (equal format-type 'confidence-spot)
564       (equal format-type 'confidence-interval)))
565
566 (defun have-confidence (clow chigh)
567   (not (or (null clow)
568            (null chigh))))
569
570 (defun fmt-prediction-spot (rate-string score)
571   (aset rate-string
572         (round (* (/ (- score grplens-minrating) rating-range)
573                   (+ (- grplens-predstringsize 4) 1.49)))
574         ?*)
575   rate-string)
576
577 (defun fmt-confidence-interval (score low high)
578   (if (have-confidence low high)
579       (format "|%4.2f-%4.2f |" low high)
580     (fmt-prediction-num score)))
581
582 (defun fmt-confidence-plus-minus (score low high)
583   (if (have-confidence low high)
584       (format "|%3.1f+/-%4.2f|" score (/ (- high low) 2.0))
585     (fmt-prediction-num score)))
586
587 (defun fmt-prediction-bar (rate-string score)
588   (let* ((i 1) 
589          (step (/ rating-range (- grplens-predstringsize 4)))
590          (half-step (/ step 2))
591          (loc (- grplens-minrating half-step)))
592     (while (< i (- grplens-predstringsize 2))
593       (if (> score loc)
594           (aset rate-string i ?#)
595         (aset rate-string i ? ))
596       (setq i (+ i 1))
597       (setq loc (+ loc step)))
598     )
599   rate-string)
600
601 (defun fmt-prediction-num (score)
602   (format "|   %4.2f   |" score))
603
604 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
605 ;;;;       Put Ratings
606 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
607
608 ;; The message-id for the current article can be found in
609 ;; (aref gnus-current-headers (nth 1 (assoc "message-id" gnus-header-index)))
610
611 (defun bbb-put-ratings ()
612   (if (and grouplens-rating-alist 
613            (member gnus-newsgroup-name grouplens-newsgroups))
614       (let ((bbb-process (bbb-connect-to-bbbd grouplens-bbb-host 
615                                           grouplens-bbb-port))
616             (rate-command (build-rate-command grouplens-rating-alist)))
617         (if bbb-process
618             (save-excursion 
619               (set-buffer (process-buffer bbb-process))
620               (gnus-message 5 "Sending Ratings...")
621               (bbb-send-command bbb-process rate-command)
622               (if (bbb-read-response bbb-process)
623                   (setq grouplens-rating-alist nil)
624                 (gnus-message 1 
625                               "Token timed out: call bbb-login and quit again")
626                 (ding))
627               (gnus-message 5 "Sending Ratings...Done"))
628           (gnus-message 3 "No BBB connection")))
629     (setq grouplens-rating-alist nil)))
630
631 (defun build-rate-command (rate-alist)
632   (let (this
633         (cmd (concat "putratings " grouplens-bbb-token 
634                      " " grouplens-current-group " \r\n")))
635     (while rate-alist
636       (setq this (car rate-alist)
637             cmd (concat cmd (car this) " :rating=" (cadr this) ".00"
638                         " :time=" (cddr this) "\r\n")
639             rate-alist (cdr rate-alist)))
640     (concat cmd ".\r\n")))
641
642 ;; Interactive rating functions.
643 (defun bbb-summary-rate-article (rating &optional midin)
644   (interactive "nRating: ")
645   (when (member gnus-newsgroup-name grouplens-newsgroups)
646     (let ((mid (or midin (get-current-id))))
647       (if (and rating 
648                (>= rating grplens-minrating) 
649                (<= rating grplens-maxrating)
650                mid)
651           (let ((oldrating (assoc mid grouplens-rating-alist)))
652             (if oldrating
653                 (setcdr oldrating (cons rating 0))
654               (push `(,mid . (,rating . 0)) grouplens-rating-alist))
655             (gnus-summary-mark-article nil (int-to-string rating)))     
656         (gnus-message 3 "Invalid rating")))))
657
658 (defun grouplens-next-unread-article (rating)
659   "Select unread article after current one."
660   (interactive "P")
661   (if rating (bbb-summary-rate-article rating))
662   (gnus-summary-next-unread-article))
663
664 (defun grouplens-best-unread-article (rating)
665   "Select unread article after current one."
666   (interactive "P")
667   (if rating (bbb-summary-rate-article rating))
668   (gnus-summary-best-unread-article))
669
670 (defun grouplens-summary-catchup-and-exit (rating)
671    "Mark all articles not marked as unread in this newsgroup as read, 
672     then exit.   If prefix argument ALL is non-nil, all articles are 
673     marked as read."
674    (interactive "P")
675    (if rating
676        (bbb-summary-rate-article rating))
677    (if (numberp rating)
678        (gnus-summary-catchup-and-exit)
679      (gnus-summary-catchup-and-exit rating)))
680
681 (defun grouplens-score-thread (score)
682   "Raise the score of the articles in the current thread with SCORE."
683   (interactive "nRating: ")
684   (let (e)
685     (save-excursion
686       (let ((articles (gnus-summary-articles-in-thread)))
687         (while articles
688           (gnus-summary-goto-subject (car articles))
689           (gnus-set-global-variables)
690           (bbb-summary-rate-article score
691                                     (mail-header-id 
692                                      (gnus-summary-article-header 
693                                       (car articles))))
694           (setq articles (cdr articles))))
695       (setq e (point)))
696     (let ((gnus-summary-check-current t))
697       (or (zerop (gnus-summary-next-subject 1 t))
698           (goto-char e))))
699   (gnus-summary-recenter)
700   (gnus-summary-position-point)
701   (gnus-set-mode-line 'summary))
702
703
704 (defun get-current-id ()
705   (if gnus-current-headers
706       (aref gnus-current-headers 
707             (nth 1 (assoc "message-id" gnus-header-index)))
708     (gnus-message 3 "You must select an article before you rate it")))
709
710 (defvar gnus-tmp-group)
711 (defun gnus-user-format-function-I (header)
712   (let ((gname (if (string-match "September" gnus-version)
713                    gnus-tmp-group
714                  group)))
715     (if (member gname grouplens-newsgroups) "  (GroupLens Enhanced)" "")))
716
717 (defun bbb-grouplens-group-p (group)
718   "Say whether GROUP is a GroupLens group."
719   (if (member group grouplens-newsgroups) " (GroupLens Enhanced)" ""))
720
721 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
722 ;;          TIME SPENT READING
723 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
724 (defvar grouplens-current-starting-time nil)
725
726 (defun grouplens-start-timer ()
727   (setq grouplens-current-starting-time (current-time)))
728
729 (defun grouplens-elapsed-time ()
730   (let ((et (time-float (current-time))))
731     (- et (time-float grouplens-current-starting-time))))
732
733 (defun time-float (timeval)
734   (+ (* (car timeval) 65536) 
735         (cadr timeval)))
736
737 (defun grouplens-do-time ()
738   (when (member gnus-newsgroup-name grouplens-newsgroups)
739     (when grouplens-previous-article
740       (let ((elapsed-time (grouplens-elapsed-time))
741             (oldrating (assoc grouplens-previous-article 
742                               grouplens-rating-alist)))
743         (if (not oldrating)
744             (push `(,grouplens-previous-article . (0 . ,elapsed-time))
745                   grouplens-rating-alist)
746           (setcdr oldrating (cons (cadr oldrating) elapsed-time)))))
747     (grouplens-start-timer)
748     (setq grouplens-previous-article (get-current-id))))
749
750 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
751 ;;          BUG REPORTING
752 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
753
754 (defconst gnus-gl-version "gnus-gl.el 2.5lars/rrh")
755 (defconst gnus-gl-maintainer-address "grouplens-bug@cs.umn.edu")
756 (defun gnus-gl-submit-bug-report ()
757   "Submit via mail a bug report on gnus-gl"
758   (interactive)
759   (require 'reporter)
760   (reporter-submit-bug-report gnus-gl-maintainer-address
761                               (concat "gnus-gl.el " gnus-gl-version)
762                               (list 'grouplens-pseudonym
763                                     'grouplens-bbb-host
764                                     'grouplens-bbb-port
765                                     'grouplens-newsgroups
766                                     'grouplens-bbb-token
767                                     'grouplens-bbb-process
768                                     'grouplens-current-group
769                                     'grouplens-previous-article
770                                     'grouplens-mid-list
771                                     'bbb-alist)
772                               nil
773                               'gnus-gl-get-trace))
774
775 (defun gnus-gl-get-trace ()
776   "Insert the contents of the BBBD trace buffer"
777   (if grouplens-bbb-buffer (insert-buffer grouplens-bbb-buffer)))
778
779 ;;;
780 ;;; Additions to make gnus-grouplens-mode  Warning Warning!!
781 ;;;      This version of the gnus-grouplens-mode does
782 ;;;      not work with gnus-5.x.  The "old" way of
783 ;;;      setting up GroupLens still works however.
784 ;;;
785 (defvar gnus-grouplens-mode nil
786   "Minor mode for providing a GroupLens interface in Gnus summary buffers.")
787
788 (defvar gnus-grouplens-mode-map nil)
789
790 (unless gnus-grouplens-mode-map
791   (setq gnus-grouplens-mode-map (make-keymap))
792   (gnus-define-keys
793    gnus-grouplens-mode-map
794    "n" grouplens-next-unread-article
795    "r" bbb-summary-rate-article
796    "k" grouplens-score-thread
797    "c" grouplens-summary-catchup-and-exit
798    "," grouplens-best-unread-article))
799
800 (defun gnus-grouplens-make-menu-bar ()
801   (unless (boundp 'gnus-grouplens-menu)
802     (easy-menu-define
803      gnus-grouplens-menu gnus-grouplens-mode-map ""
804      '("GroupLens"
805        ["Login" bbb-login t]
806        ["Rate" bbb-summary-rate-article t]
807        ["Next article" grouplens-next-unread-article t]
808        ["Best article" grouplens-best-unread-article t]
809        ["Raise thread" grouplens-score-thread t]
810        ["Report bugs" gnus-gl-submit-bug-report t]))))
811
812 (defun gnus-grouplens-mode (&optional arg)
813   "Minor mode for providing a GroupLens interface in Gnus summary buffers."
814   (interactive "P")
815   (when (and (eq major-mode 'gnus-summary-mode)
816              (member gnus-newsgroup-name grouplens-newsgroups))
817     (make-local-variable 'gnus-grouplens-mode)
818     (setq gnus-grouplens-mode 
819           (if (null arg) (not gnus-grouplens-mode)
820             (> (prefix-numeric-value arg) 0)))
821     (when gnus-grouplens-mode
822       (if (not (fboundp 'make-local-hook))
823          (add-hook 'gnus-select-article-hook 'grouplens-do-time)
824        (make-local-hook 'gnus-select-article-hook)
825        (add-hook 'gnus-select-article-hook 'grouplens-do-time nil 'local))
826       (if (not (fboundp 'make-local-hook))
827          (add-hook 'gnus-exit-group-hook 'bbb-put-ratings)
828        (make-local-hook 'gnus-exit-group-hook)
829        (add-hook 'gnus-exit-group-hook 'bbb-put-ratings nil 'local))
830       (make-local-variable 'gnus-score-find-score-files-function)
831       (if gnus-grouplens-override-scoring
832           (setq gnus-score-find-score-files-function 
833                 'bbb-build-mid-scores-alist)
834         (add-hook 'gnus-select-group-hook 
835                   '(lambda() 
836                      (bbb-build-mid-scores-alist gnus-newsgroup-name))))
837       (make-local-variable 'gnus-summary-line-format)
838       (setq gnus-summary-line-format gnus-summary-grouplens-line-format)
839       (make-local-variable 'gnus-summary-line-format-spec)
840
841       ;; Set up the menu.
842       (when (and menu-bar-mode
843                  (gnus-visual-p 'grouplens-menu 'menu))
844         (gnus-grouplens-make-menu-bar))
845       (unless (assq 'gnus-grouplens-mode minor-mode-alist)
846         (push '(gnus-grouplens-mode " GroupLens") minor-mode-alist))
847       (unless (assq 'gnus-grouplens-mode minor-mode-map-alist)
848         (push (cons 'gnus-grouplens-mode gnus-grouplens-mode-map)
849               minor-mode-map-alist))
850       (run-hooks 'gnus-grouplens-mode-hook))))
851
852 (provide 'gnus-gl)
853
854 ;;; end gnus-gl.el