Files
speckle-unreal/Source/SpeckleUnreal/Private/API/ClientAPI.cpp
T
JR-Morgan 8159860223 - Removed branch/stream/etc/ fetching from transports.
- Aligned names with .NET core.
- Added DP checks for error/edge cases.
- Tried to cleanup/reduce duplicated code.
2022-06-07 16:12:07 +01:00

365 lines
13 KiB
C++

#include "API/ClientAPI.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "LogSpeckle.h"
#include "Mixpanel.h"
#include "Objects/HighLevel/SpeckleBranch.h"
#include "Objects/HighLevel/SpeckleCommit.h"
#include "Objects/HighLevel/SpeckleGlobals.h"
#include "Objects/HighLevel/SpeckleUser.h"
#include "Transports/Transport.h"
void FClientAPI::StreamsGet(const FString& ServerUrl, const FString& AuthToken,
const int Limit,
const FFetchStreamDelegate OnCompleteAction, const FErrorDelegate OnErrorAction)
{
const FString RequestLogName = FString(__FUNCTION__);
const FString PostPayload =
"{\"query\": \"query{user {streams(limit:" + FString::FromInt(Limit) + ") {totalCount items {id name description updatedAt createdAt isPublic role collaborators{id name role company avatar}}}}}\"}";
auto OnError = [=](const FString& Message) mutable
{
ensureAlwaysMsgf(OnErrorAction.ExecuteIfBound(Message), TEXT("%s: Unhandled error - %s"), *RequestLogName , *Message);
};
auto ResponseHandler = [=](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) mutable
{
if(CheckRequestFailed(bWasSuccessful, Response, RequestLogName, OnError)) return;
TSharedPtr<FJsonObject> Obj;
if(!GetResponseAsJSON(Response, RequestLogName, Obj, OnError)) return;
TArray<TSharedPtr<FJsonValue>> StreamsArrJSON = Obj->GetObjectField(TEXT("data"))
->GetObjectField(TEXT("user"))
->GetObjectField(TEXT("streams"))
->GetArrayField(TEXT("items"));
TArray<FSpeckleStream> Streams;
Streams.Reserve(StreamsArrJSON.Num());
for (const TSharedPtr<FJsonValue> v : StreamsArrJSON)
{
FSpeckleStream Stream = FSpeckleStream(v);
Streams.Add(Stream);
}
UE_LOG(LogSpeckle, Log, TEXT("Operation %s completed successfully. Fetched %d items"), *RequestLogName, Streams.Num());
ensureAlwaysMsgf(OnCompleteAction.ExecuteIfBound(Streams), TEXT("%s: Complete handler was not bound properly"), *RequestLogName);
};
const FHttpRequestRef Request = CreateGraphQLRequest(ServerUrl, AuthToken, PostPayload);
Request->OnProcessRequestComplete().BindLambda(ResponseHandler);
SendGraphQLRequest(Request, RequestLogName, OnError);
}
void FClientAPI::StreamGetBranches(
const FString& ServerUrl, const FString& AuthToken,
const FString& StreamId, const int32 Limit,
const FFetchBranchDelegate OnCompleteAction, const FErrorDelegate OnErrorAction)
{
const FString RequestLogName = FString(__FUNCTION__);
//TODO limit
FString PostPayload = "{\"query\": \"query{ stream (id: \\\"" +
StreamId + "\\\"){id name branches(limit : " + FString::FromInt(Limit) + ") {totalCount cursor items{ id name description}}}}\"}";
// The above can be extended with author information
//query{stream(id:"a18f8c8569"){id name branches{totalCount items{id name description author{id, name, email, commits{cursor}}}}}}
auto OnError = [=](const FString& Message) mutable
{
ensureAlwaysMsgf(OnErrorAction.ExecuteIfBound(Message), TEXT("%s: Unhandled error - %s"), *RequestLogName , *Message);
};
auto ResponseHandler = [=](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) mutable
{
if(CheckRequestFailed(bWasSuccessful, Response, RequestLogName, OnError)) return;
TSharedPtr<FJsonObject> Obj;
if(!GetResponseAsJSON(Response, RequestLogName, Obj, OnError)) return;
TArray<TSharedPtr<FJsonValue>> BranchesArrJSON = Obj->GetObjectField(TEXT("data"))
->GetObjectField(TEXT("stream"))
->GetObjectField(TEXT("branches"))
->GetArrayField(TEXT("items"));
TArray<FSpeckleBranch> Branches;
Branches.Reserve(BranchesArrJSON.Num());
for (const TSharedPtr<FJsonValue>& v : BranchesArrJSON)
{
const TSharedPtr<FJsonObject>* BranchObj;
if(ensureAlways(v->TryGetObject(BranchObj)))
{
FSpeckleBranch Branch = FSpeckleBranch(*BranchObj);
Branches.Add(Branch);
}
}
UE_LOG(LogSpeckle, Log, TEXT("Operation %s completed successfully. Fetched %d items"), *RequestLogName, Branches.Num());
ensureAlwaysMsgf(OnCompleteAction.ExecuteIfBound(Branches), TEXT("%s: Complete handler was not bound properly"), *RequestLogName);
};
const FHttpRequestRef Request = CreateGraphQLRequest(ServerUrl, AuthToken, PostPayload);
Request->OnProcessRequestComplete().BindLambda(ResponseHandler);
SendGraphQLRequest(Request, RequestLogName, OnError);
}
void FClientAPI::StreamGetCommits(const FString& ServerUrl, const FString& AuthToken,
const FString& StreamId, const FString& BranchName, const int32 Limit,
const FFetchCommitDelegate OnCompleteAction, const FErrorDelegate OnErrorAction)
{
const FString RequestLogName = FString(__FUNCTION__);
FString PostPayload = "{\"query\": \"query{stream (id: \\\"" + StreamId +
"\\\"){id name createdAt updatedAt "
+ "branch(name: \\\"" + BranchName + "\\\" ){id name description author{name id email} " +
"commits(limit: " + FString::FromInt(Limit) + "){totalCount items {id referencedObject sourceApplication totalChildrenCount " +
"branchName parents authorName authorId message createdAt commentCount}}}}}\"}"; // authorAvatar
auto OnError = [=](const FString& Message) mutable
{
ensureAlwaysMsgf(OnErrorAction.ExecuteIfBound(Message), TEXT("%s: Unhandled error - %s"), *RequestLogName , *Message);
};
auto ResponseHandler = [=](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) mutable
{
if(CheckRequestFailed(bWasSuccessful, Response, RequestLogName, OnError)) return;
TSharedPtr<FJsonObject> Obj;
if(!GetResponseAsJSON(Response, RequestLogName, Obj, OnError)) return;
TArray<TSharedPtr<FJsonValue>> CommitsArrJSON = Obj->GetObjectField(TEXT("data"))
->GetObjectField(TEXT("stream"))
->GetObjectField(TEXT("branch"))
->GetObjectField(TEXT("commits"))
->GetArrayField(TEXT("items"));
TArray<FSpeckleCommit> ArrayOfCommits;
ArrayOfCommits.Reserve(CommitsArrJSON.Num());
for (const TSharedPtr<FJsonValue> v : CommitsArrJSON)
{
FSpeckleCommit Commit = FSpeckleCommit(v);
ArrayOfCommits.Add( Commit );
}
UE_LOG(LogSpeckle, Log, TEXT("Operation %s completed successfully. Fetched %d items"), *RequestLogName, ArrayOfCommits.Num());
ensureAlwaysMsgf(OnCompleteAction.ExecuteIfBound(ArrayOfCommits), TEXT("%s: Complete handler was not bound properly"), *RequestLogName);
};
const FHttpRequestRef Request = CreateGraphQLRequest(ServerUrl, AuthToken, PostPayload);
Request->OnProcessRequestComplete().BindLambda(ResponseHandler);
SendGraphQLRequest(Request, RequestLogName, OnError);
}
void FClientAPI::FetchGlobals(const FString& ServerUrl, const FString& AuthToken,
const FString& StreamId, const FString& ReferencedObjectId,
const FFetchGlobalsDelegate OnCompleteAction, const FErrorDelegate OnErrorAction)
{
const FString RequestLogName = FString(__FUNCTION__);
FString PostPayload = "{\"query\": \"query{stream (id:\\\"" + StreamId +
"\\\"){id name description updatedAt createdAt role isPublic object(id:\\\"" +
ReferencedObjectId +
"\\\"){id data}}}\"}";
auto OnError = [=](const FString& Message) mutable
{
ensureAlwaysMsgf(OnErrorAction.ExecuteIfBound(Message), TEXT("%s: Unhandled error - %s"), *RequestLogName , *Message);
};
auto ResponseHandler = [=](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) mutable
{
if(CheckRequestFailed(bWasSuccessful, Response, RequestLogName, OnError)) return;
TSharedPtr<FJsonObject> Obj;
if(!GetResponseAsJSON(Response, RequestLogName, Obj, OnError)) return;
TSharedPtr<FJsonObject> GlobalsJSONObject = Obj->GetObjectField(TEXT("data"))
->GetObjectField(TEXT("stream"))
->GetObjectField(TEXT("object"))
->GetObjectField(TEXT("data"));
FSpeckleGlobals Globals = FSpeckleGlobals(GlobalsJSONObject);
UE_LOG(LogSpeckle, Log, TEXT("Operation %s completed successfully"), *RequestLogName);
ensureAlwaysMsgf(OnCompleteAction.ExecuteIfBound(Globals), TEXT("%s: Complete handler was not bound properly"), *RequestLogName);
};
const FHttpRequestRef Request = CreateGraphQLRequest(ServerUrl, AuthToken, PostPayload);
Request->OnProcessRequestComplete().BindLambda(ResponseHandler);
SendGraphQLRequest(Request, RequestLogName, OnError);
}
void FClientAPI::FetchUserData(const FString& ServerUrl, const FString& AuthToken,
const FFetchUserDelegate OnCompleteAction, const FErrorDelegate OnErrorAction)
{
const FString RequestLogName = FString(__FUNCTION__);
const FString PostPayload =
"{\"query\": \"query{user{id name email company role suuid bio profiles avatar}}\"}";
auto OnError = [=](const FString& Message) mutable
{
UE_LOG(LogSpeckle, Warning, TEXT("%s: failed - %s"), *RequestLogName , *Message);
ensureAlwaysMsgf(OnErrorAction.ExecuteIfBound(Message), TEXT("%s: Unhandled error - %s"), *RequestLogName , *Message);
};
auto ResponseHandler = [=](FHttpRequestPtr, FHttpResponsePtr Response, bool bWasSuccessful) mutable
{
if(CheckRequestFailed(bWasSuccessful, Response, RequestLogName, OnError)) return;
TSharedPtr<FJsonObject> Obj;
if(!GetResponseAsJSON(Response, RequestLogName, Obj, OnError)) return;
TSharedPtr<FJsonObject> UserJSONObject = Obj->GetObjectField(TEXT("data"))
->GetObjectField(TEXT("user"));
FSpeckleUser MyUserData = FSpeckleUser( UserJSONObject );
UE_LOG(LogSpeckle, Log, TEXT("Operation %s completed successfully"), *RequestLogName);
ensureAlwaysMsgf(OnCompleteAction.ExecuteIfBound(MyUserData), TEXT("%s: Complete handler was not bound properly"), *RequestLogName);
};
const FHttpRequestRef Request = CreateGraphQLRequest(ServerUrl, AuthToken, PostPayload);
Request->OnProcessRequestComplete().BindLambda(ResponseHandler);
SendGraphQLRequest(Request, RequestLogName, OnError);
}
bool FClientAPI::GetResponseAsJSON(const FHttpResponsePtr Response, const FString& RequestLogName, TSharedPtr<FJsonObject>& OutObject, const TFunctionRef<void(const FString& Message)> OnErrorAction)
{
const FString JsonResponse = Response->GetContentAsString();
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonResponse);
if(!FJsonSerializer::Deserialize(Reader, OutObject))
{
const FString Message = FString::Printf(
TEXT("Recieved a response from \"%s\" for \"%s\" request, but the response failed to deserialize: %s"),
*Response->GetURL(), *RequestLogName, *JsonResponse);
OnErrorAction(Message);
return false;
}
FString Error;
if(CheckForOperationErrors(OutObject, Error))
{
OnErrorAction(Error);
return false;
}
return true;
}
FHttpRequestRef FClientAPI::CreateGraphQLRequest(const FString& ServerUrl, const FString AuthToken, const FString& PostPayload, const FString& Encoding)
{
FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
Request->SetURL(ServerUrl + "/graphql");
Request->SetVerb(TEXT("POST"));
Request->SetHeader("Accept-Encoding", Encoding);
Request->SetHeader("Content-Type", TEXT("application/json"));
Request->SetHeader("Authorization", "Bearer " + AuthToken);
Request->SetContentAsString(PostPayload);
return Request;
}
bool FClientAPI::SendGraphQLRequest(const FHttpRequestRef Request, const FString& RequestName,
const TFunctionRef<void(const FString& Message)> OnErrorAction)
{
const bool RequestSent = Request->ProcessRequest();
if(!RequestSent)
{
const FString Message = FString::Printf(TEXT("Request \"%s\" at \"%s\" failed: \nHTTP request failed to start"),
*RequestName, *Request->GetURL());
OnErrorAction(Message);
}
else
{
UE_LOG(LogSpeckle, Log, TEXT("POST Request %s to %s was sent, awaiting response"), *RequestName, *Request->GetURL() );
FAnalytics::TrackEvent("unknown", Request->GetURL(), "NodeRun", TMap<FString, FString> { {"name", RequestName }});
}
return RequestSent;
}
bool FClientAPI::CheckRequestFailed(bool bWasSuccessful, FHttpResponsePtr Response, const FString& RequestLogName, const TFunctionRef<void(const FString& Message)> OnErrorAction)
{
//Check the request was sent
if(!bWasSuccessful)
{
FString Message = FString::Printf(TEXT("Request \"%s\" to \"%s\" was unsuccessful: %s"),
*RequestLogName, *Response->GetURL(), *Response->GetContentAsString());
OnErrorAction(Message);
return true;
}
//Check Response code
const int32 ResponseCode = Response->GetResponseCode();
if (ResponseCode != 200)
{
FString Message = FString::Printf(TEXT("Request \"%s\" to \"%s\" failed with HTTP response %d"),
*RequestLogName, *Response->GetURL(), ResponseCode);
OnErrorAction(Message);
return true;
}
UE_LOG(LogSpeckle, Log, TEXT("Operation %s recieved a response from %s"), *RequestLogName, *Response->GetURL());
return false;
}
bool FClientAPI::CheckForOperationErrors(const TSharedPtr<FJsonObject> GraphQLResponse, FString& OutErrorMessage)
{
check(GraphQLResponse != nullptr);
bool WasError = false;
const TArray<TSharedPtr<FJsonValue>>* Errors;
if(GraphQLResponse->TryGetArrayField(TEXT("Errors"), Errors))
{
for(const TSharedPtr<FJsonValue>& e : *Errors)
{
FString Message;
const TSharedPtr<FJsonObject>* ErrorObject;
bool HadMessage = e->TryGetObject(ErrorObject)
&& (*ErrorObject)->TryGetStringField("message", Message);
if(!HadMessage)
{
Message = "An operation error occured but had no message!\n";
UE_LOG(LogSpeckle, Warning, TEXT("%s"), *Message);
}
OutErrorMessage.Append(Message + "\n");
WasError = true;
}
}
return WasError;
}