twitter plugin: change the api urls to receive RTs
[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 #
10 # Copyright:: (C) 2007 Carter Parks
11 # Copyright:: (C) 2007 Giuseppe Bilotta
12 #
13 # Users can setup their twitter username and password and then begin updating
14 # twitter whenever
15
16 begin
17   require 'oauth'
18 rescue LoadError
19   error "OAuth module could not be loaded, twits will not be submitted and protected twits will not be accessible"
20 end
21
22 require 'yaml'
23 require 'rexml/rexml'
24
25 class TwitterPlugin < Plugin
26    Config.register Config::StringValue.new('twitter.key',
27       :default => "",
28       :desc => "Twitter OAuth Consumer Key")
29
30    Config.register Config::StringValue.new('twitter.secret',
31       :default => "",
32       :desc => "Twitter OAuth Consumer Secret")
33
34     Config.register Config::IntegerValue.new('twitter.status_count',
35       :default => 1, :validate => Proc.new { |v| v > 0 && v <= 10},
36       :desc => "Maximum number of status updates shown by 'twitter status'")
37
38     Config.register Config::IntegerValue.new('twitter.friends_status_count',
39       :default => 3, :validate => Proc.new { |v| v > 0 && v <= 10},
40       :desc => "Maximum number of status updates shown by 'twitter friends status'")
41
42   def initialize
43     super
44
45     @has_oauth = defined? OAuth
46
47     class << @registry
48       def store(val)
49         val
50       end
51       def restore(val)
52         val
53       end
54     end
55   end
56
57   def report_oauth_missing(m, failed_action)
58     m.reply [failed_action, "I cannot authenticate to Twitter (OAuth not available)"].join(' because ')
59   end
60
61   def report_key_missing(m, failed_action)
62     m.reply [failed_action, "no Twitter Consumer Key/Secret is defined"].join(' because ')
63   end
64
65   def help(plugin, topic="")
66     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 ...)"
67   end
68
69   # update the status on twitter
70   def get_status(m, params)
71     friends = params[:friends]
72
73     if @registry.has_key?(m.sourcenick + "_access_token")
74       @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
75       nick = params[:nick] || @access_token.params[:screen_name]
76     else
77       if friends
78         if @has_oauth
79           m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
80         else
81           report_oauth_missing(m, "I cannot retrieve your friends status")
82         end
83         return false
84       end
85       nick = params[:nick]
86     end
87
88     if not nick
89       m.reply "you should specify the username of the twitter to use, or identify using 'twitter authorize'"
90       return false
91     end
92
93     count = friends ? @bot.config['twitter.friends_status_count'] : @bot.config['twitter.status_count']
94     user = URI.escape(nick)
95     # receive the public timeline per default (this works even without an access_token)
96     uri = "https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=#{user}&count=#{count}&include_rts=true"
97     if @has_oauth and @registry.has_key?(m.sourcenick + "_access_token")
98         if friends
99           #no change to count variable
100           uri = "https://api.twitter.com/1/statuses/friends_timeline.xml?count=#{count}&include_rts=true"
101         end
102         response = @access_token.get(uri).body
103     else
104        response = @bot.httputil.get(uri, :cache => false)
105     end
106     debug response
107
108     texts = []
109
110     if response
111       begin
112         rex = REXML::Document.new(response)
113         rex.root.elements.each("status") { |st|
114           # month, day, hour, min, sec, year = st.elements['created_at'].text.match(/\w+ (\w+) (\d+) (\d+):(\d+):(\d+) \S+ (\d+)/)[1..6]
115           # debug [year, month, day, hour, min, sec].inspect
116           # time = Time.local(year.to_i, month, day.to_i, hour.to_i, min.to_i, sec.to_i)
117           time = Time.parse(st.elements['created_at'].text)
118           now = Time.now
119           # Sometimes, time can be in the future; invert the relation in this case
120           delta = ((time > now) ? time - now : now - time)
121           msg = st.elements['text'].to_s + " (#{Utils.secs_to_string(delta.to_i)} ago via #{st.elements['source'].to_s})"
122           author = ""
123           if friends
124             author = Utils.decode_html_entities(st.elements['user'].elements['name'].text) + ": " rescue ""
125           end
126           texts << author+Utils.decode_html_entities(msg).ircify_html
127         }
128         if friends
129           # friends always return the latest 20 updates, so we clip the count
130           texts[count..-1]=nil
131         end
132       rescue
133         error $!
134         if friends
135           m.reply "could not parse status for #{nick}'s friends"
136         else
137           m.reply "could not parse status for #{nick}"
138         end
139         return false
140       end
141       if texts.empty?
142         m.reply "No status updates!"
143       else
144         m.reply texts.reverse.join("\n")
145       end
146       return true
147     else
148       if friends
149         rep = "could not get status for #{nick}'s friends"
150         rep << ", try asking in private" unless m.private?
151       else
152         rep = "could not get status for #{nick}"
153       end
154       m.reply rep
155       return false
156     end
157   end
158
159   def deauthorize(m, params)
160     if @registry.has_key?(m.sourcenick + "_request_token")
161       @registry.delete(m.sourcenick + "_request_token")
162     end
163     if @registry.has_key?(m.sourcenick + "_access_token")
164       @registry.delete(m.sourcenick + "_access_token")
165     end
166     m.reply "Done! You can reauthorize this account in the future by using 'twitter authorize'"
167   end
168
169   def authorize(m, params)
170     failed_action = "we can't complete the authorization process"
171     unless @has_oauth
172       report_oauth_missing(m, failed_action)
173       return false
174     end
175
176     #remove all old authorization data
177     if @registry.has_key?(m.sourcenick + "_request_token")
178       @registry.delete(m.sourcenick + "_request_token")
179     end
180     if @registry.has_key?(m.sourcenick + "_access_token")
181       @registry.delete(m.sourcenick + "_access_token")
182     end
183
184     key = @bot.config['twitter.key']
185     secret = @bot.config['twitter.secret']
186     if key.empty? or secret.empty?
187       report_key_missing(m, failed_action)
188       return false
189     end
190
191     @consumer = OAuth::Consumer.new(key, secret, {
192       :site => "https://api.twitter.com",
193       :request_token_path => "/oauth/request_token",
194       :access_token_path => "/oauth/access_token",
195       :authorize_path => "/oauth/authorize"
196     } )
197     begin
198       @request_token = @consumer.get_request_token
199     rescue OAuth::Unauthorized
200       m.reply _("My authorization failed! Did you block me? Or is my Twitter Consumer Key/Secret pair incorrect?")
201       return false
202     end
203     @registry[m.sourcenick + "_request_token"] = YAML::dump(@request_token)
204     m.reply "Go to this URL to get your authorization PIN, then use 'twitter pin <pin>' to finish authorization: " + @request_token.authorize_url
205   end
206
207   def pin(m, params)
208      unless @registry.has_key?(m.sourcenick + "_request_token")
209        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"
210        return false
211      end
212      @request_token = YAML::load(@registry[m.sourcenick + "_request_token"])
213      begin
214        @access_token = @request_token.get_access_token( { :oauth_verifier => params[:pin] } )
215      rescue
216        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"
217        return false
218      end
219      @registry[m.sourcenick + "_access_token"] = YAML::dump(@access_token)
220      m.reply "Okay, you're all set"
221   end
222
223   # update the status on twitter
224   def update_status(m, params)
225     unless @has_oauth
226       report_oauth_missing(m, "I cannot update your status")
227       return false
228     end
229
230     unless @registry.has_key?(m.sourcenick + "_access_token")
231        m.reply "You must first authorize your Twitter account before tweeting."
232        return false;
233     end
234     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
235
236     uri = "https://api.twitter.com/statuses/update.json"
237     msg = params[:status].to_s
238
239     if msg.length > 140
240       m.reply "your status message update is too long, please keep it under 140 characters"
241       return
242     end
243
244     response = @access_token.post(uri, { :status => msg })
245     debug response
246
247     reply_method = params[:notify] ? :notify : :reply
248     if response.class == Net::HTTPOK
249       m.__send__(reply_method, "status updated")
250     else
251       m.__send__(reply_method, "could not update status")
252     end
253   end
254
255   # ties a nickname to a twitter username and password
256   def identify(m, params)
257     @registry[m.sourcenick + "_username"] = params[:username].to_s
258     @registry[m.sourcenick + "_password"] = params[:password].to_s
259     m.reply "you're all set up!"
260   end
261
262   # update on ACTION if the user has enabled the option
263   # Possible TODO: move the has_oauth check further down and alert
264   # the user the first time we do not update because of the missing oauth
265   def ctcp_listen(m)
266     return unless @has_oauth
267     return unless m.action?
268     return unless @registry[m.sourcenick + "_actions"]
269     update_status(m, :status => m.message, :notify => true)
270   end
271
272   # show or toggle action twitting
273   def actions(m, params)
274     case params[:toggle]
275     when 'on'
276       @registry[m.sourcenick + "_actions"] = true
277       m.okay
278     when 'off'
279       @registry.delete(m.sourcenick + "_actions")
280       m.okay
281     else
282       if @registry[m.sourcenick + "_actions"]
283         m.reply _("actions will be twitted")
284       else
285         m.reply _("actions will not be twitted")
286       end
287     end
288   end
289 end
290
291 # create an instance of our plugin class and register for the "length" command
292 plugin = TwitterPlugin.new
293 plugin.map 'twitter update *status', :action => "update_status", :threaded => true
294 plugin.map 'twitter authorize', :action => "authorize", :public => false
295 plugin.map 'twitter deauthorize', :action => "deauthorize", :public => false
296 plugin.map 'twitter pin :pin', :action => "pin", :public => false
297 plugin.map 'twitter actions [:toggle]', :action => "actions", :requirements => { :toggle => /^on|off$/ }
298 plugin.map 'twitter status [:nick]', :action => "get_status", :threaded => true
299 plugin.map 'twitter :friends [status] [:nick]', :action => "get_status", :requirements => { :friends => /^friends?$/ }, :threaded => true