nandi/frqpublic Fork 0
c0121fead9e4d8106da374c9f69746212f02914c
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

parse.cljc · 93 lines · 3.6 KBGDScript3 Blame HistoryRaw
Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago1(ns frq.irc.parse
2 "The IRC wire format, as text. No socket, no host, no platform.
3
4 Split out of `frq.irc` so both compilers can have it: the transport under it
5 is a blocking reader thread on the desktop and a `Stream` over
6 `SecureSocket` on the phone, and neither of those is this. Everything here
7 is a string going in and a map coming out, which is the half of IRC that is
8 the same everywhere.
9
10 `frq.irc` still re-exports these under its own name, so callers that say
11 `irc/tag-value` — and there are twenty-three of them in `frq.state` and
12 `frq.av` — did not have to move."
13 (:require [clojure.string :as str]))
14
15(defn parse-line
16 "An IRC line into {:tags :prefix :command :params}. The trailing parameter
17 (after \" :\") keeps its spaces; everything before it splits on whitespace.
18
19 IRCv3 tags come first when there are any. A connection that negotiates CAP
20 gets them where a bare one does not — which is why a client that ignores them
21 looks fine as a guest and goes silent once it authenticates."
22 [line]
23 (let [line (str/trimr line)
24 [tags line] (if (str/starts-with? line "@")
25 (let [i (str/index-of line " ")]
26 [(subs line 1 i) (str/triml (subs line i))])
27 [nil line])
28 [prefix rest-line] (if (str/starts-with? line ":")
29 (let [i (str/index-of line " ")]
30 [(subs line 1 i) (subs line (inc i))])
31 [nil line])
32 i (str/index-of rest-line " :")
33 head (if i (subs rest-line 0 i) rest-line)
34 trailing (when i (subs rest-line (+ i 2)))
35 parts (remove str/blank? (str/split head #" "))]
36 {:tags tags
37 :account (when tags
38 (second (re-find #"(?:^|;)account=([^;]*)" tags)))
39 :prefix prefix
40 :command (str/upper-case (or (first parts) ""))
41 :params (cond-> (vec (rest parts)) trailing (conj trailing))}))
42
43(defn unescape-tag
44 "An IRCv3 tag value with its escapes undone.
45
46 `\\:` is a semicolon, `\\s` a space, and `\\\\`, `\\r` and `\\n` themselves —
47 the escaping exists because `;` separates tags and a space ends them. It
48 matters for any value that can contain either: a reaction tally is
49 `emoji:nick;emoji:nick` on the wire and arrives with every one of those
50 semicolons written `\\:`, so a reader that skips this step sees one tally
51 where there were three, and counts to match."
52 [v]
53 (when v
54 (loop [in (seq v) out []]
55 (if-let [c (first in)]
56 (if (and (= \\ c) (second in))
57 (recur (drop 2 in)
58 (conj out (case (second in)
59 \: \;
60 \s \space
61 \r \return
62 \n \newline
63 (second in))))
64 (recur (rest in) (conj out c)))
65 (apply str out)))))
66
67(defn escape-tag-value
68 "The inverse, for a tag this client sends. An emoji needs none of it; a
69 message id could, and the cost of being right is a pass over a short string."
70 [v]
71 (-> (str v)
72 (str/replace "\\" "\\\\")
73 (str/replace ";" "\\:")
74 (str/replace " " "\\s")
75 (str/replace "\r" "\\r")
76 (str/replace "\n" "\\n")))
77
78(defn tag-value
79 "One IRCv3 tag's value, unescaped, or nil."
80 [tags key]
81 (when tags
82 (some (fn [pair]
83 (let [[k v] (str/split pair #"=" 2)]
84 (when (= k key) (unescape-tag v))))
85 (str/split tags #";"))))
86
87(defn nick-of
88 "The nick half of a `nick!user@host` prefix."
89 [prefix]
90 (when prefix
91 (let [i (str/index-of prefix "!")]
92 (if i (subs prefix 0 i) prefix))))
93