async def _handle(self, data: Dict[str, Any]) -> None: """Handles message from websocket.""" server_log.debug(f"Received ws: {data}") op = data["op"] if op == Opcode.HEARTBEAT.value: self._last_hb = time.time() await self.notify(opcode=Opcode.HEARTBEAT_ACK) elif op == Opcode.IDENTIFY.value: try: token = Token.from_string( data["d"]["token"], self._emitter._app["pg_conn"] ) if not await token.verify(): # ValueError possible raise ValueError except (ValueError, RuntimeError): await self.notify(opcode=Opcode.INVALIDATE_SESSION) await self.close(code=CloseCode.BAD_TOKEN) return self.user_id = token.user_id await self._emitter.add_listener(self)
async def send_message( targets: List[str], text: str, config: Dict[str, Any], loop: Optional[asyncio.AbstractEventLoop] = None, ) -> None: if loop is None: loop = asyncio.get_running_loop() server_log.debug(f"Sending email to {targets}") config = config["email-confirmation"]["smtp"] for i in ("host", "login", "password"): if config.get(i) is None: server_log.info( f"Bad configuration, unable to send the following email:\n{text}" ) return await loop.run_in_executor( None, _send_message, targets, text, config["host"], config["login"], config["password"], )
async def match_info_validator( req: web.Request, handler: HandlerType ) -> web.Response: req["match_info"] = {} for key, value in req.match_info.items(): if not key.endswith("_id"): continue if value == "@me": try: token_header = req.headers["Authorization"] except KeyError: raise web.HTTPUnauthorized( reason="No access token passed, unable to decode @me" ) try: # not doing propper token checks here, not even token structure if token_header.startswith("Bearer "): token_header = token_header[7:] value = Token.decode_user_id(token_header.partition(".")[0]) except (ValueError, binascii.Error): raise web.HTTPUnauthorized(reason="Bad access token passed") try: req["match_info"][key] = await converters.ID().convert( value, req.app ) except (converters.ConvertError, converters.CheckError) as e: server_log.debug(f"match_validator: {value}: {e}") raise web.HTTPNotFound return await handler(req)
async def wrapper(req: web.Request) -> web.StreamResponse: content_type_matches = False for content_type in content_types: if content_type.value == req.content_type: content_type_matches = True break if not content_type_matches: return web.json_response( { "message": f"Bad content type. Expected: {[c.value for c in content_types]}" }, status=400, ) if content_type == ContentType.JSON: try: query = await req.json() except json.JSONDecodeError as e: return web.json_response( {"message": f"Error parsing json from body: {e}"}, status=400, ) elif content_type in ( ContentType.URLENCODED, ContentType.FORM_DATA, ): query = await req.post() else: server_log.debug( f"body_params: unknown content type: {content_type}") if unique: repeating = get_repeating(query.keys()) if repeating is not None: if json_response: return web.json_response( {repeating: "Repeats in body"}, status=400) raise web.HTTPBadRequest( reason=f"{repeating}: Repeats in body") req["body"] = req.get("body", {}) try: req["body"].update( **await converters.convert_map(params, query, req.app)) except converters.ConvertError as e: return e.to_bad_request(json_response) return await endpoint(req)
async def notify_everyone(self, event: GlobalEvent) -> None: """Discpatches event for all connected users.""" server_log.debug(f"Notifying everyone: {event}") to_close = [] async with self._lock: for listeners in self._listeners.values(): for listener in listeners: if not await listener.event_notify(event): to_close.append(listener) for listener in to_close: await listener.close()
async def notify_channel(self, event: LocalEvent) -> None: """Dispatches event for all users in channel of event.""" server_log.debug(f"Notifying channel {event.channel_id}: {event}") to_close = [] async with self._lock: for user_id in self._channels.get(event.channel_id, ()): for listener in self._listeners[user_id]: if not await listener.event_notify(event): to_close.append(listener) for listener in to_close: await listener.close()
async def notify( self, *, opcode: Opcode, data: Optional[Any] = None ) -> bool: """Sends data to websocket.""" try: if data is None: await self.ws.send_json({"op": opcode.value}) else: await self.ws.send_json({"op": opcode.value, "d": data}) except RuntimeError: # ws closed (dirty) server_log.debug("WS closed by user (dirty)") return False return True
async def event_notify(self, event: Event) -> bool: """Sends dispatch message to websocket with event payload.""" try: await self.ws.send_json( { "op": Opcode.DISPATCH.value, "d": event.payload, "t": event.name, } ) except RuntimeError: # ws closed (dirty) server_log.debug("WS closed by user (dirty)") return False return True
async def notify_channels(self, event: OuterEvent) -> None: """ Dispatches event for all users sharing channel with user of event. """ server_log.debug(f"Notifying user {event.user_id} channels: {event}") to_close = [] async with self._lock: for channel_id in self._users.get(event.user_id, ()): for user_id in self._channels[channel_id]: for listener in self._listeners[user_id]: if not await listener.event_notify(event): to_close.append(listener) for listener in to_close: await listener.close()
async def _check_hb(self) -> None: """ A task that checks for user heartbeat responses every HEARTBEAT_INTERVAL milliseconds with one tenth precision. """ interval = HEARTBEAT_INTERVAL / 1000 response_error_treshold = interval / 10 while not self.ws.closed: await asyncio.sleep(interval) response_error = (time.time() - self._last_hb) - interval if response_error > response_error_treshold: server_log.debug(f"Heartbeat: expired for user {self.user_id}") break server_log.debug(f"Heartbeat: closing listener {self}") await self.close()
async def convert_map( converters: typing.Dict[str, Converter], query: typing.Mapping[str, InputType], app: aiohttp.web.Application, location: str = "body", ) -> typing.Dict[str, typing.Any]: """ Converts mapping of elements generating new map with the same keys. Supports recursive maps/lists conversion. Propperly indicates problematic value in case of error with ConvertError. Known issues: Allows type implicit type casts, e.g: 1 can be treated as string """ result = {} if not isinstance(query, typing.Mapping): raise ConvertError(f"Expected dict, got {type(query).__name__}", "") for name, converter in converters.items(): if name not in query: try: # not converting default value to type, be careful result[name] = converter.get_default() continue except KeyError: # no default value server_log.debug(f"Parameter {name}: missing") raise ConvertError(f"Missing from {location}", name) try: result[name] = await converter.convert(query[name], app) except ConvertError as e: e.update_parameter(name) if type(e) is BadArgument: server_log.debug( f"Bad argument for parameter {e.parameter}: {e}") raise ConvertError( f"Should be of type {converter} in {location}", e.parameter) elif type(e) is CheckError: server_log.debug(f"Parameter {name}: check failed: {e}") raise ConvertError(f"Check failed in {location}: {e}", e.parameter) else: raise ConvertError(str(e), e.parameter) return result