| 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))
|
|---|