]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/commitdiff
updates the twitter plugin to use the new api 1.1
authorMatthias H <apoc@sixserv.org>
Fri, 19 Jul 2013 14:26:24 +0000 (16:26 +0200)
committerMatthias H <apoc@sixserv.org>
Fri, 19 Jul 2013 14:26:24 +0000 (16:26 +0200)
The old 1.0 api is no longer available.
Its using oauth2 for application-only authentication and
oauth(1.0a) for user authentication.
Now requests and parses json (the new API doesn't support XML)

data/rbot/plugins/twitter.rb

index 8725ac2cd21de783cbad33c30709de7849aa580b..e661e63326a76daa4b4a071dc75ab6649256844a 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")
+  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 => "",
-      :desc => "Twitter OAuth Consumer Secret")
+  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.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'")
+  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'")
 
   def twitter_filter(s)
     loc = Utils.check_location(s, Regexp.new('twitter\.com/#!/.*/status/\d+'))
     return nil unless loc
     id = loc.first.match(/\/status\/(\d+)/)[1]
 
   def twitter_filter(s)
     loc = Utils.check_location(s, Regexp.new('twitter\.com/#!/.*/status/\d+'))
     return nil unless loc
     id = loc.first.match(/\/status\/(\d+)/)[1]
-    xml = @bot.httputil.get('http://api.twitter.com/1/statuses/show.xml?id=' + id)
-    return nil unless xml
-    root = REXML::Document.new(xml).root
-    status = {
-      :date => (Time.parse(root.elements["created_at"].text) rescue "<unknown>"),
-      :id => (root.elements["id"].text rescue "<unknown>"),
-      :text => (root.elements["text"].text.ircify_html rescue "<error>"),
-      :source => (root.elements["source"].text rescue "<unknown>"),
-      :user => (root.elements["user/name"].text rescue "<unknown>"),
-      :user_nick => (root.elements["user/screen_name"] rescue "<unknown>")
-      # TODO other entries
-    }
-    status[:nicedate] = String === status[:date] ? status[:date] : Utils.timeago(status[:date])
-    return {
-      :title => "#{status[:user]}/#{status[:id]}",
-      :content => "#{status[:text]} (#{status[:nicedate]} via #{status[:source]})"
-    }
+
+    response = @app_access_token.get('/1.1/statuses/show/'+id).body
+    begin
+      tweet = JSON.parse(response).first
+      status = {
+        :date => (Time.parse(tweet["created_at"]) rescue "<unknown>"),
+        :id => (tweet["id"].text rescue "<unknown>"),
+        :text => (tweet["text"].ircify_html rescue "<error>"),
+        :source => (tweet["source"].text 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]}/#{status[:id]}",
+        :content => "#{status[:text]} (#{status[:nicedate]} via #{status[:source]})"
+      }
+    rescue
+    end
   end
 
   def initialize
     super
 
   end
 
   def initialize
     super
 
-    @has_oauth = defined? OAuth
-
     class << @registry
       def store(val)
         val
     class << @registry
       def store(val)
         val
@@ -76,11 +76,18 @@ class TwitterPlugin < Plugin
       end
     end
 
       end
     end
 
-    @bot.register_filter(:twitter, :htmlinfo) { |s| twitter_filter(s) }
-  end
+    # setup the application authentication
 
 
-  def report_oauth_missing(m, failed_action)
-    m.reply [failed_action, "I cannot authenticate to Twitter (OAuth not available)"].join(' because ')
+    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)
   end
 
   def report_key_missing(m, failed_action)
@@ -88,76 +95,75 @@ class TwitterPlugin < Plugin
   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 = friends ? @bot.config['twitter.friends_status_count'] : @bot.config['twitter.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)
-    # receive the public timeline per default (this works even without an access_token)
-    uri = "https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=#{user}&count=#{count}&include_rts=true"
-    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}&include_rts=true"
-        end
-        response = @access_token.get(uri).body
+    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
-       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
@@ -170,8 +176,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}"
@@ -193,10 +199,6 @@ class TwitterPlugin < Plugin
 
   def authorize(m, params)
     failed_action = "we can't complete the authorization process"
 
   def authorize(m, params)
     failed_action = "we can't complete the authorization process"
-    unless @has_oauth
-      report_oauth_missing(m, failed_action)
-      return false
-    end
 
     #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")
@@ -214,7 +216,7 @@ class TwitterPlugin < Plugin
     end
 
     @consumer = OAuth::Consumer.new(key, secret, {
     end
 
     @consumer = OAuth::Consumer.new(key, secret, {
-      :site => "https://api.twitter.com",
+      :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"
@@ -247,33 +249,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
 
@@ -285,10 +289,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)
@@ -321,4 +322,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
+