diff options
| author | EuAndreh <eu@euandre.org> | 2026-04-27 22:45:33 -0300 |
|---|---|---|
| committer | EuAndreh <eu@euandre.org> | 2026-04-27 22:45:33 -0300 |
| commit | e4c1e8e82d80d15efa864d626792f2db02e632a8 (patch) | |
| tree | 127eb351bf07fb8b3c658fc6b8d1f3b23c4b0e4f /src/papod.clj | |
| parent | chghost, setname, invite-notify capabilities (diff) | |
| download | papod-e4c1e8e82d80d15efa864d626792f2db02e632a8.tar.gz papod-e4c1e8e82d80d15efa864d626792f2db02e632a8.tar.xz | |
MONITOR command and online-state notifications
Adds the IRCv3 MONITOR command (+/-/C/L/S subcommands), advertised via
the new ISUPPORT MONITOR=100 token. Per-client subscriptions live in
:monitor-targets (set of lowercased nicks); a global :nick-monitors
atom keyed by lowercased nick maps to the set of watching client atoms
for fast reverse lookup.
RPL_MONONLINE (730) is emitted to subscribed watchers when:
- a client completes registration (welcome path),
- a registered client changes nick to a different lowercased value.
RPL_MONOFFLINE (731) is emitted when:
- the canonical session for a nick disconnects (only if no sibling
multi-session entry remains),
- a registered client changes nick to a different lowercased value
(the old nick).
Case-only nick changes do not emit either, matching the spec.
MONITOR + filters out invalid nicks (which would include hostmasks)
and reports them as offline rather than rejecting the whole batch, so
testMonitorForbidsMasks's RPL_MONOFFLINE branch is satisfied.
Disconnect cleanup also drops the leaving session from every
:nick-monitors entry it was watching, so we don't deliver to closed
writers.
Diffstat (limited to 'src/papod.clj')
| -rw-r--r-- | src/papod.clj | 153 |
1 files changed, 151 insertions, 2 deletions
diff --git a/src/papod.clj b/src/papod.clj index d155b27..9f148c3 100644 --- a/src/papod.clj +++ b/src/papod.clj @@ -1058,6 +1058,7 @@ true)) (declare join-one!) +(declare notify-monitors!) (defn- persistent-channels "Channel handles where `nick` has stored membership." @@ -1190,7 +1191,8 @@ " MAXTARGETS=4" " :are supported by this server")) (numeric-reply client "005" - (str "MODES=4 NETWORK=papod" + (str "MODES=4 MONITOR=100" + " NETWORK=papod" " NICKLEN=30 PREFIX=(ov)@+" " STATUSMSG=@+" " TARGMAX=KICK:1,LIST:,WHOIS:1,PRIVMSG:4,NAMES:" @@ -1206,6 +1208,7 @@ " unread memo(s)." " Use /msg MemoServ LIST")]) rejoins (auto-rejoin-replies client components)] + (notify-monitors! components nick "730") (vec (concat welcome memo-line rejoins))))))) (defn- valid-nick? @@ -1323,6 +1326,13 @@ @notified mn)))] (deliver-to-client! (:w m) line) (swap! notified conj mn)))) + ;; MONITOR notifications: only on real (non-case) + ;; nick changes — old goes offline, new comes + ;; online. + (when (not= (string/lower-case old-nick) + (string/lower-case new-nick)) + (notify-monitors! components old-nick "731") + (notify-monitors! components new-nick "730")) [line]) ;; Pre-registration: just set the nick (do (swap! client assoc :nick new-nick) @@ -3778,6 +3788,130 @@ [(numeric-reply client "306" ":You have been marked as being away")])))) +(def- +monitor-limit+ 100) + +(defn- nick-online? + [clients nick] + (let [low (string/lower-case nick)] + (boolean + (some (fn [[k _]] (= (string/lower-case k) low)) + @clients)))) + +(defn- mononline-line + [client target] + (numeric-reply client "730" (str ":" target))) + +(defn- monoffline-line + [client target] + (numeric-reply client "731" (str ":" target))) + +(defn- handle-monitor + [params client components] + (let [{:keys [clients nick-monitors]} components + sub (some-> (first params) string/upper-case) + arg (second params) + targets (when arg (string/split arg #",")) + valid? (fn [t] (and (seq t) (valid-nick? t)))] + (case sub + "+" + (let [valid-ts (filter valid? targets) + invalid-ts (remove valid? targets) + current (or (:monitor-targets @client) #{}) + new-lows (->> valid-ts + (map string/lower-case) + (remove current)) + next-set (into current new-lows)] + (cond + (> (count next-set) +monitor-limit+) + [(numeric-reply client "734" + (str +monitor-limit+ " " + (string/join "," targets) + " :Monitor list is full."))] + :else + (do (swap! client assoc :monitor-targets next-set) + (when nick-monitors + (doseq [low new-lows] + (swap! nick-monitors update low + (fnil conj #{}) client))) + (let [online (filter + #(nick-online? clients %) + valid-ts) + offline (concat + invalid-ts + (remove + #(nick-online? clients %) + valid-ts))] + (cond-> [] + (seq online) + (conj (mononline-line client + (string/join "," online))) + (seq offline) + (conj (monoffline-line client + (string/join "," offline)))))))) + + "-" + (let [removed-lows (->> targets + (map string/lower-case) + set) + current (or (:monitor-targets @client) #{}) + next-set (into #{} + (remove removed-lows current))] + (swap! client assoc :monitor-targets next-set) + (when nick-monitors + (doseq [low removed-lows] + (swap! nick-monitors update low + (fnil disj #{}) client))) + []) + + "C" + (let [current (or (:monitor-targets @client) #{})] + (swap! client assoc :monitor-targets #{}) + (when nick-monitors + (doseq [low current] + (swap! nick-monitors update low + (fnil disj #{}) client))) + []) + + "L" + (let [current (or (:monitor-targets @client) #{}) + list-line (when (seq current) + [(numeric-reply client "732" + (str ":" + (string/join "," current)))])] + (vec + (concat list-line + [(numeric-reply client "733" + ":End of MONITOR list")]))) + + "S" + (let [current (or (:monitor-targets @client) #{}) + online (filter #(nick-online? clients %) + current) + offline (remove (set online) current)] + (cond-> [] + (seq online) + (conj (mononline-line client + (string/join "," online))) + (seq offline) + (conj (monoffline-line client + (string/join "," offline))))) + + [(numeric-reply client "461" + "MONITOR :Not enough parameters")]))) + +(defn- notify-monitors! + [components nick numeric] + (when-let [nm (:nick-monitors components)] + (let [low (string/lower-case nick) + watchers (get @nm low #{})] + (doseq [watcher watchers + :let [w (:w @watcher) + wn (or (:nick @watcher) "*")] + :when w] + (deliver-to-client! w + (str ":" +server-name+ " " numeric " " + wn " :" nick)))))) + (defn- handle-setname [params client components] (let [{:keys [clients channels]} components @@ -4174,6 +4308,9 @@ (= command "SETNAME") (handle-setname params client components) + (= command "MONITOR") + (handle-monitor params client components) + (= command "NOTICE") (handle-notice params client components) @@ -5334,6 +5471,7 @@ :process-id process-id :clients (atom {}) :account-clients (atom {}) + :nick-monitors (atom {}) :channels (atom {}) :ops (atom {}) :voiced (atom {}) @@ -5422,7 +5560,18 @@ (let [entry (get @(:clients components) nick)] (when (or (nil? entry) (= client (:client-atom entry))) - (swap! (:clients components) dissoc nick)))) + (swap! (:clients components) dissoc nick) + ;; Notify monitors only when the nick is + ;; actually going offline (no other session + ;; remains for it). + (when (:registered? @client) + (notify-monitors! components nick + "731"))))) + ;; Drop our own monitor subscriptions. + (when-let [nm (:nick-monitors components)] + (doseq [low (or (:monitor-targets @client) + #{})] + (swap! nm update low (fnil disj #{}) client))) (when (:channels components) (doseq [[ch _] @(:channels components)] (swap! (:channels components) update ch disj nick))) |
