using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Sheets.v4;
using Google.Apis.Util.Store;
using UnityEngine;
namespace UnityEditor.Localization.Plugins.Google
{
///
/// See https://cloud.google.com/docs/authentication
///
public enum AuthenticationType
{
///
/// No authentication has been specified.
///
None,
///
/// Accessing private data.
///
OAuth,
///
/// Accessing public data anonymously.
///
APIKey
}
///
/// Configuration for connecting to a Google Sheet.
///
public interface IGoogleSheetsService
{
///
/// The Google Sheet service that will be created using the Authorization API.
///
SheetsService Service { get; }
}
///
/// The Sheets service provider performs the authentication to Google and keeps track of the authentication tokens
/// so that you do not need to authenticate each time.
/// The Sheets service provider also includes general sheet properties, such as default sheet styles, that are used when creating a new sheet.
///
///
/// Unity recommends to have a asset pre-configured for use, however this example does create a new one.
///
///
[CreateAssetMenu(fileName = "Google Sheets Service", menuName = "Localization/Google Sheets Service")]
[HelpURL("https://developers.google.com/sheets/api/guides/authorizing#AboutAuthorization")]
public partial class SheetsServiceProvider : ScriptableObject, IGoogleSheetsService, ISerializationCallbackReceiver
{
[SerializeField]
string m_ApiKey;
[SerializeField]
string m_ClientId;
[SerializeField]
string m_ClientSecret;
[SerializeField]
AuthenticationType m_AuthenticationType;
[SerializeField]
string m_ApplicationName;
[SerializeField]
NewSheetProperties m_NewSheetProperties = new NewSheetProperties();
SheetsService m_SheetsService;
// The Google API access application we are requesting.
static readonly string[] k_Scopes = { SheetsService.Scope.Spreadsheets };
///
/// Used to make sure the access and refresh tokens persist. Uses a FileDataStore by default with "Library/Google/{name}" as the path.
///
public IDataStore DataStore { get; set; }
///
/// The Google Sheet service that will be created using the Authorization API.
///
public virtual SheetsService Service
{
get
{
if (m_SheetsService == null)
m_SheetsService = Connect();
return m_SheetsService;
}
}
///
/// The authorization methodology to use.
/// See
///
public AuthenticationType Authentication => m_AuthenticationType;
///
/// The API Key to use when using authentication.
///
public string ApiKey => m_ApiKey;
///
/// Client Id when using OAuth authentication.
/// See also
///
public string ClientId => m_ClientId;
///
/// Client secret when using OAuth authentication.
/// See also
///
public string ClientSecret => m_ClientSecret;
///
/// The name of the application that will be sent when connecting.
///
public string ApplicationName
{
get => m_ApplicationName;
set => m_ApplicationName = value;
}
///
/// Properties to use when creating a new Google Spreadsheet sheet.
///
public NewSheetProperties NewSheetProperties
{
get => m_NewSheetProperties;
set => m_NewSheetProperties = value;
}
///
/// Set the API Key. An API key can only be used for reading from a public Google Spreadsheet.
///
///
public void SetApiKey(string apiKey)
{
m_ApiKey = apiKey;
m_AuthenticationType = AuthenticationType.APIKey;
}
///
/// Enable OAuth 2.0 authentication and extract the and from the supplied json.
///
///
public void SetOAuthCredentials(string credentialsJson)
{
var secrets = LoadSecrets(credentialsJson);
m_ClientId = secrets.ClientId;
m_ClientSecret = secrets.ClientSecret;
m_AuthenticationType = AuthenticationType.OAuth;
}
///
/// Enable OAuth 2.0 authentication with the provided client Id and client secret.
///
///
///
public void SetOAuthCredentials(string clientId, string clientSecret)
{
m_ClientId = clientId;
m_ClientSecret = clientSecret;
m_AuthenticationType = AuthenticationType.OAuth;
}
SheetsService Connect()
{
if (Authentication == AuthenticationType.None)
throw new Exception("No connection credentials. You must provide either OAuth2.0 credentials or an Api Key.");
if (Authentication == AuthenticationType.OAuth)
return ConnectWithOAuth2();
return ConnectWithApiKey();
}
///
/// When calling an API that does not access private user data, you can use a simple API key.
/// This key is by Google to authenticate your application for accounting purposes.
/// If you do need to access private user data, you must use OAuth 2.0.
///
SheetsService ConnectWithApiKey()
{
SheetsService sheetsService = new SheetsService(new BaseClientService.Initializer
{
ApiKey = m_ApiKey,
ApplicationName = ApplicationName
});
return sheetsService;
}
///
/// Call to preauthorize when using OAuth authorization. This will cause a browser to open a Google authorization
/// page after which the token will be stored in IDataStore so that this does not need to be done each time.
/// If this is not called then the first time is called it will be performed then.
///
///
public UserCredential AuthorizeOAuth()
{
// Prevents Unity locking up if the user canceled the auth request.
// Auto cancel after 60 secs
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var connectTask = AuthorizeOAuthAsync(cts.Token);
if (!connectTask.IsCompleted)
connectTask.RunSynchronously();
if (connectTask.Status == TaskStatus.Faulted)
{
throw new Exception($"Failed to connect to Google Sheets.\n{connectTask.Exception}");
}
return connectTask.Result;
}
///
/// Call to preauthorize when using OAuth authorization. This will cause a browser to open a Google authorization
/// page after which the token will be stored in IDataStore so that this does not need to be done each time.
/// If this is not called then the first time is called it will be performed then.
///
/// Token that can be used to cancel the task prematurely.
/// The authorization Task that can be monitored.
public Task AuthorizeOAuthAsync(CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(ClientSecret))
throw new Exception($"{nameof(ClientSecret)} is empty");
if (string.IsNullOrEmpty(ClientId))
throw new Exception($"{nameof(ClientId)} is empty");
// We create a separate area for each so that multiple providers don't clash.
var dataStore = DataStore ?? new FileDataStore($"Library/Google/{name}", true);
var secrets = new ClientSecrets { ClientId = m_ClientId, ClientSecret = m_ClientSecret };
// We use the client Id for the user so that we can generate a unique token file and prevent conflicts when using multiple OAuth authentications. (LOC-188)
var user = m_ClientId;
var connectTask = GoogleWebAuthorizationBroker.AuthorizeAsync(secrets, k_Scopes, user, cancellationToken, dataStore);
return connectTask;
}
///
/// When calling an API that will access private user data, O Auth 2.0 credentials must be used.
///
SheetsService ConnectWithOAuth2()
{
var userCredentials = AuthorizeOAuth();
var sheetsService = new SheetsService(new BaseClientService.Initializer
{
HttpClientInitializer = userCredentials,
ApplicationName = ApplicationName,
});
return sheetsService;
}
internal static ClientSecrets LoadSecrets(string credentials)
{
if (string.IsNullOrEmpty(credentials))
throw new ArgumentException(nameof(credentials));
using (var stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(credentials)))
{
var gcs = GoogleClientSecrets.FromStream(stream);
return gcs.Secrets;
}
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
if (string.IsNullOrEmpty(m_ApplicationName))
m_ApplicationName = PlayerSettings.productName;
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
}
}
}