Files
2025-09-18 17:55:52 -04:00

537 lines
18 KiB
C++

#include "stdafx.h"
#include "V8DataModel/AssetService.h"
#include "V8DataModel/ContentProvider.h"
#include "V8DataModel/MarketplaceService.h"
#include "v8datamodel/HttpRbxApiService.h"
#include "Network/Players.h"
#include "Util/RobloxGoogleAnalytics.h"
#include "Util/standardout.h"
#include "Util/LuaWebService.h"
#include "V8Xml/WebParser.h"
#include "Util/Http.h"
#include <boost/lexical_cast.hpp>
DYNAMIC_FASTINTVARIABLE(CreatePlacePerMinute, 5)
DYNAMIC_FASTINTVARIABLE(CreatePlacePerPlayerPerMinute, 1)
DYNAMIC_FASTINTVARIABLE(SavePlacePerMinute, 10)
namespace RBX {
const char* const sAssetService = "AssetService";
REFLECTION_BEGIN();
static Reflection::BoundFuncDesc<AssetService, void(std::string)> func_SetPlaceAccessUrl(&AssetService::setPlaceAccessUrl, "SetPlaceAccessUrl", "accessUrl", Security::LocalUser);
static Reflection::BoundFuncDesc<AssetService, void(std::string)> func_SetAssetRevertUrl(&AssetService::setAssetRevertUrl, "SetAssetRevertUrl", "revertUrl", Security::LocalUser);
static Reflection::BoundFuncDesc<AssetService, void(std::string)> func_setAssetVersionsUrl(&AssetService::setAssetVersionsUrl, "SetAssetVersionsUrl", "versionsUrl", Security::LocalUser);
static Reflection::BoundYieldFuncDesc<AssetService, bool(int, int)> func_revertAsset(&AssetService::revertAsset, "RevertAsset", "placeId", "versionNumber", Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, bool(int, AssetService::AccessType, shared_ptr<const Reflection::ValueArray>)>
func_updatePermissions(&AssetService::setPlacePermissions, "SetPlacePermissions", "placeId", "accessType", AssetService::EVERYONE, "inviteList", shared_ptr<const Reflection::ValueArray>(), Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, shared_ptr<const Reflection::ValueTable>(int)> func_getPlacePermissions(&AssetService::getPlacePermissions, "GetPlacePermissions", "placeId", Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, shared_ptr<const Reflection::ValueTable>(int, int)> func_getAssetVersions(&AssetService::getAssetVersions, "GetAssetVersions", "placeId", "pageNum", 1, Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, int(int)> func_getCreatorAssetID(&AssetService::getCreatorAssetID, "GetCreatorAssetID", "creationID", Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, int(std::string, int, std::string)> func_createPlace
(&AssetService::createPlaceAsync, "CreatePlaceAsync", "placeName","templatePlaceID","description","",Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, int(shared_ptr<Instance>, std::string, int, std::string)> func_createPlaceInPlayerInventory
(&AssetService::createPlaceInPlayerInventoryAsync, "CreatePlaceInPlayerInventoryAsync", "player",
"placeName", "templatePlaceID", "description", "",Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, void()> func_savePlace
(&AssetService::savePlaceAsync, "SavePlaceAsync", Security::None);
static Reflection::BoundYieldFuncDesc<AssetService, shared_ptr<Instance>()>
func_getGamePlacesAsync(&AssetService::getGamePlacesAsync, "GetGamePlacesAsync", Security::None);
REFLECTION_END();
namespace Reflection {
template<>
EnumDesc<AssetService::AccessType>::EnumDesc()
:EnumDescriptor("AccessType")
{
addPair(AssetService::ME, "Me");
addPair(AssetService::FRIENDS, "Friends");
addPair(AssetService::EVERYONE, "Everyone");
addPair(AssetService::INVITEONLY, "InviteOnly");
}
template<>
AssetService::AccessType& Variant::convert<AssetService::AccessType>(void)
{
return genericConvert<AssetService::AccessType>();
}
}//namespace Reflection
template<>
bool StringConverter<AssetService::AccessType>::convertToValue(const std::string& text, AssetService::AccessType& value)
{
return Reflection::EnumDesc<AssetService::AccessType>::singleton().convertToValue(text.c_str(),value);
}
AssetService::AssetService()
:placeAccessUrl("")
,assetRevertUrl("")
,assetVersionsUrl("")
,createPlaceThrottle(&DFInt::CreatePlacePerMinute, &DFInt::CreatePlacePerPlayerPerMinute)
,savePlaceThrottle(&DFInt::SavePlacePerMinute)
{
setName(sAssetService);
}
void AssetService::setPlaceAccessUrl(std::string url)
{
placeAccessUrl = url;
}
void AssetService::setAssetRevertUrl(std::string url)
{
assetRevertUrl = url;
}
void AssetService::setAssetVersionsUrl(std::string url)
{
assetVersionsUrl = url;
}
std::string enumToString(AssetService::AccessType eType)
{
switch(eType)
{
case AssetService::ME:
return std::string("Me");
case AssetService::FRIENDS:
return std::string("Friends");
case AssetService::EVERYONE:
return std::string("Everyone");
case AssetService::INVITEONLY:
return std::string("InviteOnly");
default:
return std::string("Me");
}
}
void AssetService::processServiceResults(std::string *result, const std::exception* httpException, boost::function<void(shared_ptr<const Reflection::ValueTable>)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(!httpException)
{
if(!result || result->empty())
{
errorFunction("Invalid response received");
}
shared_ptr<const Reflection::ValueTable> lTable;
std::stringstream resultStream;
resultStream << *result;
if( WebParser::parseJSONTable(*result, lTable) )
resumeFunction(lTable);
else
errorFunction("AssetService error occurred");
}
else
{
errorFunction(RBX::format("Http exception occurred %s", httpException->what()));
}
}
namespace {
static void sendCreatePlaceStats()
{
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "CreatePlace");
}
static void sendCreatePlaceInPlayerInventoryStats()
{
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "CreatePlaceInPlayerInventory");
}
} // namespace
static void CreatePlaceSuccessHelper(weak_ptr<DataModel> dm, const std::string& response, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if (shared_ptr<DataModel> sharedDM = dm.lock())
{
if (!response.empty())
{
int newPlaceID = 0;
try
{
newPlaceID = boost::lexical_cast<int>(response.c_str());
}
catch (boost::bad_lexical_cast const& e)
{
errorFunction( RBX::format("Game:CreatePlace response was not a valid placeId because %s",e.what()) );
return;
}
if (newPlaceID > 0)
{
resumeFunction( newPlaceID );
}
else
{
errorFunction("Game:CreatePlace response was not a valid placeId because id <= 0");
}
}
else
{
errorFunction("Game:CreatePlace had no valid response");
}
}
}
static void CreatePlaceErrorHelper(weak_ptr<DataModel> dm, const std::string& error, boost::function<void(std::string)> errorFunction)
{
if (shared_ptr<DataModel> sharedDM = dm.lock())
{
if (!error.empty())
{
errorFunction( RBX::format("Game:CreatePlace received and error: %s.",error.c_str()) );
}
else
{
errorFunction("Game:CreatePlace had no valid response in error helper.");
}
}
}
bool AssetService::checkCreatePlaceAccess(const std::string& placeName, int templatePlaceId, std::string& message)
{
DataModel* dm = DataModel::get(this);
RBXASSERT(dm);
if(!Network::Players::backendProcessing(this))
{
message = "CreatePlaceAsync can only be called from a server script, aborting create function";
return false;
}
if (placeName.empty())
{
message = "CreatePlaceAsync placeName argument is empty!";
return false;
}
if (templatePlaceId <= 0)
{
message = "CreatePlaceAsync templatePlaceId <= 0, should be a positive value";
return false;
}
if(dm->getPlaceID() <= 0)
{
message = "CreatePlaceAsync called on a Place with id <= 0, place should be opened with Edit button to access CreatePlace";
return false;
}
if(!createPlaceThrottle.checkLimit(Network::Players::getPlayerCount(this)))
{
message = "CreatePlaceAsync requests limit reached";
return false;
}
if (LuaWebService* lws = dm->create<LuaWebService>())
{
if (!lws->isApiAccessEnabled())
{
message = "Studio API access is not enabled. Enable it by going to the Game Settings page.";
return false;
}
}
return true;
}
void AssetService::createPlaceAsync(std::string placeName, int templatePlaceId, std::string desc,
boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
std::string message;
if(!checkCreatePlaceAccess(placeName, templatePlaceId, message))
{
errorFunction(message);
return;
}
createPlaceAsyncInternal(true, placeName, templatePlaceId, desc, shared_ptr<Instance>(), resumeFunction, errorFunction);
}
void AssetService::createPlaceInPlayerInventoryAsync(shared_ptr<Instance> player, std::string placeName, int templatePlaceId,
std::string desc, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
std::string message;
if(!checkCreatePlaceAccess(placeName, templatePlaceId, message))
{
errorFunction(message);
return;
}
MarketplaceService* marketPlaceService = ServiceProvider::create<MarketplaceService>(this);
marketPlaceService->launchClientLuaDialog("Do you allow game to create new place in your inventory?", "Yes", "No", player,
boost::bind(&AssetService::createPlaceAsyncInternal, this, _1, placeName, templatePlaceId, desc, _2, resumeFunction, errorFunction));
}
void AssetService::createPlaceAsyncInternal(bool check, std::string placeName, int templatePlaceId, std::string desc,
shared_ptr<Instance> playerInstance, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(!check)
{
StandardOut::singleton()->print(MESSAGE_WARNING, "User didn't allow place creation");
resumeFunction(-1);
return;
}
shared_ptr<Network::Player> player = fastSharedDynamicCast<Network::Player>(playerInstance);
if(player && player->getUserID() <= 0)
{
errorFunction("CreatePlaceAsync requires player to be logged in");
return;
}
if(player)
{
Network::Players* players = ServiceProvider::find<Network::Players>(this);
if(!players || players->getPlayerByID(player->getUserID()) != player)
{
errorFunction("Player no longer valid");
return;
}
}
DataModel* dm = DataModel::get(this);
std::string parameters = RBX::format("?currentPlaceId=%d&placeName=%s&templatePlaceId=%d",
dm->getPlaceID(),RBX::Http::urlEncode(placeName).c_str(),templatePlaceId);
std::string baseUrl = ServiceProvider::create<ContentProvider>(this)->getApiBaseUrl();
std::string playerParameter = "";
if(player)
playerParameter = RBX::format("&playerId=%i", player->getUserID());
if (player)
{
static boost::once_flag flag = BOOST_ONCE_INIT;
boost::call_once(&sendCreatePlaceInPlayerInventoryStats, flag);
}
else
{
static boost::once_flag flag = BOOST_ONCE_INIT;
boost::call_once(&sendCreatePlaceStats, flag);
}
try
{
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
// now post to the website
Http createHttp((baseUrl + "universes/new-place" + parameters + playerParameter).c_str());
#ifndef _WIN32
createHttp.setAuthDomain(baseUrl);
#endif
std::string postString("CreatePlacePost");
apiService->postAsync(createHttp, postString, RBX::PRIORITY_SERVER_ELEVATED, HttpService::TEXT_PLAIN,
boost::bind(&CreatePlaceSuccessHelper, shared_from(dm), _1, resumeFunction, errorFunction),
boost::bind(&CreatePlaceErrorHelper, shared_from(dm), _1, errorFunction));
}
}
catch (base_exception &e)
{
errorFunction( RBX::format("Game:CreatePlace failed because %s",e.what()) );
}
}
namespace {
static void sendSavePlaceStats()
{
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SavePlace");
}
} // namespace
void AssetService::savePlaceAsync(boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
{
DataModel* dm = DataModel::get(this);
dm->checkFetchExperimentalFeatures();
if (dm->getPlaceID() <= 0)
{
errorFunction("Game:SavePlace placeID is not valid!");
return;
}
if(!savePlaceThrottle.checkLimit())
{
errorFunction("Game:SavePlace requests limit reached");
return;
}
if(Network::Players::frontendProcessing(this))
{
errorFunction("Game:SavePlace can only be called from a server script, aborting save function");
return;
}
else if(Network::Players::backendProcessing(this))
{
if (LuaWebService* lws = dm->create<LuaWebService>())
{
if (!lws->isApiAccessEnabled())
{
errorFunction("Studio API access is not enabled. Enable it by going to the Game Settings page.");
return;
}
}
{
static boost::once_flag flag = BOOST_ONCE_INIT;
boost::call_once(&sendSavePlaceStats, flag);
}
// construct the url
std::string parameters = "?assetId=" + boost::lexical_cast<std::string>(dm->getPlaceID());
parameters += "&isAppCreation=true";
std::string baseUrl = ServiceProvider::create<ContentProvider>(this)->getBaseUrl();
// save the place
dm->uploadPlace(baseUrl + "ide/publish/UploadExistingAsset" + parameters, SAVE_ALL, resumeFunction, errorFunction);
}
else
{
errorFunction("Game:SavePlace could not determine if client or server");
return;
}
}
void AssetService::getAssetVersions(int assetId, int pageNum, boost::function<void(shared_ptr<const Reflection::ValueTable>)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
Http http(RBX::format(assetVersionsUrl.c_str(), assetId, pageNum));
http.get(boost::bind(&AssetService::processServiceResults, this, _1, _2, resumeFunction, errorFunction));
}
void AssetService::getPlacePermissions(int placeId, boost::function<void(shared_ptr<const Reflection::ValueTable>)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
Http http(RBX::format(placeAccessUrl.c_str(), placeId));
http.get(boost::bind(&AssetService::processServiceResults, this, _1, _2, resumeFunction, errorFunction));
}
void AssetService::httpPostHelper(std::string* response, std::exception* httpException, boost::function<void(bool)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(!httpException)
{
if(!response)
resumeFunction(false);
if(*response=="")
resumeFunction(false);
else
resumeFunction(true);
}
else
{
errorFunction(httpException->what());
}
}
void AssetService::revertAsset(int assetId, int versionNumber, boost::function<void(bool)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
Http http(RBX::format(assetRevertUrl.c_str(), assetId, versionNumber));
std::string in;
http.post(in, Http::kContentTypeDefaultUnspecified, true,
boost::bind(&AssetService::httpPostHelper, this, _1, _2, resumeFunction, errorFunction));
}
void AssetService::setPlacePermissions(int placeId, AccessType type, shared_ptr<const Reflection::ValueArray> inviteList,
boost::function<void(bool)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
std::string url = placeAccessUrl;
url = RBX::format(url.append("/update?").c_str(), placeId);
url = RBX::format(url.append("access=%s").c_str(), enumToString(type).c_str());
if(type == INVITEONLY && inviteList)
{
for(Reflection::ValueArray::const_iterator iter = inviteList->begin(); iter != inviteList->end(); ++iter)
{
std::stringstream convert;
convert << iter->get<std::string>();
url.append("&players=").append(convert.str());
}
}
Http http(url);
std::string in;
http.post(in, Http::kContentTypeDefaultUnspecified, true,
boost::bind(&AssetService::httpPostHelper, this, _1, _2, resumeFunction, errorFunction));
}
void AssetService::getCreatorAssetIDSuccessHelper(std::string response, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if( response.length() == 0 )
{
resumeFunction(0);
return;
}
int creatorID = 0;
try
{
creatorID = boost::lexical_cast<int>(response);
}
catch( boost::bad_lexical_cast const& e)
{
errorFunction( RBX::format("AssetService:GetCreatorAssetID response could not convert to number because %s",e.what()) );
return;
}
if(creatorID <= 0)
{
errorFunction("AssetService:GetCreatorAssetID response converted but is an invalid assetID");
return;
}
resumeFunction(creatorID);
}
void AssetService::getCreatorAssetIDErrorHelper(std::string error, boost::function<void(std::string)> errorFunction)
{
if (error.length() == 0)
{
errorFunction("AssetService::GetCreatorAssetID did not get a response or an error.");
}
errorFunction(RBX::format("AssetService:GetCreatorAssetID error: %s", error.c_str()));
}
void AssetService::getCreatorAssetID(int creationID, boost::function<void(int)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if (creationID <= 0)
{
errorFunction("creationID is not a valid number (should be a positive integer)");
return;
}
std::string baseUrl = RBX::ServiceProvider::create<ContentProvider>(this)->getApiBaseUrl();
std::string parameters = RBX::format("?creationID=%d",creationID);
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
apiService->getAsync("GetCreatorAssetID" + parameters, true, RBX::PRIORITY_DEFAULT,
boost::bind(&AssetService::getCreatorAssetIDSuccessHelper, this, _1, resumeFunction, errorFunction),
boost::bind(&AssetService::getCreatorAssetIDErrorHelper, this, _1, errorFunction) );
}
}
void AssetService::getGamePlacesAsync(boost::function<void(shared_ptr<Instance>) > resumeFunction, boost::function<void(std::string)> errorFunction)
{
std::string fetchUrl;
ContentProvider* cp = DataModel::get(this)->find<ContentProvider>();
if (cp)
fetchUrl = format("%suniverses/get-universe-places?placeid=%i", cp->getApiBaseUrl().c_str(), DataModel::get(this)->getPlaceID());
shared_ptr<Pages> pagination = Creatable<Instance>::create<StandardPages>(weak_from(DataModel::get(this)), fetchUrl, "Places");
pagination->fetchNextChunk(boost::bind(resumeFunction, pagination), errorFunction);
}
}