" Vimball Archiver by Charles E. Campbell, Jr., Ph.D. UseVimball finish plugin/twitvim.vim [[[1 5952 " ============================================================== " TwitVim - Post to Twitter from Vim " Based on Twitter Vim script by Travis Jeffery " " Version: 0.8.2 " License: Vim license. See :help license " Language: Vim script " Maintainer: Po Shan Cheah " Created: March 28, 2008 " Last updated: March 26, 2014 " " GetLatestVimScripts: 2204 1 twitvim.vim " ============================================================== " Load this module only once. if exists('g:loaded_twitvim') finish endif let g:loaded_twitvim = '0.8.2 2014-03-26' " Check Vim version. if v:version < 703 echohl WarningMsg echomsg 'You need Vim 7.3 or later for this version of TwitVim' echohl None finish endif " Avoid side-effects from cpoptions setting. let s:save_cpo = &cpo set cpo&vim " User agent header string. let s:user_agent = 'TwitVim '.g:loaded_twitvim " Twitter character limit. Twitter used to accept tweets up to 246 characters " in length and display those in truncated form, but that is no longer the " case. So 140 is now the hard limit. let s:char_limit = 140 " Info on OAuth-based service providers. let s:service_info = { \ 'twitter' : { \ 'dispname' : 'Twitter', \ 'consumer_key' : 'HyshEU8SbcsklPQ6ouF0g', \ 'consumer_secret' : 'U1uvxLjZxlQAasy9Kr5L2YAFnsvYTOqx1bk7uJuezQ', \ 'req_url' : 'https://api.twitter.com/oauth/request_token', \ 'access_url' : 'https://api.twitter.com/oauth/access_token', \ 'authorize_url' : 'https://api.twitter.com/oauth/authorize', \ 'api_root' : 'https://api.twitter.com/1.1', \ 'search_api' : 'https://api.twitter.com/1.1/search/tweets.json', \ }, \ 'identica' : { \ 'dispname' : 'identi.ca', \ 'consumer_key' : '2ed2fd2bbd62f7c07ae6bb786b664680', \ 'consumer_secret' : '75edf3d8a5e692d6afcccf7a51611e94', \ 'req_url' : 'https://identi.ca/api/oauth/request_token', \ 'access_url' : 'https://identi.ca/api/oauth/access_token', \ 'authorize_url' : 'https://identi.ca/api/oauth/authorize', \ 'api_root' : 'http://identi.ca/api', \ 'search_api' : 'http://identi.ca/api/search.json', \ }, \ } let s:default_service = 'twitter' let s:default_api_root = s:service_info[s:default_service]['api_root'] let s:cur_service = '' " Attempt to get the current service even on startup and even if user has not " logged in yet. Returns '' if cannot determine. function! s:get_cur_service() if s:is_oauth() " Get current service from token file or do OAuth login. if s:cur_service == '' call s:read_tokens() if s:cur_service == '' let [ status, error ] = s:do_login() if status < 0 return '' endif endif endif return s:cur_service else return '' endif endfunction " Allow the user to override the API root for services other than Twitter and identi.ca. function! s:get_api_root() if s:is_oauth() let svc = s:get_cur_service() if svc == '' return s:default_api_root else return s:service_info[svc]['api_root'] endif else return g:twitvim_api_root endif endfunction " Service display name. function! s:get_svc_disp_name() return s:is_oauth() ? s:service_info[s:cur_service]['dispname'] : 'user-defined service' endfunction function! s:get_disp_name(service) return get(s:service_info, a:service, { 'dispname' : a:service })['dispname'] endfunction " Are we using one of the OAuth services? function! s:is_oauth() " For backward compatibility. We used to enable OAuth by specifying the " Twitter API root. return !exists('g:twitvim_api_root') || g:twitvim_api_root =~? 'twitter\.com' endfunction " Allow user to set the format for retweets. function! s:get_retweet_fmt() return exists('g:twitvim_retweet_format') ? g:twitvim_retweet_format : "RT %s: %t" endfunction " Allow user to enable Python networking code by setting twitvim_enable_python. function! s:get_enable_python() return exists('g:twitvim_enable_python') ? g:twitvim_enable_python : 0 endfunction " Allow user to enable Python 3 networking code by setting twitvim_enable_python3. function! s:get_enable_python3() return exists('g:twitvim_enable_python3') ? g:twitvim_enable_python3 : 0 endfunction " Allow user to enable Perl networking code by setting twitvim_enable_perl. function! s:get_enable_perl() return exists('g:twitvim_enable_perl') ? g:twitvim_enable_perl : 0 endfunction " Allow user to enable Ruby code by setting twitvim_enable_ruby. function! s:get_enable_ruby() return exists('g:twitvim_enable_ruby') ? g:twitvim_enable_ruby : 0 endfunction " Allow user to enable Tcl code by setting twitvim_enable_tcl. function! s:get_enable_tcl() return exists('g:twitvim_enable_tcl') ? g:twitvim_enable_tcl : 0 endfunction " Force SSL mode. function! s:get_force_ssl() if exists('g:twitvim_force_ssl') return g:twitvim_force_ssl elseif exists('g:twitvim_api_root') && g:twitvim_api_root =~? '^https:' " For backward compatibility. The old way to force SSL was to specify a " https API root. return 1 else return 0 endif endfunction " Get proxy setting from twitvim_proxy in .vimrc or _vimrc. " Format is proxysite:proxyport function! s:get_proxy() return exists('g:twitvim_proxy') ? g:twitvim_proxy : '' endfunction " If twitvim_proxy_login exists, use that as the proxy login. " Format is proxyuser:proxypassword " If twitvim_proxy_login_b64 exists, use that instead. This is the proxy " user:password in base64 encoding. function! s:get_proxy_login() if exists('g:twitvim_proxy_login_b64') && g:twitvim_proxy_login_b64 != '' return g:twitvim_proxy_login_b64 else return exists('g:twitvim_proxy_login') ? g:twitvim_proxy_login : '' endif endfunction " Get twitvim_count, if it exists. This will be the number of tweets returned " by :FriendsTwitter, :UserTwitter, and :SearchTwitter. function! s:get_count() if exists('g:twitvim_count') if g:twitvim_count < 1 return 1 elseif g:twitvim_count > 200 return 200 else return g:twitvim_count endif endif return 0 endfunction " User setting to show/hide header in the buffer. Default: show header. function! s:get_show_header() return exists('g:twitvim_show_header') ? g:twitvim_show_header : 1 endfunction " User config for name of OAuth access token file. function! s:get_token_file() return exists('g:twitvim_token_file') ? g:twitvim_token_file : $HOME . "/.twitvim.token" endfunction " User config to disable the OAuth access token file. function! s:get_disable_token_file() return exists('g:twitvim_disable_token_file') ? g:twitvim_disable_token_file : 0 endfunction " User config to enable the filter. function! s:get_filter_enable() return exists('g:twitvim_filter_enable') ? g:twitvim_filter_enable : 0 endfunction " User config for filter. function! s:get_filter_regex() return exists('g:twitvim_filter_regex') ? g:twitvim_filter_regex : '' endfunction " User config for Trends WOEID. " Default to 1 for worldwide. function! s:get_twitvim_woeid() return exists('g:twitvim_woeid') ? g:twitvim_woeid : 1 endfunction " Allow user to override consumer key. function! s:get_consumer_key() return exists('g:twitvim_consumer_key') ? g:twitvim_consumer_key : s:service_info[s:cur_service]['consumer_key'] endfunction " Allow user to override consumer secret. function! s:get_consumer_secret() return exists('g:twitvim_consumer_secret') ? g:twitvim_consumer_secret : s:service_info[s:cur_service]['consumer_secret'] endfunction " Allow user to customize timestamp format in timeline display. " Default is HH:MM AM/PM Mon DD, YYYY function! s:get_timestamp_format() return exists('g:twitvim_timestamp_format') ? g:twitvim_timestamp_format : '%I:%M %p %b %d, %Y' endfunction " Allow user to customize network timeout in seconds. " Default is 0 for no timeout, which defers to the system socket timeout. function! s:get_net_timeout() return exists('g:twitvim_net_timeout') ? g:twitvim_net_timeout : 0 endfunction " If nonzero, use old API for friends/followers. " Default is 0 for Twitter and 1 for all other services. function! s:get_use_old_friends_api() if exists('g:twitvim_use_old_friends_api') return g:twitvim_use_old_friends_api else let svc = s:get_cur_service() if svc == '' " Assume that all unknown services have to use the old API. return 1 else " Only Twitter supports the new friends/followers API for now. return svc != 'twitter' endif endif endfunction " Display an error message in the message area. function! s:errormsg(msg) redraw echohl ErrorMsg echomsg a:msg echohl None endfunction " Display a warning message in the message area. function! s:warnmsg(msg) redraw echohl WarningMsg echomsg a:msg echohl None endfunction " Get Twitter login info from twitvim_login in vimrc. " Format is username:password " If twitvim_login_b64 exists, use that instead. This is the user:password " in base64 encoding. " " This function is for services with Twitter-compatible APIs that use Basic " authentication, e.g. identi.ca function! s:get_twitvim_login_noerror() if exists('g:twitvim_login_b64') && g:twitvim_login_b64 != '' return g:twitvim_login_b64 elseif exists('g:twitvim_login') && g:twitvim_login != '' return g:twitvim_login else return '' endif endfunction " Throw away saved login tokens and reset login info. function! s:reset_twitvim_login() call inputsave() let answer = input('Delete all login info? (y/n) ') call inputrestore() if answer != 'y' && answer != 'Y' redraw echo 'Login info not deleted.' return endif let s:access_token = "" let s:access_token_secret = "" let s:tokens = {} call delete(s:get_token_file()) let s:cached_username = "" endfunction " Log in to a Twitter account. function! s:prompt_twitvim_login() call s:do_login() endfunction function! s:logins_menu(userlist, what) let menu = [] call add(menu, 'Choose a login to '.a:what) let namecount = 0 for userrec in a:userlist let namecount += 1 call add(menu, namecount.'. '. userrec.name . ' on ' . s:get_disp_name(userrec.service)) endfor call inputsave() let input = inputlist(menu) call inputrestore() if input < 1 || input > len(a:userlist) " Invalid input cancels the command. return {} endif return a:userlist[input - 1] endfunction " Delete a Twitter login. function! s:delete_twitvim_login(user) if s:tokens == {} call s:read_tokens() endif let user = a:user if user == '' let userlist = s:list_tokens_for_del() if userlist == [] call s:errormsg('No logins to delete.') return endif let userrec = s:logins_menu(userlist, 'delete') if userrec == {} " User canceled. return endif else let [ name, service ] = split(a:user, ',') let userrec = { 'name' : name, 'service' : service } endif call s:delete_token(userrec.name, userrec.service) call s:write_tokens(s:cached_username) endfunction " Switch to a different Twitter user. function! s:switch_twitvim_login(user) if s:tokens == {} call s:read_tokens() endif let user = a:user if user == '' let userlist = s:list_tokens() if userlist == [] call s:errormsg('No logins to switch to. Use :SetLoginTwitter to log in.') return endif let userrec = s:logins_menu(userlist, 'switch to') if userrec == {} " User canceled. return endif else let [ name, service ] = split(a:user, ',') let userrec = { 'name' : name, 'service' : service } endif call s:switch_token(userrec.name, userrec.service) call s:write_tokens(s:cached_username) endfunction let s:cached_login = '' let s:cached_username = '' " See if we can save time by using the cached username. function! s:get_twitvim_cached_username() if s:is_oauth() if s:cached_username == '' return '' endif else " In Twitter-compatible services that use Basic authentication, the " user may have changed the login info on the fly. So we have to watch " out for that. let login = s:get_twitvim_login_noerror() if login == '' || login != s:cached_login return '' endif endif return s:cached_username endfunction " Get Twitter user name by verifying login credentials function! s:get_twitvim_username() " If we already have the info, no need to get it again. let username = s:get_twitvim_cached_username() if username != '' return username endif redraw echo "Verifying login credentials..." let url = s:get_api_root()."/account/verify_credentials.json" let [error, output] = s:run_curl_oauth_get(url, {}) let result = s:parse_json(output) if error != '' let errormsg = get(result, 'error', '') call s:errormsg("Error verifying login credentials: ".(errormsg != '' ? errormsg : error)) return '' endif redraw echo "Login credentials verified." let username = get(result, 'screen_name', '') " Save it so we don't have to do it again unless the user switches to " a different login. let s:cached_username = username let s:cached_login = s:get_twitvim_login_noerror() return username endfunction " If set, twitvim_cert_insecure turns off certificate verification if using " https Twitter API over cURL or Ruby. function! s:get_twitvim_cert_insecure() return exists('g:twitvim_cert_insecure') ? g:twitvim_cert_insecure : 0 endfunction " === JSON parser === function! s:parse_json(str) try let true = 1 let false = 0 let null = '' let str = substitute(a:str, '\\u\(\x\{4}\)', '\=s:nr2enc_char("0x".submatch(1))', 'g') sandbox let result = eval(str) return result catch call s:errormsg('JSON parse error: '.v:exception) return {} endtry endfunction " === XML helper functions === " Get the content of the n'th element in a series of elements. function! s:xml_get_nth(xmlstr, elem, n) let matchres = matchlist(a:xmlstr, '<'.a:elem.'\%( [^>]*\)\?>\(.\{-}\)', -1, a:n) return matchres == [] ? "" : matchres[1] endfunction " Get all elements in a series of elements. function! s:xml_get_all(xmlstr, elem) let pat = '<'.a:elem.'\%( [^>]*\)\?>\(.\{-}\)' let matches = [] let pos = 0 while 1 let matchres = matchlist(a:xmlstr, pat, pos) if matchres == [] return matches endif call add(matches, matchres[1]) let pos = matchend(a:xmlstr, pat, pos) endwhile endfunction " Get the content of the specified element. function! s:xml_get_element(xmlstr, elem) return s:xml_get_nth(a:xmlstr, a:elem, 1) endfunction " Remove any number of the specified element from the string. Used for removing " sub-elements so that you can parse the remaining elements safely. function! s:xml_remove_elements(xmlstr, elem) return substitute(a:xmlstr, '<'.a:elem.'>.\{-}', '', "g") endfunction " Get the attributes of the n'th element in a series of elements. function! s:xml_get_attr_nth(xmlstr, elem, n) let matchres = matchlist(a:xmlstr, '<'.a:elem.'\s\+\([^>]*\)>', -1, a:n) if matchres == [] return {} endif let matchcount = 1 let attrstr = matchres[1] let attrs = {} while 1 let matchres = matchlist(attrstr, '\(\w\+\)="\([^"]*\)"', -1, matchcount) if matchres == [] break endif let attrs[matchres[1]] = matchres[2] let matchcount += 1 endwhile return attrs endfunction " Get attributes of the specified element. function! s:xml_get_attr(xmlstr, elem) return s:xml_get_attr_nth(a:xmlstr, a:elem, 1) endfunction " === End of XML helper functions === " === Time parser === " Convert date to Julian date. function! s:julian(year, mon, mday) let month = (a:mon - 1 + 10) % 12 let year = a:year - month / 10 return a:mday + 365 * year + year / 4 - year / 100 + year / 400 + ((month * 306) + 5) / 10 endfunction " Calculate number of days since UNIX Epoch. function! s:daygm(year, mon, mday) return s:julian(a:year, a:mon, a:mday) - s:julian(1970, 1, 1) endfunction " Convert date/time to UNIX time. (seconds since Epoch) function! s:timegm(year, mon, mday, hour, min, sec) return a:sec + a:min * 60 + a:hour * 60 * 60 + s:daygm(a:year, a:mon, a:mday) * 60 * 60 * 24 endfunction let s:monthnames = { 'jan' : 1, 'feb' : 2, 'mar' : 3, 'apr' : 4, 'may' : 5, 'jun' : 6, 'jul' : 7, 'aug' : 8, 'sep' : 9, 'oct' : 10, 'nov' : 11, 'dec' : 12 } " Convert abbreviated month name to month number. function! s:conv_month(s) return get(s:monthnames, tolower(a:s)) endfunction function! s:timegm2(matchres, indxlist) let args = [] for i in a:indxlist if i < 0 let mon = s:conv_month(a:matchres[-i]) if mon == 0 return -1 endif let args = add(args, mon) else let args = add(args, a:matchres[i] + 0) endif endfor return call('s:timegm', args) endfunction " Parse a Twitter time string. function! s:parse_time(str) " This timestamp format is used by Twitter in timelines. let matchres = matchlist(a:str, '^\w\+,\s\+\(\d\+\)\s\+\(\w\+\)\s\+\(\d\+\)\s\+\(\d\+\):\(\d\+\):\(\d\+\)\s\++0000$') if matchres != [] return s:timegm2(matchres, [3, -2, 1, 4, 5, 6]) endif " This timestamp format is used by Twitter in response to an update. let matchres = matchlist(a:str, '^\w\+\s\+\(\w\+\)\s\+\(\d\+\)\s\+\(\d\+\):\(\d\+\):\(\d\+\)\s\++0000\s\+\(\d\+\)$') if matchres != [] return s:timegm2(matchres, [6, -1, 2, 3, 4, 5]) endif " This timestamp format is used by Twitter Search. let matchres = matchlist(a:str, '^\(\d\+\)-\(\d\+\)-\(\d\+\)T\(\d\+\):\(\d\+\):\(\d\+\)Z$') if matchres != [] return s:timegm2(matchres, range(1, 6)) endif " This timestamp format is used by Twitter Rate Limit. let matchres = matchlist(a:str, '^\(\d\+\)-\(\d\+\)-\(\d\+\)T\(\d\+\):\(\d\+\):\(\d\+\)+00:00$') if matchres != [] return s:timegm2(matchres, range(1, 6)) endif return -1 endfunction " Convert time_t value to time string. function! s:time_fmt(tm) if !exists("*strftime") return ''.a:tm endif return strftime(s:get_timestamp_format(), a:tm) endfunction " Convert the Twitter timestamp to local time and simplify it. function! s:time_filter(str) if !exists("*strftime") return a:str endif let t = s:parse_time(a:str) return t < 0 ? a:str : strftime(s:get_timestamp_format(), t) endfunction " === End of time parser === " === Token Management code === " Each token record holds the following fields: " " token: access token " secret: access token secret " name: screen name " A lowercased copy of the screen name,service name is the hash key. let s:tokens = {} let s:token_header = 'TwitVim 0.8' function! s:find_token(name, service) return get(s:tokens, tolower(a:name . ',' . a:service), {}) endfunction function! s:save_token(tokenrec) let tokenrec = a:tokenrec let s:tokens[tolower(tokenrec.name . ',' . tokenrec.service)] = tokenrec endfunction " Delete an access token. function! s:delete_token(name, service) let tokenrec = s:find_token(a:name, a:service) if tokenrec == {} call s:errormsg("No saved login for user ".a:name." on ".s:get_disp_name(a:service).".") elseif a:name ==? s:cached_username && a:service ==? s:cur_service call s:errormsg("Can't delete currently logged-in user.") else unlet! s:tokens[tolower(a:name . ',' . a:service)] redraw echo 'Login token deleted.' endif endfunction " Switch to another access token. Note that the token file should be written " out again after this to reflect the new current user. function! s:switch_token(name, service) let tokenrec = s:find_token(a:name, a:service) if tokenrec == {} call s:errormsg("Can't switch to user ".a:name." on ".s:get_disp_name(a:service).".") else let s:cur_service = tokenrec.service let s:access_token = tokenrec.token let s:access_token_secret = tokenrec.secret let s:cached_username = tokenrec.name redraw echo "Logged in as ".s:cached_username." on ".s:get_svc_disp_name()."." endif endfunction " Returns a list of screen names. This is for prompting the user to pick a login " to which to switch. function! s:list_tokens() return map(values(s:tokens), '{ "name" : v:val.name, "service" : v:val.service }') endfunction " Returns a newline-delimited list of screen names. This is for command " completion when switching logins. function! s:name_list_tokens(ArgLead, CmdLine, CursorPos) return join(map(s:list_tokens(), 'v:val.name . "," . v:val.service'), "\n") endfunction " Returns a list of screen names except for the current user. This is for " prompting the user to pick a login to delete. function! s:list_tokens_for_del() return map(filter(values(s:tokens), 'v:val.name !=? s:cached_username || v:val.service !=? s:cur_service'), '{ "name" : v:val.name, "service" : v:val.service }') endfunction " Returns a newline-delimited list of screen names except for the current user. " This is for command completion when deleting a login. function! s:name_list_tokens_for_del(ArgLead, CmdLine, CursorPos) return join(map(s:list_tokens_for_del(), 'v:val.name . "," . v:val.service'), "\n") endfunction " Write the token file. function! s:write_tokens(current_user) if !s:get_disable_token_file() let tokenfile = s:get_token_file() let json_tokens = map(values(s:tokens), '{ "name" : v:val.name, "token" : v:val.token, "secret" : v:val.secret, "service" : v:val.service }') let json_obj = { 'current_service' : s:cur_service, 'current_user' : a:current_user, 'tokens' : json_tokens } let lines = [] call add(lines, s:token_header) call add(lines, '') call add(lines, string(json_obj)) if writefile(lines, tokenfile) < 0 call s:errormsg('Error writing token file: '.v:errmsg) endif " Check and change file permissions for security. if has('unix') let perms = getfperm(tokenfile) if perms != '' && perms[-6:] != '------' silent! execute "!chmod go-rwx '".tokenfile."'" endif endif endif endfunction " Read the token file. function! s:read_tokens() let tokenfile = s:get_token_file() if !s:get_disable_token_file() && filereadable(tokenfile) let [hdr, current_user; tokens] = readfile(tokenfile, 't', 500) if tokens == [] " Legacy token file only has token and secret. let s:access_token = hdr let s:access_token_secret = current_user let s:cached_username = '' let s:cur_service = s:default_service let user = s:get_twitvim_username() if user == '' call s:errormsg('Invalid token in token file. Please relogin with :SetLoginTwitter.') return endif let tokenrec = {} let tokenrec.token = s:access_token let tokenrec.secret = s:access_token_secret let tokenrec.name = user let tokenrec.service = s:cur_service call s:save_token(tokenrec) call s:write_tokens(user) else if hdr == 'TwitVim 0.6' let service = s:default_service " New token file contains tokens, 3 lines per record. for i in range(0, len(tokens) - 1, 3) let tokenrec = {} let tokenrec.name = tokens[i] let tokenrec.token = tokens[i + 1] let tokenrec.secret = tokens[i + 2] let tokenrec.service = s:default_service call s:save_token(tokenrec) endfor else let json_obj = s:parse_json(tokens[0]) let current_user = json_obj['current_user'] let service = get(json_obj, 'current_service', s:default_service) let json_tokens = json_obj['tokens'] for json_token in json_tokens let tokenrec = {} let tokenrec.name = json_token.name let tokenrec.token = json_token.token let tokenrec.secret = json_token.secret let tokenrec.service = get(json_token, 'service', s:default_service) call s:save_token(tokenrec) endfor endif call s:switch_token(current_user, service) endif endif endfunction " === End of Token Management code === " === OAuth code === " Check if we can use Perl for HMAC-SHA1 digests. function! s:check_perl_hmac() let can_perl = 1 perl <import; }; if ($@) { VIM::DoCommand('let can_perl = 0'); } EOF return can_perl endfunction " Compute HMAC-SHA1 digest. (Perl version) function! s:perl_hmac_sha1_digest(key, str) perl <import; my $key = VIM::Eval('a:key'); my $str = VIM::Eval('a:str'); my $hmac = Digest::HMAC_SHA1->new($key); $hmac->add($str); my $signature = $hmac->b64digest; # Length of 27 VIM::DoCommand("let signature = '$signature'"); EOF return signature endfunction " Check if we can use Python for HMAC-SHA1 digests. function! s:check_python_hmac() let can_python = 1 python <reset_hmac_method() endif " For debugging. Show current Hmac method. if !exists(":TwitVimShowHmacMethod") command TwitVimShowHmacMethod :call show_hmac_method() endif " Simple nonce value generator. This needs to be randomized better. function! s:nonce() if !exists("s:nonce_val") || s:nonce_val < 1 let s:nonce_val = localtime() + 109 endif let retval = s:nonce_val let s:nonce_val += 109 return retval endfunction " Produce signed content using the parameters provided via parms using the " chosen method, url and provided token secret. Note that in the case of " getting a new Request token, the secret will be "" function! s:getOauthResponse(url, method, parms, token_secret) let parms = copy(a:parms) " Add some constants to hash let parms["oauth_consumer_key"] = s:get_consumer_key() let parms["oauth_signature_method"] = "HMAC-SHA1" let parms["oauth_version"] = "1.0" " Get the timestamp and add to hash let parms["oauth_timestamp"] = localtime() let parms["oauth_nonce"] = s:nonce() " Alphabetically sort by key and form a string that has " the format key1=value1&key2=value2&... " Must UTF8 encode and then URL encode the values. let content = "" for key in sort(keys(parms)) let value = s:url_encode(parms[key]) let content .= key . "=" . value . "&" endfor let content = content[0:-2] " Form the signature base string which is comprised of 3 " pieces, with each piece URL encoded. " [METHOD_UPPER_CASE]&[url]&content let signature_base_str = a:method . "&" . s:url_encode(a:url) . "&" . s:url_encode(content) let hmac_sha1_key = s:url_encode(s:get_consumer_secret()) . "&" . s:url_encode(a:token_secret) let signature = s:hmac_sha1_digest(hmac_sha1_key, signature_base_str) " Add padding character to make a multiple of 4 per the " requirement of OAuth. if strlen(signature) % 4 let signature .= "=" endif let content = "OAuth " for key in keys(parms) if key =~ "oauth" let value = s:url_encode(parms[key]) let content .= key . '="' . value . '", ' endif endfor let content .= 'oauth_signature="' . s:url_encode(signature) . '"' return content endfunction " Convert an OAuth endpoint to https if user sets twitvim_force_ssl. function! s:to_https(url) return s:get_force_ssl() ? substitute(a:url, '^\chttp:', 'https:', '') : a:url endfunction " Perform the OAuth dance to authorize this client with Twitter. function! s:do_oauth() " Call oauth/request_token to get request token from Twitter. let parms = { "oauth_callback": "oob", "dummy" : "1" } let req_url = s:to_https(s:service_info[s:cur_service]['req_url']) let oauth_hdr = s:getOauthResponse(req_url, "POST", parms, "") let [error, output] = s:run_curl(req_url, oauth_hdr, s:get_proxy(), s:get_proxy_login(), { "dummy" : "1" }) if error != '' call s:errormsg("Error from oauth/request_token: ".error) return [-1, '', '', ''] endif let request_token = '' let matchres = matchlist(output, 'oauth_token=\([^&]\+\)&') if matchres != [] let request_token = matchres[1] endif let token_secret = '' let matchres = matchlist(output, 'oauth_token_secret=\([^&]\+\)&') if matchres != [] let token_secret = matchres[1] endif if request_token == '' || token_secret == '' call s:errormsg("Unable to parse result from oauth/request_token: ".output) return [-1, '', '', ''] endif " Launch web browser to let user allow or deny the authentication request. let auth_url = s:to_https(s:service_info[s:cur_service]['authorize_url'] . "?oauth_token=" . request_token) " If user has not set up twitvim_browser_cmd, just display the " authentication URL and ask the user to visit that URL. if !exists('g:twitvim_browser_cmd') || g:twitvim_browser_cmd == '' " Attempt to shorten the auth URL. let newurl = s:call_isgd(auth_url) if newurl != "" let auth_url = newurl else let newurl = s:call_bitly(auth_url) if newurl != "" let auth_url = newurl endif endif echo "Visit the following URL in your browser to authenticate TwitVim:" echo auth_url else if s:launch_browser(auth_url) < 0 return [-2, '', '', ''] endif endif call inputsave() let pin = input("Enter OAuth PIN: ") call inputrestore() if pin == "" call s:warnmsg("No OAuth PIN entered") return [-3, '', '', ''] endif " Call oauth/access_token to swap request token for access token. let parms = { "dummy" : 1, "oauth_token" : request_token, "oauth_verifier" : pin } let access_url = s:to_https(s:service_info[s:cur_service]['access_url']) let oauth_hdr = s:getOauthResponse(access_url, "POST", parms, token_secret) let [error, output] = s:run_curl(access_url, oauth_hdr, s:get_proxy(), s:get_proxy_login(), { "dummy" : 1 }) if error != '' call s:errormsg("Error from oauth/access_token: ".error) return [-4, '', '', ''] endif let matchres = matchlist(output, 'oauth_token=\([^&]\+\)') if matchres != [] let request_token = matchres[1] endif let matchres = matchlist(output, 'oauth_token_secret=\([^&]\+\)') if matchres != [] let token_secret = matchres[1] endif let screen_name = '' let matchres = matchlist(output, 'screen_name=\([^&]\+\)') if matchres != [] let screen_name = matchres[1] endif return [ 0, request_token, token_secret, screen_name ] endfunction " Perform an OAuth login. function! s:do_login() let keys = [ '' ] let menu = [ 'Pick a service to login to:' ] let i = 0 for [key, svc] in items(s:service_info) let i += 1 call add(keys, key) call add(menu, printf('%2d. %s', i, svc['dispname'])) endfor call inputsave() let input = inputlist(menu) call inputrestore() if input < 1 || input >= len(menu) return [ -1, 'Login canceled.' ] endif let s:cur_service = keys[input] let [ retval, s:access_token, s:access_token_secret, s:cached_username ] = s:do_oauth() if retval < 0 return [ -1, "Error from do_oauth(): ".retval ] endif if s:cached_username == '' let s:cached_username = s:get_twitvim_username() if s:cached_username == '' return [ -1, "Error getting user name when logging in." ] endif endif let tokenrec = {} let tokenrec.token = s:access_token let tokenrec.secret = s:access_token_secret let tokenrec.name = s:cached_username let tokenrec.service = s:cur_service call s:save_token(tokenrec) call s:write_tokens(s:cached_username) redraw echo "Logged in as ".s:cached_username." on ".s:get_svc_disp_name()."." return [ 0, '' ] endfunction function! s:run_curl_oauth_get(url, parms) return s:run_curl_oauth('GET', a:url, a:parms) endfunction function! s:run_curl_oauth_post(url, parms) return s:run_curl_oauth('POST', a:url, a:parms) endfunction " Sign a request with OAuth and send it. " In this version of run_curl_oauth, always specify the method: GET or POST. " Add all parameters to parms, not the url, even in GET calls. function! s:run_curl_oauth(method, url, parms) " The lower-level run_curl() still needs a dummy parameter to use POST. if a:method != 'GET' && a:parms == {} let a:parms.dummy = 'dummy1' endif let runurl = s:to_https(a:url) let runparms = a:parms if a:method == 'GET' let runparms = {} for [key, value] in items(a:parms) let runurl = s:add_to_url(runurl, key.'='.s:url_encode(value)) endfor endif if s:is_oauth() " Get access tokens from token file or do OAuth login. if !exists('s:access_token') || s:access_token == '' call s:read_tokens() if !exists('s:access_token') || s:access_token == '' let [ status, error ] = s:do_login() if status < 0 return [ error, '' ] endif endif endif let url = s:to_https(a:url) let parms = copy(a:parms) let parms.oauth_token = s:access_token let oauth_hdr = s:getOauthResponse(url, a:method, parms, s:access_token_secret) return s:run_curl(runurl, oauth_hdr, s:get_proxy(), s:get_proxy_login(), runparms) else let login = s:get_twitvim_login_noerror() if login == '' return [ 'Login info not set. Please add to vimrc: let twitvim_login="USER:PASS"', '' ] endif return s:run_curl(runurl, login, s:get_proxy(), s:get_proxy_login(), runparms) endif endfunction " === End of OAuth code === " === Networking code === function! s:url_encode_char(c) let utf = iconv(a:c, &encoding, "utf-8") if utf == "" let utf = a:c endif let s = "" for i in range(strlen(utf)) let s .= printf("%%%02X", char2nr(utf[i])) endfor return s endfunction " URL-encode a string. function! s:url_encode(str) return substitute(a:str, '[^a-zA-Z0-9_.~-]', '\=s:url_encode_char(submatch(0))', 'g') endfunction " URL-decode a string. function! s:url_decode(str) let s = substitute(a:str, '+', ' ', 'g') let s = substitute(s, '%\([a-zA-Z0-9]\{1,2}\)', '\=nr2char("0x".submatch(1))', 'g') let encoded = iconv(s, 'utf-8', &encoding) if encoded != '' let s = encoded endif return s endfunction " Use curl to fetch a web page. function! s:curl_curl(url, login, proxy, proxylogin, parms) let error = "" let output = "" let curlcmd = "curl -s -S " if s:get_twitvim_cert_insecure() let curlcmd .= "-k " endif let curlcmd .= '-m '.s:get_net_timeout().' ' if a:proxy != "" let curlcmd .= '-p -x "'.a:proxy.'" ' endif if a:proxylogin != "" if stridx(a:proxylogin, ':') != -1 let curlcmd .= '-U "'.a:proxylogin.'" ' else let curlcmd .= '-H "Proxy-Authorization: Basic '.a:proxylogin.'" ' endif endif if a:login != "" if a:login =~ "^OAuth " let curlcmd .= '-H "Authorization: '.a:login.'" ' elseif stridx(a:login, ':') != -1 let curlcmd .= '-u "'.a:login.'" ' else let curlcmd .= '-H "Authorization: Basic '.a:login.'" ' endif endif let got_json = 0 for [k, v] in items(a:parms) if k == '__json' let got_json = 1 let vsub = substitute(v, '"', '\\"', 'g') if has('win32') || has('win64') " Under Windows only, we need to quote some special characters. let vsub = substitute(vsub, '[\\&|><^]', '"&"', 'g') endif let curlcmd .= '-d "'.vsub.'" ' else let curlcmd .= '-d "'.s:url_encode(k).'='.s:url_encode(v).'" ' endif endfor if got_json let curlcmd .= '-H "Content-Type: application/json" ' endif let curlcmd .= '-H "User-Agent: '.s:user_agent.'" ' let curlcmd .= '"'.a:url.'"' let output = system(curlcmd) let errormsg = s:xml_get_element(output, 'error') if v:shell_error != 0 let error = output elseif errormsg != '' let error = errormsg endif return [ error, output ] endfunction " Check if we can use Python. function! s:check_python() let can_python = 1 python < 0: net_timeout = t except: pass try: socket.setdefaulttimeout(net_timeout) url = vim.eval("a:url") parms = vim.eval("a:parms") if parms.get('__json') is not None: req = urllib2.Request(url, parms['__json']) req.add_header('Content-Type', 'application/json') else: req = parms == {} and urllib2.Request(url) or urllib2.Request(url, urllib.urlencode(parms)) login = vim.eval("a:login") if login != "": if login[0:6] == "OAuth ": req.add_header('Authorization', login) else: req.add_header('Authorization', 'Basic %s' % make_base64(login)) proxy = vim.eval("a:proxy") if proxy != "": req.set_proxy(proxy, 'http') proxylogin = vim.eval("a:proxylogin") if proxylogin != "": req.add_header('Proxy-Authorization', 'Basic %s' % make_base64(proxylogin)) req.add_header('User-Agent', vim.eval("s:user_agent")) f = urllib2.urlopen(req) out = ''.join(f.readlines()) except urllib2.HTTPError, (httperr): vim.command("let error='%s'" % str(httperr).replace("'", "''")) vim.command("let output='%s'" % httperr.read().replace("'", "''")) except: exctype, value = sys.exc_info()[:2] errmsg = (exctype.__name__ + ': ' + str(value)).replace("'", "''") vim.command("let error='%s'" % errmsg) vim.command("let output='%s'" % errmsg) else: vim.command("let output='%s'" % out.replace("'", "''")) EOF return [ error, output ] endfunction " Check if we can use Python 3. function! s:check_python3() let can_python3 = 1 python3 < 0: net_timeout = t except: pass try: socket.setdefaulttimeout(net_timeout) # hello = make_base64("test:hello") url = vim.eval("a:url") parms = vim.eval("a:parms") if parms.get('__json') is not None: req = urllib.request.Request(url, str.encode(parms['__json'])) req.add_header('Content-Type', 'application/json') else: req = parms == {} and urllib.request.Request(url) or urllib.request.Request(url, str.encode(urllib.parse.urlencode(parms))) login = vim.eval("a:login") if login != "": if login[0:6] == "OAuth ": req.add_header('Authorization', login) else: req.add_header('Authorization', 'Basic %s' % make_base64(login)) proxy = vim.eval("a:proxy") if proxy != "": req.set_proxy(proxy, 'http') proxylogin = vim.eval("a:proxylogin") if proxylogin != "": req.add_header('Proxy-Authorization', 'Basic %s' % make_base64(proxylogin)) req.add_header('User-Agent', vim.eval("s:user_agent")) f = urllib.request.urlopen(req) out = ''.join([bytes.decode(s) for s in f.readlines()]) except urllib.error.HTTPError as httperr: vim.command("let error='%s'" % str(httperr).replace("'", "''")) vim.command("let output='%s'" % bytes.decode(httperr.read()).replace("'", "''")) except: exctype, value = sys.exc_info()[:2] errmsg = (exctype.__name__ + ': ' + str(value)).replace("'", "''") vim.command("let error='%s'" % errmsg) vim.command("let output='%s'" % errmsg) else: vim.command("let output='%s'" % out.replace("'", "''")) EOF return [ error, output ] endfunction " Check if we can use Perl. function! s:check_perl() let can_perl = 1 perl <import; require LWP::UserAgent; LWP::UserAgent->import; }; if ($@) { VIM::DoCommand('let can_perl = 0'); } EOF return can_perl endfunction " Use Perl to fetch a web page. function! s:perl_curl(url, login, proxy, proxylogin, parms) let error = "" let output = "" perl <import; require LWP::UserAgent; LWP::UserAgent->import; sub make_base64 { my $s = shift; $s =~ /:/ ? encode_base64($s) : $s; } my $ua = LWP::UserAgent->new; my $timeout = VIM::Eval('s:get_net_timeout()'); $ua->timeout($timeout); my $url = VIM::Eval('a:url'); my $proxy = VIM::Eval('a:proxy'); $proxy ne '' and $ua->proxy(['http', 'https'], "http://$proxy"); my $proxylogin = VIM::Eval('a:proxylogin'); $proxylogin ne '' and $ua->default_header('Proxy-Authorization' => 'Basic '.make_base64($proxylogin)); my %parms = (); my $keys = VIM::Eval('keys(a:parms)'); for $k (split(/\n/, $keys)) { $parms{$k} = VIM::Eval("a:parms['$k']"); } my $login = VIM::Eval('a:login'); if ($login ne '') { if ($login =~ /^OAuth /) { $ua->default_header('Authorization' => $login); } else { $ua->default_header('Authorization' => 'Basic '.make_base64($login)); } } $ua->default_header('User-Agent' => VIM::Eval("s:user_agent")); if (VIM::Eval('s:get_twitvim_cert_insecure()')) { $ua->ssl_opts(verify_hostname => 0); } my $response; if (defined $parms{'__json'}) { $response = $ua->post($url, 'Content-Type' => 'application/json', Content => $parms{'__json'}); } else { $response = %parms ? $ua->post($url, \%parms) : $ua->get($url); } if ($response->is_success) { my $output = $response->content; $output =~ s/'/''/g; VIM::DoCommand("let output ='$output'"); } else { my $output = $response->content; $output =~ s/'/''/g; VIM::DoCommand("let output ='$output'"); my $error = $response->status_line; $error =~ s/'/''/g; VIM::DoCommand("let error ='$error'"); } EOF return [ error, output ] endfunction " Check if we can use Ruby. " " Note: Before the networking code will function in Ruby under Windows, you " need the patch from here: " http://www.mail-archive.com/vim_dev@googlegroups.com/msg03693.html " " and Bram's correction to the patch from here: " http://www.mail-archive.com/vim_dev@googlegroups.com/msg03713.html " function! s:check_ruby() let can_ruby = 1 ruby < exc VIM.command("let error='#{exc.message}'") end EOF return [error, output] endfunction " Check if we can use Tcl. " " Note: ActiveTcl 8.5 doesn't include Tcllib in the download. You need to run the following after installing ActiveTcl: " " teacup install tcllib " function! s:check_tcl() let can_tcl = 1 tcl <= 0 } { return [base64::encode $s] } return $s } set url [::vim::expr a:url] if {[string tolower [string range $url 0 7]] == "https://"} { # Load and register support for https URLs. package require tls ::http::register https 443 ::tls::socket } set headers [list] ::http::config -proxyhost "" set proxy [::vim::expr a:proxy] if { $proxy != "" } { array set prox [uri::split "http://$proxy"] ::http::config -proxyhost $prox(host) ::http::config -proxyport $prox(port) } set proxylogin [::vim::expr a:proxylogin] if { $proxylogin != "" } { lappend headers "Proxy-Authorization" "Basic [make_base64 $proxylogin]" } set login [::vim::expr a:login] if { $login != "" } { if {[string range $login 0 5] == "OAuth "} { lappend headers "Authorization" $login } else { lappend headers "Authorization" "Basic [make_base64 $login]" } } lappend headers "User-Agent" [::vim::expr "s:user_agent"] set nettimeout [::vim::expr "s:get_net_timeout()"] set nettimeout [expr {round($nettimeout * 1000.0)}] set parms [list] set keys [split [::vim::expr "keys(a:parms)"] "\n"] if { [llength $keys] > 0 } { if { [lsearch -exact $keys "__json"] != -1 } { set query [::vim::expr "a:parms\['__json']"] lappend headers "Content-Type" "application/json" } else { foreach key $keys { lappend parms $key [::vim::expr "a:parms\['$key']"] } set query [eval [concat ::http::formatQuery $parms]] } set res [::http::geturl $url -headers $headers -query $query -timeout $nettimeout] } else { set res [::http::geturl $url -headers $headers -timeout $nettimeout] } upvar #0 $res state if { $state(status) == "ok" } { if { [ ::http::ncode $res ] >= 400 } { set error $state(http) ::vim::command "let error = '$error'" set output [string map {' ''} $state(body)] ::vim::command "let output = '$output'" } else { set output [string map {' ''} $state(body)] ::vim::command "let output = '$output'" } } else { if { [ info exists state(error) ] } { set error [string map {' ''} $state(error)] } else { set error "$state(status) error" } ::vim::command "let error = '$error'" } ::http::cleanup $res EOF return [error, output] endfunction " Find out which method we can use to fetch a web page. function! s:get_curl_method() if !exists('s:curl_method') let s:curl_method = 'curl' if s:get_enable_perl() && has('perl') && s:check_perl() let s:curl_method = 'perl' elseif s:get_enable_python() && has('python') && s:check_python() let s:curl_method = 'python' elseif s:get_enable_python3() && has('python3') && s:check_python3() let s:curl_method = 'python3' elseif s:get_enable_ruby() && has('ruby') && s:check_ruby() let s:curl_method = 'ruby' elseif s:get_enable_tcl() && has('tcl') && s:check_tcl() let s:curl_method = 'tcl' endif endif return s:curl_method endfunction " We need to convert our parameters to UTF-8. In curl_curl() this is already " handled as part of our url_encode() function, so we only need to do this for " other net methods. Also, of course, we don't have to do anything if the " encoding is already UTF-8. function! s:iconv_parms(parms) if s:get_curl_method() == 'curl' || &encoding == 'utf-8' return a:parms endif let parms2 = {} for k in keys(a:parms) let v = iconv(a:parms[k], &encoding, 'utf-8') if v == '' let v = a:parms[k] endif let parms2[k] = v endfor return parms2 endfunction function! s:run_curl(url, login, proxy, proxylogin, parms) return s:{s:get_curl_method()}_curl(a:url, a:login, a:proxy, a:proxylogin, s:iconv_parms(a:parms)) endfunction function! s:reset_curl_method() unlet! s:curl_method endfunction function! s:show_curl_method() echo 'Net Method:' s:get_curl_method() endfunction " For debugging. Reset networking method. if !exists(":TwitVimResetMethod") command TwitVimResetMethod :call reset_curl_method() endif " For debugging. Show current networking method. if !exists(":TwitVimShowMethod") command TwitVimShowMethod :call show_curl_method() endif " === End of networking code === " === Buffer stack code === " Each buffer record holds the following fields: " " buftype: Buffer type = dmrecv, dmsent, search, public, friends, user, " replies, list, retweeted_by_me, retweeted_to_me, favorites, trends " user: For user buffers if other than current user " list: List slug if displaying a Twitter list. " page: Keep track of pagination. " statuses: Tweet IDs. For use by in_reply_to_status_id " inreplyto: IDs of predecessor messages for @-replies. " dmids: Direct Message IDs. (for buftype dmrecv or dmsent) " buffer: The buffer text. " view: viewport saved with winsaveview() " showheader: 1 if header is shown in this buffer, 0 if header is hidden. let s:curbuffer = {} " The info buffer record holds the following fields: " " buftype: profile, friends, followers, listmembers, listsubs, userlists, " userlistmem, userlistsubs, listinfo " next_cursor: Used for paging. " prev_cursor: Used for paging. " cursor: Used for refresh. " user: User name " list: List name " buffer: The buffer text. " view: viewport saved with winsaveview() " showheader: 1 if header is shown in this buffer, 0 if header is hidden. " " flist: List of friends/followers IDs. " findex: Starting index within flist of the friends/followers info displayed " in this buffer. let s:infobuffer = {} " ptr = Buffer stack pointer. -1 if no items yet. May not point to the end of " the list if user has gone back one or more buffers. let s:bufstack = { 'ptr': -1, 'stack': [] } let s:infobufstack = { 'ptr': -1, 'stack': [] } " Maximum items in the buffer stack. Adding a new item after this limit will " get rid of the first item. let s:bufstackmax = 10 " Add current buffer to the buffer stack at the next position after current. " Remove all buffers after that. function! s:add_buffer(infobuf) let stack = a:infobuf ? s:infobufstack : s:bufstack let cur = a:infobuf ? s:infobuffer : s:curbuffer " If stack is already full, remove the buffer at the bottom of the stack to " make room. if stack.ptr >= s:bufstackmax call remove(stack.stack, 0) let stack.ptr -= 1 endif let stack.ptr += 1 " Suppress errors because there may not be anything to remove after current " position. silent! call remove(stack.stack, stack.ptr, -1) call add(stack.stack, cur) endfunction " Check if two buffers show the same info based on attributes. function! s:is_same(infobuf, a, b) let a = a:a let b = a:b if a:infobuf if a.buftype == b.buftype && a.cursor == b.cursor && a.user == b.user && a.list == b.list return 1 endif else if a.buftype == b.buftype && a.list == b.list && a.user == b.user && a.page == b.page return 1 endif endif return 0 endfunction " If current buffer is same type as the buffer at the buffer stack pointer then " just copy it into the buffer stack. Otherwise, add it to buffer stack. function! s:save_buffer(infobuf) let stack = a:infobuf ? s:infobufstack : s:bufstack let cur = a:infobuf ? s:infobuffer : s:curbuffer let winname = a:infobuf ? s:user_winname : s:twit_winname if cur == {} return endif " Save buffer contents and cursor position. let twit_bufnr = bufwinnr('^'.winname.'$') if twit_bufnr > 0 let curwin = winnr() execute twit_bufnr . "wincmd w" let cur.buffer = getline(1, '$') let cur.view = winsaveview() execute curwin . "wincmd w" " If current buffer is the same type as buffer at the top of the stack, " then just copy it. if stack.ptr >= 0 && s:is_same(a:infobuf, cur, stack.stack[stack.ptr]) let stack.stack[stack.ptr] = deepcopy(cur) else " Otherwise, push the current buffer onto the stack. call s:add_buffer(a:infobuf) endif endif " If twit_bufnr returned -1, the user closed the window manually. So we " have nothing to save. Do not alter the buffer stack. endfunction " Go back one buffer in the buffer stack. function! s:back_buffer(infobuf) let stack = a:infobuf ? s:infobufstack : s:bufstack call s:save_buffer(a:infobuf) if stack.ptr < 1 call s:warnmsg("Already at oldest buffer. Can't go back further.") return -1 endif let stack.ptr -= 1 if a:infobuf let s:infobuffer = deepcopy(stack.stack[stack.ptr]) else let s:curbuffer = deepcopy(stack.stack[stack.ptr]) endif let cur = a:infobuf ? s:infobuffer : s:curbuffer let wintype = a:infobuf ? 'userinfo' : 'timeline' call s:twitter_wintext_view(cur.buffer, wintype, cur.view) return 0 endfunction " Go forward one buffer in the buffer stack. function! s:fwd_buffer(infobuf) let stack = a:infobuf ? s:infobufstack : s:bufstack call s:save_buffer(a:infobuf) if stack.ptr + 1 >= len(stack.stack) call s:warnmsg("Already at newest buffer. Can't go forward.") return -1 endif let stack.ptr += 1 if a:infobuf let s:infobuffer = deepcopy(stack.stack[stack.ptr]) else let s:curbuffer = deepcopy(stack.stack[stack.ptr]) endif let cur = a:infobuf ? s:infobuffer : s:curbuffer let wintype = a:infobuf ? 'userinfo' : 'timeline' call s:twitter_wintext_view(cur.buffer, wintype, cur.view) return 0 endfunction if !exists(":BackTwitter") command BackTwitter :call back_buffer(0) endif if !exists(":ForwardTwitter") command ForwardTwitter :call fwd_buffer(0) endif if !exists(":BackInfoTwitter") command BackInfoTwitter :call back_buffer(1) endif if !exists(":ForwardInfoTwitter") command ForwardInfoTwitter :call fwd_buffer(1) endif " For debugging. Show the buffer stack. function! s:show_bufstack(infobuf) let stack = a:infobuf ? s:infobufstack : s:bufstack for i in range(len(stack.stack) - 1, 0, -1) let s = i.':' let s .= ' type='.stack.stack[i].buftype let s .= ' user='.stack.stack[i].user let s .= ' list='.stack.stack[i].list if a:infobuf let s .= ' cursor='.stack.stack[i].cursor else let s .= ' page='.stack.stack[i].page endif echo s endfor endfunction if !exists(":TwitVimShowBufstack") command TwitVimShowBufstack :call show_bufstack(0) endif if !exists(":TwitVimShowInfoBufstack") command TwitVimShowInfoBufstack :call show_bufstack(1) endif " For debugging. Show curbuffer variable. if !exists(":TwitVimShowCurbuffer") command TwitVimShowCurbuffer :echo s:curbuffer endif " For debugging. Show infobuffer variable. if !exists(":TwitVimShowInfobuffer") command TwitVimShowInfobuffer :echo s:infobuffer endif " === End of buffer stack code === " Add update to Twitter buffer if public, friends, or user timeline. function! s:add_update(result) if has_key(s:curbuffer, 'buftype') && (s:curbuffer.buftype == "public" || s:curbuffer.buftype == "friends" || s:curbuffer.buftype == "user" || s:curbuffer.buftype == "replies" || s:curbuffer.buftype == "list" || s:curbuffer.buftype == "retweeted_by_me" || s:curbuffer.buftype == "retweeted_to_me") " Parse the output from the Twitter update call. let line = s:format_status_json(a:result) " Line number where new tweet will be inserted. It should be 3 if " header is shown and 1 if header is hidden. let insline = s:curbuffer.showheader ? 3 : 1 " Add the status ID to the current buffer's statuses list. call insert(s:curbuffer.statuses, get(a:result, 'id_str', get(a:result, 'id', '')), insline) " Add in-reply-to ID to current buffer's in-reply-to list. call insert(s:curbuffer.inreplyto, get(a:result, 'in_reply_to_status_id_str', get(a:result, 'in_reply_to_status_id', '')), insline) let twit_bufnr = bufwinnr('^'.s:twit_winname.'$') if twit_bufnr > 0 let curwin = winnr() execute twit_bufnr . "wincmd w" setlocal modifiable call append(insline - 1, line) execute "normal! ".insline."G" setlocal nomodifiable let s:curbuffer.buffer = getline(1, '$') execute curwin . "wincmd w" endif endif endfunction " Count number of characters in a multibyte string. Use technique from " :help strlen(). function! s:mbstrlen(s) return strlen(substitute(a:s, ".", "x", "g")) endfunction let s:short_url_length = 0 let s:short_url_length_https = 0 let s:last_config_query_time = 0 " Get Twitter short URL lengths. function! s:get_short_url_lengths() let now = localtime() " Do the config query the first time it is needed and once a day thereafter. if s:short_url_length == 0 || s:short_url_length_https == 0 || now - s:last_config_query_time > 24 * 60 * 60 let url = s:get_api_root().'/help/configuration.json' let [error, output] = s:run_curl_oauth_get(url, {}) let result = s:parse_json(output) if error == '' let s:short_url_length = get(result, 'short_url_length', 0) let s:short_url_length_https = get(result, 'short_url_length_https', 0) let s:last_config_query_time = now endif endif return [ s:short_url_length, s:short_url_length_https ] endfunction " Simulate Twitter's URL shortener by replacing any matching URLs with dummy strings. function! s:sim_shorten_urls(mesg) let [url_len, secure_url_len] = s:get_short_url_lengths() let mesg = a:mesg if url_len > 0 && secure_url_len > 0 let mesg = substitute(mesg, s:URLMATCH_HTTPS, repeat('*', secure_url_len), 'g') let mesg = substitute(mesg, s:URLMATCH_NON_HTTPS, repeat('*', url_len), 'g') endif return mesg endfunction " Common code to post a message to Twitter. function! s:post_twitter(mesg, inreplyto) let parms = {} " Add in_reply_to_status_id if status ID is available. if a:inreplyto != 0 let parms["in_reply_to_status_id"] = a:inreplyto endif let mesg = a:mesg " Remove trailing newline. You see that when you visual-select an entire " line. Don't let it count towards the tweet length. let mesg = substitute(mesg, '\n$', '', "") " Convert internal newlines to spaces. let mesg = substitute(mesg, '\n', ' ', "g") let mesglen = s:mbstrlen(mesg) " Check for zero-length tweets or user cancel at prompt. if mesglen < 1 call s:warnmsg("Your tweet was empty. It was not sent.") return end " Only Twitter has a built-in URL wrapper thus far. if s:get_cur_service() == 'twitter' " Pretend to shorten URLs. let sim_mesg = s:sim_shorten_urls(mesg) else " Assume that identi.ca and other non-Twitter services don't do this " URL-shortening madness. let sim_mesg = mesg endif let mesglen = s:mbstrlen(sim_mesg) " Check tweet length. Note that the tweet length should be checked before " URL-encoding the special characters because URL-encoding increases the " string length. if mesglen > s:char_limit call s:warnmsg("Your tweet has ".(mesglen - s:char_limit)." too many characters. It was not sent.") else redraw echo "Posting update..." let url = s:get_api_root()."/statuses/update.json" let parms["status"] = mesg let parms["source"] = "twitvim" let parms["include_entities"] = "true" let [error, output] = s:run_curl_oauth_post(url, parms) let result = s:parse_json(output) if error != '' let errormsg = get(result, 'error', '') call s:errormsg("Error posting your tweet: ".(errormsg != '' ? errormsg : error)) else call s:add_update(result) redraw echo "Your tweet was sent. You used ".mesglen." characters." endif endif endfunction " Prompt user for tweet and then post it. " If initstr is given, use that as the initial input. function! s:CmdLine_Twitter(initstr, inreplyto) call inputsave() redraw let mesg = input("Tweet: ", a:initstr) call inputrestore() call s:post_twitter(mesg, a:inreplyto) endfunction " Extract the user name from a line in the timeline. function! s:get_user_name(line) let line = substitute(a:line, '^+ ', '', '') let matchres = matchlist(line, '^\(\w\+\):') return matchres != [] ? matchres[1] : "" endfunction " This is for a local mapping in the timeline. Start an @-reply on the command " line to the author of the tweet on the current line. function! s:Quick_Reply() let username = s:get_user_name(getline('.')) if username != "" " If the status ID is not available, get() will return 0 and " post_twitter() won't add in_reply_to_status_id to the update. call s:CmdLine_Twitter('@'.username.' ', get(s:curbuffer.statuses, line('.'))) endif endfunction " Extract all user names from a line in the timeline. Return the poster's name as well as names from all the @replies. function! s:get_all_names(line) let names = [] let dictnames = {} let username = s:get_user_name(getline('.')) if username != "" " Add this to the beginning of the list because we want the tweet " author to be the main addressee in the reply to all. let names = [ username ] let dictnames[tolower(username)] = 1 endif let matchcount = 1 while 1 let matchres = matchlist(a:line, '@\(\w\+\)', -1, matchcount) if matchres == [] break endif let name = matchres[1] " Don't add duplicate names. if !has_key(dictnames, tolower(name)) call add(names, name) let dictnames[tolower(name)] = 1 endif let matchcount += 1 endwhile return names endfunction " Reply to everyone mentioned on a line in the timeline. function! s:Reply_All() let names = s:get_all_names(getline('.')) " Remove the author from the reply list so that he doesn't end up replying " to himself. let user = s:get_twitvim_username() let replystr = '@'.join(filter(names, 'v:val !=? user'), ' @').' ' if names != [] " If the status ID is not available, get() will return 0 and " post_twitter() won't add in_reply_to_status_id to the update. call s:CmdLine_Twitter(replystr, get(s:curbuffer.statuses, line('.'))) endif endfunction " This is for a local mapping in the timeline. Start a direct message on the " command line to the author of the tweet on the current line. function! s:Quick_DM() let username = s:get_user_name(getline('.')) if username != "" " call s:CmdLine_Twitter('d '.username.' ', 0) call s:send_dm(username, '') endif endfunction " Allow user to switch to old-style retweets by setting twitvim_old_retweet. function! s:get_old_retweet() return exists('g:twitvim_old_retweet') ? g:twitvim_old_retweet : 0 endfunction " Extract the tweet text from a timeline buffer line. function! s:get_tweet(line) let line = substitute(a:line, '^\w\+:\s\+', '', '') let line = substitute(line, '\s\+|[^|]\+|$', '', '') " Remove newlines. let line = substitute(line, "\n", '', 'g') return line endfunction " Retweet is for replicating a tweet from another user. function! s:Retweet() let line = getline('.') let username = s:get_user_name(line) if username != "" let retweet = substitute(s:get_retweet_fmt(), '%s', '@'.username, '') let retweet = substitute(retweet, '%t', '\=s:get_tweet(line)', '') " From @mattn_jp: Add in-reply-to status ID to old-style retweet. call s:CmdLine_Twitter(retweet, get(s:curbuffer.statuses, line('.'))) endif endfunction " Use new-style retweet API to retweet a tweet from another user. function! s:Retweet_2() " Do an old-style retweet if user has set twitvim_old_retweet. if s:get_old_retweet() call s:Retweet() return endif let status = get(s:curbuffer.statuses, line('.')) if status == 0 " Fall back to old-style retweeting if we can't get this tweet's status " ID. call s:Retweet() return endif " Confirm with user before retweeting. Only for new-style retweets because " old-style retweets have their own prompt. call inputsave() let answer = input('Retweet "'.s:strtrunc(getline('.'), 40).'"? (y/n) ') call inputrestore() if answer != 'y' && answer != 'Y' redraw echo "Not retweeted." return endif let url = s:get_api_root()."/statuses/retweet/".status.".json" redraw echo "Retweeting..." let [error, output] = s:run_curl_oauth_post(url, {}) let result = s:parse_json(output) if error != '' let errormsg = get(result, 'error', '') call s:errormsg("Error retweeting: ".(errormsg != '' ? errormsg : error)) else call s:add_update(result) redraw echo "Retweeted." endif endfunction " Show which tweet this one is replying to below the current line. function! s:show_inreplyto() let lineno = line('.') let inreplyto = get(s:curbuffer.inreplyto, lineno) if inreplyto == 0 call s:warnmsg("No in-reply-to information for current line.") return endif redraw echo "Querying for in-reply-to tweet..." let url = s:get_api_root()."/statuses/show/".inreplyto.".json" let [error, output] = s:run_curl_oauth_get(url, { 'include_entities' : 'true' }) let result = s:parse_json(output) if error != '' let errormsg = get(result, 'error', '') call s:errormsg("Error getting in-reply-to tweet: ".(errormsg != '' ? errormsg : error)) return endif let line = s:format_status_json(result) " Add the status ID to the current buffer's statuses list. call insert(s:curbuffer.statuses, get(result, 'id_str', get(result, 'id', '')), lineno + 1) " Add in-reply-to ID to current buffer's in-reply-to list. call insert(s:curbuffer.inreplyto, get(result, 'in_reply_to_status_id_str', get(result, 'in_reply_to_status_id', '')), lineno + 1) " Already in the correct buffer so no need to search or switch buffers. setlocal modifiable call append(lineno, '+ '.line) setlocal nomodifiable let s:curbuffer.buffer = getline(1, '$') redraw echo "In-reply-to tweet found." endfunction " Truncate a string. Add '...' to the end of string was longer than " the specified number of characters. function! s:strtrunc(s, len) let slen = strlen(substitute(a:s, ".", "x", "g")) let s = substitute(a:s, '^\(.\{,'.a:len.'}\).*$', '\1', '') if slen > a:len let s .= '...' endif return s endfunction " Delete tweet or DM on current line. function! s:do_delete_tweet() let lineno = line('.') let isdm = (s:curbuffer.buftype == "dmrecv" || s:curbuffer.buftype == "dmsent") let obj = isdm ? "message" : "tweet" let uobj = isdm ? "Message" : "Tweet" let id = get(isdm ? s:curbuffer.dmids : s:curbuffer.statuses, lineno) let url = s:get_api_root().'/'.(isdm ? "direct_messages" : "statuses")."/destroy/".id.".json" let [error, output] = s:run_curl_oauth_post(url, {}) let result = s:parse_json(output) if error != '' let errormsg = get(result, 'error', '') call s:errormsg("Error deleting ".obj.": ".(errormsg != '' ? errormsg : error)) return endif if isdm call remove(s:curbuffer.dmids, lineno) else call remove(s:curbuffer.statuses, lineno) call remove(s:curbuffer.inreplyto, lineno) endif " Already in the correct buffer so no need to search or switch buffers. setlocal modifiable normal! dd setlocal nomodifiable let s:curbuffer.buffer = getline(1, '$') redraw echo uobj "deleted." endfunction " Delete tweet or DM on current line. function! s:delete_tweet() let lineno = line('.') let isdm = (s:curbuffer.buftype == "dmrecv" || s:curbuffer.buftype == "dmsent") let obj = isdm ? "message" : "tweet" let uobj = isdm ? "Message" : "Tweet" let id = get(isdm ? s:curbuffer.dmids : s:curbuffer.statuses, lineno) if id == 0 call s:warnmsg("No erasable ".obj." on current line.") return endif call inputsave() let answer = input('Delete "'.s:strtrunc(getline('.'), 40).'"? (y/n) ') call inputrestore() if answer == 'y' || answer == 'Y' call s:do_delete_tweet() else redraw echo uobj "not deleted." endif endfunction " Fave or Unfave tweet on current line. function! s:fave_tweet(unfave) let id = get(s:curbuffer.statuses, line('.')) if id == 0 call s:warnmsg('Nothing to '.(a:unfave ? 'unfavorite' : 'favorite').' on current line.') return endif redraw echo (a:unfave ? 'Unfavoriting' : 'Favoriting') 'the tweet...' if s:get_cur_service() == 'twitter' let url = s:get_api_root().'/favorites/'.(a:unfave ? 'destroy' : 'create').'.json' else let url = s:get_api_root().'/favorites/'.(a:unfave ? 'destroy' : 'create').'/'.id.'.json' endif let [error, output] = s:run_curl_oauth_post(url, { 'id' : id }) let result = s:parse_json(output) if error != '' let errormsg = get(result, 'error', '') call s:errormsg("Error ".(a:unfave ? 'unfavoriting' : 'favoriting')." the tweet: ".(errormsg != '' ? errormsg : error)) return endif redraw echo 'Tweet' (a:unfave ? 'unfavorited.' : 'favorited.') endfunction " Prompt user for tweet. if !exists(":PosttoTwitter") command PosttoTwitter :call CmdLine_Twitter('', 0) endif nnoremenu Plugin.TwitVim.Post\ from\ cmdline :call CmdLine_Twitter('', 0) " Post current line to Twitter. if !exists(":CPosttoTwitter") command CPosttoTwitter :call post_twitter(getline('.'), 0) endif nnoremenu Plugin.TwitVim.Post\ current\ line :call post_twitter(getline('.'), 0) " Post entire buffer to Twitter. if !exists(":BPosttoTwitter") command BPosttoTwitter :call post_twitter(join(getline(1, "$")), 0) endif " Post visual selection to Twitter. noremap Visual y:call post_twitter(@", 0) noremap