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

Do not let a room we refused build itself back out of the echo

Refusing a membership the file does not claim sends PART, and the server
echoes it back. `assoc-in` on a channel that is not in the map does not
fail — it invents one, a buffer holding a `:joined?` and nothing else,
no name and no unread — so the echo of leaving put the room straight
back in the list, where drawing it threw on `(pos? nil)` and took the
client down a frame later.

Four ways in, and the fourth is why guarding the first three was not
enough: `push-message!` builds the buffer it is handed through
`ensure-channel`, so even "eve left" re-creates the room it is about.
PART and KICK for our own nick are now conditional on holding the room,
`remove-user!` is too, and the join/part/kick lines about other people
are only pushed into rooms we have. A room we are in still reports its
comings and goings; a room we are not is silence.

`conversation-row` also defaults the count it reads. Not the fix —
everything that makes a buffer goes through `ensure-channel` — but the
list draws whatever is in the atom, and a half-made room arriving there
should be a row that reads as quiet rather than the frame that killed
the client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-06T02:55:49-07:00 Browse files
2ccf442 parent: 88741e9
modified src/frq/app.jolt +5 -1
@@ -179,7 +179,11 @@
179179
180180 (defn conversation-row [buffer]
181181 (let [name (:name buffer)
182- unread (:unread buffer)]
182+ ;; Defaulted rather than assumed. Everything that builds a buffer
183+ ;; goes through `ensure-channel`, but the list draws whatever is in
184+ ;; the atom, and a half-made room reaching here should be a row that
185+ ;; reads as quiet rather than the frame that killed the client.
186+ unread (:unread buffer 0)]
183187 [:card {:key name}
184188 ;; The name gets the line, and membership and unread count the one under
185189 ;; it. All three on one line is what the list pane has no room for: the
@@ -179,7 +179,11 @@
179 179
180 (defn conversation-row [buffer]180 (defn conversation-row [buffer]
181 (let [name (:name buffer)181 (let [name (:name buffer)
182- unread (:unread buffer)]182+ ;; Defaulted rather than assumed. Everything that builds a buffer
183+ ;; goes through `ensure-channel`, but the list draws whatever is in
184+ ;; the atom, and a half-made room reaching here should be a row that
185+ ;; reads as quiet rather than the frame that killed the client.
186+ unread (:unread buffer 0)]
183 [:card {:key name}187 [:card {:key name}
184 ;; The name gets the line, and membership and unread count the one under188 ;; The name gets the line, and membership and unread count the one under
185 ;; it. All three on one line is what the list pane has no room for: the189 ;; it. All three on one line is what the list pane has no room for: the
modified src/frq/state.jolt +33 -14
@@ -667,7 +667,9 @@
667667
668668 (defn- remove-user! [channel nick]
669669 (when (and channel nick)
670- (swap! channels #(update-in % [channel :users] dissoc nick))))
670+ (swap! channels #(if (contains? % channel)
671+ (update-in % [channel :users] dissoc nick)
672+ %))))
671673
672674 (defn- remove-user-everywhere!
673675 "A QUIT names no channel — the person left the server, so they left every
@@ -926,9 +928,14 @@
926928 ;; Adopted or asked for, it is ours now and the file should
927929 ;; say so before the next connect judges it.
928930 (remember-rooms! true)))
929- (do (add-user! ch from)
930- (when-not @hide-join-part?
931- (push-message! ch "*" (str from " joined"))))))
931+ ;; Somebody else arriving in a room we do not hold is not a
932+ ;; reason to start holding it: `add-user!` and `push-message!`
933+ ;; both build the buffer they are given, so either one would
934+ ;; put the refused room back in the list.
935+ (when (contains? @channels ch)
936+ (add-user! ch from)
937+ (when-not @hide-join-part?
938+ (push-message! ch "*" (str from " joined"))))))
932939 ;; NAMES, a line at a time. The channel is the parameter that names one:
933940 ;; the reply is `<us> <symbol> <channel> :<names>`, and a server that
934941 ;; leaves the symbol out shifts everything before the list along by one.
@@ -945,21 +952,33 @@
945952 (irc/send-line! @conn
946953 (str "CHATHISTORY LATEST " ch " * " history-limit))))
947954
955+ ;; `assoc-in` on a channel that is not there does not fail, it invents
956+ ;; one — a buffer with a `:joined?` and nothing else, no name and no
957+ ;; unread, which the room list then tries to draw. That is not
958+ ;; hypothetical now: refusing a room the file does not claim sends PART,
959+ ;; and the server echoes it straight back at us. A membership changing in
960+ ;; a room we do not hold is nothing to record.
948961 "PART" (let [ch (first params)]
949962 (if (= from @form-nick)
950- (swap! channels #(-> % (assoc-in [ch :joined?] false)
951- (assoc-in [ch :joining?] false)
952- (assoc-in [ch :users] {})))
953- (do (remove-user! ch from)
954- (when-not @hide-join-part?
955- (push-message! ch "*" (str from " left"))))))
963+ (swap! channels #(if (contains? % ch)
964+ (-> % (assoc-in [ch :joined?] false)
965+ (assoc-in [ch :joining?] false)
966+ (assoc-in [ch :users] {}))
967+ %))
968+ (when (contains? @channels ch)
969+ (remove-user! ch from)
970+ (when-not @hide-join-part?
971+ (push-message! ch "*" (str from " left"))))))
956972 "KICK" (let [[ch who] params]
957973 (if (= who @form-nick)
958- (swap! channels #(-> % (assoc-in [ch :joined?] false)
959- (assoc-in [ch :joining?] false)
960- (assoc-in [ch :users] {})))
974+ (swap! channels #(if (contains? % ch)
975+ (-> % (assoc-in [ch :joined?] false)
976+ (assoc-in [ch :joining?] false)
977+ (assoc-in [ch :users] {}))
978+ %))
961979 (remove-user! ch who))
962- (push-message! ch "*" (str who " was kicked by " from)))
980+ (when (contains? @channels ch)
981+ (push-message! ch "*" (str who " was kicked by " from))))
963982 ;; A QUIT and a NICK name no channel, so both are folded into every
964983 ;; buffer the person was listed in — and said out loud only where they
965984 ;; were, which is what keeps a stranger's rename out of a quiet room.
@@ -667,7 +667,9 @@
667 667
668 (defn- remove-user! [channel nick]668 (defn- remove-user! [channel nick]
669 (when (and channel nick)669 (when (and channel nick)
670- (swap! channels #(update-in % [channel :users] dissoc nick))))670+ (swap! channels #(if (contains? % channel)
671+ (update-in % [channel :users] dissoc nick)
672+ %))))
671 673
672 (defn- remove-user-everywhere!674 (defn- remove-user-everywhere!
673 "A QUIT names no channel — the person left the server, so they left every675 "A QUIT names no channel — the person left the server, so they left every
@@ -926,9 +928,14 @@
926 ;; Adopted or asked for, it is ours now and the file should928 ;; Adopted or asked for, it is ours now and the file should
927 ;; say so before the next connect judges it.929 ;; say so before the next connect judges it.
928 (remember-rooms! true)))930 (remember-rooms! true)))
929- (do (add-user! ch from)931+ ;; Somebody else arriving in a room we do not hold is not a
930- (when-not @hide-join-part?932+ ;; reason to start holding it: `add-user!` and `push-message!`
931- (push-message! ch "*" (str from " joined"))))))933+ ;; both build the buffer they are given, so either one would
934+ ;; put the refused room back in the list.
935+ (when (contains? @channels ch)
936+ (add-user! ch from)
937+ (when-not @hide-join-part?
938+ (push-message! ch "*" (str from " joined"))))))
932 ;; NAMES, a line at a time. The channel is the parameter that names one:939 ;; NAMES, a line at a time. The channel is the parameter that names one:
933 ;; the reply is `<us> <symbol> <channel> :<names>`, and a server that940 ;; the reply is `<us> <symbol> <channel> :<names>`, and a server that
934 ;; leaves the symbol out shifts everything before the list along by one.941 ;; leaves the symbol out shifts everything before the list along by one.
@@ -945,21 +952,33 @@
945 (irc/send-line! @conn952 (irc/send-line! @conn
946 (str "CHATHISTORY LATEST " ch " * " history-limit))))953 (str "CHATHISTORY LATEST " ch " * " history-limit))))
947 954
955+ ;; `assoc-in` on a channel that is not there does not fail, it invents
956+ ;; one — a buffer with a `:joined?` and nothing else, no name and no
957+ ;; unread, which the room list then tries to draw. That is not
958+ ;; hypothetical now: refusing a room the file does not claim sends PART,
959+ ;; and the server echoes it straight back at us. A membership changing in
960+ ;; a room we do not hold is nothing to record.
948 "PART" (let [ch (first params)]961 "PART" (let [ch (first params)]
949 (if (= from @form-nick)962 (if (= from @form-nick)
950- (swap! channels #(-> % (assoc-in [ch :joined?] false)963+ (swap! channels #(if (contains? % ch)
951- (assoc-in [ch :joining?] false)964+ (-> % (assoc-in [ch :joined?] false)
952- (assoc-in [ch :users] {})))965+ (assoc-in [ch :joining?] false)
953- (do (remove-user! ch from)966+ (assoc-in [ch :users] {}))
954- (when-not @hide-join-part?967+ %))
955- (push-message! ch "*" (str from " left"))))))968+ (when (contains? @channels ch)
969+ (remove-user! ch from)
970+ (when-not @hide-join-part?
971+ (push-message! ch "*" (str from " left"))))))
956 "KICK" (let [[ch who] params]972 "KICK" (let [[ch who] params]
957 (if (= who @form-nick)973 (if (= who @form-nick)
958- (swap! channels #(-> % (assoc-in [ch :joined?] false)974+ (swap! channels #(if (contains? % ch)
959- (assoc-in [ch :joining?] false)975+ (-> % (assoc-in [ch :joined?] false)
960- (assoc-in [ch :users] {})))976+ (assoc-in [ch :joining?] false)
977+ (assoc-in [ch :users] {}))
978+ %))
961 (remove-user! ch who))979 (remove-user! ch who))
962- (push-message! ch "*" (str who " was kicked by " from)))980+ (when (contains? @channels ch)
981+ (push-message! ch "*" (str who " was kicked by " from))))
963 ;; A QUIT and a NICK name no channel, so both are folded into every982 ;; A QUIT and a NICK name no channel, so both are folded into every
964 ;; buffer the person was listed in — and said out loud only where they983 ;; buffer the person was listed in — and said out loud only where they
965 ;; were, which is what keeps a stranger's rename out of a quiet room.984 ;; were, which is what keeps a stranger's rename out of a quiet room.