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
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)
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, )
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, )
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")
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
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)
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)
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"))
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"))
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)
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, )
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)
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)
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()
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, )
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)
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, )
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, )
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)