]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/twitter.rb
045b6172ff39a0fdbc6cb19b13e29e2a0793da1a
[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 #
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 help(plugin, topic="")
62     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 ...)"
63   end
64
65   # update the status on twitter
66   def get_status(m, params)
67     friends = params[:friends]
68
69     if @registry.has_key?(m.sourcenick + "_access_token")
70       @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
71       nick = params[:nick] || @access_token.params[:screen_name]
72     else
73       if friends
74         if @has_oauth
75           m.reply "You are not authorized with Twitter. Please use 'twitter authorize' first to use this feature."
76         else
77           report_oauth_missing(m, "I cannot retrieve your friends status")
78         end
79         return false
80       end
81       nick = params[:nick]
82     end
83
84     if not nick
85       m.reply "you should specify the username of the twitter to use, or identify using 'twitter authorize'"
86       return false
87     end
88
89     count = @bot.config['twitter.friends_status_count']
90     user = URI.escape(nick)
91     if @has_oauth and @registry.has_key?(m.sourcenick + "_access_token")
92         if friends
93           #no change to count variable
94           uri = "https://api.twitter.com/1/statuses/friends_timeline.xml?count=#{count}"
95           response = @access_token.get(uri).body
96         else
97           count = @bot.config['twitter.status_count']
98           uri = "https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=#{user}&count=#{count}"
99           response = @access_token.get(uri).body
100         end
101     else
102        #unauthorized user, will try to get from public timeline the old way
103        uri = "http://twitter.com/statuses/user_timeline/#{user}.xml?count=#{count}"
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     unless @has_oauth
171       report_oauth_missing(m, "we can't complete the authorization process")
172       return false
173     end
174
175     #remove all old authorization data
176     if @registry.has_key?(m.sourcenick + "_request_token")
177       @registry.delete(m.sourcenick + "_request_token")
178     end
179     if @registry.has_key?(m.sourcenick + "_access_token")
180       @registry.delete(m.sourcenick + "_access_token")
181     end
182
183     key = @bot.config['twitter.key']
184     secret = @bot.config['twitter.secret']
185     @consumer = OAuth::Consumer.new(key, secret, {   
186       :site => "https://api.twitter.com",
187       :request_token_path => "/oauth/request_token",
188       :access_token_path => "/oauth/access_token",
189       :authorize_path => "/oauth/authorize"
190       } )
191     @request_token = @consumer.get_request_token
192     @registry[m.sourcenick + "_request_token"] = YAML::dump(@request_token)        
193     m.reply "Go to this URL to get your authorization PIN, then use 'twitter pin <pin>' to finish authorization: " + @request_token.authorize_url
194   end
195
196   def pin(m, params)
197      unless @registry.has_key?(m.sourcenick + "_request_token")
198        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"
199        return false
200      end
201      @request_token = YAML::load(@registry[m.sourcenick + "_request_token"])
202      begin
203        @access_token = @request_token.get_access_token( { :oauth_verifier => params[:pin] } )
204      rescue
205        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"
206        return false
207      end
208      @registry[m.sourcenick + "_access_token"] = YAML::dump(@access_token)
209      m.reply "Okay, you're all set"
210   end
211
212   # update the status on twitter
213   def update_status(m, params)
214     unless @has_oauth
215       report_oauth_missing(m, "I cannot update your status")
216       return false
217     end
218
219     unless @registry.has_key?(m.sourcenick + "_access_token")
220        m.reply "You must first authorize your Twitter account before tweeting."
221        return false;
222     end
223     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
224
225     uri = "https://api.twitter.com/statuses/update.json"
226     msg = params[:status].to_s
227
228     if msg.length > 140
229       m.reply "your status message update is too long, please keep it under 140 characters"
230       return
231     end
232
233     response = @access_token.post(uri, { :status => msg })
234     debug response
235
236     reply_method = params[:notify] ? :notify : :reply
237     if response.class == Net::HTTPOK
238       m.__send__(reply_method, "status updated")
239     else
240       m.__send__(reply_method, "could not update status")
241     end
242   end
243
244   # ties a nickname to a twitter username and password
245   def identify(m, params)
246     @registry[m.sourcenick + "_username"] = params[:username].to_s
247     @registry[m.sourcenick + "_password"] = params[:password].to_s
248     m.reply "you're all set up!"
249   end
250
251   # update on ACTION if the user has enabled the option
252   # Possible TODO: move the has_oauth check further down and alert
253   # the user the first time we do not update because of the missing oauth
254   def ctcp_listen(m)
255     return unless @has_oauth
256     return unless m.action?
257     return unless @registry[m.sourcenick + "_actions"]
258     update_status(m, :status => m.message, :notify => true)
259   end
260
261   # show or toggle action twitting
262   def actions(m, params)
263     case params[:toggle]
264     when 'on'
265       @registry[m.sourcenick + "_actions"] = true
266       m.okay
267     when 'off'
268       @registry.delete(m.sourcenick + "_actions")
269       m.okay
270     else
271       if @registry[m.sourcenick + "_actions"]
272         m.reply _("actions will be twitted")
273       else
274         m.reply _("actions will not be twitted")
275       end
276     end
277   end
278 end
279
280 # create an instance of our plugin class and register for the "length" command
281 plugin = TwitterPlugin.new
282 plugin.map 'twitter update *status', :action => "update_status", :threaded => true
283 plugin.map 'twitter authorize', :action => "authorize", :public => false
284 plugin.map 'twitter deauthorize', :action => "deauthorize", :public => false
285 plugin.map 'twitter pin :pin', :action => "pin", :public => false
286 plugin.map 'twitter actions [:toggle]', :action => "actions", :requirements => { :toggle => /^on|off$/ }
287 plugin.map 'twitter status [:nick]', :action => "get_status", :threaded => true
288 plugin.map 'twitter :friends [status] [:nick]', :action => "get_status", :requirements => { :friends => /^friends?$/ }, :threaded => true