#!/usr/bin/ruby require 'socket' require 'libhttpclient' require 'openssl' require 'time' class Twitter def initialize @oauth = {} # TODO fill this with your consumer key & consumer secret (check http://twitter/ login / api / register my application) # then run the 'twit.rb register' command, and add the given credentials here before you do anything else. @oauth[:consumer_key], @oauth[:consumer_secret], @oauth[:token], @oauth[:token_secret] = <bar => def fold_xml(parse) lastag = nil parse.delete_if { |tag| if tag.type == 'String'; lastag['str'] = twit_decode_html(tag['content']) ; true elsif lastag and tag.type == '/'+lastag.type; lastag = nil; true else lastag = tag; false end } end def twit_decode_html(str) HttpServer.htmlentitiesdec(HttpServer.htmlentitiesdec(str)).gsub(/&#(x?\d+);/) { # utf-8 encode v = ($1[0] == ?x) ? $1[1..-1].to_i(16) : $1.to_i next v.chr if v <= 255 next 'lol' if v > 0x1fffff raw = '' limit = 0x3f while v > limit raw << (0x80 | (v & 0x3f)) v >>= 6 limit >>= 1 end len = raw.length+1 raw << (((0xff << (8-len)) & 0xff) | v) raw.reverse! } end def date2delay(date) dt = (Time.now - date).to_i if dt > 3600*36; " il y a #{dt/3600/24}j" elsif dt > 3600; " il y a #{dt/3600}h#{(dt%3600)/60}" elsif dt > 15*60; " il y a #{dt/60}mn" elsif dt > 2*60; " il y a #{dt/60}mn#{dt%60}" end end def show_twitter(name) rss = parsehtml oauth_get('/1/statuses/user_timeline.rss', 'screen_name' => name).content, true fold_xml(rss) date = text = nil rss.reverse_each { |tag| val = tag['str'] case tag.type when 'pubdate'; date = Time.parse(val) when 'description'; text = val when 'item' puts "#{text.inspect[1...-1]}#{date2delay(date)}" end } end def poll_twitter rss = parsehtml oauth_get('/1/statuses/home_timeline.rss').content, true fold_xml(rss) good = date = text = nil rss.reverse_each { |tag| val = tag['str'] case tag.type when 'link'; good = (val and !val.include?("/#{account}/")) when 'pubdate'; date = Time.parse(val) when 'description'; text = val when 'item' if good puts "#{text.inspect[1...-1]}#{date2delay(date)}" end end } puts rpl = parsehtml oauth_get('/1/statuses/mentions.xml').content, true fold_xml(rpl) date = text = user = fol = nil rpl.reverse_each { |tag| val = tag['str'] case tag.type when 'screen_name'; user = val when 'following'; fol = val when 'text'; text = val when 'created_at'; date = Time.parse(val) when 'status' if fol != 'true' puts "#{user}: #{text.inspect[1...-1]}#{date2delay(date)}" end end } end def post_update(upd) msg = auto2utf(upd) pg = oauth_post('/1/statuses/update.xml', 'status' => msg) puts(pg.status == 200 ? "http://twitter.com/#{account}" : 'fail') end def post_retwit(name, upd) msg = auto2utf(upd) rss = parsehtml oauth_get('/1/statuses/user_timeline.rss', 'screen_name' => name).content, true fold_xml(rss) rt_id = nil id = text = nil rss.reverse_each { |tag| val = tag['str'] case tag.type when 'description'; text = val when 'link'; id = val[/statuses\/([0-9]+)/, 1] when 'item' if text.include?(msg) rt_id = id break end end } if rt_id pg = oauth_post("/1/statuses/retweet/#{rt_id}.xml") puts(pg.status == 200 ? text : 'fail') else puts "cant find original message" end end def follow(n) pg = oauth_post("/1/friendships/create.xml", 'screen_name' => n, 'follow' => 'true') puts(pg.status == 200 ? 'ok' : 'fail') end def nofollow(n) pg = oauth_post("/1/friendships/destroy.xml", 'screen_name' => n) puts(pg.status == 200 ? 'ok' : 'fail') end # http get to a oauth-enabled server # url should be the base url, with request parameters passed as a hash # e.g. to get /foo/bar?a=b&c=d, use oauth_get("/foo/bar", "a" => "b", "c" => "d") # (this is needed for the oauth signature) def oauth_get(url, parms={}) pdata = parms.map { |k, v| v ? oauth_escape(k) + '=' + oauth_escape(v) : oauth_escape(k) }.join('&') hdrs = oauth_hdr('GET', url, parms) url += '?' + pdata if pdata != '' HttpClient.open("https://api.twitter.com/") { |hc| hc.get(url, nil, hdrs) } end # post to a oauth-enabled server # XXX we append the post data to the url (and still send as POST form) to work with the twitter website, # but this is contrary to the OAuth RFC (from my understanding) def oauth_post(url, parms={}) pdata = parms.map { |k, v| oauth_escape(k) + '=' + oauth_escape(v) }.join('&') hdrs = oauth_hdr('POST', url, parms).merge('Content-type' => 'application/x-www-form-encoded') url += '?' + pdata if pdata != '' # XXX twitter-specific workaround HttpClient.open("https://api.twitter.com/") { |hc| hc.post_raw(url, pdata, hdrs) } end # return the OAuth Authorization header def oauth_hdr(method, url, parms={}) oauth = oauth_parms(method, url, parms) { 'Authorization' => 'OAuth ' + oauth.map { |k, v| "#{k}=\"#{oauth_escape(v)}\"" }.join(",\n ") } end # return the oauth params hash (to be used in the authorization header / get params) # from the method, pure url (no query parameters), and request parameters # additionnal 'oauth_' header entries can be specified in the @oauth[:oauth_supp] hash (deleted here after use) def oauth_parms(method, url, parms, base_url='https://api.twitter.com') base_str = method.upcase + '&' + oauth_escape(base_url.downcase + url) + '&' oauth = { 'oauth_consumer_key' => @oauth[:consumer_key], 'oauth_token' => @oauth[:token], 'oauth_nonce' => rand(1<<32).to_s(16), 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_timestamp' => (Time.now.to_i + rand(180)-90) } oauth.update @oauth.delete(:oauth_supp) if @oauth[:oauth_supp] parms = oauth.merge parms bdata = parms.to_a #bdata += @oauth[:getp].to_a if @oauth[:getp] # copy of the url parameters when POSTing # get all request param, sorted, encode them individually, then reencode the full string base_str += oauth_escape(bdata.sort.map { |k, v| oauth_escape(k) + '=' + oauth_escape(v) }.join('&')) oauth.merge 'oauth_signature' => oauth_hmacsha1(base_str) end # oauth-specific url encoding (needed for crypto signature correctness) def oauth_escape(str) str.to_s.gsub(/[^a-zA-Z0-9_~.-]/) { |o| '%%%02X' % o.unpack('C') } end def oauth_hmacsha1(text) key = oauth_escape(@oauth[:consumer_secret]) + '&' + oauth_escape(@oauth[:token_secret]) mac = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), key, text) [mac].pack('m*').split.join # base64 encode end # to create your oauth parameters: log on twitter, click the 'api' link, create your application # initialize the @oauth hash with the provided consummer token/secret # run this function, it will query the website for a temporary token, and direct you to an url # visit the url, accept, and paste the pin code to the prompt, this will create the oauth_creds file # with the user credentials. def oauth_register_new_user @oauth[:token] = @oauth[:token_secret] = '' @oauth[:oauth_supp] = { 'oauth_callback' => 'oob' } ans = oauth_post('/oauth/request_token') if ans.status != 200 puts ans, "failed to request temporary token :(" return end foo = ans.content.split('&').inject({}) { |h, s| h.update Hash[*s.split('=', 2)] } @oauth[:token] = foo['oauth_token'] puts "Please visit https://api.twitter.com/oauth/authorize?oauth_token=#{@oauth[:token]}" puts "Pin code?" pin = $stdin.gets.chomp @oauth[:oauth_supp] = { 'oauth_verifier' => pin } ans = oauth_post('/oauth/access_token') if ans.status != 200 puts ans, "failed to request user token - bad pin ?" return end foo = ans.content.split('&').inject({}) { |h, s| h.update Hash[*s.split('=', 2)] } @oauth[:token] = foo['oauth_token'] @oauth[:token_secret] = foo['oauth_token_secret'] puts @oauth[:consumer_key], @oauth[:consumer_secret], @oauth[:token], @oauth[:token_secret] end # take a string, convert it to utf8 if it is not already # works pretty well for iso-8859-1, untested with others def auto2utf(s) b = s.unpack('C*') if b.find { |c| c >= 0x80 } and not b.find { |c| c & 0xc0 == 0x80 } b.map { |b| b >= 0x80 ? [0xc0 | ((b & 0xc0) >> 6), 0x80 | (b & 0x3f)] : b }.flatten.pack('C*') else s end end end if $0 == __FILE__ t = Twitter.new case n = ARGV.shift when nil, '' t.poll_twitter when 'follow' ARGV.each { |f| t.follow f } when 'nofollow' ARGV.each { |f| t.nofollow f } when 'register' t.oauth_register_new_user when 'post' if ARGV[0] == 'RT' ARGV.shift n = ARGV.shift[/@?(\S+):?/, 1] t.post_retwit(n, ARGV.join(' ')) else t.post_update ARGV.join(' ') end else t.show_twitter n end end