| 1 | ;;; org-irc.el --- Store links to IRC sessions |
| 2 | ;; |
| 3 | ;; Copyright (C) 2008, 2009, 2010 Free Software Foundation, Inc. |
| 4 | ;; |
| 5 | ;; Author: Philip Jackson <emacs@shellarchive.co.uk> |
| 6 | ;; Keywords: erc, irc, link, org |
| 7 | ;; Version: 7.7 |
| 8 | ;; |
| 9 | ;; This file is part of GNU Emacs. |
| 10 | ;; |
| 11 | ;; GNU Emacs is free software: you can redistribute it and/or modify |
| 12 | ;; it under the terms of the GNU General Public License as published by |
| 13 | ;; the Free Software Foundation, either version 3 of the License, or |
| 14 | ;; (at your option) any later version. |
| 15 | |
| 16 | ;; GNU Emacs is distributed in the hope that it will be useful, |
| 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 19 | ;; GNU General Public License for more details. |
| 20 | |
| 21 | ;; You should have received a copy of the GNU General Public License |
| 22 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. |
| 23 | |
| 24 | ;;; Commentary: |
| 25 | |
| 26 | ;; This file implements links to an IRC session from within Org-mode. |
| 27 | ;; Org-mode loads this module by default - if this is not what you want, |
| 28 | ;; configure the variable `org-modules'. |
| 29 | ;; |
| 30 | ;; Please customize the variable `org-modules' to select |
| 31 | ;; extensions you would like to use, and to deselect those which you don't |
| 32 | ;; want. |
| 33 | ;; |
| 34 | ;; Please note that at the moment only ERC is supported. Other clients |
| 35 | ;; shouldn't be difficult to add though. |
| 36 | ;; |
| 37 | ;; Then set `org-irc-link-to-logs' to non-nil if you would like a |
| 38 | ;; file:/ type link to be created to the current line in the logs or |
| 39 | ;; to t if you would like to create an irc:/ style link. |
| 40 | ;; |
| 41 | ;; Links within an org buffer might look like this: |
| 42 | ;; |
| 43 | ;; [[irc:/irc.freenode.net/#emacs/bob][chat with bob in #emacs on freenode]] |
| 44 | ;; [[irc:/irc.freenode.net/#emacs][#emacs on freenode]] |
| 45 | ;; [[irc:/irc.freenode.net/]] |
| 46 | ;; |
| 47 | ;; If, when the resulting link is visited, there is no connection to a |
| 48 | ;; requested server then one will be created. |
| 49 | |
| 50 | ;;; Code: |
| 51 | |
| 52 | (require 'org) |
| 53 | |
| 54 | ;; Declare the function form ERC that we use. |
| 55 | (declare-function erc-current-logfile "erc-log" (&optional buffer)) |
| 56 | (declare-function erc-prompt "erc" ()) |
| 57 | (declare-function erc-default-target "erc" ()) |
| 58 | (declare-function erc-channel-p "erc" (channel)) |
| 59 | (declare-function erc-buffer-filter "erc" (predicate &optional proc)) |
| 60 | (declare-function erc-server-buffer "erc" ()) |
| 61 | (declare-function erc-get-server-nickname-list "erc" ()) |
| 62 | (declare-function erc-cmd-JOIN "erc" (channel &optional key)) |
| 63 | (declare-function org-pop-to-buffer-same-window |
| 64 | "org-compat" (&optional buffer-or-name norecord label)) |
| 65 | |
| 66 | (defvar org-irc-client 'erc |
| 67 | "The IRC client to act on.") |
| 68 | (defvar org-irc-link-to-logs nil |
| 69 | "Non-nil will store a link to the logs, nil will store an irc: style link.") |
| 70 | |
| 71 | (defvar erc-default-port) ; dynamically scoped from erc.el |
| 72 | (defvar erc-session-port) ; dynamically scoped form erc-backend.el |
| 73 | (defvar erc-session-server) ; dynamically scoped form erc-backend.el |
| 74 | |
| 75 | ;; Generic functions/config (extend these for other clients) |
| 76 | |
| 77 | (add-to-list 'org-store-link-functions 'org-irc-store-link) |
| 78 | |
| 79 | (org-add-link-type "irc" 'org-irc-visit nil) |
| 80 | |
| 81 | (defun org-irc-visit (link) |
| 82 | "Parse LINK and dispatch to the correct function based on the client found." |
| 83 | (let ((link (org-irc-parse-link link))) |
| 84 | (cond |
| 85 | ((eq org-irc-client 'erc) |
| 86 | (org-irc-visit-erc link)) |
| 87 | (t |
| 88 | (error "erc only known client"))))) |
| 89 | |
| 90 | (defun org-irc-parse-link (link) |
| 91 | "Parse an IRC LINK and return the attributes found. |
| 92 | 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." |
| 95 | (let* ((parts (split-string link "/" t)) |
| 96 | (len (length parts))) |
| 97 | (when (or (< len 1) (> len 3)) |
| 98 | (error "Failed to parse link needed 1-3 parts, got %d" len)) |
| 99 | (setcar parts (split-string (car parts) ":" t)) |
| 100 | parts)) |
| 101 | |
| 102 | ;;;###autoload |
| 103 | (defun org-irc-store-link () |
| 104 | "Dispatch to the appropriate function to store a link to an IRC session." |
| 105 | (cond |
| 106 | ((eq major-mode 'erc-mode) |
| 107 | (org-irc-erc-store-link)))) |
| 108 | |
| 109 | (defun org-irc-elipsify-description (string &optional after) |
| 110 | "Remove unnecessary white space from STRING and add ellipses if necessary. |
| 111 | Strip starting and ending white space from STRING and replace any |
| 112 | chars that the value AFTER with '...'" |
| 113 | (let* ((after (number-to-string (or after 30))) |
| 114 | (replace-map (list (cons "^[ \t]*" "") |
| 115 | (cons "[ \t]*$" "") |
| 116 | (cons (concat "^\\(.\\{" after |
| 117 | "\\}\\).*") "\\1...")))) |
| 118 | (mapc (lambda (x) |
| 119 | (when (string-match (car x) string) |
| 120 | (setq string (replace-match (cdr x) nil nil string)))) |
| 121 | replace-map) |
| 122 | string)) |
| 123 | |
| 124 | ;; ERC specific functions |
| 125 | |
| 126 | (defun org-irc-erc-get-line-from-log (erc-line) |
| 127 | "Find the best line to link to from the ERC logs given ERC-LINE as a start. |
| 128 | If the user is on the ERC-prompt then search backward for the |
| 129 | first non-blank line, otherwise return the current line. The |
| 130 | result is a cons of the filename and search string." |
| 131 | (erc-save-buffer-in-logs) |
| 132 | (require 'erc-log) |
| 133 | (with-current-buffer (find-file-noselect (erc-current-logfile)) |
| 134 | (goto-char (point-max)) |
| 135 | (list |
| 136 | (abbreviate-file-name buffer-file-name) |
| 137 | ;; can we get a '::' part? |
| 138 | (if (string= erc-line (erc-prompt)) |
| 139 | (progn |
| 140 | (goto-char (point-at-bol)) |
| 141 | (when (search-backward-regexp "^[^ ]" nil t) |
| 142 | (buffer-substring-no-properties (point-at-bol) |
| 143 | (point-at-eol)))) |
| 144 | (when (search-backward erc-line nil t) |
| 145 | (buffer-substring-no-properties (point-at-bol) |
| 146 | (point-at-eol))))))) |
| 147 | |
| 148 | (defun org-irc-erc-store-link () |
| 149 | "Store a link to the IRC log file or the session itself. |
| 150 | Depending on the variable `org-irc-link-to-logs' store either a |
| 151 | link to the log file for the current session or an irc: link to |
| 152 | the session itself." |
| 153 | (require 'erc-log) |
| 154 | (if org-irc-link-to-logs |
| 155 | (let* ((erc-line (buffer-substring-no-properties |
| 156 | (point-at-bol) (point-at-eol))) |
| 157 | (parsed-line (org-irc-erc-get-line-from-log erc-line))) |
| 158 | (if (erc-logging-enabled nil) |
| 159 | (progn |
| 160 | (org-store-link-props |
| 161 | :type "file" |
| 162 | :description (concat "'" (org-irc-elipsify-description |
| 163 | (cadr parsed-line) 20) |
| 164 | "' from an IRC conversation") |
| 165 | :link (concat "file:" (car parsed-line) "::" |
| 166 | (cadr parsed-line))) |
| 167 | t) |
| 168 | (error "This ERC session is not being logged"))) |
| 169 | (let* ((link-text (org-irc-get-erc-link)) |
| 170 | (link (org-irc-parse-link link-text))) |
| 171 | (if link-text |
| 172 | (progn |
| 173 | (org-store-link-props |
| 174 | :type "irc" |
| 175 | :link (org-make-link "irc:/" link-text) |
| 176 | :description (concat "irc session '" link-text "'") |
| 177 | :server (car (car link)) |
| 178 | :port (or (string-to-number (cadr (pop link))) erc-default-port) |
| 179 | :nick (pop link)) |
| 180 | t) |
| 181 | (error "Failed to create ('irc:/' style) ERC link"))))) |
| 182 | |
| 183 | (defun org-irc-get-erc-link () |
| 184 | "Return an org compatible irc:/ link from an ERC buffer." |
| 185 | (let* ((session-port (if (numberp erc-session-port) |
| 186 | (number-to-string erc-session-port) |
| 187 | erc-session-port)) |
| 188 | (link (concat erc-session-server ":" session-port))) |
| 189 | (concat link "/" |
| 190 | (if (and (erc-default-target) |
| 191 | (erc-channel-p (erc-default-target)) |
| 192 | (car (get-text-property (point) 'erc-data))) |
| 193 | ;; we can get a nick |
| 194 | (let ((nick (car (get-text-property (point) 'erc-data)))) |
| 195 | (concat (erc-default-target) "/" nick)) |
| 196 | (erc-default-target))))) |
| 197 | |
| 198 | (defun org-irc-get-current-erc-port () |
| 199 | "Return the current port as a number. |
| 200 | Return the current port number or, if none is set, return the ERC |
| 201 | default." |
| 202 | (cond |
| 203 | ((stringp erc-session-port) |
| 204 | (string-to-number erc-session-port)) |
| 205 | ((numberp erc-session-port) |
| 206 | erc-session-port) |
| 207 | (t |
| 208 | erc-default-port))) |
| 209 | |
| 210 | (defun org-irc-visit-erc (link) |
| 211 | "Visit an ERC buffer based on criteria found in LINK." |
| 212 | (require 'erc) |
| 213 | (require 'erc-log) |
| 214 | (let* ((server (car (car link))) |
| 215 | (port (or (string-to-number (cadr (pop link))) erc-default-port)) |
| 216 | (server-buffer) |
| 217 | (buffer-list |
| 218 | (erc-buffer-filter |
| 219 | (lambda nil |
| 220 | (let ((tmp-server-buf (erc-server-buffer))) |
| 221 | (and tmp-server-buf |
| 222 | (with-current-buffer tmp-server-buf |
| 223 | (and |
| 224 | (eq (org-irc-get-current-erc-port) port) |
| 225 | (string= erc-session-server server) |
| 226 | (setq server-buffer tmp-server-buf))))))))) |
| 227 | (if buffer-list |
| 228 | (let ((chan-name (pop link))) |
| 229 | ;; if we got a channel name then switch to it or join it |
| 230 | (if chan-name |
| 231 | (let ((chan-buf (catch 'found |
| 232 | (dolist (x buffer-list) |
| 233 | (if (string= (buffer-name x) chan-name) |
| 234 | (throw 'found x)))))) |
| 235 | (if chan-buf |
| 236 | (progn |
| 237 | (org-pop-to-buffer-same-window chan-buf) |
| 238 | ;; if we got a nick, and they're in the chan, |
| 239 | ;; then start a chat with them |
| 240 | (let ((nick (pop link))) |
| 241 | (when nick |
| 242 | (if (member nick (erc-get-server-nickname-list)) |
| 243 | (progn |
| 244 | (goto-char (point-max)) |
| 245 | (insert (concat nick ": "))) |
| 246 | (error "%s not found in %s" nick chan-name))))) |
| 247 | (progn |
| 248 | (org-pop-to-buffer-same-window server-buffer) |
| 249 | (erc-cmd-JOIN chan-name)))) |
| 250 | (org-pop-to-buffer-same-window server-buffer))) |
| 251 | ;; no server match, make new connection |
| 252 | (erc-select :server server :port port)))) |
| 253 | |
| 254 | (provide 'org-irc) |
| 255 | |
| 256 | ;; arch-tag: 018d7dda-53b8-4a35-ba92-6670939e525a |
| 257 | |
| 258 | ;;; org-irc.el ends here |