Embed Twitch Live Streaming (#2591)
* Get a job created that can create a webhook subscription for a twitch user login * Remove ngork url * Refactor add store the access token in the cache * Get a controller stood up to recieve the webhooks. Now they just need to be processed * Get User columns added and got webhook controller bones working * Update the webhook job to use the User * Add a way for the User to input their Twitch User Name. Plus a linter fix * Delay webhook registration when profile is updated * Don't add _ in username * Use String columns * Quick fix and add some more requests specs * Specs for the webhook job * Get a show page Twitch Live Streams. Just a straight embed of the Twitch Everything Embed UI. Works surprisingly well responsively, and works on all screen sizes * Fix Gemfile.lock from merge issues * Add support for expired tokens and add spec * Add secrets to webhook registration and clean up spec to remove token logic * Verify webhook secret and spec it * Add rake task to enqueue webhook registration for all Users. This can be used from Heroku Scheduler * Update the lease seconds to be for 5 days * Actually lets do 7 so we can refresh twice a week and try to make sure that we can always miss one * Hijack the existing Twitch logo instead of making a duplicate one * Remove comment and replace with log line * Remove some white space * Log to Airbrake when webhook errors occur * Move to passing in an id instead of User object * Extract logic from Job to Service object * Capitilize in the view * Move out of models and into services * Remove letover stub * Remove one usage of Faraday * Use HTTParty for all the HTTP here
This commit is contained in:
parent
5b0340351b
commit
ec388804df
24 changed files with 498 additions and 5 deletions
5
Envfile
5
Envfile
|
|
@ -144,6 +144,11 @@ variable :TWILIO_ACCOUNT_SID, :String, default: "Optional"
|
|||
variable :TWILIO_VIDEO_API_KEY, :String, default: "Optional"
|
||||
variable :TWILIO_VIDEO_API_SECRET, :String, default: "Optional"
|
||||
|
||||
# For Twitch live stream integration
|
||||
variable :TWITCH_CLIENT_ID, :String, default: "Optional"
|
||||
variable :TWITCH_CLIENT_SECRET, :String, default: "Optional"
|
||||
variable :TWITCH_WEBHOOK_SECRET, :String, default: "Optional"
|
||||
|
||||
# Trending tags on home page
|
||||
variable :TRENDING_TAGS, :String, default: "git,beginners"
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ body {
|
|||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.ptr--ptr{
|
||||
|
|
@ -38,7 +38,7 @@ body {
|
|||
overflow:hidden;
|
||||
min-height:88vh;
|
||||
visibility: visible;
|
||||
&.stories-index,&.notifications-index,&.stories-search,&.podcast_episodes-index,&.reading_list_items-index,.tags-index{
|
||||
&.stories-index,&.notifications-index,&.stories-search,&.podcast_episodes-index,&.reading_list_items-index,.tags-index,&.twitch_live_streams-show{
|
||||
margin-top:68px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
app/controllers/twitch_live_streams_controller.rb
Normal file
12
app/controllers/twitch_live_streams_controller.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class TwitchLiveStreamsController < ApplicationController
|
||||
before_action :set_cache_control_headers
|
||||
|
||||
def show
|
||||
@user = User.find_by!(username: params[:username].tr("@", "").downcase)
|
||||
if @user.twitch_username.present?
|
||||
render :show
|
||||
else
|
||||
render :no_twitch
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/controllers/twitch_stream_updates_controller.rb
Normal file
44
app/controllers/twitch_stream_updates_controller.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
class TwitchStreamUpdatesController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
def show
|
||||
if params["hub.mode"] == "denied"
|
||||
airbrake_logger.error("Twitch Webhook was denied: #{params.permit('hub.mode', 'hub.reason', 'hub.topic').to_json}")
|
||||
head :no_content
|
||||
else
|
||||
render plain: params["hub.challenge"]
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
head :no_content
|
||||
|
||||
unless secret_verified?
|
||||
airbrake_logger.warn("Twitch Webhook Recieved for which the webhook could not be verified")
|
||||
return
|
||||
end
|
||||
|
||||
user = User.find(params[:user_id])
|
||||
|
||||
if params[:data].first.present?
|
||||
user.update!(currently_streaming_on: :twitch)
|
||||
else
|
||||
user.update!(currently_streaming_on: nil)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def airbrake_logger
|
||||
Airbrake::AirbrakeLogger.new(Rails.logger)
|
||||
end
|
||||
|
||||
def secret_verified?
|
||||
twitch_sha = request.headers["x-hub-signature"]
|
||||
digest = Digest::SHA256.new
|
||||
digest << ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
|
||||
digest << request.raw_post
|
||||
|
||||
twitch_sha == "sha256=#{digest.hexdigest}"
|
||||
end
|
||||
end
|
||||
|
|
@ -24,6 +24,7 @@ class UsersController < ApplicationController
|
|||
# raise permitted_attributes(@user).to_s
|
||||
if @user.update(permitted_attributes(@user))
|
||||
RssReader.new.delay.fetch_user(@user) if @user.feed_url.present?
|
||||
Streams::TwitchWebhookRegistrationJob.perform_later(@user.id) if @user.twitch_username.present?
|
||||
notice = "Your profile was successfully updated."
|
||||
if @user.export_requested?
|
||||
notice += " The export will be emailed to you shortly."
|
||||
|
|
|
|||
12
app/jobs/streams/twitch_webhook_registration_job.rb
Normal file
12
app/jobs/streams/twitch_webhook_registration_job.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
module Streams
|
||||
class TwitchWebhookRegistrationJob < ApplicationJob
|
||||
queue_as :twitch_webhook_registration
|
||||
|
||||
def perform(user_id, service = TwitchWebhook::Register)
|
||||
user = User.find_by(id: user_id)
|
||||
return if user.blank? || user.twitch_username.blank?
|
||||
|
||||
service.call(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -130,6 +130,7 @@ class User < ApplicationRecord
|
|||
validates :mentee_description, :mentor_description,
|
||||
length: { maximum: 1000 }
|
||||
validates :inbox_type, inclusion: { in: %w[open private] }
|
||||
validates :currently_streaming_on, inclusion: { in: %w[twitch] }, allow_nil: true
|
||||
validate :conditionally_validate_summary
|
||||
validate :validate_mastodon_url
|
||||
validate :validate_feed_url, if: :feed_url_changed?
|
||||
|
|
@ -145,7 +146,7 @@ class User < ApplicationRecord
|
|||
before_update :mentorship_status_update
|
||||
before_validation :set_username
|
||||
# make sure usernames are not empty, to be able to use the database unique index
|
||||
before_validation :verify_twitter_username, :verify_github_username, :verify_email
|
||||
before_validation :verify_twitter_username, :verify_github_username, :verify_email, :verify_twitch_username
|
||||
before_validation :set_config_input
|
||||
before_validation :downcase_email
|
||||
before_validation :check_for_username_change
|
||||
|
|
@ -400,6 +401,14 @@ class User < ApplicationRecord
|
|||
roles.where(name: "tag_moderator").any?
|
||||
end
|
||||
|
||||
def currently_streaming?
|
||||
currently_streaming_on.present?
|
||||
end
|
||||
|
||||
def currently_streaming_on_twitch?
|
||||
currently_streaming_on == "twitch"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_welcome_notification
|
||||
|
|
@ -418,6 +427,10 @@ class User < ApplicationRecord
|
|||
self.email = nil if email == ""
|
||||
end
|
||||
|
||||
def verify_twitch_username
|
||||
self.twitch_username = nil if twitch_username == ""
|
||||
end
|
||||
|
||||
def set_username
|
||||
set_temp_username if username.blank?
|
||||
self.username = username&.downcase
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ class UserPolicy < ApplicationPolicy
|
|||
summary
|
||||
text_color_hex
|
||||
twitch_url
|
||||
twitch_username
|
||||
username
|
||||
website_url
|
||||
export_requested]
|
||||
|
|
|
|||
35
app/services/streams/twitch_access_token/get.rb
Normal file
35
app/services/streams/twitch_access_token/get.rb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
module Streams
|
||||
module TwitchAccessToken
|
||||
class Get
|
||||
ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY = :twitch_access_token_with_expiration
|
||||
def self.call
|
||||
new.call
|
||||
end
|
||||
|
||||
def call
|
||||
token, exp = Rails.cache.fetch(ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY)
|
||||
|
||||
if token.nil? || Time.zone.now >= exp
|
||||
token, exp = get_new_token
|
||||
Rails.cache.write(ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY, [token, exp])
|
||||
end
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_new_token
|
||||
resp = HTTParty.post(
|
||||
"https://id.twitch.tv/oauth2/token",
|
||||
body: {
|
||||
client_id: ApplicationConfig["TWITCH_CLIENT_ID"],
|
||||
client_secret: ApplicationConfig["TWITCH_CLIENT_SECRET"],
|
||||
grant_type: :client_credentials
|
||||
},
|
||||
)
|
||||
[resp["access_token"], resp["expires_in"].seconds.from_now]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/services/streams/twitch_webhook/register.rb
Normal file
49
app/services/streams/twitch_webhook/register.rb
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
module Streams
|
||||
module TwitchWebhook
|
||||
class Register
|
||||
WEBHOOK_LEASE_SECONDS = 7.days.to_i
|
||||
|
||||
def initialize(user, access_token_service = TwitchAccessToken::Get)
|
||||
@user = user
|
||||
@access_token_service = access_token_service
|
||||
end
|
||||
|
||||
def self.call(*args)
|
||||
new(*args).call
|
||||
end
|
||||
|
||||
def call
|
||||
user_resp = HTTParty.get("https://api.twitch.tv/helix/users", query: { login: user.twitch_username }, headers: authentication_request_headers)
|
||||
twitch_user_id = user_resp["data"].first["id"]
|
||||
|
||||
HTTParty.post(
|
||||
"https://api.twitch.tv/helix/webhooks/hub",
|
||||
body: webhook_request_body(twitch_user_id),
|
||||
headers: authentication_request_headers,
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :access_token_service
|
||||
|
||||
def webhook_request_body(twitch_user_id)
|
||||
{
|
||||
"hub.callback" => twitch_stream_updates_url_for_user(user),
|
||||
"hub.mode" => "subscribe",
|
||||
"hub.lease_seconds" => WEBHOOK_LEASE_SECONDS,
|
||||
"hub.topic" => "https://api.twitch.tv/helix/streams?user_id=#{twitch_user_id}",
|
||||
"hub.secret" => ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def authentication_request_headers
|
||||
{ "Authorization" => "Bearer #{access_token_service.call}" }
|
||||
end
|
||||
|
||||
def twitch_stream_updates_url_for_user(user)
|
||||
Rails.application.routes.url_helpers.user_twitch_stream_updates_url(user_id: user.id, host: ApplicationConfig["APP_DOMAIN"])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3
app/views/twitch_live_streams/no_twitch.html.erb
Normal file
3
app/views/twitch_live_streams/no_twitch.html.erb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<div class="home">
|
||||
This user does not use Twitch
|
||||
</div>
|
||||
21
app/views/twitch_live_streams/show.html.erb
Normal file
21
app/views/twitch_live_streams/show.html.erb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<div class="home" id="twitch-embed">
|
||||
</div>
|
||||
|
||||
<script id="twitch-embed-js" src="https://embed.twitch.tv/embed/v1.js"></script>
|
||||
<script type="text/javascript">
|
||||
function load_twitch() {
|
||||
new Twitch.Embed("twitch-embed", {
|
||||
width: "100%",
|
||||
height: "600px",
|
||||
channel: "<%= @user.twitch_username %>"
|
||||
});
|
||||
}
|
||||
|
||||
var scr = document.createElement('script'),
|
||||
head = document.head || document.getElementsByTagName('head')[0];
|
||||
scr.src = 'https://embed.twitch.tv/embed/v1.js';
|
||||
scr.onload = load_twitch;
|
||||
scr.async = true;
|
||||
|
||||
head.insertBefore(scr, head.firstChild);
|
||||
</script>
|
||||
|
|
@ -150,6 +150,11 @@
|
|||
<%= f.label :twitch_url, "Twitch URL" %>
|
||||
<%= f.url_field :twitch_url %>
|
||||
</div>
|
||||
<p><strong>Streaming</strong></p>
|
||||
<div class="field">
|
||||
<%= f.label :twitch_username, "Twitch User Name" %>
|
||||
<%= f.text_field :twitch_username %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label></label>
|
||||
<%= f.hidden_field :tab, value: @tab %>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,11 @@
|
|||
<%= inline_svg("gitlab.svg", class: "icon-img") %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% if @user.twitch_url? %>
|
||||
<% if @user.twitch_username? %>
|
||||
<%= link_to twitch_live_stream_path(username: @user.username) do %>
|
||||
<%= inline_svg("twitch-logo.svg", class: "icon-img", id: "icon-twitch") %>
|
||||
<% end %>
|
||||
<% elsif @user.twitch_url? %>
|
||||
<a href="<%= @user.twitch_url %>" target="_blank" rel="noopener nofollow me">
|
||||
<%= inline_svg("twitch-logo.svg", class: "icon-img") %>
|
||||
</a>
|
||||
|
|
@ -139,6 +143,15 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @user.currently_streaming_on_twitch? %>
|
||||
<style>
|
||||
#icon-twitch path {
|
||||
fill: green !important;
|
||||
stroke: green !important;
|
||||
}
|
||||
</style>
|
||||
<% end %>
|
||||
|
||||
<div class="home sub-home" id="index-container"
|
||||
data-params="<%= params.to_json(only: %i[tag username q]) %>" data-which="<%= @list_of %>"
|
||||
data-algolia-tag="<%= "user_#{@user.id}" %>"
|
||||
|
|
|
|||
|
|
@ -108,7 +108,10 @@ Rails.application.routes.draw do
|
|||
resources :article_mutes, only: %i[update]
|
||||
resources :comments, only: %i[create update destroy]
|
||||
resources :comment_mutes, only: %i[update]
|
||||
resources :users, only: [:update]
|
||||
resources :users, only: [:update] do
|
||||
resource :twitch_stream_updates, only: %i[show create]
|
||||
end
|
||||
resources :twitch_live_streams, only: :show, param: :username
|
||||
resources :reactions, only: %i[index create]
|
||||
resources :feedback_messages, only: %i[index create]
|
||||
get "/reports/:slug", to: "feedback_messages#show"
|
||||
|
|
|
|||
|
|
@ -15,3 +15,7 @@ PUSHER_APP_ID:
|
|||
PUSHER_KEY:
|
||||
PUSHER_SECRET:
|
||||
PUSHER_CLUSTER:
|
||||
|
||||
TWITCH_CLIENT_ID:
|
||||
TWITCH_CLIENT_SECRET:
|
||||
TWITCH_WEBHOOK_SECRET:
|
||||
|
|
|
|||
8
db/migrate/20190420000607_add_twitch_columns_to_user.rb
Normal file
8
db/migrate/20190420000607_add_twitch_columns_to_user.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class AddTwitchColumnsToUser < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
change_table :users do |t|
|
||||
t.string :twitch_username, index: true, unique: true
|
||||
t.string :currently_streaming_on
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This file is auto-generated from the current state of the database. Instead
|
||||
# of editing this file, please use the migrations feature of Active Record to
|
||||
# incrementally modify your database, and then regenerate this schema definition.
|
||||
|
|
@ -809,6 +811,7 @@ ActiveRecord::Schema.define(version: 2019_04_25_210432) do
|
|||
t.inet "current_sign_in_ip"
|
||||
t.string "currently_hacking_on"
|
||||
t.string "currently_learning"
|
||||
t.string "currently_streaming_on"
|
||||
t.boolean "display_sponsors", default: true
|
||||
t.string "dribbble_url"
|
||||
t.string "editor_version", default: "v1"
|
||||
|
|
@ -920,6 +923,7 @@ ActiveRecord::Schema.define(version: 2019_04_25_210432) do
|
|||
t.string "text_only_name"
|
||||
t.string "top_languages"
|
||||
t.string "twitch_url"
|
||||
t.string "twitch_username"
|
||||
t.datetime "twitter_created_at"
|
||||
t.integer "twitter_followers_count"
|
||||
t.integer "twitter_following_count"
|
||||
|
|
|
|||
8
lib/tasks/twitch.rake
Normal file
8
lib/tasks/twitch.rake
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace :twitch do
|
||||
desc "Register for Webhooks for all User's Registered with Twitch"
|
||||
task wehbook_register_all: :environment do
|
||||
User.where.not(twitch_username: nil).find_each do |user|
|
||||
Streams::TwitchWebhookRegistrationJob.perform_later(user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
33
spec/jobs/streams/twitch_webhook_registration_job_spec.rb
Normal file
33
spec/jobs/streams/twitch_webhook_registration_job_spec.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Streams::TwitchWebhookRegistrationJob, type: :job do
|
||||
let(:user) { create(:user, twitch_username: "test-username") }
|
||||
|
||||
let(:service) { double }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:call)
|
||||
end
|
||||
|
||||
context "when the user does NOT have a twitch username present" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "noops" do
|
||||
described_class.perform_now(user.id, service)
|
||||
|
||||
expect(service).not_to have_received(:call)
|
||||
end
|
||||
end
|
||||
|
||||
it "noops when the id passed does not belong to a user" do
|
||||
described_class.perform_now(987_654_321, service)
|
||||
|
||||
expect(service).not_to have_received(:call)
|
||||
end
|
||||
|
||||
it "registers for webhooks" do
|
||||
described_class.perform_now(user.id, service)
|
||||
|
||||
expect(service).to have_received(:call).with(user)
|
||||
end
|
||||
end
|
||||
|
|
@ -57,6 +57,8 @@ VCR.configure do |config|
|
|||
)
|
||||
end
|
||||
|
||||
RSpec::Matchers.define_negated_matcher :not_change, :change
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.use_transactional_fixtures = true
|
||||
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
||||
|
|
|
|||
127
spec/requests/twitch_stream_updates_spec.rb
Normal file
127
spec/requests/twitch_stream_updates_spec.rb
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "TwitchStramUpdates", type: :request do
|
||||
let(:user) { create(:user, twitch_username: "my-twtich-username", currently_streaming_on: currently_streaming_on) }
|
||||
let(:currently_streaming_on) { nil }
|
||||
|
||||
describe "GET /users/:user_id/twitch_stream_updates" do
|
||||
context "when the subscription was successfull" do
|
||||
let(:challenge) { "FAKE_CHALLENGE" }
|
||||
let(:twitch_webhook_subscription_params) do
|
||||
{
|
||||
"hub.mode" => "subscribe",
|
||||
"hub.topic" => "SOME_TOPIC_URL",
|
||||
"hub.lease_seconds" => "864000",
|
||||
"hub.challenge" => challenge
|
||||
}
|
||||
end
|
||||
|
||||
it "returns the challenge" do
|
||||
get "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_subscription_params
|
||||
|
||||
expect(response.body).to eq challenge
|
||||
end
|
||||
end
|
||||
|
||||
context "when the subscription is denied" do
|
||||
let(:twitch_webhook_subscription_params) do
|
||||
{
|
||||
"hub.mode" => "denied",
|
||||
"hub.topic" => "SOME_TOPIC_URL",
|
||||
"hub.reason" => "unauthorized"
|
||||
}
|
||||
end
|
||||
|
||||
it "returns a 204 and logs" do
|
||||
get "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_subscription_params
|
||||
|
||||
expect(response.status).to eq 204
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /users/:user_id/twitch_stream_updates" do
|
||||
context "when the user was not streaming and starts streaming" do
|
||||
let(:currently_streaming_on) { nil }
|
||||
|
||||
let(:twitch_webhook_params) do
|
||||
{
|
||||
data: [{
|
||||
id: "0123456789",
|
||||
user_id: "5678",
|
||||
user_name: "wjdtkdqhs",
|
||||
game_id: "21779",
|
||||
community_ids: [],
|
||||
type: "live",
|
||||
title: "Best Stream Ever",
|
||||
viewer_count: 417,
|
||||
started_at: "2017-12-01T10:09:45Z",
|
||||
language: "en",
|
||||
thumbnail_url: "https://link/to/thumbnail.jpg"
|
||||
}]
|
||||
}
|
||||
end
|
||||
let(:twitch_webhook_secret_sha) do
|
||||
digest = Digest::SHA256.new
|
||||
digest << ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
|
||||
digest << twitch_webhook_params.to_json
|
||||
"sha256=#{digest.hexdigest}"
|
||||
end
|
||||
|
||||
it "updates the Users twitch streaming status" do
|
||||
expect { post "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_params.to_json, headers: { "Content-Type" => "application/json", "X-Hub-Signature" => twitch_webhook_secret_sha } }.
|
||||
to change { user.reload.currently_streaming? }.from(false).to(true).
|
||||
and change { user.reload.currently_streaming_on_twitch? }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the webhook secret was NOT verified" do
|
||||
let(:twitch_webhook_params) do
|
||||
{
|
||||
data: [{
|
||||
id: "0123456789",
|
||||
user_id: "5678",
|
||||
user_name: "wjdtkdqhs",
|
||||
game_id: "21779",
|
||||
community_ids: [],
|
||||
type: "live",
|
||||
title: "Best Stream Ever",
|
||||
viewer_count: 417,
|
||||
started_at: "2017-12-01T10:09:45Z",
|
||||
language: "en",
|
||||
thumbnail_url: "https://link/to/thumbnail.jpg"
|
||||
}]
|
||||
}
|
||||
end
|
||||
let(:twitch_webhook_secret_sha) { "sha256=BAD_HASH" }
|
||||
|
||||
it "noops" do
|
||||
expect { post "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_params.to_json, headers: { "Content-Type" => "application/json", "X-Hub-Signature" => twitch_webhook_secret_sha } }.
|
||||
to not_change { user.reload.currently_streaming? }.from(false).
|
||||
and not_change { user.reload.currently_streaming_on_twitch? }.from(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the user was streaming and stops" do
|
||||
let(:currently_streaming_on) { :twitch }
|
||||
|
||||
let(:twitch_webhook_params) do
|
||||
{
|
||||
data: []
|
||||
}
|
||||
end
|
||||
let(:twitch_webhook_secret_sha) do
|
||||
digest = Digest::SHA256.new
|
||||
digest << ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
|
||||
digest << twitch_webhook_params.to_json
|
||||
"sha256=#{digest.hexdigest}"
|
||||
end
|
||||
|
||||
it "updates the Users twitch streaming status" do
|
||||
expect { post "/users/#{user.id}/twitch_stream_updates", params: twitch_webhook_params.to_json, headers: { "Content-Type" => "application/json", "X-Hub-Signature" => twitch_webhook_secret_sha } }.
|
||||
to change { user.reload.currently_streaming? }.from(true).to(false).
|
||||
and change { user.reload.currently_streaming_on_twitch? }.from(true).to(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
50
spec/services/streams/twitch_access_token/get_spec.rb
Normal file
50
spec/services/streams/twitch_access_token/get_spec.rb
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Streams::TwitchAccessToken::Get, type: :service do
|
||||
describe ".access_token" do
|
||||
let(:expected_twitch_token_body) do
|
||||
{
|
||||
client_id: "FAKE_TWITCH_CLIENT_ID",
|
||||
client_secret: "FAKE_TWITCH_CLIENT_SECRET",
|
||||
grant_type: "client_credentials"
|
||||
}
|
||||
end
|
||||
let!(:twitch_token_stubbed_route) do
|
||||
stub_request(:post, "https://id.twitch.tv/oauth2/token").
|
||||
with(body: expected_twitch_token_body).
|
||||
and_return(body: { access_token: "FAKE_BRAND_NEW_TWITCH_TOKEN", expires_in: 5_184_000 }.to_json, headers: { "Content-Type" => "application/json" })
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ApplicationConfig).to receive(:[]).and_call_original
|
||||
allow(ApplicationConfig).to receive(:[]).with("TWITCH_CLIENT_ID").and_return("FAKE_TWITCH_CLIENT_ID")
|
||||
allow(ApplicationConfig).to receive(:[]).with("TWITCH_CLIENT_SECRET").and_return("FAKE_TWITCH_CLIENT_SECRET")
|
||||
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache.lookup_store(:memory_store))
|
||||
end
|
||||
|
||||
context "when there is an unexpired token in the cache" do
|
||||
it "returns the cached token" do
|
||||
Rails.cache.write(described_class::ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY, ["FAKE_UNEXPIRED_TWITCH_TOKEN", 15.days.from_now])
|
||||
|
||||
expect(described_class.call).to eq "FAKE_UNEXPIRED_TWITCH_TOKEN"
|
||||
expect(twitch_token_stubbed_route).not_to have_been_requested
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an expired token in the cache" do
|
||||
it "requests a new token and caches it" do
|
||||
Rails.cache.write(described_class::ACCESS_TOKEN_AND_EXPIRATION_CACHE_KEY, ["FAKE_EXPIRED_TWITCH_TOKEN", 15.days.ago])
|
||||
|
||||
expect(described_class.call).to eq "FAKE_BRAND_NEW_TWITCH_TOKEN"
|
||||
expect(twitch_token_stubbed_route).to have_been_requested
|
||||
end
|
||||
end
|
||||
|
||||
context "when the token is not in the cache" do
|
||||
it "requests a new token and caches it" do
|
||||
expect(described_class.call).to eq "FAKE_BRAND_NEW_TWITCH_TOKEN"
|
||||
expect(twitch_token_stubbed_route).to have_been_requested
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
40
spec/services/streams/twitch_webhook/register_spec.rb
Normal file
40
spec/services/streams/twitch_webhook/register_spec.rb
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Streams::TwitchWebhook::Register, type: :service do
|
||||
describe "::call" do
|
||||
let(:twitch_access_token_get) { instance_double(Streams::TwitchAccessToken::Get, call: "FAKE_TWITCH_TOKEN") }
|
||||
let(:user) { create(:user, twitch_username: "test-username") }
|
||||
|
||||
let(:expected_headers) do
|
||||
{ "Authorization" => "Bearer FAKE_TWITCH_TOKEN" }
|
||||
end
|
||||
let(:expected_twitch_webhook_params) do
|
||||
{
|
||||
"hub.callback" => "http://#{ApplicationConfig['APP_DOMAIN']}/users/#{user.id}/twitch_stream_updates",
|
||||
"hub.mode" => "subscribe",
|
||||
"hub.lease_seconds" => 604_800,
|
||||
"hub.topic" => "https://api.twitch.tv/helix/streams?user_id=654321",
|
||||
"hub.secret" => ApplicationConfig["TWITCH_WEBHOOK_SECRET"]
|
||||
}.to_json
|
||||
end
|
||||
let!(:twitch_webhook_registration_stubbed_route) do
|
||||
stub_request(:post, "https://api.twitch.tv/helix/webhooks/hub").
|
||||
with(body: expected_twitch_webhook_params, headers: expected_headers).
|
||||
and_return(status: 204)
|
||||
end
|
||||
|
||||
let(:expected_twitch_user_params) { { login: "test-username" } }
|
||||
let!(:twitch_user_stubbed_route) do
|
||||
stub_request(:get, "https://api.twitch.tv/helix/users").
|
||||
with(query: expected_twitch_user_params, headers: expected_headers).
|
||||
and_return(body: { data: [{ id: 654_321 }] }.to_json, headers: { "Content-Type" => "application/json" })
|
||||
end
|
||||
|
||||
it "registers for webhooks" do
|
||||
described_class.call(user, twitch_access_token_get)
|
||||
|
||||
expect(twitch_webhook_registration_stubbed_route).to have_been_requested
|
||||
expect(twitch_user_stubbed_route).to have_been_requested
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue