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