[Python, Разработка мобильных приложений, Разработка игр, Unity] Интеграция и серверная валидация инаппов для стора Google Play — как защититься от читеров
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Онлайн-проекты рано или поздно сталкиваются со взломом внутреннего стора, когда читеры накручивают себе игровые предметы, оружие или валюту. Классика. Наш PvP-шутер не стал исключением — брешь мы в итоге закрыли, хотя и пришлось повозиться.В этой статье расскажу про интеграцию и серверную валидацию инаппов с точки зрения клиента: какой плагин использовать для Google Play и на что обращать внимание независимо от платформы, а моя коллега поделится кодом серверной части.Как уже говорилось в блоге, наш флагманский проект — это мобильный PvP-шутер с DAU около 1 млн пользователей, большинство из которых на Android. В игре сотни видов оружия и предметов. И чтобы защититься от взлома, естественно, нужна валидация покупок. Пойдем по порядку.В Google Play наш проект использует consumable in-apps, которые после успешной покупки и валидации начисляются игроку и сразу потребляются. По историческим причинам для Google Play мы используем плагин от Prime31.Отмечу, что если бы мы сегодня добавляли встроенные покупки с нуля на эти платформы, то взяли бы Unity IAP (а, например, на Huawei публиковались бы через Unity Distribution Portal).В игре много спецпредложений, но мы не стали заводить отдельный id инаппа под каждую акцию, вместо этого используем один набор айдишников инаппов для разных предметов и предложений. В момент нажатия на конкретную покупку мы запоминаем контент, который нужно выдать игроку при успешной покупке. При завершении покупки — выдаем его.Перейдем к коду покупки и валидации инаппов.На старте приложения подписываемся на события покупки:
// GoogleIABManager — класс из плагина Prime31
GoogleIABManager.purchaseSucceededEvent += HandleGooglePurchaseSucceeded;
Когда игрок нажимает на инапп в интерфейсе — запускаем покупку:
// GoogleIAB — класс из плагина Prime31
GoogleIAB.purchaseProduct(productId);
В обработчике успешного завершения покупки оборачиваем платформо-специфичную покупку в объект, реализующий IMarketPurchase. IMarketPurchase мы используем на всех платформах, чтобы сделать код валидации кроссплатформенным. В этот интерфейс мы оборачиваем классы из плагинов конкретных магазинов.
public interface IMarketPurchase
{
string ProductId { get; }
string OrderId { get; }
string PurchaseToken { get; }
object NativePurchase { get; }
}
class GoogleMarketPurchase : IMarketPurchase
{
internal GoogleMarketPurchase(GooglePurchase purchase)
{
_purchase = purchase;
}
public string ProductId => _purchase.productId;
public string OrderId => _purchase.orderId;
public string PurchaseToken => _purchase.purchaseToken;
public object NativePurchase => _purchase;
private GooglePurchase _purchase;
}
internal static class MarketPurchaseFactory
{
// GooglePurchase — класс из плагина Prime31
internal static IMarketPurchase CreateMarketPurchase(GooglePurchase purchase)
{
return new GoogleMarketPurchase(purchase);
}
}
private void IapManagerOnBuyProductSuccess(PurchaseResultInfo purchaseResult)
{
var purchaseData = new InAppPurchaseData(purchaseResult.InAppPurchaseData);
IMarketPurchase marketPurchase = MarketPurchaseFactory.CreateMarketPurchase(purchaseData);
ValidatePurchase( marketPurchase );
}
Отправляем покупку на наш сервер на валидацию:
private void ValidatePurchase(IMarketPurchase purchase)
{
var request = new InappValidationRequest
{
orderId = purchase.OrderId,
productId = purchase.ProductId,
purchaseToken = purchase.PurchaseToken,
OnSuccess = () => ProvidePurchase(purchase),
OnFail = () => Consume(purchase)
};
WebSocketCallbacks.Subscribe(ServerEventNames.PurchasePrevalidate, PrevalidatePurchaseHandler);
Dictionary<object, object> data = new Dictionary<object, object>();
data.Add("orderId", request.orderId);
data.Add("productId", request.productId);
data.Add("data", request.purchaseToken);
int reqId = WebSocketManager.Instance.Send(ServerEventNames.PurchasePrevalidate, data);
_valdationRequests.Add(reqId, request);
}
Если валидация проходит неуспешно — потребляем (Consume) продукт без начисления пользователю.Если все хорошо — потребляем продукт с начислением пользователю:
void ProvidePurchase(IMarketPurchase purchase)
{
GiveInGameCurrencyAndItems(purchase);
Consume(purchase);
}
Важный момент: метод Consume перед отправкой в магазин запроса на потребление запоминает, что мы уже начислили покупку игроку. Это нужно, если из-за проблем с сетью (или каких-то других) запрос на консьюм не дойдет до магазина. В таком случае, когда после перезапуска приложения нам придут незаконсьюмленные покупки, мы увидим, за какие из них уже начисляли игроку валюту и предметы.Обработчик ответа с сервера:
private const int ERROR_CODE_SERVER_ERROR = 30;
private const int ERROR_CODE_VALIDATION_ERROR = 31;
private void PrevalidatePurchaseHandler(Dictionary<string, object> response)
{
int reqId = Convert.ToInt32(response["req_id"], CultureInfo.InvariantCulture);
_valdationRequests.TryGetValue(reqId, out InappValidationRequest request);
if (request == null)
return;
_valdationRequests.Remove(reqId);
if (response["status"].Equals("ok"))
{
request.OnSuccess();
}
else
{
int code = Convert.ToInt32(response["err_code"], CultureInfo.InvariantCulture);
switch (code)
{
case ERROR_CODE_VALIDATION_ERROR:
request.OnFail();
break;
case ERROR_CODE_SERVER_ERROR:
CoroutineRunner.DeferredAction(5f, () => TryValidateAgain());
break;
default:
// неизвестная ошибка, начисляем инапп (поступаем в пользу игрока)
request.OnSuccess(null);
break;
}
}
}
В случае, если сервер вернул OK в статусе валидации, производим начисление и консьюм покупки. Если сервер вернул неизвестную ошибку, трактуем результат валидации в пользу игрока.Для следующего раздела передаю слово нашему серверному программисту Ире Поповой. Серверная валидацияВалидация на сервере состоит из двух этапов:
- превалидация — когда данные по платежу отправляются на сервер соответствующей платформы для проверки валидности;
- начисление — в случае успешно пройденной валидации купленных позиций.
Сначала сервер получает в качестве входных параметров данные, необходимые для проведения валидации. В Android — это id позиции и токен. Методы валидации являются платформо-зависимыми. Но, как правило, включают в себя логику отправки данных на сервер валидации соответствующей платформы, обработку полученного результата и возврат соответствующего ответа на клиент. Дополнительно результат валидации записывается в redis для последующей быстрой проверки при начислении.
def validate_receipt(self, uid, data, platform):
InAppSlot = PlayerProgress.first(f"player_id={uid} AND slot_id='35'")
if not InAppSlot:
raise RuntimeError(f"Fail get slot purchases: not found player:{uid} data:{data}")
tid = data.get("tid")
params = []
orders_data = []
valid_orders = []
if not tid or tid in InAppSlot.content:
return False
params = str(tid).split(self.IN_APP_ID_SEPARATOR)
if platform == "ios":
transaction_id = params[0]
product_id = params[1]
orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id)
error("[VALIDATION] {} {} {}".format(transaction_id, product_id, orders_data))
elif platform == "android":
product_id = params[1]
purchase_token = data.get("data")
orders_data = self._get_receipt_android(product_id, purchase_token)
elif platform == "amazon":
receipt_sku = params[0]
user_id = params[1]
orders_data = self._get_receipt_amazon(user_id, receipt_sku)
elif platform == "huawei":
product_id = params[1]
orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""), data.get("account_flag", 0))
elif platform == "udp":
product_id = params[1]
orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", ""))
elif platform == "samsung":
product_id = params[1]
transaction_id = params[0]
product_id = params[1]
orders_data = self._get_receipt_samsung(data.get("data", ""), product_id)
else:
error("[InAppValidator] unknown platform")
return False
if not orders_data:
error(f"[InAppValidator] fail get receipt {platform} player:{uid} data:{data}")
return False
key = f"inapp:{uid}:{tid}"
for order in orders_data:
if not order.is_success():
continue
valid_orders.append(order)
try:
self.inapp_redis.setex(key, order.to_json(), 86400)
except Exception as ex:
exception(f"[InAppValidator] fail save inapp to redis: {ex}")
if not valid_orders:
warning(f"[InAppValidator] not valid receipt {orders_data[0].order_id}")
return False
return True
Пример получения данных с соответствующего сервера валидации для Android. Для обращения к серверу Google были использованы пакеты Google для Python apiclient и oauth2client.
def _get_receipt_android(self, product_id, token):
if not self.android_authorized:
self._android_auth()
debug(f"[InAppValidator] android product_id: {product_id}, token: {token}")
try:
product = self.android_publisher.purchases().products().get(
packageName=config.GOOGLE_SERVICE_ACCOUNT['package_name'], productId=product_id, token=token).execute()
except client.AccessTokenRefreshError:
self.android_authorized = False
return self._get_receipt_android(product_id, token)
except google_errors.HttpError as ex:
if ex.resp.status == 401 or ex.resp.status == 503:
self.android_authorized = False
return self._get_receipt_android(product_id, token)
return False
if not product:
warning("[InAppValidator] android product is NONE")
return None
order_id = product.get('orderId')
if not order_id:
warning(f"order_id is NONE: {product}")
return None
return [Receipt(order_id, product.get('purchaseState', -1), product_id)]
class Receipt:
def __init__(self, order_id, status, product_id, user_id=None, expire=0, trial=0, refund=0, latest_receipt=''):
self.order_id = order_id
self.status = status
self.product_id = product_id
self.user_id = user_id
self.expire = expire
if str(trial) == 'true':
self.trial = 1
else:
self.trial = 0
self.refund = refund
self.latest_receipt = latest_receipt
def is_success(self):
return self.status == 0
def is_canceled(self):
return self.status == 3
def is_valid(self):
return self.order_id and self.product_id
def to_dict(self):
return {"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt}
def to_json(self):
return json.dumps({"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt})
Отдельной командой/набором команд происходит начисление купленных позиций. Одна позиция может содержать разнотипные итемы (например, деньги и оружие), и для каждого типа итема на сервере существует отдельная команда начисления.Чтобы логически объединить несколько команд, привязанных к одному действию игрока, на клиенте и на сервере введено понятие снапшота — специальной конструкции, представляющей собой объединение команд, в которой ни одна команда не выполнится, если хотя бы какая-то не пройдет проверку. Можно сказать, что это некий аналог транзакций в БД. В данном случае снапшот включает специальную команду валидации и команды начисления купленных позиций. Команда валидации:
def validate_receipt(self, data):
neededSlotsNames = [self.slotName]
self.slots = self.get_slots_data(*neededSlotsNames)
InAppSlot = self.slots.get(self.slotName, [])
tid = data.get("tid")
platform = data.get("pl")
params = []
orders_data = []
valid_orders = []
if not tid:
self.ThrowFail("not found required parameter")
elif tid in InAppSlot:
self.ThrowFail("already in slot")
if not self.IsFail():
params = str(tid).split(self.IN_APP_ID_SEPARATOR)
if not self.IsFail():
inapp_storage = InappStorage.get_instance()
if inapp_storage.exists_transaction(self.platform, params[0]):
self.ThrowFail("already_purchased {0} d".format(params[0]),
VALIDATOR_RESULT_CODE.ALREADY_PURCHASED)
self.FinalizeRequest({self.slotName: InAppSlot}, data)
return
# Try get from redis
player_platform = self.platform
if platform is not None and int(platform) == 4:
player_platform = "udp"
_prevalidate_order = self.inapp_redis.check_tid(self._player_id, tid)
if _prevalidate_order:
orders_data = Receipt.from_json(_prevalidate_order)
elif player_platform == "ios":
transaction_id = params[0]
product_id = params[1]
if not transaction_id or not product_id:
self.ThrowFail(f"fail get receipt {self.platform}")
else:
orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id)
elif player_platform == "android":
product_id = params[1]
purchase_token = data.get("data")
orders_data = self._get_receipt_android(product_id, purchase_token)
elif player_platform == "amazon":
receipt_sku = params[0]
user_id = params[1]
orders_data = self._get_receipt_amazon(user_id, receipt_sku)
elif player_platform == "huawei":
product_id = params[1]
orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""),
data.get("account_flag", 0), data.get("subscribe"))
elif platform == "udp":
product_id = params[1]
orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", ""))
elif platform == "samsung":
product_id = params[1]
transaction_id = params[0]
product_id = params[1]
orders_data = self._get_receipt_samsung(data.get("data", ""), product_id)
else:
self.ThrowFail("unknown platform")
if not orders_data:
self.ThrowFail(f"fail get receipt {player_platform} {self.platform}")
if not self.IsFail():
for order in orders_data:
if order.is_success():
valid_orders.append(order)
if not valid_orders:
self.ThrowFail("already_purchased {0}".format(orders_data[0].order_id),
VALIDATOR_RESULT_CODE.ALREADY_PURCHASED)
else:
InAppSlot.append(tid)
self.SetRequestSuccessful()
if self._player_id in LOG_PLAYER_IDS:
HashLog.error(f"[INAPP] id:{self._player_id} receipt:{data}")
self.FinalizeRequest({self.slotName: InAppSlot}, data)
Команда валидации проверяет транзакцию — если есть данные превалидации, то используются они. В противном случае, данные отправляются на сервер валидации для соответствующей платформы.В случае успешного начисления, id транзакции сохраняется в соответствующий слот игрока — запись в БД, которая хранит данные по платежным транзакциям данного игрока. Во избежание взлома платежки методом, когда одну валидную транзакцию используют для многократного начисления, в рамках валидации осуществляется проверка на существование данного id транзакции.Кроме того, в завершение начисления отправляется соответствующая статистика на сервер аналитики.На что еще обратить вниманиеВне зависимости от платформы, для которой реализуются встроенные покупки, важно проверить и обработать следующие ситуации:
- При показе нативных окон магазина в процессе покупки игра может вылететь по памяти. Поэтому следует протестировать такой сценарий, чтобы удостовериться, что покупка после перезапуска корректно завершается и начисляется игроку.
- На большинстве платформ в процессе взаимодействия с окнами платформенного магазина приложение уходит в бэкграунд, и при завершении покупки выводится из бэкграунда. За это время игра вполне может дисконнектнуться от серверов. Если для валидации или начисления покупки нужен коннект с сервером, то после возвращения в приложение нужно будет соединиться с ним вновь, и только потом производить валидацию или начисление.
- Нужно тестировать сценарий, когда во время покупки и валидации игрок запускает новую покупку. Мы после тестирования этого сценария обнаружили баги и добавляли запрет запуска покупки, пока идет покупка другого инаппа.
Дополнительные ссылкиИ последнее: когда мы реализовывали валидацию инаппов для Google Play несколько лет назад, нам оказалось полезной статья на Хабре, вам она тоже может пригодиться. Также использовали решения, предложенные здесь и здесь. Ссылка на документацию по API серверной валидации Google — здесь.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка мобильных приложений, Разработка под Android] Получаем результат правильно (Часть 2). Fragment Result API
- [Разработка веб-сайтов, Safari, Разработка мобильных приложений] Как баг с потерянными днями рождения привёл нас в историю СССР
- [Python, Программирование] Создание функции губки из MD5 (перевод)
- [JavaScript, Разработка мобильных приложений, Swift, ReactJS] Как Лёня с React на Swift переезжал
- [Python, Программирование, Открытые данные, Машинное обучение] Датасет о мобильных приложениях
- [Разработка веб-сайтов, Разработка мобильных приложений, Карьера в IT-индустрии] Без тимлида не обойтись, а что насчет техлида?
- [Информационная безопасность, Программирование, Производство и разработка электроники, Гаджеты, Игры и игровые приставки] Часть 3: ESPboy2 — гаджет для ретро игр и экспериментов с IoT, новости проекта 2021
- [Сетевые технологии, Исследования и прогнозы в IT] Отнимаем и делим — исследуем целостность Рунета
- [Мессенджеры, Платежные системы, Python, API] Как принимать платежи в Telegram | API Yoomoney Python
- [Python, ECM/СЭД, Хранение данных] Как мы запустили документооборот в Telegram и что из этого вышло? Да, это не сон
Теги для поиска: #_python, #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_igr (Разработка игр), #_unity, #_razrabotka_igr (разработка игр), #_validatsija (валидация), #_inapp, #_chitery (читеры), #_gejmdev (геймдев), #_onlajnshuter (онлайн-шутер), #_inapp (инапп), #_gamedev, #_python, #_unity, #_blog_kompanii_lightmap (
Блог компании Lightmap
), #_python, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_igr (
Разработка игр
), #_unity
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:53
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Онлайн-проекты рано или поздно сталкиваются со взломом внутреннего стора, когда читеры накручивают себе игровые предметы, оружие или валюту. Классика. Наш PvP-шутер не стал исключением — брешь мы в итоге закрыли, хотя и пришлось повозиться.В этой статье расскажу про интеграцию и серверную валидацию инаппов с точки зрения клиента: какой плагин использовать для Google Play и на что обращать внимание независимо от платформы, а моя коллега поделится кодом серверной части.Как уже говорилось в блоге, наш флагманский проект — это мобильный PvP-шутер с DAU около 1 млн пользователей, большинство из которых на Android. В игре сотни видов оружия и предметов. И чтобы защититься от взлома, естественно, нужна валидация покупок. Пойдем по порядку.В Google Play наш проект использует consumable in-apps, которые после успешной покупки и валидации начисляются игроку и сразу потребляются. По историческим причинам для Google Play мы используем плагин от Prime31.Отмечу, что если бы мы сегодня добавляли встроенные покупки с нуля на эти платформы, то взяли бы Unity IAP (а, например, на Huawei публиковались бы через Unity Distribution Portal).В игре много спецпредложений, но мы не стали заводить отдельный id инаппа под каждую акцию, вместо этого используем один набор айдишников инаппов для разных предметов и предложений. В момент нажатия на конкретную покупку мы запоминаем контент, который нужно выдать игроку при успешной покупке. При завершении покупки — выдаем его.Перейдем к коду покупки и валидации инаппов.На старте приложения подписываемся на события покупки: // GoogleIABManager — класс из плагина Prime31
GoogleIABManager.purchaseSucceededEvent += HandleGooglePurchaseSucceeded; // GoogleIAB — класс из плагина Prime31
GoogleIAB.purchaseProduct(productId); public interface IMarketPurchase
{ string ProductId { get; } string OrderId { get; } string PurchaseToken { get; } object NativePurchase { get; } } class GoogleMarketPurchase : IMarketPurchase { internal GoogleMarketPurchase(GooglePurchase purchase) { _purchase = purchase; } public string ProductId => _purchase.productId; public string OrderId => _purchase.orderId; public string PurchaseToken => _purchase.purchaseToken; public object NativePurchase => _purchase; private GooglePurchase _purchase; } internal static class MarketPurchaseFactory { // GooglePurchase — класс из плагина Prime31 internal static IMarketPurchase CreateMarketPurchase(GooglePurchase purchase) { return new GoogleMarketPurchase(purchase); } } private void IapManagerOnBuyProductSuccess(PurchaseResultInfo purchaseResult) { var purchaseData = new InAppPurchaseData(purchaseResult.InAppPurchaseData); IMarketPurchase marketPurchase = MarketPurchaseFactory.CreateMarketPurchase(purchaseData); ValidatePurchase( marketPurchase ); } private void ValidatePurchase(IMarketPurchase purchase)
{ var request = new InappValidationRequest { orderId = purchase.OrderId, productId = purchase.ProductId, purchaseToken = purchase.PurchaseToken, OnSuccess = () => ProvidePurchase(purchase), OnFail = () => Consume(purchase) }; WebSocketCallbacks.Subscribe(ServerEventNames.PurchasePrevalidate, PrevalidatePurchaseHandler); Dictionary<object, object> data = new Dictionary<object, object>(); data.Add("orderId", request.orderId); data.Add("productId", request.productId); data.Add("data", request.purchaseToken); int reqId = WebSocketManager.Instance.Send(ServerEventNames.PurchasePrevalidate, data); _valdationRequests.Add(reqId, request); } void ProvidePurchase(IMarketPurchase purchase)
{ GiveInGameCurrencyAndItems(purchase); Consume(purchase); } private const int ERROR_CODE_SERVER_ERROR = 30;
private const int ERROR_CODE_VALIDATION_ERROR = 31; private void PrevalidatePurchaseHandler(Dictionary<string, object> response) { int reqId = Convert.ToInt32(response["req_id"], CultureInfo.InvariantCulture); _valdationRequests.TryGetValue(reqId, out InappValidationRequest request); if (request == null) return; _valdationRequests.Remove(reqId); if (response["status"].Equals("ok")) { request.OnSuccess(); } else { int code = Convert.ToInt32(response["err_code"], CultureInfo.InvariantCulture); switch (code) { case ERROR_CODE_VALIDATION_ERROR: request.OnFail(); break; case ERROR_CODE_SERVER_ERROR: CoroutineRunner.DeferredAction(5f, () => TryValidateAgain()); break; default: // неизвестная ошибка, начисляем инапп (поступаем в пользу игрока) request.OnSuccess(null); break; } } }
def validate_receipt(self, uid, data, platform):
InAppSlot = PlayerProgress.first(f"player_id={uid} AND slot_id='35'") if not InAppSlot: raise RuntimeError(f"Fail get slot purchases: not found player:{uid} data:{data}") tid = data.get("tid") params = [] orders_data = [] valid_orders = [] if not tid or tid in InAppSlot.content: return False params = str(tid).split(self.IN_APP_ID_SEPARATOR) if platform == "ios": transaction_id = params[0] product_id = params[1] orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id) error("[VALIDATION] {} {} {}".format(transaction_id, product_id, orders_data)) elif platform == "android": product_id = params[1] purchase_token = data.get("data") orders_data = self._get_receipt_android(product_id, purchase_token) elif platform == "amazon": receipt_sku = params[0] user_id = params[1] orders_data = self._get_receipt_amazon(user_id, receipt_sku) elif platform == "huawei": product_id = params[1] orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""), data.get("account_flag", 0)) elif platform == "udp": product_id = params[1] orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", "")) elif platform == "samsung": product_id = params[1] transaction_id = params[0] product_id = params[1] orders_data = self._get_receipt_samsung(data.get("data", ""), product_id) else: error("[InAppValidator] unknown platform") return False if not orders_data: error(f"[InAppValidator] fail get receipt {platform} player:{uid} data:{data}") return False key = f"inapp:{uid}:{tid}" for order in orders_data: if not order.is_success(): continue valid_orders.append(order) try: self.inapp_redis.setex(key, order.to_json(), 86400) except Exception as ex: exception(f"[InAppValidator] fail save inapp to redis: {ex}") if not valid_orders: warning(f"[InAppValidator] not valid receipt {orders_data[0].order_id}") return False return True def _get_receipt_android(self, product_id, token):
if not self.android_authorized: self._android_auth() debug(f"[InAppValidator] android product_id: {product_id}, token: {token}") try: product = self.android_publisher.purchases().products().get( packageName=config.GOOGLE_SERVICE_ACCOUNT['package_name'], productId=product_id, token=token).execute() except client.AccessTokenRefreshError: self.android_authorized = False return self._get_receipt_android(product_id, token) except google_errors.HttpError as ex: if ex.resp.status == 401 or ex.resp.status == 503: self.android_authorized = False return self._get_receipt_android(product_id, token) return False if not product: warning("[InAppValidator] android product is NONE") return None order_id = product.get('orderId') if not order_id: warning(f"order_id is NONE: {product}") return None return [Receipt(order_id, product.get('purchaseState', -1), product_id)] class Receipt: def __init__(self, order_id, status, product_id, user_id=None, expire=0, trial=0, refund=0, latest_receipt=''): self.order_id = order_id self.status = status self.product_id = product_id self.user_id = user_id self.expire = expire if str(trial) == 'true': self.trial = 1 else: self.trial = 0 self.refund = refund self.latest_receipt = latest_receipt def is_success(self): return self.status == 0 def is_canceled(self): return self.status == 3 def is_valid(self): return self.order_id and self.product_id def to_dict(self): return {"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt} def to_json(self): return json.dumps({"id": self.order_id, "s": self.status, "p": self.product_id, "u": self.user_id, "e":self.expire, "t":self.trial,"r":self.refund,"lr":self.latest_receipt}) def validate_receipt(self, data):
neededSlotsNames = [self.slotName] self.slots = self.get_slots_data(*neededSlotsNames) InAppSlot = self.slots.get(self.slotName, []) tid = data.get("tid") platform = data.get("pl") params = [] orders_data = [] valid_orders = [] if not tid: self.ThrowFail("not found required parameter") elif tid in InAppSlot: self.ThrowFail("already in slot") if not self.IsFail(): params = str(tid).split(self.IN_APP_ID_SEPARATOR) if not self.IsFail(): inapp_storage = InappStorage.get_instance() if inapp_storage.exists_transaction(self.platform, params[0]): self.ThrowFail("already_purchased {0} d".format(params[0]), VALIDATOR_RESULT_CODE.ALREADY_PURCHASED) self.FinalizeRequest({self.slotName: InAppSlot}, data) return # Try get from redis player_platform = self.platform if platform is not None and int(platform) == 4: player_platform = "udp" _prevalidate_order = self.inapp_redis.check_tid(self._player_id, tid) if _prevalidate_order: orders_data = Receipt.from_json(_prevalidate_order) elif player_platform == "ios": transaction_id = params[0] product_id = params[1] if not transaction_id or not product_id: self.ThrowFail(f"fail get receipt {self.platform}") else: orders_data = self._get_receipt_ios(data.get("data"), data.get("test") == 1, transaction_id, product_id) elif player_platform == "android": product_id = params[1] purchase_token = data.get("data") orders_data = self._get_receipt_android(product_id, purchase_token) elif player_platform == "amazon": receipt_sku = params[0] user_id = params[1] orders_data = self._get_receipt_amazon(user_id, receipt_sku) elif player_platform == "huawei": product_id = params[1] orders_data = self._get_receipt_huawei(product_id, tid, data.get("data", ""), data.get("account_flag", 0), data.get("subscribe")) elif platform == "udp": product_id = params[1] orders_data = self._get_receipt_udp(product_id, params[0], data.get("data", "")) elif platform == "samsung": product_id = params[1] transaction_id = params[0] product_id = params[1] orders_data = self._get_receipt_samsung(data.get("data", ""), product_id) else: self.ThrowFail("unknown platform") if not orders_data: self.ThrowFail(f"fail get receipt {player_platform} {self.platform}") if not self.IsFail(): for order in orders_data: if order.is_success(): valid_orders.append(order) if not valid_orders: self.ThrowFail("already_purchased {0}".format(orders_data[0].order_id), VALIDATOR_RESULT_CODE.ALREADY_PURCHASED) else: InAppSlot.append(tid) self.SetRequestSuccessful() if self._player_id in LOG_PLAYER_IDS: HashLog.error(f"[INAPP] id:{self._player_id} receipt:{data}") self.FinalizeRequest({self.slotName: InAppSlot}, data)
=========== Источник: habr.com =========== Похожие новости:
Блог компании Lightmap ), #_python, #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_igr ( Разработка игр ), #_unity |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:53
Часовой пояс: UTC + 5