Refactoring an API request on Rails - ruby-on-rails

I have a rails worker using redis/sidekiq where I send some data to an API (Active Campaign), so I normally use all the http configurations to send data. I want to have it nice and clean, so it's part of a refactor thing. My worker currently looks like this:
class UpdateLeadIdWorker
include Sidekiq::Worker
BASE_URL = Rails.application.credentials.dig(:active_campaign, :url)
private_constant :BASE_URL
API_KEY = Rails.application.credentials.dig(:active_campaign, :key)
private_constant :API_KEY
def perform(ac_id, current_user_id)
lead = Lead.where(user_id: current_user_id).last
url = URI("#{BASE_URL}/api/3/contacts/#{ac_id}") #<--- need this endpoint
https = bindable_lead_client.assign(url)
pr = post_request.assign(url)
case lead.quote_type
when 'renter'
data = { contact: { fieldValues: [{ field: '5', value: lead.lead_id }] } }
when 'home'
data = { contact: { fieldValues: [{ field: '4', value: lead.lead_id }] } }
when 'auto'
data = { contact: { fieldValues: [{ field: '3', value: lead.lead_id }] } }
else
raise 'Invalid quote type'
end
pr.body = JSON.dump(data)
response = JSON.parse(https.request(pr).read_body).symbolize_keys
if response.code == '200'
Rails.logger.info "Successfully updated contact #{ac_id} with lead id #{lead.lead_id}"
else
raise "Error creating contact: #{response.body}"
end
end
def bindable_lead_client
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http
end
def post_request
post_request_ = Net::HTTP::Put.new(url)
post_request_['Accept'] = 'application/json'
post_request_['Content-Type'] = 'application/json'
post_request_['api-token'] = API_KEY
post_request_
end
end
But whenever I run this I get:
2022-07-28T00:52:08.683Z pid=24178 tid=1s1u WARN: NameError: undefined local variable or method `url' for #<UpdateLeadIdWorker:0x00007fc713442be0 #jid="e2b9ddb6d5f4b8aecffa4d8b">
Did you mean? URI
I don't want everything stuck in one method. How could I achieve to make this cleaner?
Thanks.

Pure ruby wise, The reason you get the error is because your method definition bindable_lead_client is missing the url argument. Hence undefined variable.
So def should look something like:
def bindable_lead_client (url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http
end
and call:
bindable_lead_client(url)
As for how to make this code better, falls under question being too subjective under StackOverflow guidelines, which encourage you to ask more specific questions.

Related

Ruby on Rails use of methods within methods

So I am writing a simple controller that will receive parameters from a Postrequest to my API. And I want to keep things cleaner and nice, so I wrote something like this:
def create
update_contact
end
def update_contact
create_token
url = URI("https://acme.api-us1.com/api/3/contacts/#{active_campaign_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['api-token'] = API_KEY
data = { contact: { fieldValues: [{ field: '1', value: contact[:email_token] }] } }
request.body = JSON.dump(data)
response = http.request(request)
end
def create_token
active_campaign_id = params[:contact][:id].to_i
generate_token = SecureRandom.urlsafe_base64(12)
contact = Contact.find_or_initialize_by(active_campaign_id: active_campaign_id, email_token: generate_token)
contact.save!
end
But whenever I run this turns into:
*** NameError Exception: undefined local variable or method `active_campaign_id'
and same goes for email_token
*** NameError Exception: undefined local variable or method `email_token'
Now whenever I do this:
def create
update_contact
end
def update_contact
active_campaign_id = params[:contact][:id].to_i
generate_token = SecureRandom.urlsafe_base64(12)
contact = Contact.find_or_initialize_by(active_campaign_id: active_campaign_id, email_token: generate_token)
contact.save!
url = URI("https://acme.api-us1.com/api/3/contacts/#{active_campaign_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['api-token'] = API_KEY
data = { contact: { fieldValues: [{ field: '1', value: contact[:email_token] }] } }
request.body = JSON.dump(data)
response = http.request(request)
end
It works! Why is that? How can I structure my code or methods as clean as possible?
And what resources could make me understand more accessing methods in rails?
Thanks for the help!
If you want to keep methods you have and make it works you can achieve this by doing next refactoring:
def create
update_contact
end
def update_contact
contact = create_contact
url = URI("https://acme.api-us1.com/api/3/contacts/#{contact.active_campaign_id}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request['api-token'] = API_KEY
data = { contact: { fieldValues: [{ field: '1', value: contact.email_token }] } }
request.body = JSON.dump(data)
response = http.request(request)
end
def create_contact
Contact.create_with(
email_token: SecureRandom.urlsafe_base64(12)
).find_or_create_by!(
active_campaign_id: params.dig(:contact, :id)&.to_i
)
end
And probably you need to use create_with method because every time when you will try to find Contact by fields pair email_token and active_campaign_id SecureRandom.urlsafe_base64(12) will generate a new email token and you always will have new object created instead of getting it from database.
It looks like you are trying to access the 'active_campaign_id' and 'email_token' variables in the 'update_contact' method, but those variables are only defined in the 'create_token' method. Try moving the 'update_contact' method inside the 'create_token' method so that it has access to those variables.
More info you could find here:
https://guides.rubyonrails.org/v6.1/action_view_overview.html#using-action-view-with-rails
https://guides.rubyonrails.org/v6.1/engines.html#using-a-controller-provided-by-the-application
https://guides.rubyonrails.org/v6.1/security.html#user-management
https://guides.rubyonrails.org/v6.1/2_3_release_notes.html#action-controller
https://guides.rubyonrails.org/v6.1/active_record_multiple_databases.html#automatic-swapping-for-horizontal-sharding

FTX.com REST API POST Authentication FAILS with Ruby on Rails and net/https

Hoping for some help as this one has me baffled...
I created a user account and API credentials at FTX.com.
They have an interesting Auth setup which is detailed here: https://docs.ftx.com/?python#authentication
They only provide code examples for python, javascript and c#, but I need to implement the integration on a RoR app.
Here's a link which also provides an example for both GET and POST calls: https://blog.ftx.com/blog/api-authentication/
I'm using:
ruby '3.0.1'
gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
also,
require 'uri'
require 'net/https'
require 'net/http'
require 'json'
I got the authentication working for GET calls as follows:
def get_market
get_market_url = 'https://ftx.com/api/markets/BTC-PERP/orderbook?depth=20'
api_get_call(get_market_url)
end
def api_get_call(url)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}GET/api/markets"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
rsp = http.get(uri, headers)
JSON.parse(rsp.body)
end
This works great and I get the correct response:
=>
{"success"=>true,
"result"=>
{"bids"=>
[[64326.0, 2.0309],
...
[64303.0, 3.1067]],
"asks"=>
[[64327.0, 4.647],
...
[64352.0, 0.01]]}}
However, I can't seem to authenticate correctly for POST calls (even though as far as I can tell I am following the instructions correctly). I use the following:
def create_subaccount
create_subaccount_url = 'https://ftx.com/api/subaccounts'
call_body =
{
"nickname": "sub2",
}.to_json
api_post_call(create_subaccount_url, call_body)
end
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri, headers)
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
Also tried passing headers via request[] directly:
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['FTX-KEY'] = ENV['FTX_API_KEY']
request['FTX-SIGN'] = signature
request['FTX-TS'] = ts.to_s
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
This is the error response:
=> {"success"=>false, "error"=>"Not logged in: Invalid signature"}
My feeling is the issue is somewhere in adding the body to signature_payload before generating the signature via HMAC here..?:
signature_payload = "#{ts}POST/api/subaccounts#{body}"
Thinking this because, if I leave out #{body} here, like so:
signature_payload = "#{ts}POST/api/subaccounts"
the response is:
=> {"success"=>false, "error"=>"Missing parameter nickname"}
I have tried several iterations of setting up the POST call method using various different net/https examples but have had no luck...
I have also contacted FTX support but have had no response.
Would truly appreciate if anyone has some insight on what I am doing wrong here?
try this headers
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
Here's a working example of a class to retrieve FTX subaccounts. Modify for your own purposes. I use HTTParty.
class Balancer
require 'uri'
require "openssl"
include HTTParty
def get_ftx_subaccounts
method = 'GET'
path = '/subaccounts'
url = "#{ENV['FTX_BASE_URL']}#{path}"
return HTTParty.get(url, headers: headers(method, path, ''))
end
def headers(*args)
{
'FTX-KEY' => ENV['FTX_API_KEY'],
'FTX-SIGN' => signature(*args),
'FTX-TS' => ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
end
def signature(*args)
OpenSSL::HMAC.hexdigest(digest, ENV['FTX_API_SECRET'], signature_payload(*args))
end
def signature_payload(method, path, query)
payload = [ts, method.to_s.upcase, "/api", path].compact
if method==:post
payload << query.to_json
elsif method==:get
payload << ("?" + URI.encode_www_form(query))
end unless query.empty?
payload.join.encode("UTF-8")
end
def ts
#ts ||= (Time.now.to_f * 1000).to_i
end
def digest
#digest ||= OpenSSL::Digest.new('sha256')
end
end

Rails app with Faraday: Problem refactoring Faraday::new() without url parameter

I'm refactoring some code in a Rails app consisting of several microservices. The faraday_middleware gem is used for communication between services.
I managed to replace several calls to Faraday::new() in different helper files with one single call to Faraday::new() in a ServiceConnectionHelper module. All of these replaced calls had an url parameter: Faraday.new(url: url)
But there's two very similar pieces of code left that I'd like to get rid of. In these cases, there is no url parameter. This is the old (working) code:
# This code calls the connection function below
def create(resource)
params = {
resource_id: resource.to_param,
version: resource.version,
file: Faraday::UploadIO.new(resource.file.path, resource.mime_type.to_s, resource.file.original_filename)
}
res = connection(resource.authorization).post(foobar_url, params)
return res.body['id'] if [200, 201].include?(res.status)
raise UploadError, res.body['error']
end
# connection function
def connection(authorization_header = nil)
Faraday.new do |conn|
conn.use FaradayMiddleware::FollowRedirects, limit: 5
conn.request :multipart
conn.request :url_encoded
conn.use FaradayMiddleware::ParseJson, content_type: 'application/json'
conn.adapter Faraday.default_adapter
conn.headers['Accept'] = 'application/json'
conn.headers['Authorization'] = authorization_header unless authorization_header.nil?
end
end
This is the code I want to use instead. It's not working because of an error inside the create function. When I catch it and log it, e.inspect is just #<UploadError: Please specify a file>
# Small change only: Te other service's url is computed in the ServiceConnectionHelper module
def create(resource)
params = {
resource_id: resource.to_param,
version: resource.version,
file: Faraday::UploadIO.new(resource.file.path, resource.mime_type.to_s, resource.file.original_filename)
}
# This is were the error happens
res = connection(resource.authorization).post('/', params)
return res.body['id'] if [200, 201].include?(res.status)
raise UploadError, res.body['error']
end
# connection function calls the new helper module now
def connection(authorization_header = nil)
ServiceConnectionHelper.connection('foobar', authorization_header)
end
# the new module
module ServiceConnectionHelper
class << self
def connection(service, oauth_token = nil)
url = service_url(service)
Faraday.new(url: url) do |conn|
conn.use FaradayMiddleware::FollowRedirects, limit: 5
conn.request :url_encoded
conn.adapter Faraday.default_adapter
conn.request :multipart
conn.use FaradayMiddleware::ParseJson, content_type: 'application/json'
conn.headers['Accept'] = 'application/json'
conn.headers['Authorization'] = oauth_token if oauth_token
end
end
private
def service_url(service)
url = case service
when 'foobar' then 'foobar_url_as_a_string'
# the same for other services
end
url
end
end
end
What can I do to make the ServiceConnectionHelper work in this case?
Compared of your first example, the order of request changed:
conn.request :url_encoded
conn.request :multipart

how to configure sensu keepalive to throw an alert to slack

I want to send out an slack alert. when a client fails a keep alive check.
What is the process to do it? can I know how to do it? I am using hiroakis/docker-sensu-server docker image.
On the slack side:
on the slack side you have to create a new incoming webhook to your desired channel.
On the sensu side:
you create a new handler that uses the webhook.
then you have to assign this handler to be used for the checks you desire in their check configuration file.
If you need a proxy to connect to the internet keep in mind to put that one in the handler as well or in a more elegant way pass it on via the config file.
eg. you can use this handler:
#!/usr/bin/env ruby
# Copyright 2014 Dan Shultz and contributors.
#
# Released under the same terms as Sensu (the MIT license); see LICENSE
# for details.
#
# In order to use this plugin, you must first configure an incoming webhook
# integration in slack. You can create the required webhook by visiting
# https://{your team}.slack.com/services/new/incoming-webhook
#
# After you configure your webhook, you'll need the webhook URL from the integration.
require 'rubygems' if RUBY_VERSION < '1.9.0'
require 'sensu-handler'
require 'json'
class Slack < Sensu::Handler
option :json_config,
description: 'Configuration name',
short: '-j JSONCONFIG',
long: '--json JSONCONFIG',
default: 'slack'
def slack_webhook_url
get_setting('webhook_url')
end
def slack_channel
get_setting('channel')
end
def slack_proxy_addr
get_setting('proxy_addr')
end
def slack_proxy_port
get_setting('proxy_port')
end
def slack_message_prefix
get_setting('message_prefix')
end
def slack_bot_name
get_setting('bot_name')
end
def slack_surround
get_setting('surround')
end
def markdown_enabled
get_setting('markdown_enabled') || true
end
def incident_key
#event['client']['name'] + '/' + #event['check']['name']
end
def get_setting(name)
settings[config[:json_config]][name]
end
def handle
description = #event['check']['notification'] || build_description
post_data("*Check*\n#{incident_key}\n\n*Description*\n#{description}")
end
def build_description
[
#event['check']['output'].strip,
#event['client']['address'],
#event['client']['subscriptions'].join(',')
].join(' : ')
end
def post_data(notice)
uri = URI(slack_webhook_url)
if (defined?(slack_proxy_addr)).nil?
http = Net::HTTP.new(uri.host, uri.port)
else
http = Net::HTTP::Proxy(slack_proxy_addr, slack_proxy_port).new(uri.host, uri.port)
end
http.use_ssl = true
begin
req = Net::HTTP::Post.new("#{uri.path}?#{uri.query}")
text = slack_surround ? slack_surround + notice + slack_surround : notice
req.body = payload(text).to_json
response = http.request(req)
verify_response(response)
rescue Exception => e
puts "An error has ocurred when posting to slack: #{e.message}"
end
end
def verify_response(response)
case response
when Net::HTTPSuccess
true
else
fail response.error!
end
end
def payload(notice)
{
icon_url: 'http://sensuapp.org/img/sensu_logo_large-c92d73db.png',
attachments: [{
text: [slack_message_prefix, notice].compact.join(' '),
color: color
}]
}.tap do |payload|
payload[:channel] = slack_channel if slack_channel
payload[:username] = slack_bot_name if slack_bot_name
payload[:attachments][0][:mrkdwn_in] = %w(text) if markdown_enabled
end
end
def color
color = {
0 => '#36a64f',
1 => '#FFCC00',
2 => '#FF0000',
3 => '#6600CC'
}
color.fetch(check_status.to_i)
end
def check_status
#event['check']['status']
end
end
and then pass a config file like this on to it
{
"handlers": {
"slack": {
"command": "/etc/sensu/handlers/slack.rb",
"type": "pipe",
"filters": [
],
"severities": [
"ok",
"critical"
]
}
}
}
which then would also include which severities to be handled by that handler

Stubbing result of Net::HTTP.new(url.host, url.port)

I have a funciton that calls Net::HTTP.new(google_url.host, google_url.port) and I am trying to figure out how to stub the result for testing. Basically I don't want to be hitting the google URL shortener every time I run my test.
def shorten_url(long_url)
google_url = URI.parse("https://www.googleapis.com/urlshortener/v1/url")
data = JSON.generate({"longUrl" => "#{long_url}"})
header = {"Content-Type" => "application/json"}
http = Net::HTTP.new(google_url.host, google_url.port)
http.use_ssl = true
short_url = ""
res = http.request_post(google_url.path, data, header)
jsonResponse = JSON.parse(res.body)
short_url = jsonResponse["id"]
end
Basically I want to be able to set the result of that function.
I've tried things like: Net::HTTP.any_instance.stubs(:HTTP.new).returns("www.test.com") but cannot figure out how to get it to work.
class HTTP < Protocol
....
# Creates a new Net::HTTP object.
# If +proxy_addr+ is given, creates an Net::HTTP object with proxy support.
# This method does not open the TCP connection.
def HTTP.new(address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil)
h = Proxy(p_addr, p_port, p_user, p_pass).newobj(address, port)
h.instance_eval {
#newimpl = ::Net::HTTP.version_1_2?
}
h
end
....
end
Check out webmock - this sounds like it will do exactly what you're looking for.

Resources