ol-irc.el 9.36 KB
Newer Older
Bastien's avatar
Bastien committed
1
;;; ol-irc.el --- Links to IRC Sessions              -*- lexical-binding: t; -*-
2
;;
Paul Eggert's avatar
Paul Eggert committed
3
;; Copyright (C) 2008-2020 Free Software Foundation, Inc.
4
;;
Carsten Dominik's avatar
Carsten Dominik committed
5 6
;; Author: Philip Jackson <emacs@shellarchive.co.uk>
;; Keywords: erc, irc, link, org
7
;;
Carsten Dominik's avatar
Carsten Dominik committed
8
;; This file is part of GNU Emacs.
9
;;
10
;; GNU Emacs is free software: you can redistribute it and/or modify
Carsten Dominik's avatar
Carsten Dominik committed
11
;; it under the terms of the GNU General Public License as published by
12 13
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
Carsten Dominik's avatar
Carsten Dominik committed
14 15 16

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
Carsten Dominik's avatar
Carsten Dominik committed
18 19 20
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
21
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
22

Carsten Dominik's avatar
Carsten Dominik committed
23
;;; Commentary:
24

Rasmus's avatar
Rasmus committed
25 26
;; This file implements links to an IRC session from within Org mode.
;; Org mode loads this module by default - if this is not what you want,
27
;; configure the variable `org-modules'.
Carsten Dominik's avatar
Carsten Dominik committed
28
;;
29
;; Please customize the variable `org-modules' to select
30 31
;; extensions you would like to use, and to deselect those which you don't
;; want.
Carsten Dominik's avatar
Carsten Dominik committed
32
;;
33 34
;; Please note that at the moment only ERC is supported.  Other clients
;; shouldn't be difficult to add though.
Carsten Dominik's avatar
Carsten Dominik committed
35 36 37 38 39 40 41 42 43 44 45 46 47 48
;;
;; Then set `org-irc-link-to-logs' to non-nil if you would like a
;; file:/ type link to be created to the current line in the logs or
;; to t if you would like to create an irc:/ style link.
;;
;; Links within an org buffer might look like this:
;;
;; [[irc:/irc.freenode.net/#emacs/bob][chat with bob in #emacs on freenode]]
;; [[irc:/irc.freenode.net/#emacs][#emacs on freenode]]
;; [[irc:/irc.freenode.net/]]
;;
;; If, when the resulting link is visited, there is no connection to a
;; requested server then one will be created.

49
;;; Code:
Carsten Dominik's avatar
Carsten Dominik committed
50

Bastien's avatar
Bastien committed
51
(require 'ol)
52

Rasmus's avatar
Rasmus committed
53 54 55
(declare-function erc-buffer-filter "erc" (predicate &optional proc))
(declare-function erc-channel-p "erc" (channel))
(declare-function erc-cmd-JOIN "erc" (channel &optional key))
56 57 58
(declare-function erc-current-logfile "erc-log" (&optional buffer))
(declare-function erc-default-target "erc" ())
(declare-function erc-get-server-nickname-list "erc" ())
Rasmus's avatar
Rasmus committed
59 60 61 62
(declare-function erc-logging-enabled "erc-log" (&optional buffer))
(declare-function erc-prompt "erc" ())
(declare-function erc-save-buffer-in-logs "erc-log" (&optional buffer))
(declare-function erc-server-buffer "erc" ())
Carsten Dominik's avatar
Carsten Dominik committed
63 64

(defvar org-irc-client 'erc
65
  "The IRC client to act on.")
Rasmus's avatar
Rasmus committed
66

Carsten Dominik's avatar
Carsten Dominik committed
67
(defvar org-irc-link-to-logs nil
68
  "Non-nil will store a link to the logs, nil will store an irc: style link.")
Carsten Dominik's avatar
Carsten Dominik committed
69

70 71 72
(defvar erc-default-port)   ; dynamically scoped from erc.el
(defvar erc-session-port)   ; dynamically scoped form erc-backend.el
(defvar erc-session-server) ; dynamically scoped form erc-backend.el
Carsten Dominik's avatar
Carsten Dominik committed
73 74 75

;; Generic functions/config (extend these for other clients)

Bastien's avatar
Bastien committed
76 77 78 79
(org-link-set-parameters "irc"
			 :follow #'org-irc-visit
			 :store #'org-irc-store-link
			 :export #'org-irc-export)
Carsten Dominik's avatar
Carsten Dominik committed
80 81

(defun org-irc-visit (link)
82
  "Parse LINK and dispatch to the correct function based on the client found."
Carsten Dominik's avatar
Carsten Dominik committed
83 84
  (let ((link (org-irc-parse-link link)))
    (cond
85 86 87 88
     ((eq org-irc-client 'erc)
      (org-irc-visit-erc link))
     (t
      (error "ERC only known client")))))
Carsten Dominik's avatar
Carsten Dominik committed
89 90

(defun org-irc-parse-link (link)
91 92
  "Parse an IRC LINK and return the attributes found.
Parse a LINK that looks like server:port/chan/user (port, chan
93
and user being optional) and return any of the port, channel or user
94
attributes that are found."
Carsten Dominik's avatar
Carsten Dominik committed
95
  (let* ((parts (split-string link "/" t))
96
	 (len (length parts)))
Carsten Dominik's avatar
Carsten Dominik committed
97
    (when (or (< len 1) (> len 3))
98
      (error "Failed to parse link needed 1-3 parts, got %d" len))
Carsten Dominik's avatar
Carsten Dominik committed
99 100 101 102 103
    (setcar parts (split-string (car parts) ":" t))
    parts))

;;;###autoload
(defun org-irc-store-link ()
104
  "Dispatch to the appropriate function to store a link to an IRC session."
Carsten Dominik's avatar
Carsten Dominik committed
105
  (cond
106 107
   ((eq major-mode 'erc-mode)
    (org-irc-erc-store-link))))
Carsten Dominik's avatar
Carsten Dominik committed
108

Paul Eggert's avatar
Paul Eggert committed
109
(defun org-irc-ellipsify-description (string &optional after)
110 111
  "Remove unnecessary white space from STRING and add ellipses if necessary.
Strip starting and ending white space from STRING and replace any
112
chars that the value AFTER with `...'"
Carsten Dominik's avatar
Carsten Dominik committed
113
  (let* ((after (number-to-string (or after 30)))
114 115 116 117
	 (replace-map (list (cons "^[ \t]*" "")
			    (cons "[ \t]*$" "")
			    (cons (concat "^\\(.\\{" after
					  "\\}\\).*") "\\1..."))))
Rasmus's avatar
Rasmus committed
118 119 120
    (dolist (x replace-map string)
      (when (string-match (car x) string)
	(setq string (replace-match (cdr x) nil nil string))))))
Carsten Dominik's avatar
Carsten Dominik committed
121 122 123 124

;; ERC specific functions

(defun org-irc-erc-get-line-from-log (erc-line)
125 126 127 128
  "Find the best line to link to from the ERC logs given ERC-LINE as a start.
If the user is on the ERC-prompt then search backward for the
first non-blank line, otherwise return the current line.  The
result is a cons of the filename and search string."
Carsten Dominik's avatar
Carsten Dominik committed
129
  (erc-save-buffer-in-logs)
130
  (require 'erc-log)
Carsten Dominik's avatar
Carsten Dominik committed
131 132 133 134 135 136
  (with-current-buffer (find-file-noselect (erc-current-logfile))
    (goto-char (point-max))
    (list
     (abbreviate-file-name buffer-file-name)
     ;; can we get a '::' part?
     (if (string= erc-line (erc-prompt))
137 138 139 140 141
	 (progn
	   (goto-char (point-at-bol))
	   (when (search-backward-regexp "^[^	]" nil t)
	     (buffer-substring-no-properties (point-at-bol)
					     (point-at-eol))))
142 143 144
       (when (search-backward erc-line nil t)
	 (buffer-substring-no-properties (point-at-bol)
					 (point-at-eol)))))))
Carsten Dominik's avatar
Carsten Dominik committed
145 146

(defun org-irc-erc-store-link ()
147 148 149
  "Store a link to the IRC log file or the session itself.
Depending on the variable `org-irc-link-to-logs' store either a
link to the log file for the current session or an irc: link to
Carsten Dominik's avatar
Carsten Dominik committed
150
the session itself."
151
  (require 'erc-log)
Carsten Dominik's avatar
Carsten Dominik committed
152 153
  (if org-irc-link-to-logs
      (let* ((erc-line (buffer-substring-no-properties
154 155 156 157
			(point-at-bol) (point-at-eol)))
	     (parsed-line (org-irc-erc-get-line-from-log erc-line)))
	(if (erc-logging-enabled nil)
	    (progn
Bastien's avatar
Bastien committed
158
	      (org-link-store-props
159
	       :type "file"
Paul Eggert's avatar
Paul Eggert committed
160
	       :description (concat "'" (org-irc-ellipsify-description
161 162 163 164 165
					 (cadr parsed-line) 20)
				    "' from an IRC conversation")
	       :link (concat "file:" (car parsed-line) "::"
			     (cadr parsed-line)))
	      t)
166 167 168 169 170
	  (error "This ERC session is not being logged")))
    (let* ((link-text (org-irc-get-erc-link))
	   (link (org-irc-parse-link link-text)))
      (if link-text
	  (progn
Bastien's avatar
Bastien committed
171
	    (org-link-store-props
172 173
	     :type "irc"
	     :link (concat "irc:/" link-text)
174
	     :description (concat "irc session `" link-text "'")
175 176 177 178 179
	     :server (car (car link))
	     :port (or (string-to-number (cadr (pop link))) erc-default-port)
	     :nick (pop link))
	    t)
	(error "Failed to create ('irc:/' style) ERC link")))))
Carsten Dominik's avatar
Carsten Dominik committed
180 181

(defun org-irc-get-erc-link ()
182 183
  "Return an org compatible irc:/ link from an ERC buffer."
  (let* ((session-port (if (numberp erc-session-port)
184
			   (number-to-string erc-session-port)
185 186
			 erc-session-port))
	 (link (concat erc-session-server ":" session-port)))
Carsten Dominik's avatar
Carsten Dominik committed
187
    (concat link "/"
188 189 190 191 192 193
	    (if (and (erc-default-target)
		     (erc-channel-p (erc-default-target))
		     (car (get-text-property (point) 'erc-data)))
		;; we can get a nick
		(let ((nick (car (get-text-property (point) 'erc-data))))
		  (concat (erc-default-target) "/" nick))
194
	      (erc-default-target)))))
Carsten Dominik's avatar
Carsten Dominik committed
195

196 197 198 199 200
(defun org-irc-get-current-erc-port ()
  "Return the current port as a number.
Return the current port number or, if none is set, return the ERC
default."
  (cond
201 202 203 204 205 206
   ((stringp erc-session-port)
    (string-to-number erc-session-port))
   ((numberp erc-session-port)
    erc-session-port)
   (t
    erc-default-port)))
207

Carsten Dominik's avatar
Carsten Dominik committed
208
(defun org-irc-visit-erc (link)
209 210 211
  "Visit an ERC buffer based on criteria found in LINK."
  (require 'erc)
  (require 'erc-log)
Carsten Dominik's avatar
Carsten Dominik committed
212
  (let* ((server (car (car link)))
Rasmus's avatar
Rasmus committed
213 214
	 (port (let ((p (cadr (pop link))))
		 (if p (string-to-number p) erc-default-port)))
215 216 217 218 219 220 221 222 223 224 225
	 (server-buffer)
	 (buffer-list
	  (erc-buffer-filter
	   (lambda nil
	     (let ((tmp-server-buf (erc-server-buffer)))
	       (and tmp-server-buf
		    (with-current-buffer tmp-server-buf
		      (and
		       (eq (org-irc-get-current-erc-port) port)
		       (string= erc-session-server server)
		       (setq server-buffer tmp-server-buf)))))))))
Carsten Dominik's avatar
Carsten Dominik committed
226
    (if buffer-list
227 228 229 230 231 232 233 234 235
	(let ((chan-name (pop link)))
	  ;; if we got a channel name then switch to it or join it
	  (if chan-name
	      (let ((chan-buf (catch 'found
				(dolist (x buffer-list)
				  (if (string= (buffer-name x) chan-name)
				      (throw 'found x))))))
		(if chan-buf
		    (progn
Rasmus's avatar
Rasmus committed
236
		      (pop-to-buffer-same-window chan-buf)
237 238 239 240 241 242 243 244
		      ;; if we got a nick, and they're in the chan,
		      ;; then start a chat with them
		      (let ((nick (pop link)))
			(when nick
			  (if (member nick (erc-get-server-nickname-list))
			      (progn
				(goto-char (point-max))
				(insert (concat nick ": ")))
245 246
			    (error "%s not found in %s" nick chan-name)))))
		  (progn
Rasmus's avatar
Rasmus committed
247
		    (pop-to-buffer-same-window server-buffer)
248
		    (erc-cmd-JOIN chan-name))))
Rasmus's avatar
Rasmus committed
249
	    (pop-to-buffer-same-window server-buffer)))
250 251
      ;; no server match, make new connection
      (erc-select :server server :port port))))
Carsten Dominik's avatar
Carsten Dominik committed
252

Bastien's avatar
Bastien committed
253 254 255 256 257 258 259 260 261 262 263
(defun org-irc-export (link description format)
  "Export an IRC link.
See `org-link-parameters' for details about LINK, DESCRIPTION and
FORMAT."
  (let ((desc (or description link)))
    (pcase format
      (`html (format "<a href=\"irc:%s\">%s</a>" link desc))
      (`md (format "[%s](irc:%s)" desc link))
      (_ nil))))

(provide 'ol-irc)
Carsten Dominik's avatar
Carsten Dominik committed
264

265 266 267 268
;; Local variables:
;; generated-autoload-file: "org-loaddefs.el"
;; End:

Bastien's avatar
Bastien committed
269
;;; ol-irc.el ends here