class ChatHandler(WebSocketHandler): @property def db(self): if hasattr(self, "_db") and self._db is not None: return self._db else: self._db = sm() return self._db @property def loop(self): return asyncio.get_event_loop() def get_chat_user(self): return self.db.query( ChatUser, User, AnyChat, ).join( User, ChatUser.user_id == User.id, ).join( AnyChat, ChatUser.chat_id == AnyChat.id, ).filter(and_( ChatUser.user_id == self.user_id, ChatUser.chat_id == self.chat_id, )).one() def set_typing(self, is_typing): if not hasattr(self, "channels") or not hasattr(self, "user_number"): return func = self.user_list.user_start_typing if is_typing else self.user_list.user_stop_typing if func(self.user_number): redis.publish(self.channels["typing"], json.dumps({ "typing": self.user_list.user_numbers_typing(), })) def write_message(self, *args, **kwargs): try: super().write_message(*args, **kwargs) except WebSocketClosedError: return def check_origin(self, origin): if "localhost" in os.environ["BASE_DOMAIN"].lower(): return True return origin_regex.match(origin) is not None @coroutine def prepare(self): self.id = str(uuid4()) self.joined = False try: self.session_id = self.cookies["newparp"].value self.chat_id = int(self.path_args[0]) self.user_id = int(redis.get("session:%s" % self.session_id)) except (KeyError, TypeError, ValueError): self.send_error(400) return try: self.chat_user, self.user, self.chat = yield thread_pool.submit(self.get_chat_user) except NoResultFound: self.send_error(404) return # Remember the user number so typing notifications can refer to it # without reopening the database session. self.user_number = self.chat_user.number queue_user_meta(self, redis, self.request.headers.get("X-Forwarded-For", self.request.remote_ip)) self.user_list = UserListStore(redis_chat, self.chat_id) try: if self.user.group != "active": raise BannedException yield thread_pool.submit(authorize_joining, self.db, self) except (UnauthorizedException, BannedException, TooManyPeopleException): self.send_error(403) return @coroutine def open(self, chat_id): sockets.add(self) if DEBUG: print("socket opened: %s %s %s" % (self.id, self.chat.url, self.user.username)) try: yield thread_pool.submit(kick_check, redis, self) except KickedException: self.write_message(json.dumps({"exit": "kick"})) self.close() return # Subscribe self.channels = { "chat": "channel:%s" % self.chat_id, "user": "******" % (self.chat_id, self.user_id), "typing": "channel:%s:typing" % self.chat_id, } if self.chat.type == "pm": self.channels["pm"] = "channel:pm:%s" % self.user_id self.redis_task = asyncio.ensure_future(self.redis_listen()) # Send backlog. try: after = int(self.request.query_arguments["after"][0]) except (KeyError, IndexError, ValueError): after = 0 messages = redis_chat.zrangebyscore("chat:%s" % self.chat_id, "(%s" % after, "+inf") self.write_message(json.dumps({ "chat": self.chat.to_dict(), "messages": [json.loads(_) for _ in messages], })) online_state_changed = self.user_list.socket_join(self.id, self.session_id, self.user_id) self.joined = True # Send a join message to everyone if we just joined, otherwise send the # user list to the client. if online_state_changed: yield thread_pool.submit(send_join_message, self.user_list, self.db, redis, self) else: userlist = yield thread_pool.submit(get_userlist, self.user_list, self.db) self.write_message(json.dumps({"users": userlist})) self.db.commit() self.db.close() def on_message(self, message): if DEBUG: print("message: %s" % message) if message == "ping": try: self.user_list.socket_ping(self.id) except PingTimeoutException: # We've been reaped, so disconnect. self.close() return elif message in ("typing", "stopped_typing"): self.set_typing(message == "typing") def on_close(self): # Unsubscribe here and let the exit callback handle disconnecting. if hasattr(self, "redis_task"): self.redis_task.cancel() if hasattr(self, "redis_client"): self.redis_client.close() if hasattr(self, "close_code") and self.close_code in (1000, 1001): message_type = "disconnect" else: message_type = "timeout" if self.joined and self.user_list.socket_disconnect(self.id, self.user_number): try: send_quit_message(self.user_list, self.db, *self.get_chat_user(), type=message_type) except NoResultFound: send_userlist(self.user_list, self.db, self.chat) self.db.commit() # Delete the database connection here and on_finish just to be sure. if hasattr(self, "_db"): self._db.close() del self._db if DEBUG: print("socket closed: %s" % self.id) try: sockets.remove(self) except KeyError: pass def on_finish(self): if hasattr(self, "_db"): self._db.close() del self._db async def redis_listen(self): self.redis_client = await asyncio_redis.Connection.create( host=os.environ["REDIS_HOST"], port=int(os.environ["REDIS_PORT"]), db=int(os.environ["REDIS_DB"]), ) # Set the connection name, subscribe, and listen. await self.redis_client.client_setname("live:%s:%s" % (self.chat_id, self.user_id)) try: subscriber = await self.redis_client.start_subscribe() await subscriber.subscribe(list(self.channels.values())) while self.ws_connection: message = await subscriber.next_published() asyncio.ensure_future(self.on_redis_message(message)) finally: self.redis_client.close() async def on_redis_message(self, message): if DEBUG: print("redis message: %s" % str(message)) self.write_message(message.value) if message.channel == self.channels["user"]: data = json.loads(message.value) if "exit" in data: self.joined = False self.close()
class ChatHandler(WebSocketHandler): @property def db(self): if hasattr(self, "_db") and self._db is not None: return self._db else: self._db = sm() return self._db @property def loop(self): return asyncio.get_event_loop() def get_chat_user(self): return self.db.query( ChatUser, User, AnyChat, ).join( User, ChatUser.user_id == User.id, ).join( AnyChat, ChatUser.chat_id == AnyChat.id, ).filter(and_( ChatUser.user_id == self.user_id, ChatUser.chat_id == self.chat_id, )).one() def set_typing(self, is_typing): if not hasattr(self, "channels") or not hasattr(self, "user_number"): return func = self.user_list.user_start_typing if is_typing else self.user_list.user_stop_typing if func(self.user_number): redis.publish(self.channels["typing"], json.dumps({ "typing": self.user_list.user_numbers_typing(), })) def write_message(self, *args, **kwargs): try: super().write_message(*args, **kwargs) except WebSocketClosedError: return def check_origin(self, origin): if "localhost" in os.environ["BASE_DOMAIN"].lower(): return True return origin_regex.match(origin) is not None @coroutine def prepare(self): self.id = str(uuid4()) self.joined = False try: self.session_id = self.cookies["newparp"].value self.chat_id = int(self.path_args[0]) self.user_id = int(redis.get("session:%s" % self.session_id)) except (KeyError, TypeError, ValueError): self.send_error(400) return try: self.chat_user, self.user, self.chat = yield thread_pool.submit(self.get_chat_user) except NoResultFound: self.send_error(404) return # Remember the user number so typing notifications can refer to it # without reopening the database session. self.user_number = self.chat_user.number queue_user_meta(self, redis, self.request.headers.get("X-Forwarded-For", self.request.remote_ip)) self.user_list = UserListStore(redis_chat, self.chat_id) try: if self.user.group != "active": raise BannedException yield thread_pool.submit(authorize_joining, self.db, self) except (UnauthorizedException, BannedException, BadAgeException, TooManyPeopleException): self.send_error(403) return @coroutine def open(self, chat_id): sockets.add(self) if DEBUG: print("socket opened: %s %s %s" % (self.id, self.chat.url, self.user.username)) try: yield thread_pool.submit(kick_check, redis, self) except KickedException: self.write_message(json.dumps({"exit": "kick"})) self.close() return # Subscribe self.channels = { "chat": "channel:%s" % self.chat_id, "user": "******" % (self.chat_id, self.user_id), "typing": "channel:%s:typing" % self.chat_id, } if self.chat.type == "pm": self.channels["pm"] = "channel:pm:%s" % self.user_id self.redis_task = asyncio.ensure_future(self.redis_listen()) # Send backlog. try: after = int(self.request.query_arguments["after"][0]) except (KeyError, IndexError, ValueError): after = 0 messages = redis_chat.zrangebyscore("chat:%s" % self.chat_id, "(%s" % after, "+inf") self.write_message(json.dumps({ "chat": self.chat.to_dict(), "messages": [json.loads(_) for _ in messages], })) online_state_changed = self.user_list.socket_join(self.id, self.session_id, self.user_id) self.joined = True # Send a join message to everyone if we just joined, otherwise send the # user list to the client. if online_state_changed: yield thread_pool.submit(send_join_message, self.user_list, self.db, redis, self) else: userlist = yield thread_pool.submit(get_userlist, self.user_list, self.db) self.write_message(json.dumps({"users": userlist})) self.db.commit() self.db.close() def on_message(self, message): if DEBUG: print("message: %s" % message) if message == "ping": try: self.user_list.socket_ping(self.id) except PingTimeoutException: # We've been reaped, so disconnect. self.close() return elif message in ("typing", "stopped_typing"): self.set_typing(message == "typing") def on_close(self): # Unsubscribe here and let the exit callback handle disconnecting. if hasattr(self, "redis_task"): self.redis_task.cancel() if hasattr(self, "redis_client"): self.redis_client.close() if hasattr(self, "close_code") and self.close_code in (1000, 1001): message_type = "disconnect" else: message_type = "timeout" if self.joined and self.user_list.socket_disconnect(self.id, self.user_number): try: send_quit_message(self.user_list, self.db, *self.get_chat_user(), type=message_type) except NoResultFound: send_userlist(self.user_list, self.db, self.chat) self.db.commit() # Delete the database connection here and on_finish just to be sure. if hasattr(self, "_db"): self._db.close() del self._db if DEBUG: print("socket closed: %s" % self.id) try: sockets.remove(self) except KeyError: pass def on_finish(self): if hasattr(self, "_db"): self._db.close() del self._db async def redis_listen(self): self.redis_client = await asyncio_redis.Connection.create( host=os.environ["REDIS_HOST"], port=int(os.environ["REDIS_PORT"]), db=int(os.environ["REDIS_DB"]), ) # Set the connection name, subscribe, and listen. await self.redis_client.client_setname("live:%s:%s" % (self.chat_id, self.user_id)) try: subscriber = await self.redis_client.start_subscribe() await subscriber.subscribe(list(self.channels.values())) while self.ws_connection: message = await subscriber.next_published() asyncio.ensure_future(self.on_redis_message(message)) finally: self.redis_client.close() async def on_redis_message(self, message): if DEBUG: print("redis message: %s" % str(message)) self.write_message(message.value) if message.channel == self.channels["user"]: data = json.loads(message.value) if "exit" in data: self.joined = False self.close()
def join(client, chat: Chat): client.get("/" + chat.url) user_list = UserListStore(NewparpRedis(connection_pool=redis_chat_pool), chat.id) user_list.socket_join(str(uuid.uuid4()), g.session_id, g.user_id)