aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorF. Jason Park2022-12-18 19:01:40 -0800
committerF. Jason Park2023-04-08 14:23:51 -0700
commit8184a815aff52cbf1f1b8680d80af2fbf2dce248 (patch)
treef168a082dd2c4f9bfd8f33fa0700ad175a671093
parent1f1cd467c6afc8100a338e9b44bae8cebfa093f6 (diff)
downloademacs-8184a815aff52cbf1f1b8680d80af2fbf2dce248.tar.gz
emacs-8184a815aff52cbf1f1b8680d80af2fbf2dce248.zip
Add erc-button helper for substituting command keys
* lisp/erc/erc-button.el (erc-button-mode, erc-button-enable): Warn if `erc-button-alist' contains deprecated FORM field in `nicknames' entry. (erc-button-alist): Discourage arbitrary sexp form for third item of entries and offer more useful bounds-modifying function in its place. Mention that anything other than `erc-button-buttonize-nicks' is deprecated as the FORM field in a `nicknames' entry. Bump package-version even though this doesn't introduce a visible change in the default value. (erc-button--maybe-warn-arbitrary-sexp): Add helper for validating third `erc-button-alist' field. (erc-button--check-nicknames-entry): Add helper to check for deprecated items in `erc-button-alist'. (erc-button--preserve-bounds): Add function to serve as default value for `erc-button--modify-nick-function). (erc-button--modify-nick-function): Add new variable to hold a function that filters nickname bounds when buttonizing. (erc-button--phantom-users, erc-button--add-phantom-speaker, erc-button--phantom-users-mode): Add new internal minor mode for treating unseen speakers of PRIVMSGs as known members of the server for things like coloring nicks during buffer playback. (erc-button--get-user-from-speaker-naive): Add temporary utility function to scrape nick from speaker in narrowed buffer. This will be replaced by an account-aware version in next major ERC release. (erc-button-add-nickname-buttons): Accommodate function variant for "form" field of `erc-button-alist' entries. Minor optimizations. This function will likely become the primary juncture for applying text properties that support nickname-related user-intelligence features. (erc-button-add-buttons-1): Show warning when arbitrary sexp for third "form" field encountered. Accommodate binary function instead. (erc-button--substitute-command-keys-in-region): Add helper function for applying key substitutions in ERC warning messages. (erc-button--display-error-notice-with-keys): Add new helper function for displaying ad hoc warnings that possibly require key substitution. (erc-button--display-error-notice-with-keys-and-warn): Add variant of `erc-button--display-error-notice-with-keys' that also emits warnings. * lisp/erc/erc-networks.el (erc-networks--ensure-announced, erc-networks--on-MOTD-end): Use new key-substitutions helper from erc-button. * test/lisp/erc/erc-tests.el (erc-button--display-error-notice-with-keys): New test. * test/lisp/erc/resources/base/assoc/bouncer-history/foonet.eld: Add unknown speaker in channel for phantom store to handle. Currently requires manual intervention to leverage. (Bug#60933.)
-rw-r--r--lisp/erc/erc-button.el208
-rw-r--r--lisp/erc/erc-networks.el20
-rw-r--r--test/lisp/erc/erc-tests.el61
-rw-r--r--test/lisp/erc/resources/base/assoc/bouncer-history/foonet.eld1
4 files changed, 270 insertions, 20 deletions
diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el
index 48f6a5d1794..33e69f3b0b8 100644
--- a/lisp/erc/erc-button.el
+++ b/lisp/erc/erc-button.el
@@ -52,7 +52,8 @@
52;;;###autoload(autoload 'erc-button-mode "erc-button" nil t) 52;;;###autoload(autoload 'erc-button-mode "erc-button" nil t)
53(define-erc-module button nil 53(define-erc-module button nil
54 "This mode buttonizes all messages according to `erc-button-alist'." 54 "This mode buttonizes all messages according to `erc-button-alist'."
55 ((add-hook 'erc-insert-modify-hook #'erc-button-add-buttons 'append) 55 ((erc-button--check-nicknames-entry)
56 (add-hook 'erc-insert-modify-hook #'erc-button-add-buttons 'append)
56 (add-hook 'erc-send-modify-hook #'erc-button-add-buttons 'append) 57 (add-hook 'erc-send-modify-hook #'erc-button-add-buttons 'append)
57 (add-hook 'erc-complete-functions #'erc-button-next-function) 58 (add-hook 'erc-complete-functions #'erc-button-next-function)
58 (erc--modify-local-map t "<backtab>" #'erc-button-previous)) 59 (erc--modify-local-map t "<backtab>" #'erc-button-previous))
@@ -165,8 +166,17 @@ REGEXP is the string matching text around the button or a symbol
165BUTTON is the number of the regexp grouping actually matching the 166BUTTON is the number of the regexp grouping actually matching the
166 button. This is ignored if REGEXP is `nicknames'. 167 button. This is ignored if REGEXP is `nicknames'.
167 168
168FORM is a Lisp expression which must eval to true for the button to 169FORM is a Lisp symbol for a special variable whose value must be
169 be added. 170 true for the button to be added. Alternatively, when REGEXP is
171 not `nicknames', FORM can be a function whose arguments are BEG
172 and END, the bounds of the button in the current buffer. It's
173 expected to return a cons of (possibly identical) bounds or
174 nil, to deny. For the extent of the call, all face options
175 defined for the button module are re-bound, shadowing
176 themselves, so the function is free to change their values.
177 When regexp is the special symbol `nicknames', FORM must be the
178 symbol `erc-button-buttonize-nicks'. Specifying anything else
179 is deprecated.
170 180
171CALLBACK is the function to call when the user push this button. 181CALLBACK is the function to call when the user push this button.
172 CALLBACK can also be a symbol. Its variable value will be used 182 CALLBACK can also be a symbol. Its variable value will be used
@@ -176,7 +186,7 @@ PAR is a number of a regexp grouping whose text will be passed to
176 CALLBACK. There can be several PAR arguments. If REGEXP is 186 CALLBACK. There can be several PAR arguments. If REGEXP is
177 `nicknames', these are ignored, and CALLBACK will be called with 187 `nicknames', these are ignored, and CALLBACK will be called with
178 the nickname matched as the argument." 188 the nickname matched as the argument."
179 :package-version '(ERC . "5.5") 189 :package-version '(ERC . "5.6") ; FIXME sync on release
180 :type '(repeat 190 :type '(repeat
181 (list :tag "Button" 191 (list :tag "Button"
182 (choice :tag "Matches" 192 (choice :tag "Matches"
@@ -277,22 +287,127 @@ specified by `erc-button-alist'."
277 (concat "\\<" (regexp-quote (car elem)) "\\>") 287 (concat "\\<" (regexp-quote (car elem)) "\\>")
278 entry))))))))))) 288 entry)))))))))))
279 289
290(defun erc-button--maybe-warn-arbitrary-sexp (form)
291 (if (and (symbolp form) (special-variable-p form))
292 (symbol-value form)
293 (unless (get 'erc-button--maybe-warn-arbitrary-sexp 'warned-arbitrary-sexp)
294 (put 'erc-button--maybe-warn-arbitrary-sexp 'warned-arbitrary-sexp t)
295 (lwarn 'erc :warning
296 (concat "Arbitrary sexps for the third FORM"
297 " slot of `erc-button-alist' entries"
298 " have been deprecated.")))
299 (eval form t)))
300
301(defun erc-button--check-nicknames-entry ()
302 ;; This helper exists because the module is defined after its options.
303 (when-let (((eq major-mode 'erc-mode))
304 (entry (alist-get 'nicknames erc-button-alist)))
305 (unless (eq 'erc-button-buttonize-nicks (nth 1 entry))
306 (erc-button--display-error-notice-with-keys-and-warn
307 "Values other than `erc-button-buttonize-nicks' in the third slot of "
308 "the `nicknames' entry of `erc-button-alist' are deprecated."))))
309
310(defun erc-button--preserve-bounds (bounds _ server-user _)
311 "Return BOUNDS.\n\n(fn BOUNDS NICKNAME SERVER-USER CHANNEL-USER)"
312 (and server-user bounds))
313
314;; This variable is intended to serve as a "core" to be wrapped by
315;; (built-in) modules during setup. It's unclear whether
316;; `add-function's practice of removing existing advice before
317;; re-adding it is desirable when integrating modules since we're
318;; mostly concerned with ensuring one "piece" precedes or follows
319;; another (specific piece), which may not yet (or ever) be present.
320
321(defvar erc-button--modify-nick-function #'erc-button--preserve-bounds
322 "Function to possibly modify aspects of nick being buttonized.
323Called with four args: BOUNDS NICKNAME SERVER-USER CHANNEL-USER.
324BOUNDS is a cons of (BEG . END) marking the position of the nick
325in the current message, which occupies the whole of the narrowed
326buffer. BEG is normally also point. NICKNAME is a case-mapped
327string without text properties. SERVER-USER and CHANNEL-USER are
328the nick's `erc-server-users' entry and its associated (though
329possibly nil) `erc-channel-user' object. The function should
330return BOUNDS or a suitable replacement to indicate that
331buttonizing ought to proceed, and nil if it should be inhibited.")
332
333(defvar-local erc-button--phantom-users nil)
334
335(defun erc-button--add-phantom-speaker (args)
336 "Maybe substitute fake `server-user' for speaker at point."
337 (pcase args
338 (`(,bounds ,downcased-nick nil ,channel-user)
339 (list bounds downcased-nick
340 ;; Like `with-memoization' but don't cache when value is nil.
341 (or (gethash downcased-nick erc-button--phantom-users)
342 (and-let* ((user (erc-button--get-user-from-speaker-naive
343 (car bounds))))
344 (puthash downcased-nick user erc-button--phantom-users)))
345 channel-user))
346 (_ args)))
347
348(define-minor-mode erc-button--phantom-users-mode
349 "Minor mode to recognize unknown speakers.
350Expect to be used by module setup code for creating placeholder
351users on the fly during history playback. Treat an unknown
352PRIVMSG speaker, like <bob>, as if they were present in a 353 and
353are thus a member of the channel. However, don't bother creating
354an actual `erc-channel-user' object because their status prefix
355is unknown. Instead, just spoof an `erc-server-user' by applying
356early (outer), args-filtering advice wrapping
357`erc-button--modify-nick-function'."
358 :interactive nil
359 (if erc-button--phantom-users-mode
360 (progn
361 (add-function :filter-args (local 'erc-button--modify-nick-function)
362 #'erc-button--add-phantom-speaker '((depth . -90)))
363 (setq erc-button--phantom-users (make-hash-table :test #'equal)))
364 (remove-function (local 'erc-button--modify-nick-function)
365 #'erc-button--add-phantom-speaker)
366 (kill-local-variable 'erc-nicks--phantom-users)))
367
368;; FIXME replace this after making ERC account-aware.
369(defun erc-button--get-user-from-speaker-naive (point)
370 "Return `erc-server-user' object for nick at POINT."
371 (when-let*
372 (((eql ?< (char-before point)))
373 ((eq (get-text-property point 'font-lock-face) 'erc-nick-default-face))
374 (parsed (erc-get-parsed-vector point)))
375 (pcase-let* ((`(,nick ,login ,host)
376 (erc-parse-user (erc-response.sender parsed))))
377 (make-erc-server-user
378 :nickname nick
379 :host (and (not (string-empty-p host)) host)
380 :login (and (not (string-empty-p login)) login)))))
381
280(defun erc-button-add-nickname-buttons (entry) 382(defun erc-button-add-nickname-buttons (entry)
281 "Search through the buffer for nicknames, and add buttons." 383 "Search through the buffer for nicknames, and add buttons."
282 (let ((form (nth 2 entry)) 384 (let ((form (nth 2 entry))
283 (fun (nth 3 entry)) 385 (fun (nth 3 entry))
284 bounds word) 386 bounds word)
285 (when (or (eq t form) 387 (when (eq form 'erc-button-buttonize-nicks)
286 (eval form t)) 388 (setq form (and (symbol-value form) erc-button--modify-nick-function)))
389 (when (or (functionp form)
390 (eq t form)
391 (and form (erc-button--maybe-warn-arbitrary-sexp form)))
287 (goto-char (point-min)) 392 (goto-char (point-min))
288 (while (erc-forward-word) 393 (while (erc-forward-word)
289 (when (setq bounds (erc-bounds-of-word-at-point)) 394 (when (setq bounds (erc-bounds-of-word-at-point))
290 (setq word (buffer-substring-no-properties 395 (setq word (buffer-substring-no-properties
291 (car bounds) (cdr bounds))) 396 (car bounds) (cdr bounds)))
292 (when (or (and (erc-server-buffer-p) (erc-get-server-user word)) 397 (let* ((erc-button-face erc-button-face)
293 (and erc-channel-users (erc-get-channel-user word))) 398 (erc-button-mouse-face erc-button-mouse-face)
294 (erc-button-add-button (car bounds) (cdr bounds) 399 (erc-button-nickname-face erc-button-nickname-face)
295 fun t (list word)))))))) 400 (down (erc-downcase word))
401 (cuser (and erc-channel-users
402 (gethash down erc-channel-users)))
403 (user (or (and cuser (car cuser))
404 (and erc-server-users
405 (gethash down erc-server-users)))))
406 (when (or (not (functionp form))
407 (setq bounds
408 (funcall form bounds down user (cdr cuser))))
409 (erc-button-add-button (car bounds) (cdr bounds)
410 fun t (list word)))))))))
296 411
297(defun erc-button-add-buttons-1 (regexp entry) 412(defun erc-button-add-buttons-1 (regexp entry)
298 "Search through the buffer for matches to ENTRY and add buttons." 413 "Search through the buffer for matches to ENTRY and add buttons."
@@ -304,7 +419,14 @@ specified by `erc-button-alist'."
304 (fun (nth 3 entry)) 419 (fun (nth 3 entry))
305 (data (mapcar #'match-string-no-properties (nthcdr 4 entry)))) 420 (data (mapcar #'match-string-no-properties (nthcdr 4 entry))))
306 (when (or (eq t form) 421 (when (or (eq t form)
307 (eval form t)) 422 (and (functionp form)
423 (let* ((erc-button-face erc-button-face)
424 (erc-button-mouse-face erc-button-mouse-face)
425 (erc-button-nickname-face erc-button-nickname-face)
426 (rv (funcall form start end)))
427 (when rv
428 (setq end (cdr rv) start (car rv)))))
429 (erc-button--maybe-warn-arbitrary-sexp form))
308 (erc-button-add-button start end fun nil data regexp))))) 430 (erc-button-add-button start end fun nil data regexp)))))
309 431
310(defun erc-button-remove-old-buttons () 432(defun erc-button-remove-old-buttons ()
@@ -513,6 +635,70 @@ and `apropos' for other symbols."
513 (message "@%s is %d:%02d local time" 635 (message "@%s is %d:%02d local time"
514 beats hours minutes))) 636 beats hours minutes)))
515 637
638(defun erc-button--substitute-command-keys-in-region (beg end)
639 "Replace command in region with keys and return new bounds"
640 (let* ((o (buffer-substring beg end))
641 (s (substitute-command-keys o)))
642 (unless (equal o s)
643 (setq erc-button-face nil))
644 (delete-region beg end)
645 (insert s))
646 (cons beg (point)))
647
648;;;###autoload
649(defun erc-button--display-error-notice-with-keys (&optional parsed buffer
650 &rest strings)
651 "Add help keys to STRINGS for configuration-related admonishments.
652Return inserted result. Expect PARSED to be an `erc-response'
653object, a string, or nil. Expect BUFFER to be a buffer, a string,
654or nil. As a special case, allow PARSED to be a buffer as long
655as BUFFER is a string or nil. If STRINGS contains any trailing
656non-strings, concatenate leading string members before applying
657`format'. Otherwise, just concatenate everything."
658 (when (stringp buffer)
659 (push buffer strings)
660 (setq buffer nil))
661 (when (stringp parsed)
662 (push parsed strings)
663 (setq parsed nil))
664 (when (bufferp parsed)
665 (cl-assert (null buffer))
666 (setq buffer parsed
667 parsed nil))
668 (let* ((op (if (seq-every-p #'stringp (cdr strings))
669 #'concat
670 (let ((head (pop strings)))
671 (while (stringp (car strings))
672 (setq head (concat head (pop strings))))
673 (push head strings))
674 #'format))
675 (string (apply op strings))
676 (erc-insert-post-hook
677 (cons (lambda ()
678 (setq string (buffer-substring (point-min)
679 (1- (point-max)))))
680 erc-insert-post-hook))
681 (erc-button-alist
682 `((,(rx "\\[" (group (+ (not "]"))) "]") 0
683 erc-button--substitute-command-keys-in-region
684 erc-button-describe-symbol 1)
685 ,@erc-button-alist)))
686 (erc-display-message parsed '(notice error) (or buffer 'active) string)
687 string))
688
689;;;###autoload
690(defun erc-button--display-error-notice-with-keys-and-warn (&rest args)
691 "Like `erc-button--display-error-notice-with-keys' but also warn."
692 (let ((string (apply #'erc-button--display-error-notice-with-keys args)))
693 (with-temp-buffer
694 (insert string)
695 (goto-char (point-min))
696 (with-syntax-table lisp-mode-syntax-table
697 (skip-syntax-forward "^-"))
698 (forward-char)
699 (display-warning
700 'erc (buffer-substring-no-properties (point) (point-max))))))
701
516(provide 'erc-button) 702(provide 'erc-button)
517 703
518;;; erc-button.el ends here 704;;; erc-button.el ends here
diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 4337d633cfa..dd481032e7e 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -67,6 +67,9 @@
67(declare-function erc-server-process-alive "erc-backend" (&optional buffer)) 67(declare-function erc-server-process-alive "erc-backend" (&optional buffer))
68(declare-function erc-set-active-buffer "erc" (buffer)) 68(declare-function erc-set-active-buffer "erc" (buffer))
69 69
70(declare-function erc-button--display-error-notice-with-keys
71 (parsed &rest strings))
72
70;; Variables 73;; Variables
71 74
72(defgroup erc-networks nil 75(defgroup erc-networks nil
@@ -1310,12 +1313,11 @@ shutting down the connection."
1310Copy source (prefix) from MOTD-ish message as a last resort." 1313Copy source (prefix) from MOTD-ish message as a last resort."
1311 ;; The 004 handler never ran; see 2004-03-10 Diane Murray in change log 1314 ;; The 004 handler never ran; see 2004-03-10 Diane Murray in change log
1312 (unless erc-server-announced-name 1315 (unless erc-server-announced-name
1313 (setq erc-server-announced-name (erc-response.sender parsed)) 1316 (require 'erc-button)
1314 (erc-display-error-notice 1317 (erc-button--display-error-notice-with-keys
1315 parsed (concat "Failed to determine server name. Using \"" 1318 parsed "Failed to determine server name. Using \""
1316 erc-server-announced-name "\" instead." 1319 (setq erc-server-announced-name (erc-response.sender parsed)) "\" instead"
1317 " If this was unexpected, consider reporting it via " 1320 ". If this was unexpected, consider reporting it via \\[erc-bug]" "."))
1318 (substitute-command-keys "\\[erc-bug]") ".")))
1319 nil) 1321 nil)
1320 1322
1321(defun erc-unset-network-name (_nick _ip _reason) 1323(defun erc-unset-network-name (_nick _ip _reason)
@@ -1493,9 +1495,9 @@ to be a false alarm. If `erc-reuse-buffers' is nil, let
1493 (memq (erc--target-symbol erc--target) 1495 (memq (erc--target-symbol erc--target)
1494 erc-networks--bouncer-targets))) 1496 erc-networks--bouncer-targets)))
1495 proc) 1497 proc)
1496 (let ((m (concat "Unexpected state detected. Please report via " 1498 (require 'erc-button)
1497 (substitute-command-keys "\\[erc-bug]") "."))) 1499 (erc-button--display-error-notice-with-keys
1498 (erc-display-error-notice parsed m)))) 1500 parsed "Unexpected state detected. Please report via \\[erc-bug].")))
1499 1501
1500 ;; For now, retain compatibility with erc-server-NNN-functions. 1502 ;; For now, retain compatibility with erc-server-NNN-functions.
1501 (or (erc-networks--ensure-announced proc parsed) 1503 (or (erc-networks--ensure-announced proc parsed)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 6e66de53edd..b155f85ab8a 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -1790,4 +1790,65 @@ connection."
1790 (put 'erc-mname-enable 'definition-name 'mname) 1790 (put 'erc-mname-enable 'definition-name 'mname)
1791 (put 'erc-mname-disable 'definition-name 'mname)))))) 1791 (put 'erc-mname-disable 'definition-name 'mname))))))
1792 1792
1793
1794;; XXX move erc-button tests to new file if more added.
1795(require 'erc-button)
1796
1797;; See also `erc-scenarios-networks-announced-missing' in
1798;; erc-scenarios-misc.el for a more realistic example.
1799(ert-deftest erc-button--display-error-notice-with-keys ()
1800 (with-current-buffer (get-buffer-create "*fake*")
1801 (let ((mode erc-button-mode)
1802 (inhibit-message noninteractive)
1803 erc-modules
1804 erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
1805 (erc-mode)
1806 (erc-tests--set-fake-server-process "sleep" "1")
1807 (erc--initialize-markers (point) nil)
1808 (erc-button-mode +1)
1809 (should (equal (erc-button--display-error-notice-with-keys
1810 "If \\[erc-bol] fails, "
1811 "see \\[erc-bug] or `erc-mode-map'.")
1812 "*** If C-a fails, see M-x erc-bug or `erc-mode-map'."))
1813 (goto-char (point-min))
1814
1815 (ert-info ("Keymap substitution succeeds")
1816 (erc-button-next)
1817 (should (looking-at "C-a"))
1818 (should (eq (get-text-property (point) 'mouse-face) 'highlight))
1819 (erc-button-press-button)
1820 (with-current-buffer "*Help*"
1821 (goto-char (point-min))
1822 (should (search-forward "erc-bol" nil t)))
1823 (erc-button-next)
1824 (erc-button-previous) ; end of interval correct
1825 (should (looking-at "a fails")))
1826
1827 (ert-info ("Extended command mapping succeeds")
1828 (erc-button-next)
1829 (should (looking-at "M-x erc-bug"))
1830 (erc-button-press-button)
1831 (should (eq (get-text-property (point) 'mouse-face) 'highlight))
1832 (with-current-buffer "*Help*"
1833 (goto-char (point-min))
1834 (should (search-forward "erc-bug" nil t))))
1835
1836 (ert-info ("Symbol-description face preserved") ; mutated by d-e-n-w-k
1837 (erc-button-next)
1838 (should (equal (get-text-property (point) 'font-lock-face)
1839 '(erc-button erc-error-face)))
1840 (should (eq (get-text-property (point) 'mouse-face) 'highlight))
1841 (should (eq erc-button-face 'erc-button))) ; extent evaporates
1842
1843 (ert-info ("Format when trailing args include non-strings")
1844 (should (equal (erc-button--display-error-notice-with-keys
1845 "abc" " %d def" " 45%s" 123 '\6)
1846 "*** abc 123 def 456")))
1847
1848 (when noninteractive
1849 (unless mode
1850 (erc-button-mode -1))
1851 (kill-buffer "*Help*")
1852 (kill-buffer)))))
1853
1793;;; erc-tests.el ends here 1854;;; erc-tests.el ends here
diff --git a/test/lisp/erc/resources/base/assoc/bouncer-history/foonet.eld b/test/lisp/erc/resources/base/assoc/bouncer-history/foonet.eld
index 58df79e19fa..f34ae02f4e4 100644
--- a/test/lisp/erc/resources/base/assoc/bouncer-history/foonet.eld
+++ b/test/lisp/erc/resources/base/assoc/bouncer-history/foonet.eld
@@ -27,6 +27,7 @@
27 (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.") 27 (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:02] alice: Here come the lovers, full of joy and mirth.")
28 (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.") 28 (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:07] bob: According to the fool's bolt, sir, and such dulcet diseases.")
29 (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.") 29 (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:10] alice: And hang himself. I pray you, do my greeting.")
30 (0 ":someone!~u@abcdefg.irc PRIVMSG #chan :[07:04:10] hi everyone.")
30 (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.") 31 (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:18] bob: And you sat smiling at his cruel prey.")
31 (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.") 32 (0 ":bob!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:21] alice: Or never after look me in the face.")
32 (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.") 33 (0 ":alice!~u@svpn88yjcdj42.irc PRIVMSG #chan :[07:04:25] bob: If that may be, than all is well. Come, sit down, every mother's son, and rehearse your parts. Pyramus, you begin: when you have spoken your speech, enter into that brake; and so every one according to his cue.")