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

993 lines
43 KiB
C++

#include "stdafx.h"
#include "V8DataModel/MarketplaceService.h"
#include "v8datamodel/HttpRbxApiService.h"
#include "Network/Player.h"
#include "Network/Players.h"
#include "Util/LuaWebService.h"
#include "Util/Analytics.h"
#include "Util/RobloxGoogleAnalytics.h"
#include "v8xml/WebParser.h"
#include <boost/algorithm/string.hpp>
DYNAMIC_FASTFLAGVARIABLE(CheckMarketplaceAvailable, false)
DYNAMIC_FASTFLAGVARIABLE(Order66, false)
DYNAMIC_FASTINTVARIABLE(ExpireMarketPlaceServiceCacheSeconds, 60)
DYNAMIC_FASTFLAGVARIABLE(RestrictSales, false)
FASTFLAGVARIABLE(UseNewPromptEndHandling, false)
// When the client reports an asset was purchased in game, make a second call to verify.
DYNAMIC_FASTFLAGVARIABLE(DoubleCheckPurchase, true)
// In the event the web call generates an error, allow the client message to pass
DYNAMIC_FASTFLAGVARIABLE(AllowClientFallback, true)
DYNAMIC_FASTFLAGVARIABLE(IgnoreDifferentPlayer, true)
// Reporting Rates.
DYNAMIC_FASTINTVARIABLE(PurchaseMismatchReportRate, 100)
DYNAMIC_FASTINTVARIABLE(PurchaseErrorReportRate, 100)
namespace RBX
{
REFLECTION_BEGIN();
// backend
static Reflection::BoundFuncDesc<MarketplaceService, void(int, int, bool)> func_signalPromptProductFinished(&MarketplaceService::signalPromptProductPurchaseFinished, "SignalPromptProductPurchaseFinished", "userId", "productId", "success", Security::RobloxScript);
static Reflection::BoundFuncDesc<MarketplaceService, void(shared_ptr<Instance>, int, bool)> func_signalPromptPurchaseFinished(&MarketplaceService::signalPromptPurchaseFinished, "SignalPromptPurchaseFinished", "player", "assetId", "success", Security::RobloxScript);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, int, bool, MarketplaceService::CurrencyType)> event_promptPurchaseRequested(&MarketplaceService::promptPurchaseRequested, "PromptPurchaseRequested", "player", "assetId", "equipIfPurchased", "currencyType", Security::RobloxScript, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::BROADCAST);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, int, bool, MarketplaceService::CurrencyType)> event_promptProductPurchaseRequested(&MarketplaceService::promptProductPurchaseRequested, "PromptProductPurchaseRequested", "player", "productId", "equipIfPurchased", "currencyType", Security::RobloxScript, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::BROADCAST);
static Reflection::BoundFuncDesc<MarketplaceService, void(std::string, int, int)> func_signalClientPurchaseSuccess(&MarketplaceService::signalClientPurchaseSuccess, "SignalClientPurchaseSuccess", "ticket", "playerId", "productId", Security::RobloxScript);
static Reflection::RemoteEventDesc<MarketplaceService, void(std::string, int, int)> event_clientPurchaseSuccess(&MarketplaceService::clientPurchaseSuccess, "ClientPurchaseSuccess", "ticket", "playerId", "productId", Security::RobloxScript, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::CLIENT_SERVER);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<const Reflection::ValueTable>)> event_serverPurchaseVerification(&MarketplaceService::serverPurchaseVerification, "ServerPurchaseVerification", "serverResponseTable", Security::RobloxScript, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::BROADCAST);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, std::string)> event_promptThirdPartyPurchaseRequested(&MarketplaceService::promptThirdPartyPurchaseRequested, "PromptThirdPartyPurchaseRequested", "player", "productId", Security::Roblox, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::BROADCAST);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, std::string)> event_promptNativePurchaseRequested(&MarketplaceService::promptNativePurchaseRequested, "PromptNativePurchaseRequested", "player", "productId", Security::Roblox, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::BROADCAST);
// frontend
// used by core script to automatically convert app store product purchases into roblox items
static Reflection::BoundFuncDesc<MarketplaceService, void(shared_ptr<Instance>, std::string)> func_PromptNativePurchase(&MarketplaceService::promptNativePurchase, "PromptNativePurchase", "player", "productId", Security::RobloxScript);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, std::string, bool)> event_PromptNativePurchaseFinished(&MarketplaceService::nativePurchaseFinished, "NativePurchaseFinished", "player","productId","wasPurchased", Security::RobloxScript, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::CLIENT_SERVER);
// used by some roblox created games to do direct purchases from app stores
static Reflection::BoundFuncDesc<MarketplaceService, void(shared_ptr<Instance>, std::string)> func_PromptTPPurchase(&MarketplaceService::promptThirdPartyPurchase, "PromptThirdPartyPurchase", "player", "productId", Security::RobloxPlace);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, std::string, std::string, bool)> event_PromptTPPurchaseFinished(&MarketplaceService::thirdPartyPurchaseFinished, "ThirdPartyPurchaseFinished", "player","productId","receipt","wasPurchased", Security::RobloxPlace, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::CLIENT_SERVER);
// rbx dev purchasing calls
static Reflection::BoundYieldFuncDesc<MarketplaceService, shared_ptr<const Reflection::ValueTable>(int, MarketplaceService::InfoType)> func_getProductInfo(&MarketplaceService::getProductInfo, "GetProductInfo", "assetId","infoType",MarketplaceService::INFO_ASSET, Security::None);
static Reflection::BoundYieldFuncDesc<MarketplaceService, bool(shared_ptr<Instance>, int)> func_playerOwnsAsset(&MarketplaceService::playerOwnsAsset, "PlayerOwnsAsset", "player", "assetId", Security::None);
static Reflection::BoundFuncDesc<MarketplaceService, void(shared_ptr<Instance>, int, bool, MarketplaceService::CurrencyType)> func_PromptPurchase(&MarketplaceService::promptPurchase, "PromptPurchase", "player", "assetId", "equipIfPurchased", true, "currencyType",MarketplaceService::CURRENCY_DEFAULT, Security::None);
static Reflection::BoundFuncDesc<MarketplaceService, void(shared_ptr<Instance>, int, bool, MarketplaceService::CurrencyType)> func_PromptProductPurchase(&MarketplaceService::promptProductPurchase, "PromptProductPurchase", "player", "productId", "equipIfPurchased", true, "currencyType",MarketplaceService::CURRENCY_DEFAULT, Security::None);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<Instance>, int, bool)> event_promptPurchaseFinished(&MarketplaceService::promptPurchaseFinished, "PromptPurchaseFinished", "player", "assetId", "isPurchased", Security::None, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::BROADCAST);
static Reflection::RemoteEventDesc<MarketplaceService, void(int, int, bool)> event_promptProductPurchaseFinished(&MarketplaceService::promptProductPurchaseFinished, "PromptProductPurchaseFinished", "userId", "productId", "isPurchased", Security::None, Reflection::RemoteEventCommon::Attributes::deprecated(Reflection::RemoteEventCommon::SCRIPTING, NULL), Reflection::RemoteEventCommon::BROADCAST);
static Reflection::RemoteEventDesc<MarketplaceService, void(shared_ptr<const Reflection::Tuple>)> event_ClientLuaDialogRequested(&MarketplaceService::clientLuaDialogRequested, "ClientLuaDialogRequested", "arguments", Security::RobloxScript, Reflection::RemoteEventCommon::SCRIPTING, Reflection::RemoteEventCommon::CLIENT_SERVER);
static Reflection::RemoteEventDesc<MarketplaceService, void(bool, shared_ptr<Instance>)> event_luaDialogCallbackSignal(&MarketplaceService::luaDialogCallbackSignal, "LuaDialogCallbackSignal", "value", "player", Security::RobloxScript, Reflection::RemoteEventCommon::REPLICATE_ONLY, Reflection::RemoteEventCommon::CLIENT_SERVER);
static Reflection::BoundFuncDesc<MarketplaceService, void(bool)> func_signalServerLuaDialogClosed(&MarketplaceService::signalServerLuaDialogClosed, "SignalServerLuaDialogClosed", "value", Security::RobloxScript);
static Reflection::BoundYieldFuncDesc<MarketplaceService, shared_ptr<Instance>()>
func_getDeveloperProductsAsync(&MarketplaceService::getDeveloperProductsAsync, "GetDeveloperProductsAsync", Security::None);
static Reflection::BoundAsyncCallbackDesc<
MarketplaceService, MarketplaceService::ProductPurchaseDecision(
shared_ptr<const Reflection::ValueTable>)> callback_ProcessReceipt(
"ProcessReceipt", &MarketplaceService::receiptCallback, "receiptInfo",
&MarketplaceService::processReceiptsCallbackChanged);
REFLECTION_END();
const char* const sMarketplaceService = "MarketplaceService";
namespace Reflection {
template<>
EnumDesc<MarketplaceService::CurrencyType>::EnumDesc()
:EnumDescriptor("CurrencyType")
{
addPair(MarketplaceService::CURRENCY_DEFAULT, "Default");
addPair(MarketplaceService::CURRENCY_ROBUX, "Robux");
addPair(MarketplaceService::CURRENCY_TIX, "Tix");
}
template<>
MarketplaceService::CurrencyType& Variant::convert<MarketplaceService::CurrencyType>(void)
{
return genericConvert<MarketplaceService::CurrencyType>();
}
template<>
EnumDesc<MarketplaceService::InfoType>::EnumDesc()
:EnumDescriptor("InfoType")
{
addPair(MarketplaceService::INFO_ASSET, "Asset");
addPair(MarketplaceService::INFO_PRODUCT, "Product");
}
template<>
MarketplaceService::InfoType& Variant::convert<MarketplaceService::InfoType>(void)
{
return genericConvert<MarketplaceService::InfoType>();
}
template<>
EnumDesc<MarketplaceService::ProductPurchaseDecision>::EnumDesc()
:EnumDescriptor("ProductPurchaseDecision")
{
addPair(MarketplaceService::DECISION_NOT_PROCESSED_YET, "NotProcessedYet");
addPair(MarketplaceService::DECISION_PURCHASE_GRANTED, "PurchaseGranted");
}
template<>
MarketplaceService::ProductPurchaseDecision& Variant::convert<MarketplaceService::ProductPurchaseDecision>(void)
{
return genericConvert<MarketplaceService::ProductPurchaseDecision>();
}
} // namespace Reflection
template<>
bool StringConverter<MarketplaceService::CurrencyType>::convertToValue(const std::string& text, MarketplaceService::CurrencyType& value)
{
return Reflection::EnumDesc<MarketplaceService::CurrencyType>::singleton().convertToValue(text.c_str(), value);
}
template<>
bool StringConverter<MarketplaceService::InfoType>::convertToValue(const std::string& text, MarketplaceService::InfoType& value)
{
return Reflection::EnumDesc<MarketplaceService::InfoType>::singleton().convertToValue(text.c_str(), value);
}
template<>
bool StringConverter<MarketplaceService::ProductPurchaseDecision>::convertToValue(const std::string& text, MarketplaceService::ProductPurchaseDecision& value)
{
return Reflection::EnumDesc<MarketplaceService::ProductPurchaseDecision>::singleton().convertToValue(text.c_str(), value);
}
MarketplaceService::MarketplaceService() :
productInfoUrl(std::string(urlApiPath()) + "/productinfo?assetId=%d")
, playerOwnsAssetUrl("ownership/hasasset?userId=%d&assetId=%d")
, devProductInfoUrl(std::string(urlApiPath()) + "/productDetails?productId=%d")
, clientLuaDialogRequested()
, luaDialogCallbackSignal()
, receiptProcessingEnabledByUser(false)
{
setName(sMarketplaceService);
luaDialogCallbackSignal.connect(boost::bind(&MarketplaceService::executeClientDialogCallback, this, _1, _2));
}
bool MarketplaceService::isValidPlayer(const shared_ptr<Instance> player, const std::string& funcName, boost::function<void(std::string)> errorFunction)
{
if(Network::Player* thePlayer = Instance::fastDynamicCast<Network::Player>(player.get()))
{
if( thePlayer->getUserID() <=0 ) // warn when we use a guest player (otherwise this call will fail in all local test scenarios!)
{
if (funcName != "PromptPurchase()" && funcName != "PromptProductPurchase()")
{
std::string longFuncName = RBX::format("MarketplaceService:%s",funcName.c_str());
longFuncName.append(" was called on a guest, please use this call only on players with an account.");
RBX::StandardOut::singleton()->print(RBX::MESSAGE_WARNING, longFuncName);
return false;
}
}
}
else // we don't have a player type object, are you even trying?
{
std::string longFuncName = RBX::format("MarketplaceService:%s",funcName.c_str());
longFuncName.append(" player should be of type Player, but is of type ");
longFuncName.append(player ? player->getClassNameStr().c_str() : "nil");
if(errorFunction)
errorFunction(longFuncName);
else
throw std::runtime_error(longFuncName);
return false;
}
return true;
}
void MarketplaceService::promptProductPurchase(const shared_ptr<Instance> player, const int productId, const bool equipIfPurchased, const CurrencyType currencyType)
{
if(!isValidPlayer(player,"PromptProductPurchase()"))
return;
if(Network::Players* players = ServiceProvider::find<RBX::Network::Players>(this))
if(Network::Players::frontendProcessing(this) && (player.get() != players->getLocalPlayer()) )
{
RBX::StandardOut::singleton()->printf( RBX::MESSAGE_WARNING, "MarketplaceService:PromptProductPurchase called from a local script, but not called on a local player. Local scripts can only prompt the local player.");
return;
}
if(productId <= 0)
throw RBX::runtime_error("MarketplaceService:PromptPurchase() second argument is not a valid productId (supplied productId was less than 0)");
DataModel* dm = DataModel::get(this);
RBXASSERT(dm);
if (dm)
if (LuaWebService* lws = dm->create<LuaWebService>())
if (!lws->isApiAccessEnabled())
throw RBX::runtime_error("Studio API access is not enabled. Enable it by going to the Game Settings page.");
// fire a signal so our purchase script can show the purchase ui and eventually call signalPromptProductPurchaseFinished
event_promptProductPurchaseRequested.fireAndReplicateEvent(this, player,productId,equipIfPurchased,currencyType);
}
void MarketplaceService::promptPurchase(const shared_ptr<Instance> player, const int assetId, const bool equipIfPurchased, const CurrencyType currencyType)
{
if(!isValidPlayer(player,"PromptPurchase()"))
return;
if(Network::Players* players = ServiceProvider::find<RBX::Network::Players>(this))
if(Network::Players::frontendProcessing(this) && (player.get() != players->getLocalPlayer()) )
{
RBX::StandardOut::singleton()->printf( RBX::MESSAGE_WARNING, "MarketplaceService:PromptPurchase called from a local script, but not called on a local player. Local scripts can only prompt the local player.");
return;
}
if(assetId <= 0)
throw RBX::runtime_error("MarketplaceService:PromptPurchase() second argument is not a valid assetId (supplied assetId was less than 0)");
DataModel* dm = DataModel::get(this);
RBXASSERT(dm);
if (dm)
if (LuaWebService* lws = dm->create<LuaWebService>())
if (!lws->isApiAccessEnabled())
throw RBX::runtime_error("Studio API access is not enabled. Enable it by going to the Game Settings page.");
// fire a signal so our purchase script can show the purchase ui and eventually call signalPromptPurchaseFinished
event_promptPurchaseRequested.fireAndReplicateEvent(this, player,assetId,equipIfPurchased,currencyType);
}
void MarketplaceService::signalPromptPurchaseFinished(const shared_ptr<Instance> player, const int assetId, const bool isPurchase)
{
event_promptPurchaseFinished.fireAndReplicateEvent(this, player, assetId, isPurchase);
}
void MarketplaceService::processRemoteEvent(const Reflection::EventDescriptor& descriptor, const Reflection::EventArguments& args, const SystemAddress& source)
{
if (DFFlag::DoubleCheckPurchase && descriptor == event_promptPurchaseFinished )
{
if (args.size() != 3 || !args[2].isType<bool>() || !args[1].isType<int>() || !args[0].isType<shared_ptr<Instance> >())
{
return;
}
bool isPurchase = args[2].cast<bool>();
if (isPurchase)
{
shared_ptr<Instance> playerInstance = args[0].cast<shared_ptr<Instance> >();
shared_ptr<Network::Player> player = Instance::fastSharedDynamicCast<Network::Player>(playerInstance);
if (!player)
{
return;
}
// it is possible game logic could ban players who fake purchases. This would result in a way
// to get other players banned by spoofing.
const SystemAddress playerSystemAddress = player->getRemoteAddressAsRbxAddress();
if (DFFlag::IgnoreDifferentPlayer && playerSystemAddress != source)
{
return;
}
weak_ptr<DataModel> weakDataModel = weak_from(DataModel::get(this));
int assetId = args[1].cast<int>();
playerOwnsAsset(player, assetId,
boost::bind(&MarketplaceService::processPurchaseFinished, weakDataModel, args, source, _1), // the arguments should be passed by value.
boost::bind(&MarketplaceService::processPurchaseError, weakDataModel, args, source, _1)); // the arguments should be passed by value.
}
else
{
Super::processRemoteEvent(descriptor, args, source);
}
}
else
{
Super::processRemoteEvent(descriptor, args, source);
}
}
void MarketplaceService::processPurchaseFinished( weak_ptr<DataModel> weakDm, Reflection::EventArguments args, SystemAddress source, const bool isOwner)
{
bool isPurchase = args[2].cast<bool>();
// purchase own = valid purchase
// purchase !own = exploit
// !purchase own = impossible
// !purchase !own = valid non-purchase
// (If a developer gives the player some additional benefits at time of purchase, this will still allow re-triggering).
if (isPurchase == isOwner)
{
if (shared_ptr<DataModel> dm = weakDm.lock())
{
// some of the arguments are copied as this is async
dm->submitTask(boost::bind(&MarketplaceService::processPurchaseFinishedCallback, _1, args, source), DataModelJob::Write);
}
}
else
{
Analytics::InfluxDb::Points purchaseMismatchAnalytics;
purchaseMismatchAnalytics.addPoint("isPurchase", isPurchase);
purchaseMismatchAnalytics.addPoint("isOwner", isOwner);
purchaseMismatchAnalytics.report("purchaseVerificationMismatch", DFInt::PurchaseMismatchReportRate);
}
}
void MarketplaceService::processPurchaseError( weak_ptr<DataModel> weakDm, Reflection::EventArguments args, SystemAddress source, const std::string msg)
{
Analytics::InfluxDb::Points purchaseErrorAnalytics;
purchaseErrorAnalytics.addPoint("msg", msg.c_str());
purchaseErrorAnalytics.report("purchaseVerificationError", DFInt::PurchaseErrorReportRate);
// There was an error with the web. It may be safer to assume the client is telling the truth.
if (DFFlag::AllowClientFallback)
{
if (shared_ptr<DataModel> dm = weakDm.lock())
{
// some of the arguments are copied as this is async
dm->submitTask(boost::bind(&MarketplaceService::processPurchaseFinishedCallback, _1, args, source), DataModelJob::Write);
}
}
}
void MarketplaceService::processPurchaseFinishedCallback(DataModel* dm, Reflection::EventArguments args, SystemAddress source)
{
if (MarketplaceService* market = dm->find<MarketplaceService>())
{
market->Super::processRemoteEvent(event_promptPurchaseFinished, args, source);
}
}
void MarketplaceService::signalPromptProductPurchaseFinished(const int userId, const int productId, const bool success)
{
event_promptProductPurchaseFinished.fireAndReplicateEvent(this,userId,productId,success);
}
template<typename ResultType>
void MarketplaceService::dispatchRequest(const std::string& url, boost::function<void(ResultType)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(LuaWebService* luaWebService = ServiceProvider::create<LuaWebService>(this))
{
try
{
luaWebService->asyncRequest(url, LUA_WEB_SERVICE_STANDARD_PRIORITY, resumeFunction, errorFunction);
}
catch(RBX::base_exception&)
{
errorFunction("MarketplaceService:getProductInfo() - Error during dispatch");
}
}
else
{
errorFunction("MarketplaceService:getProductInfo() - Shutting down");
}
}
void MarketplaceService::getProductInfo(const int assetId, const MarketplaceService::InfoType infoType, boost::function<void(shared_ptr<const Reflection::ValueTable>)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(infoType == MarketplaceService::INFO_ASSET && productInfoUrl.empty())
{
errorFunction("MarketplaceService:GetProductInfo() productInfoUrl is empty");
return;
}
else if (infoType == MarketplaceService::INFO_PRODUCT && devProductInfoUrl.empty())
{
errorFunction("MarketplaceService:GetProductInfo() devProductInfoUrl is empty");
return;
}
else if(infoType == MarketplaceService::INFO_NONE)
{
errorFunction("MarketplaceService:GetProductInfo() was passed an invalid info type (NONE)");
return;
}
if(assetId <= 0)
{
errorFunction("MarketplaceService:GetProductInfo() argument is not a valid assetId (supplied assetId was less than 0)");
return;
}
std::string url = "";
if (ContentProvider* contentProvider = ServiceProvider::create<ContentProvider>(this))
{
url = contentProvider->getApiBaseUrl();
}
if( infoType == MarketplaceService::INFO_ASSET )
url += productInfoUrl;
else if( infoType == MarketplaceService::INFO_PRODUCT)
url += devProductInfoUrl;
if(url.empty())
{
errorFunction("MarketplaceService:GetProductInfo() could not find correct url to call");
return;
}
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
std::string infoPath = "";
if( infoType == MarketplaceService::INFO_ASSET )
infoPath = productInfoUrl;
else if( infoType == MarketplaceService::INFO_PRODUCT)
infoPath = devProductInfoUrl;
std::string infoPathFull(RBX::format(infoPath.c_str(),assetId));
UrlToResponseMap::const_iterator it = mMap.find(infoPathFull);
if(it != mMap.end())
{
ResponseCache cachedResponse = it->second;
Time::Interval elapsedTime = Time::now<Time::Fast>() - cachedResponse.lastFetch;
if(elapsedTime < Time::Interval(DFInt::ExpireMarketPlaceServiceCacheSeconds))
{
resumeFunction(cachedResponse.values);
return;
}
}
apiService->getAsync(infoPathFull, true, RBX::PRIORITY_DEFAULT,
boost::bind(&MarketplaceService::onReceivedRawProductInfoSuccess, this, infoPathFull, _1, weak_from(RBX::DataModel::get(this)), resumeFunction, errorFunction),
boost::bind(&MarketplaceService::onReceivedRawProductInfoError, this, _1, weak_from(RBX::DataModel::get(this)), errorFunction) );
}
}
void MarketplaceService::onReceivedRawProductInfoSuccess(std::string url, std::string response, boost::weak_ptr<RBX::DataModel> weakDM, boost::function<void(shared_ptr<const Reflection::ValueTable>)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(shared_ptr<RBX::DataModel> sharedDM = weakDM.lock())
{
if( response.empty() )
{
errorFunction( RBX::format("MarketplaceService:getProductInfo() failed because rawProductInfo was empty") );
return;
}
shared_ptr<const Reflection::ValueTable> values;
std::stringstream rawProductStream;
rawProductStream << response;
if( WebParser::parseJSONTable(response, values) )
{
resumeFunction(values);
ResponseCache responseToCache;
responseToCache.lastFetch = Time::now<Time::Fast>();
responseToCache.values = values;
mMap[url] = responseToCache;
}
else
errorFunction("MarketplaceService::getProductInfo() an error occured while parsing web response");
}
}
void MarketplaceService::onReceivedRawProductInfoError(std::string error, boost::weak_ptr<RBX::DataModel> weakDM, boost::function<void(std::string)> errorFunction)
{
if(shared_ptr<RBX::DataModel> sharedDM = weakDM.lock())
{
if (!error.empty())
{
errorFunction( RBX::format("MarketplaceService:getProductInfo() failed because %s", error.c_str()) );
}
else
{
errorFunction("MarketplaceService:getProductInfo() failed but with no known error");
}
}
}
void MarketplaceService::processPlayerOwnsAssetResponseSuccess(std::string response, boost::function<void(bool)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if (response.empty())
{
errorFunction("MarketPlaceService::PlayerOwnsAsset failed because the response was nil");
return;
}
boost::algorithm::to_lower(response);
bool isOwner = false;
std::istringstream is(response);
is >> std::boolalpha >> isOwner;
resumeFunction(isOwner);
}
void MarketplaceService::processPlayerOwnsAssetResponseError(std::string error, boost::function<void(std::string)> errorFunction)
{
if (!error.empty())
{
errorFunction(RBX::format("MarketPlaceService::PlayerOwnsAsset failed because %s",error.c_str()));
return;
}
else
{
errorFunction("MarketPlaceService::PlayerOwnsAsset failed with an unknown error");
}
}
void MarketplaceService::playerOwnsAsset(const shared_ptr<Instance> player, const int assetId, boost::function<void(bool)> resumeFunction, boost::function<void(std::string)> errorFunction)
{
if(playerOwnsAssetUrl.empty())
{
errorFunction("MarketplaceService:PlayerOwnsAsset() playerOwnsAssetUrl is empty");
return;
}
if(assetId <= 0)
{
errorFunction("MarketplaceService:PlayerOwnsAsset() second argument is not a valid assetId (supplied assetId was less than 0)");
return;
}
if(!isValidPlayer(player,"PlayerOwnsAsset()",errorFunction))
{
resumeFunction(false);
return;
}
int userId = 0;
if(Network::Player* thePlayer = Instance::fastDynamicCast<Network::Player>(player.get()))
userId = thePlayer->getUserID();
std::string url = "";
if (ContentProvider* contentProvider = ServiceProvider::create<ContentProvider>(this))
{
url = contentProvider->getApiBaseUrl() + playerOwnsAssetUrl;
}
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
apiService->getAsync( RBX::format(playerOwnsAssetUrl.c_str(), userId, assetId), true, RBX::PRIORITY_DEFAULT,
boost::bind(&MarketplaceService::processPlayerOwnsAssetResponseSuccess, this, _1, resumeFunction,errorFunction),
boost::bind(&MarketplaceService::processPlayerOwnsAssetResponseError, this, _1, errorFunction) );
}
}
void MarketplaceService::verifyPurchaseResponseNoDMLock(std::string response, int userId, int productId)
{
shared_ptr<const Reflection::ValueTable> values;
std::stringstream responseStream;
responseStream << response;
if( WebParser::parseJSONTable(response, values) )
event_serverPurchaseVerification.fireAndReplicateEvent(this, values);
else
verifyPurchaseErrorNoDMLock("VerifyResponse Failed", userId, productId);
}
void MarketplaceService::verifyPurchaseErrorNoDMLock(std::string error, int userId, int productId)
{
int placeId = 0;
if(RBX::DataModel* dataModel = RBX::DataModel::get(this))
placeId = dataModel->getPlaceID();
shared_ptr<Reflection::ValueTable> values(rbx::make_shared<Reflection::ValueTable>());
(*values)["playerId"] = userId;
(*values)["placeId"] = placeId;
(*values)["isValid"] = false;
(*values)["productId"] = productId;
(*values)["errorReason"] = error;
event_serverPurchaseVerification.fireAndReplicateEvent(this, values);
}
void MarketplaceService::verifyPurchaseTicket(std::string ticket, int userId, int productId)
{
if (Network::Players::backendProcessing(this, false))
{
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
apiService->postAsync( RBX::format("%s/validatepurchase?receipt=%s", urlApiPath(), ticket.c_str()), std::string("GameServerVerifyPurchase"), true, RBX::PRIORITY_EXTREME, HttpService::TEXT_PLAIN,
boost::bind(&MarketplaceService::verifyPurchaseResponseNoDMLock, this, _1, userId, productId),
boost::bind(&MarketplaceService::verifyPurchaseErrorNoDMLock, this, _1, userId, productId) );
}
}
}
void MarketplaceService::signalClientPurchaseSuccess(std::string ticket, int userId, int productId)
{
event_clientPurchaseSuccess.fireAndReplicateEvent(this,ticket,userId,productId);
}
bool MarketplaceService::launchClientLuaDialog(std::string message, std::string accept, std::string decline, shared_ptr<Instance> player, boost::function<void(bool, shared_ptr<Instance>)> callback)
{
if (Network::Player* thePlayer = Instance::fastDynamicCast<Network::Player>(player.get()))
{
Network::Players* players = ServiceProvider::find<Network::Players>(this);
if (!players ||
thePlayer->getParent() != players ||
players->clientIsPresent(this) ||
callbackFunctionMap.find(player) != callbackFunctionMap.end())
return false;
if (!playerRemovingConnection.connected())
playerRemovingConnection = players->playerRemovingSignal.connect(boost::bind(&MarketplaceService::executeClientDialogCallback, this, false, _1));
callbackFunctionMap[player] = callback;
const RBX::SystemAddress& target = thePlayer->getRemoteAddressAsRbxAddress();
shared_ptr<Reflection::Tuple> args(new Reflection::Tuple());
args->values.push_back(message);
args->values.push_back(accept);
args->values.push_back(decline);
Reflection::EventArguments eventArgs(1);
eventArgs[0] = shared_ptr<const Reflection::Tuple>(args);
if (players->serverIsPresent(this))
raiseEventInvocation(event_ClientLuaDialogRequested, eventArgs, &target);
else
event_ClientLuaDialogRequested.fireEvent(this, eventArgs);
return true;
}
return false;
}
void MarketplaceService::executeClientDialogCallback(bool value, shared_ptr<Instance> player)
{
if (callbackFunctionMap.find(player) == callbackFunctionMap.end())
return;
if (!(&callbackFunctionMap[player])->empty())
{
boost::function<void(bool, shared_ptr<Instance>)> callbackFunction = callbackFunctionMap[player];
callbackFunctionMap.erase(player);
callbackFunction(value, player);
}
}
void MarketplaceService::signalServerLuaDialogClosed(bool value)
{
if (!Network::Players::serverIsPresent(this))
{
Instance* player = Network::Players::findLocalPlayer(this);
Reflection::EventArguments args(2);
args[0] = value;
args[1] = shared_from(player);
if (Network::Players::clientIsPresent(this))
raiseEventInvocation(event_luaDialogCallbackSignal, args, NULL);
else
event_luaDialogCallbackSignal.fireEvent(this, args);
}
}
void MarketplaceService::onServiceProvider(ServiceProvider* oldSp, ServiceProvider* newSp)
{
playerAddedReceiptConnection.disconnect();
playerPurchaseReceiptConnection.disconnect();
verifyPurchaseConnection.disconnect();
Instance::onServiceProvider(oldSp, newSp);
if (newSp && Network::Players::backendProcessing(newSp))
{
setupBackendOnlyReceiptHandling();
Network::Players* players = ServiceProvider::create<Network::Players>(this);
// Attach listener for player joined to query for receipts
playerAddedReceiptConnection = players->getOrCreateChildAddedSignal()->connect(boost::bind(
&MarketplaceService::getReceiptsForJoiningPlayer, this, _1));
// Attach listener for player purchases
playerPurchaseReceiptConnection = clientPurchaseSuccess.connect(boost::bind(
&MarketplaceService::getReceiptsAfterPurchase, this, _1, _2, _3));
}
if (newSp)
{
verifyPurchaseConnection = clientPurchaseSuccess.connect(boost::bind(&RBX::MarketplaceService::verifyPurchaseTicket,
this, _1, _2, _3));
}
}
void MarketplaceService::processReceiptsCallbackChanged(const ReceiptCallback& oldValue)
{
if (!Network::Players::backendProcessing(this))
{
// Property changed callbacks are called after the value has been updated,
// so if we are not backend processing we should revert the value
receiptCallback.clear();
throw RBX::runtime_error("Can only register ProcessReceipt callback on server");
}
receiptProcessingEnabledByUser = true;
}
static void alwaysApproveReceiptsCallback(Reflection::Variant,
Reflection::AsyncCallbackDescriptor::ResumeFunction resumeFunc,
Reflection::AsyncCallbackDescriptor::ErrorFunction)
{
shared_ptr<Reflection::Tuple> t(new Reflection::Tuple(1));
t->values[0] = MarketplaceService::DECISION_PURCHASE_GRANTED;
resumeFunc(t);
}
void MarketplaceService::setupBackendOnlyReceiptHandling()
{
RBXASSERT(Network::Players::backendProcessing(this));
RBXASSERT(!receiptProcessingEnabledByUser);
receiptProcessingEnabledByUser = false;
receiptCallback = boost::bind(&alwaysApproveReceiptsCallback, _1, _2, _3);
}
void MarketplaceService::getReceiptsForJoiningPlayer(shared_ptr<Instance> inst)
{
if (shared_ptr<Network::Player> player = Instance::fastSharedDynamicCast<Network::Player>(inst))
{
fetchAndProcessTransactions(player->getUserID());
}
}
void MarketplaceService::getReceiptsAfterPurchase(std::string response, int userId, int productId)
{
fetchAndProcessTransactions(userId);
}
void MarketplaceService::fetchAndProcessTransactions(int userId)
{
if (Network::Players::backendProcessing(this) &&
ServiceProvider::create<LuaWebService>(this)->isApiAccessEnabled())
{
int placeId = DataModel::get(this)->getPlaceID();
if (placeId > 0 && userId > 0)
{
std::string apiBaseUrl = ServiceProvider::create<ContentProvider>(this)->getApiBaseUrl();
Http http(format("%sgametransactions/getpendingtransactions/?PlaceId=%d&PlayerId=%d",
apiBaseUrl.c_str(), placeId, userId));
http.additionalHeaders["Cache-Control"] = "no-cache";
http.doNotUseCachedResponse = true;
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
apiService->getAsync(http, RBX::PRIORITY_EXTREME,
boost::bind(&MarketplaceService::handleReceiptQueryResponseSuccess, weak_from(this), _1),
boost::bind(&MarketplaceService::handleReceiptQueryResponseFailure, weak_from(this), _1)
);
}
}
}
}
static shared_ptr<const Reflection::ValueTable> buildReceiptInfoFromJson(const Reflection::Variant& jsonReceiptAsVariant)
{
shared_ptr<const Reflection::ValueTable> sourceTable =
jsonReceiptAsVariant.cast<shared_ptr<const Reflection::ValueTable> >();
shared_ptr<Reflection::ValueTable> receiptTable(new Reflection::ValueTable);
(*receiptTable)["PlayerId"] = sourceTable->at("playerId");
(*receiptTable)["PlaceIdWherePurchased"] = sourceTable->at("placeId");
(*receiptTable)["PurchaseId"]= sourceTable->at("receipt");
if (sourceTable->at("actionArgs").isType<shared_ptr<const Reflection::ValueArray> >())
{
shared_ptr<const Reflection::ValueArray> argsArray =
sourceTable->at("actionArgs").cast<shared_ptr<const Reflection::ValueArray> >();
for (size_t j = 0; j < argsArray->size(); ++j)
{
if (argsArray->at(j).isType<shared_ptr<const Reflection::ValueTable> >())
{
shared_ptr<const Reflection::ValueTable> kvPair =
argsArray->at(j).cast<shared_ptr<const Reflection::ValueTable> >();
int intValue;
bool isNumber = StringConverter<int>::convertToValue(kvPair->at("Value").get<std::string>(), intValue);
if (kvPair->at("Key").get<std::string>() == "productId" && isNumber)
{
(*receiptTable)["ProductId"] = intValue;
}
if (kvPair->at("Key").get<std::string>() == "currencyTypeId" && isNumber)
{
(*receiptTable)["CurrencyType"] = static_cast<MarketplaceService::CurrencyType>(intValue);
}
if (kvPair->at("Key").get<std::string>() == "unitPrice" && isNumber)
{
(*receiptTable)["CurrencySpent"] = intValue;
}
}
}
}
return shared_ptr<const Reflection::ValueTable>(receiptTable);
}
void MarketplaceService::handleReceiptQueryResponseSuccess(weak_ptr<MarketplaceService> weakMk, std::string response)
{
if (!response.empty())
{
if (shared_ptr<MarketplaceService> m = weakMk.lock())
{
Reflection::Variant jsonVariant;
WebParser::parseJSONObject(response, jsonVariant);
if (jsonVariant.isType<shared_ptr<const Reflection::ValueArray> >())
{
shared_ptr<const Reflection::ValueArray> receiptArray =
jsonVariant.cast<shared_ptr<const Reflection::ValueArray> >();
for (size_t i = 0; i < receiptArray->size(); ++i)
{
if (receiptArray->at(i).isType<shared_ptr<const Reflection::ValueTable> >())
{
DataModel::get(m.get())->submitTask(
boost::bind(&MarketplaceService::processReceipt,
weakMk, buildReceiptInfoFromJson(receiptArray->at(i))),
DataModelJob::Write);
}
}
}
}
}
}
void MarketplaceService::handleReceiptQueryResponseFailure(weak_ptr<MarketplaceService> weakMk, std::string error)
{
// nothing to do here, maybe in the future we will need to handle/report error
}
static void doNothingHttpCallbackSuccess(std::string response)
{
}
static void doNothingHttpCallbackError(std::string error)
{
}
static void doNothingErrorHandler(std::string error)
{
}
void MarketplaceService::processReceipt(weak_ptr<MarketplaceService> weakMk, shared_ptr<const Reflection::ValueTable> receiptInfo)
{
if (shared_ptr<MarketplaceService> mk = weakMk.lock())
{
if (!mk->receiptCallback.empty())
{
try
{
// try to get metadata for logging
int currencyType = -1;
int currencySpent = -1;
try
{
currencyType = receiptInfo->at("CurrencyType").get<CurrencyType>();
currencySpent = receiptInfo->at("CurrencySpent").get<int>();
}
catch (const RBX::base_exception&)
{
RBXASSERT(false);
}
std::string receiptId = receiptInfo->at("PurchaseId").get<std::string>();
mk->receiptCallback(receiptInfo,
boost::bind(&MarketplaceService::processReceiptSuccessContinuation, mk,
receiptId, currencyType, currencySpent, _1),
boost::bind(&doNothingErrorHandler, _1));
}
catch (const RBX::base_exception&)
{
}
}
}
}
void MarketplaceService::processReceiptSuccessContinuation(shared_ptr<MarketplaceService> mk,
std::string receiptId, int currencyType, int currencySpent, shared_ptr<const Reflection::Tuple> result)
{
ProductPurchaseDecision decision = DECISION_NOT_PROCESSED_YET;
try {
if (result->values.size() == 1)
{
decision = result->at(0).get<MarketplaceService::ProductPurchaseDecision>();
}
}
catch (const RBX::base_exception&)
{
}
if (decision == DECISION_PURCHASE_GRANTED)
{
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(mk.get()))
{
apiService->postAsync("gametransactions/settransactionstatuscomplete", format("receipt=%s", receiptId.c_str()), true, RBX::PRIORITY_EXTREME, HttpService::APPLICATION_URLENCODED,
boost::bind(&doNothingHttpCallbackSuccess, _1),
boost::bind(&doNothingHttpCallbackError, _1) );
}
const char* currencyName = "unknown";
if (currencyType == CURRENCY_ROBUX)
{
currencyName = "Robux";
}
else if (currencyType == CURRENCY_TIX)
{
currencyName = "Ticket";
}
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME,
mk->receiptProcessingEnabledByUser ? "BuyDeveloperProduct" : "BuyDeveloperProduct_AutoAccepted",
currencyName, std::max(0, currencySpent));
}
}
void MarketplaceService::promptThirdPartyPurchase(const shared_ptr<Instance> player, std::string thirdPartyProductId)
{
if(!isValidPlayer(player,"PromptThirdPartyPurchase()"))
return;
if(Network::Players* players = ServiceProvider::find<RBX::Network::Players>(this))
{
if(Network::Players::frontendProcessing(this) && (player.get() != players->getLocalPlayer()) )
{
RBX::StandardOut::singleton()->printf( RBX::MESSAGE_WARNING, "MarketplaceService:PromptThirdPartyPurchase called from a local script, but not called on a local player. Local scripts can only prompt the local player.");
return;
}
}
if(thirdPartyProductId.empty())
throw RBX::runtime_error("MarketplaceService:PromptThirdPartyPurchase() productId is empty");
// fire a signal so the native platform can do the appropriate purchase calls
event_promptThirdPartyPurchaseRequested.fireAndReplicateEvent(this, player, thirdPartyProductId);
}
void MarketplaceService::signalPromptThirdPartyPurchaseFinished(const shared_ptr<Instance> player, std::string productId, std::string receipt, const bool success)
{
event_PromptTPPurchaseFinished.fireAndReplicateEvent(this,player,productId,receipt,success);
}
void MarketplaceService::promptNativePurchase(const shared_ptr<Instance> player, std::string nativeProductId)
{
if(!isValidPlayer(player,"PromptNativePurchase()"))
return;
if(Network::Players* players = ServiceProvider::find<RBX::Network::Players>(this))
{
if(Network::Players::frontendProcessing(this) && (player.get() != players->getLocalPlayer()) )
{
RBX::StandardOut::singleton()->printf( RBX::MESSAGE_WARNING, "MarketplaceService:PromptNativePurchase called from a local script, but not called on a local player. Local scripts can only prompt the local player.");
return;
}
}
if(nativeProductId.empty())
throw RBX::runtime_error("MarketplaceService:PromptNativePurchase() productId is empty");
// fire a signal so the native platform can do the appropriate purchase calls
event_promptNativePurchaseRequested.fireAndReplicateEvent(this, player, nativeProductId);
}
void MarketplaceService::signalPromptNativePurchaseFinished(const shared_ptr<Instance> player, std::string productId, const bool success)
{
event_PromptNativePurchaseFinished.fireAndReplicateEvent(this, player, productId, success);
}
void MarketplaceService::getDeveloperProductsAsync(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("%sdeveloperproducts/list?placeid=%i", cp->getApiBaseUrl().c_str(), DataModel::get(this)->getPlaceID());
shared_ptr<Pages> pagination = Creatable<Instance>::create<StandardPages>(weak_from(DataModel::get(this)), fetchUrl, "DeveloperProducts");
pagination->fetchNextChunk(boost::bind(resumeFunction, pagination), errorFunction);
}
} // namespace RBX