]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/twitter.rb
chucknorris: fix loading
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / twitter.rb
index 1d86472d940abbeb28b0c57456c94662888a64e8..3ed8e62c1c432ea3e9303550ed2adc8fb4ca3b45 100644 (file)
@@ -6,6 +6,7 @@
 # Author:: Carter Parks (carterparks) <carter@carterparks.com>
 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
 # Author:: NeoLobster <neolobster@snugglenets.com>
 # Author:: Carter Parks (carterparks) <carter@carterparks.com>
 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
 # Author:: NeoLobster <neolobster@snugglenets.com>
+# Author:: Matthias Hecker <apoc@sixserv.org>
 #
 # Copyright:: (C) 2007 Carter Parks
 # Copyright:: (C) 2007 Giuseppe Bilotta
 #
 # Copyright:: (C) 2007 Carter Parks
 # Copyright:: (C) 2007 Giuseppe Bilotta
 # twitter whenever
 
 require 'oauth'
 # twitter whenever
 
 require 'oauth'
+require 'oauth2'
 require 'yaml'
 require 'yaml'
-require 'rexml/rexml'
+require 'json'
 
 class TwitterPlugin < Plugin
 
 class TwitterPlugin < Plugin
-   Config.register Config::StringValue.new('twitter.key',
-      :default => "",
-      :desc => "Twitter OAuth Consumer Key")
+  URL = 'https://api.twitter.com'
 
 
-   Config.register Config::StringValue.new('twitter.secret',
-      :default => "",
-      :desc => "Twitter OAuth Consumer Secret")
+  Config.register Config::StringValue.new('twitter.key',
+    :default => "BdCN4FCokm9hkf8sIDmIJA",
+    :desc => "Twitter OAuth Consumer Key")
 
 
-    Config.register Config::IntegerValue.new('twitter.status_count',
-      :default => 1, :validate => Proc.new { |v| v > 0 && v <= 10},
-      :desc => "Maximum number of status updates shown by 'twitter status'")
+  Config.register Config::StringValue.new('twitter.secret',
+    :default => "R4V00wUdEXlMr38SKOQR9UFQLqAmc3P7cpft7ohuqo",
+    :desc => "Twitter OAuth Consumer Secret")
 
 
-    Config.register Config::IntegerValue.new('twitter.friends_status_count',
-      :default => 3, :validate => Proc.new { |v| v > 0 && v <= 10},
-      :desc => "Maximum number of status updates shown by 'twitter friends status'")
+  Config.register Config::IntegerValue.new('twitter.status_count',
+    :default => 1, :validate => Proc.new { |v| v > 0 && v <= 10},
+    :desc => "Maximum number of status updates shown by 'twitter status'")
+
+  Config.register Config::IntegerValue.new('twitter.timeline_status_count',
+    :default => 3, :validate => Proc.new { |v| v > 0 && v <= 10},
+    :desc => "Maximum number of status updates shown by 'twitter [home|mentions|retweets] status'")
+
+  URL_PATTERN = %r{twitter\.com/([^/]+)(?:/status/(\d+))?}
+
+  def twitter_filter(s)
+    loc = Utils.check_location(s, URL_PATTERN)
+    return nil unless loc
+    matches = loc.first.match URL_PATTERN
+    if matches[2] # status id matched
+      id = matches[2]
+      url = '/1.1/statuses/show/%s.json' % id
+    else # no status id, get the latest status of that user
+      user = matches[1]
+      url = '/1.1/statuses/user_timeline.json?screen_name=%s&count=1&include_rts=true' % user
+    end
+    response = @app_access_token.get(url).body
+    begin
+      tweet = JSON.parse(response)
+      tweet = tweet.first if tweet.instance_of? Array
+      status = {
+        :date => (Time.parse(tweet["created_at"]) rescue "<unknown>"),
+        :id => (tweet["id_str"] rescue "<unknown>"),
+        :text => (tweet["text"].ircify_html rescue "<error>"),
+        :source => (tweet["source"].ircify_html rescue "<unknown>"),
+        :user => (tweet["user"]["name"] rescue "<unknown>"),
+        :user_nick => (tweet["user"]["screen_name"] rescue "<unknown>")
+        # TODO other entries
+      }
+      status[:nicedate] = String === status[:date] ? status[:date] : Utils.timeago(status[:date])
+      return {
+        :title => "@#{status[:user_nick]}",
+        :content => "#{status[:text]} (#{status[:nicedate]} via #{status[:source]})"
+      }
+    rescue
+    end
+  end
 
   def initialize
     super
 
   def initialize
     super
@@ -45,88 +84,109 @@ class TwitterPlugin < Plugin
         val
       end
     end
         val
       end
     end
+
+    # setup the application authentication
+
+    key = @bot.config['twitter.key']
+    secret = @bot.config['twitter.secret']
+    @client = OAuth2::Client.new(key, secret, 
+                                :token_url => '/oauth2/token',
+                                :site => URL)
+    @app_access_token = @client.client_credentials.get_token
+
+    debug "app access-token generated: #{@app_access_token.inspect}"
+
+    @bot.register_filter(:twitter, :htmlinfo) { |s| twitter_filter(s) }
+  end
+
+  def report_key_missing(m, failed_action)
+    m.reply [failed_action, "no Twitter Consumer Key/Secret is defined"].join(' because ')
   end
 
   def help(plugin, topic="")
   end
 
   def help(plugin, topic="")
-    return "twitter status [nick] => show nick's (or your) status, use 'twitter friends status [nick]' to also show the friends' timeline | twitter update [status] => updates your status on twitter | twitter authorize => Generates an authorization URL which will give you a PIN to authorize the bot to use your twitter account. | twitter pin [pin] => Finishes bot authorization using the PIN provided by the URL from twitter authorize. | twitter deauthorize => Makes the bot forget your Twitter account. | twitter actions [on|off] => enable/disable twitting of actions (/me does ...)"
+    return "twitter status [nick] => show nick's (or your) status, use 'twitter [home/mentions/retweets] status' to show your timeline | twitter update [status] => updates your status on twitter | twitter authorize => Generates an authorization URL which will give you a PIN to authorize the bot to use your twitter account. | twitter pin [pin] => Finishes bot authorization using the PIN provided by the URL from twitter authorize. | twitter deauthorize => Makes the bot forget your Twitter account. | twitter actions [on|off] => enable/disable twitting of actions (/me does ...)"
   end
 
   end
 
-  # update the status on twitter
+  # show latest status of a twitter user or the users timeline/mentions/retweets
   def get_status(m, params)
   def get_status(m, params)
-    friends = params[:friends]
+    nick = params[:nick] # (optional)
+    type = params[:type] # (optional) home, mentions, retweets
+
     if @registry.has_key?(m.sourcenick + "_access_token")
       @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
     if @registry.has_key?(m.sourcenick + "_access_token")
       @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
-      nick = params[:nick] || @access_token.params[:screen_name]
-    else
-      if friends
-        m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
-        return false
+      
+      if not nick
+        nick = @access_token.params[:screen_name]
       end
       end
-      nick = params[:nick]
+    elsif type
+      m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
+      return false
     end
 
     end
 
-    if not nick
+    if not nick and not type
       m.reply "you should specify the username of the twitter to use, or identify using 'twitter authorize'"
       return false
     end
 
       m.reply "you should specify the username of the twitter to use, or identify using 'twitter authorize'"
       return false
     end
 
-    count = @bot.config['twitter.friends_status_count']
+    # use the application-only authentication
+    if not @access_token
+      @access_token = @app_access_token
+    end
+
+    count = type ? @bot.config['twitter.timeline_status_count'] : @bot.config['twitter.status_count']
     user = URI.escape(nick)
     user = URI.escape(nick)
-    if @registry.has_key?(m.sourcenick + "_access_token")
-        if friends
-          #no change to count variable
-          uri = "https://api.twitter.com/1/statuses/friends_timeline.xml?count=#{count}"
-          response = @access_token.get(uri).body
-        else
-          count = @bot.config['twitter.status_count']
-          uri = "https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=#{user}&count=#{count}"
-          response = @access_token.get(uri).body
-        end
+    if not type
+      url = "/1.1/statuses/user_timeline.json?screen_name=#{nick}&count=#{count}&include_rts=true"
+    elsif type == 'retweets'
+      url = "/1.1/statuses/retweets_of_me.json?count=#{count}&include_rts=true"
     else
     else
-       #unauthorized user, will try to get from public timeline the old way
-       uri = "http://twitter.com/statuses/user_timeline/#{user}.xml?count=#{count}"
-       response = @bot.httputil.get(uri, :cache => false)
+      url = "/1.1/statuses/#{type || 'user'}_timeline.json?count=#{count}&include_rts=true"
     end
     end
-    debug response
+    response = @access_token.get(url).body
 
     texts = []
 
     if response
       begin
 
     texts = []
 
     if response
       begin
-        rex = REXML::Document.new(response)
-        rex.root.elements.each("status") { |st|
-          # month, day, hour, min, sec, year = st.elements['created_at'].text.match(/\w+ (\w+) (\d+) (\d+):(\d+):(\d+) \S+ (\d+)/)[1..6]
-          # debug [year, month, day, hour, min, sec].inspect
-          # time = Time.local(year.to_i, month, day.to_i, hour.to_i, min.to_i, sec.to_i)
-          time = Time.parse(st.elements['created_at'].text)
-          now = Time.now
-          # Sometimes, time can be in the future; invert the relation in this case
-          delta = ((time > now) ? time - now : now - time)
-          msg = st.elements['text'].to_s + " (#{Utils.secs_to_string(delta.to_i)} ago via #{st.elements['source'].to_s})"
-          author = ""
-          if friends
-            author = Utils.decode_html_entities(st.elements['user'].elements['name'].text) + ": " rescue ""
+        tweets = JSON.parse(response)
+        if tweets.class == Array
+          tweets.each do |tweet|
+            time = Time.parse(tweet['created_at'])
+            now = Time.now
+            # Sometimes, time can be in the future; invert the relation in this case
+            delta = ((time > now) ? time - now : now - time)
+            msg = tweet['text'] + " (#{Utils.secs_to_string(delta.to_i)} ago via #{tweet['source'].to_s})"
+            author = ""
+            if type
+              author = tweet['user']['name'] + ": " rescue ""
+            end
+            texts << author+Utils.decode_html_entities(msg).ircify_html
           end
           end
-          texts << author+Utils.decode_html_entities(msg).ircify_html
-        }
-        if friends
+        else
+          raise 'timeline response: ' + response
+        end
+        if type
           # friends always return the latest 20 updates, so we clip the count
           texts[count..-1]=nil
         end
       rescue
         error $!
           # friends always return the latest 20 updates, so we clip the count
           texts[count..-1]=nil
         end
       rescue
         error $!
-        if friends
-          m.reply "could not parse status for #{nick}'s friends"
+        if type
+          m.reply "could not parse status for #{nick}'s timeline"
         else
           m.reply "could not parse status for #{nick}"
         end
         return false
       end
         else
           m.reply "could not parse status for #{nick}"
         end
         return false
       end
-      m.reply texts.reverse.join("\n")
+      if texts.empty?
+        m.reply "No status updates!"
+      else
+        m.reply texts.reverse.join("\n")
+      end
       return true
     else
       return true
     else
-      if friends
-        rep = "could not get status for #{nick}'s friends"
+      if type
+        rep = "could not get status for #{nick}'s #{type} timeline"
         rep << ", try asking in private" unless m.private?
       else
         rep = "could not get status for #{nick}"
         rep << ", try asking in private" unless m.private?
       else
         rep = "could not get status for #{nick}"
@@ -147,6 +207,8 @@ class TwitterPlugin < Plugin
   end
 
   def authorize(m, params)
   end
 
   def authorize(m, params)
+    failed_action = "we can't complete the authorization process"
+
     #remove all old authorization data
     if @registry.has_key?(m.sourcenick + "_request_token")
       @registry.delete(m.sourcenick + "_request_token")
     #remove all old authorization data
     if @registry.has_key?(m.sourcenick + "_request_token")
       @registry.delete(m.sourcenick + "_request_token")
@@ -157,14 +219,24 @@ class TwitterPlugin < Plugin
 
     key = @bot.config['twitter.key']
     secret = @bot.config['twitter.secret']
 
     key = @bot.config['twitter.key']
     secret = @bot.config['twitter.secret']
-    @consumer = OAuth::Consumer.new(key, secret, {   
-      :site => "https://api.twitter.com",
+    if key.empty? or secret.empty?
+      report_key_missing(m, failed_action)
+      return false
+    end
+
+    @consumer = OAuth::Consumer.new(key, secret, {
+      :site => URL,
       :request_token_path => "/oauth/request_token",
       :access_token_path => "/oauth/access_token",
       :authorize_path => "/oauth/authorize"
       :request_token_path => "/oauth/request_token",
       :access_token_path => "/oauth/access_token",
       :authorize_path => "/oauth/authorize"
-      } )
-    @request_token = @consumer.get_request_token
-    @registry[m.sourcenick + "_request_token"] = YAML::dump(@request_token)        
+    } )
+    begin
+      @request_token = @consumer.get_request_token
+    rescue OAuth::Unauthorized
+      m.reply _("My authorization failed! Did you block me? Or is my Twitter Consumer Key/Secret pair incorrect?")
+      return false
+    end
+    @registry[m.sourcenick + "_request_token"] = YAML::dump(@request_token)
     m.reply "Go to this URL to get your authorization PIN, then use 'twitter pin <pin>' to finish authorization: " + @request_token.authorize_url
   end
 
     m.reply "Go to this URL to get your authorization PIN, then use 'twitter pin <pin>' to finish authorization: " + @request_token.authorize_url
   end
 
@@ -192,22 +264,29 @@ class TwitterPlugin < Plugin
     end
     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
 
     end
     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
 
-    uri = "https://api.twitter.com/statuses/update.json"
-    msg = params[:status].to_s
+    #uri = URL + '/statuses/update.json'
+    status = params[:status].to_s
 
 
-    if msg.length > 140
+    if status.length > 140
       m.reply "your status message update is too long, please keep it under 140 characters"
       return
     end
 
       m.reply "your status message update is too long, please keep it under 140 characters"
       return
     end
 
-    response = @access_token.post(uri, { :status => msg })
+    response = @access_token.post('/1.1/statuses/update.json', { :status => status })
     debug response
 
     reply_method = params[:notify] ? :notify : :reply
     if response.class == Net::HTTPOK
       m.__send__(reply_method, "status updated")
     else
     debug response
 
     reply_method = params[:notify] ? :notify : :reply
     if response.class == Net::HTTPOK
       m.__send__(reply_method, "status updated")
     else
-      m.__send__(reply_method, "could not update status")
+      debug 'twitter update response: ' + response.body
+      error = '?'
+      begin
+        json = JSON.parse(response.body)
+        error = json['errors'].first['message'] || '?'
+      rescue
+      end
+      m.__send__(reply_method, "could not update status: #{error}")
     end
   end
 
     end
   end
 
@@ -252,4 +331,5 @@ plugin.map 'twitter deauthorize', :action => "deauthorize", :public => false
 plugin.map 'twitter pin :pin', :action => "pin", :public => false
 plugin.map 'twitter actions [:toggle]', :action => "actions", :requirements => { :toggle => /^on|off$/ }
 plugin.map 'twitter status [:nick]', :action => "get_status", :threaded => true
 plugin.map 'twitter pin :pin', :action => "pin", :public => false
 plugin.map 'twitter actions [:toggle]', :action => "actions", :requirements => { :toggle => /^on|off$/ }
 plugin.map 'twitter status [:nick]', :action => "get_status", :threaded => true
-plugin.map 'twitter :friends [status] [:nick]', :action => "get_status", :requirements => { :friends => /^friends?$/ }, :threaded => true
+plugin.map 'twitter :type [status] [:nick]', :action => "get_status", :requirements => { :type => /^(home|mentions|retweets)?$/ }, :threaded => true
+