Add support for getting all commands with a given label

This allows treating IRC like a request-response protocol, which is much
easier to deal with.
This commit is contained in:
Val Lorentz 2023-09-15 19:00:50 +02:00
parent acd0559f34
commit f53e9de41e
2 changed files with 106 additions and 14 deletions

View File

@ -59,11 +59,13 @@
(.write writer "\r\n"))
(defn send-command [client command]
(info "send:" command)
(write-command (:writer client) command)
(.flush (:writer client)))
(defn send-commands [client commands]
(doseq [command commands]
(info "send:" command)
(write-command (:writer client) command))
(.flush (:writer client)))
@ -93,16 +95,60 @@
#"^(?:@([^ ]+) )?(?::([^ ]+) )?([^ ]+)(?: (.*?))??(?: :(.*))?$")
(defn parse-command [line]
(let [[_ tags source cmd params trailing] (re-matches line-re line)]
{:tags (if tags (parse-tags tags) {})
:source source
:cmd cmd
:params (let [params (if (empty? params)
[]
(str/split params #" "))]
(if trailing
(conj params trailing)
params))}))
(if line
(let [[_ tags source cmd params trailing] (re-matches line-re line)]
{:tags (if tags (parse-tags tags) {})
:source source
:cmd cmd
:params (let [params (if (empty? params)
[]
(str/split params #" "))]
(if trailing
(conj params trailing)
params))})
nil))
(defn read-response-batch [client label ref-tags acc]
(let [cmd (parse-command (.readLine (:reader client)))
[inner-ref-tag & outer-ref-tags] ref-tags]
(assert cmd "got empty line / end of stream within a batch")
(info "recv:" cmd)
(if (= (:cmd cmd) "BATCH")
(let [[ref-tag batch-type & _] (:params cmd)]
(case (first ref-tag)
; new (inner) batch, add its ref-tag to the stack
\+ (read-response-batch client label
(conj ref-tags (str/replace-first ref-tag "+" ""))
(conj acc cmd))
; closing batch
\- (if (= [(str "-" inner-ref-tag)] (:params cmd))
(if (empty? outer-ref-tags)
; end of outer-most batch, return commands
(conj acc cmd)
; end of inner-most batch, continue
(read-response-batch client label outer-ref-tags (conj acc cmd)))
; end of unrelated batch, ignore
(read-response-batch client label ref-tags acc))))
; note: this assumes outer batches don't get new messages while an inner batch is open.
(if (= (get (:tags cmd) "batch" nil) inner-ref-tag)
; add this command, continue
(read-response-batch client label ref-tags (conj acc cmd))
; command not in batch, ignore and continue
(read-response-batch client label ref-tags acc)))))
(defn read-response [client label]
(let [cmd (parse-command (.readLine (:reader client)))]
(assert cmd "got empty line / end of stream")
(info "recv:" cmd)
(if (= (get (:tags cmd) "label" nil) label)
(if (= (:cmd cmd) "BATCH")
(let [[ref-tag & _] (:params cmd)]
; read & return the batch
(read-response-batch client label (conj '() (str/replace-first ref-tag "+" "")) [cmd]))
[cmd]) ; return only this line
(read-response client label)))) ; ignore this line, skip to next
(defrecord Client [socket reader writer]
client/Client
@ -115,10 +161,13 @@
(send-commands this [{:cmd "CAP" :params ["LS" "302"]}
{:cmd "CAP" :params ["REQ" "labeled-response batch"]}
{:cmd "NICK" :params [(str "test-" node)]}
{:cmd "USER" :params ["test" "*" "*" "test"]}
{:cmd "CAP" :params ["END"]}
{:cmd "JOIN" :params ["#chan"]}
{:cmd "MODE" :params ["#chan" "-t"]}])
{:cmd "USER" :params [(str "test-" node) "*" "*" (str "test " node)]}
{:tags {"label" "cap-end"} :cmd "CAP" :params ["END"]}])
(read-response this "cap-end") ; block until end of registration
(send-command this {:tags {"label" "join-#chan"} :cmd "JOIN" :params ["#chan"]})
(read-response this "join-#chan") ; block until joined
(send-command this {:tags {"label" "mode-topic"} :cmd "MODE" :params ["#chan" "-t"]})
(read-response this "mode-topic") ; block until mode is set
this)))
(setup! [this test])

View File

@ -105,3 +105,46 @@
(testing "parsing a command with all fields"
(is (= (parse-command "@label=abc;time=123 :server. KICK #chan badperson :reason")
{:tags {"label" "abc", "time", "123"} :source "server." :cmd "KICK" :params ["#chan" "badperson" "reason"]}))))
(deftest read-response-test
(testing "reading single-line response"
(let [buf (java.io.BufferedReader. (java.io.StringReader. "@label=abc PONG\r\n"))]
(is (= (read-response {:reader buf} "abc")
[{:tags {"label" "abc"} :source nil :cmd "PONG" :params []}]))))
(testing "ignoring unlabelled line"
(let [buf (java.io.BufferedReader. (java.io.StringReader. "PONG\r\n@label=abc ACK\r\n"))]
(is (= (read-response {:reader buf} "abc")
[{:tags {"label" "abc"} :source nil :cmd "ACK" :params []}]))))
(testing "ignoring line with different label"
(let [buf (java.io.BufferedReader. (java.io.StringReader. "@label=other PONG\r\n@label=abc ACK\r\n"))]
(is (= (read-response {:reader buf} "abc")
[{:tags {"label" "abc"} :source nil :cmd "ACK" :params []}]))))
(testing "reading single-command response batch"
(let [buf (java.io.BufferedReader. (java.io.StringReader. "@label=abc BATCH +def labeled-response\r\n@batch=def PONG\r\nBATCH -def\r\n"))]
(is (= (read-response {:reader buf} "abc")
[{:tags {"label" "abc"} :source nil :cmd "BATCH" :params ["+def" "labeled-response"]}
{:tags {"batch" "def"} :source nil :cmd "PONG" :params []}
{:tags {} :source nil :cmd "BATCH" :params ["-def"]}]))))
(testing "reading multi-command response batch"
(let [buf (java.io.BufferedReader. (java.io.StringReader. "@label=abc BATCH +def labeled-response\r\n@batch=def PRIVMSG nick :msg 1\r\n@batch=def PRIVMSG nick :msg 2\r\nBATCH -def\r\n"))]
(is (= (read-response {:reader buf} "abc")
[{:tags {"label" "abc"} :source nil :cmd "BATCH" :params ["+def" "labeled-response"]}
{:tags {"batch" "def"} :source nil :cmd "PRIVMSG" :params ["nick" "msg 1"]}
{:tags {"batch" "def"} :source nil :cmd "PRIVMSG" :params ["nick" "msg 2"]}
{:tags {} :source nil :cmd "BATCH" :params ["-def"]}]))))
(testing "reading nested response batch"
(let [buf (java.io.BufferedReader. (java.io.StringReader. "@label=abc BATCH +def labeled-response\r\n@batch=def BATCH +ghi draft/multiline\r\n@batch=ghi PRIVMSG nick :msg 1\r\n@batch=ghi PRIVMSG nick :msg 2\r\n@batch=def BATCH -ghi\r\nBATCH -def\r\n"))]
(is (= (read-response {:reader buf} "abc")
[{:tags {"label" "abc"} :source nil :cmd "BATCH" :params ["+def" "labeled-response"]}
{:tags {"batch" "def"} :source nil :cmd "BATCH" :params ["+ghi" "draft/multiline"]}
{:tags {"batch" "ghi"} :source nil :cmd "PRIVMSG" :params ["nick" "msg 1"]}
{:tags {"batch" "ghi"} :source nil :cmd "PRIVMSG" :params ["nick" "msg 2"]}
{:tags {"batch" "def"} :source nil :cmd "BATCH" :params ["-ghi"]}
{:tags {} :source nil :cmd "BATCH" :params ["-def"]}]))))
)