wschenk-workstreamer_api 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/.document +5 -0
  2. data/.gitignore +9 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +23 -0
  5. data/Rakefile +52 -0
  6. data/VERSION +1 -0
  7. data/lib/workstreamer_api.rb +375 -0
  8. data/rails_example/Rakefile +10 -0
  9. data/rails_example/app/controllers/application.rb +15 -0
  10. data/rails_example/app/controllers/user_controller.rb +12 -0
  11. data/rails_example/app/controllers/workstreamer_controller.rb +154 -0
  12. data/rails_example/app/helpers/application_helper.rb +3 -0
  13. data/rails_example/app/helpers/user_helper.rb +2 -0
  14. data/rails_example/app/helpers/workstreamer_helper.rb +2 -0
  15. data/rails_example/app/models/user.rb +2 -0
  16. data/rails_example/app/views/layouts/application.rhtml +158 -0
  17. data/rails_example/app/views/workstreamer/index.html.erb +1 -0
  18. data/rails_example/app/views/workstreamer/info.html.erb +25 -0
  19. data/rails_example/app/views/workstreamer/stream.html.erb +24 -0
  20. data/rails_example/config/app_config.yml +3 -0
  21. data/rails_example/config/boot.rb +109 -0
  22. data/rails_example/config/database.yml +22 -0
  23. data/rails_example/config/environment.rb +77 -0
  24. data/rails_example/config/environments/development.rb +17 -0
  25. data/rails_example/config/environments/production.rb +24 -0
  26. data/rails_example/config/environments/test.rb +22 -0
  27. data/rails_example/config/initializers/inflections.rb +10 -0
  28. data/rails_example/config/initializers/mime_types.rb +5 -0
  29. data/rails_example/config/initializers/new_rails_defaults.rb +17 -0
  30. data/rails_example/config/locales/en.yml +5 -0
  31. data/rails_example/config/routes.rb +43 -0
  32. data/rails_example/db/migrate/20090611183940_create_users.rb +15 -0
  33. data/rails_example/db/schema.rb +23 -0
  34. data/rails_example/doc/README_FOR_APP +5 -0
  35. data/rails_example/public/404.html +30 -0
  36. data/rails_example/public/422.html +30 -0
  37. data/rails_example/public/500.html +33 -0
  38. data/rails_example/public/dispatch.cgi +10 -0
  39. data/rails_example/public/dispatch.fcgi +24 -0
  40. data/rails_example/public/dispatch.rb +10 -0
  41. data/rails_example/public/favicon.ico +0 -0
  42. data/rails_example/public/images/rails.png +0 -0
  43. data/rails_example/public/javascripts/application.js +2 -0
  44. data/rails_example/public/javascripts/controls.js +963 -0
  45. data/rails_example/public/javascripts/dragdrop.js +973 -0
  46. data/rails_example/public/javascripts/effects.js +1128 -0
  47. data/rails_example/public/javascripts/prototype.js +4320 -0
  48. data/rails_example/public/robots.txt +5 -0
  49. data/rails_example/script/about +4 -0
  50. data/rails_example/script/console +3 -0
  51. data/rails_example/script/dbconsole +3 -0
  52. data/rails_example/script/destroy +3 -0
  53. data/rails_example/script/generate +3 -0
  54. data/rails_example/script/performance/benchmarker +3 -0
  55. data/rails_example/script/performance/profiler +3 -0
  56. data/rails_example/script/performance/request +3 -0
  57. data/rails_example/script/plugin +3 -0
  58. data/rails_example/script/process/inspector +3 -0
  59. data/rails_example/script/process/reaper +3 -0
  60. data/rails_example/script/process/spawner +3 -0
  61. data/rails_example/script/runner +3 -0
  62. data/rails_example/script/server +3 -0
  63. data/rails_example/test/fixtures/users.yml +7 -0
  64. data/rails_example/test/functional/user_controller_test.rb +8 -0
  65. data/rails_example/test/functional/workstreamer_controller_test.rb +8 -0
  66. data/rails_example/test/performance/browsing_test.rb +9 -0
  67. data/rails_example/test/test_helper.rb +38 -0
  68. data/rails_example/test/unit/user_test.rb +8 -0
  69. data/spec/spec_helper.rb +9 -0
  70. data/spec/workstreamer_api_spec.rb +7 -0
  71. metadata +143 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ rails_example/tmp
7
+ rails_example/log/*
8
+ rails_example/db/*.sqlite3
9
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Will Schenk
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,23 @@
1
+ = workstreamer_api
2
+
3
+ This gem provides a ruby wrapper to interface with workstreamer.com. It
4
+ depends on the "nokogiri" and "oauth" gems. Also included is an example
5
+ rails application which has an example of how to use the gem.
6
+
7
+ Documentation on how to use the API can be found http://workstreamer.pbworks.com/
8
+
9
+ To use the rails application, you need to first get a ClientApplication key from
10
+ workstreamer as documented here: http://workstreamer.pbworks.com/Authentication
11
+
12
+ Then edit rails_example/config/app_config.yml
13
+
14
+ == Copyright
15
+
16
+ Copyright (c) 2009 Workstreamer, LLC. See LICENSE for details.
17
+ = workstreamer_api
18
+
19
+ Description goes here.
20
+
21
+ == Copyright
22
+
23
+ Copyright (c) 2009 Will Schenk. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "workstreamer_api"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = "wschenk@gmail.com"
10
+ gem.homepage = "http://github.com/wschenk/workstreamer_api"
11
+ gem.authors = ["Will Schenk"]
12
+ gem.rubyforge_project = "workstreamer_api"
13
+ gem.add_dependency 'oauth'
14
+ gem.add_dependency 'nokogiri'
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+
18
+ Jeweler::RubyforgeTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ if File.exist?('VERSION.yml')
41
+ config = YAML.load(File.read('VERSION.yml'))
42
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
43
+ else
44
+ version = ""
45
+ end
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "workstreamer_api #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
52
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,375 @@
1
+ require 'rubygems'
2
+ require 'oauth'
3
+ require 'pp'
4
+ require 'json'
5
+ require 'nokogiri'
6
+
7
+ ##
8
+ # The WsApi module wraps all functionality needed to interface with the
9
+ # workstreamer system.
10
+ module WorkstreamerApi
11
+ ##
12
+ # Thrown if any call returns a 401; this means that you don't have a valid
13
+ # AccessToken. It either never existed, or the token was invalidated by the
14
+ # user.
15
+ class Unauthorized < Exception ; end
16
+ class Forbidden < Exception ; end
17
+
18
+ ##
19
+ # This is the main workstreamer class. The key and secret need to have
20
+ # been generated on the workstreamer site. If the user doesn't have an
21
+ # AccessToken, this class can be used to generate a RequestToken.
22
+ class Api
23
+ ##
24
+ # Create a new workstreamer api instance.
25
+ #
26
+ # key - the ClientApplication key generated for your application by
27
+ # workstreamer.com at http://alpha.workstreamer.com/oauth_clients
28
+ # secret - that matches the key
29
+ # site - normally "alpha.workstreamer.com" TODO This needs to be domain aware
30
+ #
31
+ # This is all that is needed to create a RequestToken for a user who
32
+ # hasn't given permission. (See request_token for more info.) If
33
+ # the user has already granted access, be sure to pass in
34
+ #
35
+ # user_token - the AccessToken back from the oath provider
36
+ # user_secret - the secret.
37
+ #
38
+ # See example/app/controllers/workstreamer_controller.callback for an example.
39
+ #
40
+ def initialize( key, secret, site = "http://localhost:3000", user_token = nil, user_secret = nil )
41
+ @key = key
42
+ @secret = secret
43
+ @site = site
44
+ @user_token = user_token
45
+ @user_secret = user_secret
46
+ end
47
+
48
+ # Utility methods
49
+
50
+ ##
51
+ # Returns a (cached) OAuth::Consumer
52
+ #
53
+ def consumer
54
+ @consumer ||= OAuth::Consumer.new @key, @secret, { :site => @site }
55
+ end
56
+
57
+ ##
58
+ # This generates a request token for your application. Once a RequestToken
59
+ # is generated you then redirect the browser to the authorize_url. e.g.
60
+ #
61
+ # request_token = workstreamer.request_token
62
+ #
63
+ # session[:request_token] = request_token.token
64
+ # session[:request_token_secret] = request_token.secret
65
+ #
66
+ # redirect_to request_token.authorize_url
67
+ #
68
+ def request_token
69
+ @request_token ||= consumer.get_request_token
70
+ end
71
+
72
+ ##
73
+ # This returns a (cached) OAuth::AccessToken which can, in turn, be used
74
+ # to make remote HTTP calls to workstreamer. All of the API wrapper calls use
75
+ # this method for the actual network transportation.
76
+ #
77
+ def access_token
78
+ @access_token ||= OAuth::AccessToken.new( consumer, @user_token, @user_secret )
79
+ end
80
+
81
+ ##
82
+ # OAuth-orized HTTP GET to workstreamer. Passed in a URL it will
83
+ # return a parsed object. If JSON is requested, it will return a Hash.
84
+ # Otherwise, it will return a nogokiri XML object. (convert_node_to_hash will
85
+ # transform a nogokiri XML object to a Hash but won't preserve the XML attributes.)
86
+ #
87
+ def get( url, json = true )
88
+ ret = access_token.get( url )
89
+ parse( ret, json )
90
+ end
91
+
92
+ ##
93
+ # OAuth-orized HTTP POST to workstreamer. Passed in a URL and a hash or
94
+ # parameters it will return a parsed object. If JSON is requested, it
95
+ # will return a Hash. Otherwise, it will return a nogokiri XML object.
96
+ # (convert_node_to_hash will transform a nogokiri XML object to a Hash
97
+ # but won't preserve the XML attributes.)
98
+ #
99
+ def post( url, parameters, json = true )
100
+ ret = access_token.post url, parameters
101
+ parse( ret, json )
102
+ end
103
+
104
+ ##
105
+ # Intepret a server response. Raise WsApi::Unauthorized or WsApi::HTTPSuccess
106
+ # if there was an error, otherwise process the body through JSON.parse or
107
+ # Nokogiri::XML.
108
+ #
109
+ # TODO Should probably do this on the returned content-type rather than parameter
110
+ def parse( ret, json = true )
111
+ case ret
112
+ when Net::HTTPSuccess
113
+ # logger.debug ret.body
114
+ if json
115
+ return JSON.parse(ret.body)
116
+ else
117
+ return Nokogiri::XML( ret.body )
118
+ end
119
+ when Net::HTTPUnauthorized
120
+ raise Unauthorized
121
+ when Net::HTTPForbidden
122
+ raise Forbidden
123
+ else
124
+ puts ret
125
+ puts ret.body
126
+ raise Unknown
127
+ end
128
+ end
129
+
130
+ ##
131
+ # Used to build the parameter options; will only create an entry if
132
+ # the options is set, and will collapse arrays to comma seperated.
133
+ #
134
+ # params - hash to set (destination)
135
+ # key - the key to move over
136
+ # options - hash that we're getting the data from (origin)
137
+ #
138
+ def set_options( params, key, options )
139
+ if options[key].is_a? Array
140
+ params[key] = options[key].join( "," )
141
+ elsif options[key]
142
+ params[key] = options[key]
143
+ end
144
+ end
145
+
146
+ ##
147
+ # Simple method to turn a Nokogiri XML object into a ruby hash.
148
+ #
149
+ def convert_node_to_hash( xml )
150
+ ret = {}
151
+
152
+ xml.children.each do |child|
153
+ if child.is_a? Nokogiri::XML::Element
154
+ subchildren = child.children
155
+ if subchildren.size == 1
156
+ ret[child.name] = child.inner_html
157
+ else
158
+ ret[child.name] = convert_node_to_hash( child )
159
+ end
160
+ end
161
+ end
162
+
163
+ ret
164
+ end
165
+
166
+ # Api methods
167
+
168
+ ##
169
+ # Looks up information on a specific user. If login is null, looks up the
170
+ # current/authorized user.
171
+ #
172
+ # login - workstreamer login or user id
173
+ #
174
+ def user_info( login = nil )
175
+ url = "/user/info?format=json"
176
+ url += "&login=#{login}" if login
177
+ get url
178
+ end
179
+
180
+ ##
181
+ # Returns the user's current stream.
182
+ #
183
+ # TODO Stream filters
184
+ # TODO Unread counts
185
+ #
186
+ def stream
187
+ xml = get( "/post_apis/index?format=xml", false )
188
+
189
+ ret = []
190
+
191
+ (xml/'master-content').each do |mc|
192
+ ret << convert_node_to_hash( mc )
193
+ end
194
+
195
+ ret
196
+ end
197
+
198
+ ##
199
+ # Generate create_content method, which all other content creating methods
200
+ # -- except for FeedItems -- delegate to. Depending upon the parameters,
201
+ # content of a different type is created.
202
+ #
203
+ # See http://workstreamer.pbworks.com/Content-API for details on the
204
+ # parameters to pass in.
205
+ #
206
+ def create_content( parameters )
207
+ xml = post( "/post_apis/create", parameters, false )
208
+
209
+ return false if xml.nil?
210
+
211
+ node = xml.at( "result" )
212
+ return false if node.nil?
213
+ node.content == "SUCCESS"
214
+ end
215
+
216
+ ##
217
+ # Create a status update. The content is parsed on the worksteamer server,
218
+ # so if you use @user or #tag the status update with be directed to a user
219
+ # and/or tagged.
220
+ #
221
+ # message - The status update.
222
+ #
223
+ # TODO: Add project
224
+ #
225
+ def post_status_update( message )
226
+ create_content( {:content => message } )
227
+ end
228
+
229
+ ##
230
+ # Create a direct message from the current user to user_name.
231
+ #
232
+ # user_name - User to send the message to
233
+ # message - The message
234
+ #
235
+ def post_direct_message( user_name, message )
236
+ create_content( {:content => "d @#{user_name} #{message}"} )
237
+ end
238
+
239
+ ##
240
+ # Lookup multiple users at the same time. Returns an array of the
241
+ # same type as user_info returns.
242
+ #
243
+ # users - list of users
244
+ #
245
+ def lookup_users( users )
246
+ users = users.gsub( /([\s,]+)/, " " ).split( / / )
247
+
248
+ ret = {}
249
+ users.each do |login|
250
+ ret[login] = user_info( login )
251
+ end
252
+
253
+ ret
254
+ end
255
+
256
+ ##
257
+ # Lookup multuple users but return an array of user_ids
258
+ #
259
+ def lookup_user_ids( users )
260
+ lookup_users( users ).collect { |x| x[1]['id'] }
261
+ end
262
+
263
+ ##
264
+ # Get project title and membership.
265
+ #
266
+ # project - Project name or project id
267
+ #
268
+ def lookup_project( project )
269
+ get "/projects/view?format=json&name=#{URI::escape project}"
270
+ end
271
+
272
+ ##
273
+ # Post a message with optional comma seperated tags.
274
+ #
275
+ # title - the title of the message
276
+ # body - the body of the message
277
+ #
278
+ # Options include:
279
+ #
280
+ # :tags => "tag1,tag2,tag3"
281
+ #
282
+ # :recipient_ids => "user_id1,user_id2,user_id3" or [user1_id, user2_id, user3_id]
283
+ # or
284
+ # :recipients => "tom,dick,harry" or ["tom", "dick", "harry"]
285
+ #
286
+ # :project => "project_name"
287
+ # or
288
+ # :project_id => "project_id"
289
+ #
290
+ def post_message( title, body, options )
291
+ params = {:title => title, :content => body }
292
+ set_options( params, :tags, options )
293
+ options[:recipient_ids] = options[:recipient_ids].join( "," ) if options[:recipient_ids]
294
+ options[:recipient_ids] = lookup_user_ids( options[:recipients] ) if options[:recipients]
295
+ options[:recipients] = options[:recipient_ids]
296
+ set_options( params, :recipients, options )
297
+
298
+ options[:project_id] = lookup_project( options[:project] )['id'] if options[:project]
299
+
300
+ set_options( params, :project_id, options )
301
+
302
+ pp params
303
+
304
+ create_content( params )
305
+ end
306
+
307
+ ##
308
+ # Creates an event
309
+ #
310
+ # title - Event title
311
+ # start_date - Defaults to today
312
+ # end_date - when the event ends
313
+ #
314
+ # TODO Add project and permissions.
315
+ def create_event( title, start_date = Date.today, end_date = nil )
316
+ params = {:title => title, :start_date => start_date }
317
+ params[:end_date] = end_date if end_date
318
+ create_content( params )
319
+ end
320
+
321
+ ##
322
+ # Creates a task
323
+ #
324
+ # title - the Task title
325
+ # assigned_to - The user name or user id of who the task is assigned to
326
+ # content - the body of the task
327
+ # project_id - the optional project
328
+ #
329
+ def create_task( title, assigned_to, content, project_id = nil )
330
+ unless assigned_to =~ /^\d+$/
331
+ assigned_to = lookup_user_ids( assigned_to ).first
332
+ end
333
+
334
+ create_content({:title => title, :assigned_to => assigned_to, :content => content, :project_id => project_id } )
335
+ end
336
+
337
+ ##
338
+ # Create a comment on an existing content.
339
+ #
340
+ # comment_text - the comment text
341
+ # parent_id - the id of the MasterContent that you are commenting on. This
342
+ # is NOT the id of the content itself, but the enclosing
343
+ # MasterContent
344
+ #
345
+ def create_comment( comment_text, parent_id )
346
+ create_content({:comment => comment_text, :reply_to => parent_id})
347
+ end
348
+
349
+ ##
350
+ # Return everything we know about a content.
351
+ #
352
+ # id - The content id.
353
+ #
354
+ def get_content( id )
355
+ url = "/post_apis/show?id=#{id}"
356
+
357
+ xml = get url, false
358
+
359
+ convert_node_to_hash xml
360
+ end
361
+
362
+ ##
363
+ # Create a feed item. Params are
364
+ #
365
+ # :remote_id - The remote id for this feed item (uninterpreted)
366
+ # :created_at - The created_at/published date, defaults to now
367
+ # :title - Title of the item
368
+ # :summary - The body of the item shown when expanded
369
+ # :url - The url that represents the "original" item
370
+ #
371
+ def post_feed_item( params )
372
+ post "/feed/post_item", params, false
373
+ end
374
+ end
375
+ end