root/lang/elisp/twittering-mode/trunk/twittering-mode.el @ 135

Revision 135, 38.0 kB (checked in by tsuyoshi, 15 years ago)

add any patches from Alberto.

Line 
1;;; twittering-mode.el --- Major mode for Twitter
2
3;; Copyright (C) 2007 Yuto Hayamizu.
4;;               2008 Tsuyoshi CHO
5
6;; Author: Y. Hayamizu <y.hayamizu@gmail.com>
7;;         Tsuyoshi CHO <Tsuyoshi.CHO+develop@Gmail.com>
8;; Created: Sep 4, 2007
9;; Version: 0.4
10;; Keywords: twitter web
11;; URL: http://lambdarepos.svnrepository.com/share/trac.cgi/browser/lang/elisp/twittering-mode
12
13;; Modified by Alberto Garcia <agarcia@igalia.com> to add the following methods:
14;; twittering-replies-timeline
15;; twittering-public-timeline
16;; twittering-user-timeline
17;; twittering-current-timeline(-(non)interractive)
18;; twittering-other-user-timeline
19;; twittering-other-user-timeline-interactive
20
21;; This file is free software; you can redistribute it and/or modify
22;; it under the terms of the GNU General Public License as published by
23;; the Free Software Foundation; either version 2, or (at your option)
24;; any later version.
25
26;; This file is distributed in the hope that it will be useful,
27;; but WITHOUT ANY WARRANTY; without even the implied warranty of
28;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
29;; GNU General Public License for more details.
30
31;; You should have received a copy of the GNU General Public License
32;; along with GNU Emacs; see the file COPYING.  If not, write to
33;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
34;; Boston, MA 02111-1307, USA.
35
36;;; Commentary:
37
38;; twittering-mode.el is a major mode for Twitter.
39;; You can check friends timeline, and update your status on Emacs.
40
41;;; Feature Request:
42
43;; URL : http://twitter.com/d00dle/statuses/577876082
44;; URL : http://twitter.com/d00dle/statuses/577879732
45;; * Status Input from Popup buffer and C-cC-c to POST.
46;; * Mark fav(star)
47;; URL : http://code.nanigac.com/source/view/419
48;; * update status for region
49
50;;; Code:
51
52(require 'cl)
53(require 'xml)
54(require 'parse-time)
55
56(defconst twittering-mode-version "0.7")
57
58(defun twittering-mode-version ()
59  "Display a message for twittering-mode version."
60  (interactive)
61  (let ((version-string
62         (format "twittering-mode-v%s" twittering-mode-version)))
63    (if (interactive-p)
64        (message "%s" version-string)
65      version-string)))
66
67(defvar twittering-mode-map (make-sparse-keymap))
68
69(defvar twittering-timer nil "Timer object for timeline refreshing will be
70stored here. DO NOT SET VALUE MANUALLY.")
71
72(defvar twittering-idle-time 20)
73
74(defvar twittering-timer-interval 90)
75
76(defvar twittering-username nil)
77
78(defvar twittering-password nil)
79
80(defvar twittering-last-timeline-retrieved nil)
81
82(defvar twittering-last-timeline-interactive nil)
83
84(defvar twittering-new-tweets-count 0
85  "Number of new tweets when `twittering-new-tweets-hook' is run")
86
87(defvar twittering-new-tweets-hook nil
88  "Hook run when new twits are received.
89
90You can read `twittering-new-tweets-count' to get the number of new
91tweets received when this hook is run.")
92
93(defvar twittering-scroll-mode nil)
94(make-variable-buffer-local 'twittering-scroll-mode)
95
96(defvar twittering-jojo-mode nil)
97(make-variable-buffer-local 'twittering-jojo-mode)
98
99(defvar twittering-status-format nil)
100(setq twittering-status-format "%i %s,  %@:\n  %t // from %f%L")
101;; %s - screen_name
102;; %S - name
103;; %i - profile_image
104;; %d - description
105;; %l - location
106;; %L - " [location]"
107;; %u - url
108;; %j - user.id
109;; %p - protected?
110;; %c - created_at (raw UTC string)
111;; %C{time-format-str} - created_at (formatted with time-format-str)
112;; %@ - X seconds ago
113;; %t - text
114;; %' - truncated
115;; %f - source
116;; %# - id
117
118(defvar twittering-buffer "*twittering*")
119(defun twittering-buffer ()
120  (twittering-get-or-generate-buffer twittering-buffer))
121
122(defvar twittering-http-buffer "*twittering-http-buffer*")
123(defun twittering-http-buffer ()
124  (twittering-get-or-generate-buffer twittering-http-buffer))
125
126(defvar twittering-timeline-data nil)
127(defvar twittering-timeline-last-update nil)
128
129(defvar twittering-username-face 'twittering-username-face)
130(defvar twittering-uri-face 'twittering-uri-face)
131
132(defun twittering-get-or-generate-buffer (buffer)
133  (if (bufferp buffer)
134      (if (buffer-live-p buffer)
135          buffer
136        (generate-new-buffer (buffer-name buffer)))
137    (if (stringp buffer)
138        (or (get-buffer buffer)
139            (generate-new-buffer buffer)))))
140
141(defun assocref (item alist)
142  (cdr (assoc item alist)))
143(defmacro list-push (value listvar)
144  `(setq ,listvar (cons ,value ,listvar)))
145
146;;; Proxy
147(defvar twittering-proxy-use nil)
148(defvar twittering-proxy-server nil)
149(defvar twittering-proxy-port 8080)
150(defvar twittering-proxy-user nil)
151(defvar twittering-proxy-password nil)
152
153(defun twittering-toggle-proxy () ""
154  (interactive)
155  (setq twittering-proxy-use
156        (not twittering-proxy-use))
157  (message "%s %s"
158           "Use Proxy:"
159           (if twittering-proxy-use
160               "on" "off")))
161
162(defun twittering-user-agent-default-function ()
163  "Twittering mode default User-Agent function."
164  (concat "Emacs/"
165          (int-to-string emacs-major-version) "." (int-to-string
166                                                   emacs-minor-version)
167          " "
168          "Twittering-mode/"
169          twittering-mode-version))
170
171(defvar twittering-user-agent-function 'twittering-user-agent-default-function)
172
173(defun twittering-user-agent ()
174  "Return User-Agent header string."
175  (funcall twittering-user-agent-function))
176
177;;; to show image files
178
179(defvar twittering-wget-buffer "*twittering-wget-buffer*")
180(defun twittering-wget-buffer ()
181  (twittering-get-or-generate-buffer twittering-wget-buffer))
182
183(defvar twittering-tmp-dir
184  (expand-file-name (concat "twmode-images-" (user-login-name))
185                    temporary-file-directory))
186
187(defvar twittering-icon-mode nil "You MUST NOT CHANGE this variable
188directory. You should change through function'twittering-icon-mode'")
189
190(make-variable-buffer-local 'twittering-icon-mode)
191(defun twittering-icon-mode (&optional arg)
192  (interactive)
193  (setq twittering-icon-mode
194        (if twittering-icon-mode
195            (if (null arg)
196                nil
197              (> (prefix-numeric-value arg) 0))
198          (when (or (null arg)
199                    (and arg (> (prefix-numeric-value arg) 0)))
200            (when (file-writable-p twittering-tmp-dir)
201              (progn
202                (if (not (file-directory-p twittering-tmp-dir))
203                    (make-directory twittering-tmp-dir))
204                t)))))
205  (twittering-render-timeline))
206
207(defun twittering-scroll-mode (&optional arg)
208  (interactive)
209  (setq twittering-scroll-mode
210        (if (null arg)
211            (not twittering-scroll-mode)
212          (> (prefix-numeric-value arg) 0))))
213
214(defun twittering-jojo-mode (&optional arg)
215  (interactive)
216  (setq twittering-jojo-mode
217        (if (null arg)
218            (not twittering-jojo-mode)
219          (> (prefix-numeric-value arg) 0))))
220
221(defvar twittering-image-stack nil)
222
223(defun twittering-image-type (file-name)
224  (cond
225   ((string-match "\\.jpe?g" file-name) 'jpeg)
226   ((string-match "\\.png" file-name) 'png)
227   ((string-match "\\.gif" file-name) 'gif)
228   (t nil)))
229
230(defun twittering-setftime (fmt string uni)
231  (format-time-string fmt ; like "%Y-%m-%d %H:%M:%S"
232                      (apply 'encode-time (parse-time-string string))
233                      uni))
234(defun twittering-local-strftime (fmt string)
235  (twittering-setftime fmt string nil))
236(defun twittering-global-strftime (fmt string)
237  (twittering-setftime fmt string t))
238
239
240(defvar twittering-debug-mode nil)
241(defvar twittering-debug-buffer "*debug*")
242(defun twittering-debug-buffer ()
243  (twittering-get-or-generate-buffer twittering-debug-buffer))
244(defmacro debug-print (obj)
245  (let ((obsym (gensym)))
246    `(let ((,obsym ,obj))
247       (if twittering-debug-mode
248           (with-current-buffer (twittering-debug-buffer)
249             (insert (prin1-to-string ,obsym))
250             (newline)
251             ,obsym)
252         ,obsym))))
253
254(defun twittering-debug-mode ()
255  (interactive)
256  (setq twittering-debug-mode
257        (not twittering-debug-mode))
258  (message (if twittering-debug-mode "debug mode:on" "debug mode:off")))
259
260(if twittering-mode-map
261    (let ((km twittering-mode-map))
262      (define-key km "\C-c\C-f" 'twittering-friends-timeline)
263      (define-key km "\C-c\C-r" 'twittering-replies-timeline)
264      (define-key km "\C-c\C-g" 'twittering-public-timeline)
265      (define-key km "\C-c\C-u" 'twittering-user-timeline)
266      (define-key km "\C-c\C-s" 'twittering-update-status-interactive)
267      (define-key km "\C-c\C-e" 'twittering-erase-old-statuses)
268      (define-key km "\C-m" 'twittering-enter)
269      (define-key km "\C-c\C-l" 'twittering-update-lambda)
270      (define-key km [mouse-1] 'twittering-click)
271      (define-key km "\C-c\C-v" 'twittering-view-user-page)
272      (define-key km "g" 'twittering-current-timeline-interactive)
273      (define-key km "v" 'twittering-other-user-timeline)
274      (define-key km "V" 'twittering-other-user-timeline-interactive)
275      ;; (define-key km "j" 'next-line)
276      ;; (define-key km "k" 'previous-line)
277      (define-key km "j" 'twittering-goto-next-status)
278      (define-key km "k" 'twittering-goto-previous-status)
279      (define-key km "l" 'forward-char)
280      (define-key km "h" 'backward-char)
281      (define-key km "0" 'beginning-of-line)
282      (define-key km "^" 'beginning-of-line-text)
283      (define-key km "$" 'end-of-line)
284      (define-key km "n" 'twittering-goto-next-status-of-user)
285      (define-key km "p" 'twittering-goto-previous-status-of-user)
286      (define-key km [backspace] 'backward-char)
287      (define-key km "G" 'end-of-buffer)
288      (define-key km "H" 'beginning-of-buffer)
289      (define-key km "i" 'twittering-icon-mode)
290      (define-key km "s" 'twittering-scroll-mode)
291      (define-key km "t" 'twittering-toggle-proxy)
292      (define-key km "\C-c\C-p" 'twittering-toggle-proxy)
293      nil))
294
295(defvar twittering-mode-syntax-table nil "")
296
297(if twittering-mode-syntax-table
298    ()
299  (setq twittering-mode-syntax-table (make-syntax-table))
300  ;; (modify-syntax-entry ?  "" twittering-mode-syntax-table)
301  (modify-syntax-entry ?\" "w"  twittering-mode-syntax-table)
302  )
303
304(defun twittering-mode-init-variables ()
305  ;; (make-variable-buffer-local 'variable)
306  ;; (setq variable nil)
307  (font-lock-mode -1)
308  (defface twittering-username-face
309    `((t nil)) "" :group 'faces)
310  (copy-face 'font-lock-string-face 'twittering-username-face)
311  (set-face-attribute 'twittering-username-face nil :underline t)
312  (defface twittering-uri-face
313    `((t nil)) "" :group 'faces)
314  (set-face-attribute 'twittering-uri-face nil :underline t)
315  (add-to-list 'minor-mode-alist '(twittering-icon-mode " tw-icon"))
316  (add-to-list 'minor-mode-alist '(twittering-scroll-mode " tw-scroll"))
317  (add-to-list 'minor-mode-alist '(twittering-jojo-mode " tw-jojo"))
318  )
319
320(defmacro case-string (str &rest clauses)
321  `(cond
322    ,@(mapcar
323       (lambda (clause)
324         (let ((keylist (car clause))
325               (body (cdr clause)))
326           `(,(if (listp keylist)
327                  `(or ,@(mapcar (lambda (key) `(string-equal ,str ,key))
328                                 keylist))
329                't)
330             ,@body)))
331       clauses)))
332
333;; If you use Emacs21, decode-char 'ucs will fail unless Mule-UCS is loaded.
334;; TODO: Show error messages if Emacs 21 without Mule-UCS
335(defmacro twittering-ucs-to-char (num)
336  (if (functionp 'ucs-to-char)
337      `(ucs-to-char ,num)
338    `(decode-char 'ucs ,num)))
339
340(defvar twittering-mode-string "Twittering mode")
341
342(defvar twittering-mode-hook nil
343  "Twittering-mode hook.")
344
345(defun twittering-mode ()
346  "Major mode for Twitter
347\\{twittering-mode-map}"
348  (interactive)
349  (switch-to-buffer (twittering-buffer))
350  (kill-all-local-variables)
351  (twittering-mode-init-variables)
352  (use-local-map twittering-mode-map)
353  (setq major-mode 'twittering-mode)
354  (setq mode-name twittering-mode-string)
355  (set-syntax-table twittering-mode-syntax-table)
356  (run-hooks 'twittering-mode-hook)
357  (font-lock-mode -1)
358  (twittering-start))
359
360;;;
361;;; Basic HTTP functions
362;;;
363
364(defun twittering-http-get (method-class method &optional parameters sentinel)
365  (if (null sentinel) (setq sentinel 'twittering-http-get-default-sentinel))
366
367  ;; clear the buffer
368  (save-excursion
369    (set-buffer (twittering-http-buffer))
370    (erase-buffer))
371
372  (let (proc server port
373             (proxy-user twittering-proxy-user)
374             (proxy-password twittering-proxy-password))
375    (condition-case nil
376        (progn
377          (if (and twittering-proxy-use twittering-proxy-server)
378              (setq server twittering-proxy-server
379                    port (if (integerp twittering-proxy-port)
380                             (int-to-string twittering-proxy-port)
381                           twittering-proxy-port))
382            (setq server "twitter.com"
383                  port "80"))
384          (setq proc
385                (open-network-stream
386                 "network-connection-process" (twittering-http-buffer)
387                 server (string-to-number port)))
388          (set-process-sentinel proc sentinel)
389          (process-send-string
390           proc
391           (let ((nl "\r\n")
392                 request)
393             (setq request
394                   (concat "GET http://twitter.com/" method-class "/" method
395                           ".xml"
396                           (when parameters
397                             (concat "?"
398                                     (mapconcat
399                                      (lambda (param-pair)
400                                        (format "%s=%s"
401                                                (twittering-percent-encode (car
402                                                                            param-pair))
403                                                (twittering-percent-encode (cdr
404                                                                            param-pair))))
405                                      parameters
406                                      "&")))
407                           " HTTP/1.1" nl
408                           "Host: twitter.com" nl
409                           "User-Agent: " (twittering-user-agent) nl
410                           "Authorization: Basic "
411                           (base64-encode-string
412                            (concat twittering-username ":"
413                                    (twittering-get-password)))
414                           nl
415                           "Accept: text/xml"
416                           ",application/xml"
417                           ",application/xhtml+xml"
418                           ",application/html;q=0.9"
419                           ",text/plain;q=0.8"
420                           ",image/png,*/*;q=0.5" nl
421                           "Accept-Charset: utf-8;q=0.7,*;q=0.7" nl
422                           (when twittering-proxy-use
423                             "Proxy-Connection: Keep-Alive" nl
424                             (when (and proxy-user proxy-password)
425                               (concat
426                                "Proxy-Authorization: Basic "
427                                (base64-encode-string
428                                 (concat proxy-user ":"
429                                         proxy-password))
430                                nl)))
431                           nl))
432             (debug-print (concat "GET Request\n" request))
433             request)))
434      (error
435       (message "Failure: HTTP GET") nil))))
436
437(defun twittering-http-get-default-sentinel (proc stat &optional suc-msg)
438  (let ((header (twittering-get-response-header))
439        (body (twittering-get-response-body))
440        (status nil)
441        )
442    (if (string-match "HTTP/1\.[01] \\([a-z0-9 ]+\\)\r?\n" header)
443        (progn
444          (setq status (match-string-no-properties 1 header))
445          (case-string
446           status
447           (("200 OK")
448            (setq twittering-new-tweets-count
449                  (count t (mapcar
450                            #'twittering-cache-status-datum
451                            (reverse (twittering-xmltree-to-status
452                                      body)))))
453            (if (and (> twittering-new-tweets-count 0)
454                     (not twittering-last-timeline-interactive))
455                (run-hooks 'twittering-new-tweets-hook))
456            (setq twittering-last-timeline-interactive t)
457            (twittering-render-timeline)
458            (message (if suc-msg suc-msg "Success: Get.")))
459           (t (message status))))
460      (message "Failure: Bad http response.")))
461  )
462
463(defun twittering-render-timeline ()
464  (with-current-buffer (twittering-buffer)
465    (let ((point (point))
466          (end (point-max)))
467      (setq buffer-read-only nil)
468      (erase-buffer)
469      (mapc (lambda (status)
470              (insert (twittering-format-status
471                       status twittering-status-format))
472              (fill-region-as-paragraph
473               (save-excursion (beginning-of-line) (point)) (point))
474              (insert "\n"))
475            twittering-timeline-data)
476      (if (and twittering-image-stack window-system)
477          (clear-image-cache))
478      (setq buffer-read-only t)
479      (debug-print (current-buffer))
480      (goto-char (+ point (if twittering-scroll-mode (- (point-max) end) 0))))
481    ))
482
483(defun twittering-format-status (status format-str)
484  (flet ((attr (key)
485               (assocref key status))
486         (profile-image
487          ()
488          (let ((profile-image-url (attr 'user-profile-image-url))
489                (icon-string "\n  "))
490            (if (string-match "/\\([^/?]+\\)\\(?:\\?\\|$\\)" profile-image-url)
491                (let ((filename (match-string-no-properties 1
492                                                            profile-image-url)))
493                  ;; download icons if does not exist
494                  (if (file-exists-p (concat twittering-tmp-dir
495                                             "/" filename))
496                      t
497                    (add-to-list 'twittering-image-stack profile-image-url))
498
499                  (when (and icon-string twittering-icon-mode)
500                    (set-text-properties
501                     1 2 `(display
502                           (image :type ,(twittering-image-type filename)
503                                  :file ,(concat twittering-tmp-dir
504                                                 "/"
505                                                 filename)))
506                     icon-string)
507                    icon-string)
508                  )))))
509    (let ((cursor 0)
510          (result ())
511          c
512          found-at)
513      (setq cursor 0)
514      (setq result '())
515      (while (setq found-at (string-match "%\\(C{\\([^}]+\\)}\\|[A-Za-z#@']\\)"
516                                          format-str cursor))
517        (setq c (string-to-char (match-string-no-properties 1 format-str)))
518        (if (> found-at cursor)
519            (list-push (substring format-str cursor found-at) result)
520          "|")
521        (setq cursor (match-end 1))
522
523        (case c
524          ((?s)                         ; %s - screen_name
525           (list-push (attr 'user-screen-name) result))
526          ((?S)                         ; %S - name
527           (list-push (attr 'user-name) result))
528          ((?i)                         ; %i - profile_image
529           (list-push (profile-image) result))
530          ((?d)                         ; %d - description
531           (list-push (attr 'user-description) result))
532          ((?l)                         ; %l - location
533           (list-push (attr 'user-location) result))
534          ((?L)                         ; %L - " [location]"
535           (let ((location (attr 'user-location)))
536             (unless (or (null location) (string= "" location))
537               (list-push (concat " [" location "]") result)) ))
538          ((?u)                         ; %u - url
539           (list-push (attr 'user-url) result))
540          ((?j)                         ; %j - user.id
541           (list-push (attr 'user-id) result))
542          ((?p)                         ; %p - protected?
543           (let ((protected (attr 'user-protected)))
544             (when (string= "true" protected)
545               (list-push "[x]" result))))
546          ((?c)                     ; %c - created_at (raw UTC string)
547           (list-push (attr 'created-at) result))
548          ((?C) ; %C{time-format-str} - created_at (formatted with
549           ; time-format-str)
550           (list-push (twittering-local-strftime
551                       (or (match-string-no-properties 2 format-str) "%H:%M:%S")
552                       (attr 'created-at))
553                      result))
554          ((?@)                         ; %@ - X seconds ago
555           (let ((created-at
556                  (apply
557                   'encode-time
558                   (parse-time-string (attr 'created-at))))
559                 (now (current-time)))
560             (let ((secs (+ (* (- (car now) (car created-at)) 65536)
561                            (- (cadr now) (cadr created-at))))
562                   time-string url)
563               (setq time-string
564                     (cond ((< secs 5) "less than 5 seconds ago")
565                           ((< secs 10) "less than 10 seconds ago")
566                           ((< secs 20) "less than 20 seconds ago")
567                           ((< secs 30) "half a minute ago")
568                           ((< secs 60) "less than a minute ago")
569                           ((< secs 150) "1 minute ago")
570                           ((< secs 2400) (format "%d minutes ago"
571                                                  (/ (+ secs 30) 60)))
572                           ((< secs 5400) "about 1 hour ago")
573                           ((< secs 84600) (format "about %d hours ago"
574                                                   (/ (+ secs 1800) 3600)))
575                           (t (format-time-string "%I:%M %p %B %d, %Y"
576                                                  created-at))))
577               (setq url (twittering-get-status-url (attr 'user-screen-name)
578                                                    (attr 'id)))
579               ;; make status url clickable
580               (add-text-properties
581                0 (length time-string)
582                `(mouse-face highlight
583                             face twittering-uri-face
584                             uri ,url)
585                time-string)
586               (list-push time-string result))))
587          ((?t)                         ; %t - text
588           (list-push                   ;(clickable-text)
589            (attr 'text)
590            result))
591          ((?')                         ; %' - truncated
592           (let ((truncated (attr 'truncated)))
593             (when (string= "true" truncated)
594               (list-push "..." result))))
595          ((?f)                         ; %f - source
596           (list-push (attr 'source) result))
597          ((?#)                         ; %# - id
598           (list-push (attr 'id) result))
599          (t
600           (list-push (char-to-string c) result)))
601        )
602      (list-push (substring format-str cursor) result)
603      (let ((formatted-status (apply 'concat (nreverse result))))
604        (add-text-properties 0 (length formatted-status)
605                             `(username ,(attr 'user-screen-name))
606                             formatted-status)
607        formatted-status)
608      )))
609
610(defun twittering-http-post
611  (method-class method &optional parameters contents sentinel)
612  "Send HTTP POST request to twitter.com
613
614METHOD-CLASS must be one of Twitter API method classes
615 (statuses, users or direct_messages).
616METHOD must be one of Twitter API method which belongs to METHOD-CLASS.
617PARAMETERS is alist of URI parameters.
618 ex) ((\"mode\" . \"view\") (\"page\" . \"6\")) => <URI>?mode=view&page=6"
619  (if (null sentinel) (setq sentinel 'twittering-http-post-default-sentinel))
620
621  ;; clear the buffer
622  (save-excursion
623    (set-buffer (twittering-http-buffer))
624    (erase-buffer))
625
626  (let (proc server port
627             (proxy-user twittering-proxy-user)
628             (proxy-password twittering-proxy-password))
629    (progn
630      (if (and twittering-proxy-use twittering-proxy-server)
631          (setq server twittering-proxy-server
632                port (if (integerp twittering-proxy-port)
633                         (int-to-string twittering-proxy-port)
634                       twittering-proxy-port))
635        (setq server "twitter.com"
636              port "80"))
637      (setq proc
638            (open-network-stream
639             "network-connection-process" (twittering-http-buffer)
640             server (string-to-number port)))
641      (set-process-sentinel proc sentinel)
642      (process-send-string
643       proc
644       (let ((nl "\r\n")
645             request)
646         (setq  request
647                (concat "POST http://twitter.com/" method-class "/" method ".xml"
648                        (when parameters
649                          (concat "?"
650                                  (mapconcat
651                                   (lambda (param-pair)
652                                     (format "%s=%s"
653                                             (twittering-percent-encode (car param-pair))
654                                             (twittering-percent-encode (cdr param-pair))))
655                                   parameters
656                                   "&")))
657                        " HTTP/1.1" nl
658                        "Host: twitter.com" nl
659                        "User-Agent: " (twittering-user-agent) nl
660                        "Authorization: Basic "
661                        (base64-encode-string
662                         (concat twittering-username ":" (twittering-get-password)))
663                        nl
664                        "Content-Type: text/plain" nl
665                        "Content-Length: 0" nl
666                        (when twittering-proxy-use
667                          "Proxy-Connection: Keep-Alive" nl
668                          (when (and proxy-user proxy-password)
669                            (concat
670                             "Proxy-Authorization: Basic "
671                             (base64-encode-string
672                              (concat proxy-user ":"
673                                      proxy-password))
674                             nl)))
675                        nl))
676         (debug-print (concat "POST Request\n" request))
677         request)))))
678
679(defun twittering-http-post-default-sentinel (proc stat &optional suc-msg)
680
681  (condition-case err-signal
682      (let ((header (twittering-get-response-header))
683            ;; (body (twittering-get-response-body)) not used now.
684            (status nil))
685        (string-match "HTTP/1\.1 \\([a-z0-9 ]+\\)\r?\n" header)
686        (setq status (match-string-no-properties 1 header))
687        (case-string status
688                     (("200 OK")
689                      (message (if suc-msg suc-msg "Success: Post")))
690                     (t (message status)))
691        )
692    (error (message (prin1-to-string err-signal))))
693  )
694
695(defun twittering-get-response-header (&optional buffer)
696  "Exract HTTP response header from HTTP response.
697`buffer' may be a buffer or the name of an existing buffer.
698 If `buffer' is omitted, the value of `twittering-http-buffer' is used as `buffer'."
699  (if (stringp buffer) (setq buffer (get-buffer buffer)))
700  (if (null buffer) (setq buffer (twittering-http-buffer)))
701  (save-excursion
702    (set-buffer buffer)
703    (let ((content (buffer-string)))
704      (substring content 0 (string-match "\r?\n\r?\n" content)))))
705
706(defun twittering-get-response-body (&optional buffer)
707  "Exract HTTP response body from HTTP response, parse it as XML, and return a
708XML tree as list. `buffer' may be a buffer or the name of an existing buffer. If
709`buffer' is omitted, the value of `twittering-http-buffer' is used as `buffer'."
710  (if (stringp buffer) (setq buffer (get-buffer buffer)))
711  (if (null buffer) (setq buffer (twittering-http-buffer)))
712  (save-excursion
713    (set-buffer buffer)
714    (let ((content (buffer-string)))
715      (let ((content (buffer-string)))
716        (xml-parse-region (+ (string-match "\r?\n\r?\n" content)
717                             (length (match-string 0 content)))
718                          (point-max)))
719      )))
720
721(defun twittering-cache-status-datum (status-datum &optional data-var)
722  "Cache status datum into data-var(default twittering-timeline-data)
723If STATUS-DATUM is already in DATA-VAR, return nil. If not, return t."
724  (if (null data-var)
725      (setf data-var 'twittering-timeline-data))
726  (let ((id (cdr (assq 'id status-datum))))
727    (if (or (null (symbol-value data-var))
728            (not (find-if
729                  (lambda (item)
730                    (string= id (cdr (assq 'id item))))
731                  (symbol-value data-var))))
732        (progn
733          (if twittering-jojo-mode
734              (twittering-update-jojo (cdr (assq 'user-screen-name
735                                                 status-datum))
736                                      (cdr (assq 'text status-datum))))
737          (set data-var (cons status-datum (symbol-value data-var)))
738          t)
739      nil)))
740
741(defun twittering-status-to-status-datum (status)
742  (flet ((assq-get (item seq)
743                   (car (cddr (assq item seq)))))
744    (let* ((status-data (cddr status))
745           id text source created-at truncated
746           (user-data (cddr (assq 'user status-data)))
747           user-id user-name
748           user-screen-name
749           user-location
750           user-description
751           user-profile-image-url
752           user-url
753           user-protected
754           regex-index)
755
756      (setq id (assq-get 'id status-data))
757      (setq text (twittering-decode-html-entities
758                  (assq-get 'text status-data)))
759      (setq source (twittering-decode-html-entities
760                    (assq-get 'source status-data)))
761      (setq created-at (assq-get 'created_at status-data))
762      (setq truncated (assq-get 'truncated status-data))
763      (setq user-id (assq-get 'id user-data))
764      (setq user-name (twittering-decode-html-entities
765                       (assq-get 'name user-data)))
766      (setq user-screen-name (twittering-decode-html-entities
767                              (assq-get 'screen_name user-data)))
768      (setq user-location (twittering-decode-html-entities
769                           (assq-get 'location user-data)))
770      (setq user-description (twittering-decode-html-entities
771                              (assq-get 'description user-data)))
772      (setq user-profile-image-url (assq-get 'profile_image_url user-data))
773      (setq user-url (assq-get 'url user-data))
774      (setq user-protected (assq-get 'protected user-data))
775
776      ;; make username clickable
777      (add-text-properties
778       0 (length user-name)
779       `(mouse-face highlight
780                    uri ,(concat "http://twitter.com/" user-screen-name)
781                    status-id ,id
782                    face twittering-username-face)
783       user-name)
784
785      ;; make screen-name clickable
786      (add-text-properties
787       0 (length user-screen-name)
788       `(mouse-face highlight
789                    uri ,(concat "http://twitter.com/" user-screen-name)
790                    status-id ,id
791                    face twittering-username-face)
792       user-screen-name)
793
794      ;; make URI clickable
795      (setq regex-index 0)
796      (while regex-index
797        (setq regex-index
798              (string-match "@\\([_a-zA-Z0-9]+\\)\\|\\(https?://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+\\)"
799                            text
800                            regex-index))
801        (when regex-index
802          (let* ((matched-string (match-string-no-properties 0 text))
803                 (screen-name (match-string-no-properties 1 text))
804                 (uri (match-string-no-properties 2 text)))
805            (add-text-properties
806             (if screen-name
807                 (+ 1 (match-beginning 0))
808               (match-beginning 0))
809             (match-end 0)
810             (if screen-name
811                 `(mouse-face
812                   highlight
813                   face twittering-uri-face
814                   uri ,(concat "http://twitter.com/" screen-name))
815               `(mouse-face highlight
816                            face twittering-uri-face
817                            uri ,uri))
818             text))
819          (setq regex-index (match-end 0)) ))
820
821
822      ;; make source pretty and clickable
823      (if (string-match "<a href=\"\\(.*\\)\">\\(.*\\)</a>" source)
824          (let ((uri (match-string-no-properties 1 source))
825                (caption (match-string-no-properties 2 source)))
826            (setq source caption)
827            (add-text-properties
828             0 (length source)
829             `(mouse-face highlight
830                          uri ,uri
831                          face twittering-uri-face
832                          source ,source)
833             source)
834            ))
835
836      ;; save last update time
837      (setq twittering-timeline-last-update created-at)
838
839      (mapcar
840       (lambda (sym)
841         `(,sym . ,(symbol-value sym)))
842       '(id text source created-at truncated
843            user-id user-name user-screen-name user-location
844            user-description
845            user-profile-image-url
846            user-url
847            user-protected)))))
848
849(defun twittering-xmltree-to-status (xmltree)
850  (mapcar #'twittering-status-to-status-datum
851          ;; quirk to treat difference between xml.el in Emacs21 and Emacs22
852          ;; On Emacs22, there may be blank strings
853          (let ((ret nil) (statuses (reverse (cddr (car xmltree)))))
854            (while statuses
855              (if (consp (car statuses))
856                  (setq ret (cons (car statuses) ret)))
857              (setq statuses (cdr statuses)))
858            ret)))
859
860(defun twittering-percent-encode (str &optional coding-system)
861  (if (or (null coding-system)
862          (not (coding-system-p coding-system)))
863      (setq coding-system 'utf-8))
864  (mapconcat
865   (lambda (c)
866     (cond
867      ((twittering-url-reserved-p c)
868       (char-to-string c))
869      ((eq c ? ) "+")
870      (t (format "%%%x" c))))
871   (encode-coding-string str coding-system)
872   ""))
873
874(defun twittering-url-reserved-p (ch)
875  (or (and (<= ?A ch) (<= ch ?z))
876      (and (<= ?0 ch) (<= ch ?9))
877      (eq ?. ch)
878      (eq ?- ch)
879      (eq ?_ ch)
880      (eq ?~ ch)))
881
882(defun twittering-decode-html-entities (encoded-str)
883  (if encoded-str
884      (let ((cursor 0)
885            (found-at nil)
886            (result '()))
887        (while (setq found-at
888                     (string-match "&\\(#\\([0-9]+\\)\\|\\([A-Za-z]+\\)\\);"
889                                   encoded-str cursor))
890          (when (> found-at cursor)
891            (list-push (substring encoded-str cursor found-at) result))
892          (let ((number-entity (match-string-no-properties 2 encoded-str))
893                (letter-entity (match-string-no-properties 3 encoded-str)))
894            (cond (number-entity
895                   (list-push
896                    (char-to-string
897                     (twittering-ucs-to-char
898                      (string-to-number number-entity))) result))
899                  (letter-entity
900                   (cond ((string= "gt" letter-entity) (list-push ">" result))
901                         ((string= "lt" letter-entity) (list-push "<" result))
902                         (t (list-push "?" result))))
903                  (t (list-push "?" result)))
904            (setq cursor (match-end 0))))
905        (list-push (substring encoded-str cursor) result)
906        (apply 'concat (nreverse result)))
907    ""))
908
909(defun twittering-timer-action (func)
910  (let ((buf (get-buffer twittering-buffer)))
911    (if (null buf)
912        (twittering-stop)
913      (funcall func)
914      )))
915
916(defun twittering-update-status-if-not-blank (status &optional reply-to-id)
917  (if (string-match "^\\s-*\\(?:@[-_a-z0-9]+\\)?\\s-*$" status)
918      nil
919    (let ((parameters `(("status" . ,status)
920                        ("source" . "twmode"))))
921      (twittering-http-post "statuses" "update"
922                            (if reply-to-id
923                                (cons `("in_reply_to_status_id" . ,(format "%s" reply-to-id))
924                                      parameters)
925                              parameters)))
926    t))
927
928(defun twittering-update-status-from-minibuffer (&optional init-str
929                                                           reply-to-id)
930  (if (null init-str) (setq init-str ""))
931  (let ((status init-str) (not-posted-p t))
932    (while not-posted-p
933      (setq status (read-from-minibuffer "status: " status nil nil nil nil t))
934      (setq not-posted-p
935            (not (twittering-update-status-if-not-blank status reply-to-id))))
936    ))
937
938(defun twittering-update-lambda ()
939  (interactive)
940  (twittering-http-post
941   "statuses" "update"
942   `(("status" . "\xd34b\xd22b\xd26f\xd224\xd224\xd268\xd34b")
943     ("source" . "twmode"))))
944
945(defun twittering-update-jojo (usr msg)
946  (if (string-match "\xde21\xd24b\\(\xd22a\xe0b0\\|\xdaae\xe6cd\\)\xd24f\xd0d6\\([^\xd0d7]+\\)\xd0d7\xd248\xdc40\xd226"
947                    msg)
948      (twittering-http-post
949       "statuses" "update"
950       `(("status" . ,(concat
951                       "@" usr " "
952                       (match-string-no-properties 2 msg)
953                       "\xd0a1\xd24f\xd243!?"))
954         ("source" . "twmode")))))
955
956;;;
957;;; Commands
958;;;
959
960(defun twittering-start (&optional action)
961  (interactive)
962  (if (null action)
963      (setq action #'twittering-current-timeline-noninteractive))
964  (if twittering-timer
965      nil
966    (setq twittering-timer
967          (run-at-time "0 sec"
968                       twittering-timer-interval
969                       #'twittering-timer-action action))))
970
971(defun twittering-stop ()
972  (interactive)
973  (cancel-timer twittering-timer)
974  (setq twittering-timer nil))
975
976(defun twittering-get-timeline (method)
977  (if (not (eq twittering-last-timeline-retrieved method))
978      (setq twittering-timeline-last-update nil
979            twittering-timeline-data nil))
980  (setq twittering-last-timeline-retrieved method)
981  (let ((buf (get-buffer twittering-buffer)))
982    (if (not buf)
983        (twittering-stop)
984      (if (not twittering-timeline-last-update)
985          (twittering-http-get "statuses" method)
986        (let* ((system-time-locale "C")
987               (since
988                (twittering-global-strftime
989                 "%a, %d %b %Y %H:%M:%S GMT"
990                 twittering-timeline-last-update)))
991          (twittering-http-get "statuses" method
992                               `(("since" . ,since)))))))
993
994  (if (and twittering-icon-mode window-system)
995      (if twittering-image-stack
996          (let ((proc
997                 (apply
998                  #'start-process
999                  "wget-images"
1000                  (twittering-wget-buffer)
1001                  "wget"
1002                  (format "--directory-prefix=%s" twittering-tmp-dir)
1003                  "--no-clobber"
1004                  "--quiet"
1005                  twittering-image-stack)))
1006            (set-process-sentinel
1007             proc
1008             (lambda (proc stat)
1009               (clear-image-cache)
1010               (save-excursion
1011                 (set-buffer (twittering-wget-buffer))
1012                 )))))))
1013
1014(defun twittering-friends-timeline ()
1015  (interactive)
1016  (twittering-get-timeline "friends_timeline"))
1017
1018(defun twittering-replies-timeline ()
1019  (interactive)
1020  (twittering-get-timeline "replies"))
1021
1022(defun twittering-public-timeline ()
1023  (interactive)
1024  (twittering-get-timeline "public_timeline"))
1025
1026(defun twittering-user-timeline ()
1027  (interactive)
1028  (twittering-get-timeline "user_timeline"))
1029
1030(defun twittering-current-timeline-interactive ()
1031  (interactive)
1032  (setq twittering-last-timeline-interactive t)
1033  (twittering-current-timeline))
1034
1035(defun twittering-current-timeline-noninteractive ()
1036  (setq twittering-last-timeline-interactive nil)
1037  (twittering-current-timeline))
1038
1039(defun twittering-current-timeline ()
1040  (if (not twittering-last-timeline-retrieved)
1041      (setq twittering-last-timeline-retrieved "friends_timeline"))
1042  (twittering-get-timeline twittering-last-timeline-retrieved))
1043
1044(defun twittering-update-status-interactive ()
1045  (interactive)
1046  (twittering-update-status-from-minibuffer))
1047
1048(defun twittering-erase-old-statuses ()
1049  (interactive)
1050  (setq twittering-timeline-data nil)
1051  (if (not twittering-last-timeline-retrieved)
1052      (setq twittering-last-timeline-retrieved "friends_timeline"))
1053  (if (not twittering-timeline-last-update)
1054      (twittering-http-get "statuses" twittering-last-timeline-retrieved)
1055    (let* ((system-time-locale "C")
1056           (since
1057            (twittering-global-strftime
1058             "%a, %d %b %Y %H:%M:%S GMT"
1059             twittering-timeline-last-update)))
1060      (twittering-http-get "statuses" twittering-last-timeline-retrieved
1061                           `(("since" . ,since))))))
1062
1063(defun twittering-click ()
1064  (interactive)
1065  (let ((uri (get-text-property (point) 'uri)))
1066    (if uri
1067        (browse-url uri))))
1068
1069(defun twittering-enter ()
1070  (interactive)
1071  (let ((username (get-text-property (point) 'username))
1072        (status-id (get-text-property (point) 'status-id))
1073        (uri (get-text-property (point) 'uri)))
1074    (if username
1075        (twittering-update-status-from-minibuffer (concat "@" username " ")
1076                                                  status-id)
1077      (if uri
1078          (browse-url uri)))))
1079
1080(defun twittering-view-user-page ()
1081  (interactive)
1082  (let ((uri (get-text-property (point) 'uri)))
1083    (if uri
1084        (browse-url uri))))
1085
1086(defun twittering-other-user-timeline ()
1087  (interactive)
1088  (let ((username (get-text-property (point) 'username)))
1089    (if (> (length username) 0)
1090        (twittering-get-timeline (concat "user_timeline/" username))
1091      (message "No user selected"))))
1092
1093(defun twittering-other-user-timeline-interactive ()
1094  (interactive)
1095  (let ((username (read-from-minibuffer "user: " (get-text-property (point) 'username))))
1096    (if (> (length username) 0)
1097        (twittering-get-timeline (concat "user_timeline/" username))
1098      (message "No user selected"))))
1099
1100(defun twittering-reply-to-user ()
1101  (interactive)
1102  (let ((username (get-text-property (point) 'username)))
1103    (if username
1104        (twittering-update-status-from-minibuffer (concat "@" username " ")))))
1105
1106(defun twittering-get-password ()
1107  (or twittering-password
1108      (setq twittering-password (read-passwd "twittering-mode: "))))
1109
1110(defun twittering-goto-next-status ()
1111  "Go to next status."
1112  (interactive)
1113  (let ((pos))
1114    (setq pos (twittering-get-next-username-face-pos (point)))
1115    (if pos
1116        (goto-char pos)
1117      (message "End of status."))))
1118
1119(defun twittering-get-next-username-face-pos (pos)
1120  (interactive)
1121  (let ((prop))
1122    (catch 'not-found
1123      (while (and pos (not (eq prop twittering-username-face)))
1124        (setq pos (next-single-property-change pos 'face))
1125        (when (eq pos nil) (throw 'not-found nil))
1126        (setq prop (get-text-property pos 'face)))
1127      pos)))
1128
1129(defun twittering-goto-previous-status ()
1130  "Go to previous status."
1131  (interactive)
1132  (let ((pos))
1133    (setq pos (twittering-get-previous-username-face-pos (point)))
1134    (if pos
1135        (goto-char pos)
1136      (message "Start of status."))))
1137
1138(defun twittering-get-previous-username-face-pos (pos)
1139  (interactive)
1140  (let ((prop))
1141    (catch 'not-found
1142      (while (and pos (not (eq prop twittering-username-face)))
1143        (setq pos (previous-single-property-change pos 'face))
1144        (when (eq pos nil) (throw 'not-found nil))
1145        (setq prop (get-text-property pos 'face)))
1146      pos)))
1147
1148(defun twittering-goto-next-status-of-user ()
1149  "Go to next status of user."
1150  (interactive)
1151  (let ((user-name (twittering-get-username-at-pos (point)))
1152        (pos (twittering-get-next-username-face-pos (point))))
1153    (while (and (not (eq pos nil))
1154                (not (equal (twittering-get-username-at-pos pos) user-name)))
1155      (setq pos (twittering-get-next-username-face-pos pos)))
1156    (if pos
1157        (goto-char pos)
1158      (if user-name
1159          (message "End of %s's status." user-name)
1160        (message "Invalid user-name.")))))
1161
1162(defun twittering-goto-previous-status-of-user ()
1163  "Go to previous status of user."
1164  (interactive)
1165  (let ((user-name (twittering-get-username-at-pos (point)))
1166        (pos (twittering-get-previous-username-face-pos (point))))
1167    (while (and (not (eq pos nil))
1168                (not (equal (twittering-get-username-at-pos pos) user-name)))
1169      (setq pos (twittering-get-previous-username-face-pos pos)))
1170    (if pos
1171        (goto-char pos)
1172      (if user-name
1173          (message "Start of %s's status." user-name)
1174        (message "Invalid user-name.")))))
1175
1176(defun twittering-get-username-at-pos (pos)
1177  (let ((start-pos pos)
1178        (end-pos))
1179    (catch 'not-found
1180      (while (eq (get-text-property start-pos 'face) twittering-username-face)
1181        (setq start-pos (1- start-pos))
1182        (when (or (eq start-pos nil) (eq start-pos 0)) (throw 'not-found nil)))
1183      (setq start-pos (1+ start-pos))
1184      (setq end-pos (next-single-property-change pos 'face))
1185      (buffer-substring start-pos end-pos))))
1186
1187(defun twittering-get-status-url (username id)
1188  "Generate status URL."
1189  (format "http://twitter.com/%s/statuses/%s" username id))
1190
1191;;;###autoload
1192(defun twit ()
1193  "Start twittering-mode."
1194  (interactive)
1195  (twittering-mode))
1196
1197(provide 'twittering-mode)
1198;;; twittering.el ends here
Note: See TracBrowser for help on using the browser.