4 # :title: Twitter Status Update for rbot
6 # Author:: Carter Parks (carterparks) <carter@carterparks.com>
7 # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8 # Author:: NeoLobster <neolobster@snugglenets.com>
10 # Copyright:: (C) 2007 Carter Parks
11 # Copyright:: (C) 2007 Giuseppe Bilotta
13 # Users can setup their twitter username and password and then begin updating
19 error "OAuth module could not be loaded, twits will not be submitted and protected twits will not be accessible"
25 class TwitterPlugin < Plugin
26 Config.register Config::StringValue.new('twitter.key',
28 :desc => "Twitter OAuth Consumer Key")
30 Config.register Config::StringValue.new('twitter.secret',
32 :desc => "Twitter OAuth Consumer Secret")
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'")
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'")
43 loc = Utils.check_location(s, Regexp.new('twitter\.com/#!/.*/status/\d+'))
45 id = loc.first.match(/\/status\/(\d+)/)[1]
46 xml = @bot.httputil.get('http://api.twitter.com/1/statuses/show.xml?id=' + id)
48 root = REXML::Document.new(xml).root
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>")
58 status[:nicedate] = String === status[:date] ? status[:date] : Utils.timeago(status[:date])
60 :title => "#{status[:user]}/#{status[:id]}",
61 :content => "#{status[:text]} (#{status[:nicedate]} via #{status[:source]})"
68 @has_oauth = defined? OAuth
79 @bot.register_filter(:twitter, :htmlinfo) { |s| twitter_filter(s) }
82 def report_oauth_missing(m, failed_action)
83 m.reply [failed_action, "I cannot authenticate to Twitter (OAuth not available)"].join(' because ')
86 def report_key_missing(m, failed_action)
87 m.reply [failed_action, "no Twitter Consumer Key/Secret is defined"].join(' because ')
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 ...)"
94 # update the status on twitter
95 def get_status(m, params)
96 friends = params[:friends]
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]
104 m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
106 report_oauth_missing(m, "I cannot retrieve your friends status")
114 m.reply "you should specify the username of the twitter to use, or identify using 'twitter authorize'"
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")
124 #no change to count variable
125 uri = "https://api.twitter.com/1/statuses/friends_timeline.xml?count=#{count}&include_rts=true"
127 response = @access_token.get(uri).body
129 response = @bot.httputil.get(uri, :cache => false)
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)
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})"
149 author = Utils.decode_html_entities(st.elements['user'].elements['name'].text) + ": " rescue ""
151 texts << author+Utils.decode_html_entities(msg).ircify_html
154 # friends always return the latest 20 updates, so we clip the count
160 m.reply "could not parse status for #{nick}'s friends"
162 m.reply "could not parse status for #{nick}"
167 m.reply "No status updates!"
169 m.reply texts.reverse.join("\n")
174 rep = "could not get status for #{nick}'s friends"
175 rep << ", try asking in private" unless m.private?
177 rep = "could not get status for #{nick}"
184 def deauthorize(m, params)
185 if @registry.has_key?(m.sourcenick + "_request_token")
186 @registry.delete(m.sourcenick + "_request_token")
188 if @registry.has_key?(m.sourcenick + "_access_token")
189 @registry.delete(m.sourcenick + "_access_token")
191 m.reply "Done! You can reauthorize this account in the future by using 'twitter authorize'"
194 def authorize(m, params)
195 failed_action = "we can't complete the authorization process"
197 report_oauth_missing(m, failed_action)
201 #remove all old authorization data
202 if @registry.has_key?(m.sourcenick + "_request_token")
203 @registry.delete(m.sourcenick + "_request_token")
205 if @registry.has_key?(m.sourcenick + "_access_token")
206 @registry.delete(m.sourcenick + "_access_token")
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)
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"
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?")
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
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"
237 @request_token = YAML::load(@registry[m.sourcenick + "_request_token"])
239 @access_token = @request_token.get_access_token( { :oauth_verifier => params[:pin] } )
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"
244 @registry[m.sourcenick + "_access_token"] = YAML::dump(@access_token)
245 m.reply "Okay, you're all set"
248 # update the status on twitter
249 def update_status(m, params)
251 report_oauth_missing(m, "I cannot update your status")
255 unless @registry.has_key?(m.sourcenick + "_access_token")
256 m.reply "You must first authorize your Twitter account before tweeting."
259 @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
261 uri = "https://api.twitter.com/statuses/update.json"
262 msg = params[:status].to_s
265 m.reply "your status message update is too long, please keep it under 140 characters"
269 response = @access_token.post(uri, { :status => msg })
272 reply_method = params[:notify] ? :notify : :reply
273 if response.class == Net::HTTPOK
274 m.__send__(reply_method, "status updated")
276 m.__send__(reply_method, "could not update status")
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!"
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
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)
297 # show or toggle action twitting
298 def actions(m, params)
301 @registry[m.sourcenick + "_actions"] = true
304 @registry.delete(m.sourcenick + "_actions")
307 if @registry[m.sourcenick + "_actions"]
308 m.reply _("actions will be twitted")
310 m.reply _("actions will not be twitted")
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