diff options
| author | EuAndreh <eu@euandre.org> | 2026-04-29 10:59:28 -0300 |
|---|---|---|
| committer | EuAndreh <eu@euandre.org> | 2026-04-29 11:00:01 -0300 |
| commit | 898205681d21fbaeae2f3362365561c8e1f47473 (patch) | |
| tree | 810356c3a976bc3d0b7d72f4599ac3f1d630ea4f /src | |
| parent | Retry JOIN persistence on concurrent channel creation (diff) | |
| download | papod-898205681d21fbaeae2f3362365561c8e1f47473.tar.gz papod-898205681d21fbaeae2f3362365561c8e1f47473.tar.xz | |
Channel modes, channel-rename, extended-isupport, metadata-2
Picks up the entire batch of optional irctest cases the previous gap
analysis identified.
ISUPPORT now advertises ACCOUNTEXTBAN=a, EXCEPTS=e, INVEX=I,
EXTBAN=~,am; CHANMODES becomes beI,k,l,Cnt so the list-mask modes
land in group A.
The +b/+e/+I MODE handler is unified — same plumbing for mask add,
mask remove, and list query (367/368, 348/349, 346/347 numerics).
Each list lives in its own components atom (:chan-bans,
:chan-excepts, :chan-invex). ban-matches? understands two extban
prefixes carried by +b/+e masks: ~a:account matches the joiner's
SASL account, while ~m:nick-mask is the mute extban that's skipped
on JOIN and consulted by PRIVMSG via mute-matches?.
JOIN: +i is bypassed by a matching +I; +b is bypassed by a matching
+e; ~m: masks no longer block JOIN.
PRIVMSG: +C drops non-ACTION CTCP to channels; ~m: bans drop
non-op/non-voiced sends with 404, except for matching +e masks.
draft/channel-rename: new RENAME command — chanop-only, refuses
already-occupied targets, persists the new :papod.channel/name, and
moves all in-memory channel state. Members with the cap receive
RENAME; the rest get a synthesised PART+JOIN pair.
draft/extended-isupport: advertised in CAP LS/REQ; no behaviour
change yet (our 005 fits in two lines anyway).
draft/metadata-2: METADATA command with GET / SET / LIST / SUB /
UNSUB / SUBS / CLEAR. Each user/channel maps to a key→value table
(:metadata atom) with per-nick subscriptions (:metadata-subs).
GET/LIST responses are wrapped in a "metadata" BATCH; SET emits 761
or 766 directly. SET also fires notify-metadata-subs! so subscribers
that share a channel with the target get a :nick METADATA push.
WHOIS now emits 760 (RPL_WHOISKEYVALUE) for any key set on the
target. Value-length and UTF-8 limits surface as
FAIL METADATA VALUE_INVALID / INVALID_UTF8; the input-too-long
handler also picks up the originating command name so those FAILs
match the spec's expected shape instead of a bare 417.
Diffstat (limited to 'src')
| -rw-r--r-- | src/papod.clj | 598 |
1 files changed, 528 insertions, 70 deletions
diff --git a/src/papod.clj b/src/papod.clj index 5142542..4be8edd 100644 --- a/src/papod.clj +++ b/src/papod.clj @@ -1069,6 +1069,8 @@ (declare join-one!) (declare notify-monitors!) +(declare ban-matches?) +(declare mute-matches?) (defn- persistent-channels "Channel handles where `nick` has stored membership." @@ -1192,12 +1194,15 @@ " nt" ;; channel modes )) (numeric-reply client "005" - (str "AWAYLEN=200 CASEMAPPING=ascii" + (str "ACCOUNTEXTBAN=a" + " AWAYLEN=200 CASEMAPPING=ascii" " CHANLIMIT=#:100" - " CHANMODES=b,k,l,nt" + " CHANMODES=beI,k,l,Cnt" " CHANNELLEN=64 CHANTYPES=#" - " ELIST= HOSTLEN=64" - " KICKLEN=300 MAXLIST=b:50" + " ELIST= EXCEPTS=e" + " EXTBAN=~,am HOSTLEN=64" + " INVEX=I KICKLEN=300" + " MAXLIST=b:50,e:50,I:50" " MAXTARGETS=4" " :are supported by this server")) (numeric-reply client "005" @@ -1429,6 +1434,9 @@ " draft/event-playback" " draft/relaymsg " reg-cap + " draft/channel-rename" + " draft/extended-isupport" + " draft/metadata-2" " draft/typing") (str "sasl message-tags server-time" " echo-message batch" @@ -1445,6 +1453,9 @@ " draft/event-playback" " draft/relaymsg " reg-cap + " draft/channel-rename" + " draft/extended-isupport" + " draft/metadata-2" " draft/typing")))]) "REQ" @@ -1465,6 +1476,9 @@ "draft/event-playback" "draft/relaymsg" "draft/account-registration" + "draft/channel-rename" + "draft/extended-isupport" + "draft/metadata-2" "draft/typing"}] (if-not (every? supported requested) [(str ":" +server-name+ " CAP " nick @@ -2248,6 +2262,15 @@ msg-id (java.util.UUID/randomUUID) msg-at (java.util.Date.) chan? (and channels (channel-handle? target)) + body (cond-> content + (string/starts-with? content ":") (subs 1)) + ctcp? (and (seq body) + (= \u0001 (first body))) + action? (and ctcp? + (string/starts-with? body "ACTION")) + chmodes (or (when (:chan-modes components) + (get @(:chan-modes components) target)) + "+nt") ;; IRCv3 +reply tag for threads reply-id (when-let [r (get tags "+reply")] (try (java.util.UUID/fromString r) @@ -2264,11 +2287,7 @@ ;; +n: no external messages (and chan? - (string/includes? - (or (get @(:chan-modes components) - target) - "+nt") - "n") + (string/includes? chmodes "n") (not (contains? (get @channels target) nick))) [(numeric-reply client "404" @@ -2277,11 +2296,7 @@ ;; +m: moderated — only ops/voiced may speak (and chan? - (string/includes? - (or (get @(:chan-modes components) - target) - "+nt") - "m") + (string/includes? chmodes "m") (not (and (:ops components) (contains? (get @(:ops components) target) @@ -2294,6 +2309,40 @@ (str target " :Cannot send to channel"))] + ;; +C: no CTCP (except ACTION) + (and chan? ctcp? (not action?) + (string/includes? chmodes "C")) + [(numeric-reply client "404" + (str target + " :Cannot send CTCP to channel (+C)"))] + + ;; ~m: mute extban — sender is muted by a +b mask + ;; (ops/voiced bypass). Excepted by matching +e. + (and chan? + (not (and (:ops components) + (contains? + (get @(:ops components) target) + nick))) + (not (and (:voiced components) + (contains? + (get @(:voiced components) target) + nick))) + (let [bans-atom (:chan-bans components) + excepts-atom (:chan-excepts components) + bans (when bans-atom + (get @bans-atom target #{})) + excepts (when excepts-atom + (get @excepts-atom target #{})) + acct (:account @client)] + (and (seq bans) + (mute-matches? bans nick) + (not (and (seq excepts) + (ban-matches? + excepts nick acct)))))) + [(numeric-reply client "404" + (str target + " :Cannot send to channel (+b)"))] + :else (do ;; PERSIST FIRST @@ -2493,15 +2542,39 @@ [nick] (str nick "!" nick "@localhost")) +(defn- nick-mask-match? + [mask nick] + (boolean + (re-matches (glob-pattern + (string/lower-case mask)) + (string/lower-case (nuh-mask nick))))) + (defn- ban-matches? - [bans nick] - (let [nuh (nuh-mask nick)] - (some (fn [mask] - (boolean - (re-matches (glob-pattern - (string/lower-case mask)) - (string/lower-case nuh)))) - bans))) + "Matches a +b/+e mask set against a join attempt. Recognises + ~a:account extbans (match by account) and ignores ~m:mute + extbans (those filter PRIVMSGs, not joins)." + ([masks nick] (ban-matches? masks nick nil)) + ([masks nick account] + (some (fn [mask] + (cond + (string/starts-with? mask "~a:") + (and account + (= (string/lower-case + (subs mask 3)) + (string/lower-case account))) + (string/starts-with? mask "~m:") + false + :else + (nick-mask-match? mask nick))) + masks))) + +(defn- mute-matches? + "Matches the ~m:<nick-mask> extban subset of a +b list." + [masks nick] + (some (fn [mask] + (and (string/starts-with? mask "~m:") + (nick-mask-match? (subs mask 3) nick))) + masks)) (defn- chan-symbol [components handle] @@ -2547,11 +2620,17 @@ [(numeric-reply client "403" (str handle " :No such channel"))] - ;; +i (invite-only): require invite + ;; +i (invite-only): require invite (bypassed by +I match) (and existing? (not (contains? (get @channels handle) nick)) (string/includes? modes "i") - (not invited?)) + (not invited?) + (let [invex-atom (:chan-invex components) + invex (when invex-atom + (get @invex-atom handle #{}))] + (not (and (seq invex) + (ban-matches? + invex nick (:account @client)))))) [(numeric-reply client "473" (str handle " :Cannot join channel (+i)"))] @@ -2571,14 +2650,22 @@ (str handle " :Cannot join channel (+l)"))] - ;; +b: banned (an active INVITE bypasses the ban) + ;; +b: banned (INVITE or matching +e exception bypasses) (and existing? (not (contains? (get @channels handle) nick)) (not invited?) (let [bans-atom (:chan-bans components) + excepts-atom (:chan-excepts components) bans (when bans-atom - (get @bans-atom handle #{}))] - (and (seq bans) (ban-matches? bans nick)))) + (get @bans-atom handle #{})) + excepts (when excepts-atom + (get @excepts-atom handle #{})) + acct (:account @client)] + (and (seq bans) + (ban-matches? bans nick acct) + (not (and (seq excepts) + (ban-matches? + excepts nick acct)))))) [(numeric-reply client "474" (str handle " :Cannot join channel (+b)"))] @@ -2795,6 +2882,86 @@ (join-one! h (get keys i) client components)) (range) handles))))) +(defn- handle-rename + [params client components] + (let [{:keys [conn clients channels]} components + nick (client-target client) + old (some-> (first params) string/lower-case) + new (some-> (second params) string/lower-case) + reason (when (> (count params) 2) + (let [r (nth params 2)] + (cond-> r + (string/starts-with? r ":") (subs 1))))] + (cond + (or (< (count params) 2) + (string/blank? old) (string/blank? new)) + [(str "FAIL RENAME NEED_MORE_PARAMS" + " :Not enough parameters")] + + (not (channel-handle? old)) + [(numeric-reply client "403" + (str old " :No such channel"))] + + (not (channel-handle? new)) + [(str "FAIL RENAME INVALID_CHAN_NAME " + new " :Invalid channel name")] + + (not (and channels (seq (get @channels old)))) + [(numeric-reply client "403" + (str old " :No such channel"))] + + (and channels (seq (get @channels new))) + [(str "FAIL RENAME CHANNEL_NAME_IN_USE " + new " :Channel name already in use")] + + (not (and (:ops components) + (contains? (get @(:ops components) old) + nick))) + [(numeric-reply client "482" + (str old + " :You're not channel operator"))] + + :else + (let [src (client-prefix client) + members (get @channels old) + rline (str ":" src " RENAME " old " " new + (when reason (str " :" reason))) + part-l (str ":" src " PART " old + (when reason (str " :" reason))) + join-l (fn [member-nick] + (str ":" member-nick " JOIN " new))] + ;; Move channel state across atoms + (doseq [a-key [:channels :ops :voiced + :chan-modes :chan-keys :chan-limits + :chan-bans :chan-excepts :chan-invex + :invites]] + (when-let [a (get components a-key)] + (swap! a (fn [m] + (let [v (get m old)] + (cond-> (dissoc m old) + v (assoc new v))))))) + ;; Persist the rename + (when conn + (try + (let [db (d/db conn) + eid (resolve-channel db old)] + (when eid + @(d/transact conn + [[:db/add eid :papod.channel/name new]]))) + (catch Exception _))) + ;; Deliver to members based on cap support + (when (and clients channels) + (doseq [mn members + :let [m (get @clients mn) + ca (:client-atom m) + caps (or (when ca (:caps @ca)) #{})] + :when m] + (if (caps "draft/channel-rename") + (deliver-to-client! (:w m) rline) + (do (deliver-to-client! (:w m) part-l) + (deliver-to-client! (:w m) (join-l mn)))))) + [])))) + (defn- handle-part [params client components] (let [{:keys [clients channels]} components @@ -3514,6 +3681,233 @@ [tagged] []))))))) +(defn- notify-metadata-subs! + "Deliver a `:source METADATA target key * [:value]` line to every + subscriber that shares a channel with the target." + [components client target-display key value] + (let [{:keys [clients channels metadata-subs]} components + nick (client-target client) + target (string/lower-case target-display) + chan? (channel-handle? target-display) + ;; nicks who could receive: members of relevant channels + relevant-chans + (when channels + (if chan? + (when (contains? @channels target) + [target]) + (filter (fn [[ch members]] + (contains? members target)) + @channels))) + candidate-nicks + (set + (mapcat + (fn [ch] + (let [members (if (string? ch) + (get @channels ch) + (val ch))] + members)) + (or relevant-chans []))) + line (if value + (str ":" nick " METADATA " + target-display " " key " * :" value) + (str ":" nick " METADATA " + target-display " " key " *"))] + (when (and clients metadata-subs) + (doseq [mn candidate-nicks + :when (not= mn nick) + :let [m (get @clients mn) + subs (get @metadata-subs + (string/lower-case mn) #{})] + :when (and m (contains? subs key))] + (deliver-to-client! (:w m) line))))) + +(defn- metadata-target-key + "Validate a METADATA target and return its canonical lower-case key + (or nil for invalid). `*` always resolves to the caller's nick." + [components client target] + (let [nick (client-target client) + lo (when target (string/lower-case target))] + (cond + (or (nil? target) (= "*" target)) + (string/lower-case nick) + + (channel-handle? target) + (when (and (:channels components) + (seq (get @(:channels components) lo))) + lo) + + :else + (when-let [clients (:clients components)] + (let [matches (filter (fn [[k _]] + (= (string/lower-case k) lo)) + @clients)] + (when (seq matches) + lo)))))) + +(defn- metadata-target-display + "Echo the user-supplied target back, resolving `*` to the caller's + own nick (some clients require the canonical name)." + [client requested] + (if (or (nil? requested) (= "*" requested)) + (client-target client) + requested)) + +(defn- handle-metadata + [params client components] + (let [nick (client-target client) + target (first params) + subcmd (when (> (count params) 1) + (string/upper-case (second params))) + rest-args (drop 2 params) + meta-atom (:metadata components) + subs-atom (:metadata-subs components) + tkey (metadata-target-key components client target) + echo-target (metadata-target-display client target) + batch-id (str (java.util.UUID/randomUUID)) + wrap-batch + (fn [body-lines] + (into [(str "BATCH +" batch-id " metadata" + (when echo-target + (str " " echo-target)))] + (concat + (map (fn [l] + (if (string/starts-with? l "@") + (let [sp (.indexOf l " ")] + (str "@batch=" batch-id ";" + (subs l 1 sp) + (subs l sp))) + (str "@batch=" batch-id " " l))) + body-lines) + [(str "BATCH -" batch-id)])))] + (cond + (or (nil? target) (string/blank? target)) + [(numeric-reply client "461" + "METADATA :Not enough parameters")] + + (nil? subcmd) + [(numeric-reply client "461" + "METADATA :Not enough parameters")] + + (and (not= subcmd "SUB") + (not= subcmd "UNSUB") + (not= subcmd "SUBS") + (nil? tkey)) + [(str "FAIL METADATA INVALID_TARGET " + target " :Invalid target")] + + :else + (case subcmd + "GET" + (let [keys-asked (vec rest-args) + cur (get @meta-atom tkey {}) + body + (mapv + (fn [k] + (if-let [v (get cur k)] + (numeric-reply client "761" + (str echo-target " " k " * :" v)) + (numeric-reply client "766" + (str echo-target " " k + " :no matching key")))) + keys-asked)] + (wrap-batch body)) + + "LIST" + (let [cur (get @meta-atom tkey {}) + body + (mapv + (fn [[k v]] + (numeric-reply client "761" + (str echo-target " " k " * :" v))) + (sort-by first cur))] + (wrap-batch body)) + + "SET" + (let [k (first rest-args) + v-parts (rest rest-args) + raw-v (when (seq v-parts) + (string/join " " v-parts)) + v (when raw-v + (cond-> raw-v + (string/starts-with? raw-v ":") (subs 1))) + v-bytes (when v + (alength (.getBytes ^String v "UTF-8")))] + (cond + (nil? k) + [(numeric-reply client "461" + "METADATA :Not enough parameters")] + + ;; Only allow setting on self (or chanop on channel) + (not (or (= tkey (string/lower-case nick)) + (and (channel-handle? target) + (:ops components) + (contains? + (get @(:ops components) tkey) + nick)))) + [(str "FAIL METADATA KEY_NO_PERMISSION " + echo-target " " k + " :You don't have permission to set this key")] + + (and v-bytes (> v-bytes 360)) + [(str "FAIL METADATA VALUE_INVALID :Value of " + k " is too long")] + + (or (nil? v) (string/blank? v)) + (do (when meta-atom + (swap! meta-atom update tkey dissoc k)) + (notify-metadata-subs! components client + echo-target k nil) + [(numeric-reply client "766" + (str echo-target " " k + " :no matching key"))]) + + :else + (do (when meta-atom + (swap! meta-atom assoc-in [tkey k] v)) + (notify-metadata-subs! components client + echo-target k v) + [(numeric-reply client "761" + (str echo-target " " k " * :" v))]))) + + "CLEAR" + (do (when meta-atom (swap! meta-atom dissoc tkey)) + [(numeric-reply client "762" + (str echo-target + " :end of metadata"))]) + + "SUB" + (let [keys-asked (vec rest-args) + k-low (string/lower-case nick)] + (when subs-atom + (swap! subs-atom update k-low + (fnil into #{}) keys-asked)) + (when (seq keys-asked) + [(numeric-reply client "770" + (string/join " " keys-asked))])) + + "UNSUB" + (let [keys-asked (vec rest-args) + k-low (string/lower-case nick)] + (when subs-atom + (swap! subs-atom update k-low + (fnil (fn [s ks] + (apply disj s ks)) #{}) keys-asked)) + (when (seq keys-asked) + [(numeric-reply client "771" + (string/join " " keys-asked))])) + + "SUBS" + (let [k-low (string/lower-case nick) + cur (when subs-atom + (get @subs-atom k-low #{}))] + [(numeric-reply client "772" + (if (seq cur) + (string/join " " cur) + ":none"))]) + + [(str "FAIL METADATA INVALID_SUBCOMMAND " + subcmd " :Unknown subcommand")])))) + (defn- handle-markread [params client components] (let [{:keys [conn]} components @@ -4641,6 +5035,21 @@ (conj (numeric-reply client "330" (str found-nick " " (:account st) " :is logged in as"))) + (seq (when-let [meta-atom (:metadata components)] + (get @meta-atom + (string/lower-case found-nick)))) + ((fn [r] + (let [meta-atom (:metadata components) + kv (when meta-atom + (get @meta-atom + (string/lower-case + found-nick) {}))] + (into r + (map (fn [[k v]] + (numeric-reply client "760" + (str found-nick " " + k " * :" v))) + (sort-by first kv)))))) true (conj (numeric-reply client "317" (str found-nick " 0 " @@ -4881,11 +5290,11 @@ (:w m) line))) []))) - ;; Simple flag modes (n/t/m/i/s/p/c) + ;; Simple flag modes (n/t/m/i/s/p/c/C) ;; Iterate every char in the mode-str so ;; "MODE #c +sp" applies both flags. (every? (fn [c] - (#{\n \t \m \i \s \p \c \+ \-} + (#{\n \t \m \i \s \p \c \C \+ \-} c)) mode-str) (let [chars (rest mode-str) @@ -4961,41 +5370,52 @@ (:w m) line))) []))) - ;; +b/-b with mask: add/remove ban - (and (= \b mode-char) mode-arg) - (let [bans-atom (:chan-bans components) - mask mode-arg - mask-key (string/lower-case mask) - line (str ":" nick " MODE " target - " " mode-str " " mask)] - (when bans-atom - (if adding? - (swap! bans-atom update target - (fnil conj #{}) mask-key) - (swap! bans-atom update target - (fnil disj #{}) mask-key))) - (when (and clients channels) - (doseq [mn (get @channels target) - :let [m (get @clients mn)] - :when m] - (deliver-to-client! (:w m) line))) - []) + ;; +b/+e/+I: list-mask modes + ;; (b=ban, e=ban-exception, I=invite-exception) + (#{\b \e \I} mode-char) + (let [[atom-key list-num end-num end-msg] + (case mode-char + \b [:chan-bans "367" "368" + ":End of channel ban list"] + \e [:chan-excepts "348" "349" + ":End of channel exception list"] + \I [:chan-invex "346" "347" + ":End of channel invite list"]) + list-atom (get components atom-key)] + (cond + mode-arg + (let [mask mode-arg + mask-key (string/lower-case mask) + line (str ":" nick " MODE " target + " " mode-str + " " mask)] + (when list-atom + (if adding? + (swap! list-atom update target + (fnil conj #{}) mask-key) + (swap! list-atom update target + (fnil disj #{}) mask-key))) + (when (and clients channels) + (doseq [mn (get @channels target) + :let [m (get @clients mn)] + :when m] + (deliver-to-client! (:w m) line))) + []) - ;; +b: ban list query - (and (= \b mode-char) (nil? mode-arg)) - (let [bans-atom (:chan-bans components) - cur-bans (when bans-atom - (get @bans-atom target #{}))] - (concat - (mapv (fn [mask] - (numeric-reply client "367" - (str target " " mask " " nick - " " (quot - (System/currentTimeMillis) - 1000)))) - (sort cur-bans)) - [(numeric-reply client "368" - (str target " :End of channel ban list"))])) + :else + (let [cur (when list-atom + (get @list-atom target #{}))] + (concat + (mapv + (fn [mask] + (numeric-reply client list-num + (str target " " mask " " nick + " " (quot + (System/currentTimeMillis) + 1000)))) + (sort cur)) + [(numeric-reply client end-num + (str target " " end-msg))])))) :else [])))) @@ -5495,6 +5915,7 @@ client components))) "RELAYMSG" (handle-relaymsg params client components) "JOIN" (handle-join params client components) + "RENAME" (handle-rename params client components) "PART" (handle-part params client components) "TOPIC" (handle-topic params client components) "KICK" (handle-kick params client components) @@ -5503,6 +5924,7 @@ "EDIT" (handle-edit params client components) "TAGMSG" (handle-tagmsg message client components) "MARKREAD" (handle-markread params client components) + "METADATA" (handle-metadata params client components) "LIST" (let [filter-ch (first params) self (client-target client) chans (when (:channels components) @@ -5722,16 +6144,48 @@ (cond (input-too-long? raw-message) (let [nick (or (:nick @client) "*") - line (str ":" +server-name+ " 417 " nick - " :Input line was too long")] - (deliver-to-client! w line)) + stripped (cond-> raw-message + (string/starts-with? raw-message "@") + (subs (inc (.indexOf raw-message " ")))) + stripped (cond-> stripped + (string/starts-with? stripped ":") + (subs (inc (.indexOf stripped " ")))) + first-word (let [s (.indexOf stripped " ")] + (if (pos? s) + (subs stripped 0 s) + stripped)) + ctx (string/upper-case + (subs first-word 0 + (min (count first-word) 32))) + ctx (if (re-matches #"[A-Z]+" ctx) ctx nil)] + (if ctx + (deliver-to-client! w + (str ":" +server-name+ " FAIL " ctx + " VALUE_INVALID :Input line was too long")) + (deliver-to-client! w + (str ":" +server-name+ " 417 " nick + " :Input line was too long")))) (has-invalid-utf8? raw-message) (let [label (extract-label raw-message) - line (str (when label (str "@label=" label " ")) - ":" +server-name+ - " FAIL * INVALID_UTF8 :Message rejected," - " contains invalid UTF-8")] + ;; Best-effort command extraction so the FAIL context + ;; matches the originating command (e.g. METADATA SET). + stripped (cond-> raw-message + (string/starts-with? raw-message "@") + (subs (inc (.indexOf raw-message " ")))) + stripped (cond-> stripped + (string/starts-with? stripped ":") + (subs (inc (.indexOf stripped " ")))) + first-word (let [s (.indexOf stripped " ")] + (if (pos? s) + (subs stripped 0 s) + stripped)) + ctx (string/upper-case first-word) + ctx (if (re-matches #"[A-Z]+" ctx) ctx "*") + line (str (when label (str "@label=" label " ")) + ":" +server-name+ + " FAIL " ctx " INVALID_UTF8 :Message" + " rejected, contains invalid UTF-8")] (deliver-to-client! w line)) :else @@ -5779,6 +6233,10 @@ :chan-keys (atom {}) :chan-limits (atom {}) :chan-bans (atom {}) + :chan-excepts (atom {}) + :chan-invex (atom {}) + :metadata (atom {}) + :metadata-subs (atom {}) :invites (atom {}) :whowas (atom []) :max-users (atom 0) |
