Integrazione YouTube Live: OAuth squadra, canale piattaforma Light e UI.

Collegamento da Dettagli squadra e admin per il refresh token Match Live TV; API e app mobile allineate ai piani Light/Full.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 19:36:56 +02:00
parent 1a84d6ae42
commit 994c1e3c09
28 changed files with 557 additions and 28 deletions

View File

@@ -4,7 +4,8 @@ module Youtube
def initialize(team)
@team = team
@credential = team.youtube_credential
@resolver = CredentialResolver.new(team)
@credential = @resolver.resolve
end
def create_broadcast!(title:, privacy_status: "unlisted", scheduled_at: nil)

View File

@@ -0,0 +1,18 @@
module Youtube
class ChannelInfo
def self.fetch(access_token:)
service = Google::Apis::YoutubeV3::YouTubeService.new
service.authorization = access_token
resp = service.list_channels("snippet", mine: true, max_results: 1)
item = resp.items&.first
return { channel_id: nil, channel_title: nil } unless item
{
channel_id: item.id,
channel_title: item.snippet&.title
}
rescue Google::Apis::Error
{ channel_id: nil, channel_title: nil }
end
end
end

View File

@@ -0,0 +1,24 @@
module Youtube
class CredentialResolver
def self.for_team(team)
new(team).resolve
end
def initialize(team)
@team = team
@mode = team.entitlements.plan.youtube_mode
end
def resolve
if @mode == "matchlivetv_light" && PlatformCredential.configured?
PlatformCredential.new
else
@team.youtube_credential
end
end
def uses_platform_channel?
@mode == "matchlivetv_light" && PlatformCredential.configured?
end
end
end

View File

@@ -13,12 +13,13 @@ module Youtube
data = JSON.parse(response.body)
raise BroadcastService::Error, data["error_description"] if data["error"]
info = ChannelInfo.fetch(access_token: data["access_token"])
{
access_token: data["access_token"],
refresh_token: data["refresh_token"],
expires_in: data["expires_in"].to_i,
channel_id: nil,
channel_title: nil
channel_id: info[:channel_id],
channel_title: info[:channel_title]
}
end
end

View File

@@ -0,0 +1,39 @@
module Youtube
class OauthState
PURPOSE = :youtube_oauth_connect
class << self
def for_team(team_id:, user_id:)
verifier.generate([team_id.to_s, user_id.to_s], expires_in: 1.hour)
end
def for_platform(admin_id:)
verifier.generate(["platform", admin_id.to_s], expires_in: 1.hour)
end
def verify!(token)
payload = verifier.verify(token)
case payload
when Array
if payload.first == "platform"
{ kind: :platform, admin_id: payload[1] }
elsif payload.size == 2
{ kind: :team, team_id: payload[0], user_id: payload[1] }
else
raise ArgumentError, "stato OAuth non valido"
end
else
raise ArgumentError, "stato OAuth non valido"
end
rescue ActiveSupport::MessageVerifier::InvalidSignature
raise ArgumentError, "stato OAuth scaduto o non valido"
end
private
def verifier
Rails.application.message_verifier(PURPOSE)
end
end
end
end

View File

@@ -6,7 +6,7 @@ module Youtube
"https://www.googleapis.com/auth/youtube.force-ssl"
].join(" ").freeze
def self.build(team_id:, redirect_uri:)
def self.build(state:, redirect_uri: ENV["YOUTUBE_REDIRECT_URI"])
params = {
client_id: ENV.fetch("YOUTUBE_CLIENT_ID", "not_configured"),
redirect_uri: redirect_uri,
@@ -14,7 +14,7 @@ module Youtube
scope: SCOPES,
access_type: "offline",
prompt: "consent",
state: team_id
state: state
}
"#{AUTH_URL}?#{URI.encode_www_form(params)}"
end

View File

@@ -0,0 +1,44 @@
module Youtube
class TeamStatus
PLATFORM_LABEL = "Match Live TV".freeze
def initialize(team)
@team = team
@ent = team.entitlements
@mode = @ent.plan.youtube_mode
end
def selectable?
return false unless @ent.youtube_enabled?
case @mode
when "matchlivetv_light"
PlatformCredential.configured?
when "team"
@team.youtube_credential.present?
else
false
end
end
def connected?
selectable?
end
def uses_platform_channel?
@mode == "matchlivetv_light" && PlatformCredential.configured?
end
def channel_title
if uses_platform_channel?
PLATFORM_LABEL
else
@team.youtube_credential&.channel_title
end
end
def needs_team_oauth?
@mode == "team" && @ent.premium_full?
end
end
end