|
348 | 348 | (re-find vote-down-pattern body-text) :down |
349 | 349 | (re-find vote-null-pattern body-text) :null))) |
350 | 350 |
|
| 351 | +(defn vote-tx |
| 352 | + "Pure: the transaction map recording `vote` for the voter, or nil |
| 353 | + when it matches `current` (a no-op revote). One entity per voter |
| 354 | + per report: a revote carries the `existing` entity id explicitly -- |
| 355 | + no upsert through the unique attr at transact time." |
| 356 | + [{:keys [vote-key report-eid email-eid voter-addr existing current vote]}] |
| 357 | + (when (not= current vote) |
| 358 | + (if existing |
| 359 | + {:db/id existing |
| 360 | + :vote/email email-eid |
| 361 | + :vote/value vote} |
| 362 | + {:vote/key vote-key |
| 363 | + :vote/report report-eid |
| 364 | + :vote/email email-eid |
| 365 | + :vote/value vote |
| 366 | + :vote/voter voter-addr}))) |
| 367 | + |
351 | 368 | ;; --------------------------------------------------------------------------- |
352 | 369 | ;; Command application (effectful) |
353 | 370 | ;; --------------------------------------------------------------------------- |
354 | 371 |
|
355 | 372 | (defn- ref-eid [v] (if (map? v) (:db/id v) v)) |
356 | 373 |
|
357 | 374 | (defn- apply-vote! |
358 | | - "Record a pre-detected `vote` (caller runs `detect-vote`)." |
| 375 | + "Record a pre-detected `vote` (caller runs `detect-vote`); the tx |
| 376 | + itself is built by the pure `vote-tx` (last vote wins)." |
359 | 377 | [conn report-eid from-addr vote email delivery source-cfg] |
360 | 378 | (if-not (common/sent-via-source-channel? delivery source-cfg) |
361 | 379 | (log/info "Vote ignored (private email on public source)" from-addr) |
|
365 | 383 | ;; keyed on the mid hash (unique attr, raw mids unbounded). |
366 | 384 | addr (some-> from-addr str/lower-case) |
367 | 385 | vote-key (str (common/mid-hash report-mid) ":" addr) |
368 | | - ;; One entity per voter per report; a new vote replaces the |
369 | | - ;; previous one (last vote wins). Explicit :db/id on revote |
370 | | - ;; -- no upsert through the unique attr at transact time. |
371 | 386 | existing (d/entid db [:vote/key vote-key]) |
372 | 387 | current (when existing |
373 | 388 | (:vote/value (d/pull db [:vote/value] existing)))] |
374 | | - (when (not= current vote) |
375 | | - (d/transact! conn [(if existing |
376 | | - {:db/id existing |
377 | | - :vote/email (:db/id email) |
378 | | - :vote/value vote} |
379 | | - {:vote/key vote-key |
380 | | - :vote/report report-eid |
381 | | - :vote/email (:db/id email) |
382 | | - :vote/value vote |
383 | | - :vote/voter addr})]) |
| 389 | + (when-let [tx (vote-tx {:vote-key vote-key |
| 390 | + :report-eid report-eid |
| 391 | + :email-eid (:db/id email) |
| 392 | + :voter-addr addr |
| 393 | + :existing existing |
| 394 | + :current current |
| 395 | + :vote vote})] |
| 396 | + (d/transact! conn [tx]) |
384 | 397 | (tracking/bump-report-updated! conn report-eid) |
385 | 398 | (log/info (cond-> (str "Vote " (case vote :up "+1" :down "-1" "0") |
386 | 399 | " by " from-addr) |
|
650 | 663 | :unset-related (str syntax ": " target-message-id) |
651 | 664 | syntax))) |
652 | 665 |
|
| 666 | +(defn partition-lines-by-scope |
| 667 | + "Pure decision behind `filter-permitted-lines`: split the `line-pred` |
| 668 | + subset of `lines` into {:allowed [...] :denied [...]} by whether the |
| 669 | + effective scope permits `from-addr` to act (`current-d` as in |
| 670 | + `scope-permits?` -- a realized snapshot makes this side-effect |
| 671 | + free)." |
| 672 | + [lines current-d from-addr is-maintainer? line-pred] |
| 673 | + (let [{allowed true denied false} |
| 674 | + (group-by (fn [{:keys [scope attr]}] |
| 675 | + (boolean (scope-permits? scope attr from-addr |
| 676 | + is-maintainer? current-d))) |
| 677 | + (filterv line-pred lines))] |
| 678 | + {:allowed (vec allowed) :denied (vec denied)})) |
| 679 | + |
653 | 680 | (defn- filter-permitted-lines |
654 | | - "Subset of `lines` passing `line-pred` whose scope permits |
655 | | - `from-addr` to act (`current-d` as in `scope-permits?`). With a |
656 | | - non-nil `failure-ctx`, scope-rejected lines are recorded as |
657 | | - :insufficient-scope failures, audience :maintainers." |
| 681 | + "Effectful shell over `partition-lines-by-scope`: with a non-nil |
| 682 | + `failure-ctx`, each denied line is recorded as an |
| 683 | + :insufficient-scope failure (audience :maintainers); the allowed |
| 684 | + lines are returned." |
658 | 685 | [lines current-d from-addr is-maintainer? line-pred failure-ctx] |
659 | | - ;; Eager realization via `filterv` so the recording side effect in |
660 | | - ;; the denial branch fires deterministically, regardless of whether |
661 | | - ;; callers seq or reduce over the result. |
662 | | - (filterv (fn [{:keys [scope attr] :as line}] |
663 | | - (and (line-pred line) |
664 | | - (or (scope-permits? scope attr from-addr is-maintainer? current-d) |
665 | | - (do (when failure-ctx |
666 | | - (record-failure! |
667 | | - (assoc failure-ctx |
668 | | - :reason :insufficient-scope |
669 | | - :audience :maintainers |
670 | | - :command (describe-denied-line line)))) |
671 | | - false)))) |
672 | | - lines)) |
| 686 | + (let [{:keys [allowed denied]} |
| 687 | + (partition-lines-by-scope lines current-d from-addr |
| 688 | + is-maintainer? line-pred)] |
| 689 | + (when failure-ctx |
| 690 | + (doseq [line denied] |
| 691 | + (record-failure! (assoc failure-ctx |
| 692 | + :reason :insufficient-scope |
| 693 | + :audience :maintainers |
| 694 | + :command (describe-denied-line line))))) |
| 695 | + allowed)) |
673 | 696 |
|
674 | 697 | (defn- report-eid-by-mid |
675 | 698 | "Report eid for `target-mid`, matching the root or any thread |
|
1121 | 1144 | [attr] |
1122 | 1145 | (some-> (attr->word-cmd attr) :id name str/capitalize (str "."))) |
1123 | 1146 |
|
| 1147 | +(defn partition-words-by-scope |
| 1148 | + "Pure decision behind `filter-words-by-scope`: split `word-result` |
| 1149 | + into {:allowed <word-map-or-nil> :denied [attr ...]} by each |
| 1150 | + bareword's effective scope. :report/close-reason is no bareword: |
| 1151 | + it rides back onto a non-empty allowed map (build-word-tx only |
| 1152 | + consumes it when :report/closed is set)." |
| 1153 | + [word-result overrides is-maintainer?] |
| 1154 | + (let [{allowed true denied false} |
| 1155 | + (group-by (fn [[attr _]] |
| 1156 | + (let [cmd (attr->word-cmd attr) |
| 1157 | + scope (or (:scope (get overrides (:id cmd))) (:scope cmd))] |
| 1158 | + (word-scope-permits? scope is-maintainer?))) |
| 1159 | + (dissoc word-result :report/close-reason)) |
| 1160 | + allowed-map (into {} allowed)] |
| 1161 | + {:allowed (when (seq allowed-map) |
| 1162 | + (cond-> allowed-map |
| 1163 | + (:report/close-reason word-result) |
| 1164 | + (assoc :report/close-reason (:report/close-reason word-result)))) |
| 1165 | + :denied (mapv key denied)})) |
| 1166 | + |
1124 | 1167 | (defn- filter-words-by-scope |
1125 | | - "Filter `word-result` to the subset allowed by the effective scope |
1126 | | - for each bareword. When `failure-ctx` is non-nil, barewords that fail |
1127 | | - the scope check are recorded as :insufficient-scope failures with |
1128 | | - \":audience :maintainers\", so denied attempts surface to maintainer |
1129 | | - subscribers via the notification loop." |
| 1168 | + "Effectful shell over `partition-words-by-scope`: with a non-nil |
| 1169 | + `failure-ctx`, each denied bareword is recorded as an |
| 1170 | + :insufficient-scope failure (audience :maintainers, so denied |
| 1171 | + attempts surface to maintainer subscribers via the notification |
| 1172 | + loop); the allowed map is returned." |
1130 | 1173 | [word-result overrides is-maintainer? failure-ctx] |
1131 | 1174 | (when word-result |
1132 | | - (let [filtered (into {} |
1133 | | - (keep (fn [[attr :as entry]] |
1134 | | - (let [cmd (attr->word-cmd attr) |
1135 | | - scope (or (:scope (get overrides (:id cmd))) (:scope cmd))] |
1136 | | - (if (word-scope-permits? scope is-maintainer?) |
1137 | | - entry |
1138 | | - (do (when failure-ctx |
1139 | | - (record-failure! |
1140 | | - (assoc failure-ctx |
1141 | | - :reason :insufficient-scope |
1142 | | - :audience :maintainers |
1143 | | - :command (describe-denied-word attr)))) |
1144 | | - nil))))) |
1145 | | - (dissoc word-result :report/close-reason))] |
1146 | | - (when (seq filtered) |
1147 | | - (cond-> filtered |
1148 | | - (:report/close-reason word-result) (assoc :report/close-reason |
1149 | | - (:report/close-reason word-result))))))) |
| 1175 | + (let [{:keys [allowed denied]} |
| 1176 | + (partition-words-by-scope word-result overrides is-maintainer?)] |
| 1177 | + (when failure-ctx |
| 1178 | + (doseq [attr denied] |
| 1179 | + (record-failure! (assoc failure-ctx |
| 1180 | + :reason :insufficient-scope |
| 1181 | + :audience :maintainers |
| 1182 | + :command (describe-denied-word attr))))) |
| 1183 | + allowed))) |
1150 | 1184 |
|
1151 | 1185 | ;; Carrier-eligible commands: when a mail simultaneously creates a new |
1152 | 1186 | ;; report AND carries one of these annotations, the annotation applies |
|
0 commit comments