*** empty log message ***
[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               beg cgroup active article result prefix)
75           (while articles
76             (setq article (assq (pop articles) nnvirtual-mapping))
77             (when (and (setq cgroup (cadr article))
78                        (gnus-check-server
79                         (gnus-find-method-for-group cgroup) t)
80                        (gnus-request-group cgroup t))
81               (setq prefix (gnus-group-real-prefix cgroup))
82               (when (setq result (gnus-retrieve-headers 
83                                   (list (caddr article)) cgroup fetch-old))
84                 (set-buffer nntp-server-buffer)
85                 (if (zerop (buffer-size))
86                     (nconc (assq cgroup unfetched) (list (caddr article)))
87                   ;; If we got HEAD headers, we convert them into NOV
88                   ;; headers.  This is slow, inefficient and, come to think
89                   ;; of it, downright evil.  So sue me.  I couldn't be
90                   ;; bothered to write a header parse routine that could
91                   ;; parse a mixed HEAD/NOV buffer.
92                   (when (eq result 'headers)
93                     (nnvirtual-convert-headers))
94                   (goto-char (point-min))
95                   (while (not (eobp))
96                     (delete-region 
97                      (point) (progn (read nntp-server-buffer) (point)))
98                     (insert (int-to-string (car article)))
99                     (beginning-of-line)
100                     (looking-at 
101                      "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
102                     (goto-char (match-end 0))
103                     (or (search-forward 
104                          "\t" (save-excursion (end-of-line) (point)) t)
105                         (end-of-line))
106                     (while (= (char-after (1- (point))) ? )
107                       (forward-char -1)
108                       (delete-char 1))
109                     (if (eolp)
110                         (progn
111                           (end-of-line)
112                           (or (= (char-after (1- (point))) ?\t)
113                               (insert ?\t))
114                           (insert (format "Xref: %s %s:%d\t" (system-name) 
115                                           cgroup (caddr article))))
116                       (insert (format "Xref: %s %s:%d " (system-name) 
117                                       cgroup (caddr article)))
118                       (if (not (string= "" prefix))
119                           (while (re-search-forward 
120                                   "[^ ]+:[0-9]+"
121                                   (save-excursion (end-of-line) (point)) t)
122                             (save-excursion
123                               (goto-char (match-beginning 0))
124                               (insert prefix))))
125                       (end-of-line)
126                       (or (= (char-after (1- (point))) ?\t)
127                           (insert ?\t)))
128                     (forward-line 1))
129                   (set-buffer vbuf)
130                   (goto-char (point-max))
131                   (insert-buffer-substring nntp-server-buffer)))))
132           
133           ;; In case some of the articles have expired or been
134           ;; cancelled, we have to mark them as read in the
135           ;; component group.
136           (while unfetched
137             (when (cdar unfetched)
138               (gnus-group-make-articles-read 
139                (caar unfetched) (sort (cdar unfetched) '<)))
140             (setq unfetched (cdr unfetched)))
141
142           ;; The headers are ready for reading, so they are inserted into
143           ;; the nntp-server-buffer, which is where Gnus expects to find
144           ;; them.
145           (prog1
146               (save-excursion
147                 (set-buffer nntp-server-buffer)
148                 (erase-buffer)
149                 (insert-buffer-substring vbuf)
150                 'nov)
151             (kill-buffer vbuf)))))))
152
153 (defun nnvirtual-open-server (server &optional something)
154   (nnheader-init-server-buffer))
155
156 (defun nnvirtual-close-server (&rest dum)
157   t)
158
159 (defun nnvirtual-request-close ()
160   (setq nnvirtual-current-group nil
161         nnvirtual-component-groups nil
162         nnvirtual-mapping nil
163         nnvirtual-group-alist nil)
164   t)
165
166 (defun nnvirtual-server-opened (&optional server)
167   (and nntp-server-buffer
168        (get-buffer nntp-server-buffer)))
169
170 (defun nnvirtual-status-message (&optional server)
171   nnvirtual-status-string)
172
173 (defun nnvirtual-request-article (article &optional group server buffer)
174   (when (and (nnvirtual-possibly-change-group group server t)
175              (numberp article))
176     (let* ((amap (assq article nnvirtual-mapping))
177            (cgroup (cadr amap)))
178       (cond
179        ((not amap)
180         (nnheader-report 'nnvirtual "No such article: %s" article))
181        ((not (gnus-check-group cgroup))
182         (nnheader-report
183          'nnvirtual "Can't open server where %s exists" cgroup))
184        ((not (gnus-request-group cgroup t))
185         (nnheader-report 'nnvirtual "Can't open component group %s" cgroup))
186        (t
187         (if buffer 
188             (save-excursion
189               (set-buffer buffer)
190               (gnus-request-article-this-buffer (caddr amap) cgroup))
191           (gnus-request-article (caddr amap) cgroup)))))))
192
193 (defun nnvirtual-request-group (group &optional server dont-check)
194   (cond
195    ((null (nnvirtual-possibly-change-group
196            group server 
197            (if nnvirtual-always-rescan nil dont-check)))
198     (setq nnvirtual-current-group nil)
199     (nnheader-report 'nnvirtual "No component groups in %s" group))
200    (t
201     (let ((len (length nnvirtual-mapping)))
202       (nnheader-insert "211 %d 1 %d %s\n" len len group)))))
203
204 (defun nnvirtual-request-type (group &optional article)
205   (when (nnvirtual-possibly-change-group group nil t)
206     (if (not article)
207         'unknown
208       (let ((mart (assq article nnvirtual-mapping)))
209         (when mart
210           (gnus-request-type (cadr mart) (car mart)))))))
211
212 (defun nnvirtual-request-update-mark (group article mark)
213   (when (nnvirtual-possibly-change-group group nil t)
214     (let* ((nart (assq article nnvirtual-mapping))
215            (cgroup (cadr nart))
216            ;; The component group might be a virtual group.
217            (nmark (gnus-request-update-mark cgroup (caddr nart) mark)))
218       (when (and (= mark nmark)
219                  (gnus-group-auto-expirable-p cgroup))
220         (setq mark gnus-expirable-mark))))
221   mark)
222     
223 (defun nnvirtual-close-group (group &optional server)
224   (when (nnvirtual-possibly-change-group group server t)
225     ;; We copy the marks from this group to the component
226     ;; groups here.
227     (nnvirtual-update-marked)
228     ;; Reset all relevant variables.
229     (setq nnvirtual-current-group nil
230           nnvirtual-component-groups nil
231           nnvirtual-mapping nil)
232     (setq nnvirtual-group-alist 
233           (delq (assoc group nnvirtual-group-alist) nnvirtual-group-alist)))
234   t)
235     
236 (defun nnvirtual-request-list (&optional server) 
237   (nnheader-report 'nnvirtual "LIST is not implemented."))
238
239 (defun nnvirtual-request-newgroups (date &optional server)
240   (nnheader-report 'nnvirtual "NEWGROUPS is not supported."))
241
242 (defun nnvirtual-request-list-newsgroups (&optional server)
243   (nnheader-report 'nnvirtual "LIST NEWSGROUPS is not implemented."))
244
245 (defun nnvirtual-request-update-info (group info &optional server)
246   (when (nnvirtual-possibly-change-group group server)
247     (let ((map nnvirtual-mapping)
248           (marks (mapcar (lambda (m) (list (cdr m))) gnus-article-mark-lists))
249           reads marks mr m op)
250       (while map
251         (setq m (pop map))
252         (unless (nth 3 m)
253           (push (car m) reads))
254         (when (setq mr (nth 4 m))
255           (while mr
256             (setcdr (setq op (assq (pop mr) marks)) (cons (car m) (cdr op))))))
257       (setq mr marks)
258       (while mr
259         (setcdr (car mr) (gnus-compress-sequence (sort (cdar mr) '<)))
260         (setq mr (cdr mr)))
261       (setcar (cddr info) (gnus-compress-sequence (nreverse reads)))
262       
263       ;; Enter these new marks into the info of the group.
264       (if (nthcdr 3 info)
265           (setcar (nthcdr 3 info) marks)
266         ;; Add the marks lists to the end of the info.
267         (when marks
268           (setcdr (nthcdr 2 info) (list marks))))
269       t)))
270
271 (defun nnvirtual-catchup-group (group &optional server all)
272   (nnvirtual-possibly-change-group group server t)
273   (let ((gnus-group-marked nnvirtual-component-groups)
274         (gnus-expert-user t))
275     (save-excursion
276       (set-buffer gnus-group-buffer)
277       (gnus-group-catchup-current nil all))))
278
279 (defun nnvirtual-find-group-art (group article)
280   "Return the real group and article for virtual GROUP and ARTICLE."
281   (nnvirtual-possibly-change-group group nil t)
282   (let ((mart (assq article nnvirtual-mapping)))
283     (when mart
284       (cons (cadr mart) (caddr mart)))))
285
286 \f
287 ;;; Internal functions.
288
289 (defun nnvirtual-convert-headers ()
290   "Convert HEAD headers into NOV headers."
291   (save-excursion
292     (set-buffer nntp-server-buffer)
293     (let* ((dependencies (make-vector 100 0))
294            (headers (gnus-get-newsgroup-headers dependencies))
295            header)
296       (erase-buffer)
297       (while (setq header (pop headers))
298         (insert (int-to-string (mail-header-number header)) "\t"
299                 (or (mail-header-subject header) "") "\t"
300                 (or (mail-header-from header) "") "\t"
301                 (or (mail-header-date header) "") "\t"
302                 (or (mail-header-id header) "") "\t"
303                 (or (mail-header-references header) "") "\t"
304                 (int-to-string (or (mail-header-chars header) 0)) "\t"
305                 (int-to-string (or (mail-header-lines header) 0)) "\t"
306                 (if (mail-header-xref header) 
307                     (concat "Xref: " (mail-header-xref header) "\t")
308                   "") "\n")))))
309
310 (defun nnvirtual-possibly-change-group (group regexp &optional dont-check)
311   (let ((inf t))
312     (unless (equal group nnvirtual-current-group)
313       (and (setq inf (assoc group nnvirtual-group-alist))
314            regexp
315            (string= (nth 3 inf) regexp)
316            (progn
317              (setq nnvirtual-current-group (car inf))
318              (setq nnvirtual-component-groups (nth 1 inf))
319              (setq nnvirtual-mapping (nth 2 inf)))))
320     (when (and regexp
321                (or (not inf)
322                    (not dont-check)))
323       (and inf (setq nnvirtual-group-alist 
324                      (delq inf nnvirtual-group-alist)))
325       (setq nnvirtual-mapping nil)
326       (setq nnvirtual-current-group group)
327       (let ((newsrc gnus-newsrc-alist)
328             (virt-group (gnus-group-prefixed-name 
329                          nnvirtual-current-group '(nnvirtual ""))))
330         (setq nnvirtual-component-groups nil)
331         (while newsrc
332           (and (string-match regexp (caar newsrc))
333                (not (string= (caar newsrc) virt-group))
334                (setq nnvirtual-component-groups
335                      (cons (caar newsrc) nnvirtual-component-groups)))
336           (setq newsrc (cdr newsrc))))
337       (if nnvirtual-component-groups
338           (progn
339             (nnvirtual-create-mapping)
340             (setq nnvirtual-group-alist
341                   (cons (list group nnvirtual-component-groups 
342                               nnvirtual-mapping regexp)
343                         nnvirtual-group-alist)))
344         (nnheader-report 'nnvirtual "No component groups: %s" group))))
345   nnvirtual-component-groups)
346
347 (defun nnvirtual-update-marked ()
348   "Copy marks from the virtual group to the component groups."
349   (let ((mark-lists gnus-article-mark-lists)
350         (uncompressed '(score bookmark))
351         type list calist mart cgroups)
352     (while mark-lists
353       (setq type (cdar mark-lists))
354       (setq list (symbol-value (intern (format "gnus-newsgroup-%s"
355                                                (car (pop mark-lists))))))
356       (setq cgroups 
357             (mapcar (lambda (g) (list g)) nnvirtual-component-groups))
358       (while list
359         (nconc (assoc (cadr (setq mart (assq (pop list) nnvirtual-mapping)))
360                       cgroups)
361                (list (caddr mart))))
362       (while cgroups
363         (gnus-add-marked-articles 
364          (caar cgroups) type (cdar cgroups) nil t)
365         (gnus-group-update-group (car (pop cgroups)) t)))))
366
367 (defun nnvirtual-marks (article marks)
368   "Return a list of mark types for ARTICLE."
369   (let (out)
370     (while marks
371       (when (memq article (cdar marks))
372         (push (caar marks) out))
373       (setq marks (cdr marks)))
374     out))
375
376 (defun nnvirtual-create-mapping ()
377   "Create an article mapping for the current group."
378   (let* (div m marks list article
379          (map (sort
380                (apply 
381                 'nconc
382                 (mapcar
383                  (lambda (g)
384                    (let* ((active (or (gnus-active g) (gnus-activate-group g)))
385                           (unreads (and active (gnus-list-of-unread-articles
386                                                 g)))
387                           (marks (gnus-uncompress-marks
388                                   (gnus-info-marks (gnus-get-info g)))))
389                      (when active
390                        (when gnus-use-cache
391                          (push (cons 'cache (gnus-cache-articles-in-group g))
392                                marks))
393                        (when active
394                          (setq div (/ (float (car active)) 
395                                       (if (zerop (cdr active))
396                                           1 (cdr active))))
397                          (mapcar (lambda (n) 
398                                    (list (* div (- n (car active)))
399                                          g n (and (memq n unreads) t)
400                                          (nnvirtual-marks n marks)))
401                                  (gnus-uncompress-range active))))))    
402          nnvirtual-component-groups))
403                (lambda (m1 m2)
404                  (< (car m1) (car m2)))))
405          (i 0))
406     (setq nnvirtual-mapping map)
407     (while (setq m (pop map))
408       (setcar m (setq article (incf i)))
409       (when (setq marks (nth 4 m))
410         (while marks
411           (set (setq list
412                      (intern (concat "gnus-newsgroup-" 
413                                      (symbol-name 
414                                       (car (rassq (pop marks)
415                                                   gnus-article-mark-lists))))))
416                (cons article (symbol-value list))))))))
417                      
418                
419
420 (provide 'nnvirtual)
421
422 ;;; nnvirtual.el ends here