nandi/frqpublic Fork 0
a16dfae
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.

Undo the escaping on a tag value before reading it

IRCv3 escapes tag values, because `;` separates tags and a space ends them:
a semicolon travels as `\:`, a space as `\s`. `tag-value` took values as they
came, which was fine for the two things it was written for — a msgid and a
timestamp contain neither character — and wrong for anything else.

A reaction tally is one of the anything else. `emoji:nick;emoji:nick` arrives
with every separator written `\:`, so a reader that skips this step sees one
tally where there were three, and counts to match: the first emoji swallowing
the rest of the line as its list of nicks.

Values are unescaped now, `\\`, `\r` and `\n` included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-08-30T12:26:16-07:00 Browse files
a16dfae parent: ef6123f
modified src/frq/irc.jolt +26 -3
@@ -73,14 +73,37 @@
7373 :command (str/upper-case (or (first parts) ""))
7474 :params (cond-> (vec (rest parts)) trailing (conj trailing))}))
7575
76+(defn unescape-tag
77+ "An IRCv3 tag value with its escapes undone.
78+
79+ `\\:` is a semicolon, `\\s` a space, and `\\\\`, `\\r` and `\\n` themselves —
80+ the escaping exists because `;` separates tags and a space ends them. It
81+ matters for any value that can contain either: a reaction tally is
82+ `emoji:nick;emoji:nick` on the wire and arrives with every one of those
83+ semicolons written `\\:`, so a reader that skips this step sees one tally
84+ where there were three, and counts to match."
85+ [v]
86+ (when v
87+ (loop [in (seq v) out []]
88+ (if-let [c (first in)]
89+ (if (and (= \\ c) (second in))
90+ (recur (drop 2 in)
91+ (conj out (case (second in)
92+ \: \;
93+ \s \space
94+ \r \return
95+ \n \newline
96+ (second in))))
97+ (recur (rest in) (conj out c)))
98+ (apply str out)))))
99+
76100 (defn tag-value
77- "One IRCv3 tag's value, or nil. Tag values escape `;` and space; the ids and
78- timestamps read here contain neither, so they are taken as they come."
101+ "One IRCv3 tag's value, unescaped, or nil."
79102 [tags key]
80103 (when tags
81104 (some (fn [pair]
82105 (let [[k v] (str/split pair #"=" 2)]
83- (when (= k key) v)))
106+ (when (= k key) (unescape-tag v))))
84107 (str/split tags #";"))))
85108
86109 (defn nick-of
@@ -73,14 +73,37 @@
73 :command (str/upper-case (or (first parts) ""))73 :command (str/upper-case (or (first parts) ""))
74 :params (cond-> (vec (rest parts)) trailing (conj trailing))}))74 :params (cond-> (vec (rest parts)) trailing (conj trailing))}))
75 75
76+(defn unescape-tag
77+ "An IRCv3 tag value with its escapes undone.
78+
79+ `\\:` is a semicolon, `\\s` a space, and `\\\\`, `\\r` and `\\n` themselves —
80+ the escaping exists because `;` separates tags and a space ends them. It
81+ matters for any value that can contain either: a reaction tally is
82+ `emoji:nick;emoji:nick` on the wire and arrives with every one of those
83+ semicolons written `\\:`, so a reader that skips this step sees one tally
84+ where there were three, and counts to match."
85+ [v]
86+ (when v
87+ (loop [in (seq v) out []]
88+ (if-let [c (first in)]
89+ (if (and (= \\ c) (second in))
90+ (recur (drop 2 in)
91+ (conj out (case (second in)
92+ \: \;
93+ \s \space
94+ \r \return
95+ \n \newline
96+ (second in))))
97+ (recur (rest in) (conj out c)))
98+ (apply str out)))))
99+
76 (defn tag-value100 (defn tag-value
77- "One IRCv3 tag's value, or nil. Tag values escape `;` and space; the ids and101+ "One IRCv3 tag's value, unescaped, or nil."
78- timestamps read here contain neither, so they are taken as they come."
79 [tags key]102 [tags key]
80 (when tags103 (when tags
81 (some (fn [pair]104 (some (fn [pair]
82 (let [[k v] (str/split pair #"=" 2)]105 (let [[k v] (str/split pair #"=" 2)]
83- (when (= k key) v)))106+ (when (= k key) (unescape-tag v))))
84 (str/split tags #";"))))107 (str/split tags #";"))))
85 108
86 (defn nick-of109 (defn nick-of