1;;; twitter.el --- Simple Emacs-based client for Twitter
6;; Copyright 2008 Neil Roberts
8;; This program is free software; you can redistribute it and/or modify
9;; it under the terms of the GNU General Public License as published by
10;; the Free Software Foundation; either version 2, or (at your option)
13;; This program is distributed in the hope that it will be useful,
14;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16;; GNU General Public License for more details.
18;; You should have received a copy of the GNU General Public License
19;; along with this program; if not, write to the Free Software
20;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
25;; A Twitter client for emacs that can view your friends timeline and
26;; publish new statuses.
28;;; Your should add the following to your Emacs configuration file:
30;; (autoload 'twitter-get-friends-timeline "twitter" nil t)
31;; (autoload 'twitter-status-edit "twitter" nil t)
32;; (global-set-key "\C-xt" 'twitter-get-friends-timeline)
33;; (add-hook 'twitter-status-edit-mode-hook 'longlines-mode)
35;; Tell it your username and password by customizing the group
38;; you can view the statuses by pressing C-x t and you can start
39;; editing a message with M-x twitter-status-edit RET. Once the
40;; message is finished press C-c C-c to publish.
47(defgroup twitter nil "Twitter status viewer"
50(defgroup twitter-faces nil "Faces for displaying Twitter statuses"
53(defface twitter-user-name-face
54 '((t (:weight bold :background "light gray")))
55 "face for user name headers"
56 :group 'twitter-faces)
58(defface twitter-time-stamp-face
59 '((t (:slant italic :background "light gray")))
60 "face for time stamp headers"
61 :group 'twitter-faces)
63(defface twitter-status-overlong-face
64 '((t (:foreground "red")))
65 "face used for characters in overly long Twitter statuses.
66The face is also used in the mode line if the character count
67remaining drops to negative.")
69(defconst twitter-friends-timeline-url
70 "http://twitter.com/statuses/friends_timeline.xml"
71 "URL used to receive the friends timeline")
73(defconst twitter-status-update-url
74 "http://twitter.com/statuses/update.xml"
75 "URL used to update Twitter status")
77(defcustom twitter-username nil
78 "Username to use for connecting to Twitter.
79If nil, you will be prompted."
80 :type '(choice (const :tag "Ask" nil) (string))
83(defcustom twitter-password nil
84 "Password to use for connecting to Twitter.
85If nil, you will be prompted."
86 :type '(choice (const :tag "Ask" nil) (string))
89(defcustom twitter-maximum-status-length 140
90 "Maximum length to allow in a Twitter status update."
94(defvar twitter-status-edit-remaining-length ""
95 "Characters remaining in a Twitter status update.
96This is displayed in the mode line.")
98(put 'twitter-status-edit-remaining-length 'risky-local-variable t)
100(defvar twitter-status-edit-overlay nil
101 "Overlay used to highlight overlong status messages.")
103(defvar twitter-status-edit-mode-map
104 (let ((map (make-sparse-keymap)))
105 (set-keymap-parent map text-mode-map)
106 (define-key map "\C-c\C-c" 'twitter-status-post)
108 "Keymap for `twitter-status-edit-mode'.")
110(defun twitter-retrieve-url (url cb)
111 "Wrapper around url-retrieve.
112Optionally sets the username and password if twitter-username and
113twitter-password are set."
114 (when (and twitter-username twitter-password)
116 (or (assoc "twitter.com:80" url-http-real-basic-auth-storage)
117 (car (push (cons "twitter.com:80" nil) url-http-real-basic-auth-storage)))))
118 (unless (assoc "Twitter API" server-cons)
119 (setcdr server-cons (cons (cons "Twitter API"
120 (base64-encode-string (concat twitter-username
121 ":" twitter-password)))
122 (cdr server-cons))))))
123 (url-retrieve url cb))
125(defun twitter-get-friends-timeline ()
126 "Fetch and display the friends timeline.
127The results are formatted and displayed in a buffer called
128*Twitter friends timeline*"
130 (twitter-retrieve-url twitter-friends-timeline-url
131 'twitter-fetched-friends-timeline))
133(defun twitter-fetched-friends-timeline (status &rest cbargs)
134 "Callback handler for fetching the Twitter friends timeline."
135 (let ((result-buffer (current-buffer)) doc)
136 ;; Make sure the temporary results buffer is killed even if the
137 ;; xml parsing raises an error
140 ;; Skip the mime headers
141 (goto-char (point-min))
142 (re-search-forward "\n\n")
143 ;; Parse the rest of the document
144 (setq doc (xml-parse-region (point) (point-max))))
145 (kill-buffer result-buffer))
146 ;; Get a clean buffer to display the results
147 (let ((buf (get-buffer-create "*Twitter friends timeline*")))
148 (with-current-buffer buf
149 (let ((inhibit-read-only t))
151 (kill-all-local-variables)
152 ;; If the GET failed then display an error instead
153 (if (plist-get status :error)
154 (twitter-show-error doc)
155 ;; Otherwise process each status node
156 (mapcar 'twitter-format-status-node (xml-get-children (car doc) 'status))))
157 (goto-char (point-min)))
160(defun twitter-get-node-text (node)
161 "Return the text of XML node NODE.
162All of the text elements are concatenated together and returned
165 (dolist (part (xml-node-children node))
167 (push part text-parts)))
168 (apply 'concat (nreverse text-parts))))
170(defun twitter-get-attrib-node (node attrib)
171 "Get the text of a child attribute node.
172If the children of XML node NODE are formatted like
173<attrib1>data</attrib1> <attrib2>data</attrib2> ... then this
174fuction will return the text of the child node named ATTRIB or
175nil if it isn't found."
176 (let ((child (xml-get-children node attrib)))
178 (twitter-get-node-text (car child))
181(defun twitter-show-error (doc)
182 "Show a Twitter error message.
183DOC should be the XML parsed document returned in the error
184message. If any information about the error can be retrieved it
185will also be displayed."
186 (insert "An error occured while trying to process a Twitter request.\n\n")
190 (eq 'hash (caar doc))
191 (setq error-node (xml-get-children (car doc) 'error)))
192 (insert (twitter-get-node-text (car error-node)))
195(defun twitter-format-status-node (status-node)
196 "Insert the contents of a Twitter status node.
197The status is formatted with text properties and insterted into
199 (let ((user-node (xml-get-children status-node 'user)) val)
201 (setq user-node (car user-node))
202 (when (setq val (twitter-get-attrib-node user-node 'name))
203 (insert (propertize val 'face 'twitter-user-name-face))))
204 (when (setq val (twitter-get-attrib-node status-node 'created_at))
205 (when (< (+ (current-column) (length val)) fill-column)
206 (setq val (concat (make-string (- fill-column
207 (+ (current-column) (length val))) ? )
209 (insert (propertize val 'face 'twitter-time-stamp-face)))
211 (when (setq val (twitter-get-attrib-node status-node 'text))
212 (fill-region (prog1 (point) (insert val)) (point)))
215(defun twitter-status-post ()
216 "Update your Twitter status.
217The contents of the current buffer are used for the status. The
218current buffer is then killed. If there is too much text in the
219buffer then you will be asked for confirmation."
221 (when (or (<= (buffer-size) twitter-maximum-status-length)
222 (y-or-n-p (format (concat "The message is %i characters long. "
223 "Are you sure? ") (buffer-size))))
224 (message "Sending status...")
225 (let ((url-request-method "POST")
226 (url-request-data (concat "status="
227 (url-hexify-string (buffer-substring
228 (point-min) (point-max))))))
229 (twitter-retrieve-url twitter-status-update-url 'twitter-status-callback))))
231(defun twitter-status-callback (status)
232 "Function called after Twitter status has been sent."
233 (let ((errmsg (plist-get status :error)))
235 (signal (car errmsg) (cdr errmsg)))
236 (kill-buffer "*Twitter Status*")
237 (message "Succesfully updated Twitter status.")))
239(defun twitter-status-edit ()
240 "Edit your twitter status in a new buffer.
241A new buffer is popped up in a special edit mode. Press
242\\[twitter-status-post] when you are finished editing to send the
245 (pop-to-buffer "*Twitter Status*")
246 (twitter-status-edit-mode))
248(defun twitter-status-edit-update-length ()
249 "Updates the character count in Twitter status buffers.
250This should be run after the text in the buffer is changed. Any
251characters after the maximum status update length are
252hightlighted in the face twitter-status-overlong-face and the
253character count on the mode line is updated."
254 ;; Update the remaining characters in the mode line
255 (let ((remaining (- twitter-maximum-status-length
257 (setq twitter-status-edit-remaining-length
260 (number-to-string remaining)
261 (propertize (number-to-string remaining)
262 'face 'twitter-status-overlong-face))
264 (force-mode-line-update)
265 ;; Highlight the characters in the buffer that are over the limit
266 (if (> (buffer-size) twitter-maximum-status-length)
267 (let ((start (+ (point-min) twitter-maximum-status-length)))
268 (if (null twitter-status-edit-overlay)
269 (overlay-put (setq twitter-status-edit-overlay
270 (make-overlay start (point-max)))
271 'face 'twitter-status-overlong-face)
272 (move-overlay twitter-status-edit-overlay
274 ;; Buffer is not too long so just hide the overlay
275 (when twitter-status-edit-overlay
276 (delete-overlay twitter-status-edit-overlay))))
278(defun twitter-status-edit-after-change (begin end old-size)
279 (twitter-status-edit-update-length))
281(define-derived-mode twitter-status-edit-mode text-mode "Twitter Status Edit"
282 "Major mode for updating your Twitter status."
283 ;; Schedule to update the character count after altering the buffer
284 (make-local-variable 'after-change-functions)
285 (add-hook 'after-change-functions 'twitter-status-edit-after-change)
286 ;; Add the remaining character count to the mode line
287 (make-local-variable 'twitter-status-edit-remaining-length)
288 ;; Copy the mode line format list so we can safely edit it without
289 ;; affecting other buffers
290 (setq mode-line-format (copy-sequence mode-line-format))
291 ;; Add the remaining characters variable after the mode display
292 (let ((n mode-line-format))
295 (when (eq 'mode-line-modes (car n))
296 (setcdr n (cons 'twitter-status-edit-remaining-length
300 ;; Make a buffer-local reference to the overlay for overlong
302 (make-local-variable 'twitter-status-edit-overlay)
303 ;; Update the mode line immediatly
304 (twitter-status-edit-update-length))
308;;; twitter.el ends here