]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - data/rbot/plugins/twitter.rb
twitter: report missing key/secret configuration
[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 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 = @bot.config['twitter.friends_status_count']
94     user = URI.escape(nick)
95     if @has_oauth and @registry.has_key?(m.sourcenick + "_access_token")
96         if friends
97           #no change to count variable
98           uri = "https://api.twitter.com/1/statuses/friends_timeline.xml?count=#{count}"
99           response = @access_token.get(uri).body
100         else
101           count = @bot.config['twitter.status_count']
102           uri = "https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=#{user}&count=#{count}"
103           response = @access_token.get(uri).body
104         end
105     else
106        #unauthorized user, will try to get from public timeline the old way
107        uri = "http://twitter.com/statuses/user_timeline/#{user}.xml?count=#{count}"
108        response = @bot.httputil.get(uri, :cache => false)
109     end
110     debug response
111
112     texts = []
113
114     if response
115       begin
116         rex = REXML::Document.new(response)
117         rex.root.elements.each("status") { |st|
118           # month, day, hour, min, sec, year = st.elements['created_at'].text.match(/\w+ (\w+) (\d+) (\d+):(\d+):(\d+) \S+ (\d+)/)[1..6]
119           # debug [year, month, day, hour, min, sec].inspect
120           # time = Time.local(year.to_i, month, day.to_i, hour.to_i, min.to_i, sec.to_i)
121           time = Time.parse(st.elements['created_at'].text)
122           now = Time.now
123           # Sometimes, time can be in the future; invert the relation in this case
124           delta = ((time > now) ? time - now : now - time)
125           msg = st.elements['text'].to_s + " (#{Utils.secs_to_string(delta.to_i)} ago via #{st.elements['source'].to_s})"
126           author = ""
127           if friends
128             author = Utils.decode_html_entities(st.elements['user'].elements['name'].text) + ": " rescue ""
129           end
130           texts << author+Utils.decode_html_entities(msg).ircify_html
131         }
132         if friends
133           # friends always return the latest 20 updates, so we clip the count
134           texts[count..-1]=nil
135         end
136       rescue
137         error $!
138         if friends
139           m.reply "could not parse status for #{nick}'s friends"
140         else
141           m.reply "could not parse status for #{nick}"
142         end
143         return false
144       end
145       if texts.empty?
146         m.reply "No status updates!"
147       else
148         m.reply texts.reverse.join("\n")
149       end
150       return true
151     else
152       if friends
153         rep = "could not get status for #{nick}'s friends"
154         rep << ", try asking in private" unless m.private?
155       else
156         rep = "could not get status for #{nick}"
157       end
158       m.reply rep
159       return false
160     end
161   end
162
163   def deauthorize(m, params)
164     if @registry.has_key?(m.sourcenick + "_request_token")
165       @registry.delete(m.sourcenick + "_request_token")
166     end
167     if @registry.has_key?(m.sourcenick + "_access_token")
168       @registry.delete(m.sourcenick + "_access_token")
169     end
170     m.reply "Done! You can reauthorize this account in the future by using 'twitter authorize'"
171   end
172
173   def authorize(m, params)
174     failed_action = "we can't complete the authorization process"
175     unless @has_oauth
176       report_oauth_missing(m, failed_action)
177       return false
178     end
179
180     #remove all old authorization data
181     if @registry.has_key?(m.sourcenick + "_request_token")
182       @registry.delete(m.sourcenick + "_request_token")
183     end
184     if @registry.has_key?(m.sourcenick + "_access_token")
185       @registry.delete(m.sourcenick + "_access_token")
186     end
187
188     key = @bot.config['twitter.key']
189     secret = @bot.config['twitter.secret']
190     if key.empty? or secret.empty?
191       report_key_missing(m, failed_action)
192       return false
193     end
194
195     @consumer = OAuth::Consumer.new(key, secret, {   
196       :site => "https://api.twitter.com",
197       :request_token_path => "/oauth/request_token",
198       :access_token_path => "/oauth/access_token",
199       :authorize_path => "/oauth/authorize"
200       } )
201     @request_token = @consumer.get_request_token
202     @registry[m.sourcenick + "_request_token"] = YAML::dump(@request_token)        
203     m.reply "Go to this URL to get your authorization PIN, then use 'twitter pin <pin>' to finish authorization: " + @request_token.authorize_url
204   end
205
206   def pin(m, params)
207      unless @registry.has_key?(m.sourcenick + "_request_token")
208        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"
209        return false
210      end
211      @request_token = YAML::load(@registry[m.sourcenick + "_request_token"])
212      begin
213        @access_token = @request_token.get_access_token( { :oauth_verifier => params[:pin] } )
214      rescue
215        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"
216        return false
217      end
218      @registry[m.sourcenick + "_access_token"] = YAML::dump(@access_token)
219      m.reply "Okay, you're all set"
220   end
221
222   # update the status on twitter
223   def update_status(m, params)
224     unless @has_oauth
225       report_oauth_missing(m, "I cannot update your status")
226       return false
227     end
228
229     unless @registry.has_key?(m.sourcenick + "_access_token")
230        m.reply "You must first authorize your Twitter account before tweeting."
231        return false;
232     end
233     @access_token = YAML::load(@registry[m.sourcenick + "_access_token"])
234
235     uri = "https://api.twitter.com/statuses/update.json"
236     msg = params[:status].to_s
237
238     if msg.length > 140
239       m.reply "your status message update is too long, please keep it under 140 characters"
240       return
241     end
242
243     response = @access_token.post(uri, { :status => msg })
244     debug response
245
246     reply_method = params[:notify] ? :notify : :reply
247     if response.class == Net::HTTPOK
248       m.__send__(reply_method, "status updated")
249     else
250       m.__send__(reply_method, "could not update status")
251     end
252   end
253
254   # ties a nickname to a twitter username and password
255   def identify(m, params)
256     @registry[m.sourcenick + "_username"] = params[:username].to_s
257     @registry[m.sourcenick + "_password"] = params[:password].to_s
258     m.reply "you're all set up!"
259   end
260
261   # update on ACTION if the user has enabled the option
262   # Possible TODO: move the has_oauth check further down and alert
263   # the user the first time we do not update because of the missing oauth
264   def ctcp_listen(m)
265     return unless @has_oauth
266     return unless m.action?
267     return unless @registry[m.sourcenick + "_actions"]
268     update_status(m, :status => m.message, :notify => true)
269   end
270
271   # show or toggle action twitting
272   def actions(m, params)
273     case params[:toggle]
274     when 'on'
275       @registry[m.sourcenick + "_actions"] = true
276       m.okay
277     when 'off'
278       @registry.delete(m.sourcenick + "_actions")
279       m.okay
280     else
281       if @registry[m.sourcenick + "_actions"]
282         m.reply _("actions will be twitted")
283       else
284         m.reply _("actions will not be twitted")
285       end
286     end
287   end
288 end
289
290 # create an instance of our plugin class and register for the "length" command
291 plugin = TwitterPlugin.new
292 plugin.map 'twitter update *status', :action => "update_status", :threaded => true
293 plugin.map 'twitter authorize', :action => "authorize", :public => false
294 plugin.map 'twitter deauthorize', :action => "deauthorize", :public => false
295 plugin.map 'twitter pin :pin', :action => "pin", :public => false
296 plugin.map 'twitter actions [:toggle]', :action => "actions", :requirements => { :toggle => /^on|off$/ }
297 plugin.map 'twitter status [:nick]', :action => "get_status", :threaded => true
298 plugin.map 'twitter :friends [status] [:nick]', :action => "get_status", :requirements => { :friends => /^friends?$/ }, :threaded => true