summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2026-04-27 06:32:24 -0300
committerEuAndreh <eu@euandre.org>2026-04-27 06:32:24 -0300
commit6237e196a2833603b60bfa482e11d6023b9b1f39 (patch)
treefd9f79f1c911ccb4246b2644a284c198b6e1d1ba /src
parentImplement TAGMSG persistence and BATCH multiline support (diff)
downloadpapod-6237e196a2833603b60bfa482e11d6023b9b1f39.tar.gz
papod-6237e196a2833603b60bfa482e11d6023b9b1f39.tar.xz
Implement CHATHISTORY TARGETS, DM redaction, account-isolated DMs
Adds the missing CHATHISTORY TARGETS subcommand returning channels and DM peers the user has activity in within an exclusive timestamp window, sorted by latest message time. REDACT now accepts DM targets: a sender can redact their own DM, and both sides receive the REDACT line if they have draft/message-redaction. DM events now record source-account and target-account at storage time. Chathistory queries filter to events whose stored account matches the requesting client's current SASL account, so a freshly-registered user who reuses a previously-anonymous nick does not see the prior identity's DMs (regression test for issue #833). DM history fetch is also case-insensitive on nick comparisons, and format-history-event renders DMs with the original target nick rather than the conversation peer, fixing replays where a self→peer reply was shown as peer→peer. The join-one\! refactor (returns echo + topic + markread + names lines in the reply vector) is retained from prior work; unit tests assert the returned replies instead of inspecting the writer bytestream so JOIN echoes can be wrapped in a labeled-response BATCH.
Diffstat (limited to 'src')
-rw-r--r--src/papod.clj383
1 files changed, 282 insertions, 101 deletions
diff --git a/src/papod.clj b/src/papod.clj
index 798cd92..ef49dd2 100644
--- a/src/papod.clj
+++ b/src/papod.clj
@@ -329,6 +329,14 @@
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.attr/preds `non-empty?}
+ {:db/ident :papod.event/source-account
+ :db/valueType :db.type/string
+ :db/cardinality :db.cardinality/one
+ :db/index true}
+ {:db/ident :papod.event/target-account
+ :db/valueType :db.type/string
+ :db/cardinality :db.cardinality/one
+ :db/index true}
{:db/ident :papod.event/payload
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
@@ -818,27 +826,43 @@
(sort-by first))))
(defn- fetch-dm-history-all
- [db nick peer]
- (let [pairs (d/q '{:find [?source ?type ?id ?ts
+ [db nick nick-account peer]
+ (let [n-low (string/lower-case nick)
+ p-low (string/lower-case peer)
+ rows (d/q '{:find [?source ?type ?id ?ts ?s-acct ?t-acct
(pull ?e [:db/id
:papod.event/payload
+ :papod.event/target-nick
:papod.event/created-at
:papod.event/edit-of
:papod.event/reply-to])]
- :in [$ ?nick ?peer]
+ :in [$ ?n-low ?p-low]
:where [[?e :papod.event/source-nick ?source]
[?e :papod.event/target-nick ?target]
[?e :papod.event/type ?type]
[?e :papod.event/id ?id]
[?e :papod.event/created-at ?ts]
- (or-join [?nick ?peer ?source ?target]
- (and [(= ?source ?nick)]
- [(= ?target ?peer)])
- (and [(= ?source ?peer)]
- [(= ?target ?nick)]))]}
- db nick peer)]
- (->> pairs
- (map (fn [[source type id ts pulled]]
+ [(get-else $ ?e
+ :papod.event/source-account
+ "") ?s-acct]
+ [(get-else $ ?e
+ :papod.event/target-account
+ "") ?t-acct]
+ [(clojure.string/lower-case ?source) ?s-low]
+ [(clojure.string/lower-case ?target) ?t-low]
+ (or-join [?n-low ?p-low ?s-low ?t-low]
+ (and [(= ?s-low ?n-low)]
+ [(= ?t-low ?p-low)])
+ (and [(= ?s-low ?p-low)]
+ [(= ?t-low ?n-low)]))]}
+ db n-low p-low)
+ my-acct (or nick-account "")]
+ (->> rows
+ (filter (fn [[source _ _ _ s-acct t-acct _]]
+ (let [me-source? (= (string/lower-case source) n-low)
+ their-acct (if me-source? s-acct t-acct)]
+ (= their-acct my-acct))))
+ (map (fn [[source type id ts _ _ pulled]]
[(.getTime ^java.util.Date ts) type
source id pulled]))
(sort-by first))))
@@ -982,6 +1006,12 @@
[components]
(:conn (:cracha components)))
+(defn- nick-account
+ [components nick]
+ (when-let [c (cracha-conn components)]
+ (when (cracha/user-by-email c nick)
+ nick)))
+
(defn- authenticate!
[client _components]
(if +server-password+
@@ -2131,7 +2161,11 @@
(:papod.channel/id
(d/entity db channel-eid)))
reply-eid (when reply-id
- (find-event-eid db reply-id))]
+ (find-event-eid db reply-id))
+ src-acct (when-not chan?
+ (:account @client))
+ tgt-acct (when-not chan?
+ (nick-account components target))]
@(d/transact conn
(cond-> [{:db/ensure :papod.event/attrs
:db/id "new-event"
@@ -2147,7 +2181,15 @@
(conj [:papod.event/add-seq "new-event" chan-uuid]))
(not chan?)
(-> (update 0 assoc
- :papod.event/target-nick target))))))
+ :papod.event/target-nick target))
+ src-acct
+ (-> (update 0 assoc
+ :papod.event/source-account
+ src-acct))
+ tgt-acct
+ (-> (update 0 assoc
+ :papod.event/target-account
+ tgt-acct))))))
;; DELIVER
(let [client-tag-pairs
(->> (or tags {})
@@ -2457,16 +2499,21 @@
(get @channels handle))))
(swap! (:ops components) update handle
(fnil conj #{}) nick))
- ;; DELIVER
+ ;; DELIVER to other channel members (not self)
(let [u (:user @client)
rname (or (:realname u) nick)
account (or (:account @client) "*")
base (str ":" nick " JOIN " handle)
ext (str base " " account " :" rname)
line (tag-line event-id base)
- ext-line (tag-line event-id ext)]
+ ext-line (tag-line event-id ext)
+ my-caps (or (:caps @client) #{})
+ my-ej? (contains? my-caps
+ "extended-join")
+ echo (if my-ej? ext-line line)]
(when (and clients channels)
(doseq [member-nick (get @channels handle)
+ :when (not= member-nick nick)
:let [member (get @clients
member-nick)
ca (:client-atom member)
@@ -2498,59 +2545,58 @@
caps
"away-notify"))]
(deliver-to-client!
- (:w member) aline))))))
- ;; Send TOPIC + NAMES to the joining client
- (let [mp? (contains?
- (or (:caps @client) #{})
- "multi-prefix")
- members (names-for components handle
- :multi-prefix? mp?)
- sym (chan-symbol components handle)
- w (:w @client)
- db' (when conn (d/db conn))
- ch-eid' (when db'
- (resolve-channel db' handle))
- ent (when ch-eid'
- (d/entity db' ch-eid'))
- topic (:papod.channel/topic ent)]
- (when (and w (seq topic))
- (deliver-to-client! w
- (str ":" +server-name+ " 332 " nick
- " " handle " :" topic))
- (deliver-to-client! w
- (str ":" +server-name+ " 333 " nick
- " " handle " " nick " "
- (quot
- (System/currentTimeMillis) 1000))))
- ;; draft/read-marker: send MARKREAD before
- ;; RPL_ENDOFNAMES if the client has the cap
- (when (and w
- (contains?
- (or (:caps @client) #{})
- "draft/read-marker"))
- (let [stored (when (and conn ch-eid')
- (ffirst
- (d/q '{:find [?ts]
- :in [$ ?ch ?nick]
- :where
- [[?r :papod.read-marker/channel ?ch]
- [?r :papod.read-marker/nick ?nick]
- [?r :papod.read-marker/timestamp ?ts]]}
- db' ch-eid' nick)))]
- (deliver-to-client! w
- (str ":" +server-name+ " MARKREAD "
- handle " "
- (if stored
- (str "timestamp=" stored)
- "*")))))
- (when (and w members)
- (deliver-to-client! w
- (str ":" +server-name+ " 353 " nick
- " " sym " " handle " :" members))
- (deliver-to-client! w
- (str ":" +server-name+ " 366 " nick " "
- handle " :End of /NAMES list"))))
- []))))))))
+ (:w member) aline)))))
+ ;; Build replies for the joining client
+ (let [mp? (contains? my-caps
+ "multi-prefix")
+ members (names-for components handle
+ :multi-prefix? mp?)
+ sym (chan-symbol components handle)
+ db' (when conn (d/db conn))
+ ch-eid' (when db'
+ (resolve-channel db' handle))
+ ent (when ch-eid'
+ (d/entity db' ch-eid'))
+ topic (:papod.channel/topic ent)
+ topic-lines
+ (when (seq topic)
+ [(str ":" +server-name+ " 332 " nick
+ " " handle " :" topic)
+ (str ":" +server-name+ " 333 " nick
+ " " handle " " nick " "
+ (quot
+ (System/currentTimeMillis)
+ 1000))])
+ markread-line
+ (when (contains? my-caps
+ "draft/read-marker")
+ (let [stored (when (and conn ch-eid')
+ (ffirst
+ (d/q '{:find [?ts]
+ :in [$ ?ch ?nick]
+ :where
+ [[?r :papod.read-marker/channel ?ch]
+ [?r :papod.read-marker/nick ?nick]
+ [?r :papod.read-marker/timestamp ?ts]]}
+ db' ch-eid' nick)))]
+ (str ":" +server-name+ " MARKREAD "
+ handle " "
+ (if stored
+ (str "timestamp=" stored)
+ "*"))))
+ names-lines
+ (when members
+ [(str ":" +server-name+ " 353 " nick
+ " " sym " " handle " :" members)
+ (str ":" +server-name+ " 366 " nick
+ " " handle
+ " :End of /NAMES list")])]
+ (vec
+ (concat
+ [echo]
+ topic-lines
+ (when markread-line [markread-line])
+ names-lines))))))))))))
(defn- handle-join
@@ -2607,6 +2653,8 @@
at (:papod.event/created-at pulled)
edit-ref (:papod.event/edit-of pulled)
reply-ref (:papod.event/reply-to pulled)
+ target (or (:papod.event/target-nick pulled)
+ handle)
extra-tags (when (= type "user-tagmsg")
(when (seq payload)
(string/split payload #";")))
@@ -2623,7 +2671,7 @@
raw (case type
"user-message"
(str ":" source-nick " PRIVMSG "
- handle " " payload)
+ target " " payload)
"user-join"
(str ":" source-nick " JOIN " handle)
"user-kick"
@@ -2631,15 +2679,15 @@
payload)
"user-edit"
(str ":" source-nick " PRIVMSG "
- handle " " payload)
+ target " " payload)
"user-delete"
(str ":" source-nick " PRIVMSG "
- handle " [deleted]")
+ target " [deleted]")
"user-topic"
(str ":" source-nick " TOPIC " handle
" :" payload)
"user-tagmsg"
- (str ":" source-nick " TAGMSG " handle)
+ (str ":" source-nick " TAGMSG " target)
(str ":" source-nick " NOTICE "
handle " :" type))]
(if at
@@ -2789,6 +2837,99 @@
[])))
+(defn- chathistory-targets
+ [db nick channels]
+ (let [chan-rows
+ (when channels
+ (for [[handle members] @channels
+ :when (contains? members nick)
+ :let [ch-eid (resolve-channel db handle)]
+ :when ch-eid
+ :let [ts (->>
+ (d/q '{:find [(max ?t)]
+ :in [$ ?ch]
+ :where
+ [[?e :papod.event/channel
+ ?ch]
+ [?e :papod.event/type
+ "user-message"]
+ [?e :papod.event/created-at
+ ?t]]}
+ db ch-eid)
+ ffirst)]
+ :when ts]
+ [handle ts]))
+ dm-pairs
+ (d/q '{:find [?source ?target ?t]
+ :in [$ ?nick]
+ :where
+ [[?e :papod.event/source-nick ?source]
+ [?e :papod.event/target-nick ?target]
+ [?e :papod.event/type "user-message"]
+ [?e :papod.event/created-at ?t]
+ (or-join [?nick ?source ?target]
+ [(= ?source ?nick)]
+ [(= ?target ?nick)])]}
+ db nick)
+ dm-rows
+ (->> dm-pairs
+ (map (fn [[s t ts]]
+ [(if (= s nick) t s) ts]))
+ (group-by first)
+ (map (fn [[peer entries]]
+ [peer (apply max-key
+ #(.getTime ^java.util.Date %)
+ (map second entries))])))]
+ (concat chan-rows dm-rows)))
+
+(defn- handle-chathistory-targets
+ [params client components]
+ (let [{:keys [conn channels]} components
+ nick (client-target client)
+ a1 (parse-chathistory-anchor (second params))
+ a2 (parse-chathistory-anchor
+ (when (>= (count params) 3) (nth params 2)))
+ limit (try
+ (Long/parseLong (last params))
+ (catch Exception _ 50))
+ db (when conn (d/db conn))]
+ (cond
+ (< (count params) 4)
+ [(str "FAIL CHATHISTORY NEED_MORE_PARAMS TARGETS "
+ ":Not enough parameters")]
+
+ (or (nil? a1) (nil? a2)
+ (not= (:kind a1) :timestamp)
+ (not= (:kind a2) :timestamp))
+ [(str "FAIL CHATHISTORY INVALID_PARAMS TARGETS "
+ ":Invalid anchor")]
+
+ :else
+ (let [start (:ts a1)
+ end (:ts a2)
+ rows (chathistory-targets db nick channels)
+ in-window (filter
+ (fn [[_ ts]]
+ (let [iso (iso-time ts)]
+ (and (pos? (compare iso start))
+ (neg? (compare iso end)))))
+ rows)
+ sorted (sort-by (fn [[_ ts]] (.getTime ^java.util.Date ts))
+ in-window)
+ chosen (take limit sorted)
+ batch-id (str (java.util.UUID/randomUUID))]
+ (into
+ [(str "BATCH +" batch-id
+ " draft/chathistory-targets")]
+ (concat
+ (map (fn [[handle ts]]
+ (str "@batch=" batch-id
+ " :" +server-name+
+ " CHATHISTORY TARGETS " handle " "
+ (iso-time ts)))
+ chosen)
+ [(str "BATCH -" batch-id)]))))))
+
(defn- handle-chathistory
[params client components]
(let [{:keys [conn channels]} components
@@ -2813,6 +2954,9 @@
(>= (count params) 4))
(parse-chathistory-anchor (nth params 3)))]
(cond
+ (= subcmd "TARGETS")
+ (handle-chathistory-targets params client components)
+
(< (count params) 3)
[(str "FAIL CHATHISTORY NEED_MORE_PARAMS "
(or subcmd "*")
@@ -2840,7 +2984,8 @@
"draft/event-playback")
all (if chan?
(fetch-history-all db chan-eid)
- (fetch-dm-history-all db nick target))
+ (fetch-dm-history-all
+ db nick (:account @client) target))
all (if event-playback?
all
(filter
@@ -2883,7 +3028,9 @@
target-id (try (java.util.UUID/fromString target-id-str)
(catch Exception _ nil))
db (when conn (d/db conn))
- chan-eid (when db (resolve-channel db handle))
+ chan? (channel-handle? handle)
+ chan-eid (when (and db chan?)
+ (resolve-channel db handle))
target-eid (when (and db target-id)
(ffirst
(d/q '{:find [?e]
@@ -2891,47 +3038,69 @@
:where [[?e :papod.event/id ?id]]}
db target-id)))]
(cond
- (nil? chan-eid)
+ (and chan? (nil? chan-eid))
[(str "FAIL REDACT INVALID_TARGET " handle
" :No such channel")]
(nil? target-eid)
[(str "FAIL REDACT UNKNOWN_MSGID " handle " "
target-id-str " :Message not found")]
- ;; Check ownership or op status
+ ;; Channel: ownership or op; DM: must be author
(let [event (d/entity db target-eid)]
- (and (not= nick (:papod.event/source-nick event))
- (not (is-op? components handle nick))
- (not (has-access? db chan-eid nick
- #{"owner" "op"}))))
+ (if chan?
+ (and (not= nick
+ (:papod.event/source-nick event))
+ (not (is-op? components handle nick))
+ (not (has-access? db chan-eid nick
+ #{"owner" "op"})))
+ (not= nick
+ (:papod.event/source-nick event))))
[(str "FAIL REDACT REDACT_FORBIDDEN " handle " " target-id-str
" :Not authorised to delete this message")]
:else
(let [msg-id (java.util.UUID/randomUUID)
- chan-uuid (:papod.channel/id (d/entity db chan-eid))]
+ chan-uuid (when chan-eid
+ (:papod.channel/id
+ (d/entity db chan-eid)))]
@(d/transact conn
- [{:db/ensure :papod.event/attrs
- :db/id "new-event"
- :papod.event/id msg-id
- :papod.event/channel chan-eid
- :papod.event/type "user-delete"
- :papod.event/source-nick nick
- :papod.event/delete-of target-eid
- :papod.event/created-at (java.util.Date.)}
- [:papod.event/add-seq "new-event" chan-uuid]])
- ;; Deliver REDACT to channel
+ (cond-> [{:db/ensure :papod.event/attrs
+ :db/id "new-event"
+ :papod.event/id msg-id
+ :papod.event/type "user-delete"
+ :papod.event/source-nick nick
+ :papod.event/delete-of target-eid
+ :papod.event/created-at (java.util.Date.)}]
+ chan?
+ (-> (update 0 assoc
+ :papod.event/channel chan-eid)
+ (conj [:papod.event/add-seq
+ "new-event" chan-uuid]))
+ (not chan?)
+ (-> (update 0 assoc
+ :papod.event/target-nick handle))))
+ ;; Deliver REDACT
(let [src (client-prefix client)
line (str ":" src " REDACT " handle
" " target-id-str
(when reason
(str " :" reason)))]
- (when (and clients channels)
- (doseq [mn (get @channels handle)
- :when (not= mn nick)
- :let [m (get @clients mn)
- ca (when m (:client-atom m))
- caps (or (some-> ca deref :caps) #{})]
- :when (and m (caps "draft/message-redaction"))]
- (deliver-to-client! (:w m) line)))
+ (cond
+ chan?
+ (when (and clients channels)
+ (doseq [mn (get @channels handle)
+ :when (not= mn nick)
+ :let [m (get @clients mn)
+ ca (when m (:client-atom m))
+ caps (or (some-> ca deref
+ :caps) #{})]
+ :when (and m (caps "draft/message-redaction"))]
+ (deliver-to-client! (:w m) line)))
+ clients
+ (when-let [m (get @clients handle)]
+ (let [ca (:client-atom m)
+ caps (or (some-> ca deref
+ :caps) #{})]
+ (when (caps "draft/message-redaction")
+ (deliver-to-client! (:w m) line)))))
[line])))))))
(defn- handle-edit
@@ -3118,7 +3287,11 @@
channel-eid (when chan? (resolve-channel db handle))
chan-uuid (when channel-eid
(:papod.channel/id
- (d/entity db channel-eid)))]
+ (d/entity db channel-eid)))
+ src-acct (when-not chan?
+ (:account @client))
+ tgt-acct (when-not chan?
+ (nick-account components handle))]
@(d/transact conn
(cond-> [{:db/ensure :papod.event/attrs
:db/id "new-event"
@@ -3132,7 +3305,15 @@
(conj [:papod.event/add-seq "new-event" chan-uuid]))
(not chan?)
(-> (update 0 assoc
- :papod.event/target-nick handle))))))
+ :papod.event/target-nick handle))
+ src-acct
+ (-> (update 0 assoc
+ :papod.event/source-account
+ src-acct))
+ tgt-acct
+ (-> (update 0 assoc
+ :papod.event/target-account
+ tgt-acct))))))
(if chan?
(doseq [mn (get @channels handle)
:when (not= mn nick)