source: branches/1.7-appstore/source/cocoa-ide/hemlock/unused/archive/netnews.lisp

Last change on this file was 6567, checked in by Gary Byers, 18 years ago

Move lots of (currently unused, often unlikely to ever be used) stuff to an
archive directory.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 94.3 KB
Line 
1;;; -*- Package: Hemlock; Log: hemlock.log -*-
2;;;
3;;; **********************************************************************
4;;; This code was written as part of the CMU Common Lisp project at
5;;; Carnegie Mellon University, and has been placed in the public domain.
6;;;
7#+CMU (ext:file-comment
8 "$Header$")
9;;;
10;;; **********************************************************************
11;;;
12;;; Written by Blaine Burks
13;;;
14;;; This file implements the reading of bulletin boards from within Hemlock
15;;; via a known NNTP server. Something should probably be done so that
16;;; when the server is down Hemlock doesn't hang as I suspect it will.
17;;;
18;;; Warning: Throughout this file, it may appear I should have bound
19;;; the nn-info-stream and nn-info-header-stream slots instead
20;;; of making multiple structure accesses. This was done on
21;;; purpose because we don't find out if NNTP timed us out until
22;;; we make an attempt to execute another command. This code
23;;; recovers by resetting the header-stream and stream slots in
24;;; the nn-info structure to new streams. If the structure
25;;; access were not made again and NNTP had timed us out, we
26;;; would be making requests on a defunct stream.
27;;;
28
29(in-package :hemlock)
30
31
32
33
34;;;; Netnews data structures.
35
36(defparameter default-netnews-headers-length 1000
37 "How long the header-cache and message-ids arrays should be made on startup.")
38
39(defstruct (netnews-info
40 (:conc-name nn-info-)
41 (:print-function
42 (lambda (nn s d)
43 (declare (ignore nn d))
44 (write-string "#<Netnews Info>" s))))
45 (updatep (ext:required-argument) :type (or null t))
46 (from-end-p nil :type (or null t))
47 ;;
48 ;; The string name of the current group.
49 (current (ext:required-argument) :type simple-string)
50 ;;
51 ;; The number of the latest message read in the current group.
52 (latest nil :type (or null fixnum))
53 ;;
54 ;; The cache of header info for the current group. Each element contains
55 ;; an association list of header fields to contents of those fields. Indexed
56 ;; by id offset by the first message in the group.
57 (header-cache nil :type (or null simple-vector))
58 ;;
59 ;; The number of HEAD requests currently waiting on the header stream.
60 (batch-count nil :type (or null fixnum))
61 ;;
62 ;; The list of newsgroups to read.
63 (groups (ext:required-argument) :type cons)
64 ;;
65 ;; A vector of message ids indexed by buffer-line for this headers buffer.
66 (message-ids nil :type (or null vector))
67 ;;
68 ;; Where to insert the next batch of headers.
69 mark
70 ;;
71 ;; The message buffer used to view article bodies.
72 buffer
73 ;;
74 ;; A list of message buffers that have been marked as undeletable by the user.
75 (other-buffers nil :type (or null cons))
76 ;;
77 ;; The window used to display buffer when \"Netnews Read Style\" is :multiple.
78 message-window
79 ;;
80 ;; The window used to display headers when \"Netnews Read Style\" is
81 ;; :multiple.
82 headers-window
83 ;;
84 ;; How long the message-ids and header-cache arrays are. Reuse this array,
85 ;; but don't break if there are more messages than we can handle.
86 (array-length default-netnews-headers-length :type fixnum)
87 ;;
88 ;; The id of the first message in the current group.
89 (first nil :type (or null fixnum))
90 ;;
91 ;; The id of the last message in the current-group.
92 (last nil :type (or null fixnum))
93 ;;
94 ;; Article number of the first visible header.
95 (first-visible nil :type (or null fixnum))
96 ;;
97 ;; Article number of the last visible header.
98 (last-visible nil :type (or null fixnum))
99 ;;
100 ;; Number of the message that is currently displayed in buffer. Initialize
101 ;; to -1 so I don't have to constantly check for the nullness of it.
102 (current-displayed-message -1 :type (or null fixnum))
103 ;;
104 ;; T if the last batch of headers is waiting on the header stream.
105 ;; This is needed so NN-WRITE-HEADERS-TO-MARK can set the messages-waiting
106 ;; slot to nil.
107 (last-batch-p nil :type (or null t))
108 ;;
109 ;; T if there are more headers in the current group. Nil otherwise.
110 (messages-waiting nil :type (or null t))
111 ;;
112 ;; The stream on which we request headers from NNTP.
113 header-stream
114 ;;
115 ;; The stream on which we request everything but headers from NNTP.
116 stream)
117
118(defmode "News-Headers" :major-p t)
119
120
121
122
123;;;; The netnews-message-info and post-info structures.
124
125(defstruct (netnews-message-info
126 (:conc-name nm-info-)
127 (:print-function
128 (lambda (nn s d)
129 (declare (ignore nn d))
130 (write-string "#<Netnews Message Info>" s))))
131 ;; The headers buffer (if there is one) associated with this message buffer.
132 headers-buffer
133 ;; The draft buffer (if there is one) associated with this message buffer.
134 draft-buffer
135 ;; The post buffer (if there is one) associated with this message buffer.
136 post-buffer
137 ;; This is need because we want to display what message this is in the
138 ;; modeline field of a message buffer.
139 (message-number nil :type (or null fixnum))
140 ;; Set to T when we do not want to reuse this buffer.
141 keep-p)
142
143(defstruct (post-info
144 (:print-function
145 (lambda (nn s d)
146 (declare (ignore nn d))
147 (write-string "#<Post Info>" s))))
148 ;; The NNTP stream over which to send this post.
149 stream
150 ;; When replying in another window, the reply window.
151 reply-window
152 ;; When replying in another window, the message window.
153 message-window
154 ;; The message buffer associated with this post.
155 message-buffer
156 ;; The Headers buffer associated with this post.
157 headers-buffer)
158
159
160
161
162;;;; Command Level Implementation of "News-Headers" mode.
163
164(defhvar "Netnews Database File"
165 "This value is merged with your home directory to get a path to your netnews
166 pointers file."
167 :value ".hemlock-netnews")
168
169(defhvar "Netnews Read Style"
170 "How you like to read netnews. A value of :single will cause netnews
171 mode to use a single window for headers and messages, and a value of
172 :multiple will cause the current window to be split so that Headers take
173 up \"Netnews Headers Proportion\" of what was the current window, and a
174 message bodies buffer the remaining portion. Changing the value of this
175 variable dynamically affects netnews reading."
176 :value :multiple)
177
178(unless (modeline-field :netnews-message)
179 (make-modeline-field
180 :name :netnews-message
181 :width 14
182 :function #'(lambda (buffer window)
183 (declare (ignore window))
184 (let* ((nm-info (variable-value 'netnews-message-info
185 :buffer buffer))
186 (nn-info (variable-value 'netnews-info
187 :buffer (nm-info-headers-buffer
188 nm-info))))
189 (format nil "~D of ~D"
190 (nm-info-message-number nm-info)
191 (1+ (- (nn-info-last nn-info)
192 (nn-info-first nn-info))))))))
193
194(unless (modeline-field :netnews-header-info)
195 (make-modeline-field
196 :name :netnews-header-info
197 :width 24
198 :function
199 #'(lambda (buffer window)
200 (declare (ignore window))
201 (let ((nn-info (variable-value 'netnews-info :buffer buffer)))
202 (format nil "~D before, ~D after"
203 (- (nn-info-first-visible nn-info) (nn-info-first nn-info))
204 (- (nn-info-last nn-info) (nn-info-last-visible nn-info)))))))
205
206(defvar *nn-headers-buffer* nil
207 "If \"Netnews\" was invoked without an argument an not exited, this
208 holds the headers buffer for reading netnews.")
209
210(defvar *netnews-kill-strings* nil)
211
212(defhvar "Netnews Kill File"
213 "This value is merged with your home directory to get the pathname of
214 your netnews kill file. If any of the strings in this file (one per
215 line) appear in a subject header while reading netnews, they will have a
216 \"K\" in front of them, and \"Netnews Next Line\" and \"Netnews Previous
217 Line\" will never land you on one. Use \"Next Line\" and \"Previous
218 Line\" to read Killed messages. Defaults to \".hemlock-kill\"."
219 :value ".hemlock-kill")
220
221(defhvar "Netnews New Group Style"
222 "Determines what happend when you read a group that you have never read
223 before. When :from-start, \"Netnews\" will read from the beginning of a
224 new group forward. When :from-end, the default, \"Netnews\" will read
225 from the end backward group. Otherwise this variable is a number
226 indicating that \"Netnews\" should start that many messages from the end
227 of the group and read forward from there."
228 :value :from-end)
229
230(defhvar "Netnews Start Over Threshold"
231 "If you have read a group before, and the number of new messages exceeds
232 this number, Hemlock asks whether you want to start reading from the end
233 of this group. The default is 300."
234 :value 300)
235
236(defcommand "Netnews" (p &optional group-name from-end-p browse-buf (updatep t))
237 "Enter a headers buffer and read groups from \"Netnews Group File\".
238 With an argument prompts for a group and reads it."
239 "Enter a headers buffer and read groups from \"Netnews Group File\".
240 With an argument prompts for a group and reads it."
241 (cond
242 ((and *nn-headers-buffer* (not p) (not group-name))
243 (change-to-buffer *nn-headers-buffer*))
244 (t
245 (let* ((single-group (if p (prompt-for-string :prompt "Group to read: "
246 :help "Type the name of ~
247 the group you want ~
248 to scan."
249 :trim t)))
250 (groups (cond
251 (group-name (list group-name))
252 (single-group (list single-group))
253 (t
254 (let ((group-file (merge-pathnames
255 (value netnews-group-file)
256 (user-homedir-pathname))))
257 (when (probe-file group-file)
258 (let ((res nil))
259 (with-open-file (s group-file :direction :input)
260 (loop
261 (let ((group (read-line s nil nil)))
262 (unless group (return (nreverse res)))
263 (pushnew group res)))))))))))
264 (unless (or p groups)
265 (editor-error "No groups to read. See \"Netnews Group File\" and ~
266 \"Netnews Browse\"."))
267 (when updatep (nn-assure-database-exists))
268 (nn-parse-kill-file)
269 (multiple-value-bind (stream header-stream) (streams-for-nntp)
270 (multiple-value-bind
271 (buffer-name clashp)
272 (nn-unique-headers-name (car groups))
273 (if (and (or p group-name) clashp)
274 (change-to-buffer (getstring clashp *buffer-names*))
275 (let* ((buffer (make-buffer
276 buffer-name
277 :modes '("News-Headers")
278 :modeline-fields
279 (append (value default-modeline-fields)
280 (list (modeline-field
281 :netnews-header-info)))
282 :delete-hook
283 (list #'netnews-headers-delete-hook)))
284 (nn-info (make-netnews-info
285 :current (car groups)
286 :groups groups
287 :updatep updatep
288 :headers-window (current-window)
289 :mark (copy-mark (buffer-point buffer))
290 :header-stream header-stream
291 :stream stream)))
292 (unless (or p group-name) (setf *nn-headers-buffer* buffer))
293 (when (and clashp (not (or p group-name)))
294 (message "Buffer ~S also contains headers for ~A"
295 clashp (car groups)))
296 (defhvar "Netnews Info"
297 "A structure containing the current group, a list of
298 groups, a book-keeping mark, a stream we get headers on,
299 and the stream on which we request articles."
300 :buffer buffer
301 :value nn-info)
302 (setf (buffer-writable buffer) nil)
303 (defhvar "Netnews Browse Buffer"
304 "This variable is the associated \"News-Browse\" buffer
305 in a \"News-Headers\" buffer created from
306 \"News-Browse\" mode."
307 :buffer buffer
308 :value browse-buf)
309 (setup-group (car groups) nn-info buffer from-end-p)))))))))
310
311
312(defun nn-parse-kill-file ()
313 (let ((filename (merge-pathnames (value netnews-kill-file)
314 (user-homedir-pathname))))
315 (when (probe-file filename)
316 (with-open-file (s filename :direction :input)
317 (loop
318 (let ((kill-string (read-line s nil nil)))
319 (unless kill-string (return))
320 (pushnew kill-string *netnews-kill-strings*)))))))
321
322;;; NETNEWS-HEADERS-DELETE-HOOK closes the stream slots in netnews-info,
323;;; deletes the bookkeeping mark into buffer, sets the headers slots of any
324;;; associated post-info or netnews-message-info structures to nil so
325;;; "Netnews Go To Headers Buffer" will not land you in a buffer that does
326;;; not exist, and sets *nn-headers-buffer* to nil so next time we invoke
327;;; "Netnews" it will start over.
328;;;
329(defun netnews-headers-delete-hook (buffer)
330 (let ((nn-info (variable-value 'netnews-info :buffer buffer)))
331 ;; Disassociate all message buffers.
332 ;;
333 (dolist (buf (nn-info-other-buffers nn-info))
334 (setf (nm-info-headers-buffer (variable-value 'netnews-message-info
335 :buffer buf))
336 nil))
337 (let ((message-buffer (nn-info-buffer nn-info)))
338 (when message-buffer
339 (setf (nm-info-headers-buffer (variable-value 'netnews-message-info
340 :buffer message-buffer))
341 nil)))
342 (close (nn-info-stream nn-info))
343 (close (nn-info-header-stream nn-info))
344 (delete-mark (nn-info-mark nn-info))
345 (when (eq *nn-headers-buffer* buffer)
346 (setf *nn-headers-buffer* nil))))
347
348(defun nn-unique-headers-name (group-name)
349 (let ((original-name (concatenate 'simple-string "Netnews " group-name)))
350 (if (getstring original-name *buffer-names*)
351 (let ((name nil)
352 (number 0))
353 (loop
354 (setf name (format nil "Netnews ~A ~D" group-name (incf number)))
355 (unless (getstring name *buffer-names*)
356 (return (values name original-name)))))
357 (values original-name nil))))
358
359;;; NN-ASSURE-DATABASE-EXISTS does just that. If the file determined by the
360;;; value of "Netnews Database Filename" does not exist, then it gets
361;;; created.
362;;;
363(defun nn-assure-database-exists ()
364 (let ((filename (merge-pathnames (value netnews-database-file)
365 (user-homedir-pathname))))
366 (unless (probe-file filename)
367 (message "Creating netnews database file.")
368 (close (open filename :direction :output :if-does-not-exist :create)))))
369
370(defhvar "Netnews Fetch All Headers"
371 "When NIL, all netnews reading commands will fetch headers in batches for
372 increased efficiency. Any other value will cause these commands to fetch
373 all the headers. This will take a long time if there are a lot."
374 :value nil)
375
376(defcommand "Netnews Look at Newsgroup" (p)
377 "Prompts for the name of a newsgroup and reads it, regardless of what is
378 in and not modifying the \"Netnews Database File\"."
379 "Prompts for the name of a newsgroup and reads it, regardless of what is
380 in and not modifying the \"Netnews Database File\"."
381 (declare (ignore p))
382 (netnews-command nil (prompt-for-string :prompt "Group to look at: "
383 :help "Type the name of ~
384 the group you want ~
385 to look at."
386 :trim t)
387 nil nil nil))
388
389;;; SETUP-GROUP is the guts of this group reader. It sets up a headers
390;;; buffer in buffer for group group-name. This consists of sending a group
391;;; command to both the header-stream and normal stream and then getting the
392;;; last message read in group-name from the database file and setting the
393;;; appropriate slots in the nn-info structure. The first batch of messages
394;;; is then requested and inserted, and room for message-ids is allocated.
395;;;
396(defun setup-group (group-name nn-info buffer &optional from-end-p)
397 ;; Do not bind stream or header-stream because if a timeout has occurred
398 ;; before these calls are invoked, they would be bogus.
399 ;;
400 (nntp-group group-name (nn-info-stream nn-info)
401 (nn-info-header-stream nn-info))
402 (process-status-response (nn-info-stream nn-info) nn-info)
403 (let ((response (process-status-response (nn-info-header-stream nn-info)
404 nn-info)))
405 (cond ((not response)
406 (message "~A is not the name of a netnews group.~%"
407 (nn-info-current nn-info))
408 (change-to-next-group nn-info buffer))
409 (t
410 (multiple-value-bind (number first last)
411 (group-response-args response)
412 (declare (ignore first))
413 (message "Setting up ~A" group-name)
414 ;; If nn-info-updatep is nil, then we fool ourselves into
415 ;; thinking we've never read this group before by making
416 ;; last-read nil. We determine first here because the first
417 ;; that NNTP gives us is way way out of line.
418 ;;
419 (let ((last-read (if (nn-info-updatep nn-info)
420 (nn-last-read-message-number group-name)))
421 (first (1+ (- last number))))
422 ;; Make sure there is at least one new message in this group.
423 (cond
424 ((and last-read (= last-read last))
425 (message "No new messages in ~A" group-name)
426 (setf (nn-info-latest nn-info) last)
427 (change-to-next-group nn-info buffer))
428 ((zerop number)
429 (message "No messages AVAILABLE in ~A" group-name)
430 (setf (nn-info-latest nn-info) last)
431 (change-to-next-group nn-info buffer))
432 (t
433 (let ((latest (if (and last-read (> last-read first))
434 last-read
435 first)))
436 (if (or (and (eq (value netnews-new-group-style) :from-end)
437 (or (= latest first)
438 (and (> (- last latest)
439 (value
440 netnews-start-over-threshold))
441 (prompt-for-y-or-n
442 :prompt
443 `("There are ~D new messages. ~
444 Read from the end of this ~
445 group? " ,(- last latest))
446 :default "Y"
447 :default-string "Y"
448 :help "Y starts reading from the ~
449 end. N starts reading where ~
450 you left off many messages ~
451 back."))))
452 from-end-p)
453 (setf (nn-info-from-end-p nn-info) t))
454
455 (cond ((nn-info-from-end-p nn-info)
456 (setf (nn-info-first-visible nn-info) nil)
457 (setf (nn-info-last-visible nn-info) last))
458 (t
459 ; (setf (nn-info-first-visible nn-info) latest)
460 (setf (nn-info-first-visible nn-info) (1+ latest))
461 (setf (nn-info-last-visible nn-info) nil)))
462 (setf (nn-info-first nn-info) first)
463 (setf (nn-info-last nn-info) last)
464 (setf (nn-info-latest nn-info) latest))
465 ;;
466 ;; Request the batch before setting message-ids so they start
467 ;; coming before we need them.
468 (nn-request-next-batch nn-info
469 (value netnews-fetch-all-headers))
470 (let ((message-ids (nn-info-message-ids nn-info))
471 (header-cache (nn-info-header-cache nn-info))
472 (length (1+ (- last first))))
473 (multiple-value-setq
474 (message-ids header-cache)
475 (cond ((> length (nn-info-array-length nn-info))
476 (setf (nn-info-array-length nn-info) length)
477 (values (make-array length :fill-pointer 0)
478 (make-array length
479 :initial-element nil)))
480 (message-ids
481 (setf (fill-pointer message-ids) 0)
482 (values message-ids header-cache))
483 (t
484 (values (make-array (nn-info-array-length nn-info)
485 :fill-pointer 0)
486 (make-array (nn-info-array-length nn-info)
487 :initial-element nil)))))
488 (setf (nn-info-message-ids nn-info) message-ids)
489 (setf (nn-info-header-cache nn-info) header-cache))
490 (nn-write-headers-to-mark nn-info buffer)
491 (change-to-buffer buffer)))))))))
492
493;;; NN-LAST-READ-MESSAGE-NUMBER reads the last read message in group-name
494;;; from the value of "Netnews Database File". It is SETF'able and the
495;;; SETF method is %SET-LAST-READ-MESSAGE-NUMBER.
496;;;
497(defun nn-last-read-message-number (group-name)
498 (with-open-file (s (merge-pathnames (value netnews-database-file)
499 (user-homedir-pathname))
500 :direction :input :if-does-not-exist :error)
501 (loop
502 (let ((read-group-name (read-line s nil nil)))
503 (unless read-group-name (return nil))
504 (when (string-equal read-group-name group-name)
505 (let ((last-read (read-line s nil nil)))
506 (if last-read
507 (return (parse-integer last-read))
508 (error "Should have been a message number ~
509 following ~S in database file."
510 group-name))))))))
511
512(defun %set-nn-last-read-message-number (group-name new-value)
513 (with-open-file (s (merge-pathnames (value netnews-database-file)
514 (user-homedir-pathname))
515 :direction :io :if-does-not-exist :error
516 :if-exists :overwrite)
517 (unless (loop
518 (let ((read-group-name (read-line s nil nil)))
519 (unless read-group-name (return nil))
520 (when (string-equal read-group-name group-name)
521 ;; File descriptor streams do not do the right thing with
522 ;; :io/:overwrite streams, so work around it by setting it
523 ;; explicitly.
524 ;;
525 (file-position s (file-position s))
526 ;; Justify the number so that if the number of digits in it
527 ;; changes, we won't overwrite the next group name.
528 ;;
529 (format s "~14D~%" new-value)
530 (return t))))
531 (write-line group-name s)
532 (format s "~14D~%" new-value))))
533
534(defsetf nn-last-read-message-number %set-nn-last-read-message-number)
535
536(defconstant nntp-eof ".
537"
538 "NNTP marks the end of a textual response with this. NNTP also recognizes
539 this as the end of a post.")
540
541;;; This macro binds a variable to each successive line of input from NNTP
542;;; and exits when it sees the NNTP end-of-file-marker, a period by itself on
543;;; a line.
544;;;
545(defmacro with-input-from-nntp ((var stream) &body body)
546 "Body is executed with var bound to successive lines of input from nntp.
547 Exits at the end of a response, returning whatever the last execution of
548 Body returns, or nil if there was no input.
549 Take note: this is only to be used for textual responses. Status responses
550 are of an entirely different nature."
551 (let ((return-value (gensym)))
552 `(let ((,return-value nil)
553 (,var ""))
554 (declare (simple-string ,var))
555 (loop
556 (setf ,var (read-line ,stream))
557 (when (string= ,var nntp-eof) (return ,return-value))
558 (setf ,return-value (progn ,@body))))))
559
560
561;;; Writing the date, from, and subject fields to a mark.
562
563(defhvar "Netnews Before Date Field Pad"
564 "How many spaces should be inserted before the date in Netnews. The default
565 is 1."
566 :value 1)
567
568(defhvar "Netnews Date Field Length"
569 "How long the date field should be in \"News-Headers\" buffers. The
570 default is 6"
571 :value 6)
572
573(defhvar "Netnews Line Field Length"
574 "How long the line field should be in \"News-Headers\" buffers. The
575 default is 3"
576 :value 3)
577
578(defhvar "Netnews From Field Length"
579 "How long the from field should be in \"News-Headers\" buffers. The
580 default is 20."
581 :value 20)
582
583(defhvar "Netnews Subject Field Length"
584 "How long the subject field should be in \"News-Headers\" buffers. The
585 default is 43."
586 :value 43)
587
588(defhvar "Netnews Field Padding"
589 "How many spaces should be left between the netnews date, from, lines, and
590 subject fields. The default is 2."
591 :value 2)
592
593;;;
594(defconstant netnews-space-string
595 (make-string 70 :initial-element #\space))
596;;;
597(defconstant missing-message (cons nil nil)
598 "Use this as a marker so nn-write-headers-to-mark doesn't try to insert
599 a message that is not really there.")
600
601;;; NN-CACHE-HEADER-INFO stashes all header information into an array for
602;;; later use.
603;;;
604(defun nn-cache-header-info (nn-info howmany use-header-stream-p)
605 (let* ((cache (nn-info-header-cache nn-info))
606 (message-ids (nn-info-message-ids nn-info))
607 (stream (if use-header-stream-p
608 (nn-info-header-stream nn-info)
609 (nn-info-stream nn-info)))
610 (from-end-p (nn-info-from-end-p nn-info))
611 (old-count 0))
612 (declare (fixnum old-count))
613 (when from-end-p
614 (setf old-count (length message-ids))
615 (do ((i (length message-ids) (1- i)))
616 ((minusp i) nil)
617 (setf (aref message-ids (+ i howmany)) (aref message-ids i)))
618 (setf (fill-pointer message-ids) 0))
619 (let ((missing-message-count 0)
620 (offset (nn-info-first nn-info)))
621 (dotimes (i howmany)
622 (let ((response (process-status-response stream)))
623 (if response
624 (let* ((id (head-response-args response))
625 (index (- id offset)))
626 (vector-push id message-ids)
627 (setf (svref cache index) nil)
628 (with-input-from-nntp (string stream)
629 (let ((colonpos (position #\: string)))
630 (when colonpos
631 (push (cons (subseq string 0 colonpos)
632 (subseq string
633 (+ colonpos 2)))
634 (svref cache index))))))
635 (incf missing-message-count))))
636 (when from-end-p
637 (when (plusp missing-message-count)
638 (dotimes (i old-count)
639 (setf (aref message-ids (- (+ i howmany) missing-message-count))
640 (aref message-ids (+ i howmany)))))
641 (setf (fill-pointer message-ids)
642 (- (+ old-count howmany) missing-message-count))))))
643
644(defconstant netnews-field-na "NA"
645 "This string gets inserted when NNTP doesn't find a field.")
646
647(defconstant netnews-field-na-length (length netnews-field-na)
648 "The length of netnews-field-na")
649
650(defun nn-write-headers-to-mark (nn-info buffer &optional fetch-rest-p
651 out-of-order-p)
652 (let* ((howmany (nn-info-batch-count nn-info))
653 (from-end-p (nn-info-from-end-p nn-info))
654 (cache (nn-info-header-cache nn-info))
655 (old-point (copy-mark (buffer-point buffer) (if from-end-p
656 :left-inserting
657 :right-inserting)))
658 (messages-waiting (nn-info-messages-waiting nn-info))
659 (mark (nn-info-mark nn-info)))
660 (unless messages-waiting
661 (return-from nn-write-headers-to-mark nil))
662 (if from-end-p
663 (buffer-start mark)
664 (buffer-end mark))
665 (nn-cache-header-info nn-info howmany (not out-of-order-p))
666 (with-writable-buffer (buffer)
667 (with-mark ((check-point mark :right-inserting))
668 (macrolet ((mark-to-pos (mark pos)
669 `(insert-string ,mark netnews-space-string
670 0 (- ,pos (mark-column ,mark))))
671 (insert-field (mark field-string field-length)
672 `(if ,field-string
673 (insert-string ,mark ,field-string
674 0 (min ,field-length
675 (1- (length ,field-string))))
676 (insert-string ,mark netnews-field-na
677 0 (min ,field-length
678 netnews-field-na-length)))))
679 (let* ((line-start (+ (value netnews-before-date-field-pad)
680 (value netnews-date-field-length)
681 (value netnews-field-padding)))
682 (from-start (+ line-start
683 (value netnews-line-field-length)
684 (value netnews-field-padding)))
685 (subject-start (+ from-start
686 (value netnews-from-field-length)
687 (value netnews-field-padding)))
688 (start (- messages-waiting (nn-info-first nn-info)))
689 (end (1- (+ start howmany))))
690 (do ((i start (1+ i)))
691 ((> i end))
692 (let ((assoc-list (svref cache i)))
693 (unless (null assoc-list)
694 (insert-string mark netnews-space-string
695 0 (value netnews-before-date-field-pad))
696 (let* ((date-field (cdr (assoc "date" assoc-list
697 :test #'string-equal)))
698 (universal-date (if date-field
699 (ext:parse-time date-field
700 :end (1- (length date-field))))))
701 (insert-field
702 mark
703 (if universal-date
704 (string-capitalize
705 (format-universal-time nil universal-date
706 :style :government
707 :print-weekday nil))
708 date-field)
709 (value netnews-date-field-length)))
710 (mark-to-pos mark line-start)
711 (insert-field mark (cdr (assoc "lines" assoc-list
712 :test #'string-equal))
713 (value netnews-line-field-length))
714 (mark-to-pos mark from-start)
715 (insert-field mark (cdr (assoc "from" assoc-list
716 :test #'string-equal))
717 (value netnews-from-field-length))
718 (mark-to-pos mark subject-start)
719 (insert-field mark (cdr (assoc "subject" assoc-list
720 :test #'string-equal))
721 (value netnews-subject-field-length))
722 (insert-character mark #\newline))))))
723 (cond (out-of-order-p
724 (setf (nn-info-first-visible nn-info) messages-waiting))
725 (t
726 (if (nn-info-from-end-p nn-info)
727 (setf (nn-info-first-visible nn-info) messages-waiting)
728 (setf (nn-info-last-visible nn-info)
729 (1- (+ messages-waiting howmany))))
730 (if (nn-info-last-batch-p nn-info)
731 (setf (nn-info-messages-waiting nn-info) nil)
732 (nn-request-next-batch nn-info fetch-rest-p))))
733 (when (mark= mark check-point)
734 (message "All messages in last batch were missing, getting more."))
735 (move-mark (buffer-point buffer) old-point)
736 (delete-mark old-point)))))
737
738;;; NN-MAYBE-GET-MORE-HEADERS gets more headers if the point of the headers
739;;; buffer is on an empty line and there are some. Returns whether it got more
740;;; headers, i.e., if it is time to go on to the next group.
741;;;
742(defun nn-maybe-get-more-headers (nn-info)
743 (let ((headers-buffer (line-buffer (mark-line (nn-info-mark nn-info)))))
744 (when (empty-line-p (buffer-point headers-buffer))
745 (cond ((and (nn-info-messages-waiting nn-info)
746 (not (nn-info-from-end-p nn-info)))
747 (nn-write-headers-to-mark nn-info headers-buffer)
748 t)
749 (t :go-on)))))
750
751(defhvar "Netnews Batch Count"
752 "Determines how many headers the Netnews facility will fetch at a time.
753 The default is 50."
754 :value 50)
755
756;;; NN-REQUEST-NEXT-BATCH requests the next batch of messages in a group.
757;;; For safety, don't do anything if there is no next-batch start.
758;;;
759(defun nn-request-next-batch (nn-info &optional fetch-rest-p)
760 (if (nn-info-from-end-p nn-info)
761 (nn-request-backward nn-info fetch-rest-p)
762 (nn-request-forward nn-info fetch-rest-p)))
763
764(defun nn-request-forward (nn-info fetch-rest-p)
765 (let* ((last-visible (nn-info-last-visible nn-info))
766 (last (nn-info-last nn-info))
767 (batch-start (if last-visible
768 (1+ (nn-info-last-visible nn-info))
769 (1+ (nn-info-latest nn-info))))
770 (header-stream (nn-info-header-stream nn-info))
771 (batch-end (if fetch-rest-p
772 last
773 (1- (+ batch-start (value netnews-batch-count))))))
774 ;; If this is the last batch, adjust batch-end appropriately.
775 ;;
776 (when (>= batch-end last)
777 (setf batch-end last)
778 (setf (nn-info-last-batch-p nn-info) t))
779 (setf (nn-info-batch-count nn-info) (1+ (- batch-end batch-start)))
780 (setf (nn-info-messages-waiting nn-info) batch-start)
781 (nn-send-many-head-requests header-stream batch-start batch-end nil)))
782
783(defun nn-request-backward (nn-info fetch-rest-p
784 &optional (use-header-stream-p t))
785 (let* ((first-visible (nn-info-first-visible nn-info))
786 (batch-end (if first-visible
787 (1- (nn-info-first-visible nn-info))
788 (nn-info-last nn-info)))
789 (stream (if use-header-stream-p
790 (nn-info-header-stream nn-info)
791 (nn-info-stream nn-info)))
792 (first (nn-info-first nn-info))
793 (batch-start (if fetch-rest-p
794 first
795 (1+ (- batch-end (value netnews-batch-count))))))
796 ;; If this is the last batch, adjust batch-end appropriately.
797 ;;
798 (when (<= batch-start first)
799 (setf batch-start first)
800 (setf (nn-info-last-batch-p nn-info) t))
801 (setf (nn-info-batch-count nn-info) (1+ (- batch-end batch-start)))
802 (setf (nn-info-messages-waiting nn-info) batch-start)
803 (nn-send-many-head-requests stream batch-start batch-end
804 (not use-header-stream-p))))
805
806;;; NN-REQUEST-OUT-OF-ORDER is called when the user is reading a group normally
807;;; and decides he wants to see some messages before the first one visible.
808;;; To accomplish this without disrupting the normal flow of things, we fool
809;;; ourselves into thinking we are reading the group from the end, remembering
810;;; several slots that could be modified in requesting thesse messages.
811;;; When we are done, return state to what it was for reading a group forward.
812;;;
813(defun nn-request-out-of-order (nn-info headers-buffer)
814 (let ((messages-waiting (nn-info-messages-waiting nn-info))
815 (batch-count (nn-info-batch-count nn-info))
816 (last-batch-p (nn-info-last-batch-p nn-info)))
817 (nn-request-backward nn-info nil nil)
818 (setf (nn-info-from-end-p nn-info) t)
819 (nn-write-headers-to-mark nn-info headers-buffer nil t)
820 (setf (nn-info-messages-waiting nn-info) messages-waiting)
821 (setf (nn-info-batch-count nn-info) batch-count)
822 (setf (nn-info-last-batch-p nn-info) last-batch-p)
823 (setf (nn-info-from-end-p nn-info) nil)))
824
825(declaim (special *nn-last-command-issued*))
826
827(defun nn-send-many-head-requests (stream first last out-of-order-p)
828 (do ((i first (1+ i)))
829 ((> i last))
830 (nntp-head i stream))
831 (setf *nn-last-command-issued*
832 (list (if out-of-order-p :out-of-order :header)
833 first last out-of-order-p)))
834
835(defvar nn-minimum-header-batch-count 30
836 "The minimum number of headers to fetch at any given time.")
837
838
839
840
841;;;; "News-Message" mode.
842
843(defmode "News-Message" :major-p t)
844
845
846
847
848;;;; Commands for viewing articles.
849
850(defcommand "Netnews Show Article" (p)
851 "Show the message the point is on. If it is the same message that is
852 already in the message buffer and \"Netnews Read Style\" is :multiple,
853 then just scroll the window down prefix argument lines"
854 "Show the message the point is on. If it is the same message that is
855 already in the message buffer and \"Netnews Read Style\" is :multiple,
856 then just scroll the window down prefix argument lines"
857 (nn-show-article (value netnews-info) p))
858
859(defcommand "Netnews Next Article" (p)
860 "Show the next article in the current newsgroup."
861 "Shows the article on the line preceeding the point in the headers buffer."
862 (declare (ignore p))
863 (let* ((what-next (netnews-next-line-command nil (nn-get-headers-buffer))))
864 (when (and (not (eq what-next :done))
865 (or (eq what-next t)
866 (eq (value netnews-last-header-style) :next-article)))
867 ;; Reget the headers buffer because the call to netnews-next-line-command
868 ;; might have moved us into a different buffer.
869 ;;
870 (nn-show-article (variable-value 'netnews-info
871 :buffer (nn-get-headers-buffer))
872 t))))
873
874(defcommand "Netnews Previous Article" (p)
875 "Show the previous article in the current newsgroup."
876 "Shows the article on the line after the point in the headers buffer."
877 (declare (ignore p))
878 (let ((buffer (nn-get-headers-buffer)))
879 (netnews-previous-line-command nil buffer)
880 (nn-show-article (variable-value 'netnews-info :buffer buffer) t)))
881
882;;; NN-SHOW-ARTICLE checks first to see if we need to get more headers. If
883;;; NN-MAYBE-GET-MORE-HEADERS returns nil then don't do anything because we
884;;; changed to the next group. Then see if the message the user has
885;;; requested is already in the message buffer. If the it isn't, put it
886;;; there. If it is, and maybe-scroll-down is t, then scroll the window
887;;; down p lines in :multiple mode, or just change to the buffer in :single
888;;; mode. I use scroll-window down becuase this function is called by
889;;; "Netnews Show Article", "Netnews Next Article", and "Netnews Previous
890;;; Article". It doesn't make sense to scroll the window down if the guy
891;;; just read a message, moved the point up one line and invoked "Netnews
892;;; Next Article". He expects to see the article again, not the second
893;;; page of it. Also check to make sure there is a message under the
894;;; point. If there is not, then get some more headers. If there are no
895;;; more headers, then go on to the next group. I can read and write. Hi
896;;; Bill. Are you having fun grokking my code? Hope so -- Dude. Nothing
897;;; like stream of consciousness is there? Come to think of it, this is
898;;; kind of like recursive stream of conscious because I'm writing down my
899;;; stream of conscious which is about my stream of conscious. I think I'm
900;;; insane. In fact I know I am.
901;;;
902(defun nn-show-article (nn-info dont-scroll-down &optional p)
903 (let ((headers-buffer (nn-get-headers-buffer))
904 (message-buffer (nn-info-buffer nn-info)))
905 (cond
906 ((eq (nn-maybe-get-more-headers nn-info) :go-on)
907 (case (value netnews-last-header-style)
908 (:this-headers (change-to-buffer headers-buffer)
909 (buffer-start (buffer-point headers-buffer))
910 (editor-error "Last header."))
911 (:next-headers (change-to-next-group nn-info headers-buffer))
912 (:next-article (change-to-next-group nn-info headers-buffer)
913 (netnews-show-article-command nil))))
914 (t
915 (cond ((and (not dont-scroll-down)
916 (= (nn-info-current-displayed-message nn-info)
917 (array-element-from-mark (buffer-point headers-buffer)
918 (nn-info-message-ids nn-info))))
919 (ecase (value netnews-read-style)
920 (:single (buffer-start (buffer-point message-buffer))
921 (change-to-buffer message-buffer))
922 (:multiple
923 (multiple-value-bind
924 (headers-window message-window newp)
925 (nn-assure-multi-windows nn-info)
926 (nn-put-buffers-in-windows headers-buffer message-buffer
927 headers-window message-window
928 :headers)
929 ;; If both windows were visible to start with, just scroll
930 ;; down. If they weren't, then show the message over
931 ;; again.
932 ;;
933 (cond (newp (buffer-start (buffer-point message-buffer))
934 (buffer-start (window-point message-window)))
935 (t (netnews-message-scroll-down-command
936 p message-buffer message-window)))))))
937 (t
938 (nn-put-article-in-buffer nn-info headers-buffer)
939 (setf message-buffer (nn-info-buffer nn-info))
940 (multiple-value-bind
941 (headers-window message-window)
942 (ecase (value netnews-read-style) ; Only need windows in
943 (:single (values nil nil)) ; :multiple mode.
944 (:multiple (nn-assure-multi-windows nn-info)))
945 (ecase (value netnews-read-style)
946 (:multiple
947 ;; When there is only one window displaying the headers
948 ;; buffer, move the window point of that buffer to the
949 ;; buffer-point.
950 (when (= (length (buffer-windows headers-buffer)) 1)
951 (move-mark (window-point headers-window)
952 (buffer-point headers-buffer)))
953 (buffer-start (window-point message-window))
954 (nn-put-buffers-in-windows headers-buffer message-buffer
955 headers-window message-window
956 :headers))
957 (:single (change-to-buffer message-buffer))))))))))
958
959(defcommand "Netnews Message Quit" (p)
960 "Destroy this message buffer, and pop back to the associated headers buffer."
961 "Destroy this message buffer, and pop back to the associated headers buffer."
962 (declare (ignore p))
963 (unless (hemlock-bound-p 'netnews-message-info)
964 (editor-error "Not in a News-Message Buffer"))
965 (let ((message-buffer (current-buffer)))
966 (change-to-buffer (nn-get-headers-buffer))
967 (delete-buffer-if-possible message-buffer)))
968
969(defhvar "Netnews Message Header Fields"
970 "When NIL, the default, all available fields are displayed in the header
971 of a message. Otherwise, this variable should containt a list of fields
972 that should be included in the message header when a message is
973 displayed. Any string name is acceptable. Fields that do not exist are
974 ignored. If an element of this list is an atom, then it should be the
975 string name of a field. If it is a cons, then the car should be the
976 string name of a field, and the cdr should be the length to which this
977 field should be limited."
978 :value nil)
979
980
981(defcommand "Netnews Show Whole Header" (p)
982 "This command will display the entire header of the message currently
983 being read."
984 "This command will display the entire header of the message currently
985 being read."
986 (declare (ignore p))
987 (let* ((headers-buffer (nn-get-headers-buffer))
988 (nn-info (variable-value 'netnews-info :buffer headers-buffer))
989 (buffer (nn-get-message-buffer nn-info)))
990 (with-writable-buffer (buffer)
991 (delete-region (buffer-region buffer))
992 (nn-put-article-in-buffer nn-info headers-buffer t))))
993
994;;; NN-PUT-ARTICLE-IN-BUFFER puts the article under the point into the
995;;; associated message buffer if it is not there already. Uses value of
996;;; "Netnews Message Header Fields" to determine what fields should appear
997;;; in the message header. Returns the number of the article under the
998;;; point.
999;;;
1000(defun nn-put-article-in-buffer (nn-info headers-buffer &optional override)
1001 (let ((stream (nn-info-stream nn-info))
1002 (article-number (array-element-from-mark
1003 (buffer-point headers-buffer)
1004 (nn-info-message-ids nn-info)))
1005 (message-buffer (nn-get-message-buffer nn-info)))
1006 (setf (nm-info-message-number (variable-value 'netnews-message-info
1007 :buffer message-buffer))
1008 (1+ (- article-number (nn-info-first nn-info))))
1009 (cond ((and (= (nn-info-current-displayed-message nn-info) article-number)
1010 (not override))
1011 (buffer-start (buffer-point message-buffer)))
1012 (t
1013 ;; Request article as soon as possible to avoid waiting for reply.
1014 ;;
1015 (nntp-body article-number stream)
1016 (setf (nn-info-current-displayed-message nn-info) article-number)
1017 (process-status-response stream nn-info)
1018 (with-writable-buffer (message-buffer)
1019 (let ((point (buffer-point message-buffer))
1020 (info (svref (nn-info-header-cache nn-info)
1021 (- article-number (nn-info-first nn-info))))
1022 (message-fields (value netnews-message-header-fields))
1023 key field-length)
1024 (cond ((and message-fields
1025 (not override))
1026 (dolist (ele message-fields)
1027 (etypecase ele
1028 (atom (setf key ele field-length nil))
1029 (cons (setf key (car ele) field-length (cdr ele))))
1030 (let ((field-string (cdr (assoc key info
1031 :test #'string-equal))))
1032 (when field-string
1033 (insert-string point (string-capitalize key))
1034 (insert-string point ": ")
1035 (insert-string point field-string
1036 0
1037 (max
1038 (if field-length
1039 (min field-length
1040 (1- (length field-string)))
1041 (1- (length field-string)))
1042 0))
1043 (insert-character point #\newline)))))
1044 (t
1045 (dolist (ele info)
1046 (insert-string point (string-capitalize (car ele)))
1047 (insert-string point ": ")
1048 (insert-string point (cdr ele)
1049 0 (max 0 (1- (length (cdr ele)))))
1050 (insert-character point #\newline))))
1051 (insert-character point #\newline)
1052 (nntp-insert-textual-response point (nn-info-stream nn-info))))
1053 (buffer-start (buffer-point message-buffer))
1054 (when (> article-number (nn-info-latest nn-info))
1055 (setf (nn-info-latest nn-info) article-number))))
1056 article-number))
1057
1058;;; NN-PUT-BUFFERS-IN-WINDOWS makes sure the message buffer goes in the message
1059;;; window and the headers buffer in the headers window. If which-current
1060;;; is :headers, the headers buffer/window will be made current, if it is
1061;;; :message, the message buffer/window will be made current.
1062;;;
1063(defun nn-put-buffers-in-windows (headers-buffer message-buffer headers-window
1064 message-window which-current)
1065 (setf (window-buffer message-window) message-buffer
1066 (window-buffer headers-window) headers-buffer)
1067 (setf (current-window) (ecase which-current
1068 (:headers headers-window)
1069 (:message message-window))
1070 (current-buffer) (case which-current
1071 (:headers headers-buffer)
1072 (:message message-buffer))))
1073
1074(defhvar "Netnews Headers Proportion"
1075 "Determines how much of the current window will display headers when
1076 \"Netnews Read Style\" is :multiple. Defaults to .25"
1077 :value .25)
1078
1079(defun nn-assure-multi-windows (nn-info)
1080 (let ((newp nil))
1081 (unless (and (member (nn-info-message-window nn-info) *window-list*)
1082 (member (nn-info-headers-window nn-info) *window-list*))
1083 (setf newp t)
1084 (setf (nn-info-message-window nn-info) (current-window)
1085 (nn-info-headers-window nn-info)
1086 (make-window (buffer-start-mark (nn-get-headers-buffer))
1087 :proportion (value netnews-headers-proportion))))
1088 (values (nn-info-headers-window nn-info)
1089 (nn-info-message-window nn-info)
1090 newp)))
1091
1092;;; NN-GET-MESSAGE-BUFFER returns the message buffer for an nn-info structure.
1093;;; If there is not one, this function makes it and sets the slot in nn-info.
1094;;;
1095(defun nn-get-message-buffer (nn-info)
1096 (let* ((message-buffer (nn-info-buffer nn-info))
1097 (nm-info (if message-buffer
1098 (variable-value 'netnews-message-info
1099 :buffer message-buffer))))
1100 (cond ((and message-buffer (not (nm-info-keep-p nm-info)))
1101 (with-writable-buffer (message-buffer)
1102 (delete-region (buffer-region message-buffer)))
1103 message-buffer)
1104 (t
1105 (let ((buf (make-buffer (nn-unique-message-buffer-name
1106 (nn-info-current nn-info))
1107 :modeline-fields
1108 (append (value default-modeline-fields)
1109 (list (modeline-field
1110 :netnews-message)))
1111 :modes '("News-Message")
1112 :delete-hook
1113 (list #'nn-message-buffer-delete-hook))))
1114 (setf (nn-info-buffer nn-info) buf)
1115 (defhvar "Netnews Message Info"
1116 "Structure that keeps track of buffers in \"News-Message\"
1117 mode."
1118 :value (make-netnews-message-info
1119 :headers-buffer (current-buffer))
1120 :buffer buf)
1121 buf)))))
1122
1123;;; The usual. Clean everything up.
1124;;;
1125(defun nn-message-buffer-delete-hook (buffer)
1126 (let* ((headers-buffer (nm-info-headers-buffer
1127 (variable-value 'netnews-message-info
1128 :buffer buffer)))
1129 (nn-info (variable-value 'netnews-info :buffer headers-buffer))
1130 (nm-info (variable-value 'netnews-message-info :buffer buffer)))
1131 (setf (nn-info-buffer nn-info) nil)
1132 (setf (nn-info-current-displayed-message nn-info) -1)
1133 (let ((post-buffer (nm-info-post-buffer nm-info)))
1134 (when post-buffer
1135 (setf (post-info-message-buffer (variable-value
1136 'post-info :buffer post-buffer))
1137 nil)))))
1138
1139
1140;;; NN-UNIQUE-MESSAGE-BUFFER-NAME likes to have a simple name, i.e.
1141;;; "Netnews Message for rec.music.synth". When there is already a buffer
1142;;; by this name, however, we start counting until the name is unique.
1143;;;
1144(defun nn-unique-message-buffer-name (group)
1145 (let ((name (concatenate 'simple-string "Netnews Message for " group))
1146 (number 0))
1147 (loop
1148 (unless (getstring name *buffer-names*) (return name))
1149 (setf name (format nil "Netnews Message ~D" number))
1150 (incf number))))
1151
1152;;; INSERT-TEXTUAL-RESPONSE inserts a textual response from nntp at mark.
1153;;;
1154(defun nntp-insert-textual-response (mark stream)
1155 (with-input-from-nntp (string stream)
1156 (insert-string mark string 0 (1- (length string)))
1157 (insert-character mark #\newline)))
1158
1159;;; NN-GET-HEADERS-BUFFER returns the headers buffer if we are in a message or
1160;;; headers buffer.
1161;;;
1162(defun nn-get-headers-buffer ()
1163 (cond ((hemlock-bound-p 'netnews-info)
1164 (current-buffer))
1165 ((hemlock-bound-p 'netnews-message-info)
1166 (nm-info-headers-buffer (value netnews-message-info)))
1167 ((hemlock-bound-p 'post-info)
1168 (post-info-headers-buffer (value post-info)))
1169 (t nil)))
1170
1171
1172(defcommand "Netnews Previous Line" (p &optional
1173 (headers-buffer (current-buffer)))
1174 "Moves the point to the last header before the point that is not in your
1175 kill file. If you move off the end of the buffer and there are more
1176 headers, then get them. Otherwise go on to the next group in \"Netnews
1177 Groups\"."
1178 "Moves the point to the last header before the point that is not in your
1179 kill file. If you move off the end of the buffer and there are more
1180 headers, then get them. Otherwise go on to the next group in \"Netnews
1181 Groups\"."
1182 (declare (ignore p))
1183 (let ((point (buffer-point headers-buffer))
1184 (nn-info (variable-value 'netnews-info :buffer headers-buffer)))
1185 (with-mark ((original-position point)
1186 (start point)
1187 (end point))
1188 (loop
1189 (unless (line-offset point -1)
1190 (cond ((and (nn-info-from-end-p nn-info)
1191 (nn-info-messages-waiting nn-info))
1192 (nn-write-headers-to-mark nn-info headers-buffer)
1193 (netnews-previous-line-command nil headers-buffer))
1194 (t
1195 (cond ((= (nn-info-first-visible nn-info)
1196 (nn-info-first nn-info))
1197 (move-mark point original-position)
1198 (editor-error "No previous unKilled headers."))
1199 (t
1200 (message "Requesting backward...")
1201 (nn-request-out-of-order nn-info headers-buffer)
1202 (netnews-previous-line-command nil headers-buffer))))))
1203 (line-start (move-mark start point))
1204 (character-offset (move-mark end start) 1)
1205 (unless (string= (region-to-string (region start end)) "K")
1206 (return))))))
1207
1208(defhvar "Netnews Last Header Style"
1209 "When you read the last message in a newsgroup, this variable determines
1210 what will happen next. Takes one of three values: :this-headers,
1211 :next-headers, or :next-article. :this-headers, the default means put me
1212 in the headers buffer for this newsgroup. :next-headers means go to the
1213 next newsgroup and put me in that headers buffer. :next-article means go
1214 on to the next newsgroup and show me the first unread article."
1215 :value :next-headers)
1216
1217(defcommand "Netnews Next Line"
1218 (p &optional (headers-buffer (current-buffer)))
1219 "Moves the point to the next header that is not in your kill file. If you
1220 move off the end of the buffer and there are more headers, then get them.
1221 Otherwise go on to the next group in \"Netnews Groups\"."
1222 "Moves the point to the next header that is not in your kill file. If you
1223 move off the end of the buffer and there are more headers, then get them.
1224 Otherwise go on to the next group in \"Netnews Groups\".
1225 Returns nil if we have gone on to the next group, :done if there are no
1226 more groups to read, or T if everything is normal."
1227 (declare (ignore p))
1228 (let* ((nn-info (variable-value 'netnews-info :buffer headers-buffer))
1229 (point (buffer-point headers-buffer)))
1230 (with-mark ((start point)
1231 (end point))
1232 (loop
1233 (line-offset point 1)
1234 (cond ((eq (nn-maybe-get-more-headers nn-info) :go-on)
1235 (cond ((eq (value netnews-last-header-style) :this-headers)
1236 (let ((headers-buffer (nn-get-headers-buffer)))
1237 (change-to-buffer headers-buffer))
1238 (editor-error "Last header."))
1239 (t
1240 (return (change-to-next-group nn-info headers-buffer)))))
1241 (t
1242 (line-start (move-mark start point))
1243 (character-offset (move-mark end start) 1)
1244 (unless (string= (region-to-string (region start end)) "K")
1245 (return t))))))))
1246
1247(defcommand "Netnews Headers Scroll Window Up" (p)
1248 "Does what \"Scroll Window Up\" does, but fetches backward when the point
1249 reaches the start of the headers buffer."
1250 "Does what \"Scroll Window Up\" does, but fetches backward when the point
1251 reaches the start of the headers buffer."
1252 (scroll-window-up-command p)
1253 (let ((headers-buffer (current-buffer))
1254 (nn-info (value netnews-info)))
1255 (when (and (displayed-p (buffer-start-mark headers-buffer)
1256 (current-window))
1257 (not (= (nn-info-first nn-info)
1258 (nn-info-first-visible nn-info))))
1259 (buffer-start (current-point))
1260 (netnews-previous-line-command nil))))
1261
1262(defcommand "Netnews Headers Scroll Window Down" (p)
1263 "Does what \"Scroll Window Down\" does, but when the point reaches the end of
1264 the headers buffer, pending headers are inserted."
1265 "Does what \"Scroll Window Down\" does, but when the point reaches the end of
1266 the headers buffer, pending headers are inserted."
1267 (scroll-window-down-command p)
1268 (let ((headers-buffer (current-buffer))
1269 (nn-info (value netnews-info)))
1270 (when (and (displayed-p (buffer-end-mark headers-buffer) (current-window))
1271 (not (= (nn-info-last nn-info) (nn-info-last-visible nn-info))))
1272 (buffer-end (current-point))
1273 (netnews-next-line-command nil))))
1274
1275(defcommand "Netnews Message Keep Buffer" (p)
1276 "Specifies that you don't want Hemlock to reuse the current message buffer."
1277 "Specifies that you don't want Hemlock to reuse the current message buffer."
1278 (declare (ignore p))
1279 (unless (hemlock-bound-p 'netnews-message-info)
1280 (editor-error "Not in a News-Message buffer."))
1281 (setf (nm-info-keep-p (value netnews-message-info)) t))
1282
1283(defcommand "Netnews Goto Headers Buffer" (p)
1284 "From \"Message Mode\", switch to the associated headers buffer."
1285 "From \"Message Mode\", switch to the associated headers buffer."
1286 (declare (ignore p))
1287 (unless (hemlock-bound-p 'netnews-message-info)
1288 (editor-error "Not in a message buffer."))
1289 (let ((headers-buffer (nm-info-headers-buffer (value netnews-message-info))))
1290 (unless headers-buffer (editor-error "Headers buffer has been deleted"))
1291 (change-to-buffer headers-buffer)))
1292
1293(defcommand "Netnews Goto Post Buffer" (p)
1294 "Change to the associated \"Post\" buffer (if there is one) from a
1295 \"News-Message\" buffer."
1296 "Change to the associated \"Post\" buffer (if there is one) from a
1297 \"News-Message\" buffer."
1298 (declare (ignore p))
1299 (unless (hemlock-bound-p 'netnews-message-info)
1300 (editor-error "Not in a News-Message buffer."))
1301 (let ((post-buffer (nm-info-post-buffer (value netnews-message-info))))
1302 (unless post-buffer (editor-error "No associated post buffer."))
1303 (change-to-buffer post-buffer)))
1304
1305(defcommand "Netnews Goto Draft Buffer" (p)
1306 "Change to the associated \"Draft\" buffer (if there is one) from a
1307 \"News-Message\" buffer."
1308 "Change to the associated \"Draft\" buffer (if there is one) from a
1309 \"News-Message\" buffer."
1310 (declare (ignore p))
1311 (unless (hemlock-bound-p 'netnews-message-info)
1312 (editor-error "Not in a News-Message buffer."))
1313 (let ((draft-buffer (nm-info-draft-buffer (value netnews-message-info))))
1314 (unless draft-buffer (editor-error "No associated post buffer."))
1315 (change-to-buffer draft-buffer)))
1316
1317(defcommand "Netnews Select Message Buffer" (p)
1318 "Change to the associated message buffer (if there is one) in \"Post\" or
1319 \"News-Headers\" modes."
1320 "Change to the associated message buffer (if there is one) in \"Post\" or
1321 \"News-Headers\" modes."
1322 (declare (ignore p))
1323 (let* ((cbuf (current-buffer))
1324 (mbuf (cond ((hemlock-bound-p 'post-info :buffer cbuf)
1325 (post-info-message-buffer (value post-info)))
1326 ((hemlock-bound-p 'netnews-info :buffer cbuf)
1327 (nn-info-buffer (value netnews-info)))
1328 (t
1329 (editor-error "Not in a \"Post\" or \"News-Headers\" ~
1330 buffer.")))))
1331 (unless mbuf (editor-error "No assocated message buffer."))
1332 (change-to-buffer mbuf)))
1333
1334;;; CHANGE-TO-NEXT-GROUP deletes nn-info's headers buffer region and sets
1335;;; up the next group in that buffer. If there are no more groups to read,
1336;;; exits gracefully.
1337;;;
1338(defun change-to-next-group (nn-info headers-buffer)
1339 (when (nn-info-updatep nn-info)
1340 (nn-update-database-file (nn-info-latest nn-info)
1341 (nn-info-current nn-info)))
1342 (let ((next-group (cadr (member (nn-info-current nn-info)
1343 (nn-info-groups nn-info) :test #'string=))))
1344 (cond (next-group
1345 (message "Going on to ~A" next-group)
1346 (force-output *echo-area-stream*)
1347 (let ((message-buffer (nn-info-buffer nn-info)))
1348 (when message-buffer
1349 (setf (buffer-name message-buffer)
1350 (nn-unique-message-buffer-name next-group))))
1351 (setf (buffer-name headers-buffer)
1352 (nn-unique-headers-name next-group))
1353 (setf (nn-info-current nn-info) next-group)
1354 (with-writable-buffer (headers-buffer)
1355 (delete-region (buffer-region headers-buffer)))
1356 (setup-group next-group nn-info headers-buffer)
1357 nil)
1358 (t
1359 (if (eq headers-buffer *nn-headers-buffer*)
1360 (message "This was your last group. Exiting Netnews.")
1361 (message "Done with ~A. Exiting Netnews."
1362 (nn-info-current nn-info)))
1363 (netnews-exit-command nil t headers-buffer)
1364 :done))))
1365
1366(defun nn-update-database-file (latest group-name)
1367 (when latest (setf (nn-last-read-message-number group-name) latest)))
1368
1369
1370
1371
1372;;;; More commands.
1373
1374(defhvar "Netnews Scroll Show Next Message"
1375 "When non-nil, the default, Hemlock will show the next message in a group
1376 when you scroll off the end of one. Otherwise Hemlock will editor error
1377 that you are at the end of the buffer."
1378 :value T)
1379
1380(defcommand "Netnews Message Scroll Down" (p &optional (buffer (current-buffer))
1381 (window (current-window)))
1382 "Scrolls the current window down one screenful, checking to see if we need
1383 to get the next message."
1384 "Scrolls the current window down one screenful, checking to see if we need
1385 to get the next message."
1386 (if (displayed-p (buffer-end-mark buffer) window)
1387 (if (value netnews-scroll-show-next-message)
1388 (netnews-next-article-command nil)
1389 (editor-error "At end of buffer."))
1390 (scroll-window-down-command p window)))
1391
1392(defcommand "Netnews Go to Next Group" (p)
1393 "Goes on to the next group in \"Netnews Group File\", setting the group
1394 pointer for this group to the the latest message read. With an argument
1395 does not modify the group pointer."
1396 "Goes on to the next group in \"Netnews Group File\", setting the group
1397 pointer for this group to the the latest message read. With an argument
1398 does not modify the group pointer."
1399 (nn-punt-headers (if p :none :latest)))
1400
1401(defcommand "Netnews Group Punt Messages" (p)
1402 "Go on to the next group in \"Netnews Group File\" setting the netnews
1403 pointer for this group to the last message. With an argument, set the
1404 pointer to the last visible message in this group."
1405 "Go on to the next group in \"Netnews Group File\" setting the netnews
1406 pointer for this group to the last message. With an argument, set the
1407 pointer to the last visible message in this group."
1408 (nn-punt-headers (if p :last-visible :punt)))
1409
1410(defcommand "Netnews Quit Starting Here" (p)
1411 "Go on to the next group in \"Netnews Group File\", setting the group
1412 pointer for this group to the message before the currently displayed one
1413 or the message under the point if none is currently displayed."
1414 "Go on to the next group in \"Netnews Group File\", setting the group
1415 pointer for this group to the message before the currently displayed one
1416 or the message under the point if none is currently displayed."
1417 (declare (ignore p))
1418 (nn-punt-headers :this-one))
1419
1420(defun nn-punt-headers (pointer-type)
1421 (let* ((headers-buffer (nn-get-headers-buffer))
1422 (nn-info (variable-value 'netnews-info :buffer headers-buffer))
1423 (stream (nn-info-header-stream nn-info)))
1424 (message "Exiting ~A" (nn-info-current nn-info))
1425 (setf (nn-info-latest nn-info)
1426 (ecase pointer-type
1427 (:latest (nn-info-latest nn-info))
1428 (:punt (nn-info-last nn-info))
1429 (:last-visible (nn-info-last-visible nn-info))
1430 (:this-one
1431 (1- (if (minusp (nn-info-current-displayed-message nn-info))
1432 (array-element-from-mark (buffer-point headers-buffer)
1433 (nn-info-message-ids nn-info))
1434 (nn-info-current-displayed-message nn-info))))
1435 (:none nil)))
1436 ;; This clears out all headers that waiting on header-stream.
1437 ;; Must process each response in case a message is not really there.
1438 ;; If it isn't, then the call to WITH-INPUT-FROM-NNTP will gobble up
1439 ;; the error message and the next real article.
1440 ;;
1441 (when (nn-info-messages-waiting nn-info)
1442 (dotimes (i (nn-info-batch-count nn-info))
1443 (let ((response (process-status-response stream)))
1444 (when response (with-input-from-nntp (string stream))))))
1445 (change-to-next-group nn-info headers-buffer)))
1446
1447(defcommand "Fetch All Headers" (p)
1448 "Fetches the rest of the headers in the current group.
1449 Warning: This will take a while if there are a lot."
1450 "Fetches the rest of the headers in the current group.
1451 Warning: This will take a while if there are a lot."
1452 (declare (ignore p))
1453 (let* ((headers-buffer (nn-get-headers-buffer))
1454 (nn-info (variable-value 'netnews-info :buffer headers-buffer)))
1455 (if (nn-info-messages-waiting nn-info)
1456 (message "Fetching the rest of the headers for ~A"
1457 (nn-info-current nn-info))
1458 (editor-error "All headers are in buffer."))
1459 ;; The first of these calls writes the headers that are waiting on the
1460 ;; headers stream and requests the rest. The second inserts the rest, if
1461 ;; there are any.
1462 ;;
1463 (nn-write-headers-to-mark nn-info headers-buffer t)
1464 (nn-write-headers-to-mark nn-info headers-buffer)))
1465
1466
1467(defcommand "List All Groups" (p &optional buffer)
1468 "Shows all available newsgroups in a buffer."
1469 "Shows all available newsgroups in a buffer."
1470 (declare (ignore p))
1471 (let* ((headers-buffer (nn-get-headers-buffer))
1472 (nn-info (if headers-buffer
1473 (variable-value 'netnews-info :buffer headers-buffer)))
1474 (stream (if headers-buffer
1475 (nn-info-stream nn-info)
1476 (connect-to-nntp))))
1477 (nntp-list stream)
1478 (message "Fetching group list...")
1479 (process-status-response stream)
1480 (let* ((buffer (or buffer (make-buffer (nn-new-list-newsgroups-name))))
1481 (point (buffer-point buffer))
1482 (groups (make-array 1500 :fill-pointer 0 :adjustable t)))
1483 (with-input-from-nntp (string (if headers-buffer
1484 (nn-info-stream nn-info)
1485 stream))
1486 (vector-push-extend string groups))
1487 (sort groups #'string<)
1488 (dotimes (i (length groups))
1489 (let ((group (aref groups i)))
1490 (multiple-value-bind (last first) (list-response-args group)
1491 (declare (ignore first))
1492 (insert-string point group 0 (position #\space group))
1493 (insert-string point (format nil ": ~D~%" last)))))
1494 (setf (buffer-modified buffer) nil)
1495 (buffer-start point)
1496 (change-to-buffer buffer))
1497 (unless headers-buffer (close stream))))
1498
1499(defun nn-new-list-newsgroups-name ()
1500 (let ((name "Newsgroups List")
1501 (number 0))
1502 (declare (simple-string name)
1503 (fixnum number))
1504 (loop
1505 (unless (getstring name *buffer-names*) (return name))
1506 (setf name (format nil "Newsgroups List ~D" number))
1507 (incf number))))
1508
1509(defhvar "Netnews Message File"
1510 "This value is merged with your home directory to get the pathname of the
1511 file to which Hemlock will append messages."
1512 :value "hemlock.messages")
1513
1514(defhvar "Netnews Exit Confirm"
1515 "When non-nil, the default, \"Netnews Exit\" will ask you if you really
1516 want to. If this variable is NIL, you will not be prompted."
1517 :value T)
1518
1519(defcommand "Netnews Exit" (p &optional no-prompt-p
1520 (headers-buf (nn-get-headers-buffer)))
1521 "Exit Netnews from a netnews headers or netnews message buffer."
1522 "Exit Netnews from a netnews headers or netnews message buffer."
1523 (declare (ignore p))
1524 (let ((browse-buffer (variable-value 'netnews-browse-buffer
1525 :buffer headers-buf)))
1526 (when (or browse-buffer
1527 no-prompt-p
1528 (not (value netnews-exit-confirm))
1529 (prompt-for-y-or-n :prompt "Exit Netnews? "
1530 :default "Y"
1531 :default-string "Y"
1532 :help "Yes exits netnews mode."))
1533 (let* ((nn-info (variable-value 'netnews-info :buffer headers-buf))
1534 (message-buffer (nn-info-buffer nn-info))
1535 (headers-window (nn-info-headers-window nn-info))
1536 (message-window (nn-info-message-window nn-info)))
1537 (when (nn-info-updatep nn-info)
1538 (nn-update-database-file (nn-info-latest nn-info)
1539 (nn-info-current nn-info)))
1540 (when (and (eq (value netnews-read-style) :multiple)
1541 (member headers-window *window-list*)
1542 (member message-window *window-list*))
1543 (delete-window message-window))
1544 (when message-buffer (delete-buffer-if-possible message-buffer))
1545 (delete-buffer-if-possible headers-buf)
1546 (when browse-buffer (change-to-buffer browse-buffer))))))
1547
1548
1549
1550
1551;;;; Commands to append messages to a file or file messages into mail folders.
1552
1553(defcommand "Netnews Append to File" (p)
1554 "In a \"News-Headers\" buffer, appends the message under the point onto
1555 the file named by \"Netnews Message File\". In a \"News-Message\" buffer,
1556 appends the message in the current buffer to the same file."
1557 "In a \"News-Headers\" buffer, appends the message under the point onto
1558 the file named by \"Netnews Message File\". In a \"News-Message\" buffer,
1559 appends the message in the current buffer to the same file."
1560 (let* ((filename (merge-pathnames (value netnews-message-file)
1561 (user-homedir-pathname)))
1562 (file (prompt-for-file :prompt "Append to what file: "
1563 :must-exist nil
1564 :default filename
1565 :default-string (namestring filename))))
1566 (when (and p (probe-file file))
1567 (delete-file file))
1568 (message "Appending message to ~S" (namestring file))
1569 (cond ((hemlock-bound-p 'netnews-info)
1570 (let* ((nn-info (value netnews-info))
1571 (stream (nn-info-stream nn-info))
1572 (article-number (array-element-from-mark
1573 (current-point)
1574 (nn-info-message-ids nn-info)
1575 "No header under point.")))
1576 (with-open-file (file file :direction :output
1577 :if-exists :append
1578 :if-does-not-exist :create)
1579 (nntp-article article-number stream)
1580 (process-status-response stream)
1581 (with-input-from-nntp (string (nn-info-stream nn-info))
1582 (write-line string file :end (1- (length string)))))))
1583 (t
1584 (write-file (buffer-region (current-buffer)) file)))
1585 ;; Put a page separator and some whitespace between messages for
1586 ;; readability when printing or scanning.
1587 ;;
1588 (with-open-file (f file :direction :output :if-exists :append)
1589 (terpri f)
1590 (terpri f)
1591 (write-line "
1592" f)
1593 (terpri f))))
1594
1595(defcommand "Netnews Headers File Message" (p)
1596 "Files the message under the point into a folder of your choice. If the
1597 folder you select does not exist, it is created."
1598 "Files the message under the point into a folder of your choice. If the
1599 folder you select does not exist, it is created."
1600 (declare (ignore p))
1601 (nn-file-message (value netnews-info) :headers))
1602
1603(defcommand "Netnews Message File Message" (p)
1604 "Files the message in the current buffer into a folder of your choice. If
1605 folder you select does not exist, it is created."
1606 "Files the message in the current buffer into a folder of your choice. If
1607 folder you select does not exist, it is created."
1608 (declare (ignore p))
1609 (nn-file-message (variable-value 'netnews-info
1610 :buffer (nn-get-headers-buffer))
1611 :message))
1612
1613(defun nn-file-message (nn-info kind)
1614 (let ((article-number (array-element-from-mark (current-point)
1615 (nn-info-message-ids nn-info)
1616 "No header under point."))
1617 (folder (prompt-for-folder :prompt "MH Folder: "
1618 :must-exist nil)))
1619 (unless (folder-existsp folder)
1620 (if (prompt-for-y-or-n
1621 :prompt "Destination folder doesn't exist. Create it? "
1622 :default t :default-string "Y")
1623 (create-folder folder)
1624 (editor-error "Not filing message.")))
1625 (message "Filing message into ~A" folder)
1626 (ecase kind
1627 (:headers (nntp-article article-number (nn-info-stream nn-info))
1628 (process-status-response (nn-info-stream nn-info))
1629 (with-open-file (s "/tmp/temp.msg" :direction :output
1630 :if-exists :rename-and-delete
1631 :if-does-not-exist :create)
1632 (with-input-from-nntp (string (nn-info-stream nn-info))
1633 (write-line string s :end (1- (length string))))))
1634 (:message (write-file (buffer-region (current-buffer)) "/tmp/temp.msg"
1635 :keep-backup nil)))
1636 (mh "inc" `(,folder "-silent" "-file" "/tmp/temp.msg"))
1637 (message "Done.")))
1638
1639
1640
1641
1642;;;; "Post" Mode and supporting commands.
1643
1644(defmode "Post" :major-p nil)
1645
1646(defun nn-unique-post-buffer-name ()
1647 (let ((name "Post")
1648 (number 0))
1649 (loop
1650 (unless (getstring name *buffer-names*) (return name))
1651 (setf name (format nil "Post ~D" number))
1652 (incf number))))
1653
1654;;; We usually know what the subject and newsgroups are, so keep these patterns
1655;;; around to make finding where to insert the information easy.
1656;;;
1657(defvar *draft-subject-pattern*
1658 (new-search-pattern :string-insensitive :forward "Subject:"))
1659
1660(defvar *draft-newsgroups-pattern*
1661 (new-search-pattern :string-insensitive :forward "Newsgroups:"))
1662
1663(defcommand "Netnews Post Message" (p)
1664 "Set up a buffer for posting to netnews."
1665 "Set up a buffer for posting to netnews."
1666 (declare (ignore p))
1667 (let ((headers-buf (nn-get-headers-buffer))
1668 (post-buf (nn-make-post-buffer)))
1669 ;; If we're in a "News-Headers" or "News-Message" buffer, fill in the
1670 ;; newsgroups: slot in the header.
1671 (when headers-buf
1672 (insert-string-after-pattern (buffer-point post-buf)
1673 *draft-newsgroups-pattern*
1674 (nn-info-current
1675 (variable-value
1676 'netnews-info :buffer headers-buf))))
1677 (nn-post-message nil post-buf)))
1678
1679(defcommand "Netnews Abort Post" (p)
1680 "Abort the current post."
1681 "Abort the current post."
1682 (declare (ignore p))
1683 (delete-buffer-if-possible (current-buffer)))
1684
1685(defun foobie-frob (post-info buffer)
1686 (declare (ignore post-info))
1687 (change-to-buffer buffer))
1688#|
1689 #'(lambda (post-info buffer)
1690 (declare (ignore post-info))
1691 (print :changing) (force-output)
1692 (change-to-buffer buffer)
1693 (print :changed) (force-output))
1694|#
1695(defvar *netnews-post-frob-windows-hook* #'foobie-frob
1696 "This hook is FUNCALled in NN-POST-MESSAGE with a post-info structure and
1697 the corresponding \"POST\" buffer before a post is done.")
1698
1699;;; NN-POST-MESSAGE sets up a buffer for posting. If message buffer is
1700;;; supplied, it is associated with the post-info structure for the post
1701;;; buffer.
1702;;;
1703(defun nn-post-message (message-buffer &optional (buffer (nn-make-post-buffer)))
1704 (setf (buffer-modified buffer) nil)
1705 (when message-buffer
1706 (setf (nm-info-post-buffer (variable-value 'netnews-message-info
1707 :buffer message-buffer))
1708 buffer))
1709 (let ((post-info (make-post-info :stream (connect-to-nntp)
1710 :headers-buffer (nn-get-headers-buffer)
1711 :message-buffer message-buffer)))
1712 (defhvar "Post Info"
1713 "Information needed to manipulate post buffers."
1714 :buffer buffer
1715 :value post-info)
1716 (funcall *netnews-post-frob-windows-hook* post-info buffer)))
1717
1718(defun nn-make-post-buffer ()
1719 (let* ((buffer (make-buffer (nn-unique-post-buffer-name)
1720 :delete-hook (list #'nn-post-buffer-delete-hook)))
1721 (stream (make-hemlock-output-stream (buffer-point buffer))))
1722 (setf (buffer-minor-mode buffer "Post") t)
1723 (write-line "Newsgroups: " stream)
1724 (write-line "Subject: " stream)
1725; (write-string "Date: " stream)
1726; (format stream "~A~%" (string-capitalize
1727; (format-universal-time nil (get-universal-time)
1728; :style :government
1729; :print-weekday nil)))
1730 (write-char #\newline stream)
1731 (write-char #\newline stream)
1732 buffer))
1733
1734;;; The usual again. NULLify the appropriate stream slots in associated
1735;;; structures. Also call NN-REPLY-CLEANUP-SPLIT-WINDOWS to see if we
1736;;; need to delete one of the current windows.
1737;;;
1738(defun nn-post-buffer-delete-hook (buffer)
1739 (when (hemlock-bound-p 'post-info)
1740 (nn-reply-cleanup-split-windows buffer)
1741 (let* ((post-info (variable-value 'post-info :buffer buffer))
1742 (message-buffer (post-info-message-buffer post-info)))
1743 (close (post-info-stream post-info))
1744 (when message-buffer
1745 (setf (nm-info-post-buffer (variable-value 'netnews-message-info
1746 :buffer message-buffer))
1747 nil)))))
1748
1749;;; NN-REPLY-USING-CURRENT-WINDOW makes sure there is only one window for a
1750;;; normal reply. *netnews-post-frob-windows-hook* is bound to this when
1751;;; "Netnews Reply to Group" is invoked."
1752;;;
1753(defun nn-reply-using-current-window (post-info buffer)
1754 (declare (ignore post-info))
1755 ;; Make sure there is only one window in :multiple mode.
1756 ;;
1757 (let* ((nn-info (variable-value 'netnews-info
1758 :buffer (nn-get-headers-buffer)))
1759 (headers-window (nn-info-headers-window nn-info))
1760 (message-window (nn-info-message-window nn-info)))
1761 (when (and (eq (value netnews-read-style) :multiple)
1762 (member message-window *window-list*)
1763 (member headers-window *window-list*))
1764 (setf (current-window) message-window)
1765 (delete-window headers-window))
1766 (change-to-buffer buffer)))
1767
1768;;; NN-REPLY-IN-OTHER-WINDOW-HOOK does what NN-REPLY-USING-CURRENT-WINDOW
1769;;; does, but in addition splits the current window in half, displaying the
1770;;; message buffer on top, and the reply buffer on the bottom. Also set some
1771;;; slots in the post info structure so the cleanup function knowd to delete
1772;;; one of the two windows we've created.
1773;;;
1774(defun nn-reply-in-other-window-hook (post-info buffer)
1775 (nn-reply-using-current-window post-info buffer)
1776 (let* ((message-window (current-window))
1777 (reply-window (make-window (buffer-start-mark buffer))))
1778 (setf (window-buffer message-window) (post-info-message-buffer post-info)
1779 (current-window) reply-window
1780 (post-info-message-window post-info) message-window
1781 (post-info-reply-window post-info) reply-window)))
1782
1783;;; NN-REPLY-CLEANUP-SPLIT-WINDOWS just deletes one of the windows that
1784;;; "Netnews Reply to Group in Other Window" created, if they still exist.
1785;;;
1786(defun nn-reply-cleanup-split-windows (post-buffer)
1787 (let* ((post-info (variable-value 'post-info :buffer post-buffer))
1788 (message-window (post-info-message-window post-info)))
1789 (when (and (member (post-info-reply-window post-info) *window-list*)
1790 (member message-window *window-list*))
1791 (delete-window message-window))))
1792
1793(defcommand "Netnews Reply to Group" (p)
1794 "Set up a POST buffer and insert the proper newgroups: and subject: fields.
1795 Should be invoked from a \"News-Message\" or \"News-Headers\" buffer.
1796 In a message buffer, reply to the message in that buffer, in a headers
1797 buffer, reply to the message under the point."
1798 "Set up a POST buffer and insert the proper newgroups: and subject: fields.
1799 Should be invoked from a \"News-Message\" or \"News-Headers\" buffer.
1800 In a message buffer, reply to the message in that buffer, in a headers
1801 buffer, reply to the message under the point."
1802 (declare (ignore p))
1803 (let ((*netnews-post-frob-windows-hook* #'nn-reply-using-current-window))
1804 (nn-reply-to-message)))
1805
1806(defcommand "Netnews Reply to Group in Other Window" (p)
1807 "Does exactly what \"Netnews Reply to Group\" does, but makes two windows.
1808 One of the windows displays the message being replied to, and the other
1809 displays the reply."
1810 "Does exactly what \"Netnews Reply to Group\" does, but makes two windows.
1811 One of the windows displays the message being replied to, and the other
1812 displays the reply."
1813 (declare (ignore p))
1814 (let ((*netnews-post-frob-windows-hook* #'nn-reply-in-other-window-hook))
1815 (nn-reply-to-message)))
1816
1817
1818(defun nn-setup-for-reply-by-mail ()
1819 (let* ((headers-buffer (nn-get-headers-buffer))
1820 (nn-info (variable-value 'netnews-info :buffer headers-buffer))
1821 (message-buffer (nn-info-buffer nn-info))
1822 (nm-info (variable-value 'netnews-message-info :buffer message-buffer))
1823 (draft-buffer (sub-setup-message-draft "comp" :to-field))
1824 (dinfo (variable-value 'draft-information :buffer draft-buffer)))
1825 (setf (buffer-delete-hook draft-buffer)
1826 (list #'cleanup-netnews-draft-buffer))
1827 (when (nm-info-draft-buffer nm-info)
1828 (delete-variable 'message-buffer :buffer (nm-info-draft-buffer nm-info)))
1829 (setf (nm-info-draft-buffer nm-info) draft-buffer)
1830 (when headers-buffer
1831 (defhvar "Headers Buffer"
1832 "This is bound in message and draft buffers to their associated
1833 headers-buffer"
1834 :value headers-buffer :buffer draft-buffer))
1835 (setf (draft-info-headers-mark dinfo)
1836 (copy-mark (buffer-point headers-buffer)))
1837 (defhvar "Message Buffer"
1838 "This is bound in draft buffers to their associated message buffer."
1839 :value message-buffer :buffer draft-buffer)
1840 (values draft-buffer message-buffer)))
1841
1842
1843(defcommand "Netnews Forward Message" (p)
1844 "Creates a Draft buffer and places a copy of the current message in
1845 it, delimited by forwarded message markers."
1846 "Creates a Draft buffer and places a copy of the current message in
1847 it, delimited by forwarded message markers."
1848 (declare (ignore p))
1849 (multiple-value-bind (draft-buffer message-buffer)
1850 (nn-setup-for-reply-by-mail)
1851 (with-mark ((mark (buffer-point draft-buffer) :left-inserting))
1852 (buffer-end mark)
1853 (insert-string mark (format nil "~%------- Forwarded Message~%~%"))
1854 (insert-string mark (format nil "~%------- End of Forwarded Message~%"))
1855 (line-offset mark -2 0)
1856 (insert-region mark (buffer-region message-buffer)))
1857 (nn-reply-using-current-window nil draft-buffer)))
1858
1859
1860(defun nn-reply-to-sender ()
1861 (let* ((headers-buffer (nn-get-headers-buffer))
1862 (nn-info (variable-value 'netnews-info :buffer headers-buffer))
1863 (article (if (and (hemlock-bound-p 'netnews-info)
1864 (minusp (nn-info-current-displayed-message
1865 nn-info)))
1866 (nn-put-article-in-buffer nn-info headers-buffer)
1867 (nn-info-current-displayed-message nn-info))))
1868 (multiple-value-bind (draft-buffer message-buffer)
1869 (nn-setup-for-reply-by-mail)
1870 (let ((point (buffer-point draft-buffer))
1871 (to-field (or (nn-get-one-field nn-info "Reply-To" article)
1872 (nn-get-one-field nn-info "From" article))))
1873 (insert-string-after-pattern point
1874 *draft-to-pattern*
1875 to-field
1876 :end (1- (length to-field)))
1877 (let ((subject-field (nn-subject-replyify
1878 (nn-get-one-field nn-info "Subject" article))))
1879 (insert-string-after-pattern point
1880 *draft-subject-pattern*
1881 subject-field
1882 :end (1- (length subject-field)))))
1883 (nn-reply-using-current-window nil draft-buffer)
1884 (values draft-buffer message-buffer))))
1885
1886(defcommand "Netnews Reply to Sender" (p)
1887 "Reply to the sender of a message via mail using the Hemlock mailer."
1888 "Reply to the sender of a message via mail using the Hemlock mailer."
1889 (declare (ignore p))
1890 (nn-reply-to-sender))
1891
1892(defcommand "Netnews Reply to Sender in Other Window" (p)
1893 "Reply to the sender of a message via mail using the Hemlock mailer. The
1894 screen will be split in half, displaying the post and the draft being
1895 composed."
1896 "Reply to the sender of a message via mail using the Hemlock mailer. The
1897 screen will be split in half, displaying the post and the draft being
1898 composed."
1899 (declare (ignore p))
1900 (multiple-value-bind (draft-buffer message-buffer)
1901 (nn-reply-to-sender)
1902 (let* ((message-window (current-window))
1903 (reply-window (make-window (buffer-start-mark draft-buffer))))
1904 (defhvar "Split Window Draft"
1905 "Indicates window needs to be cleaned up for draft."
1906 :value t :buffer draft-buffer)
1907 (setf (window-buffer message-window) message-buffer
1908 (current-window) reply-window))))
1909
1910;;; CLEANUP-NETNEWS-DRAFT-BUFFER replaces the normal draft buffer delete hook
1911;;; because the generic one tries to set some slots in the related message-info
1912;;; structure which doesn't exist. This function just sets the draft buffer
1913;;; slot of netnews-message-info to nil so it won't screw you when you try
1914;;; to change to the associated draft buffer.
1915;;;
1916(defun cleanup-netnews-draft-buffer (buffer)
1917 (when (hemlock-bound-p 'message-buffer :buffer buffer)
1918 (setf (nm-info-draft-buffer
1919 (variable-value 'netnews-message-info
1920 :buffer (variable-value 'message-buffer
1921 :buffer buffer)))
1922 nil)))
1923
1924;;; NN-REPLYIFY-SUBJECT simply adds "Re: " to the front of a string if it is
1925;;; not already there.
1926;;;
1927(defun nn-subject-replyify (subject)
1928 (if (>= (length subject) 3)
1929 (if (not (string= subject "Re:" :end1 3 :end2 3))
1930 (concatenate 'simple-string "Re: " subject)
1931 subject)
1932 (concatenate 'simple-string "Re: " subject)))
1933
1934(defun insert-string-after-pattern (mark search-pattern string
1935 &key (start 0) (end (length string)))
1936 (buffer-start mark)
1937 (when (and (plusp end)
1938 (find-pattern mark search-pattern))
1939 (insert-string (line-end mark) string start end))
1940 (buffer-end mark))
1941
1942(defun nn-reply-to-message ()
1943 (let* ((headers-buffer (nn-get-headers-buffer))
1944 (nn-info (variable-value 'netnews-info :buffer headers-buffer))
1945 (article (if (and (hemlock-bound-p 'netnews-info)
1946 (minusp (nn-info-current-displayed-message nn-info)))
1947 (nn-put-article-in-buffer nn-info headers-buffer)
1948 (nn-info-current-displayed-message nn-info)))
1949 (post-buffer (nn-make-post-buffer))
1950 (point (buffer-point post-buffer)))
1951
1952 (let ((groups-field (nn-get-one-field nn-info "Newsgroups" article)))
1953 (insert-string-after-pattern point
1954 *draft-newsgroups-pattern*
1955 groups-field
1956 :end (1- (length groups-field))))
1957 (let ((subject-field (nn-subject-replyify
1958 (nn-get-one-field nn-info "Subject" article))))
1959 (insert-string-after-pattern point
1960 *draft-subject-pattern*
1961 subject-field
1962 :end (1- (length subject-field))))
1963 (nn-post-message (nn-info-buffer nn-info) post-buffer)))
1964
1965(defun nn-get-one-field (nn-info field article)
1966 (cdr (assoc field (svref (nn-info-header-cache nn-info)
1967 (- article (nn-info-first nn-info)))
1968 :test #'string-equal)))
1969
1970(defvar *nntp-timeout-handler* 'nn-recover-from-timeout
1971 "This function gets FUNCALled when NNTP times out on us with the note passed
1972 to PROCESS-STATUS-RESPONSE. The default assumes the note is an NN-INFO
1973 structure and tries to recover from the timeout.")
1974
1975(defvar *nn-last-command-issued* nil
1976 "The last string issued to one of the NNTP streams. Used to recover from
1977 a nntp timeout.")
1978
1979;;; NN-RECOVER-FROM-POSTING-TIMEOUT is the recover method used when posting.
1980;;; It just resets the value of \"NNTP Stream\" and issues the last command
1981;;; again.
1982;;;
1983(defun nn-recover-from-posting-timeout (ignore)
1984 (declare (ignore ignore))
1985 (let ((stream (connect-to-nntp)))
1986 (setf (post-info-stream (value post-info)) stream)
1987 (write-nntp-command *nn-last-command-issued* stream :recover)
1988 (process-status-response stream)))
1989
1990(defhvar "Netnews Reply Address"
1991 "What the From: field will be when you post messages. If this is nil,
1992 the From: field will be determined using the association of :USER
1993 in *environment-list* and your machine name."
1994 :value NIL)
1995
1996(defhvar "Netnews Signature Filename"
1997 "This value is merged with your home directory to get the pathname your
1998 signature, which is appended to every post you make."
1999 :value ".hemlock-sig")
2000
2001(defhvar "Netnews Deliver Post Confirm"
2002 "This determines whether Netnews Deliver Post will ask for confirmation
2003 before posting the current message."
2004 :value t)
2005
2006(defcommand "Netnews Deliver Post" (p)
2007 "Deliver the current Post buffer to the NNTP server. If the file named by
2008 the value of \"Netnews Signature Filename\" exists, it is appended to the
2009 end of the message after adding a newline."
2010 "Deliver the current Post buffer to the NNTP server, cleaning up any windows
2011 we need and landing us in the headers buffer if this was a reply."
2012 (declare (ignore p))
2013 (when (or (not (value netnews-deliver-post-confirm))
2014 (prompt-for-y-or-n :prompt "Post message? " :default t))
2015 (let* ((*nntp-timeout-handler* #'nn-recover-from-posting-timeout)
2016 (stream (post-info-stream (value post-info))))
2017 (nntp-post stream)
2018 (let ((winp (process-status-response stream))
2019 ;; Rebind stream here because the stream may have been pulled out
2020 ;; from under us by an NNTP timeout. The recover method for posting
2021 ;; resets the Hemlock Variable.
2022 (stream (post-info-stream (value post-info))))
2023 (unless winp (editor-error "Posting prohibited in this group."))
2024 (let ((buffer (current-buffer))
2025 (username (value netnews-reply-address)))
2026 (nn-write-line (format nil "From: ~A"
2027 (if username
2028 username
2029 (string-downcase
2030 (format nil "~A@~A"
2031 (cdr (assoc :user
2032 ext:*environment-list*))
2033 (machine-instance)))))
2034 stream)
2035 (filter-region #'(lambda (string)
2036 (when (string= string ".")
2037 (write-char #\. stream))
2038 (nn-write-line string stream))
2039 (buffer-region buffer))
2040 ;; Append signature
2041 ;;
2042 (let ((filename (merge-pathnames (value netnews-signature-filename)
2043 (user-homedir-pathname))))
2044 (when (probe-file filename)
2045 (with-open-file (istream filename :direction :input)
2046 (loop
2047 (let ((line (read-line istream nil nil)))
2048 (unless line (return))
2049 (nn-write-line line stream))))))
2050 (write-line nntp-eof stream)
2051 (delete-buffer-if-possible buffer)
2052 (let ((headers-buffer (nn-get-headers-buffer)))
2053 (when headers-buffer (change-to-buffer headers-buffer)))
2054 (message "Message Posted."))))))
2055
2056(defun nn-write-line (line stream)
2057 (write-string line stream)
2058 (write-char #\return stream)
2059 (write-char #\newline stream)
2060 line)
2061
2062
2063
2064
2065;;;; News-Browse mode.
2066
2067(defmode "News-Browse" :major-p t)
2068
2069(defhvar "Netnews Group File"
2070 "If the value of \"Netnews Groups\" is nil, \"Netnews\" merges this
2071 variable with your home directory and looks there for a list of newsgroups
2072 (one per line) to read. Groups may be added using \"Netnews Browse\ and
2073 related commands, or by editing this file."
2074 :value ".hemlock-groups")
2075
2076(defcommand "Netnews Browse" (p)
2077 "Puts all netnews groups in a buffer and provides commands for reading them
2078 and adding them to the file specified by the merge of \"Netnews Group File\"
2079 and your home directory."
2080 "Puts all netnews groups in a buffer and provides commands for reading them
2081 and adding them to the file specified by the merge of \"Netnews Group File\"
2082 and your home directory."
2083 (declare (ignore p))
2084 (let ((buffer (make-buffer "Netnews Browse")))
2085 (cond (buffer
2086 (list-all-groups-command nil buffer)
2087 (setf (buffer-major-mode buffer) "News-Browse")
2088 (setf (buffer-writable buffer) nil))
2089 (t (change-to-buffer (getstring "Netnews Browse" *buffer-names*))))))
2090
2091(defcommand "Netnews Quit Browse" (p)
2092 "Exit News-Browse Mode."
2093 "Exit News-Browse Mode."
2094 (declare (ignore p))
2095 (delete-buffer-if-possible (current-buffer)))
2096
2097(defcommand "Netnews Browse Read Group" (p &optional (mark (current-point)))
2098 "Read the group on the line under the current point paying no attention to
2099 the \"Hemlock Database File\" entry for this group. With an argument, use
2100 and modify the database file."
2101 "Read the group on the line under the current point paying no attention to
2102 the \"Hemlock Database File\" entry for this group. With an argument, use
2103 and modify the database file."
2104 (let ((group-info-string (line-string (mark-line mark))))
2105 (netnews-command nil (subseq group-info-string
2106 0 (position #\: group-info-string))
2107 nil (current-buffer) p)))
2108
2109(defcommand "Netnews Browse Pointer Read Group" (p)
2110 "Read the group on the line where you just clicked paying no attention to the
2111 \"Hemlock Databse File\" entry for this group. With an argument, use and
2112 modify the databse file."
2113 "Read the group on the line where you just clicked paying no attention to the
2114 \"Hemlock Databse File\" entry for this group. With an argument, use and
2115 modify the databse file."
2116 (multiple-value-bind (x y window) (last-key-event-cursorpos)
2117 (unless window (editor-error "Couldn't figure out where last click was."))
2118 (unless y (editor-error "There is no group in the modeline."))
2119 (netnews-browse-read-group-command p (cursorpos-to-mark x y window))))
2120
2121(defcommand "Netnews Browse Add Group to File" (p &optional
2122 (mark (current-point)))
2123 "Append the newsgroup on the line under the point to the file specified by
2124 \"Netnews Group File\". With an argument, delete all groups that were
2125 there to start with."
2126 "Append the newsgroup on the line under the point to the file specified by
2127 \"Netnews Group File\". With an argument, delete all groups that were
2128 there to start with."
2129 (declare (ignore p))
2130 (let* ((group-info-string (line-string (mark-line mark)))
2131 (group (subseq group-info-string 0 (position #\: group-info-string))))
2132 (with-open-file (s (merge-pathnames (value netnews-group-file)
2133 (user-homedir-pathname))
2134 :direction :output
2135 :if-exists :append
2136 :if-does-not-exist :create)
2137 (write-line group s))
2138 (message "Adding ~S to newsgroup file." group)))
2139
2140(defcommand "Netnews Browse Pointer Add Group to File" (p)
2141 "Append the newsgroup you just clicked on to the file specified by
2142 \"Netnews Group File\"."
2143 "Append the newsgroup you just clicked on to the file specified by
2144 \"Netnews Group File\"."
2145 (declare (ignore p))
2146 (multiple-value-bind (x y window) (last-key-event-cursorpos)
2147 (unless window (editor-error "Couldn't figure out where last click was."))
2148 (unless y (editor-error "There is no group in the modeline."))
2149 (netnews-browse-add-group-to-file-command
2150 nil (cursorpos-to-mark x y window))))
2151
2152
2153
2154
2155;;;; Low-level stream operations.
2156
2157(defun streams-for-nntp ()
2158 (clear-echo-area)
2159 (format *echo-area-stream* "Connecting to NNTP...~%")
2160 (force-output *echo-area-stream*)
2161 (values (connect-to-nntp) (connect-to-nntp)))
2162
2163
2164(defparameter *nntp-port* 119
2165 "The nntp port number for NNTP as specified in RFC977.")
2166
2167(defhvar "Netnews NNTP Server"
2168 "The hostname of the NNTP server to use for reading Netnews."
2169 :value "netnews.srv.cs.cmu.edu")
2170
2171(defhvar "Netnews NNTP Timeout Period"
2172 "The number of seconds to wait before timing out when trying to connect
2173 to the NNTP server."
2174 :value 30)
2175
2176(defun raw-connect-to-nntp ()
2177 (let ((stream (system:make-fd-stream
2178 (ext:connect-to-inet-socket (value netnews-nntp-server)
2179 *nntp-port*)
2180 :input t :output t :buffering :line :name "NNTP"
2181 :timeout (value netnews-nntp-timeout-period))))
2182 (process-status-response stream)
2183 stream))
2184
2185(defun connect-to-nntp ()
2186 (handler-case
2187 (raw-connect-to-nntp)
2188 (io-timeout ()
2189 (editor-error "Connection to NNTP timed out. Try again later."))))
2190
2191(defvar *nn-last-command-type* nil
2192 "Used to recover from a nntp timeout.")
2193
2194(defun write-nntp-command (command stream type)
2195 (setf *nn-last-command-type* type)
2196 (setf *nn-last-command-issued* command)
2197 (write-string command stream)
2198 (write-char #\return stream)
2199 (write-char #\newline stream)
2200 (force-output stream))
2201
2202
2203
2204
2205;;;; PROCESS-STATUS-RESPONSE and NNTP error handling.
2206
2207(defconstant nntp-error-codes '(#\4 #\5)
2208 "These codes signal that NNTP could not complete the request you asked for.")
2209
2210(defvar *nntp-error-handlers* nil)
2211
2212;;; PROCESS-STATUS-RESPONSE makes sure a response waiting at the server is
2213;;; valid. If the response code starts with a 4 or 5, then look it up in
2214;;; *nntp-error-handlers*. If an error handler is defined, then FUNCALL it
2215;;; on note. Otherwise editor error. If the response is not an error code,
2216;;; then just return what NNTP returned to us for parsing later.
2217;;;
2218(defun process-status-response (stream &optional note)
2219 (let ((str (read-line stream)))
2220 (if (member (schar str 0) nntp-error-codes :test #'char=)
2221 (let ((error-handler (cdr (assoc str *nntp-error-handlers*
2222 :test #'(lambda (string1 string2)
2223 (string= string1 string2
2224 :end1 3
2225 :end2 3))))))
2226 (unless error-handler
2227 (error "NNTP error -- ~A" (subseq str 4 (1- (length str)))))
2228 (funcall error-handler note))
2229 str)))
2230
2231(defun nn-recover-from-timeout (nn-info)
2232 (message "NNTP timed out, attempting to reconnect and continue...")
2233 (let ((stream (nn-info-stream nn-info))
2234 (header-stream (nn-info-header-stream nn-info)))
2235 ;; If some messages are waiting on the header stream, insert them.
2236 ;;
2237 (when (listen header-stream)
2238 (nn-write-headers-to-mark nn-info (nn-get-headers-buffer)))
2239 (close stream)
2240 (close header-stream)
2241 (setf stream (connect-to-nntp)
2242 header-stream (connect-to-nntp)
2243 (nn-info-stream nn-info) stream
2244 (nn-info-header-stream nn-info) header-stream)
2245 (let ((last-command *nn-last-command-issued*)
2246 (last-command-type *nn-last-command-type*)
2247 (current (nn-info-current nn-info)))
2248 (nntp-group current stream header-stream)
2249 (process-status-response stream)
2250 (process-status-response header-stream)
2251 (if (consp last-command)
2252 (let ((stream-type (car last-command)))
2253 (apply #'nn-send-many-head-requests
2254 (cons (if (eq stream-type :header) header-stream stream)
2255 (cdr last-command))))
2256 (ecase last-command-type
2257 ((:list :article :body)
2258 (write-nntp-command last-command stream :recover)
2259 (process-status-response stream))
2260 ((:header-group :normal-group)
2261 (write-nntp-command last-command stream :recover)
2262 (write-nntp-command last-command header-stream :recover)))))))
2263
2264;;; DEF-NNTP-ERROR-HANDLER takes a code and a function and associates the two
2265;;; in *nntp-error-handlers*. If while PROCESSING a STATUS RESPONSE we come
2266;;; across one of these error codes, then FUNCALL the appropriate handler.
2267;;;
2268(defun def-nntp-error-handler (code function)
2269 (pushnew (cons (format nil "~D" code) function) *nntp-error-handlers*))
2270
2271;;; 503 is an NNTP timeout. The code I wrote reconnects and recovers
2272;;; completely.
2273;;;
2274(def-nntp-error-handler 503 #'(lambda (note)
2275 (funcall *nntp-timeout-handler* note)))
2276
2277;;; 400 means NNTP is cutting us of for some reason. There is really nothing
2278;;; we can do.
2279;;;
2280(def-nntp-error-handler 400 #'(lambda (ignore)
2281 (declare (ignore ignore))
2282 (editor-error "NNTP discontinued service. ~
2283 You should probably quit netnews and try ~
2284 again later.")))
2285
2286;;; Some functions just need to know that something went wrong so they can
2287;;; do something about it, so let them know by returning nil.
2288;;;
2289;;; 411 - The group you tried to read is not a netnews group.
2290;;; 423 - You requested a message that wasn't really there.
2291;;; 440 - Posting is not allowed.
2292;;; 441 - Posting is allowed, but the attempt failed for some other reason.
2293;;;
2294(flet ((nil-function (ignore)
2295 (declare (ignore ignore))
2296 nil))
2297 (def-nntp-error-handler 423 #'nil-function)
2298 (def-nntp-error-handler 411 #'nil-function)
2299 (def-nntp-error-handler 440 #'nil-function)
2300 (def-nntp-error-handler 441 #'nil-function))
2301
2302
2303
2304
2305;;;; Implementation of NNTP response argument parsing.
2306
2307;;; DEF-NNTP-ARG-PARSER returns a form that parses a string for arguments
2308;;; corresponding to each element of types. For instance, if types is
2309;;; (:integer :string :integer :integer), this function returns a form that
2310;;; parses an integer, a string, and two more integers out of an nntp status
2311;;; response.
2312;;;
2313(defmacro def-nntp-arg-parser (types)
2314 (let ((form (gensym))
2315 (start (gensym))
2316 (res nil))
2317 (do ((type types (cdr type)))
2318 ((endp type) form)
2319 (ecase (car type)
2320 (:integer
2321 (push `(parse-integer string :start ,start
2322 :end (setf ,start
2323 (position #\space string
2324 :start (1+ ,start)))
2325 :junk-allowed t)
2326 res))
2327 (:string
2328 (push `(subseq string (1+ ,start)
2329 (position #\space string
2330 :start (setf ,start (1+ ,start))))
2331 res))))
2332 `(let ((,start (position #\space string)))
2333 (values ,@(nreverse res)))))
2334
2335(defun def-nntp-xhdr-arg-parser (string)
2336 (let ((position (position #\space string)))
2337 (values (subseq string (1+ position))
2338 (parse-integer string :start 0 :end position))))
2339
2340(defun xhdr-response-args (string)
2341 (def-nntp-xhdr-arg-parser string))
2342
2343;;; GROUP-RESPONSE-ARGS, ARTICLER-RESPONSE-ARGS, HEAD-RESPONSE-ARGS,
2344;;; BODY-RESPONSE-ARGS, and STAT-RESPONSE-ARGS define NNTP argument parsers
2345;;; for the types of arguments each command will return.
2346;;;
2347(defun group-response-args (string)
2348 "Group response args are an estimate of how many messages there are, the
2349 number of the first message, the number of the last message, and \"y\"
2350 or \"n\", indicating whether the user has rights to post in this group."
2351 (def-nntp-arg-parser (:integer :integer :integer)))
2352
2353(defun list-response-args (string)
2354 (def-nntp-arg-parser (:integer :integer)))
2355
2356(defun article-response-args (string)
2357 "Article response args are the message number and the message ID."
2358 (def-nntp-arg-parser (:integer :string)))
2359
2360(defun head-response-args (string)
2361 "Head response args are the message number and the message ID."
2362 (def-nntp-arg-parser (:integer :string)))
2363
2364(defun body-response-args (string)
2365 "Body response args are the message number and the message ID."
2366 (def-nntp-arg-parser (:integer :string)))
2367
2368(defun stat-response-args (string)
2369 "Stat response args are the message number and the message ID."
2370 (def-nntp-arg-parser (:integer :string)))
2371
2372
2373
2374
2375;;;; Functions that send standard NNTP commands.
2376
2377;;; NNTP-XHDR sends an XHDR command to the NNTP server. We think this is a
2378;;; local extension, but not using it is not pragmatic. It takes over three
2379;;; minutes to HEAD every message in a newsgroup.
2380;;;
2381(defun nntp-xhdr (field start end stream)
2382 (write-nntp-command (format nil "xhdr ~A ~D-~D"
2383 field
2384 (if (numberp start) start (parse-integer start))
2385 (if (numberp end) end (parse-integer end)))
2386 stream
2387 :xhdr))
2388
2389(defun nntp-group (group-name stream header-stream)
2390 (let ((command (concatenate 'simple-string "group " group-name)))
2391 (write-nntp-command command stream :normal-group)
2392 (write-nntp-command command header-stream :header-group)))
2393
2394(defun nntp-list (stream)
2395 (write-nntp-command "list" stream :list))
2396
2397(defun nntp-head (article stream)
2398 (write-nntp-command (format nil "head ~D" article) stream :head))
2399
2400(defun nntp-article (number stream)
2401 (write-nntp-command (format nil "article ~D" number) stream :article))
2402
2403(defun nntp-body (number stream)
2404 (write-nntp-command (format nil "body ~D" number) stream :body))
2405
2406(defun nntp-post (stream)
2407 (write-nntp-command "post" stream :post))
Note: See TracBrowser for help on using the repository browser.