Пример #1
0
    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
Пример #2
0
def reap_chat(chat_id):
    user_list = UserListStore(NewparpRedis(connection_pool=redis_chat_pool), chat_id)
    old_user_ids = user_list.user_ids_online()
    if not old_user_ids:
        return

    with session_scope() as db:
        chat = db.query(Chat).filter(Chat.id == chat_id).one()
        chat_users = {
            _.user_id: _
            for _ in db.query(ChatUser).filter(and_(
                ChatUser.chat_id == chat_id,
                ChatUser.user_id.in_(old_user_ids),
            ))
        }

        for socket_id, user_id in user_list.inconsistent_entries():
            chat_user = chat_users[user_id]
            dead = user_list.socket_disconnect(socket_id, chat_user.number)
            if dead:
                logger.debug("dead: %s" % chat_user)
                # TODO optimise this when reaping several people at once?
                if chat_user.computed_group == "silent" or chat.type in ("pm", "roulette"):
                    send_userlist(user_list, db, chat)
                else:
                    send_message(db, reap_chat.redis, Message(
                        chat=chat,
                        user_id=chat_user.user_id,
                        type="timeout",
                        name=chat_user.name,
                        text="%s's connection timed out." % chat_user.name,
                    ), user_list)
Пример #3
0
def generate_counters():
    logger.info("Generating user counters.")
    redis_chat = NewparpRedis(connection_pool=redis_chat_pool)

    user_ids_online = list(UserListStore.multi_user_ids_online(
        redis_chat,
        UserListStore.scan_active_chats(redis_chat),
    ))

    generate_counters.redis.set(
        "connected_users",
        len(set.union(*user_ids_online)) if user_ids_online else 0,
    )
Пример #4
0
def generate_counters():
    logger.info("Generating user counters.")
    redis_chat = NewparpRedis(connection_pool=redis_chat_pool)

    user_ids_online = list(
        UserListStore.multi_user_ids_online(
            redis_chat,
            UserListStore.scan_active_chats(redis_chat),
        ))

    generate_counters.redis.set(
        "connected_users",
        len(set.union(*user_ids_online)) if user_ids_online else 0,
    )
Пример #5
0
def update_lastonline():
    redis = update_lastonline.redis
    redis_chat = NewparpRedis(connection_pool=redis_chat_pool)

    if redis.exists("lock:lastonline"):
        return
    redis.setex("lock:lastonline", 60, 1)

    chat_ids = redis.hgetall("queue:lastonline")

    # Reset the list for the next iteration.
    redis.delete("queue:lastonline")

    for chat_id, posted in chat_ids.items():
        online_user_ids = UserListStore(redis_chat, chat_id).user_ids_online()

        try:
            posted = datetime.datetime.utcfromtimestamp(float(posted))
        except ValueError:
            continue

        with session_scope() as db:
            db.query(Chat).filter(Chat.id == chat_id).update(
                {"last_message": posted}, synchronize_session=False)
            if len(online_user_ids) != 0:
                db.query(ChatUser).filter(
                    and_(
                        ChatUser.user_id.in_(online_user_ids),
                        ChatUser.chat_id == chat_id,
                    )).update({"last_online": posted},
                              synchronize_session=False)

    redis.delete("lock:lastonline")
Пример #6
0
    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
Пример #7
0
def get_chat_user():
    try:
        g.chat_user, g.user, g.chat = g.db.query(
            ChatUser,
            User,
            AnyChat,
        ).join(
            User,
            ChatUser.user_id == User.id,
        ).join(
            AnyChat,
            ChatUser.chat_id == AnyChat.id,
        ).filter(
            and_(
                ChatUser.user_id == g.user_id,
                ChatUser.chat_id == int(request.form["chat_id"]),
            )).one()
    except NoResultFound:
        abort(400)

    queue_user_meta(
        g, g.redis, request.headers.get("X-Forwarded-For",
                                        request.remote_addr))

    if g.user.group != "active":
        abort(403)

    g.ip_banned = get_ip_banned(
        request.headers.get("X-Forwarded-For", request.remote_addr), g.db,
        g.redis)
    if g.ip_banned and not g.user.is_admin:
        abort(403)

    g.user_list = UserListStore(NewparpRedis(connection_pool=redis_chat_pool),
                                g.chat.id)
Пример #8
0
def reap_chat(chat_id):
    user_list = UserListStore(NewparpRedis(connection_pool=redis_chat_pool),
                              chat_id)
    old_user_ids = user_list.user_ids_online()
    if not old_user_ids:
        return

    with session_scope() as db:
        chat = db.query(Chat).filter(Chat.id == chat_id).one()
        chat_users = {
            _.user_id: _
            for _ in db.query(ChatUser).filter(
                and_(
                    ChatUser.chat_id == chat_id,
                    ChatUser.user_id.in_(old_user_ids),
                ))
        }

        for socket_id, user_id in user_list.inconsistent_entries():
            chat_user = chat_users[user_id]
            dead = user_list.socket_disconnect(socket_id, chat_user.number)
            if dead:
                logger.debug("dead: %s" % chat_user)
                # TODO optimise this when reaping several people at once?
                if chat_user.computed_group == "silent" or chat.type in (
                        "pm", "roulette"):
                    send_userlist(user_list, db, chat)
                else:
                    send_message(
                        db, reap_chat.redis,
                        Message(
                            chat=chat,
                            user_id=chat_user.user_id,
                            type="timeout",
                            name=chat_user.name,
                            text="%s's connection timed out." % chat_user.name,
                        ), user_list)
Пример #9
0
def broadcast_post():

    title = request.form.get("title", "Global Announcement").strip()
    text = request.form["text"].strip()
    if not text:
        abort(400)

    if request.form["color"][0] == "#":
        color = request.form["color"][1:]
    else:
        color = request.form["color"]
    if not color_validator.match(color):
        abort(400)

    if request.form["headercolor"][0] == "#":
        headercolor = request.form["headercolor"][1:]
    else:
        headercolor = request.form["headercolor"]
    if not color_validator.match(headercolor):
        abort(400)

    g.db.add(
        AdminLogEntry(
            action_user=g.user,
            type="broadcast",
            description=text,
        ))

    message_json = json.dumps({
        "messages": [{
            "id": None,
            "user_number": None,
            "posted": time.time(),
            "type": "global",
            "color": color,
            "headercolor": headercolor,
            "acronym": "",
            "name": "",
            "text": text,
            "title": title,
            "important": "important" in request.form
        }]
    })

    for chat_id in UserListStore.scan_active_chats(
            NewparpRedis(connection_pool=redis_chat_pool)):
        g.redis.publish("channel:%s" % chat_id, message_json)

    return redirect(url_for("admin_broadcast"))
Пример #10
0
def broadcast_post():

    title = request.form.get("title", "Global Announcement").strip()
    text = request.form["text"].strip()
    if not text:
        abort(400)

    if request.form["color"][0] == "#":
        color = request.form["color"][1:]
    else:
        color = request.form["color"]
    if not color_validator.match(color):
        abort(400)
        
    if request.form["headercolor"][0] == "#":
        headercolor = request.form["headercolor"][1:]
    else:
        headercolor = request.form["headercolor"]
    if not color_validator.match(headercolor):
        abort(400)

    g.db.add(AdminLogEntry(
        action_user=g.user,
        type="broadcast",
        description=text,
    ))

    message_json = json.dumps({
        "messages": [{
            "id": None,
            "user_number": None,
            "posted": time.time(),
            "type": "global",
            "color": color,
            "headercolor": headercolor,
            "acronym": "",
            "name": "",
            "text": text,
            "title": title,
            "important": "important" in request.form
        }]
    })

    for chat_id in UserListStore.scan_active_chats(NewparpRedis(connection_pool=redis_chat_pool)):
        g.redis.publish("channel:%s" % chat_id, message_json)

    return redirect(url_for("admin_broadcast"))
Пример #11
0
def reap():
    redis_chat = NewparpRedis(connection_pool=redis_chat_pool)
    for chat_id in UserListStore.scan_active_chats(redis_chat):
        reap_chat.delay(chat_id)
Пример #12
0
def chat_list(fmt=None, type=None, page=1):
    if type is None:
        type = "all"

    try:
        ChatClass = chat_classes[type]
    except KeyError:
        abort(404)

    chats = g.db.query(ChatUser, ChatClass).join(ChatClass).filter(and_(
        ChatUser.user_id == g.user.id,
        ChatUser.subscribed == True,
    ))
    if type == "unread":
        chats = chats.filter(ChatClass.last_message > ChatUser.last_online)

    chats = chats.order_by(
        ChatClass.last_message.desc(),
    ).offset((page - 1) * 50).limit(50).all()

    if len(chats) == 0 and page != 1:
        abort(404)

    chat_count = g.db.query(func.count('*')).select_from(ChatUser).filter(and_(
        ChatUser.user_id == g.user.id,
        ChatUser.subscribed == True,
    ))
    if type == "unread":
        chat_count = chat_count.join(ChatClass).filter(
            ChatClass.last_message > ChatUser.last_online,
        )
    elif type is not None:
        chat_count = chat_count.join(ChatClass)
    chat_count = chat_count.scalar()

    online_userlists = UserListStore.multi_user_ids_online(
        NewparpRedis(connection_pool=redis_chat_pool),
        (c[1].id for c in chats),
    )

    chat_dicts = []
    for (chat_user, chat), online_user_ids in zip(chats, online_userlists):

        if chat.type == "pm":
            pm_chat_user = g.db.query(ChatUser).filter(and_(
                ChatUser.chat_id == chat.id,
                ChatUser.user_id != g.user.id,
            )).options(joinedload(ChatUser.user)).first()
        else:
            pm_chat_user = None

        cd = chat.to_dict(pm_user=pm_chat_user.user if pm_chat_user is not None else None)

        cd["online"] = len(set(online_user_ids))
        if chat.type == "pm":
            cd["partner_online"] = pm_chat_user.user.id in (int(_) for _ in online_user_ids)

        cd["unread"] = chat.last_message > chat_user.last_online

        chat_dicts.append({
            "chat_user": chat_user.to_dict(include_title_and_notes=True),
            "chat": cd,
        })

    if fmt == "json":

        return jsonify({
            "total": chat_count,
            "chats": chat_dicts,
        })

    paginator = paginate.Page(
        [],
        page=page,
        items_per_page=50,
        item_count=chat_count,
        url_maker=lambda page: url_for("rp_chat_list", page=page, type=type),
    )

    return render_template(
        "chat_list.html",
        level_options=level_options,
        type=type,
        chats=chat_dicts,
        paginator=paginator,
        chat_classes=chat_classes,
    )
Пример #13
0
    def decorated_function(url, fmt=None, *args, **kwargs):

        # Helper for doing some special URL stuff with PM chats.
        # Normally we just query for a Chat object with the url. However, PM chat
        # URLs take the form "pm/<username>", so we have to look up the username,
        # find the User it belongs to, and use our URL and theirs to create a
        # special URL.

        if url == "pm":
            abort(404)

        elif url.startswith("pm/"):

            username = url[3:]
            if username == "":
                abort(404)

            # You can't PM yourself.
            if g.user is None or username.lower() == g.user.username.lower():
                abort(404)

            try:
                pm_user = g.db.query(User).filter(
                    func.lower(User.username) == username.lower()).one()
            except NoResultFound:
                abort(404)

            # Fix case if necessary.
            if pm_user.username != username:
                if request.method != "GET":
                    abort(404)
                return redirect(
                    url_for(request.endpoint,
                            url="pm/" + pm_user.username,
                            fmt=fmt))

            # Generate URL from our user ID and their user ID.
            # Sort so they're always in the same order.
            pm_url = "pm/" + ("/".join(
                sorted([str(g.user.id), str(pm_user.id)])))
            try:
                chat = g.db.query(PMChat).filter(PMChat.url == pm_url, ).one()
            except NoResultFound:
                # Only create a new PMChat on the main chat page.
                if request.endpoint != "rp_chat":
                    abort(404)
                chat = PMChat(url=pm_url)
                g.db.add(chat)
                g.db.flush()
                # Create ChatUser for the other user.
                pm_chat_user = ChatUser.from_user(pm_user,
                                                  chat_id=chat.id,
                                                  number=1,
                                                  subscribed=True)
                g.db.add(pm_chat_user)
                g.db.flush()

            return f(chat, pm_user, url, fmt, *args, **kwargs)

        # Force lower case.
        if url != url.lower():
            if request.method != "GET":
                abort(404)
            return redirect(url_for(request.endpoint, url=url.lower(),
                                    fmt=fmt))

        try:
            chat = g.db.query(AnyChat).filter(AnyChat.url == url).one()
        except NoResultFound:
            abort(404)

        g.chat = chat
        g.chat_id = chat.id
        g.user_list = UserListStore(
            NewparpRedis(connection_pool=redis_chat_pool), chat.id)
        try:
            authorize_joining(g.db, g)
        except BannedException:
            if request.endpoint != "rp_chat" or chat.url == "theoubliette":
                abort(403)
            if request.method != "GET":
                abort(404)
            return redirect(
                url_for(request.endpoint, url="theoubliette", fmt=fmt))
        except UnauthorizedException:
            if request.endpoint != "rp_chat_unsubscribe":
                return render_template("chat/private.html", chat=chat), 403
        except TooManyPeopleException:
            if request.endpoint == "rp_chat":
                return render_template("chat/too_many_people.html",
                                       chat=chat), 403

        return f(chat, None, url, fmt, *args, **kwargs)
Пример #14
0
def send_message(db, redis, message, user_list=None, force_userlist=False):

    db.add(message)
    db.flush()

    message_dict = message.to_dict()

    # Cache before sending.
    cache_key = "chat:%s" % message.chat_id
    redis.zadd(cache_key, message.id, json.dumps(message_dict))
    redis.zremrangebyrank(cache_key, 0, -51)

    # Prepare pubsub message
    redis_message = {
        "messages": [message_dict],
    }

    # Reload userlist if necessary.
    if message.type in (
            "join",
            "disconnect",
            "timeout",
            "user_info",
            "user_group",
            "user_action",
    ) or force_userlist:
        if user_list is None:
            user_list = UserListStore(
                NewparpRedis(connection_pool=redis_chat_pool), message.chat_id)
        redis_message["users"] = get_userlist(user_list, db)

    # Reload chat metadata if necessary.
    if message.type == "chat_meta":
        redis_message["chat"] = message.chat.to_dict()

    redis.publish("channel:%s" % message.chat_id, json.dumps(redis_message))

    # Send notifications.
    if message.type in ("ic", "ooc", "me", "spamless"):

        # Queue an update for the last_online field.
        # TODO move the PM stuff here too
        redis.hset(
            "queue:lastonline", message.chat.id,
            time.mktime(message.posted.timetuple()) +
            float(message.posted.microsecond) / 1000000)

        if user_list is None:
            user_list = UserListStore(
                NewparpRedis(connection_pool=redis_chat_pool), message.chat_id)
        online_user_ids = user_list.user_ids_online()
        if message.chat.type == "pm":
            offline_chat_users = db.query(ChatUser).filter(
                and_(
                    ~ChatUser.user_id.in_(online_user_ids),
                    ChatUser.chat_id == message.chat.id,
                ))
            for chat_user in offline_chat_users:
                # Only send a notification if it's not already unread.
                if message.chat.last_message <= chat_user.last_online:
                    redis.publish("channel:pm:%s" % chat_user.user_id,
                                  "{\"pm\":\"1\"}")

    # And send the message to spamless last.
    # 1 second delay to prevent the task from executing before we commit the message.
    celery.send_task("newparp.tasks.spamless.CheckSpamTask",
                     args=(message.chat_id, redis_message),
                     countdown=1)
Пример #15
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()
Пример #16
0
def groups(fmt=None):

    style_filter = set()
    for style in GroupChat.style.type.enums:
        if style in request.args:
            style_filter.add(style)
    if not style_filter:
        if g.user is not None:
            style_filter = g.user.group_chat_styles
        else:
            style_filter.add("script")

    level_filter = set()
    for level in GroupChat.level.type.enums:
        if level in request.args:
            level_filter.add(level)
    if not level_filter:
        if g.user is not None:
            level_filter = g.user.group_chat_levels
        else:
            level_filter.add("sfw")

    if g.user is not None:
        g.user.group_chat_styles = style_filter
        g.user.group_chat_levels = level_filter

    groups_query = g.db.query(GroupChat).filter(
        and_(
            GroupChat.publicity.in_(("listed", "pinned")),
            GroupChat.style.in_(style_filter),
            GroupChat.level.in_(level_filter),
        )).all()

    online_userlists = UserListStore.multi_user_ids_online(
        NewparpRedis(connection_pool=redis_chat_pool),
        (group.id for group in groups_query),
    )

    groups = []
    for group, online_users in zip(groups_query, online_userlists):
        online_user_count = len(set(online_users))
        if online_user_count > 0:
            groups.append((group, online_user_count))
    groups.sort(key=lambda _: (_[0].publicity, _[1]), reverse=True)

    chat_dicts = []
    for chat, online in groups:
        cd = chat.to_dict()
        cd["online"] = online
        chat_dicts.append(cd)

    if fmt == "json":
        return jsonify({
            "chats": chat_dicts,
        })

    return render_template(
        "groups.html",
        level_options=level_options,
        groups=chat_dicts,
        style_filter=style_filter,
        level_filter=level_filter,
    )
Пример #17
0
def send_message(db, redis, message, user_list=None, force_userlist=False):

    db.add(message)
    db.flush()

    message_dict = message.to_dict()

    if user_list is None:
        redis_chat = NewparpRedis(connection_pool=redis_chat_pool)
        user_list  = UserListStore(redis_chat, message.chat_id)
    else:
        redis_chat = user_list.redis

    # Cache before sending.
    cache_key = "chat:%s" % message.chat_id
    redis_chat.zadd(cache_key, message.id, json.dumps(message_dict))
    redis_chat.zremrangebyrank(cache_key, 0, -51)
    redis_chat.expire(cache_key, 604800)

    # Prepare pubsub message
    redis_message = {
        "messages": [message_dict],
    }

    # Reload userlist if necessary.
    if message.type in (
        "join",
        "disconnect",
        "timeout",
        "user_info",
        "user_group",
        "user_action",
    ) or force_userlist:
        redis_message["users"] = get_userlist(user_list, db)

    # Reload chat metadata if necessary.
    if message.type == "chat_meta":
        redis_message["chat"] = message.chat.to_dict()

    redis.publish("channel:%s" % message.chat_id, json.dumps(redis_message))

    # Send notifications.
    if message.type in ("ic", "ooc", "me", "spamless"):

        # Queue an update for the last_online field.
        # TODO move the PM stuff here too
        redis.hset("queue:lastonline", message.chat.id, time.mktime(message.posted.timetuple()) + float(message.posted.microsecond) / 1000000)

        online_user_ids = user_list.user_ids_online()
        if message.chat.type == "pm":
            offline_chat_users = db.query(ChatUser).filter(and_(
                ~ChatUser.user_id.in_(online_user_ids),
                ChatUser.chat_id == message.chat.id,
            ))
            for chat_user in offline_chat_users:
                # Only send a notification if it's not already unread.
                if message.chat.last_message <= chat_user.last_online:
                    redis.publish("channel:pm:%s" % chat_user.user_id, "{\"pm\":\"1\"}")

    # And send the message to spamless last.
    # 1 second delay to prevent the task from executing before we commit the message.
    celery.send_task("newparp.tasks.spamless.CheckSpamTask", args=(message.chat_id, redis_message), countdown=1)
Пример #18
0
def chat_list(fmt=None, type=None, page=1):
    if type is None:
        type = "all"

    try:
        ChatClass = chat_classes[type]
    except KeyError:
        abort(404)

    chats = g.db.query(ChatUser, ChatClass).join(ChatClass).filter(
        and_(
            ChatUser.user_id == g.user.id,
            ChatUser.subscribed == True,
        ))
    if type == "unread":
        chats = chats.filter(ChatClass.last_message > ChatUser.last_online)

    chats = chats.order_by(ChatClass.last_message.desc(), ).offset(
        (page - 1) * 50).limit(50).all()

    if len(chats) == 0 and page != 1:
        abort(404)

    chat_count = g.db.query(func.count('*')).select_from(ChatUser).filter(
        and_(
            ChatUser.user_id == g.user.id,
            ChatUser.subscribed == True,
        ))
    if type == "unread":
        chat_count = chat_count.join(ChatClass).filter(
            ChatClass.last_message > ChatUser.last_online, )
    elif type is not None:
        chat_count = chat_count.join(ChatClass)
    chat_count = chat_count.scalar()

    online_userlists = UserListStore.multi_user_ids_online(
        NewparpRedis(connection_pool=redis_chat_pool),
        (c[1].id for c in chats),
    )

    chat_dicts = []
    for (chat_user, chat), online_user_ids in zip(chats, online_userlists):

        if chat.type == "pm":
            pm_chat_user = g.db.query(ChatUser).filter(
                and_(
                    ChatUser.chat_id == chat.id,
                    ChatUser.user_id != g.user.id,
                )).options(joinedload(ChatUser.user)).first()
        else:
            pm_chat_user = None

        cd = chat.to_dict(
            pm_user=pm_chat_user.user if pm_chat_user is not None else None)

        cd["online"] = len(set(online_user_ids))
        if chat.type == "pm":
            cd["partner_online"] = pm_chat_user.user.id in (
                int(_) for _ in online_user_ids)

        cd["unread"] = chat.last_message > chat_user.last_online

        chat_dicts.append({
            "chat_user":
            chat_user.to_dict(include_title_and_notes=True),
            "chat":
            cd,
        })

    if fmt == "json":

        return jsonify({
            "total": chat_count,
            "chats": chat_dicts,
        })

    paginator = paginate.Page(
        [],
        page=page,
        items_per_page=50,
        item_count=chat_count,
        url_maker=lambda page: url_for("rp_chat_list", page=page, type=type),
    )

    return render_template(
        "chat_list.html",
        level_options=level_options,
        type=type,
        chats=chat_dicts,
        paginator=paginator,
        chat_classes=chat_classes,
    )
Пример #19
0
def groups(fmt=None):

    style_filter = set()
    for style in GroupChat.style.type.enums:
        if style in request.args:
            style_filter.add(style)
    if not style_filter:
        if g.user is not None:
            style_filter = g.user.group_chat_styles
        else:
            style_filter.add("script")

    allowed_levels = g.user.level_options if g.user else allowed_level_options[AgeGroup.unknown]

    level_filter = set()
    for level in allowed_levels:
        if level in request.args:
            level_filter.add(level)
    if not level_filter:
        if g.user is not None:
            level_filter = g.user.group_chat_levels
        else:
            level_filter.add("sfw")

    if g.user is not None:
        g.user.group_chat_styles = style_filter
        g.user.group_chat_levels = level_filter

    groups_query = g.db.query(GroupChat).filter(and_(
        GroupChat.publicity.in_(("listed", "pinned")),
        GroupChat.style.in_(style_filter),
        GroupChat.level.in_(level_filter),
    )).all()

    online_userlists = UserListStore.multi_user_ids_online(
        NewparpRedis(connection_pool=redis_chat_pool),
        (group.id for group in groups_query),
    )

    groups = []
    for group, online_users in zip(groups_query, online_userlists):
        online_user_count = len(set(online_users))
        if online_user_count > 0:
            groups.append((group, online_user_count))
    groups.sort(key=lambda _: (_[0].publicity, _[1]), reverse=True)

    chat_dicts = []
    for chat, online in groups:
        cd = chat.to_dict()
        cd["online"] = online
        chat_dicts.append(cd)

    if fmt == "json":
        return jsonify({
            "chats": chat_dicts,
        })

    return render_template(
        "groups.html",
        allowed_levels=allowed_levels,
        level_options=level_options,
        AgeGroup=AgeGroup,
        groups=chat_dicts,
        style_filter=style_filter,
        level_filter=level_filter,
    )
Пример #20
0
def reap():
    redis_chat = NewparpRedis(connection_pool=redis_chat_pool)
    for chat_id in UserListStore.scan_active_chats(redis_chat):
        reap_chat.delay(chat_id)
Пример #21
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()
Пример #22
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)