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