| Reach irc.freeq.at over TLS from the phone e80514e nandi 8d ago | 1 | (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 | |