instedd-pigeon 0.1.3 → 0.2.0
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/.rspec +2 -0
- data/app/helpers/pigeon/channel_helper.rb +6 -0
- data/app/models/pigeon/channel.rb +4 -0
- data/app/models/pigeon/channel_attribute.rb +1 -1
- data/app/models/pigeon/nuntium_channel.rb +10 -4
- data/app/models/pigeon/verboice_channel.rb +8 -5
- data/config/locales/en.yml +6 -0
- data/config/schemas/nuntium/nuntium.yml +17 -27
- data/config/schemas/nuntium/smpp.yml +71 -0
- data/lib/pigeon.rb +12 -0
- data/lib/pigeon/errors.rb +18 -0
- data/lib/pigeon/nuntium.rb +290 -5
- data/lib/pigeon/utils.rb +70 -0
- data/lib/pigeon/verboice.rb +149 -5
- data/lib/pigeon/version.rb +1 -1
- data/pigeon.gemspec +3 -3
- data/spec/lib/pigeon/nuntium_spec.rb +208 -0
- data/spec/lib/pigeon/verboice_spec.rb +181 -0
- metadata +25 -17
data/lib/pigeon/utils.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Pigeon
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
def to_query(hash)
|
5
|
+
hash.map do |key, value|
|
6
|
+
"#{key.to_s}=#{CGI.escape(value.to_s)}"
|
7
|
+
end.join('&')
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle_channel_error(error)
|
11
|
+
if error.is_a? RestClient::BadRequest
|
12
|
+
response = JSON.parse error.response.body
|
13
|
+
raise self.class.error_class.new response['summary'], Hash[response['properties'].map {|e| [e.keys[0], e.values[0]]}]
|
14
|
+
else
|
15
|
+
raise self.class.error_class.new error.message
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_indifferent_access(hash)
|
20
|
+
if hash.respond_to? :with_indifferent_access
|
21
|
+
hash.with_indifferent_access
|
22
|
+
else
|
23
|
+
hash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def get(path)
|
28
|
+
resource = RestClient::Resource.new @url, @options
|
29
|
+
resource = resource[path].get
|
30
|
+
yield resource, nil
|
31
|
+
rescue => ex
|
32
|
+
yield nil, ex
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_json(path)
|
36
|
+
get(path) do |response, error|
|
37
|
+
raise self.class.error_class.new error.message if error
|
38
|
+
|
39
|
+
elem = JSON.parse response.body
|
40
|
+
elem.map! { |x| with_indifferent_access x } if elem.is_a? Array
|
41
|
+
elem
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def post(path, data)
|
46
|
+
resource = RestClient::Resource.new @url, @options
|
47
|
+
resource = resource[path].post(data)
|
48
|
+
yield resource, nil
|
49
|
+
rescue => ex
|
50
|
+
yield nil, ex
|
51
|
+
end
|
52
|
+
|
53
|
+
def put(path, data)
|
54
|
+
resource = RestClient::Resource.new @url, @options
|
55
|
+
resource = resource[path].put(data)
|
56
|
+
yield resource, nil
|
57
|
+
rescue => ex
|
58
|
+
yield nil, ex
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete(path)
|
62
|
+
resource = RestClient::Resource.new @url, @options
|
63
|
+
resource = resource[path].delete
|
64
|
+
yield resource, nil
|
65
|
+
rescue => ex
|
66
|
+
yield nil, ex
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
data/lib/pigeon/verboice.rb
CHANGED
@@ -1,10 +1,154 @@
|
|
1
|
-
|
1
|
+
# Provides access to the Verboice Public API.
|
2
|
+
# Taken from verboice-api-ruby gem version 0.7.0
|
3
|
+
# See http://bitbucket.org/instedd/verboice-api-ruby
|
4
|
+
#
|
2
5
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
+
require 'net/http'
|
7
|
+
require 'json'
|
8
|
+
require 'rest_client'
|
9
|
+
require 'cgi'
|
10
|
+
require 'time'
|
11
|
+
require 'pigeon/errors'
|
12
|
+
require 'pigeon/utils'
|
13
|
+
|
14
|
+
module Pigeon
|
15
|
+
class Verboice
|
16
|
+
include Pigeon::Utils
|
17
|
+
|
18
|
+
def self.error_class
|
19
|
+
Pigeon::VerboiceException
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_config
|
23
|
+
config = Pigeon.config
|
24
|
+
|
25
|
+
self.new config.verboice_host, config.verboice_account, config.verboice_password
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates an account-authenticated Verboice api access.
|
29
|
+
def initialize(url, account, password, default_channel = nil)
|
30
|
+
@url = url
|
31
|
+
@account = account
|
32
|
+
@password = password
|
33
|
+
@default_channel = default_channel
|
34
|
+
@options = {
|
35
|
+
:user => account,
|
36
|
+
:password => password,
|
37
|
+
:headers => {:content_type => 'application/json'},
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def call address, options = {}
|
42
|
+
options = options.dup
|
43
|
+
|
44
|
+
args = {}
|
45
|
+
if channel = options.delete(:channel)
|
46
|
+
args[:channel] = channel
|
47
|
+
else
|
48
|
+
args[:channel] = @default_channel
|
49
|
+
end
|
50
|
+
|
51
|
+
args[:address] = address
|
52
|
+
|
53
|
+
if not_before = options.delete(:not_before)
|
54
|
+
args[:not_before] = not_before.iso8601
|
55
|
+
end
|
56
|
+
|
57
|
+
flow = options.delete(:flow)
|
58
|
+
callback_url = options.delete(:callback_url)
|
59
|
+
|
60
|
+
args.merge!(options)
|
61
|
+
|
62
|
+
if flow
|
63
|
+
post "/api/call?#{to_query args}", flow do |response, error|
|
64
|
+
raise ::Pigeon::VerboiceException.new error.message if error
|
65
|
+
JSON.parse response.body
|
66
|
+
end
|
67
|
+
else
|
68
|
+
args[:callback_url] = callback_url if callback_url
|
69
|
+
get_json "/api/call?#{to_query args}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def call_state id
|
74
|
+
get_json "/api/calls/#{id}/state"
|
75
|
+
end
|
76
|
+
|
77
|
+
def call_redirect id
|
78
|
+
get_json "/api/calls/#{id}/redirect"
|
79
|
+
end
|
80
|
+
|
81
|
+
def channel(name)
|
82
|
+
get("/api/channels/#{name}.json") do |response, error|
|
83
|
+
handle_channel_error error if error
|
84
|
+
|
85
|
+
channel = JSON.parse response.body
|
86
|
+
with_indifferent_access channel
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_channel(channel)
|
91
|
+
post "/api/channels.json", channel.to_json do |response, error|
|
92
|
+
handle_channel_error error if error
|
93
|
+
|
94
|
+
channel = JSON.parse response.body
|
95
|
+
with_indifferent_access channel
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def update_channel(channel, name = channel['name'])
|
100
|
+
put "/api/channels/#{name}.json", channel.to_json do |response, error|
|
101
|
+
handle_channel_error error if error
|
102
|
+
|
103
|
+
channel = JSON.parse response.body
|
104
|
+
with_indifferent_access channel
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Deletes a channel given its name.
|
109
|
+
#
|
110
|
+
# Raises Verboice::Exception if something goes wrong.
|
111
|
+
def delete_channel(name)
|
112
|
+
delete "/api/channels/#{name}" do |response, error|
|
113
|
+
raise ::Pigeon::VerboiceException.new error.message if error
|
114
|
+
|
115
|
+
response
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def list_channels()
|
120
|
+
get_json "/api/channels.json"
|
121
|
+
end
|
122
|
+
|
123
|
+
def schedules(project_id)
|
124
|
+
get_json "/api/projects/#{project_id}/schedules.json"
|
125
|
+
end
|
126
|
+
|
127
|
+
def schedule(project_id, name)
|
128
|
+
get_json "/api/projects/#{project_id}/schedules/#{name}.json"
|
129
|
+
end
|
130
|
+
|
131
|
+
def create_schedule(project_id, schedule)
|
132
|
+
post "/api/projects/#{project_id}/schedules", schedule.to_json do |response, error|
|
133
|
+
raise ::Pigeon::VerboiceException.new error.message if error
|
134
|
+
response
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def update_schedule(project_id, name, schedule)
|
139
|
+
put "/api/projects/#{project_id}/schedules/#{name}", schedule.to_json do |response, error|
|
140
|
+
raise ::Pigeon::VerboiceException.new error.message if error
|
141
|
+
response
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def delete_schedule(project_id, name)
|
146
|
+
delete "/api/projects/#{project_id}/schedules/#{name}" do |response, error|
|
147
|
+
raise ::Pigeon::VerboiceException.new error.message if error
|
148
|
+
response
|
149
|
+
end
|
150
|
+
end
|
6
151
|
|
7
|
-
Verboice.new config.verboice_host, config.verboice_account, config.verboice_password
|
8
152
|
end
|
9
153
|
end
|
10
154
|
|
data/lib/pigeon/version.rb
CHANGED
data/pigeon.gemspec
CHANGED
@@ -12,10 +12,10 @@ Gem::Specification.new do |gem|
|
|
12
12
|
gem.summary = %q{This gem handles creating, updating and destroying channels in Nuntium and Verboice for your Rails application.}
|
13
13
|
gem.homepage = "https://bitbucket.org/instedd/pigeon"
|
14
14
|
|
15
|
-
gem.add_dependency 'rails', '~> 3.2
|
16
|
-
gem.add_dependency 'nuntium_api', '~> 0.19'
|
17
|
-
gem.add_dependency 'verboice', '0.7.0'
|
15
|
+
gem.add_dependency 'rails', '~> 3.2'
|
18
16
|
gem.add_dependency 'twitter_oauth'
|
17
|
+
gem.add_dependency 'rest-client'
|
18
|
+
gem.add_dependency 'json'
|
19
19
|
|
20
20
|
gem.files = `git ls-files`.split($/)
|
21
21
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Pigeon
|
4
|
+
describe Nuntium do
|
5
|
+
let(:url) { "http://example.com" }
|
6
|
+
let(:options) { {:user => "account/application", :password => "password", :headers => {:content_type => 'application/json'}} }
|
7
|
+
let(:api) { Nuntium.new url, "account", "application", "password" }
|
8
|
+
|
9
|
+
it "gets countries" do
|
10
|
+
should_receive_http_get '/api/countries.json', %([{"name": "Argentina", "iso2": "ar"}])
|
11
|
+
|
12
|
+
api.countries.should eq(['name' => 'Argentina', 'iso2' => 'ar'])
|
13
|
+
end
|
14
|
+
|
15
|
+
it "gets country" do
|
16
|
+
should_receive_http_get '/api/countries/ar.json', %({"name": "Argentina", "iso2": "ar"})
|
17
|
+
|
18
|
+
api.country('ar').should eq({'name' => 'Argentina', 'iso2' => 'ar'})
|
19
|
+
end
|
20
|
+
|
21
|
+
it "gets carriers" do
|
22
|
+
should_receive_http_get '/api/carriers.json', %([{"name": "Argentina", "iso2": "ar"}])
|
23
|
+
|
24
|
+
api.carriers.should eq(['name' => 'Argentina', 'iso2' => 'ar'])
|
25
|
+
end
|
26
|
+
|
27
|
+
it "gets carriers for a country" do
|
28
|
+
should_receive_http_get '/api/carriers.json?country_id=ar', %([{"name": "Argentina", "iso2": "ar"}])
|
29
|
+
|
30
|
+
api.carriers('ar').should eq(['name' => 'Argentina', 'iso2' => 'ar'])
|
31
|
+
end
|
32
|
+
|
33
|
+
it "gets carrier" do
|
34
|
+
should_receive_http_get '/api/carriers/ar.json', %({"name": "Argentina", "iso2": "ar"})
|
35
|
+
|
36
|
+
api.carrier('ar').should eq({'name' => 'Argentina', 'iso2' => 'ar'})
|
37
|
+
end
|
38
|
+
|
39
|
+
it "gets channels" do
|
40
|
+
should_receive_http_get '/api/channels.json', %([{"name": "Argentina", "configuration": [{"name": "foo", "value": "bar"}]}])
|
41
|
+
|
42
|
+
api.channels.should eq([{'name' => 'Argentina', 'configuration' => {'foo' => 'bar'}}])
|
43
|
+
end
|
44
|
+
|
45
|
+
it "gets channel" do
|
46
|
+
should_receive_http_get '/api/channels/Argentina.json', %({"name": "Argentina", "configuration": [{"name": "foo", "value": "bar"}]})
|
47
|
+
|
48
|
+
api.channel('Argentina').should eq({'name' => 'Argentina', 'configuration' => {'foo' => 'bar'}})
|
49
|
+
end
|
50
|
+
|
51
|
+
it "creates channel" do
|
52
|
+
channel_json = %({"name":"Argentina","configuration":[{"name":"foo","value":"bar"}]})
|
53
|
+
should_receive_http_post '/api/channels.json', channel_json, channel_json
|
54
|
+
|
55
|
+
channel = {'name' => 'Argentina', 'configuration' => {'foo' => 'bar'}}
|
56
|
+
|
57
|
+
api.create_channel(channel).should eq(channel)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "updates channel", :focus => true do
|
61
|
+
channel_json = %({"name":"Argentina","configuration":[{"name":"foo","value":"bar"}]})
|
62
|
+
should_receive_http_put '/api/channels/Argentina.json', channel_json, channel_json
|
63
|
+
|
64
|
+
channel = {'name' => 'Argentina', 'configuration' => {'foo' => 'bar'}}
|
65
|
+
|
66
|
+
api.update_channel(channel).should eq(channel)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "deletes channel", :focus => true do
|
70
|
+
should_receive_http_delete '/api/channels/Argentina'
|
71
|
+
|
72
|
+
api.delete_channel('Argentina')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "gets candidate channels for ao", :focus => true do
|
76
|
+
should_receive_http_get "/api/candidate/channels.json?from=#{CGI.escape 'sms://1234'}&body=Hello", %([{"name":"Argentina","configuration":[{"value":"bar","name":"foo"}]}])
|
77
|
+
|
78
|
+
channels = api.candidate_channels_for_ao :from => 'sms://1234', :body => 'Hello'
|
79
|
+
channels.should eq([{'name' => 'Argentina', 'configuration' => {'foo' => 'bar'}}])
|
80
|
+
end
|
81
|
+
|
82
|
+
it "sends single ao" do
|
83
|
+
should_receive_http_get_with_headers "/account/application/send_ao?from=#{CGI.escape 'sms://1234'}&body=Hello", :x_nuntium_id => '1', :x_nuntium_guid => '2', :x_nuntium_token => '3'
|
84
|
+
response = api.send_ao :from => 'sms://1234', :body => 'Hello'
|
85
|
+
response.should eq("id" => '1', "guid" => '2', "token" => '3')
|
86
|
+
end
|
87
|
+
|
88
|
+
it "sends many aos" do
|
89
|
+
post_body = %([{"from":"sms://1234","body":"Hello"}])
|
90
|
+
should_receive_http_post_with_headers "/account/application/send_ao.json", post_body, :x_nuntium_token => '3'
|
91
|
+
response = api.send_ao [{:from => 'sms://1234', :body => 'Hello'}]
|
92
|
+
response.should eq("token" => '3')
|
93
|
+
end
|
94
|
+
|
95
|
+
it "gets ao" do
|
96
|
+
should_receive_http_get '/account/application/get_ao.json?token=foo', %([{"name": "Argentina", "iso2": "ar"}])
|
97
|
+
|
98
|
+
api.get_ao('foo').should eq([{'name' => 'Argentina', 'iso2' => 'ar'}])
|
99
|
+
end
|
100
|
+
|
101
|
+
it "gets custom attributes" do
|
102
|
+
should_receive_http_get '/api/custom_attributes?address=foo', %([{"name": "Argentina", "iso2": "ar"}])
|
103
|
+
|
104
|
+
api.get_custom_attributes('foo').should eq([{'name' => 'Argentina', 'iso2' => 'ar'}])
|
105
|
+
end
|
106
|
+
|
107
|
+
it "sets custom attributes" do
|
108
|
+
should_receive_http_post '/api/custom_attributes?address=foo', %({"application":"bar"}), ''
|
109
|
+
|
110
|
+
api.set_custom_attributes('foo', :application => :bar)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "creates twitter friendship" do
|
114
|
+
should_receive_http_get '/api/channels/twit/twitter/friendships/create?user=foo&follow=true'
|
115
|
+
|
116
|
+
api.twitter_friendship_create 'twit', 'foo', true
|
117
|
+
end
|
118
|
+
|
119
|
+
it "authorizes twitter channel" do
|
120
|
+
should_receive_http_get '/api/channels/twit/twitter/authorize?callback=foo', "http://bar"
|
121
|
+
|
122
|
+
url = api.twitter_authorize 'twit', 'foo'
|
123
|
+
url.should eq("http://bar")
|
124
|
+
end
|
125
|
+
|
126
|
+
it "adds xmpp contact" do
|
127
|
+
should_receive_http_get "/api/channels/chan/xmpp/add_contact?jid=#{CGI.escape 'foo@bar.com'}"
|
128
|
+
|
129
|
+
api.xmpp_add_contact 'chan', 'foo@bar.com'
|
130
|
+
end
|
131
|
+
|
132
|
+
def should_receive_http_get(path, body = nil)
|
133
|
+
resource = mock 'resource'
|
134
|
+
RestClient::Resource.should_receive(:new).with(url, options).and_return(resource)
|
135
|
+
|
136
|
+
resource2 = mock 'resource2'
|
137
|
+
resource.should_receive(:[]).with(path).and_return(resource2)
|
138
|
+
|
139
|
+
resource3 = mock 'resource3'
|
140
|
+
resource2.should_receive(:get).and_return(resource3)
|
141
|
+
|
142
|
+
resource3.should_receive(:body).and_return(body) if body
|
143
|
+
end
|
144
|
+
|
145
|
+
def should_receive_http_get_with_headers(path, headers)
|
146
|
+
resource = mock 'resource'
|
147
|
+
RestClient::Resource.should_receive(:new).with(url, options).and_return(resource)
|
148
|
+
|
149
|
+
resource2 = mock 'resource2'
|
150
|
+
resource.should_receive(:[]).with(path).and_return(resource2)
|
151
|
+
|
152
|
+
resource3 = mock 'resource3'
|
153
|
+
resource2.should_receive(:get).and_return(resource3)
|
154
|
+
|
155
|
+
resource3.stub(:headers) { headers }
|
156
|
+
end
|
157
|
+
|
158
|
+
def should_receive_http_post(path, data, body)
|
159
|
+
resource = mock 'resource'
|
160
|
+
RestClient::Resource.should_receive(:new).with(url, options).and_return(resource)
|
161
|
+
|
162
|
+
resource2 = mock 'resource2'
|
163
|
+
resource.should_receive(:[]).with(path).and_return(resource2)
|
164
|
+
|
165
|
+
resource3 = mock 'resource3'
|
166
|
+
resource2.should_receive(:post).with(data).and_return(resource3)
|
167
|
+
|
168
|
+
resource3.stub(:body) { body }
|
169
|
+
end
|
170
|
+
|
171
|
+
def should_receive_http_post_with_headers(path, data, headers)
|
172
|
+
resource = mock 'resource'
|
173
|
+
RestClient::Resource.should_receive(:new).with(url, options).and_return(resource)
|
174
|
+
|
175
|
+
resource2 = mock 'resource2'
|
176
|
+
resource.should_receive(:[]).with(path).and_return(resource2)
|
177
|
+
|
178
|
+
resource3 = mock 'resource3'
|
179
|
+
resource2.should_receive(:post).with(data).and_return(resource3)
|
180
|
+
|
181
|
+
resource3.stub(:headers) { headers }
|
182
|
+
end
|
183
|
+
|
184
|
+
def should_receive_http_put(path, data, body)
|
185
|
+
resource = mock 'resource'
|
186
|
+
RestClient::Resource.should_receive(:new).with(url, options).and_return(resource)
|
187
|
+
|
188
|
+
resource2 = mock 'resource2'
|
189
|
+
resource.should_receive(:[]).with(path).and_return(resource2)
|
190
|
+
|
191
|
+
resource3 = mock 'resource3'
|
192
|
+
resource2.should_receive(:put).with(data).and_return(resource3)
|
193
|
+
|
194
|
+
resource3.should_receive(:body).and_return(body)
|
195
|
+
end
|
196
|
+
|
197
|
+
def should_receive_http_delete(path)
|
198
|
+
resource = mock 'resource'
|
199
|
+
RestClient::Resource.should_receive(:new).with(url, options).and_return(resource)
|
200
|
+
|
201
|
+
resource2 = mock 'resource2'
|
202
|
+
resource.should_receive(:[]).with(path).and_return(resource2)
|
203
|
+
|
204
|
+
resource3 = mock 'resource3'
|
205
|
+
resource2.should_receive(:delete)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|