jam-cloud/web/lib/google_client.rb

375 lines
13 KiB
Ruby
Raw Normal View History

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'
# 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()
Rails.logger.info("Initializing client...")
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'
)
#youtube = client.discovered_api('youtube', 'v3')
end
# Return a login URL that will show a web page with
def get_login_url(username=nil)
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://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
# 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'
)
flow = Google::APIClient::InstalledAppFlow.new(
:client_id => config.google_client_id,
:client_secret => config.google_secret,
:redirect_uri=>redirect_uri,
:scope => 'email profile'
)
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',
'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',
'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
socket=nil
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