rename WebhooksController to WebRequestsController; refactor and support deprecated methods for now

This commit is contained in:
Andrew Cantino 2014-04-08 09:38:06 -07:00
parent ea8963a8cf
commit db0fbe4c93
17 changed files with 193 additions and 122 deletions

View file

@ -1,5 +1,6 @@
# Changes
* 0.4 (April 10, 2014) - WebHooksController has been renamed to WebRequestsController and all HTTP verbs are now accepted and passed through to Agents' #receive_web_request method. The new DataOutputAgent returns JSON or RSS feeds of incoming Events via external web request.
* 0.31 (Jan 2, 2014) - Agents now have an optional keep\_events\_for option that is propagated to created events' expires\_at field, and they update their events' expires\_at fields on change.
* 0.3 (Jan 1, 2014) - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML. Migration will perform conversion and adjust tables to be UTF-8. Recommend making a DB backup before migrating.
* 0.2 (Nov 6, 2013) - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`. Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall.

View file

@ -0,0 +1,41 @@
# This controller is designed to allow your Agents to receive cross-site Webhooks (POSTs), or to output data streams.
# When a POST or GET is received, your Agent will have #receive_web_request called on itself with the incoming params,
# method, and requested content-type.
#
# Requests are routed as follows:
# http://yourserver.com/users/:user_id/web_requests/:agent_id/:secret
# where :user_id is a User's id, :agent_id is an Agent's id, and :secret is a token that should be user-specifiable in
# an Agent that implements #receive_web_request. It is highly recommended that every Agent verify this token whenever
# #receive_web_request is called. For example, one of your Agent's options could be :secret and you could compare this
# value to params[:secret] whenever #receive_web_request is called on your Agent, rejecting invalid requests.
#
# Your Agent's #receive_web_request method should return an Array of json_or_string_response, status_code, and
# optional mime type. For example:
# [{status: "success"}, 200]
# or
# ["not found", 404, 'text/plain']
class WebRequestsController < ApplicationController
skip_before_filter :authenticate_user!
def handle_request
user = User.find_by_id(params[:user_id])
if user
agent = user.agents.find_by_id(params[:agent_id])
if agent
content, status, content_type = agent.trigger_web_request(params.except(:action, :controller, :agent_id, :user_id, :format), request.method_symbol.to_s, request.format.to_s)
if content.is_a?(String)
render :text => content, :status => status || 200, :content_type => content_type || 'text/plain'
elsif content.is_a?(Hash)
render :json => content, :status => status || 200
else
head(status || 200)
end
else
render :text => "agent not found", :status => 404
end
else
render :text => "user not found", :status => 404
end
end
end

View file

@ -1,39 +0,0 @@
# This controller is designed to allow your Agents to receive cross-site Webhooks (POSTs), or to output data streams.
# When a POST or GET is received, your Agent will have #receive_webhook called on itself with the incoming params.
#
# To implement webhooks, make POSTs to the following URL:
# http://yourserver.com/users/:user_id/webhooks/:agent_id/:secret
# where :user_id is your User's id, :agent_id is an Agent's id, and :secret is a token that should be
# user-specifiable in your Agent. It is highly recommended that you verify this token whenever #receive_webhook
# is called. For example, one of your Agent's options could be :secret and you could compare this value
# to params[:secret] whenever #receive_webhook is called on your Agent, rejecting invalid requests.
#
# Your Agent's #receive_webhook method should return an Array of [json_or_string_response, status_code, optional mime type]. For example:
# [{status: "success"}, 200]
# or
# ["not found", 404, 'text/plain']
class WebhooksController < ApplicationController
skip_before_filter :authenticate_user!
def handle_request
user = User.find_by_id(params[:user_id])
if user
agent = user.agents.find_by_id(params[:agent_id])
if agent
content, status, content_type = agent.trigger_webhook(params.except(:action, :controller, :agent_id, :user_id, :format), request.method_symbol.to_s, request.format.to_s)
if content.is_a?(String)
render :text => content, :status => status || 200, :content_type => content_type || 'text/plain'
elsif content.is_a?(Hash)
render :json => content, :status => status || 200
else
head(status || 200)
end
else
render :text => "agent not found", :status => 404
end
else
render :text => "user not found", :status => 404
end
end
end

View file

@ -73,7 +73,7 @@ class Agent < ActiveRecord::Base
# Implement me in your subclass of Agent.
end
def receive_webhook(params, method, format)
def receive_web_request(params, method, format)
# Implement me in your subclass of Agent.
["not implemented", 404]
end
@ -136,10 +136,18 @@ class Agent < ActiveRecord::Base
message.gsub(/<([^>]+)>/) { Utils.value_at(payload, $1) || "??" }
end
def trigger_webhook(params, method, format)
receive_webhook(params, method, format).tap do
self.last_webhook_at = Time.now
save!
def trigger_web_request(params, method, format)
if respond_to?(:receive_webhook)
Rails.logger.warn "DEPRECATED: The .receive_webhook method is deprecated, please switch your Agent to use .receive_web_request."
receive_webhook(params).tap do
self.last_web_request_at = Time.now
save!
end
else
receive_web_request(params, method, format).tap do
self.last_web_request_at = Time.now
save!
end
end
end

View file

@ -8,7 +8,7 @@ module Agents
This Agent will output data at:
`https://#{ENV['DOMAIN']}/users/#{user.id}/webhooks/#{id || '<id>'}/:secret.xml`
`https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<id>'}/:secret.xml`
where `:secret` is one of the allowed secrets specified in your options and the extension can be `xml` or `json`.
@ -75,7 +75,7 @@ module Agents
options['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent"
end
def receive_webhook(params, method, format)
def receive_web_request(params, method, format)
if options['secrets'].include?(params['secret'])
items = received_events.order('id desc').limit(events_to_show).map do |event|
interpolated = Utils.recursively_interpolate_jsonpaths(options['template']['item'], event.payload, :leading_dollarsign_is_jsonpath => true)

View file

@ -75,10 +75,10 @@ module Agents
end
def post_url(server_url,secret)
"#{server_url}/users/#{self.user.id}/webhooks/#{self.id}/#{secret}"
"#{server_url}/users/#{self.user.id}/web_requests/#{self.id}/#{secret}"
end
def receive_webhook(params, method, format)
def receive_web_request(params, method, format)
if memory['pending_calls'].has_key? params['secret']
response = Twilio::TwiML::Response.new {|r| r.Say memory['pending_calls'][params['secret']], :voice => 'woman'}
memory['pending_calls'].delete params['secret']

View file

@ -1,6 +1,7 @@
module Agents
class WebhookAgent < Agent
cannot_be_scheduled!
cannot_receive_events!
description do
<<-MD
@ -8,7 +9,7 @@ module Agents
In order to create events with this agent, make a POST request to:
```
https://#{ENV['DOMAIN']}/users/#{user.id}/webhooks/#{id || '<id>'}/:secret
https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<id>'}/:secret
``` where `:secret` is specified in your options.
The
@ -36,9 +37,10 @@ module Agents
"payload_path" => "payload"}
end
def receive_webhook(params, method, format)
def receive_web_request(params, method, format)
secret = params.delete('secret')
return ["Not Authorized", 401] unless secret == options['secret'] && method == "post"
return ["Please use POST requests only", 401] unless method == "post"
return ["Not Authorized", 401] unless secret == options['secret']
create_event(:payload => payload_for(params))

View file

@ -5,7 +5,7 @@
<ul>
<% @agent.options['secrets'].each do |secret| %>
<% url = lambda { |format| webhooks_url(:agent_id => @agent.id, :user_id => current_user.id, :secret => secret, :format => format) } %>
<% url = lambda { |format| web_requests_url(:agent_id => @agent.id, :user_id => current_user.id, :secret => secret, :format => format) } %>
<li><%= link_to url.call(:json), url.call(:json), :target => :blank %></li>
<li><%= link_to url.call(:xml), url.call(:xml), :target => :blank %></li>
<% end %>

View file

@ -0,0 +1,3 @@
<p>
Send WebHooks (POST requests) to this Agent at <%= link_to web_requests_url(:agent_id => @agent.id, :user_id => current_user.id, :secret => @agent.options['secret']), web_requests_url(:agent_id => @agent.id, :user_id => current_user.id, :secret => @agent.options['secret']), :target => :blank %>
</p>

View file

@ -31,7 +31,9 @@ Huginn::Application.routes.draw do
match "/worker_status" => "worker_status#show"
post "/users/:user_id/update_location/:secret" => "user_location_updates#create"
match "/users/:user_id/webhooks/:agent_id/:secret" => "webhooks#handle_request", :as => :webhooks
match "/users/:user_id/web_requests/:agent_id/:secret" => "web_requests#handle_request", :as => :web_requests
post "/users/:user_id/webhooks/:agent_id/:secret" => "web_requests#handle_request" # legacy
# match "/delayed_job" => DelayedJobWeb, :anchor => false
devise_for :users, :sign_out_via => [ :post, :delete ]

View file

@ -0,0 +1,9 @@
class RenameWebhookToWebRequest < ActiveRecord::Migration
def up
rename_column :agents, :last_webhook_at, :last_web_request_at
end
def down
rename_column :agents, :last_web_request_at, :last_webhook_at
end
end

View file

@ -11,21 +11,21 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20140216201250) do
ActiveRecord::Schema.define(:version => 20140408150825) do
create_table "agent_logs", :force => true do |t|
t.integer "agent_id", :null => false
t.text "message", :limit => 16777215, :null => false
t.integer "level", :default => 3, :null => false
t.integer "agent_id", :null => false
t.text "message", :null => false
t.integer "level", :default => 3, :null => false
t.integer "inbound_event_id"
t.integer "outbound_event_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "agents", :force => true do |t|
t.integer "user_id"
t.text "options", :limit => 16777215
t.text "options"
t.string "type"
t.string "name"
t.string "schedule"
@ -36,10 +36,10 @@ ActiveRecord::Schema.define(:version => 20140216201250) do
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.text "memory", :limit => 2147483647
t.datetime "last_webhook_at"
t.datetime "last_web_request_at"
t.integer "keep_events_for", :default => 0, :null => false
t.datetime "last_event_at"
t.datetime "last_error_log_at"
t.integer "keep_events_for", :default => 0, :null => false
t.boolean "propagate_immediately", :default => false, :null => false
end
@ -47,19 +47,11 @@ ActiveRecord::Schema.define(:version => 20140216201250) do
add_index "agents", ["type"], :name => "index_agents_on_type"
add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at"
create_table "contacts", :force => true do |t|
t.text "message"
t.string "name"
t.string "email"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
create_table "delayed_jobs", :force => true do |t|
t.integer "priority", :default => 0
t.integer "attempts", :default => 0
t.text "handler", :limit => 16777215
t.text "last_error", :limit => 16777215
t.text "last_error"
t.datetime "run_at"
t.datetime "locked_at"
t.datetime "failed_at"
@ -74,11 +66,11 @@ ActiveRecord::Schema.define(:version => 20140216201250) do
create_table "events", :force => true do |t|
t.integer "user_id"
t.integer "agent_id"
t.decimal "lat", :precision => 15, :scale => 10
t.decimal "lng", :precision => 15, :scale => 10
t.text "payload", :limit => 2147483647
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.decimal "lat", :precision => 15, :scale => 10
t.decimal "lng", :precision => 15, :scale => 10
t.text "payload", :limit => 16777215
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.datetime "expires_at"
end

View file

@ -1,15 +1,15 @@
require 'spec_helper'
describe WebhooksController do
class Agents::WebhookReceiverAgent < Agent
describe WebRequestsController do
class Agents::WebRequestReceiverAgent < Agent
cannot_receive_events!
cannot_be_scheduled!
def receive_webhook(params, method, format)
def receive_web_request(params, method, format)
if params.delete(:secret) == options[:secret]
memory[:webhook_values] = params
memory[:webhook_format] = format
memory[:webhook_method] = method
memory[:web_request_values] = params
memory[:web_request_format] = format
memory[:web_request_method] = method
["success", 200, memory['content_type']]
else
["failure", 404]
@ -18,32 +18,32 @@ describe WebhooksController do
end
before do
stub(Agents::WebhookReceiverAgent).valid_type?("Agents::WebhookReceiverAgent") { true }
@agent = Agents::WebhookReceiverAgent.new(:name => "something", :options => { :secret => "my_secret" })
stub(Agents::WebRequestReceiverAgent).valid_type?("Agents::WebRequestReceiverAgent") { true }
@agent = Agents::WebRequestReceiverAgent.new(:name => "something", :options => { :secret => "my_secret" })
@agent.user = users(:bob)
@agent.save!
end
it "should not require login to trigger a webhook" do
@agent.last_webhook_at.should be_nil
it "should not require login to receive a web request" do
@agent.last_web_request_at.should be_nil
post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
@agent.reload.last_webhook_at.should be_within(2).of(Time.now)
@agent.reload.last_web_request_at.should be_within(2).of(Time.now)
response.body.should == "success"
response.should be_success
end
it "should call receive_webhook" do
it "should call receive_web_request" do
post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
@agent.reload
@agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:webhook_format].should == "text/html"
@agent.memory[:webhook_method].should == "post"
@agent.memory[:web_request_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:web_request_format].should == "text/html"
@agent.memory[:web_request_method].should == "post"
response.body.should == "success"
response.headers['Content-Type'].should == 'text/plain; charset=utf-8'
response.should be_success
post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go"
@agent.reload.memory[:webhook_values].should_not == { 'no' => "go" }
@agent.reload.memory[:web_request_values].should_not == { 'no' => "go" }
response.body.should == "failure"
response.should be_missing
end
@ -51,9 +51,9 @@ describe WebhooksController do
it "should accept gets" do
get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
@agent.reload
@agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:webhook_format].should == "text/html"
@agent.memory[:webhook_method].should == "get"
@agent.memory[:web_request_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:web_request_format].should == "text/html"
@agent.memory[:web_request_method].should == "get"
response.body.should == "success"
response.should be_success
end
@ -61,21 +61,21 @@ describe WebhooksController do
it "should pass through the received format" do
get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :json
@agent.reload
@agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:webhook_format].should == "application/json"
@agent.memory[:webhook_method].should == "get"
@agent.memory[:web_request_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:web_request_format].should == "application/json"
@agent.memory[:web_request_method].should == "get"
post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :xml
@agent.reload
@agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:webhook_format].should == "application/xml"
@agent.memory[:webhook_method].should == "post"
@agent.memory[:web_request_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:web_request_format].should == "application/xml"
@agent.memory[:web_request_method].should == "post"
put :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :atom
@agent.reload
@agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:webhook_format].should == "application/atom+xml"
@agent.memory[:webhook_method].should == "put"
@agent.memory[:web_request_values].should == { 'key' => "value", 'another_key' => "5" }
@agent.memory[:web_request_format].should == "application/atom+xml"
@agent.memory[:web_request_method].should == "put"
end
it "can accept a content-type to return" do

View file

@ -514,7 +514,55 @@ describe Agent do
end
end
end
end
describe ".trigger_web_request" do
class Agents::WebRequestReceiver < Agent
cannot_be_scheduled!
end
before do
stub(Agents::WebRequestReceiver).valid_type?("Agents::WebRequestReceiver") { true }
end
context "when .receive_web_request is defined" do
before do
@agent = Agents::WebRequestReceiver.new(:name => "something")
@agent.user = users(:bob)
@agent.save!
def @agent.receive_web_request(params, method, format)
memory['last_request'] = [params, method, format]
['Ok!', 200]
end
end
it "calls the .receive_web_request hook, updates last_web_request_at, and saves" do
@agent.trigger_web_request({ :some_param => "some_value" }, "post", "text/html")
@agent.reload.memory['last_request'].should == [ { "some_param" => "some_value" }, "post", "text/html" ]
@agent.last_web_request_at.to_i.should be_within(1).of(Time.now.to_i)
end
end
context "when .receive_webhook is defined" do
before do
@agent = Agents::WebRequestReceiver.new(:name => "something")
@agent.user = users(:bob)
@agent.save!
def @agent.receive_webhook(params)
memory['last_webhook_request'] = params
['Ok!', 200]
end
end
it "outputs a deprecation warning and calls .receive_webhook with the params" do
mock(Rails.logger).warn("DEPRECATED: The .receive_webhook method is deprecated, please switch your Agent to use .receive_web_request.")
@agent.trigger_web_request({ :some_param => "some_value" }, "post", "text/html")
@agent.reload.memory['last_webhook_request'].should == { "some_param" => "some_value" }
@agent.last_web_request_at.to_i.should be_within(1).of(Time.now.to_i)
end
end
end
describe "recent_error_logs?" do

View file

@ -62,7 +62,7 @@ describe Agents::DataOutputAgent do
end
end
describe "#receive_webhook" do
describe "#receive_web_request" do
before do
current_time = Time.now
stub(Time).now { current_time }
@ -70,15 +70,15 @@ describe Agents::DataOutputAgent do
end
it "requires a valid secret" do
content, status, content_type = agent.receive_webhook({ 'secret' => 'fake' }, 'get', 'text/xml')
content, status, content_type = agent.receive_web_request({ 'secret' => 'fake' }, 'get', 'text/xml')
status.should == 401
content.should == "Not Authorized"
content, status, content_type = agent.receive_webhook({ 'secret' => 'fake' }, 'get', 'application/json')
content, status, content_type = agent.receive_web_request({ 'secret' => 'fake' }, 'get', 'application/json')
status.should == 401
content.should == { :error => "Not Authorized" }
content, status, content_type = agent.receive_webhook({ 'secret' => 'secret1' }, 'get', 'application/json')
content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'application/json')
status.should == 200
end
@ -100,7 +100,7 @@ describe Agents::DataOutputAgent do
end
it "can output RSS" do
content, status, content_type = agent.receive_webhook({ 'secret' => 'secret1' }, 'get', 'text/xml')
content, status, content_type = agent.receive_web_request({ 'secret' => 'secret1' }, 'get', 'text/xml')
status.should == 200
content_type.should == 'text/xml'
content.gsub(/\s+/, '').should == Utils.unindent(<<-XML).gsub(/\s+/, '')
@ -137,7 +137,7 @@ describe Agents::DataOutputAgent do
it "can output JSON" do
agent.options['template']['item']['foo'] = "hi"
content, status, content_type = agent.receive_webhook({ 'secret' => 'secret2' }, 'get', 'application/json')
content, status, content_type = agent.receive_web_request({ 'secret' => 'secret2' }, 'get', 'application/json')
status.should == 200
content.should == {

View file

@ -10,11 +10,11 @@ describe Agents::WebhookAgent do
end
let(:payload) { {'some' => 'info'} }
describe 'receive_webhook' do
describe 'receive_web_request' do
it 'should create event if secret matches' do
out = nil
lambda {
out = agent.receive_webhook({ 'secret' => 'foobar', 'payload' => payload }, "post", "text/html")
out = agent.receive_web_request({ 'secret' => 'foobar', 'payload' => payload }, "post", "text/html")
}.should change { Event.count }.by(1)
out.should eq(['Event Created', 201])
Event.last.payload.should eq(payload)
@ -23,7 +23,7 @@ describe Agents::WebhookAgent do
it 'should not create event if secrets dont match' do
out = nil
lambda {
out = agent.receive_webhook({ 'secret' => 'bazbat', 'payload' => payload }, "post", "text/html")
out = agent.receive_web_request({ 'secret' => 'bazbat', 'payload' => payload }, "post", "text/html")
}.should change { Event.count }.by(0)
out.should eq(['Not Authorized', 401])
end
@ -31,9 +31,9 @@ describe Agents::WebhookAgent do
it "should only accept POSTs" do
out = nil
lambda {
out = agent.receive_webhook({ 'secret' => 'foobar', 'payload' => payload }, "get", "text/html")
out = agent.receive_web_request({ 'secret' => 'foobar', 'payload' => payload }, "get", "text/html")
}.should change { Event.count }.by(0)
out.should eq(['Not Authorized', 401])
out.should eq(['Please use POST requests only', 401])
end
end
end

View file

@ -1,19 +1,23 @@
require 'spec_helper'
describe "routing for webhooks" do
describe "routing for web requests" do
it "routes to handle_request" do
resulting_params = { :user_id => "6", :agent_id => "2", :secret => "foobar" }
get("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
post("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
put("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
delete("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
get("/users/6/web_requests/2/foobar").should route_to("web_requests#handle_request", resulting_params)
post("/users/6/web_requests/2/foobar").should route_to("web_requests#handle_request", resulting_params)
put("/users/6/web_requests/2/foobar").should route_to("web_requests#handle_request", resulting_params)
delete("/users/6/web_requests/2/foobar").should route_to("web_requests#handle_request", resulting_params)
end
it "supports the legacy /webhooks/ route" do
post("/users/6/webhooks/2/foobar").should route_to("web_requests#handle_request", :user_id => "6", :agent_id => "2", :secret => "foobar")
end
it "routes with format" do
get("/users/6/webhooks/2/foobar.json").should route_to("webhooks#handle_request",
get("/users/6/web_requests/2/foobar.json").should route_to("web_requests#handle_request",
{ :user_id => "6", :agent_id => "2", :secret => "foobar", :format => "json" })
get("/users/6/webhooks/2/foobar.atom").should route_to("webhooks#handle_request",
get("/users/6/web_requests/2/foobar.atom").should route_to("web_requests#handle_request",
{ :user_id => "6", :agent_id => "2", :secret => "foobar", :format => "atom" })
end
end