]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - data/rbot/plugins/twitter.rb
plugin(oxford): moved to lexico.com, closes #13
[user/henk/code/ruby/rbot.git] / data / rbot / plugins / twitter.rb
index 045b6172ff39a0fdbc6cb19b13e29e2a0793da1a..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
 # Users can setup their twitter username and password and then begin updating
 # twitter whenever
 
 # Users can setup their twitter username and password and then begin updating
 # twitter whenever
 
-begin
-  require 'oauth'
-rescue LoadError
-  error "OAuth module could not be loaded, twits will not be submitted and protected twits will not be accessible"
-end
-
+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")
-
-   Config.register Config::StringValue.new('twitter.secret',
-      :default => "",
-      :desc => "Twitter OAuth Consumer Secret")
-
-    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.friends_status_count',
-      :default => 3, :validate => Proc.new { |v| v > 0 && v <= 10},
-      :desc => "Maximum number of status updates shown by 'twitter friends status'")
+  URL = 'https://api.twitter.com'
+
+  Config.register Config::StringValue.new('twitter.key',
+    :default => "BdCN4FCokm9hkf8sIDmIJA",
+    :desc => "Twitter OAuth Consumer Key")
+
+  Config.register Config::StringValue.new('twitter.secret',
+    :default => "R4V00wUdEXlMr38SKOQR9UFQLqAmc3P7cpft7ohuqo",
+    :desc => "Twitter OAuth Consumer Secret")
+
+  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
 
-    @has_oauth = defined? OAuth
-
     class << @registry
       def store(val)
         val
     class << @registry
       def store(val)
         val
@@ -52,87 +84,95 @@ 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
 
   end
 
-  def report_oauth_missing(m, failed_action)
-    m.reply [failed_action, "I cannot authenticate to Twitter (OAuth not available)"].join(' because ')
+  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
-        if @has_oauth
-          m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
-        else
-          report_oauth_missing(m, "I cannot retrieve your friends status")
-        end
-        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 @has_oauth and @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
         else
           m.reply "could not parse status for #{nick}"
         end
@@ -145,8 +185,8 @@ class TwitterPlugin < Plugin
       end
       return true
     else
       end
       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}"
@@ -167,10 +207,7 @@ class TwitterPlugin < Plugin
   end
 
   def authorize(m, params)
   end
 
   def authorize(m, params)
-    unless @has_oauth
-      report_oauth_missing(m, "we can't complete the authorization process")
-      return false
-    end
+    failed_action = "we can't complete the authorization process"
 
     #remove all old authorization data
     if @registry.has_key?(m.sourcenick + "_request_token")
 
     #remove all old authorization data
     if @registry.has_key?(m.sourcenick + "_request_token")
@@ -182,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
 
@@ -211,33 +258,35 @@ class TwitterPlugin < Plugin
 
   # update the status on twitter
   def update_status(m, params)
 
   # update the status on twitter
   def update_status(m, params)
-    unless @has_oauth
-      report_oauth_missing(m, "I cannot update your status")
-      return false
-    end
-
     unless @registry.has_key?(m.sourcenick + "_access_token")
        m.reply "You must first authorize your Twitter account before tweeting."
        return false;
     end
     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
 
     unless @registry.has_key?(m.sourcenick + "_access_token")
        m.reply "You must first authorize your Twitter account before tweeting."
        return false;
     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
 
@@ -249,10 +298,7 @@ class TwitterPlugin < Plugin
   end
 
   # update on ACTION if the user has enabled the option
   end
 
   # update on ACTION if the user has enabled the option
-  # Possible TODO: move the has_oauth check further down and alert
-  # the user the first time we do not update because of the missing oauth
   def ctcp_listen(m)
   def ctcp_listen(m)
-    return unless @has_oauth
     return unless m.action?
     return unless @registry[m.sourcenick + "_actions"]
     update_status(m, :status => m.message, :notify => true)
     return unless m.action?
     return unless @registry[m.sourcenick + "_actions"]
     update_status(m, :status => m.message, :notify => true)
@@ -285,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
+