Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
10754 lines (10065 sloc) 390 KB
;;; twittering-mode.el --- Major mode for Twitter
;; Copyright (C) 2007, 2009, 2010 Yuto Hayamizu.
;; 2008 Tsuyoshi CHO
;; Author: Y. Hayamizu <y.hayamizu@gmail.com>
;; Tsuyoshi CHO <Tsuyoshi.CHO+develop@Gmail.com>
;; Alberto Garcia <agarcia@igalia.com>
;; Created: Sep 4, 2007
;; Version: HEAD
;; Identity: $Id$
;; Keywords: twitter web
;; URL: http://twmode.sf.net/
;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.
;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; twittering-mode.el is a major mode for Twitter.
;; You can check friends timeline, and update your status on Emacs.
;;; Feature Request:
;; URL : http://twitter.com/d00dle/statuses/577876082
;; * Status Input from Popup buffer and C-cC-c to POST.
;; URL : http://code.nanigac.com/source/view/419
;; * update status for region
;;; Code:
(eval-when-compile (require 'cl))
(require 'xml)
(eval-and-compile
;; On byte-compilation, Emacs21 requires loading the libraries
;; distributed with twittering-mode.el for macros defined in them.
(when (> 22 emacs-major-version)
(setq load-path
(append (mapcar (lambda (dir)
(expand-file-name
dir
(if load-file-name
(or (file-name-directory load-file-name)
".")
".")))
'("url-emacs21" "emacs21"))
load-path))))
(when (> 22 emacs-major-version)
(and (require 'un-define nil t)
;; the explicitly require 'unicode to update a workaround with
;; navi2ch. see a comment of `twittering-ucs-to-char' for more
;; details.
(require 'unicode nil t))
(defadvice url-scheme-register-proxy (around twittering-fix-process-env (scheme) activate)
(let ((process-environment
(apply 'append
(let ((env-var (concat scheme "_proxy")))
(mapcar
(lambda (str)
(if (string-match
(concat "^\\("
(regexp-opt (list (upcase env-var)
(downcase env-var)))
"\\)=$")
str)
nil
(list str)))
process-environment)))))
ad-do-it)))
(require 'url)
(defconst twittering-mode-version "HEAD")
(defconst twittering-mode-identity "$Id$")
(defvar twittering-api-host "api.twitter.com")
(defvar twittering-api-search-host "search.twitter.com")
(defvar twittering-web-host "twitter.com")
(defvar twittering-oauth-request-token-url-without-scheme
"://api.twitter.com/oauth/request_token")
(defvar twittering-oauth-authorization-url-base-without-scheme
"://api.twitter.com/oauth/authorize?oauth_token=")
(defvar twittering-oauth-access-token-url-without-scheme
"://api.twitter.com/oauth/access_token")
(defun twittering-mode-version ()
"Display a message for twittering-mode version."
(interactive)
(let ((version-string
(format "twittering-mode-v%s" twittering-mode-version)))
(if (interactive-p)
(message "%s" version-string)
version-string)))
(defvar twittering-auth-method 'oauth
"*Authentication method for `twittering-mode'.
The symbol `basic' means Basic Authentication. The symbol `oauth' means
OAuth Authentication. The symbol `xauth' means xAuth Authentication.
OAuth Authentication requires `twittering-oauth-consumer-key' and
`twittering-oauth-consumer-secret'. Additionally, it requires an external
command `curl' or another command included in `tls-program', which may be
`openssl' or `gnutls-cli', for SSL.")
(defvar twittering-account-authorization nil
"State of account authorization for `twittering-username' and
`twittering-password'. The value is one of the following symbols:
nil -- The account have not been authorized yet.
queried -- The authorization has been queried, but not finished yet.
authorized -- The account has been authorized.")
(defvar twittering-oauth-use-ssl t
"*Whether to use SSL on authentication via OAuth. Twitter requires SSL
on authorization via OAuth.")
(defvar twittering-oauth-invoke-browser nil
"*Whether to invoke a browser on authorization of access key automatically.")
(defvar twittering-oauth-consumer-key nil)
(defvar twittering-oauth-consumer-secret nil)
(defvar twittering-oauth-access-token-alist nil)
(defconst twittering-max-number-of-tweets-on-retrieval 200
"The maximum number of `twittering-number-of-tweets-on-retrieval'.")
(defvar twittering-number-of-tweets-on-retrieval 20
"*The number of tweets which will be retrieved in one request.
The upper limit is `twittering-max-number-of-tweets-on-retrieval'.")
(defvar twittering-tinyurl-service 'tinyurl
"*The service to shorten URI.
This must be one of key symbols of `twittering-tinyurl-services-map'.
To use 'bit.ly or 'j.mp, you have to configure `twittering-bitly-login' and
`twittering-bitly-api-key'.")
(defvar twittering-tinyurl-services-map
'((bit.ly twittering-make-http-request-for-bitly
(lambda (service reply)
(if (string-match "\n\\'" reply)
(substring reply 0 (match-beginning 0))
reply)))
(goo.gl
(lambda (service longurl)
(twittering-make-http-request-from-uri
"POST" '(("Content-Type" . "application/json"))
"https://www.googleapis.com/urlshortener/v1/url"
(concat "{\"longUrl\": \"" longurl "\"}")))
(lambda (service reply)
(when (string-match "\"id\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\""
reply)
(match-string 1 reply))))
(is.gd . "http://is.gd/create.php?format=simple&url=")
(j.mp twittering-make-http-request-for-bitly
(lambda (service reply)
(if (string-match "\n\\'" reply)
(substring reply 0 (match-beginning 0))
reply)))
(migre.me . "http://migre.me/api.txt?url=")
(tinyurl . "http://tinyurl.com/api-create.php?url=")
(toly
(lambda (service longurl)
(twittering-make-http-request-from-uri
"POST" nil
"http://to.ly/api.php"
(concat "longurl=" (twittering-percent-encode longurl))))))
"Alist of URL shortening services.
The key is a symbol specifying the service.
The value is a string or a list consisting of two elements at most.
If the value is a string, `(concat THE-FIRST-ELEMENT longurl)' is used as the
URL invoking the service.
If the value is a list, it is interpreted as follows.
The first element specifies how to make a HTTP request for shortening a URL.
If the first element is a string, `(concat THE-FIRST-ELEMENT longurl)' is
used as the URL invoking the service.
If the first element is a function, it is called as `(funcall THE-FIRST-ELEMENT
service-symbol longurl)' to obtain a HTTP request alist for invoking the
service, which must be generated by `twittering-make-http-request'.
The second element specifies how to post-process a HTTP reply by the HTTP
request.
If the second element is nil, the reply is directly used as a shortened URL.
If the second element is a function, it is called as `(funcall
THE-SECOND-ELEMENT service-symbol HTTP-reply-string)' and its result is used
as a shortened URL.")
(defvar twittering-bitly-login nil
"*The login name for URL shortening service bit.ly and j.mp.")
(defvar twittering-bitly-api-key nil
"*The API key for URL shortening service bit.ly and j.mp.")
(defvar twittering-mode-map (make-sparse-keymap))
(defvar twittering-mode-menu-on-uri-map (make-sparse-keymap "Twittering Mode"))
(defvar twittering-mode-on-uri-map (make-sparse-keymap))
(defvar twittering-tweet-history nil)
(defvar twittering-user-history nil)
(defvar twittering-timeline-history nil)
(defvar twittering-hashtag-history nil)
(defvar twittering-search-history nil)
(defvar twittering-current-hashtag nil
"A hash tag string currently set. You can set it by calling
`twittering-set-current-hashtag'.")
(defvar twittering-timer nil
"Timer object for timeline refreshing will be stored here.
DO NOT SET VALUE MANUALLY.")
(defvar twittering-timer-interval 90
"The interval of auto reloading. You should use 60 or more
seconds for this variable because the number of API call is
limited by the hour.")
(defvar twittering-timer-for-redisplaying nil
"Timer object for timeline redisplay statuses will be stored here.
DO NOT SET VALUE MANUALLY.")
(defvar twittering-timer-interval-for-redisplaying 5.0
"The interval of auto redisplaying statuses.
Each time Emacs remains idle for the interval, twittering-mode updates parts
requiring to be redrawn.")
(defvar twittering-username nil
"*An username of your Twitter account.")
(defvar twittering-password nil
"*A password of your Twitter account. Leave it blank is the
recommended way because writing a password in .emacs file is so
dangerous.")
(defvar twittering-initial-timeline-spec-string ":home"
"*An initial timeline spec string or a list of timeline spec strings.
This specifies one or more initial timeline spec strings, which are
automatically visited when invoking `twittering-mode' or `twit'.
If it is a string, it specifies a timeline spec string.
If it is a list of strings, it specifies multiple timeline spec strings.")
(defvar twittering-timeline-spec nil
"The timeline spec for the current buffer.")
(defvar twittering-timeline-spec-string ""
"The timeline spec string for the current buffer.")
(defvar twittering-timeline-spec-alias nil
"*Alist for aliases of timeline spec.
Each element is (NAME . SPEC-STRING), where NAME is a string and
SPEC-STRING is a string or a function that returns a timeline spec string.
The alias can be referred as \"$NAME\" or \"$NAME(ARG)\" in timeline spec
string. If SPEC-STRING is a string, ARG is simply ignored.
If SPEC-STRING is a function, it is called with a string argument.
For the style \"$NAME\", the function is called with nil.
For the style \"$NAME(ARG)\", the function is called with a string ARG.
For example, if you specify
`((\"FRIENDS\" . \"my-account/friends-list\")
(\"related-to\" .
,(lambda (username)
(if username
(format \":search/to:%s OR from:%s OR @%s/\"
username username username)
\":home\")))),
then you can use \"$FRIENDS\" and \"$related-to(USER)\" as
\"my-account/friends-list\" and \":search/to:USER OR from:USER OR @USER/\",
respectively.")
(defvar twittering-current-timeline-spec-string nil
"The current timeline spec string. This variable should not be referred
directly. Use `twittering-current-timeline-spec-string' or
`twittering-current-timeline-spec'.")
(defvar twittering-list-index-retrieved nil)
(defvar twittering-process-info-alist nil
"Alist of active process and timeline spec retrieved by the process.")
(defvar twittering-server-info-alist nil
"Alist of server information.")
(defvar twittering-mode-init-hook nil
"*Hook run after initializing global variables for `twittering-mode'.")
(defvar twittering-mode-hook nil
"*Hook run every time a buffer is initialized as a twittering-mode buffer.")
(defvar twittering-new-tweets-count 0
"Number of new tweets when `twittering-new-tweets-hook' is run.")
(defvar twittering-new-tweets-spec nil
"Timeline spec, which new tweets belong to, when
`twittering-new-tweets-hook' is run.")
(defvar twittering-new-tweets-statuses nil
"New tweet status messages, when
`twittering-new-tweets-hook' is run.")
(defvar twittering-new-tweets-hook nil
"*Hook run when new tweets are received.
You can read `twittering-new-tweets-count' or `twittering-new-tweets-spec'
to get the number of new tweets received when this hook is run.")
(defvar twittering-rendered-new-tweets-spec nil
"A timeline spec of newly rendered tweets.
This variable is bound when invoking hooks registered with
`twittering-new-tweets-rendered-hook'.")
(defvar twittering-rendered-new-tweets-spec-string nil
"A timeline spec string of newly rendered tweets.
This variable is bound when invoking hooks registered with
`twittering-new-tweets-rendered-hook'.")
(defvar twittering-rendered-new-tweets nil
"A list of newly rendered tweets.
Hooks registered with `twittering-new-tweets-rendered-hook' can use this
variable as a list of rendered tweets. Each tweet is represented as an alist.
You can refer to a property of a tweet alist as
(cdr (assq PROPERTY-SYMBOL TWEET-ALIST)).
Valid symbols are following; id, text, user-name, user-screen-name, user-id,
source, source-uri.
In the list, tweets are placed in order of time. The car of the list is the
latest one, and the last is the oldest one.")
(defvar twittering-new-tweets-rendered-hook nil
"*Hook run when new tweets are rendered.
When the registered functions are called, the current buffer is the buffer
that the new tweets are just rendered on.
The functions can refer to the timeline spec and timeline spec string as
`twittering-rendered-new-tweets-spec' and
`twittering-rendered-new-tweets-spec-string', repectively.
Hooks can also use the local variable `twittering-rendered-new-tweets' as a
list of rendered tweets.
For the detail of the representation of tweets, see the variable
`twittering-rendered-new-tweets'.")
(defvar twittering-active-mode nil
"Non-nil if new statuses should be retrieved periodically.
Do not modify this variable directly. Use `twittering-activate-buffer',
`twittering-deactivate-buffer', `twittering-toggle-activate-buffer' or
`twittering-set-active-flag-for-buffer'.")
(defvar twittering-jojo-mode nil)
(defvar twittering-reverse-mode nil
"*Non-nil means tweets are aligned in reverse order of `http://twitter.com/'.")
(defvar twittering-display-remaining nil
"*If non-nil, display remaining of rate limit on the mode-line.")
(defvar twittering-display-connection-method t
"*If non-nil, display the current connection method on the mode-line.")
(defvar twittering-status-format "%i %s, %@:\n%FILL[ ]{%T // from %f%L%r%R}\n "
"Format string for rendering statuses.
Ex. \"%i %s, %@:\\n%FILL{ %T // from %f%L%r%R}\n \"
Items:
%s - screen_name
%S - name
%i - profile_image
%d - description
%l - location
%L - \" [location]\"
%r - \" sent to user\" (use on direct_messages{,_sent})
%r - \" in reply to user\" (use on other standard timeline)
%R - \" (retweeted by user)\"
%RT{...} - strings rendered only when the tweet is a retweet.
The braced strings are rendered with the information of the
retweet itself instead of that of the retweeted original tweet.
For example, %s for a retweet means who posted the original
tweet, but %RT{%s} means who retweeted it.
%u - url
%j - user.id
%p - protected?
%c - created_at (raw UTC string)
%C{time-format-str} - created_at (formatted with time-format-str)
%@ - X seconds ago
%T - raw text
%t - text filled as one paragraph
%' - truncated
%FACE[face-name]{...} - strings decorated with the specified face.
%FILL[prefix]{...} - strings filled as a paragraph. The prefix is optional.
You can use any other specifiers in braces.
%FOLD[prefix]{...} - strings folded within the frame width.
The prefix is optional. This keeps newlines and does not
squeeze a series of white spaces.
You can use any other specifiers in braces.
%f - source
%# - id
")
(defvar twittering-retweet-format '(nil _ " RT: %t (via @%s)")
"*A format string or a skeleton for retweet.
If the value is a string, it means a format string for generating an initial
string of a retweet. The format string is converted with the below replacement
table. And then, the cursor is placed on the next of the initial string.
It is equivalent to the skeleton '(nil STRING _).
Note that this string is inserted before the edit skeleton specified by
`twittering-edit-skeleton' is performed.
If the value is a list, it is treated as a skeleton used with
`skeleton-insert'. The strings included in the list are converted with the
following replacement table. And then, the list with converted strings is
inserted by `skeleton-insert'.
Note that this skeleton is performed before the edit skeleton specified by
`twittering-edit-skeleton' is performed.
Replacement table:
%s - The screen-name of the cited tweet.
%t - The text of the cited tweet.
%u - The URL of the cited tweet.
%# - The ID of the cited tweet.
%% - % itself.")
(defvar twittering-fill-column nil
"*The fill-column used for \"%FILL{...}\" in `twittering-status-format'.
If nil, the fill-column is automatically calculated.")
(defvar twittering-show-replied-tweets t
"*The number of replied tweets which will be showed in one tweet.
If the value is not a number and is non-nil, show all replied tweets
which is already fetched.
If the value is nil, doesn't show replied tweets.")
(defvar twittering-default-show-replied-tweets nil
"*The number of default replied tweets which will be showed in one tweet.
This value will be used only when showing new tweets.
See `twittering-show-replied-tweets' for more details.")
(defvar twittering-disable-overlay-on-too-long-string nil
"*If non-nil, disable overlay on too long string on edit buffer.
If nil, `twittering-edit-mode' puts an overlay `twittering-warning-overlay' on
characters following the 140th character.
On some environments, some input methods seem to interfere the update of the
overlay. In such case, you may avoid the problems by setting this variable to
non-nil.")
(defvar twittering-use-show-minibuffer-length t
"*Show current length of minibuffer if this variable is non-nil.
We suggest that you should set to nil to disable the showing function
when it conflict with your input method (such as AquaSKK, etc.)")
(defvar twittering-notify-successful-http-get t)
(defvar twittering-use-ssl t
"Use SSL connection if this variable is non-nil.
SSL connections use 'curl' command as a backend.")
(defvar twittering-allow-insecure-server-cert nil
"*If non-nil, twittering-mode allows insecure server certificates.")
(defvar twittering-curl-program nil
"Cache a result of `twittering-find-curl-program'.
DO NOT SET VALUE MANUALLY.")
(defvar twittering-curl-program-https-capability nil
"Cache a result of `twittering-start-http-session-curl-https-p'.
DO NOT SET VALUE MANUALLY.")
(defvar twittering-wget-program nil
"Cache a result of `twittering-find-wget-program'.
DO NOT SET VALUE MANUALLY.")
(defvar twittering-tls-program nil
"*List of strings containing commands to start TLS stream to a host.
Each entry in the list is tried until a connection is successful.
%h is replaced with server hostname, %p with port to connect to.
Also see `tls-program'.
If nil, this is initialized with a list of valied entries extracted from
`tls-program'.")
(defvar twittering-connection-type-order
'(curl wget urllib-http native urllib-https))
"*A list of connection methods in the preferred order."
(defvar twittering-connection-type-table
'((native (check . t)
(https . twittering-start-http-session-native-tls-p)
(send-http-request . twittering-send-http-request-native)
(pre-process-buffer . twittering-pre-process-buffer-native))
(curl (check . twittering-start-http-session-curl-p)
(https . twittering-start-http-session-curl-https-p)
(send-http-request . twittering-send-http-request-curl)
(pre-process-buffer . twittering-pre-process-buffer-curl))
(wget (check . twittering-start-http-session-wget-p)
(https . t)
(send-http-request . twittering-send-http-request-wget)
(pre-process-buffer . twittering-pre-process-buffer-wget))
(urllib-http
(display-name . "urllib")
(check . twittering-start-http-session-urllib-p)
(https . nil)
(send-http-request . twittering-send-http-request-urllib)
(pre-process-buffer . twittering-pre-process-buffer-urllib))
(urllib-https
(display-name . "urllib")
(check . twittering-start-http-session-urllib-p)
(https . twittering-start-http-session-urllib-https-p)
(send-http-request . twittering-send-http-request-urllib)
(pre-process-buffer . twittering-pre-process-buffer-urllib)))
"A list of alist of connection methods.")
(defvar twittering-format-status-function-source ""
"The status format string that has generated the current
`twittering-format-status-function'.")
(defvar twittering-format-status-function nil
"The formating function generated from `twittering-format-status-function-source'.")
(defvar twittering-format-status-function-without-compile nil
"The formating function generated from `twittering-format-status-function-source',
which is a lambda expression without being compiled.")
(defvar twittering-timeline-data-table (make-hash-table :test 'equal))
(defvar twittering-username-face 'twittering-username-face)
(defvar twittering-uri-face 'twittering-uri-face)
(defvar twittering-use-native-retweet nil
"Post retweets using native retweets if this variable is non-nil.")
(defvar twittering-update-status-function
'twittering-update-status-from-pop-up-buffer
"The function used to posting a tweet. It takes 5 arguments,
INIT-STR, REPLY-TO-ID, USERNAME, TWEET-TYPE, CURRENT-SPEC.
The first argument INIT-STR is nil or an initial text to be edited.
REPLY-TO-ID and USERNAME are an ID and a user-screen-name of a tweet to
which you are going to reply. If the tweet is not a reply, they are nil.
TWEET-TYPE is a symbol specifying a type of a tweet being edited. It must
be one of 'direct-message, 'normal, 'organic-retweet and 'reply.
CURRENT-SPEC means on which timeline the function is called.
Twittering-mode provides two functions for updating status:
* `twittering-update-status-from-minibuffer': edit tweets in minibuffer
* `twittering-update-status-from-pop-up-buffer': edit tweets in pop-up buffer")
(defvar twittering-request-confirmation-on-posting nil
"*If *non-nil*, confirmation will be requested on posting a tweet edited in
pop-up buffer.")
(defvar twittering-use-master-password nil
"*Wheter to store private information encrypted with a master password.")
(defvar twittering-private-info-file
(expand-file-name "~/.twittering-mode.gpg")
"*File for storing encrypted private information when
`twittering-use-master-password' is non-nil.")
(defvar twittering-private-info-file-loaded nil
"Whether the private info file has been loaded or not.")
(defvar twittering-variables-stored-with-encryption
'(twittering-oauth-access-token-alist))
(defvar twittering-api-prefix "1/")
(defvar twittering-search-api-method "search")
(defvar twittering-web-path-prefix "")
(defvar twittering-service-method 'twitter
"*Service method for `twittering-mode'.
The symbol `twitter' means Twitter Service. The symbol `statusnet' means
StatusNet Service.")
(defvar twittering-service-method-table
'((twitter (status-url . twittering-get-status-url-twitter)
(search-url . twittering-get-search-url-twitter))
(statusnet (status-url . twittering-get-status-url-statusnet)
(search-url . twittering-get-search-url-statusnet)))
"A list of alist of service methods.")
(defvar twittering-timeline-header-face 'twittering-timeline-header-face
"*Face for the header on `twittering-mode'.
The face is used for rendering `twittering-timeline-header'.")
(defvar twittering-timeline-footer-face 'twittering-timeline-footer-face
"*Face for the footer on `twittering-mode'.
The face is used for rendering `twittering-timeline-footer'.")
(defvar twittering-timeline-header "-- Press Enter here to update --\n"
"*Timeline header string on `twittering-mode'.
The string is rendered on the beginning of a `twittering-mode' buffer.
Its face is specified by `twittering-timeline-header-face'.")
(defvar twittering-timeline-footer "-- Press Enter here to update --"
"*Timeline footer string on `twittering-mode'.
The string is rendered on the end of a `twittering-mode' buffer.
Its face is specified by `twittering-timeline-footer-face'.")
(defvar twittering-pop-to-buffer-function
'twittering-pop-to-buffer-in-bottom-largest-window
"*Function being invoked by `twittering-pop-to-buffer'.
It will receive an argument, the buffer being selected.
For example, the following functions can be used; `pop-to-buffer',
`twittering-pop-to-buffer-simple',
`twittering-pop-to-buffer-in-current-window',
`twittering-pop-to-buffer-in-largest-window', and
`twittering-pop-to-buffer-in-bottom-largest-window'.")
;;;;
;;;; Macro and small utility function
;;;;
(defun assocref (item alist)
(cdr (assoc item alist)))
(defmacro list-push (value listvar)
`(setq ,listvar (cons ,value ,listvar)))
(defmacro case-string (str &rest clauses)
`(cond
,@(mapcar
(lambda (clause)
(let ((keylist (car clause))
(body (cdr clause)))
`(,(if (listp keylist)
`(or ,@(mapcar (lambda (key) `(string-equal ,str ,key))
keylist))
't)
,@body)))
clauses)))
(defmacro twittering-wait-while (timeout interval condition &optional form &rest timeout-forms)
"Wait while CONDITION returns non-nil until TIMEOUT seconds passes.
The form CONDITION is repeatedly evaluated for every INTERVAL seconds
until CONDITION returns nil or TIMEOUT seconds passes unless TIMEOUT is nil.
If TIMEOUT is nil, there is no time limit.
If CONDITION returns nil, evaluate the form FORM and return its value.
If TIMEOUT seconds passes, evaluate the forms TIMEOUT-FORMS and return
the value of the last form in TIMEOUT-FORMS."
`(lexical-let (,@(when timeout `((timeout ,timeout)))
(interval ,interval)
(current 0.0))
(while (and ,@(when timeout '((< current timeout)))
,condition)
(sit-for interval)
(setq current (+ current interval)))
,(when (or form timeout-forms)
(if (null timeout)
form
`(if (< current timeout)
,form
,@timeout-forms)))))
(defun twittering-extract-matched-substring-all (regexp str)
(let ((pos 0)
(result nil))
(while (string-match regexp str pos)
(setq result (cons (match-string 1 str) result))
(setq pos (match-end 0)))
(reverse result)))
(defun twittering-process-alive-p (proc)
"Return non-nil if PROC is alive."
(not (memq (process-status proc) '(nil closed exit failed signal))))
(defun twittering-start-process-with-sentinel (name buffer program args sentinel)
"Start a program in a subprocess with a sentinel.
This function is the same as `start-process' except that SENTINEL must
be invoked when the process is successfully started."
(let ((proc (apply 'start-process name buffer program args)))
(when (and proc (functionp sentinel))
(if (twittering-process-alive-p proc)
(set-process-sentinel proc sentinel)
;; Ensure that the sentinel is invoked if a subprocess is
;; successfully started.
(funcall sentinel proc "finished")))
proc))
;;;;
;;;; Utility for portability
;;;;
(defun twittering-remove-duplicates (list)
"Return a copy of LIST with all duplicate elements removed.
This is non-destructive version of `delete-dups' which is not
defined in Emacs21."
(if (fboundp 'delete-dups)
(delete-dups (copy-sequence list))
(let ((rest list)
(result nil))
(while rest
(unless (member (car rest) result)
(setq result (cons (car rest) result)))
(setq rest (cdr rest)))
(nreverse result))))
(defun twittering-completing-read (prompt collection &optional predicate require-match initial-input hist def inherit-input-method)
"Read a string in the minibuffer, with completion.
This is a modified version of `completing-read' and accepts candidates
as a list of a string on Emacs21."
;; completing-read() of Emacs21 does not accepts candidates as
;; a list. Candidates must be given as an alist.
(let* ((collection (twittering-remove-duplicates collection))
(collection
(if (and (> 22 emacs-major-version)
(listp collection)
(stringp (car collection)))
(mapcar (lambda (x) (cons x nil)) collection)
collection)))
(completing-read prompt collection predicate require-match
initial-input hist def inherit-input-method)))
(defun twittering-add-to-history (history-var elt &optional maxelt keep-all)
(if (functionp 'add-to-history)
(add-to-history history-var elt maxelt keep-all)
(let* ((added (cons elt
(if (and (not keep-all)
(boundp 'history-delete-duplicates)
history-delete-duplicates)
(delete elt (symbol-value history-var))
(symbol-value history-var))))
(maxelt (or maxelt history-length))
(len (length added)))
(set history-var
(if (<= len maxelt)
added
(butlast added (- len maxelt)))))))
;;;;
;;;; Debug mode
;;;;
(defvar twittering-debug-mode nil)
(defvar twittering-debug-buffer "*debug*")
(defun twittering-get-or-generate-buffer (buffer)
(if (bufferp buffer)
(if (buffer-live-p buffer)
buffer
(generate-new-buffer (buffer-name buffer)))
(if (stringp buffer)
(or (get-buffer buffer)
(generate-new-buffer buffer)))))
(defun twittering-debug-buffer ()
(twittering-get-or-generate-buffer twittering-debug-buffer))
(defmacro debug-print (obj)
(let ((obsym (gensym)))
`(let ((,obsym ,obj))
(if twittering-debug-mode
(with-current-buffer (twittering-debug-buffer)
(insert "[debug] " (prin1-to-string ,obsym))
(newline)
,obsym)
,obsym))))
(defun debug-printf (fmt &rest args)
(when twittering-debug-mode
(with-current-buffer (twittering-debug-buffer)
(insert "[debug] " (apply 'format fmt args))
(newline))))
(defun twittering-debug-mode ()
(interactive)
(setq twittering-debug-mode
(not twittering-debug-mode))
(message (if twittering-debug-mode "debug mode:on" "debug mode:off")))
;;;;
;;;; Proxy setting / functions
;;;;
(defvar twittering-proxy-use nil)
(defvar twittering-proxy-server nil
"*Proxy server for `twittering-mode'.
If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.
To use individual proxies for HTTP and HTTPS, both `twittering-proxy-server'
and `twittering-proxy-port' must be nil.")
(defvar twittering-proxy-port nil
"*Port number for `twittering-mode'.
If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.
To use individual proxies for HTTP and HTTPS, both `twittering-proxy-server'
and `twittering-proxy-port' must be nil.")
(defvar twittering-proxy-keep-alive nil)
(defvar twittering-proxy-user nil
"*Username for `twittering-proxy-server'.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-proxy-password nil
"*Password for `twittering-proxy-server'.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-http-proxy-server nil
"*HTTP proxy server for `twittering-mode'.
If nil, it is initialized on entering `twittering-mode'.
The port number is specified by `twittering-http-proxy-port'.
For HTTPS connection, the proxy specified by `twittering-https-proxy-server'
and `twittering-https-proxy-port' is used.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-http-proxy-port nil
"*Port number of a HTTP proxy server for `twittering-mode'.
If nil, it is initialized on entering `twittering-mode'.
The server is specified by `twittering-http-proxy-server'.
For HTTPS connection, the proxy specified by `twittering-https-proxy-server'
and `twittering-https-proxy-port' is used.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-http-proxy-keep-alive nil
"*If non-nil, the Keep-alive is enabled. This is experimental.")
(defvar twittering-http-proxy-user nil
"*Username for `twittering-http-proxy-server'.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-http-proxy-password nil
"*Password for `twittering-http-proxy-server'.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-https-proxy-server nil
"*HTTPS proxy server for `twittering-mode'.
If nil, it is initialized on entering `twittering-mode'.
The port number is specified by `twittering-https-proxy-port'.
For HTTP connection, the proxy specified by `twittering-http-proxy-server'
and `twittering-http-proxy-port' is used.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-https-proxy-port nil
"*Port number of a HTTPS proxy server for `twittering-mode'.
If nil, it is initialized on entering `twittering-mode'.
The server is specified by `twittering-https-proxy-server'.
For HTTP connection, the proxy specified by `twittering-http-proxy-server'
and `twittering-http-proxy-port' is used.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-https-proxy-keep-alive nil
"*If non-nil, the Keep-alive is enabled. This is experimental.")
(defvar twittering-https-proxy-user nil
"*Username for `twittering-https-proxy-server'.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defvar twittering-https-proxy-password nil
"*Password for `twittering-https-proxy-server'.
NOTE: If both `twittering-proxy-server' and `twittering-proxy-port' are
non-nil, the variables `twittering-proxy-*' have priority over other
variables `twittering-http-proxy-*' or `twittering-https-proxy-*'
regardless of HTTP or HTTPS.")
(defun twittering-normalize-proxy-vars ()
"Normalize the type of `twittering-http-proxy-port' and
`twittering-https-proxy-port'."
(mapc (lambda (sym)
(let ((value (symbol-value sym)))
(cond
((null value)
nil)
((integerp value)
nil)
((stringp value)
(set sym (string-to-number value)))
(t
(set sym nil)))))
'(twittering-proxy-port
twittering-http-proxy-port
twittering-https-proxy-port)))
(defun twittering-proxy-info (scheme &optional item)
"Return an alist for proxy configuration registered for SCHEME.
SCHEME must be a string \"http\", \"https\" or a symbol 'http or 'https.
The server name is a string and the port number is an integer."
(twittering-normalize-proxy-vars)
(let ((scheme (if (symbolp scheme)
(symbol-name scheme)
scheme))
(info-list
`((("http" "https")
. ((server . ,twittering-proxy-server)
(port . ,twittering-proxy-port)
(keep-alive . ,twittering-proxy-keep-alive)
(user . ,twittering-proxy-user)
(password . ,twittering-proxy-password)))
(("http")
. ((server . ,twittering-http-proxy-server)
(port . ,twittering-http-proxy-port)
(keep-alive . ,twittering-http-proxy-keep-alive)
(user . ,twittering-http-proxy-user)
(password . ,twittering-http-proxy-password)))
(("https")
. ((server . ,twittering-https-proxy-server)
(port . ,twittering-https-proxy-port)
(keep-alive . ,twittering-https-proxy-keep-alive)
(user . ,twittering-https-proxy-user)
(password . ,twittering-https-proxy-password))))))
(let ((info
(car (remove nil
(mapcar
(lambda (entry)
(when (member scheme (car entry))
(let ((info (cdr entry)))
(when (and (cdr (assq 'server info))
(cdr (assq 'port info)))
info))))
info-list)))))
(if item
(cdr (assq item info))
info))))
(defun twittering-url-proxy-services ()
"Return the current proxy configuration for `twittering-mode' in the format
of `url-proxy-services'."
(remove nil (mapcar
(lambda (scheme)
(let ((server (twittering-proxy-info scheme 'server))
(port (twittering-proxy-info scheme 'port)))
(when (and server port)
`(,scheme . ,(format "%s:%s" server port)))))
'("http" "https"))))
(defun twittering-find-proxy (scheme)
"Find proxy server and its port from the environmental variables and return
a cons pair of them.
SCHEME must be \"http\" or \"https\"."
(cond
((require 'url-methods nil t)
(url-scheme-register-proxy scheme)
(let* ((proxy-service (assoc scheme url-proxy-services))
(proxy (if proxy-service (cdr proxy-service) nil)))
(if (and proxy
(string-match "^\\([^:]+\\):\\([0-9]+\\)$" proxy))
(let ((host (match-string 1 proxy))
(port (string-to-number (match-string 2 proxy))))
(cons host port))
nil)))
(t
(let* ((env-var (concat scheme "_proxy"))
(env-proxy (or (getenv (upcase env-var))
(getenv (downcase env-var))))
(default-port (if (string= "https" scheme) "443" "80")))
(if (and env-proxy
(string-match
"^\\(https?://\\)?\\([^:/]+\\)\\(:\\([0-9]+\\)\\)?/?$"
env-proxy))
(let* ((host (match-string 2 env-proxy))
(port-str (or (match-string 4 env-proxy) default-port))
(port (string-to-number port-str)))
(cons host port))
nil)))))
(defun twittering-setup-proxy ()
(when (require 'url-methods nil t)
;; If `url-scheme-registry' is not initialized,
;; `url-proxy-services' will be reset by calling
;; `url-insert-file-contents' or `url-retrieve-synchronously', etc.
;; To avoid it, initialize `url-scheme-registry' by calling
;; `url-scheme-get-property' before calling such functions.
(url-scheme-get-property "http" 'name)
(url-scheme-get-property "https" 'name))
(unless (and twittering-http-proxy-server
twittering-http-proxy-port)
(let ((info (twittering-find-proxy "http")))
(setq twittering-http-proxy-server (car-safe info))
(setq twittering-http-proxy-port (cdr-safe info))))
(unless (and twittering-https-proxy-server
twittering-https-proxy-port)
(let ((info (twittering-find-proxy "https")))
(setq twittering-https-proxy-server (car-safe info))
(setq twittering-https-proxy-port (cdr-safe info))))
(if (and twittering-proxy-use
(null (twittering-proxy-info "http"))
(null (twittering-proxy-info "https")))
(progn
(message "Disabling proxy due to lack of configuration.")
(setq twittering-proxy-use nil))
t))
(defun twittering-toggle-proxy ()
(interactive)
(setq twittering-proxy-use
(not twittering-proxy-use))
(if (twittering-setup-proxy)
(message (if twittering-proxy-use "Use Proxy:on" "Use Proxy:off")))
(twittering-update-mode-line))
;;;;
;;;; Functions for URL library
;;;;
(defvar twittering-url-show-status nil
"*Whether to show a running total of bytes transferred.")
;;;;
;;;; CA certificate
;;;;
(defvar twittering-cert-file nil
"The full-path of the file including the certificates authorizing
servers on SSL.")
(defconst twittering-ca-cert-list
'(;; Equifax Secure Certificate Authority
;; subject= /C=US/O=Equifax/OU=Equifax Secure Certificate Authority
;; SHA1 Fingerprint=D2:32:09:AD:23:D3:14:23:21:74:E4:0D:7F:9D:62:13:97:86:63:3A
;; Retrieved from: https://www.geotrust.com/resources/root-certificates/index.html
;; URL: https://www.geotrust.com/resources/root_certificates/certificates/Equifax_Secure_Certificate_Authority.cer
;; for www.googleapis.com
"-----BEGIN CERTIFICATE-----
MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
-----END CERTIFICATE-----
"
;; VeriSign Class 3 Public Primary CA - G2
;; subject= /C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network
;; SHA1 Fingerprint=85:37:1C:A6:E5:50:14:3D:CE:28:03:47:1B:DE:3A:09:E8:F8:77:0F
;; Retrieved from: https://www.verisign.com/support/roots.html
;; URL: https://www.verisign.com/repository/roots/root-certificates/PCA-3G2.pem
;; for api.twitter.com
"-----BEGIN CERTIFICATE-----
MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ
BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh
c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy
MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp
emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X
DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw
FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg
UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo
YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5
MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4
pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0
13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID
AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk
U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i
F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY
oJ2daZH9
-----END CERTIFICATE-----
"
;; GeoTrust Global CA
;; subject= /C=US/O=GeoTrust Inc./CN=GeoTrust Global CA
;; SHA1 Fingerprint=DE:28:F4:A4:FF:E5:B9:2F:A3:C5:03:D1:A3:49:A7:F9:96:2A:82:12
;; Retrieved from: https://www.geotrust.com/resources/root-certificates/index.html
;; URL: https://www.geotrust.com/resources/root_certificates/certificates/GeoTrust_Global_CA.cer
;; for search.twitter.com
"-----BEGIN CERTIFICATE-----
MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT
MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i
YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG
EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg
R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9
9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq
fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv
iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU
1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+
bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW
MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA
ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l
uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn
Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS
tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF
PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un
hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV
5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw==
-----END CERTIFICATE-----
"))
(defun twittering-delete-ca-cert ()
(when (and twittering-cert-file
(file-exists-p twittering-cert-file))
(delete-file twittering-cert-file))
(setq twittering-cert-file nil))
(defun twittering-ensure-ca-cert ()
"Return a full-path of the file including CA certificates.
If it does not exist, create it. The directory includes root certificates
in \"hash format\". In detail, see verify(1SSL)."
(unless twittering-cert-file
(let ((coding-system-for-write 'iso-safe)
(file (make-temp-file "twmode-cacert")))
(with-temp-file file
(apply 'insert twittering-ca-cert-list))
(setq twittering-cert-file file)
(add-hook 'kill-emacs-hook 'twittering-delete-ca-cert)))
twittering-cert-file)
;;;;
;;;; User agent
;;;;
(defvar twittering-user-agent-function 'twittering-user-agent-default-function)
(defun twittering-user-agent-default-function ()
"Twittering mode default User-Agent function."
(format "Emacs/%d.%d Twittering-mode/%s"
emacs-major-version emacs-minor-version
twittering-mode-version))
(defun twittering-user-agent ()
"Return User-Agent header string."
(funcall twittering-user-agent-function))
;;;;
;;;; Basic HTTP functions (general)
;;;;
(defun twittering-percent-encode (str &optional coding-system)
"Encode STR according to Percent-Encoding defined in RFC 3986."
(twittering-oauth-url-encode str coding-system))
(defun twittering-lookup-connection-type (use-ssl &optional order table)
"Return available entry extracted fron connection type table.
TABLE is connection type table, which is an alist of type symbol and its
item alist, such as
'((native (check . t)
(https . twittering-start-http-session-native-tls-p)
(start . twittering-start-http-session-native))
(curl (check . twittering-start-http-session-curl-p)
(https . twittering-start-http-session-curl-https-p)
(start . twittering-start-http-session-curl))) .
ORDER means the priority order of type symbols.
If USE-SSL is nil, the item `https' is ignored.
When the type `curl' has priority and is available for the above table,
the function returns
'((check . twittering-start-http-session-curl-p)
(https . twittering-start-http-session-curl-https-p)
(start . twittering-start-http-session-curl)) ."
(let ((rest (or order twittering-connection-type-order))
(table (or table twittering-connection-type-table))
(result nil))
(while (and rest (null result))
(let* ((candidate (car rest))
(entry (cons `(symbol . ,candidate)
(cdr (assq candidate table))))
(entry (if (assq 'display-name entry)
entry
(cons `(display-name . ,(symbol-name candidate))
entry)))
(validate (lambda (item)
(let ((v (cdr (assq item entry))))
(or (null v) (eq t v) (functionp v)))))
(confirm (lambda (item)
(let ((v (cdr (assq item entry))))
(cond
((null v) nil)
((eq t v) t)
((functionp v) (funcall v)))))))
(if (and (funcall validate 'check)
(or (not use-ssl) (funcall validate 'https)))
(cond
((and (funcall confirm 'check)
(or (not use-ssl) (funcall confirm 'https)))
(setq rest nil)
(setq result entry))
(t
(setq rest (cdr rest))))
(message "The configuration for conncetion type `%s' is invalid."
candidate)
(setq rest nil))))
result))
(defun twittering-get-connection-method-name (use-ssl)
"Return a name of the preferred connection method.
If USE-SSL is non-nil, return a connection method for HTTPS.
If USE-SSL is nil, return a connection method for HTTP."
(cdr (assq 'display-name (twittering-lookup-connection-type use-ssl))))
(defun twittering-lookup-http-start-function (&optional order table)
"Decide a connection method from currently available methods."
(let ((entry
(twittering-lookup-connection-type twittering-use-ssl order table)))
(cdr (assq 'send-http-request entry))))
(defun twittering-ensure-connection-method (&optional order table)
"Ensure a connection method with a compromise.
Return nil if no connection methods are available with a compromise."
(let* ((use-ssl (or twittering-use-ssl twittering-oauth-use-ssl))
(entry (twittering-lookup-connection-type use-ssl order table)))
(cond
(entry
t)
((and (null entry) use-ssl
(yes-or-no-p "HTTPS(SSL) is unavailable. Use HTTP instead? "))
;; Fall back on connection without SSL.
(setq twittering-use-ssl nil)
(setq twittering-oauth-use-ssl nil)
(twittering-update-mode-line)
(twittering-ensure-connection-method order table))
(t
(message "No connection methods are available.")
nil))))
(defun twittering-make-http-request (method header-list host port path query-parameters post-body use-ssl)
"Returns an alist specifying a HTTP request.
METHOD specifies HTTP method. It must be \"GET\" or \"POST\".
HEADER-LIST is a list of (field-name . field-value) specifying HTTP header
fields. The fields \"Host\", \"User-Agent\" and \"Content-Length\" are
automatically filled if necessary.
HOST specifies the host.
PORT specifies the port. This must be an integer.
PATH specifies the absolute path in URI (without query string).
QUERY-PARAMTERS is a string or an alist.
If QUERY-PARAMTERS is a string, it is treated as an encoded query string.
If QUERY-PARAMTERS is an alist, it represents a list of cons pairs of
string, (query-key . query-value).
POST-BODY specifies the post body sent when METHOD equals to \"POST\".
If POST-BODY is nil, no body is posted.
If USE-SSL is non-nil, the request is performed with SSL.
The result alist includes the following keys, where a key is a symbol.
method: HTTP method such as \"GET\" or \"POST\".
scheme: the scheme name. \"http\" or \"https\".
host: the host to which the request is sent.
port: the port to which the request is sent (integer).
path: the absolute path string. Note that it does not include query string.
query-string: the query string.
encoded-query-alist: the alist consisting of pairs of encoded query-name and
encoded query-value.
uri: the URI. It includes the query string.
uri-without-query: the URI without the query string.
header-list: an alist specifying pairs of a parameter and its value in HTTP
header field.
post-body: the entity that will be posted."
(let* ((scheme (if use-ssl "https" "http"))
(default-port (if use-ssl 443 80))
(port (if port port default-port))
(query-string
(cond
((stringp query-parameters)
query-parameters)
((consp query-parameters)
(mapconcat (lambda (pair)
(cond
((stringp pair)
(twittering-percent-encode pair))
((consp pair)
(format
"%s=%s"
(twittering-percent-encode (car pair))
(twittering-percent-encode (cdr pair))))
(t
nil)))
query-parameters
"&"))
(t
nil)))
(encoded-query-alist
(cond
((stringp query-parameters)
;; Query name and its value must be already encoded.
(mapcar (lambda (str)
(if (string-match "=" str)
(let ((key (substring str 0 (match-beginning 0)))
(value (substring str (match-end 0))))
`(,key . ,value))
`(,str . nil)))
(split-string query-parameters "&")))
((consp query-parameters)
(mapcar (lambda (pair)
(cond
((stringp pair)
(cons (twittering-percent-encode pair) nil))
((consp pair)
(cons (twittering-percent-encode (car pair))
(twittering-percent-encode (cdr pair))))
(t
nil)))
query-parameters))
(t
nil)))
(uri-without-query
(concat scheme "://"
host
(when (and port (not (= port default-port)))
(format ":%d" port))
path))
(uri
(if query-string
(concat uri-without-query "?" query-string)
uri-without-query))
(header-list
`(,@(when (and (string= method "POST")
(not (assoc "Content-Length" header-list)))
`(("Content-Length" . ,(format "%d" (length post-body)))))
,@(unless (assoc "Host" header-list)
`(("Host" . ,host)))
,@(unless (assoc "User-Agent" header-list)
`(("User-Agent" . ,(twittering-user-agent))))
,@header-list)))
(cond
((not (member method '("POST" "GET")))
(error "Unknown HTTP method: %s" method)
nil)
((not (string-match "^/" path))
(error "Invalid HTTP path: %s" path)
nil)
(t
`((method . ,method)
(scheme . ,scheme)
(host . ,host)
(port . ,port)
(path . ,path)
(query-string . ,query-string)
(encoded-query-alist . ,encoded-query-alist)
(uri . ,uri)
(uri-without-query . ,uri-without-query)
(header-list . ,header-list)
(post-body . ,post-body))))))
(defun twittering-make-http-request-from-uri (method header-list uri &optional post-body)
"Returns an alist specifying a HTTP request.
The result alist has the same form as an alist generated by
`twittering-make-http-request'.
METHOD specifies HTTP method. It must be \"GET\" or \"POST\".
HEADER-LIST is a list of (field-name . field-value) specifying HTTP header
fields. The fields \"Host\" and \"User-Agent\" are automatically filled
if necessary.
URI specifies the URI including query string.
POST-BODY specifies the post body sent when METHOD equals to \"POST\".
If POST-BODY is nil, no body is posted."
(let* ((parts-alist
(let ((parsed-url (url-generic-parse-url uri)))
;; This is required for the difference of url library
;; distributed with Emacs 22 and 23.
(cond
((and (fboundp 'url-p) (url-p parsed-url))
;; Emacs 23 and later.
`((scheme . ,(url-type parsed-url))
(host . ,(url-host parsed-url))
(port . ,(url-portspec parsed-url))
(path . ,(url-filename parsed-url))))
((vectorp parsed-url)
;; Emacs 22.
`((scheme . ,(aref parsed-url 0))
(host . ,(aref parsed-url 3))
(port . ,(aref parsed-url 4))
(path . ,(aref parsed-url 5))))
(t
nil))))
(path (let ((path (cdr (assq 'path parts-alist))))
(if (string-match "\\`\\(.*\\)\\?" path)
(match-string 1 path)
path)))
(query-string (let ((path (cdr (assq 'path parts-alist))))
(if (string-match "\\?\\(.*\\)\\'" path)
(match-string 1 path)
nil))))
(twittering-make-http-request method header-list
(cdr (assq 'host parts-alist))
(cdr (assq 'port parts-alist))
path
query-string
post-body
(string= "https"
(cdr (assq 'scheme parts-alist))))))
(defun twittering-make-connection-info (request &optional additional order table)
"Make an alist specifying the information of connection for REQUEST.
REQUEST must be an alist that has the same keys as that generated by
`twittering-make-http-request'.
ADDITIONAL is appended to the tail of the result alist.
Following ADDITIONAL, an entry in TABLE is also appended to the result alist,
where `twittering-lookup-connection-type' determines the entry according to
the priority order ORDER.
If ORDER is nil, `twittering-connection-type-order' is used in place of ORDER.
If TABLE is nil, `twittering-connection-type-table' is used in place of TABLE.
The parameter symbols are following:
use-ssl: whether SSL is enabled or not.
allow-insecure-server-cert: non-nil if an insecure server certificate is
allowed on SSL.
cacert-file-fullpath: the full-path of a file including the certificates
authorizing a server certificate on SSL. The file must be in PEM format.
use-proxy: non-nil if using a proxy.
proxy-server: a proxy server or nil.
proxy-port: a port for connecting the proxy (integer) or nil.
proxy-user: a username for connecting the proxy or nil.
proxy-password: a password for connecting the proxy or nil.
request: an alist specifying a HTTP request."
(let* ((order (or order twittering-connection-type-order))
(table (or table twittering-connection-type-table))
(scheme (cdr (assq 'scheme request)))
(use-ssl (string= "https" scheme))
(entry (twittering-lookup-connection-type use-ssl order table)))
`((use-ssl . ,use-ssl)
(allow-insecure-server-cert
. ,twittering-allow-insecure-server-cert)
(cacert-file-fullpath
. ,(when use-ssl (twittering-ensure-ca-cert)))
(use-proxy . ,twittering-proxy-use)
,@(when twittering-proxy-use
`((proxy-server . ,(twittering-proxy-info scheme 'server))
(proxy-port . ,(twittering-proxy-info scheme 'port))
(proxy-user . ,(twittering-proxy-info scheme 'user))
(proxy-password . ,(twittering-proxy-info scheme 'password))))
(request . ,request)
,@additional
,@entry)))
(defun twittering-get-response-header (buffer)
"Extract HTTP response header from HTTP response.
BUFFER may be a buffer or the name of an existing buffer which contains the HTTP response."
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(if (search-forward-regexp "\r?\n\r?\n" nil t)
(prog1
(buffer-substring (point-min) (match-end 0))
(when twittering-debug-mode
(debug-printf "connection-info=%s\n" connection-info)
(debug-print "HTTP response header:\n--BEGIN\n")
(debug-print (buffer-substring (point-min) (match-end 0)))
(debug-print "--END\n")))
nil))))
(defun twittering-make-header-info-alist (header-str)
"Make HTTP header alist from HEADER-STR.
The alist consists of pairs of field-name and field-value, such as
'((\"Content-Type\" . \"application/xml\; charset=utf-8\")
(\"Content-Length\" . \"2075\"))."
(let* ((lines (split-string header-str "\r?\n"))
(status-line (car lines))
(header-lines (cdr lines)))
(when (string-match
"^\\(HTTP/1\.[01]\\) \\([0-9][0-9][0-9]\\) \\(.*\\)$"
status-line)
(append `((status-line . ,status-line)
(http-version . ,(match-string 1 status-line))
(status-code . ,(match-string 2 status-line))
(reason-phrase . ,(match-string 3 status-line)))
(remove nil
(mapcar
(lambda (line)
(when (string-match "^\\([^: ]*\\): *\\(.*\\)$" line)
(cons (match-string 1 line) (match-string 2 line))))
header-lines))))))
(defun twittering-decode-response-body (header-info)
"Decode the current buffer according to the content-type in HEADER-INFO."
(let* ((content-type
;; According to RFC2616, field name of a HTTP header is
;; case-insensitive.
(car
(remove
nil
(mapcar (lambda (entry)
(when (and (stringp (car entry))
(let ((case-fold-search t))
(string-match "\\`content-type\\'"
(car entry))))
(cdr entry)))
header-info))))
(parameters (when (stringp content-type)
(cdr (split-string content-type ";"))))
(regexp "^[[:space:]]*charset=utf-8[[:space:]]*$")
(encoded-with-utf-8
(let ((case-fold-search t))
(remove nil
(mapcar (lambda (entry)
(string-match regexp entry))
parameters)))))
(when encoded-with-utf-8
(decode-coding-region (point-min) (point-max) 'utf-8))))
(defun twittering-send-http-request-internal (request additional-info sentinel &optional order table)
"Open a connection and return a subprocess object for the connection.
REQUEST must be an alist that has the same keys as that generated by
`twittering-make-http-request'.
SENTINEL is called as a function when the process changes state.
It gets three arguments: the process, a string describing the change, and
the connection-info, which is generated by `twittering-make-connection-info'
and also includes an alist ADDITIONAL-INFO.
How to perform the request is selected from TABLE according to the priority
order ORDER. ORDER and TABLE are directly sent to
`twittering-make-connection-info'.
If ORDER is nil, `twittering-connection-type-order' is used in place of ORDER.
If TABLE is nil, `twittering-connection-type-table' is used in place of TABLE.
"
(let* ((order (or order twittering-connection-type-order))
(table (or table twittering-connection-type-table))
(connection-info
(twittering-make-connection-info request additional-info
order table))
(func (cdr (assq 'send-http-request connection-info)))
(temp-buffer (generate-new-buffer "*twmode-http-buffer*"))
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(cond
((and func (functionp func))
(funcall func "*twmode-generic*" temp-buffer
connection-info
(when (and sentinel (functionp sentinel))
(lexical-let ((sentinel sentinel)
(connection-info connection-info))
(lambda (proc status)
(apply sentinel proc status connection-info nil))))))
(t
(error "No valid connection method is found")
nil))))
(defun twittering-send-http-request (request additional-info func &optional clean-up-func)
"Send a HTTP request and return a subprocess object for the connection.
REQUEST must be an alist that has the same keys as that generated by
`twittering-make-http-request'.
FUNC is called when a HTTP response has been received without errors.
It is called with the current buffer containing the HTTP response (without
HTTP headers). FUNC is called with four arguments: the process, a symbol
describing the status of the process, a connection-info generated by
`twittering-make-connection-info', and a header-info generated by
`twittering-get-response-header'.
The connection-info also includes an alist ADDITIONAL-INFO.
If FUNC returns non-nil and `twittering-buffer-related-p' is non-nil, the
returned value is displayed as a message.
And also, if FUNC returns a string and it matches the regular expression
\"^\\\\(Failuare\\\\|Response\\\\): \", the returned value is displayed
as a message.
CLEAN-UP-FUNC is called whenever the sentinel of the subprocess for the
connection is called (as `set-process-sentinel').
It is called with three arguments: the process, a symbol describing the status
of the proess, and a connection-info generated by
`twittering-make-connection-info'.
They are the same as arguments for FUNC.
When a HTTP response has been received, FUNC is called in advance of
CLEAN-UP-FUNC. CLEAN-UP-FUNC can overwrite the message displayed by FUNC.
If the subprocess has exited, the buffer bound to it is automatically killed
after calling CLEAN-UP-FUNC.
The method to perform the request is determined from
`twittering-connection-type-table' according to the priority order
`twittering-connection-type-order'."
(lexical-let ((func func)
(clean-up-func clean-up-func))
(twittering-send-http-request-internal
request additional-info
(lambda (proc status-str connection-info)
(let ((status (cond
((string= status-str "urllib-finished") 'exit)
((processp proc) (process-status proc))
(t nil)))
(buffer (process-buffer proc))
(exit-status (cond
((string= status-str "urllib-finished") 0)
((processp proc) (process-exit-status proc))
(t 1)))
(command (process-command proc))
(pre-process-func
(cdr (assq 'pre-process-buffer connection-info)))
(mes nil))
(unwind-protect
(setq mes
(cond
((null status)
(format "Failure: process %s does not exist" proc))
((or (memq status '(run stop open listen connect))
(not (memq status '(exit signal closed failed))))
;; If the process is running, FUNC is not called.
nil)
((and command
(not (= 0 exit-status)))
;; If the process abnormally exited,
(format "Failure: %s exited abnormally (exit-status=%s)"
(car command) exit-status))
((not (buffer-live-p buffer))
(format "Failure: the buffer for %s is already killed"
proc))
(t
(when (functionp pre-process-func)
;; Pre-process buffer.
(funcall pre-process-func proc buffer connection-info))
(let* ((header (twittering-get-response-header buffer))
(header-info
(and header
(twittering-update-server-info header))))
(with-current-buffer buffer
(goto-char (point-min))
(when (search-forward-regexp "\r?\n\r?\n" nil t)
;; delete HTTP headers.
(delete-region (point-min) (match-end 0)))
;; It may be necessary to decode the contents of
;; the buffer by UTF-8 because
;; `twittering-http-application-headers' specifies
;; utf-8 as one of acceptable charset.
;; For the present, only UTF-8 is taken into account.
(twittering-decode-response-body header-info)
(apply func proc status connection-info
header-info nil))))))
;; unwind-forms
(setq mes
(cond
((null mes)
nil)
((string-match "^\\(Failure\\|Response\\): " mes)
(let* ((request (cdr (assq 'request connection-info)))
(method (cdr (assq 'method request)))
(uri (cdr (assq 'uri request))))
(concat mes " for " method " " uri)))
((twittering-buffer-related-p)
mes)))
(when mes
;; CLEAN-UP-FUNC can overwrite a message from the return value
;; of FUNC.
(message "%s" mes))
(when (functionp clean-up-func)
(funcall clean-up-func proc status connection-info))
(when (and (memq status '(exit signal closed failed))
(buffer-live-p buffer)
(not twittering-debug-mode))
(kill-buffer buffer))))))))
;;;;
;;;; Basic HTTP functions with tls and Emacs builtins.
;;;;
(eval-when-compile (require 'tls nil t))
(defun twittering-start-http-session-native-tls-p ()
(when (and (not twittering-proxy-use)
(require 'tls nil t))
(unless twittering-tls-program
(let ((programs
(remove nil
(mapcar (lambda (cmd)
(when (string-match "\\`\\([^ ]+\\) " cmd)
(when (executable-find (match-string 1 cmd))
cmd)))
tls-program))))
(setq twittering-tls-program
(if twittering-allow-insecure-server-cert
(mapcar
(lambda (str)
(cond
((string-match "^\\([^ ]*/\\)?openssl s_client " str)
(concat (match-string 0 str) "-verify 0 "
(substring str (match-end 0))))
((string-match "^\\([^ ]*/\\)?gnutls-cli " str)
(concat (match-string 0 str) "--insecure "
(substring str (match-end 0))))
(t
str)))
programs)
programs))))
(not (null twittering-tls-program))))
;; TODO: proxy
(defun twittering-send-http-request-native (name buffer connection-info sentinel)
(let* ((request (cdr (assq 'request connection-info)))
(uri (cdr (assq 'uri connection-info)))
(method (cdr (assq 'method request)))
(scheme (cdr (assq 'scheme request)))
(host (cdr (assq 'host request)))
(port (cdr (assq 'port request)))
(path (cdr (assq 'path request)))
(query-string (cdr (assq 'query-string request)))
(post-body (cdr (assq 'post-body request)))
(use-proxy (cdr (assq 'use-proxy connection-info)))
(proxy-server (cdr (assq 'proxy-server connection-info)))
(proxy-port (cdr (assq 'proxy-port connection-info)))
(proxy-user (cdr (assq 'proxy-user connection-info)))
(proxy-password (cdr (assq 'proxy-password connection-info)))
(proxy-credentials
(when (and proxy-user proxy-password)
(concat "Basic "
(base64-encode-string
(concat proxy-user ":" proxy-password)))))
(header-list
(let ((original-header-list (cdr (assq 'header-list request))))
(if proxy-credentials
(cons
`("Proxy-Authorization" ,proxy-credentials)
original-header-list)
original-header-list)))
(use-ssl (cdr (assq 'use-ssl connection-info)))
(allow-insecure-server-cert
(cdr (assq 'allow-insecure-server-cert connection-info)))
(connect-host (or proxy-server host))
(connect-port (or proxy-port port))
(request-str
(format "%s %s HTTP/1.1\r\n%s\r\n\r\n%s\r\n"
method
(if use-proxy
;; As described in 5.1.2 of RFC2616, the
;; absolute URI is required here if the connection
;; uses a proxy.
uri
(concat path
(when query-string
(concat "?" query-string))))
(mapconcat (lambda (pair)
(format "%s: %s" (car pair) (cdr pair)))
header-list "\r\n")
(or post-body "")))
(coding-system-for-read 'binary)
(coding-system-for-write 'binary)
(tls-program twittering-tls-program)
(proc
(funcall (if use-ssl
'open-tls-stream
'open-network-stream)
"network-connection-process"
nil connect-host connect-port)))
(when proc
(set-process-buffer proc buffer)
(when (functionp sentinel)
(if (twittering-process-alive-p proc)
(set-process-sentinel proc sentinel)
(funcall sentinel proc "finished")))
(process-send-string proc request-str)
proc)))
(defun twittering-pre-process-buffer-native (proc buffer connection-info)
(let ((use-ssl (cdr (assq 'use-ssl connection-info)))
(args (process-command proc)))
(cond
((and use-ssl args
(car
(remove nil
(mapcar (lambda (cmd)
(string-match "^\\(.*/\\)?gnutls-cli\\b" cmd))
args))))
(with-current-buffer buffer
(save-excursion
(goto-char (point-max))
(when (search-backward-regexp
"- Peer has closed the GNUTLS connection\r?\n\\'")
(let ((beg (match-beginning 0))
(end (match-end 0)))
(delete-region beg end))))))
((and use-ssl args
(car
(remove nil
(mapcar
(lambda (cmd)
(string-match "^\\(.*/\\)?openssl s_client\\b" cmd))
args))))
(with-current-buffer buffer
(save-excursion
(goto-char (point-max))
(when (search-backward-regexp "closed\r?\n\\'")
(let ((beg (match-beginning 0))
(end (match-end 0)))
(delete-region beg end))))))
(t
nil))))
;;;;
;;;; Basic HTTP functions with curl
;;;;
(defun twittering-find-curl-program ()
"Returns an appropriate `curl' program pathname or nil if not found."
(or (executable-find "curl")
(let ((windows-p (memq system-type '(windows-nt cygwin)))
(curl.exe
(expand-file-name
"curl.exe"
(expand-file-name
"win-curl"
(file-name-directory (symbol-file 'twit))))))
(and windows-p
(file-exists-p curl.exe) curl.exe))))
(defun twittering-start-http-session-curl-p ()
"Return t if curl was installed, otherwise nil."
(unless twittering-curl-program
(setq twittering-curl-program (twittering-find-curl-program)))
(not (null twittering-curl-program)))
(defun twittering-start-http-session-curl-https-p ()
"Return t if curl was installed and the curl support HTTPS, otherwise nil."
(when (twittering-start-http-session-curl-p)
(unless twittering-curl-program-https-capability
(with-temp-buffer
(let ((coding-system-for-read 'iso-safe)
(coding-system-for-write 'iso-safe)
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(call-process twittering-curl-program
nil (current-buffer) nil
"--version")
(goto-char (point-min))
(setq twittering-curl-program-https-capability
(if (search-forward-regexp "^Protocols: .*https" nil t)
'capable
'incapable)))))
(eq twittering-curl-program-https-capability 'capable)))
(defun twittering-send-http-request-curl (name buffer connection-info sentinel)
(let* ((request (cdr (assq 'request connection-info)))
(method (cdr (assq 'method request)))
(uri (cdr (assq 'uri request)))
(header-list (cdr (assq 'header-list request)))
(post-body (cdr (assq 'post-body request)))
(use-proxy (cdr (assq 'use-proxy connection-info)))
(proxy-server (cdr (assq 'proxy-server connection-info)))
(proxy-port (cdr (assq 'proxy-port connection-info)))
(proxy-user (cdr (assq 'proxy-user connection-info)))
(proxy-password (cdr (assq 'proxy-password connection-info)))
(use-ssl (cdr (assq 'use-ssl connection-info)))
(allow-insecure-server-cert
(cdr (assq 'allow-insecure-server-cert connection-info)))
(cacert-file-fullpath
(cdr (assq 'cacert-file-fullpath connection-info)))
(cacert-file-base-directory
(when cacert-file-fullpath
(file-name-directory cacert-file-fullpath)))
(cacert-file-body
(when cacert-file-fullpath
(file-name-nondirectory cacert-file-fullpath)))
(header-list
`(,@header-list
;; Make `curl' remove the HTTP header field "Expect" for
;; avoiding '417 Expectation Failed' HTTP response error.
;; The header field is automatically added for a HTTP request
;; exceeding 1024 byte. See
;; http://d.hatena.ne.jp/imait/20091228/1262004813 and
;; http://www.escafrace.co.jp/blog/09/10/16/1008
("Expect" . "")))
(curl-args
`("--include" "--silent"
,@(apply 'append
(mapcar
(lambda (pair)
;; Do not overwrite internal headers `curl' would use.
;; Thanks to William Xu.
;; "cURL - How To Use"
;; http://curl.haxx.se/docs/manpage.html
(unless (string= (car pair) "Host")
`("-H" ,(format "%s: %s" (car pair) (cdr pair)))))
header-list))
,@(when use-ssl `("--cacert" ,cacert-file-body))
,@(when (and use-ssl allow-insecure-server-cert)
`("--insecure"))
,@(when (and use-proxy proxy-server proxy-port)
(append
`("-x" ,(format "%s:%s" proxy-server proxy-port))
(when (and proxy-user proxy-password)
`("-U" ,(format "%s:%s" proxy-user proxy-password)))))
,@(when (string= "POST" method)
`("-d" ,(or post-body "")))
,uri))
(coding-system-for-read 'binary)
(coding-system-for-write 'binary)
(default-directory
;; If `use-ssl' is non-nil, the `curl' process
;; is executed at the same directory as the temporary cert file.
;; Without changing directory, `curl' misses the cert file if
;; you use Emacs on Cygwin because the path on Emacs differs
;; from Windows.
;; With changing directory, `curl' on Windows can find the cert
;; file if you use Emacs on Cygwin.
(if use-ssl
cacert-file-base-directory
default-directory)))
(twittering-start-process-with-sentinel name buffer
twittering-curl-program
curl-args sentinel)))
(defun twittering-pre-process-buffer-curl (proc buffer connection-info)
(let ((use-ssl (cdr (assq 'use-ssl connection-info)))
(use-proxy (cdr (assq 'use-proxy connection-info))))
(when (and use-ssl use-proxy)
;; When using SSL via a proxy with CONNECT method,
;; omit a successful HTTP response and headers if they seem to be
;; sent from the proxy.
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(let ((first-regexp
;; successful HTTP response
"\\`HTTP/1\.[01] 2[0-9][0-9] .*?\r?\n")
(next-regexp
;; following HTTP response
"^\\(\r?\n\\)HTTP/1\.[01] [0-9][0-9][0-9] .*?\r?\n"))
(when (and (search-forward-regexp first-regexp nil t)
(search-forward-regexp next-regexp nil t))
(let ((beg (point-min))
(end (match-end 1)))
(delete-region beg end)))))))))
;;;;
;;;; Basic HTTP functions with wget
;;;;
(defun twittering-find-wget-program ()
"Returns an appropriate `wget' program pathname or nil if not found."
(executable-find "wget"))
(defun twittering-start-http-session-wget-p ()
"Return t if `wget' was installed, otherwise nil."
(unless twittering-wget-program
(setq twittering-wget-program (twittering-find-wget-program)))
(not (null twittering-wget-program)))
(defun twittering-send-http-request-wget (name buffer connection-info sentinel)
(let* ((request (cdr (assq 'request connection-info)))
(method (cdr (assq 'method request)))
(scheme (cdr (assq 'scheme request)))
(uri (cdr (assq 'uri request)))
(header-list (cdr (assq 'header-list request)))
(post-body (cdr (assq 'post-body request)))
(use-proxy (cdr (assq 'use-proxy connection-info)))
(proxy-server (cdr (assq 'proxy-server connection-info)))
(proxy-port (cdr (assq 'proxy-port connection-info)))
(proxy-user (cdr (assq 'proxy-user connection-info)))
(proxy-password (cdr (assq 'proxy-password connection-info)))
(use-ssl (cdr (assq 'use-ssl connection-info)))
(allow-insecure-server-cert
(cdr (assq 'allow-insecure-server-cert connection-info)))
(cacert-file-fullpath
(cdr (assq 'cacert-file-fullpath connection-info)))
(cacert-file-base-directory
(when cacert-file-fullpath
(file-name-directory cacert-file-fullpath)))
(cacert-file-body
(when cacert-file-fullpath
(file-name-nondirectory cacert-file-fullpath)))
(args
`("--save-headers"
"--quiet"
"--output-document=-"
,@(remove nil
(mapcar
(lambda (pair)
(unless (string= (car pair) "Host")
(format "--header=%s: %s" (car pair) (cdr pair))))
header-list))
,@(when use-ssl
`(,(format "--ca-certificate=%s" cacert-file-body)))
,@(when (and use-ssl allow-insecure-server-cert)
`("--no-check-certificate"))
,@(cond
((not use-proxy)
'("--no-proxy"))
((and use-proxy proxy-server proxy-port
proxy-user proxy-password)
`(,(format "--proxy-user=%s" proxy-user)
,(format "--proxy-password=%s" proxy-password)))
(t
nil))
,@(when (string= "POST" method)
`(,(concat "--post-data=" (or post-body ""))))
,uri))
(coding-system-for-read 'binary)
(coding-system-for-write 'binary)
(default-directory
;; If `use-ssl' is non-nil, the `wget' process
;; is executed at the same directory as the temporary cert file.
;; Without changing directory, `wget' misses the cert file if
;; you use Emacs on Cygwin because the path on Emacs differs
;; from Windows.
;; With changing directory, `wget' on Windows can find the cert
;; file if you use Emacs on Cygwin.
(if use-ssl
cacert-file-base-directory
default-directory))
(process-environment
`(,@(when (and use-proxy proxy-server proxy-port)
`(,(format "%s_proxy=%s://%s:%s/" scheme
scheme proxy-server proxy-port)))
,@process-environment)))
(twittering-start-process-with-sentinel name buffer
twittering-wget-program args
sentinel)))
(defun twittering-pre-process-buffer-wget (proc buffer connection-info)
(with-current-buffer buffer
(save-excursion
(goto-char (point-min))
(when (search-forward-regexp "\\`[^\n]*?\r\r\n" (point-max) t)
;; When `wget.exe' writes HTTP response in text mode,
;; CRLF may be converted into CRCRLF.
(goto-char (point-min))
(while (search-forward "\r\n" nil t)
(replace-match "\n" nil t)))
(goto-char (point-max))
(when (search-backward-regexp "\nProcess [^\n]* finished\n\\'"
(point-min) t)
(replace-match "" nil t))
)))
;;;;
;;;; Basic HTTP functions with url library
;;;;
(defun twittering-start-http-session-urllib-p ()
"Return t if url library is available, otherwise nil."
(require 'url nil t))
(defun twittering-start-http-session-urllib-https-p ()
"Return t if url library can be used for HTTPS, otherwise nil."
(and (not twittering-proxy-use)
(require 'url nil t)
(cond
((<= 22 emacs-major-version)
;; On Emacs22 and later, `url' requires `tls'.
(twittering-start-http-session-native-tls-p))
((require 'ssl nil t)
;; On Emacs21, `url' requires `ssl'.
t)
((or (and (fboundp 'open-ssl-stream)
;; Since `url-gw' (required by `url') defines autoload of
;; `open-ssl-stream' from "ssl",
;; (fboundp 'open-ssl-stream) will be non-nil even if
;; "ssl" cannot be loaded and `open-ssl-stream' is
;; unavailable.
;; Here, the availability is confirmed by `documentation'.
(documentation 'open-ssl-stream))
;; On Emacs21, `url' requires `ssl' in order to use
;; `open-ssl-stream', which is included in `ssl.el'.
;; Even if `ssl' cannot be loaded, `open-tls-stream' can be
;; used as an alternative of the function.
(and (twittering-start-http-session-native-tls-p)
(defalias 'open-ssl-stream 'open-tls-stream)))
(provide 'ssl)
t)
(t
nil))))
(defun twittering-send-http-request-urllib (name buffer connection-info sentinel)
(let* ((request (cdr (assq 'request connection-info)))
(method (cdr (assq 'method request)))
(scheme (cdr (assq 'scheme request)))
(uri (cdr (assq 'uri request)))
(header-list (cdr (assq 'header-list request)))
(post-body (cdr (assq 'post-body request)))
(use-proxy (cdr (assq 'use-proxy connection-info)))
(proxy-server (cdr (assq 'proxy-server connection-info)))
(proxy-port (cdr (assq 'proxy-port connection-info)))
(proxy-user (cdr (assq 'proxy-user connection-info)))
(proxy-password (cdr (assq 'proxy-password connection-info)))
(proxy-credentials
(when (and proxy-user proxy-password)
(concat "Basic "
(base64-encode-string
(concat proxy-user ":" proxy-password)))))
(coding-system-for-read 'binary)
(coding-system-for-write 'binary)
(url-proxy-services
(when use-proxy
`((,scheme . ,(format "%s:%s" proxy-server proxy-port)))))
(url-request-method method)
(url-request-extra-headers
;; Remove some headers that should be configured by url library.
;; They may break redirections by url library because
;; `url-request-extra-headers' overwrites the new headers
;; that are adapted to redirected connection.
(apply 'append
(mapcar (lambda (pair)
(if (member (car pair)
'("Host" "Content-Length"))
nil
`(,pair)))
(if proxy-credentials
(cons
`("Proxy-Authorization" ,proxy-credentials)
header-list)
header-list))))
(url-request-data post-body)
(url-show-status twittering-url-show-status)
(url-http-attempt-keepalives nil)
(tls-program twittering-tls-program)
(coding-system-for-read 'binary)
(coding-system-for-write 'binary))
(lexical-let ((sentinel sentinel)
(buffer buffer))
(let ((result-buffer
(url-retrieve
uri
(lambda (&rest args)
(let ((proc url-http-process)
(url-buffer (current-buffer))
(status-str
(if (and (< emacs-major-version 22)
(boundp 'url-http-end-of-headers)
url-http-end-of-headers)
"urllib-finished"
"finished")))
;; Callback may be called multiple times.
;; (as filter and sentinel?)
(unless (local-variable-if-set-p 'twittering-retrieved)
(set (make-local-variable 'twittering-retrieved)
'not-completed)
(with-current-buffer buffer
(set-buffer-multibyte nil)
(insert-buffer-substring url-buffer))
(set-process-buffer proc buffer)
(unwind-protect
(apply sentinel proc status-str nil)
(set-process-buffer proc url-buffer)
(if (eq twittering-retrieved 'exited)
(url-mark-buffer-as-dead url-buffer)
(setq twittering-retrieved 'completed))))
(when (memq (process-status proc)
'(nil closed exit failed signal))
;; Mark `url-buffer' as dead when the process exited
;; and `sentinel' is completed.
;; If this `lambda' is evaluated via a filter, the
;; process may exit before it is finished to evaluate
;; `(apply sentinel ...)'. In the case, `buffer' should
;; not be killed. It should be killed after the
;; evaluation of `sentinel'.
(if (eq twittering-retrieved 'completed)
(url-mark-buffer-as-dead url-buffer)
(setq twittering-retrieved 'exited))))))))
(when (buffer-live-p result-buffer)
(with-current-buffer result-buffer
(set (make-local-variable 'url-show-status)
twittering-url-show-status)
;; Make `url-http-attempt-keepalives' buffer-local
;; in order to send the current value of the variable
;; to the sentinel invoked for HTTP redirection,
(make-local-variable 'url-http-attempt-keepalives))
(get-buffer-process result-buffer))))))
(defun twittering-pre-process-buffer-urllib (proc buffer connection-info)
(with-current-buffer buffer
(save-excursion
(goto-char (point-max))
(cond
((search-backward-regexp
"- Peer has closed the GNUTLS connection\r?\n\\'"
nil t)
(let ((beg (match-beginning 0))
(end (match-end 0)))
(delete-region beg end)))
((search-backward-regexp "closed\r?\n\\'" nil t)
(let ((beg (match-beginning 0))
(end (match-end 0)))
(delete-region beg end)))
(t nil)))))
;;;;
;;;; HTTP functions for twitter-like serivce
;;;;
(defun twittering-http-application-headers (&optional method headers)
"Return an assoc list of HTTP headers for twittering-mode."
(unless method
(setq method "GET"))
(let ((headers headers))
(push (cons "User-Agent" (twittering-user-agent)) headers)
(when (string= "GET" method)
(push (cons "Accept"
(concat
"text/xml"
",application/xml"
",application/xhtml+xml"
",application/html;q=0.9"
",text/plain;q=0.8"
",image/png,*/*;q=0.5"))
headers)
(push (cons "Accept-Charset" "utf-8;q=0.7,*;q=0.7")
headers))
(when (string= "POST" method)
(push (cons "Content-Type" "text/plain") headers))
headers
))
(defun twittering-add-application-header-to-http-request (request)
(let* ((method (cdr (assq 'method request)))
(auth-str
(cond
((eq twittering-auth-method 'basic)
(concat "Basic "
(base64-encode-string
(concat (twittering-get-username)
":" (twittering-get-password)))))
((memq twittering-auth-method '(oauth xauth))
(let ((access-token
(cdr (assoc "oauth_token"
twittering-oauth-access-token-alist)))
(access-token-secret
(cdr (assoc "oauth_token_secret"
twittering-oauth-access-token-alist))))
(unless (and (stringp access-token)
(stringp access-token-secret))
(error "OAuth token has not been prepared. Call `twittering-ensure-preparation-for-api-invocation' in advance"))
(twittering-oauth-auth-str-access
method
(cdr (assq 'uri-without-query request))
(cdr (assq 'encoded-query-alist request))
twittering-oauth-consumer-key twittering-oauth-consumer-secret
access-token access-token-secret)))
(t
nil)))
(application-headers
`(,@(twittering-http-application-headers method)
("Authorization" . ,auth-str))))
(mapcar (lambda (entry)
(if (eq (car entry) 'header-list)
`(header-list
. ,(append (cdr entry) application-headers))
entry))
request)))
(defun twittering-get-error-message (header-info connection-info &optional buffer)
"Return an error message generated from the arguments.
HEADER-INFO must be an alist generated by `twittering-get-response-header'.
CONNECTION-INFO must be an alist generated by
`twittering-make-connection-info'. It may include some additional information
which is added by `twittering-send-http-request'.
BUFFER must be nil or a HTTP response body, which includes error messages from
the server when the HTTP status code equals to 400 or 403.
If BUFFER is nil, the current buffer is used instead."
(let ((buffer (or buffer (current-buffer)))
(status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(cond
((and (buffer-live-p buffer)
(member status-code '("400" "401" "403" "404")))
;; Twitter returns an error message as a HTTP response body if
;; HTTP status is "400 Bad Request" or "403 Forbidden".
;; See "HTTP Response Codes and Errors | dev.twitter.com"
;; http://dev.twitter.com/pages/responses_errors .
;;
;; However, Twitter seems to return an error message even when
;; the HTTP status is "401 Unauthorized" or "404 Not Found".
(let* ((format (cdr (assq 'format connection-info)))
(error-mes
(cond
((eq format 'xml)
(let ((xmltree
(with-current-buffer buffer
(twittering-xml-parse-region (point-min)
(point-max)))))
(car (cddr (assq 'error (or (assq 'errors xmltree)
(assq 'hash xmltree)))))))
((eq format 'json)
(let ((json-object (with-current-buffer buffer
(twittering-json-read))))
(cdr (assq 'error json-object))))
(t
;; ATOM is not supported.
nil))))
(if error-mes
(format "%s (%s)" status-line error-mes)
status-line)))
(t
status-line))))
(defun twittering-http-get (host method &optional parameters format additional-info sentinel clean-up-sentinel)
(let* ((format (or format "xml"))
(sentinel (or sentinel 'twittering-http-get-default-sentinel))
(path (concat "/" method "." format))
(headers nil)
(port nil)
(post-body "")
(request
(twittering-add-application-header-to-http-request
(twittering-make-http-request "GET" headers host port path
parameters post-body
twittering-use-ssl))))
(twittering-send-http-request request additional-info
sentinel clean-up-sentinel)))
(defun twittering-http-get-default-sentinel (proc status connection-info header-info)
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(case-string
status-code
(("200")
(debug-printf "connection-info=%s" connection-info)
(let* ((spec (cdr (assq 'timeline-spec connection-info)))
(spec-string (cdr (assq 'timeline-spec-string connection-info)))
(format (cdr (assq 'format connection-info)))
(statuses
(cond
((eq format 'json)
(let ((json-array (twittering-json-read)))
(cond
((null json-array)
nil)
((eq (car spec) 'search)
(mapcar 'twittering-json-object-to-a-status-on-search
(cdr (assq 'results json-array))))
((twittering-timeline-spec-is-direct-messages-p spec)
(mapcar
'twittering-json-object-to-a-status-on-direct-messages
json-array))
(t
(mapcar 'twittering-json-object-to-a-status
json-array)))))
((eq format 'xml)
(let ((xmltree
(twittering-xml-parse-region (point-min) (point-max))))
(when xmltree
(twittering-xmltree-to-status xmltree))))
((eq format 'atom)
(let ((xmltree
(twittering-xml-parse-region (point-min) (point-max))))
(when xmltree
(twittering-atom-xmltree-to-status xmltree))))
(t
nil))))
(when statuses
(let ((new-statuses
(twittering-add-statuses-to-timeline-data statuses spec))
(buffer (twittering-get-buffer-from-spec spec)))
;; FIXME: We should retrieve un-retrieved statuses until
;; statuses is nil. twitter server returns nil as
;; xmltree with HTTP status-code is "200" when we
;; retrieved all un-retrieved statuses.
(when (and new-statuses buffer)
(twittering-render-timeline buffer new-statuses t))))
(if twittering-notify-successful-http-get
(format "Fetching %s. Success." spec-string)
nil)))
(("404")
;; The requested resource does not exist.
(let ((spec (cdr (assq 'timeline-spec connection-info)))
(spec-string (cdr (assq 'timeline-spec-string connection-info))))
;; Remove specs related to the invalid spec from history.
(mapc
(lambda (buffer)
(let ((other-spec (twittering-get-timeline-spec-for-buffer buffer))
(other-spec-string
(twittering-get-timeline-spec-string-for-buffer buffer)))
(when (twittering-timeline-spec-depending-on-p other-spec spec)
(twittering-remove-timeline-spec-string-from-history
other-spec-string))))
(twittering-get-buffer-list)))
(format "Response: %s"
(twittering-get-error-message header-info connection-info)))
(t
(format "Response: %s"
(twittering-get-error-message header-info connection-info))))))
(defun twittering-retrieve-single-tweet-sentinel (proc status connection-info header-info)
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(case-string
status-code
(("200" "403" "404")
(debug-printf "connection-info=%s" connection-info)
(let* ((id (cdr (assq 'id connection-info)))
(format (cdr (assq 'format connection-info)))
(user-screen-name (cdr (assq 'user-screen-name connection-info)))
(status
(cond
((string= status-code "403")
;; Forbidden. Maybe a protected tweet?
(twittering-make-alist-of-forbidden-tweet id
user-screen-name))
((string= status-code "404")
;; The requested resource does not exist.
(twittering-make-alist-of-non-existent-tweet id
user-screen-name))
((eq format 'json)
(let ((json-object (twittering-json-read)))
(twittering-json-object-to-a-status json-object)))
((eq format 'xml)
(let ((xmltree
(twittering-xml-parse-region (point-min) (point-max))))
(when xmltree
(car
(twittering-xmltree-to-status
`((statuses nil ,@xmltree)))))))
(t
nil))))
(when status
(twittering-add-statuses-to-timeline-data `(,status) '(:single))
(let ((buffer (cdr (assq 'buffer connection-info)))
(spec (cdr (assq 'timeline-spec connection-info)))
(prop
(cdr (assq 'property-to-be-redisplayed connection-info))))
(cond
(spec
;; The process has been invoked via `twittering-call-api' with
;; the command `retrieve-timeline', not the command
;; `retrieve-single-tweet'.
(let ((new-statuses `(,status))
(buffer (twittering-get-buffer-from-spec spec)))
(when (and new-statuses buffer)
(twittering-render-timeline buffer new-statuses t))))
((and buffer prop (buffer-live-p buffer))
(twittering-redisplay-status-on-each-buffer buffer prop)
(with-current-buffer buffer
(save-excursion
(let ((buffer-read-only nil))
(lexical-let ((prop prop))
(twittering-for-each-property-region
prop
(lambda (beg end value)
;; Remove the property required no longer.
(remove-text-properties beg end `(,prop nil))
(goto-char beg)
(twittering-render-replied-statuses)))))))))))
(cond
((string= status-code "403")
(format "You are not authorized to see this tweet (ID %s)." id))
((string= status-code "404")
(format "The tweet with ID %s does not exist." id))
(twittering-notify-successful-http-get
(format "Fetching %s. Success." id))
(t
nil))))
(t
(format "Response: %s"
(twittering-get-error-message header-info connection-info))))))
(defmacro twittering-http-get-list-sentinel-base (what)
`(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info)))
(indexes nil)
(mes nil))
(case-string
status-code
(("200")
(let ((xmltree (twittering-xml-parse-region (point-min) (point-max))))
(when xmltree
(setq indexes
(mapcar
(lambda (c-node)
(caddr (assq ,what c-node)))
(remove nil
(mapcar
(lambda (node)
(and (consp node) (eq 'list (car node))
node))
(cdr-safe
(assq 'lists (assq 'lists_list xmltree))))
))
))))
(t
(setq mes (format "Response: %s"
(twittering-get-error-message header-info
connection-info)))))
(setq twittering-list-index-retrieved
(or indexes
mes
"")) ;; set "" explicitly if user does not have a list.
mes))
(defun twittering-http-get-list-index-sentinel (proc status connection-info header-info)
(twittering-http-get-list-sentinel-base 'slug))
(defun twittering-http-get-list-subscriptions-sentinel (proc status connection-info header-info)
(let ((result (twittering-http-get-list-sentinel-base 'full_name)))
(when (listp twittering-list-index-retrieved)
(setq twittering-list-index-retrieved
(mapcar (lambda (str)
(and (string-match "\\`@\\(.*\\)\\'" str)
(match-string 1 str)))
twittering-list-index-retrieved)))
result))
(defun twittering-http-post (host method &optional parameters format additional-info sentinel clean-up-sentinel)
"Send HTTP POST request to api.twitter.com (or search.twitter.com)
HOST is hostname of remote side, api.twitter.com (or search.twitter.com).
METHOD must be one of Twitter API method classes
(statuses, users or direct_messages).
PARAMETERS is alist of URI parameters.
ex) ((\"mode\" . \"view\") (\"page\" . \"6\")) => <URI>?mode=view&page=6
FORMAT is a response data format (\"xml\", \"atom\", \"json\")"
(let* ((format (or format "xml"))
(sentinel (or sentinel 'twittering-http-post-default-sentinel))
(path (concat "/" method "." format))
(headers nil)
(port nil)
(post-body "")
(request
(twittering-add-application-header-to-http-request
(twittering-make-http-request "POST" headers host port path
parameters post-body
twittering-use-ssl))))
(twittering-send-http-request request additional-info
sentinel clean-up-sentinel)))
(defun twittering-http-post-default-sentinel (proc status connection-info header-info)
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(case-string
status-code
(("200")
"Success: Post.")
(t
(format "Response: %s"
(twittering-get-error-message header-info connection-info))))))
(defun twittering-http-post-destroy-status-sentinel (proc status connection-info header-info)
"A sentinel for deleting a status invoked via `twittering-call-api'."
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(case-string
status-code
(("200")
(let* ((xml (twittering-xml-parse-region (point-min) (point-max)))
(id (elt (assq 'id (assq 'status xml)) 2))
(text (elt (assq 'text (assq 'status xml)) 2)))
(cond
(id
(twittering-delete-status-from-data-table id)
(format "Deleting \"%s\". Success." text))
(t
"Failure: the response for deletion could not be parsed."))))
(t
(format "Response: %s"
(twittering-get-error-message header-info connection-info))))))
;;;;
;;;; OAuth
;;;;
(defun twittering-oauth-url-encode (str &optional coding-system)
"Encode string according to Percent-Encoding defined in RFC 3986."
(let ((coding-system (or (when (and coding-system
(coding-system-p coding-system))
coding-system)
'utf-8)))
(mapconcat
(lambda (c)
(cond
((or (and (<= ?A c) (<= c ?Z))
(and (<= ?a c) (<= c ?z))
(and (<= ?0 c) (<= c ?9))
(eq ?. c)
(eq ?- c)
(eq ?_ c)
(eq ?~ c))
(char-to-string c))
(t (format "%%%02X" c))))
(encode-coding-string str coding-system)
"")))
(defun twittering-oauth-unhex (c)
(cond
((and (<= ?0 c) (<= c ?9))
(- c ?0))
((and (<= ?A c) (<= c ?F))
(+ 10 (- c ?A)))
((and (<= ?a c) (<= c ?f))
(+ 10 (- c ?a)))
))
(defun twittering-oauth-url-decode (str &optional coding-system)
(let* ((coding-system (or (when (and coding-system
(coding-system-p coding-system))
coding-system)
'utf-8))
(substr-list (split-string str "%"))
(head (car substr-list))
(tail (cdr substr-list)))
(decode-coding-string
(concat
head
(mapconcat
(lambda (substr)
(if (string-match "\\`\\([0-9a-fA-F]\\)\\([0-9a-fA-F]\\)\\(.*\\)\\'"
substr)
(let* ((c1 (string-to-char (match-string 1 substr)))
(c0 (string-to-char (match-string 2 substr)))
(tail (match-string 3 substr))
(ch (+ (* 16 (twittering-oauth-unhex c1))
(twittering-oauth-unhex c0))))
(concat (char-to-string ch) tail))
substr))
tail
""))
coding-system)))
(defun twittering-oauth-make-signature-base-string (method base-url parameters)
;; "OAuth Core 1.0a"
;; http://oauth.net/core/1.0a/#anchor13
(let* ((sorted-parameters (copy-sequence parameters))
(sorted-parameters
(sort sorted-parameters
(lambda (entry1 entry2)
(string< (car entry1) (car entry2))))))
(concat
method
"&"
(twittering-oauth-url-encode base-url)
"&"
(mapconcat
(lambda (entry)
(let ((key (car entry))
(value (cdr entry)))
(concat (twittering-oauth-url-encode key)
"%3D"
(twittering-oauth-url-encode value))))
sorted-parameters
"%26"))))
(defun twittering-oauth-make-random-string (len)
(let* ((table
(concat
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"))
(n (length table))
(l 0)
(result (make-string len ?0)))
(while (< l len)
(aset result l (aref table (random n)))
(setq l (1+ l)))
result))
(defun twittering-sha1 (&rest args)
"Return the SHA1 (Secure Hash Algorithm) of an object.
This is equivalent to the function `sha1' except that
`coding-system-for-read' and `coding-system-for-write' are bound to the
symbol `binary'.
The function `sha1' uses an external program for large object. However,
the coding system for transferring data from/to the program is not fixed,
at least in the implementation distributed with GNU Emacs 21.4.1, 22.2.1
and 23.2.1.
Therefore, the result from the function `sha1' may depend on the current
coding system.
This function avoid the dependency by binding `coding-system-for-read' and
`coding-system-for-write' to the symbol `binary'."
(require 'sha1)
(let ((coding-system-for-read 'binary)
(coding-system-for-write 'binary)
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(apply 'sha1 args)))
;;;
;;; The below function is derived from `hmac-sha1' retrieved
;;; from http://www.emacswiki.org/emacs/HmacShaOne.
;;;
(defun twittering-hmac-sha1 (key message)
"Return an HMAC-SHA1 authentication code for KEY and MESSAGE.
KEY and MESSAGE must be unibyte strings. The result is a unibyte
string. Use the function `encode-hex-string' or the function
`base64-encode-string' to produce human-readable output.
See URL:<http://en.wikipedia.org/wiki/HMAC> for more information
on the HMAC-SHA1 algorithm.
The Emacs multibyte representation actually uses a series of
8-bit values under the hood, so we could have allowed multibyte
strings as arguments. However, internal 8-bit values don't
correspond to any external representation \(at least for major
version 22). This makes multibyte strings useless for generating
hashes.
Instead, callers must explicitly pick and use an encoding for
their multibyte data. Most callers will want to use UTF-8
encoding, which we can generate as follows:
(let ((unibyte-key (encode-coding-string key 'utf-8 t))
(unibyte-value (encode-coding-string value 'utf-8 t)))
(twittering-hmac-sha1 unibyte-key unibyte-value))
For keys and values that are already unibyte, the
`encode-coding-string' calls just return the same string."
;;; Return an HMAC-SHA1 authentication code for KEY and MESSAGE.
;;;
;;; KEY and MESSAGE must be unibyte strings. The result is a unibyte
;;; string. Use the function `encode-hex-string' or the function
;;; `base64-encode-string' to produce human-readable output.
;;;
;;; See URL:<http://en.wikipedia.org/wiki/HMAC> for more information
;;; on the HMAC-SHA1 algorithm.
;;;
;;; The Emacs multibyte representation actually uses a series of
;;; 8-bit values under the hood, so we could have allowed multibyte
;;; strings as arguments. However, internal 8-bit values don't
;;; correspond to any external representation \(at least for major
;;; version 22). This makes multibyte strings useless for generating
;;; hashes.
;;;
;;; Instead, callers must explicitly pick and use an encoding for
;;; their multibyte data. Most callers will want to use UTF-8
;;; encoding, which we can generate as follows:
;;;
;;; (let ((unibyte-key (encode-coding-string key 'utf-8 t))
;;; (unibyte-value (encode-coding-string value 'utf-8 t)))
;;; (hmac-sha1 unibyte-key unibyte-value))
;;;
;;; For keys and values that are already unibyte, the
;;; `encode-coding-string' calls just return the same string.
;;;
;;; Author: Derek Upham - sand (at) blarg.net
;;;
;;; Copyright: This code is in the public domain.
(require 'sha1)
(when (multibyte-string-p key)
(error "key must be unibyte"))
(when (multibyte-string-p message)
(error "message must be unibyte"))
;; The key block is always exactly the block size of the hash
;; algorithm. If the key is too small, we pad it with zeroes (or
;; instead, we initialize the key block with zeroes and copy the
;; key onto the nulls). If the key is too large, we run it
;; through the hash algorithm and use the hashed value (strange
;; but true).
(let ((+hmac-sha1-block-size-bytes+ 64)) ; SHA-1 uses 512-bit blocks
(when (< +hmac-sha1-block-size-bytes+ (length key))
(setq key (twittering-sha1 key nil nil t)))
(let ((key-block (make-vector +hmac-sha1-block-size-bytes+ 0)))
(dotimes (i (length key))
(aset key-block i (aref key i)))
(let ((opad (make-vector +hmac-sha1-block-size-bytes+ #x5c))
(ipad (make-vector +hmac-sha1-block-size-bytes+ #x36)))
(dotimes (i +hmac-sha1-block-size-bytes+)
(aset ipad i (logxor (aref ipad i) (aref key-block i)))
(aset opad i (logxor (aref opad i) (aref key-block i))))
(when (fboundp 'unibyte-string)
;; `concat' of Emacs23 (and later?) generates a multi-byte
;; string from a vector of characters with eight bit.
;; Since `opad' and `ipad' must be unibyte, we have to
;; convert them by using `unibyte-string'.
;; We cannot use `string-as-unibyte' here because it encodes
;; bytes with the manner of UTF-8.
(setq opad (apply 'unibyte-string (mapcar 'identity opad)))
(setq ipad (apply 'unibyte-string (mapcar 'identity ipad))))
(twittering-sha1 (concat opad
(twittering-sha1 (concat ipad message)
nil nil t))
nil nil t)))))
(defun twittering-oauth-auth-str (method base-url query-parameters oauth-parameters key)
"Generate the value for HTTP Authorization header on OAuth.
QUERY-PARAMETERS is an alist for query parameters, where name and value
must be encoded into the same as they will be sent."
(let* ((parameters (append query-parameters oauth-parameters))
(base-string
(twittering-oauth-make-signature-base-string method base-url parameters))
(key (if (multibyte-string-p key)
(string-make-unibyte key)
key))
(base-string (if (multibyte-string-p base-string)
(string-make-unibyte base-string)
base-string))
(signature
(base64-encode-string (twittering-hmac-sha1 key base-string))))
(concat
"OAuth "
(mapconcat
(lambda (entry)
(concat (car entry) "=\"" (cdr entry) "\""))
oauth-parameters
",")
",oauth_signature=\"" (twittering-oauth-url-encode signature) "\"")))
(defun twittering-oauth-auth-str-request-token (url query-parameters consumer-key consumer-secret &optional oauth-parameters)
(let ((key (concat consumer-secret "&"))
(oauth-params
(or oauth-parameters
`(("oauth_nonce" . ,(twittering-oauth-make-random-string 43))
("oauth_callback" . "oob")
("oauth_signature_method" . "HMAC-SHA1")
("oauth_timestamp" . ,(format-time-string "%s"))
("oauth_consumer_key" . ,consumer-key)
("oauth_version" . "1.0")))))
(twittering-oauth-auth-str "POST" url query-parameters oauth-params key)))
(defun twittering-oauth-auth-str-exchange-token (url query-parameters consumer-key consumer-secret request-token request-token-secret verifier &optional oauth-parameters)
(let ((key (concat consumer-secret "&" request-token-secret))
(oauth-params
(or oauth-parameters
`(("oauth_consumer_key" . ,consumer-key)
("oauth_nonce" . ,(twittering-oauth-make-random-string 43))
("oauth_signature_method" . "HMAC-SHA1")
("oauth_timestamp" . ,(format-time-string "%s"))
("oauth_version" . "1.0")
("oauth_token" . ,request-token)
("oauth_verifier" . ,verifier)))))
(twittering-oauth-auth-str "POST" url query-parameters oauth-params key)))
(defun twittering-oauth-auth-str-access (method url query-parameters consumer-key consumer-secret access-token access-token-secret &optional oauth-parameters)
"Generate a string for Authorization in HTTP header on OAuth.
METHOD means HTTP method such as \"GET\", \"POST\", etc. URL means a simple
URL without port number and query parameters.
QUERY-PARAMETERS means an alist of query parameters such as
'((\"status\" . \"test%20tweet\")
(\"in_reply_to_status_id\" . \"12345678\")),
where name and value must be encoded into the same as they will be sent.
CONSUMER-KEY and CONSUMER-SECRET specifies the consumer.
ACCESS-TOKEN and ACCESS-TOKEN-SECRET must be authorized before calling this
function."
(let ((key (concat consumer-secret "&" access-token-secret))
(oauth-params
(or oauth-parameters
`(("oauth_consumer_key" . ,consumer-key)
("oauth_nonce" . ,(twittering-oauth-make-random-string 43))
("oauth_signature_method" . "HMAC-SHA1")
("oauth_timestamp" . ,(format-time-string "%s"))
("oauth_version" . "1.0")
("oauth_token" . ,access-token)))))
(twittering-oauth-auth-str method url query-parameters oauth-params key)))
;; "Using xAuth | dev.twitter.com"
;; http://dev.twitter.com/pages/xauth
(defun twittering-xauth-auth-str-access-token (url query-parameters consumer-key consumer-secret username password &optional oauth-parameters)
(let ((key (concat consumer-secret "&"))
(oauth-params
(or oauth-parameters
`(("oauth_nonce" . ,(twittering-oauth-make-random-string 43))
("oauth_signature_method" . "HMAC-SHA1")
("oauth_timestamp" . ,(format-time-string "%s"))
("oauth_consumer_key" . ,consumer-key)
("oauth_version" . "1.0"))))
(query-params
(append query-parameters
`(("x_auth_mode" . "client_auth")
("x_auth_password"
. ,(twittering-oauth-url-encode password))
("x_auth_username"
. ,(twittering-oauth-url-encode username))))))
(twittering-oauth-auth-str "POST" url query-params oauth-params key)))
;; "OAuth Core 1.0a"
;; http://oauth.net/core/1.0a/#response_parameters
(defun twittering-oauth-make-response-alist (str)
(mapcar
(lambda (entry)
(let* ((pair (split-string entry "="))
(name-entry (car pair))
(value-entry (cadr pair))
(name (and name-entry (twittering-oauth-url-decode name-entry)))
(value (and value-entry
(twittering-oauth-url-decode value-entry))))
`(,name . ,value)))
(split-string str "&")))
(defun twittering-oauth-get-response-alist (buffer)
(with-current-buffer buffer
(goto-char (point-min))
(when (search-forward-regexp
"\\`\\(\\(HTTP/1\.[01]\\) \\([0-9][0-9][0-9]\\) \\(.*?\\)\\)\r?\n"
nil t)
(let ((status-line (match-string 1))
(http-version (match-string 2))
(status-code (match-string 3))
(reason-phrase (match-string 4)))
(cond
((not (string-match "2[0-9][0-9]" status-code))
(message "Response: %s" status-line)
nil)
((search-forward-regexp "\r?\n\r?\n" nil t)
(let ((beg (match-end 0))
(end (point-max)))
(twittering-oauth-make-response-alist (buffer-substring beg end))))
(t
(message "Response: %s" status-line)
nil))))))
(defun twittering-oauth-get-token-alist-url (url auth-str post-body)
(let* ((url-request-method "POST")
(url-request-extra-headers
`(("Authorization" . ,auth-str)
("Accept-Charset" . "us-ascii")
("Content-Type" . "application/x-www-form-urlencoded")
("Content-Length" . ,(format "%d" (length post-body)))))
(url-request-data post-body)
(coding-system-for-read 'utf-8-unix))
(lexical-let ((result 'queried))
(let ((buffer
(url-retrieve
url
(lambda (&rest args)
(let* ((status (if (< 21 emacs-major-version)
(car args)
nil))
(callback-args (if (< 21 emacs-major-version)
(cdr args)
args))
(response-buffer (current-buffer)))
(setq result
(twittering-oauth-get-response-alist response-buffer))
)))))
(while (eq result 'queried)
(sit-for 0.1))
(unless twittering-debug-mode
(kill-buffer buffer))
result))))
(defun twittering-oauth-get-token-alist (url auth-str &optional post-body)
(let ((request
(twittering-make-http-request-from-uri
"POST"
`(("Authorization" . ,auth-str)
("Accept-Charset" . "us-ascii")
("Content-Type" . "application/x-www-form-urlencoded"))
url post-body)))
(lexical-let ((result 'queried))
(let ((proc
(twittering-send-http-request
request nil
(lambda (proc status connection-info header-info)
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(case-string
status-code
(("200")
(when twittering-debug-mode
(let ((buffer (current-buffer)))
(with-current-buffer (twittering-debug-buffer)
(insert-buffer-substring buffer))))
(setq result
(twittering-oauth-make-response-alist
(buffer-string)))
nil)
(t
(setq result nil)
(format "Response: %s" status-line)))))
(lambda (proc status connection-info)
(when (and (not (twittering-process-alive-p proc))
(eq result 'queried))
(setq result nil))))))
(twittering-wait-while nil 0.1
(and (eq result 'queried)
(twittering-process-alive-p proc)))
(when (and (eq result 'queried)
(not (twittering-process-alive-p proc)))
;; If the process has been dead, wait a moment because
;; Emacs may be in the middle of evaluating the sentinel.
(twittering-wait-while 10 0.1
(eq result 'queried)
nil
(setq result nil)))
result))))
(defun twittering-oauth-get-request-token (url consumer-key consumer-secret)
(let ((auth-str
(twittering-oauth-auth-str-request-token
url nil consumer-key consumer-secret)))
(twittering-oauth-get-token-alist url auth-str)))
(defun twittering-oauth-exchange-request-token (url consumer-key consumer-secret request-token request-token-secret verifier)
(let ((auth-str
(twittering-oauth-auth-str-exchange-token
url nil
consumer-key consumer-secret
request-token request-token-secret verifier)))
(twittering-oauth-get-token-alist url auth-str)))
(defun twittering-oauth-get-access-token (request-token-url authorize-url-func access-token-url consumer-key consumer-secret consumer-name)
"Return an alist of authorized access token.
The function retrieves a request token from the site specified by
REQUEST-TOKEN-URL. Then, The function asks a WWW browser to authorize the
token by calling `browse-url'. The URL for authorization is calculated by
calling AUTHORIZE-URL-FUNC with the request token as an argument.
AUTHORIZE-URL-FUNC is called as `(funcal AUTHORIZE-URL-FUNC request-token)',
where the request-token is a string.
After calling `browse-url', the function waits for user to input the PIN code
that is displayed in the browser. The request token is authorized by the
PIN code, and then it is exchanged for the access token on the site
specified by ACCESS-TOKEN-URL.
CONSUMER-KEY and CONSUMER-SECRET specify the consumer.
CONSUMER-NAME is displayed at the guide of authorization.
The access token is returned as a list of a cons pair of name and value
like following:
((\"oauth_token\"
. \"819797-Jxq8aYUDRmykzVKrgoLhXSq67TEa5ruc4GJC2rWimw\")
(\"oauth_token_secret\"
. \"J6zix3FfA9LofH0awS24M3HcBYXO5nI1iYe8EfBA\")
(\"user_id\" . \"819797\")
(\"screen_name\" . \"episod\"))
."
(let* ((request-token-alist
(twittering-oauth-get-request-token
request-token-url consumer-key consumer-secret))
(request-token (cdr (assoc "oauth_token" request-token-alist)))
(request-token-secret
(cdr (assoc "oauth_token_secret" request-token-alist)))
(authorize-url (funcall authorize-url-func request-token))
(str
(concat
(propertize "Authorization via OAuth\n" 'face 'bold)
"\n"
"1.Allow access by " consumer-name " on the below site.\n"
"\n "
(propertize authorize-url 'url authorize-url 'face 'bold)
"\n"
"\n"
(when twittering-oauth-invoke-browser
(concat
" Emacs invokes your browser by the function `browse-url'.\n"
" If the site is not opened automatically, you have to open\n"
" the site manually.\n"
"\n"))
"2.After allowing access, the site will display the PIN code."
"\n"
" Input the PIN code "
(propertize "at the below minibuffer." 'face 'bold))))
(cond
(request-token-alist
(with-temp-buffer
(switch-to-buffer (current-buffer))
(let* ((str-height (length (split-string str "\n")))
(height (max 0 (- (/ (- (window-text-height) 1) 2)
(/ str-height 2)))))
(insert (make-string height ?\n) str)
(if twittering-oauth-invoke-browser
(browse-url authorize-url)
(when (y-or-n-p "Open authorization URL with browser? (using `browse-url')")
(browse-url authorize-url)))
(let* ((pin
(block pin-input-block
(while t
(let ((pin-input (read-string "Input PIN code: ")))
(when (string-match "^\\s-*\\([0-9]+\\)\\s-*$" pin-input)
(return-from pin-input-block
(match-string 1 pin-input)))))))
(verifier pin))
(twittering-oauth-exchange-request-token
access-token-url
consumer-key consumer-secret
request-token request-token-secret verifier)))))
(t
(error "Failed to retrieve a request token")
nil))))
(defun twittering-xauth-get-access-token (access-token-url consumer-key consumer-secret username password)
(let ((auth-str
(twittering-xauth-auth-str-access-token
access-token-url nil consumer-key consumer-secret
username password))
(post-body
(mapconcat (lambda (pair)
(format "%s=%s" (car pair)
(twittering-oauth-url-encode (cdr pair))))
`(("x_auth_mode" . "client_auth")
("x_auth_password" . ,password)
("x_auth_username" . ,username))
"&")))
(twittering-oauth-get-token-alist access-token-url auth-str post-body)))
;;;;
;;;; Private storage
;;;;
(defun twittering-private-info-loaded-p ()
twittering-private-info-file-loaded)
(defun twittering-load-private-info ()
(let* ((file twittering-private-info-file)
(decrypted-str (twittering-read-from-encrypted-file file))
(loaded-alist
(when decrypted-str
(condition-case nil
(read decrypted-str)
(error
nil)))))
(when loaded-alist
(remove
nil
(mapcar
(lambda (pair)
(when (consp pair)
(let ((sym (car pair))
(value (cdr pair)))
(cond
((memq sym twittering-variables-stored-with-encryption)
(set sym value)
sym)
(t
nil)))))
loaded-alist)))))
(defun twittering-load-private-info-with-guide ()
(let ((str (concat
"Loading authorized access token for OAuth from\n"
(format "%s.\n" twittering-private-info-file)
"\n"
(propertize "Please input the master password.\n" 'face 'bold)
"\n"
"To cancel it, you may need to press C-g multiple times.\n"
)))
(with-temp-buffer
(switch-to-buffer (current-buffer))
(let* ((str-height (length (split-string str "\n")))
(height (max 0 (- (/ (- (window-text-height) 1) 2)
(/ str-height 2)))))
(insert (make-string height ?\n) str)
(set-buffer-modified-p nil)
(twittering-load-private-info)))))
(defun twittering-save-private-info ()
(let* ((obj (mapcar (lambda (sym)
`(,sym . ,(symbol-value sym)))
twittering-variables-stored-with-encryption))
(str (with-output-to-string (pp obj)))
(file twittering-private-info-file))
(when (twittering-write-and-encrypt file str)
(set-file-modes file #o600)
(setq twittering-private-info-file-loaded t))))
(defun twittering-save-private-info-with-guide ()
(let ((str (concat
"Saving authorized access token for OAuth to "
(format "%s.\n" twittering-private-info-file)
"\n"
(propertize "Please input a master password twice."
'face 'bold))))
(with-temp-buffer
(switch-to-buffer (current-buffer))
(let* ((str-height (length (split-string str "\n")))
(height (max 0 (- (/ (- (window-text-height) 1) 2)
(/ str-height 2)))))
(insert (make-string height ?\n) str)
(set-buffer-modified-p nil)
(twittering-save-private-info)))))
(defun twittering-capable-of-encryption-p ()
(and (or (require 'epa nil t) (require 'alpaca nil t))
(executable-find "gpg")))
(eval-when-compile
(require 'epa nil t)
(require 'alpaca nil t))
(defun twittering-read-from-encrypted-file (file)
"Decrypt contents from FILE and return them.
Read encrypted contents from FILE and return the decrypted contents.
This function requires `epa' or `alpaca' library."
(cond
((not (file-readable-p file))
(error "Failed to read %s" file)
nil)
((require 'epa nil t)
(let ((context (epg-make-context epa-protocol))
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(epg-context-set-passphrase-callback
context #'epa-passphrase-callback-function)
(epg-context-set-progress-callback
context
(cons #'epa-progress-callback-function
(format "Decrypting %s..." (file-name-nondirectory file))))
(message "Decrypting %s..." (file-name-nondirectory file))
(condition-case err
(let ((full-path (expand-file-name file)))
;; `epg-decrypt-file' included in EasyPG 1.0.0, which is
;; distributed with Emacs 23.2, requires the expanded full path
;; as the argument CIPHER. This is because CIPHER is directly
;; used as an argument of the command `gpg'.
(epg-decrypt-file context full-path nil))
(error
(message "%s" (cdr err))
nil))))
((require 'alpaca nil t)
(with-temp-buffer
(let ((buffer-file-name (expand-file-name file))
(alpaca-regex-suffix ".*")
(coding-system-for-read 'binary)
(coding-system-for-write 'binary)
(temp-buffer (current-buffer))
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(insert-file-contents-literally file)
(set-buffer-modified-p nil)
(condition-case nil
(progn
(alpaca-after-find-file)
(if (eq temp-buffer (current-buffer))
(buffer-string)
;; `alpaca-after-find-file' kills the current buffer
;; if the decryption is failed.
nil))
(error
(when (eq temp-buffer (current-buffer))
(delete-region (point-min) (point-max)))
nil)))))
(t
nil)))
(defun twittering-write-and-encrypt (file str)
(cond
((require 'epg nil t)
(let ((context (epg-make-context epa-protocol))
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(epg-context-set-passphrase-callback
context #'epa-passphrase-callback-function)
(epg-context-set-progress-callback
context (cons #'epa-progress-callback-function "Encrypting..."))
(message "Encrypting...")
(condition-case err
(unwind-protect
;; In order to prevent `epa-file' to encrypt the file double,
;; `epa-file-name-regexp' is temorarily changed into the null
;; regexp that never matches any string.
(let ((epa-file-name-regexp "\\`\\'")
(coding-system-for-read 'binary)
(coding-system-for-write 'binary))
(when (fboundp 'epa-file-name-regexp-update)
(epa-file-name-regexp-update))
(with-temp-file file
(set-buffer-multibyte nil)
(delete-region (point-min) (point-max))
(insert (epg-encrypt-string context str nil))
(message "Encrypting...wrote %s" file)
t))
(when (fboundp 'epa-file-name-regexp-update)
(epa-file-name-regexp-update)))
(error
(message "%s" (cdr err))
nil))))
((require 'alpaca nil t)
;; Create the file.
;; This is required because `alpaca-save-buffer' checks its timestamp.
(with-temp-file file)
(with-temp-buffer
(let ((buffer-file-name file)
(coding-system-for-read 'binary)
(coding-system-for-write 'binary)
;; Bind `default-directory' to the temporary directory
;; because it is possible that the directory pointed by
;; `default-directory' has been already removed.
(default-directory temporary-file-directory))
(insert str)
(condition-case nil
(if (alpaca-save-buffer)
t
(delete-file file)
nil)
(error
(when (file-exists-p file)
(delete-file file))
nil)))))
(t
nil)))
(defun twittering-ensure-private-info ()
"Ensure that private information is loaded if necessary.
Return non-nil if `twittering-use-master-password' is nil or private
information has been already loaded. Also, return non-nil
if `twittering-use-master-password' is non-nil and this function succeeded
in loading private information.
Return nil if private information cannot be loaded."
(if (or (not twittering-use-master-password)
(twittering-private-info-loaded-p))
;; The private information is unnecessary or already loaded.
t
(cond
((not (twittering-capable-of-encryption-p))
(message "You need GnuPG and (EasyPG or alpaca.el) for master password!")
nil)
((and (memq twittering-auth-method '(oauth xauth))
(file-exists-p twittering-private-info-file))
(cond
((twittering-load-private-info-with-guide)
(setq twittering-private-info-file-loaded t)
(message "The authorized token is loaded.")
t)
(t
(message "Failed to load an authorized token from \"%s\"."
twittering-private-info-file)
nil)))
(t
;; The file for private infomation does not exist now.
t))))
;;;;
;;;; Asynchronous retrieval
;;;;
(defvar twittering-url-data-hash (make-hash-table :test 'equal))
(defvar twittering-url-request-list nil)
(defvar twittering-url-request-sentinel-hash (make-hash-table :test 'equal))
(defvar twittering-internal-url-queue nil)
(defvar twittering-url-request-resolving-p nil)
(defvar twittering-url-request-retry-limit 3)
(defvar twittering-url-request-sentinel-delay 1.0
"*Delay from completing retrieval to invoking associated sentinels.
Sentinels registered by `twittering-url-retrieve-async' will be invoked
after retrieval is completed and Emacs remains idle a certain time, which
this variable specifies. The unit is second.")
(defun twittering-remove-redundant-queries (queue)
(remove nil
(mapcar
(lambda (url)
(let ((current (gethash url twittering-url-data-hash)))
(when (or (null current)
(and (integerp current)
(< current twittering-url-request-retry-limit)))
url)))
(twittering-remove-duplicates queue))))
(defun twittering-resolve-url-request ()
"Resolve requests of asynchronous URL retrieval."
(when (null twittering-url-request-resolving-p)
(setq twittering-url-request-resolving-p t)
;; It is assumed that the following part is not processed
;; in parallel.
(setq twittering-internal-url-queue
(append twittering-internal-url-queue twittering-url-request-list))
(setq twittering-url-request-list nil)
(setq twittering-internal-url-queue
(twittering-remove-redundant-queries twittering-internal-url-queue))
(if (null twittering-internal-url-queue)
(setq twittering-url-request-resolving-p nil)
(let* ((url (car twittering-internal-url-queue))
(request (twittering-make-http-request-from-uri "GET" nil url))
(additional-info `((uri . ,url))))
(twittering-send-http-request
request additional-info
'twittering-url-retrieve-async-sentinel
'twittering-url-retrieve-async-clean-up-sentinel)))))
(defun twittering-url-retrieve-async-sentinel (proc status connection-info header-info)
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info)))
(uri (cdr (assq 'uri (assq 'request connection-info)))))
(when (string= status-code "200")
(let ((body (string-as-unibyte (buffer-string))))
(puthash uri body twittering-url-data-hash)
(setq twittering-internal-url-queue
(remove uri twittering-internal-url-queue))
(let ((sentinels (gethash uri twittering-url-request-sentinel-hash)))
(when sentinels
(remhash uri twittering-url-request-sentinel-hash))
(twittering-run-on-idle twittering-url-request-sentinel-delay
(lambda (sentinels uri body)
(mapc (lambda (func)
(funcall func uri body))
sentinels)
;; Resolve the rest of requests.
(setq twittering-url-request-resolving-p
nil)
(twittering-resolve-url-request))
sentinels uri body)
;; Without the following nil, it seems that the value of
;; `sentinels' is displayed.
nil)))))
(defun twittering-url-retrieve-async-clean-up-sentinel (proc status connection-info)
(when (memq status '(exit signal closed failed))
(let* ((uri (cdr (assq 'uri connection-info)))
(current (gethash uri twittering-url-data-hash)))
(when (or (null current) (integerp current))
;; Increment the counter on failure and then retry retrieval.
(puthash uri (1+ (or current 0)) twittering-url-data-hash)
(setq twittering-url-request-resolving-p nil)
(twittering-resolve-url-request)))))
(defun twittering-url-retrieve-async (url &optional sentinel)
"Retrieve URL asynchronously and call SENTINEL with the retrieved data.
The request is placed at the last of queries queue. When the data has been
retrieved and Emacs remains idle a certain time specified by
`twittering-url-request-sentinel-delay', SENTINEL will be called as
(funcall SENTINEL URL url-data).
The retrieved data can be referred as (gethash URL twittering-url-data-hash)."
(let ((data (gethash url twittering-url-data-hash)))
(cond
((or (null data) (integerp data))
(add-to-list 'twittering-url-request-list url t)
(when sentinel
(let ((current (gethash url twittering-url-request-sentinel-hash)))
(unless (member sentinel current)
(puthash url (cons sentinel current)
twittering-url-request-sentinel-hash))))
(twittering-resolve-url-request)
nil)
(t
;; URL has been already retrieved.
(twittering-run-on-idle twittering-url-request-sentinel-delay
sentinel url data)
data))))
;;;;
;;;; XML parser
;;;;
(defun twittering-ucs-to-char-internal (code-point)
;; Check (featurep 'unicode) is a workaround with navi2ch to avoid
;; error "error in process sentinel: Cannot open load file:
;; unicode".
;;
;; Details: navi2ch prior to 1.8.3 (which is currently last release
;; version as of 2010-01-18) always define `ucs-to-char' as autoload
;; file "unicode(.el)" (which came from Mule-UCS), hence it breaks
;; `ucs-to-char' under non Mule-UCS environment. The problem is
;; fixed in navi2ch dated 2010-01-16 or later, but not released yet.
(if (and (featurep 'unicode) (functionp 'ucs-to-char))
(ucs-to-char code-point)
;; Emacs21 have a partial support for UTF-8 text, so it can decode
;; only parts of a text with Japanese.
(decode-char 'ucs code-point)))
(defvar twittering-unicode-replacement-char
;; "Unicode Character 'REPLACEMENT CHARACTER' (U+FFFD)"
(or (twittering-ucs-to-char-internal #xFFFD)
??)
"*Replacement character returned by `twittering-ucs-to-char' when it fails
to decode a code.")
(defun twittering-ucs-to-char (code-point)
"Return a character specified by CODE-POINT in Unicode.
If it fails to decode the code, return `twittering-unicode-replacement-char'."
(or (twittering-ucs-to-char-internal code-point)
twittering-unicode-replacement-char))
(defadvice decode-char (after twittering-add-fail-over-to-decode-char)
(when (null ad-return-value)
(setq ad-return-value twittering-unicode-replacement-char)))
(defun twittering-xml-parse-region (&rest args)
"Wrapped `xml-parse-region' in order to avoid decoding errors.
After activating the advice `twittering-add-fail-over-to-decode-char',
`xml-parse-region' is called. This prevents `xml-parse-region' from
exiting abnormally by decoding unknown numeric character reference."
(let ((activated (ad-is-active 'decode-char)))
(ad-enable-advice
'decode-char 'after 'twittering-add-fail-over-to-decode-char)
(ad-activate 'decode-char)
(unwind-protect
(condition-case err
(apply 'xml-parse-region args)
(error
(message "Failed to parse the retrieved XML.")
nil))
(ad-disable-advice 'decode-char 'after
'twittering-add-fail-over-to-decode-char)
(if activated
(ad-activate 'decode-char)
(ad-deactivate 'decode-char)))))
;;;;
;;;; JSON parser with a fallback character
;;;;
(defun twittering-json-read (&rest args)
"Wrapped `json-read' in order to avoid decoding errors.
`json-read' is called after activating the advice
`twittering-add-fail-over-to-decode-char'.
This prevents `json-read' from exiting abnormally by decoding an unknown
numeric character reference."
(let ((activated (ad-is-active 'decode-char)))
(ad-enable-advice
'decode-char 'after 'twittering-add-fail-over-to-decode-char)
(ad-activate 'decode-char)
(unwind-protect
(condition-case err
(apply 'json-read args)
(error
(message "Failed to parse the retrieved JSON.")
nil))
(ad-disable-advice 'decode-char 'after
'twittering-add-fail-over-to-decode-char)
(if activated
(ad-activate 'decode-char)
(ad-deactivate 'decode-char)))))
;;;;
;;;; Window configuration
;;;;
(defun twittering-set-window-end (window pos)
(let* ((height (window-text-height window))
(n (- (- height 1))))
(while (progn (setq n (1+ n))
(set-window-start
window
(with-current-buffer (window-buffer window)
(save-excursion
(goto-char pos)
(line-beginning-position n))))
(not (pos-visible-in-window-p pos window))))))
(defun twittering-current-window-config (window-list)
"Return window parameters of WINDOW-LIST."
(mapcar (lambda (win)
(let ((start (window-start win))
(point (window-point win)))
`(,win ,start ,point)))
window-list))
(defun twittering-restore-window-config-after-modification (config beg end)
"Restore window parameters changed by modification on given region.
CONFIG is window parameters made by `twittering-current-window-config'.
BEG and END mean a region that had been modified."
(mapc (lambda (entry)
(let ((win (elt entry 0))
(start (elt entry 1))
(point (elt entry 2)))
(when (and (< beg start) (< start end))
(set-window-start win start))
(when (and (< beg point) (< point end))
(set-window-point win point))))
config))
(defun twittering-pop-to-buffer (buf)
"Select the buffer BUF in some window.
The behavior is determined by the function specified by
`twittering-pop-to-buffer-function'."
(funcall twittering-pop-to-buffer-function buf))
(defun twittering-pop-to-buffer-simple (buf)
"Select the buffer BUF by using `pop-to-buffer'."
(let ((win (selected-window)))
(pop-to-buffer buf)
;; This is required because the new window generated by `pop-to-buffer'
;; may hide the region following the current position.
(twittering-ensure-whole-of-status-is-visible win)))
(defun twittering-pop-to-buffer-in-current-window (buf &optional win)
"Select the buffer BUF in the window WIN by splitting it.
If WIN is nil, the selected window is splitted."
(let* ((win (or win (selected-window)))
(size
(let ((rest (- (window-height win) 15)))
(if (<= rest 3)
;; To avoid an error due to a too small window.
nil
rest)))
(new-win (split-window win size)))
(select-window new-win)
(switch-to-buffer buf)))
(defun twittering-pop-to-buffer-in-largest-window (buf)
"Select the buffer BUF in the largest window by splitting it."
(let ((win
(lexical-let ((max-area 0)
(largest-win nil))
(walk-windows
(lambda (win)
(let ((area (* (window-height win) (window-width win))))
(when (< max-area area)
(setq max-area area)
(setq largest-win win)))))
largest-win)))
(twittering-pop-to-buffer-in-current-window buf win)))
(defun twittering-pop-to-buffer-in-bottom-largest-window (buf)
"Select the buffer BUF in the window largest on bottom by splitting it."
(let* ((bottom-win-list
(lexical-let ((win-list '())
(max-bottom 0))
(walk-windows
(lambda (win)
(let ((bottom (nth 3 (window-edges win))))
(cond
((< max-bottom bottom)
(setq max-bottom bottom)
(setq win-list `(,win)))
((= max-bottom bottom)
(setq win-list (cons win win-list)))
(t
nil)))))
win-list))
(win
(lexical-let ((max-area 0)
(largest-win nil))
(mapc (lambda (win)
(let ((area (* (window-height win) (window-width win))))
(when (< max-area area)
(setq largest-win win)
(setq max-area area))))
bottom-win-list)
largest-win)))
(twittering-pop-to-buffer-in-current-window buf win)))
;;;;
;;;; URI shortening
;;;;
(defun twittering-tinyurl-get (longurl &optional service)
"Shorten LONGURL with the service specified by `twittering-tinyurl-service'."
(let* ((service (or service twittering-tinyurl-service))
(api (cdr (assq service twittering-tinyurl-services-map)))
(request-generator (when (listp api) (elt api 0)))
(post-process (when (listp api) (elt api 1)))
(encoded-url (twittering-percent-encode longurl))
(request
(cond
((stringp api)
(twittering-make-http-request-from-uri
"GET" nil (concat api encoded-url)))
((stringp request-generator)
(twittering-make-http-request-from-uri
"GET" nil (concat request-generator encoded-url)))
((functionp request-generator)
(funcall request-generator service longurl))
(t
(error "%s is invalid. try one of %s"
(symbol-name service)
(mapconcat (lambda (x) (symbol-name (car x)))
twittering-tinyurl-services-map ", "))
nil)))
(additional-info `((longurl . ,longurl))))
(cond
((null request)
(error "Failed to generate a HTTP request for shortening %s with %s"
longurl (symbol-name service))
nil)
(t
(lexical-let ((result 'queried))
(let ((proc
(twittering-send-http-request
request additional-info
(lambda (proc status connection-info header-info)
(let ((status-line (cdr (assq 'status-line header-info)))
(status-code (cdr (assq 'status-code header-info))))
(case-string
status-code
(("200")
(setq result (buffer-string))
nil)
(t
(setq result nil)
(format "Response: %s" status-line)))))
(lambda (proc status connection-info)
(when (and (not (twittering-process-alive-p proc))
(eq result 'queried))
(setq result nil))))))
(twittering-wait-while nil 0.1
(and (eq result 'queried)
(twittering-process-alive-p proc)))
(when (and (eq result 'queried)
(not (twittering-process-alive-p proc)))
;; If the process has been dead, wait a moment because
;; Emacs may be in the middle of evaluating the sentinel.
(twittering-wait-while 10 0.1
(eq result 'queried)
nil
;; Reset `result' on timeout.
(setq result nil))))
(let ((processed-result (if (and result (functionp post-process))
(funcall post-process service result)
result)))
(if processed-result
processed-result
(error "Failed to shorten a URL %s with %s"
longurl (symbol-name service))
nil)))))))
(defun twittering-tinyurl-replace-at-point ()
"Replace the url at point with a tiny version."
(interactive)
(let ((url-bounds (bounds-of-thing-at-point 'url)))
(when url-bounds
(let ((url (twittering-tinyurl-get (thing-at-point 'url))))
(when url
(save-restriction
(narrow-to-region (car url-bounds) (cdr url-bounds))
(delete-region (point-min) (point-max))
(insert url)))))))
(defun twittering-make-http-request-for-bitly (service longurl)
"Make a HTTP request for URL shortening service bit.ly or j.mp.
Before calling this, you have to configure `twittering-bitly-login' and
`twittering-bitly-api-key'."
(let* ((query-string
(mapconcat
(lambda (entry)
(concat (car entry) "=" (cdr entry)))
`(("login" . ,twittering-bitly-login)
("apiKey" . ,twittering-bitly-api-key)
("format" . "txt")
("longUrl" . ,(twittering-percent-encode longurl)))
"&"))
(prefix
(cdr (assq service '((bit.ly . "http://api.bit.ly/v3/shorten?")
(j.mp . "http://api.j.mp/v3/shorten?")))))
(uri (concat prefix query-string)))
(twittering-make-http-request-from-uri "GET" nil uri)))
;;;;
;;;; Timeline spec
;;;;
;;; Timeline spec as S-expression
;;; - (user USER): timeline of the user whose name is USER. USER is a string.
;;; - (list USER LIST):
;;; the list LIST of the user USER. LIST and USER are strings.
;;;
;;; - (direct_messages): received direct messages.
;;; - (direct_messages_sent): sent direct messages.
;;; - (favorites): favorites timeline for the current user.
;;; - (favorites USER): favorites timeline for the specified user.
;;; - (friends): friends timeline.
;;; - (home): home timeline.
;;; - (mentions): mentions timeline.
;;; mentions (status containing @username) for the authenticating user.
;;; - (public): public timeline.
;;; - (replies): replies.
;;; - (retweeted_by_me): retweets posted by the authenticating user.
;;; - (retweeted_by_user USER): retweets posted by the user.
;;; - (retweeted_to_me): retweets posted by the authenticating user's friends.
;;; - (retweeted_to_user USER): retweets posted to the user.
;;; - (retweets_of_me):
;;; tweets of the authenticated user that have been retweeted by others.
;;; - (single ID): the single tweet specified by ID.
;;;
;;; - (search STRING): the result of searching with query STRING.
;;;
;;; - (exclude-if FUNC SPEC):
;;; the same timeline as SPEC, except that it does not include tweets
;;; that FUNC returns non-nil for.
;;; - (exclude-re REGEXP-STRING SPEC):
;;; the same timeline as SPEC, except that it does not include tweets
;;; that matches the regular expression specified by REGEXP-STRING.
;;;
;;; - (merge SPEC1 SPEC2 ...): result of merging timelines SPEC1 SPEC2 ...
;;;
;;; Timeline spec string
;;;
;;; SPEC ::= PRIMARY | COMPOSITE
;;; PRIMARY ::= USER | LIST | DIRECT_MESSSAGES | DIRECT_MESSSAGES_SENT
;;; | FRIENDS | HOME | MENTIONS | PUBLIC | REPLIES
;;; | RETWEETED_BY_ME | RETWEETED_BY_USER
;;; | RETWEETED_TO_ME | RETWEETED_TO_USER | RETWEETS_OF_ME
;;; | SEARCH
;;; COMPOSITE ::= EXCLUDE-IF | EXCLUDE-RE | MERGE
;;;
;;; USER ::= /[a-zA-Z0-9_-]+/
;;; LIST ::= USER "/" LISTNAME
;;; LISTNAME ::= /[a-zA-Z0-9_-]+/
;;; DIRECT_MESSSAGES ::= ":direct_messages"
;;; DIRECT_MESSSAGES_SENT ::= ":direct_messages_sent"
;;; FAVORITES ::= ":favorites" | ":favorites/" USER
;;; FRIENDS ::= ":friends"
;;; HOME ::= ":home" | "~"
;;; MENTIONS ::= ":mentions"
;;; PUBLIC ::= ":public"
;;; REPLIES ::= ":replies" | "@"
;;; RETWEETED_BY_ME ::= ":retweeted_by_me"
;;; RETWEETED_BY_USER ::= ":retweeted_by_user/" USER
;;; RETWEETED_TO_ME ::= ":retweeted_to_me"
;;; RETWEETED_TO_USER ::= ":retweeted_to_user/" USER
;;; RETWEETS_OF_ME ::= ":retweets_of_me"
;;; SINGLE ::= ":single/" ID
;;; ID ::= /[0-9]+/
;;;
;;; SEARCH ::= ":search/" QUERY_STRING "/"
;;; QUERY_STRING ::= any string, where "/" is escaped by a backslash.
;;;
;;; EXCLUDE-IF ::= ":exclude-if/" FUNC "/" SPEC
;;; FUNC ::= LAMBDA EXPRESSION | SYMBOL
;;; EXCLUDE-RE ::= ":exclude-re/" REGEXP "/" SPEC
;;;
;;; MERGE ::= "(" MERGED_SPECS ")"
;;; MERGED_SPECS ::= SPEC | SPEC "+" MERGED_SPECS
;;;
(defvar twittering-regexp-hash
(let ((full-width-number-sign (twittering-ucs-to-char #xff03)))
;; Unicode Character 'FULLWIDTH NUMBER SIGN' (U+FF03)
(concat "\\(?:#\\|" (char-to-string full-width-number-sign) "\\)")))
(defvar twittering-regexp-atmark
(let ((full-width-commercial-at (twittering-ucs-to-char #xff20)))
;; Unicode Character 'FULLWIDTH COMMERCIAL AT' (U+FF20)
(concat "\\(?:@\\|" (char-to-string full-width-commercial-at) "\\)")))
(defun twittering-timeline-spec-to-string (timeline-spec &optional shorten)
"Convert TIMELINE-SPEC into a string.
If SHORTEN is non-nil, the abbreviated expression will be used."
(let ((type (car timeline-spec))
(value (cdr timeline-spec)))
(cond
;; user
((eq type 'user) (car value))
;; list
((eq type 'list) (concat (car value) "/" (cadr value)))
;; simple
((eq type 'direct_messages) ":direct_messages")
((eq type 'direct_messages_sent) ":direct_messages_sent")
((eq type 'favorites)
(if value
(concat ":favorites/" (car value))
":favorites"))
((eq type 'friends) ":friends")
((eq type 'home) (if shorten "~" ":home"))
((eq type 'mentions) ":mentions")
((eq type 'public) ":public")
((eq type 'replies) (if shorten "@" ":replies"))
((eq type 'retweeted_by_me) ":retweeted_by_me")
((eq type 'retweeted_by_user) (concat ":retweeted_by_user/" (car value)))
((eq type 'retweeted_to_me) ":retweeted_to_me")
((eq type 'retweeted_to_user) (concat ":retweeted_to_user/" (car value)))
((eq type 'retweets_of_me) ":retweets_of_me")
((eq type 'single) (concat ":single/" (car value)))
((eq type 'search)
(let ((query (car value)))
(concat ":search/"
(replace-regexp-in-string "\\(\\\\\\|/\\)" "\\\\\\1" query)
"/")))
;; composite
((eq type 'exclude-if)
(let ((func (car value))
(spec (cadr value))
(print-level nil))
(concat ":exclude-if/" (prin1-to-string func) "/"
(twittering-timeline-spec-to-string spec))))
((eq type 'exclude-re)
(let ((regexp-str (car value))
(spec (cadr value))
(print-level nil))
(concat ":exclude-re/"
(replace-regexp-in-string "/" "\\\\\/" regexp-str)
"/"
(twittering-timeline-spec-to-string spec))))
((eq type 'merge)
(concat "("
(mapconcat 'twittering-timeline-spec-to-string value "+")
")"))
(t
nil))))
(eval-and-compile
(defmacro twittering-make-user-timeline-spec-direct (user)
`(list 'user ,user))
(defmacro twittering-make-list-timeline-spec-direct (owner listname)
`(list 'list ,owner ,listname))
(defmacro twittering-make-hashtag-timeline-spec-direct (tag)
`(list 'search (concat "#" ,tag)))
(defmacro twittering-make-hashtag-timeline-spec-string-direct (tag)
`(concat "#" ,tag)))
(defun twittering-extract-timeline-spec (str &optional unresolved-aliases)
"Extract one timeline spec from STR.
Return cons of the spec and the rest string."
(cond
((null str)
(error "STR is nil")
nil)