From a104a2d8657b9c85eb04aec6261f298f643bf4b2 Mon Sep 17 00:00:00 2001 From: Circling Skies Date: Thu, 5 Dec 2024 18:23:19 -0300 Subject: [PATCH] Fiat lux! --- Makefile | 79 + accounts.lisp | 9 + build-exe.lisp | 5 + build-index-from-fs.lisp | 7 + cron-remove-inactive-users.lisp | 8 + groups/local.control.news/1 | 7 + groups/local.control.news/2 | 7 + groups/local.control.news/3 | 7 + groups/local.control.news/4 | 7 + groups/local.control.news/5 | 7 + groups/local.control.news/6 | 7 + groups/local.control.news/7 | 7 + groups/local.test/1 | 7 + groups/local.test/1.~1~ | 7 + images/gnus-summary.png | Bin 0 -> 50181 bytes images/sylpheed-summary.png | Bin 0 -> 72959 bytes images/tbird-summary.png | Bin 0 -> 58338 bytes loop.asd | 14 + loop.lisp | 1194 ++++++++++++++ loop.nw | 2565 +++++++++++++++++++++++++++++++ noweb.sty | 989 ++++++++++++ peat | 224 +++ 22 files changed, 5157 insertions(+) create mode 100644 Makefile create mode 100644 accounts.lisp create mode 100644 build-exe.lisp create mode 100644 build-index-from-fs.lisp create mode 100644 cron-remove-inactive-users.lisp create mode 100644 groups/local.control.news/1 create mode 100644 groups/local.control.news/2 create mode 100644 groups/local.control.news/3 create mode 100644 groups/local.control.news/4 create mode 100644 groups/local.control.news/5 create mode 100644 groups/local.control.news/6 create mode 100644 groups/local.control.news/7 create mode 100644 groups/local.test/1 create mode 100644 groups/local.test/1.~1~ create mode 100644 images/gnus-summary.png create mode 100644 images/sylpheed-summary.png create mode 100644 images/tbird-summary.png create mode 100644 loop.asd create mode 100644 loop.lisp create mode 100644 loop.nw create mode 100644 noweb.sty create mode 100644 peat diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1dbc8dc --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +SHELL=/bin/sh +REMOTE=dbastos@antartida.xyz +REMOTE_LIB_PATH=quicklisp/local-projects +REMOTE_EXE_PATH=loop-test +SERVICE_NAME=loop-test + +default: + @echo "Sorry. You need to read the Makefile to know what I can make for you." + +all: loop.lisp build-exe.lisp exe run \ +migrate-add-creation-date.lisp cron-remove-inactive-users.lisp + +live: all remote-copy # remote-build-exe + +remote-copy: + scp loop.asd loop.lisp \ + $(REMOTE):$(REMOTE_LIB_PATH)/loop + scp build-exe.lisp \ + $(REMOTE):$(REMOTE_EXE_PATH)/ + scp migrate-add-creation-date.lisp \ + $(REMOTE):$(REMOTE_EXE_PATH)/ + scp cron-remove-inactive-users.lisp \ + $(REMOTE):$(REMOTE_EXE_PATH)/ + +sync-users: + scp $(REMOTE):$(REMOTE_EXE_PATH)/accounts.lisp . + +remote-build-exe: + plink -ssh $(REMOTE) cd $(REMOTE_EXE_PATH)/ && \ + sbcl --script build-exe.lisp && \ + echo "Executable built." + +remote-migrate-account-creation: + plink -ssh $(REMOTE) cd $(REMOTE_EXE_PATH)/ && \ + sbcl --script migrate-add-creation-date.lisp + +remote-cron-remove-inactive-users: + plink -ssh $(REMOTE) cd $(REMOTE_EXE_PATH)/ && \ + sbcl --script remote-cron-remove-inactive-users.lisp + +livedoc: + echo loop.nw | python peat -C 'make loop.pdf' + +run: loop.nw + (any tangle -Rrun < loop.nw > run.tmp || (rm run.tmp && exit 1)) && \ + mv run.tmp run.lisp && \ + chmod 0755 run + +loop.tex: loop.nw + any weave -delay -index loop.nw > loop.tex + +loop.pdf: loop.tex + latexmk -pdf loop + +loop.lisp: loop.nw + (any tangle -Rloop.lisp < loop.nw > loop.tmp || (rm loop.tmp && exit 1)) && \ + mv loop.tmp loop.lisp + +build-exe.lisp: loop.nw + (any tangle -Rbuild-exe.lisp < loop.nw > build-exe.tmp || (rm build-exe.tmp && exit 1)) && \ + mv build-exe.tmp build-exe.lisp + +build-index-from-fs.lisp: loop.nw + (any tangle -Rbuild-index-from-fs.lisp < loop.nw > build-index-from-fs.tmp || (rm build-index-from-fs.tmp && exit 1)) && \ + mv build-index-from-fs.tmp build-index-from-fs.lisp + +cron-remove-inactive-users.lisp: loop.nw + (any tangle -Rcron-remove-inactive-users.lisp < loop.nw > cron-remove-inactive-users.tmp || (rm cron-remove-inactive-users.tmp && exit 1)) && \ + mv cron-remove-inactive-users.tmp cron-remove-inactive-users.lisp + +migrate-add-creation-date.lisp: loop.nw + (any tangle -Rmigrate-add-creation-date.lisp < loop.nw > migrate-add-creation-date.tmp || (rm migrate-add-creation-date.tmp && exit 1)) && \ + mv migrate-add-creation-date.tmp migrate-add-creation-date.lisp + +exe: loop.lisp build-exe.lisp + sbcl --script build-exe.lisp && echo "Executable built okay." + +service: run + sudo ln -s $(pwd) /service/$(SERVICE_NAME) diff --git a/accounts.lisp b/accounts.lisp new file mode 100644 index 0000000..cadd35b --- /dev/null +++ b/accounts.lisp @@ -0,0 +1,9 @@ +(#S(LOOP::ACCOUNT + :USERNAME "ANONYMOUS" + :SEEN 3935609919 + :LAST-POST NIL + :FRIENDS NIL + :PASS 2335603191554807875 + :PASS-LOCKED NIL + :PASS-LOCKED-WHY NIL + :CREATION 3913066800)) diff --git a/build-exe.lisp b/build-exe.lisp new file mode 100644 index 0000000..cf687f8 --- /dev/null +++ b/build-exe.lisp @@ -0,0 +1,5 @@ +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(sb-ext:save-lisp-and-die #P"loop.exe" + :toplevel #'loop:main + :executable t) diff --git a/build-index-from-fs.lisp b/build-index-from-fs.lisp new file mode 100644 index 0000000..531c769 --- /dev/null +++ b/build-index-from-fs.lisp @@ -0,0 +1,7 @@ +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(in-package #:loop) +(connect-index! "message-id.db") +(remake-index!) +(index-from-fs!) +(format t "Index built.~%") diff --git a/cron-remove-inactive-users.lisp b/cron-remove-inactive-users.lisp new file mode 100644 index 0000000..788c1ba --- /dev/null +++ b/cron-remove-inactive-users.lisp @@ -0,0 +1,8 @@ +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(in-package #:loop) +;; (format t *default-pathname-defaults*) +(read-accounts!) +(connect-index! "message-id.db") +(remove-inactive-users!) +(write-accounts!) diff --git a/groups/local.control.news/1 b/groups/local.control.news/1 new file mode 100644 index 0000000..adc869e --- /dev/null +++ b/groups/local.control.news/1 @@ -0,0 +1,7 @@ +Date: 2024-03-07 21:44:31 GMT-3 +Message-Id: +From: Loop +Subject: let there be light +Newsgroups: local.control.news + +Administrative news will be posted here by me. -- Loop diff --git a/groups/local.control.news/2 b/groups/local.control.news/2 new file mode 100644 index 0000000..8fb0618 --- /dev/null +++ b/groups/local.control.news/2 @@ -0,0 +1,7 @@ +Date: 2024-12-05 07:27:01 GMT-3 +Message-Id: +From: Loop +Subject: account HIMMEL removed by Loop +Newsgroups: local.control.news + +HIMMEL didn't log in a first time (for 1 month) since account creation. diff --git a/groups/local.control.news/3 b/groups/local.control.news/3 new file mode 100644 index 0000000..9efd2e1 --- /dev/null +++ b/groups/local.control.news/3 @@ -0,0 +1,7 @@ +Date: 2024-12-05 07:52:39 GMT-3 +Message-Id: +From: Loop +Subject: account HIMMEL removed by Loop +Newsgroups: local.control.news + +HIMMEL didn't log in a first time (for 1 month) since account creation. diff --git a/groups/local.control.news/4 b/groups/local.control.news/4 new file mode 100644 index 0000000..5108f91 --- /dev/null +++ b/groups/local.control.news/4 @@ -0,0 +1,7 @@ +Date: 2024-12-05 07:52:39 GMT-3 +Message-Id: +From: Loop +Subject: account MFELIX locked by Loop +Newsgroups: local.control.news + +MFELIX disappeared for over 3 months. diff --git a/groups/local.control.news/5 b/groups/local.control.news/5 new file mode 100644 index 0000000..6b578e6 --- /dev/null +++ b/groups/local.control.news/5 @@ -0,0 +1,7 @@ +Date: 2024-12-05 07:52:39 GMT-3 +Message-Id: +From: Loop +Subject: account KIMOCHI locked by Loop +Newsgroups: local.control.news + +KIMOCHI disappeared for over 3 months. diff --git a/groups/local.control.news/6 b/groups/local.control.news/6 new file mode 100644 index 0000000..ffe4546 --- /dev/null +++ b/groups/local.control.news/6 @@ -0,0 +1,7 @@ +Date: 2024-12-05 07:52:39 GMT-3 +Message-Id: +From: Loop +Subject: account WILLIAMP locked by Loop +Newsgroups: local.control.news + +WILLIAMP disappeared for over 3 months. diff --git a/groups/local.control.news/7 b/groups/local.control.news/7 new file mode 100644 index 0000000..a1f976f --- /dev/null +++ b/groups/local.control.news/7 @@ -0,0 +1,7 @@ +Date: 2024-12-05 07:52:39 GMT-3 +Message-Id: +From: Loop +Subject: account JPMAB locked by Loop +Newsgroups: local.control.news + +JPMAB disappeared for over 3 months. diff --git a/groups/local.test/1 b/groups/local.test/1 new file mode 100644 index 0000000..3aaf4f9 --- /dev/null +++ b/groups/local.test/1 @@ -0,0 +1,7 @@ +Date: 2024-03-07 21:44:31 GMT-3 +Message-Id: +From: Loop +Subject: let there be light +Newsgroups: local.test + +A sample group. diff --git a/groups/local.test/1.~1~ b/groups/local.test/1.~1~ new file mode 100644 index 0000000..014728f --- /dev/null +++ b/groups/local.test/1.~1~ @@ -0,0 +1,7 @@ +Date: 2024-03-07 21:44:31 GMT-3 +Message-Id: +From: Loop +Subject: let there be light +Newsgroups: local.control.news + +A sample group. diff --git a/images/gnus-summary.png b/images/gnus-summary.png new file mode 100644 index 0000000000000000000000000000000000000000..a58f949da02695e1322565808b1973c7c084068b GIT binary patch literal 50181 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 v 1) "s" "")) + +(defun debug? () nil) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun fmt (cstr &rest args) + (apply #'format nil (list* cstr args)))) + +(defun stderr (&rest args) + (when (debug?) + (apply #'format (cons *error-output* args)))) + +(defun enumerate (ls &optional (first-index 0)) + (loop for e in ls and i from first-index + collect (cons i e))) + +(defun ucs-2->ascii (bs) + ;; I'm a Windows user. + #-win32 bs #+win32 (remove-if #'zerop bs)) + +(defun bad-input (r msg &key code) + (make-response :code (or code 400) :data msg :request r)) + +(defun integer->string (n) + (format nil "~a" n)) + +(defun mkstr (&rest args) ;; a utility + (with-output-to-string (s) + (dolist (a args) (princ a s)))) + +(defun data (&rest args) ;; a utility + (flatten (map 'list #'data->bytes args))) + +(defun crlf () + (vector 13 10)) + +(defun crlf-string () + (format nil "~c~c" #\return #\linefeed)) + +(defun flatten (obj) + (do* ((result (list obj)) + (node result)) + ((null node) (delete nil result)) + (cond ((consp (car node)) + (when (cdar node) (push (cdar node) (cdr node))) + (setf (car node) (caar node))) + (t (setf node (cdr node)))))) + +(defmacro mac (&rest body) + `(macroexpand-1 ,@body)) +(defmacro in-dir (dir &rest body) + `(let ((*default-pathname-defaults* (truename ,dir))) + (uiop:with-current-directory (,dir) + ,@body))) + +(defmacro in-groups (&rest body) `(in-dir "groups/" ,@body)) + +(defun in-group-lambda (g fn) (in-dir g (funcall fn))) + +(defmacro in-group (g &rest body) + `(in-group-lambda ,(fmt "groups/~a/" g) (lambda () ,@body))) + +(defmacro with-group (g r &rest body) + (let ((g-var (gensym)) + (r-var (gensym))) + `(let ((,g-var ,g) + (,r-var ,r)) + (if (not (group? ,g-var)) + (make-response :code 411 :request ,r-var + :data (format nil "no such group ``~a''" ,g-var)) + (progn ,@body))))) + +(defmacro with-n-args (n r &rest body) + (let ((args-var (gensym)) + (message-var (gensym)) + (n-var n)) + `(let ((,args-var (request-args r)) + (,message-var ,(fmt "bad arguments: needs exactly ~a" n-var))) + (if (not (= ,n-var (length ,args-var))) + (make-response :code 400 :request ,r :data ,message-var) + (progn ,@body))))) + +(defmacro with-group-set (&rest body) + (let ((g-var (gensym))) + `(let ((,g-var (client-group *client*))) + (if (not ,g-var) + (bad-input r "must say GROUP first") + ,@body)))) + +(defmacro with-auth (&rest body) + `(if (not (auth?)) + (make-response :code 400 :data "You must authenticate first.") + (progn ,@body))) + +(defstruct client group (article 1) (username "ANONYMOUS") (auth? 'no)) +(defparameter *client* (make-client)) +(defstruct command fn verb description) +(defparameter *commands-assoc* nil) + +(defun table-of-commands () + `(("GROUP" ,#'cmd-group "sets the current group") + ("NEXT" ,#'cmd-next "increments the article pointer") + ("HELP" ,#'cmd-help "displays this menu") + ("LIST" ,#'cmd-list "lists all groups") + ("AUTHINFO" ,#'cmd-authinfo "makes me trust you") + ("LOGIN" ,#'cmd-login "shorter interface to AUTHINFO") + ("HEAD" ,#'cmd-head "fetches article headers") + ("MODE" ,#'cmd-mode "handles the mode request from clients") + ("BODY" ,#'cmd-body "fetches an article body") + ("POST" ,#'cmd-post "posts your article") + ("ARTICLE" ,#'cmd-article "fetches full articles") + ("XOVER" ,#'cmd-xover "fetches the overview database of a group") + ("CREATE-GROUP" ,#'cmd-create-group + "creates a new group so you can discuss your favorite topic") + ("CREATE-ACCOUNT",#'cmd-create-account + "creates an account so you can invite a friend") + ("PASSWD" ,#'cmd-passwd "changes your password") + ("USERS" ,#'cmd-list-users "lists all users") + ("DD" ,#'cmd-dd "[d]isplays [d]ata: your state of affairs") + ("QUIT" ,#'cmd-quit "politely says good-bye") + ("DATE" ,#'cmd-date "displays the current date at this server") + ("UNLOCK-ACCOUNT" ,#'cmd-unlock-account "unlocks an account"))) + +(defun set-up-tables! () + (labels ((build-commands-assoc (ls) + (if (null ls) + nil + (cons (apply #'make-command-pair (car ls)) + (build-commands-assoc (cdr ls))))) + (make-command-pair (name fn desc) + (cons name (make-command :fn fn :verb name :description desc)))) + (setf *commands-assoc* + (sort + (build-commands-assoc (table-of-commands)) + #'string-lessp :key #'car)))) + +(defun get-command (key) + (let ((cmd (assoc key *commands-assoc* :test #'string=))) + (labels ((unrecognized-command () + (make-command :fn #'(lambda (r) + (make-response :code 400 + :data "unrecognized command" + :request r)) + :verb 'unrecognized + :description "a command for all commands typed wrong"))) + (or (cdr cmd) (unrecognized-command))))) +(defstruct request verb args said) +(defstruct response code data request multi-line) + +(defun empty-response () (make-response :code 400 :data "I beg your pardon?")) +(defun prepend-response-with (message r) + (make-response + :code (response-code r) + :data (data message (crlf) (response-data r)) + :multi-line (response-multi-line r) + :request (response-request r))) +(defun append-crlf-if-needed (seq) + (cond + ((stringp seq) + (append-crlf-if-needed (string->bytes seq))) + ((listp seq) + (append seq + (when (not (= (car (last seq)) 10)) + (list 13 10)))) + (t (error (format nil "append-crlf-if-needed: unsupported type: ~a" (type-of seq)))))) + +(defun send-response! (r) + (let ((bs (data (integer->string (response-code r)) " " + (append-crlf-if-needed (response-data r))))) + (my-write bs *standard-output*) + (stderr ">>> ~a" (bytes->string (ucs-2->ascii bs)))) + (when (response-multi-line r) + (let ((bs (data "." (crlf)))) + (my-write bs *standard-output*) + (stderr ">>> ~a" (bytes->string (ucs-2->ascii bs))))) + (force-output) + r) +(defun my-write (ls-of-bytes s) + (if (interactive-stream-p s) + (write-sequence (mapcar #'code-char ls-of-bytes) s) + (write-sequence ls-of-bytes s))) +(defun parse-request (r) + (let* ((collapsed-s (str:collapse-whitespaces (request-said r))) + (ls (str:split " " collapsed-s :omit-nulls 'please))) + ;; What are we going to do with a null request? + (cond ((null ls) (make-request :said (request-said r))) + (t (let ((verb (car ls)) + (args (cdr ls))) + (make-request :said (request-said r) + :verb (str:upcase verb) + :args args)))))) +(defun main-loop () + (let* ((bs (nntp-read-line)) + (ln (bytes->string (ucs-2->ascii bs)))) + (if ln + (let ((r (send-response! (dispatch-line ln)))) + (when (not (response-quit? r)) + (main-loop))) + (progn + (stderr "eof~%") + 'eof)))) + +(defun request-quit? (r) (and r (string= 'quit (request-verb r)))) +(defun response-quit? (r) (and r (request-quit? (response-request r)))) + +(defun main () + (send-banner!) + (set-up-tables!) + (read-accounts!) + (connect-index! "message-id.db") + (create-index!) + (main-loop)) + +(defun send-banner! () + (send-response! + (make-response :code 200 :data "Welcome! Say ``help'' for a menu."))) +(defun split-vector (delim v acc &key limit (so-far 1)) + (let ((len (length v))) + (split-vector-helper delim v len acc limit so-far 0))) + +(defun split-vector-helper (delim v len acc limit so-far start) + (if (zerop len) + acc + (let ((pos (search delim v :start2 start :end2 len))) + (cond ((or (not pos) (and limit (= so-far limit))) + (nreverse (cons (subseq v start len) acc))) + (t (split-vector-helper + delim + v + len + (cons (subseq v start (or pos len)) acc) + limit + (1+ so-far) + (+ pos (length delim)))))))) +(defstruct article headers body) + +(defun parse-article (v) + (let ((parts (split-vector (vector 13 10 13 10) v nil :limit 2))) + (make-article :headers (map 'string #'code-char (car parts)) :body (cadr parts)))) + +(defun hs-space-collapsed (hs) + (cl-ppcre:regex-replace-all (format nil "~a[ ~a]+" (crlf-string) #\tab) hs " ")) + +(defun hs-lines (lines) (str:split (crlf-string) lines)) + +(defun parse-header (header) + (let* ((h (str:collapse-whitespaces header)) + (pos (search ":" h))) + (when (null pos) + (throw 'article-syntax-error + (values nil (format nil "missing colon in header |~a|" h)))) + (when (<= (length h) (+ 2 pos)) + (throw 'article-syntax-error + (values nil (format nil "empty header ~a" h)))) + (multiple-value-bind (key val) + (values (subseq h 0 pos) (subseq h (+ 2 pos))) + (cons (str:downcase key) val)))) + +(defun parse-headers (hs) + (let ((ls (hs-lines (hs-space-collapsed hs)))) + (mapcar #'(lambda (h) (parse-header h)) ls))) + +(defun string-integer? (s) (ignore-errors (parse-integer s))) +(defun get-header-from-article (h a) + (get-header h (parse-headers (article-headers (parse-article a))))) + +(defun get-header (key hs) + (let ((pair (assoc key hs :test #'string=))) + (if pair (cdr pair) ""))) + +(defun fetch-headers (g i) + (let* ((a-string (fetch-article g i)) + (a-parsed (parse-article a-string)) + (headers (parse-headers (article-headers a-parsed)))) + (enrich-headers headers a-string))) + +(defun enrich-headers (hs a) + (append hs + `(("line-count" . ,(format nil "~a" (nlines (article-body (parse-article a))))) + ("byte-count" . ,(format nil "~a" (length a)))))) + +(defun nlines (v) (length (split-vector (crlf) v nil))) +(defun fetch-article (g i) + (in-groups + (read-file-raw (format nil "~a/~a" g i)))) + +(defun read-file-raw (path) + (let* ((size (sb-posix:stat-size (sb-posix:stat path))) + (a (make-array size))) + (with-open-file (in path :element-type '(unsigned-byte 8)) + (read-sequence a in) + a))) + +(defun fetch-body (g i) + (article-body (parse-article (fetch-article g i)))) +(defun encode-body (a) a) +(defun extract-mid (a) + (lookup "message-id" (parse-headers (article-headers (parse-article a))))) +(defun lookup (key table) + (cdr (assoc key table :test #'string=))) +(defun dispatch (r) + (let* ((verb (request-verb r))) + (if (null verb) + (empty-response) + (funcall (command-fn (get-command verb)) r)))) + +(defun dispatch-line (ln) + (dispatch (parse-request (make-request :said ln)))) +(defun cmd-authinfo (r) + (let* ((args (mapcar #'str:upcase (request-args r)))) + (cond + ((not (= (length args) 2)) + (bad-input r "No, no: I take exactly two arguments.")) + (t + (multiple-value-bind (cmd arg) (apply #'values args) + (cond + ((string= cmd "USER") + (setf (client-username *client*) arg) + (make-response :code 381 :request r + :data (format nil "Hey, ~a, please tell us your password." arg))) + ((string= cmd "PASS") + (if (authinfo-check (client-username *client*) arg) + (progn + (log-user-in!) + (make-response + :code 281 :request r + :data (fmt "Welcome, ~a." (client-username *client*)))) + (make-response :code 400 :request r :data "Sorry. Wrong password."))) + (t (make-response :code 400 :request r :data "Syntax error. Say ``authinfo USER /your-name/'' then ``authinfo PASS /your-pass/''.")))))))) + +(defun authinfo-check (username passwd) + (pass? username passwd)) + +(defun auth? () + (eq 'yes (client-auth? *client*))) + +(defun log-user-in! () + (setf (client-auth? *client*) 'yes) + (let ((u (get-account (client-username *client*)))) + (setf (account-seen u) (get-universal-time))) + (write-accounts!)) +(defun cmd-mode (r) ;; Whatever. + (make-response :code 200 :request r :data "Sure thing.")) +(defun typical-cmd-head-body-article (r fn-name) + (with-auth + (with-group-set + (let ((args (request-args r))) + (cond ((null args) + (funcall fn-name r (client-group *client*) (client-article *client*))) + ((= 1 (length args)) + (let* ((n-or-mid (car args))) + (cond ((string-integer? n-or-mid) + (funcall fn-name r (client-group *client*) n-or-mid)) + (t (multiple-value-bind (group n-str) (lookup-index n-or-mid) + (if (and group n-str) + (funcall fn-name r group n-str) + (bad-input r (format nil "Unknown article ~a." n-or-mid)))))))) + (t (bad-input r "No, no: it takes at most two arguments."))))))) + +(defun cmd-head (r) + (typical-cmd-head-body-article r #'head-response)) +(defun cmd-body (r) + (typical-cmd-head-body-article r #'body-response)) +(defun cmd-article (r) + (typical-cmd-head-body-article r #'article-response)) + +(defun article-response (r g i) + (typical-cmd-response 220 r g i #'(lambda (a) (encode-body a)))) +(defun head-response (r g i) + (typical-cmd-response 221 r g i #'(lambda (a) (article-headers (parse-article a))))) +(defun body-response (r g i) + (typical-cmd-response 222 r g i #'(lambda (a) (encode-body (article-body (parse-article a)))))) +(defun typical-cmd-response (code r g i get-data) + (let ((a (handler-case (fetch-article g i) + (sb-posix:syscall-error (c) + (make-response :code 400 :request r + :data (format nil "article ~a/~a: ~a" g i c))) + (sb-ext:file-does-not-exist (c) + (declare (ignore c)) + (make-response :code 400 :request r + :data (format nil "article ~a/~a does not exist" g i)))))) + (cond ((typep a 'response) a) + (t (prepend-response-with + (format nil "~a ~a" i (extract-mid a)) + (make-response :multi-line 'yes :code code + :request r :data (funcall get-data a))))))) +(defun cmd-next (r) + (with-auth + (let ((g (client-group *client*)) + (n-cur (client-article *client*))) + (cond + ((not g) (bad-input :code 412 r "must say GROUP first")) + (t (multiple-value-bind (low high len) (group-high-low g) + (declare (ignore low len)) + (cond ((= n-cur high) (bad-input r "you are at the last article already")) + (t (article-next! r g))))))))) + +(defun article-next! (r g) + (setf (client-article *client*) (1+ (client-article *client*))) + (let ((cur (client-article *client*))) + (make-response :code 223 + :request r + :data (format nil "~a ~a" cur (mid-by-name g cur))))) + +(defun mid-by-name (g name) + (extract-mid (fetch-article g name))) +(defun cmd-xover (r) + (with-auth + (with-group-set + (let ((args (request-args r))) + (cond ((null args) + (xover r (client-article *client*) (client-article *client*))) + ((= 1 (length args)) + (multiple-value-bind (s v) + (cl-ppcre:scan-to-strings "([0-9]+)([-]?)([0-9]*)" (car args)) + (cond + ((not s) (make-response :code 502 :request r :data "bad syntax")) + (t (let ((fr (parse-integer (aref v 0))) + (hifen (aref v 1)) + (to (ignore-errors (parse-integer (aref v 2))))) + (when (not (string= hifen "-")) + (setq to fr)) + (xover r fr to)))))) + (t (make-response :code 502 :request r :data "bad syntax"))))))) + +(defun xover (r from to) + (assert (client-group *client*)) + (let* ((g (client-group *client*)) + (ls (get-articles g from to))) + (cond ((= 0 (length ls)) + (make-response :code 420 :request r :data "no articles in the range")) + (t + (prepend-response-with + "Okay, your overview follows..." + (make-response + :code 224 :request r :multi-line 'yes + :data (str:join + (crlf-string) + (loop for i in ls + collect (xover-format-line + i + (remove-if-not + #'(lambda (h) + (member (car h) (xover-headers) + :test #'string=)) + (fetch-headers g i))))))))))) +(defun xover-format-line (i hs) + (str:concat (format nil "~a~a" i #\tab) + (str:join #\tab + (mapcar #'(lambda (h) (get-header h hs)) + (xover-headers))))) +(defun xover-headers () + '("subject" "from" "date" "message-id" "references" "line-count" "byte-count")) +(defun cmd-group (r) + (with-auth + (with-n-args 1 r + (let ((g (car (request-args r)))) + (with-group g r + (set-group! g) + (multiple-value-bind (low high len) (group-high-low g) + (let ((ln (format nil "~a ~a ~a ~a" len low high g))) + (setf (client-article *client*) low) + (make-response :code 211 :request r :data ln)))))))) + +(defun group? (g) + (in-groups + (cl-fad:directory-exists-p g))) + +(defun xgroup? (g) + (cl-fad:directory-exists-p g)) + +(defun set-group! (g) + (setf (client-group *client*) g)) +(defstruct group name high low) + +(defun cmd-list (r) + (prepend-response-with + "Get in the loop! Lots to choose from." + (make-response :code 215 :multi-line 'yes + :data (str:join (crlf-string) (build-groups-lines (build-groups-structs))) + :request r))) + +(defun build-groups-lines (ls) + (reverse + (mapcar + #'(lambda (g) + (format nil "~a ~a ~a y" (group-name g) (group-high g) (group-low g))) + ls))) + +(defun build-groups-structs () + (let ((ret-ls nil)) + (dolist (g (list-groups) ret-ls) + (multiple-value-bind (low high len) (group-high-low g) + (declare (ignore len)) + (setf ret-ls (cons (make-group :name g :high high :low low) ret-ls)))))) + +(defun between? (x from to) + (<= from x to)) +(declaim (inline between?)) + +(defun filesize (path) + (sb-posix:stat-size + (sb-posix:stat path))) + +(defun zero-file? (path) + (= (filesize path) 0)) + +(defun temporary-article? (path) + (or (zero-file? path) + (cl-ppcre:scan "\.tmp$" (namestring path)))) + +(defun article-ready? (path) + (not (temporary-article? path))) + +(defun get-articles (g &optional from to) + (in-groups ;; We might want to optimize this some day. Most likely, + ;; though, we'll not be using directories. That's a + ;; problem to be studied. + (let ((as (articles->integers + (remove-if #'temporary-article? (cl-fad:list-directory g))))) + (sort (remove-if-not + #'(lambda (x) (between? x (or from x) (or to x))) + as) + #'<)))) + +(defun group-high-low (g) + (let* ((articles (get-articles g)) + (sorted-ints (sort articles #'<))) + (values (or (car sorted-ints) 0) + (or (car (last sorted-ints)) 0) + (length sorted-ints)))) + +(defun articles->integers (ls) + (remove-if #'null + (mapcar #'(lambda (g) + (ignore-errors + (parse-integer (basename (uiop:unix-namestring g))))) + ls))) + +(defun list-groups () + (let ((groups (in-groups (cl-fad:list-directory ".")))) + (sort (mapcar #'(lambda (g) (basename (uiop:unix-namestring g))) groups) + #'string-lessp))) + +(defun last-char (s) (char s (1- (length s)))) +(defun basename (path) + (let ((s (str:collapse-whitespaces path))) + (if (char= #\/ (last-char s)) + (car (last (pathname-directory s))) + (file-namestring s)))) +(defun cmd-help (r) + (let ((lines (menu *commands-assoc*))) + (prepend-response-with + "What's on the menu today?" + (make-response :code 200 :multi-line 'yes + :request r + :data (str:join (crlf-string) lines))))) +(defun menu (ls) + (if (null ls) + nil + (cons (display-fn (car ls)) (menu (cdr ls))))) + +(defun display-fn (cmd-pair) + (let ((cmd (cdr cmd-pair))) + (format nil "~A ~A" + (command-verb cmd) + (command-description cmd)))) +(defun cmd-quit (r) + (make-response :code 205 :data "Good-bye." :request r)) +(defun cmd-date (r) + (make-response :code 201 + :request r + :data + (format-timestring nil (now)))) +(defun conforms? (bs) + (catch 'article-syntax-error ;; parse-headers might throw + (let ((headers (parse-headers (article-headers (parse-article bs))))) + (let ((result (dolist (h (headers-required-from-clients)) + (when (not (lookup h headers)) + (return (format nil "missing the /~a/ header" h))))) + (content-type (get-header "content-type" headers))) + (cond + ((stringp result) (values nil result)) + ((not (text/plain? content-type)) + (values nil (format nil "content-type must be plain/text, but it's ~a" content-type))) + (t (values t nil))))))) + +(defun text/plain? (header-s) + ;; I say T when S begins with "text/plain" or when S is "". + (let* ((s (str:collapse-whitespaces header-s)) + (needle "text/plain") + (len (min (length needle) (length s)))) + (or (zerop len) + (and (<= (length needle) (length s)) + (string= needle s :end1 len :end2 len))))) + +(defun headers-required-from-clients () + '("from" "newsgroups" "subject")) +(defun suggest-message-id (&optional (n 20)) + (format nil "<~a@loop>" (random-string n))) + +(defun random-string (size) + (let* ((universe "abcdefghijklmnopqrstuvwxyz") + (len (length universe)) + (state (make-random-state t)) + mid) + (dotimes (c size) + (setq mid (cons (char universe (random len state)) mid))) + (coerce mid 'string))) +(defun unparse-article (parsed) + (data + (let ((ls)) + (dolist (h (parse-headers (article-headers parsed))) + (setq ls (cons (data (str:capitalize (car h)) ": " (cdr h) (crlf)) ls))) + (nreverse ls)) + (crlf) + (article-body parsed))) +(defun ensure-header (h fn bs) + (let* ((headers (parse-headers (article-headers (parse-article bs))))) + (if (lookup h headers) + bs + (unparse-article + (make-article + :headers + (str:join (crlf-string) + (mapcar (lambda (h) + (format nil "~a: ~a" (car h) (cdr h))) + (cons (cons h (funcall fn)) headers))) + :body (article-body (parse-article bs))))))) + +(defun get-date () + (multiple-value-bind (s m h day mon year dow dst-p tz) + (get-decoded-time) + (declare (ignore dow dst-p)) + (format nil "~4,'0d-~2,'0d-~2,'0d ~2,'0d:~2,'0d:~2,'0d GMT~a" + year mon day h m s (- tz)))) + +(defun ensure-mid (bs) + (ensure-header "message-id" #'suggest-message-id bs)) +(defun ensure-date (bs) + (ensure-header "date" #'get-date bs)) +(defun newsgroups-header->list (s) + (mapcar (lambda (n) (str:trim (string-downcase n))) (str:split "," s))) + +(defun cmd-post (r) + (with-auth + (send-response! + (make-response :code 340 + :data (format nil "Okay, go ahead. Suggested message-id ~a." + (suggest-message-id)))) + (let* ((bs (nntp-read-article))) + (multiple-value-bind (okay? error) (conforms? bs) + (if (not okay?) + (make-response :code 400 :request r + :data (format nil "Sorry. Your article doesn't conform: ~a." error)) + (multiple-value-bind (code reply) (post bs) + (make-response :code code :request r :data reply))))))) + +(defun post (bs) + (let ((ngs (newsgroups-header->list + (get-header "newsgroups" (parse-headers + (article-headers + (parse-article bs)))))) + ngs-dont-exist) + (dolist (ng ngs) + (if (and (group-name-conforms? ng) + (group? ng)) + (progn + (let ((a (ensure-date (ensure-mid bs)))) + (save-article-insist ng (get-next-article-id ng) a (extract-mid a)) + (update-last-post-date! (client-username *client*)))) + (push ng ngs-dont-exist))) + (if (zerop (- (length ngs) (length ngs-dont-exist))) + (values 400 "Sorry. There was not a single valid newsgroup specified.") + (values 240 (data "Thank you! Your article has been saved." + (when ngs-dont-exist + (data " However, the groups " + (str:join ", " (sort ngs-dont-exist #'string<)) + " just don't exist."))))))) +(defun update-last-post-date! (username) + (let ((u (get-account username))) + (setf (account-last-post u) (get-universal-time)))) +(defun rename-no-extension (old new) + (rename-file old (make-pathname :name new :type :unspecific))) + +(defun save-article-try (name-try bs) + (let ((name (format nil "~a" name-try)) + (tmp (format nil "~a.tmp" name-try))) + (with-open-file + (s name + :direction :output + :if-exists :error ;; an atomic operation + :if-does-not-exist :create)) + ;(format t "save-article-try: ~a~%" name) + (with-open-file + (s tmp + :direction :output + :if-exists :error + :if-does-not-exist :create + :element-type '(unsigned-byte 8)) + (write-sequence bs s)) + (rename-no-extension tmp name))) +(defun save-article-insist (g name a message-id) + (loop for name from name do + (in-dir (format nil "groups/~a/" g) + (handler-case + (save-article-try name a) + (sb-ext:file-exists () + ;; We might want to log the fact. + ;(format t "name ~a already exists...~%" name) + ) + (:no-error (new before after) ;; the return values from return-file + (declare (ignore new before after)) + (return (values name (insert-index message-id g (fmt "~a" name))))))))) + +(defun get-next-article-name (g) + (format nil "~a" (get-next-article-id g))) + +(defun get-next-article-id (g) + (multiple-value-bind (low high len) (group-high-low g) + (declare (ignore low len)) + (1+ high))) +(defun nntp-read-article (&optional acc) + ;; Returns List-of Byte. + (let* ((ls (ucs-2->ascii (nntp-read-line)))) + (cond ;; 46 == (byte #\.) + ((equal (list 46) ls) (flatten (add-crlf-between acc))) + (t (nntp-read-article (append acc (list ls))))))) +(defun nntp-read-line (&optional (s *standard-input*) acc) + ;; Returns List-of Byte. + (let ((x (read-byte s))) + (cond ((or (null x) (= x 10)) + (let ((bs (and acc (nreverse (if (= (car acc) 13) (cdr acc) acc))))) + (stderr "<<< ~a~%" (bytes->string (ucs-2->ascii bs))) + bs)) + (t (nntp-read-line s (cons x acc)))))) + +(defun list->bytes (ls) + (mapcar #'data->bytes ls)) + +(defun vector->bytes (v) + (mapcar #'data->bytes (coerce v 'list))) + +(defun data->bytes (d) + (cond ((null d) nil) + ((integerp d) (list d)) + ((stringp d) (string->bytes d)) + ((consp d) (list->bytes d)) + ((vectorp d) (vector->bytes d)) + (t (error (format nil "type ~a is not supported" (type-of d)))))) + +(defun add-crlf-between (ls-of-ls) + ;; Add \r\n to each ``line''. Returns List-of Byte. + (mapcar (lambda (ls) (append ls (list 13 10))) ls-of-ls)) + +(defun string->bytes (s) + (map 'list #'char-code s)) + +(defun bytes->string (ls) + (map 'string #'code-char ls)) +(defun cmd-create-group (r) + (with-n-args 1 r + (let ((g (string-downcase (car (request-args r))))) + (multiple-value-bind (okay? reason) + (group-name-conforms? g) + (if (not okay?) + (make-response :code 580 :request r + :data (format nil "group name does not conform: ~a" reason)) + (progn + (multiple-value-bind (path created?) + (in-groups (ensure-directories-exist (concatenate 'string g "/"))) + (declare (ignore created?)) + (if (not path) + (make-response :code 581 :request r + :data (format nil "could not create group ~a" + (if (group? g) + "because it already exists" + "but we don't know why---sorry!"))) + (progn + (notify-group-created g) + (make-response :code 280 :request r + :data (format nil "group ~a created" g))))))))))) + +(defun group-name-conforms? (g) + (let ((okay? (cl-ppcre:scan-to-strings "^([a-z0-9]+)" g))) + (if okay? + (values t nil) + (values nil "must match ^([a-z0-9]+)")))) +(defun cmd-create-account (r) + (with-auth + (with-n-args 1 r + (let* ((args (mapcar #'str:upcase (request-args r))) + (username (car args))) + (multiple-value-bind (username pass-or-error) (new-account! username) + (if (not username) + (make-response :code 400 :request r + :data (fmt "~a. Choose a new name." pass-or-error)) + (progn + (notify-user-created username) + (make-response :code 200 :request r + :data (fmt "Okay, account ~a created with password ``~a''." + username pass-or-error))))))))) + +(defparameter *accounts* nil) +(defstruct account username seen last-post friends pass pass-locked pass-locked-why creation) + +(defun read-accounts! () + (let ((*package* (find-package '#:loop))) + (with-open-file + (s "accounts.lisp" + :direction :input) + (setq *accounts* (read s)))) + *accounts*) + +(defun new-account! (username) + (let* ((u (str:upcase username)) + (p (random-string 6)) + (a (make-account :username u + :pass (sxhash (str:upcase p)) + :creation (get-universal-time)))) + (if (get-account u) + (values nil (fmt "account ~a already exists" u)) + (let ((c (get-account (client-username *client*)))) + (push u (account-friends c)) + (push a *accounts*) + (write-accounts!) + (values (str:upcase username) p))))) +(defun write-accounts! () + (let ((name + (loop + (let* ((tmp (random-string 10)) + (name (format nil "~a.tmp" tmp))) + (when + (ignore-errors + (with-open-file + (s name + :direction :output + :if-exists :error + :if-does-not-exist :create) + (write *accounts* :stream s))) + (return name)))))) + (if (ignore-errors (rename-file name "accounts.lisp")) + (values t *accounts*) + (values nil (format nil "could not rename ~a to accounts.lisp" name))))) + +(defun get-account (username) + (loop for u in *accounts* + do (when (string= (str:upcase username) (account-username u)) + (return u)))) +(defun cmd-unlock-account (r) + (with-auth + (with-n-args 1 r + (let* ((args (mapcar #'str:upcase (request-args r))) + (username (car args))) + (cond ((not (get-account username)) + (make-response :code 400 :request r + :data "No such account ~a." username)) + ((not (locked? username)) + (make-response :code 400 :request r + :data (fmt "Can't unlock ~a because it's not locked." username))) + (t + (unlock-account! username) + (notify-user-unlocked username) + (make-response :code 200 :request r + :data (fmt "Okay, account ~a unlocked." username)))))))) + +(defun unlock-account! (username) + (let ((u (get-account username))) + (cond ((not u) + (values nil "no such account")) + ((not (locked? username)) + (values nil "account isn't locked")) + (t + (setf (account-pass u) (account-pass-locked u)) + (setf (account-pass-locked u) nil) + (setf (account-pass-locked-why u) nil))))) +(defun cmd-login (r) + (let* ((args (mapcar #'str:upcase (request-args r)))) + (cond + ((not (= (length args) 2)) + (bad-input r "Usage: login your-username your-password")) + (t + (multiple-value-bind (name pass) (apply #'values args) + (cond + ((pass? name pass) + (log-user-in-as! name) + (make-response :code 200 :request r + :data (fmt "Welcome, ~a." name))) + (t (make-response :code 400 :request r + :data (fmt "Wrong password."))))))))) + +(defun log-user-in-as! (name) + (setf (client-username *client*) name) + (log-user-in!)) +(defun cmd-passwd (r) + (with-auth + (let* ((args (mapcar #'str:upcase (request-args r)))) + (cond + ((not (= (length args) 2)) + (bad-input r "Usage: passwd current-password new-password")) + (t + (multiple-value-bind (cur new) (apply #'values args) + (cond + ((pass? (client-username *client*) cur) + (multiple-value-bind (okay? problem) (change-passwd! (client-username *client*) new) + (if okay? + (make-response :code 200 :request r + :data "You got it. Password changed.") + (make-response :code 500 :request r + :data (fmt "Sorry: ~a" problem))))) + (t (make-response :code 400 :request r + :data (fmt "Sorry. Wrong password.")))))))))) + +(defun pass? (username pass) + (let ((u (get-account username))) + (and u + (eq (sxhash pass) (account-pass u))))) + +(defun change-passwd! (username newpass) + (let ((u (get-account username))) + (when (not u) + (error "I could not find account ~a." username)) + (setf (account-pass u) (sxhash newpass)) + (write-accounts!))) + +(defun notify-group-created (g) + (post-notification + :subject (fmt "new group ~a by ~a" g (client-username *client*)) + :body (fmt "Blame ~a for the group ~a just created." (client-username *client*) g))) + +(defun notify-user-created (u) + (post-notification + :subject (fmt "new account ~a by ~a" u (client-username *client*)) + :body (fmt "Blame ~a for inviting ~a." (client-username *client*) u))) + +(defun notify-user-unlocked (u) + (let ((guilty (client-username *client*))) + (post-notification + :subject (fmt "account ~a unlocked by ~a" u guilty) + :body (fmt "Blame ~a for unlocking ~a." guilty u)))) + +(defun post-notification (&key subject body) + (in-groups (ensure-directories-exist "local.control.news/")) + (when (group? "local.control.news") + (let ((a (make-news :subject subject :body body))) + (post (concatenate 'vector (article-headers a) (crlf) (article-body a) (crlf)))))) + +(defun make-news (&key subject body) + (make-article + :headers (data + (add-crlf-between + (mapcar + (lambda (p) (data (format nil "~a: ~a" (car p) (cdr p)))) + `(("from" . "Loop") + ("subject" . ,subject) + ("newsgroups" . "local.control.news"))))) + :body (data body))) +(defun cmd-list-users (r) + (with-auth + (prepend-response-with + "List of current users:" + (make-response + :code 200 :request r :multi-line 'yes + :data (str:join (crlf-string) (list-users)))))) + +(defun size-of-longest-username () + (loop for u in *accounts* + maximizing (length (account-username u)))) + +(defun list-users () + (read-accounts!) + (mapcar (lambda (row) (cadr row)) + (sort + (loop for u in *accounts* + collect (list (account-username u) + (fmt "~v@a~a, ~a, invited ~a" + (size-of-longest-username) + (account-username u) + (if (locked? (account-username u)) + (fmt " (account locked: ~a)" + (account-pass-locked-why u)) + "") + (if (last-time-seen (account-username u)) + (fmt "last seen on ~a" (last-time-seen (account-username u))) + "never logged in") + + (or (account-friends u) "nobody")))) + #'string<= :key (lambda (row) (car row))))) + +(defun universal-to-human (s) + (format-timestring + nil + (universal-to-timestamp s) + :format +asctime-format+)) + +(defun last-time-seen (username) + (let ((u (get-account username))) + (if u (let ((s (account-seen u))) + (if s (universal-to-human s)))))) +(defun cmd-dd (r) + (make-response :code 200 :data (format nil "state: ~a" *client*) :request r)) +(setq lisp-unit:*print-failures* t) +(define-test first-test-of-the-west + (assert-equal 0 0)) + +(define-test requests + (let ((nil-request-1 (make-request)) + (nil-request-2 (make-request :said " "))) + (assert-true (request=? nil-request-1 (parse-request nil-request-1))) + (assert-true (request=? nil-request-2 (parse-request nil-request-2))) + (assert-true (request=? nil-request-1 nil-request-2)))) + +(define-test commands + (let ((ht (make-hash-table)) + (c1 (make-command :fn #'cons :verb 'c1 :description "cons cmd")) + (c2 (make-command :fn #'list :verb 'c2 :description "list cmd"))))) + +(define-test dispatching + (assert-true (equalp (empty-response) (dispatch (empty-request))))) +(defun index-from-fs! () + (loop for path in (in-groups (directory "**/*")) + do (let* ((g (str:trim (first (last (pathname-directory path))))) + (i (str:trim (pathname-name path))) + (m (str:trim (extract-mid (fetch-article g i))))) + (when (> (length m) 0) + (format t "article ~a/~a indexed by ~a~%" g i m) + (insert-index m g i))))) + +(defun remake-index-from-fs () + (remake-index!) + (index-from-fs!)) diff --git a/loop.nw b/loop.nw new file mode 100644 index 0000000..00f8d2a --- /dev/null +++ b/loop.nw @@ -0,0 +1,2565 @@ +% -*- mode: noweb; noweb-default-code-mode: lisp-mode; -*- +\documentclass[a4paper,11pt]{article} +\usepackage[text={6.75in,10in},centering]{geometry} + +\usepackage{graphicx} + +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage{csquotes} +\usepackage[brazil]{babel} + +\usepackage{etoolbox} +\AtBeginEnvironment{quote}{\small} +\AtBeginEnvironment{verbatim}{\small} + +%% \usepackage[backend=biber]{biblatex} +%% \addbibresource{refs.bib} +%% \renewcommand{\cite}{\parencite} +\usepackage[hyperref]{xcolor} +\usepackage[colorlinks=true,citecolor=]{hyperref} % linkcolor=red + +\usepackage{amsmath,amsthm,amssymb} +\allowdisplaybreaks +\usepackage{lmodern} +\usepackage{noweb} +\noweboptions{brazil,longchunks,smallcode} +\DeclareMathOperator{\mdc}{mdc} +\DeclareMathOperator{\gcdext}{gcdext} +\DeclareMathOperator{\remainder}{remainder} +\DeclareMathOperator{\quotient}{quotient} +\DeclareMathOperator{\diff}{diff} + +\def\nwendcode{\endtrivlist \endgroup} +\let\nwdocspar=\par + +%% Popular words. +\newcommand{\lxxp}{{\em loop}} +\newcommand{\Lp}{{\tt LOOP}} +\newcommand{\lp}{\Lp} +\newcommand{\bug}{{\em bug}} +\newcommand{\symlink}{{\em symbolic link}} +\newcommand{\symlinks}{\symlink s} + +\title{\Lp\\ + {a circle out of fashion}} +\date{January 2024} +\begin{document} +\fontfamily{cmr}\selectfont +\maketitle +%\setlength{\parskip}{3pt} +%\setlength{\parindent}{0pt} + +\Lp\ is an out-of-fashion program, used as medium of communication by +antiquated people. \Lp\ members insist that technical communication +be made in writing and not in a hurry. That's how backwards they are. +To give you an idea, they write \Lp\ in Lisp---jurassic technology. +We surely wouldn't pay them any attention. +% +\begin{verbatim} + Drunk and dressed in their best brown baggies and their platform soles + They don't give a damn about any trumpet-playing band + It ain't what they call rock and roll + -- Mark Knopfler, 1978. +\end{verbatim} + +It's easy to make a conference on the Internet. E-mail works. When +we write an e-mail to various friends to discuss a certain subject, we +form a circle. When we decide to add another person to this +quickly-made conference by e-mail, sometimes we use a well-known +idiom---``adding John to the loop''. We add John's e-mail address to +the list of destinaries. So long as everyone replies to everyone, +John, too, will start getting all the messages. If anyone violates +this rule of replying to everyone involved, the loop is broken. + +There are surely inconveniences in using e-mail as conference medium. +For example, after John has been added to the loop, he is not able to +leave by his own account. He needs to ask everyone involved to stop +writing to him. This is usually easy to do, but instead people tend +to ask for more technology such as mailing lists. A mailing list is +nothing by an automated version of this idea of writing to various +people at once. When the mailing address is written to, a program +resends the message to all subscribers of the mailing list and, this +way, the conference takes place. + +But \lp\ has nothing to do with e-mail. \Lp\ uses a communication +strategy---called a ``protocol''---that is even older than the web +itself. The web started out around 1989--1990 and the protocol +\lp\ uses was conceived in 1979 and implemented in 1980. The name of +the protocol used by \lp\ is NNTP---Network News Transfer Protocol. +Since e-mail was already daily practice of members of the Internet +back then, many things from e-mail were taken by the NNTP designers. +So, an NNTP message looks a lot like an e-mail message and the +two---NNTP and SMTP (the protocol used by e-mail)---can often mingle +seamlessly. The impression we get from using NNTP is that we're +sending e-mail to a certain group of people. It's as though the +message goes into a collective mailbox and anyone interested in that +mailbox reads the messages there. If anyone would like to reply to a +message, they do so and, this way, communication flows among the +interested crowd. If anyone would like to leave the group, nothing is +needed---the person just doesn't go back to read any more messages. +Unlike mailing lists, there is no need to formally commit to reading +one of these collective mailboxes and no need to formally notify +anyone or any system that you're not interested in that group any +longer. These collective mailboxes are called ``news groups'' and are +often written as ``newsgroups''. And the messages posted to these +news groups are called ``articles''. + +Just like e-mail and the web, network news is an open protocol. +Anyone could write a program capable of speaking NNTP. There are many +NNTP-aware programs. You could write your own. Figures +\ref{fg:gnus}--\ref{fg:sylpheed} show a few programs for reading +network news via NNTP. + +\begin{figure}[!htb] + \centering \includegraphics[width=0.8\linewidth]{images/gnus-summary.png} + \caption{Gnus, a news reader embedded in the GNU EMACS text editor.} + \label{fg:gnus} +\end{figure} + +\begin{figure}[!htb] + \centering \includegraphics[width=0.8\linewidth]{images/tbird-summary.png} + \caption{Thunderbird, a news reader produced by the Mozilla Foundation.} + \label{fg:bird} +\end{figure} + +\begin{figure}[!htb] + \centering \includegraphics[width=0.8\linewidth]{images/sylpheed-summary.png} + \caption{Sylpheed, a news reader produced by Hiroyuki Yamamoto.} + \label{fg:sylpheed} +\end{figure} + +{\bf Principles for a discussion group}. We believe a discussion group +should be small and grow slowly. By ``slowly'', we mean that each +member comes in through an invitation. This way, the group being +closed by definition, we keep spam out and give members a certain +sense of privilege. + +A discussion group should be formed by interested people. If a +participant doesn't log-in for a certain period of time, \lp locks the +participant's account---see Section \ref{sec:inactive-users}. The +account can be reactivated, but it will take asking another +participant (with an active account) to do so. In other words, +there's an encouragement for an uninterested member not to come back +to the \lp. The idea is to keep a certain cohesion in the discussion +groups. When an account is locked or unlocked, an article is posted +to the group {\tt local.control.news}, so everyone knows who is +leaving and arriving. This way, participants get to have an idea of +who is reading them. + +Each invitation comes with a certain responsibility: it's possible to +see who invited who. If {\tt BOB} misbehaves, everyone gets to see +that {\tt ALICE} doesn't have nice friends. The {\tt USERS} command +shows the relationship graph: +% +\begin{verbatim} +USERS +200 List of current users: +ANONYMOUS, last seen on Fri Mar 8 19:01:56 2024, invited (ALICE) + ALICE, last seen on Sun Mar 10 21:25:45 2024, invited (BOB CARLA) + BOB, last seen on Sun Mar 10 21:17:30 2024, invited nobody + CARLA, last seen on Sun Mar 10 18:30:48 2024, invited nobody +\end{verbatim} + +We conjecture that a discussion group tends to prosper when each +member feels as important as any other member. So we think each +member should have as much managerial power as any other. In an +attempt to realize this ideal, each member is able to not only invite +other people---see the [[CREATE-ACCOUNT]] command---, but also to +create new groups---see the [[CREATE-GROUP]] command. + +Despite this introduction, this is not a tutorial on the history of +the Internet or how NNTP works. This is the source code of \lp. +Hereafter, our conversation continues in Lisp. Understanding how +\lp\ is made is only necessary if you intend to modify it. If you +just want to use the system, you probably should stop right here. + +\section{The implementation strategy}\label{sec:design} + +Anything a user sends to the \lp\ is wrapped in a [[request]] and +any command processing must produce a [[response]]: +% +\begin{verbatim} + request ---> process ---> response. +\end{verbatim} +% +The request arrives, \lp\ interprets it, finds out which command +should look at the request and then dispatches the the request to the +right command. The command process the request and returns a +response. Then \lp\ takes this response and sends it out back to the +user through the network using [[send-response!]]. That's all there +is to \lp. Everything else is just the details of this strategy. + +Before you investigate the source code of \lp, you should have {\tt + SBCL} installed---the Common Lisp compiler we've been using---and +the package [[:loop]]. This way you can interact with the system: +% +\begin{verbatim} +CL-USER> (ql:quickload :loop) +To load "loop": + Load 1 ASDF system: + loop +; Loading "loop" +.................. +(:LOOP) +CL-USER> (in-package :loop) +# +LOOP> (make-request "HELP") +#S(REQUEST :VERB "HELP" :ARGS NIL :SAID NIL) +\end{verbatim} +% +The procedure [[make-request]] constructs a [[request]]. When a user +connects to the \lp, each command sent by the user is packaged inside +a [[request]] structure like this one above. To see how \lp\ answers +this request for help, we can invoke the procedure [[dispatch]], which +is responsible to find the correct procedure to reply to the request. + +\begin{verbatim} +LOOP> (dispatch (make-request :verb "HELP")) +#S(RESPONSE + :CODE 400 + :DATA "unrecognized command" + :REQUEST #S(REQUEST :VERB "HELP" :ARGS NIL :SAID NIL) + :MULTI-LINE NIL) +\end{verbatim} + +It turns that \lp\ doesn't recognize the command. That's because we +only loaded the [[:loop]] package. The reason it doesn't recognize +the command is that the command table has not been built. Let's build +it and try again: + +\begin{verbatim} +LOOP> (set-up-tables!) +(("ARTICLE" + . #S(COMMAND + :FN # + :VERB "ARTICLE" + :DESCRIPTION "fetches full articles")) +[...] +("HELP" + . #S(COMMAND + :FN # + :VERB "HELP" + :DESCRIPTION "displays this menu")) [...]) + +LOOP> (dispatch (make-request :verb "HELP")) +#S(RESPONSE + :CODE 200 + :DATA (87 104 97 [...] 112) + :REQUEST #S(REQUEST :VERB "HELP" :ARGS NIL :SAID NIL) + :MULTI-LINE YES) +\end{verbatim} + +That's better. These numbers we see in the response are the bytes in +the response. You can get a string version of these numbers: + +\begin{verbatim} +LOOP> (bytes->string (response-data (dispatch (make-request :verb "HELP")))) +"What's on the menu today? +ARTICLE fetches full articles +AUTHINFO makes me trust you +BODY fetches an article body +GROUP sets the current group +HEAD fetches article headers +HELP displays this menu +LIST lists all groups +MODE handles the mode request from clients +NEXT increments the article pointer +POST posts your article +QUIT politely says good-bye +XDD displays your state of affairs +XOVER fetches the overview database of a group" +\end{verbatim} + +This is the text that the user sees when they ask for {\tt HELP}. In +other words, the field {\tt data} in a [[response]] stores the data to +be delivered back to the user. The program {\tt nc}---for +``netcat''---that we use below is capable of opening a TCP connection +and handling that connection to our keyboard so that we can interact +with the \lp. You can effectively achieve the same thing using a +program such as {\tt telnet}. + +\begin{verbatim} +C:\>nc antartida.xyz 119 +200 Welcome! Say ``help'' for a menu. +help +200 What's on the menu today? +ARTICLE fetches full articles +AUTHINFO makes me trust you +BODY fetches an article body +GROUP sets the current group +HEAD fetches article headers +HELP displays this menu +LIST lists all groups +MODE handles the mode request from clients +NEXT increments the article pointer +POST posts your article +QUIT politely says good-bye +XDD displays your state of affairs +XOVER fetches the overview database of a group +. +quit +205 Good-bye. +\end{verbatim} + +But keep in mind that \lp\ was not made to talk to users directly. +\Lp\ was made to talk to your NNTP client, programs such as the ones +illustrated by Figures \ref{fg:gnus}--\ref{fg:sylpheed}. That's why +we see these numbers in the responses given by \lp. These numbers are +there to help clients understand how the conversation is going. Each +specific such number is determined by the NNTP protocol. But, despite +the protocol being made for machines to talk to each other, it's +perfectly possible for a user to interact with \lp\ directly using a +keyboard and a command-line tool such as {\tt nc} or {\tt telnet}. In +fact, \lp\ takes advantage of that to be hackable. \Lp\ was written +so that it can talk to NNTP clients---such as Gnus, Sylpheed {\em et + cetera}---but also to users directly. Commands such as +[[CREATE-ACCOUNT]], [[CREATE-GROUP]], [[PASSWD]] are not part of the +NNTP protocol, so users need to know how to use {\tt nc} or {\tt + telnet} to take advantage of all of \lp's capabilities. + +\section{The NNTP protocol} + +An Internet protocol is usually defined by a document whose tradition +calls RFC---for ``[r]equest [f]or [c]omments''. The NNTP protocol is +defined by RFCs 977, 2980, 3977, 4643 and 5536. RFC 977 was the first +and replaced by 3977. Still, reading RFC 977 is interesting precisely +because it gives us a historical account of the protocol, making it +easier to understanding the evolution of the system. The objective of +RFC 2980 was to implement new ideas to the NNTP protocol---to extend +the protocol. RFC 3977 adopts some of these extensions. RFC 4643 +also extends RFC 2980---addressing concerns with authentication. + +\section{It's a network server} + +The \lp\ program is a network server, that is, it serves connections +on the network. However, instead of implementing the handling of TCP +connections, we use what is called a ``TCP superserver'' or ``TCP +wrappers'' or perhaps just ``TCP server''. The idea is---a program +called ``TCP server'' waits for connections from the network. When a +client arrives, the TCP server handles the network connection to \lp. +From \lp's perspective, the client is just another keyboard talk to it +directly. This strategy simplifies the implementation of \lp. Both +programs---the TCP server and \lp---have nothing to do with one +another, but their work together make the system work. This +implementation strategy is typically found in UNIX programs. +% +\begin{quote}\small +This is the Unix philosophy. Write programs that do one thing and do +it well. Write programs to work together. Write programs that handle +text streams, because that is a universal interface.\\ -- Doug +McIlroy, 1989, interviewed by Michael S.~Mahoney. +\end{quote} +% +The TCP server just just one thing---listens for new connections and +handles them to the interested program. It does the handling and does +it well. \Lp, on the other hand, concerns itself with the NNTP +protocol and does not worry about handling network connections. This +way, they work together. And \lp\ handles only a text stream, which +is why it's so easy to connect a keyboard to it and interact with it +through the command line as illustrated in Section~\ref{sec:design}. + +\section{The representation of a client} + +How do we represent a client? A client is typically reading a group +and an article; it's has authenticated itself or not yet. So we need +a global structure to annonate the client's state. + +<>= +(defstruct client group (article 1) (username "ANONYMOUS") (auth? 'no)) +(defparameter *client* (make-client)) +@ %def client *client* + + +\section{The representation of a command} + +What does a client typically tell \lp? A client typically sends +commands. Commands typically need arguments. Each command is +dispatched to a procedure that answers it---it's the purpose of +\verb|fn| in the command. Together, all commands make up a table of +commands, which is essentially what the user sees when ask for +\verb|HELP|. (Be aware that some clients use the output of +\verb|HELP|. For example, Gnus v5.13.) + +<>= +(defstruct command fn verb description) +(defparameter *commands-assoc* nil) + +(defun table-of-commands () + `(("GROUP" ,#'cmd-group "sets the current group") + ("NEXT" ,#'cmd-next "increments the article pointer") + ("HELP" ,#'cmd-help "displays this menu") + ("LIST" ,#'cmd-list "lists all groups") + ("AUTHINFO" ,#'cmd-authinfo "makes me trust you") + ("LOGIN" ,#'cmd-login "shorter interface to AUTHINFO") + ("HEAD" ,#'cmd-head "fetches article headers") + ("MODE" ,#'cmd-mode "handles the mode request from clients") + ("BODY" ,#'cmd-body "fetches an article body") + ("POST" ,#'cmd-post "posts your article") + ("ARTICLE" ,#'cmd-article "fetches full articles") + ("XOVER" ,#'cmd-xover "fetches the overview database of a group") + ("CREATE-GROUP" ,#'cmd-create-group + "creates a new group so you can discuss your favorite topic") + ("CREATE-ACCOUNT",#'cmd-create-account + "creates an account so you can invite a friend") + ("PASSWD" ,#'cmd-passwd "changes your password") + ("USERS" ,#'cmd-list-users "lists all users") + ("DD" ,#'cmd-dd "[d]isplays [d]ata: your state of affairs") + ("QUIT" ,#'cmd-quit "politely says good-bye") + ("DATE" ,#'cmd-date "displays the current date at this server") + ("UNLOCK-ACCOUNT" ,#'cmd-unlock-account "unlocks an account"))) + +(defun set-up-tables! () + (labels ((build-commands-assoc (ls) + (if (null ls) + nil + (cons (apply #'make-command-pair (car ls)) + (build-commands-assoc (cdr ls))))) + (make-command-pair (name fn desc) + (cons name (make-command :fn fn :verb name :description desc)))) + (setf *commands-assoc* + (sort + (build-commands-assoc (table-of-commands)) + #'string-lessp :key #'car)))) + +(defun get-command (key) + (let ((cmd (assoc key *commands-assoc* :test #'string=))) + (labels ((unrecognized-command () + (make-command :fn #'(lambda (r) + (make-response :code 400 + :data "unrecognized command" + :request r)) + :verb 'unrecognized + :description "a command for all commands typed wrong"))) + (or (cdr cmd) (unrecognized-command))))) +@ %def *commands-assoc* set-up-tables! get-command + +\section{The representation of requests and responses} + +Each command is given through a text line written by the user. Let's +call this text line the [[request]]. When \lp\ parses the request, it +will extract (from the request) a verb and some arguments. We will +take a verbatim copy of everything the user has said, possibly for +debugging purposes. + +How do we represent a [[response]]? A [[response]] is always a +reaction to a [[request]]. The NNTP protocol always specifies an +integer as the code to a response, which is what we call the +\verb|code| in the response. Long responses end with a period and we +mark such responses with [[multi-line]]. + +<>= +(defstruct request verb args said) +(defstruct response code data request multi-line) + +(defun empty-response () (make-response :code 400 :data "I beg your pardon?")) +(defun prepend-response-with (message r) + (make-response + :code (response-code r) + :data (data message (crlf) (response-data r)) + :multi-line (response-multi-line r) + :request (response-request r))) +@ %def request response make-response make-request empty-response prepend-response-with + +Here's how to send a [[response]] to a client. + +<>= +(defun append-crlf-if-needed (seq) + (cond + ((stringp seq) + (append-crlf-if-needed (string->bytes seq))) + ((listp seq) + (append seq + (when (not (= (car (last seq)) 10)) + (list 13 10)))) + (t (error (format nil "append-crlf-if-needed: unsupported type: ~a" (type-of seq)))))) + +(defun send-response! (r) + (let ((bs (data (integer->string (response-code r)) " " + (append-crlf-if-needed (response-data r))))) + (my-write bs *standard-output*) + (stderr ">>> ~a" (bytes->string (ucs-2->ascii bs)))) + (when (response-multi-line r) + (let ((bs (data "." (crlf)))) + (my-write bs *standard-output*) + (stderr ">>> ~a" (bytes->string (ucs-2->ascii bs))))) + (force-output) + r) +@ %def send-response! + +The Windows Console---the one we use when we run {\tt cmd.exe}---makes +the Lisp procedure [[write-sequence]] produce UCS-2. Windows will do +what we need of [[write-sequence]] if we're not operating \lp\ on the +Windows Console. Recall that we do not run \lp\ on Windows Console, +so whatever happens because of it is of little importance to us. +However, we implement \lp\ on Windows and so it's convenient for us +that \lp\ and Windows play nicely with each other. A cheap solution +here is to simply convert the bytes to a string if \lp\ is directly +connected to an interactive Lisp stream. This way we effectively +eliminate the UCS-2 encoding used by Windows. It is perfectly fine +for us to destroy the encoding of articles while we're writing \Lp. +It is not fine, however, when it's running in production. But, in +production, {\tt (interactive-stream-p s)} will always be false. How +else should we handle this? + +<>= +(defun my-write (ls-of-bytes s) + (if (interactive-stream-p s) + (write-sequence (mapcar #'code-char ls-of-bytes) s) + (write-sequence ls-of-bytes s))) +@ %def my-write + +\section{The parsing of requests} + +The commands themselves we call {\tt verbs} and everything else the +user types we call {\tt args}. Observe that upper and lower case +letters are equivalent in request verbs. + +<>= +(defun parse-request (r) + (let* ((collapsed-s (str:collapse-whitespaces (request-said r))) + (ls (str:split " " collapsed-s :omit-nulls 'please))) + ;; What are we going to do with a null request? + (cond ((null ls) (make-request :said (request-said r))) + (t (let ((verb (car ls)) + (args (cdr ls))) + (make-request :said (request-said r) + :verb (str:upcase verb) + :args args)))))) +@ %def parse-request + +\section{The main loop} + +Every command consumes a [[request]] and produces a [[response]]. If +any procedure always produces a [[response]], then delivering a +[[response]] to the user is a matter of sending a string composed of +the [[response]] code contacated with the [[response]] data. + +What does \lp\ do? It repetitively reads a line from the user, +processes that line and always replies something back. Then \lp\ is +back at waiting for the user to say something else again. If the user +says {\tt QUIT}, then we should identify it and terminate \lp's +execution. That's even [[send-response!]] returns the [[request]] +itself---so we can cascade actions based on a user's request. + +<
>= +(defun main-loop () + (let* ((bs (nntp-read-line)) + (ln (bytes->string (ucs-2->ascii bs)))) + (if ln + (let ((r (send-response! (dispatch-line ln)))) + (when (not (response-quit? r)) + (main-loop))) + (progn + (stderr "eof~%") + 'eof)))) + +(defun request-quit? (r) (and r (string= 'quit (request-verb r)))) +(defun response-quit? (r) (and r (request-quit? (response-request r)))) + +(defun main () + (send-banner!) + (set-up-tables!) + (read-accounts!) + (connect-index! "message-id.db") + (create-index!) + (main-loop)) + +(defun send-banner! () + (send-response! + (make-response :code 200 :data "Welcome! Say ``help'' for a menu."))) +@ %def main main-loop + +\section{The request dispatching mechanism} + +Dispatching requests means consuming one and invoking the correct +procedure that will process the request. The invoked procedure must +produce a [[response]]. The work of dispatching is just delivering +the task to an response-producing procedure and then raising the +response to whoever needs to catch it. For example, +[[response-quit?]] is used by [[main-loop]] to identify when the user +has issued {\tt QUIT}, in which case we terminate [[main-loop]]. + +<>= +(defun dispatch (r) + (let* ((verb (request-verb r))) + (if (null verb) + (empty-response) + (funcall (command-fn (get-command verb)) r)))) + +(defun dispatch-line (ln) + (dispatch (parse-request (make-request :said ln)))) +@ %def dispatch dispatch-line + +\section{The representation and parsing of articles} + +An article is made of two parts, the head and the body. We do need to +parse the head, but we never parse the body: we don't want to +intervene in anything that a user might be doing in the body of the +article. The headers, however, are mostly under the jurisdiction of +the server. This decision is due to the fact servers must read +headers. For example, how do we know to which groups an article was +posted? We look at the header {\tt newsgroups}. So, the server must +understand the encoding of headers. Therefore, we assume ASCII +encoding of all headers that we need to parse. + +The member [[headers]] of the structure [[article]] is just a string, +while body is a vector of bytes. To get a list of pairs out of the +set of all headers of an article, we can ask [[parse-headers]]. Yes, +I should've called the member [[headers]] as [[head]] and not +[[headers]] because both the word ``headers'' and its plural used here +suggest a list of parsed headers. We're going to rename this in due +time. %% TODO + +<>= +(defstruct article headers body) + +(defun parse-article (v) + (let ((parts (split-vector (vector 13 10 13 10) v nil :limit 2))) + (make-article :headers (map 'string #'code-char (car parts)) :body (cadr parts)))) + +(defun hs-space-collapsed (hs) + (cl-ppcre:regex-replace-all (format nil "~a[ ~a]+" (crlf-string) #\tab) hs " ")) + +(defun hs-lines (lines) (str:split (crlf-string) lines)) + +(defun parse-header (header) + (let* ((h (str:collapse-whitespaces header)) + (pos (search ":" h))) + (when (null pos) + (throw 'article-syntax-error + (values nil (format nil "missing colon in header |~a|" h)))) + (when (<= (length h) (+ 2 pos)) + (throw 'article-syntax-error + (values nil (format nil "empty header ~a" h)))) + (multiple-value-bind (key val) + (values (subseq h 0 pos) (subseq h (+ 2 pos))) + (cons (str:downcase key) val)))) + +(defun parse-headers (hs) + (let ((ls (hs-lines (hs-space-collapsed hs)))) + (mapcar #'(lambda (h) (parse-header h)) ls))) + +(defun string-integer? (s) (ignore-errors (parse-integer s))) +@ %def parse-article parse-headers + +We now write some procedures that we use when we're build the {\em + overview} of the command \verb|XOVER|. + +<>= +(defun get-header-from-article (h a) + (get-header h (parse-headers (article-headers (parse-article a))))) + +(defun get-header (key hs) + (let ((pair (assoc key hs :test #'string=))) + (if pair (cdr pair) ""))) + +(defun fetch-headers (g i) + (let* ((a-string (fetch-article g i)) + (a-parsed (parse-article a-string)) + (headers (parse-headers (article-headers a-parsed)))) + (enrich-headers headers a-string))) + +(defun enrich-headers (hs a) + (append hs + `(("line-count" . ,(format nil "~a" (nlines (article-body (parse-article a))))) + ("byte-count" . ,(format nil "~a" (length a)))))) + +(defun nlines (v) (length (split-vector (crlf) v nil))) +@ %def get-header fetch-headers + +\section{How to extract articles from the database} + +Notice that we do not care about which encoding is used in articles. +We just read the article bytes and handle them to the client. It's +the article viewer's---the NNTP client's, that is---responsibility of +interpreting such bytes. That's why we call [[read-sequence]] here. + +<>= +(defun fetch-article (g i) + (in-groups + (read-file-raw (format nil "~a/~a" g i)))) + +(defun read-file-raw (path) + (let* ((size (sb-posix:stat-size (sb-posix:stat path))) + (a (make-array size))) + (with-open-file (in path :element-type '(unsigned-byte 8)) + (read-sequence a in) + a))) + +(defun fetch-body (g i) + (article-body (parse-article (fetch-article g i)))) +@ %def fetch-article + +The purpose of [[encode-body]] is to eventually worry about the +appearance of a period on a line by itself in the middle of an +article. Since ``\verb|.\r\n|'' is part of the NNTP protocol, we must +handle this gracefully---but notice we have not done anything about +that so far. So we are essentially writing a bug right here. + +<>= +(defun encode-body (a) a) +@ %def encode-body + +The procedures [[extract-mid]] and [[lookup]] also belong belong in +this section. Notice that I also wrote [[mid-by-name]], which should +merge with [[extract-mid]]. I think I also wrote more +redundancies---perhaps in the implementatio nof [[xover]]---for not +using [[lookup]]. I need to seek out all such places and organize. %% TODO + +<>= +(defun extract-mid (a) + (lookup "message-id" (parse-headers (article-headers (parse-article a))))) +(defun lookup (key table) + (cdr (assoc key table :test #'string=))) +@ %def extract-mid lookup + +\section{The commands} + +\subsection{{\tt HELP}} + +When someone asks for help, we present a table of commands. The table +construction is made by [[menu]]. The procedure [[menu]] was one of +the first things I wrote. (This is my first program written in Common +Lisp.) I didn't want to get involved with the famous [[loop]] macro, +so I used recursion in [[menu]]\footnote{I'd like to isolate these +auxiliary procedures inside a single function that uses them. Common +Lisp offers me [[labels]], but [[labels]] don't seem so helpful when +I'm at the REPL. When I use [[defun]], I'm able to always invoke the +procedure at the REPL, but that's not so with [[labels]]. I guess the +use of [[labels]] is when the procedure is so trivial that we have no +reason to think we're doing to debug it.} %% TODO + +<>= +(defun cmd-help (r) + (let ((lines (menu *commands-assoc*))) + (prepend-response-with + "What's on the menu today?" + (make-response :code 200 :multi-line 'yes + :request r + :data (str:join (crlf-string) lines))))) +(defun menu (ls) + (if (null ls) + nil + (cons (display-fn (car ls)) (menu (cdr ls))))) + +(defun display-fn (cmd-pair) + (let ((cmd (cdr cmd-pair))) + (format nil "~A ~A" + (command-verb cmd) + (command-description cmd)))) +@ + +\subsection{{\tt AUTHINFO}}\label{sec:authinfo} + +The implementation of {\tt AUTHINFO}. When we connect to +\lp\ directly from a keyboard, it's a bit painful to authenticate with +two commands---{\tt AUTHINFO user} and {\tt AUTHINFO pass}. So we +also implemented the command {\tt LOGIN}---see +Section~\ref{sec:login}. To check the user's password, we use the +procedure [[pass?]] that's defined in the implementation of {\tt + PASSWD}. Perhaps we should have called it {\tt + is-password-correct?} or something more obvious. + +<>= +Syntax error. Say ``authinfo USER /your-name/'' then ``authinfo PASS /your-pass/''. +@ + +<>= +(defun cmd-authinfo (r) + (let* ((args (mapcar #'str:upcase (request-args r)))) + (cond + ((not (= (length args) 2)) + (bad-input r "No, no: I take exactly two arguments.")) + (t + (multiple-value-bind (cmd arg) (apply #'values args) + (cond + ((string= cmd "USER") + (setf (client-username *client*) arg) + (make-response :code 381 :request r + :data (format nil "Hey, ~a, please tell us your password." arg))) + ((string= cmd "PASS") + (if (authinfo-check (client-username *client*) arg) + (progn + (log-user-in!) + (make-response + :code 281 :request r + :data (fmt "Welcome, ~a." (client-username *client*)))) + (make-response :code 400 :request r :data "Sorry. Wrong password."))) + (t (make-response :code 400 :request r :data "<>")))))))) + +(defun authinfo-check (username passwd) + (pass? username passwd)) + +(defun auth? () + (eq 'yes (client-auth? *client*))) + +(defun log-user-in! () + (setf (client-auth? *client*) 'yes) + (let ((u (get-account (client-username *client*)))) + (setf (account-seen u) (get-universal-time))) + (write-accounts!)) +@ %def auth? log-user-in! + +\subsection{{\tt CREATE-ACCOUNT}} + +We allow authenticated members to invite their friends. + +%% A propósito, estamos removendo a conta {\tt ROOT} de exibição. O que +%% significa que {\tt ROOT} não nem mesmo se conectar ao \Lp. Se +%% desejarmos que {\tt ROOT} se conecte, talvez a gente possa fazer +%% código especialmente pra gerenciar a conta dele. Fazemos assim pra +%% não permitir que usuários tenham qualquer chance de +%% +%% (remove-if #'(lambda (u) (equal "ROOT" (account-username u))) +%% (read s)) + +<>= +(defun cmd-create-account (r) + (with-auth + (with-n-args 1 r + (let* ((args (mapcar #'str:upcase (request-args r))) + (username (car args))) + (multiple-value-bind (username pass-or-error) (new-account! username) + (if (not username) + (make-response :code 400 :request r + :data (fmt "~a. Choose a new name." pass-or-error)) + (progn + (notify-user-created username) + (make-response :code 200 :request r + :data (fmt "Okay, account ~a created with password ``~a''." + username pass-or-error))))))))) + +(defparameter *accounts* nil) +(defstruct account username seen last-post friends pass pass-locked pass-locked-why creation) + +(defun read-accounts! () + (let ((*package* (find-package '#:loop))) + (with-open-file + (s "accounts.lisp" + :direction :input) + (setq *accounts* (read s)))) + *accounts*) + +(defun new-account! (username) + (let* ((u (str:upcase username)) + (p (random-string 6)) + (a (make-account :username u + :pass (sxhash (str:upcase p)) + :creation (get-universal-time)))) + (if (get-account u) + (values nil (fmt "account ~a already exists" u)) + (let ((c (get-account (client-username *client*)))) + (push u (account-friends c)) + (push a *accounts*) + (write-accounts!) + (values (str:upcase username) p))))) +@ %def CREATE-ACCOUNT + +Notice that we have a race condition in [[write-accounts]]. What is +the problem? Two processes in parallel may ask for the writing of +[[accounts.lisp]]. The process that loses the race will have its +modifications lost. What do we need to do? Either we use file +locking or we do something smarter without a real file locking +mechanism. It's not clear to me what is possible here, but this is +definitely a problem that we need to solve. + +<>= +(defun write-accounts! () + (let ((name + (loop + (let* ((tmp (random-string 10)) + (name (format nil "~a.tmp" tmp))) + (when + (ignore-errors + (with-open-file + (s name + :direction :output + :if-exists :error + :if-does-not-exist :create) + (write *accounts* :stream s))) + (return name)))))) + (if (ignore-errors (rename-file name "accounts.lisp")) + (values t *accounts*) + (values nil (format nil "could not rename ~a to accounts.lisp" name))))) + +(defun get-account (username) + (loop for u in *accounts* + do (when (string= (str:upcase username) (account-username u)) + (return u)))) +@ %def read-accounts! write-accounts! get-account + +\subsection{{\tt UNLOCK-ACCOUNT}} + +Inactive accounts are removed or locked---see Section +\ref{sec:inactive-users}. When an account is locked, any member can +unlock it. + +<>= +(defun cmd-unlock-account (r) + (with-auth + (with-n-args 1 r + (let* ((args (mapcar #'str:upcase (request-args r))) + (username (car args))) + (cond ((not (get-account username)) + (make-response :code 400 :request r + :data "No such account ~a." username)) + ((not (locked? username)) + (make-response :code 400 :request r + :data (fmt "Can't unlock ~a because it's not locked." username))) + (t + (unlock-account! username) + (notify-user-unlocked username) + (make-response :code 200 :request r + :data (fmt "Okay, account ~a unlocked." username)))))))) + +(defun unlock-account! (username) + (let ((u (get-account username))) + (cond ((not u) + (values nil "no such account")) + ((not (locked? username)) + (values nil "account isn't locked")) + (t + (setf (account-pass u) (account-pass-locked u)) + (setf (account-pass-locked u) nil) + (setf (account-pass-locked-why u) nil))))) +@ %def unlock-account! + +\subsection{{\tt LOGIN}}\label{sec:login} + +Besides {\tt AUTHINFO} in Section~\ref{sec:authinfo}, we also +implement a more convenient command for authenticationwhen we interact +with \lp\ through a command-line interface. Instead of having to say +two commands, we can just say {\tt login user password}. + +<>= +(defun cmd-login (r) + (let* ((args (mapcar #'str:upcase (request-args r)))) + (cond + ((not (= (length args) 2)) + (bad-input r "Usage: login your-username your-password")) + (t + (multiple-value-bind (name pass) (apply #'values args) + (cond + ((pass? name pass) + (log-user-in-as! name) + (make-response :code 200 :request r + :data (fmt "Welcome, ~a." name))) + (t (make-response :code 400 :request r + :data (fmt "Wrong password."))))))))) + +(defun log-user-in-as! (name) + (setf (client-username *client*) name) + (log-user-in!)) +@ %def log-user-in-as! + +\subsection{{\tt PASSWD}}\label{sec:passwd} + +A change of password is made with {\tt PASSWD current new}. Observe +that we are duplicating code from other command procedures. I think +there is a macro emerging here called [[with-upcase-args]]. %% TODO + +<>= +(defun cmd-passwd (r) + (with-auth + (let* ((args (mapcar #'str:upcase (request-args r)))) + (cond + ((not (= (length args) 2)) + (bad-input r "Usage: passwd current-password new-password")) + (t + (multiple-value-bind (cur new) (apply #'values args) + (cond + ((pass? (client-username *client*) cur) + (multiple-value-bind (okay? problem) (change-passwd! (client-username *client*) new) + (if okay? + (make-response :code 200 :request r + :data "You got it. Password changed.") + (make-response :code 500 :request r + :data (fmt "Sorry: ~a" problem))))) + (t (make-response :code 400 :request r + :data (fmt "Sorry. Wrong password.")))))))))) + +(defun pass? (username pass) + (let ((u (get-account username))) + (and u + (eq (sxhash pass) (account-pass u))))) + +(defun change-passwd! (username newpass) + (let ((u (get-account username))) + (when (not u) + (error "I could not find account ~a." username)) + (setf (account-pass u) (sxhash newpass)) + (write-accounts!))) + +@ %def PASSWD pass? change-passwd! + +\subsection{{\tt USERS}}\label{sec:users} + +The tree of users and their friends is public. Anyone can know who +invited who. + +<>= +(defun cmd-list-users (r) + (with-auth + (prepend-response-with + "List of current users:" + (make-response + :code 200 :request r :multi-line 'yes + :data (str:join (crlf-string) (list-users)))))) + +(defun size-of-longest-username () + (loop for u in *accounts* + maximizing (length (account-username u)))) + +(defun list-users () + (read-accounts!) + (mapcar (lambda (row) (cadr row)) + (sort + (loop for u in *accounts* + collect (list (account-username u) + (fmt "~v@a~a, ~a, invited ~a" + (size-of-longest-username) + (account-username u) + (if (locked? (account-username u)) + (fmt " (account locked: ~a)" + (account-pass-locked-why u)) + "") + (if (last-time-seen (account-username u)) + (fmt "last seen on ~a" (last-time-seen (account-username u))) + "never logged in") + + (or (account-friends u) "nobody")))) + #'string<= :key (lambda (row) (car row))))) + +(defun universal-to-human (s) + (format-timestring + nil + (universal-to-timestamp s) + :format +asctime-format+)) + +(defun last-time-seen (username) + (let ((u (get-account username))) + (if u (let ((s (account-seen u))) + (if s (universal-to-human s)))))) +@ %def list-users last-time-seen size-of-longest-username + +\subsection{{\tt LIST}}\label{sec:list} + +The database of groups and articles is a UNIX directory. We just need +to discover which directories exist and produce a listing. The heavy +work here is finding the index interval of articles in the group. (I +think we should already be optimizing this by merely caching the +information in a file that is read at start-up. I think we should +even cache the overview of the group.) %% TODO + +<>= +(defstruct group name high low) + +(defun cmd-list (r) + (prepend-response-with + "Get in the loop! Lots to choose from." + (make-response :code 215 :multi-line 'yes + :data (str:join (crlf-string) (build-groups-lines (build-groups-structs))) + :request r))) + +(defun build-groups-lines (ls) + (reverse + (mapcar + #'(lambda (g) + (format nil "~a ~a ~a y" (group-name g) (group-high g) (group-low g))) + ls))) + +(defun build-groups-structs () + (let ((ret-ls nil)) + (dolist (g (list-groups) ret-ls) + (multiple-value-bind (low high len) (group-high-low g) + (declare (ignore len)) + (setf ret-ls (cons (make-group :name g :high high :low low) ret-ls)))))) + +(defun between? (x from to) + (<= from x to)) +(declaim (inline between?)) + +(defun filesize (path) + (sb-posix:stat-size + (sb-posix:stat path))) + +(defun zero-file? (path) + (= (filesize path) 0)) + +(defun temporary-article? (path) + (or (zero-file? path) + (cl-ppcre:scan "\.tmp$" (namestring path)))) + +(defun article-ready? (path) + (not (temporary-article? path))) + +(defun get-articles (g &optional from to) + (in-groups ;; We might want to optimize this some day. Most likely, + ;; though, we'll not be using directories. That's a + ;; problem to be studied. + (let ((as (articles->integers + (remove-if #'temporary-article? (cl-fad:list-directory g))))) + (sort (remove-if-not + #'(lambda (x) (between? x (or from x) (or to x))) + as) + #'<)))) + +(defun group-high-low (g) + (let* ((articles (get-articles g)) + (sorted-ints (sort articles #'<))) + (values (or (car sorted-ints) 0) + (or (car (last sorted-ints)) 0) + (length sorted-ints)))) + +(defun articles->integers (ls) + (remove-if #'null + (mapcar #'(lambda (g) + (ignore-errors + (parse-integer (basename (uiop:unix-namestring g))))) + ls))) + +(defun list-groups () + (let ((groups (in-groups (cl-fad:list-directory ".")))) + (sort (mapcar #'(lambda (g) (basename (uiop:unix-namestring g))) groups) + #'string-lessp))) + +(defun last-char (s) (char s (1- (length s)))) +@ + +In [[basename]], what we want to do is---if the path ends with a +slash, we invoke [[pathname]]. Otherwise, we invoke {\tt + file-namestring}. + +<>= +(defun basename (path) + (let ((s (str:collapse-whitespaces path))) + (if (char= #\/ (last-char s)) + (car (last (pathname-directory s))) + (file-namestring s)))) +@ %def get-articles group-high-low + +\subsection{{\tt GROUP}}\label{sec:group} + +We just need to verify if the group exists and modify [[*client*]]. + +<>= +(defun cmd-group (r) + (with-auth + (with-n-args 1 r + (let ((g (car (request-args r)))) + (with-group g r + (set-group! g) + (multiple-value-bind (low high len) (group-high-low g) + (let ((ln (format nil "~a ~a ~a ~a" len low high g))) + (setf (client-article *client*) low) + (make-response :code 211 :request r :data ln)))))))) + +(defun group? (g) + (in-groups + (cl-fad:directory-exists-p g))) + +(defun xgroup? (g) + (cl-fad:directory-exists-p g)) + +(defun set-group! (g) + (setf (client-group *client*) g)) +@ %def group? + +Why have I written {\tt group?} and {\tt xgroup?}? There's probably a +clean-up task here. %% TODO + +\subsection{{\tt BODY}, {\tt HEAD}, {\tt ARTICLE} e {\tt NEXT}} +\label{sec:typical-cmds} + +Here we have an illustration of the expressive power of first-class +procedures. We want to implement the commands {\tt BODY}, {\tt HEAD}, +{\tt ARTICLE} and {\tt NEXT}. The ways to invoke them are (1) with no +argument, (2) with a single integer argument and (3) with a text +argument. In case (1), we use the number of the current +article---which is kept in [[*client*]]. In case (2), the NNTP client +specifies the article number. In case (3), the NNTP client specifies +the exact {\tt message-id}, which forces us to query the index---see +Section~\ref{sec:index} for the implementation of the index. + +<>= +(defun typical-cmd-head-body-article (r fn-name) + (with-auth + (with-group-set + (let ((args (request-args r))) + (cond ((null args) + (funcall fn-name r (client-group *client*) (client-article *client*))) + ((= 1 (length args)) + (let* ((n-or-mid (car args))) + (cond ((string-integer? n-or-mid) + (funcall fn-name r (client-group *client*) n-or-mid)) + (t (multiple-value-bind (group n-str) (lookup-index n-or-mid) + (if (and group n-str) + (funcall fn-name r group n-str) + (bad-input r (format nil "Unknown article ~a." n-or-mid)))))))) + (t (bad-input r "No, no: it takes at most two arguments."))))))) + +(defun cmd-head (r) + (typical-cmd-head-body-article r #'head-response)) +(defun cmd-body (r) + (typical-cmd-head-body-article r #'body-response)) +(defun cmd-article (r) + (typical-cmd-head-body-article r #'article-response)) + +(defun article-response (r g i) + (typical-cmd-response 220 r g i #'(lambda (a) (encode-body a)))) +(defun head-response (r g i) + (typical-cmd-response 221 r g i #'(lambda (a) (article-headers (parse-article a))))) +(defun body-response (r g i) + (typical-cmd-response 222 r g i #'(lambda (a) (encode-body (article-body (parse-article a)))))) +@ + +When we process such commands, it's typical that we need to verify the +existence of files {\em et cetera}. The procedure that might throw +[[sb-posix:syscall-error]] is [[sb-posix:stat-size]], which we use to +know how many bytes are there in an article, a necessary task in +producing the \verb|OVERVIEW|. + +<>= +(defun typical-cmd-response (code r g i get-data) + (let ((a (handler-case (fetch-article g i) + (sb-posix:syscall-error (c) + (make-response :code 400 :request r + :data (format nil "article ~a/~a: ~a" g i c))) + (sb-ext:file-does-not-exist (c) + (declare (ignore c)) + (make-response :code 400 :request r + :data (format nil "article ~a/~a does not exist" g i)))))) + (cond ((typep a 'response) a) + (t (prepend-response-with + (format nil "~a ~a" i (extract-mid a)) + (make-response :multi-line 'yes :code code + :request r :data (funcall get-data a))))))) +@ %def typical-cmd-response + +The command \verb|NEXT| has a slight different semantics. + +<>= +(defun cmd-next (r) + (with-auth + (let ((g (client-group *client*)) + (n-cur (client-article *client*))) + (cond + ((not g) (bad-input :code 412 r "must say GROUP first")) + (t (multiple-value-bind (low high len) (group-high-low g) + (declare (ignore low len)) + (cond ((= n-cur high) (bad-input r "you are at the last article already")) + (t (article-next! r g))))))))) + +(defun article-next! (r g) + (setf (client-article *client*) (1+ (client-article *client*))) + (let ((cur (client-article *client*))) + (make-response :code 223 + :request r + :data (format nil "~a ~a" cur (mid-by-name g cur))))) + +(defun mid-by-name (g name) + (extract-mid (fetch-article g name))) +@ %def cmd-next article-next! mid-by-name + +\subsection{{\tt XOVER}}\label{sec:xover} + +The procedure [[cmd-xover]] is used to figure out what the user said. +Once we have that figured out, we invoke [[xover]], which finishes the +work. Notice that when the argument [[to]] from [[xover]] is [[NIL]], +then the user is asking for articles indexed from the integer [[fr]] +to the last one. + +<>= +(defun cmd-xover (r) + (with-auth + (with-group-set + (let ((args (request-args r))) + (cond ((null args) + (xover r (client-article *client*) (client-article *client*))) + ((= 1 (length args)) + (multiple-value-bind (s v) + (cl-ppcre:scan-to-strings "([0-9]+)([-]?)([0-9]*)" (car args)) + (cond + ((not s) (make-response :code 502 :request r :data "bad syntax")) + (t (let ((fr (parse-integer (aref v 0))) + (hifen (aref v 1)) + (to (ignore-errors (parse-integer (aref v 2))))) + (when (not (string= hifen "-")) + (setq to fr)) + (xover r fr to)))))) + (t (make-response :code 502 :request r :data "bad syntax"))))))) + +(defun xover (r from to) + (assert (client-group *client*)) + (let* ((g (client-group *client*)) + (ls (get-articles g from to))) + (cond ((= 0 (length ls)) + (make-response :code 420 :request r :data "no articles in the range")) + (t + (prepend-response-with + "Okay, your overview follows..." + (make-response + :code 224 :request r :multi-line 'yes + :data (str:join + (crlf-string) + (loop for i in ls + collect (xover-format-line + i + (remove-if-not + #'(lambda (h) + (member (car h) (xover-headers) + :test #'string=)) + (fetch-headers g i))))))))))) +(defun xover-format-line (i hs) + (str:concat (format nil "~a~a" i #\tab) + (str:join #\tab + (mapcar #'(lambda (h) (get-header h hs)) + (xover-headers))))) +(defun xover-headers () + '("subject" "from" "date" "message-id" "references" "line-count" "byte-count")) +@ + +\subsection{{\tt MODE READER}}\label{sec:mode-reader} + +So, we're always in reader mode, so we just ignore this command. + +<>= +(defun cmd-mode (r) ;; Whatever. + (make-response :code 200 :request r :data "Sure thing.")) +@ %def + +\subsection{{\tt DATE}}\label{sec:date} + +It's always useful to know the time and date at a computer. We should +surely format it a bit better than what {\tt now} does. + +<>= +(defun cmd-date (r) + (make-response :code 201 + :request r + :data + (format-timestring nil (now)))) +@ %def + +\subsection{{\tt QUIT}}\label{sec:quit} + +The use of {\tt QUIT} has a conection to [[main-loop]]: when the user +says {\tt QUIT}, [[main-loop]] must terminate. + +<>= +(defun cmd-quit (r) + (make-response :code 205 :data "Good-bye." :request r)) +@ %def + +\subsection{{\tt DD}}\label{sec:dd} + +The command {\tt DD} means ``[d]isplay client [d]ata structures''. It +shows to the client the internal state of how the server sees it. +I've used only for debugging and it's not really useful any longer. +I'm going to remove this very soon. + +<>= +(defun cmd-dd (r) + (make-response :code 200 :data (format nil "state: ~a" *client*) :request r)) +@ %def + +\subsection{{\tt POST}}\label{sec:post} + +If the client says {\tt POST}, then we continue to read line after +line until we find \verb|".\r\n"|. Having done that, we must check +whether we have a conformant article at hands. The definition of +conformant is given by [[conforms?]]. In a few words, the article +must have \verb|message-id|, \verb|subject|, \verb|from|, +\verb|newsgroups|. If the client doesn't provide us with a +\verb|message-id|, then \lp\ adds one. (Similarly for \verb|date|.) + +<>= +<> +(defun suggest-message-id (&optional (n 20)) + (format nil "<~a@loop>" (random-string n))) + +(defun random-string (size) + (let* ((universe "abcdefghijklmnopqrstuvwxyz") + (len (length universe)) + (state (make-random-state t)) + mid) + (dotimes (c size) + (setq mid (cons (char universe (random len state)) mid))) + (coerce mid 'string))) +@ + +Sometimes we parse an article and sometimes we want to undo that +parsing. Am I doing something wrong? I wonder. %% TODO + +<>= +(defun unparse-article (parsed) + (data + (let ((ls)) + (dolist (h (parse-headers (article-headers parsed))) + (setq ls (cons (data (str:capitalize (car h)) ": " (cdr h) (crlf)) ls))) + (nreverse ls)) + (crlf) + (article-body parsed))) +@ %def unparse-article + +If an article being posted has no \verb|message-id| or \verb|date|, +\lp\ provides these headers. We kill these two rabbits with +[[ensure-header]], but we should probably make a table of headers and +procedures that would generate such headers if they're missing. Right +now, however, we have only these two to worry about. + +<>= +(defun ensure-header (h fn bs) + (let* ((headers (parse-headers (article-headers (parse-article bs))))) + (if (lookup h headers) + bs + (unparse-article + (make-article + :headers + (str:join (crlf-string) + (mapcar (lambda (h) + (format nil "~a: ~a" (car h) (cdr h))) + (cons (cons h (funcall fn)) headers))) + :body (article-body (parse-article bs))))))) + +(defun get-date () + (multiple-value-bind (s m h day mon year dow dst-p tz) + (get-decoded-time) + (declare (ignore dow dst-p)) + (format nil "~4,'0d-~2,'0d-~2,'0d ~2,'0d:~2,'0d:~2,'0d GMT~a" + year mon day h m s (- tz)))) + +(defun ensure-mid (bs) + (ensure-header "message-id" #'suggest-message-id bs)) +(defun ensure-date (bs) + (ensure-header "date" #'get-date bs)) +@ %def ensure-mid ensure-date + +Now it's time to look at the header \verb|newsgroups|. (XXX: Our code +here is a bit confusing, but I don't know the best to do here, so I'm +going ahead unpretentiously.) If we get approved by [[conforms?]], +then we verify the list of newsgroups right away. + +The name of each group must conform to the expression + +<
>= +^([a-z0-9]+) +@ %def the-form-of-newsgroup-names + +I think people should have total freedom in naming groups. If users +create groups that mess up the local organization, then people should +discuss the matter and find a solution. Let's let people mess it up +instead of trying to stop them---the way of the hacker. + +<>= +(defun newsgroups-header->list (s) + (mapcar (lambda (n) (str:trim (string-downcase n))) (str:split "," s))) + +(defun cmd-post (r) + (with-auth + (send-response! + (make-response :code 340 + :data (format nil "Okay, go ahead. Suggested message-id ~a." + (suggest-message-id)))) + (let* ((bs (nntp-read-article))) + (multiple-value-bind (okay? error) (conforms? bs) + (if (not okay?) + (make-response :code 400 :request r + :data (format nil "Sorry. Your article doesn't conform: ~a." error)) + (multiple-value-bind (code reply) (post bs) + (make-response :code code :request r :data reply))))))) + +(defun post (bs) + (let ((ngs (newsgroups-header->list + (get-header "newsgroups" (parse-headers + (article-headers + (parse-article bs)))))) + ngs-dont-exist) + (dolist (ng ngs) + (if (and (group-name-conforms? ng) + (group? ng)) + (progn + (let ((a (ensure-date (ensure-mid bs)))) + (save-article-insist ng (get-next-article-id ng) a (extract-mid a)) + (update-last-post-date! (client-username *client*)))) + (push ng ngs-dont-exist))) + (if (zerop (- (length ngs) (length ngs-dont-exist))) + (values 400 "Sorry. There was not a single valid newsgroup specified.") + (values 240 (data "Thank you! Your article has been saved." + (when ngs-dont-exist + (data " However, the groups " + (str:join ", " (sort ngs-dont-exist #'string<)) + " just don't exist."))))))) +@ %def post + +XXX: Oh, have a look at that. We accept the article even if there are +invalid groups. We should not do that. A user might only want to +post at all if his message is cross-posted to a few groups. A user +might easily mistype a group name. The Right Thing here is more +likely to stop posting completely with an error message telling the +user to either remove the invalid group of type it up properly. + +<>= +(defun update-last-post-date! (username) + (let ((u (get-account username))) + (setf (account-last-post u) (get-universal-time)))) +@ %def update-last-post-date! + +If [[save-article-try]] returns [[NIL]], then [[probe-file]] +has found an article with name [[name-try]], that is, the procedure +is only successful if [[name-try]] is not yet taken and the writing +takes place successfully. + +<>= +(defun rename-no-extension (old new) + (rename-file old (make-pathname :name new :type :unspecific))) + +(defun save-article-try (name-try bs) + (let ((name (format nil "~a" name-try)) + (tmp (format nil "~a.tmp" name-try))) + (with-open-file + (s name + :direction :output + :if-exists :error ;; an atomic operation + :if-does-not-exist :create)) + ;(format t "save-article-try: ~a~%" name) + (with-open-file + (s tmp + :direction :output + :if-exists :error + :if-does-not-exist :create + :element-type '(unsigned-byte 8)) + (write-sequence bs s)) + (rename-no-extension tmp name))) +@ + +The procedure [[save-article-insist]] can return [[NIL]] and still +have perfectly done its job: it's possible for [[insert-index]] to +return [[NIL]] because [[message-id]] may already exist in the index, +but that may be no error---for example, when cross-posting. The +strategy is to write the article using [[name-try]]. If it's not +possible to write it because of a [[sb-ext:file-exists]] condition, +then we try the new name {\tt (1+ name-try)} and we repeat these +attempts until we make it. If other condition appears, we let it +propagate up the stack. If we get to the second [[let]], it's because +the article has been saved successfully, so we finish with whatever it +is that [[insert-index-or-log-failure]] must do. + +<>= +(defun save-article-insist (g name a message-id) + (loop for name from name do + (in-dir (format nil "groups/~a/" g) + (handler-case + (save-article-try name a) + (sb-ext:file-exists () + ;; We might want to log the fact. + ;(format t "name ~a already exists...~%" name) + ) + (:no-error (new before after) ;; the return values from return-file + (declare (ignore new before after)) + (return (values name (insert-index message-id g (fmt "~a" name))))))))) + +(defun get-next-article-name (g) + (format nil "~a" (get-next-article-id g))) + +(defun get-next-article-id (g) + (multiple-value-bind (low high len) (group-high-low g) + (declare (ignore low len)) + (1+ high))) +@ + +{\bf How to read lines in the NNTP protocol?} We've implemented the +most trivial strategy possible. It's also the slowest. What I think +we need to do here is to use [[vector-push-extend]]. But this is to +be done in [[nntp-read-line]]. I hope to be able to get a faster +procedure in [[nntp-read-line]] and keep [[nntp-read-article]] as it +is. This is important to speed up posting. For instance, if we allow +attachments (which we don't), the performance penalty is clearly +noticeable. %%TODO + +<>= +(defun nntp-read-article (&optional acc) + ;; Returns List-of Byte. + (let* ((ls (ucs-2->ascii (nntp-read-line)))) + (cond ;; 46 == (byte #\.) + ((equal (list 46) ls) (flatten (add-crlf-between acc))) + (t (nntp-read-article (append acc (list ls))))))) +@ %def nntp-read-article + +The NNTP protocol establishes that line termination is done with +\verb|\r\n|, but it's useful to support UNIX line terminations, too, +because we are using the command-line and interact directly with the +server using tools that will not always send \verb|\r\n| as line +termination. For example, when someone is typing directly from the +keyboard and insert an empty line, we need the {\tt (and acc ...)} +because sometimes the list [[acc]] comes out empty. But an empty line +never comes from the NNTP protocol because there's is always a {\tt + CR} before {\tt LF}, but that's not true when someone is using the +keyboard directly. + +<>= +(defun nntp-read-line (&optional (s *standard-input*) acc) + ;; Returns List-of Byte. + (let ((x (read-byte s))) + (cond ((or (null x) (= x 10)) + (let ((bs (and acc (nreverse (if (= (car acc) 13) (cdr acc) acc))))) + (stderr "<<< ~a~%" (bytes->string (ucs-2->ascii bs))) + bs)) + (t (nntp-read-line s (cons x acc)))))) + +(defun list->bytes (ls) + (mapcar #'data->bytes ls)) + +(defun vector->bytes (v) + (mapcar #'data->bytes (coerce v 'list))) + +(defun data->bytes (d) + (cond ((null d) nil) + ((integerp d) (list d)) + ((stringp d) (string->bytes d)) + ((consp d) (list->bytes d)) + ((vectorp d) (vector->bytes d)) + (t (error (format nil "type ~a is not supported" (type-of d)))))) + +(defun add-crlf-between (ls-of-ls) + ;; Add \r\n to each ``line''. Returns List-of Byte. + (mapcar (lambda (ls) (append ls (list 13 10))) ls-of-ls)) + +(defun string->bytes (s) + (map 'list #'char-code s)) + +(defun bytes->string (ls) + (map 'string #'code-char ls)) +@ %def nntp-read-line nntp-read-article + +{\bf Does the article conform?} RFC 5536 obliges every article to +have exactly \verb|date|, \verb|from|, \verb|message-id|, +\verb|newsgroups|, \verb|path| and \verb|subject| headers. The +headers \verb|path| is \Lp's responsibility, but it's used only in a +network of servers---so we're leaving that out for now. The header +\verb|message-id| is added by \Lp\ if the client doesn't write it +itself. + +Let's criticize the writing of [[conforms?]]. We have a [[catch]] +here and a [[throw]] in [[parse-headers]]. We also have a [[return]] +here. It's getting hard to read this procedure because it's not easy +to know that a procedure has to return a certain value to match the +expectation of another procedure. I don't remember what [[catch]] +does. I need to review this and then add the explanation for myself. +If I don't remember how this works, other beginners won't know it +either. %% TODO + +<>= +(defun conforms? (bs) + (catch 'article-syntax-error ;; parse-headers might throw + (let ((headers (parse-headers (article-headers (parse-article bs))))) + (let ((result (dolist (h (headers-required-from-clients)) + (when (not (lookup h headers)) + (return (format nil "missing the /~a/ header" h))))) + (content-type (get-header "content-type" headers))) + (cond + ((stringp result) (values nil result)) + ((not (text/plain? content-type)) + (values nil (format nil "content-type must be plain/text, but it's ~a" content-type))) + (t (values t nil))))))) + +(defun text/plain? (header-s) + ;; I say T when S begins with "text/plain" or when S is "". + (let* ((s (str:collapse-whitespaces header-s)) + (needle "text/plain") + (len (min (length needle) (length s)))) + (or (zerop len) + (and (<= (length needle) (length s)) + (string= needle s :end1 len :end2 len))))) + +(defun headers-required-from-clients () + '("from" "newsgroups" "subject")) +@ %def conforms? + +Notice that up to this point we've only verified if the necessary +headers are present. The \verb|newsgroups| header is a direct +influence to the article storage. For instance, \verb|newsgroups| +must mention only groups that exist. When we save the article, we +check each group. If at least one group exists, we save the article; +if at least one group doesn't exist, we report to the user all groups +that don't exist, but we do save the article if at least one does +exist. That's probably not the best thing to do. We should probably +warn the user that one group doesn't exist because that could make all +the difference to the user. For instance, someone might decide not to +post at all if they can't cross post to all the groups they wish to. +One typo in one group name and the article would be posted to some +groups, but not to the misstyped one. We need to change this. %% TODO + +Also, do notice that to simplify matters we're duplicating articles +cross-posted. What we should do is write the article to the first +group in the list of \verb|newsgroups| and then make a symbolic link +to all others. The problem is that I don't know how to do that on +Windows. I'm not sure if Windows supports symbolic links at all. We +could perhaps duplicate articles only when on Windows. %% TODO + +\subsection{{\tt CREATE-GROUP}} + +We allow every user to create their own groups. (Are we crazy?) When +someone craetes a group, we post an article to {\tt + local.control.news} notifying everyone that a new group has been +created. People then have a chance to subscribe to the new group. We +assume that when someone creates a group, it's either no problem at +all or it has been discussed with the community beforehand. + +<>= +(defun cmd-create-group (r) + (with-n-args 1 r + (let ((g (string-downcase (car (request-args r))))) + (multiple-value-bind (okay? reason) + (group-name-conforms? g) + (if (not okay?) + (make-response :code 580 :request r + :data (format nil "group name does not conform: ~a" reason)) + (progn + (multiple-value-bind (path created?) + (in-groups (ensure-directories-exist (concatenate 'string g "/"))) + (declare (ignore created?)) + (if (not path) + (make-response :code 581 :request r + :data (format nil "could not create group ~a" + (if (group? g) + "because it already exists" + "but we don't know why---sorry!"))) + (progn + (notify-group-created g) + (make-response :code 280 :request r + :data (format nil "group ~a created" g))))))))))) + +(defun group-name-conforms? (g) + (let ((okay? (cl-ppcre:scan-to-strings "<>" g))) + (if okay? + (values t nil) + (values nil "must match <>")))) +@ %def CREATE-GROUP group-name-conforms? + +\section{The publication of news} + +If you're interested in being notified about what's going on in the +\lp, then subscribe to {\tt local.control.news}. Group creation, +invitations {\em et cetera} are published there. + +<>= +(defun notify-group-created (g) + (post-notification + :subject (fmt "new group ~a by ~a" g (client-username *client*)) + :body (fmt "Blame ~a for the group ~a just created." (client-username *client*) g))) + +(defun notify-user-created (u) + (post-notification + :subject (fmt "new account ~a by ~a" u (client-username *client*)) + :body (fmt "Blame ~a for inviting ~a." (client-username *client*) u))) + +(defun notify-user-unlocked (u) + (let ((guilty (client-username *client*))) + (post-notification + :subject (fmt "account ~a unlocked by ~a" u guilty) + :body (fmt "Blame ~a for unlocking ~a." guilty u)))) + +(defun post-notification (&key subject body) + (in-groups (ensure-directories-exist "local.control.news/")) + (when (group? "local.control.news") + (let ((a (make-news :subject subject :body body))) + (post (concatenate 'vector (article-headers a) (crlf) (article-body a) (crlf)))))) + +(defun make-news (&key subject body) + (make-article + :headers (data + (add-crlf-between + (mapcar + (lambda (p) (data (format nil "~a: ~a" (car p) (cdr p)))) + `(("from" . "Loop") + ("subject" . ,subject) + ("newsgroups" . "local.control.news"))))) + :body (data body))) +@ %def notify-group-created notify-user-created + +\section{The algorithm of {\tt split-vector}} + +How should we describe the algorithm of [[split-vector]]? The +objective is to consume lists of bytes such as +[[(1 2 3 13 10 4 5 6 13 10 7 8 9)]] and produce lists of bytes +such as [[((1 2 3) (4 5 6) (7 8 9))]]. We use [[search]] to find [[13 10]]. +When we find this line termination, we have its position in the +consumed list. With the position, we collect the line and we iterate +searching for the next line. + +<>= +(defun split-vector (delim v acc &key limit (so-far 1)) + (let ((len (length v))) + (split-vector-helper delim v len acc limit so-far 0))) + +(defun split-vector-helper (delim v len acc limit so-far start) + (if (zerop len) + acc + (let ((pos (search delim v :start2 start :end2 len))) + (cond ((or (not pos) (and limit (= so-far limit))) + (nreverse (cons (subseq v start len) acc))) + (t (split-vector-helper + delim + v + len + (cons (subseq v start (or pos len)) acc) + limit + (1+ so-far) + (+ pos (length delim)))))))) +@ %def split-vector + +\section{The index article}\label{sec:index} + +Every NNTP server needs to have an index of articles. Each article is +indexed by its message-id. For example, the article +% +\begin{verbatim} + From: Eva Lu + Newsgroups: comp.unix + Subject: xv6: an introduction + Date: Wed, 24 Jul 2024 22:42:50 -0300 + Message-ID: <87plr25js5.fsf@tor.soy> + Cancel-Lock: sha1:82E8hqL1D1WQ9xc+CfnEYbFAaJo= + MIME-Version: 1.0 + Content-Type: text/plain; charset=utf-8 + Content-Transfer-Encoding: 8bit +\end{verbatim} +% +is indexed by the header {\tt message-id}. If you ask for article +{\tt <87plr25js5.fsf@tor.soy>}, \Lp\ will tell you that you can find +the article in group {\tt comp.unix} and its numeric ID is 37. Note +that the number 37 is not inside the article, but only in the name of +the file stored in the file system. The index, therefore, knows in +which file is each {\tt message-id}. This fact implies that you +cannot rename files in the file system---of course not: you'd changing +identifiers inside a database. If you have rename a file, you will +need to rebuild the index. Given that the index is an SQL table, you +can adjust the index relative to the any files you may need to rename +for whatever reason. You can also rebuild the entire index by reading +the file system---unless you have a lot of files, that's probably the +easiest thing to do. + +The use of [[*default-database*]] by the library [[clsql]] is very +convenient for us: we don't need to specify with which database we're +working. Since we work with only one, we pretty much never need to +specify anything. + +<>= +(defvar *default-database* nil) +@ %def *default-database* + +<>= +(defun connect-index! (filename) + (setq *default-database* (clsql:connect (list filename) :database-type :sqlite3))) + +(defun create-index! () + (clsql:execute-command "create table if not exists indices + (id varchar(1000), grp varchar(1000), article varchar(300))") + (clsql:execute-command "create unique index if not exists idx_id_1 + on indices (id)")) + +(defun remake-index! () + (clsql:execute-command "drop table if exists indices") + (create-index!)) +@ %def create-index! remake-index! connect-index! + +Of course, the creation and connection of the index must occur before +[[main-loop]], so it takes place in [[main]]. + +When someone requests an article, it's either by its numeric index but +then the client has already chosen a group, or it's by its {\tt + message-id}. We don't need to tell the client to which groups the +article belongs; we just give the entire article to the client. It +is, therefore, the client's responsibility what to do with the +article. However, to fetch an article, we need to know where in the +database (in the file system) is the article; in other words, we must +know one group in which the article was stored. This implies that the +index must know at least one group. We've decided to always index the +first group in the {\tt newsgroups} header. So the index's anatomy is +$(m, g, i)$, where $m$ is the {\tt message-id}, $g$ is the name of the +group and $i$, is the name of the article in the file system. This +also defines the anatomy of the SQL table. + +Should we store more information in the index? Not really. If we +need anything about an article, we can get it after we fetch it from +the file system. For example, suppose that a search command wishes to +display the fact that article was posted in various groups. Suppose +further the command has already located in the index an article to be +displayed. This means the command has the {\tt message-id} and one of +the groups in which the article was posted. The command is then able +to fetch the entire article from the file system. Now it's a matter +of reading the article itself to know almost everything there is to +know about it. (It's also interesting that we keep the index thin +because we need to allow it to grow to great sizes.) + +%% (clsql:create-table "INDICES" '(([id] (string 1000)) ([grp] (string 1000)) ([article] (string 300)))) +%% (clsql:execute-command "create table if not exists indices (id varchar(1000), grp varchar(1000), article varchar(300))") + +%% LOOP> (clsql:create-index "idx_id_1" :on "indices" :attributes "id" :unique "id") +%% ; No value +%% (clsql:execute-command "CREATE UNIQUE INDEX if not exists idx_id_1 ON INDICES (ID)") + +%% CL-USER> (clsql:list-attributes "indices") +%% ("ID" "GRP" "ARTICLE") + +%% CL-USER> (clsql:insert-records :into "indices" :attributes '(id grp article) :values '("<87plr25js5.fsf@tor.soy>" "comp.unix" 37)) +%% ; No value + +%% CL-USER> (clsql:select 'id 'grp 'article :from "indices") +%% (("<87plr25js5.fsf@tor.soy>" "comp.unix")) +%% ("ID" "GRP") + +\section{Essential operations relative to the index} + +Here's how to query the index or how to insert a new article into it. +If [[insert-index]] returns [[nil]], then it's because it found no +errors at all. The return value, therefore, indicates which error +ocurred. + +We don't consider an error that an article has already been added to +the index. For instance, we write multiple messages to the storage +when someone cross-posts, but we'll add just a single record to the +index, of course. So, an article already indexed is normal situation. +Sure---in the future, we will not duplicate articles in storage; we +will make symbolic links. We don't do that right now because Windows +doesn't really support symbolic links. + +<>= +(defun insert-index (m g i) + (handler-case + (clsql:insert-records + :into "indices" + :attributes '(id grp article) + :values (list (str:trim m) (str:trim g) (str:trim i))) + (clsql-sys:sql-database-data-error (c) + (cond ((= (slot-value c 'clsql-sys::error-id) 19) + 'already-indexed) + (t + ; We should log this error. + ;(format t "other error: ~a" (slot-value c 'clsql-sys::database-message)) + 'sql-error))) + (:no-error () + nil))) + +(defun lookup-index (mid) + (let* ((found (clsql:select [grp] [article] + :from [indices] + :where [= [id] (str:trim mid)])) + (article (first found)) + (grp (first article)) + (art (second article))) + (when found + (values grp art)))) +@ %def insert-index lookup-index + +\section{A procedure to import the index from the file system} + +%% get group: +%% (first (last (pathname-directory (car (in-groups (directory "**/*")))))) + +%% get article name: +%% (pathname-name (car (in-groups (directory "**/*")))) + +%% get all articles +%% LOOP> (in-groups (directory "**/*")) + +%% get the newsgroup header from the article +%% LOOP> (get-header-from-article "newsgroups" (fetch-article "comp.lang.lisp" "1")) + +When we build the index from scratch, it's important to have a +procedure capable of reading all articles in the file system and index +them one by one. This is what we implement right here. For each news +group, we get the name of the file relative to the article---that's +the numeric ID of the article---and its {\tt message-id}. That's all +we need to index it. + +<>= +(defun index-from-fs! () + (loop for path in (in-groups (directory "**/*")) + do (let* ((g (str:trim (first (last (pathname-directory path))))) + (i (str:trim (pathname-name path))) + (m (str:trim (extract-mid (fetch-article g i))))) + (when (> (length m) 0) + (format t "article ~a/~a indexed by ~a~%" g i m) + (insert-index m g i))))) + +(defun remake-index-from-fs () + (remake-index!) + (index-from-fs!)) +@ + +Here's a program to build the index from a UNIX shell. + +<>= +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(in-package #:loop) +(connect-index! "message-id.db") +(remake-index!) +(index-from-fs) +(format t "Index built.~%") +@ + +Be careful when using this program: it will build the database +[[message-id.db]], which is an operation that needs to be done only +once. Here's how to use it: +% +\begin{verbatim} +%pwd +/home/dbastos/loop + +%sbcl --script build-index-from-fs.lisp +Index built. + +%ls -l message-id.db +-rw-r--r-- 1 dbastos wheel 65536 Aug 26 13:32 message-id.db +\end{verbatim} + +\section{Deletion and locking of inactive accounts}\label{sec:inactive-users} + +In [[remove-friend]], note that [[username]] is the account name and +[[friend]] is the name of the account being removed. Notice as well +that we only know who invited the person after we can get a hold of +the account in [[accounts.lisp]]. This means we must scan each +account to delete an account---we can't delete an account and still +leave the account as someone's friend. + +The program [[cron-remove-inactive-users.lisp]] can be executed every +day at midnight, say. + +<>= +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(in-package #:loop) +;; (format t *default-pathname-defaults*) +(read-accounts!) +(connect-index! "message-id.db") +(remove-inactive-users!) +(write-accounts!) +@ %def cron-remove-inactive-users.lisp + +In [[remove-account]], we probably should use [[delete-if]] as well on +the list of friends since it is effectively what we are doing there +with [[setf]]. %% TODO + +<>= +(defun remove-inactive-users! () + (loop for u in *accounts* do + (let ((username (account-username u))) + (format t "Username: ~a~%" username) + (cond ((and (not (locked? username)) + (inactive-from-never-logged-in? username)) + (post-notification + :subject (fmt "account ~a removed by Loop" username) + :body (fmt "~a didn't log in a first time (for ~a month~a) since account creation." + username *months-never-logged-in* + (plural *months-never-logged-in* "s"))) + (remove-account! username) + (format t "Removed ~a due to never logging in.~%" username)) + ((and (not (locked? username)) + (inactive-from-last-seen? username)) + (post-notification + :subject (fmt "account ~a locked by Loop" username) + :body (fmt "~a disappeared for over ~a month~a." + username *months-inactive-allowed* + (plural *months-inactive-allowed* "s"))) + (lock-account! username + (fmt "disappeared for over ~a months" + *months-inactive-allowed*)) + (format t "Locked ~a due to long-time-no-see.~%" username)))))) + +(defun remove-account! (username) + (loop for u in *accounts* do + (delete-if #'(lambda (x) (equal x username)) (account-friends u))) + (delete-if #'(lambda (x) (equal username (account-username x))) *accounts*)) + +(defun lock-account! (username why) + (let ((u (get-account username))) + (setf (account-pass-locked u) (account-pass u)) + (setf (account-pass u) "locked") + (setf (account-pass-locked-why u) why))) + +(defun remove-friend (username friend) + (remove-if #'(lambda (x) (equal x friend)) + (account-friends (get-account username)))) +@ %def remove-account! remove-friend + +Accounts that do not have a creation date up until today---Tue Sep 17 +21:37:18 ESAST 2024---will have its creation dates migrated to the +\Lp\ epoch, which is January 1st 2024, the exact month in which +\Lp\ was written. But notice that this migration is done only once. +New system administrators of \Lp\ will never need to run this. + +<>= +(defun loop-epoch () + (encode-timestamp 0 0 0 0 1 1 2024)) + +(defun migrate-add-creation-and-post-date! () + (read-accounts!) + (loop for u in *accounts* + do (if (not (account-creation u)) + (setf (account-creation u) (timestamp-to-universal (loop-epoch))) + (setf (account-last-post u) (account-seen u)))) + (write-accounts!)) +@ %def migrate-add-creation-and-post-date! loop-epoch + +Here's a program to run the migration in a UNIX shell. + +<>= +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(in-package #:loop) +(migrate-add-creation-and-post-date!) +(format t "Accounts rewritten.~%") +@ %def migrate-add-creation-date.lisp + +Now we write the procedures that discover what accounts are inactive. + +<>= +(defparameter *months-inactive-allowed* 3) +(defparameter *months-never-logged-in* 1) + +(defun user-inactive? (username) + (or (inactive-from-never-logged-in? username) + (inactive-from-last-seen? username))) + +(defun inactive-from-never-logged-in? (username) + (let ((u (get-account username))) + (if (ever-logged-in? username) + NIL + (inactive-from? username *months-never-logged-in* + #'(lambda () (account-creation u)))))) + +(defun locked? (username) + (equal "locked" (account-pass (get-account username)))) + +(defun inactive-from-last-post? (username) + (let ((last-post (account-last-post (get-account username))) + (creation (account-creation (get-account username)))) + (inactive-from? username *months-inactive-allowed* + (if last-post + #'(lambda () last-post) + #'(lambda () creation))))) + +(defun inactive-from-last-seen? (username) + (let* ((u (get-account username)) + (last-seen (account-seen u)) + (creation (account-creation u))) + (inactive-from? username *months-inactive-allowed* + (if last-seen + #'(lambda () last-seen) + #'(lambda () creation))))) + +(defun inactive-from? (username months timestamp-source) + (timestamp< + (timestamp+ + (universal-to-timestamp + (funcall timestamp-source)) months :month) + (now))) + +(defun ever-logged-in? (username) + (account-seen (get-account username))) + +(defun never-logged-in? (username) + (not (ever-logged-in? username))) + +(defun list-inactive-users () + (loop for u in *accounts* do + (format t "Username ~a is inactive? ~a~%" + (account-username u) + (user-inactive? (account-username u))))) +@ %def list-inactive-users + +\section{A special-purpose language to ease writing}\label{sec:dsl} + +These macros make up a tiny language to ease the writing of \lp. For +example, when we need to access the group database, we use +[[in-groups]]. When a certain command demands authentication, we use +[[with-auth]]. + +<>= +(defmacro in-dir (dir &rest body) + `(let ((*default-pathname-defaults* (truename ,dir))) + (uiop:with-current-directory (,dir) + ,@body))) + +(defmacro in-groups (&rest body) `(in-dir "groups/" ,@body)) + +(defun in-group-lambda (g fn) (in-dir g (funcall fn))) + +(defmacro in-group (g &rest body) + `(in-group-lambda ,(fmt "groups/~a/" g) (lambda () ,@body))) + +(defmacro with-group (g r &rest body) + (let ((g-var (gensym)) + (r-var (gensym))) + `(let ((,g-var ,g) + (,r-var ,r)) + (if (not (group? ,g-var)) + (make-response :code 411 :request ,r-var + :data (format nil "no such group ``~a''" ,g-var)) + (progn ,@body))))) + +(defmacro with-n-args (n r &rest body) + (let ((args-var (gensym)) + (message-var (gensym)) + (n-var n)) + `(let ((,args-var (request-args r)) + (,message-var ,(fmt "bad arguments: needs exactly ~a" n-var))) + (if (not (= ,n-var (length ,args-var))) + (make-response :code 400 :request ,r :data ,message-var) + (progn ,@body))))) + +(defmacro with-group-set (&rest body) + (let ((g-var (gensym))) + `(let ((,g-var (client-group *client*))) + (if (not ,g-var) + (bad-input r "must say GROUP first") + ,@body)))) + +(defmacro with-auth (&rest body) + `(if (not (auth?)) + (make-response :code 400 :data "You must authenticate first.") + (progn ,@body))) + +@ %def in-groups with-group with-n-args with-group-set with-auth + +\section{Other procedures} + +Of small importance, they have nothing. Notice that [[ucs-2->ascii]] +is iseful only in Windows systems---and just for development. The +procedure destructively converts UCS-2 to ASCII, so it's only really +useful when we're converting an implicitly ASCII-content in the form +of UCS-2. Despite the name UCS-2, notice it is UTF-16. The name UCS +stands for ``Universal Character Set'' and I speculate the number 2 +means 2 bytes. So our conversion is just removing the first byte. + +<>= +(defun plural (v suffix) + (if (> v 1) "s" "")) + +(defun debug? () nil) + +(eval-when (:compile-toplevel :load-toplevel :execute) + (defun fmt (cstr &rest args) + (apply #'format nil (list* cstr args)))) + +(defun stderr (&rest args) + (when (debug?) + (apply #'format (cons *error-output* args)))) + +(defun enumerate (ls &optional (first-index 0)) + (loop for e in ls and i from first-index + collect (cons i e))) + +(defun ucs-2->ascii (bs) + ;; I'm a Windows user. + #-win32 bs #+win32 (remove-if #'zerop bs)) + +(defun bad-input (r msg &key code) + (make-response :code (or code 400) :data msg :request r)) + +(defun integer->string (n) + (format nil "~a" n)) + +(defun mkstr (&rest args) ;; a utility + (with-output-to-string (s) + (dolist (a args) (princ a s)))) + +(defun data (&rest args) ;; a utility + (flatten (map 'list #'data->bytes args))) + +(defun crlf () + (vector 13 10)) + +(defun crlf-string () + (format nil "~c~c" #\return #\linefeed)) + +(defun flatten (obj) + (do* ((result (list obj)) + (node result)) + ((null node) (delete nil result)) + (cond ((consp (car node)) + (when (cdar node) (push (cdar node) (cdr node))) + (setf (car node) (caar node))) + (t (setf node (cdr node)))))) + +(defmacro mac (&rest body) + `(macroexpand-1 ,@body)) +@ %def bad-input crlf mkstr data crlf-string flatten ucs-2->ascii enumerate + +\section{Tests} + +I studied the minimum to be able to add these tests as we comprehend +better the direction in which we're going. A test system is essential +for us to trust we can move forward without breaking past decisions in +the code. + +<>= +(setq lisp-unit:*print-failures* t) +(define-test first-test-of-the-west + (assert-equal 0 0)) + +(define-test requests + (let ((nil-request-1 (make-request)) + (nil-request-2 (make-request :said " "))) + (assert-true (request=? nil-request-1 (parse-request nil-request-1))) + (assert-true (request=? nil-request-2 (parse-request nil-request-2))) + (assert-true (request=? nil-request-1 nil-request-2)))) + +(define-test commands + (let ((ht (make-hash-table)) + (c1 (make-command :fn #'cons :verb 'c1 :description "cons cmd")) + (c2 (make-command :fn #'list :verb 'c2 :description "list cmd"))))) + +(define-test dispatching + (assert-true (equalp (empty-response) (dispatch (empty-request))))) +@ %def + +\section{How to produce the binary executable} + +Just say {\tt make exe} to your shell. + +<>= +(load "~/.sbclrc") +(ql:quickload :loop :silent t) +(sb-ext:save-lisp-and-die #P"loop.exe" + :toplevel #'loop:main + :executable t) +@ + +\section{How to update the remote server}\label{sec:live} + +We automate here the process of updating and compilation of a new +version of \lp. It's certain that what we document here is specific +to a single UNIX system, but what's important is that you (dear +reader) see exactly what must be done to go live with the system. + +The system is composed of the Lisp package [[loop]]. +The first thing to do is copy the files of each package to their +destinations in the remote server. The system depends on +[[quicklisp]] and we use the directory called [[local-projects]] as +the repository of our packages. So we just need ask {\tt ssh} to copy +the files. We begin with [[make]] to extract all source files from +[[loop.nw]], which is the master source code of \lp. +% +\begin{verbatim} +%scp loop.asd loop.lisp me@remote:quicklisp/local-projects/loop +loop.asd | 0 kB | 0.2 kB/s | ETA: 00:00:00 | 100% +loop.lisp | 37 kB | 37.5 kB/s | ETA: 00:00:00 | 100% +\end{verbatim} + +Files copied. Now it's time to produce the executabler from the newly +installed source code. To produce the executable, we run +[[build-exe.lisp]]. I'm going to demonstrate how to run this from my +own development machine. Since I'm running Windows, I use [[plink]] +and not [[ssh]]. +% +\begin{verbatim} +%scp build-exe.lisp me@remote:loop/ +build-exe.lisp | 0 kB | 0.2 kB/s | ETA: 00:00:00 | 100% + +%plink -ssh me@remote cd loop/ && sbcl --script loop/build-exe.lisp \ + echo "Executable built." +\end{verbatim} + +Produce the executable is sufficient because we're using Daniel +J. Bernstein's [[tcpserver]]. After replacing the executable in the +file system, new TCP connections will invoke the new executable while +older connections still alive will keep using the older executable +already loaded in memory. There's nothing to restart, in other +words. + +The target [[live]] in the [[Makefile]] automates the steps that we +have just described. Have a look at the [[Makefile]], which is not +included here in this literate document. With this automation, we +update the remote system with: +% +\begin{verbatim} +%make live +scp loop.asd loop.lisp \ + dbastos@antartida.xyz:quicklisp/local-projects/loop +loop.asd | 0 kB | 0.2 kB/s | ETA: 00:00:00 | 100% +loop.lisp | 37 kB | 37.5 kB/s | ETA: 00:00:00 | 100% +scp build-exe.lisp \ + dbastos@antartida.xyz:loop/ +build-exe.lisp | 0 kB | 0.2 kB/s | ETA: 00:00:00 | 100% +plink -ssh dbastos@antartida.xyz cd loop/ && \ + sbcl --script build-exe.lisp && \ + echo "Executable built." +Executable built. +\end{verbatim} + +Yes, we could parameterize the command with the address of the remote +server and remote path to the installation. But perhaps we will +always be the ones using this system, so we will delay this task until +further notice. %% TODO + +\section{The package {\tt loop.lisp} as the compiler needs it} + +We now put together all source code chunks in the order the compiler +needs to read it. By the way, you see this call to +[[enable-sql-reader-syntax]]? We need it at the top-level of any file +that uses the SQL syntax from [[clsql]]. You can see an illustration +of the syntax in, for example, [[lookup-index]]. + +One thing to keep in mind here is---I wonder if people that might read +this source code would read the literate programming \LaTeX\ output or +would they read [[loop.lisp]] directly. For literate programmers, it +doesn't matter how [[loop.lisp]] turns out because only the compiler +reads [[loop.lisp]]. But if we care about anyone who might read +[[loop.lisp]], then we should perhaps tell our literate programming +tools to generate a nice-looking file. For instance, I declare global +variables in the chunks where it's used. But for someone reading +[[loop.lisp]] directly, it is perhaps better if they would see all +global variables at the top of the file. That's something to think +about. + +<>= +;;; -*- mode: LISP; syntax: COMMON-LISP; package: LOOP; -*- +(eval-when (:compile-toplevel :load-toplevel :execute) + (ql:quickload + '(:lisp-unit + :str + :uiop + :cl-fad + :cl-ppcre + :local-time + :iterate + :clsql-sqlite3) + :silent t)) + +(clsql:enable-sql-reader-syntax) + +(defpackage #:loop + (:use :common-lisp :local-time) + (:import-from :lisp-unit define-test) + (:import-from :iterate iter) + (:export :main)) + +(in-package #:loop) + +<> +<> +<> +<> +<> +<> +<> +<> +<> +<
> +<> +<
> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +<> +@ %def + +<<*>>= +<> +<> +<> +<> +@ + +<>= +;;; -*- Mode: LISP; Syntax: COMMON-LISP; -*- +(asdf:defsystem :loop + :version "0.0.1" + :description "An NNTP server for a group of friends." + :depends-on (:lisp-unit :str :uiop :cl-fad :cl-ppcre) + :components ((:file "loop"))) +@ %def :loop + +\section{The UNIX service} + +We use the {\tt tcpserver} program by Daniel J. Bernstein from the +package \href{https://cr.yp.to/ucspi-tcp.html}{\tt ucspi-tcp}. + +<>= +/home/dbastos/loop +@ %def + +<>= +119 +@ %def + +<>= +#!/bin/sh +echo loop +cd <> +exec <>/tcpserver -HR 0.0.0.0 <> <>/loop.exe +@ %def + +How should you update the server if you modified the source-code? See +Section~\ref{sec:live}. + +\section{The writing process} + +The program {\tt latexmk} is iseful when I'm writing \LaTeX\ in +general, but to get the attention of {\tt latexmk} we need to rewrite +{\tt loop.tex}. So what I do while writing \lp\ is to have a +program---called \href{https://github.com/sjl/peat}{[[peat]]} by Steve +Losh---monitor the NOWEB source code {\tt loop.nw} effectively +invoking [[latexmk]] whenever {\tt loop.nw} is modified. Have a look +at the target [[livedoc]] in the [[Makefile]]. + +\section{Why isn't {\tt Makefile} in {\tt loop.nw}} + +I don't include {\tt Makefile} in the literate source code because I +use [[make]] to drive the literate programming tools. It is true that +we could include the {\tt Makefile}, then run {\tt noweb} once to +extract the {\tt Makefile} from {\tt loop.nw} and then use [[make]] +after that. However, I prefer to build a package that's totally +independent from the literate programming tools because, more often +than not, literate programming tools are usually unavailable in the +typical UNIX system out there. This way, the package we offer the +public can be considered a typical UNIX source code package and +programmers need only worry about literate programming tools if they +decide to modify the source code. + +The way I particularly run {\tt noweb} is always by asking for +specific chunks to be extracted. So the command line I'd usually +write is, for example, +% +\begin{verbatim} +build-exe.lisp: loop.nw + (any tangle -Rbuild-exe.lisp < loop.nw > build-exe.tmp || \ + (rm build-exe.tmp && exit 1)) && \ + mv build-exe.tmp build-exe.lisp +\end{verbatim} +% +In other words, I dump the chunk into a temporary file so that I don't +destroy the previous version of the source code unless the extraction +produces no error. This is too long of a command line and should be +issued by [[make]] itself. + +\section*{Index of chunks} +\nowebchunks + +\section*{Index of names} +\nowebindex + +\end{document} diff --git a/noweb.sty b/noweb.sty new file mode 100644 index 0000000..1ea980b --- /dev/null +++ b/noweb.sty @@ -0,0 +1,989 @@ +% noweb.sty -- LaTeX support for noweb +% DON'T read or edit this file! Use ...noweb-source/tex/support.nw instead. +{\obeyspaces\AtBeginDocument{\global\let =\ }} % from texbook, p 381 +\def\nwopt@nomargintag{\let\nwmargintag=\@gobble} +\def\nwopt@margintag{% + \def\nwmargintag##1{\leavevmode\llap{##1\kern\nwmarginglue\kern\codemargin}}} +\def\nwopt@margintag{% + \def\nwmargintag##1{\leavevmode\kern-\codemargin\nwthemargintag{##1}\kern\codemargin}} +\def\nwthemargintag#1{\llap{#1\kern\nwmarginglue}} +\nwopt@margintag +\newdimen\nwmarginglue +\nwmarginglue=0.3in +\def\nwtagstyle{\footnotesize\Rm} +% make \hsize in code sufficient for 88 columns +\setbox0=\hbox{\tt m} +\newdimen\codehsize +\codehsize=91\wd0 % 88 columns wasn't enough; I don't know why +\newdimen\codemargin +\codemargin=0pt +\newdimen\nwdefspace +\nwdefspace=\codehsize +% need to use \textwidth in {\LaTeX} to handle styles with +% non-standard margins (David Bruce). Don't know why we sometimes +% wanted \hsize. 27 August 1997. +%% \advance\nwdefspace by -\hsize\relax +\ifx\textwidth\undefined + \advance\nwdefspace by -\hsize\relax +\else + \advance\nwdefspace by -\textwidth\relax +\fi +\chardef\other=12 +\def\setupcode{% + \chardef\\=`\\ + \chardef\{=`\{ + \chardef\}=`\} + \catcode`\$=\other + \catcode`\&=\other + \catcode`\#=\other + \catcode`\%=\other + \catcode`\~=\other + \catcode`\_=\other + \catcode`\^=\other + \catcode`\"=\other % fixes problem with german.sty + \obeyspaces\Tt +} +%\let\nwlbrace=\{ +%\let\nwrbrace=\} +\def\nwendquote{\relax\ifhmode\spacefactor=1000 \fi} +{\catcode`\^^M=\active % make CR an active character + \gdef\newlines{\catcode`\^^M=\active % make CR an active character + \def^^M{\par\startline}}% + \gdef\eatline#1^^M{\relax}% +} +%%% DON'T \gdef^^M{\par\startline}}% in case ^^M appears in a \write +\def\startline{\noindent\hskip\parindent\ignorespaces} +\def\nwnewline{\ifvmode\else\hfil\break\leavevmode\hbox{}\fi} +\def\setupmodname{% + \catcode`\$=3 + \catcode`\&=4 + \catcode`\#=6 + \catcode`\%=14 + \catcode`\~=13 + \catcode`\_=8 + \catcode`\^=7 + \catcode`\ =10 + \catcode`\^^M=5 + \let\nwlbrace\lbrace + \let\nwrbrace\rbrace + \let\{\nwlbrace + \let\}\nwrbrace + % bad news --- don't know what catcode to give " + \Rm} +\def\LA{\begingroup\maybehbox\bgroup\setupmodname\It$\langle$} +\def\RA{\/$\rangle$\egroup\endgroup} +\def\code{\leavevmode\begingroup\setupcode\newlines} +\def\edoc{\endgroup} +\let\maybehbox\relax +\newbox\equivbox +\setbox\equivbox=\hbox{$\equiv$} +\newbox\plusequivbox +\setbox\plusequivbox=\hbox{$\mathord{+}\mathord{\equiv}$} +% \moddef can't have an argument because there might be \code...\edoc +\def\moddef{\leavevmode\kern-\codemargin\LA} +\def\endmoddef{\RA\ifmmode\equiv\else\unhcopy\equivbox\fi + \nobreak\hfill\nobreak} +\def\plusendmoddef{\RA\ifmmode\mathord{+}\mathord{\equiv}\else\unhcopy\plusequivbox\fi + \nobreak\hfill\nobreak} +\def\chunklist{% +\errhelp{I changed \chunklist to \nowebchunks. +I'll try to avoid such incompatible changes in the future.}% +\errmessage{Use \string\nowebchunks\space instead of \string\chunklist}} +\def\nowebchunks{\message{}} +\def\nowebindex{\message{}} +% here is support for the new-style (capitalized) font-changing commands +% thanks to Dave Love +\ifx\documentstyle\undefined + \let\Rm=\rm \let\It=\it \let\Tt=\tt % plain +\else\ifx\selectfont\undefined + \let\Rm=\rm \let\It=\it \let\Tt=\tt % LaTeX OFSS +\else % LaTeX NFSS + \def\Rm{\reset@font\rm} + \def\It{\reset@font\it} + \def\Tt{\reset@font\tt} + \def\Bf{\reset@font\bf} +\fi\fi +\ifx\reset@font\undefined \let\reset@font=\relax \fi +\def\noweboptions#1{% + \def\@nwoptionlist{#1}% + \@for\@nwoption:=\@nwoptionlist\do{% + \@ifundefined{nwopt@\@nwoption}{% + \@latexerr{There is no such noweb option as '\@nwoption'}\@eha}{% + \csname nwopt@\@nwoption\endcsname}}} +\codemargin=10pt +\advance\codehsize by \codemargin % make room for indentation of code +\advance\nwdefspace by \codemargin % and fix adjustment for def/use +\def\setcodemargin#1{% + \advance\codehsize by -\codemargin % make room for indentation of code + \advance\nwdefspace by -\codemargin % and fix adjustment for def/use + \codemargin=#1 + \advance\codehsize by \codemargin % make room for indentation of code + \advance\nwdefspace by \codemargin % and fix adjustment for + % def/use +} +\def\nwopt@shift{% + \dimen@=-0.8in + \if@twoside % Values for two-sided printing: + \advance\evensidemargin by \dimen@ + \else % Values for one-sided printing: + \advance\evensidemargin by \dimen@ + \advance\oddsidemargin by \dimen@ + \fi +% \advance \marginparwidth -\dimen@ +} +\let\nwopt@noshift\@empty +\def\nwbegincode#1{% + \begingroup + \topsep \nwcodetopsep + \@beginparpenalty \@highpenalty + \@endparpenalty -\@highpenalty + \@begincode } +\def\nwendcode{\endtrivlist \endgroup \filbreak} % keeps code on 1 page + +\newenvironment{webcode}{% + \@begincode +}{% + \endtrivlist} +\def\@begincode{% + \trivlist \item[]% + \leftskip\@totalleftmargin \advance\leftskip\codemargin + \rightskip\hsize \advance\rightskip -\codehsize + \parskip\z@ \parindent\z@ \parfillskip\@flushglue + \linewidth\codehsize + \@@par + \def\par{\leavevmode\null \@@par \penalty\nwcodepenalty}% + \obeylines + \@noligs \ifx\verbatim@nolig@list\undefined\else + \let\do=\nw@makeother \verbatim@nolig@list \do@noligs\` + \fi + \setupcode \frenchspacing \@vobeyspaces + \nowebsize \setupcode + \let\maybehbox\mbox } + \newskip\nwcodetopsep \nwcodetopsep = 3pt plus 1.2pt minus 1pt + \let\nowebsize=\normalsize + \def\nwopt@tinycode{\let\nowebsize=\tiny} + \def\nwopt@footnotesizecode{\let\nowebsize=\footnotesize} + \def\nwopt@scriptsizecode{\let\nowebsize=\scriptsize} + \def\nwopt@smallcode{\let\nowebsize=\small} + \def\nwopt@normalsizecode{\let\nowebsize=\normalsize} + \def\nwopt@largecode{\let\nowebsize=\large} + \def\nwopt@Largecode{\let\nowebsize=\Large} + \def\nwopt@LARGEcode{\let\nowebsize=\LARGE} + \def\nwopt@hugecode{\let\nowebsize=\huge} + \def\nwopt@Hugecode{\let\nowebsize=\Huge} +\newcount\nwcodepenalty \nwcodepenalty=\@highpenalty +\def\nw@makeother#1{\catcode`#1=12 } +\def\nwbegindocs#1{\ifvmode\noindent\fi} +\let\nwenddocs=\relax +\let\nwdocspar=\filbreak +\def\@nwsemifilbreak#1{\vskip0pt plus#1\penalty-200\vskip0pt plus -#1} +\newdimen\nwbreakcodespace +\nwbreakcodespace=0.2in % by default, leave no more than this on a page +\def\nwopt@breakcode{% + \def\nwdocspar{\@nwsemifilbreak{0.2in}}% + \def\nwendcode{\endtrivlist\endgroup} % ditches filbreak +} +\raggedbottom +\def\code{\leavevmode\begingroup\setupcode\@vobeyspaces\obeylines} +\let\edoc=\endgroup +\newdimen\@original@textwidth +\def\ps@noweb{% + \@original@textwidth=\textwidth + \let\@mkboth\@gobbletwo + \def\@oddfoot{}\def\@evenfoot{}% No feet. + \if@twoside % If two-sided printing. + \def\@evenhead{\hbox to \@original@textwidth{% + \Rm \thepage\qquad{\Tt\leftmark}\hfil\today}}% Left heading. + \def\@oddhead{\hbox to \@original@textwidth{% + \Rm \today\hfil{\Tt\leftmark}\qquad\thepage}}% Right heading. + \else % If one-sided printing. + \def\@oddhead{\hbox to \@original@textwidth{% + \Rm \today\hfil{\Tt\leftmark}\qquad\thepage}}% Right heading. + \let\@evenhead\@oddhead + \fi + \let\chaptermark\@gobble + \let\sectionmark\@gobble + \let\subsectionmark\@gobble + \let\subsubsectionmark\@gobble + \let\paragraphmark\@gobble + \let\subparagraphmark\@gobble + \def\nwfilename{\begingroup\let\do\@makeother\dospecials + \catcode`\{=1 \catcode`\}=2 \nw@filename} + \def\nw@filename##1{\endgroup\markboth{##1}{##1}\let\nw@filename=\nw@laterfilename}% +} +\def\nw@laterfilename#1{\endgroup\clearpage \markboth{#1}{#1}} +\let\nwfilename=\@gobble +\def\nwcodecomment#1{\@@par\penalty\nwcodepenalty + \if@firstnwcodecomment + \vskip\nwcodecommentsep\penalty\nwcodepenalty\@firstnwcodecommentfalse + \fi% + \hspace{-\codemargin}{% + \rightskip=0pt plus1in + \interlinepenalty\nwcodepenalty + \let\\\relax\footnotesize\Rm #1\@@par\penalty\nwcodepenalty}} +\def\@nwalsodefined#1{\nwcodecomment{\@nwlangdepdef\ \nwpageprep\ \@pagesl{#1}.}} +\def\@nwused#1{\nwcodecomment{\@nwlangdepcud\ \nwpageprep\ \@pagesl{#1}.}} +\def\@nwnotused#1{\nwcodecomment{\@nwlangdeprtc.}} +\def\nwoutput#1{\nwcodecomment{\@nwlangdepcwf\ {\Tt \@stripstar#1*\stripped}.}} +\def\@stripstar#1*#2\stripped{#1} +\newcommand{\nwprevdefptr}[1]{% + \mbox{$\mathord{\triangleleft}\,\mathord{\mbox{\subpageref{#1}}}$}} +\newcommand{\nwnextdefptr}[1]{% + \mbox{$\mathord{\mbox{\subpageref{#1}}}\,\mathord{\triangleright}$}} + +\newcommand{\@nwprevnextdefs}[2]{% + {\nwtagstyle + \ifx\relax#1\else ~~\nwprevdefptr{#1}\fi + \ifx\relax#2\else ~~\nwnextdefptr{#2}\fi}} +\newcommand{\@nwusesondefline}[1]{{\nwtagstyle~~(\@pagenumsl{#1})}} +\newcommand{\@nwstartdeflinemarkup}{\nobreak\hskip 1.5em plus 1fill\nobreak} +\newcommand{\@nwenddeflinemarkup}{\nobreak\hskip \nwdefspace minus\nwdefspace\nobreak} +\def\nwopt@longxref{% + \let\nwalsodefined\@nwalsodefined + \let\nwused\@nwused + \let\nwnotused\@nwnotused + \let\nwprevnextdefs\@gobbletwo + \let\nwusesondefline\@gobble + \let\nwstartdeflinemarkup\relax + \let\nwenddeflinemarkup\relax +} +\def\nwopt@shortxref{% + \let\nwalsodefined\@gobble + \let\nwused\@gobble + \let\nwnotused\@gobble + \let\nwprevnextdefs\@nwprevnextdefs + \let\nwusesondefline\@nwusesondefline + \let\nwstartdeflinemarkup\@nwstartdeflinemarkup + \let\nwenddeflinemarkup\@nwenddeflinemarkup +} +\def\nwopt@noxref{% + \let\nwalsodefined\@gobble + \let\nwused\@gobble + \let\nwnotused\@gobble + \let\nwprevnextdefs\@gobbletwo + \let\nwusesondefline\@gobble + \let\nwstartdeflinemarkup\relax + \let\nwenddeflinemarkup\relax +} +\nwopt@shortxref % to hell with backward compatibility! +\newskip\nwcodecommentsep \nwcodecommentsep=3pt plus 1pt minus 1pt +\newif\if@firstnwcodecomment\@firstnwcodecommenttrue +\newcount\@nwlopage\newcount\@nwhipage % range lo..hi-1 +\newcount\@nwlosub % subpage of lo +\newcount\@nwhisub % subpage of hi +\def\@nwfirstpage#1#2#3{% subpage page xref-tag + \@nwlopage=#2 \@nwlosub=#1 + \def\@nwloxreftag{#3}% + \advance\@nwpagecount by \@ne + \@nwhipage=\@nwlopage\advance\@nwhipage by \@ne } +\def\@nwnextpage#1#2#3{% subpage page xref-tag + \ifnum\@nwhipage=#2 + \advance\@nwhipage by \@ne + \advance\@nwpagecount by \@ne + \@nwhisub=#1 + \def\@nwhixreftag{#3}\else + \ifnum#2<\@nwlopage \advance\@nwhipage by \m@ne + \ifnum\@nwhipage=\@nwlopage + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}}% + \else + \count@=\@nwhipage \advance\count@ by \m@ne + \ifnum\count@=\@nwlopage % consecutive pages + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}% + \noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwhisub}{\number\@nwhipage}} + {\@nwhixreftag}}}% + \else \ifnum\@nwlopage<110 \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 \multiply\count@ by 100 + \ifnum\count@=\@nwlopage \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 + \@nwpagetemp=\@nwhipage \divide\@nwpagetemp by 100 + \ifnum\count@=\@nwpagetemp % lo--least 2 digits of hi + \multiply\@nwpagetemp by 100 + \advance \@nwhipage by -\@nwpagetemp + \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \else \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \fi + \fi + \fi% + \fi + \fi% + \edef\@tempa{\noexpand\nwix@cons\noexpand\nw@pages{\@tempa}}\@tempa\@nwfirstpage{#1}{#2}{#3}\else + \ifnum#2>\@nwhipage \advance\@nwhipage by \m@ne + \ifnum\@nwhipage=\@nwlopage + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}}% + \else + \count@=\@nwhipage \advance\count@ by \m@ne + \ifnum\count@=\@nwlopage % consecutive pages + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}% + \noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwhisub}{\number\@nwhipage}} + {\@nwhixreftag}}}% + \else \ifnum\@nwlopage<110 \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 \multiply\count@ by 100 + \ifnum\count@=\@nwlopage \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 + \@nwpagetemp=\@nwhipage \divide\@nwpagetemp by 100 + \ifnum\count@=\@nwpagetemp % lo--least 2 digits of hi + \multiply\@nwpagetemp by 100 + \advance \@nwhipage by -\@nwpagetemp + \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \else \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \fi + \fi + \fi% + \fi + \fi% + \edef\@tempa{\noexpand\nwix@cons\noexpand\nw@pages{\@tempa}}\@tempa\@nwfirstpage{#1}{#2}{#3}\else + \@nwlosub=0 \@nwhisub=0 + \fi\fi\fi + } +\newcount\@nwpagetemp +\newcount\@nwpagecount +\def\@nwfirstpagel#1{% label + \@ifundefined{r@#1}{\@warning{Reference `#1' on page \thepage \space undefined}% + \nwix@cons\nw@pages{\\{\bf ??}}}{% + \edef\@tempa{\noexpand\@nwfirstpage\subpagepair{#1}{#1}}\@tempa}} +\def\@nwnextpagel#1{% label + \@ifundefined{r@#1}{\@warning{Reference `#1' on page \thepage \space undefined}% + \nwix@cons\nw@pages{\\{\bf ??}}}{% + \edef\@tempa{\noexpand\@nwnextpage\subpagepair{#1}{#1}}\@tempa}} +\def\@pagesl#1{% list of labels + \gdef\nw@pages{}\@nwpagecount=0 + \def\\##1{\@nwfirstpagel{##1}\let\\=\@nwnextpagel}#1% + \advance\@nwhipage by \m@ne + \ifnum\@nwhipage=\@nwlopage + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}}% + \else + \count@=\@nwhipage \advance\count@ by \m@ne + \ifnum\count@=\@nwlopage % consecutive pages + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}% + \noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwhisub}{\number\@nwhipage}} + {\@nwhixreftag}}}% + \else \ifnum\@nwlopage<110 \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 \multiply\count@ by 100 + \ifnum\count@=\@nwlopage \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 + \@nwpagetemp=\@nwhipage \divide\@nwpagetemp by 100 + \ifnum\count@=\@nwpagetemp % lo--least 2 digits of hi + \multiply\@nwpagetemp by 100 + \advance \@nwhipage by -\@nwpagetemp + \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \else \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \fi + \fi + \fi% + \fi + \fi% + \edef\@tempa{\noexpand\nwix@cons\noexpand\nw@pages{\@tempa}}\@tempa\def\\##1{\@nwhyperpagenum##1}% + \ifnum\@nwpagecount=1 \nwpageword \else \nwpagesword\fi~\commafy{\nw@pages}} +\def\@nwhyperpagenum#1#2{\nwhyperreference{#2}{#1}} + +\def\@pagenumsl#1{% list of labels -- doesn't include word `pages', commas, or `and' + \gdef\nw@pages{}\@nwpagecount=0 + \def\\##1{\@nwfirstpagel{##1}\let\\=\@nwnextpagel}#1% + \advance\@nwhipage by \m@ne + \ifnum\@nwhipage=\@nwlopage + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}}% + \else + \count@=\@nwhipage \advance\count@ by \m@ne + \ifnum\count@=\@nwlopage % consecutive pages + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}% + \noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwhisub}{\number\@nwhipage}} + {\@nwhixreftag}}}% + \else \ifnum\@nwlopage<110 \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 \multiply\count@ by 100 + \ifnum\count@=\@nwlopage \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 + \@nwpagetemp=\@nwhipage \divide\@nwpagetemp by 100 + \ifnum\count@=\@nwpagetemp % lo--least 2 digits of hi + \multiply\@nwpagetemp by 100 + \advance \@nwhipage by -\@nwpagetemp + \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \else \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \fi + \fi + \fi% + \fi + \fi% + \edef\@tempa{\noexpand\nwix@cons\noexpand\nw@pages{\@tempa}}\@tempa% + \def\\##1{\@nwhyperpagenum##1\let\\=\@nwpagenumslrest}\nw@pages} +\def\@nwpagenumslrest#1{~\@nwhyperpagenum#1} +\def\subpages#1{% list of {{subpage}{page}} + \gdef\nw@pages{}\@nwpagecount=0 + \def\\##1{\edef\@tempa{\noexpand\@nwfirstpage##1{}}\@tempa + \def\\####1{\edef\@tempa{\noexpand\@nwnextpage####1}\@tempa}}#1% + \advance\@nwhipage by \m@ne + \ifnum\@nwhipage=\@nwlopage + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}}% + \else + \count@=\@nwhipage \advance\count@ by \m@ne + \ifnum\count@=\@nwlopage % consecutive pages + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}% + \noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwhisub}{\number\@nwhipage}} + {\@nwhixreftag}}}% + \else \ifnum\@nwlopage<110 \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 \multiply\count@ by 100 + \ifnum\count@=\@nwlopage \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 + \@nwpagetemp=\@nwhipage \divide\@nwpagetemp by 100 + \ifnum\count@=\@nwpagetemp % lo--least 2 digits of hi + \multiply\@nwpagetemp by 100 + \advance \@nwhipage by -\@nwpagetemp + \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \else \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \fi + \fi + \fi% + \fi + \fi% + \edef\@tempa{\noexpand\nwix@cons\noexpand\nw@pages{\@tempa}}\@tempa\def\\##1{\@firstoftwo##1}% + \ifnum\@nwpagecount=1 \nwpageword \else \nwpagesword\fi~\commafy{\nw@pages}} +\def\@nwaddrange{\advance\@nwhipage by \m@ne + \ifnum\@nwhipage=\@nwlopage + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}}% + \else + \count@=\@nwhipage \advance\count@ by \m@ne + \ifnum\count@=\@nwlopage % consecutive pages + \edef\@tempa{\noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwlosub}{\number\@nwlopage}}% + {\@nwloxreftag}}% + \noexpand\noexpand\noexpand\\% + {{\nwthepagenum{\number\@nwhisub}{\number\@nwhipage}} + {\@nwhixreftag}}}% + \else \ifnum\@nwlopage<110 \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 \multiply\count@ by 100 + \ifnum\count@=\@nwlopage \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}\else + \count@=\@nwlopage \divide\count@ by 100 + \@nwpagetemp=\@nwhipage \divide\@nwpagetemp by 100 + \ifnum\count@=\@nwpagetemp % lo--least 2 digits of hi + \multiply\@nwpagetemp by 100 + \advance \@nwhipage by -\@nwpagetemp + \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \else \edef\@tempa{\noexpand\noexpand\noexpand\\{{\number\@nwlopage--\number\@nwhipage}{}}}% + \fi + \fi + \fi% + \fi + \fi% + \edef\@tempa{\noexpand\nwix@cons\noexpand\nw@pages{\@tempa}}\@tempa} +\def\nwpageword{\@nwlangdepchk} % chunk, was page +\def\nwpagesword{\@nwlangdepchks} % chunk, was page +\def\nwpageprep{\@nwlangdepin} % in, was on +\newcommand\nw@genericref[2]{% what to do, name of ref + \expandafter\nw@g@nericref\csname r@#2\endcsname#1{#2}} +\newcommand\nw@g@nericref[3]{% control sequence, what to do, name + \ifx#1\relax + \ref{#3}% trigger the standard `undefined ref' mechanisms + \else + \expandafter#2#1.\\% + \fi} +\def\nw@selectone#1#2#3\\{#1} +\def\nw@selecttwo#1#2#3\\{#2} +\def\nw@selectonetwo#1#2#3\\{{#1}{#2}} +\newcommand{\subpageref}[1]{% + \nwhyperreference{#1}{\nw@genericref\@subpageref{#1}}} +\def\@subpageref#1#2#3\\{% + \@ifundefined{2on#2}{#2}{\nwthepagenum{#1}{#2}}} +\newcommand{\subpagepair}[1]{% % produces {subpage}{page} + \@ifundefined{r@#1}% + {{0}{0}}% + {\nw@genericref\@subpagepair{#1}}} +\def\@subpagepair#1#2#3\\{% + \@ifundefined{2on#2}{{0}{#2}}{{#1}{#2}}} +\newcommand{\sublabel}[1]{% + \leavevmode % needed to make \@bsphack work + \@bsphack + \nwblindhyperanchor{#1}% + \if@filesw {\let\thepage\relax + \def\protect{\noexpand\noexpand\noexpand}% + \edef\@tempa{\write\@auxout{\string + \newsublabel{#1}{{}{\thepage}}}}% + \expandafter}\@tempa + \if@nobreak \ifvmode\nobreak\fi\fi\fi\@esphack} +\newcommand{\nosublabel}[1]{% + \@bsphack\if@filesw {\let\thepage\relax + \def\protect{\noexpand\noexpand\noexpand}% + \edef\@tempa{\write\@auxout{\string + \newlabel{#1}{{0}{\thepage}}}}% + \expandafter}\@tempa + \if@nobreak \ifvmode\nobreak\fi\fi\fi\@esphack} +\newcommand\newsublabel{% + \nw@settrailers + \global\let\newsublabel\@newsublabel + \@newsublabel} +\newcommand{\@newsublabel}[2]{% + \edef\this@page{\@cdr#2\@nil}% + \ifx\this@page\last@page\else + \sub@page=\z@ + \fi + \edef\last@page{\this@page} + \advance\sub@page by \@ne + \ifnum\sub@page=\tw@ + \global\@namedef{2on\this@page}{}% + \fi + \pendingsublabel{#1}% + \edef\@tempa##1{\noexpand\newlabel{##1}% + {{\number\sub@page}{\this@page}\nw@labeltrailers}}% + \pending@sublabels + \def\pending@sublabels{}} +\newcommand\nw@settrailers{% -- won't work on first run + \@ifpackageloaded{nameref}% + {\gdef\nw@labeltrailers{{}{}{}}}% + {\gdef\nw@labeltrailers{}}} +\renewcommand\nw@settrailers{% + \@ifundefined{@secondoffive}% + {\gdef\nw@labeltrailers{}}% + {\gdef\nw@labeltrailers{{}{}{}}}} +\newcommand{\nextchunklabel}[1]{% + \nwblindhyperanchor{#1}% % looks slightly bogus --- nr + \@bsphack\if@filesw {\let\thepage\relax + \edef\@tempa{\write\@auxout{\string\pendingsublabel{#1}}}% + \expandafter}\@tempa + \if@nobreak \ifvmode\nobreak\fi\fi\fi\@esphack} +\newcommand\pendingsublabel[1]{% + \def\@tempa{\noexpand\@tempa}% + \edef\pending@sublabels{\noexpand\@tempa{#1}\pending@sublabels}} +\def\pending@sublabels{} +\def\last@page{\relax} +\newcount\sub@page +\def\@alphasubpagenum#1#2{#2\ifnum#1=0 \else\@alph{#1}\fi} +\def\@nosubpagenum#1#2{#2} +\def\@numsubpagenum#1#2{#2\ifnum#1=0 \else.\@arabic{#1}\fi} +\def\nwopt@nosubpage{\let\nwthepagenum=\@nosubpagenum\nwopt@nomargintag} +\def\nwopt@numsubpage{\let\nwthepagenum=\@numsubpagenum} +\def\nwopt@alphasubpage{\let\nwthepagenum=\@alphasubpagenum} +\nwopt@alphasubpage +\newcount\@nwalph@n +\let\@nwalph@d\@tempcnta +\let\@nwalph@bound\@tempcntb +\def\@nwlongalph#1{{% + \@nwalph@n=#1\advance\@nwalph@n by-1 + \@nwalph@bound=26 + \loop\ifnum\@nwalph@n<\@nwalph@bound\else + \advance\@nwalph@n by -\@nwalph@bound + \multiply\@nwalph@bound by 26 + \repeat + \loop\ifnum\@nwalph@bound>1 + \divide\@nwalph@bound by 26 + \@nwalph@d=\@nwalph@n\divide\@nwalph@d by \@nwalph@bound + % d := d * bound ; n -:= d; d := d / bound --- saves a temporary + \multiply\@nwalph@d by \@nwalph@bound + \advance\@nwalph@n by -\@nwalph@d + \divide\@nwalph@d by \@nwalph@bound + \advance\@nwalph@d by 1 \@alph{\@nwalph@d}% + \repeat +}} +\newcount\nw@chunkcount +\nw@chunkcount=\@ne +\newcommand{\weblabel}[1]{% + \@bsphack + \nwblindhyperanchor{#1}% + \if@filesw {\let\thepage\relax + \def\protect{\noexpand\noexpand\noexpand}% + \edef\@tempa{\write\@auxout{\string + \newsublabel{#1}{{}{\number\nw@chunkcount}}}}% + \expandafter}\@tempa + \global\advance\nw@chunkcount by \@ne + \if@nobreak \ifvmode\nobreak\fi\fi\fi\@esphack} +\def\nwopt@webnumbering{% + \let\sublabel=\weblabel + \def\nwpageword{chunk}\def\nwpagesword{chunks}% + \def\nwpageprep{in}} +% \nwindexdefn{printable name}{identifying label}{label of chunk} +% \nwindexuse{printable name}{identifying label}{label of chunk} + +\def\nwindexdefn#1#2#3{\@auxix{\protect\nwixd}{#2}{#3}} +\def\nwindexuse#1#2#3{\@auxix{\protect\nwixu}{#2}{#3}} + +\def\@auxix#1#2#3{% {marker}{id label}{subpage label} + \@bsphack\if@filesw {\let\nwixd\relax\let\nwixu\relax + \def\protect{\noexpand\noexpand\noexpand}% + \edef\@tempa{\write\@auxout{\string\nwixadd{#1}{#2}{#3}}}% + \expandafter}\@tempa + \if@nobreak \ifvmode\nobreak\fi\fi\fi\@esphack} +% \nwixadd{marker}{idlabel}{subpage label} +\def\nwixadd#1#2#3{% + \@ifundefined{nwixl@#2}% + {\global\@namedef{nwixl@#2}{#1{#3}}}% + {\expandafter\nwix@cons\csname nwixl@#2\endcsname{#1{#3}}}} +\def\@nwsubscriptident#1#2{\mbox{$\mbox{#1}_{\mathrm{\subpageref{#2}}}$}} +\def\@nwnosubscriptident#1#2{#1} +\def\@nwhyperident#1#2{\leavevmode\nwhyperreference{#2}{#1}} +\def\nwopt@subscriptidents{% + \let\nwlinkedidentq\@nwsubscriptident + \let\nwlinkedidentc\@nwsubscriptident +} +\def\nwopt@nosubscriptidents{% + \let\nwlinkedidentq\@nwnosubscriptident + \let\nwlinkedidentc\@nwnosubscriptident +} +\def\nwopt@hyperidents{% + \let\nwlinkedidentq\@nwhyperident + \let\nwlinkedidentc\@nwhyperident +} +\def\nwopt@nohyperidents{% + \let\nwlinkedidentq\@nwnosubscriptident + \let\nwlinkedidentc\@nwnosubscriptident +} +\def\nwopt@subscriptquotedidents{% + \let\nwlinkedidentq\@nwsubscriptident +} +\def\nwopt@nosubscriptquotedidents{% + \let\nwlinkedidentq\@nwnosubscriptident +} +\def\nwopt@hyperquotedidents{% + \let\nwlinkedidentq\@nwhyperident +} +\def\nwopt@nohyperquotedidents{% + \let\nwlinkedidentq\@nwnosubscriptident +} +\nwopt@hyperidents +\newcount\@commacount +\def\commafy#1{% + {\nwix@listcount{#1}\@commacount=\nwix@counter + \let\@comma@each=\\% + \ifcase\@commacount\let\\=\@comma@each\or\let\\=\@comma@each\or + \def\\{\def\\{ \@nwlangdepand\ \@comma@each}\@comma@each}\else + \def\\{\def\\{, % + \advance\@commacount by \m@ne + \ifnum\@commacount=1 \@nwlangdepand~\fi\@comma@each}\@comma@each}\fi + #1}} +\def\nwix@cons#1#2{% {list}{\marker{element}} + {\toks0=\expandafter{#1}\def\@tempa{#2}\toks2=\expandafter{\@tempa}% + \xdef#1{\the\toks0 \the\toks2 }}} +\def\nwix@uses#1{% {label} + \def\nwixu{\\}\let\nwixd\@gobble\@nameuse{nwixl@#1}} +\def\nwix@defs#1{% {label} + \def\nwixd{\\}\let\nwixu\@gobble\@nameuse{nwixl@#1}} +\newcount\nwix@counter +\def\nwix@listcount#1{% {list with \\} + {\count@=0 + \def\\##1{\advance\count@ by \@ne }% + #1\global\nwix@counter=\count@ }} +\def\nwix@usecount#1{\nwix@listcount{\nwix@uses{#1}}} +\def\nwix@defcount#1{\nwix@listcount{\nwix@defs{#1}}} +\def\nwix@id@defs#1{% index pair + {{\Tt \@car#1\@nil}% + \def\\##1{\nwix@defs@space\subpageref{##1}}\nwix@defs{\@cdr#1\@nil}}} + % useful above to change ~ into something that can break +% this option is undocumented because I think breakdefs is always right +\def\nwopt@breakdefs{\def\nwix@defs@space{\penalty200\ }} +\def\nwopt@nobreakdefs{\def\nwix@defs@space{~}} % old code +\nwopt@breakdefs +\def\nwidentuses#1{% list of index pairs + \nwcodecomment{\@nwlangdepuss\ \let\\=\nwix@id@defs\commafy{#1}.}} +\def\nwix@totaluses#1{% list of index pairs + {\count@=0 + \def\\##1{\nwix@usecount{\@cdr##1\@nil}\advance\count@ by\nwix@counter}% + #1\global\nwix@counter\count@ }} +\def\nwix@id@uses#1#2{% {ident}{label} + \nwix@usecount{#2}\ifnum\nwix@counter>0 + {\advance\leftskip by \codemargin + \nwcodecomment{{\Tt #1}, \@nwlangdepusd\ \nwpageprep\ \@pagesl{\nwix@uses{#2}}.}}% + \else + \ifnw@hideunuseddefs\else + {\advance\leftskip by \codemargin \nwcodecomment{{\Tt #1}, \@nwlangdepnvu.}}% + \fi + \fi} +\def\nwidentdefs#1{% list of index pairs + \ifnw@hideunuseddefs\nwix@totaluses{#1}\else\nwix@listcount{#1}\fi + \ifnum\nwix@counter>0 + \nwcodecomment{\@nwlangdepdfs:}% + {\def\\##1{\nwix@id@uses ##1}#1}% + \fi} +\newif\ifnw@hideunuseddefs\nw@hideunuseddefsfalse +\def\nwopt@hideunuseddefs{\nw@hideunuseddefstrue} +\def\nwopt@noidentxref{% + \let\nwidentdefs\@gobble + \let\nwidentuses\@gobble} +\def\nw@underlinedefs{% {list with \nwixd, \nwixu} + \let\\=\relax\def\nw@comma{, } + \def\nwixd##1{\\\underline{\subpageref{##1}}\let\\\nw@comma}% + \def\nwixu##1{\\\subpageref{##1}\let\\\nw@comma}} + +\def\nw@indexline#1#2{% + {\indent {\Tt #1}: \nw@underlinedefs\@nameuse{nwixl@#2}\par}} + +\newenvironment{thenowebindex}{\parindent=-10pt \parskip=\z@ + \advance\leftskip by 10pt + \advance\rightskip by 0pt plus1in\par\@afterindenttrue + \def\\##1{\nw@indexline##1}}{} +\def\nowebindex{% + \@ifundefined{nwixs@i}% + {\@warning{The \string\nowebindex\space is empty}}% + {\begin{thenowebindex}\@nameuse{nwixs@i}\end{thenowebindex}}} +\def\nowebindex@external{% + {\let\nwixadds@c=\@gobble + \def\nwixadds@i##1{\nw@indexline##1}% + \def\nwixaddsx##1##2{\@nameuse{nwixadds@##1}{##2}}% + \begin{thenowebindex}\@input{\jobname.nwi}\end{thenowebindex}}} +\def\nwixlogsorted#1#2{% list data + \@bsphack\if@filesw + \toks0={#2}\immediate\write\@auxout{\string\nwixadds{#1}{\the\toks0}} + \if@nobreak \ifvmode\nobreak\fi\fi\fi\@esphack} +\def\nwixadds#1#2{% + \@ifundefined{nwixs@#1}% + {\global\@namedef{nwixs@#1}{\\{#2}}}% + {\expandafter\nwix@cons\csname nwixs@#1\endcsname{\\{#2}}}} +\let\nwixaddsx=\@gobbletwo +\def\nwopt@externalindex{% + \ifx\nwixadds\@gobbletwo % already called + \else + \let\nwixaddsx=\nwixadds \let\nwixadds=\@gobbletwo + \let\nowebindex=\nowebindex@external + \let\nowebchunks=\nowebchunks@external + \fi} +\def\nowebchunks{% + \@ifundefined{nwixs@c}% + {\@warning{The are no \string\nowebchunks}}% + {\begin{thenowebchunks}\@nameuse{nwixs@c}\end{thenowebchunks}}} +\def\nowebchunks@external{% + {\let\nwixadds@i=\@gobble + \def\nwixadds@c##1{\nw@onechunk##1}% + \def\nwixaddsx##1##2{\@nameuse{nwixadds@##1}{##2}}% + \begin{thenowebchunks}\@input{\jobname.nwi}\end{thenowebchunks}}} + \@namedef{r@nw@notdef}{{0}{(\@nwlangdepnvd)}} +\def\nw@chunkunderlinedefs{% {list of labels with \nwixd, \nwixu} + \let\\=\relax\def\nw@comma{, } + \def\nwixd##1{\\\underline{\subpageref{##1}}\let\\\nw@comma}% + \def\nwixu##1{\\\subpageref{##1}\let\\\nw@comma}} +\def\nw@onechunk#1#2#3{% {name}{label of first definition}{list with \nwixd, \nwixu} + \@ifundefined{r@#2}{}{% + \indent\LA #1~{\nwtagstyle\subpageref{#2}}\RA + \if@nwlongchunks{~\nw@chunkunderlinedefs#3}\fi\par}} +\newenvironment{thenowebchunks}{\vskip3pt + \parskip=\z@\parindent=-10pt \advance\leftskip by 10pt + \advance\rightskip by 0pt plus10pt \@afterindenttrue + \def\\##1{\nw@onechunk##1}}{} +\newif\if@nwlongchunks +\@nwlongchunksfalse +\let\nwopt@longchunks\@nwlongchunkstrue +\newcommand\@nw@hyper@ref{\hyperreference} % naras +\newcommand\@nw@hyper@anc{\blindhyperanchor} % naras +\newcommand\@nw@hyperref@ref[2]{\hyperlink{noweb.#1}{#2}} % nr +\newcommand\@nw@hyperref@anc[1]{\hypertarget{noweb.#1}{\relax}} % nr +%%\renewcommand\@nw@hyperref@ref[2]{{#2}} % nr +%%\renewcommand\@nw@hyperref@anc[1]{} % nr +\newcommand\nwhyperreference{% + \@ifundefined{hyperlink} + {\@ifundefined{hyperreference} + {\global\let\nwhyperreference\@gobble} + {\global\let\nwhyperreference\@nw@hyper@ref}} + {\global\let\nwhyperreference\@nw@hyperref@ref}% + \nwhyperreference +} + +\newcommand\nwblindhyperanchor{% + \@ifundefined{hyperlink} + {\@ifundefined{hyperreference} + {\global\let\nwblindhyperanchor\@gobble} + {\global\let\nwblindhyperanchor\@nw@hyper@anc}} + {\global\let\nwblindhyperanchor\@nw@hyperref@anc}% + \nwblindhyperanchor +} +\newcommand\nwanchorto{% + \begingroup\let\do\@makeother\dospecials + \catcode`\{=1 \catcode`\}=2 \nw@anchorto} +\newcommand\nw@anchorto[1]{\endgroup\def\nw@next{#1}\nw@anchortofin} +\newcommand\nw@anchortofin[1]{#1\footnote{See URL \texttt{\nw@next}.}} +\let\nwanchorname\@gobble +\newif\ifhtml +\htmlfalse +\let\nwixident=\relax +\def\nwbackslash{\char92} +\def\nwlbrace{\char123} +\def\nwrbrace{\char125} +\def\nwopt@english{% + \def\@nwlangdepdef{This definition is continued}% + \def\@nwlangdepcud{This code is used}% + \def\@nwlangdeprtc{Root chunk (not used in this document)}% + \def\@nwlangdepcwf{This code is written to file}% + \def\@nwlangdepchk{chunk}% + \def\@nwlangdepchks{chunks}% + \def\@nwlangdepin{in}% + \def\@nwlangdepand{and}% + \def\@nwlangdepuss{Uses}% + \def\@nwlangdepusd{used}% + \def\@nwlangdepnvu{never used}% + \def\@nwlangdepdfs{Defines}% + \def\@nwlangdepnvd{never defined}% +} +\let\nwopt@american\nwopt@english +\def\nwopt@portuges{% + \def\@nwlangdepdef{Defini\c{c}\~ao continuada em}% + % This definition is continued + \def\@nwlangdepcud{C\'odigo usado em}% + % This code is used + \def\@nwlangdeprtc{Fragmento de topo (sem uso no documento)}% + % Root chunk (not used in this document) + \def\@nwlangdepcwf{Este c\'odigo foi escrito no ficheiro}% + % This code is written to file + \def\@nwlangdepchk{fragmento}% + % chunk + \def\@nwlangdepchks{fragmentos}% + % chunks + \def\@nwlangdepin{no(s)}% + % in + \def\@nwlangdepand{e}% + % and + \def\@nwlangdepuss{Usa}% + % Uses + \def\@nwlangdepusd{usado}% + % used + \def\@nwlangdepnvu{nunca usado}% + % never used + \def\@nwlangdepdfs{Define}% + % Defines + \def\@nwlangdepnvd{nunca definido}% + % never defined +} +\def\nwopt@brazil{% + \def\@nwlangdepdef{Defini\c{c}\~{a}o continua}% + % This definition is continued + \def\@nwlangdepcud{C\'odigo usado}% + % This code is used + \def\@nwlangdeprtc{Trecho raiz, isto é, este trecho come\c{c}a aqui}% + % Root chunk (not used in this document) + \def\@nwlangdepcwf{C\'odigo escrito no arquivo}% + % This code is written to file + \def\@nwlangdepchk{trecho}% + % chunk + \def\@nwlangdepchks{trechos}% + % chunks + \def\@nwlangdepin{@}% + % in + \def\@nwlangdepand{e}% + % and + \def\@nwlangdepuss{Usa}% + % Uses + \def\@nwlangdepusd{usado}% + % used + \def\@nwlangdepnvu{nunca usado}% + % never used + \def\@nwlangdepdfs{Define}% + % Defines + \def\@nwlangdepnvd{nunca definido}% + % never defined +} +\def\nwopt@frenchb{% + \def\@nwlangdepdef{Suite de la d\'efinition}% + % This definition is continued + \def\@nwlangdepcud{Ce code est employ\'e}% + % This code is used + \def\@nwlangdeprtc{Morceau racine (pas employ\'e dans ce document)}% + % Root chunk (not used in this document) + \def\@nwlangdepcwf{Ce code est \'ecrit dans le fichier}% + % This code is written to file + \def\@nwlangdepchk{le morceau}% + % chunk + \def\@nwlangdepchks{les morceaux}% + % chunks + \def\@nwlangdepin{dans}% + % in + \def\@nwlangdepand{et}% + % and + \def\@nwlangdepuss{Utilise}% + % Uses + \def\@nwlangdepusd{utilis\'{e}}% + % used + \def\@nwlangdepnvu{jamais employ\'{e}}% + % never used + \def\@nwlangdepdfs{D\'{e}finit}% + % Defines + % Cannot use the accent here: \def\@nwlangdepnvd{jamais d\'{e}fini}% + \def\@nwlangdepnvd{jamais defini}% + % never defined +} +\let\nwopt@french\nwopt@frenchb +\def\nwopt@german{% + \def\@nwlangdepdef{Diese Definition wird fortgesetzt}% + % This definition is continued + \def\@nwlangdepcud{Dieser Code wird benutzt}% + % This code is used + \def\@nwlangdeprtc{Hauptteil (nicht in diesem Dokument benutzt)}% + % Root chunk (not used in this document) + %\def\@nwlangdepcwf{Dieser Code schreibt man zum File}% + \def\@nwlangdepcwf{Dieser Code geht in Datei}% + % This code is written to file + \def\@nwlangdepchk{dem Teil}% + % chunk + %\def\@nwlangdepchks{Teils}% + \def\@nwlangdepchks{den Teilen}% + % chunks + \def\@nwlangdepin{in}% + % in + \def\@nwlangdepand{und}% + % and + \def\@nwlangdepuss{Benutzt}% + % Uses + \def\@nwlangdepusd{benutzt}% + % used + \def\@nwlangdepnvu{nicht benutzt}% + % never used + \def\@nwlangdepdfs{Definiert}% + % Defines + \def\@nwlangdepnvd{nicht definiert}% + % never defined +} +\def\nwopt@german{% + \def\@nwlangdepdef{Diese Definition wird fortgesetzt}% + % This definition is continued + \def\@nwlangdepcud{Dieser Code wird benutzt}% + % This code is used + \def\@nwlangdeprtc{Hauptteil (nicht in diesem Dokument benutzt)}% + % Root chunk (not used in this document) + \def\@nwlangdepcwf{Dieser Code schreibt man zum File}% + % This code is written to file + \def\@nwlangdepchk{Teil}% + % chunk + \def\@nwlangdepchks{Teils}% + % chunks + \def\@nwlangdepin{im}% + % in + \def\@nwlangdepand{und}% + % and + \def\@nwlangdepuss{Benutzt}% + % Uses + \def\@nwlangdepusd{benutzt}% + % used + \def\@nwlangdepnvu{nicht benutzt}% + % never used + \def\@nwlangdepdfs{Definiert}% + % Defines + \def\@nwlangdepnvd{nicht definiert}% + % never defined +} +\let\nwopt@ngerman\nwopt@german +\ifx\languagename\undefined % default is English + \noweboptions{english} +\else + \@ifundefined{nwopt@\languagename} + {\noweboptions{english}} + {\expandafter\noweboptions\expandafter{\languagename}} +\fi +\let\obeyedspace\@xobeysp diff --git a/peat b/peat new file mode 100644 index 0000000..5078c80 --- /dev/null +++ b/peat @@ -0,0 +1,224 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + + ############################## + # ____ ___ ____ ______ # + # | \ / _] / T| T # + # | o )/ [_ Y o || | # + # | _/Y _]| |l_j l_j # + # | | | [_ | _ | | | # + # | | | T| | | | | # + # l__j l_____jl__j__j l__j # + # # + ##### ##### + # Repeat commands! # + ################## + +import errno, os, subprocess, sys, time +from optparse import OptionParser + + +interval = 1.0 +command = 'true' +clear = True +get_paths = lambda: set() +verbose = True +dynamic = None +last_run = None + + +USAGE = r"""usage: %prog [options] COMMAND + +COMMAND should be given as a single argument using a shell string. + +A list of paths to watch should be piped in on standard input. + +For example: + + find . | peat './test.sh' + find . -name '*.py' | peat 'rm *.pyc' + find . -name '*.py' -print0 | peat -0 'rm *.pyc' + +If --dynamic is used, the given command will be run each time to generate the +list of files to check: + + peat --dynamic 'find .' './test.sh' + peat --dynamic 'find . -name '\''*.py'\''' 'rm *.pyc' +""" + + +def log(s): + if verbose: + print(s) + +def die(s): + sys.stderr.write('ERROR: ' + s + '\n') + sys.exit(1) + +def check(paths): + for p in paths: + try: + if os.stat(p).st_mtime >= last_run: + return True + except OSError as e: + # If the file has been deleted since we started watching, don't + # worry about it. + if e.errno == errno.ENOENT: + pass + else: + raise + return False + +def run(): + global last_run + last_run = time.time() + log("running: " + command) + subprocess.call(command, shell=True) + +def build_option_parser(): + p = OptionParser(USAGE) + + # Main options + p.add_option('-i', '--interval', default=None, + help='interval between checks in milliseconds', + metavar='N') + p.add_option('-I', '--smart-interval', dest='interval', + action='store_const', const=None, + help='determine the interval based on number of files watched (default)') + + p.add_option('-d', '--dynamic', default=None, + help='run COMMAND before each run to generate the list of files to check', + metavar='COMMAND') + p.add_option('-D', '--no-dynamic', dest='dynamic', + action='store_const', const=None, + help='take a list of files to watch on standard in (default)') + + p.add_option('-c', '--clear', default=True, + action='store_true', dest='clear', + help='clear screen before runs (default)') + p.add_option('-C', '--no-clear', + action='store_false', dest='clear', + help="don't clear screen before runs") + + p.add_option('-v', '--verbose', default=True, + action='store_true', dest='verbose', + help='show extra logging output (default)') + p.add_option('-q', '--quiet', + action='store_false', dest='verbose', + help="don't show extra logging output") + + p.add_option('-w', '--whitespace', default=None, + action='store_const', dest='sep', const=None, + help="assume paths are separated by whitespace (default)") + p.add_option('-n', '--newlines', + action='store_const', dest='sep', const='\n', + help="assume paths are separated by newlines") + p.add_option('-s', '--spaces', + action='store_const', dest='sep', const=' ', + help="assume paths are separated by spaces") + p.add_option('-0', '--zero', + action='store_const', dest='sep', const='\0', + help="assume paths are separated by null bytes") + + return p + + +def _main(): + if dynamic: + log("Running the following command to generate watch list:") + log(' ' + dynamic) + log('') + + log("Watching the following paths:") + for p in get_paths(): + log(' ' + p) + log('') + log('Checking for changes every %d milliseconds.' % int(interval * 1000)) + log('') + + run() + + while True: + time.sleep(interval) + if check(get_paths()): + if clear: + subprocess.check_call('clear') + run() + +def smart_interval(count): + """Return the smart interval to use in milliseconds.""" + if count >= 50: + return 1000 + else: + sq = lambda n: n * n + return int(1000 * (1 - (sq(50.0 - count) / sq(50)))) + +def _parse_interval(options): + global get_paths + if options.interval: + i = int(options.interval) + elif options.dynamic: + i = 1000 + else: + i = smart_interval(len(get_paths())) + + return i / 1000.0 + +def _parse_paths(sep, data): + if not sep: + paths = data.split() + else: + paths = data.split(sep) + + paths = [p.rstrip('\n') for p in paths if p] + paths = map(os.path.abspath, paths) + paths = set(paths) + + return paths + +def main(): + global interval, command, clear, get_paths, verbose, dynamic + + (options, args) = build_option_parser().parse_args() + + if len(args) != 1: + die("exactly one command must be given") + + command = args[0] + clear = options.clear + verbose = options.verbose + sep = options.sep + dynamic = options.dynamic + + if dynamic: + def _get_paths(): + data = subprocess.check_output(dynamic, shell=True) + return _parse_paths(sep, data) + + get_paths = _get_paths + else: + data = sys.stdin.read() + paths = _parse_paths(sep, data) + + if not paths: + die("no paths to watch were given on standard input") + + for path in paths: + if not os.path.exists(path): + die('path to watch does not exist: ' + repr(path)) + + get_paths = lambda: paths + + interval = _parse_interval(options) + + _main() + + +if __name__ == '__main__': + import signal + def sigint_handler(signal, frame): + sys.stdout.write('\n') + sys.exit(130) + signal.signal(signal.SIGINT, sigint_handler) + main() +