More general cleanup in MCP
This commit is contained in:
@@ -1,227 +0,0 @@
|
|||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Dom/JsonObject.h"
|
|
||||||
#include "UObject/UnrealType.h"
|
|
||||||
#include "UObject/EnumProperty.h"
|
|
||||||
|
|
||||||
namespace MCPPopulate
|
|
||||||
{
|
|
||||||
|
|
||||||
// Try to set a single FProperty on a handler from a JSON field.
|
|
||||||
// Returns an empty string on success, or an error message on failure.
|
|
||||||
FString SetPropertyFromJson(
|
|
||||||
void* Container,
|
|
||||||
FProperty* Prop,
|
|
||||||
const FString& FieldName,
|
|
||||||
const FJsonObject* Json)
|
|
||||||
{
|
|
||||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
|
||||||
|
|
||||||
// FString
|
|
||||||
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
|
||||||
}
|
|
||||||
StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName));
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// int32
|
|
||||||
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
|
||||||
}
|
|
||||||
IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName));
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// float
|
|
||||||
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
|
||||||
}
|
|
||||||
FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName));
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// double
|
|
||||||
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
|
||||||
}
|
|
||||||
DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName));
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// bool
|
|
||||||
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::Boolean>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName);
|
|
||||||
}
|
|
||||||
BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName));
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enum (FEnumProperty — C++ enum class)
|
|
||||||
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
|
||||||
}
|
|
||||||
FString ValueStr = Json->GetStringField(FieldName);
|
|
||||||
UEnum* Enum = EnumProp->GetEnum();
|
|
||||||
int64 EnumVal = Enum->GetValueByNameString(ValueStr);
|
|
||||||
if (EnumVal == INDEX_NONE)
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
|
||||||
}
|
|
||||||
FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty();
|
|
||||||
UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal);
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enum (FByteProperty with Enum — old-style UENUM)
|
|
||||||
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (ByteProp->Enum)
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
|
||||||
}
|
|
||||||
FString ValueStr = Json->GetStringField(FieldName);
|
|
||||||
int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr);
|
|
||||||
if (EnumVal == INDEX_NONE)
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
|
||||||
}
|
|
||||||
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal);
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
// Plain byte without enum — treat as number
|
|
||||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
|
||||||
}
|
|
||||||
ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName));
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// FMCPJsonObject — stash a JSON object into the struct
|
|
||||||
if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
|
|
||||||
{
|
|
||||||
if (StructProp->Struct == FMCPJsonObject::StaticStruct())
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::Object>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be an object"), *FieldName);
|
|
||||||
}
|
|
||||||
FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr<FMCPJsonObject>(Container);
|
|
||||||
Obj->Json = Json->GetObjectField(FieldName);
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// FMCPJsonArray — stash a JSON array into the struct
|
|
||||||
if (StructProp->Struct == FMCPJsonArray::StaticStruct())
|
|
||||||
{
|
|
||||||
if (!Json->HasTypedField<EJson::Array>(FieldName))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("'%s' must be an array"), *FieldName);
|
|
||||||
}
|
|
||||||
FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr<FMCPJsonArray>(Container);
|
|
||||||
Arr->Array = Json->GetArrayField(FieldName);
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return FString::Printf(TEXT("'%s': unsupported property type '%s'"),
|
|
||||||
*FieldName, *Prop->GetCPPType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert a property name from PascalCase to camelCase, matching JSON conventions.
|
|
||||||
// e.g. "BlueprintName" -> "blueprintName", "PosX" -> "posX"
|
|
||||||
FString PropertyNameToJsonKey(const FString& PropName)
|
|
||||||
{
|
|
||||||
if (PropName.IsEmpty())
|
|
||||||
{
|
|
||||||
return PropName;
|
|
||||||
}
|
|
||||||
FString Result = PropName;
|
|
||||||
Result[0] = FChar::ToLower(Result[0]);
|
|
||||||
return Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace MCPPopulate
|
|
||||||
|
|
||||||
FString MCPUtils::PopulateFromJson(
|
|
||||||
UStruct* StructType,
|
|
||||||
void* Container,
|
|
||||||
const TSharedPtr<FJsonValue>& JsonValue)
|
|
||||||
{
|
|
||||||
if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
|
|
||||||
{
|
|
||||||
return TEXT("Expected a JSON object");
|
|
||||||
}
|
|
||||||
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get());
|
|
||||||
}
|
|
||||||
|
|
||||||
FString MCPUtils::PopulateFromJson(
|
|
||||||
UStruct* StructType,
|
|
||||||
void* Container,
|
|
||||||
const FJsonObject* Json)
|
|
||||||
{
|
|
||||||
// Build a set of known property names (as JSON keys) for the unknown-field check.
|
|
||||||
TSet<FString> KnownKeys;
|
|
||||||
TArray<FProperty*> Properties;
|
|
||||||
|
|
||||||
for (TFieldIterator<FProperty> It(StructType, EFieldIterationFlags::None); It; ++It)
|
|
||||||
{
|
|
||||||
FProperty* Prop = *It;
|
|
||||||
Properties.Add(Prop);
|
|
||||||
KnownKeys.Add(MCPPopulate::PropertyNameToJsonKey(Prop->GetName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for unknown fields in the JSON
|
|
||||||
for (const auto& KV : Json->Values)
|
|
||||||
{
|
|
||||||
if (!KnownKeys.Contains(KV.Key))
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate each property from JSON
|
|
||||||
for (FProperty* Prop : Properties)
|
|
||||||
{
|
|
||||||
FString JsonKey = MCPPopulate::PropertyNameToJsonKey(Prop->GetName());
|
|
||||||
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
|
||||||
|
|
||||||
if (!Json->HasField(JsonKey))
|
|
||||||
{
|
|
||||||
if (!bOptional)
|
|
||||||
{
|
|
||||||
return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString Error = MCPPopulate::SetPropertyFromJson(Container, Prop, JsonKey, Json);
|
|
||||||
if (!Error.IsEmpty())
|
|
||||||
{
|
|
||||||
return Error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return FString();
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "MCPUtils.h"
|
#include "MCPUtils.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
#include "BlueprintActionDatabase.h"
|
#include "BlueprintActionDatabase.h"
|
||||||
#include "BlueprintNodeSpawner.h"
|
#include "BlueprintNodeSpawner.h"
|
||||||
#include "Dom/JsonValue.h"
|
#include "Dom/JsonValue.h"
|
||||||
@@ -1220,3 +1221,220 @@ TArray<UBlueprintNodeSpawner*> MCPUtils::SearchNodeSpawners(const FString& Query
|
|||||||
}
|
}
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// PopulateFromJson — fill a USTRUCT from a JSON object
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
#include "UObject/EnumProperty.h"
|
||||||
|
|
||||||
|
FString MCPUtils::PropertyNameToJsonKey(const FString& PropName)
|
||||||
|
{
|
||||||
|
if (PropName.IsEmpty())
|
||||||
|
{
|
||||||
|
return PropName;
|
||||||
|
}
|
||||||
|
FString Result = PropName;
|
||||||
|
Result[0] = FChar::ToLower(Result[0]);
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString MCPUtils::SetPropertyFromJson(
|
||||||
|
void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json)
|
||||||
|
{
|
||||||
|
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
||||||
|
|
||||||
|
// FString
|
||||||
|
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||||
|
}
|
||||||
|
StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName));
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// int32
|
||||||
|
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||||
|
}
|
||||||
|
IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName));
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// float
|
||||||
|
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||||
|
}
|
||||||
|
FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName));
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// double
|
||||||
|
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||||
|
}
|
||||||
|
DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName));
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// bool
|
||||||
|
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::Boolean>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName);
|
||||||
|
}
|
||||||
|
BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName));
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum (FEnumProperty — C++ enum class)
|
||||||
|
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||||
|
}
|
||||||
|
FString ValueStr = Json->GetStringField(FieldName);
|
||||||
|
UEnum* Enum = EnumProp->GetEnum();
|
||||||
|
int64 EnumVal = Enum->GetValueByNameString(ValueStr);
|
||||||
|
if (EnumVal == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
||||||
|
}
|
||||||
|
FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty();
|
||||||
|
UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal);
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enum (FByteProperty with Enum — old-style UENUM)
|
||||||
|
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (ByteProp->Enum)
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||||
|
}
|
||||||
|
FString ValueStr = Json->GetStringField(FieldName);
|
||||||
|
int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr);
|
||||||
|
if (EnumVal == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
||||||
|
}
|
||||||
|
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal);
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
// Plain byte without enum — treat as number
|
||||||
|
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||||
|
}
|
||||||
|
ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName));
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FMCPJsonObject — stash a JSON object into the struct
|
||||||
|
if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
|
||||||
|
{
|
||||||
|
if (StructProp->Struct == FMCPJsonObject::StaticStruct())
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::Object>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be an object"), *FieldName);
|
||||||
|
}
|
||||||
|
FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr<FMCPJsonObject>(Container);
|
||||||
|
Obj->Json = Json->GetObjectField(FieldName);
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FMCPJsonArray — stash a JSON array into the struct
|
||||||
|
if (StructProp->Struct == FMCPJsonArray::StaticStruct())
|
||||||
|
{
|
||||||
|
if (!Json->HasTypedField<EJson::Array>(FieldName))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("'%s' must be an array"), *FieldName);
|
||||||
|
}
|
||||||
|
FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr<FMCPJsonArray>(Container);
|
||||||
|
Arr->Array = Json->GetArrayField(FieldName);
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FString::Printf(TEXT("'%s': unsupported property type '%s'"),
|
||||||
|
*FieldName, *Prop->GetCPPType());
|
||||||
|
}
|
||||||
|
|
||||||
|
FString MCPUtils::PopulateFromJson(
|
||||||
|
UStruct* StructType,
|
||||||
|
void* Container,
|
||||||
|
const TSharedPtr<FJsonValue>& JsonValue)
|
||||||
|
{
|
||||||
|
if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
|
||||||
|
{
|
||||||
|
return TEXT("Expected a JSON object");
|
||||||
|
}
|
||||||
|
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
FString MCPUtils::PopulateFromJson(
|
||||||
|
UStruct* StructType,
|
||||||
|
void* Container,
|
||||||
|
const FJsonObject* Json)
|
||||||
|
{
|
||||||
|
// Build a set of known property names (as JSON keys) for the unknown-field check.
|
||||||
|
TSet<FString> KnownKeys;
|
||||||
|
TArray<FProperty*> Properties;
|
||||||
|
|
||||||
|
for (TFieldIterator<FProperty> It(StructType, EFieldIterationFlags::None); It; ++It)
|
||||||
|
{
|
||||||
|
FProperty* Prop = *It;
|
||||||
|
Properties.Add(Prop);
|
||||||
|
KnownKeys.Add(PropertyNameToJsonKey(Prop->GetName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unknown fields in the JSON
|
||||||
|
for (const auto& KV : Json->Values)
|
||||||
|
{
|
||||||
|
if (!KnownKeys.Contains(KV.Key))
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate each property from JSON
|
||||||
|
for (FProperty* Prop : Properties)
|
||||||
|
{
|
||||||
|
FString JsonKey = PropertyNameToJsonKey(Prop->GetName());
|
||||||
|
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
||||||
|
|
||||||
|
if (!Json->HasField(JsonKey))
|
||||||
|
{
|
||||||
|
if (!bOptional)
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString Error = SetPropertyFromJson(Container, Prop, JsonKey, Json);
|
||||||
|
if (!Error.IsEmpty())
|
||||||
|
{
|
||||||
|
return Error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,4 +94,8 @@ public:
|
|||||||
// ----- Property population -----
|
// ----- Property population -----
|
||||||
static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
|
static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
|
||||||
static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
|
static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static FString PropertyNameToJsonKey(const FString& PropName);
|
||||||
|
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user