aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorF. Jason Park2024-05-22 22:59:54 -0700
committerF. Jason Park2024-05-27 16:46:40 -0700
commit6888bbbe832e14c3aaaa2c9750ed27e577e0983d (patch)
tree890d80927ef23f1b7d330ca6aa9a1ebd265fe963
parent5f84213c9802181b4d800615915e3c8dded7b94f (diff)
downloademacs-6888bbbe832e14c3aaaa2c9750ed27e577e0983d.tar.gz
emacs-6888bbbe832e14c3aaaa2c9750ed27e577e0983d.zip
Add ERC module querypoll as monitor placeholder
* doc/misc/erc.texi: Add module `querypoll' to list of built-in modules'. * etc/ERC-NEWS: Mention new module `querypoll', and explain new default behavior for deriving query membership from that of channels. * lisp/erc/erc-goodies.el (erc--querypoll-ring) (erc--querypoll-timer): New variables. (erc-querypoll-exclude-regexp): New option. (erc-querypoll-mode, erc-querypoll-enable, erc-querypoll-disable): New module for polling with "WHO" requests for the presence of otherwise "untracked" query targets. (erc-querypoll-period-params): New variable. (erc--querypoll-compute-period) (erc--querypoll-target-in-chan-p) (erc--querypoll-get-length) (erc--querypoll-get-next) (erc--querypoll-subscribe) (erc--querypoll-on-352) (erc--querypoll-send): New functions. * lisp/erc/erc-speedbar.el (erc-speedbar-buttons): Dispatch queries as if they were channels when `erc--queries-current-p' returns non-nil. That is, show head counts alongside query targets as users come and go. (erc-speedbar-insert-target): Defer to `erc--queries-current-p' to know whether to show a query in the style of a channel. This affects both the plain speedbar integration as well as the `nickbar' module added for bug#63595. Also, use question marks rather than the empty string for query bullets, so that query and channel items are aligned vertically. * lisp/erc/erc.el (erc--queries-current-p): New function. * test/lisp/erc/erc-goodies-tests.el (erc--querypoll-compute-period) (erc--querypoll-target-in-chan-p) (erc--querypoll-get-length) (erc--querypoll-get-next): New tests. (Bug#70928)
-rw-r--r--doc/misc/erc.texi4
-rw-r--r--etc/ERC-NEWS28
-rw-r--r--lisp/erc/erc-goodies.el190
-rw-r--r--lisp/erc/erc-speedbar.el13
-rw-r--r--lisp/erc/erc.el5
-rw-r--r--test/lisp/erc/erc-goodies-tests.el57
6 files changed, 292 insertions, 5 deletions
diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index 0c7e3b09f41..c7cbf7908b8 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -518,6 +518,10 @@ or your nickname is mentioned
518@item page 518@item page
519Process CTCP PAGE requests from IRC 519Process CTCP PAGE requests from IRC
520 520
521@cindex modules, querypoll
522@item querypoll
523Update query participant data by continually polling the server
524
521@cindex modules, readonly 525@cindex modules, readonly
522@item readonly 526@item readonly
523Make displayed lines read-only 527Make displayed lines read-only
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index acad0f03572..1fad62e1999 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -100,6 +100,18 @@ one's optionally accessible from the keyboard, just like any other
100side window. Hit '<RET>' over a nick to spawn a "/QUERY" or a 100side window. Hit '<RET>' over a nick to spawn a "/QUERY" or a
101"Lastlog" (Occur) session. See 'erc-nickbar-mode' for more. 101"Lastlog" (Occur) session. See 'erc-nickbar-mode' for more.
102 102
103** New module to keep tabs on query pals who aren't in your channels.
104ERC has gotten a bit pickier about managing participants in query
105buffers. "Untracked" correspondents no longer appear automatically in
106membership tables, even if you respond or initiate contact. Instead,
107ERC only adds and removes participant data when these same users join
108and leave channels. Anyone uncomfortable with the apparent
109uncertainty this brings can look to the new 'querypoll' module, which
110periodically sends WHO requests to keep track of correspondents.
111Those familiar with the IRCv3 Monitor extension can think of this as
112"fallback code" and a temporary placeholder for the real thing.
113Add 'querypoll' (and 'nickbar') to 'erc-modules' to try it out.
114
103** Option 'erc-timestamp-use-align-to' more versatile. 115** Option 'erc-timestamp-use-align-to' more versatile.
104While this option has always offered to right-align stamps via the 116While this option has always offered to right-align stamps via the
105'display' text property, it's now more effective at doing so when set 117'display' text property, it's now more effective at doing so when set
@@ -563,6 +575,22 @@ redubbed 'erc-channel-members'. Similarly, the utility function
563'erc-get-channel-user' has been renamed to 'erc-get-channel-member'. 575'erc-get-channel-user' has been renamed to 'erc-get-channel-member'.
564Expect deprecations of the old names to follow in a future release. 576Expect deprecations of the old names to follow in a future release.
565 577
578*** Query participant tables now depend on channel membership.
579ERC has always been inconsistent and difficult to predict in its
580handling of records describing other IRC users. This has made simple
581things like detecting the online status of query peers and the
582presence of one's own user in 'erc-server-users' especially
583unreliable. From now on, ERC resolves to be more sensible and
584conservative in such areas. For example, it now retains its own user
585info, once discovered, for the remainder of a session. It also relies
586solely on channel membership to "drive" query participant information.
587That is, when another IRC user departs their last known channel, any
588queries with them will consider them absent, even if they're likely
589still online. Anyone with difficulty adapting to this new paradigm
590should contact the mailing list to inquire about associated
591compatibility flags, which can be made public on request. Also see
592the related news item announcing the module 'querypoll'.
593
566*** The 'erc-channel-user' struct has a changed internally. 594*** The 'erc-channel-user' struct has a changed internally.
567The five boolean slots for membership prefixes have been folded 595The five boolean slots for membership prefixes have been folded
568("encoded") into a single integer slot. However, the old 'setf'-able 596("encoded") into a single integer slot. However, the old 'setf'-able
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index fe44c3bdfcb..9837ec302ee 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -1114,6 +1114,196 @@ servers. If called from a program, PROC specifies the server process."
1114 nil erc-server-process))) 1114 nil erc-server-process)))
1115 (multi-occur (erc-buffer-list nil proc) string)) 1115 (multi-occur (erc-buffer-list nil proc) string))
1116 1116
1117
1118;;;; querypoll
1119
1120(declare-function ring-empty-p "ring" (ring))
1121(declare-function ring-insert "ring" (ring item))
1122(declare-function ring-insert+extend "ring" (ring item))
1123(declare-function ring-length "ring" (ring))
1124(declare-function ring-member "ring" (ring item))
1125(declare-function ring-ref "ring" (ring index))
1126(declare-function ring-remove "ring" (ring &optional index))
1127
1128(defvar-local erc--querypoll-ring nil)
1129(defvar-local erc--querypoll-timer nil)
1130
1131(defcustom erc-querypoll-exclude-regexp
1132 (rx bot (or (: "*" (+ nonl)) (: (+ (in "A-Za-z")) "Serv")) eot)
1133 "Pattern to skip polling for bots and services you regularly query."
1134 :group 'erc
1135 :package-version '(ERC . "5.6")
1136 :type 'regexp)
1137
1138;;;###autoload(autoload 'erc-querypoll-mode "erc-goodies" nil t)
1139(define-erc-module querypoll nil
1140 "Send periodic \"WHO\" requests for each query buffer.
1141Omit query participants who are currently present in some channel.
1142Instead of announcing arrivals and departures, rely on other modules,
1143like `nickbar', to provide UI feedback when changes occur.
1144
1145Once ERC implements the `monitor' extension, this module will serve as
1146an optional fallback for keeping query-participant rolls up to date on
1147servers that lack support or are stingy with their allotments. Until
1148such time, this module should be considered experimental.
1149
1150This is a local ERC module, so selectively polling only a subset of
1151query targets is possible but cumbersome. To do so, ensure
1152`erc-querypoll-mode' is enabled in the server buffer, and then toggle it
1153as appropriate in desired query buffers. To stop polling for the
1154current connection, toggle off the command \\[erc-querypoll-mode] from a
1155server buffer, or run \\`M-x C-u erc-querypoll-disable RET' from a
1156target buffer."
1157 ((if erc--target
1158 (if (erc-query-buffer-p)
1159 (progn ; accommodate those who eschew `erc-modules'
1160 (erc-with-server-buffer
1161 (unless erc-querypoll-mode
1162 (erc-querypoll-mode +1)))
1163 (erc--querypoll-subscribe (current-buffer)))
1164 (erc-querypoll-mode -1))
1165 (cl-assert (not erc--decouple-query-and-channel-membership-p))
1166 (setq-local erc--querypoll-ring (make-ring 5))
1167 (erc-with-all-buffers-of-server erc-server-process nil
1168 (unless erc-querypoll-mode
1169 (erc-querypoll-mode +1)))))
1170 ((when erc--querypoll-timer
1171 (cancel-timer erc--querypoll-timer))
1172 (if erc--target
1173 (when-let (((erc-query-buffer-p))
1174 (ring (erc-with-server-buffer erc--querypoll-ring))
1175 (index (ring-member ring (current-buffer)))
1176 ((not (erc--querypoll-target-in-chan-p (current-buffer)))))
1177 (ring-remove ring index)
1178 (unless (erc-current-nick-p (erc-target))
1179 (erc-remove-current-channel-member (erc-target))))
1180 (erc-with-all-buffers-of-server erc-server-process #'erc-query-buffer-p
1181 (erc-querypoll-mode -1)))
1182 (kill-local-variable 'erc--querypoll-ring)
1183 (kill-local-variable 'erc--querypoll-timer))
1184 'local)
1185
1186(cl-defmethod erc--queries-current-p (&context (erc-querypoll-mode (eql t))) t)
1187
1188(defvar erc-querypoll-period-params '(10 10 1)
1189 "Parameters affecting the delay with respect to the number of buffers.
1190The elements represent some parameters of an exponential decay function,
1191a(e)^{-x/b}+c. The first number (a) affects the overall scaling. A
1192higher value means longer delays for all query buffers relative to queue
1193length. The second number (b) determines how quickly the delay
1194decreases as the queue length increases. Larger values make the delay
1195taper off more gradually. The last number (c) sets the minimum delay
1196between updates regardless of queue length.")
1197
1198(defun erc--querypoll-compute-period (queue-size)
1199 "Calculate delay based on QUEUE-SIZE."
1200 (let ((scale (nth 0 erc-querypoll-period-params))
1201 (rate (* 1.0 (nth 1 erc-querypoll-period-params)))
1202 (min (nth 2 erc-querypoll-period-params)))
1203 (+ (* scale (exp (/ (- queue-size) rate))) min)))
1204
1205(defun erc--querypoll-target-in-chan-p (buffer)
1206 "Determine whether buffer's target, as a user, is joined to any channels."
1207 (and-let*
1208 ((target (erc--target-string (buffer-local-value 'erc--target buffer)))
1209 (user (erc-get-server-user target))
1210 (buffers (erc-server-user-buffers user))
1211 ((seq-some #'erc-channel-p buffers)))))
1212
1213(defun erc--querypoll-get-length (ring)
1214 "Return the effective length of RING, discounting chan members."
1215 (let ((count 0))
1216 (dotimes (i (ring-length ring))
1217 (unless (erc--querypoll-target-in-chan-p (ring-ref ring i))
1218 (cl-incf count 1)))
1219 count))
1220
1221(defun erc--querypoll-get-next (ring)
1222 (let ((n (ring-length ring)))
1223 (catch 'found
1224 (while (natnump (cl-decf n))
1225 (when-let ((buffer (ring-remove ring))
1226 ((buffer-live-p buffer)))
1227 ;; Push back buffers for users joined to some chan.
1228 (if (erc--querypoll-target-in-chan-p buffer)
1229 (ring-insert ring buffer)
1230 (throw 'found buffer)))))))
1231
1232(defun erc--querypoll-subscribe (query-buffer &optional penalty)
1233 "Add QUERY-BUFFER to FIFO and ensure timer is running."
1234 (when query-buffer
1235 (cl-assert (erc-query-buffer-p query-buffer)))
1236 (erc-with-server-buffer
1237 (when (and query-buffer
1238 (not (with-current-buffer query-buffer
1239 (or (erc-current-nick-p (erc-target))
1240 (string-match erc-querypoll-exclude-regexp
1241 (erc-target)))))
1242 (not (ring-member erc--querypoll-ring query-buffer)))
1243 (ring-insert+extend erc--querypoll-ring query-buffer))
1244 (unless erc--querypoll-timer
1245 (setq erc--querypoll-timer
1246 (let* ((length (erc--querypoll-get-length erc--querypoll-ring))
1247 (period (erc--querypoll-compute-period length)))
1248 (run-at-time (+ (or penalty 0) period)
1249 nil #'erc--querypoll-send (current-buffer)))))))
1250
1251(defun erc--querypoll-on-352 (target-nick args)
1252 "Add or update `erc-server-users' data for TARGET-NICK from ARGS.
1253Then add user to participant rolls in any existing query buffers."
1254 (pcase-let
1255 ((`(,_ ,channel ,login ,host ,_server ,nick ,_flags, hop-real) args))
1256 (when (and (string= channel "*") (erc-nick-equal-p nick target-nick))
1257 (if-let ((user (erc-get-server-user nick)))
1258 (erc-update-user user nick host login
1259 (erc--extract-352-full-name hop-real))
1260 ;; Don't add unless target is already known.
1261 (when (erc-get-buffer nick erc-server-process)
1262 (erc-add-server-user
1263 nick (make-erc-server-user
1264 :nickname nick :login login :host host
1265 :full-name (erc--extract-352-full-name hop-real)))))
1266 (erc--ensure-query-member nick)
1267 t)))
1268
1269;; This uses heuristics to associate replies to the initial request
1270;; because ERC does not yet support `labeled-response'.
1271(defun erc--querypoll-send (server-buffer)
1272 "Send a captive \"WHO\" in SERVER-BUFFER."
1273 (when (and (buffer-live-p server-buffer)
1274 (buffer-local-value 'erc-server-connected server-buffer))
1275 (with-current-buffer server-buffer
1276 (setq erc--querypoll-timer nil)
1277 (if-let ((buffer (erc--querypoll-get-next erc--querypoll-ring)))
1278 (letrec
1279 ((target (erc--target-string
1280 (buffer-local-value 'erc--target buffer)))
1281 (penalty 0)
1282 (here-fn (erc-once-with-server-event
1283 "352" (lambda (_ parsed)
1284 (erc--querypoll-on-352
1285 target (erc-response.command-args parsed)))))
1286 (done-fn (erc-once-with-server-event
1287 "315"
1288 (lambda (_ parsed)
1289 (if (memq here-fn erc-server-352-functions)
1290 (erc-remove-user
1291 (nth 1 (erc-response.command-args parsed)))
1292 (remove-hook 'erc-server-352-functions here-fn t))
1293 (remove-hook 'erc-server-263-functions fail-fn t)
1294 (remove-hook 'erc-server-315-functions done-fn t)
1295 (erc--querypoll-subscribe buffer penalty)
1296 t)))
1297 (fail-fn (erc-once-with-server-event
1298 "263"
1299 (lambda (proc parsed)
1300 (setq penalty 60)
1301 (funcall done-fn proc parsed)
1302 t))))
1303 (erc-server-send (concat "WHO " target)))
1304 (unless (ring-empty-p erc--querypoll-ring)
1305 (erc--querypoll-subscribe nil 30))))))
1306
1117(provide 'erc-goodies) 1307(provide 'erc-goodies)
1118 1308
1119;;; erc-goodies.el ends here 1309;;; erc-goodies.el ends here
diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index 9cde452be58..d4f91bb363a 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -133,7 +133,7 @@ This will add a speedbar major display mode."
133(defun erc-speedbar-buttons (buffer) 133(defun erc-speedbar-buttons (buffer)
134 "Create buttons for speedbar in BUFFER." 134 "Create buttons for speedbar in BUFFER."
135 (erase-buffer) 135 (erase-buffer)
136 (let (serverp chanp queryp) 136 (let (serverp chanp queryp queries-current-p)
137 (with-current-buffer buffer 137 (with-current-buffer buffer
138 ;; The function `dframe-help-echo' checks the default value of 138 ;; The function `dframe-help-echo' checks the default value of
139 ;; `dframe-help-echo-function' when deciding whether to visit 139 ;; `dframe-help-echo-function' when deciding whether to visit
@@ -145,13 +145,14 @@ This will add a speedbar major display mode."
145 (setq-local dframe-help-echo-function #'ignore) 145 (setq-local dframe-help-echo-function #'ignore)
146 (setq serverp (erc--server-buffer-p)) 146 (setq serverp (erc--server-buffer-p))
147 (setq chanp (erc-channel-p (erc-default-target))) 147 (setq chanp (erc-channel-p (erc-default-target)))
148 (setq queryp (erc-query-buffer-p))) 148 (setq queryp (erc-query-buffer-p)
149 queries-current-p (erc--queries-current-p)))
149 (defvar erc-nickbar-mode) 150 (defvar erc-nickbar-mode)
150 (cond ((and erc-nickbar-mode (null (get-buffer-window speedbar-buffer))) 151 (cond ((and erc-nickbar-mode (null (get-buffer-window speedbar-buffer)))
151 (run-at-time 0 nil #'erc-nickbar-mode -1)) 152 (run-at-time 0 nil #'erc-nickbar-mode -1))
152 (serverp 153 (serverp
153 (erc-speedbar-channel-buttons nil 0 buffer)) 154 (erc-speedbar-channel-buttons nil 0 buffer))
154 (chanp 155 ((or chanp (and queryp queries-current-p))
155 (erc-speedbar-insert-target buffer 0) 156 (erc-speedbar-insert-target buffer 0)
156 (forward-line -1) 157 (forward-line -1)
157 (erc-speedbar-expand-channel "+" buffer 0)) 158 (erc-speedbar-expand-channel "+" buffer 0))
@@ -205,7 +206,8 @@ This will add a speedbar major display mode."
205 t))))) 206 t)))))
206 207
207(defun erc-speedbar-insert-target (buffer depth) 208(defun erc-speedbar-insert-target (buffer depth)
208 (if (erc--target-channel-p (buffer-local-value 'erc--target buffer)) 209 (if (with-current-buffer buffer
210 (or (erc--target-channel-p erc--target) (erc--queries-current-p)))
209 (progn 211 (progn
210 (speedbar-make-tag-line 212 (speedbar-make-tag-line
211 'bracket ?+ 'erc-speedbar-expand-channel buffer 213 'bracket ?+ 'erc-speedbar-expand-channel buffer
@@ -218,8 +220,9 @@ This will add a speedbar major display mode."
218 (speedbar-add-indicator (format "(%d)" (hash-table-count table))) 220 (speedbar-add-indicator (format "(%d)" (hash-table-count table)))
219 (rx "(" (+ (any "0-9")) ")")))) 221 (rx "(" (+ (any "0-9")) ")"))))
220 ;; Query target 222 ;; Query target
223 (cl-assert (erc-query-buffer-p buffer))
221 (speedbar-make-tag-line 224 (speedbar-make-tag-line
222 nil nil nil nil 225 'bracket ?? nil nil
223 (buffer-name buffer) 'erc-speedbar-goto-buffer buffer nil 226 (buffer-name buffer) 'erc-speedbar-goto-buffer buffer nil
224 depth))) 227 depth)))
225 228
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 565f18163df..b375df1edb6 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -557,6 +557,11 @@ user from `erc-server-users'. Note that enabling this compatibility
557flag degrades the user experience and isn't guaranteed to correctly 557flag degrades the user experience and isn't guaranteed to correctly
558restore the described historical behavior.") 558restore the described historical behavior.")
559 559
560(cl-defmethod erc--queries-current-p ()
561 "Return non-nil if ERC actively updates query manifests."
562 (and (not erc--decouple-query-and-channel-membership-p)
563 (erc-query-buffer-p) (erc-get-channel-member (erc-target))))
564
560(defun erc--ensure-query-member (nick) 565(defun erc--ensure-query-member (nick)
561 "Populate membership table in query buffer for online NICK." 566 "Populate membership table in query buffer for online NICK."
562 (erc-with-buffer (nick) 567 (erc-with-buffer (nick)
diff --git a/test/lisp/erc/erc-goodies-tests.el b/test/lisp/erc/erc-goodies-tests.el
index 7cbaa39d3f7..ead0bf5a979 100644
--- a/test/lisp/erc/erc-goodies-tests.el
+++ b/test/lisp/erc/erc-goodies-tests.el
@@ -609,4 +609,61 @@
609 (should (equal '(3 . 11) (erc--get-inserted-msg-bounds arg)))))) 609 (should (equal '(3 . 11) (erc--get-inserted-msg-bounds arg))))))
610 610
611 611
612;;;; querypoll
613
614(ert-deftest erc--querypoll-compute-period ()
615 (should (equal (mapcar (lambda (i)
616 (/ (round (* 100 (erc--querypoll-compute-period i)))
617 100.0))
618 (number-sequence 0 10))
619 '(11.0 10.05 9.19 8.41 7.7 7.07 6.49 5.97 5.49 5.07 4.68))))
620
621(declare-function ring-insert "ring" (ring item))
622
623(ert-deftest erc--querypoll-target-in-chan-p ()
624 (erc-tests-common-make-server-buf)
625 (with-current-buffer (erc--open-target "#chan")
626 (erc-update-current-channel-member "bob" "bob" 'addp))
627
628 (with-current-buffer (erc--open-target "bob")
629 (should (erc--querypoll-target-in-chan-p (current-buffer))))
630
631 (with-current-buffer (erc--open-target "alice")
632 (should-not (erc--querypoll-target-in-chan-p (current-buffer))))
633
634 (when noninteractive
635 (erc-tests-common-kill-buffers)))
636
637(ert-deftest erc--querypoll-get-length ()
638 (erc-tests-common-make-server-buf)
639 (with-current-buffer (erc--open-target "#chan")
640 (erc-update-current-channel-member "bob" "bob" 'addp))
641
642 (let ((ring (make-ring 5)))
643 (ring-insert ring (with-current-buffer (erc--open-target "bob")))
644 (should (= 0 (erc--querypoll-get-length ring)))
645 (ring-insert ring (with-current-buffer (erc--open-target "alice")))
646 (should (= 1 (erc--querypoll-get-length ring))))
647
648 (when noninteractive
649 (erc-tests-common-kill-buffers)))
650
651(ert-deftest erc--querypoll-get-next ()
652 (erc-tests-common-make-server-buf)
653 (with-current-buffer (erc--open-target "#chan")
654 (erc-update-current-channel-member "bob" "bob" 'addp)
655 (erc-update-current-channel-member "alice" "alice" 'addp))
656
657 (let ((ring (make-ring 5)))
658 (ring-insert ring (with-current-buffer (erc--open-target "bob")))
659 (ring-insert ring (with-current-buffer (erc--open-target "dummy")))
660 (ring-insert ring (with-current-buffer (erc--open-target "alice")))
661 (ring-insert ring (with-current-buffer (erc--open-target "tester")))
662 (kill-buffer (get-buffer "dummy"))
663
664 (should (eq (get-buffer "tester") (erc--querypoll-get-next ring))))
665
666 (when noninteractive
667 (erc-tests-common-kill-buffers)))
668
612;;; erc-goodies-tests.el ends here 669;;; erc-goodies-tests.el ends here