【GCP/UE】UE から Google Cloud の API を叩く基本的な流れ

# はじめに
## テスト環境
- IDE
    - UE：5.3.2
    - Visual Studio 2022：17.11.5
- プラグイン等
    - TsubasamusuUnrealLibrary：1.10.0
# 手順
## サービスアカウントの作成
### 手順1
以下の記事などを参考にして Google アカウントの作成や無料トライアルへの登録、プロジェクトの作成などの Googe Cloud の初期設定を行う。
- [Google Cloud を初めて利用する方は、ここから始めましょう](https://cloud.google.com/blog/ja/topics/developers-practitioners/new-google-cloud-start-here)
- [【超初心者向け】Google Cloudを始めるための事前準備](https://www.topgate.co.jp/blog/google-service/29633)
- [基礎から学ぶGCP　第1回 〜GCPを使う準備をする〜](https://cloud-ace.jp/column/detail148/)
- [Google Cloud（旧GCP）無料で使ってみた！クラウド初心者もかんたんに開設、始め方大解説（前編：説明編）](https://blog.g-gen.co.jp/entry/googlecloud-start-01)
- [【Google Cloud】【最短10分】無料ではじめるGoogle Cloud入門](https://sight-r.sts-inc.co.jp/google_cloud_article/free_trial/)
- [【初心者向け】Google Cloudのアカウント作成手順を分かりやすく解説！](https://note.com/ogawa_analyze/n/n0d8c628a4b9e)
### 手順2
[Google Cloud](https://console.cloud.google.com) にアクセスして「[IAM と管理](https://console.cloud.google.com/iam-admin)＞サービスアカウント」に行き、「サービスアカウントを作成」を押す。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/30808909-1fd4-9e76-8dd3-f6d177aaa354.png">

### 手順3
「サービスアカウント名」と「サービスアカウント ID」を入力して「作成して続行」を押す。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/4dd073b5-f525-a4e4-f6cd-38531ea6f077.png">

### 手順4
必要に応じて「ロールを選択」を押してロールを選択し、「続行」を押す。

例：Cloud Storage のオブジェクトを読み取る場合は「Cloud Storage ＞ Storage オブジェクト閲覧者」など

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/5aa40251-a1a2-b103-c0eb-cdc3603b8681.png">

### 手順5
必要に応じて「サービスアカウントユーザーロール」と「サービスアカウント管理者ロール」を設定して「完了」を押す。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/621bf755-5338-87a2-f7b2-af3ff0277b44.png">

## 秘密鍵のダウンロード
### 手順6
作成したサービスアカウントを選択して「キー＞鍵を追加＞新しい鍵を作成」を押す。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/e18bdc2e-c71a-d1c2-fddd-0a71af20d1f5.png">

### 手順7
「キーのタイプ」で「JSON」を選択して右下の「作成」を押し、JSON ファイルをダウンロードする。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/43eeebff-d37d-8eba-7aed-7733b4e2c9a7.png">

## API の使用（Cloud Translation API の場合）
### 手順8
Google Cloud に戻り、使用したい API の「有効にする」を押す。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/3fd941ab-6b1a-6b21-0176-81fb7b5e6da5.png">

### 手順9
UE にて、Google Cloud の API を叩く際に必要なアクセストークンを取得する以下のような関数を作成する。
（以下は [TsubasamusuUnrealLibrary（v1.10.0）](https://github.com/tsubasamusu/TsubasamusuUnrealLibrary/releases/tag/v1.10.0) のコード）
<details>
<summary>UTsubasamusuStringConvertLibrary</summary>

```cpp:TsubasamusuStringConvertLibrary.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "TsubasamusuStringConvertLibrary.generated.h"

UCLASS()
class TSUBASAMUSUUNREALLIBRARY_API UTsubasamusuStringConvertLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintPure, Category = "TSUBASAMUSU|Convert|String")
	static FString ConvertToBase64(const FString& String);

	UFUNCTION(BlueprintCallable, Category = "TSUBASAMUSU|Convert|String")
	static TArray<uint8> ConvertToUtf8(const FString& String);
	
	static TArray<char> ConvertToCharArray(const FString& String);
};
```
```cpp:TsubasamusuStringConvertLibrary.cpp
#include "Convert/TsubasamusuStringConvertLibrary.h"

FString UTsubasamusuStringConvertLibrary::ConvertToBase64(const FString& String)
{
    FTCHARToUTF8 Utf8String(*String);

    return FBase64::Encode((const uint8*)Utf8String.Get(), Utf8String.Length());
}

TArray<uint8> UTsubasamusuStringConvertLibrary::ConvertToUtf8(const FString& String)
{
    FTCHARToUTF8 Utf8(*String);

    TArray<uint8> StringData;

    StringData.Append((uint8*)Utf8.Get(), Utf8.Length());

    return StringData;
}

TArray<char> UTsubasamusuStringConvertLibrary::ConvertToCharArray(const FString& String)
{
    FTCHARToUTF8 UTF8String(*String);

    TArray<char> CharArray;

    CharArray.Reserve(UTF8String.Length());

    CharArray.Append(UTF8String.Get(), UTF8String.Length());

    return CharArray;
}
```

</details>
<details>
<summary>UAsyncActionGetGoogleAccessToken</summary>

```cpp:AsyncActionGetGoogleAccessToken.h
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncActionGetGoogleAccessToken.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSucceededToGetGoogleAccessToken, const FString&, AccessToken, int64, ExpirationUnixTime);

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFailedToGetGoogleAccessToken);

UCLASS()
class TSUBASAMUSUUNREALLIBRARY_API UAsyncActionGetGoogleAccessToken : public UBlueprintAsyncActionBase
{
	GENERATED_BODY()

public:
	UPROPERTY(BlueprintAssignable)
	FOnSucceededToGetGoogleAccessToken Succeeded;

	UPROPERTY(BlueprintAssignable)
	FOnFailedToGetGoogleAccessToken Failed;

	UFUNCTION(BlueprintCallable, Category = "TSUBASAMUSU|Google", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", AutoCreateRefTerm = "Scopes"))
	static UAsyncActionGetGoogleAccessToken* AsyncGetGoogleAccessToken(UObject* WorldContextObject, const FString& PrivateKey, const FString& ServiceAccountMailAddress, const TArray<FString>& Scopes);

	void Activate() override;

private:
	FString PrivateKey;

	FString ServiceAccountMailAddress;

	TArray<FString> Scopes;

	int64 ExpirationUnixTime;

	FString GetGoogleAccessTokenHeader();

	FString GetGoogleAccessTokenPayload();

	FString GetGoogleCloudJwt();

	FString GetGoogleCloudJsonContent();

	void OnSucceeded(const FString& AccessToken);

	void OnFailed(const FString& TriedThing);
};
```
```cpp:AsyncActionGetGoogleAccessToken.cpp
#include "Google/AsyncActionGetGoogleAccessToken.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"

#define UI UI_ST
THIRD_PARTY_INCLUDES_START
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
THIRD_PARTY_INCLUDES_END
#undef UI

UAsyncActionGetGoogleAccessToken* UAsyncActionGetGoogleAccessToken::AsyncGetGoogleAccessToken(UObject* WorldContextObject, const FString& PrivateKey, const FString& ServiceAccountMailAddress, const TArray<FString>& Scopes)
{
	UAsyncActionGetGoogleAccessToken* Action = NewObject<UAsyncActionGetGoogleAccessToken>();

	Action->PrivateKey = PrivateKey;
	Action->ServiceAccountMailAddress = ServiceAccountMailAddress;
	Action->Scopes = Scopes;

	Action->RegisterWithGameInstance(WorldContextObject);

	return Action;
}

void UAsyncActionGetGoogleAccessToken::Activate()
{
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();

	FString Url = TEXT("https://oauth2.googleapis.com/token");

	HttpRequest->SetURL(Url);
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
	HttpRequest->SetContentAsString(GetGoogleCloudJsonContent());

	HttpRequest->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr HttpRequestPtr, FHttpResponsePtr HttpResponsePtr, bool bSucceeded)
		{
			if (!bSucceeded)
			{
				OnFailed(TEXT("send a HTTP request"));

				return;
			}

			if (!HttpResponsePtr.IsValid())
			{
				OnFailed(TEXT("get a FHttpResponsePtr"));

				return;
			}

			FString JsonResponse = HttpResponsePtr->GetContentAsString();

			TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonResponse);

			TSharedPtr<FJsonObject> JsonObject;

			if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid())
			{
				UTsubasamusuLogLibrary::LogError(TEXT("The JSON response is \"") + JsonResponse + TEXT("\"."));

				OnFailed(TEXT("deserialize the JSON response"));

				return;
			}

			FString AccessToken;

			if (JsonObject->TryGetStringField(TEXT("access_token"), AccessToken) || JsonObject->TryGetStringField(TEXT("id_token"), AccessToken))
			{
				OnSucceeded(AccessToken);

				return;
			}

			FString ErrorMessage, ErrorDescription;

			if (JsonObject->TryGetStringField(TEXT("error"), ErrorMessage) && JsonObject->TryGetStringField(TEXT("error_description"), ErrorDescription))
			{
				UTsubasamusuLogLibrary::LogError(TEXT("The error is \"") + ErrorMessage + TEXT("\"."));
				UTsubasamusuLogLibrary::LogError(TEXT("The error description is \"") + ErrorDescription + TEXT("\"."));

				OnFailed(TEXT("get an access token field from the JSON response"));

				return;
			}

			UTsubasamusuLogLibrary::LogError(TEXT("The JSON response is \"") + JsonResponse + TEXT("\"."));

			OnFailed(TEXT("get any field from the JSON response"));
		});

	if (!HttpRequest->ProcessRequest()) OnFailed(TEXT("process a HTTP request"));
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleAccessTokenHeader()
{
	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("alg"), TEXT("RS256"));
	JsonObject->SetStringField(TEXT("typ"), TEXT("JWT"));

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON header"));

	return TEXT("");
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleAccessTokenPayload()
{
	int64 IssuedUnixTime = FDateTime::UtcNow().ToUnixTimestamp();
	ExpirationUnixTime = IssuedUnixTime + 3600;

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("iss"), ServiceAccountMailAddress);
	JsonObject->SetStringField(TEXT("scope"), FString::Join(Scopes, TEXT(" ")));
	JsonObject->SetStringField(TEXT("aud"), TEXT("https://oauth2.googleapis.com/token"));
	JsonObject->SetNumberField(TEXT("exp"), ExpirationUnixTime);
	JsonObject->SetNumberField(TEXT("iat"), IssuedUnixTime);

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON payload"));

	return TEXT("");
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleCloudJwt()
{
	TArray<char> PrivateKeyCharArray = UTsubasamusuStringConvertLibrary::ConvertToCharArray(PrivateKey.Replace(TEXT("\\n"), TEXT("\n")));

	BIO* Bio = BIO_new_mem_buf((void*)PrivateKeyCharArray.GetData(), -1);

	if (!Bio)
	{
		OnFailed(TEXT("create a BIO"));

		return TEXT("");
	}

	RSA* Rsa = PEM_read_bio_RSAPrivateKey(Bio, nullptr, nullptr, nullptr);

	BIO_free(Bio);

	if (!Rsa)
	{
		OnFailed(TEXT("create a RSA"));

		return TEXT("");
	}

	FString HeaderBase64 = UTsubasamusuStringConvertLibrary::ConvertToBase64(GetGoogleAccessTokenHeader());
	FString PayloadBase64 = UTsubasamusuStringConvertLibrary::ConvertToBase64(GetGoogleAccessTokenPayload());

	FString Message = HeaderBase64 + TEXT(".") + PayloadBase64;

	TArray<uint8> MessageDigest = UTsubasamusuStringConvertLibrary::ConvertToUtf8(Message);

	uint8 Digest[SHA256_DIGEST_LENGTH];

	SHA256(MessageDigest.GetData(), MessageDigest.Num(), Digest);

	uint8 Signature[256];

	unsigned int SignatureLen;

	if (RSA_sign(NID_sha256, Digest, SHA256_DIGEST_LENGTH, Signature, &SignatureLen, Rsa) != 1)
	{
		RSA_free(Rsa);

		OnFailed(TEXT("sign the RSA"));

		return TEXT("");
	}

	RSA_free(Rsa);

	return Message + TEXT(".") + FBase64::Encode(Signature, SignatureLen);
}

FString UAsyncActionGetGoogleAccessToken::GetGoogleCloudJsonContent()
{
	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetStringField(TEXT("grant_type"), TEXT("urn:ietf:params:oauth:grant-type:jwt-bearer"));
	JsonObject->SetStringField(TEXT("assertion"), GetGoogleCloudJwt());

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	if (FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter)) return JsonString;

	OnFailed(TEXT("create a JSON content"));

	return TEXT("");
}

void UAsyncActionGetGoogleAccessToken::OnSucceeded(const FString& AccessToken)
{
	Succeeded.Broadcast(AccessToken, ExpirationUnixTime);

	SetReadyToDestroy();
}

void UAsyncActionGetGoogleAccessToken::OnFailed(const FString& TriedThing)
{
	UTsubasamusuLogLibrary::LogError(TEXT("Failed to ") + TriedThing + TEXT(" to get an access token for Google Cloud."));

	Failed.Broadcast();

	SetReadyToDestroy();
}
```

</details>

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/689694e6-3b95-8567-6007-8f65dc2f72e1.png">

### 手順10
API ドキュメントを確認しながら API を叩く関数を作成する。
ヘッダーの「Authorization」に Google Cloud 用のアクセストークンを渡してあげるのがポイント。
（以下は Cloud Translation API の例）
```cpp:SampleBlueprintFunctionLibrary.cpp
#include "SampleBlueprintFunctionLibrary.h"
#include "Interfaces/IHttpRequest.h"
#include "HttpModule.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Interfaces/IHttpResponse.h"

void USampleBlueprintFunctionLibrary::Hoge(const FString& GoogleCloudAccessToken)
{
	TArray<TSharedPtr<FJsonValue>> JsonValues;

	JsonValues.Add(MakeShared<FJsonValueString>(TEXT("Hello world")));
	JsonValues.Add(MakeShared<FJsonValueString>(TEXT("My name is Tsubasamusu.")));

	TSharedPtr<FJsonObject> JsonObject = MakeShared<FJsonObject>();

	JsonObject->SetArrayField(TEXT("q"), JsonValues);
	JsonObject->SetStringField(TEXT("target"), TEXT("de"));

	FString JsonString;

	TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&JsonString);

	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();

	FJsonSerializer::Serialize(JsonObject.ToSharedRef(), JsonWriter);

	HttpRequest->SetURL(TEXT("https://translation.googleapis.com/language/translate/v2"));
	HttpRequest->SetVerb(TEXT("POST"));
	HttpRequest->SetHeader(TEXT("Content-type"), TEXT("application/json"));
	HttpRequest->SetHeader(TEXT("Authorization"), TEXT("Bearer ") + GoogleCloudAccessToken);//アクセストークンを渡す
	HttpRequest->SetContentAsString(JsonString);

	HttpRequest->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr HttpRequestPtr, FHttpResponsePtr HttpResponsePtr, bool bSucceeded)
		{
			UKismetSystemLibrary::PrintString(nullptr, HttpResponsePtr->GetContentAsString());
		});

	HttpRequest->ProcessRequest();
}
```
### 手順11
実際に API を叩いて動作を確認する。
アクセストークンを発行する際に、API ドキュメントに書かれている必要なスコープを渡してあげるのがポイント。

<img width="500" alt="TSUBASAMUSU.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2670397/cef925d5-f4ad-b87a-13d8-34ea0d13589a.png">

# 最後に
## 参考記事
- [テキストの翻訳（Basic）](https://cloud.google.com/translate/docs/basic/translating-text)
- [Cloud Translation API を使用する際のポイント](https://qiita.com/howdy39/items/89614a342d02d346fb72)
- [Unreal Engine で OAuth 2.0 JWT Bearer Token Flow(Google Cloud APIにアクセスしてみよう)](https://docs.google.com/presentation/d/1JuhDk32onn3LY9nvaJ73KSVlaZFTUrhIOQwVcKSye60)
- [OpenSSL コンパイル エラー](https://forums.unrealengine.com/t/openssl-compile-error/402878)
- [How to load a third-party library from the engine?](https://forums.unrealengine.com/t/how-to-load-a-third-party-library-from-the-engine/393920)