uservoice-ruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ /spec/config.yml
6
+ *.sw*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in uservoice.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ UserVoice gem for API connections
2
+ =================================
3
+
4
+ This gem allows you to easily:
5
+ * Generate SSO token for creating SSO users / logging them into UserVoice (http://uservoice.com).
6
+ * Do 3-legged and 2-legged UserVoice API calls safely without having to worry about the cryptographic details.
7
+
8
+ Installation
9
+ ============
10
+
11
+ Place this in your Gemfile:
12
+ ```ruby
13
+ gem 'uservoice', :git => 'https://github.com/uservoice/uservoice-ruby'
14
+ ```
15
+ Run the bundle command and then try one of the examples below.
16
+
17
+ Examples
18
+ ========
19
+
20
+ Prerequisites:
21
+ * Suppose your UserVoice site is at http://uservoice-subdomain.uservoice.com/ and **USERVOICE\_SUBDOMAIN** = uservoice-subdomain
22
+ * **SSO\_KEY** = 982c88f2df72572859e8e23423eg87ed (Admin Console -> Settings -> General -> User Authentication)
23
+ * The account has a following API client (Admin Console -> Settings -> Channels -> API):
24
+ * **API\_KEY** = oQt2BaunWNuainc8BvZpAm
25
+ * **API\_SECRET** = 3yQMSoXBpAwuK3nYHR0wpY6opE341inL9a2HynGF2
26
+
27
+
28
+ SSO-token generation using uservoice gem
29
+ ----------------------------------------
30
+
31
+ SSO-token can be used to create sessions for SSO users. They are capable of synchronizing the user information from one system to another.
32
+ Generating the SSO token from SSO key and given uservoice subdomain can be done by calling UserVoice.generate\_sso\_token method like this:
33
+
34
+ ```ruby
35
+ require 'uservoice'
36
+ sso_token = UserVoice.generate_sso_token(USERVOICE_SUBDOMAIN, SSO_KEY, {
37
+ :guid => 1001,
38
+ :display_name => "John Doe",
39
+ :email => 'john.doe@example.com'
40
+ })
41
+
42
+ # Now this URL will log John Doe in:
43
+ puts "https://#{USERVOICE_SUBDOMAIN}.uservoice.com/?sso=#{sso_token}"
44
+ ```
45
+
46
+ Making API calls
47
+ ----------------
48
+
49
+ With the gem you need to create an instance of UserVoice::Oauth. You get
50
+ API_KEY and API_SECRET from an API client which you can create in Admin Console
51
+ -> Settings -> Channels -> API.
52
+
53
+ ```ruby
54
+ require 'uservoice'
55
+ begin
56
+ uservoice_client = UserVoice::Client.new(USERVOICE_SUBDOMAIN, API_KEY, API_SECRET)
57
+
58
+ # Get users of a subdomain (requires trusted client, but no user)
59
+ users = uservoice_client.get("/api/v1/users.json?per_page=3")['users']
60
+ users.each do |user|
61
+ puts "User: \"#{user['name']}\", Profile URL: #{user['url']}"
62
+ end
63
+
64
+ # Now, let's login as mailaddress@example.com, a regular user
65
+ uservoice_client.login_as('mailaddress@example.com')
66
+
67
+ # Example request #1: Get current user.
68
+ user = uservoice_client.get("/api/v1/users/current.json")['user']
69
+
70
+ puts "User: \"#{user['name']}\", Profile URL: #{user['url']}"
71
+
72
+ # Login as account owner
73
+ uservoice_client.login_as_owner
74
+
75
+ # Example request #2: Create a new private forum limited to only example.com email domain.
76
+ forum = uservoice_client.post("/api/v1/forums.json", :forum => {
77
+ :name => 'Example.com Private Feedback',
78
+ :private => true,
79
+ :allow_by_email_domain => true,
80
+ :allowed_email_domains => [{:domain => 'example.com'}]
81
+ })['forum']
82
+
83
+ puts "Forum '#{forum['name']}' created! URL: #{forum['url']}"
84
+ rescue UserVoice::Unauthorized => e
85
+ # Thrown usually due to faulty tokens, untrusted client or if attempting
86
+ # operations without Admin Privileges
87
+ raise
88
+ end
89
+ ```
90
+
91
+ Verifying a UserVoice user
92
+ --------------------------
93
+
94
+ If you want to make calls on behalf of a user, but want to make sure he or she
95
+ actually owns certain email address in UserVoice, you need to use 3-Legged API
96
+ calls. Just pass your user an authorize link to click, so that user may grant
97
+ your site permission to access his or her data in UserVoice.
98
+
99
+ ```ruby
100
+ require 'uservoice'
101
+ CALLBACK_URL = 'http://localhost:3000/' # your site
102
+
103
+ uservoice_client = UserVoice::Client.new(USERVOICE_SUBDOMAIN, API_KEY, API_SECRET, :callback => CALLBACK_URL)
104
+
105
+ # At this point you want to print/redirect to uservoice_client.authorize_url in your application.
106
+ # Here we just output them as this is a command-line example.
107
+ puts "1. Go to #{uservoice_client.authorize_url} and click \"Allow access\"."
108
+ puts "2. Then type the oauth_verifier which is passed as a GET parameter to the callback URL:"
109
+
110
+ # In a web app we would get the oauth_verifier through a redirect from UserVoice (after a redirection back to CALLBACK_URL).
111
+ # In this command-line example we just read it from stdin:
112
+ uservoice_client.login_verified_user(gets.match('\w*').to_s)
113
+
114
+ # All done. Now we can read the current user to know user's email address:
115
+ user = uservoice_client.get("/api/v1/users/current.json")['user']
116
+
117
+ puts "User logged in, Name: #{user['name']}, email: #{user['email']}"
118
+ ```
119
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :spec do
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.pattern = './spec/**/*_spec.rb'
7
+ end
8
+ end
@@ -0,0 +1,160 @@
1
+ require "uservoice/version"
2
+ require 'rubygems'
3
+ require 'ezcrypto'
4
+ require 'json'
5
+ require 'cgi'
6
+ require 'base64'
7
+ require 'oauth'
8
+
9
+ module UserVoice
10
+ EMAIL_FORMAT = %r{^(\w[-+.\w!\#\$%&'\*\+\-/=\?\^_`\{\|\}~]*@([-\w]*\.)+[a-zA-Z]{2,9})$}
11
+ DEFAULT_HEADERS = { 'Content-Type'=> 'application/json', 'Accept'=> 'application/json' }
12
+
13
+ class APIError < RuntimeError
14
+ end
15
+ Unauthorized = Class.new(APIError)
16
+ NotFound = Class.new(APIError)
17
+ ApplicationError = Class.new(APIError)
18
+
19
+ def self.generate_sso_token(subdomain_key, sso_key, user_hash, valid_for = 5 * 60)
20
+ user_hash[:expires] ||= (Time.now.utc + valid_for).to_s unless valid_for.nil?
21
+ unless user_hash[:email].to_s.match(EMAIL_FORMAT)
22
+ raise Unauthorized.new("'#{user_hash[:email]}' is not a valid email address")
23
+ end
24
+ unless sso_key.to_s.length > 1
25
+ raise Unauthorized.new("Please specify your SSO key")
26
+ end
27
+
28
+ key = EzCrypto::Key.with_password(subdomain_key, sso_key)
29
+ encrypted = key.encrypt(user_hash.to_json)
30
+ encoded = Base64.encode64(encrypted).gsub(/\n/,'')
31
+
32
+ return CGI.escape(encoded)
33
+ end
34
+
35
+ class Client
36
+
37
+ def initialize(*args)
38
+ case args.size
39
+ when 3,4
40
+ init_subdomain_and_api_keys(*args)
41
+ when 1,2
42
+ init_consumer_and_access_token(*args)
43
+ end
44
+ end
45
+
46
+ def init_subdomain_and_api_keys(subdomain_name, api_key, api_secret, attrs={})
47
+ consumer = OAuth::Consumer.new(api_key, api_secret, {
48
+ :site => "#{attrs[:protocol] || 'https'}://#{subdomain_name}.#{attrs[:uservoice_domain] || 'uservoice.com'}"
49
+ })
50
+ init_consumer_and_access_token(consumer, attrs)
51
+ end
52
+
53
+ def init_consumer_and_access_token(consumer, attrs={})
54
+ @consumer = consumer
55
+ @token = OAuth::AccessToken.new(@consumer, attrs[:oauth_token] || '', attrs[:oauth_token_secret] || '')
56
+ @response_format = attrs[:response_format] || :hash
57
+ @callback = attrs[:callback]
58
+ end
59
+
60
+ def authorize_url
61
+ request_token.authorize_url
62
+ end
63
+
64
+ def login_with_verifier(oauth_verifier)
65
+ token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
66
+ Client.new(@consumer, :oauth_token => token.token, :oauth_token_secret => token.secret)
67
+ end
68
+
69
+ def login_with_access_token(oauth_token, oauth_token_secret, &block)
70
+ token = Client.new(@consumer, :oauth_token => oauth_token, :oauth_token_secret => oauth_token_secret)
71
+ if block_given?
72
+ yield token
73
+ else
74
+ return token
75
+ end
76
+ end
77
+
78
+ def token
79
+ @token.token
80
+ end
81
+
82
+ def secret
83
+ @token.secret
84
+ end
85
+
86
+ def request_token
87
+ @consumer.get_request_token(:oauth_callback => @callback)
88
+ end
89
+
90
+ def login_as_owner(&block)
91
+ token = post('/api/v1/users/login_as_owner.json', {
92
+ 'request_token' => request_token.token
93
+ })['token']
94
+ if token
95
+ login_with_access_token(token['oauth_token'], token['oauth_token_secret'], &block)
96
+ else
97
+ raise Unauthorized.new("Could not get Access Token")
98
+ end
99
+ end
100
+
101
+ def login_as(email, &block)
102
+ unless email.to_s.match(EMAIL_FORMAT)
103
+ raise Unauthorized.new("'#{email}' is not a valid email address")
104
+ end
105
+ token = post('/api/v1/users/login_as.json', {
106
+ :user => { :email => email },
107
+ :request_token => request_token.token
108
+ })['token']
109
+
110
+ if token
111
+ login_with_access_token(token['oauth_token'], token['oauth_token_secret'], &block)
112
+ else
113
+ raise Unauthorized.new("Could not get Access Token")
114
+ end
115
+ end
116
+
117
+ def request(method, uri, request_body={}, headers={})
118
+ headers = DEFAULT_HEADERS.merge(headers)
119
+
120
+ if headers['Content-Type'] == 'application/json' && request_body.is_a?(Hash)
121
+ request_body = request_body.to_json
122
+ end
123
+
124
+ response = case method.to_sym
125
+ when :post, :put
126
+ @token.request(method, uri, request_body, headers)
127
+ when :head, :delete, :get
128
+ @token.request(method, uri, headers)
129
+ else
130
+ raise RuntimeError.new("Invalid HTTP method #{method}")
131
+ end
132
+
133
+ return case @response_format.to_s
134
+ when 'raw'
135
+ response
136
+ else
137
+ attrs = JSON.parse(response.body)
138
+ if attrs && attrs['errors']
139
+ case attrs['errors']['type']
140
+ when 'unauthorized'
141
+ raise Unauthorized.new(attrs)
142
+ when 'record_not_found'
143
+ raise NotFound.new(attrs)
144
+ when 'application_error'
145
+ raise ApplicationError.new(attrs)
146
+ else
147
+ raise APIError.new(attrs)
148
+ end
149
+ end
150
+ attrs
151
+ end
152
+ end
153
+
154
+ %w(get post delete put).each do |method|
155
+ define_method(method) do |*args|
156
+ request(method, *args)
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,3 @@
1
+ module Uservoice
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ require 'uservoice/user_voice'
@@ -0,0 +1,4 @@
1
+ subdomain_name: uservoice
2
+ api_key: GIOIuigIGuisguiGisg3Gd
3
+ api_secret: GIIGDSDUGI678sGoOrGvGJSGISKSDGTEWUfhGoW9Gu
4
+ sso_key: 678bfe76feb876feb786fbe876feb67e
@@ -0,0 +1,186 @@
1
+ require 'spec_helper'
2
+
3
+ describe UserVoice do
4
+
5
+ it "should generate SSO token" do
6
+ token = UserVoice.generate_sso_token(config['subdomain_name'], config['sso_key'], {
7
+ :display_name => "User Name",
8
+ :email => 'mailaddress@example.com'
9
+ })
10
+ encrypted_raw_data = Base64.decode64(CGI.unescape(token))
11
+
12
+ key = EzCrypto::Key.with_password(config['subdomain_name'], config['sso_key'])
13
+ key.decrypt(encrypted_raw_data).should match('mailaddress@example.com')
14
+ end
15
+
16
+ describe UserVoice::Client do
17
+ subject { UserVoice::Client.new(config['subdomain_name'],
18
+ config['api_key'],
19
+ config['api_secret'],
20
+ :uservoice_domain => config['uservoice_domain'],
21
+ :protocol => config['protocol']) }
22
+
23
+ it "should get user names from the API" do
24
+ users = subject.get("/api/v1/users.json?per_page=3")
25
+ user_names = users['users'].map { |user| user['name'] }
26
+ user_names.all?.should == true
27
+ user_names.size.should == 3
28
+ end
29
+
30
+ it "should not get current user without logged in user" do
31
+ lambda do
32
+ user = subject.get("/api/v1/users/current.json")
33
+ end.should raise_error(UserVoice::Unauthorized)
34
+ end
35
+
36
+ it "should be able to get access token as owner" do
37
+ subject.login_as_owner do |owner|
38
+ owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
39
+
40
+ owner.login_as('regular@example.com') do |regular|
41
+ owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
42
+ @user = regular.get("/api/v1/users/current.json")['user']
43
+ @user['roles']['owner'].should == false
44
+ end
45
+
46
+ owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
47
+ end
48
+ # ensure blocks got run
49
+ @user['email'].should == 'regular@example.com'
50
+ end
51
+
52
+ it "should not be able to create KB article as nobody" do
53
+ lambda do
54
+ result = subject.post("/api/v1/articles.json", :article => {
55
+ :title => 'good morning'
56
+ })
57
+ end.should raise_error(UserVoice::Unauthorized)
58
+ end
59
+
60
+ it "should be able to create and delete a forum as the owner" do
61
+ owner = subject.login_as_owner
62
+ forum = owner.post("/api/v1/forums.json", :forum => {
63
+ :name => 'Test forum from RSpec',
64
+ 'private' => true,
65
+ 'allow_by_email_domain' => true,
66
+ 'allowed_email_domains' => [{'domain' => 'raimo.rspec.example.com'}]
67
+ })['forum']
68
+
69
+ forum['id'].should be_a(Integer)
70
+
71
+ deleted_forum = owner.delete("/api/v1/forums/#{forum['id']}.json")['forum']
72
+ deleted_forum['id'].should == forum['id']
73
+ end
74
+
75
+ it "should get current user with 2-legged call" do
76
+ user = subject.login_as('mailaddress@example.com') do |token|
77
+ token.get("/api/v1/users/current.json")['user']
78
+ end
79
+
80
+ user['email'].should == 'mailaddress@example.com'
81
+ end
82
+
83
+ it "should get current user with copied access token" do
84
+ original_token = subject.login_as('mailaddress@example.com')
85
+
86
+ client = UserVoice::Client.new(config['subdomain_name'],
87
+ config['api_key'],
88
+ config['api_secret'],
89
+ :uservoice_domain => config['uservoice_domain'],
90
+ :protocol => config['protocol'],
91
+ :oauth_token => original_token.token,
92
+ :oauth_token_secret => original_token.secret)
93
+ # Also this works but creates an extra object:
94
+ # client = client.login_with_access_token(original_token.token, original_token.secret)
95
+
96
+ user = client.get("/api/v1/users/current.json")['user']
97
+
98
+ user['email'].should == 'mailaddress@example.com'
99
+ end
100
+
101
+ it "should login as an owner" do
102
+ me = subject.login_as_owner
103
+
104
+ owner = me.get("/api/v1/users/current.json")['user']
105
+ owner['roles']['owner'].should == true
106
+ end
107
+
108
+ it "should not be able to delete when not deleting on behalf of anyone" do
109
+ lambda {
110
+ result = subject.delete("/api/v1/users/#{234}.json")
111
+ }.should raise_error(UserVoice::Unauthorized, /user required/i)
112
+ end
113
+
114
+ it "should not be able to delete owner" do
115
+ owner_access_token = subject.login_as_owner
116
+
117
+ owner = owner_access_token.get("/api/v1/users/current.json")['user']
118
+
119
+ lambda {
120
+ result = owner_access_token.delete("/api/v1/users/#{owner['id']}.json")
121
+ }.should raise_error(UserVoice::Unauthorized, /last owner/i)
122
+ end
123
+
124
+ it "should not be able to delete user without login" do
125
+ regular_user = subject.login_as('somebodythere@example.com').get("/api/v1/users/current.json")['user']
126
+
127
+ lambda {
128
+ subject.delete("/api/v1/users/#{regular_user['id']}.json")
129
+ }.should raise_error(UserVoice::Unauthorized)
130
+ end
131
+
132
+ it "should be able to identify suggestions" do
133
+ owner_token = subject.login_as_owner
134
+ external_scope='sync_to_moon'
135
+ suggestions = owner_token.get("/api/v1/suggestions.json?filter=with_external_id&external_scope=#{external_scope}&manual_action=#{external_scope}")['suggestions']
136
+
137
+ identifications = suggestions.map {|s| { :id => s['id'], :external_id => s['id'].to_i*10 } }
138
+
139
+ ids = owner_token.put("/api/v1/suggestions/identify.json",
140
+ :external_scope => external_scope,
141
+ :identifications => identifications)['identifications']['ids']
142
+ ids.should == identifications.map { |s| s[:id] }.sort
143
+ end
144
+
145
+ it "should be able to delete itself" do
146
+ my_token = subject.login_as('somebodythere@example.com')
147
+
148
+ # whoami
149
+ my_id = my_token.get("/api/v1/users/current.json")['user']['id']
150
+
151
+ # Delete myself!
152
+ my_token.delete("/api/v1/users/#{my_id}.json")['user']['id'].should == my_id
153
+
154
+ # I don't exist anymore
155
+ lambda {
156
+ my_token.get("/api/v1/users/current.json")
157
+ }.should raise_error(UserVoice::NotFound)
158
+ end
159
+
160
+ it "should/be able to delete random user and login as him after that" do
161
+ somebody = subject.login_as('somebodythere@example.com')
162
+ owner = subject.login_as_owner
163
+
164
+ # somebody is still there...
165
+ regular_user = somebody.get("/api/v1/users/current.json")['user']
166
+ regular_user['email'].should == 'somebodythere@example.com'
167
+
168
+ # delete somebody!
169
+ owner.delete("/api/v1/users/#{regular_user['id']}.json")['user']['id'].should == regular_user['id']
170
+
171
+ # not found anymore!
172
+ lambda {
173
+ somebody.get("/api/v1/users/current.json")['errors']['type']
174
+ }.should raise_error(UserVoice::NotFound)
175
+
176
+ # this recreates somebody
177
+ somebody = subject.login_as('somebodythere@example.com')
178
+ somebody.get("/api/v1/users/current.json")['user']['id'].should_not == regular_user['id']
179
+ end
180
+
181
+ it "should raise error with invalid email parameter" do
182
+ expect { subject.login_as('ma') }.to raise_error(UserVoice::Unauthorized)
183
+ expect { subject.login_as(nil) }.to raise_error(UserVoice::Unauthorized)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,10 @@
1
+ require 'uservoice'
2
+ require 'yaml'
3
+
4
+ def config
5
+ begin
6
+ YAML.load_file(File.expand_path('../config.yml', __FILE__))
7
+ rescue Errno::ENOENT
8
+ raise "Configure your own config.yml and place it in the spec directory"
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "uservoice/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "uservoice-ruby"
7
+ s.version = Uservoice::VERSION
8
+ s.authors = ["Raimo Tuisku"]
9
+ s.email = ["dev@usevoice.com"]
10
+ s.homepage = "http://developer.uservoice.com"
11
+ s.summary = %q{Client library for UserVoice API}
12
+ s.description = %q{The gem provides Ruby-bindings to UserVoice API and helps generating Single-Sign-On tokens.}
13
+
14
+ s.rubyforge_project = "uservoice-ruby"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "rspec"
22
+ s.add_runtime_dependency 'ezcrypto'
23
+ s.add_runtime_dependency 'json'
24
+ s.add_runtime_dependency 'oauth'
25
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uservoice-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Raimo Tuisku
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70340927735480 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70340927735480
25
+ - !ruby/object:Gem::Dependency
26
+ name: ezcrypto
27
+ requirement: &70340927734840 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70340927734840
36
+ - !ruby/object:Gem::Dependency
37
+ name: json
38
+ requirement: &70340927732680 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70340927732680
47
+ - !ruby/object:Gem::Dependency
48
+ name: oauth
49
+ requirement: &70340927731780 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70340927731780
58
+ description: The gem provides Ruby-bindings to UserVoice API and helps generating
59
+ Single-Sign-On tokens.
60
+ email:
61
+ - dev@usevoice.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - Gemfile
68
+ - README.md
69
+ - Rakefile
70
+ - lib/uservoice-ruby.rb
71
+ - lib/uservoice/user_voice.rb
72
+ - lib/uservoice/version.rb
73
+ - spec/config.yml.templ
74
+ - spec/lib/user_voice_spec.rb
75
+ - spec/spec_helper.rb
76
+ - uservoice-ruby.gemspec
77
+ homepage: http://developer.uservoice.com
78
+ licenses: []
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project: uservoice-ruby
97
+ rubygems_version: 1.8.15
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: Client library for UserVoice API
101
+ test_files:
102
+ - spec/config.yml.templ
103
+ - spec/lib/user_voice_spec.rb
104
+ - spec/spec_helper.rb