AlexaController.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. # @Author : Rocky
  2. # @File : AlexaController.py
  3. # @Time : 2023/12/25 10:46
  4. import time
  5. import requests
  6. from django.views import View
  7. from Model.models import AlexaOauth
  8. from Object.ResponseObject import ResponseObject
  9. from Object.TokenObject import TokenObject
  10. from Ansjer.config import CONFIG_INFO, CONFIG_TEST, CONFIG_EUR
  11. # 域名
  12. ALEXA_DOMAIN = 'smart.loocam2.com'
  13. AMAZON_API_DOMAIN = 'api.amazon.com'
  14. UPDATE_TOKEN_URL = 'https://{}/appToApp/oa2/updateAmazonToken'.format(ALEXA_DOMAIN)
  15. # Alexa loocam skill配置信息
  16. # https://developer.amazon.com/alexa/console/ask
  17. # 开发中: development, 已上线: live
  18. LOOCAM_SKILL_STAGE = 'development' if CONFIG_INFO == CONFIG_TEST else 'live'
  19. LOOCAM_SKILL_ASIN = 'B0C94Q7H1L'
  20. LOOCAM_SKILL_ID = 'amzn1.ask.skill.ff5a5074-7ec7-442b-979b-cb57095f7a94'
  21. LOOCAM_CLIENT_ID = 'amzn1.application-oa2-client.98a01914518743e481d51115144dafb0'
  22. LOOCAM_CLIENT_SECRET = '43353cac67670aefd64a5f95309754ddd6bcfe8a087cc3cad1348b626f64b132'
  23. class AppToAppView(View):
  24. def get(self, request, *args, **kwargs):
  25. request.encoding = 'utf-8'
  26. operation = kwargs.get('operation')
  27. return self.validation(request.GET, operation, request)
  28. def post(self, request, *args, **kwargs):
  29. request.encoding = 'utf-8'
  30. operation = kwargs.get('operation')
  31. return self.validation(request.POST, operation, request)
  32. def validation(self, request_dict, operation, request):
  33. response = ResponseObject()
  34. if operation == 'updateToken': # 更新token
  35. return self.update_token(request_dict, response)
  36. token = TokenObject(request.META.get('HTTP_AUTHORIZATION'))
  37. if token.code != 0:
  38. return response.json(token.code)
  39. user_id = token.userID
  40. if operation == 'getAlexaAppURLAndLWAFallbackURL': # 获取Alexa App和LWA fallback URL
  41. return self.get_alexa_app_url_and_lwa_fallback_url(response)
  42. elif operation == 'accountLinkWithAmazonAuthorizationCode': # 通过亚马逊授权码连接账号
  43. return self.account_link_with_amazon_authorization_code(user_id, request_dict, response)
  44. elif operation == 'getAccountLinkingAndSkillStatus': # 获取账号连接和skill状态
  45. return self.get_account_linking_and_skill_status(user_id, response)
  46. elif operation == 'disableSkillAndUnlinkAccount': # 取消连接skill和账号
  47. return self.disable_skill_and_unlink_account(user_id, response)
  48. elif operation == 'getSkillPageURL': # 获取skill页面URL(取消链接)
  49. return self.get_skill_page_url(response)
  50. elif operation == 'getAlexaAppUrl': # 获取重定向至Alexa app的url
  51. return self.get_alexa_app_url(user_id, request_dict, response)
  52. else:
  53. return response.json(414)
  54. @staticmethod
  55. def update_token(request_dict, response):
  56. user_id = request_dict.get('user_id', None)
  57. amazon_access_token = request_dict.get('access_token', None)
  58. amazon_refresh_token = request_dict.get('refresh_token', None)
  59. if not all([user_id, amazon_access_token, amazon_refresh_token]):
  60. return response.json(444)
  61. now_time = int(time.time())
  62. try:
  63. # 保存令牌数据
  64. alexa_oauth_qs = AlexaOauth.objects.filter(user_id=user_id)
  65. if alexa_oauth_qs.exists():
  66. alexa_oauth_qs.update(amazon_access_token=amazon_access_token,
  67. amazon_refresh_token=amazon_refresh_token,
  68. update_time=now_time)
  69. else:
  70. AlexaOauth.objects.create(user_id=user_id, amazon_access_token=amazon_access_token,
  71. amazon_refresh_token=amazon_refresh_token, create_time=now_time,
  72. update_time=now_time)
  73. return response.json(0)
  74. except Exception as e:
  75. return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
  76. @staticmethod
  77. def get_alexa_app_url_and_lwa_fallback_url(response):
  78. skill_stage = LOOCAM_SKILL_STAGE
  79. redirect_uri = 'https://{}'.format(ALEXA_DOMAIN)
  80. alexa_app_url = 'https://alexa.amazon.com/spa/skill-account-linking-consent?' \
  81. 'fragment=skill-account-linking-consent&client_id={}&' \
  82. 'scope=alexa::skills:account_linking&skill_stage={}&response_type=code&' \
  83. 'redirect_uri={}'.format(LOOCAM_CLIENT_ID, skill_stage, redirect_uri)
  84. lwa_fallback_url = 'https://www.amazon.com/ap/oa?' \
  85. 'client_id={}&scope=alexa::skills:account_linking&response_type=code&redirect_uri={}&'.\
  86. format(LOOCAM_CLIENT_ID, redirect_uri)
  87. res = {
  88. 'alexa_app_url': alexa_app_url,
  89. 'lwa_fallback_url': lwa_fallback_url
  90. }
  91. return response.json(0, res)
  92. @classmethod
  93. def account_link_with_amazon_authorization_code(cls, user_id, request_dict, response):
  94. amazon_authorization_code = request_dict.get('amazon_authorization_code', None)
  95. if not amazon_authorization_code:
  96. return response.json(444)
  97. now_time = int(time.time())
  98. # 获取亚马逊访问令牌
  99. # https://developer.amazon.com/zh/docs/login-with-amazon/authorization-code-grant.html#access-token-request
  100. url = 'https://{}/auth/o2/token'.format(AMAZON_API_DOMAIN)
  101. redirect_uri = 'https://{}'.format(ALEXA_DOMAIN)
  102. data = {
  103. 'grant_type': 'authorization_code',
  104. 'code': amazon_authorization_code,
  105. 'client_id': LOOCAM_CLIENT_ID,
  106. 'client_secret': LOOCAM_CLIENT_SECRET,
  107. 'redirect_uri': redirect_uri
  108. }
  109. try:
  110. r = requests.post(url=url, data=data, timeout=10)
  111. assert r.status_code == 200
  112. res_data = eval(r.content)
  113. assert res_data.get('access_token')
  114. assert res_data.get('refresh_token')
  115. amazon_access_token = res_data['access_token']
  116. amazon_refresh_token = res_data['refresh_token']
  117. # 更新Alexa服务器令牌
  118. data = {
  119. 'user_id': user_id,
  120. 'access_token': amazon_access_token,
  121. 'refresh_token': amazon_refresh_token
  122. }
  123. r = requests.post(url=UPDATE_TOKEN_URL, data=data, timeout=10)
  124. assert r.status_code == 200
  125. # 保存令牌数据
  126. alexa_oauth_qs = AlexaOauth.objects.filter(user_id=user_id)
  127. if alexa_oauth_qs.exists():
  128. alexa_oauth_qs.update(amazon_access_token=amazon_access_token,
  129. amazon_refresh_token=amazon_refresh_token,
  130. update_time=now_time)
  131. else:
  132. AlexaOauth.objects.create(user_id=user_id, amazon_access_token=amazon_access_token,
  133. amazon_refresh_token=amazon_refresh_token, create_time=now_time,
  134. update_time=now_time)
  135. res = cls.get_auth_code_and_token(user_id)
  136. user_authorization_code = res['res']['user_authorization_code']
  137. data = {
  138. "stage": LOOCAM_SKILL_STAGE,
  139. "accountLinkRequest": {
  140. "redirectUri": redirect_uri,
  141. "authCode": user_authorization_code,
  142. "type": "AUTH_CODE"
  143. }
  144. }
  145. # 请求连接skill
  146. # https://developer.amazon.com/en-US/docs/alexa/smapi/skill-enablement.html
  147. headers = {
  148. 'Content-Type': 'application/json',
  149. 'Authorization': 'Bearer {}'.format(amazon_access_token)
  150. }
  151. alexa_api_endpoint_list = ['api.amazonalexa.com', 'api.eu.amazonalexa.com', 'api.fe.amazonalexa.com']
  152. for alexa_api_endpoint in alexa_api_endpoint_list:
  153. url = 'https://{}/v1/users/~current/skills/{}/enablement'.format(alexa_api_endpoint, LOOCAM_SKILL_ID)
  154. r = requests.post(headers=headers, url=url, json=data, timeout=30)
  155. if r.status_code == 201:
  156. AlexaOauth.objects.filter(user_id=user_id).\
  157. update(alexa_api_endpoint=alexa_api_endpoint, link_status=1)
  158. res_data = eval(r.content)
  159. return response.json(0, res_data)
  160. return response.json(0)
  161. except Exception as e:
  162. return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
  163. @classmethod
  164. def get_account_linking_and_skill_status(cls, user_id, response):
  165. # 未连接状态响应数据
  166. res_data = {
  167. 'accountLink': {
  168. 'status': 'NOT_LINKED'
  169. },
  170. 'status': 'DISABLED'
  171. }
  172. try:
  173. alexa_oauth_qs = AlexaOauth.objects.filter(user_id=user_id).values('link_status')
  174. if alexa_oauth_qs.exists():
  175. link_status = alexa_oauth_qs[0]['link_status']
  176. # 连接状态为1,通过api获取状态
  177. if link_status == 1:
  178. request_method = 'get'
  179. try:
  180. r = cls.refresh_access_token(user_id, request_method)
  181. except AssertionError:
  182. alexa_oauth_qs.update(link_status=0)
  183. else:
  184. res_data = eval(r.content)
  185. return response.json(0, res_data)
  186. except Exception as e:
  187. return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
  188. @classmethod
  189. def disable_skill_and_unlink_account(cls, user_id, response):
  190. request_method = 'delete'
  191. try:
  192. r = cls.refresh_access_token(user_id, request_method)
  193. if r is not None:
  194. # 2xx响应状态码为成功
  195. assert str(r.status_code)[:1] == '2'
  196. AlexaOauth.objects.filter(user_id=user_id).update(link_status=0)
  197. return response.json(0)
  198. except Exception as e:
  199. return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
  200. @staticmethod
  201. def get_skill_page_url(response):
  202. skill_page_url = 'https://alexa.amazon.com/spa/index.html#skills/dp/{}'.format(LOOCAM_SKILL_ASIN)
  203. lwa_page_url = 'https://www.amazon.com/dp/{}'.format(LOOCAM_SKILL_ASIN)
  204. res = {
  205. 'skill_page_url': skill_page_url,
  206. 'lwa_page_url': lwa_page_url
  207. }
  208. return response.json(0, res)
  209. @staticmethod
  210. def refresh_access_token(user_id, request_method):
  211. if request_method not in ['get', 'delete']:
  212. return
  213. alexa_oauth_qs = AlexaOauth.objects.filter(user_id=user_id).\
  214. values('alexa_api_endpoint', 'amazon_refresh_token')
  215. if not alexa_oauth_qs:
  216. return
  217. now_time = int(time.time())
  218. # 使用刷新令牌获取新的访问令牌
  219. # https://developer.amazon.com/zh/docs/login-with-amazon/authorization-code-grant.html#using-refresh-tokens
  220. alexa_api_endpoint = alexa_oauth_qs[0]['alexa_api_endpoint']
  221. amazon_refresh_token = alexa_oauth_qs[0]['amazon_refresh_token']
  222. url = 'https://{}/auth/o2/token'.format(AMAZON_API_DOMAIN)
  223. data = {
  224. 'grant_type': 'refresh_token',
  225. 'refresh_token': amazon_refresh_token,
  226. 'client_id': LOOCAM_CLIENT_ID,
  227. 'client_secret': LOOCAM_CLIENT_SECRET
  228. }
  229. r = requests.post(url=url, data=data, timeout=10)
  230. assert r.status_code == 200
  231. res_data = eval(r.content)
  232. assert res_data.get('access_token')
  233. assert res_data.get('refresh_token')
  234. new_access_token = res_data['access_token']
  235. new_refresh_token = res_data['refresh_token']
  236. # 更新Alexa服务器令牌
  237. data = {
  238. 'user_id': user_id,
  239. 'access_token': new_access_token,
  240. 'refresh_token': new_refresh_token
  241. }
  242. r = requests.post(url=UPDATE_TOKEN_URL, data=data, timeout=10)
  243. assert r.status_code == 200
  244. alexa_oauth_qs.update(
  245. amazon_access_token=new_access_token, amazon_refresh_token=new_refresh_token, update_time=now_time)
  246. headers = {
  247. 'Content-Type': 'application/json',
  248. 'Authorization': 'Bearer {}'.format(new_access_token)
  249. }
  250. url = 'https://{}/v1/users/~current/skills/{}/enablement'.format(alexa_api_endpoint, LOOCAM_SKILL_ID)
  251. if request_method == 'get':
  252. r = requests.get(headers=headers, url=url, timeout=30)
  253. elif request_method == 'delete':
  254. r = requests.delete(headers=headers, url=url, timeout=30)
  255. return r
  256. @classmethod
  257. def get_alexa_app_url(cls, user_id, request_dict, response):
  258. response_type = request_dict.get('response_type', None)
  259. operate = request_dict.get('operate', None)
  260. state = request_dict.get('state', None)
  261. redirect_uri = request_dict.get('redirect_uri', None)
  262. if not all([state, redirect_uri]) or response_type not in ['code', 'token'] or operate not in ['accept', 'deny']:
  263. return response.json(444)
  264. try:
  265. redirect_uri += '?state={}'.format(state)
  266. if operate == 'accept':
  267. redirect_uri += '&source=app'
  268. res = cls.get_auth_code_and_token(user_id)
  269. if response_type == 'code':
  270. # 获取用户授权码
  271. user_authorization_code = res['res']['user_authorization_code']
  272. redirect_uri += '&code={}'.format(user_authorization_code)
  273. elif response_type == 'token':
  274. # 获取令牌
  275. refresh_token = res['res']['refresh_token']
  276. redirect_uri += '&token={}&token_type=Bearer&expiration_time=3600'.format(refresh_token)
  277. AlexaOauth.objects.filter(user_id=user_id).update(link_status=1)
  278. else:
  279. AlexaOauth.objects.filter(user_id=user_id).update(link_status=0)
  280. redirect_uri += '&error=access_denied&error_description=The%20user%20denied%20the%20request.%20'
  281. res = {
  282. 'redirect_uri': redirect_uri
  283. }
  284. return response.json(0, res)
  285. except Exception as e:
  286. return response.json(500, 'error_line:{}, error_msg:{}'.format(e.__traceback__.tb_lineno, repr(e)))
  287. @staticmethod
  288. def get_auth_code_and_token(user_id):
  289. """
  290. 请求Alexa项目接口获取验证码和令牌
  291. @param user_id: 用户id
  292. @return:
  293. """
  294. base_url = 'https://{}'.format(ALEXA_DOMAIN)
  295. url = base_url + '/appToApp/oa2/getAuthCodeAndToken'
  296. region_code = 'EU'
  297. if CONFIG_INFO != CONFIG_EUR:
  298. region_code = 'US'
  299. params = {
  300. 'user_id': user_id,
  301. 'region_code': region_code
  302. }
  303. r = requests.get(url=url, params=params, timeout=10)
  304. assert r.status_code == 200
  305. return eval(r.content)