Example #1
0
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()
Example #2
0
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()
Example #3
0
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)