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