updates the twitter plugin to use the new api 1.1
[rbot] / data / rbot / plugins / twitter.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Twitter Status Update for rbot
5 #
6 # Author:: Carter Parks (carterparks) <carter@carterparks.com>
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 # Author:: NeoLobster <neolobster@snugglenets.com>
9 # Author:: Matthias Hecker <apoc@sixserv.org>
10 #
11 # Copyright:: (C) 2007 Carter Parks
12 # Copyright:: (C) 2007 Giuseppe Bilotta
13 #
14 # Users can setup their twitter username and password and then begin updating
15 # twitter whenever
16
17 require 'oauth'
18 require 'oauth2'
19 require 'yaml'
20 require 'json'
21
22 class TwitterPlugin < Plugin
23   URL = 'https://api.twitter.com'
24
25   Config.register Config::StringValue.new('twitter.key',
26     :default => "BdCN4FCokm9hkf8sIDmIJA",
27     :desc => "Twitter OAuth Consumer Key")
28
29   Config.register Config::StringValue.new('twitter.secret',
30     :default => "R4V00wUdEXlMr38SKOQR9UFQLqAmc3P7cpft7ohuqo",
31     :desc => "Twitter OAuth Consumer Secret")
32
33   Config.register Config::IntegerValue.new('twitter.status_count',
34     :default => 1, :validate => Proc.new { |v| v > 0 && v <= 10},
35     :desc => "Maximum number of status updates shown by 'twitter status'")
36
37   Config.register Config::IntegerValue.new('twitter.timeline_status_count',
38     :default => 3, :validate => Proc.new { |v| v > 0 && v <= 10},
39     :desc => "Maximum number of status updates shown by 'twitter [home|mentions|retweets] status'")
40
41   def twitter_filter(s)
42     loc = Utils.check_location(s, Regexp.new('twitter\.com/#!/.*/status/\d+'))
43     return nil unless loc
44     id = loc.first.match(/\/status\/(\d+)/)[1]
45
46     response = @app_access_token.get('/1.1/statuses/show/'+id).body
47     begin
48       tweet = JSON.parse(response).first
49       status = {
50         :date => (Time.parse(tweet["created_at"]) rescue "<unknown>"),
51         :id => (tweet["id"].text rescue "<unknown>"),
52         :text => (tweet["text"].ircify_html rescue "<error>"),
53         :source => (tweet["source"].text rescue "<unknown>"),
54         :user => (tweet["user"]["name"] rescue "<unknown>"),
55         :user_nick => (tweet["user"]["screen_name"] rescue "<unknown>")
56         # TODO other entries
57       }
58       status[:nicedate] = String === status[:date] ? status[:date] : Utils.timeago(status[:date])
59       return {
60         :title => "#{status[:user]}/#{status[:id]}",
61         :content => "#{status[:text]} (#{status[:nicedate]} via #{status[:source]})"
62       }
63     rescue
64     end
65   end
66
67   def initialize
68     super
69
70     class << @registry
71       def store(val)
72         val
73       end
74       def restore(val)
75         val
76       end
77     end
78
79     # setup the application authentication
80
81     key = @bot.config['twitter.key']
82     secret = @bot.config['twitter.secret']
83     @client = OAuth2::Client.new(key, secret, 
84                                 :token_url => '/oauth2/token',
85                                 :site => URL)
86     @app_access_token = @client.client_credentials.get_token
87
88     debug "app access-token generated: #{@app_access_token.inspect}"
89
90     @bot.register_filter(:twitter, :htmlinfo) { |s| twitter_filter(s) }
91   end
92
93   def report_key_missing(m, failed_action)
94     m.reply [failed_action, "no Twitter Consumer Key/Secret is defined"].join(' because ')
95   end
96
97   def help(plugin, topic="")
98     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 ...)"
99   end
100
101   # show latest status of a twitter user or the users timeline/mentions/retweets
102   def get_status(m, params)
103     nick = params[:nick] # (optional)
104     type = params[:type] # (optional) home, mentions, retweets
105
106     if @registry.has_key?(m.sourcenick + "_access_token")
107       @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
108       
109       if not nick
110         nick = @access_token.params[:screen_name]
111       end
112     elsif type
113       m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
114       return false
115     end
116
117     if not nick and not type
118       m.reply "you should specify the username of the twitter to use, or identify using 'twitter authorize'"
119       return false
120     end
121
122     # use the application-only authentication
123     if not @access_token
124       @access_token = @app_access_token
125     end
126
127     count = type ? @bot.config['twitter.timeline_status_count'] : @bot.config['twitter.status_count']
128     user = URI.escape(nick)
129     if not type
130       url = "/1.1/statuses/user_timeline.json?screen_name=#{nick}&count=#{count}&include_rts=true"
131     elsif type == 'retweets'
132       url = "/1.1/statuses/retweets_of_me.json?count=#{count}&include_rts=true"
133     else
134       url = "/1.1/statuses/#{type || 'user'}_timeline.json?count=#{count}&include_rts=true"
135     end
136     response = @access_token.get(url).body
137
138     texts = []
139
140     if response
141       begin
142         tweets = JSON.parse(response)
143         if tweets.class == Array
144           tweets.each do |tweet|
145             time = Time.parse(tweet['created_at'])
146             now = Time.now
147             # Sometimes, time can be in the future; invert the relation in this case
148             delta = ((time > now) ? time - now : now - time)
149             msg = tweet['text'] + " (#{Utils.secs_to_string(delta.to_i)} ago via #{tweet['source'].to_s})"
150             author = ""
151             if type
152               author = tweet['user']['name'] + ": " rescue ""
153             end
154             texts << author+Utils.decode_html_entities(msg).ircify_html
155           end
156         else
157           raise 'timeline response: ' + response
158         end
159         if type
160           # friends always return the latest 20 updates, so we clip the count
161           texts[count..-1]=nil
162         end
163       rescue
164         error $!
165         if type
166           m.reply "could not parse status for #{nick}'s timeline"
167         else
168           m.reply "could not parse status for #{nick}"
169         end
170         return false
171       end
172       if texts.empty?
173         m.reply "No status updates!"
174       else
175         m.reply texts.reverse.join("\n")
176       end
177       return true
178     else
179       if type
180         rep = "could not get status for #{nick}'s #{type} timeline"
181         rep << ", try asking in private" unless m.private?
182       else
183         rep = "could not get status for #{nick}"
184       end
185       m.reply rep
186       return false
187     end
188   end
189
190   def deauthorize(m, params)
191     if @registry.has_key?(m.sourcenick + "_request_token")
192       @registry.delete(m.sourcenick + "_request_token")
193     end
194     if @registry.has_key?(m.sourcenick + "_access_token")
195       @registry.delete(m.sourcenick + "_access_token")
196     end
197     m.reply "Done! You can reauthorize this account in the future by using 'twitter authorize'"
198   end
199
200   def authorize(m, params)
201     failed_action = "we can't complete the authorization process"
202
203     #remove all old authorization data
204     if @registry.has_key?(m.sourcenick + "_request_token")
205       @registry.delete(m.sourcenick + "_request_token")
206     end
207     if @registry.has_key?(m.sourcenick + "_access_token")
208       @registry.delete(m.sourcenick + "_access_token")
209     end
210
211     key = @bot.config['twitter.key']
212     secret = @bot.config['twitter.secret']
213     if key.empty? or secret.empty?
214       report_key_missing(m, failed_action)
215       return false
216     end
217
218     @consumer = OAuth::Consumer.new(key, secret, {
219       :site => URL,
220       :request_token_path => "/oauth/request_token",
221       :access_token_path => "/oauth/access_token",
222       :authorize_path => "/oauth/authorize"
223     } )
224     begin
225       @request_token = @consumer.get_request_token
226     rescue OAuth::Unauthorized
227       m.reply _("My authorization failed! Did you block me? Or is my Twitter Consumer Key/Secret pair incorrect?")
228       return false
229     end
230     @registry[m.sourcenick + "_request_token"] = YAML::dump(@request_token)
231     m.reply "Go to this URL to get your authorization PIN, then use 'twitter pin <pin>' to finish authorization: " + @request_token.authorize_url
232   end
233
234   def pin(m, params)
235      unless @registry.has_key?(m.sourcenick + "_request_token")
236        m.reply "You must first use twitter authorize to get an authorization URL, which you can use to get a PIN for me to use to verify your Twitter account"
237        return false
238      end
239      @request_token = YAML::load(@registry[m.sourcenick + "_request_token"])
240      begin
241        @access_token = @request_token.get_access_token( { :oauth_verifier => params[:pin] } )
242      rescue
243        m.reply "Error: There was a problem registering your Twitter account. Please make sure you have the right PIN. If the problem persists, use twitter authorize again to get a new PIN"
244        return false
245      end
246      @registry[m.sourcenick + "_access_token"] = YAML::dump(@access_token)
247      m.reply "Okay, you're all set"
248   end
249
250   # update the status on twitter
251   def update_status(m, params)
252     unless @registry.has_key?(m.sourcenick + "_access_token")
253        m.reply "You must first authorize your Twitter account before tweeting."
254        return false;
255     end
256     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
257
258     #uri = URL + '/statuses/update.json'
259     status = params[:status].to_s
260
261     if status.length > 140
262       m.reply "your status message update is too long, please keep it under 140 characters"
263       return
264     end
265
266     response = @access_token.post('/1.1/statuses/update.json', { :status => status })
267     debug response
268
269     reply_method = params[:notify] ? :notify : :reply
270     if response.class == Net::HTTPOK
271       m.__send__(reply_method, "status updated")
272     else
273       debug 'twitter update response: ' + response.body
274       error = '?'
275       begin
276         json = JSON.parse(response.body)
277         error = json['errors'].first['message'] || '?'
278       rescue
279       end
280       m.__send__(reply_method, "could not update status: #{error}")
281     end
282   end
283
284   # ties a nickname to a twitter username and password
285   def identify(m, params)
286     @registry[m.sourcenick + "_username"] = params[:username].to_s
287     @registry[m.sourcenick + "_password"] = params[:password].to_s
288     m.reply "you're all set up!"
289   end
290
291   # update on ACTION if the user has enabled the option
292   def ctcp_listen(m)
293     return unless m.action?
294     return unless @registry[m.sourcenick + "_actions"]
295     update_status(m, :status => m.message, :notify => true)
296   end
297
298   # show or toggle action twitting
299   def actions(m, params)
300     case params[:toggle]
301     when 'on'
302       @registry[m.sourcenick + "_actions"] = true
303       m.okay
304     when 'off'
305       @registry.delete(m.sourcenick + "_actions")
306       m.okay
307     else
308       if @registry[m.sourcenick + "_actions"]
309         m.reply _("actions will be twitted")
310       else
311         m.reply _("actions will not be twitted")
312       end
313     end
314   end
315 end
316
317 # create an instance of our plugin class and register for the "length" command
318 plugin = TwitterPlugin.new
319 plugin.map 'twitter update *status', :action => "update_status", :threaded => true
320 plugin.map 'twitter authorize', :action => "authorize", :public => false
321 plugin.map 'twitter deauthorize', :action => "deauthorize", :public => false
322 plugin.map 'twitter pin :pin', :action => "pin", :public => false
323 plugin.map 'twitter actions [:toggle]', :action => "actions", :requirements => { :toggle => /^on|off$/ }
324 plugin.map 'twitter status [:nick]', :action => "get_status", :threaded => true
325 plugin.map 'twitter :type [status] [:nick]', :action => "get_status", :requirements => { :type => /^(home|mentions|retweets)?$/ }, :threaded => true
326