-;;;; nnvirtual.el --- virtual newsgroups access for (ding) Gnus
-;; Copyright (C) 1994,95 Free Software Foundation, Inc.
+;;; nnvirtual.el --- virtual newsgroups access for Gnus
+;; Copyright (C) 1994,95,96,97 Free Software Foundation, Inc.
-;; Author: Lars Ingebrigtsen <larsi@ifi.uio.no>
+;; Author: David Moore <dmoore@ucsd.edu>
+;; Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
;; Masanobu UMEDA <umerin@flab.flab.fujitsu.junet>
;; Keywords: news
;; 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; see the file COPYING. If not, write to the
+;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+;; Boston, MA 02111-1307, USA.
;;; 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)
+(eval-when-compile (require 'cl))
+
+(nnoo-declare nnvirtual)
+
+(defvoo nnvirtual-always-rescan nil
+ "*If non-nil, always scan groups for unread articles when entering a group.
+If this variable is nil (which is the default) 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")
-(defvar nnvirtual-group-alist nil)
-(defvar nnvirtual-current-group nil)
-(defvar nnvirtual-current-groups nil)
-(defvar nnvirtual-current-mapping nil)
+(defvoo nnvirtual-current-group nil)
-(defvar nnvirtual-do-not-open nil)
+(defvoo nnvirtual-mapping-table nil
+ "Table of rules on how to map between component group and article number
+to virtual article number.")
-(defvar nnvirtual-status-string "")
+(defvoo nnvirtual-mapping-offsets nil
+ "Table indexed by component group to an offset to be applied to article numbers in that group.")
-\f
+(defvoo nnvirtual-mapping-len 0
+ "Number of articles in this virtual group.")
-;;; Interface functions.
+(defvoo nnvirtual-mapping-reads nil
+ "Compressed sequence of read articles on the virtual group as computed from the unread status of individual component groups.")
-(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
- (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
- ;; bothered to write a header parse routine that could
- ;; parse a mixed HEAD/NOV buffer.
- (and (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)))
- (end-of-line)
- (setq beg (point))
- (search-backward "\t")
- (if (not (search-forward "Xref:" beg t))
- (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)))))
- (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
- (save-excursion
- (set-buffer nntp-server-buffer)
- (erase-buffer)
- (insert-buffer-substring "*virtual headers*")
- 'nov)
- (kill-buffer (current-buffer))))))
+(defvoo nnvirtual-mapping-marks nil
+ "Compressed marks alist for the virtual group as computed from the marks of individual component groups.")
-(defun nnvirtual-open-server (newsgroups &optional something)
- "Open a virtual newsgroup that contains NEWSGROUPS."
- (nnheader-init-server-buffer))
+(defvoo nnvirtual-info-installed nil
+ "T if we have already installed the group info for this group, and shouldn't blast over it again.")
-(defun nnvirtual-close-server (&rest dum)
- "Close news server."
- t)
+(defvoo nnvirtual-status-string "")
-(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 ()
- "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)))
+(eval-and-compile
+ (autoload 'gnus-cache-articles-in-group "gnus-cache"))
-(defun nnvirtual-request-list (&optional server)
- (setq nnvirtual-status-string "nnvirtual: LIST is not implemented.")
- nil)
+\f
+
+;;; Interface functions.
+
+(nnoo-define-basics nnvirtual)
-(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)
+(deffoo nnvirtual-retrieve-headers (articles &optional newsgroup
+ server fetch-old)
+ (when (nnvirtual-possibly-change-server server)
+ (save-excursion
+ (set-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
+ ;; bothered to write a header parse routine that could
+ ;; parse a mixed HEAD/NOV buffer.
+ (when (eq result 'headers)
+ (nnvirtual-convert-headers))
+ (goto-char (point-min))
+ (while (not (eobp))
+ (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
+ (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
+ (save-excursion
+ (set-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
+ (when buffer
+ (set-buffer buffer))
+ (let ((method (gnus-find-method-for-group
+ nnvirtual-last-accessed-component-group)))
+ (funcall (gnus-get-function method 'request-article)
+ article nil (nth 1 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
+ (save-excursion
+ (set-buffer buffer)
+ (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)
+ (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
+ (when (or (not dont-check)
+ nnvirtual-always-rescan)
+ (nnvirtual-create-mapping))
+ (setq nnvirtual-current-group 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
+ (let ((mart (nnvirtual-map-article article)))
+ (when mart
+ (gnus-request-type (car mart) (cdr mart))))))
+
+(deffoo nnvirtual-request-update-mark (group article mark)
+ (let* ((nart (nnvirtual-map-article article))
+ (cgroup (car nart))
+ ;; The component group might be a virtual group.
+ (nmark (gnus-request-update-mark cgroup (cdr nart) mark)))
+ (when (and nart
+ (= mark nmark)
+ (gnus-group-auto-expirable-p cgroup))
+ (setq mark gnus-expirable-mark)))
+ mark)
-(fset 'nnvirtual-request-post 'nntp-request-post)
+
+(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)
+
-(fset 'nnvirtual-request-post-buffer 'nntp-request-post-buffer)
+(deffoo nnvirtual-request-list (&optional server)
+ (nnheader-report 'nnvirtual "LIST is not implemented."))
+
+
+(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.
+ (mapcar
+ (lambda (g)
+ (when (not (numberp (car (gnus-gethash g gnus-newsrc-hashtb))))
+ (gnus-activate-group g)))
+ nnvirtual-component-groups)
+ (save-excursion
+ (set-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))
\f
;;; Internal functions.
-;; Convert HEAD headers into NOV headers.
(defun nnvirtual-convert-headers ()
+ "Convert HEAD headers into NOV headers."
(save-excursion
(set-buffer nntp-server-buffer)
- (let* ((gnus-newsgroup-dependencies (make-vector 100 0))
- (headers (gnus-get-newsgroup-headers))
+ (let* ((dependencies (make-vector 100 0))
+ (headers (gnus-get-newsgroup-headers dependencies))
header)
(erase-buffer)
- (while headers
- (setq header (car headers)
- headers (cdr headers))
- (insert (int-to-string (header-number header)) "\t"
- (or (header-subject header) "") "\t"
- (or (header-from header) "") "\t"
- (or (header-date header) "") "\t"
- (or (header-id header) "") "\t"
- (or (header-references header) "") "\t"
- (int-to-string (or (header-chars header) 0)) "\t"
- (int-to-string (or (header-lines header) 0)) "\t"
- (or (header-xref header) "") "\n")))))
-
-(defun nnvirtual-possibly-change-newsgroups (group regexp &optional check)
- (let ((inf t))
- (or (not group)
- (and nnvirtual-current-group
- (string= group nnvirtual-current-group))
- (and (setq inf (assoc group nnvirtual-group-alist))
- (string= (nth 3 inf) regexp)
- (progn
- (setq nnvirtual-current-group (car inf))
- (setq nnvirtual-current-groups (nth 1 inf))
- (setq nnvirtual-current-mapping (nth 2 inf)))))
- (if (or (not check) (not inf))
- (progn
- (and inf (setq nnvirtual-group-alist
- (delq inf nnvirtual-group-alist)))
- (setq nnvirtual-current-mapping nil)
- (setq nnvirtual-current-group group)
- (let ((newsrc gnus-newsrc-assoc))
- (setq nnvirtual-current-groups nil)
- (while newsrc
- (and (string-match regexp (car (car newsrc)))
- (setq nnvirtual-current-groups
- (cons (car (car newsrc)) nnvirtual-current-groups)))
- (setq newsrc (cdr newsrc))))
- (if nnvirtual-current-groups
- (progn
- (nnvirtual-create-mapping group)
- (setq nnvirtual-group-alist
- (cons (list group nnvirtual-current-groups
- nnvirtual-current-mapping regexp)
- nnvirtual-group-alist)))
- (setq nnvirtual-status-string
- (format
- "nnvirtual: No newsgroups for this virtual newsgroup"))))))
- nnvirtual-current-groups)
-
-(defun nnvirtual-create-mapping (group)
- (let* ((group (gnus-group-prefixed-name group (list 'nnvirtual "")))
- (info (nth 2 (gnus-gethash group gnus-newsrc-hashtb)))
- (groups nnvirtual-current-groups)
- (i 1)
- (total 0)
- (offset 0)
- reads unread igroup itotal itreads ireads)
- ;; The virtual group doesn't exist. (?)
- (or info (error "No such group: %s" group))
- (setq nnvirtual-current-mapping nil)
- (while groups
- ;; Added by Sudish Joseph <joseph@cis.ohio-state.edu>.
- (setq igroup (car groups))
- (let ((info (nth 2 (gnus-gethash igroup gnus-newsrc-hashtb)))
- (active (gnus-gethash igroup gnus-active-hashtb)))
- ;; See if the group has had its active list read this session
- ;; if not, we do it now.
- (if (null active)
- (if (gnus-activate-newsgroup igroup)
- (gnus-get-unread-articles-in-group
- info (gnus-gethash igroup gnus-active-hashtb))
- (message "Couldn't request newsgroup %s" group)
- (ding)))
- (setq itotal (1+ (- (cdr active) (car active))))
- (if (setq ireads (nth 2 info))
- (let ((itreads
- (if (atom (car ireads))
- (setq ireads (list (cons (car ireads) (cdr ireads))))
- (setq ireads (copy-alist ireads)))))
- (if (< (cdr (car ireads)) (car active))
- (setq ireads (setq itreads (cdr ireads))))
- (if (< (car (car ireads)) (car active))
- (setcar (car ireads) (1+ (car active))))
- (while itreads
- (setcar (car itreads)
- (+ (- (car (car itreads)) (car active)) offset))
- (setcdr (car itreads)
- (+ (- (cdr (car itreads)) (car active)) offset))
- (setq itreads (cdr itreads)))
- (setq reads (nconc reads ireads))))
- (setq offset (+ offset (1- itotal)))
- (setq nnvirtual-current-mapping
- (cons (list offset igroup (car active))
- nnvirtual-current-mapping))
- (setq groups (cdr groups))))
- (setq nnvirtual-current-mapping
- (nreverse nnvirtual-current-mapping))
- (gnus-sethash group (cons 1 offset) gnus-active-hashtb)
- (setcar (nthcdr 2 info) reads)
-
- ;; Then we deal with the marks.
- (let ((map nnvirtual-current-mapping)
- (marks '(tick dormant reply expire score))
- (offset 0)
- tick dormant reply expire score marked active)
- (while map
- (setq igroup (nth 1 (car map)))
- (setq active (nth 2 (car map)))
- (setq marked (nth 3 (nth 2 (gnus-gethash igroup gnus-newsrc-hashtb))))
- (let ((m marks))
- (while m
- (and (assq (car m) marked)
- (set (car m)
- (nconc (mapcar
- (lambda (art)
- (if (numberp art)
- (+ (- art active) offset)
- (cons (+ (- (car art) active) offset)
- (cdr art))))
- (cdr (assq (car m) marked)))
- (symbol-value (car m)))))
- (setq m (cdr m))))
- (setq offset (car (car map)))
- (setq map (cdr map)))
- ;; Put the list of marked articles in the info of the virtual group.
- (let ((m marks)
- marked)
- (while m
- (and (symbol-value (car m))
- (setq marked (cons (cons (car m) (symbol-value (car m)))
- marked)))
- (setq m (cdr m)))
- (if (nthcdr 3 info)
- (setcar (nthcdr 3 info) marked)
- (setcdr (nthcdr 2 info) (list marked)))))))
-
-(defun nnvirtual-update-marked ()
- (let ((mark-lists '((gnus-newsgroup-marked . tick)
- (gnus-newsgroup-dormant . dormant)
- (gnus-newsgroup-expirable . expire)
- (gnus-newsgroup-replied . reply)))
- marks art-group group-alist g)
- (while mark-lists
- (setq marks (symbol-value (car (car mark-lists))))
- (while marks
- (setq art-group (nnvirtual-art-group (car marks)))
- (if (setq g (assoc (car art-group) group-alist))
- (nconc g (list (cdr art-group)))
- (setq group-alist (cons (list (car art-group) (cdr art-group))
- group-alist)))
- (setq marks (cdr marks)))
- (while group-alist
- (gnus-add-marked-articles
- (car (car group-alist)) (cdr (car mark-lists))
- (cdr (car group-alist)) nil t)
- (gnus-group-update-group (car (car group-alist)))
- (setq group-alist (cdr group-alist)))
- (setq mark-lists (cdr mark-lists)))))
-
-(defun nnvirtual-art-group (article)
- (let ((map nnvirtual-current-mapping)
- (offset 0))
- (while (< (car (car map)) (if (numberp article) article (car article)))
- (setq offset (car (car map))
- map (cdr map)))
- (cons (nth 1 (car map))
- (if (numberp article)
- (- (+ article (nth 2 (car map))) offset)
- (cons (- (+ (car article) (nth 2 (car map))) offset)
- (cdr article))))))
+ (while (setq header (pop headers))
+ (nnheader-insert-nov header)))))
+
+
+(defun nnvirtual-update-xref-header (group article prefix system-name)
+ "Edit current NOV header in current buffer to have an xref to the component group, and also server prefix any existing xref lines."
+ ;; Move to beginning of Xref field, creating a slot if needed.
+ (beginning-of-line)
+ (looking-at
+ "[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t[^\t]*\t")
+ (goto-char (match-end 0))
+ (unless (search-forward "\t" (gnus-point-at-eol) 'move)
+ (insert "\t"))
+
+ ;; Remove any spaces at the beginning of the Xref field.
+ (while (= (char-after (1- (point))) ? )
+ (forward-char -1)
+ (delete-char 1))
+
+ (insert "Xref: " system-name " " group ":")
+ (princ article (current-buffer))
+
+ ;; If there were existing xref lines, clean them up to have the correct
+ ;; component server prefix.
+ (let ((xref-end (save-excursion
+ (search-forward "\t" (gnus-point-at-eol) 'move)
+ (point)))
+ (len (length prefix)))
+ (unless (= (point) xref-end)
+ (insert " ")
+ (when (not (string= "" prefix))
+ (while (re-search-forward "[^ ]+:[0-9]+" xref-end t)
+ (save-excursion
+ (goto-char (match-beginning 0))
+ (insert prefix))
+ (setq xref-end (+ xref-end len)))
+ )))
+
+ ;; Ensure a trailing \t.
+ (end-of-line)
+ (or (= (char-after (1- (point))) ?\t)
+ (insert ?\t)))
+
+
+(defun nnvirtual-possibly-change-server (server)
+ (or (not server)
+ (nnoo-current-server-p 'nnvirtual server)
+ (nnvirtual-open-server server)))
+
+
+(defun nnvirtual-update-read-and-marked (read-p update-p)
+ "Copy marks from the virtual group to the component groups.
+If READ-P is not nil, update the (un)read status of the components.
+If UPDATE-P is not nil, call gnus-group-update-group on the components."
+ (when nnvirtual-current-group
+ (let ((unreads (and read-p
+ (nnvirtual-partition-sequence
+ (gnus-list-of-unread-articles
+ (nnvirtual-current-group)))))
+ (type-marks (mapcar (lambda (ml)
+ (cons (car ml)
+ (nnvirtual-partition-sequence (cdr ml))))
+ (gnus-info-marks (gnus-get-info
+ (nnvirtual-current-group)))))
+ mark type groups carticles info entry)
+
+ ;; Ok, atomically move all of the (un)read info, clear any old
+ ;; marks, and move all of the current marks. This way if someone
+ ;; hits C-g, you won't leave the component groups in a half-way state.
+ (gnus-atomic-progn
+ ;; move (un)read
+ (let ((gnus-newsgroup-active nil)) ;workaround guns-update-read-articles
+ (while (setq entry (pop unreads))
+ (gnus-update-read-articles (car entry) (cdr entry))))
+
+ ;; clear all existing marks on the component groups
+ (setq groups nnvirtual-component-groups)
+ (while groups
+ (when (and (setq info (gnus-get-info (pop groups)))
+ (gnus-info-marks info))
+ (gnus-info-set-marks info nil)))
+
+ ;; Ok, currently type-marks is an assq list with keys of a mark type,
+ ;; with data of an assq list with keys of component group names
+ ;; and the articles which correspond to that key/group pair.
+ (while (setq mark (pop type-marks))
+ (setq type (car mark))
+ (setq groups (cdr mark))
+ (while (setq carticles (pop groups))
+ (gnus-add-marked-articles (car carticles) type (cdr carticles)
+ nil t))))
+
+ ;; possibly update the display, it is really slow
+ (when update-p
+ (setq groups nnvirtual-component-groups)
+ (while groups
+ (gnus-group-update-group (pop groups) t))))))
+
+
+(defun nnvirtual-current-group ()
+ "Return the prefixed name of the current nnvirtual group."
+ (concat "nnvirtual:" nnvirtual-current-group))
+
+
+
+;;; This is currently O(kn^2) to merge n lists of length k.
+;;; You could do it in O(knlogn), but we have a small n, and the
+;;; overhead of the other approach is probably greater.
+(defun nnvirtual-merge-sorted-lists (&rest lists)
+ "Merge many sorted lists of numbers."
+ (if (null (cdr lists))
+ (car lists)
+ (apply 'nnvirtual-merge-sorted-lists
+ (merge 'list (car lists) (cadr lists) '<)
+ (cddr lists))))
+
+
+
+;;; We map between virtual articles and real articles in a manner
+;;; which keeps the size of the virtual active list the same as
+;;; the sum of the component active lists.
+;;; To achieve fair mixing of the groups, the last article in
+;;; each of N component groups will be in the the last N articles
+;;; in the virtual group.
+
+;;; If you have 3 components A, B and C, with articles 1-8, 1-5, and 6-7
+;;; resprectively, then the virtual article numbers look like:
+;;;
+;;; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+;;; A1 A2 A3 A4 B1 A5 B2 A6 B3 A7 B4 C6 A8 B5 C7
+
+;;; To compute these mappings we generate a couple tables and then
+;;; do some fast operations on them. Tables for the example above:
+;;;
+;;; Offsets - [(A 0) (B -3) (C -1)]
+;;;
+;;; a b c d e
+;;; Mapping - ([ 3 0 1 3 0 ]
+;;; [ 6 3 2 9 3 ]
+;;; [ 8 6 3 15 9 ])
+;;;
+;;; (note column 'e' is different in real algorithm, which is slightly
+;;; different than described here, but this gives you the methodology.)
+;;;
+;;; The basic idea is this, when going from component->virtual, apply
+;;; the appropriate offset to the article number. Then search the first
+;;; column of the table for a row where 'a' is less than or equal to the
+;;; modified number. You can see that only group A can therefore go to
+;;; the first row, groups A and B to the second, and all to the last.
+;;; The third column of the table is telling us the number of groups
+;;; which might be able to reach that row (it might increase by more than
+;;; 1 if several groups have the same size).
+;;; Then column 'b' provides an additional offset you apply when you have
+;;; found the correct row. You then multiply by 'c' and add on the groups
+;;; _position_ in the offset table. The basic idea here is that on
+;;; any given row we are going to map back and forth using X'=X*c+Y and
+;;; X=(X'/c), Y=(X' mod c). Then once you've done this transformation,
+;;; you apply a final offset from column 'e' to give the virtual article.
+;;;
+;;; Going the other direction, you instead search on column 'd' instead
+;;; of 'a', and apply everything in reverse order.
+
+;;; Convert component -> virtual:
+;;; set num = num - Offset(group)
+;;; find first row in Mapping where num <= 'a'
+;;; num = (num-'b')*c + Position(group) + 'e'
+
+;;; Convert virtual -> component:
+;;; find first row in Mapping where num <= 'd'
+;;; num = num - 'e'
+;;; group_pos = num mod 'c'
+;;; num = (num / 'c') + 'b' + Offset(group_pos)
+
+;;; Easy no? :)
+;;;
+;;; Well actually, you need to keep column e offset smaller by the 'c'
+;;; column for that line, and always add 1 more when going from
+;;; component -> virtual. Otherwise you run into a problem with
+;;; unique reverse mapping.
+
+(defun nnvirtual-map-article (article)
+ "Return a cons of the component group and article corresponding to the given virtual ARTICLE."
+ (let ((table nnvirtual-mapping-table)
+ entry group-pos)
+ (while (and table
+ (> article (aref (car table) 3)))
+ (setq table (cdr table)))
+ (when (and table
+ (> article 0))
+ (setq entry (car table))
+ (setq article (- article (aref entry 4) 1))
+ (setq group-pos (mod article (aref entry 2)))
+ (cons (car (aref nnvirtual-mapping-offsets group-pos))
+ (+ (/ article (aref entry 2))
+ (aref entry 1)
+ (cdr (aref nnvirtual-mapping-offsets group-pos)))
+ ))
+ ))
+
+
+
+(defun nnvirtual-reverse-map-article (group article)
+ "Return the virtual article number corresponding to the given component GROUP and ARTICLE."
+ (let ((table nnvirtual-mapping-table)
+ (group-pos 0)
+ entry)
+ (while (not (string= group (car (aref nnvirtual-mapping-offsets
+ group-pos))))
+ (setq group-pos (1+ group-pos)))
+ (setq article (- article (cdr (aref nnvirtual-mapping-offsets
+ group-pos))))
+ (while (and table
+ (> article (aref (car table) 0)))
+ (setq table (cdr table)))
+ (setq entry (car table))
+ (when (and entry
+ (> article 0)
+ (< group-pos (aref entry 2))) ; article not out of range below
+ (+ (aref entry 4)
+ group-pos
+ (* (- article (aref entry 1))
+ (aref entry 2))
+ 1))
+ ))
+
+
+(defun nnvirtual-reverse-map-sequence (group articles)
+ "Return list of virtual article numbers for all ARTICLES in GROUP.
+The ARTICLES should be sorted, and can be a compressed sequence.
+If any of the article numbers has no corresponding virtual article,
+then it is left out of the result."
+ (when (numberp (cdr-safe articles))
+ (setq articles (list articles)))
+ (let (result a i j new-a)
+ (while (setq a (pop articles))
+ (if (atom a)
+ (setq i a
+ j a)
+ (setq i (car a)
+ j (cdr a)))
+ (while (<= i j)
+ ;; If this is slow, you can optimize by moving article checking
+ ;; into here. You don't have to recompute the group-pos,
+ ;; nor scan the table every time.
+ (when (setq new-a (nnvirtual-reverse-map-article group i))
+ (push new-a result))
+ (setq i (1+ i))))
+ (nreverse result)))
+
+
+(defun nnvirtual-partition-sequence (articles)
+ "Return an association list of component article numbers.
+These are indexed by elements of nnvirtual-component-groups, based on
+the sequence ARTICLES of virtual article numbers. ARTICLES should be
+sorted, and can be a compressed sequence. If any of the article
+numbers has no corresponding component article, then it is left out of
+the result."
+ (when (numberp (cdr-safe articles))
+ (setq articles (list articles)))
+ (let ((carticles (mapcar (lambda (g) (list g))
+ nnvirtual-component-groups))
+ a i j article entry)
+ (while (setq a (pop articles))
+ (if (atom a)
+ (setq i a
+ j a)
+ (setq i (car a)
+ j (cdr a)))
+ (while (<= i j)
+ (when (setq article (nnvirtual-map-article i))
+ (setq entry (assoc (car article) carticles))
+ (setcdr entry (cons (cdr article) (cdr entry))))
+ (setq i (1+ i))))
+ (mapc '(lambda (x) (setcdr x (nreverse (cdr x))))
+ carticles)
+ carticles))
+
+
+(defun nnvirtual-create-mapping ()
+ "Build the tables necessary to map between component (group, article) to virtual article.
+Generate the set of read messages and marks for the virtual group
+based on the marks on the component groups."
+ (let ((cnt 0)
+ (tot 0)
+ (M 0)
+ (i 0)
+ actives all-unreads all-marks
+ active min max size unreads marks
+ next-M next-tot
+ reads beg)
+ ;; Ok, we loop over all component groups and collect a lot of
+ ;; information:
+ ;; Into actives we place (g size max), where size is max-min+1.
+ ;; Into all-unreads we put (g unreads).
+ ;; Into all-marks we put (g marks).
+ ;; We also increment cnt and tot here, and compute M (max of sizes).
+ (mapc (lambda (g)
+ (setq active (gnus-activate-group g)
+ min (car active)
+ max (cdr active))
+ (when (and active (>= max min) (not (zerop max)))
+ ;; store active information
+ (push (list g (- max min -1) max) actives)
+ ;; collect unread/mark info for later
+ (setq unreads (gnus-list-of-unread-articles g))
+ (setq marks (gnus-info-marks (gnus-get-info g)))
+ (when gnus-use-cache
+ (push (cons 'cache
+ (gnus-cache-articles-in-group g))
+ marks))
+ (push (cons g unreads) all-unreads)
+ (push (cons g marks) all-marks)
+ ;; count groups, total #articles, and max size
+ (setq size (- max min -1))
+ (setq cnt (1+ cnt)
+ tot (+ tot size)
+ M (max M size))))
+ nnvirtual-component-groups)
+
+ ;; Number of articles in the virtual group.
+ (setq nnvirtual-mapping-len tot)
+
+
+ ;; We want the actives list sorted by size, to build the tables.
+ (setq actives (sort actives (lambda (g1 g2) (< (nth 1 g1) (nth 1 g2)))))
+
+ ;; Build the offset table. Largest sized groups are at the front.
+ (setq nnvirtual-mapping-offsets
+ (vconcat
+ (nreverse
+ (mapcar (lambda (entry)
+ (cons (nth 0 entry)
+ (- (nth 2 entry) M)))
+ actives))))
+
+ ;; Build the mapping table.
+ (setq nnvirtual-mapping-table nil)
+ (setq actives (mapcar (lambda (entry) (nth 1 entry)) actives))
+ (while actives
+ (setq size (car actives))
+ (setq next-M (- M size))
+ (setq next-tot (- tot (* cnt size)))
+ ;; make current row in table
+ (push (vector M next-M cnt tot (- next-tot cnt))
+ nnvirtual-mapping-table)
+ ;; update M and tot
+ (setq M next-M)
+ (setq tot next-tot)
+ ;; subtract the current size from all entries.
+ (setq actives (mapcar (lambda (x) (- x size)) actives))
+ ;; remove anything that went to 0.
+ (while (and actives
+ (= (car actives) 0))
+ (pop actives)
+ (setq cnt (- cnt 1))))
+
+
+ ;; Now that the mapping tables are generated, we can convert
+ ;; and combine the separate component unreads and marks lists
+ ;; into single lists of virtual article numbers.
+ (setq unreads (apply 'nnvirtual-merge-sorted-lists
+ (mapcar (lambda (x)
+ (nnvirtual-reverse-map-sequence
+ (car x) (cdr x)))
+ all-unreads)))
+ (setq marks (mapcar
+ (lambda (type)
+ (cons (cdr type)
+ (gnus-compress-sequence
+ (apply
+ 'nnvirtual-merge-sorted-lists
+ (mapcar (lambda (x)
+ (nnvirtual-reverse-map-sequence
+ (car x)
+ (cdr (assq (cdr type) (cdr x)))))
+ all-marks)))))
+ gnus-article-mark-lists))
+
+ ;; Remove any empty marks lists, and store.
+ (setq nnvirtual-mapping-marks (delete-if-not 'cdr marks))
+
+ ;; We need to convert the unreads to reads. We compress the
+ ;; sequence as we go, otherwise it could be huge.
+ (while (and (<= (incf i) nnvirtual-mapping-len)
+ unreads)
+ (if (= i (car unreads))
+ (setq unreads (cdr unreads))
+ ;; try to get a range.
+ (setq beg i)
+ (while (and (<= (incf i) nnvirtual-mapping-len)
+ (not (= i (car unreads)))))
+ (setq i (- i 1))
+ (if (= i beg)
+ (push i reads)
+ (push (cons beg i) reads))
+ ))
+ (when (<= i nnvirtual-mapping-len)
+ (if (= i nnvirtual-mapping-len)
+ (push i reads)
+ (push (cons i nnvirtual-mapping-len) reads)))
+
+ ;; Store the reads list for later use.
+ (setq nnvirtual-mapping-reads (nreverse reads))
+
+ ;; Throw flag to show we changed the info.
+ (setq nnvirtual-info-installed nil)
+ ))
(provide 'nnvirtual)