Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Daemonize sending #7

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
79455bc
Update some gems
Jan 8, 2015
aaee5f3
Set response status code
Jan 8, 2015
97fee9f
Smooth some crinkles and make sure staging mail can only be sampled b…
Jan 8, 2015
68a6366
Update readme with new behavior
Jan 9, 2015
4d0e4f9
Update rspec gem
Jan 9, 2015
c0d3fac
Update spec_helper to match new rspec version
Jan 9, 2015
04e5e7c
Just save the message, dont send it to the provider
Jan 9, 2015
ba3607e
Add new state queued
Jan 9, 2015
e74ded0
Update specs for new behavior
Jan 9, 2015
47118e1
Add pebbles-river gem
Jan 9, 2015
5ee5f9a
Add barebones message queue listener daemon
Jan 9, 2015
52db691
Shape up the readme some more
Jan 20, 2015
94b5173
Nail the pebbles-river version
Jan 20, 2015
aff6e52
Update lock file
Jan 20, 2015
526794c
Move around some config code
Jan 20, 2015
98a1393
Bring the rest of the code up to speed with the new config
Jan 20, 2015
de67ead
Add new class for brokering pebblebed connectors
Jan 20, 2015
77cb01c
Message queue listener up and running, with test
Jan 21, 2015
d806603
Remove some test output
Jan 21, 2015
3c632b1
Only subscribe to create events
Jan 21, 2015
a682c9b
Use the common pebbles connector
Jan 21, 2015
e84000f
Improve readability
Jan 21, 2015
435cfda
Get rid of some rspec deprecation warnings
Jan 21, 2015
c204f8c
Include restricted true on message params
Jan 21, 2015
4a49b3d
Tweak code flow around posting to grove and add a class description
Jan 21, 2015
33b4fd7
Temporarily overwrite recipients
Jan 21, 2015
ed40f08
Update activesupport gem
Jan 22, 2015
a7335d2
Remove temp recipient overwrite
Jan 22, 2015
4bbca7f
Remove some logging
Jan 22, 2015
127e54e
Test data with prettier hash
Jan 22, 2015
80c01e9
Symbolize keys of incoming hash for more consistent handling
Jan 22, 2015
c723eee
Remove orphaned piece of text
Jan 23, 2015
266be18
Consistent hash syntax
Jan 23, 2015
1fe03bb
Rename a method to justify its existence
Jan 23, 2015
333babe
Update test
Jan 23, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ gem 'rake'
gem 'sinatra', '~> 1.3.2'
gem 'rack', '~> 1.4'
gem 'rack-contrib', '~> 1.1.0'
gem "activesupport", '~> 3.2.8'
gem "activesupport", '~> 4.2.0'
gem 'yajl-ruby', '~> 1.1.0', :require => "yajl"
gem 'pebblebed', '~> 0.2.1'
gem 'pebblebed', '~> 0.3.1'
gem 'pebbles-cors', git: 'https://github.com/bengler/pebbles-cors.git'
gem 'pebbles-river', '~> 0.2.1', git: 'https://github.com/bengler/pebbles-river.git'
gem 'nokogiri', '~> 1.5.2'
gem 'excon', '~> 0.12.0'
gem 'crack', '~> 0.3.2'
gem 'httpclient'
gem 'pebbles-uid'

group :test do
gem 'rspec', '~> 2.8'
gem 'rspec', '~> 2.99.0'
gem 'rack-test'
gem 'simplecov', :require => false
gem 'webmock'
Expand Down
62 changes: 42 additions & 20 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,58 @@ GIT
pebblebed
rack

GIT
remote: https://github.com/bengler/pebbles-river.git
revision: f2d4764ef8a9617513906bcf74880e85df7bccbd
specs:
pebbles-river (0.2.1)
activesupport (>= 3.0)
bunny (= 1.6.0.rc2)
mercenary (~> 0.3.3)
pebblebed (~> 0.3.0)
servolux (~> 0.10)

GEM
remote: https://rubygems.org/
specs:
activesupport (3.2.18)
i18n (~> 0.6, >= 0.6.4)
multi_json (~> 1.0)
activesupport (4.2.0)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.3.2)
airbrake (3.1.6)
builder
girl_friday
amq-protocol (1.9.2)
builder (3.1.4)
bunny (1.3.1)
bunny (1.6.0.rc2)
amq-protocol (>= 1.9.2)
connection_pool (0.9.2)
crack (0.3.2)
curb (0.8.5)
curb (0.8.6)
daemons (1.1.9)
dalli (2.7.2)
deepstruct (0.0.7)
diff-lcs (1.1.3)
diff-lcs (1.2.5)
eventmachine (1.0.0)
excon (0.12.0)
futurevalue (0.0.2)
girl_friday (0.10.0)
connection_pool (~> 0.9.0)
httpclient (2.5.0)
i18n (0.6.9)
i18n (0.7.0)
json (1.8.2)
kgio (2.9.2)
mercenary (0.3.5)
minitest (5.5.1)
multi_json (1.10.1)
nokogiri (1.5.11)
pathbuilder (0.0.2)
pebblebed (0.2.1)
pebblebed (0.3.1)
activesupport
bunny (~> 1.3.1)
bunny (= 1.6.0.rc2)
curb (>= 0.8.4)
deepstruct (>= 0.0.4)
futurevalue
Expand All @@ -60,14 +77,15 @@ GEM
rack (>= 1.0)
raindrops (0.13.0)
rake (0.9.2.2)
rspec (2.11.0)
rspec-core (~> 2.11.0)
rspec-expectations (~> 2.11.0)
rspec-mocks (~> 2.11.0)
rspec-core (2.11.1)
rspec-expectations (2.11.3)
diff-lcs (~> 1.1.3)
rspec-mocks (2.11.3)
rspec (2.99.0)
rspec-core (~> 2.99.0)
rspec-expectations (~> 2.99.0)
rspec-mocks (~> 2.99.0)
rspec-core (2.99.2)
rspec-expectations (2.99.2)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.99.2)
servolux (0.10.0)
simplecov (0.7.1)
multi_json (~> 1.0)
simplecov-html (~> 0.7.1)
Expand All @@ -80,7 +98,10 @@ GEM
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
thread_safe (0.3.4)
tilt (1.3.3)
tzinfo (1.2.2)
thread_safe (~> 0.1)
unicorn (4.8.3)
kgio (~> 2.6)
rack
Expand All @@ -94,20 +115,21 @@ PLATFORMS
ruby

DEPENDENCIES
activesupport (~> 3.2.8)
activesupport (~> 4.2.0)
airbrake (~> 3.1.4)
crack (~> 0.3.2)
excon (~> 0.12.0)
httpclient
nokogiri (~> 1.5.2)
pebblebed (~> 0.2.1)
pebblebed (~> 0.3.1)
pebbles-cors!
pebbles-river (~> 0.2.1)!
pebbles-uid
rack (~> 1.4)
rack-contrib (~> 1.1.0)
rack-test
rake
rspec (~> 2.8)
rspec (~> 2.99.0)
simplecov
sinatra (~> 1.3.2)
thin
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ See [this reference](./PROVIDERS.md) for details about available providers for t

With the example above, it's possible to send SMS and email for the `test` realm. We may then post messages with the API, via `POST` requests:


### Email

To send email, `POST` to `/api/hermes/v1/test/messages/email`. With the [pebblebed](//github.com/bengler/pebblebed) gem:
Expand All @@ -62,6 +63,13 @@ connector.hermes.post("/endeavor/messages/email", {

To send SMS messages, `POST` to `/api/hermes/v1/test/messages/sms`.

### Backend behavior
Upon receiving a post, Hermes writes a `post.hermes_message` to Grove tagged `queued`, and returns 201.
Asynchronously, a daemon picks up created `post.hermes_message`s, tags it with `inprogress` and routes the message to its correct provider. Upon receiving a callback from the provider, the `post.hermes_message` is tagged with either `delivered` or `failed` depending on provider result. Other tags such as `bounced` or `dropped` might also appear, check out any specific provider implementation for details.

If the client included a sensible value in the `batch_label` parameter when posting a message, this parameter will be written to `post.hermes_message.document.batch_label` and can thus be used for keeping track of the send status on a collection of messages. The `batch_label` field is not unique or prefixed in any way, so the client is advised to come up with suitably narrow label.


## Testing and staging

You probably don't want to send actual SMS or email messages in environments that is not production.
Expand Down
24 changes: 4 additions & 20 deletions api/v1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,18 @@ class V1 < Sinatra::Base
end

before do
@configuration = Configuration.instance

LOGGER.info "Processing #{request.url}"
LOGGER.info "Params: #{params.inspect}"

cache_control :private, :no_cache, :no_store, :must_revalidate
headers "Content-Type" => "application/json; charset=utf8"
end

private

def realm_and_provider(realm_name, provider_kind)
realm = @configuration.realm(realm_name)
provider = realm.provider(provider_kind)
return realm, provider
end

def pebblebed_connector(realm, checkpoint_identity)
unless realm.name == checkpoint_identity.realm
halt 500, "Wrong realm #{realm.name.inspect}, " \
"expected #{checkpoint_identity.realm.inspect}"
end
Pebblebed::Connector.new(realm.session_key, host: request.host)
end

def logger
LOGGER
end
def logger
LOGGER
end

end
end
end
15 changes: 9 additions & 6 deletions api/v1/incomings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class V1 < Sinatra::Base
# @category Hermes/Receiving
# @path /api/hermes/v1/:realm/incoming/:kind
# @http GET
get '/:realm/incoming/:kind' do |realm, kind|
do_incoming(realm, kind)
get '/:realm/incoming/:kind' do |realm_name, kind|
do_incoming(realm_name, kind)
end

# @apidoc
Expand All @@ -19,14 +19,17 @@ class V1 < Sinatra::Base
# @category Hermes/Receiving
# @path /api/hermes/v1/:realm/incoming/:kind
# @http POST
post '/:realm/incoming/:kind' do |realm, kind|
do_incoming(realm, kind)
post '/:realm/incoming/:kind' do |realm_name, kind|
do_incoming(realm_name, kind)
end


private

def do_incoming(realm_name, kind)
realm, provider = realm_and_provider(realm_name, kind)
realm = CONFIG.realm(realm_name)
provider = realm.provider(kind)

unless provider.respond_to?(:parse_message)
halt 400, "Provider does not support handling incoming messages"
end
Expand Down Expand Up @@ -69,4 +72,4 @@ def do_incoming(realm_name, kind)
end

end
end
end
68 changes: 25 additions & 43 deletions api/v1/messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ class V1 < Sinatra::Base
# @example /api/hermes/v1/apdm/messages/latest
# @required [String] realm The realm sending messages for.
# @status 200 The twenty latest messages
get '/:realm/messages/latest' do |realm|
if ENV['RACK_ENV'] == "production"
halt 403, "Not allowed in production environment"
end
Message.find(realm, "post.hermes_message:*").to_json
get '/:realm/messages/latest' do |realm_name|
require_god
halt 403, "Not allowed in production environment" if ENV['RACK_ENV'] == "production"
[200, Message.find(realm_name, "post.hermes_message:*").to_json]
end

# @apidoc
Expand All @@ -35,11 +34,11 @@ class V1 < Sinatra::Base
# @required [String] realm The realm sending messages for.
# @required [String] uid The message UID
# @status 200 The message as stored in Grove with status of the message stored in the 'tags' field.
get '/:realm/messages/:uid' do |realm, uid|
get '/:realm/messages/:uid' do |realm_name, uid|
require_god
message = Message.get(realm, uid)
return halt 404, "No such message" unless message
halt 200, message.to_json
message = Message.get(realm_name, uid)
halt 404, "No such message" unless message
[200, message.to_json]
end

# @apidoc
Expand All @@ -64,16 +63,18 @@ class V1 < Sinatra::Base
# @optional [String] force A recipient mobile number or email address to send the message to, for testing purposes. Overrides what's given in the recipient parameters.
# @optional [String] callback_url A URL which will be called when the message is delivered.
# @optional [String] path Grove path to post internal message to.
# @optional [String] batch_label Arbitrary handle decided upon by the client. Use it to later look up the send status of a collection messages.
# @status 200 The message as stored in Grove with status of the message stored in the 'tags' field.
post '/:realm/messages/:kind' do |realm, kind|
post '/:realm/messages/:kind' do |realm_name, kind|
require_god

@realm, @provider = realm_and_provider(realm, kind)

realm = CONFIG.realm(realm_name)
host = request.host
message = {
text: params[:text],
callback_url: params[:callback_url]
}

case kind.to_sym
when :sms
message[:recipient_number] = params[:recipient_number]
Expand All @@ -91,43 +92,24 @@ class V1 < Sinatra::Base
message[:html] = params[:html]
end
message.select! { |k, v| !v.blank? }

raw_message = message.dup
raw_message.delete(:callback_url)
raw_message[:receipt_url] = "http://#{request.host}:#{request.port}/api/hermes/v1/#{realm}/receipt/#{kind}"
if (forced_value = params[:force])
case kind
when 'email'
raw_message[:recipient_email] = forced_value
when 'sms'
raw_message[:recipient_number] = forced_value
end
end
message[:receipt_url] = "http://#{host}:#{request.port}/api/hermes/v1/#{realm.name}/receipt/#{kind}"

if params[:force]
raw_message[:recipient_email] = params[:force] if kind == "email"
raw_message[:recipient_number] = params[:force] if kind == "sms"
message[:recipient_email] = params[:force] if kind == 'email'
message[:recipient_number] = params[:force] if kind == 'sms'
end

grove_path = "/posts/post.hermes_message:" + [@realm.name, params[:path]].compact.join('.')
grove_post = {
document: message.merge(kind: kind),
restricted: true
document = message.merge(kind: kind)
document[:batch_label] = params[:batch_label] if params[:batch_label]
post = {
document: document,
restricted: true,
tags: ['queued']
}

begin
id = @provider.send_message!(raw_message)
rescue ProviderError
logger.info("Provider failed to send message (#{kind} via #{@provider.class.name}): #{message.inspect}")
grove_post[:tags] = ['failed']
pebblebed_connector(@realm, current_identity).grove.post(grove_path, post: grove_post)
raise
else
logger.info("Sent message (#{kind} via #{@provider.class.name}): #{message.inspect}")
grove_post[:tags] = ['inprogress']
grove_post[:external_id] = Message.build_external_id(@provider, id)
pebblebed_connector(@realm, current_identity).grove.post(grove_path, post: grove_post).to_json
end
path = "/posts/post.hermes_message:" + [realm.name, params[:path]].compact.join('.')
result = PebblesProxy.connector_for(realm, current_identity, host).grove.post(path, post: post)
[200, result.to_json]
end

end
Expand Down
14 changes: 7 additions & 7 deletions api/v1/receipts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,17 @@ class V1 < Sinatra::Base
# @required [String] realm The realm sending messages for.
# @required [String] kind The kind of message, 'email', 'sms'
# @status 200
get '/:realm/receipt/:kind' do |realm, kind|
do_receipt(realm, kind, request, params)
get '/:realm/receipt/:kind' do |realm_name, kind|
do_receipt(realm_name, kind, request, params)
end

private

def do_receipt(realm, kind, request, params)
realm, provider = realm_and_provider(realm, kind)

def do_receipt(realm_name, kind, request, params)
realm = CONFIG.realm(realm_name)
provider = realm.provider(kind)
result = provider.parse_receipt(request)
logger.info("Receipt status: #{result.inspect}")

if result[:id] and result[:status]
if (message = Message.find_by_external_id(
Message.build_external_id(provider, result[:id]), realm.name))
Expand All @@ -62,4 +62,4 @@ def do_receipt(realm, kind, request, params)
end

end
end
end
Loading