af07285d5cd7d98f821111136bf3c2d8e374a4fa
[gnus] / lisp / nnvirtual.el
1 ;;; nnvirtual.el --- virtual newsgroups access for Gnus
2 ;; Copyright (C) 1994,95,96 Free Software Foundation, Inc.
3
4 ;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
5 ;;      Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;; The other access methods (nntp, nnspool, etc) are general news
28 ;; access methods. This module relies on Gnus and can not be used
29 ;; separately.
30
31 ;;; Code:
32
33 (require 'nntp)
34 (require 'nnheader)
35 (require 'gnus)
36 (eval-when-compile (require 'cl))
37
38 (defvar nnvirtual-always-rescan nil
39   "*If non-nil, always scan groups for unread articles when entering a group.
40 If this variable is nil (which is the default) and you read articles
41 in a component group after the virtual group has been activated, the
42 read articles from the component group will show up when you enter the
43 virtual group.")
44
45 \f
46
47 (defconst nnvirtual-version "nnvirtual 1.0"
48   "Version number of this version of nnvirtual.")
49
50 (defvar nnvirtual-group-alist nil)
51 (defvar nnvirtual-current-group nil)
52 (defvar nnvirtual-component-groups nil)
53 (defvar nnvirtual-mapping nil)
54
55 (defvar nnvirtual-status-string "")
56
57 (eval-and-compile
58   (autoload 'gnus-cache-articles-in-group "gnus-cache"))
59
60 \f
61
62 ;;; Interface functions.
63
64 (defun nnvirtual-retrieve-headers (articles &optional newsgroup server fetch-old)
65   (when (nnvirtual-possibly-change-group newsgroup server t)
66     (save-excursion
67       (if (stringp (car articles))
68           'headers
69         (let ((map nnvirtual-mapping)
70               (vbuf (nnheader-set-temp-buffer 
71                      (get-buffer-create " *virtual headers*")))
72               (unfetched (mapcar (lambda (g) (list g))
73                                  nnvirtual-component-groups))
74               (system-name (system-name))
75               beg cgroup active article result prefix)
76           (while articles
77             (setq article (assq (pop articles) nnvirtual-mapping))
78             (when (and (setq cgroup (cadr article))
79                        (gnus-check-server
80                         (gnus-find-method-for-group cgroup) t)
81                        (gnus-request-group cgroup t))
82               (setq prefix (gnus-group-real-prefix cgroup))
83               (when (setq result (gnus-retrieve-headers 
84                                   (list (caddr article)) cgroup fetch-old))
85                 (set-buffer nntp-server-buffer)
86                 (if (zerop (buffer-size))
87                     (nconc (assq cgroup unfetched) (list (caddr article)))
88                   ;; If we got HEAD headers, we convert them into NOV
89                   ;; headers.  This is slow, inefficient and, come to think
90                   ;; of it, downright evil.  So sue me.  I couldn't be
91                   ;; bothered to write a header parse routine that could
92                   ;; parse a mixed HEAD/NOV buffer.
93                   (when (eq result 'headers)
94                     (nnvirtual-convert-headers))
95                   (goto-char (point-min))
96                   (while (not (eobp))
97                     (delete-region 
98                      (point) (progn (read nntp-server-buffer) (point)))
99                     (princ (car article) (current-buffer))
100                     (beginning-of-line)
101                     (looking-at 
102                      "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
103                     (goto-char (match-end 0))
104                     (or (search-forward 
105                          "\t" (save-excursion (end-of-line) (point)) t)
106                         (end-of-line))
107                     (while (= (char-after (1- (point))) ? )
108                       (forward-char -1)
109                       (delete-char 1))
110                     (if (eolp)
111                         (progn
112                           (end-of-line)
113                           (or (= (char-after (1- (point))) ?\t)
114                               (insert ?\t))
115                           (insert "Xref: " system-name " " cgroup ":")
116                           (princ (caddr article) (current-buffer))
117                           (insert "\t"))
118                       (insert "Xref: " system-name " " cgroup ":")
119                       (princ (caddr article) (current-buffer))
120                       (insert " ")
121                       (if (not (string= "" prefix))
122                           (while (re-search-forward 
123                                   "[^ ]+:[0-9]+"
124                                   (save-excursion (end-of-line) (point)) t)
125                             (save-excursion
126                               (goto-char (match-beginning 0))
127                               (insert prefix))))
128                       (end-of-line)
129                       (or (= (char-after (1- (point))) ?\t)
130                           (insert ?\t)))
131                     (forward-line 1))
132                   (set-buffer vbuf)
133                   (goto-char (point-max))
134                   (insert-buffer-substring nntp-server-buffer)))))
135           
136           ;; In case some of the articles have expired or been
137           ;; cancelled, we have to mark them as read in the
138           ;; component group.
139           (while unfetched
140             (when (cdar unfetched)
141               (gnus-group-make-articles-read 
142                (caar unfetched) (sort (cdar unfetched) '<)))
143             (setq unfetched (cdr unfetched)))
144
145           ;; The headers are ready for reading, so they are inserted into
146           ;; the nntp-server-buffer, which is where Gnus expects to find
147           ;; them.
148           (prog1
149               (save-excursion
150                 (set-buffer nntp-server-buffer)
151                 (erase-buffer)
152                 (insert-buffer-substring vbuf)
153                 'nov)
154             (kill-buffer vbuf)))))))
155
156 (defun nnvirtual-open-server (server &optional something)
157   (nnheader-init-server-buffer))
158
159 (defun nnvirtual-close-server (&rest dum)
160   t)
161
162 (defun nnvirtual-request-close ()
163   (setq nnvirtual-current-group nil
164         nnvirtual-component-groups nil
165         nnvirtual-mapping nil
166         nnvirtual-group-alist nil)
167   t)
168
169 (defun nnvirtual-server-opened (&optional server)
170   (and nntp-server-buffer
171        (get-buffer nntp-server-buffer)))
172
173 (defun nnvirtual-status-message (&optional server)
174   nnvirtual-status-string)
175
176 (defun nnvirtual-request-article (article &optional group server buffer)
177   (when (and (nnvirtual-possibly-change-group group server t)
178              (numberp article))
179     (let* ((amap (assq article nnvirtual-mapping))
180            (cgroup (cadr amap)))
181       (cond
182        ((not amap)
183         (nnheader-report 'nnvirtual "No such article: %s" article))
184        ((not (gnus-check-group cgroup))
185         (nnheader-report
186          'nnvirtual "Can't open server where %s exists" cgroup))
187        ((not (gnus-request-group cgroup t))
188         (nnheader-report 'nnvirtual "Can't open component group %s" cgroup))
189        (t
190         (if buffer 
191             (save-excursion
192               (set-buffer buffer)
193               (gnus-request-article-this-buffer (caddr amap) cgroup))
194           (gnus-request-article (caddr amap) cgroup)))))))
195
196 (defun nnvirtual-request-group (group &optional server dont-check)
197   (cond
198    ((null (nnvirtual-possibly-change-group
199            group server 
200            (if nnvirtual-always-rescan nil (not dont-check))))
201     (setq nnvirtual-current-group nil)
202     (nnheader-report 'nnvirtual "No component groups in %s" group))
203    (t
204     (let ((len (length nnvirtual-mapping)))
205       (nnheader-insert "211 %d 1 %d %s\n" len len group)))))
206
207 (defun nnvirtual-request-type (group &optional article)
208   (when (nnvirtual-possibly-change-group group nil t)
209     (if (not article)
210         'unknown
211       (let ((mart (assq article nnvirtual-mapping)))
212         (when mart
213           (gnus-request-type (cadr mart) (car mart)))))))
214
215 (defun nnvirtual-request-update-mark (group article mark)
216   (when (nnvirtual-possibly-change-group group nil t)
217     (let* ((nart (assq article nnvirtual-mapping))
218            (cgroup (cadr nart))
219            ;; The component group might be a virtual group.
220            (nmark (gnus-request-update-mark cgroup (caddr nart) mark)))
221       (when (and (= mark nmark)
222                  (gnus-group-auto-expirable-p cgroup))
223         (setq mark gnus-expirable-mark))))
224   mark)
225     
226 (defun nnvirtual-close-group (group &optional server)
227   (when (nnvirtual-possibly-change-group group server t)
228     ;; We copy the marks from this group to the component
229     ;; groups here.
230     (nnvirtual-update-marked)
231     ;; Reset all relevant variables.
232     (setq nnvirtual-current-group nil
233           nnvirtual-component-groups nil
234           nnvirtual-mapping nil)
235     (setq nnvirtual-group-alist 
236           (delq (assoc group nnvirtual-group-alist) nnvirtual-group-alist)))
237   t)
238     
239 (defun nnvirtual-request-list (&optional server) 
240   (nnheader-report 'nnvirtual "LIST is not implemented."))
241
242 (defun nnvirtual-request-newgroups (date &optional server)
243   (nnheader-report 'nnvirtual "NEWGROUPS is not supported."))
244
245 (defun nnvirtual-request-list-newsgroups (&optional server)
246   (nnheader-report 'nnvirtual "LIST NEWSGROUPS is not implemented."))
247
248 (defun nnvirtual-request-update-info (group info &optional server)
249   (when (nnvirtual-possibly-change-group group server)
250     (let ((map nnvirtual-mapping)
251           (marks (mapcar (lambda (m) (list (cdr m))) gnus-article-mark-lists))
252           reads marks mr m op)
253       (while map
254         (setq m (pop map))
255         (unless (nth 3 m)
256           (push (car m) reads))
257         (when (setq mr (nth 4 m))
258           (while mr
259             (setcdr (setq op (assq (pop mr) marks)) (cons (car m) (cdr op))))))
260       (setq mr marks)
261       (while mr
262         (setcdr (car mr) (gnus-compress-sequence (sort (cdar mr) '<)))
263         (setq mr (cdr mr)))
264       (setcar (cddr info) (gnus-compress-sequence (nreverse reads)))
265       
266       ;; Enter these new marks into the info of the group.
267       (if (nthcdr 3 info)
268           (setcar (nthcdr 3 info) marks)
269         ;; Add the marks lists to the end of the info.
270         (when marks
271           (setcdr (nthcdr 2 info) (list marks))))
272       t)))
273
274 (defun nnvirtual-catchup-group (group &optional server all)
275   (nnvirtual-possibly-change-group group server t)
276   (let ((gnus-group-marked nnvirtual-component-groups)
277         (gnus-expert-user t))
278     (save-excursion
279       (set-buffer gnus-group-buffer)
280       (gnus-group-catchup-current nil all))))
281
282 (defun nnvirtual-find-group-art (group article)
283   "Return the real group and article for virtual GROUP and ARTICLE."
284   (nnvirtual-possibly-change-group group nil t)
285   (let ((mart (assq article nnvirtual-mapping)))
286     (when mart
287       (cons (cadr mart) (caddr mart)))))
288
289 \f
290 ;;; Internal functions.
291
292 (defun nnvirtual-convert-headers ()
293   "Convert HEAD headers into NOV headers."
294   (save-excursion
295     (set-buffer nntp-server-buffer)
296     (let* ((dependencies (make-vector 100 0))
297            (headers (gnus-get-newsgroup-headers dependencies))
298            header)
299       (erase-buffer)
300       (while (setq header (pop headers))
301         (insert (int-to-string (mail-header-number header)) "\t"
302                 (or (mail-header-subject header) "") "\t"
303                 (or (mail-header-from header) "") "\t"
304                 (or (mail-header-date header) "") "\t"
305                 (or (mail-header-id header) "") "\t"
306                 (or (mail-header-references header) "") "\t"
307                 (int-to-string (or (mail-header-chars header) 0)) "\t"
308                 (int-to-string (or (mail-header-lines header) 0)) "\t"
309                 (if (mail-header-xref header) 
310                     (concat "Xref: " (mail-header-xref header) "\t")
311                   "") "\n")))))
312
313 (defun nnvirtual-possibly-change-group (group regexp &optional check)
314   (let ((inf t))
315     (when (or (not (equal group nnvirtual-current-group))
316               check)
317       (and (setq inf (assoc group nnvirtual-group-alist))
318            regexp
319            (string= (nth 3 inf) regexp)
320            (progn
321              (setq nnvirtual-current-group (car inf))
322              (setq nnvirtual-component-groups (nth 1 inf))
323              (setq nnvirtual-mapping (nth 2 inf))))
324       (when (and regexp (not inf))
325         (and inf (setq nnvirtual-group-alist 
326                        (delq inf nnvirtual-group-alist)))
327         (setq nnvirtual-mapping nil)
328         (setq nnvirtual-current-group group)
329         (let ((newsrc gnus-newsrc-alist)
330               (virt-group (gnus-group-prefixed-name 
331                            nnvirtual-current-group '(nnvirtual ""))))
332           (setq nnvirtual-component-groups nil)
333           (while newsrc
334             (and (string-match regexp (caar newsrc))
335                  (not (string= (caar newsrc) virt-group))
336                  (setq nnvirtual-component-groups
337                        (cons (caar newsrc) nnvirtual-component-groups)))
338             (setq newsrc (cdr newsrc))))
339         (if nnvirtual-component-groups
340             (progn
341               (nnvirtual-create-mapping)
342               (setq nnvirtual-group-alist
343                     (cons (list group nnvirtual-component-groups 
344                                 nnvirtual-mapping regexp)
345                           nnvirtual-group-alist)))
346           (nnheader-report 'nnvirtual "No component groups: %s" group)))))
347   nnvirtual-component-groups)
348
349 (defun nnvirtual-update-marked ()
350   "Copy marks from the virtual group to the component groups."
351   (let ((mark-lists gnus-article-mark-lists)
352         (uncompressed '(score bookmark))
353         type list calist mart cgroups)
354     (while mark-lists
355       (setq type (cdar mark-lists))
356       (setq list (symbol-value (intern (format "gnus-newsgroup-%s"
357                                                (car (pop mark-lists))))))
358       (setq cgroups 
359             (mapcar (lambda (g) (list g)) nnvirtual-component-groups))
360       (while list
361         (nconc (assoc (cadr (setq mart (assq (pop list) nnvirtual-mapping)))
362                       cgroups)
363                (list (caddr mart))))
364       (while cgroups
365         (gnus-add-marked-articles 
366          (caar cgroups) type (cdar cgroups) nil t)
367         (gnus-group-update-group (car (pop cgroups)) t)))))
368
369 (defsubst nnvirtual-marks (article marks)
370   "Return a list of mark types for ARTICLE."
371   (let (out)
372     (while marks
373       (when (memq article (cdar marks))
374         (push (caar marks) out))
375       (setq marks (cdr marks)))
376     out))
377
378 (defun nnvirtual-create-mapping ()
379   "Create an article mapping for the current group."
380   (let* (div m marks list article
381          (map (sort
382                (apply 
383                 'nconc
384                 (mapcar
385                  (lambda (g)
386                    (let* ((active (or (gnus-active g) (gnus-activate-group g)))
387                           (unreads (and active (gnus-list-of-unread-articles
388                                                 g)))
389                           (marks (gnus-uncompress-marks
390                                   (gnus-info-marks (gnus-get-info g)))))
391                      (when active
392                        (when gnus-use-cache
393                          (push (cons 'cache (gnus-cache-articles-in-group g))
394                                marks))
395                        (when active
396                          (setq div (/ (float (car active)) 
397                                       (if (zerop (cdr active))
398                                           1 (cdr active))))
399                          (mapcar (lambda (n) 
400                                    (list (* div (- n (car active)))
401                                          g n (and (memq n unreads) t)
402                                          (nnvirtual-marks n marks)))
403                                  (gnus-uncompress-range active))))))    
404          nnvirtual-component-groups))
405                (lambda (m1 m2)
406                  (< (car m1) (car m2)))))
407          (i 0))
408     (setq nnvirtual-mapping map)
409     (while (setq m (pop map))
410       (setcar m (setq article (incf i)))
411       (when (setq marks (nth 4 m))
412         (while marks
413           (set (setq list
414                      (intern (concat "gnus-newsgroup-" 
415                                      (symbol-name 
416                                       (car (rassq (pop marks)
417                                                   gnus-article-mark-lists))))))
418                (cons article (symbol-value list))))))))
419                      
420                
421
422 (provide 'nnvirtual)
423
424 ;;; nnvirtual.el ends here