NoMethodError: undefined method `sort` from Twilio voice example - ruby-on-rails

I am trying to set up an example Twilio Rails project that calls a person. I am following the tutorial associated with this repo and have basically a carbon copy of the codebase. I'm getting an error that I think is from this line #validator = Twilio::Util::RequestValidator.new(##twilio_token).
Here's my twilio_controller.rb
class TwilioController < ApplicationController
# Before we allow the incoming request to connect, verify
# that it is a Twilio request
skip_before_action :verify_authenticity_token
before_action :authenticate_twilio_request, :only => [
:connect
]
##twilio_sid = ENV['TWILIO_ACCOUNT_SID']
##twilio_token = ENV['TWILIO_AUTH_TOKEN']
##twilio_number = ENV['TWILIO_NUMBER']
##api_host = ENV['TWILIO_HOST']
# Render home page
def index
render 'index'
end
def voice
response = Twilio::TwiML::Response.new do |r|
r.Say "Yay! You're on Rails!", voice: "alice"
r.Sms "Well done building your first Twilio on Rails 5 app!"
end
render :xml => response.to_xml
end
# Handle a POST from our web form and connect a call via REST API
def call
contact = Contact.new
contact.user_phone = params[:userPhone]
contact.sales_phone = params[:salesPhone]
# Validate contact
if contact.valid?
#client = Twilio::REST::Client.new ##twilio_sid, ##twilio_token
# Connect an outbound call to the number submitted
#call = #client.calls.create(
:from => ##twilio_number,
:to => contact.user_phone,
:url => "#{##api_host}/connect/#{contact.encoded_sales_phone}" # Fetch instructions from this URL when the call connects
)
# Let's respond to the ajax call with some positive reinforcement
#msg = { :message => 'Phone call incoming!', :status => 'ok' }
else
# Oops there was an error, lets return the validation errors
#msg = { :message => contact.errors.full_messages, :status => 'ok' }
end
respond_to do |format|
format.json { render :json => #msg }
end
end
# This URL contains instructions for the call that is connected with a lead
# that is using the web form.
def connect
# Our response to this request will be an XML document in the "TwiML"
# format. Our Ruby library provides a helper for generating one
# of these documents
response = Twilio::TwiML::Response.new do |r|
r.Say 'FUCK.', :voice => 'alice'
# r.Dial params[:sales_number]
end
render text: response.text
end
# Authenticate that all requests to our public-facing TwiML pages are
# coming from Twilio. Adapted from the example at
# http://twilio-ruby.readthedocs.org/en/latest/usage/validation.html
# Read more on Twilio Security at https://www.twilio.com/docs/security
private
def authenticate_twilio_request
twilio_signature = request.headers['HTTP_X_TWILIO_SIGNATURE']
# Helper from twilio-ruby to validate requests.
#validator = Twilio::Util::RequestValidator.new(##twilio_token)
# the POST variables attached to the request (eg "From", "To")
# Twilio requests only accept lowercase letters. So scrub here:
post_vars = params.reject {|k, v| k.downcase == k}
is_twilio_req = #validator.validate(request.url, post_vars, twilio_signature)
unless is_twilio_req
render :xml => (Twilio::TwiML::Response.new {|r| r.Hangup}).text, :status => :unauthorized
false
end
end
end
Error image:
I am using ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-darwin15] and Rails 5.1.0.

Your code is most likely failing at is_twilio_req = #validator.validate(request.url, post_vars, twilio_signature) because upon inspection of the gem's code, it is failing at sort below
data = url + params.sort.join
This is because in Rails 5.1, ActionController::Parameters no longer inherits from Hash, so Hash methods like sort (see Hash docs) will no longer work.
You will need to convert params into hash explicitly:
def authenticate_twilio_request
twilio_signature = request.headers['HTTP_X_TWILIO_SIGNATURE']
#validator = Twilio::Util::RequestValidator.new(##twilio_token)
# convert `params` which is an `ActionController::Parameters` object into `Hash`
# you will need `permit!` to strong-params-permit EVERYTHING so that they will be included in the converted `Hash` (you don't need to specifically whitelist specific parameters for now as the params are used by the Twilio gem)
params_hash = params.permit!.to_hash
post_vars = params_hash.reject {|k, v| k.downcase == k}
is_twilio_req = #validator.validate(request.url, post_vars, twilio_signature)
unless is_twilio_req
render :xml => (Twilio::TwiML::Response.new {|r| r.Hangup}).text, :status => :unauthorized
false
end
end

Related

How to get Host and Port configuration for Test Environment in Rspec 3.10 Rails

Hi I have a controller file at app/controllers/lti_controller.rb and I need to add Test for the POST #launch endpoint which is defined as /lti/launch in routes.rb and working as intended.
Problem
I need to send some data to the /lti/launch in the encoding format x-www-form-urlencoded and by using Net::HTTP I am doing that from the spec file.
But as Net::HTTP requires host and port for their operation and I have given localhost:3000 as of now and which is going to the development environment.
Question
How to access the host and port of the test environment in Rspec 3.10 such that I can pass them in spec file ?
Is there any way to use post '/lti/launch' , req.body : data in x-www-form-urlencoded format` ? and also get the host and port of the test environment ?
My Spec file
spec/requests/lti_spec.rb
# frozen_string_literal: true
require "rails_helper"
require 'uri'
require 'net/http'
describe LtiController, type: :request do
before do
# shared keys for lms access
#oauth_consumer_key_fromlms = 'some_keys'
#oauth_shared_secret_fromlms = 'some_secrets'
end
describe "#launch" do
before do
# creation of assignment and required users
#mentor = FactoryBot.create(:user)
#group = FactoryBot.create(:group, mentor: #mentor)
#member = FactoryBot.create(:user)
FactoryBot.create(:group_member, user: #member, group: #group)
#assignment = FactoryBot.create(:assignment, group: #group, lti_consumer_key: #oauth_consumer_key_fromlms, lti_shared_secret: #oauth_shared_secret_fromlms, status: "open")
end
def launch_uri
launch_url = "http://0.0.0.0:3000/lti/launch"
URI(launch_url)
end
let(:parameters) {
{
'launch_url' => launch_uri().to_s,
'user_id' => #member.id,
'launch_presentation_return_url' => 'http://localhost:3000/tool_return',
'lti_version' => 'LTI-1p0',
'lti_message_type' => 'basic-lti-launch-request',
'resource_link_id' => '88391-e1919-bb3456',
'lis_person_contact_email_primary' => #member.email,
'tool_consumer_info_product_family_code' => 'moodle',
'context_title' => 'sample Course',
'lis_outcome_service_url' => 'http://localhost:3000/grade_passback',
'lis_result_sourcedid' => SecureRandom.hex(10)
}
}
def consumer_data(oauth_consumer_key_fromlms, oauth_shared_secret_fromlms, parameters)
consumer = IMS::LTI::ToolConsumer.new(oauth_consumer_key_fromlms, oauth_shared_secret_fromlms, parameters)
allow(consumer).to receive(:to_params).and_return(parameters)
consumer.generate_launch_data
end
context "lti parameters are valid" do
it "returns success if assignment key and secret are ok and group member is present" do
data = consumer_data(#oauth_consumer_key_fromlms, #oauth_shared_secret_fromlms, parameters)
response = Net::HTTP.post_form(launch_uri(), data)
expect(response.code).to eq("200")
end
end
end
end
My Controller file
app/controllers/lti_controller.rb
class LtiController < ApplicationController
skip_before_action :verify_authenticity_token, only: :launch # for lti integration
before_action :set_group_assignment, only: %i[launch]
before_action :set_lti_params, only: %i[launch]
after_action :allow_iframe_lti, only: %i[launch]
def launch
session[:is_lti]=true # the lti session starting
require 'oauth/request_proxy/action_controller_request'
if #assignment.blank?
# if no assignment is found
flash[:notice] = t("lti.launch.notice_no_assignment")
render :launch_error, status: 401
return
end
if #group.present? # if there is a valid group based for the lti_token_key
#provider = IMS::LTI::ToolProvider.new(
params[:oauth_consumer_key], # lms_oauth_consumer_key
#assignment.lti_shared_secret, # the group's lti_token
params
)
if !#provider.valid_request?(request) # checking the lti request from the lms end
render :launch_error, status: 401
return
end
lms_lti_host = URI.join #launch_url_from_lms, '/' # identifies the domain and saves in session
session[:lms_domain]=lms_lti_host
#user = User.find_by(email: #email_from_lms) # find user by matching email with circuitverse and lms
if #user.present? # user is present in cv
if #user.id == #group.mentor_id # user is teacher
sign_in(#user) # passwordless sign_in the user as the authenticity is verified via lms
lms_auth_success_notice = t("lti.launch.notice_lms_auth_success_teacher", email_from_lms: #email_from_lms, lms_type: #lms_type, course_title_from_lms: #course_title_from_lms)
redirect_to group_assignment_path(#group, #assignment), notice: lms_auth_success_notice # if auth_success send to group page
else
user_in_group = GroupMember.find_by(user_id:#user.id,group_id:#group.id) # check if the user belongs to the cv group
if user_in_group.present? # user is member of the group
# render the button
flash[:notice] = t("lti.launch.notice_students_open_in_cv")
create_project_if_student_present() # create project with lis_result_sourced_id for the student
render :open_incv, status: 200
else # user is not a member of the group
# send the user an email
flash[:notice] = t("lti.launch.notice_ask_teacher")
render :launch_error, status: 401
end
end
else # no such user in circuitverse,showing a notice to create an account in cv
flash[:notice] = t("lti.launch.notice_no_account_in_cv", email_from_lms: #email_from_lms )
render :launch_error, status: 400
end
else # there is no valid group present for the lti_consumer_key
flash[:notice] = t("lti.launch.notice_invalid_group")
render :launch_error, status: 400
end
end
def allow_iframe_lti
return unless session[:is_lti]
response.headers["X-FRAME-OPTIONS"] = "ALLOW-FROM #{session[:lms_domain]}"
end
def create_project_if_student_present
#user = User.find_by(email: #email_from_lms)
#project = Project.find_by(author_id: #user.id, assignment_id: #assignment.id) # find if the project is already present
if #project.blank? # if not then create one
#project = #user.projects.new
#project.name = "#{#user.name}/#{#assignment.name}"
#project.assignment_id = #assignment.id
#project.project_access_type = "Private"
#project.build_project_datum
#project.lis_result_sourced_id = params[:lis_result_sourcedid] # this param is required for grade submission
#project.save
end
end
private
def set_group_assignment # query db and check lms_oauth_consumer_key is equal to which assignment and find the group also
#assignment = Assignment.find_by(lti_consumer_key: params[:oauth_consumer_key])
if #assignment.present?
#group =#assignment.group
end
end
def set_lti_params # get some of the parameters from the lti request
#email_from_lms = params[:lis_person_contact_email_primary] # the email from the LMS
#lms_type = params[:tool_consumer_info_product_family_code] # type of lms like moodle/canvas
#course_title_from_lms = params[:context_title] # the course titile from lms
#launch_url_from_lms = params[:launch_presentation_return_url]
session[:lis_outcome_service_url] = params[:lis_outcome_service_url] # requires for grade submission
session[:oauth_consumer_key] = params[:oauth_consumer_key] # requires for grade submission
end
end
Sample data needed to be sent
{"oauth_consumer_key"=>"some_keys", "oauth_signature_method"=>"HMAC-SHA1", "oauth_timestamp"=>"1627879512", "oauth_nonce"=>"Id3rLYZBqMvnRuSpisMEEgFkLFnkxZPS2oqoyBJZLM", "oauth_version"=>"1.0", "context_title"=>"sample Course", "launch_presentation_return_url"=>"http://localhost:3000/tool_return", "launch_url"=>"http://0.0.0.0:3000/lti/launch", "lis_outcome_service_url"=>"http://localhost:3000/grade_passback", "lis_person_contact_email_primary"=>"chung#towne-littel.org", "lis_result_sourcedid"=>"3e87e3aa8f5056260a12", "lti_message_type"=>"basic-lti-launch-request", "lti_version"=>"LTI-1p0", "resource_link_id"=>"88391-e1919-bb3456", "tool_consumer_info_product_family_code"=>"moodle", "user_id"=>"461", "oauth_signature"=>"lgsHKJxHolBU1rTZ5M9zXg688hU="}
You don't need to know the host and port. And you should not perform the request directly with Net::HTTP manually. get/post/... helpers handle that. https://relishapp.com/rspec/rspec-rails/v/5-0/docs/request-specs/request-spec

session method cannot be called 2 times in controller

In the code below, when the #call method executes, it redirects the call to the #connect method to play an audio, then the #connect method redirects to #menu_selection where everything breaks. the error I get in heroku logs is that sessions is nil or defined.
What I dont understand is that I am already using session[:user_id] in the first method #call. why is it no defined in the #menu_selection method?
def call
#list = User.find_by(id: session[:user_id]).contact_lists.find_by(id: session[:last_contact_list_id])
#contacts = #list.contacts
#client = Twilio::REST::Client.new(##account_sid, ##auth_token)
#contacts.each do |contact|
#call = #client.account.calls.create(
:from => '+18056234397', # From your Twilio number
:to => '+1' + contact.phone , # To any number
:url => root_url + "connect"
)
end
redirect_to root_path
end
def connect
response = Twilio::TwiML::Response.new do |r|
r.Play 'https://clyp.it/l1qz52x5.mp3'
r.Gather numDigits: '1', action: menu_path do |g|
g.Play 'https://a.clyp.it/2mue3ocn.mp3'
end
end
render :xml => response.to_xml
end
def menu_selection
list = User.find_by(id: session[:user_id]).contact_lists.find_by(id: session[:last_contact_list_id])
user_selection = params[:Digits]
#client = Twilio::REST::Client.new(##account_sid, ##auth_token)
case user_selection
when "1"
#output = "say something."
twiml_say(#output, true)
when "2"
twiml_dial("+1805XXXXX")
when "3"
#output = "Bye Bye..."
twiml_say(#output, true)
end
end
In the #menu_selection method I get the error : undefined local variable or method `session'
Its in the first line where I'm defining the "list" variable.
I never had this kind of issue before. If anyone knows whats going on, I would appreciate your help.
I tried defining the first #list variable as a class variable outside of the method #call but It gives me the same error that I get now. I also tried making it a class variable inside the #call method to try using it in #menu_selection method, but I get an "##list is undefined" error.
Twilio developer evangelist here.
The problem here is that the session in the #call action is between your user and the server. However, when you initiate a call and Twilio calls back to your server the session between Twilio and the server is completely different.
In this situation, you need to pass the data you need Twilio to know through your URL. So, instead of just sending the connect URL, send it with the parameters you need later. So your #call action would look like:
def call
#list = User.find_by(id: session[:user_id]).contact_lists.find_by(id: session[:last_contact_list_id])
#contacts = #list.contacts
#client = Twilio::REST::Client.new(##account_sid, ##auth_token)
#contacts.each do |contact|
#call = #client.account.calls.create(
:from => '+18056234397', # From your Twilio number
:to => '+1' + contact.phone , # To any number
:url => root_url + "connect?user_id=#{session[:user_id]}&last_contact_list_id=#{session[:last_contact_list_id]}"
)
end
redirect_to root_path
end
Then, your #connect action will need to pass those on to your #menu_selection action too (note the menu_path line):
def connect
response = Twilio::TwiML::Response.new do |r|
r.Play 'https://clyp.it/l1qz52x5.mp3'
r.Gather numDigits: '1', action: menu_path(:user_id => params[:user_id], :last_contact_list_id => params[:last_contact_list_id] do |g|
g.Play 'https://a.clyp.it/2mue3ocn.mp3'
end
end
render :xml => response.to_xml
end
Finally, you will be able to use those parameters, instead of the session, in your #menu_selection action:
def menu_selection
list = User.find_by(id: params[:user_id]).contact_lists.find_by(id: params[:last_contact_list_id])
user_selection = params[:Digits]
#client = Twilio::REST::Client.new(##account_sid, ##auth_token)
case user_selection
when "1"
#output = "say something."
twiml_say(#output, true)
when "2"
twiml_dial("+1805XXXXX")
when "3"
#output = "Bye Bye..."
twiml_say(#output, true)
end
end
Let me know if this helps!

uninitialized constant ActiveSupport::Memoizable

Been trying to get fb_graph working so i can get things like someones friendlist and i cannot get rid of this error. The ActiveSupport::Memoizable is included in the facebook class. Trying figure it out from a fb_graph example application here https://github.com/nov/fb_graph_sample
image of error: http://imgur.com/VXSHhJf
facebook model:
class Facebook < ActiveRecord::Base
def profile
#profile ||= FbGraph::User.me(self.access_token).fetch
end
class << self
extend ActiveSupport::Memoizable
def config
#config ||= if ENV['fb_client_id'] && ENV['fb_client_secret'] && ENV['fb_scope']
{
:client_id => ENV['fb_client_id'],
:client_secret => ENV['fb_client_secret'],
:scope => ENV['fb_scope'],
}
else
YAML.load_file("#{Rails.root}/config/facebook.yml")[Rails.env].symbolize_keys
end
rescue Errno::ENOENT => e
raise StandardError.new("config/facebook.yml could not be loaded.")
end
def app
FbGraph::Application.new config[:client_id], :secret => config[:client_secret]
end
def auth(redirect_uri = nil)
FbGraph::Auth.new config[:client_id], config[:client_secret], :redirect_uri => redirect_uri
end
def identify(fb_user)
_fb_user_ = find_or_initialize_by_identifier(fb_user.identifier.try(:to_s))
_fb_user_.access_token = fb_user.access_token.access_token
_fb_user_.save!
_fb_user_
end
end
end
and here is facebooks_controller
require 'rack/oauth2'
class FacebooksController < ApplicationController
before_filter :require_authentication, :only => :destroy
rescue_from Rack:.center.hero-unit
%h1 Welcome to Dropshare
%h2
This is the home page for Dropshare
%p (at least for time being)
= render 'layouts/facebook_signup'
= render 'layouts/drive_signup'
/
<haml:loud> provide(:title, &#39;Home&#39;)</haml:loud>
<h1>Home</h1>
<p>This is the home page (for the time being) for Dropshare</p>
Sign up now!
:OAuth2::Client::Error, :with => :oauth2_error
# handle Facebook Auth Cookie generated by JavaScript SDK
def show
auth = Facebook.auth.from_cookie(cookies)
authenticate Facebook.identify(auth.user)
redirect_to dashboard_url
end
# handle Normal OAuth flow: start
def new
client = Facebook.auth(callback_facebook_url).client
redirect_to client.authorization_uri(
:scope => Facebook.config[:scope]
)
end
# handle Normal OAuth flow: callback
def create
client = Facebook.auth(callback_facebook_url).client
client.authorization_code = params[:code]
access_token = client.access_token! :client_auth_body
user = FbGraph::User.me(access_token).fetch
authenticate Facebook.identify(user)
redirect_to dashboard_url
end
def destroy
unauthenticate
redirect_to root_url
end
private
def oauth2_error(e)
flash[:error] = {
:title => e.response[:error][:type],
:message => e.response[:error][:message]
}
redirect_to root_url
end
end
Solution
replace
ActiveSupport::Memoizable
with memoist and require 'memoist'
I think you may be running into the fact that ActiveSupport::Memoizable was deprecated & removed from Rails.
https://github.com/rails/rails/commit/36253916b0b788d6ded56669d37c96ed05c92c5c
The author of that gem is running this version of Rails in their gemfile, so I would presume it's supported through this:
gem 'rails', '~>3.2.11'
I'm guessing you're running a newer version of Rails.

422 error when testing stripe webhook

I keep getting a 422 error when testing stripe's webhook for customer.subscription.deleted
I placed this in my config routes
post 'stripewebhooks/receive'
here is my controller
class StripewebhooksController < ApplicationController
Stripe::api_key = ENV['STRIPE_SECRET_KEY']
require 'json'
def receive
data_json = JSON.parse request.body.read
p data_json['data']['object']['customer']
if data_json[:type] == "customer.subscription.deleted"
cancel_subscription(data_event)
end
end
def cancel_subscription(data_event)
#subscription = Subscription.find_by_stripe_customer_token(data['data']['object']['customer'])
#subscription.update_attribute(:subscription_status, "inactive")
end
end
I am unclear on what is suppose to go in the parenthesis after
def cancel_subscription
I am not sure that I am suppose to put data_event or what this means.
When you get a post data from stripe, you need to return a 200 status code from your application.
try this
def receive
data_json = JSON.parse request.body.read
p data_json['data']['object']['customer']
if data_json[:type] == "customer.subscription.deleted"
# Why did you send data_event? send the parsed data_json as parameter
cancel_subscription(data_json)
end
# Return a 200 status code
render :text => '{}', :status => :ok
end

How do you delay a rendering job?

This method works OK, but if I add delayed_job's handle_asychronously, I get can't convert nil into String:
def onixtwo
s = render_to_string(:template=>"isbns/onix.xml.builder")
send_data(s, :type=>"text/xml",:filename => "onix2.1.xml")
end
handle_asynchronously :onixtwo
So rendering with delayed job is clearly having a problem with params being passed. I've tried putting this job in a rake task but render_to_string is a controller action - and I'm using a current_user variable which needs to be referenced in the controller or view only. So... what's the best way to delay a rendering job?
/////////update////////
Given that I'm currently pair-programming with a toddler, I don't have the free hands to investigate additional class methods as wisely recommended in the comments - so as a quick and dirty I tried this:
def onixtwo
system " s = render_to_string(:template=>'isbns/onix.xml.builder') ; send_data(s, :type=>'text/xml',:filename => 'onix2.1.xml') & "
redirect_to isbns_path, :target => "_blank", :flash => { :success => "ONIX message being generated in the background." }
end
Why doesn't it work? No error message just no file produced - which is the case when I remove system ... &
For what it's worth, this is what I did, bypassing render_to_stream entirely. This is in /lib or app/classes (adding config.autoload_paths += %W(#{config.root}/classes into config/application.rb):
#classes/bookreport.rb
# -*- encoding : utf-8 -*-
require 'delayed_job'
require 'delayed/tasks'
class Bookreport
# This method takes args from the book report controller. First it sets the current period. Then it sorts out which report wants calling. Then it sends on the arguments to the correct class.
def initialize(method_name, client, user, books)
current_period = Period.where(["currentperiod = ? and client_id = ?", true, client]).first
get_class = method_name.capitalize.constantize.new
get_class.send(method_name.to_sym, client, user, books.to_a, current_period.enddate)
end
end
#app/classes/onixtwo.rb
require 'builder'
class Onixtwo
def onixtwo(client_id, user_id, books, enddate)
report_name = "#{Client.find_by_id(client_id).client_name}-#{Time.now}-onix-21"
filename = "#{Rails.root}/public/#{report_name}.xml"
current_company = Company.where(:client_id => client_id).first
File.open(filename, "w") do |file|
xml = ::Builder::XmlMarkup.new(:target => file, :indent => 2)
xml.instruct!(:xml, :version => "1.0", :encoding => "utf-8")
xml.declare! :DOCTYPE, :ONIXMessage, :SYSTEM, "http://www.editeur.org/onix/2.1/03/reference/onix-international.dtd"
xml.ONIXMessage do
xml.Header do
#masses of Builder code goes here...
end
end #of file
xmlfile = File.open(filename, "r")
onx = Onixarchive.new(:client_id => client_id, :user_id => user_id)
onx.xml = xmlfile
onx.save!
end #of method
handle_asynchronously :onixtwo
end #of class
Called from the view like this:
= link_to("Export to ONIX 2.1", params.merge({:controller=>"bookreports" , :action=>:new, :method_name => "onixtwo"}))
Via a controller like this:
class Books::BookreportsController < ApplicationController
#uses Ransack for search, hence the #q variable
def new
#q = Book.search(params[:q])
#books = #q.result.order('pub_date DESC')
method_name = params[:method_name]
Bookreport.new(method_name, #client, #user, #books)
redirect_to books_path, :flash => {:success => t("#{method_name.to_sym}").html_safe}
end
end

Resources