Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

10672 lines (9785 sloc) 455.727 kB
;;; twittering-mode.el --- Major mode for Twitter -*- coding: utf-8 -*-
;; Copyright (C) 2007, 2009, 2010 Yuto Hayamizu.
;; 2008 Tsuyoshi CHO
;; 2010, 2011, 2012 William Xu
;; Author: Y. Hayamizu <y.hayamizu@gmail.com>
;; Tsuyoshi CHO <Tsuyoshi.CHO+develop@Gmail.com>
;; Alberto Garcia <agarcia@igalia.com>
;; William Xu <william.xwl@gmail.com>
;; Created: Sep 4, 2007
;; Version: HEAD
;; 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://code.nanigac.com/source/view/419
;; * update status for region
;;; Code:
(eval-when-compile (require 'cl))
(require 'json)
(require 'parse-time)
(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))
(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)))
(require 'url)
(require 'oauth2)
(defconst twittering-mode-version "HEAD")
(defun twittering-mode-version ()
"Display a message for twittering-mode version."
(interactive)
(let ((version-string
(format "twittering-mode-v%s" twittering-mode-version)))
(if (called-interactively-p 'interactive)
(message "%s" version-string)
version-string)))
;;;
;;; User Customizable
;;;
(defgroup twittering nil
"Twitter client."
:prefix "twittering-"
:group 'applications)
(defcustom 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'."
:type 'integer
:group 'twittering)
(defcustom 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."
:type 'integer
:group 'twittering)
(defcustom twittering-initial-timeline-spec-string '(":home@twitter")
"*The initial timeline spec strings."
:type 'list
:group 'twittering)
(defcustom twittering-format-thumbnail-prefix " "
"Thumbnail (like in sina weibo) prefix."
:type 'string
:group 'twittering)
(defcustom twittering-status-format
"%FACE[twittering-zebra-1-face,twittering-zebra-2-face]{%i %g %s, from %f%L%r%R:\n%FOLD[ ]{%t}\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)
%g - format %c using `gnus-user-date' (Note: this assumes you will not keep
latest statuses for more than a week)
%C{time-format-str} - created_at (formatted with time-format-str)
%@ - X seconds ago
%t - text filled as one paragraph
%' - truncated
%FACE[face-name]{...} - strings decorated with the specified face. You can
provide two faces, separated by colon, to create a
zebra-like background.
%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
"
:type 'string
:group 'twittering)
(defcustom twittering-my-status-format
"%FACE[twittering-zebra-1-face,twittering-zebra-2-face]{%g %s, from %f%L%r%R: %i\n%FOLD[]{%t}\n}"
"Specific format for my posts.
See `twittering-status-format'. "
:type 'string
:group 'twittering)
(defcustom twittering-retweet-format "RT: %t (via @%s)"
"Format string when retweeting.
Items:
%s - screen_name
%t - text
%% - %
"
:type 'string
:group 'twittering)
(defcustom twittering-fill-column nil
"*The fill-column used for \"%FILL{...}\" in `twittering-status-format'.
If nil, the fill-column is automatically calculated."
:type 'integer
:group 'twittering)
(defcustom twittering-my-fill-column nil
"Similar to `twittering-fill-column', specially for tweets sent by myself."
:type 'integer
:group 'twittering)
(defcustom 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."
:type 'symbol
:group 'twittering)
(defcustom 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."
:type 'symbol
:group 'twittering)
(defcustom 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.)"
:type 'symbol
:group 'twittering)
(defcustom twittering-notify-successful-http-get t
"Non-nil will notify successful http GET in minibuffer."
:type 'symbol
:group 'twittering)
(defcustom twittering-timeline-most-active-spec-strings '(":home" ":replies")
"See `twittering-timeline-spec-most-active-p'."
:type 'list
:group 'twittering)
(defcustom twittering-request-confirmation-on-posting nil
"*If *non-nil*, confirmation will be requested on posting a tweet edited in
pop-up buffer."
:type 'symbol
:group 'twittering)
(defcustom 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\" . \"(USER1+USER2+USER3)\")
(\"to_me\" . \"(:mentions+:retweets_of_me+:direct_messages)\")
(\"related-to\" .
,(lambda (username)
(if username
(format \":search/to:%s OR from:%s OR @%s/\"
username username username)
\":home\")))),
then you can use \"$to_me\" as
\"(:mentions+:retweets_of_me+:direct_messages)\"."
:type 'list
:group 'twittering)
(defcustom twittering-convert-fix-size 48
"Size of user icon.
When nil, don't convert, simply use original size.
Most default profile_image_url in status is already an
avatar(48x48). So normally we don't have to convert it at all."
:type 'number
:group 'twittering)
(defcustom twittering-new-tweets-count-excluding-me nil
"Non-nil will exclude my own tweets when counting received new tweets."
:type 'boolean
:group 'twittering)
(defcustom twittering-new-tweets-count-excluding-replies-in-home nil
"Non-nil will exclude replies in home timeline when counting received new
tweets."
:type 'boolean
:group 'twittering)
(defcustom twittering-need-to-be-updated-indicator " "
"A string indicating it is being updated.
The string should not be empty. "
:type 'string
:group 'twittering)
(defcustom twittering-curl-socks-proxy '()
"Socks parameters for curl session.
Don't use it together with http proxy. "
:type 'list
:group 'twittering)
(defcustom twittering-auto-adjust-fill-column? t
"If t, adjust `twittering-fill-column' and `twittering-my-fill-column'
automatically when window resizes. "
:type 'boolean
:group 'twittering)
(defcustom twittering-tweet-separator ""
"A string for separating tweets. "
:type 'string
:group 'twittering)
;;;
;;; Internal Variables
;;;
(defvar twittering-account-authorization '()
"Alist of state of account authorization for each service.
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.")
(defun twittering-get-account-authorization (&optional service)
(cadr (assq (or service (twittering-extract-service))
twittering-account-authorization)))
(defun twittering-update-account-authorization (auth)
(setq twittering-account-authorization
`((,(twittering-extract-service) ,auth)
,@(assq-delete-all twittering-service-method
twittering-account-authorization))))
(defvar twittering-oauth-invoke-browser nil
"*Whether to invoke a browser on authorization of access key automatically.")
(defconst twittering-max-number-of-tweets-on-retrieval 200
"The maximum number of `twittering-number-of-tweets-on-retrieval'.")
(defconst twittering-max-number-of-tweets-on-search 100
"The maximum number for search API: http://search.twitter.com/api/")
(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)))
(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-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-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-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-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-get-simple-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-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-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-scroll-mode nil)
(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-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-last-status-format ""
"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-last-status-format'.")
(defvar twittering-last-my-status-format "")
(defvar twittering-format-my-status-function nil)
(defvar twittering-timeline-data-table (make-hash-table :test 'equal))
(defface twittering-zebra-1-face `((t (:background "#e6e6fa"))) "" :group 'faces)
(defface twittering-zebra-2-face `((t (:background "white"))) "" :group 'faces)
(defface twittering-verify-face `((t (:inherit 'font-lock-variable-name-face)))
"Face for decorating the V symbol used by weibo.com. "
:group 'faces)
(defvar twittering-use-native-retweet nil
"Post retweets using native retweets if this variable is non-nil.
This is default value, you can also set it separately for each service in
`twittering-accounts', like (retweet organic) or (retweet native).")
(defvar twittering-update-status-function
'twittering-update-status-from-pop-up-buffer
"The function used to posting a tweet. It takes two arguments:
the first argument INIT-STR is initial text to be edited and the
second argument REPLY-TO-ID is a user ID of a tweet to which you
are going to reply.
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-invoke-buffer nil
"The buffer where we invoke `twittering-get-and-render-timeline'.
If we invoke `twittering-get-and-render-timeline' from a twittering buffer, then
do not display unread notifier on mode line.")
(defvar twittering-use-master-password nil
"*Whether to store private information encrypted with a master password.")
(defvar twittering-private-info-file
(expand-file-name "~/.emacs.d/twittering/.twittering.gpg")
"*File for storing encrypted private information when
`twittering-use-master-password' is non-nil.")
(defvar twittering-variables-stored-with-encryption
'(twittering-oauth-access-token-alist))
(defvar twittering-oauth-access-token-alist '())
;; buffer local, internal use.
(defvar twittering-service-method nil)
(defcustom twittering-service-method-table
`((twitter (api "api.twitter.com")
(search "search.twitter.com")
(web "twitter.com")
(stream "stream.twitter.com")
(userstream "userstream.twitter.com")
(api-prefix "1.1/")
(search-method "search")
(oauth-request-token-url-without-scheme
"://api.twitter.com/oauth/request_token")
(oauth-authorization-url-base-without-scheme
"://api.twitter.com/oauth/authorize?oauth_token=")
(oauth-access-token-url-without-scheme
"://api.twitter.com/oauth/access_token")
(oauth-consumer-key
,(base64-decode-string
"bzY1aXZsUXdoeUdQVmdCODVHVFln"))
(oauth-consumer-secret
,(base64-decode-string
"VjJBYldtYVN0ajFTejB5Q1NBWnBnSVdFOUNFOWtEb1MyaE16a294UVdN"))
(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))
(sina (api "api.weibo.com")
(web "weibo.com")
;; search is restricted by sina..
(api-prefix "2/")
(oauth-authorization-url-base-without-scheme
"://api.weibo.com/oauth2/authorize")
(oauth-access-token-url-without-scheme
"://api.weibo.com/oauth2/access_token")
(oauth-consumer-key
,(base64-decode-string "MTg4MjM3MzM0NQ=="))
(oauth-consumer-secret
,(base64-decode-string
"ZTYxOTM3NWI5N2MzOGFmMGNlZWUxMTg2MzNlODc5ZTM="))
(status-url twittering-get-status-url-sina)
(search-url twittering-get-search-url-twitter))
(douban (api "api.douban.com")
(web "www.douban.com")
,@(mapcar
(lambda (i)
`(,(car i) ,(concat "://www.douban.com/service/auth/" (cadr i))))
'((oauth-request-token-url-without-scheme "request_token")
(oauth-authorization-url-base-without-scheme "authorize?oauth_token=")
(oauth-access-token-url-without-scheme "access_token")))
(oauth-consumer-key
,(base64-decode-string "MDNhYjJiM2JiYjY1YWExYTBiMDE4MWIzMzIzMGZlNGU="))
(oauth-consumer-secret
,(base64-decode-string "MjE4OWUxYTdmYmE4ZTkxYw=="))
(status-url twittering-get-status-url-douban)
(search-url twittering-get-search-url-twitter))
(socialcast (status-url twittering-get-status-url-socialcast)
(search-url twittering-get-search-url-twitter)))
"A list of alist of service methods.
Following services are supported:
'twitter -- http://www.twitter.com
'statusnet -- http://status.net
'sina -- http://weibo.com
'douban -- http://www.douban.com
'socialcast -- your own socialcast web(http://www.socialcast.com/)"
:type 'list
:group 'twittering)
(defun twittering-lookup-service-method-table (attr)
"Lookup ATTR value for `twittering-service-method'."
(or (cadr (assq attr (assqref (twittering-extract-service)
twittering-service-method-table)))
""))
(defun twittering-lookup-oauth-access-token-alist ()
(assqref (twittering-extract-service) twittering-oauth-access-token-alist))
(defun twittering-update-oauth-access-token-alist (alist)
(setq twittering-oauth-access-token-alist
`((,(twittering-extract-service) ,@alist)
,@(assq-delete-all (twittering-extract-service)
twittering-oauth-access-token-alist))))
;; (retweet organic)) ; Default Retweet style: `native', `organic'.
(defcustom twittering-accounts '((twitter (ssl t))
(sina (ssl t))
(socialcast (ssl t)))
"Account settings per service.
((service-method
(auth oauth) ; Authentication method: `oauth', `basic'
(ssl nil) ; Use SSL connection: `nil', `t'
(quotation before) ; Where to place quotation: `before', `after'
(status-format STRING)
(my-status-format STRING)
;; Only necessary for `basic' auth.
(username \"FOO\")
(password \"PASSWORD\")
)
...)
How To Choose Authentication Methods
------------------------------------
The symbol `basic' means Basic Authentication. The symbol `oauth'means OAuth
Authentication. The symbol `xauth' means xAuth Authentication. OAuth
Authentication requires a consumer-key and a 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."
:type 'list
:group 'twittering-mode)
(defvar twittering-enabled-services nil)
(defun twittering-get-accounts (attr)
(let ((token-alist (twittering-lookup-oauth-access-token-alist)))
(or (cadr (assq attr (assqref (twittering-extract-service) twittering-accounts)))
(case attr
((auth) 'oauth)
((username) (or (assocref "screen_name" token-alist)
(assocref "user_id" token-alist)
(assocref "douban_user_id" token-alist)
(assqref 'uid (assocref "access-response" token-alist))))
((password) (assocref "password" token-alist))))))
(defvar twittering-accounts-internal '((douban (oauth . 1.0))
(twitter (oauth . 1.0)))
"Internal service wise configuration. ")
(defun twittering-get-accounts-internal (attr)
(assqref attr (assqref (twittering-extract-service)
twittering-accounts-internal)))
(defun twittering-update-accounts-internal (alist)
(let ((service (twittering-extract-service)))
(setq twittering-accounts-internal
`(,@(remove-if (lambda (i) (eq (car i) service))
twittering-accounts-internal)
(,service ,@alist)))))
;;; Macros
(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)))
;;; magic 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)))
;;; ============================================= 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))
;;;
;; 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 (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))))
(sha1 (concat opad
(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) "\""))
;; TODO, what to set? (xwl)
`(("realm" . ,(twittering-oauth-url-encode "http://127.0.0.1/"))
,@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)
,@(unless (string-match "douban.com" url)
`(("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-token-alist (url auth-str &optional post-body)
(let ((additional-info '((sync . t)))
(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)))
(let (result)
(twittering-send-http-request
request additional-info
(lambda (proc status connection-info header-info)
(let ((status-line (assqref 'status-line header-info))
(status-code (assqref '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
(format "Response: %s" status-line))))))
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 (assocref "oauth_token" request-token-alist))
(request-token-secret
(assocref "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))))
(when 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
(if (string-match "douban.com" access-token-url)
(unless (y-or-n-p "Have you allowed twittering-mode to access douban? ")
(error "Access request rejected"))
(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)))))))
(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)))
(defvar twittering-regexp-uri
"\\(https?://[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#]+\\)")
;;; ============================================= Timeline spec
;;;;
;; Timeline spec as S-expression
;; - ((SERVICE-METHOD) user USER): timeline of the user whose name is USER. USER is a string.
;; - ((SERVICE-METHOD) list USER LIST):
;; the list LIST of the user USER. LIST and USER are strings.
;; specially,
;; ((SERVICE-METHOD) list USER following): friends that USER is following.
;; ((SERVICE-METHOD) list USER followers): followers of USER.
;;
;; - ((SERVICE-METHOD) direct_messages): received direct messages.
;; - ((SERVICE-METHOD) direct_messages_sent): sent direct messages.
;; - ((SERVICE-METHOD) friends): friends timeline.
;; - ((SERVICE-METHOD) home): home timeline.
;; - ((SERVICE-METHOD) mentions): mentions timeline.
;; mentions ((SERVICE-METHOD) status containing @username) for the authenticating user.
;; - ((SERVICE-METHOD) public): public timeline.
;; - ((SERVICE-METHOD) replies): replies.
;; - ((SERVICE-METHOD) retweeted_by_me): retweets posted by the authenticating user.
;; - ((SERVICE-METHOD) retweeted_to_me): retweets posted by the authenticating user's friends.
;; - ((SERVICE-METHOD) retweets_of_me):
;; tweets of the authenticated user that have been retweeted by others.
;;
;; - ((SERVICE-METHOD) search STRING): the result of searching with query STRING.
;; - ((SERVICE-METHOD) merge SPEC1 SPEC2 ...): result of merging timelines SPEC1 SPEC2 ...
;; - ((SERVICE-METHOD) filter REGEXP SPEC): timeline filtered with REGEXP.
;;
;; Timeline spec string
;;
;; SPEC ::= {PRIMARY | COMPOSITE} "@" SERVICE-METHOD
;; PRIMARY ::= USER | LIST | DIRECT_MESSSAGES | DIRECT_MESSSAGES_SENT
;; | FRIENDS | HOME | MENTIONS | PUBLIC | REPLIES
;; | RETWEETED_BY_ME | RETWEETED_TO_ME | RETWEETS_OF_ME
;; | FOLLOWING | FOLLOWERS
;; | SEARCH
;; COMPOSITE ::= MERGE | FILTER
;;
;; USER ::= /[a-zA-Z0-9_-]+/
;; LIST ::= USER "/" LISTNAME
;; LISTNAME ::= /[a-zA-Z0-9_-]+/
;; DIRECT_MESSSAGES ::= ":direct_messages"
;; DIRECT_MESSSAGES_SENT ::= ":direct_messages_sent"
;; FRIENDS ::= ":friends"
;; HOME ::= ":home" | "~"
;; MENTIONS ::= ":mentions"
;; PUBLIC ::= ":public"
;; REPLIES ::= ":replies" | "@"
;; RETWEETED_BY_ME ::= ":retweeted_by_me"
;; RETWEETED_TO_ME ::= ":retweeted_to_me"
;; RETWEETS_OF_ME ::= ":retweets_of_me"
;; FOLLOWING ::= USER "/following"
;; FOLLOWERS ::= USER "/followers"
;;
;; SEARCH ::= ":search/" QUERY_STRING "/"
;; QUERY_STRING ::= any string, where "/" is escaped by a backslash.
;; MERGE ::= "(" MERGED_SPECS ")"
;; MERGED_SPECS ::= SPEC | SPEC "+" MERGED_SPECS
;; FILTER ::= ":filter/" REGEXP "/" SPEC
;;
(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."
(ignore-errors ; TODO, hack, TIMELINE-SPEC should be complete.
(unless (listp (car timeline-spec))
(setq timeline-spec `((,(twittering-extract-service)) ,@timeline-spec))))
(let* ((service (caar timeline-spec))
(timeline-spec (cdr timeline-spec))
(type (car timeline-spec))
(value (cdr timeline-spec)))
(format
"%s@%S"
(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 '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_to_me) ":retweeted_to_me")
((eq type 'retweets_of_me) ":retweets_of_me")
((eq type 'search)
(let ((query (car value)))
(concat ":search/"
(replace-regexp-in-string "/" "\\/" query nil t)
"/")))
;; composite
((eq type 'filter)
(let ((regexp (car value))
(spec (cadr value)))
(concat ":filter/"
(replace-regexp-in-string "/" "\\/" regexp nil t)
"/"
(twittering-timeline-spec-to-string spec))))
((eq type 'merge)
(concat "("
(mapconcat 'twittering-timeline-spec-to-string value "+")
")")))
service)))
(defun twittering-extract-timeline-spec (str &optional unresolved-aliases)
"Extract one timeline spec from STR.
Return cons of the spec and the rest string."
(let ((re "@\\([a-zA-Z0-9_-]+\\)")) ; service method
(cond
((null str)
(error "STR is nil"))
((string-match (concat "^\\([a-zA-Z0-9_-]+\\)/\\([a-zA-Z0-9_-]+\\)" re) str)
(let ((user (match-string 1 str))
(listname (match-string 2 str))
(service (intern (match-string 3 str)))
(rest (substring str (match-end 0))))
`(((,service) list ,user ,listname) . ,rest)))
((string-match (concat "^\\([a-zA-Z0-9_-]+\\)" re) str)
(let ((user (match-string 1 str))
(service (intern (match-string 2 str)))
(rest (substring str (match-end 0))))
`(((,service) user ,user) . ,rest)))
((string-match (concat "^~" re) str)
(let ((service (intern (match-string 1 str)))
(rest (substring str (match-end 0))))
`(((,service) home) . ,rest)))
;; Disable this as @ is used for separating timeline name and service
;; name. (xwl)
;; ((string-match (concat "^" twittering-regexp-atmark) str)
;; `((replies) . ,(substring str (match-end 0))))
((string-match (concat "^" twittering-regexp-hash "\\([a-zA-Z0-9_-]+\\)" re)
str)
(let* ((tag (match-string 1 str))
(service (intern (match-string 2 str)))
(query (concat "#" tag))
(rest (substring str (match-end 0))))
`(((,service) search ,query) . ,rest)))
((string-match (concat "^:\\([a-z_/-]+\\)" re) str)
(let ((type (match-string 1 str))
(service (intern (match-string 2 str)))
(following (substring str (match-end 0)))
(alist '(("direct_messages" direct_messages)
("direct_messages_sent" direct_messages_sent)
("friends" friends)
("home" home)
("mentions" mentions)
("public" public)
("replies" replies)
("retweeted_by_me" retweeted_by_me)
("retweeted_to_me" retweeted_to_me)
("retweets_of_me" retweets_of_me))))
(cond
((assoc type alist)
(let ((first-spec (assocref type alist)))
`(((,service) ,@first-spec) . ,following)))
((string-match (concat ;; "^:search/\\(\\(.*?[^\\]\\)??\\(\\\\\\\\\\)*\\)??/"
"^:search/\\([^/]+\\)/"
re)
str)
(let* ((escaped-query (or (match-string 1 str) ""))
(service (intern (match-string 2 str)))
(query (replace-regexp-in-string
"\\\\/" "/" escaped-query nil t))
(rest (substring str (match-end 0))))
(if (not (string= "" escaped-query))
`(((,service) search ,query) . ,rest)
(error "\"%s\" has no valid regexp" str)
nil)))
((string-match (concat "^:filter/\\(\\(.*?[^\\]\\)??\\(\\\\\\\\\\)*\\)??/" re)
str)
(let* ((escaped-regexp (or (match-string 1 str) ""))
(service (intern (match-string 3 str)))
(regexp (replace-regexp-in-string
"\\\\/" "/" escaped-regexp nil t))
(following (substring str (match-end 0)))
(pair (twittering-extract-timeline-spec
following unresolved-aliases))
(spec (car pair))
(rest (cdr pair)))
`(((,service) filter ,regexp ,spec) . ,rest)))
;; (error "\"%s\" has no valid regexp" str)
;; nil))
(t
(error "\"%s\" is invalid as a timeline spec" str)
nil))))
;; TODO, check you later. (xwl)
((string-match "^\\$\\([a-zA-Z0-9_-]+\\)\\(?:(\\([^)]*\\))\\)?" str)
(let* ((name (match-string 1 str))
(rest (substring str (match-end 0)))
(value (cdr-safe (assoc name twittering-timeline-spec-alias)))
(arg (match-string 2 str)))
(if (member name unresolved-aliases)
(error "Alias \"%s\" includes a recursive reference" name)
(cond
((stringp value)
(twittering-extract-timeline-spec
(concat value rest)
(cons name unresolved-aliases)))
((functionp value)
(twittering-extract-timeline-spec
(funcall value arg)
(cons name unresolved-aliases)))
(t
(error "Alias \"%s\" is undefined" name))))))
((string-match "^(" str)
(let ((rest (concat "+" (substring str (match-end 0))))
(result '()))
(while (and rest (string-match "^\\+" rest))
(let* ((spec-string (substring rest (match-end 0)))
(pair (twittering-extract-timeline-spec
spec-string unresolved-aliases))
(spec (car pair))
(next-rest (cdr pair)))
(setq result (cons spec result))
(setq rest next-rest)))
(if (and rest (string-match "^)" rest))
(let ((spec-list
(apply 'append
(mapcar (lambda (x) (if (eq 'merge (car x))
(cdr x)
(list x)))
(reverse result)))))
(if (= 1 (length spec-list))
`(,(car spec-list) . ,(substring rest 1))
`((merge ,@spec-list) . ,(substring rest 1))))
(if rest
;; The string following the opening parenthesis `('
;; can be interpreted without errors,
;; but there is no corresponding closing parenthesis.
(error "\"%s\" lacks a closing parenthesis" str))
;; Does not display additional error messages if an error
;; occurred on interpreting the string following
;; the opening parenthesis `('.
nil)))
;; (sina) Treat all chinese string as USER. Put this match at back.
((string-match (concat "^\\([^@]+\\)" re) str)
(let ((user (match-string 1 str))
(service (intern (match-string 2 str)))
(rest (substring str (match-end 0))))
`(((,service) user ,user) . ,rest)))
(t
(error "\"%s\" is invalid as a timeline spec" str)))))
(defun twittering-extract-service (&optional spec)
(let ((service (cond ((stringp spec)
(when (string-match "@\\(.+\\)" spec)
(intern (match-string 1 spec))))
((consp spec)
(when (consp (car spec))
(caar spec))))))
(unless service
(when (twittering-current-timeline-spec)
(setq service (caar (twittering-current-timeline-spec)))))
;; Fall back to twittering-service-method under `let'.
(unless service
(setq service twittering-service-method))
(unless service
(error "Null service!"))
service))
(defun twittering-string-to-timeline-spec (spec-str)
"Convert SPEC-STR into a timeline spec.
Return nil if SPEC-STR is invalid as a timeline spec."
(unless (string-match "@" spec-str)
(setq spec-str (format "%s@%S" spec-str (twittering-extract-service))))
(let ((result-pair (twittering-extract-timeline-spec spec-str)))
(when (and result-pair (string= "" (cdr result-pair)))
(car result-pair))))
(defun twittering-timeline-spec-primary-p (spec)
"Return non-nil if SPEC is a primary timeline spec.
`primary' means that the spec is not a composite timeline spec such as
`filter' and `merge'."
(let* ((spec (cdr spec))
(primary-spec-types
'(user list
direct_messages direct_messages_sent
friends home mentions public replies
search
retweeted_by_me retweeted_to_me retweets_of_me))
(type (car spec)))
(memq type primary-spec-types)))
(defun twittering-timeline-spec-user-p (spec)
"Return non-nil if SPEC is a user timeline spec."
(let ((spec (cdr spec)))
(and spec (eq (car spec) 'user))))
(defun twittering-timeline-spec-list-p (spec)
"Return non-nil if SPEC is a list timeline spec."
(let ((spec (cdr spec)))
(and spec (eq (car spec) 'list))))
(defun twittering-timeline-spec-direct-messages-p (spec)
"Return non-nil if SPEC is a timeline spec which is related of
direct_messages."
(let ((spec (cdr spec)))
(and spec
(memq (car spec) '(direct_messages direct_messages_sent)))))
(defun twittering-timeline-spec-user-methods-p (spec)
"Return non-nil if SPEC belongs to `User Methods' API."
(let ((spec (cdr spec)))
(and spec
(eq (car spec) 'list)
(member (car (last spec)) '("following" "followers")))))
(defun twittering-timeline-spec-most-active-p (spec)
"Return non-nil if SPEC is a very active timeline spec.
For less active spec, do not update it every
`twittering-timer-interval', rather, at the start of each hour.
Or we could easily exceed requests limit of Twitter API,
currently 150/hour. SPEC is such as '(home). The complete list
is specified in `twittering-timeline-most-active-spec-strings'."
(and spec
(string-match
(regexp-opt twittering-timeline-most-active-spec-strings)
(twittering-timeline-spec-to-string spec))))
(defun twittering-equal-string-as-timeline (spec-str1 spec-str2)
"Return non-nil if SPEC-STR1 equals SPEC-STR2 as a timeline spec."
(when (and (stringp spec-str1) (stringp spec-str2))
(let ((spec1 (twittering-string-to-timeline-spec spec-str1))
(spec2 (twittering-string-to-timeline-spec spec-str2)))
(equal spec1 spec2))))
;;;;
;;;; Retrieved statuses (timeline data)
;;;;
(defun twittering-current-timeline-id-table (&optional spec)
(let ((spec (or spec (twittering-current-timeline-spec))))
(if spec
(elt (gethash spec twittering-timeline-data-table) 0)
nil)))
(defun twittering-current-timeline-referring-id-table (&optional spec)
"Return the hash from a ID to the ID of the first observed status
referring the former ID."
(let ((spec (or spec (twittering-current-timeline-spec))))
(if spec
(elt (gethash spec twittering-timeline-data-table) 1)
nil)))
(defun twittering-current-timeline-data (&optional spec)
(let ((spec (or spec (twittering-current-timeline-spec))))
(if spec
(elt (gethash spec twittering-timeline-data-table) 2)
nil)))
(defun twittering-remove-timeline-data (&optional spec)
(let ((spec (or spec (twittering-current-timeline-spec))))
(remhash spec twittering-timeline-data-table)))
(defun twittering-find-status (id)
(let ((result nil))
(maphash
(lambda (spec pair)
(let* ((id-table (car pair))
(entry (gethash id id-table)))
;; Take the most detailed status.
(when (and entry
(or (null result) (< (length result) (length entry))))
(setq result entry))))
twittering-timeline-data-table)
result))
(defun twittering-delete-status-from-data-table (id)
(let ((modified-spec nil))
(maphash
(lambda (spec data)
(let* ((id-table (elt data 0))
(referring-id-table (elt data 1))
(timeline-data (elt data 2))
(status (gethash id id-table)))
(when status
(remhash id id-table)
;; Here, `referring-id-table' is not modified.
;; Therefore, the retweet observed secondly will not appear even
;; if the retweet observed first for the same tweet is deleted.
(setq modified-spec
(cons `(,spec
,id-table
,referring-id-table
,(remove status timeline-data))
modified-spec)))))
twittering-timeline-data-table)
(mapc
(lambda (data)
(let* ((spec (car data))
(buffer (twittering-get-buffer-from-spec spec)))
(puthash spec (cdr data) twittering-timeline-data-table)
(when (buffer-live-p buffer)
(with-current-buffer buffer
(save-excursion
(twittering-for-each-property-region
'id
(lambda (beg end value)
(when (twittering-status-id= id value)
(let ((buffer-read-only nil)
(separator-pos (min (point-max) (1+ end))))
(delete-region beg separator-pos)
(goto-char beg))))
buffer))))))
modified-spec)))
(defun twittering-get-replied-statuses (id &optional count)
"Return a list of replied statuses starting from the status specified by ID.
Statuses are stored in ascending-order with respect to their IDs."
(let ((result nil)
(status (twittering-find-status id)))
(case twittering-service-method
((sina)
(maphash
(lambda (spec pair)
(let ((id-table (car pair)))
(maphash
(lambda (i entry)
(when (and (assqref 'status entry)
(equal (assqref 'id (assqref 'status entry)) id))
;; Only show comment.
(let ((st (assqref 'status entry)))
(when st
(setq entry `((hide-status ,@st)
,@(remove-if (lambda (i) (eq (car i) 'status))
entry)))))
(let ((rc (assqref 'reply-comment entry)))
(when rc
(setq entry `((hide-reply-comment ,@rc)
,@(remove-if (lambda (i) (eq (car i) 'reply-comment))
entry)))))
(setq result (cons entry result))))
id-table)))
twittering-timeline-data-table))
(t
(while
(and (if (numberp count)
(<= 0 (setq count (1- count)))
t)
(let ((replied-id (or (assqref 'in-reply-to-status-id status) "")))
(unless (string= "" replied-id)
(let ((replied-status (twittering-find-status replied-id)))
(when replied-status
(setq result (cons replied-status result))
(setq status replied-status)
t))))))))
result))
(defun twittering-have-replied-statuses-p (id)
(let ((status (twittering-find-status id)))
(when status
(case twittering-service-method
((sina)
(let ((end (or (twittering-get-next-status-head) (point-max)))
(pos (point))
has?)
(save-excursion
(while (and (not has?)
pos
(setq pos (next-single-property-change
pos 'need-to-be-updated nil end)))
(goto-char pos)
(when (looking-at "轉發.* 評論(\\([0-9]+\\))")
(setq has? (match-string 1)))
(when (eq pos end)
(setq pos nil))))
has?))
(t
(let ((replied-id (assqref 'in-reply-to-status-id status)))
(and replied-id (not (string= "" replied-id)))))))))
(defun twittering-add-statuses-to-timeline-data (statuses &optional spec)
(let* ((spec (or spec (twittering-current-timeline-spec)))
(id-table
(or (twittering-current-timeline-id-table spec)
(make-hash-table :test 'equal)))
(referring-id-table
(or (twittering-current-timeline-referring-id-table spec)
(make-hash-table :test 'equal)))
(timeline-data (twittering-current-timeline-data spec)))
(let ((new-statuses
(remove nil
(mapcar
(lambda (status)
(ignore-errors
(let ((id (assqref 'id status))
(retweeted-id
(assqref 'id (assqref 'retweeted-status status))))
(unless (or (not retweeted-id)
(gethash retweeted-id referring-id-table))
;; Store the id of the first observed tweet
;; that refers `retweeted-id'.
(puthash retweeted-id id referring-id-table))
(unless (gethash id id-table)
(puthash id status id-table)
(puthash id id referring-id-table)
`((source-spec . ,spec)
,@status)))))
statuses))))
(when new-statuses
(puthash spec `(,id-table
,referring-id-table
;; Decreasingly by `id' except `followers', which is
;; sorted by recency one starts following me.
,(append new-statuses timeline-data))
twittering-timeline-data-table)
(when (twittering-jojo-mode-p spec)
(mapc (lambda (status)
(twittering-update-jojo (assqref 'screen-name (assqref 'user status))
(assqref 'text status)))
new-statuses))
(let* ((twittering-new-tweets-spec spec)
(twittering-new-tweets-statuses new-statuses)
(spec-string (twittering-timeline-spec-to-string spec))
(twittering-new-tweets-count
(if (twittering-timeline-spec-user-methods-p spec)
(twittering-count-unread-for-user-methods spec statuses)
(count-if (lambda (st) (twittering-is-unread-status-p st spec))
new-statuses))))
(let ((latest
(if (twittering-timeline-spec-user-methods-p spec)
(assqref 'screen-name (assqref 'user (car statuses)))
(assqref 'id (car new-statuses)))))
(setq twittering-cache-lastest-statuses
`((,spec-string . ,latest)
,@(remove-if (lambda (entry) (equal spec-string (car entry)))
twittering-cache-lastest-statuses))))
(run-hooks 'twittering-new-tweets-hook)
(if (and (twittering-timeline-spec-user-methods-p spec)
;; Insert all when buffer is empty.
(> (buffer-size) 0))
(twittering-take twittering-new-tweets-count statuses)
new-statuses))))))
(defcustom twittering-status-filter nil
"Filter for whether to show a status in timeline or not.
It will be called with one argument -- `status', only when it returns t, will
the status be shown. ")
(defun twittering-timeline-data-collect (&optional spec timeline-data)
"Collect visible statuses for `twittering-render-timeline'."
(let* ((spec (or spec (twittering-current-timeline-spec)))
(service (twittering-extract-service spec))
(referring-id-table
(twittering-current-timeline-referring-id-table spec))
(timeline-data
(or timeline-data (twittering-current-timeline-data spec))))
(remove
nil
(mapcar
(lambda (status)
(let ((id (assqref 'id status))
(is-retweeting (twittering-status-has-quotation? status))
(retweeted-id (assqref 'id (assqref 'retweeted-status status))))
(when (and (or (twittering-timeline-spec-user-methods-p spec)
(if is-retweeting
(and (twittering-status-id=
id (gethash retweeted-id referring-id-table)))
t)
(eq (twittering-extract-service spec) 'sina))
(or (not twittering-status-filter)
(funcall twittering-status-filter status))
(not (assqref 'twittering-reply? status))
;; filter sina ad tweet
(not (and (eq service 'sina)
(twittering-timeline-spec-user-p spec)
(not (member (assqref 'screen-name (assqref 'user status))
(twittering-friends))))))
status)))
timeline-data))))
(defun twittering-timeline-data-is-previous-p (timeline-data)
"Are TIMELINE-DATA previous statuses?
This is done by comparing statues in current buffer with TIMELINE-DATA."
(let ((status (car timeline-data)))
(if (twittering-timeline-spec-user-methods-p
(twittering-current-timeline-spec))
(let* ((previous-cursor (cdr-safe (assq 'previous-cursor status)))
(new-follower-p (string= previous-cursor "0")))
(not new-follower-p))
(let* ((buf-id (get-text-property
(twittering-get-current-status-head
(if twittering-reverse-mode (point-min) (point-max)))
'id))
(id (assqref 'id status)))
(and buf-id (twittering-status-id< id buf-id))))))
(defun twittering-is-unread-status-p (status &optional spec)
(let ((spec-string
(twittering-timeline-spec-to-string
(or spec (setq spec (twittering-current-timeline-spec))))))
(cond
((or (and twittering-new-tweets-count-excluding-me
(twittering-my-status-p status)
(not (equal spec '(retweets_of_me))))
(and twittering-new-tweets-count-excluding-replies-in-home
(equal spec '(home))
(twittering-is-replies-p status)))
nil)
(t
(twittering-status-id<
(assocref spec-string twittering-cache-lastest-statuses)
(assqref 'id status))))))
(defun twittering-count-unread-for-user-methods (spec new-statuses)
(let ((latest-username
(or (with-current-buffer (twittering-get-buffer-from-spec spec)
(goto-char (funcall (if twittering-reverse-mode 'point-max 'point-min)))
(get-text-property (twittering-get-current-status-head) 'username))
(let ((spec-string (twittering-timeline-spec-to-string spec)))
(assocref spec-string twittering-cache-lastest-statuses)))))
(if (not latest-username)
(length new-statuses)
(let ((statuses new-statuses)
(count 0))
(while (and statuses
(not (string= latest-username
(assqref 'screen-name (assqref 'user (car statuses))))))
(setq count (1+ count)
statuses (cdr statuses)))
count))))
;;;;
;;; ============================================= HTTP
;;;; 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)
(assqref candidate table)))
(entry (if (assq 'display-name entry)
entry
(cons `(display-name . ,(symbol-name candidate))
entry)))
(validate (lambda (item)
(let ((v (assqref item entry)))
(or (null v) (eq t v) (functionp v)))))
(confirm (lambda (item)
(let ((v (assqref 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."
(assqref '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-get-accounts 'ssl) order table)))
(assqref '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 (twittering-get-accounts '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? "))
(twittering-update-mode-line)
(twittering-ensure-connection-method order table)
t)
(t
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 (assqref 'path parts-alist)))
(if (string-match "\\`\\(.*\\)\\?" path)
(match-string 1 path)
path)))
(query-string (let ((path (assqref 'path parts-alist)))
(if (string-match "\\?\\(.*\\)\\'" path)
(match-string 1 path)
nil))))
(twittering-make-http-request method header-list
(assqref 'host parts-alist)
(assqref 'port parts-alist)
path
query-string
post-body
(string= "https"
(assqref '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-fullpath: the full-path of the certificate authorizing a server
certificate on SSL.
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 (assqref 'scheme request))
(use-ssl (string= "https" scheme))
(use-proxy (twittering-proxy-use-match (assqref 'host request)))
(entry (twittering-lookup-connection-type use-ssl order table)))
`((use-ssl . ,use-ssl)
(allow-insecure-server-cert
. ,twittering-allow-insecure-server-cert)
(cacert-fullpath
. ,(when use-ssl (twittering-ensure-ca-cert)))
(use-proxy . ,use-proxy)
,@(when use-proxy
`((proxy-server . ,(twittering-proxy-info scheme 'server))
(proxy-port . ,(twittering-proxy-info scheme 'port))
(proxy-user . ,(if use-ssl
twittering-https-proxy-user
twittering-http-proxy-user))
(proxy-password . ,(if use-ssl
twittering-https-proxy-password
twittering-http-proxy-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
(let (end prev)
(goto-char (point-min))
(when (search-forward-regexp "\r?\n\r?\n" nil t 1)
(setq end (match-end 0))
;; Remove 302 redirection.
(while (search-backward-regexp "\\`HTTP/1\.[01] \\(302 \\|200 Connection established\\)" nil t 1)
(setq prev (buffer-substring (point-min) end))
(delete-region (point-min) end)
(setq end nil)
(when (search-forward-regexp "\r?\n\r?\n" nil t 1)
(setq end (match-end 0))))
(if end
(buffer-substring (point-min) end)
prev))))))
(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-remove-response-header ()
(goto-char (point-min))
(let ((end (point-min))
(inhibit-read-only t))
(while (search-forward-regexp "^\\(HTTP/1\.[01]\\) \\([0-9][0-9][0-9]\\)" nil t)
(setq end (search-forward-regexp "\r?\n\r?\n" nil t 1)))
(delete-region (point-min) end)))
(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 (assqref 'send-http-request connection-info))
(stream? (assqref 'stream connection-info))
(proc-name (if stream?
(format "*twmode-stream-%s*" (assqref 'timeline-spec-string connection-info))
"*twmode-generic*"))
(temp-buffer (unless stream? (generate-new-buffer "*twmode-http-buffer*"))))
(when (and func (functionp func))
(funcall func proc-name 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))))))))
(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.
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-service-method twittering-service-method)
(sync? (assqref 'sync additional-info))
(info "(twmode) Retrieving..."))
(when sync?
(message info))
(twittering-send-http-request-internal
request additional-info
(lambda (proc status-code-or-str connection-info)
(let ((status (cond
(sync? status-code-or-str)
((string= status-code-or-str "urllib-finished") 'exit)
((processp proc) (process-status proc))
(t nil)))
(pre-process-func (assqref 'pre-process-buffer connection-info))
(buffer (if sync? (current-buffer) (process-buffer proc)))
(mes 'unset)
(error? t))
(unwind-protect
(progn
(unless sync?
(let ((exit-status (cond
((string= status-code-or-str "urllib-finished") 0)
((processp proc) (process-exit-status proc))
(t 1)))
(command (process-command proc)))
(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 mes))))) ; Leave it unchanged
(when (or sync? (eq mes 'unset))
(setq error? nil)
(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
(twittering-remove-response-header)
(setq mes (apply func proc status connection-info header-info nil))))))
;; unwind-forms
(when (not error?)
(when (and sync? (not (stringp mes)))
(setq mes (concat info "done")))
;; CLEAN-UP-FUNC can overwrite a message from the return value
;; of FUNC.
(when (stringp mes)
(message "%s" mes)))
(when (functionp clean-up-func)
(funcall clean-up-func proc status connection-info))
(when (and (or sync?
(memq status '(exit signal closed failed)))
(buffer-live-p buffer)
;; (not twittering-debug-mode)
)
(kill-buffer buffer))
(when error?
(unless (assqref 'stream connection-info)
(error "%s" mes)))))))))
;;;;
;;;; 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 (assqref 'request connection-info))
(method (assqref 'method request))
(scheme (assqref 'scheme request))
(host (assqref 'host request))
(port (assqref 'port request))
(path (assqref 'path request))
(query-string (assqref 'query-string request))
(header-list (assqref 'header-list request))
(post-body (assqref 'post-body request))
(use-proxy (assqref 'use-proxy connection-info))
(proxy-server (assqref 'proxy-server connection-info))
(proxy-port (assqref 'proxy-port connection-info))
(proxy-user (assqref 'proxy-user connection-info))
(proxy-password (assqref 'proxy-password connection-info))
(use-ssl (assqref 'use-ssl connection-info))
(allow-insecure-server-cert
(assqref 'allow-insecure-server-cert connection-info))
(cacert-fullpath (assqref 'cacert-fullpath connection-info))
(cacert-dir (when cacert-fullpath
(file-name-directory cacert-fullpath)))
(cacert-filename (when cacert-fullpath
(file-name-nondirectory cacert-fullpath)))
(proxy-info
(when (twittering-proxy-use-match host)
(twittering-proxy-info scheme)))
(connect-host (if proxy-info
(assqref 'server proxy-info)
host))
(connect-port (if proxy-info
(assqref 'port proxy-info)
port))
(request-str
(format "%s %s%s HTTP/1.1\r\n%s\r\n\r\n%s\r\n"
method path
(if 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)
(set-process-sentinel proc sentinel)
(when (assqref 'stream connection-info)
(set-process-filter proc 'twittering-stream-filter))
(process-send-string proc request-str)
(when (assqref 'sync connection-info)
(while (let ((status (process-status proc)))
(and (memq status '(run stop open listen connect))
(not (memq status '(exit signal closed failed)))))
(sit-for 0.1)))
proc)))
(defun twittering-pre-process-buffer-native (proc buffer connection-info)
(let ((use-ssl (assqref '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
(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 (assqref 'request connection-info))
(method (assqref 'method request))
(uri (assqref 'uri request))
(header-list (assqref 'header-list request))
(post-body (assqref 'post-body request))
(use-proxy (assqref 'use-proxy connection-info))
(proxy-server (assqref 'proxy-server connection-info))
(proxy-port (assqref 'proxy-port connection-info))
(proxy-user (assqref 'proxy-user connection-info))
(proxy-password (assqref 'proxy-password connection-info))
(use-ssl (assqref 'use-ssl connection-info))
(allow-insecure-server-cert
(assqref 'allow-insecure-server-cert connection-info))
(cacert-fullpath (assqref 'cacert-fullpath connection-info))
(cacert-dir (when cacert-fullpath
(file-name-directory cacert-fullpath)))
(cacert-filename (when cacert-fullpath
(file-name-nondirectory cacert-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"
"--location"
"--request" ,method
,@(unless twittering-debug-curl '("--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-filename))
,@(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)))))
;; ad-hoc
,@(when (and twittering-curl-socks-proxy
(string-match twittering-uri-regexp-to-proxy uri))
twittering-curl-socks-proxy)
,@(when (string= "POST" method)
(let ((opt
(if (twittering-is-uploading-file-p post-body)
"-F"
"-d")))
;; (or (mapcan (lambda (pair)
;; (let ((n (car pair))
;; (v (cdr pair)))
;; (when (string= opt "-d")
;; (setq n (twittering-percent-encode n)
;; v (twittering-percent-encode v)))
;; (list opt (format "%s=%s" n v))))
;; post-body)
;; Even if no data to post.. or it will fail for favorite,
;; retweet, etc. This is to ensure curl will use POST?
`(,opt ,(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-dir
default-directory))
proc)
(debug-printf "curl args: %S" curl-args)
(if (or (assqref 'sync connection-info) twittering-debug-curl)
(with-current-buffer buffer
(when twittering-debug-curl
(switch-to-buffer buffer))
(let ((status
(apply 'call-process twittering-curl-program nil t nil curl-args)))
(when sentinel
(funcall sentinel nil status))))
(setq proc (apply 'start-process name buffer
twittering-curl-program curl-args))
(when (and proc (functionp sentinel))
(when (assqref 'stream connection-info)
(set-process-filter proc 'twittering-stream-filter))
(set-process-sentinel proc sentinel))
proc)))
(defun twittering-pre-process-buffer-curl (proc buffer connection-info)
(let ((use-ssl (assqref 'use-ssl connection-info))
(use-proxy (assqref '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 (assqref 'request connection-info))
(method (assqref 'method request))
(scheme (assqref 'scheme request))
(uri (assqref 'uri request))
(header-list (assqref 'header-list request))
(post-body (assqref 'post-body request))
(use-proxy (assqref 'use-proxy connection-info))
(proxy-server (assqref 'proxy-server connection-info))
(proxy-port (assqref 'proxy-port connection-info))
(proxy-user (assqref 'proxy-user connection-info))
(proxy-password (assqref 'proxy-password connection-info))
(use-ssl (assqref 'use-ssl connection-info))
(allow-insecure-server-cert
(assqref 'allow-insecure-server-cert connection-info))
(cacert-fullpath (assqref 'cacert-fullpath connection-info))
(cacert-dir (when cacert-fullpath
(file-name-directory cacert-fullpath)))
(cacert-filename (when cacert-fullpath
(file-name-nondirectory cacert-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-filename)))
,@(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-dir
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))
proc)
(if (assqref 'sync connection-info)
(with-current-buffer buffer
(let ((status
(apply 'call-process twittering-wget-program nil t nil args)))
(when sentinel
(funcall sentinel nil status))))
(setq proc (apply 'start-process name buffer
twittering-wget-program args))
(when (and proc (functionp sentinel))
(when (assqref 'stream connection-info)
(set-process-filter proc 'twittering-stream-filter))
(set-process-sentinel proc sentinel))
proc)))
(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 (assqref 'request connection-info))
(method (assqref 'method request))
(scheme (assqref 'scheme request))
(uri (assqref 'uri request))
(header-list (assqref 'header-list request))
(post-body (assqref 'post-body request))
(use-proxy (assqref 'use-proxy connection-info))
(proxy-server (assqref 'proxy-server connection-info))
(proxy-port (assqref 'proxy-port connection-info))
(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)))
header-list)))
(url-request-data post-body)
(url-show-status twittering-url-show-status)
(url-http-attempt-keepalives (assqref 'stream connection-info))
(tls-program twittering-tls-program)
(coding-system-for-read 'binary)
(coding-system-for-write 'binary))
(lexical-let ((sentinel sentinel)
(buffer buffer))
(let ((callback
(lambda (&rest args)
(let ((proc url-http-process)
(url-buffer (current-buffer))
(status-str "urllib-finished")
;; TODO: necessary?
;; (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))))))
result-buffer)
(if (assqref 'sync connection-info)
(with-current-buffer (url-retrieve-synchronously uri)
(when sentinel
(funcall sentinel nil 'exit)))
(url-retrieve uri callback)
(when (buffer-live-p result-buffer)
(when (assqref 'stream connection-info)
(set-process-filter (get-buffer-process result-buffer)
'twittering-stream-filter))
(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"
(if (eq (twittering-extract-service) 'douban)
"application/atom+xml"
"text/plain"))
headers))
;; This makes update-profile-image fail.
;; (when (string= "POST" method)
;; (push (cons "Content-Length" "0") headers)
;; (push (cons "Content-Type" "text/plain") headers))
(when twittering-proxy-use
(let* ((scheme (if (twittering-get-accounts 'ssl) "https" "http"))
(keep-alive (twittering-proxy-info scheme 'keep-alive))
(user (twittering-proxy-info scheme 'user))
(password (twittering-proxy-info scheme 'password)))
(when (twittering-proxy-info scheme 'keep-alive)
(push (cons "Proxy-Connection" "Keep-Alive")
headers))
(when (and user password)
(push (cons
"Proxy-Authorization"
(concat "Basic "
(base64-encode-string (concat user ":" password))))
headers))))
headers
))
(defun twittering-add-application-header-to-http-request (request)
(let* ((method (assqref 'method request))
(auth-str
(cond
((eq (twittering-get-accounts 'auth) 'basic)
(concat "Basic "
(base64-encode-string
(concat (twittering-get-accounts 'username)
":" (twittering-get-accounts 'password)))))
((memq (twittering-get-accounts 'auth) '(oauth xauth))
(let ((access-token
(assocref "oauth_token" (twittering-lookup-oauth-access-token-alist)))
(access-token-secret
(assocref "oauth_token_secret" (twittering-lookup-oauth-access-token-alist))))
(twittering-oauth-auth-str-access
method
(assqref 'uri-without-query request)
(assqref 'encoded-query-alist request)
(twittering-lookup-service-method-table 'oauth-consumer-key)
(twittering-lookup-service-method-table '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 buffer)
"Return an error message generated from HEADER-INFO and BUFFER.
HEADER-INFO must be an alist generated by `twittering-get-response-header'.
BUFFER must be a HTTP response body, which includes error messages from
the server when the HTTP status code equals to 400 or 403."
(let ((status-line (assqref 'status-line header-info))
(status-code (assqref 'status-code header-info)))
;; http://dev.twitter.com/pages/responses_errors
(when (buffer-live-p buffer)
(let ((error-msg (ignore-errors (assqref 'error (twittering-construct-statuses)))))
(if error-msg
(format "%s (%s)" status-line error-msg)
status-line)))))
(defun twittering-http-get (host method &optional parameters additional-info sentinel clean-up-sentinel)
(let* ((service (twittering-extract-service))
(sentinel (or sentinel 'twittering-http-get-default-sentinel))
(path (concat "/" method (if (eq service 'douban) "" ".json")))
(request
(if (not (equal (twittering-get-accounts-internal 'oauth) 1.0))
(twittering-make-http-request
"GET" nil host nil path `(,@parameters ,(assoc "access_token" (twittering-lookup-oauth-access-token-alist)))
"" (twittering-get-accounts 'ssl))
(twittering-add-application-header-to-http-request
(twittering-make-http-request
"GET" nil host nil path parameters "" (twittering-get-accounts 'ssl))))))
(twittering-send-http-request
request additional-info sentinel clean-up-sentinel)))
(defvar twittering-oauth2-wait-user-for-reverifying nil)
(defun twittering-http-get-default-sentinel (proc status connection-info header-info)
(let ((status-line (assqref 'status-line header-info))
(status-code (assqref 'status-code header-info))
(spec (assqref 'timeline-spec connection-info))
(spec-string (assqref 'timeline-spec-string connection-info)))
(cond
((string= status-code "200")
(debug-printf "connection-info=%s" connection-info)
(let ((statuses (twittering-construct-statuses)))
(ignore-errors
(when (assqref 'id (car statuses))
;; Are we are fetching replies?
(when (eq (assqref 'command connection-info) 'show)
(setq statuses
(mapcar (lambda (st) `(,@st (twittering-reply? . t)))
statuses)))
(twittering-update-timeline statuses spec)))
(when (and twittering-notify-successful-http-get
(not (assqref 'noninteractive connection-info)))
(format "Fetching %s. Success." spec-string))))
(t
(let ((twittering-service-method (twittering-extract-service spec-string))
(error-msg (twittering-get-error-message header-info
(current-buffer))))
;; (unwind-protect
(if (and ;; (not twittering-oauth2-wait-user-for-reverifying)
;; (setq twittering-oauth2-wait-user-for-reverifying t)
(eq twittering-service-method 'sina)
(or (and (equal status-code "400")
(string-match "expired_token" error-msg))
;; HTTP/1.1 403 Forbidden (invalid_access_token)
(and (equal status-code "403")
(string-match "invalid_access_token" error-msg)))
;;(y-or-n-p "Access token expired, weibo.com asks you to verify again, OK? ")
)
(message "Sina weibo access token expired, M-x twittering-oauth2-force-verify-credentials to reverify")
;; (twittering-oauth2-force-verify-credentials)
(format "Response from `%s': %s" spec-string error-msg)))))))
;; (setq twittering-oauth2-wait-user-for-reverifying nil)))))))
(defun twittering-http-post (host method &optional parameters 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
"
(let* ((service (twittering-extract-service))
(sentinel (or sentinel 'twittering-http-post-default-sentinel))
(path (concat "/" method (if (eq service 'douban) "" ".json")))
(post-body (if (eq service 'douban)
(format
"<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns:ns0=\"http://www.w3.org/2005/Atom\" xmlns:db=\"http://www.douban.com/xmlns/\">
<content>%s</content></entry>"
(encode-coding-string
(assocref "status" parameters) 'utf-8))
""))
;; TODO
;; "POST" url (and (not (twittering-is-uploading-file-p parameters))
;; parameters))))
(request
(if (not (equal (twittering-get-accounts-internal 'oauth) 1.0))
(twittering-make-http-request "POST" nil host nil path
(unless (eq (twittering-extract-service) 'douban)
`(,@parameters ,(assoc "access_token" (twittering-lookup-oauth-access-token-alist)))
)
post-body
(twittering-get-accounts 'ssl))
(twittering-add-application-header-to-http-request
(twittering-make-http-request "POST" nil host nil path
(unless (eq (twittering-extract-service) 'douban)
parameters)
post-body
(twittering-get-accounts '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 (assqref 'status-line header-info))
(status-code (assqref 'status-code header-info)))
(case-string
status-code
(("200")
"Success: Post.")
(t
(format "Response from `%s': %s"
(assqref 'timeline-spec-string connection-info)
(twittering-get-error-message header-info (current-buffer)))))))
(defun twittering-update-timeline (statuses spec)
(let* ((twittering-service-method (twittering-extract-service spec))
(spec-string (twittering-timeline-spec-to-string spec)))
(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 t new-statuses))))
(twittering-add-timeline-history spec-string)))
;;; ============================================= Commands
;;;
;;;; Commands for changing modes
(defun twittering-scroll-mode (&optional arg)
(interactive "P")
(let ((prev-mode twittering-scroll-mode))
(setq twittering-scroll-mode
(if (null arg)
(not twittering-scroll-mode)
(< 0 (prefix-numeric-value arg))))
(unless (eq prev-mode twittering-scroll-mode)
(twittering-update-mode-line))))
(defun twittering-jojo-mode (&optional arg)
(interactive "P")
(let ((prev-mode twittering-jojo-mode))
(setq twittering-jojo-mode
(if (null arg)
(not twittering-jojo-mode)
(< 0 (prefix-numeric-value arg))))
(unless (eq prev-mode twittering-jojo-mode)
(twittering-update-mode-line))))
(defun twittering-jojo-mode-p (spec)
(let ((buffer (twittering-get-buffer-from-spec spec)))
(when (buffer-live-p buffer)
(with-current-buffer buffer
twittering-jojo-mode))))
(defun twittering-toggle-reverse-mode (&optional arg)
(interactive "P")
(let ((prev-mode twittering-reverse-mode))
(setq twittering-reverse-mode
(if (null arg)
(not twittering-reverse-mode)
(< 0 (prefix-numeric-value arg))))
(unless (eq prev-mode twittering-reverse-mode)
(let ((id (twittering-get-id-at)))
(twittering-update-mode-line)
(twittering-render-timeline (current-buffer))
(twittering-restore-point id)))))
(defun twittering-set-current-hashtag (&optional tag)
(interactive)
(unless tag
(setq tag (twittering-completing-read "hashtag (blank to clear): #"
twittering-hashtag-history
nil nil
twittering-current-hashtag
'twittering-hashtag-history))
(message
(if (eq 0 (length tag))
(progn (setq twittering-current-hashtag nil)
"Current hashtag is not set.")
(progn
(setq twittering-current-hashtag tag)
(format "Current hashtag is #%s" twittering-current-hashtag))))))
;;;; Commands for switching buffers
(defun twittering-switch-to-next-timeline ()
(interactive)
(when (twittering-buffer-p)
(let* ((buffer-list (twittering-get-buffer-list))
(following-buffers (cdr (memq (current-buffer) buffer-list)))
(next (if following-buffers
(car following-buffers)
(car buffer-list))))
(unless (eq (current-buffer) next)
(switch-to-buffer next)))))
(defun twittering-switch-to-previous-timeline ()
(interactive)
(when (twittering-buffer-p)
(let* ((buffer-list (reverse (twittering-get-buffer-list)))
(preceding-buffers (cdr (memq (current-buffer) buffer-list)))
(previous (if preceding-buffers
(car preceding-buffers)
(car buffer-list))))
(unless (eq (current-buffer) previous)
(switch-to-buffer previous)))))
;;;; Commands for visiting a timeline
(defun twittering-visit-timeline (&optional spec initial)
(interactive)
(unless spec
(setq spec (twittering-read-timeline-spec-with-completion
"timeline: " initial t)))
(let ((twittering-service-method (twittering-extract-service spec)))
(cond
((stringp spec)
(unless (string-match "@" spec)
(setq spec (format "%s@%S" spec twittering-service-method))))
((consp spec)
(unless (consp (car spec))
(setq spec `((,twittering-service-method) ,@spec)))))
(cond
((twittering-ensure-connection-method)
(twittering-initialize-global-variables-if-necessary)
(switch-to-buffer (twittering-get-managed-buffer spec)))
(t
(message "No connection methods are available.")
nil))))
(defun twittering-friends-timeline ()
(interactive)
(twittering-visit-timeline '(friends)))
(defun twittering-home-timeline ()
(interactive)
(twittering-visit-timeline '(home)))
(defun twittering-replies-timeline ()
(interactive)
(twittering-visit-timeline '(replies)))
(defun twittering-public-timeline ()
(interactive)
(twittering-visit-timeline '(public)))
(defun twittering-user-timeline ()
(interactive)
(twittering-visit-timeline `(user ,(twittering-get-accounts 'username))))
(defun twittering-direct-messages-timeline ()
(interactive)
(twittering-visit-timeline '(direct_messages)))
(defun twittering-sent-direct-messages-timeline ()
(interactive)
(twittering-visit-timeline '(direct_messages_sent)))
(defun twittering-other-user-timeline ()
(interactive)
(let* ((username (get-text-property (point) 'username))
(goto-spec (get-text-property (point) 'goto-spec))
(screen-name-in-text
(get-text-property (point) 'screen-name-in-text))
(spec ;; Sequence is important.
(cond (goto-spec
goto-spec) ; FIXME: better get name for "retweeted by XXX"
(screen-name-in-text
`((,(twittering-extract-service)) user ,screen-name-in-text))
(username
`((,(twittering-extract-service)) user ,username)))))
(if spec
(twittering-visit-timeline spec)
(message "No user selected"))))
(defun twittering-other-user-timeline-interactive ()
(interactive)
(let ((username (or (twittering-read-username-with-completion
"user: " nil
'twittering-user-history)
"")))
(if (string= "" username)
(message "No user selected")
(twittering-visit-timeline `(user ,username)))))
(defun twittering-other-user-list-interactive ()
(interactive)
(let* ((username (copy-sequence (get-text-property (point) 'username)))
(username (progn
(set-text-properties 0 (length username) nil username)
(or (twittering-read-username-with-completion
"Whose list: "
username
'twittering-user-history)
""))))
(if (string= "" username)
(message "No user selected")
(let* ((list-name (twittering-read-list-name username))
(spec `(list ,username ,list-name)))
(if list-name
(twittering-visit-timeline spec)
;; Don't show message here to prevent an overwrite of a
;; message which is outputted by `twittering-read-list-name'.
)))))
(defun twittering-search (&optional word)
(interactive)
(let ((word (or word
(read-from-minibuffer "search: " nil nil nil
'twittering-search-history nil t)
"")))
(if (string= "" word)
(message "No query string")
(let ((spec `(search ,word)))
(twittering-visit-timeline spec)))))
;;;; Commands for retrieving statuses
(defun twittering-current-timeline-noninteractive ()
(twittering-current-timeline t))
(defun twittering-current-timeline (&optional noninteractive)
(interactive)
(when (twittering-buffer-p)
(twittering-get-and-render-timeline noninteractive)))
;;;; Commands for posting a status
(defun twittering-update-status-interactive (&optional ask)
"Non-nil ASK will ask user to select a service from `twittering-enabled-services'. "
(interactive "P")
(let ((spec (twittering-current-timeline-spec)))
(when (or ask (null spec))
(setq spec
`((,(intern
(completing-read
"Post to: "
`(,@(mapcar 'symbol-name twittering-enabled-services) "all")))))))
(funcall twittering-update-status-function
nil nil nil spec)))
(defun twittering-update-lambda ()
(interactive)
(when (and (string= "Japanese" current-language-environment)
(or (< 21 emacs-major-version)
(eq 'utf-8 (terminal-coding-system))))
(let ((text (mapconcat
'char-to-string
(mapcar 'twittering-ucs-to-char
'(955 12363 12431 12356 12356 12424 955)) "")))
(twittering-call-api 'update-status `((status . ,text))))))
(defun twittering-update-jojo (usr msg)
(when (and (not (string= usr (twittering-get-accounts 'username)))
(string= "Japanese" current-language-environment)
(or (< 21 emacs-major-version)
(eq 'utf-8 (terminal-coding-system))))
(if (string-match
(mapconcat
'char-to-string
(mapcar 'twittering-ucs-to-char
'(27425 12395 92 40 12362 21069 92 124 36020 27096
92 41 12399 12300 92 40 91 94 12301 93 43 92
41 12301 12392 35328 12358)) "")
msg)
(let ((text (concat "@" usr " "
(match-string-no-properties 2 msg)
(mapconcat
'char-to-string
(mapcar 'twittering-ucs-to-char
'(12288 12399 12387 33 63)) ""))))
(twittering-call-api 'update-status `((status . ,text)))))))
(defun twittering-direct-message (&optional ask)
(interactive "P")
(let ((username (or (and (not ask)
(get-text-property (point) 'username))
(twittering-read-username-with-completion
"Who would you like to receive the DM? "
(get-text-property (point) 'username)
'twittering-user-history)))
(spec (or (get-text-property (point) 'source-spec)
`((,(twittering-extract-service)) direct_messages))))
(if (string= "" username)
(message "No user selected")
(funcall twittering-update-status-function
(concat "d " username " ") nil username spec))))
(defun twittering-reply-to-user (&optional quote)
"Non-nil QUOTE will quote status using `twittering-generate-organic-retweet'.
However, QUOTO has no effect on sina weibo. "
(interactive "P")
(let* ((username (get-text-property (point) 'username))
(id (get-text-property (point) 'id))
(spec (get-text-property (point) 'belongs-spec))
(status (twittering-find-status id))
(reply-to-quotation nil)
(init-str (if quote
(twittering-generate-organic-retweet)
(concat "@" username " ")))
(quoted-status (twittering-status-has-quotation? status)))
(when (memq (twittering-extract-service) '(sina socialcast douban))
(when quoted-status
(setq username
(ido-completing-read
"Reply to: "
`(,(assqref 'name (assqref 'user status))
,(assqref 'name (assqref 'user quoted-status)))))
(when (string= username (assqref 'name (assqref 'user quoted-status)))
(setq reply-to-quotation t
id (assqref 'id quoted-status))))
;; (setq init-str (concat " // @" username))
;; (unless reply-to-quotation
;; ;; (sina) Quote by default.
;; (let ((s (assqref 'text (or status quoted-status))))
;; (setq init-str (concat init-str " " s))))
(setq init-str ""))
(if username
(progn
(funcall twittering-update-status-function init-str id username spec)
(when (or quote (eq (twittering-extract-service spec) 'sina))
(goto-char (line-beginning-position))))
(message "No user selected"))))
(defun twittering-reply-all (&optional quote)
"Reply(@) all mentioned users in the tweet.
Non-nil QUOTE will quote status using `twittering-generate-organic-retweet'.
However, QUOTO has no effect on sina weibo. "
(interactive "P")
(unless (memq (twittering-extract-service) '(sina twitter))
(error "twittering-reply-all not yet implemented for %S" (twittering-extract-service)))
(let* ((username (get-text-property (point) 'username))
(id (get-text-property (point) 'id))
(spec (get-text-property (point) 'belongs-spec))
(status (twittering-find-status id))
(reply-to-quotation nil)
(me (assqref 'screen-name (twittering-lookup-user-info-alist 'basic)))
(init-str (if quote
(twittering-generate-organic-retweet)
(concat (mapconcat (lambda (u) (concat "@" u))
(remove me (twittering-get-all-usernames-at-pos))
" ")
" ")))
(quoted-status (twittering-status-has-quotation? status)))
(when (memq (twittering-extract-service) '(sina socialcast douban))
(when quoted-status
(setq username
(ido-completing-read
"Reply to: "
`(,(assqref 'name (assqref 'user status))
,(assqref 'name (assqref 'user quoted-status)))))
(when (string= username (assqref 'name (assqref 'user quoted-status)))
(setq reply-to-quotation t
id (assqref 'id quoted-status))))
;; (setq init-str (concat " // @" username))
;; (unless reply-to-quotation
;; ;; (sina) Quote by default.
;; (let ((s (assqref 'text (or status quoted-status))))
;; (setq init-str (concat init-str " " s))))
;; (setq init-str "")
)
(if username
(progn
(funcall twittering-update-status-function init-str id username spec)
;; (when (or quote (eq (twittering-extract-service spec) 'sina))
;; (goto-char (line-beginning-position)))
)
(message "No user selected"))))
(defun twittering-erase-all ()
(interactive)
(let ((inhibit-read-only t))
(erase-buffer)))
;;;; Commands for deleting a status
(defun twittering-delete-status (&optional id)
(interactive)
(let* ((id (get-text-property (point) 'id))
(username (get-text-property (point) 'username))
(text (copy-sequence (get-text-property (point) 'text)))
(text (progn
(set-text-properties 0 (length text) nil text)
text))
(width (max 40 ;; XXX
(- (frame-width)
1 ;; margin for wide characters
11 ;; == (length (concat "Delete \"" "\"? "))
9) ;; == (length "(y or n) ")
))
(mes (format "Delete \"%s\"? "
(if (< width (string-width text))
(concat
(truncate-string-to-width text (- width 3))
"...")
text))))
(cond
((not (string= username (twittering-get-accounts 'username)))
(message "The status is not yours!"))
((not id)
(message "No status selected"))
((y-or-n-p mes)
(twittering-call-api 'destroy-status `((id . ,id)))
(twittering-delete-status-from-data-table id))
(t
(message "Request canceled")))))
;;;; Commands for retweet
(defun twittering-retweet (&optional ask)
(interactive "P")
(let* ((orig-service (twittering-extract-service))
(service orig-service))
(when ask
(setq service
(intern
(ido-completing-read
"Post to: "
`(,@(mapcar 'symbol-name twittering-enabled-services) "all")))))
(mapc (lambda (s)
(if (eq s orig-service)
(let ((twittering-service-method s))
(if twittering-use-native-retweet
(twittering-native-retweet)
(twittering-organic-retweet)))
(twittering-organic-retweet `((,s)))))
(if (eq service 'all) twittering-enabled-services `(,service)))))
(defun twittering-organic-retweet (&optional spec)
(interactive)
(twittering-ensure-retweeting-allowed)
(if spec
(funcall twittering-update-status-function
(format "[%s] %s" (symbol-name (twittering-extract-service))
(twittering-generate-organic-retweet t))
nil
nil
spec)
(funcall twittering-update-status-function
(twittering-generate-organic-retweet)
(get-text-property (point) 'id)
nil
(twittering-current-timeline-spec)
t))
(goto-char (line-beginning-position)))
(defun twittering-native-retweet ()
(interactive)
(twittering-ensure-retweeting-allowed)
(let ((id (or (get-text-property (point) 'retweeted-id)
(get-text-property (point) 'id)))
(text (copy-sequence (get-text-property (point) 'text)))
(user (get-text-property (point) 'username))
(width (max 40 ;; XXX
(- (frame-width)
1 ;; margin for wide characters
12 ;; == (length (concat "Retweet \"" "\"? "))
9) ;; == (length "(y or n) ")
)))
(set-text-properties 0 (length text) nil text)
(if id
(if (not (string= user (twittering-get-accounts 'username)))
(let ((mes (format "Retweet \"%s\"? "
(if (< width (string-width text))
(concat
(truncate-string-to-width text (- width 3))
"...")
text))))
(if (y-or-n-p mes)
(if (eq (twittering-extract-service) service)
(twittering-call-api 'retweet `((id . ,id)))
;; cross retweet
(funcall twittering-update-status-function
text nil nil `((,service))))
(message "Request canceled")))
(message "Cannot retweet your own tweet"))
(message "No status selected"))))
(defun twittering-generate-organic-retweet (&optional cross-retweet)
(let* ((id (get-text-property (point) 'id))
(status (twittering-find-status id))
(username (assqref 'screen-name (assqref 'user status)))
;; (get-text-property (point) 'username))
(text (get-text-property (point) 'text))
(retweet-time (current-time))
(service (twittering-extract-service))
(format-str (or twittering-retweet-format "RT: %t (via @%s)")))
(when username
(if (and (not cross-retweet) (eq service 'sina))
(if (twittering-status-has-quotation? status)
(format " //@%s:%s" username (assqref 'text status))
"")
(let* ((prefix "%")
(replace-table
`(("%" . "%")
("s" . ,username)
("t" . ,text)
("#" . ,id)
("C{\\([^}]*\\)}" .
(lambda (context)
(let ((str (assqref 'following-string context))
(match-data (assqref 'match-data context)))
(store-match-data match-data)
(format-time-string (match-string 1 str) ',retweet-time))))))
(ret (twittering-format-string format-str prefix replace-table)))
(when (and (eq service 'sina) (assqref 'original-pic status))
(setq ret (concat ret " " (assqref 'original-pic status))))
ret)))))
(defun twittering-ensure-retweeting-allowed ()
(let* ((id (twittering-get-id-at))
(status (twittering-find-status (twittering-get-id-at))))
(when (equal "true" (assqref 'protected (assqref 'user status)))
(error "Cannot retweet protected tweets."))))
;;;; Commands for browsing information related to a status
(defun twittering-click ()
(interactive)
(let ((uri (get-text-property (point) 'uri)))
(if uri
(browse-url uri))))
(defun twittering-enter ()
(interactive)
(let ((username (get-text-property (point) 'username))
(id (twittering-get-id-at (point)))
(uri (get-text-property (point) 'uri))
(spec (get-text-property (point) 'source-spec))
(screen-name-in-text
(get-text-property (point) 'screen-name-in-text))
(send-message (lambda (name)
(funcall twittering-update-status-function
(if (twittering-timeline-spec-direct-messages-p spec)
(concat "d " name " ")
(concat "@" name " "))
id name spec))))
(cond (screen-name-in-text
(funcall send-message screen-name-in-text))
(uri
(browse-url uri))
(username
(funcall send-message username)))))
(defun twittering-view-user-page ()
(interactive)
(let ((uri (get-text-property (point) 'uri)))
(if uri
(browse-url uri))))
;;;;
;;;; Commands corresponding to operations on Twitter
;;;;
(defun twittering-follow (&optional remove)
"Follow a user or a list.
Non-nil optional REMOVE will do the opposite, unfollow. "
(interactive "P")
(let ((user-or-list (twittering-read-username-with-completion
(concat (if remove "Unfollow" "Follow") " who: ")
"" 'twittering-user-history))
prompt-format method args)
(when (string= "" user-or-list)
(error "No user or list selected"))
;; (set-text-properties 0 (length user-or-list) nil user-or-list)
(if (string-match "/" user-or-list)
(let ((spec (twittering-string-to-timeline-spec user-or-list)))
(setq prompt-format (if remove "Unfollowing list `%s'? "
"Following list `%s'? ")
method (if remove 'unsubscribe-list 'subscribe-list)
args `((timeline-spec . ,spec))))
(setq prompt-format (if remove "Unfollowing `%s'? " "Following `%s'? ")
method (if remove 'destroy-friendships 'create-friendships)
args `((username . ,user-or-list))))
(if (y-or-n-p (format prompt-format user-or-list))
(twittering-call-api method args)
(message "Request canceled"))))
(defun twittering-unfollow ()
"Unfollow a user or a list."
(interactive)
(twittering-follow t))
(defun twittering-add-list-members (&optional remove)
"Add a user to a list.
Non-nil optional REMOVE will do the opposite, remove a user from
a list. "
(interactive "P")
(let* ((username (twittering-read-username-with-completion
(if remove "Remove who from list: " "Add who to list: ")
"" 'twittering-user-history))
(listname
(unless (string= username "")
(or (let ((s (twittering-current-timeline-spec-string)))
(when (and s
(twittering-timeline-spec-list-p
(twittering-current-timeline-spec))
(y-or-n-p (format "from list: `%s'? " s)))
s))
(twittering-read-list-name (twittering-get-accounts 'username))
(read-string
"Failed to retrieve your list, enter list name manually or retry: ")))))
(when (or (string= username "") (string= listname ""))
(error "No user or list selected"))
(unless (string-match "/" listname)
(setq listname (concat (twittering-get-accounts 'username) "/" listname)))
(if (y-or-n-p (format (if remove "Remove `%s' from `%s'? " "Add `%s' to `%s'? ")
username listname))
(twittering-call-api
(if remove 'delete-list-members 'add-list-members)
`((id . ,username)
(timeline-spec . ,(twittering-string-to-timeline-spec listname))))
(message "Request canceled"))))
(defun twittering-delete-list-members ()
"Delete a user from a list. "
(interactive)
(twittering-add-list-members t))
(defun twittering-favorite (&optional remove)
(interactive "P")
(let ((id (get-text-property (point) 'id))
(text (copy-sequence (get-text-property (point) 'text)))
(width (max 40 ;; XXX
(- (frame-width)
1 ;; margin for wide characters
15 ;; == (length (concat "Unfavorite \"" "\"? "))
9) ;; == (length "(y or n) ")
))
(method (if remove 'destroy-favorites 'create-favorites)))
(set-text-properties 0 (length text) nil text)
(if id
(let ((mes (format "%s \"%s\"? "
(if remove "Unfavorite" "Favorite")
(if (< width (string-width text))
(concat
(truncate-string-to-width text (- width 3))
"...")
text))))
(if (y-or-n-p mes)
(twittering-call-api method `((id . ,id)))
(message "Request canceled")))
(message "No status selected"))))
(defun twittering-unfavorite ()
(interactive)
(twittering-favorite t))
(defun twittering-update-profile-image (image)
"Update a new profile image.
Note: the new image might not appear in your timeline immediately (this seems
some limitation of twitter API?), but you can see your new image from web
browser right away."
(interactive "fUpdate profile image: ")
(twittering-call-api 'update-profile-image `((image . ,image))))
(defun twittering-block ()
"Block a user who posted the tweet at the current position."
(interactive)
(let* ((id (twittering-get-id-at))
(status (when id (twittering-find-status id)))
(username
(cond
((assq 'retweeted-id status)
(let* ((retweeting-username
(cdr (assq 'retweeting-user-screen-name status)))
(retweeted-username
(cdr (assq 'retweeted-user-screen-name status)))
(prompt "Who do you block? ")
(candidates (list retweeted-username retweeting-username)))
(twittering-completing-read prompt candidates nil t)))
(status
(assqref 'screen-name (assqref 'user status)))
(t
nil))))
(cond
((or (null username) (string= "" username))
(message "No user selected"))
((yes-or-no-p (format "Really block \"%s\"? " username))
(twittering-call-api 'block `((username . ,username))))
(t
(message "Request canceled")))))
(defun twittering-block-and-report-as-spammer ()
"Report a user who posted the tweet at the current position as a spammer.
The user is also blocked."
(interactive)
(let* ((id (twittering-get-id-at))
(status (when id (twittering-find-status id)))
(username
(cond
((assq 'retweeted-id status)
(let* ((retweeting-username
(cdr (assq 'retweeting-user-screen-name status)))
(retweeted-username
(cdr (assq 'retweeted-user-screen-name status)))
(prompt "Who do you report as a spammer? ")
(candidates (list retweeted-username retweeting-username)))
(twittering-completing-read prompt candidates nil t)))
(status
(assqref 'screen-name (assqref 'user status)))
(t
nil))))
(cond
((or (null username) (string= "" username))