Rails - Action cable : Broadcasting to specific resource - ruby-on-rails

I am using action cable and rails 6.
I am trying to broadcast messages to a particular organization.
My client js code picks up the organization id on the page an creates a subscription on the organization channel, passing up the organization_id.
const organizationID = document.getElementById("organization-wrapper").dataset.organizationId
consumer.subscriptions.create({
channel: "OrganizationChannel",
organization_id: organizationID
},{
connected() {
console.log('connected')
},
received(data) {
alert("hey")
const budgetSpentCents = parseInt(data['budget_spent_cents'])
const budgetSpentUnit = budgetSpentCents / 100.0
const trainingBudgetTotal = document.getElementById('training-budget-total')
const currentTrainingBudgetTotal = parseInt(trainingBudgetTotal.textContent)
trainingBudgetTotal.textContent = currentTrainingBudgetTotal + budgetSpentUnit
}
This works well, and I can see the ruby channel code being executed :
class OrganizationChannel < ApplicationCable::Channel
def subscribed
binding.pry
organization = Organization.find(params[:organization_id])
stream_from organization
end
def unsubscribed
stop_all_streams
# Any cleanup needed when channel is unsubscribed
end
end
However I am stuck when I try to broadcast to that particular client (organization). Here 's my attempt :
OrganizationChannel.broadcast_to(
Organization.first,
budget_spent_cents: Registration.last&.training_budget_transaction&.total_cents
)
which returns 0 and
[ActionCable] Broadcasting to organization:Z2lkOi8veXVub28vT3JnYW5pemF0aW9uLzE: {:budget_spent_cents=>-5000}
the received(data) hook in my client code is never executed... Im not sure why ? My guess is because Z2lkOi8veXVub28vT3JnYW5pemF0aW9uLzE is not the actual id of the organization (what is that string actually ?) and it doesnt find the proper channel to broadcast to.
How would you setup the broadcasting right in that situation ?

Z2lkOi8veXVub28vT3JnYW5pemF0aW9uLzE is the global id for the organization you are testing with. organization:Z2lkOi8veXVub28vT3JnYW5pemF0aW9uLzE is the channel name generated and used by the OrganizationChannel.broadcast_to method based on the arguments you are passing.
On your OrganizationChannel class, instead of using stream_from organization use stream_for organization. That will tell the client to properly stream from the organization:#{global_id} channel.

Related

Rails how to save data upon creation with an external API?

In my app for bike_rental_shops, I'm making it possible for these shops to manage their bike rentals.
Context
Bike rental companies also offer their bikes on website of external parties, therefore I'm connecting my Rails application with these external websites. I'm currently handling this in my controller when a user goes to the index page. Before loading the index page an API call is made to the external rental website and new bike rentals should be saved in the database.
Question
How to save only new rentals and not all rentals linked to a certain external rental website?
Current consideration
The only thing I can come up with is adding a database column with {external_website}_rental_id for a specific external website, so I can match them. However, this would mean that I need to add a seperate rental_id for every external rental website.
Code
rentals_controller.rb
def index
shop = Shop.find(params[:id])
request_rental_api
#bikes = shop.bikes
end
private
def request_rental_api
# set variables
base_url = "https://www.rentalwebsite.com"
url = "/rest/api/rentals"
token = 'TOKEN'
# init connection object
connection = Faraday.new(:url => base_url) do |c|
c.use Faraday::Request::UrlEncoded
c.use Faraday::Response::Logger
c.use FaradayMiddleware::FollowRedirects
c.adapter Faraday::Adapter::NetHttp
end
# send request
response = connection.get url do |request|
request.headers["Authorization"] = token
request.headers["Accept"] = "application/json"
end
bookings = JSON.parse(response.body['results'])
# check if rental is unique, and if so save it.
# Rental.create(????)
end
JSON output API
{
"next": null,
"previous": null,
"results": [
{
"activation_requested": false,
"id": 21664,
"slug": "rental-test"
#more information....
}
}]
you can create 2 columns
provider_rental_id id returned in response JSON
provider name of provider, to which request was made
Then to only create new records
rental_ids = bookings['results'].map { |r| r['id'] }
return if rental_ids.none?
existing_rental_ids = Rental.where(provider_rental_id: rental_ids, provider: 'Name of Provider').pluck(:provider_rental_id)
new_rental_ids = rental_ids - existing_rental_ids
new_rental_ids.each do |rental_id|
Rental.create(
provider: 'Name of Provider',
provider_rental_id: rental_id,
...
or if you are using rails 6 you can check upsert_all
Note: It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Additionally try to move this into a background cronjob

Why am I getting a 401 when trying to connect to an external API in my Rails app?

I'm trying to make a call to the Open Weather Map API using a a gem by the same name. I'm trying to fetch the current forecast in a city. I've looked at their FAQ but can't work it out. When I try and load http://localhost:3000/forecasts/berlin_weather this is returned: Berlin forecast: {"cod"=>401, "message"=>"Invalid API key. Please see http://openweathermap.org/faq#error401 for more info."} Which suggests it is connecting, but not sure why I am not getting the response I expected. Here is my code.
services/open_weather_api.rb
class OpenWeatherApi
require 'open_weather'
def initialize(city, appid = "blahblahblah", units = "metric")
#options = { city: city, units: units, APPID: appid }
end
def berlin_forecast
OpenWeather::Forecast.city(#options)
end
end
forecasts_controller.rb
class ForecastsController < ApplicationController
def berlin_weather
#forecast = OpenWeatherApi.new("Berlin").berlin_forecast
end
end
berlin_weather.html.erb
<p>Berlin forecast: <%= #forecast %></p>
Before making a request to API, you should to register and get API KEY from them!
Follow the instruction here!
https://openweathermap.org/appid
Then when they will provide a key for you, you can use this key in your request!
def initialize(city, appid = "API KEY WHAT THEY WILL GIVE YOU", units="metric")
#options = { city: city, units: units, APPID: appid }
end
require 'open_weather'
# get current weather by city name
OpenWeather::Current.city("Cochin, IN", options)
You need to provide country code as well.
Code sample taken from open_weather ruby wrapper github page.
Edit
https://openweathermap.org/data/2.5/forecast/daily/?appid=b6907d289e10d714blah&id=1298824&units=metric
I believe the api key that send to your mail during registration was wrong. You have to inspect the network call in openweathermap.org to get the real api key

Rails: Action Cable: How to authorize user to connect specific channel based on role?

In my rails auction app, authorized users can connect to 2 channels simultaneously within the product page (one is all_users channel for the product, the other is user specific channel for direct messaging.)
Now I would like to send sensitive information only to admin group users. I tought I can define a third channel connection request (admin_channel) in the coffee script but I couldn't figured out how I can authorize user connection for the 3rd channel based on role.
Another alternative might be utilizing the existing user specific channel but here I couldn't figured out how backend classes may know which users in the admin group currently is online (has a user channel up&running)..
Do you have any idea how I can achieve it? Any kind of support will be appreciated..
Below you can find my existing connection.rb file and coffeescript files.
Here is my connection.rb file:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
coffee script:
$( document ).ready ->
App.myauction = App.cable.subscriptions.create({
channel: 'MyauctionChannel'
id: $('#auctionID').attr('data-id')
},
connected: ->
console.log "Connected"
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
speak: (message) ->
#perform 'speak', message: message
received: (data) ->
console.log(data)
# Called when there's incoming data on the websocket for this channel
)
App.myauctionuser = App.cable.subscriptions.create({
channel: 'MyauctionChannel',
id: $('#auctionID').attr('data-uuid-code')
},
connected: ->
console.log "user connected"
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
speak: (message) ->
#perform 'speak', message: message
received: (data) ->
# console.log ("user channel ")
# console.log(data)
)
$(document).ready ->
App.privateAdminMesssagesChannel = App.cable.subscriptions.create({
channel: 'PrivateAdminMessagesChannel'
},
connected: ->
disconnected: ->
// call this function to send a message from a Non-Admin to All Admins
sendMessageToAdmins: (message) ->
#perform 'send_messsage_to_admins', message: message
// call this function to send a messsage from an Admin to (a Non-admin + all Admins)
sendMessageToUserAndAdmins: (message, toUserId) ->
#perform 'send_messsage_to_user_and_admins', message: message, to_user_id: toUserId
received: (data) ->
console.log(data.from_user_id)
console.log(data.to_user_id)
console.log(data.message)
if data.to_user_id
// this means the message was sent from an Admin to (a Non-admin + all admins)
else
// this means the message was sent from a Non-admin to All Admins
// do some logic here i.e. if current user is an admin, open up one Chatbox
// on the page for each unique `from_user_id`, and put data.message
// in that box accordingly
)
private_admin_messages_channel.rb
class PrivateAdminMessagesChannel < ActionCable::Channel::Base
def subscribed
stream_from :private_admin_messages_channel, coder: ActiveSupport::JSON do |data|
from_user = User.find(data.fetch('from_user_id'))
to_user = User.find(data['to_user_id']) if data['to_user_id']
message = data.fetch('message')
# authorize if "message" is sent to you (a non-admin), and also
# authorize if "message" is sent to you (an admin)
if (to_user && to_user == current_user) || (!to_user && current_user.is_admin?)
# now, finally send the Hash data below and transmit it to the client to be received in the JS-side "received(data)" callback
transmit(
from_user_id: from_user.id,
to_user_id: to_user&.id,
message: message
)
end
end
end
def send_message_to_admins(data)
ActionCable.server.broadcast 'private_admin_messages_channel',
from_user_id: current_user.id,
message: data.fetch('message')
end
def send_message_to_user_and_admins(data)
from_user = current_user
reject unless from_user.is_admin?
ActionCable.server.broadcast 'private_admin_messages_channel',
from_user_id: from_user.id,
to_user_id: data.fetch('to_user_id'),
message: data.fetch('message')
end
end
Above is the easiest way I could think of. Not the most efficient one because there's an extra level of authorization happening per stream (see inside stream_from block) unlike if we have different broadcast-names of which the authorization would happen only once on the "connecting" itself, and not each "streaming"... which can be done via something like:
Admin User1 opens page then JS-subscribes to UserConnectedChannel
Non-admin User2 opens page then JS-subscribes to PrivateAdminMessagesChannel passing in data: user_id: CURRENT_USER_ID
From 2. above, as User2 has just subscribed; then on the backend, inside def subscribed upon connection, you ActionCable.server.broadcast :user_connected, { user_id: current_user.id }
Admin User1 being subscribed to UserConnectedChannel then receives with data { user_id: THAT_USER2_id }
From 4 above, inside the JS received(data) callback, you then now JS-subscribe to PrivateAdminMessagesChannel passing in data: THAT_USER2_id`.
Now User1 and User2 are both subscribed to PrivateAdminMessagesChannel user_id: THAT_USER2_id which means that they can privately talk with each other (other admins should also have had received :user_connected's JS data: { user_id: THAT_USER2_ID }, and so they should also be subscribed as well, because it makes sense that AdminUser1, NonAdminUser2, and AdminUser3 can talk in the same chat channel... from what I was getting with your requirements)
TODO: From 1 to 6 above, do something similar also with the "disconnection" process
Trivias:
Those you define with identified_by in your ApplicationCable::Connection can be acceessed in your channel files. In particular, in this case, current_user can be called.
Regarding, rejecting subscriptions, see docs here

Action Cable (Rails 5) Pulling data from Stock Exchange

I am hoping to stream data that I receive from a stock exchange (via a websocket) and broadcast that data to the client. I believe action cable is the best approach, but have been unsuccessful thus far.
The "DataProvider" built an SDK on Ruby to create the connection between my server and the stock-exchange server. I have been able to successfully connect, but have not seen data stream in.
Any suggestions?
Channel:
class StockPriceChannel < ApplicationCable::Channel
def subscribed
stream_from "portfolio_#{params[:portfolio_id]}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
EventMachine.stop()
end
def receive(data)
tickers = data["message"]
tickers = tickers.split(",")
puts tickers
options = {
username: ENV["DataProvider_USERNAME"],
password: ENV["DataProvider_PASSWORD"],
channels: tickers
}
EventMachine.run do
client = DataProvider::Realtime::Client.new(options)
client.on_quote do |quote|
StockPriceChannel.server.broadcast_to("portfolio_#
{params[:portfolio_id]}",quote)
end
client.connect()
EventMachine.add_timer(1) do
puts "joining tickers"
client.join(tickers)
end
# EventMachine.add_timer(10) do
# client.disconnect()
# EventMachine.stop()
# end
end
end
end
ReactJS (in componentDidMount() )
App.portfolioChannel = App.cable.subscriptions.create(
{
channel:"StockPriceChannel",
portfolio_id:this.props.match.params.port_id
},
{
connected: () => console.log("StockPriceChannel connected"),
disconnected: () => console.log("StockPriceChannel disconnected"),
received: data => {
debugger;
console.log(data)
}
}
)
App.portfolioChannel.send({
message:"AAPL,MSFT"
})
Well... I hope this (at the very least) helps some one else.
I realized my issue was
StockPriceChannel.server.broadcast_to("portfolio_#
{params[:portfolio_id]}",quote)
should be
ActionCable.server.broadcast_to("portfolio_#
{params[:portfolio_id]}",quote)

How can I test unpaid subscriptions in Stripe with Ruby on Rails?

I'm trying to test the scenario in my Rails app where a customer has allowed a subscription to go to 'unpaid' (usually because the card expired and was not updated during the two weeks while Stripe retried the charge) and is finally getting around to updating the card and reactivating the account. I believe I have the logic correct (update the card and pay each unpaid invoice) but I'd like to be able to test it, or better yet, write some RSpec tests (a feature test and possibly a controller test). The problem is, I can't figure out how to create a mock situation in which a subscription is 'unpaid'. (I suppose I could create a bunch of accounts with expired cards and wait two weeks to test, but that's not an acceptable solution. I can't even change the subscription retry settings for only the 'test' context to speed up the process.) I found stripe-ruby-mock but I can't manually set the status of a subscription.
Here's what I've tried:
plan = Stripe::Plan.create(id: 'test')
customer = Stripe::Customer.create(id: 'test_customer', card: 'tk', plan: 'test')
sub = customer.subscriptions.retrieve(customer.subscriptions.data.first.id)
sub.status = 'unpaid'
sub.save
sub = customer.subscriptions.retrieve(customer.subscriptions.data.first.id)
expect(sub.status).to eq 'unpaid'
This was the result with stripe-ruby-mock:
Failure/Error: expect(sub.status).to eq 'unpaid'
expected: "unpaid"
got: "active"
Stripe's recommended procedure is:
Setup the customer with the card 4000000000000341 ("Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.")
Give the customer a subscription but with a trial date ending today/tomorrow.
Wait
Step three is annoying, but it'll get the job done.
Taking #VoteyDisciple's suggestion, I've worked through something to have this reasonably automated in RSpec ('reasonably', given the circumstances). I'm using VCR to capture the API calls (against the test Stripe environment), which is essential, as it means the sleep call only happens when recording the test the first time.
Using comments to indicate behaviour done via Stripe's API, as my implementation is rather caught up in my project, and that's not so useful.
VCR.use_cassette('retry') do |cassette|
# Clear out existing Stripe data: customers, coupons, plans.
# This is so the test is reliably repeatable.
Stripe::Customer.all.each &:delete
Stripe::Coupon.all.each &:delete
Stripe::Plan.all.each &:delete
# Create a plan, in my case it has the id 'test'.
Stripe::Plan.create(
id: 'test',
amount: 100_00,
currency: 'AUD',
interval: 'month',
interval_count: 1,
name: 'RSpec Test'
)
# Create a customer
customer = Stripe::Customer.create email: 'test#test.test'
token = card_token cassette, '4000000000000341'
# Add the card 4000000000000341 to the customer
customer.sources.create token: 'TOKEN for 0341'
# Create a subscription with a trial ending in two seconds.
subscription = customer.subscriptions.create(
plan: 'test',
trial_end: 2.seconds.from_now.to_i
)
# Wait for Stripe to create a proper invoice. I wish this
# was unavoidable, but I don't think it is.
sleep 180 if cassette.recording?
# Grab the invoice that actually has a dollar value.
# There's an invoice for the trial, and we don't care about that.
invoice = customer.invoices.detect { |invoice| invoice.total > 0 }
# Force Stripe to attempt payment for the first time (instead
# of waiting for hours).
begin
invoice.pay
rescue Stripe::CardError
# Expecting this to fail.
end
invoice.refresh
expect(invoice.paid).to eq(false)
expect(invoice.attempted).to eq(true)
# Add a new (valid) card to the customer.
token = card_token cassette, '4242424242424242'
card = customer.sources.create token: token
# and set it as the default
customer.default_source = card.id
customer.save
# Run the code in your app that retries the payment, which
# essentially invokes invoice.pay again.
# THIS IS FOR YOU TO IMPLEMENT
# And now we can check that the invoice wass successfully paid
invoice.refresh
expect(invoice.paid).to eq(true)
end
Getting card tokens in an automated fashion is a whole new area of complexity. What I've got may not work for others, but here's the ruby method, calling out to phantomjs (you'll need to send the VCR cassette object through):
def card_token(cassette, card = '4242424242424242')
return 'tok_my_default_test_token' unless cassette.recording?
token = `phantomjs --ignore-ssl-errors=true --ssl-protocol=any ./spec/fixtures/stripe_tokens.js #{ENV['STRIPE_PUBLISH_KEY']} #{card}`.strip
raise "Unexpected token: #{token}" unless token[/^tok_/]
token
end
And the javascript file that phantomjs is running (stripe_tokens.js) contains:
var page = require('webpage').create(),
system = require('system');
var key = system.args[1],
card = system.args[2];
page.onCallback = function(data) {
console.log(data);
phantom.exit();
};
page.open('spec/fixtures/stripe_tokens.html', function(status) {
if (status == 'success') {
page.evaluate(function(key, card) {
Stripe.setPublishableKey(key);
Stripe.card.createToken(
{number: card, cvc: "123", exp_month: "12", exp_year: "2019"},
function(status, response) { window.callPhantom(response.id) }
);
}, key, card);
}
});
Finally, the HTML file involved (stripe_tokens.html) is pretty simple:
<html>
<head>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
</head>
<body></body>
</html>
Put all of that together, and, well, it might work! It does for our app :)

Resources