b36104023d09f0bc4e292523a045f9505c6ed91b
[gnus] / lisp / nnweb.el
1 ;;; nnweb.el --- retrieving articles via web search engines
2 ;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002
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', `reference', and `altavista'.")
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 . nnweb-google-wash-article)
65      ;;(id . "http://groups.google.com/groups?as_umsgid=%s")
66      (article . ignore)
67      (id . "http://groups.google.com/groups?selm=%s&output=gplain")
68      ;;(reference . nnweb-google-reference)
69      (reference . identity)
70      (map . nnweb-google-create-mapping)
71      (search . nnweb-google-search)
72      (address . "http://groups.google.com/groups")
73      (identifier . nnweb-google-identity))
74     (reference
75      (article . nnweb-reference-wash-article)
76      (map . nnweb-reference-create-mapping)
77      (search . nnweb-reference-search)
78      (address . "http://www.reference.com/cgi-bin/pn/go")
79      (identifier . identity))
80     (altavista
81      (article . nnweb-altavista-wash-article)
82      (map . nnweb-altavista-create-mapping)
83      (search . nnweb-altavista-search)
84      (address . "http://www.altavista.digital.com/cgi-bin/query")
85      (id . "/cgi-bin/news?id@%s")
86      (identifier . identity)))
87   "Type-definition alist.")
88
89 (defvoo nnweb-search nil
90   "Search string to feed to DejaNews.")
91
92 (defvoo nnweb-max-hits 999
93   "Maximum number of hits to display.")
94
95 (defvoo nnweb-ephemeral-p nil
96   "Whether this nnweb server is ephemeral.")
97
98 ;;; Internal variables
99
100 (defvoo nnweb-articles nil)
101 (defvoo nnweb-buffer nil)
102 (defvoo nnweb-group-alist nil)
103 (defvoo nnweb-group nil)
104 (defvoo nnweb-hashtb nil)
105
106 ;;; Interface functions
107
108 (nnoo-define-basics nnweb)
109
110 (deffoo nnweb-retrieve-headers (articles &optional group server fetch-old)
111   (nnweb-possibly-change-server group server)
112   (save-excursion
113     (set-buffer nntp-server-buffer)
114     (erase-buffer)
115     (let (article header)
116       (mm-with-unibyte-current-buffer
117         (while (setq article (pop articles))
118           (when (setq header (cadr (assq article nnweb-articles)))
119             (nnheader-insert-nov header))))
120       'nov)))
121
122 (deffoo nnweb-request-scan (&optional group server)
123   (nnweb-possibly-change-server group server)
124   (if nnweb-ephemeral-p
125       (setq nnweb-hashtb (gnus-make-hashtable 4095)))
126   (funcall (nnweb-definition 'map))
127   (unless nnweb-ephemeral-p
128     (nnweb-write-active)
129     (nnweb-write-overview group)))
130
131 (deffoo nnweb-request-group (group &optional server dont-check)
132   (nnweb-possibly-change-server nil server)
133   (when (and group
134              (not (equal group nnweb-group))
135              (not nnweb-ephemeral-p))
136     (setq nnweb-group group
137           nnweb-articles nil)
138     (let ((info (assoc group nnweb-group-alist)))
139       (when info
140         (setq nnweb-type (nth 2 info))
141         (setq nnweb-search (nth 3 info))
142         (unless dont-check
143           (nnweb-read-overview group)))))
144   (cond
145    ((not nnweb-articles)
146     (nnheader-report 'nnweb "No matching articles"))
147    (t
148     (let ((active (if nnweb-ephemeral-p
149                       (cons (caar nnweb-articles)
150                             (caar (last nnweb-articles)))
151                     (cadr (assoc group nnweb-group-alist)))))
152       (nnheader-report 'nnweb "Opened group %s" group)
153       (nnheader-insert
154        "211 %d %d %d %s\n" (length nnweb-articles)
155        (car active) (cdr active) group)))))
156
157 (deffoo nnweb-close-group (group &optional server)
158   (nnweb-possibly-change-server group server)
159   (when (gnus-buffer-live-p nnweb-buffer)
160     (save-excursion
161       (set-buffer nnweb-buffer)
162       (set-buffer-modified-p nil)
163       (kill-buffer nnweb-buffer)))
164   t)
165
166 (deffoo nnweb-request-article (article &optional group server buffer)
167   (nnweb-possibly-change-server group server)
168   (save-excursion
169     (set-buffer (or buffer nntp-server-buffer))
170     (let* ((header (cadr (assq article nnweb-articles)))
171            (url (and header (mail-header-xref header))))
172       (when (or (and url
173                      (mm-with-unibyte-current-buffer
174                        (mm-url-insert url)))
175                 (and (stringp article)
176                      (nnweb-definition 'id t)
177                      (let ((fetch (nnweb-definition 'id))
178                            art active)
179                        (when (string-match "^<\\(.*\\)>$" article)
180                          (setq art (match-string 1 article)))
181                        (when (and fetch art)
182                          (setq url (format fetch art))
183                          (mm-with-unibyte-current-buffer
184                            (mm-url-insert url))
185                          (if (nnweb-definition 'reference t)
186                              (setq article
187                                    (funcall (nnweb-definition
188                                              'reference) article)))))))
189         (unless nnheader-callback-function
190           (funcall (nnweb-definition 'article)))
191         (nnheader-report 'nnweb "Fetched article %s" article)
192         (cons group (and (numberp article) article))))))
193
194 (deffoo nnweb-close-server (&optional server)
195   (when (and (nnweb-server-opened server)
196              (gnus-buffer-live-p nnweb-buffer))
197     (save-excursion
198       (set-buffer nnweb-buffer)
199       (set-buffer-modified-p nil)
200       (kill-buffer nnweb-buffer)))
201   (nnoo-close-server 'nnweb server))
202
203 (deffoo nnweb-request-list (&optional server)
204   (nnweb-possibly-change-server nil server)
205   (save-excursion
206     (set-buffer nntp-server-buffer)
207     (nnmail-generate-active nnweb-group-alist)
208     t))
209
210 (deffoo nnweb-request-update-info (group info &optional server)
211   (nnweb-possibly-change-server group server))
212
213 (deffoo nnweb-asynchronous-p ()
214   t)
215
216 (deffoo nnweb-request-create-group (group &optional server args)
217   (nnweb-possibly-change-server nil server)
218   (nnweb-request-delete-group group)
219   (push `(,group ,(cons 1 0) ,@args) nnweb-group-alist)
220   (nnweb-write-active)
221   t)
222
223 (deffoo nnweb-request-delete-group (group &optional force server)
224   (nnweb-possibly-change-server group server)
225   (gnus-pull group nnweb-group-alist t)
226   (nnweb-write-active)
227   (gnus-delete-file (nnweb-overview-file group))
228   t)
229
230 (nnoo-define-skeleton nnweb)
231
232 ;;; Internal functions
233
234 (defun nnweb-read-overview (group)
235   "Read the overview of GROUP and build the map."
236   (when (file-exists-p (nnweb-overview-file group))
237     (mm-with-unibyte-buffer
238       (nnheader-insert-file-contents (nnweb-overview-file group))
239       (goto-char (point-min))
240       (let (header)
241         (while (not (eobp))
242           (setq header (nnheader-parse-nov))
243           (forward-line 1)
244           (push (list (mail-header-number header)
245                       header (mail-header-xref header))
246                 nnweb-articles)
247           (nnweb-set-hashtb header (car nnweb-articles)))))))
248
249 (defun nnweb-write-overview (group)
250   "Write the overview file for GROUP."
251   (with-temp-file (nnweb-overview-file group)
252     (let ((articles nnweb-articles))
253       (while articles
254         (nnheader-insert-nov (cadr (pop articles)))))))
255
256 (defun nnweb-set-hashtb (header data)
257   (gnus-sethash (nnweb-identifier (mail-header-xref header))
258                 data nnweb-hashtb))
259
260 (defun nnweb-get-hashtb (url)
261   (gnus-gethash (nnweb-identifier url) nnweb-hashtb))
262
263 (defun nnweb-identifier (ident)
264   (funcall (nnweb-definition 'identifier) ident))
265
266 (defun nnweb-overview-file (group)
267   "Return the name of the overview file of GROUP."
268   (nnheader-concat nnweb-directory group ".overview"))
269
270 (defun nnweb-write-active ()
271   "Save the active file."
272   (gnus-make-directory nnweb-directory)
273   (with-temp-file (nnheader-concat nnweb-directory "active")
274     (prin1 `(setq nnweb-group-alist ',nnweb-group-alist) (current-buffer))))
275
276 (defun nnweb-read-active ()
277   "Read the active file."
278   (load (nnheader-concat nnweb-directory "active") t t t))
279
280 (defun nnweb-definition (type &optional noerror)
281   "Return the definition of TYPE."
282   (let ((def (cdr (assq type (assq nnweb-type nnweb-type-definition)))))
283     (when (and (not def)
284                (not noerror))
285       (error "Undefined definition %s" type))
286     def))
287
288 (defun nnweb-possibly-change-server (&optional group server)
289   (nnweb-init server)
290   (when server
291     (unless (nnweb-server-opened server)
292       (nnweb-open-server server)))
293   (unless nnweb-group-alist
294     (nnweb-read-active))
295   (unless nnweb-hashtb
296     (setq nnweb-hashtb (gnus-make-hashtable 4095)))
297   (when group
298     (when (and (not nnweb-ephemeral-p)
299                (equal group nnweb-group))
300       (nnweb-request-group group nil t))))
301
302 (defun nnweb-init (server)
303   "Initialize buffers and such."
304   (unless (gnus-buffer-live-p nnweb-buffer)
305     (setq nnweb-buffer
306           (save-excursion
307             (mm-with-unibyte
308               (nnheader-set-temp-buffer
309                (format " *nnweb %s %s %s*"
310                        nnweb-type nnweb-search server))
311               (current-buffer))))))
312
313 ;; (defun nnweb-fetch-url (url)
314 ;;   (let (buf)
315 ;;     (save-excursion
316 ;;       (if (not nnheader-callback-function)
317 ;;        (progn
318 ;;          (with-temp-buffer
319 ;;            (mm-enable-multibyte)
320 ;;            (let ((coding-system-for-read 'binary)
321 ;;                  (coding-system-for-write 'binary)
322 ;;                  (default-process-coding-system 'binary))
323 ;;              (nnweb-insert url))
324 ;;            (setq buf (buffer-string)))
325 ;;          (erase-buffer)
326 ;;          (insert buf)
327 ;;          t)
328 ;;      (nnweb-url-retrieve-asynch
329 ;;       url 'nnweb-callback (current-buffer) nnheader-callback-function)
330 ;;      t))))
331
332 ;; (defun nnweb-callback (buffer callback)
333 ;;   (when (gnus-buffer-live-p url-working-buffer)
334 ;;     (save-excursion
335 ;;       (set-buffer url-working-buffer)
336 ;;       (funcall (nnweb-definition 'article))
337 ;;       (nnweb-decode-entities)
338 ;;       (set-buffer buffer)
339 ;;       (goto-char (point-max))
340 ;;       (insert-buffer-substring url-working-buffer))
341 ;;     (funcall callback t)
342 ;;     (gnus-kill-buffer url-working-buffer)))
343
344 ;; (defun nnweb-url-retrieve-asynch (url callback &rest data)
345 ;;   (let ((url-request-method "GET")
346 ;;      (old-asynch url-be-asynchronous)
347 ;;      (url-request-data nil)
348 ;;      (url-request-extra-headers nil)
349 ;;      (url-working-buffer (generate-new-buffer-name " *nnweb*")))
350 ;;     (setq-default url-be-asynchronous t)
351 ;;     (save-excursion
352 ;;       (set-buffer (get-buffer-create url-working-buffer))
353 ;;       (setq url-current-callback-data data
354 ;;          url-be-asynchronous t
355 ;;          url-current-callback-func callback)
356 ;;       (url-retrieve url nil))
357 ;;     (setq-default url-be-asynchronous old-asynch)))
358
359 ;; (if (fboundp 'url-retrieve-synchronously)
360 ;;     (defun nnweb-url-retrieve-asynch (url callback &rest data)
361 ;;       (url-retrieve url callback data)))
362
363 ;;;
364 ;;; DejaNews functions.
365 ;;;
366
367 (defun nnweb-dejanews-create-mapping ()
368   "Perform the search and create an number-to-url alist."
369   (save-excursion
370     (set-buffer nnweb-buffer)
371     (erase-buffer)
372     (when (funcall (nnweb-definition 'search) nnweb-search)
373       (let ((i 0)
374             (more t)
375             (case-fold-search t)
376             (active (or (cadr (assoc nnweb-group nnweb-group-alist))
377                         (cons 1 0)))
378             subject date from
379             map url parse a table group text)
380         (while more
381           ;; Go through all the article hits on this page.
382           (goto-char (point-min))
383           (setq parse (w3-parse-buffer (current-buffer))
384                 table (nth 1 (nnweb-parse-find-all 'table parse)))
385           (dolist (row (nth 2 (car (nth 2 table))))
386             (setq a (nnweb-parse-find 'a row)
387                   url (cdr (assq 'href (nth 1 a)))
388                   text (nreverse (nnweb-text row)))
389             (when a
390               (setq subject (nth 4 text)
391                     group (nth 2 text)
392                     date (nth 1 text)
393                     from (nth 0 text))
394               (if (string-match "\\([0-9]+\\)/\\([0-9]+\\)/\\([0-9]+\\)" date)
395                   (setq date (format "%s %s 00:00:00 %s"
396                                      (car (rassq (string-to-number
397                                                   (match-string 2 date))
398                                                  parse-time-months))
399                                      (match-string 3 date)
400                                      (match-string 1 date)))
401                 (setq date "Jan 1 00:00:00 0000"))
402               (incf i)
403               (setq url (concat url "&fmt=text"))
404               (when (string-match "&context=[^&]+" url)
405                 (setq url (replace-match "" t t url)))
406               (unless (nnweb-get-hashtb url)
407                 (push
408                  (list
409                   (incf (cdr active))
410                   (make-full-mail-header
411                    (cdr active) (concat subject " (" group ")") from date
412                    (concat "<" (nnweb-identifier url) "@dejanews>")
413                    nil 0 0 url))
414                  map)
415                 (nnweb-set-hashtb (cadar map) (car map)))))
416           ;; See whether there is a "Get next 20 hits" button here.
417           (goto-char (point-min))
418           (if (or (not (re-search-forward
419                         "HREF=\"\\([^\"]+\\)\"[<>b]+Next result" nil t))
420                   (>= i nnweb-max-hits))
421               (setq more nil)
422             ;; Yup -- fetch it.
423             (setq more (match-string 1))
424             (erase-buffer)
425             (mm-url-insert more)))
426         ;; Return the articles in the right order.
427         (setq nnweb-articles
428               (sort (nconc nnweb-articles map) 'car-less-than-car))))))
429
430 (defun nnweb-dejanews-search (search)
431   (mm-url-insert
432    (concat
433     (nnweb-definition 'address)
434     "?"
435     (mm-url-encode-www-form-urlencoded
436      `(("ST" . "PS")
437        ("svcclass" . "dnyr")
438        ("QRY" . ,search)
439        ("defaultOp" . "AND")
440        ("DBS" . "1")
441        ("OP" . "dnquery.xp")
442        ("LNG" . "ALL")
443        ("maxhits" . "100")
444        ("threaded" . "0")
445        ("format" . "verbose2")
446        ("showsort" . "date")
447        ("agesign" . "1")
448        ("ageweight" . "1")))))
449   t)
450
451 ;; (defun nnweb-dejanewsold-search (search)
452 ;;   (nnweb-fetch-form
453 ;;    (nnweb-definition 'address)
454 ;;    `(("query" . ,search)
455 ;;      ("defaultOp" . "AND")
456 ;;      ("svcclass" . "dnold")
457 ;;      ("maxhits" . "100")
458 ;;      ("format" . "verbose2")
459 ;;      ("threaded" . "0")
460 ;;      ("showsort" . "date")
461 ;;      ("agesign" . "1")
462 ;;      ("ageweight" . "1")))
463 ;;   t)
464
465 (defun nnweb-dejanews-identity (url)
466   "Return an unique identifier based on URL."
467   (if (string-match "AN=\\([0-9]+\\)" url)
468       (match-string 1 url)
469     url))
470
471 ;;;
472 ;;; InReference
473 ;;;
474
475 (defun nnweb-reference-create-mapping ()
476   "Perform the search and create an number-to-url alist."
477   (save-excursion
478     (set-buffer nnweb-buffer)
479     (erase-buffer)
480     (when (funcall (nnweb-definition 'search) nnweb-search)
481       (let ((i 0)
482             (more t)
483             (case-fold-search t)
484             (active (or (cadr (assoc nnweb-group nnweb-group-alist))
485                         (cons 1 0)))
486             Subject Score Date Newsgroups From Message-ID
487             map url)
488         (while more
489           ;; Go through all the article hits on this page.
490           (goto-char (point-min))
491           (search-forward "</pre><hr>" nil t)
492           (delete-region (point-min) (point))
493           (goto-char (point-min))
494           (while (re-search-forward "^ +[0-9]+\\." nil t)
495             (narrow-to-region
496              (point)
497              (if (re-search-forward "^$" nil t)
498                  (match-beginning 0)
499                (point-max)))
500             (goto-char (point-min))
501             (when (looking-at ".*href=\"\\([^\"]+\\)\"")
502               (setq url (match-string 1)))
503             (mm-url-remove-markup)
504             (goto-char (point-min))
505             (while (search-forward "\t" nil t)
506               (replace-match " "))
507             (goto-char (point-min))
508             (while (re-search-forward "^\\([^:]+\\): \\(.*\\)$" nil t)
509               (set (intern (match-string 1)) (match-string 2)))
510             (widen)
511             (search-forward "</pre>" nil t)
512             (incf i)
513             (unless (nnweb-get-hashtb url)
514               (push
515                (list
516                 (incf (cdr active))
517                 (make-full-mail-header
518                  (cdr active) (concat  "(" Newsgroups ") " Subject) From Date
519                  Message-ID
520                  nil 0 (string-to-int Score) url))
521                map)
522               (nnweb-set-hashtb (cadar map) (car map))))
523           (setq more nil))
524         ;; Return the articles in the right order.
525         (setq nnweb-articles
526               (sort (nconc nnweb-articles map) 'car-less-than-car))))))
527
528 (defun nnweb-reference-wash-article ()
529   (let ((case-fold-search t))
530     (goto-char (point-min))
531     (re-search-forward "^</center><hr>" nil t)
532     (delete-region (point-min) (point))
533     (search-forward "<pre>" nil t)
534     (forward-line -1)
535     (let ((body (point-marker)))
536       (search-forward "</pre>" nil t)
537       (delete-region (point) (point-max))
538       (mm-url-remove-markup)
539       (goto-char (point-min))
540       (while (looking-at " *$")
541         (gnus-delete-line))
542       (narrow-to-region (point-min) body)
543       (while (and (re-search-forward "^$" nil t)
544                   (not (eobp)))
545         (gnus-delete-line))
546       (goto-char (point-min))
547       (while (looking-at "\\(^[^ ]+:\\) *")
548         (replace-match "\\1 " t)
549         (forward-line 1))
550       (goto-char (point-min))
551       (when (re-search-forward "^References:" nil t)
552         (narrow-to-region
553          (point) (if (re-search-forward "^$\\|^[^:]+:" nil t)
554                      (match-beginning 0)
555                    (point-max)))
556         (goto-char (point-min))
557         (while (not (eobp))
558           (unless (looking-at "References")
559             (insert "\t")
560             (forward-line 1)))
561         (goto-char (point-min))
562         (while (search-forward "," nil t)
563           (replace-match " " t t)))
564       (widen)
565       (mm-url-decode-entities)
566       (set-marker body nil))))
567
568 (defun nnweb-reference-search (search)
569   (mm-url-insert
570    (concat
571     (nnweb-definition 'address)
572     "?"
573     (mm-url-encode-www-form-urlencoded
574      `(("search" . "advanced")
575        ("querytext" . ,search)
576        ("subj" . "")
577        ("name" . "")
578        ("login" . "")
579        ("host" . "")
580        ("organization" . "")
581        ("groups" . "")
582        ("keywords" . "")
583        ("choice" . "Search")
584        ("startmonth" . "Jul")
585        ("startday" . "25")
586        ("startyear" . "1996")
587        ("endmonth" . "Aug")
588        ("endday" . "24")
589        ("endyear" . "1996")
590        ("mode" . "Quick")
591        ("verbosity" . "Verbose")
592        ("ranking" . "Relevance")
593        ("first" . "1")
594        ("last" . "25")
595        ("score" . "50")))))
596   (setq buffer-file-name nil)
597   t)
598
599 ;;;
600 ;;; Alta Vista
601 ;;;
602
603 (defun nnweb-altavista-create-mapping ()
604   "Perform the search and create an number-to-url alist."
605   (save-excursion
606     (set-buffer nnweb-buffer)
607     (erase-buffer)
608     (let ((part 0))
609       (when (funcall (nnweb-definition 'search) nnweb-search part)
610         (let ((i 0)
611               (more t)
612               (case-fold-search t)
613               (active (or (cadr (assoc nnweb-group nnweb-group-alist))
614                           (cons 1 0)))
615               subject date from id group
616               map url)
617           (while more
618             ;; Go through all the article hits on this page.
619             (goto-char (point-min))
620             (search-forward "<dt>" nil t)
621             (delete-region (point-min) (match-beginning 0))
622             (goto-char (point-min))
623             (while (search-forward "<dt>" nil t)
624               (replace-match "\n<blubb>"))
625             (mm-url-decode-entities)
626             (goto-char (point-min))
627             (while (re-search-forward "<blubb>.*href=\"\\([^\"]+\\)\"><strong>\\([^>]*\\)</strong></a><dd>\\([^-]+\\)- <b>\\([^<]+\\)<.*href=\"news:\\([^\"]+\\)\">.*\">\\(.+\\)</a><P>"
628                                       nil t)
629               (setq url (match-string 1)
630                     subject (match-string 2)
631                     date (match-string 3)
632                     group (match-string 4)
633                     id (concat "<" (match-string 5) ">")
634                     from (match-string 6))
635               (incf i)
636               (unless (nnweb-get-hashtb url)
637                 (push
638                  (list
639                   (incf (cdr active))
640                   (make-full-mail-header
641                    (cdr active) (concat  "(" group ") " subject) from date
642                    id nil 0 0 url))
643                  map)
644                 (nnweb-set-hashtb (cadar map) (car map))))
645             ;; See if we want more.
646             (when (or (not nnweb-articles)
647                       (>= i nnweb-max-hits)
648                       (not (funcall (nnweb-definition 'search)
649                                     nnweb-search (incf part))))
650               (setq more nil)))
651           ;; Return the articles in the right order.
652           (setq nnweb-articles
653                 (sort (nconc nnweb-articles map) 'car-less-than-car)))))))
654
655 (defun nnweb-altavista-wash-article ()
656   (goto-char (point-min))
657   (let ((case-fold-search t))
658     (when (re-search-forward "^<strong>" nil t)
659       (delete-region (point-min) (match-beginning 0)))
660     (goto-char (point-min))
661     (while (looking-at "<strong>\\([^ ]+\\) +</strong> +\\(.*\\)$")
662       (replace-match "\\1: \\2" t)
663       (forward-line 1))
664     (when (re-search-backward "^References:" nil t)
665       (narrow-to-region (point) (progn (forward-line 1) (point)))
666       (goto-char (point-min))
667       (while (re-search-forward "<A.*\\?id@\\([^\"]+\\)\">[0-9]+</A>" nil t)
668         (replace-match "&lt;\\1&gt; " t)))
669     (widen)
670     (mm-url-remove-markup)
671     (mm-url-decode-entities)))
672
673 (defun nnweb-altavista-search (search &optional part)
674   (mm-url-insert
675    (concat
676     (nnweb-definition 'address)
677     "?"
678     (mm-url-encode-www-form-urlencoded
679      `(("pg" . "aq")
680        ("what" . "news")
681        ,@(when part `(("stq" . ,(int-to-string (* part 30)))))
682        ("fmt" . "d")
683        ("q" . ,search)
684        ("r" . "")
685        ("d0" . "")
686        ("d1" . "")))))
687   (setq buffer-file-name nil)
688   t)
689
690 ;;;
691 ;;; Deja bought by google.com
692 ;;;
693
694 (defun nnweb-google-wash-article ()
695   (let ((case-fold-search t) url)
696     (goto-char (point-min))
697     (re-search-forward "^<pre>" nil t)
698     (narrow-to-region (point-min) (point))
699     (search-backward "<table " nil t 2)
700     (delete-region (point-min) (point))
701     (if (re-search-forward "Search Result [0-9]+" nil t)
702         (replace-match ""))
703     (if (re-search-forward "View complete thread ([0-9]+ articles?)" nil t)
704         (replace-match ""))
705     (goto-char (point-min))
706     (while (search-forward "<br>" nil t)
707       (replace-match "\n"))
708     (mm-url-remove-markup)
709     (goto-char (point-min))
710     (while (re-search-forward "^[ \t]*\n" nil t)
711       (replace-match ""))
712     (goto-char (point-max))
713     (insert "\n")
714     (widen)
715     (narrow-to-region (point) (point-max))
716     (search-forward "</pre>" nil t)
717     (delete-region (point) (point-max))
718     (mm-url-remove-markup)
719     (widen)))
720
721 (defun nnweb-google-parse-1 (&optional Message-ID)
722   (let ((i 0)
723         (case-fold-search t)
724         (active (cadr (assoc nnweb-group nnweb-group-alist)))
725         Subject Score Date Newsgroups From
726         map url mid)
727     (unless active
728       (push (list nnweb-group (setq active (cons 1 0))
729                   nnweb-type nnweb-search)
730             nnweb-group-alist))
731     ;; Go through all the article hits on this page.
732     (goto-char (point-min))
733     (while (re-search-forward
734             "a href=/groups\\(\\?[^ \">]*selm=\\([^ &\">]+\\)\\)" nil t)
735       (setq mid (match-string 2)
736             url (format
737                  "http://groups.google.com/groups?selm=%s&output=gplain" mid))
738       (narrow-to-region (search-forward ">" nil t)
739                         (search-forward "</a>" nil t))
740       (mm-url-remove-markup)
741       (mm-url-decode-entities)
742       (setq Subject (buffer-string))
743       (goto-char (point-max))
744       (widen)
745       (forward-line 1)
746       (when (looking-at "<br><font[^>]+>")
747         (goto-char (match-end 0)))
748       (if (not (looking-at "<a[^>]+>"))
749           (skip-chars-forward " \t")
750         (narrow-to-region (point)
751                           (search-forward "</a>" nil t))
752         (mm-url-remove-markup)
753         (mm-url-decode-entities)
754         (setq Newsgroups (buffer-string))
755         (goto-char (point-max))
756         (widen)
757         (skip-chars-forward "- \t"))
758       (when (looking-at
759              "\\([0-9]+\\)[/ ]\\([A-Za-z]+\\)[/ ]\\([0-9]+\\)[ \t]*by[ \t]*\\([^<]*\\) - <a")
760         (setq From (match-string 4)
761               Date (format "%s %s 00:00:00 %s"
762                            (match-string 2) (match-string 1)
763                            (match-string 3))))
764       (forward-line 1)
765       (incf i)
766       (unless (nnweb-get-hashtb url)
767         (push
768          (list
769           (incf (cdr active))
770           (make-full-mail-header
771            (cdr active) (if Newsgroups
772                             (concat  "(" Newsgroups ") " Subject)
773                           Subject)
774            From Date (or Message-ID mid)
775            nil 0 0 url))
776          map)
777         (nnweb-set-hashtb (cadar map) (car map))))
778     map))
779
780 (defun nnweb-google-reference (id)
781   (let ((map (nnweb-google-parse-1 id)) header)
782     (setq nnweb-articles
783           (nconc nnweb-articles map))
784     (when (setq header (cadar map))
785       (mm-with-unibyte-current-buffer
786         (mm-url-insert (mail-header-xref header)))
787       (caar map))))
788
789 (defun nnweb-google-create-mapping ()
790   "Perform the search and create an number-to-url alist."
791   (save-excursion
792     (set-buffer nnweb-buffer)
793     (erase-buffer)
794     (when (funcall (nnweb-definition 'search) nnweb-search)
795         (let ((more t)
796               (i 0))
797           (while more
798             (setq nnweb-articles
799                   (nconc nnweb-articles (nnweb-google-parse-1)))
800             ;; Check if there are more articles to fetch
801             (goto-char (point-min))
802             (incf i 100)
803             (if (or (not (re-search-forward
804                           "<td nowrap><a href=\\([^>]+\\).*<span class=b>Next</span>" nil t))
805                     (>= i nnweb-max-hits))
806                 (setq more nil)
807               ;; Yup, there are more articles
808               (setq more (concat "http://groups.google.com" (match-string 1)))
809             (when more
810               (erase-buffer)
811               (mm-url-insert more))))
812           ;; Return the articles in the right order.
813           (setq nnweb-articles
814                 (sort nnweb-articles 'car-less-than-car))))))
815
816 (defun nnweb-google-search (search)
817   (mm-url-insert
818    (concat
819     (nnweb-definition 'address)
820     "?"
821     (mm-url-encode-www-form-urlencoded
822      `(("q" . ,search)
823        ("num". "100")
824        ("hq" . "")
825        ("hl" . "")
826        ("lr" . "")
827        ("safe" . "off")
828        ("sites" . "groups")))))
829   t)
830
831 (defun nnweb-google-identity (url)
832   "Return an unique identifier based on URL."
833   (if (string-match "selm=\\([^ &>]+\\)" url)
834       (match-string 1 url)
835     url))
836
837 ;;;
838 ;;; General web/w3 interface utility functions
839 ;;;
840
841 (defun nnweb-insert-html (parse)
842   "Insert HTML based on a w3 parse tree."
843   (if (stringp parse)
844       (insert (nnheader-string-as-multibyte parse))
845     (insert "<" (symbol-name (car parse)) " ")
846     (insert (mapconcat
847              (lambda (param)
848                (concat (symbol-name (car param)) "="
849                        (prin1-to-string
850                         (if (consp (cdr param))
851                             (cadr param)
852                           (cdr param)))))
853              (nth 1 parse)
854              " "))
855     (insert ">\n")
856     (mapcar 'nnweb-insert-html (nth 2 parse))
857     (insert "</" (symbol-name (car parse)) ">\n")))
858
859 (defun nnweb-parse-find (type parse &optional maxdepth)
860   "Find the element of TYPE in PARSE."
861   (catch 'found
862     (nnweb-parse-find-1 type parse maxdepth)))
863
864 (defun nnweb-parse-find-1 (type contents maxdepth)
865   (when (or (null maxdepth)
866             (not (zerop maxdepth)))
867     (when (consp contents)
868       (when (eq (car contents) type)
869         (throw 'found contents))
870       (when (listp (cdr contents))
871         (dolist (element contents)
872           (when (consp element)
873             (nnweb-parse-find-1 type element
874                                 (and maxdepth (1- maxdepth)))))))))
875
876 (defun nnweb-parse-find-all (type parse)
877   "Find all elements of TYPE in PARSE."
878   (catch 'found
879     (nnweb-parse-find-all-1 type parse)))
880
881 (defun nnweb-parse-find-all-1 (type contents)
882   (let (result)
883     (when (consp contents)
884       (if (eq (car contents) type)
885           (push contents result)
886         (when (listp (cdr contents))
887           (dolist (element contents)
888             (when (consp element)
889               (setq result
890                     (nconc result (nnweb-parse-find-all-1 type element))))))))
891     result))
892
893 (defvar nnweb-text)
894 (defun nnweb-text (parse)
895   "Return a list of text contents in PARSE."
896   (let ((nnweb-text nil))
897     (nnweb-text-1 parse)
898     (nreverse nnweb-text)))
899
900 (defun nnweb-text-1 (contents)
901   (dolist (element contents)
902     (if (stringp element)
903         (push element nnweb-text)
904       (when (and (consp element)
905                  (listp (cdr element)))
906         (nnweb-text-1 element)))))
907
908 (provide 'nnweb)
909
910 ;;; nnweb.el ends here