T-gnus 6.16.2 r01.
[elisp/gnus.git-] / lisp / nnrss.el
1 ;;; nnrss.el --- interfacing with RSS
2 ;; Copyright (C) 2001, 2002, 2003  Free Software Foundation, Inc.
3
4 ;; Author: Shenghuo Zhu <zsh@cs.rochester.edu>
5 ;; Keywords: RSS
6
7 ;; This file is part of GNU Emacs.
8
9 ;; GNU Emacs is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published
11 ;; by the Free Software Foundation; either version 2, or (at your
12 ;; option) any later version.
13
14 ;; GNU Emacs is distributed in the hope that it will be useful, but
15 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 ;; General Public License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
21 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
22 ;; Boston, MA 02111-1307, USA.
23
24 ;;; Commentary:
25
26 ;;; Code:
27
28 (eval-when-compile (require 'cl))
29
30 (require 'gnus)
31 (require 'nnoo)
32 (require 'nnmail)
33 (require 'message)
34 (require 'mm-util)
35 (require 'gnus-util)
36 (require 'time-date)
37 (require 'rfc2231)
38 (require 'mm-url)
39 (eval-when-compile
40   (ignore-errors
41     (require 'xml)))
42 (eval '(require 'xml))
43
44 (nnoo-declare nnrss)
45
46 (defvoo nnrss-directory (nnheader-concat gnus-directory "rss/")
47   "Where nnrss will save its files.")
48
49 ;; (group max rss-url)
50 (defvoo nnrss-server-data nil)
51
52 ;; (num timestamp url subject author date extra)
53 (defvoo nnrss-group-data nil)
54 (defvoo nnrss-group-max 0)
55 (defvoo nnrss-group-min 1)
56 (defvoo nnrss-group nil)
57 (defvoo nnrss-group-hashtb nil)
58 (defvoo nnrss-status-string "")
59
60 (defconst nnrss-version "nnrss 1.0")
61
62 (defvar nnrss-group-alist '()
63   "List of RSS addresses.")
64
65 (defvar nnrss-use-local nil)
66
67 (defvar nnrss-description-field 'X-Gnus-Description
68   "Field name used for DESCRIPTION.
69 To use the description in headers, put this name into `nnmail-extra-headers'.")
70
71 (defvar nnrss-url-field 'X-Gnus-Url
72   "Field name used for URL.
73 To use the description in headers, put this name into `nnmail-extra-headers'.")
74
75 (defvar nnrss-content-function nil
76   "A function which is called in `nnrss-request-article'.
77 The arguments are (ENTRY GROUP ARTICLE).
78 ENTRY is the record of the current headline. GROUP is the group name.
79 ARTICLE is the article number of the current headline.")
80
81 (nnoo-define-basics nnrss)
82
83 ;;; Interface functions
84
85 (deffoo nnrss-retrieve-headers (articles &optional group server fetch-old)
86   (nnrss-possibly-change-group group server)
87   (let (e)
88     (save-excursion
89       (set-buffer nntp-server-buffer)
90       (erase-buffer)
91       (dolist (article articles)
92         (if (setq e (assq article nnrss-group-data))
93             (insert (number-to-string (car e)) "\t" ;; number
94                     (if (nth 3 e)
95                         (nnrss-format-string (nth 3 e)) "")
96                     "\t" ;; subject
97                     (if (nth 4 e)
98                         (nnrss-format-string (nth 4 e))
99                       "(nobody)")
100                     "\t" ;;from
101                     (or (nth 5 e) "")
102                     "\t" ;; date
103                     (format "<%d@%s.nnrss>" (car e) group)
104                     "\t" ;; id
105                     "\t" ;; refs
106                     "-1" "\t" ;; chars
107                     "-1" "\t" ;; lines
108                     "" "\t" ;; Xref
109                     (if (and (nth 6 e)
110                              (memq nnrss-description-field
111                                    nnmail-extra-headers))
112                         (concat (symbol-name nnrss-description-field)
113                                 ": "
114                                 (nnrss-format-string (nth 6 e))
115                                 "\t")
116                       "")
117                     (if (and (nth 2 e)
118                              (memq nnrss-url-field
119                                    nnmail-extra-headers))
120                         (concat (symbol-name nnrss-url-field)
121                                 ": "
122                                 (nnrss-format-string (nth 2 e))
123                                 "\t")
124                       "")
125                     "\n")))))
126   'nov)
127
128 (deffoo nnrss-request-group (group &optional server dont-check)
129   (nnrss-possibly-change-group group server)
130   (if dont-check
131       t
132     (nnrss-check-group group server)
133     (nnheader-report 'nnrss "Opened group %s" group)
134     (nnheader-insert
135      "211 %d %d %d %s\n" nnrss-group-max nnrss-group-min nnrss-group-max
136      (prin1-to-string group)
137      t)))
138
139 (deffoo nnrss-close-group (group &optional server)
140   t)
141
142 (deffoo nnrss-request-article (article &optional group server buffer)
143   (nnrss-possibly-change-group group server)
144   (let ((e (assq article nnrss-group-data))
145         (boundary "=-=-=-=-=-=-=-=-=-")
146         (nntp-server-buffer (or buffer nntp-server-buffer))
147         post err)
148     (when e
149       (catch 'error
150         (with-current-buffer nntp-server-buffer
151           (erase-buffer)
152           (goto-char (point-min))
153           (insert "Mime-Version: 1.0\nContent-Type: multipart/alternative; boundary=\"" boundary "\"\n")
154           (if group
155               (insert "Newsgroups: " group "\n"))
156           (if (nth 3 e)
157               (insert "Subject: " (nnrss-format-string (nth 3 e)) "\n"))
158           (if (nth 4 e)
159               (insert "From: " (nnrss-format-string (nth 4 e)) "\n"))
160           (if (nth 5 e)
161               (insert "Date: " (nnrss-format-string (nth 5 e)) "\n"))
162           (insert "Message-ID: " (format "<%d@%s.nnrss>" (car e) group) "\n")
163           (insert "\n")
164           (let ((text (if (nth 6 e)
165                           (nnrss-string-as-multibyte (nth 6 e))))
166                 (link (if (nth 2 e)
167                           (nth 2 e))))
168             (insert "\n\n--" boundary "\nContent-Type: text/plain\n\n")
169             (let ((point (point)))
170               (if text
171                   (progn (insert text)
172                          (goto-char point)
173                          (while (re-search-forward "\n" nil t)
174                            (replace-match " "))
175                          (goto-char (point-max))
176                          (insert "\n\n")))
177               (if link
178                   (insert link)))
179             (insert "\n\n--" boundary "\nContent-Type: text/html\n\n")
180             (let ((point (point)))
181               (if text
182                   (progn (insert "<html><head></head><body>\n" text "\n</body></html>")
183                          (goto-char point)
184                          (while (re-search-forward "\n" nil t)
185                            (replace-match " "))
186                          (goto-char (point-max))
187                          (insert "\n\n")))
188               (if link
189                   (insert "<p><a href=\"" link "\">link</a></p>\n"))))
190           (if nnrss-content-function
191               (funcall nnrss-content-function e group article)))))
192     (cond
193      (err
194       (nnheader-report 'nnrss err))
195      ((not e)
196       (nnheader-report 'nnrss "no such id: %d" article))
197      (t
198       (nnheader-report 'nnrss "article %s retrieved" (car e))
199       ;; we return the article number.
200       (cons nnrss-group (car e))))))
201
202 (deffoo nnrss-request-list (&optional server)
203   (nnrss-possibly-change-group nil server)
204   (nnrss-generate-active)
205   t)
206
207 (deffoo nnrss-open-server (server &optional defs connectionless)
208   (nnrss-read-server-data server)
209   (nnoo-change-server 'nnrss server defs)
210   t)
211
212 (deffoo nnrss-request-expire-articles
213     (articles group &optional server force)
214   (nnrss-possibly-change-group group server)
215   (let (e days not-expirable changed)
216     (dolist (art articles)
217       (if (and (setq e (assq art nnrss-group-data))
218                (nnmail-expired-article-p
219                 group
220                 (if (listp (setq days (nth 1 e))) days
221                   (days-to-time (- days (time-to-days '(0 0)))))
222                 force))
223           (setq nnrss-group-data (delq e nnrss-group-data)
224                 changed t)
225         (push art not-expirable)))
226     (if changed
227         (nnrss-save-group-data group server))
228     not-expirable))
229
230 (deffoo nnrss-request-delete-group (group &optional force server)
231   (nnrss-possibly-change-group group server)
232   (setq nnrss-server-data
233         (delq (assoc group nnrss-server-data) nnrss-server-data))
234   (nnrss-save-server-data server)
235   (let ((file (expand-file-name
236                (nnrss-translate-file-chars
237                 (concat group (and server
238                                    (not (equal server ""))
239                                    "-")
240                         server ".el")) nnrss-directory)))
241     (ignore-errors
242       (delete-file file)))
243   t)
244
245 (deffoo nnrss-request-list-newsgroups (&optional server)
246   (nnrss-possibly-change-group nil server)
247   (save-excursion
248     (set-buffer nntp-server-buffer)
249     (erase-buffer)
250     (dolist (elem nnrss-group-alist)
251       (if (third elem)
252           (insert (car elem) "\t" (third elem) "\n"))))
253   t)
254
255 (nnoo-define-skeleton nnrss)
256
257 ;;; Internal functions
258 (eval-when-compile (defun xml-rpc-method-call (&rest args)))
259 (defun nnrss-fetch (url &optional local)
260   "Fetch the url and put it in a the expected lisp structure."
261   (with-temp-buffer
262   ;some CVS versions of url.el need this to close the connection quickly
263     (let* (xmlform htmlform)
264       ;; bit o' work necessary for w3 pre-cvs and post-cvs
265       (if local
266           (let ((coding-system-for-read 'binary))
267             (insert-file-contents url))
268         (mm-url-insert url))
269
270 ;; Because xml-parse-region can't deal with anything that isn't
271 ;; xml and w3-parse-buffer can't deal with some xml, we have to
272 ;; parse with xml-parse-region first and, if that fails, parse
273 ;; with w3-parse-buffer.  Yuck.  Eventually, someone should find out
274 ;; why w3-parse-buffer fails to parse some well-formed xml and
275 ;; fix it.
276
277     (condition-case err
278         (setq xmlform (xml-parse-region (point-min) (point-max)))
279       (error (if (fboundp 'w3-parse-buffer)
280                  (setq htmlform (caddar (w3-parse-buffer
281                                          (current-buffer))))
282                (message "nnrss: Not valid XML and w3 parse not available (%s)"
283                         url))))
284     (if htmlform
285         htmlform
286       xmlform))))
287
288 (defun nnrss-possibly-change-group (&optional group server)
289   (when (and server
290              (not (nnrss-server-opened server)))
291     (nnrss-open-server server))
292   (when (and group (not (equal group nnrss-group)))
293     (nnrss-read-group-data group server)
294     (setq nnrss-group group)))
295
296 (defvar nnrss-extra-categories '(nnrss-snarf-moreover-categories))
297
298 (defun nnrss-generate-active ()
299   (if (y-or-n-p "fetch extra categories? ")
300       (dolist (func nnrss-extra-categories)
301         (funcall func)))
302   (save-excursion
303     (set-buffer nntp-server-buffer)
304     (erase-buffer)
305     (dolist (elem nnrss-group-alist)
306       (insert (prin1-to-string (car elem)) " 0 1 y\n"))
307     (dolist (elem nnrss-server-data)
308       (unless (assoc (car elem) nnrss-group-alist)
309         (insert (prin1-to-string (car elem)) " 0 1 y\n")))))
310
311 ;;; data functions
312
313 (defun nnrss-read-server-data (server)
314   (setq nnrss-server-data nil)
315   (let ((file (expand-file-name
316                (nnrss-translate-file-chars
317                 (concat "nnrss" (and server
318                                      (not (equal server ""))
319                                      "-")
320                         server
321                         ".el"))
322                nnrss-directory)))
323     (when (file-exists-p file)
324       (with-temp-buffer
325         (let ((coding-system-for-read 'binary)
326               (input-coding-system 'binary)
327               emacs-lisp-mode-hook)
328           (insert-file-contents file)
329           (emacs-lisp-mode)
330           (goto-char (point-min))
331           (eval-buffer))))))
332
333 (defun nnrss-save-server-data (server)
334   (gnus-make-directory nnrss-directory)
335   (let ((file (expand-file-name
336                (nnrss-translate-file-chars
337                 (concat "nnrss" (and server
338                                      (not (equal server ""))
339                                      "-")
340                         server ".el"))
341                nnrss-directory)))
342     (let ((coding-system-for-write 'binary)
343           (output-coding-system 'binary)
344           print-level print-length)
345       (with-temp-file file
346         (insert "(setq nnrss-group-alist '"
347                 (prin1-to-string nnrss-group-alist)
348                 ")\n")
349         (insert "(setq nnrss-server-data '"
350                 (prin1-to-string nnrss-server-data)
351                 ")\n")))))
352
353 (defun nnrss-read-group-data (group server)
354   (setq nnrss-group-data nil)
355   (setq nnrss-group-hashtb (gnus-make-hashtable))
356   (let ((pair (assoc group nnrss-server-data)))
357     (setq nnrss-group-max (or (cadr pair) 0))
358     (setq nnrss-group-min (+ nnrss-group-max 1)))
359   (let ((file (expand-file-name
360                (nnrss-translate-file-chars
361                 (concat group (and server
362                                    (not (equal server ""))
363                                    "-")
364                         server ".el"))
365                nnrss-directory)))
366     (when (file-exists-p file)
367       (with-temp-buffer
368         (let ((coding-system-for-read 'binary)
369               (input-coding-system 'binary)
370               emacs-lisp-mode-hook)
371           (insert-file-contents file)
372           (emacs-lisp-mode)
373           (goto-char (point-min))
374           (eval-buffer)))
375       (dolist (e nnrss-group-data)
376         (gnus-sethash (nth 2 e) e nnrss-group-hashtb)
377         (if (and (car e) (> nnrss-group-min (car e)))
378             (setq nnrss-group-min (car e)))
379         (if (and (car e) (< nnrss-group-max (car e)))
380             (setq nnrss-group-max (car e)))))))
381
382 (defun nnrss-save-group-data (group server)
383   (gnus-make-directory nnrss-directory)
384   (let ((file (expand-file-name
385                (nnrss-translate-file-chars
386                 (concat group (and server
387                                    (not (equal server ""))
388                                    "-")
389                         server ".el"))
390                nnrss-directory)))
391     (let ((coding-system-for-write 'binary)
392           (output-coding-system 'binary)
393           print-level print-length)
394       (with-temp-file file
395         (insert "(setq nnrss-group-data '"
396                 (prin1-to-string nnrss-group-data)
397                 ")\n")))))
398
399 ;;; URL interface
400
401 (defun nnrss-no-cache (url)
402   "")
403
404 (defun nnrss-insert-w3 (url)
405   (mm-with-unibyte-current-buffer
406     (mm-url-insert url)))
407
408 (defun nnrss-decode-entities-unibyte-string (string)
409   (if string
410       (mm-with-unibyte-buffer
411         (insert string)
412         (mm-url-decode-entities-nbsp)
413         (buffer-string))))
414
415 (defalias 'nnrss-insert 'nnrss-insert-w3)
416
417 (if (featurep 'xemacs)
418     (defalias 'nnrss-string-as-multibyte 'identity)
419   (defalias 'nnrss-string-as-multibyte 'string-as-multibyte))
420
421 ;;; Snarf functions
422
423 (defun nnrss-check-group (group server)
424   (let (file xml subject url extra changed author
425              date rss-ns rdf-ns content-ns dc-ns)
426     (if (and nnrss-use-local
427              (file-exists-p (setq file (expand-file-name
428                                         (nnrss-translate-file-chars
429                                          (concat group ".xml"))
430                                         nnrss-directory))))
431         (nnrss-fetch file t)
432       (setq url (or (nth 2 (assoc group nnrss-server-data))
433                     (second (assoc group nnrss-group-alist))))
434       (unless url
435         (setq url
436              (cdr
437               (assoc 'href
438                      (nnrss-discover-feed
439                       (read-string
440                        (format "URL to search for %s: " group) "http://")))))
441         (let ((pair (assoc group nnrss-server-data)))
442           (if pair
443               (setcdr (cdr pair) (list url))
444             (push (list group nnrss-group-max url) nnrss-server-data)))
445         (setq changed t))
446       (setq xml (nnrss-fetch url)))
447     ;; See
448     ;; http://feeds.archive.org/validator/docs/howto/declare_namespaces.html
449     ;; for more RSS namespaces.
450     (setq dc-ns (nnrss-get-namespace-prefix xml "http://purl.org/dc/elements/1.1/")
451           rdf-ns (nnrss-get-namespace-prefix xml "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
452           rss-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/")
453           content-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/modules/content/"))
454     (dolist (item (nreverse (nnrss-find-el (intern (concat rss-ns "item")) xml)))
455       (when (and (listp item)
456                  (eq (intern (concat rss-ns "item")) (car item))
457                  (setq url (nnrss-decode-entities-unibyte-string
458                             (nnrss-node-text rss-ns 'link (cddr item))))
459                  (not (gnus-gethash url nnrss-group-hashtb)))
460         (setq subject (nnrss-node-text rss-ns 'title item))
461         (setq extra (or (nnrss-node-text content-ns 'encoded item)
462                         (nnrss-node-text rss-ns 'description item)))
463         (setq author (or (nnrss-node-text rss-ns 'author item)
464                          (nnrss-node-text dc-ns 'creator item)))
465         (setq date (or (nnrss-node-text dc-ns 'date item)
466                        (nnrss-node-text rss-ns 'pubDate item)
467                        (message-make-date)))
468         (push
469          (list
470           (incf nnrss-group-max)
471           (current-time)
472           url
473           (and subject (nnrss-decode-entities-unibyte-string subject))
474           (and author (nnrss-decode-entities-unibyte-string author))
475           date
476           (and extra (nnrss-decode-entities-unibyte-string extra)))
477          nnrss-group-data)
478         (gnus-sethash url (car nnrss-group-data) nnrss-group-hashtb)
479         (setq changed t)))
480     (when changed
481       (nnrss-save-group-data group server)
482       (let ((pair (assoc group nnrss-server-data)))
483         (if pair
484             (setcar (cdr pair) nnrss-group-max)
485           (push (list group nnrss-group-max) nnrss-server-data)))
486       (nnrss-save-server-data server))))
487
488 (defun nnrss-generate-download-script ()
489   "Generate a download script in the current buffer.
490 It is useful when `(setq nnrss-use-local t)'."
491   (interactive)
492   (insert "#!/bin/sh\n")
493   (insert "WGET=wget\n")
494   (insert "RSSDIR='" (expand-file-name nnrss-directory) "'\n")
495   (dolist (elem nnrss-server-data)
496     (let ((url (or (nth 2 elem)
497                    (second (assoc (car elem) nnrss-group-alist)))))
498       (insert "$WGET -q -O \"$RSSDIR\"/'"
499               (nnrss-translate-file-chars (concat (car elem) ".xml"))
500               "' '" url "'\n"))))
501
502 (defun nnrss-translate-file-chars (name)
503   (let ((nnheader-file-name-translation-alist
504          (append nnheader-file-name-translation-alist '((?' . ?_)))))
505     (nnheader-translate-file-chars name)))
506
507 (defvar nnrss-moreover-url
508   "http://w.moreover.com/categories/category_list_rss.html"
509   "The url of moreover.com categories.")
510
511 (defun nnrss-snarf-moreover-categories ()
512   "Snarf RSS links from moreover.com."
513   (interactive)
514   (let (category name url changed)
515     (with-temp-buffer
516       (nnrss-insert nnrss-moreover-url)
517       (goto-char (point-min))
518       (while (re-search-forward
519               "<a name=\"\\([^\"]+\\)\">\\|<a href=\"\\(http://[^\"]*moreover\\.com[^\"]+page\\?c=\\([^\"&]+\\)&o=rss\\)" nil t)
520         (if (match-string 1)
521             (setq category (match-string 1))
522           (setq url (match-string 2)
523                 name (mm-url-decode-entities-string
524                       (rfc2231-decode-encoded-string
525                        (match-string 3))))
526           (if category
527               (setq name (concat category "." name)))
528           (unless (assoc name nnrss-server-data)
529             (setq changed t)
530             (push (list name 0 url) nnrss-server-data)))))
531     (if changed
532         (nnrss-save-server-data ""))))
533
534 (defun nnrss-format-string (string)
535   (gnus-replace-in-string (nnrss-string-as-multibyte string) " *\n *" " "))
536
537 (defun nnrss-node-text (namespace local-name element)
538   (let* ((node (assq (intern (concat namespace (symbol-name local-name)))
539                      element))
540          (text (if (and node (listp node))
541                    (nnrss-node-just-text node)
542                  node))
543          (cleaned-text (if text (gnus-replace-in-string
544                                  text "^[\000-\037\177]+\\|^ +\\| +$" ""))))
545     (if (string-equal "" cleaned-text)
546         nil
547       cleaned-text)))
548
549 (defun nnrss-node-just-text (node)
550   (if (and node (listp node))
551       (mapconcat 'nnrss-node-just-text (cddr node) " ")
552     node))
553
554 (defun nnrss-find-el (tag data &optional found-list)
555   "Find the all matching elements in the data.  Careful with this on
556 large documents!"
557   (if (listp data)
558       (mapcar (lambda (bit)
559                 (if (car-safe bit)
560                     (progn (if (equal tag (car bit))
561                                (setq found-list
562                                      (append found-list
563                                              (list bit))))
564                            (if (and (listp (car-safe (caddr bit)))
565                                     (not (stringp (caddr bit))))
566                                (setq found-list
567                                      (append found-list
568                                              (nnrss-find-el
569                                               tag (caddr bit))))
570                              (setq found-list
571                                    (append found-list
572                                            (nnrss-find-el
573                                             tag (cddr bit))))))))
574                 data))
575   found-list)
576
577 (defun nnrss-rsslink-p (el)
578   "Test if the element we are handed is an RSS autodiscovery link."
579   (and (eq (car-safe el) 'link)
580        (string-equal (cdr (assoc 'rel (cadr el))) "alternate")
581        (or (string-equal (cdr (assoc 'type (cadr el))) 
582                          "application/rss+xml")
583            (string-equal (cdr (assoc 'type (cadr el))) "text/xml"))))
584
585 (defun nnrss-get-rsslinks (data)
586   "Extract the <link> elements that are links to RSS from the parsed data."
587   (delq nil (mapcar 
588              (lambda (el)
589                (if (nnrss-rsslink-p el) el))
590              (nnrss-find-el 'link data))))
591
592 (defun nnrss-extract-hrefs (data)
593   "Recursively extract hrefs from a page's source.  DATA should be
594 the output of xml-parse-region or w3-parse-buffer."
595   (mapcar (lambda (ahref)
596             (cdr (assoc 'href (cadr ahref))))
597           (nnrss-find-el 'a data)))
598
599 (defmacro nnrss-match-macro (base-uri item 
600                                            onsite-list offsite-list)
601   `(cond ((or (string-match (concat "^" ,base-uri) ,item)
602                (not (string-match "://" ,item)))
603            (setq ,onsite-list (append ,onsite-list (list ,item))))
604           (t (setq ,offsite-list (append ,offsite-list (list ,item))))))
605
606 (defun nnrss-order-hrefs (base-uri hrefs)
607   "Given a list of hrefs, sort them using the following priorities:
608   1. links ending in .rss
609   2. links ending in .rdf
610   3. links ending in .xml
611   4. links containing the above
612   5. offsite links
613
614 BASE-URI is used to determine the location of the links and
615 whether they are `offsite' or `onsite'."
616   (let (rss-onsite-end  rdf-onsite-end  xml-onsite-end
617         rss-onsite-in   rdf-onsite-in   xml-onsite-in
618         rss-offsite-end rdf-offsite-end xml-offsite-end
619         rss-offsite-in rdf-offsite-in xml-offsite-in)
620     (mapcar (lambda (href)
621               (if (not (null href))
622               (cond ((string-match "\\.rss$" href)
623                      (nnrss-match-macro
624                       base-uri href rss-onsite-end rss-offsite-end))
625                     ((string-match "\\.rdf$" href)
626                      (nnrss-match-macro 
627                       base-uri href rdf-onsite-end rdf-offsite-end))
628                     ((string-match "\\.xml$" href)
629                      (nnrss-match-macro
630                       base-uri href xml-onsite-end xml-offsite-end))
631                     ((string-match "rss" href)
632                      (nnrss-match-macro
633                       base-uri href rss-onsite-in rss-offsite-in))
634                     ((string-match "rdf" href)
635                      (nnrss-match-macro
636                       base-uri href rdf-onsite-in rdf-offsite-in))
637                     ((string-match "xml" href)
638                      (nnrss-match-macro
639                       base-uri href xml-onsite-in xml-offsite-in)))))
640             hrefs)
641     (append 
642      rss-onsite-end  rdf-onsite-end  xml-onsite-end
643      rss-onsite-in   rdf-onsite-in   xml-onsite-in
644      rss-offsite-end rdf-offsite-end xml-offsite-end
645      rss-offsite-in rdf-offsite-in xml-offsite-in)))
646
647 (defun nnrss-discover-feed (url)
648   "Given a page, find an RSS feed using Mark Pilgrim's
649 `ultra-liberal rss locator' (http://diveintomark.org/2002/08/15.html)."
650
651   (let ((parsed-page (nnrss-fetch url)))
652
653 ;;    1. if this url is the rss, use it.
654     (if (nnrss-rss-p parsed-page)
655         (let ((rss-ns (nnrss-get-namespace-prefix parsed-page "http://purl.org/rss/1.0/")))
656           (nnrss-rss-title-description rss-ns parsed-page url))
657
658 ;;    2. look for the <link rel="alternate"
659 ;;    type="application/rss+xml" and use that if it is there.
660       (let ((links (nnrss-get-rsslinks parsed-page)))
661         (if links
662             (let* ((xml (nnrss-fetch
663                          (cdr (assoc 'href (cadar links)))))
664                    (rss-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/")))
665               (nnrss-rss-title-description rss-ns xml (cdr (assoc 'href (cadar links)))))
666
667 ;;    3. look for links on the site in the following order:
668 ;;       - onsite links ending in .rss, .rdf, or .xml
669 ;;       - onsite links containing any of the above
670 ;;       - offsite links ending in .rss, .rdf, or .xml
671 ;;       - offsite links containing any of the above
672           (let* ((base-uri (progn (string-match ".*://[^/]+/?" url)
673                                   (match-string 0 url)))
674                  (hrefs (nnrss-order-hrefs 
675                          base-uri (nnrss-extract-hrefs parsed-page)))
676                  (rss-link nil))
677           (while (and (eq rss-link nil) (not (eq hrefs nil)))
678             (let ((href-data (nnrss-fetch (car hrefs))))
679               (if (nnrss-rss-p href-data)
680                   (let* ((rss-ns (nnrss-get-namespace-prefix href-data "http://purl.org/rss/1.0/")))
681                     (setq rss-link (nnrss-rss-title-description
682                                     rss-ns href-data (car hrefs))))
683                 (setq hrefs (cdr hrefs)))))
684           (if rss-link rss-link
685
686 ;;    4. check syndic8
687             (nnrss-find-rss-via-syndic8 url))))))))
688
689 (defun nnrss-find-rss-via-syndic8 (url)
690   "query syndic8 for the rss feeds it has for the url."
691   (if (not (locate-library "xml-rpc"))
692       (message "XML-RPC is not available... not checking Syndic8.")
693     (require 'xml-rpc)
694     (let ((feedid (xml-rpc-method-call
695                    "http://www.syndic8.com/xmlrpc.php"
696                    'syndic8.FindSites
697                    url)))
698       (when feedid
699         (let* ((feedinfo (xml-rpc-method-call 
700                           "http://www.syndic8.com/xmlrpc.php"
701                           'syndic8.GetFeedInfo
702                           feedid))
703                (urllist
704                 (delq nil 
705                       (mapcar
706                        (lambda (listinfo)
707                          (if (string-equal 
708                               (cdr (assoc "status" listinfo))
709                               "Syndicated")
710                              (cons
711                               (cdr (assoc "sitename" listinfo))
712                               (list
713                                (cons 'title
714                                      (cdr (assoc 
715                                            "sitename" listinfo)))
716                                (cons 'href
717                                      (cdr (assoc
718                                            "dataurl" listinfo)))))))
719                        feedinfo))))
720           (if (not (> (length urllist) 1))
721               (cdar urllist)
722             (let ((completion-ignore-case t)
723                   (selection 
724                    (mapcar (lambda (listinfo)
725                              (cons (cdr (assoc "sitename" listinfo)) 
726                                    (string-to-int 
727                                     (cdr (assoc "feedid" listinfo)))))
728                            feedinfo)))
729               (cdr (assoc 
730                     (completing-read
731                      "Multiple feeds found.  Select one: "
732                      selection nil t) urllist)))))))))
733
734 (defun nnrss-rss-p (data)
735   "Test if data is an RSS feed.  Simply ensures that the first
736 element is rss or rdf."
737   (or (eq (caar data) 'rss)
738       (eq (caar data) 'rdf:RDF)))
739
740 (defun nnrss-rss-title-description (rss-namespace data url)
741   "Return the title of an RSS feed."
742   (if (nnrss-rss-p data)
743       (let ((description (intern (concat rss-namespace "description")))
744             (title (intern (concat rss-namespace "title")))
745             (channel (nnrss-find-el (intern (concat rss-namespace "channel"))
746                                     data)))
747         (list
748          (cons 'description (caddr (nth 0 (nnrss-find-el description channel))))
749          (cons 'title (caddr (nth 0 (nnrss-find-el title channel))))
750          (cons 'href url)))))
751
752 (defun nnrss-get-namespace-prefix (el uri)
753   "Given EL (containing a parsed element) and URI (containing a string
754 that gives the URI for which you want to retrieve the namespace
755 prefix), return the prefix."
756   (let* ((prefix (car (rassoc uri (cadar el))))
757          (nslist (if prefix 
758                      (split-string (symbol-name prefix) ":")))
759          (ns (cond ((eq (length nslist) 1) ; no prefix given
760                     "")
761                    ((eq (length nslist) 2) ; extract prefix
762                     (cadr nslist)))))
763     (if (and ns (not (eq ns "")))
764         (concat ns ":")
765       ns)))
766
767 (provide 'nnrss)
768
769
770 ;;; nnrss.el ends here
771