summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2026-04-25 16:10:56 -0300
committerEuAndreh <eu@euandre.org>2026-04-25 16:10:56 -0300
commiteab3929972d624c4cbbc84d53cc20601e4324392 (patch)
treebc71860eaac386134992609d6dae11ec8e95cdc8
parentAdd NICK validation, voice tracking, multi-prefix, OPER/TIME (diff)
downloadpapod-eab3929972d624c4cbbc84d53cc20601e4324392.tar.gz
papod-eab3929972d624c4cbbc84d53cc20601e4324392.tar.xz
Expand RPL_ISUPPORT and have OPER emit MODE +o
The 005 reply now advertises CASEMAPPING, CHANMODES, CHANTYPES, PREFIX, NETWORK, NICKLEN, CHANNELLEN, TOPICLEN, KICKLEN, MAXLIST, MODES, TARGMAX, USERLEN, STATUSMSG, AWAYLEN and UTF8ONLY across two 005 lines. This satisfies what irctest's isupport/multi_prefix tests look for. OPER now follows RFC convention by sending an explicit ":server MODE <nick> :+o" line after RPL_YOUREOPER so clients learn about their new mode without a separate query.
-rw-r--r--src/papod.clj22
-rw-r--r--tests/unit.clj58
2 files changed, 74 insertions, 6 deletions
diff --git a/src/papod.clj b/src/papod.clj
index bf41507..ffcf1c2 100644
--- a/src/papod.clj
+++ b/src/papod.clj
@@ -953,7 +953,20 @@
" nt" ;; channel modes
))
(numeric-reply client "005"
- "NETWORK=papod :are supported by this server")
+ (str "AWAYLEN=200 CASEMAPPING=ascii"
+ " CHANLIMIT=#:100 CHANMODES=,,l,nt"
+ " CHANNELLEN=64 CHANTYPES=#"
+ " ELIST= HOSTLEN=64"
+ " KICKLEN=300 MAXLIST=b:50"
+ " MAXTARGETS=4 MODES=4"
+ " NETWORK=papod NICKLEN=30"
+ " PREFIX=(ov)@+ STATUSMSG=@+"
+ " :are supported by this server"))
+ (numeric-reply client "005"
+ (str "TARGMAX=KICK:1,LIST:1,WHOIS:1"
+ " TOPICLEN=390 USERLEN=18"
+ " UTF8ONLY"
+ " :are supported by this server"))
(numeric-reply client "422"
":MOTD File is missing")]
memos (pending-memos (:conn components) nick)]
@@ -3050,12 +3063,15 @@
"OPER :Not enough parameters")]
:else
(let [oper-name (first params)
- oper-pass (second params)]
+ oper-pass (second params)
+ nick (client-target client)]
(if (and (= oper-name "operuser")
(= oper-pass "operpassword"))
(do (swap! client assoc :oper? true)
[(numeric-reply client "381"
- ":You are now an IRC operator")])
+ ":You are now an IRC operator")
+ (str ":" +server-name+ " MODE "
+ nick " :+o")])
[(numeric-reply client "464"
":Password incorrect")])))
diff --git a/tests/unit.clj b/tests/unit.clj
index c5cabaa..6b9e36b 100644
--- a/tests/unit.clj
+++ b/tests/unit.clj
@@ -179,7 +179,7 @@
{:command "USER"
:params ["" "joe" "0" "x" ":Joe"]}
c no-conn)]
- (is (= 6 (count replies)))
+ (is (= 7 (count replies)))
(is (string/includes? (first replies) "001"))
(is (:registered? @c)))))
(testing "USER before NICK does not register"
@@ -552,7 +552,7 @@
(is (:authenticated? @c2)))
;; CAP END completes registration
(let [replies (handle-cap ["END"] c2 components)]
- (is (= 6 (count replies)))
+ (is (= 7 (count replies)))
(is (string/includes? (first replies) "001"))
(is (:registered? @c2))))))
(testing "SASL with wrong password"
@@ -715,7 +715,7 @@
(let [replies (@#'papod/replies-for!
{:command "USER" :params ["" "bob" "0" "x" ":B"]}
c components)]
- (is (= 7 (count replies)))
+ (is (= 8 (count replies)))
(is (string/includes? (first replies) "001"))
(is (string/includes? (last replies) "MemoServ")))))))
@@ -1736,6 +1736,58 @@
(let [replies (handle-markread ["#test"] alice components)]
(is (string/includes? (first replies) "2026-04-22T10:00:00.000Z"))))))
+(deftest test_isupport-and-oper
+ (testing "RPL_ISUPPORT advertises required tokens"
+ (let [c (client)]
+ (replies-for! {:command "NICK" :params ["" "joe"]}
+ c no-conn)
+ (let [replies (replies-for!
+ {:command "USER"
+ :params ["" "joe" "0" "x" ":Joe"]}
+ c no-conn)
+ isupport (filter #(re-find #" 005 " %) replies)
+ joined (string/join " " isupport)]
+ (is (seq isupport))
+ (is (string/includes? joined "PREFIX=(ov)@+"))
+ (is (string/includes? joined "CHANTYPES=#"))
+ (is (string/includes? joined "CASEMAPPING=ascii"))
+ (is (string/includes? joined "NETWORK=papod"))
+ (is (string/includes? joined "NICKLEN="))
+ (is (string/includes? joined "CHANNELLEN="))
+ (is (string/includes? joined "TARGMAX=")))))
+ (testing "OPER with correct credentials sends 381 + MODE +o"
+ (let [c (registered-client "baz" (java.io.ByteArrayOutputStream.))]
+ (let [replies (replies-for!
+ {:command "OPER"
+ :params ["" "operuser" "operpassword"]}
+ c no-conn)]
+ (is (= 2 (count replies)))
+ (is (string/includes? (first replies) "381"))
+ (is (re-find #"MODE baz :?\+o" (second replies)))
+ (is (:oper? @c)))))
+ (testing "OPER with wrong password returns 464"
+ (let [c (registered-client "baz" (java.io.ByteArrayOutputStream.))]
+ (let [replies (replies-for!
+ {:command "OPER"
+ :params ["" "operuser" "wrong"]}
+ c no-conn)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "464"))
+ (is (not (:oper? @c))))))
+ (testing "OPER missing args returns 461"
+ (let [c (registered-client "baz" (java.io.ByteArrayOutputStream.))]
+ (let [replies (replies-for!
+ {:command "OPER" :params ["" "only"]}
+ c no-conn)]
+ (is (string/includes? (first replies) "461")))))
+ (testing "TIME returns 391"
+ (let [c (registered-client "baz" (java.io.ByteArrayOutputStream.))]
+ (let [replies (replies-for!
+ {:command "TIME" :params []}
+ c no-conn)]
+ (is (= 1 (count replies)))
+ (is (string/includes? (first replies) "391"))))))
+
(defn -main
[& _args]
(binding [*out* *err*]