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

Reach irc.freeq.at over TLS from the phone e80514e · on bc0e14f897638a00ceeff31e1927f74d9bd15a71 · nandi · 7d ago
dart.cljd · 74 lines · 3.6 KBGDScript3 Blame HistoryRaw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
(ns frq.net.dart
  "An IRC socket over dart:io. The thing the jolt APK could never have.

  `frq.irc` reaches OpenSSL through the dynamic loader for TLS, and Android
  ships no public libssl  so on the phone `:6697` was unreachable, sign-in was
  desktop-only, and the connect screen fell back to the plain `:6667` listener
  on its own. `SecureSocket` is in the Dart runtime. There is nothing to load
  and nothing to find.

  The shape is different from the desktop's and deliberately so. jolt has one
  future blocking on a read, which also has to flush the outbox because the
  reader owns the connection; Dart has an event loop, so a socket is a `Stream`
  and a write is a write. No thread, no outbox, no poll interval.

  What comes out is `frq.irc.parse/parse-line`'s maps — the same parser the
  desktop runs, out of common/."
  (:require ["dart:convert" :as conv]
            ["dart:io" :as io]
            [frq.irc.parse :as parse]))

(defn ^:async connect!
  "Open a connection and start reading it.

  `on-msg` is called with a parsed map per line; `on-close` with a reason, or
  nil where the peer simply went away. Returns the socket, which `send-line!`
  and `close!` take."
  [{:keys [host port tls? on-msg on-close]}]
  (let [buffer (atom "")
        sock (await (if tls?
                      (io/SecureSocket.connect host port)
                      (io/Socket.connect host port)))]
    ;; Lines are cut here rather than by a LineSplitter in the chain. Two
    ;; transforms deep, cljd hands the splitter a CastStream<String, dynamic>
    ;; where it wants a Stream<String>, and it throws at the first byte  the
    ;; generic is lost through the dynamic call. Buffering the decoded chunks
    ;; is a few lines and has no type to lose.
    ;;
    ;; \r\n or \n: IRC says the former and servers send both.
    (-> sock
        (.transform (.-decoder conv/utf8))
        (.listen (fn [chunk]
                   ;; Split rather than a LineSplitter in the chain: two
                   ;; transforms deep, cljd hands the splitter a
                   ;; CastStream<String, dynamic> where it wants a
                   ;; Stream<String> and it throws at the first byte  the
                   ;; generic is lost through the dynamic call.
                   ;;
                   ;; The tail after the last newline is a partial line and
                   ;; goes back in the buffer. A chunk boundary falls wherever
                   ;; TCP puts it, which is not where IRC put its lines.
                   (let [parts (.split (str @buffer chunk) "\n")
                         whole (butlast parts)]
                     (reset! buffer (str (last parts)))
                     (doseq [raw whole]
                       (let [line (.trim (str raw))]
                         (when (and on-msg (pos? (count line)))
                           (let [m (parse/parse-line line)]
                             ;; PING is answered here rather than upstairs: a
                             ;; client that leaves it to the reducer is one
                             ;; queue away from a timeout, and nothing about
                             ;; the answer is a decision.
                             (when (= "PING" (:command m))
                               (.write sock (str "PONG :" (first (:params m)) "\r\n")))
                             (on-msg m)))))))
                 .onDone (fn [] (when on-close (on-close nil)))
                 .onError (fn [e _] (when on-close (on-close (str e))))
                 .cancelOnError true))
    sock))

(defn send-line! [sock line]
  (.write sock (str line "\r\n")))

(defn close! [sock]
  (try (.destroy sock) (catch Exception _ nil)))