Add.
[gnus] / lisp / nnweb.el
1 ;;; nnweb.el --- retrieving articles via web search engines
2 ;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
3 ;;        Free Software Foundation, Inc.
4
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
6 ;; Keywords: news
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
13 ;; any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;; Note: You need to have `w3' installed for some functions to work.
28
29 ;;; Code:
30
31 (eval-when-compile (require 'cl))
32
33 (require 'nnoo)
34 (require 'message)
35 (require 'gnus-util)
36 (require 'gnus)
37 (require 'nnmail)
38 (require 'mm-util)
39 (require 'mm-url)
40 (eval-and-compile
41   (ignore-errors
42     (require 'url)))
43 (autoload 'w3-parse-buffer "w3-parse")
44
45 (nnoo-declare nnweb)
46
47 (defvoo nnweb-directory (nnheader-concat gnus-directory "nnweb/")
48   "Where nnweb will save its files.")
49
50 (defvoo nnweb-type 'google
51   "What search engine type is being used.
52 Valid types include `google', `dejanews', and `gmane'.")
53
54 (defvar nnweb-type-definition
55   '((google
56      (article . ignore)
57      (id . "http://groups.google.com/groups?selm=%s&output=gplain")
58      (reference . identity)
59      (map . nnweb-google-create-mapping)
60      (search . nnweb-google-search)
61      (address . "http://groups.google.com/groups")
62      (identifier . nnweb-google-identity))
63     (dejanews ;; alias of google
64      (article . ignore)
65      (id . "http://groups.google.com/groups?selm=%s&output=gplain")
66      (reference . identity)
67      (map . nnweb-google-create-mapping)
68      (search . nnweb-google-search)
69      (address . "http://groups.google.com/groups")
70      (identifier . nnweb-google-identity))
71     (gmane
72      (article . nnweb-gmane-wash-article)
73      (id . "http://gmane.org/view.php?group=%s")
74      (reference . identity)
75      (map . nnweb-gmane-create-mapping)
76      (search . nnweb-gmane-search)
77      (address . "http://gmane.org/")
78      (identifier . nnweb-gmane-identity)))
79   "Type-definition alist.")
80
81 (defvoo nnweb-search nil
82   "Search string to feed to Google.")
83
84 (defvoo nnweb-max-hits 999
85   "Maximum number of hits to display.")
86
87 (defvoo nnweb-ephemeral-p nil
88   "Whether this nnweb server is ephemeral.")
89
90 ;;; Internal variables
91
92 (defvoo nnweb-articles nil)
93 (defvoo nnweb-buffer nil)
94 (defvoo nnweb-group-alist nil)
95 (defvoo nnweb-group nil)
96 (defvoo nnweb-hashtb nil)
97
98 ;;; Interface functions
99
100 (nnoo-define-basics nnweb)
101
102 (deffoo nnweb-retrieve-headers (articles &optional group server fetch-old)
103   (nnweb-possibly-change-server group server)
104   (save-excursion
105     (set-buffer nntp-server-buffer)
106     (erase-buffer)
107     (let (article header)
108       (mm-with-unibyte-current-buffer
109         (while (setq article (pop articles))
110           (when (setq header (cadr (assq article nnweb-articles)))
111             (nnheader-insert-nov header))))
112       'nov)))
113
114 (deffoo nnweb-request-scan (&optional group server)
115   (nnweb-possibly-change-server group server)
116   (if nnweb-ephemeral-p
117       (setq nnweb-hashtb (gnus-make-hashtable 4095)))
118   (funcall (nnweb-definition 'map))
119   (unless nnweb-ephemeral-p
120     (nnweb-write-active)
121     (nnweb-write-overview group)))
122
123 (deffoo nnweb-request-group (group &optional server dont-check)
124   (nnweb-possibly-change-server nil server)
125   (when (and group
126              (not (equal group nnweb-group))
127              (not nnweb-ephemeral-p))
128     (setq nnweb-group group
129           nnweb-articles nil)
130     (let ((info (assoc group nnweb-group-alist)))
131       (when info
132         (setq nnweb-type (nth 2 info))
133         (setq nnweb-search (nth 3 info))
134         (unless dont-check
135           (nnweb-read-overview group)))))
136   (cond
137    ((not nnweb-articles)
138     (nnheader-report 'nnweb "No matching articles"))
139    (t
140     (let ((active (if nnweb-ephemeral-p
141                       (cons (caar nnweb-articles)
142                             (caar (last nnweb-articles)))
143                     (cadr (assoc group nnweb-group-alist)))))
144       (nnheader-report 'nnweb "Opened group %s" group)
145       (nnheader-insert
146        "211 %d %d %d %s\n" (length nnweb-articles)
147        (car active) (cdr active) group)))))
148
149 (deffoo nnweb-close-group (group &optional server)
150   (nnweb-possibly-change-server group server)
151   (when (gnus-buffer-live-p nnweb-buffer)
152     (save-excursion
153       (set-buffer nnweb-buffer)
154       (set-buffer-modified-p nil)
155       (kill-buffer nnweb-buffer)))
156   t)
157
158 (deffoo nnweb-request-article (article &optional group server buffer)
159   (nnweb-possibly-change-server group server)
160   (save-excursion
161     (set-buffer (or buffer nntp-server-buffer))
162     (let* ((header (cadr (assq article nnweb-articles)))
163            (url (and header (mail-header-xref header))))
164       (when (or (and url
165                      (mm-with-unibyte-current-buffer
166                        (mm-url-insert url)))
167                 (and (stringp article)
168                      (nnweb-definition 'id t)
169                      (let ((fetch (nnweb-definition 'id))
170                            art active)
171                        (when (string-match "^<\\(.*\\)>$" article)
172                          (setq art (match-string 1 article)))
173                        (when (and fetch art)
174                          (setq url (format fetch art))
175                          (mm-with-unibyte-current-buffer
176                            (mm-url-insert url))
177                          (if (nnweb-definition 'reference t)
178                              (setq article
179                                    (funcall (nnweb-definition
180                                              'reference) article)))))))
181         (unless nnheader-callback-function
182           (funcall (nnweb-definition 'article)))
183         (nnheader-report 'nnweb "Fetched article %s" article)
184         (cons group (and (numberp article) article))))))
185
186 (deffoo nnweb-close-server (&optional server)
187   (when (and (nnweb-server-opened server)
188              (gnus-buffer-live-p nnweb-buffer))
189     (save-excursion
190       (set-buffer nnweb-buffer)
191       (set-buffer-modified-p nil)
192       (kill-buffer nnweb-buffer)))
193   (nnoo-close-server 'nnweb server))
194
195 (deffoo nnweb-request-list (&optional server)
196   (nnweb-possibly-change-server nil server)
197   (save-excursion
198     (set-buffer nntp-server-buffer)
199     (nnmail-generate-active nnweb-group-alist)
200     t))
201
202 (deffoo nnweb-request-update-info (group info &optional server)
203   (nnweb-possibly-change-server group server))
204
205 (deffoo nnweb-asynchronous-p ()
206   nil)
207
208 (deffoo nnweb-request-create-group (group &optional server args)
209   (nnweb-possibly-change-server nil server)
210   (nnweb-request-delete-group group)
211   (push `(,group ,(cons 1 0) ,@args) nnweb-group-alist)
212   (nnweb-write-active)
213   t)
214
215 (deffoo nnweb-request-delete-group (group &optional force server)
216   (nnweb-possibly-change-server group server)
217   (gnus-pull group nnweb-group-alist t)
218   (nnweb-write-active)
219   (gnus-delete-file (nnweb-overview-file group))
220   t)
221
222 (nnoo-define-skeleton nnweb)
223
224 ;;; Internal functions
225
226 (defun nnweb-read-overview (group)
227   "Read the overview of GROUP and build the map."
228   (when (file-exists-p (nnweb-overview-file group))
229     (mm-with-unibyte-buffer
230       (nnheader-insert-file-contents (nnweb-overview-file group))
231       (goto-char (point-min))
232       (let (header)
233         (while (not (eobp))
234           (setq header (nnheader-parse-nov))
235           (forward-line 1)
236           (push (list (mail-header-number header)
237                       header (mail-header-xref header))
238                 nnweb-articles)
239           (nnweb-set-hashtb header (car nnweb-articles)))))))
240
241 (defun nnweb-write-overview (group)
242   "Write the overview file for GROUP."
243   (with-temp-file (nnweb-overview-file group)
244     (let ((articles nnweb-articles))
245       (while articles
246         (nnheader-insert-nov (cadr (pop articles)))))))
247
248 (defun nnweb-set-hashtb (header data)
249   (gnus-sethash (nnweb-identifier (mail-header-xref header))
250                 data nnweb-hashtb))
251
252 (defun nnweb-get-hashtb (url)
253   (gnus-gethash (nnweb-identifier url) nnweb-hashtb))
254
255 (defun nnweb-identifier (ident)
256   (funcall (nnweb-definition 'identifier) ident))
257
258 (defun nnweb-overview-file (group)
259   "Return the name of the overview file of GROUP."
260   (nnheader-concat nnweb-directory group ".overview"))
261
262 (defun nnweb-write-active ()
263   "Save the active file."
264   (gnus-make-directory nnweb-directory)
265   (with-temp-file (nnheader-concat nnweb-directory "active")
266     (prin1 `(setq nnweb-group-alist ',nnweb-group-alist) (current-buffer))))
267
268 (defun nnweb-read-active ()
269   "Read the active file."
270   (load (nnheader-concat nnweb-directory "active") t t t))
271
272 (defun nnweb-definition (type &optional noerror)
273   "Return the definition of TYPE."
274   (let ((def (cdr (assq type (assq nnweb-type nnweb-type-definition)))))
275     (when (and (not def)
276                (not noerror))
277       (error "Undefined definition %s" type))
278     def))
279
280 (defun nnweb-possibly-change-server (&optional group server)
281   (nnweb-init server)
282   (when server
283     (unless (nnweb-server-opened server)
284       (nnweb-open-server server)))
285   (unless nnweb-group-alist
286     (nnweb-read-active))
287   (unless nnweb-hashtb
288     (setq nnweb-hashtb (gnus-make-hashtable 4095)))
289   (when group
290     (when (and (not nnweb-ephemeral-p)
291                (equal group nnweb-group))
292       (nnweb-request-group group nil t))))
293
294 (defun nnweb-init (server)
295   "Initialize buffers and such."
296   (unless (gnus-buffer-live-p nnweb-buffer)
297     (setq nnweb-buffer
298           (save-excursion
299             (mm-with-unibyte
300               (nnheader-set-temp-buffer
301                (format " *nnweb %s %s %s*"
302                        nnweb-type nnweb-search server))
303               (current-buffer))))))
304
305 ;;;
306 ;;; Deja bought by google.com
307 ;;;
308
309 (defun nnweb-google-wash-article ()
310   (let ((case-fold-search t) url)
311     (goto-char (point-min))
312     (re-search-forward "^<pre>" nil t)
313     (narrow-to-region (point-min) (point))
314     (search-backward "<table " nil t 2)
315     (delete-region (point-min) (point))
316     (if (re-search-forward "Search Result [0-9]+" nil t)
317         (replace-match ""))
318     (if (re-search-forward "View complete thread ([0-9]+ articles?)" nil t)
319         (replace-match ""))
320     (goto-char (point-min))
321     (while (search-forward "<br>" nil t)
322       (replace-match "\n"))
323     (mm-url-remove-markup)
324     (goto-char (point-min))
325     (while (re-search-forward "^[ \t]*\n" nil t)
326       (replace-match ""))
327     (goto-char (point-max))
328     (insert "\n")
329     (widen)
330     (narrow-to-region (point) (point-max))
331     (search-forward "</pre>" nil t)
332     (delete-region (point) (point-max))
333     (mm-url-remove-markup)
334     (widen)))
335
336 (defun nnweb-google-parse-1 (&optional Message-ID)
337   (let ((i 0)
338         (case-fold-search t)
339         (active (cadr (assoc nnweb-group nnweb-group-alist)))
340         Subject Score Date Newsgroups From
341         map url mid)
342     (unless active
343       (push (list nnweb-group (setq active (cons 1 0))
344                   nnweb-type nnweb-search)
345             nnweb-group-alist))
346     ;; Go through all the article hits on this page.
347     (goto-char (point-min))
348     (while (re-search-forward
349             "a href=/groups\\(\\?[^ \">]*selm=\\([^ &\">]+\\)\\)" nil t)
350       (setq mid (match-string 2)
351             url (format
352                  "http://groups.google.com/groups?selm=%s&output=gplain" mid))
353       (narrow-to-region (search-forward ">" nil t)
354                         (search-forward "</a>" nil t))
355       (mm-url-remove-markup)
356       (mm-url-decode-entities)
357       (setq Subject (buffer-string))
358       (goto-char (point-max))
359       (widen)
360       (forward-line 2)
361       (when (looking-at "<br><font[^>]+>")
362         (goto-char (match-end 0)))
363       (if (not (looking-at "<a[^>]+>"))
364           (skip-chars-forward " \t")
365         (narrow-to-region (point)
366                           (search-forward "</a>" nil t))
367         (mm-url-remove-markup)
368         (mm-url-decode-entities)
369         (setq Newsgroups (buffer-string))
370         (goto-char (point-max))
371         (widen)
372         (skip-chars-forward "- \t"))
373       (when (looking-at
374              "\\([0-9]+\\)[/ ]\\([A-Za-z]+\\)[/ ]\\([0-9]+\\)[ \t]*by[ \t]*\\([^<]*\\) - <a")
375         (setq From (match-string 4)
376               Date (format "%s %s 00:00:00 %s"
377                            (match-string 2) (match-string 1)
378                            (match-string 3))))
379       (forward-line 1)
380       (incf i)
381       (unless (nnweb-get-hashtb url)
382         (push
383          (list
384           (incf (cdr active))
385           (make-full-mail-header
386            (cdr active) (if Newsgroups
387                             (concat  "(" Newsgroups ") " Subject)
388                           Subject)
389            From Date (or Message-ID mid)
390            nil 0 0 url))
391          map)
392         (nnweb-set-hashtb (cadar map) (car map))))
393     map))
394
395 (defun nnweb-google-reference (id)
396   (let ((map (nnweb-google-parse-1 id)) header)
397     (setq nnweb-articles
398           (nconc nnweb-articles map))
399     (when (setq header (cadar map))
400       (mm-with-unibyte-current-buffer
401         (mm-url-insert (mail-header-xref header)))
402       (caar map))))
403
404 (defun nnweb-google-create-mapping ()
405   "Perform the search and create a number-to-url alist."
406   (save-excursion
407     (set-buffer nnweb-buffer)
408     (erase-buffer)
409     (when (funcall (nnweb-definition 'search) nnweb-search)
410         (let ((more t)
411               (i 0))
412           (while more
413             (setq nnweb-articles
414                   (nconc nnweb-articles (nnweb-google-parse-1)))
415             ;; Check if there are more articles to fetch
416             (goto-char (point-min))
417             (incf i 100)
418             (if (or (not (re-search-forward
419                           "<td nowrap><a href=\\([^>]+\\).*<span class=b>Next</span>" nil t))
420                     (>= i nnweb-max-hits))
421                 (setq more nil)
422               ;; Yup, there are more articles
423               (setq more (concat "http://groups.google.com" (match-string 1)))
424             (when more
425               (erase-buffer)
426               (mm-url-insert more))))
427           ;; Return the articles in the right order.
428           (setq nnweb-articles
429                 (sort nnweb-articles 'car-less-than-car))))))
430
431 (defun nnweb-google-search (search)
432   (mm-url-insert
433    (concat
434     (nnweb-definition 'address)
435     "?"
436     (mm-url-encode-www-form-urlencoded
437      `(("q" . ,search)
438        ("num". "100")
439        ("hq" . "")
440        ("hl" . "")
441        ("lr" . "")
442        ("safe" . "off")
443        ("sites" . "groups")))))
444   t)
445
446 (defun nnweb-google-identity (url)
447   "Return an unique identifier based on URL."
448   (if (string-match "selm=\\([^ &>]+\\)" url)
449       (match-string 1 url)
450     url))
451
452 ;;;
453 ;;; gmane.org
454 ;;;
455 (defun nnweb-gmane-create-mapping ()
456   "Perform the search and create a number-to-url alist."
457   (save-excursion
458     (set-buffer nnweb-buffer)
459     (erase-buffer)
460     (when (funcall (nnweb-definition 'search) nnweb-search)
461       (let ((more t)
462             (case-fold-search t)
463             (active (or (cadr (assoc nnweb-group nnweb-group-alist))
464                         (cons 1 0)))
465             subject group url
466             map)
467           ;; Remove stuff from the beginning of results
468         (goto-char (point-min))
469         (search-forward "Search Results</h1><ul>" nil t)
470         (delete-region (point-min) (point))
471         (goto-char (point-min))
472         ;; Iterate over the actual hits
473         (while (re-search-forward ".*href=\"\\([^\"]+\\)\">\\(.*\\)" nil t)
474             (setq url (concat "http://gmane.org/" (match-string 1)))
475             (setq subject (match-string 2))
476           (unless (nnweb-get-hashtb url)
477             (push
478              (list
479               (incf (cdr active))
480               (make-full-mail-header
481                (cdr active) (concat  "(" group ") " subject) nil nil
482                nil nil 0 0 url))
483              map)
484             (nnweb-set-hashtb (cadar map) (car map))))
485         ;; Return the articles in the right order.
486         (setq nnweb-articles
487               (sort (nconc nnweb-articles map) 'car-less-than-car))))))
488
489 (defun nnweb-gmane-wash-article ()
490   (let ((case-fold-search t))
491     (goto-char (point-min))
492     (re-search-forward "<!--X-Head-of-Message-->" nil t)
493     (delete-region (point-min) (point))
494     (goto-char (point-min))
495     (while (looking-at "^<li><em>\\([^ ]+\\)</em>.*</li>")
496       (replace-match "\\1\\2" t)
497       (forward-line 1))
498     (mm-url-remove-markup)))
499
500 (defun nnweb-gmane-search (search)
501   (mm-url-insert
502    (concat
503     (nnweb-definition 'address)
504     "?"
505     (mm-url-encode-www-form-urlencoded
506      `(("query" . ,search)))))
507   (setq buffer-file-name nil)
508   t)
509
510
511 (defun nnweb-gmane-identity (url)
512   "Return a unique identifier based on URL."
513   (if (string-match "group=\\(.+\\)" url)
514       (match-string 1 url)
515     url))
516
517 ;;;
518 ;;; General web/w3 interface utility functions
519 ;;;
520
521 (defun nnweb-insert-html (parse)
522   "Insert HTML based on a w3 parse tree."