217 lines
7.3 KiB
Python
217 lines
7.3 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
import requests
|
|
import time
|
|
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.addons.sale_shopee import const
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
# === CUSTOM EXCEPTION CLASSES === #
|
|
|
|
class ShopeeRateLimitError(Exception):
|
|
""" When the API rate limit of Shopee is reached. """
|
|
|
|
def __init__(self, operation):
|
|
self.operation = operation
|
|
super().__init__()
|
|
|
|
|
|
# === ONBOARDING === #
|
|
|
|
def get_public_sign(account, path, timestamp):
|
|
""" Given the request params, returns the signing string used to sign the request.
|
|
|
|
:param account: shopee.account
|
|
:param str path: The path of the request
|
|
:param int timestamp: The timestamp of the request
|
|
:return: The public signing string
|
|
:rtype: str
|
|
"""
|
|
account.ensure_one()
|
|
return hmac.new(
|
|
account.partner_key.encode(),
|
|
_get_public_signing_string(account, path, timestamp).encode(),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
|
|
# === API COMMUNICATIONS === #
|
|
|
|
def request_access_token(shop, main_account_id=False, is_refresh=False):
|
|
""" Request the access tokens for a specific shop.
|
|
|
|
see https://open.shopee.com/developer-guide/20
|
|
|
|
Note: self.ensure_one()
|
|
|
|
:param shop: shopee.shop
|
|
:param int main_account_id:
|
|
:param bool is_refresh: If True, the request is to refresh the token
|
|
:return: The list of shop identifiers associated with the main account
|
|
:rtype: list
|
|
:raise: UserError if authorization_code is missing but it's not a refresh request
|
|
"""
|
|
shop.ensure_one()
|
|
|
|
authorization_code = shop.env.context.get('authorization_code')
|
|
if not is_refresh and not authorization_code:
|
|
raise UserError(shop.env._(
|
|
"Authorization Code is required to request an Access Token."
|
|
" Please authorize the connection first."
|
|
))
|
|
|
|
body = {
|
|
'partner_id': shop.account_id.partner_identifier,
|
|
}
|
|
if main_account_id:
|
|
body['main_account_id'] = main_account_id
|
|
else:
|
|
body['shop_id'] = shop.shop_identifier
|
|
|
|
if is_refresh:
|
|
body['refresh_token'] = shop.refresh_token
|
|
operation = 'refresh_token'
|
|
else:
|
|
body['code'] = authorization_code
|
|
operation = 'get_token'
|
|
|
|
response = make_shopee_api_request(shop, operation, body=body, method='POST')
|
|
expire_in = response['expire_in']
|
|
shop.update({
|
|
'refresh_token': response['refresh_token'],
|
|
'access_token': response['access_token'],
|
|
'access_token_expiration_date': datetime.now() + timedelta(seconds=expire_in),
|
|
})
|
|
return response.get('shop_id_list')
|
|
|
|
|
|
def make_shopee_api_request(shop, operation, params=None, body=None, method='GET'):
|
|
""" Send an API request to Shopee.
|
|
|
|
:param shop: shopee.shop
|
|
:param operation: str
|
|
:param dict params: Query parameters
|
|
:param dict body: Request body
|
|
:param str method: HTTP method
|
|
:return: The response content of the request
|
|
:rtype: dict
|
|
:raise: UserError if the request fails
|
|
"""
|
|
shop.ensure_one()
|
|
account = shop.account_id
|
|
api_type = const.API_OPERATIONS_MAPPING[operation]['api_type']
|
|
|
|
if not params:
|
|
params = {}
|
|
|
|
if api_type != 'public': # Require an access token
|
|
params['shop_id'] = shop.shop_identifier
|
|
# Ensure that we have an access token. If not, we request one.
|
|
if not shop.access_token or not shop.access_token_expiration_date:
|
|
request_access_token(shop)
|
|
# If we have one, but it expired, we refresh it
|
|
elif shop.access_token_expiration_date < datetime.now() + timedelta(minutes=5):
|
|
request_access_token(shop, is_refresh=True)
|
|
params['access_token'] = shop.access_token
|
|
|
|
path = const.API_OPERATIONS_MAPPING[operation]['url_path']
|
|
timestamp = int(time.time())
|
|
params.update({
|
|
'partner_id': account.partner_identifier,
|
|
'sign': get_api_sign(shop, operation, timestamp),
|
|
'timestamp': timestamp,
|
|
})
|
|
|
|
_logger.info(
|
|
"Operation: %(operation)s\n\tParams: %(params)s\n\tBody: %(body)s",
|
|
{'operation': operation, 'params': params, 'body': body if body else '/'},
|
|
)
|
|
|
|
resp = requests.request(
|
|
method,
|
|
f'{const.API_PATHS[account.api_endpoint]}{path}',
|
|
params=params,
|
|
json=body,
|
|
)
|
|
if resp.status_code == 429:
|
|
raise ShopeeRateLimitError(operation)
|
|
if resp.status_code != 200:
|
|
raise ValidationError(shop.env._(
|
|
"At shop - %(shop_name)s, error %(error_code)s occurred during operation"
|
|
" %(operation)s when contacting the Shopee API:\n\n%(error_message)s",
|
|
shop_name=shop.name,
|
|
operation=operation,
|
|
error_code=resp.status_code,
|
|
error_message=resp.text,
|
|
))
|
|
if not 'application/json' in resp.headers.get('Content-Type', ''):
|
|
response_content = resp.content
|
|
else:
|
|
response_content = resp.json()
|
|
|
|
# Simple error catching with a clear message on the issue.
|
|
if response_content.get('error'):
|
|
message = _get_error_message(response_content)
|
|
raise UserError(shop.env._(
|
|
"At shop - %(shop_name)s, error %(error_code)s occurred during operation"
|
|
" %(operation)s when contacting the Shopee API. Shopee sent:\n\n%(error_message)s",
|
|
shop_name=shop.name,
|
|
operation=operation,
|
|
error_code=response_content['error'],
|
|
error_message=message,
|
|
))
|
|
_logger.info(response_content)
|
|
if isinstance(response_content, dict) and response_content.get('response'):
|
|
return response_content['response']
|
|
return response_content
|
|
|
|
|
|
def get_api_sign(shop, operation, timestamp):
|
|
""" Get the API sign for a specific shop.
|
|
|
|
:param shop: shopee.shop
|
|
:param str operation: The operation to perform
|
|
:param int timestamp: The timestamp of the request
|
|
:return: The API sign
|
|
:rtype: str
|
|
"""
|
|
shop.ensure_one()
|
|
account = shop.account_id
|
|
api_type = const.API_OPERATIONS_MAPPING[operation]['api_type']
|
|
path = const.API_OPERATIONS_MAPPING[operation]['url_path']
|
|
signing_string = _get_public_signing_string(account, path, timestamp)
|
|
if api_type != 'public':
|
|
signing_string += f'{shop.access_token}{shop.shop_identifier}'
|
|
return hmac.new(
|
|
account.partner_key.encode(),
|
|
signing_string.encode(),
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
|
|
|
|
def _get_public_signing_string(account, path, timestamp):
|
|
""" Given the request params, returns the signing string used to sign the request. """
|
|
return f'{account.partner_identifier}{path}{timestamp}'
|
|
|
|
|
|
def _get_error_message(response_content):
|
|
""" Create an error message from the response content of a Shopee API request.
|
|
|
|
:param dict response_content: The response content of the request
|
|
:return: The error message
|
|
:rtype: str
|
|
"""
|
|
message = response_content['message']
|
|
if response_content.get('response'):
|
|
for order in response_content['response'].get('result_list', []):
|
|
if order and order.get('fail_message'):
|
|
message += f"\n{order['order_sn']}: {order['fail_message']}"
|
|
return message
|