summaryrefslogtreecommitdiff
path: root/src/papod.clj
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2026-04-27 22:45:33 -0300
committerEuAndreh <eu@euandre.org>2026-04-27 22:45:33 -0300
commite4c1e8e82d80d15efa864d626792f2db02e632a8 (patch)
tree127eb351bf07fb8b3c658fc6b8d1f3b23c4b0e4f /src/papod.clj
parentchghost, setname, invite-notify capabilities (diff)
downloadpapod-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.clj153
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)))