Revision: miles@gnu.org--gnu-2005/gnus--devo--0--patch-151
[gnus] / lisp / nnultimate.el
1 ;;; nnultimate.el --- interfacing with the Ultimate Bulletin Board system
2
3 ;; Copyright (C) 1999, 2000, 2001 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., 51 Franklin Street, Fifth Floor,
23 ;; Boston, MA 02110-1301, 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 (require 'mm-url)
41 (require 'nnweb)
42 (require 'parse-time)
43 (autoload 'w3-parse-buffer "w3-parse")
44
45 (nnoo-declare nnultimate)
46
47 (defvoo nnultimate-directory (nnheader-concat gnus-directory "ultimate/")
48   "Where nnultimate will save its files.")
49
50 (defvoo nnultimate-address ""
51   "The address of the Ultimate bulletin board.")
52
53 ;;; Internal variables
54
55 (defvar nnultimate-groups-alist nil)
56 (defvoo nnultimate-groups nil)
57 (defvoo nnultimate-headers nil)
58 (defvoo nnultimate-articles nil)
59 (defvar nnultimate-table-regexp
60   "postings.*editpost\\|forumdisplay\\|Forum[0-9]+/HTML\\|getbio")
61
62 ;;; Interface functions
63
64 (nnoo-define-basics nnultimate)
65
66 (deffoo nnultimate-retrieve-headers (articles &optional group server fetch-old)
67   (nnultimate-possibly-change-server group server)
68   (unless gnus-nov-is-evil
69     (let* ((last (car (last articles)))
70            (did nil)
71            (start 1)
72            (entry (assoc group nnultimate-groups))
73            (sid (nth 2 entry))
74            (topics (nth 4 entry))
75            (mapping (nth 5 entry))
76            (old-total (or (nth 6 entry) 1))
77            (furl "forumdisplay.cgi?action=topics&number=%d&DaysPrune=1000")
78            (furls (list (concat nnultimate-address (format furl sid))))
79            (nnultimate-table-regexp
80             "postings.*editpost\\|forumdisplay\\|getbio")
81            headers article subject score from date lines parent point
82            contents tinfo fetchers map elem a href garticles topic old-max
83            inc datel table current-page total-contents pages
84            farticles forum-contents parse furl-fetched mmap farticle)
85       (setq map mapping)
86       (while (and (setq article (car articles))
87                   map)
88         ;; Skip past the articles in the map until we reach the
89         ;; article we're looking for.
90         (while (and map
91                     (or (> article (caar map))
92                         (< (cadar map) (caar map))))
93           (pop map))
94         (when (setq mmap (car map))
95           (setq farticle -1)
96           (while (and article
97                       (<= article (nth 1 mmap)))
98             ;; Do we already have a fetcher for this topic?
99             (if (setq elem (assq (nth 2 mmap) fetchers))
100                 ;; Yes, so we just add the spec to the end.
101                 (nconc elem (list (cons article
102                                         (+ (nth 3 mmap) (incf farticle)))))
103               ;; No, so we add a new one.
104               (push (list (nth 2 mmap)
105                           (cons article
106                                 (+ (nth 3 mmap) (incf farticle))))
107                     fetchers))
108             (pop articles)
109             (setq article (car articles)))))
110       ;; Now we have the mapping from/to Gnus/nnultimate article numbers,
111       ;; so we start fetching the topics that we need to satisfy the
112       ;; request.
113       (if (not fetchers)
114           (save-excursion
115             (set-buffer nntp-server-buffer)
116             (erase-buffer))
117         (setq nnultimate-articles nil)
118         (mm-with-unibyte-buffer
119           (dolist (elem fetchers)
120             (setq pages 1
121                   current-page 1
122                   total-contents nil)
123             (while (<= current-page pages)
124               (erase-buffer)
125               (setq subject (nth 2 (assq (car elem) topics)))
126               (setq href (nth 3 (assq (car elem) topics)))
127               (if (= current-page 1)
128                   (mm-url-insert href)
129                 (string-match "\\.html$" href)
130                 (mm-url-insert (concat (substring href 0 (match-beginning 0))
131                                       "-" (number-to-string current-page)
132                                       (match-string 0 href))))
133               (goto-char (point-min))
134               (setq contents
135                     (ignore-errors (w3-parse-buffer (current-buffer))))
136               (setq table (nnultimate-find-forum-table contents))
137               (goto-char (point-min))
138               (when (re-search-forward "topic is \\([0-9]+\\) pages" nil t)
139                 (setq pages (string-to-number (match-string 1))))
140               (setq contents (cdr (nth 2 (car (nth 2 table)))))
141               (setq total-contents (nconc total-contents contents))
142               (incf current-page))
143             (when t
144               (let ((i 0))
145                 (dolist (co total-contents)
146                   (push (list (or (nnultimate-topic-article-to-article
147                                    group (car elem) (incf i))
148                                   1)
149                               co subject)
150                         nnultimate-articles))))
151             (when nil
152               (dolist (art (cdr elem))
153                 (when (nth (1- (cdr art)) total-contents)
154                   (push (list (car art)
155                               (nth (1- (cdr art)) total-contents)
156                               subject)
157                         nnultimate-articles))))))
158         (setq nnultimate-articles
159               (sort nnultimate-articles 'car-less-than-car))
160         ;; Now we have all the articles, conveniently in an alist
161         ;; where the key is the Gnus article number.
162         (dolist (articlef nnultimate-articles)
163           (setq article (nth 0 articlef)
164                 contents (nth 1 articlef)
165                 subject (nth 2 articlef))
166           (setq from (mapconcat 'identity
167                                 (nnweb-text (car (nth 2 contents)))
168                                 " ")
169                 datel (nnweb-text (nth 2 (car (cdr (nth 2 contents))))))
170           (while datel
171             (when (string-match "Posted" (car datel))
172               (setq date (substring (car datel) (match-end 0))
173                     datel nil))
174             (pop datel))
175           (when date
176             (setq date (delete "" (split-string date "[-, \n\t\r    ]")))
177             (setq date
178                   (if (or (member "AM" date)
179                           (member "PM" date))
180                       (format
181                        "%s %s %s %s"
182                        (nth 1 date)
183                        (if (and (>= (length (nth 0 date)) 3)
184                                 (assoc (downcase
185                                         (substring (nth 0 date) 0 3))
186                                        parse-time-months))
187                            (substring (nth 0 date) 0 3)
188                          (car (rassq (string-to-number (nth 0 date))
189                                      parse-time-months)))
190                        (nth 2 date) (nth 3 date))
191                     (format "%s %s %s %s"
192                             (car (rassq (string-to-number (nth 1 date))
193                                         parse-time-months))
194                             (nth 0 date) (nth 2 date) (nth 3 date)))))
195           (push
196            (cons
197             article
198             (make-full-mail-header
199              article subject
200              from (or date "")
201              (concat "<" (number-to-string sid) "%"
202                      (number-to-string article)
203                      "@ultimate." server ">")
204              "" 0
205              (/ (length (mapconcat
206                          'identity
207                          (nnweb-text
208                           (cdr (nth 2 (nth 1 (nth 2 contents)))))
209                          ""))
210                 70)
211              nil nil))
212            headers))
213         (setq nnultimate-headers (sort headers 'car-less-than-car))
214         (save-excursion
215           (set-buffer nntp-server-buffer)
216           (mm-with-unibyte-current-buffer
217             (erase-buffer)
218             (dolist (header nnultimate-headers)
219               (nnheader-insert-nov (cdr header))))))
220       'nov)))
221
222 (defun nnultimate-topic-article-to-article (group topic article)
223   (catch 'found
224     (dolist (elem (nth 5 (assoc group nnultimate-groups)))
225       (when (and (= topic (nth 2 elem))
226                  (>= article (nth 3 elem))
227                  (< article (+ (- (nth 1 elem) (nth 0 elem)) 1
228                                (nth 3 elem))))
229         (throw 'found
230                (+ (nth 0 elem) (- article (nth 3 elem))))))))
231
232 (deffoo nnultimate-request-group (group &optional server dont-check)
233   (nnultimate-possibly-change-server nil server)
234   (when (not nnultimate-groups)
235     (nnultimate-request-list))
236   (unless dont-check
237     (nnultimate-create-mapping group))
238   (let ((elem (assoc group nnultimate-groups)))
239     (cond
240      ((not elem)
241       (nnheader-report 'nnultimate "Group does not exist"))
242      (t
243       (nnheader-report 'nnultimate "Opened group %s" group)
244       (nnheader-insert
245        "211 %d %d %d %s\n" (cadr elem) 1 (cadr elem)
246        (prin1-to-string group))))))
247
248 (deffoo nnultimate-request-close ()
249   (setq nnultimate-groups-alist nil
250         nnultimate-groups nil))
251
252 (deffoo nnultimate-request-article (article &optional group server buffer)
253   (nnultimate-possibly-change-server group server)
254   (let ((contents (cdr (assq article nnultimate-articles))))
255     (setq contents (cddr (nth 2 (nth 1 (nth 2 (car contents))))))
256     (when contents
257       (save-excursion
258         (set-buffer (or buffer nntp-server-buffer))
259         (erase-buffer)
260         (nnweb-insert-html (cons 'p (cons nil (list contents))))
261         (goto-char (point-min))
262         (insert "Content-Type: text/html\nMIME-Version: 1.0\n")
263         (let ((header (cdr (assq article nnultimate-headers))))
264           (mm-with-unibyte-current-buffer
265             (nnheader-insert-header header)))
266         (nnheader-report 'nnultimate "Fetched article %s" article)
267         (cons group article)))))
268
269 (deffoo nnultimate-request-list (&optional server)
270   (nnultimate-possibly-change-server nil server)
271   (mm-with-unibyte-buffer
272     (mm-url-insert
273      (if (string-match "/$" nnultimate-address)
274          (concat nnultimate-address "Ultimate.cgi")
275        nnultimate-address))
276     (let ((contents (nth 2 (car (nth 2
277                                      (nnultimate-find-forum-table
278                                       (w3-parse-buffer (current-buffer)))))))
279           sid elem description articles a href group forum
280           a1 a2)
281       (dolist (row contents)
282         (setq row (nth 2 row))
283         (when (setq a (nnweb-parse-find 'a row))
284           (setq group (car (last (nnweb-text a)))
285                 href (cdr (assq 'href (nth 1 a))))
286           (setq description (car (last (nnweb-text (nth 1 row)))))
287           (setq a1 (car (last (nnweb-text (nth 2 row)))))
288           (setq a2 (car (last (nnweb-text (nth 3 row)))))
289           (when (string-match "^[0-9]+$" a1)
290             (setq articles (string-to-number a1)))
291           (when (and a2 (string-match "^[0-9]+$" a2))
292             (setq articles (max articles (string-to-number a2))))
293           (when href
294             (string-match "number=\\([0-9]+\\)" href)
295             (setq forum (string-to-number (match-string 1 href)))
296             (if (setq elem (assoc group nnultimate-groups))
297                 (setcar (cdr elem) articles)
298               (push (list group articles forum description nil nil nil nil)
299                     nnultimate-groups))))))
300     (nnultimate-write-groups)
301     (nnultimate-generate-active)
302     t))
303
304 (deffoo nnultimate-request-newgroups (date &optional server)
305   (nnultimate-possibly-change-server nil server)
306   (nnultimate-generate-active)
307   t)
308
309 (nnoo-define-skeleton nnultimate)
310
311 ;;; Internal functions
312
313 (defun nnultimate-prune-days (group time)
314   "Compute the number of days to fetch info for."
315   (let ((old-time (nth 7 (assoc group nnultimate-groups))))
316     (if (null old-time)
317         1000
318       (- (time-to-days time) (time-to-days old-time)))))
319
320 (defun nnultimate-create-mapping (group)
321   (let* ((entry (assoc group nnultimate-groups))
322          (sid (nth 2 entry))
323          (topics (nth 4 entry))
324          (mapping (nth 5 entry))
325          (old-total (or (nth 6 entry) 1))
326          (current-time (current-time))
327          (furl
328           (concat "forumdisplay.cgi?action=topics&number=%d&DaysPrune="
329                   (number-to-string
330                    (nnultimate-prune-days group current-time))))
331          (furls (list (concat nnultimate-address (format furl sid))))
332          contents forum-contents furl-fetched a subject href
333          garticles topic tinfo old-max inc parse)
334     (mm-with-unibyte-buffer
335       (while furls
336         (erase-buffer)
337         (mm-url-insert (pop furls))
338         (goto-char (point-min))
339         (setq parse (w3-parse-buffer (current-buffer)))
340         (setq contents
341               (cdr (nth 2 (car (nth 2 (nnultimate-find-forum-table
342                                        parse))))))
343         (setq forum-contents (nconc contents forum-contents))
344         (unless furl-fetched
345           (setq furl-fetched t)
346           ;; On the first time through this loop, we find all the
347           ;; forum URLs.
348           (dolist (a (nnweb-parse-find-all 'a parse))
349             (let ((href (cdr (assq 'href (nth 1 a)))))
350               (when (and href
351                          (string-match "forumdisplay.*startpoint" href))
352                 (push href furls))))
353           (setq furls (nreverse furls))))
354       ;; The main idea here is to map Gnus article numbers to
355       ;; nnultimate article numbers.  Say there are three topics in
356       ;; this forum, the first with 4 articles, the seconds with 2,
357       ;; and the third with 1.  Then this will translate into 7 Gnus
358       ;; article numbers, where 1-4 comes from the first topic, 5-6
359       ;; from the second and 7 from the third.  Now, then next time
360       ;; the group is entered, there's 2 new articles in topic one
361       ;; and 1 in topic three.  Then Gnus article number 8-9 be 5-6
362       ;; in topic one and 10 will be the 2 in topic three.
363       (dolist (row (nreverse forum-contents))
364         (setq row (nth 2 row))
365         (when (setq a (nnweb-parse-find 'a row))
366           (setq subject (car (last (nnweb-text a)))
367                 href (cdr (assq 'href (nth 1 a))))
368           (let ((artlist (nreverse (nnweb-text row)))
369                 art)
370             (while (and (not art)
371                         artlist)
372               (when (string-match "^[0-9]+$" (car artlist))
373                 (setq art (1+ (string-to-number (car artlist)))))
374               (pop artlist))
375             (setq garticles art))
376           (when garticles
377             (string-match "/\\([0-9]+\\).html" href)
378             (setq topic (string-to-number (match-string 1 href)))
379             (if (setq tinfo (assq topic topics))
380                 (progn
381                   (setq old-max (cadr tinfo))
382                   (setcar (cdr tinfo) garticles))
383               (setq old-max 0)
384               (push (list topic garticles subject href) topics)
385               (setcar (nthcdr 4 entry) topics))
386             (when (not (= old-max garticles))
387               (setq inc (- garticles old-max))
388               (setq mapping (nconc mapping
389                                    (list
390                                     (list
391                                      old-total (1- (incf old-total inc))
392                                      topic (1+ old-max)))))
393               (incf old-max inc)
394               (setcar (nthcdr 5 entry) mapping)
395               (setcar (nthcdr 6 entry) old-total))))))
396     (setcar (nthcdr 7 entry) current-time)
397     (setcar (nthcdr 1 entry) (1- old-total))
398     (nnultimate-write-groups)
399     mapping))
400
401 (defun nnultimate-possibly-change-server (&optional group server)
402   (nnultimate-init server)
403   (when (and server
404              (not (nnultimate-server-opened server)))
405     (nnultimate-open-server server))
406   (unless nnultimate-groups-alist
407     (nnultimate-read-groups)
408     (setq nnultimate-groups (cdr (assoc nnultimate-address
409                                         nnultimate-groups-alist)))))
410
411 (deffoo nnultimate-open-server (server &optional defs connectionless)
412   (nnheader-init-server-buffer)
413   (if (nnultimate-server-opened server)
414       t
415     (unless (assq 'nnultimate-address defs)
416       (setq defs (append defs (list (list 'nnultimate-address server)))))
417     (nnoo-change-server 'nnultimate server defs)))
418
419 (defun nnultimate-read-groups ()
420   (setq nnultimate-groups-alist nil)
421   (let ((file (expand-file-name "groups" nnultimate-directory)))
422     (when (file-exists-p file)
423       (mm-with-unibyte-buffer
424         (insert-file-contents file)
425         (goto-char (point-min))
426         (setq nnultimate-groups-alist (read (current-buffer)))))))
427
428 (defun nnultimate-write-groups ()
429   (setq nnultimate-groups-alist
430         (delq (assoc nnultimate-address nnultimate-groups-alist)
431               nnultimate-groups-alist))
432   (push (cons nnultimate-address nnultimate-groups)
433         nnultimate-groups-alist)
434   (with-temp-file (expand-file-name "groups" nnultimate-directory)
435     (prin1 nnultimate-groups-alist (current-buffer))))
436
437 (defun nnultimate-init (server)
438   "Initialize buffers and such."
439   (unless (file-exists-p nnultimate-directory)
440     (gnus-make-directory nnultimate-directory)))
441
442 (defun nnultimate-generate-active ()
443   (save-excursion
444     (set-buffer nntp-server-buffer)
445     (erase-buffer)
446     (dolist (elem nnultimate-groups)
447       (insert (prin1-to-string (car elem))
448               " " (number-to-string (cadr elem)) " 1 y\n"))))
449
450 (defun nnultimate-find-forum-table (contents)
451   (catch 'found
452     (nnultimate-find-forum-table-1 contents)))
453
454 (defun nnultimate-find-forum-table-1 (contents)
455   (dolist (element contents)
456     (unless (stringp element)
457       (when (and (eq (car element) 'table)
458                  (nnultimate-forum-table-p element))
459         (throw 'found element))
460       (when (nth 2 element)
461         (nnultimate-find-forum-table-1 (nth 2 element))))))
462
463 (defun nnultimate-forum-table-p (parse)
464   (when (not (apply 'gnus-or
465                     (mapcar
466                      (lambda (p)
467                        (nnweb-parse-find 'table p))
468                      (nth 2 parse))))
469     (let ((href (cdr (assq 'href (nth 1 (nnweb-parse-find 'a parse 20)))))
470           case-fold-search)
471       (when (and href (string-match nnultimate-table-regexp href))
472         t))))
473
474 (provide 'nnultimate)
475
476 ;; Local Variables:
477 ;; coding: iso-8859-1
478 ;; End:
479
480 ;;; arch-tag: ab6bfc45-8fe1-4647-9c78-41050eb152b8
481 ;;; nnultimate.el ends here