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