1 ;;; nnshimbun.el --- interfacing with web newspapers -*- coding: junet; -*-
3 ;; Authors: TSUCHIYA Masatoshi <tsuchiya@namazu.org>,
4 ;; Akihiro Arisawa <ari@atesoft.advantest.co.jp>,
5 ;; Katsumi Yamaoka <yamaoka@jpl.org>,
6 ;; Yuuichi Teranishi <teranisi@gohome.org>
11 ;; This file is a part of Semi-Gnus.
13 ;; This program is free software; you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation; either version 2, or (at your option)
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program; if not, you can either send email to this
25 ;; program's maintainer or write to: The Free Software Foundation,
26 ;; Inc.; 59 Temple Place, Suite 330; Boston, MA 02111-1307, USA.
30 ;; Gnus (or gnus) backend to read newspapers on the World Wide Web.
31 ;; This module requires the Emacs-W3M and the external command W3M.
32 ;; Visit the following pages for more information.
34 ;; http://namazu.org/~tsuchiya/emacs-w3m/
35 ;; http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/
37 ;; If you would like to use this module in Gnus (not T-gnus), put this
38 ;; file into the lisp/ directory in the Gnus source tree and run
39 ;; `make install'. And then, copy the function definition of
40 ;; `gnus-group-make-shimbun-group' from the file gnus-group.el of
41 ;; T-gnus to somewhere else, for example .gnus file as follows:
43 ;;(eval-after-load "gnus-group"
44 ;; '(if (not (fboundp 'gnus-group-make-shimbun-group))
45 ;; (defun gnus-group-make-shimbun-group ()
46 ;; "Create a nnshimbun group."
47 ;; [...a function definition...])))
51 (gnus-declare-backend "nnshimbun" 'address)
53 (eval-when-compile (require 'cl))
63 (nnoo-declare nnshimbun)
65 (defvoo nnshimbun-directory (nnheader-concat gnus-directory "shimbun/")
66 "Where nnshimbun will save its files.")
68 (defvoo nnshimbun-nov-is-evil nil
69 "*Non-nil means that nnshimbun will never retrieve NOV headers.")
71 (defvoo nnshimbun-nov-file-name ".overview")
73 (defvoo nnshimbun-pre-fetch-article nil
74 "*Non nil means that nnshimbun fetch unread articles when scanning groups.")
76 (defvoo nnshimbun-use-entire-index t
77 "*Nil means that nnshimbun check the last index of articles.")
79 ;; set by nnshimbun-possibly-change-group
80 (defvoo nnshimbun-buffer nil)
81 (defvoo nnshimbun-current-directory nil)
82 (defvoo nnshimbun-current-group nil)
84 ;; set by nnshimbun-open-server
85 (defvoo nnshimbun-shimbun nil)
86 (defvoo nnshimbun-server-directory nil)
88 (defvoo nnshimbun-status-string "")
89 (defvoo nnshimbun-nov-last-check nil)
90 (defvoo nnshimbun-nov-buffer-alist nil)
91 (defvoo nnshimbun-nov-buffer-file-name nil)
93 (defvoo nnshimbun-keep-backlog 300)
94 (defvoo nnshimbun-backlog-articles nil)
95 (defvoo nnshimbun-backlog-hashtb nil)
98 (defmacro nnshimbun-backlog (&rest form)
99 `(let ((gnus-keep-backlog nnshimbun-keep-backlog)
100 (gnus-backlog-buffer (format " *nnshimbun backlog %s*" (nnoo-current-server 'nnshimbun)))
101 (gnus-backlog-articles nnshimbun-backlog-articles)
102 (gnus-backlog-hashtb nnshimbun-backlog-hashtb))
105 (setq nnshimbun-backlog-articles gnus-backlog-articles
106 nnshimbun-backlog-hashtb gnus-backlog-hashtb))))
107 (put 'nnshimbun-backlog 'lisp-indent-function 0)
108 (put 'nnshimbun-backlog 'edebug-form-spec '(form body))
111 ;;; Interface Functions
112 (nnoo-define-basics nnshimbun)
114 (deffoo nnshimbun-open-server (server &optional defs)
115 (push (list 'nnshimbun-shimbun
117 (shimbun-open server (luna-make-entity 'shimbun-gnus-mua))
118 (error (nnheader-report 'nnshimbun "%s" (error-message-string err)))))
120 ;; Set directory for server working files.
121 (push (list 'nnshimbun-server-directory
122 (file-name-as-directory
123 (expand-file-name server nnshimbun-directory)))
125 (nnoo-change-server 'nnshimbun server defs)
126 (nnshimbun-possibly-change-group nil server)
128 (unless (file-exists-p nnshimbun-directory)
129 (ignore-errors (make-directory nnshimbun-directory t)))
131 ((not (file-exists-p nnshimbun-directory))
132 (nnshimbun-close-server)
133 (nnheader-report 'nnshimbun "Couldn't create directory: %s" nnshimbun-directory))
134 ((not (file-directory-p (file-truename nnshimbun-directory)))
135 (nnshimbun-close-server)
136 (nnheader-report 'nnshimbun "Not a directory: %s" nnshimbun-directory))
138 (unless (file-exists-p nnshimbun-server-directory)
139 (ignore-errors (make-directory nnshimbun-server-directory t)))
141 ((not (file-exists-p nnshimbun-server-directory))
142 (nnshimbun-close-server)
143 (nnheader-report 'nnshimbun "Couldn't create directory: %s" nnshimbun-server-directory))
144 ((not (file-directory-p (file-truename nnshimbun-server-directory)))
145 (nnshimbun-close-server)
146 (nnheader-report 'nnshimbun "Not a directory: %s" nnshimbun-server-directory))
148 (nnheader-report 'nnshimbun "Opened server %s using directory %s"
149 server nnshimbun-server-directory)
152 (deffoo nnshimbun-close-server (&optional server)
153 (shimbun-close nnshimbun-shimbun)
154 (and (nnshimbun-server-opened server)
155 (gnus-buffer-live-p nnshimbun-buffer)
156 (kill-buffer nnshimbun-buffer))
157 (nnshimbun-backlog (gnus-backlog-shutdown))
159 (nnoo-close-server 'nnshimbun server)
165 (let ((gnus (locate-library "gnus"))
166 ;; Gnus has mailcap.el in the same directory of gnus.el.
167 (mailcap (locate-library "mailcap")))
169 (string-equal (file-name-directory gnus)
170 (file-name-directory mailcap)))))))
173 (defmacro nnshimbun-mail-header-subject (header)
174 `(mail-header-subject ,header))
175 (defmacro nnshimbun-mail-header-from (header)
176 `(mail-header-from ,header)))
177 (defmacro nnshimbun-mail-header-subject (header)
178 `(mime-entity-fetch-field ,header 'Subject))
179 (defmacro nnshimbun-mail-header-from (header)
180 `(mime-entity-fetch-field ,header 'From)))))
182 (defun nnshimbun-make-shimbun-header (header)
184 (mail-header-number header)
185 (nnshimbun-mail-header-subject header)
186 (nnshimbun-mail-header-from header)
187 (mail-header-date header)
188 (or (cdr (assq 'X-Nnshimbun-Id (mail-header-extra header)))
189 (mail-header-id header))
190 (mail-header-references header)
191 (mail-header-chars header)
192 (mail-header-lines header)
193 (let ((xref (mail-header-xref header)))
194 (if (and xref (string-match "^Xref: " xref))
198 (defun nnshimbun-request-article-1 (article &optional group server to-buffer)
199 (if (nnshimbun-backlog
200 (gnus-backlog-request-article
201 group article (or to-buffer nntp-server-buffer)))
203 (let* ((header (with-current-buffer (nnshimbun-open-nov group)
204 (and (nnheader-find-nov-line article)
205 (nnshimbun-make-shimbun-header
206 (nnheader-parse-nov)))))
207 (original-id (shimbun-header-id header)))
209 (with-current-buffer (or to-buffer nntp-server-buffer)
210 (delete-region (point-min) (point-max))
211 (shimbun-article nnshimbun-shimbun header)
212 (when (> (buffer-size) 0)
213 (nnshimbun-replace-nov-entry group article header original-id)
215 (gnus-backlog-enter-article group article (current-buffer)))
216 (nnheader-report 'nnshimbun "Article %s retrieved"
217 (shimbun-header-id header))
218 (cons group article)))))))
220 (deffoo nnshimbun-request-article (article &optional group server to-buffer)
221 (when (nnshimbun-possibly-change-group group server)
222 (when (stringp article)
223 (setq article (nnshimbun-search-id group article)))
224 (if (integerp article)
225 (nnshimbun-request-article-1 article group server to-buffer)
226 (nnheader-report 'nnshimbun "Couldn't retrieve article: %s"
227 (prin1-to-string article))
230 (deffoo nnshimbun-request-group (group &optional server dont-check)
231 (let ((file-name-coding-system nnmail-pathname-coding-system)
232 (pathname-coding-system nnmail-pathname-coding-system))
234 ((not (nnshimbun-possibly-change-group group server))
235 (nnheader-report 'nnshimbun "Invalid group (no such directory)"))
236 ((not (file-exists-p nnshimbun-current-directory))
237 (nnheader-report 'nnshimbun "Directory %s does not exist"
238 nnshimbun-current-directory))
239 ((not (file-directory-p nnshimbun-current-directory))
240 (nnheader-report 'nnshimbun "%s is not a directory"
241 nnshimbun-current-directory))
243 (nnheader-report 'nnshimbun "Group %s selected" group)
247 (with-current-buffer (nnshimbun-open-nov group)
248 (goto-char (point-min))
249 (setq beg (ignore-errors (read (current-buffer))))
250 (goto-char (point-max))
252 (setq end (ignore-errors (read (current-buffer)))
253 lines (count-lines (point-min) (point-max))))
254 (nnheader-report 'nnshimbunw "Selected group %s" group)
255 (nnheader-insert "211 %d %d %d %s\n"
256 lines (or beg 0) (or end 0) group))))))
258 (deffoo nnshimbun-request-scan (&optional group server)
259 (nnshimbun-possibly-change-group group server)
260 (nnshimbun-generate-nov-database group))
262 (deffoo nnshimbun-close-group (group &optional server)
263 (nnshimbun-write-nov group)
266 (deffoo nnshimbun-request-list (&optional server)
267 (with-current-buffer nntp-server-buffer
268 (delete-region (point-min) (point-max))
269 (dolist (group (shimbun-groups nnshimbun-shimbun))
270 (when (nnshimbun-possibly-change-group group server)
272 (with-current-buffer (nnshimbun-open-nov group)
273 (goto-char (point-min))
274 (setq beg (ignore-errors (read (current-buffer))))
275 (goto-char (point-max))
277 (setq end (ignore-errors (read (current-buffer)))))
278 (insert (format "%s %d %d n\n" group (or end 0) (or beg 0)))))))
281 (deffoo nnshimbun-retrieve-headers (articles &optional group server fetch-old)
282 (when (nnshimbun-possibly-change-group group server)
283 (if (nnshimbun-retrieve-headers-with-nov articles fetch-old)
285 (with-current-buffer nntp-server-buffer
286 (delete-region (point-min) (point-max))
288 (dolist (art articles)
290 (setq art (nnshimbun-search-id group art)))
293 (with-current-buffer (nnshimbun-open-nov group)
294 (and (nnheader-find-nov-line art)
295 (nnheader-parse-nov))))
296 (insert (format "220 %d Article retrieved.\n" art))
297 (shimbun-header-insert
299 (nnshimbun-make-shimbun-header header))
301 (delete-region (point) (point-max))))))
304 (defun nnshimbun-retrieve-headers-with-nov (articles &optional fetch-old)
305 (if (or gnus-nov-is-evil nnshimbun-nov-is-evil)
307 (let ((nov (expand-file-name nnshimbun-nov-file-name nnshimbun-current-directory)))
308 (when (file-exists-p nov)
310 (set-buffer nntp-server-buffer)
312 (nnheader-insert-file-contents nov)
313 (if (and fetch-old (not (numberp fetch-old)))
314 t ; Don't remove anything.
315 (nnheader-nov-delete-outside-range
316 (if fetch-old (max 1 (- (car articles) fetch-old))
318 (and articles (nth (1- (length articles)) articles)))
323 ;;; Nov Database Operations
325 (defvar nnshimbun-tmp-string nil
326 "Internal variable used to just a rest for a temporary string. The
327 macro `nnshimbun-string-or' uses it exclusively.")
329 (defmacro nnshimbun-string-or (&rest strings)
330 "Return the first element of STRINGS that is a non-blank string. It
331 should run fast, especially if two strings are given. Each string can
333 (cond ((null strings)
335 ((= 1 (length strings))
336 ;; Return irregularly nil if one blank string is given.
337 `(unless (zerop (length (setq nnshimbun-tmp-string ,(car strings))))
338 nnshimbun-tmp-string))
339 ((= 2 (length strings))
340 ;; Return the second string when the first string is blank.
341 `(if (zerop (length (setq nnshimbun-tmp-string ,(car strings))))
343 nnshimbun-tmp-string))
345 `(let ((strings (list ,@strings)))
347 (setq strings (if (zerop (length (setq nnshimbun-tmp-string
350 nnshimbun-tmp-string))))
352 (defsubst nnshimbun-insert-nov (number header &optional id)
355 (let ((header-id (nnshimbun-string-or (shimbun-header-id header)))
356 ;; Force `princ' to work in the current buffer.
357 (standard-output (current-buffer))
358 (xref (nnshimbun-string-or (shimbun-header-xref header)))
360 (unless (and (stringp id)
362 (string-equal id header-id))
367 (nnshimbun-string-or (shimbun-header-subject header) "(none)") "\t"
368 (nnshimbun-string-or (shimbun-header-from header) "(nobody)") "\t"
369 (nnshimbun-string-or (shimbun-header-date header) (message-make-date))
371 (or header-id (nnmail-message-id)) "\t"
372 (or (shimbun-header-references header) "") "\t")
373 (princ (or (shimbun-header-chars header) 0))
375 (princ (or (shimbun-header-lines header) 0))
379 (insert "Xref: " xref "\t")
381 (insert "X-Nnshimbun-Id: " id "\t")))
383 (insert "\tX-Nnshimbun-Id: " id "\t")))
384 ;; Replace newlines with spaces in the current NOV line.
388 (backward-delete-char 1)
392 (defun nnshimbun-generate-nov-database (group)
393 (nnshimbun-possibly-change-group group)
394 (with-current-buffer (nnshimbun-open-nov group)
395 (goto-char (point-max))
397 (let ((i (or (ignore-errors (read (current-buffer))) 0)))
398 (dolist (header (shimbun-headers nnshimbun-shimbun))
399 (unless (nnshimbun-search-id group (shimbun-header-id header))
400 (goto-char (point-max))
401 (nnshimbun-insert-nov (setq i (1+ i)) header)
402 (when nnshimbun-pre-fetch-article
403 (nnshimbun-request-article-1 i group nil nnshimbun-buffer)))))
404 (nnshimbun-write-nov group)))
406 (defun nnshimbun-replace-nov-entry (group article header &optional id)
407 (with-current-buffer (nnshimbun-open-nov group)
408 (when (nnheader-find-nov-line article)
409 (delete-region (point) (progn (forward-line 1) (point)))
410 (nnshimbun-insert-nov article header id))))
412 (defun nnshimbun-search-id (group id &optional nov)
413 (with-current-buffer (nnshimbun-open-nov group)
414 (goto-char (point-min))
416 (while (and (not found)
417 (search-forward id nil t)) ; We find the ID.
418 ;; And the id is in the fourth field.
419 (if (not (and (search-backward "\t" nil t 4)
420 (not (search-backward "\t" (gnus-point-at-bol) t))))
425 (goto-char (point-min))
426 (setq id (concat "X-Nnshimbun-Id: " id))
427 (while (and (not found)
428 (search-forward id nil t))
429 (if (not (search-backward "\t" (gnus-point-at-bol) t 8))
436 ;; We return the article number.
437 (ignore-errors (read (current-buffer))))))))
439 (defun nnshimbun-open-nov (group)
440 (let ((buffer (cdr (assoc group nnshimbun-nov-buffer-alist))))
441 (if (buffer-live-p buffer)
443 (setq buffer (gnus-get-buffer-create
444 (format " *nnshimbun overview %s %s*"
445 (nnoo-current-server 'nnshimbun) group)))
448 (set (make-local-variable 'nnshimbun-nov-buffer-file-name)
450 nnshimbun-nov-file-name
451 (nnmail-group-pathname group nnshimbun-server-directory)))
453 (when (file-exists-p nnshimbun-nov-buffer-file-name)
454 (nnheader-insert-file-contents nnshimbun-nov-buffer-file-name))
455 (set-buffer-modified-p nil))
456 (push (cons group buffer) nnshimbun-nov-buffer-alist)
459 (defun nnshimbun-write-nov (group)
460 (let ((buffer (cdr (assoc group nnshimbun-nov-buffer-alist))))
461 (when (buffer-live-p buffer)
465 (nnmail-write-region 1 (point-max) nnshimbun-nov-buffer-file-name
468 (defun nnshimbun-save-nov ()
470 (while nnshimbun-nov-buffer-alist
471 (when (buffer-name (cdar nnshimbun-nov-buffer-alist))
472 (set-buffer (cdar nnshimbun-nov-buffer-alist))
473 (when (buffer-modified-p)
474 (nnmail-write-region 1 (point-max) nnshimbun-nov-buffer-file-name
476 (set-buffer-modified-p nil)
477 (kill-buffer (current-buffer)))
478 (setq nnshimbun-nov-buffer-alist (cdr nnshimbun-nov-buffer-alist)))))
480 (defvar nnshimbun-keep-last-article t
481 "*If non-nil, nnshimbun will never delete a group's last article. It
482 can be marked expirable, so it will be deleted when it is no longer
485 (defvar nnshimbun-keep-unparsable-dated-articles t
486 "*If non-nil, nnshimbun will never delete articles whose NOV date is
487 unparsable. Even so, you can expire such articles using the command
488 `nnshimbun-expire-nov-databases' with a prefix argument.")
490 (deffoo nnshimbun-request-expire-articles (articles group
491 &optional server force)
492 "Do expire for the specified ARTICLES in the nnshimbun GROUP. Notice
493 that nnshimbun does not actually delete any articles, it just delete
494 the corresponding entries in the NOV database locally. If ARTICLES is
495 `all', the expiring is performed on all the NOV lines. It does expire
496 only when the current SERVER is specified and the NOV is open.
497 However, the optional FORCE if it is non-nil (it is supposed to be
498 specified by the command `nnshimbun-expire-nov-databases'), it does
499 expire for the SERVER:GROUP even if whose NOV is not open."
500 (let ((buffer (cdr (assoc group nnshimbun-nov-buffer-alist)))
501 (progress-msg (format "Expiring NOV database for nnshimbun+%s:%s "
505 should-close-nov name article expirable end time)
508 (or (let ((current (nnoo-current-server 'nnshimbun)))
510 (string-equal server current)
511 (buffer-live-p buffer)))
513 (setq should-close-nov t
514 buffer (gnus-get-buffer-create
515 (format " *nnshimbun overview %s %s*"
519 (set (make-local-variable 'nnshimbun-nov-buffer-file-name)
521 nnshimbun-nov-file-name
526 nnshimbun-directory))))
528 (nnheader-insert-file-contents
529 nnshimbun-nov-buffer-file-name))
530 (set-buffer-modified-p nil)
534 (setq name (concat "nnshimbun+" server ":" group))
536 (when (eq 'all articles)
538 (goto-char (point-min))
540 (when (looking-at "[0-9]+\t")
541 (push (read buffer) articles))
543 (setq articles (nreverse articles)))
544 (setq expirable (copy-sequence articles))
546 (setq article (pop expirable))
547 (when (and (nnheader-find-nov-line article)
548 (setq end (line-end-position))
549 (not (and nnshimbun-keep-last-article
550 (= (point-max) (1+ end)))))
551 (setq time (and (search-forward "\t" end t)
552 (search-forward "\t" end t)
553 (search-forward "\t" end t)
557 (if (search-forward "\t" end t)
561 (or (setq time (condition-case nil
562 (apply 'encode-time time)
564 ;; Inhibit expiring if there's no parsable date
565 ;; and the following option is non-nil.
566 (not nnshimbun-keep-unparsable-dated-articles))
567 (nnmail-expired-article-p name time nil))
570 (message "%s(%c)..." progress-msg article))
572 (delete-region (point) (1+ end))
573 (setq articles (delq article articles)))
579 (logand 3 (1+ counter)))))))))
580 (when (buffer-modified-p)
581 (nnmail-write-region 1 (point-max)
582 nnshimbun-nov-buffer-file-name
584 (set-buffer-modified-p nil))
586 (when should-close-nov
587 (kill-buffer buffer)))
591 (defun nnshimbun-expire-nov-databases (&optional arg)
592 "Expire NOV databases for all the auto expirable nnshimbun groups.
593 If the prefix argument is given, the value of
594 `nnshimbun-keep-unparsable-dated-articles' will be ignored (treated as
597 (let ((nnshimbun-keep-unparsable-dated-articles
599 nnshimbun-keep-unparsable-dated-articles))
603 (if (and (not (string-equal ".." dir))
604 (file-directory-p (expand-file-name
606 nnshimbun-directory)))
608 (directory-files nnshimbun-directory))))
609 server directory groups group nov did)
611 (setq server (car servers)
612 servers (cdr servers)
613 directory (expand-file-name server nnshimbun-directory)
615 (mapcar (lambda (dir)
616 (if (and (not (string-equal ".." dir))
621 (directory-files directory))))
623 (setq group (car groups)
625 nov (expand-file-name nnshimbun-nov-file-name
626 (expand-file-name group directory)))
627 (when (and (gnus-group-auto-expirable-p (concat "nnshimbun+"
630 (message "Expiring NOV database for nnshimbun+%s:%s..."
632 (nnshimbun-request-expire-articles 'all group server t)
635 "Expiring NOV databases...done"
636 "Nothing to be done"))))
640 ;;; Server Initialize
642 (defun nnshimbun-possibly-change-group (group &optional server)
644 (unless (nnshimbun-server-opened server)
645 (nnshimbun-open-server server)))
646 (unless (gnus-buffer-live-p nnshimbun-buffer)
647 (setq nnshimbun-buffer
649 (nnheader-set-temp-buffer
650 (format " *nnshimbun %s*" (nnoo-current-server 'nnshimbun))))))
654 (shimbun-open-group nnshimbun-shimbun group)
655 (error (nnheader-report 'nnshimbun "%s" (error-message-string err))))
656 (let ((pathname (nnmail-group-pathname group nnshimbun-server-directory))
657 (file-name-coding-system nnmail-pathname-coding-system)
658 (pathname-coding-system nnmail-pathname-coding-system))
659 (unless (equal pathname nnshimbun-current-directory)
660 (setq nnshimbun-current-directory pathname
661 nnshimbun-current-group group))
662 (unless (file-exists-p nnshimbun-current-directory)
663 (ignore-errors (make-directory nnshimbun-current-directory t)))
665 ((not (file-exists-p nnshimbun-current-directory))
666 (nnheader-report 'nnshimbun "Couldn't create directory: %s" nnshimbun-current-directory))
667 ((not (file-directory-p (file-truename nnshimbun-current-directory)))
668 (nnheader-report 'nnshimbun "Not a directory: %s" nnshimbun-current-directory))
674 (luna-define-class shimbun-gnus-mua (shimbun-mua) ())
676 (luna-define-method shimbun-mua-search-id ((mua shimbun-gnus-mua) id)
678 (shimbun-current-group-internal (shimbun-mua-shimbun-internal mua))
681 (luna-define-method shimbun-mua-use-entire-index ((mua shimbun-gnus-mua))
682 nnshimbun-use-entire-index)
686 ;;; nnshimbun.el ends here.