diff --git a/api/account/account_api/api.py b/api/account/account_api/api.py index c7c1a976..8ecf0b42 100644 --- a/api/account/account_api/api.py +++ b/api/account/account_api/api.py @@ -64,7 +64,7 @@ device_endpoint = DeviceEndpoint.as_view('device_endpoint') acct.add_url_rule( '/api/devices', view_func=device_endpoint, - methods=['GET'] + methods=['GET', 'POST'] ) preferences_endpoint = AccountPreferencesEndpoint.as_view( diff --git a/api/account/account_api/endpoints/device.py b/api/account/account_api/endpoints/device.py index afc317e0..a211663b 100644 --- a/api/account/account_api/endpoints/device.py +++ b/api/account/account_api/endpoints/device.py @@ -1,25 +1,68 @@ -from dataclasses import asdict from http import HTTPStatus +from logging import getLogger + +from flask import json +from schematics import Model +from schematics.types import StringType +from schematics.exceptions import ValidationError from selene.api import SeleneEndpoint from selene.data.device import DeviceRepository +from selene.util.cache import SeleneCache from selene.util.db import get_db_connection +ONE_DAY = 86400 + +_log = getLogger() + + +def validate_pairing_code(pairing_code): + cache_key = 'pairing.code:' + pairing_code + cache = SeleneCache() + pairing_cache = cache.get(cache_key) + + if pairing_cache is None: + raise ValidationError('pairing code not found') + + +class NewDeviceRequest(Model): + city = StringType(required=True) + country = StringType(required=True) + name = StringType(required=True) + pairing_code = StringType(required=True, validators=[validate_pairing_code]) + placement = StringType() + region = StringType(required=True) + timezone = StringType(required=True) + wake_word = StringType(required=True) + voice = StringType(required=True) + class DeviceEndpoint(SeleneEndpoint): + def __init__(self): + super(DeviceEndpoint, self).__init__() + self.devices = None + self.cache = SeleneCache() + def get(self): self._authenticate() + self._get_devices() + response_data = self._build_response() + + return response_data, HTTPStatus.OK + + def _get_devices(self): with get_db_connection(self.config['DB_CONNECTION_POOL']) as db: device_repository = DeviceRepository(db) - devices = device_repository.get_devices_by_account_id( + self.devices = device_repository.get_devices_by_account_id( self.account.id ) + def _build_response(self): response_data = [] - for device in devices: + for device in self.devices: wake_word = dict( id=device.wake_word.id, - name=device.wake_word.wake_word + name=device.wake_word.display_name ) voice = dict( id=device.text_to_speech.id, @@ -28,7 +71,7 @@ class DeviceEndpoint(SeleneEndpoint): geography = dict( id=device.geography.id, country=device.geography.country, - region=device.geography.state, + region=device.geography.region, city=device.geography.city, timezone=device.geography.time_zone, latitude=device.geography.latitude, @@ -52,4 +95,70 @@ class DeviceEndpoint(SeleneEndpoint): ) ) - return response_data, HTTPStatus.OK + return response_data + + def post(self): + self._authenticate() + device = self._validate_request() + device_id = self._pair_device(device) + + return device_id, HTTPStatus.OK + + def _validate_request(self) -> NewDeviceRequest: + request_data = json.loads(self.request.data) + device = NewDeviceRequest() + device.city = request_data['city'] + device.country = request_data['country'] + device.name = request_data['name'] + device.pairing_code = request_data['pairingCode'] + device.placement = request_data['placement'] + device.region = request_data['region'] + device.timezone = request_data['timezone'] + device.wake_word = request_data['wakeWord'] + device.voice = request_data['voice'] + device.validate() + + return device + + def _pair_device(self, device): + with get_db_connection(self.config['DB_CONNECTION_POOL']) as db: + db.autocommit = False + try: + pairing_data = self._get_pairing_data(device.pairing_code) + device_id = self._add_device(device) + pairing_data['uuid'] = device_id + self.cache.delete('pairing.code:{}'.format(device.pairing_code)) + self._build_pairing_token(pairing_data) + except Exception: + db.rollback() + raise + else: + db.commit() + + return device_id + + def _get_pairing_data(self, pairing_code: str) -> dict: + """Checking if there's one pairing session for the pairing code.""" + cache_key = 'pairing.code:' + pairing_code + pairing_cache = self.cache.get(cache_key) + pairing_data = json.loads(pairing_cache) + + return pairing_data + + def _add_device(self, device: NewDeviceRequest): + """Creates a device and associate it to a pairing session""" + with get_db_connection(self.config['DB_CONNECTION_POOL']) as db: + device_repository = DeviceRepository(db) + device_id = device_repository.add_device( + self.account.id, + device.to_native() + ) + + return device_id + + def _build_pairing_token(self, pairing_data): + self.cache.set_with_expiration( + key='pairing.token:' + pairing_data['token'], + value=json.dumps(pairing_data), + expiration=ONE_DAY + )