Synch to Gnus 200312032205.
[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         (setq xml (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                          (nnrss-node-text dc-ns 'contributor item)))
466         (setq date (or (nnrss-node-text dc-ns 'date item)
467                        (nnrss-node-text rss-ns 'pubDate item)
468                        (message-make-date)))
469         (push
470          (list
471           (incf nnrss-group-max)
472           (current-time)
473           url
474           (and subject (nnrss-decode-entities-unibyte-string subject))
475           (and author (nnrss-decode-entities-unibyte-string author))
476           date
477           (and extra (nnrss-decode-entities-unibyte-string extra)))
478          nnrss-group-data)
479         (gnus-sethash url (car nnrss-group-data) nnrss-group-hashtb)
480         (setq changed t)))
481     (when changed
482       (nnrss-save-group-data group server)
483       (let ((pair (assoc group nnrss-server-data)))
484         (if pair
485             (setcar (cdr pair) nnrss-group-max)
486           (push (list group nnrss-group-max) nnrss-server-data)))
487       (nnrss-save-server-data server))))
488
489 (defun nnrss-generate-download-script ()
490   "Generate a download script in the current buffer.
491 It is useful when `(setq nnrss-use-local t)'."
492   (interactive)
493   (insert "#!/bin/sh\n")
494   (insert "WGET=wget\n")
495   (insert "RSSDIR='" (expand-file-name nnrss-directory) "'\n")
496   (dolist (elem nnrss-server-data)
497     (let ((url (or (nth 2 elem)
498                    (second (assoc (car elem) nnrss-group-alist)))))
499       (insert "$WGET -q -O \"$RSSDIR\"/'"
500               (nnrss-translate-file-chars (concat (car elem) ".xml"))
501               "' '" url "'\n"))))
502
503 (defun nnrss-translate-file-chars (name)
504   (let ((nnheader-file-name-translation-alist
505          (append nnheader-file-name-translation-alist '((?' . ?_)))))
506     (nnheader-translate-file-chars name)))
507
508 (defvar nnrss-moreover-url
509   "http://w.moreover.com/categories/category_list_rss.html"
510   "The url of moreover.com categories.")
511
512 (defun nnrss-snarf-moreover-categories ()
513   "Snarf RSS links from moreover.com."
514   (interactive)
515   (let (category name url changed)
516     (with-temp-buffer
517       (nnrss-insert nnrss-moreover-url)
518       (goto-char (point-min))
519       (while (re-search-forward
520               "<a name=\"\\([^\"]+\\)\">\\|<a href=\"\\(http://[^\"]*moreover\\.com[^\"]+page\\?c=\\([^\"&]+\\)&o=rss\\)" nil t)
521         (if (match-string 1)
522             (setq category (match-string 1))
523           (setq url (match-string 2)
524                 name (mm-url-decode-entities-string
525                       (rfc2231-decode-encoded-string
526                        (match-string 3))))
527           (if category
528               (setq name (concat category "." name)))
529           (unless (assoc name nnrss-server-data)
530             (setq changed t)
531             (push (list name 0 url) nnrss-server-data)))))
532     (if changed
533         (nnrss-save-server-data ""))))
534
535 (defun nnrss-format-string (string)
536   (gnus-replace-in-string (nnrss-string-as-multibyte string) " *\n *" " "))
537
538 (defun nnrss-node-text (namespace local-name element)
539   (let* ((node (assq (intern (concat namespace (symbol-name local-name)))
540                      element))
541          (text (if (and node (listp node))
542                    (nnrss-node-just-text node)
543                  node))
544          (cleaned-text (if text (gnus-replace-in-string
545                                  text "^[\000-\037\177]+\\|^ +\\| +$" ""))))
546     (if (string-equal "" cleaned-text)
547         nil
548       cleaned-text)))
549
550 (defun nnrss-node-just-text (node)
551   (if (and node (listp node))
552       (mapconcat 'nnrss-node-just-text (cddr node) " ")
553     node))
554
555 (defun nnrss-find-el (tag data &optional found-list)
556   "Find the all matching elements in the data.  Careful with this on
557 large documents!"
558   (if (listp data)
559       (mapcar (lambda (bit)
560                 (if (car-safe bit)
561                     (progn (if (equal tag (car bit))
562                                (setq found-list
563                                      (append found-list
564                                              (list bit))))
565                            (if (and (listp (car-safe (caddr bit)))
566                                     (not (stringp (caddr bit))))
567                                (setq found-list
568                                      (append found-list
569                                              (nnrss-find-el
570                                               tag (caddr bit))))
571                              (setq found-list
572                                    (append found-list
573                                            (nnrss-find-el
574                                             tag (cddr bit))))))))
575                 data))
576   found-list)
577
578 (defun nnrss-rsslink-p (el)
579   "Test if the element we are handed is an RSS autodiscovery link."
580   (and (eq (car-safe el) 'link)
581        (string-equal (cdr (assoc 'rel (cadr el))) "alternate")
582        (or (string-equal (cdr (assoc 'type (cadr el))) 
583                          "application/rss+xml")
584            (string-equal (cdr (assoc 'type (cadr el))) "text/xml"))))
585
586 (defun nnrss-get-rsslinks (data)
587   "Extract the <link> elements that are links to RSS from the parsed data."
588   (delq nil (mapcar 
589              (lambda (el)
590                (if (nnrss-rsslink-p el) el))
591              (nnrss-find-el 'link data))))
592
593 (defun nnrss-extract-hrefs (data)
594   "Recursively extract hrefs from a page's source.  DATA should be
595 the output of xml-parse-region or w3-parse-buffer."
596   (mapcar (lambda (ahref)
597             (cdr (assoc 'href (cadr ahref))))
598           (nnrss-find-el 'a data)))
599
600 (defmacro nnrss-match-macro (base-uri item 
601                                            onsite-list offsite-list)
602   `(cond ((or (string-match (concat "^" ,base-uri) ,item)
603                (not (string-match "://" ,item)))
604            (setq ,onsite-list (append ,onsite-list (list ,item))))
605           (t (setq ,offsite-list (append ,offsite-list (list ,item))))))
606
607 (defun nnrss-order-hrefs (base-uri hrefs)
608   "Given a list of hrefs, sort them using the following priorities:
609   1. links ending in .rss
610   2. links ending in .rdf
611   3. links ending in .xml
612   4. links containing the above
613   5. offsite links
614
615 BASE-URI is used to determine the location of the links and
616 whether they are `offsite' or `onsite'."
617   (let (rss-onsite-end  rdf-onsite-end  xml-onsite-end
618         rss-onsite-in   rdf-onsite-in   xml-onsite-in
619         rss-offsite-end rdf-offsite-end xml-offsite-end
620         rss-offsite-in rdf-offsite-in xml-offsite-in)
621     (mapcar (lambda (href)
622               (if (not (null href))
623               (cond ((string-match "\\.rss$" href)
624                      (nnrss-match-macro
625                       base-uri href rss-onsite-end rss-offsite-end))
626                     ((string-match "\\.rdf$" href)
627                      (nnrss-match-macro 
628                       base-uri href rdf-onsite-end rdf-offsite-end))
629                     ((string-match "\\.xml$" href)
630                      (nnrss-match-macro
631                       base-uri href xml-onsite-end xml-offsite-end))
632                     ((string-match "rss" href)
633                      (nnrss-match-macro
634                       base-uri href rss-onsite-in rss-offsite-in))
635                     ((string-match "rdf" href)
636                      (nnrss-match-macro
637                       base-uri href rdf-onsite-in rdf-offsite-in))
638                     ((string-match "xml" href)
639                      (nnrss-match-macro
640                       base-uri href xml-onsite-in xml-offsite-in)))))
641             hrefs)
642     (append 
643      rss-onsite-end  rdf-onsite-end  xml-onsite-end
644      rss-onsite-in   rdf-onsite-in   xml-onsite-in
645      rss-offsite-end rdf-offsite-end xml-offsite-end
646      rss-offsite-in rdf-offsite-in xml-offsite-in)))
647
648 (defun nnrss-discover-feed (url)
649   "Given a page, find an RSS feed using Mark Pilgrim's
650 `ultra-liberal rss locator' (http://diveintomark.org/2002/08/15.html)."
651
652   (let ((parsed-page (nnrss-fetch url)))
653
654 ;;    1. if this url is the rss, use it.
655     (if (nnrss-rss-p parsed-page)
656         (let ((rss-ns (nnrss-get-namespace-prefix parsed-page "http://purl.org/rss/1.0/")))
657           (nnrss-rss-title-description rss-ns parsed-page url))
658
659 ;;    2. look for the <link rel="alternate"
660 ;;    type="application/rss+xml" and use that if it is there.
661       (let ((links (nnrss-get-rsslinks parsed-page)))
662         (if links
663             (let* ((xml (nnrss-fetch
664                          (cdr (assoc 'href (cadar links)))))
665                    (rss-ns (nnrss-get-namespace-prefix xml "http://purl.org/rss/1.0/")))
666               (nnrss-rss-title-description rss-ns xml (cdr (assoc 'href (cadar links)))))
667
668 ;;    3. look for links on the site in the following order:
669 ;;       - onsite links ending in .rss, .rdf, or .xml
670 ;;       - onsite links containing any of the above
671 ;;       - offsite links ending in .rss, .rdf, or .xml
672 ;;       - offsite links containing any of the above
673           (let* ((base-uri (progn (string-match ".*://[^/]+/?" url)
674                                   (match-string 0 url)))
675                  (hrefs (nnrss-order-hrefs 
676                          base-uri (nnrss-extract-hrefs parsed-page)))
677                  (rss-link nil))
678           (while (and (eq rss-link nil) (not (eq hrefs nil)))
679             (let ((href-data (nnrss-fetch (car hrefs))))
680               (if (nnrss-rss-p href-data)
681                   (let* ((rss-ns (nnrss-get-namespace-prefix href-data "http://purl.org/rss/1.0/")))
682                     (setq rss-link (nnrss-rss-title-description
683                                     rss-ns href-data (car hrefs))))
684                 (setq hrefs (cdr hrefs)))))
685           (if rss-link rss-link
686
687 ;;    4. check syndic8
688             (nnrss-find-rss-via-syndic8 url))))))))
689
690 (defun nnrss-find-rss-via-syndic8 (url)
691   "query syndic8 for the rss feeds it has for the url."
692   (if (not (locate-library "xml-rpc"))
693       (progn
694         (message "XML-RPC is not available... not checking Syndic8.")
695         nil)
696     (require 'xml-rpc)
697     (let ((feedid (xml-rpc-method-call
698                    "http://www.syndic8.com/xmlrpc.php"
699                    'syndic8.FindSites
700                    url)))
701       (when feedid
702         (let* ((feedinfo (xml-rpc-method-call 
703                           "http://www.syndic8.com/xmlrpc.php"
704                           'syndic8.GetFeedInfo
705                           feedid))
706                (urllist
707                 (delq nil 
708                       (mapcar
709                        (lambda (listinfo)
710                          (if (string-equal 
711                               (cdr (assoc "status" listinfo))
712                               "Syndicated")
713                              (cons
714                               (cdr (assoc "sitename" listinfo))
715                               (list
716                                (cons 'title
717                                      (cdr (assoc 
718                                            "sitename" listinfo)))
719                                (cons 'href
720                                      (cdr (assoc
721                                            "dataurl" listinfo)))))))
722                        feedinfo))))
723           (if (not (> (length urllist) 1))
724               (cdar urllist)
725             (let ((completion-ignore-case t)
726                   (selection 
727                    (mapcar (lambda (listinfo)
728                              (cons (cdr (assoc "sitename" listinfo)) 
729                                    (string-to-int 
730                                     (cdr (assoc "feedid" listinfo)))))
731                            feedinfo)))
732               (cdr (assoc 
733                     (completing-read
734                      "Multiple feeds found.  Select one: "
735                      selection nil t) urllist)))))))))
736
737 (defun nnrss-rss-p (data)
738   "Test if data is an RSS feed.  Simply ensures that the first
739 element is rss or rdf."
740   (or (eq (caar data) 'rss)
741       (eq (caar data) 'rdf:RDF)))
742
743 (defun nnrss-rss-title-description (rss-namespace data url)
744   "Return the title of an RSS feed."
745   (if (nnrss-rss-p data)
746       (let ((description (intern (concat rss-namespace "description")))
747             (title (intern (concat rss-namespace "title")))
748             (channel (nnrss-find-el (intern (concat rss-namespace "channel"))
749                                     data)))
750         (list
751          (cons 'description (caddr (nth 0 (nnrss-find-el description channel))))
752          (cons 'title (caddr (nth 0 (nnrss-find-el title channel))))
753          (cons 'href url)))))
754
755 (defun nnrss-get-namespace-prefix (el uri)
756   "Given EL (containing a parsed element) and URI (containing a string
757 that gives the URI for which you want to retrieve the namespace
758 prefix), return the prefix."
759   (let* ((prefix (car (rassoc uri (cadar el))))
760          (nslist (if prefix 
761                      (split-string (symbol-name prefix) ":")))
762          (ns (cond ((eq (length nslist) 1) ; no prefix given
763                     "")
764                    ((eq (length nslist) 2) ; extract prefix
765                     (cadr nslist)))))
766     (if (and ns (not (eq ns "")))
767         (concat ns ":")
768       ns)))
769
770 (provide 'nnrss)
771
772
773 ;;; nnrss.el ends here
774