-;;;; nnvirtual.el --- virtual newsgroups access for (ding) Gnus
-;; Copyright (C) 1994,95 Free Software Foundation, Inc.
+;;; nnvirtual.el --- virtual newsgroups access for Gnus
-;; Author: Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
-;; Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
+;; Copyright (C) 1994-2013 Free Software Foundation, Inc.
+
+;; Author: David Moore <dmoore@ucsd.edu>
+;; Lars Magne Ingebrigtsen <larsi@gnus.org>
+;; Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
;; Keywords: news
;; This file is part of GNU Emacs.
-;; GNU Emacs is free software; you can redistribute it and/or modify
+;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation; either version 2, or (at your option)
-;; any later version.
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
-;; along with GNU Emacs; see the file COPYING. If not, write to
-;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; The other access methods (nntp, nnspool, etc) are general news
-;; access methods. This module relies on Gnus and can not be used
+;; access methods. This module relies on Gnus and can not be used
;; separately.
;;; Code:
(require 'nntp)
(require 'nnheader)
(require 'gnus)
+(require 'nnoo)
+(require 'gnus-util)
+(require 'gnus-start)
+(require 'gnus-sum)
+(require 'gnus-msg)
+(eval-when-compile (require 'cl))
+
+(nnoo-declare nnvirtual)
+
+(defvoo nnvirtual-always-rescan t
+ "If non-nil, always scan groups for unread articles when entering a group.
+If this variable is nil and you read articles in a component group
+after the virtual group has been activated, the read articles from the
+component group will show up when you enter the virtual group.")
+
+(defvoo nnvirtual-component-regexp nil
+ "Regexp to match component groups.")
+
+(defvoo nnvirtual-component-groups nil
+ "Component group in this nnvirtual group.")
\f
-(defconst nnvirtual-version "nnvirtual 0.0"
- "Version number of this version of nnvirtual.")
+(defconst nnvirtual-version "nnvirtual 1.1")
+
+(defvoo nnvirtual-current-group nil)
+
+(defvoo nnvirtual-mapping-table nil
+ "Table of rules on how to map between component group and article number to virtual article number.")
+
+(defvoo nnvirtual-mapping-offsets nil
+ "Table indexed by component group to an offset to be applied to article numbers in that group.")
+
+(defvoo nnvirtual-mapping-len 0
+ "Number of articles in this virtual group.")
-(defvar nnvirtual-group-alist nil)
-(defvar nnvirtual-current-group nil)
-(defvar nnvirtual-current-groups nil)
-(defvar nnvirtual-current-mapping nil)
+(defvoo nnvirtual-mapping-reads nil
+ "Compressed sequence of read articles on the virtual group as computed from the unread status of individual component groups.")
-(defvar nnvirtual-do-not-open nil)
+(defvoo nnvirtual-mapping-marks nil
+ "Compressed marks alist for the virtual group as computed from the marks of individual component groups.")
-(defvar nnvirtual-status-string "")
+(defvoo nnvirtual-info-installed nil
+ "T if we have already installed the group info for this group, and shouldn't blast over it again.")
+
+(defvoo nnvirtual-status-string "")
+
+(autoload 'gnus-cache-articles-in-group "gnus-cache")
\f
;;; Interface functions.
-(defun nnvirtual-retrieve-headers (sequence &optional newsgroup server)
- "Retrieve the headers for the articles in SEQUENCE."
- (nnvirtual-possibly-change-newsgroups newsgroup server t)
- (save-excursion
- (set-buffer (get-buffer-create "*virtual headers*"))
- (buffer-disable-undo (current-buffer))
- (erase-buffer)
- (let ((map nnvirtual-current-mapping)
- (offset 0)
- articles beg group active top article result prefix)
- (while sequence
- (while (< (car (car map)) (car sequence))
- (setq offset (car (car map)))
- (setq map (cdr map)))
- (setq top (car (car map)))
- (setq group (nth 1 (car map)))
- (setq prefix (gnus-group-real-prefix group))
- (setq active (nth 2 (car map)))
- (setq articles nil)
- (while (and sequence (<= (car sequence) top))
- (setq articles (cons (- (+ active (car sequence)) offset) articles))
- (setq sequence (cdr sequence)))
- (setq articles (nreverse articles))
- (if (and articles
- (setq result (gnus-retrieve-headers articles group)))
- (save-excursion
+(nnoo-define-basics nnvirtual)
+
+
+(deffoo nnvirtual-retrieve-headers (articles &optional newsgroup
+ server fetch-old)
+ (when (nnvirtual-possibly-change-server server)
+ (with-current-buffer nntp-server-buffer
+ (erase-buffer)
+ (if (stringp (car articles))
+ 'headers
+ (let ((vbuf (nnheader-set-temp-buffer
+ (get-buffer-create " *virtual headers*")))
+ (carticles (nnvirtual-partition-sequence articles))
+ (system-name (system-name))
+ cgroup carticle article result prefix)
+ (while carticles
+ (setq cgroup (caar carticles))
+ (setq articles (cdar carticles))
+ (pop carticles)
+ (when (and articles
+ (gnus-check-server
+ (gnus-find-method-for-group cgroup) t)
+ (gnus-request-group cgroup t)
+ (setq prefix (gnus-group-real-prefix cgroup))
+ ;; FIX FIX FIX we want to check the cache!
+ ;; This is probably evil if people have set
+ ;; gnus-use-cache to nil themselves, but I
+ ;; have no way of finding the true value of it.
+ (let ((gnus-use-cache t))
+ (setq result (gnus-retrieve-headers
+ articles cgroup nil))))
(set-buffer nntp-server-buffer)
;; If we got HEAD headers, we convert them into NOV
- ;; headers. This is slow, inefficient and, come to think
- ;; of it, downright evil. So sue me. I couldn't be
+ ;; headers. This is slow, inefficient and, come to think
+ ;; of it, downright evil. So sue me. I couldn't be
;; bothered to write a header parse routine that could
;; parse a mixed HEAD/NOV buffer.
- (and (eq result 'headers) (nnvirtual-convert-headers))
+ (when (eq result 'headers)
+ (nnvirtual-convert-headers))
(goto-char (point-min))
(while (not (eobp))
- (setq beg (point))
- (setq article (read nntp-server-buffer))
- (delete-region beg (point))
- (insert (int-to-string (+ (- article active) offset)))
- (beginning-of-line)
- (looking-at "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
- (goto-char (match-end 0))
- (or (search-forward
- "\t" (save-excursion (end-of-line) (point)) t)
- (end-of-line))
- (while (= (char-after (1- (point))) ? )
- (forward-char -1)
- (delete-char 1))
- (if (eolp)
+ (delete-region (point)
+ (progn
+ (setq carticle (read nntp-server-buffer))
+ (point)))
+
+ ;; We remove this article from the articles list, if
+ ;; anything is left in the articles list after going through
+ ;; the entire buffer, then those articles have been
+ ;; expired or canceled, so we appropriately update the
+ ;; component group below. They should be coming up
+ ;; generally in order, so this shouldn't be slow.
+ (setq articles (delq carticle articles))
+
+ (setq article (nnvirtual-reverse-map-article cgroup carticle))
+ (if (null article)
+ ;; This line has no reverse mapping, that means it
+ ;; was an extra article reference returned by nntp.
(progn
- (end-of-line)
- (or (= (char-after (1- (point))) ?\t)
- (insert ?\t))
- (insert (format "Xref: %s %s:%d\t" (system-name)
- group article)))
- (if (not (string= "" prefix))
- (while (re-search-forward
- "[^ ]+:[0-9]+"
- (save-excursion (end-of-line) (point)) t)
- (save-excursion
- (goto-char (match-beginning 0))
- (insert prefix))))
- (end-of-line)
- (or (= (char-after (1- (point))) ?\t)
- (insert ?\t)))
- (forward-line 1))))
- (goto-char (point-max))
- (insert-buffer-substring nntp-server-buffer))
- ;; The headers are ready for reading, so they are inserted into
- ;; the nntp-server-buffer, which is where Gnus expects to find
- ;; them.
- (prog1
+ (beginning-of-line)
+ (delete-region (point) (progn (forward-line 1) (point))))
+ ;; Otherwise insert the virtual article number,
+ ;; and clean up the xrefs.
+ (princ article nntp-server-buffer)
+ (nnvirtual-update-xref-header cgroup carticle
+ prefix system-name)
+ (forward-line 1))
+ )
+
+ (set-buffer vbuf)
+ (goto-char (point-max))
+ (insert-buffer-substring nntp-server-buffer))
+ ;; Anything left in articles is expired or canceled.
+ ;; Could be smart and not tell it about articles already known?
+ (when articles
+ (gnus-group-make-articles-read cgroup articles))
+ )
+
+ ;; The headers are ready for reading, so they are inserted into
+ ;; the nntp-server-buffer, which is where Gnus expects to find
+ ;; them.
+ (prog1
+ (with-current-buffer nntp-server-buffer
+ (erase-buffer)
+ (insert-buffer-substring vbuf)
+ ;; FIX FIX FIX, we should be able to sort faster than
+ ;; this if needed, since each cgroup is sorted, we just
+ ;; need to merge
+ (sort-numeric-fields 1 (point-min) (point-max))
+ 'nov)
+ (kill-buffer vbuf)))))))
+
+
+(defvoo nnvirtual-last-accessed-component-group nil)
+
+(deffoo nnvirtual-request-article (article &optional group server buffer)
+ (when (nnvirtual-possibly-change-server server)
+ (if (stringp article)
+ ;; This is a fetch by Message-ID.
+ (cond
+ ((not nnvirtual-last-accessed-component-group)
+ (nnheader-report
+ 'nnvirtual "Don't know what server to request from"))
+ (t
(save-excursion
- (set-buffer nntp-server-buffer)
- (erase-buffer)
- (insert-buffer-substring "*virtual headers*")
- 'nov)
- (kill-buffer (current-buffer))))))
-
-(defun nnvirtual-open-server (newsgroups &optional something)
- "Open a virtual newsgroup that contains NEWSGROUPS."
- (nnheader-init-server-buffer))
-
-(defun nnvirtual-close-server (&rest dum)
- "Close news server."
+ (when buffer
+ (set-buffer buffer))
+ (let* ((gnus-override-method nil)
+ (gnus-command-method
+ (gnus-find-method-for-group
+ nnvirtual-last-accessed-component-group)))
+ (funcall (gnus-get-function gnus-command-method 'request-article)
+ article nil (nth 1 gnus-command-method) buffer)))))
+ ;; This is a fetch by number.
+ (let* ((amap (nnvirtual-map-article article))
+ (cgroup (car amap)))
+ (cond
+ ((not amap)
+ (nnheader-report 'nnvirtual "No such article: %s" article))
+ ((not (gnus-check-group cgroup))
+ (nnheader-report
+ 'nnvirtual "Can't open server where %s exists" cgroup))
+ ((not (gnus-request-group cgroup t))
+ (nnheader-report 'nnvirtual "Can't open component group %s" cgroup))
+ (t
+ (setq nnvirtual-last-accessed-component-group cgroup)
+ (if buffer
+ (with-current-buffer buffer
+ ;; We bind this here to avoid double decoding.
+ (let ((gnus-article-decode-hook nil))
+ (gnus-request-article-this-buffer (cdr amap) cgroup)))
+ (gnus-request-article (cdr amap) cgroup))))))))
+
+
+(deffoo nnvirtual-open-server (server &optional defs)
+ (unless (assq 'nnvirtual-component-regexp defs)
+ (push `(nnvirtual-component-regexp ,server)
+ defs))
+ (nnoo-change-server 'nnvirtual server defs)
+ (if nnvirtual-component-groups
+ t
+ (setq nnvirtual-mapping-table nil
+ nnvirtual-mapping-offsets nil
+ nnvirtual-mapping-len 0
+ nnvirtual-mapping-reads nil
+ nnvirtual-mapping-marks nil
+ nnvirtual-info-installed nil)
+ (when nnvirtual-component-regexp
+ ;; Go through the newsrc alist and find all component groups.
+ (let ((newsrc (cdr gnus-newsrc-alist))
+ group)
+ (while (setq group (car (pop newsrc)))
+ (when (string-match nnvirtual-component-regexp group) ; Match
+ ;; Add this group to the list of component groups.
+ (setq nnvirtual-component-groups
+ (cons group (delete group nnvirtual-component-groups)))))))
+ (if (not nnvirtual-component-groups)
+ (nnheader-report 'nnvirtual "No component groups: %s" server)
+ t)))
+
+
+(deffoo nnvirtual-request-group (group &optional server dont-check info)
+ (nnvirtual-possibly-change-server server)
+ (setq nnvirtual-component-groups
+ (delete (nnvirtual-current-group) nnvirtual-component-groups))
+ (cond
+ ((null nnvirtual-component-groups)
+ (setq nnvirtual-current-group nil)
+ (nnheader-report 'nnvirtual "No component groups in %s" group))
+ (t
+ (setq nnvirtual-current-group group)
+ (nnvirtual-create-mapping dont-check)
+ (when nnvirtual-always-rescan
+ (nnvirtual-request-update-info
+ (nnvirtual-current-group)
+ (gnus-get-info (nnvirtual-current-group))))
+ (nnheader-insert "211 %d 1 %d %s\n"
+ nnvirtual-mapping-len nnvirtual-mapping-len group))))
+
+
+(deffoo nnvirtual-request-type (group &optional article)
+ (if (not article)
+ 'unknown
+ (if (numberp article)
+ (let ((mart (nnvirtual-map-article article)))
+ (if mart
+ (gnus-request-type (car mart) (cdr mart))))
+ (gnus-request-type
+ nnvirtual-last-accessed-component-group nil))))
+
+(deffoo nnvirtual-request-update-mark (group article mark)
+ (let* ((nart (nnvirtual-map-article article))
+ (cgroup (car nart)))
+ (when (and nart
+ (memq mark gnus-auto-expirable-marks)
+ ;; The component group might be a virtual group.
+ (= mark (gnus-request-update-mark cgroup (cdr nart) mark))
+ (gnus-group-auto-expirable-p cgroup))
+ (setq mark gnus-expirable-mark)))
+ mark)
+
+
+(deffoo nnvirtual-close-group (group &optional server)
+ (when (and (nnvirtual-possibly-change-server server)
+ (not (gnus-ephemeral-group-p (nnvirtual-current-group))))
+ (nnvirtual-update-read-and-marked t t))
t)
-(defun nnvirtual-server-opened (&optional server)
- "Return server process status, T or NIL.
-If the stream is opened, return T, otherwise return NIL."
- (and nntp-server-buffer
- (get-buffer nntp-server-buffer)))
-
-(defun nnvirtual-status-message (&optional server)
- "Return server status response as string."
- nnvirtual-status-string)
-
-(defun nnvirtual-request-article (article &optional newsgroup server buffer)
- "Select article by message number."
- (nnvirtual-possibly-change-newsgroups newsgroup server t)
- (and (numberp article)
- (let ((map nnvirtual-current-mapping)
- (offset 0))
- (while (< (car (car map)) article)
- (setq offset (car (car map)))
- (setq map (cdr map)))
- (gnus-request-group (nth 1 (car map)) t)
- (gnus-request-article (- (+ (nth 2 (car map)) article) offset)
- (nth 1 (car map)) buffer))))
-
-(defun nnvirtual-request-group (group &optional server dont-check)
- "Make GROUP the current newsgroup."
- (nnvirtual-possibly-change-newsgroups group server dont-check)
- (if (not dont-check)
- (let ((map nnvirtual-current-mapping))
- (while (cdr map)
- (setq map (cdr map)))
- (save-excursion
- (set-buffer nntp-server-buffer)
- (erase-buffer)
- (insert (format "211 %d 1 %d %s\n" (car (car map))
- (car (car map)) group)))))
- t)
-
-(defun nnvirtual-close-group (group &optional server)
- (nnvirtual-possibly-change-newsgroups group server t)
- (nnvirtual-update-marked)
- (setq nnvirtual-current-group nil
- nnvirtual-current-groups nil
- nnvirtual-current-mapping nil)
- (setq nnvirtual-group-alist
- (delq (assoc group nnvirtual-group-alist) nnvirtual-group-alist)))
-
-(defun nnvirtual-request-list (&optional server)
- (setq nnvirtual-status-string "nnvirtual: LIST is not implemented.")
- nil)
-
-(defun nnvirtual-request-newgroups (date &optional server)
- "List new groups."
- (setq nnvirtual-status-string "NEWGROUPS is not supported.")
- nil)
-
-(defun nnvirtual-request-list-newsgroups (&optional server)
- (setq nnvirtual-status-string
- "nnvirtual: LIST NEWSGROUPS is not implemented.")
- nil)
-
-(fset 'nnvirtual-request-post 'nntp-request-post)
-
-(fset 'nnvirtual-request-post-buffer 'nntp-request-post-buffer)
+
+(deffoo nnvirtual-request-newgroups (date &optional server)
+ (nnheader-report 'nnvirtual "NEWGROUPS is not supported."))
+
+
+(deffoo nnvirtual-request-list-newsgroups (&optional server)
+ (nnheader-report 'nnvirtual "LIST NEWSGROUPS is not implemented."))
+
+
+(deffoo nnvirtual-request-update-info (group info &optional server)
+ (when (and (nnvirtual-possibly-change-server server)
+ (not nnvirtual-info-installed))
+ ;; Install the precomputed lists atomically, so the virtual group
+ ;; is not left in a half-way state in case of C-g.
+ (gnus-atomic-progn
+ (setcar (cddr info) nnvirtual-mapping-reads)
+ (if (nthcdr 3 info)
+ (setcar (nthcdr 3 info) nnvirtual-mapping-marks)
+ (when nnvirtual-mapping-marks
+ (setcdr (nthcdr 2 info) (list nnvirtual-mapping-marks))))
+ (setq nnvirtual-info-installed t))
+ t))
+
+
+(deffoo nnvirtual-catchup-group (group &optional server all)
+ (when (and (nnvirtual-possibly-change-server server)
+ (not (gnus-ephemeral-group-p (nnvirtual-current-group))))
+ ;; copy over existing marks first, in case they set anything
+ (nnvirtual-update-read-and-marked nil nil)
+ ;; do a catchup on all component groups
+ (let ((gnus-group-marked (copy-sequence nnvirtual-component-groups))
+ (gnus-expert-user t))
+ ;; Make sure all groups are activated.
+ (mapc
+ (lambda (g)
+ (when (not (numberp (gnus-group-unread g)))
+ (gnus-activate-group g)))
+ nnvirtual-component-groups)
+ (with-current-buffer gnus-group-buffer
+ (gnus-group-catchup-current nil all)))))
+
+
+(deffoo nnvirtual-find-group-art (group article)
+ "Return the real group and article for virtual GROUP and ARTICLE."
+ (nnvirtual-map-article article))
+
+
+(deffoo nnvirtual-request-post (&optional server)
+ (if (not gnus-message-group-art)
+ (nnheader-report 'nnvirtual "Can't post to an nnvirtual group")
+ (let ((group (car (nnvirtual-find-group-art
+ (car gnus-message-group-art)
+ &n