import json import tornado from core.utils import safe_json_dumps from webserver.handlers.api.addspot import APISpotHandler from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler from webserver.handlers.api.lookups import APILookupActivityRefHandler from webserver.handlers.api.options import APIOptionsHandler from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler from webserver.handlers.api.status import APIStatusHandler # Query parameters renamed in v3 _V2_TO_V3_QUERY_PARAMS = { "sig": "activity", "needs_sig": "needs_activity", "needs_sig_ref": "needs_activity_ref", } # Values of query parameters renamed in v3 _V2_TO_V3_QUERY_VALUES = { "activity": {"NO_SIG": "NO_ACTIVITY"}, "fields": {"sig": "activity", "sig_refs": "activity_refs"}, } # Keys of JSON objects in API responses renamed in v3 _V3_TO_V2_RESPONSE_KEYS = { "activity": "sig", "activity_refs": "sig_refs", "activity_type": "sig_type", "activities": "sigs", "activity_ref_data_providers": "sig_ref_data_providers", "activity_name": "sig_name", } # Values of JSON objects in API responses renamed in v3 _V3_TO_V2_RESPONSE_VALUES = { "dx_location_source": {"ACTIVITY REF LOOKUP": "SIG REF LOOKUP"}, } def _translate_v2_query_params(handler): """Rename any v2 query parameters (and values) in the request to their v3 equivalents, so the v3 handler can understand them.""" for arguments in (handler.request.arguments, handler.request.query_arguments): for v2_name, v3_name in _V2_TO_V3_QUERY_PARAMS.items(): if v2_name in arguments: arguments[v3_name] = arguments.pop(v2_name) for name, value_map in _V2_TO_V3_QUERY_VALUES.items(): if name in arguments: arguments[name] = [ ",".join(value_map.get(item.strip(), item) for item in v.decode("utf-8").split(",")).encode("utf-8") for v in arguments[name] ] def _translate_v3_response_object(obj): """Rename keys and values in an object from their v3 names to their v2 names.""" if isinstance(obj, dict): translated = {} for k, v in obj.items(): if k in _V3_TO_V2_RESPONSE_VALUES and isinstance(v, str): v = _V3_TO_V2_RESPONSE_VALUES[k].get(v, v) translated[_V3_TO_V2_RESPONSE_KEYS.get(k, k)] = _translate_v3_response_object(v) return translated if isinstance(obj, list): return [_translate_v3_response_object(i) for i in obj] return obj def translate_v3_response(chunk): """Translate a JSON string output by a v3 handler into its v2 equivalent""" if not isinstance(chunk, str): return chunk try: return safe_json_dumps(_translate_v3_response_object(json.loads(chunk))) except ValueError: return chunk def translate_v2_spot(spot_data): """Translate a spot provided by a client in v2 format into v3 format""" spot_data = dict(spot_data) if "sig" in spot_data: spot_data["activity"] = spot_data.pop("sig") if "sig_refs" in spot_data: spot_data["activity_refs"] = spot_data.pop("sig_refs") if isinstance(spot_data.get("activity_refs"), list): refs = [] for ref in spot_data["activity_refs"]: if isinstance(ref, dict) and "sig" in ref: ref = dict(ref) ref["activity"] = ref.pop("sig") refs.append(ref) spot_data["activity_refs"] = refs return spot_data class _V2ResponseTranslationMixin: """Mixin for request handlers that translates v2 query params to v3 on the way in, and v3 JSON responses to v2 on the way out""" def prepare(self): _translate_v2_query_params(self) super().prepare() def write(self, chunk): super().write(translate_v3_response(chunk)) class _V2StreamTranslationMixin: """Mixin for SSE handlers that translates v2 query params to v3 on the way in, and v3 JSON messages to v2 on the way out""" def prepare(self): _translate_v2_query_params(self) super().prepare() def write_message(self, name=None, msg=True, wait=None, evt_id=None): if not name: msg = translate_v3_response(msg) return super().write_message(name=name, msg=msg, wait=wait, evt_id=evt_id) class V2APISpotsHandler(_V2ResponseTranslationMixin, APISpotsHandler): """API request handler for /api/v2/spots (GET). Included in Spothole v3 for backwards compatibility.""" class V2APISpotsStreamHandler(_V2StreamTranslationMixin, APISpotsStreamHandler): """API request handler for /api/v2/spots/stream (SSE). Included in Spothole v3 for backwards compatibility.""" class V2APIAlertsHandler(_V2ResponseTranslationMixin, APIAlertsHandler): """API request handler for /api/v2/alerts (GET). Included in Spothole v3 for backwards compatibility.""" class V2APIAlertsStreamHandler(_V2StreamTranslationMixin, APIAlertsStreamHandler): """API request handler for /api/v2/alerts/stream (SSE). Included in Spothole v3 for backwards compatibility.""" class V2APIOptionsHandler(_V2ResponseTranslationMixin, APIOptionsHandler): """API request handler for /api/v2/options (GET). Included in Spothole v3 for backwards compatibility.""" class V2APIStatusHandler(_V2ResponseTranslationMixin, APIStatusHandler): """API request handler for /api/v2/status (GET). Included in Spothole v3 for backwards compatibility.""" class V2APILookupSigRefHandler(_V2ResponseTranslationMixin, APILookupActivityRefHandler): """API request handler for /api/v2/lookup/sigref (GET). Included in Spothole v3 for backwards compatibility. This is the v2 equivalent of /api/v3/lookup/activityref.""" class V2APISpotHandler(APISpotHandler): """API request handler for /api/v2/spot (POST). Included in Spothole v3 for backwards compatibility. Translates the spot in the request body from v2 to v3 format. The response is a plain status message so needs no translation.""" def post(self): # Translate the request body if we can. If the body is empty or invalid JSON, leave it alone and let the v3 # handler return the appropriate error. try: json_body = tornado.escape.json_decode(self.request.body) if isinstance(json_body, dict) and isinstance(json_body.get("spot"), dict): json_body["spot"] = translate_v2_spot(json_body["spot"]) self.request.body = json.dumps(json_body).encode("utf-8") except ValueError: pass super().post()