require 'faraday' require 'launchy' require 'cgi' require 'json' require 'google/api_client' require 'google/api_client/client_secrets' require 'google/api_client/auth/installed_app' require 'socket' #Google::Apis.logger.level = Logger::DEBUG YOUTUBE_API_SERVICE_NAME = 'youtube' YOUTUBE_API_VERSION = 'v3' # Youtube OAuth and API functionality: module JamRuby class GoogleClient attr_accessor :client attr_accessor :api attr_accessor :request attr_accessor :server attr_accessor :socket attr_accessor :config attr_accessor :redirect_uri def initialize() self.config = Rails.application.config self.redirect_uri='http://localhost:2112/auth/google_login/callback' self.client = Google::APIClient.new( :application_name => 'JamKazam', :application_version => '1.0.0' ) end def youtube @youtube ||= client.discovered_api('youtube', 'v3') end def create_authorization(user_auth, scope, autorefresh) authorization = Signet::OAuth2::Client.new( :authorization_uri => "https://accounts.google.com/o/oauth2/auth", :token_credential_uri => "https://accounts.google.com/o/oauth2/token", :client_id => @config.google_client_id, :client_secret => @config.google_secret, #:redirect_uri => credentials.redirect_uris.first, :scope => scope ) authorization.access_token = user_auth.token authorization.refresh_token = user_auth.refresh_token authorization.expires_at = user_auth.token_expiration if autorefresh && (user_auth.token_expiration < (Time.now - 15)) # add 15 second buffer to this time, because OAUth server does not respond with timestamp, but 'expires_in' which is just offset seconds # XXX: what to do when this fails? authorization.refresh! user_auth.token = authorization.access_token user_auth.token_expiration = authorization.issued_at + authorization.expires_in user_auth.save end authorization end def create_client Google::APIClient.new( :application_name => 'JamKazam', :application_version => '1.0.0', ) end # Return a login URL that will show a web page with def get_login_url(username=nil) puts "GET LOGIN URL" uri = "https://accounts.google.com/o/oauth2/auth" uri << "?scope=#{CGI.escape('https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube https://gdata.youtube.com email profile ')}" # # https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload uri << "&client_id=#{CGI.escape(self.config.google_email)}" uri << "&response_type=code" uri << "&access_type=online" uri << "&prompt=consent" uri << "&state=4242" uri << "&redirect_uri=#{redirect_uri}" if username.present? uri << "&login_hint=#{(username)}" end uri end # create youtube broadcast def create_broadcast(user, broadcast_options) auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? raise JamPermissionError, "No current google token found for user #{user}" end broadcast_data = { "snippet" => broadcast_options[:snippet], "status" => broadcast_options[:status], "contentDetails" => broadcast_options[:contentDetails] } begin #secrets = Google::APIClient::ClientSecrets.new({"web" => {"access_token" => auth.token, "refresh_token" => auth.refresh_token, "client_id" => @config.google_client_id, "client_secret" => @config.google_secret}}) my_client = create_client my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) puts "BROADCAST DATA: #{broadcast_data}" #y = my_client.discovered_api('youtube', 'v3') response = my_client.execute!(:api_method => youtube.live_broadcasts.insert, :parameters => {:part => 'contentDetails,status,snippet'}, :body_object => broadcast_data) body = JSON.parse(response.body) puts "CREATE BROADCAST RESPONSE: #{body}" return body rescue Google::APIClient::ClientError => e # ex: =begin ex = { "error": { "errors": [ { "domain": "youtube.liveBroadcast", "reason": "liveStreamingNotEnabled", "message": "The user is not enabled for live streaming.", "extendedHelp": "https://www.youtube.com/features" } ], "code": 403, "message": "The user is not enabled for live streaming." } } ex = { "error": { "errors": [ { "domain": "youtube.liveBroadcast", "reason": "insufficientLivePermissions", "message": "Request is not authorized", "extendedHelp": "https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/insert#auth_required" } ], "code": 403, "message": "Request is not authorized" } } =end puts e.result.body raise e end end def bind_broadcast(user, broadcast_id, stream_id) auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? raise JamPermissionError, "No current google token found for user #{user}" end begin my_client = create_client my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) #y = my_client.discovered_api('youtube', 'v3') response = my_client.execute!(:api_method => youtube.live_broadcasts.bind, :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id, :streamId => stream_id }) body = JSON.parse(response.body) puts "BIND RESPONSE: #{body}" return body rescue Google::APIClient::ClientError => e puts e.result.body raise e end end def get_broadcast(user, broadcast_id) auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? raise JamPermissionError, "No current google token found for user #{user}" end begin my_client = create_client my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) #y = my_client.discovered_api('youtube', 'v3') response = my_client.execute!(:api_method => youtube.live_broadcasts.list, :parameters => {:part => 'id,contentDetails,status,snippet', :id => broadcast_id }) body = JSON.parse(response.body) puts "BIND RESPONSE: #{body}" return body["items"][0] # returns array of items. meh rescue Google::APIClient::ClientError => e puts e.result.body raise e end end def create_stream(user, stream_options) auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? raise JamPermissionError, "No current google token found for user #{user}" end broadcast_data = { "snippet" => stream_options[:snippet], "cdn" => stream_options[:cdn], "contentDetails" => stream_options[:contentDetails] } begin my_client = create_client my_client.authorization = create_authorization(auth, 'https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.force-ssl', true) puts "STREAM DATA: #{broadcast_data}" #y = my_client.discovered_api('youtube', 'v3') response = my_client.execute!(:api_method => youtube.live_streams.insert, :parameters => {:part => 'id,contentDetails,cdn,status,snippet'}, :body_object => broadcast_data) body = JSON.parse(response.body) puts "CREATE STREAM RESPONSE: #{body}" return body rescue Google::APIClient::ClientError => e puts e.result.body raise e end end # create youtube broadcast def update_broadcast(user, broadcast_options) end # Contacts youtube and prepares an upload to youtube. This # process is somewhat painful, even in ruby, so we do the preparation # and the client does the actual upload using the URL returned: # https://developers.google.com/youtube/v3/docs/videos/insert # https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol def sign_youtube_upload(user, filename, length) raise ArgumentError, "Length is required and should be > 0" if length.to_i.zero? # Something like this: # POST /upload/youtube/v3/videos?uploadType=resumable&part=snippet,status,contentDetails HTTP/1.1 # Host: www.googleapis.com # Authorization: Bearer AUTH_TOKEN # Content-Length: 278 # Content-Type: application/json; charset=UTF-8 # X-Upload-Content-Length: 3000000 # X-Upload-Content-Type: video/* # { # "snippet": { # "title": "My video title", # "description": "This is a description of my video", # "tags": ["cool", "video", "more keywords"], # "categoryId": 22 # }, # "status": { # "privacyStatus": "public", # "embeddable": True, # "license": "youtube" # } # } auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? raise SecurityError, "No current google token found for user #{user}" end video_data = { "snippet" => { "title" => filename, "description" => filename, "tags" => ["cool", "video", "more keywords"], "categoryId" => 1 }, "status" => { "privacyStatus" => "public", "embeddable" => true, "license" => "youtube" } } conn = Faraday.new(:url => "https://www.googleapis.com", :ssl => {:verify => false}) do |faraday| faraday.request :url_encoded faraday.adapter Faraday.default_adapter end video_json=video_data.to_json result = conn.post("/upload/youtube/v3/videos?access_token=#{CGI.escape(auth.token)}&uploadType=resumable&part=snippet,status,contentDetails", video_json, { 'content-type' => 'application/json;charset=utf-8', 'x-Upload-Content-Length' => "#{length}", 'x-upload-content-type' => "video/*" } ) # Response should something look like: # HTTP/1.1 200 OK # Location: https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&upload_id=xa298sd_f&part=snippet,status,contentDetails # Content-Length: 0 if (result.nil? || result.status!=200 || result.headers['location'].blank?) msg = "Failed signing with status=#{result.status} #{result.inspect}: " if result.body.present? && result.body.length > 2 msg << result.body.inspect # JSON.parse(result.body).inspect end # TODO: how to test for this: # If reason is "youtubeSignupRequired" # If the user's youtube account is unlinked, they'll have to go here. # http://m.youtube.com/create_channel. With v3, there is no automated way to do this. raise msg else # This has everything one needs to start the upload to youtube: { "method" => "PUT", "url" => result.headers['location'], "Authorization" => "Bearer #{auth.token}", "Content-Length" => length, "Content-Type" => "video/*" } end end # https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol#Check_Upload_Status def youtube_upload_status(user, upload_url, length) auth = UserAuthorization.google_auth(user).first if auth.nil? || auth.token.nil? raise SecurityError, "No current google token found for user #{user}" end # PUT UPLOAD_URL HTTP/1.1 # Authorization: Bearer AUTH_TOKEN # Content-Length: 0 # Content-Range: bytes */CONTENT_LENGTH RestClient.put(upload_url, nil, { 'Authorization' => "Bearer #{auth.token}", 'Content-Length' => "0", 'Content-Range' => "bytes */#{length}" }) do |response, request, result| # Result looks like this: # 308 Resume Incomplete # Content-Length: 0 # Range: bytes=0-999999 case (response.code) when 200..207 result_hash = { "offset" => 0, "length" => length, "status" => response.code } when 308 range_str = response.headers['Range'] if range_str.nil? range = 0..length else range = range_str.split("-") end result_hash = { "offset" => range.first.to_i, "length" => range.last.to_i, "status" => response.code } else raise "Unexpected status from youtube: [#{response.code}] with headers: #{response.headers.inspect}" end result_hash end end # @return true if file specified by URL uploaded, false otherwise def verify_youtube_upload(user, upload_url, length) status_hash=youtube_upload_status(user, upload_url, length) (status_hash['status']>=200 && status_hash['status']<300) end # Set fully_uploaded if the upload can be verified. # @return true if verified; false otherwise: def complete_upload(recorded_video) if (verify_youtube_upload(recorded_video.user, recorded_video.url, recorded_video.length)) recorded_video.update_attribute(:fully_uploaded, true) else false end end def verify_recaptcha(recaptcha_response) success = false if !Rails.application.config.recaptcha_enable success = true else Rails.logger.info "Login with: #{recaptcha_response}" RestClient.get("https://www.google.com/recaptcha/api/siteverify", params: { secret: Rails.application.config.recaptcha_private_key, response: recaptcha_response } ) do |response, request, result| Rails.logger.info "response: #{response.inspect}" case (response.code) when 200..207 json = JSON.parse(response.to_str) if json['success'] success = true else Rails.logger.info("Error verifying recaptcha: #{json['error-codes'].inspect}") end else Rails.logger.info("Unexpected status from google_recaptcha: [#{response.code}] with headers: #{response.headers.inspect}") end #case end #do end # if success end #def # This will also sign in and prompt for login as necessary; # currently requires the server to be running at localhost:3000 def signin_flow() config = Rails.application.config self.client = Google::APIClient.new( :application_name => 'JamKazam', :application_version => '1.0.0' ) raise "SIGNIN FLOW!!" flow = Google::APIClient::InstalledAppFlow.new( :client_id => config.google_client_id, :client_secret => config.google_secret, :redirect_uri => redirect_uri, :scope => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload' ) self.client.authorization = flow.authorize end # Must manually confirm to obtain refresh token: def get_refresh_token config = Rails.application.config conn = Faraday.new(:url => 'https://accounts.google.com', :ssl => {:verify => false}) do |faraday| faraday.request :url_encoded faraday.adapter Faraday.default_adapter end wait_for_callback do |refresh_token| Rails.logger.info("The refresh_token is #{refresh_token}") end result = conn.get '/o/oauth2/auth', { 'scope' => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload', 'client_id' => config.google_client_id, 'response_type' => "code", 'access_type' => "offline", 'redirect_uri' => redirect_uri } end def get_access_token(refresh_token) refresh_token = "4/g9uZ8S4lq2Bj1J8PPIkgOFKhTKmCHSmRe68iHA75hRg.gj8Nt5bpVYQdPm8kb2vw2M23tnRnkgI" config = Rails.application.config conn = Faraday.new(:url => 'https://accounts.google.com', :ssl => {:verify => false}) do |faraday| faraday.request :url_encoded faraday.adapter Faraday.default_adapter end wait_for_callback do |access_token| Rails.logger.info("The access_token is #{access_token}") end result = conn.post '/o/oauth2/token', nil, { 'scope' => 'email profile https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/youtube.upload', 'client_id' => config.google_client_id, 'client_secret' => config.google_secret, 'refresh_token' => refresh_token, 'grant_type' => "refresh_token", 'redirect_uri' => redirect_uri } Rails.logger.info("REsult: #{result.inspect}\n\n") end def wait_for_callback(port=3000) shutdown() self.server = Thread.new { Rails.logger.info("STARTING SERVER THREAD...") tcp_server = TCPServer.new('localhost', port) self.socket = tcp_server.accept if self.socket request = self.socket.gets Rails.logger.info("REQUEST: #{request}") params=CGI.parse(request) code = params['code'].first # Whack the end part: access_code = code ? code.split(" ").first : "" status = (access_code.present?) ? 'OK' : 'EMPTY' Rails.logger.info("access_code is #{status}") token=exchange_for_token(access_code) yield(token) response = "#{status}\n" self.socket.print "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: #{response.bytesize}\r\n" + "Connection: close\r\n" self.socket.print "\r\n" self.socket.print response self.socket.close self.socket=nil else puts "WHY WOULD THIS EVER HAPPEN?" raise "WHY WOULD THIS EVER HAPPEN?" end } end def exchange_for_token(access_code) Rails.logger.info("Exchanging token for code: [#{access_code}]") conn = Faraday.new(:url => "https://accounts.google.com", :ssl => {:verify => false}) do |faraday| faraday.request :url_encoded faraday.adapter Faraday.default_adapter end exchange_parms={ 'grant_type' => 'authorization_code', 'code' => (access_code), 'client_id' => (config.google_email), 'client_secret' => (config.google_secret), 'redirect_uri' => (redirect_uri), } result = conn.post('/o/oauth2/token', exchange_parms) if result.body.nil? || result.body.blank? raise "Result not in correct form: [#{result.body}]" end body_hash = JSON.parse(result.body) body_hash['access_token'] end # shutdown def shutdown() Rails.logger.info("Stopping oauth server...") if (self.socket) begin self.socket.close rescue IOError # Expected for most cases: Rails.logger.info("Socket already closed.") end self.socket = nil end end end # class end # module