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