class MessageHandler(WebSocketHandler): def __init__(self, *args, **kwargs): super(MessageHandler, self).__init__(*args, **kwargs) self.redis = Client() self.redis.connect() def get_current_user(self): user = self.get_secure_cookie("user") if user is None: return '' else: return user.strip('" ') def on_message(self, msg): msginfo = loads(msg) #listens for handshake from page if "user:" in msginfo['msg']: self.channel = msginfo['msg'].split(':')[1] #need to split the rest off to new func so it can be asynchronous self.listen() # Decorator turns the function into an asynchronous generator object @tornado.gen.engine def listen(self): #runs task given, with the yield required to get returned value #equivalent of callback/wait pairing from tornado.gen yield tornado.gen.Task(self.redis.subscribe, self.channel) if not self.redis.subscribed: self.write_message('ERROR IN SUBSCRIPTION') #listen from tornadoredis makes the listen object asynchronous #if using standard redis lib, it blocks while listening self.redis.listen(self.callback) # Try and fight race condition by loading from redis after listen # started need to use std redis lib because tornadoredis is in # subscribed state oldmessages = r_server.lrange(self.channel + ':messages', 0, -1) if oldmessages is not None: for message in oldmessages: self.write_message(message) def callback(self, msg): if msg.kind == 'message': self.write_message(str(msg.body)) @tornado.gen.engine def on_close(self): yield tornado.gen.Task(self.redis.unsubscribe, self.channel) self.redis.disconnect()
class MessageHandler(WebSocketHandler): def __init__(self, *args, **kwargs): super(MessageHandler, self).__init__(*args, **kwargs) self.redis = Client() self.redis.connect() def get_current_user(self): user = self.get_secure_cookie("user") if user == None: return '' else: return user.strip('" ') def on_message(self, msg): msginfo = loads(msg) #listens for handshake from page if "user:" in msginfo['msg']: self.channel = msginfo['msg'].split(':')[1] #need to split the rest off to new func so it can be asynchronous self.listen() #decorator turns the function into an asynchronous generator object @tornado.gen.engine def listen(self): #runs task given, with the yield required to get returned value #equivalent of callback/wait pairing from tornado.gen yield tornado.gen.Task(self.redis.subscribe, self.channel) if not self.redis.subscribed: self.write_message('ERROR IN SUBSCRIPTION') #listen from tornadoredis makes the listen object asynchronous #if using standard redis lib, it blocks while listening self.redis.listen(self.callback) #try and fight race condition by loading from redis after listen started #need to use std redis lib because tornadoredis is in subscribed state oldmessages = r_server.lrange(self.channel + ':messages', 0, -1) if oldmessages != None: for message in oldmessages: self.write_message(message) def callback(self, msg): if msg.kind == 'message': self.write_message(str(msg.body)) @tornado.gen.engine def on_close(self): yield tornado.gen.Task(self.redis.unsubscribe, self.channel) self.redis.disconnect()
class ChatHandler(WebSocketHandler): user = None chat = None chat_user = None socket_id = None redis_channels = () ignore_next_message = False redis_client = None def check_origin(self, origin): return origin == config.get("app:main", "cherubplay.socket_origin") def open(self, chat_url): sockets.add(self) with db_session() as db: self.user = get_user(db, self.cookies) if self.user is None: self.close() return self.chat = get_chat(db, chat_url) if self.chat is None: self.close() return self.chat_user = get_chat_user(db, self.chat.id, self.user.id) if self.chat_user is None: self.close() return self.socket_id = str(uuid4()) # Fire online message, but only if this is the only tab we have open. online_handles = online_user_store.online_handles(self.chat) if self.chat_user.handle not in online_handles: publish_client.publish(pubsub_channel(self.chat), json.dumps({ "action": "online", "handle": self.chat_user.handle, })) # See if anyone else is online. online_handles_except_self = online_handles - {self.chat_user.handle} if len(online_handles_except_self) == 1: self.write_message({"action": "online", "handle": next(iter(online_handles_except_self))}) elif len(online_handles_except_self) >= 1: self.write_message({ "action": "online_list", "handles": sorted(list(online_handles)), }) online_user_store.connect(self.chat_user, self.socket_id) self.redis_channels = ( pubsub_channel(self.chat), pubsub_channel(self.user), pubsub_channel(self.chat_user), ) self.redis_listen() # Send the backlog if necessary. if "after" in self.request.query_arguments: try: after = int(self.request.query_arguments["after"][0]) except ValueError: return for message in ( db.query(Message) .options(joinedload(Message.chat_user)) .filter(and_(Message.chat_id == self.chat.id, Message.id > after)) ): self.write_message({ "action": "message", "message": { "id": message.id, "type": message.type.value, "colour": message.colour, "symbol": message.symbol_character, "name": message.chat_user.name if message.chat_user else None, "text": message.text.as_plain_text(), "html": message.text.as_html(), } }) def on_message(self, message_string): message = json.loads(message_string) if message["action"] in ("typing", "stopped_typing"): publish_client.publish(pubsub_channel(self.chat), json.dumps({ "action": message["action"], "handle": self.chat_user.handle, })) # Ignore our own typing messages. self.ignore_next_message = True def on_close(self): # Unsubscribe here and let the exit callback handle disconnecting. if self.redis_client: self.redis_client.unsubscribe(self.redis_channels) online_user_store.disconnect(self.chat, self.socket_id) # Fire offline message, but only if we don't have any other tabs open. if self.chat_user.handle not in online_user_store.online_handles(self.chat): publish_client.publish(pubsub_channel(self.chat), json.dumps({ "action": "offline", "handle": self.chat_user.handle, })) sockets.remove(self) @engine def redis_listen(self): self.redis_client = Client(unix_socket_path=config.get("app:main", "cherubplay.socket_pubsub")) yield Task(self.redis_client.subscribe, self.redis_channels) self.redis_client.listen(self.on_redis_message, self.on_redis_unsubscribe) def on_redis_message(self, message): if message.kind == "message": if message.body == "kicked": self.write_message("{\"action\":\"kicked\"}") self.close() elif not self.ignore_next_message: self.write_message(message.body) else: self.ignore_next_message = False def on_redis_unsubscribe(self, callback): self.redis_client.disconnect()
class ChatHandler(WebSocketHandler): 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): command = redis.sadd if is_typing else redis.srem typing_key = "chat:%s:typing" % self.chat_id if command(typing_key, self.user_number): redis.publish(self.channels["typing"], json.dumps({ "typing": list(int(_) for _ in redis.smembers(typing_key)), })) def check_origin(self, origin): return origin_regex.match(origin) is not None def prepare(self): self.id = str(uuid4()) self.joined = False try: self.session_id = self.cookies["session"].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 self.db = sm() try: self.chat_user, self.user, self.chat = 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 self.user.last_online = datetime.now() self.user.last_ip = self.request.headers["X-Forwarded-For"] if self.user.group == "banned": self.send_error(403) return try: authorize_joining(redis, self.db, self) except (UnauthorizedException, BannedException, TooManyPeopleException): self.send_error(403) return def open(self, chat_id): sockets.add(self) redis.sadd("chat:%s:sockets:%s" % (self.chat_id, self.session_id), self.id) print "socket opened:", self.id, self.chat.url, self.user.username try: 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_listen() # Send backlog. try: after = int(self.request.query_arguments["after"][0]) except (KeyError, IndexError, ValueError): after = 0 messages = redis.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], })) join_message_sent = join(redis, self.db, self) self.joined = True # Send userlist if nothing was sent by join(). if not join_message_sent: self.write_message(json.dumps({"users": get_userlist(self.db, redis, self.chat)})) self.db.commit() def on_message(self, message): print "message:", message if 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_client"): self.redis_client.unsubscribe(self.redis_client.subscribed) if self.joined and disconnect(redis, self.chat_id, self.id): try: send_quit_message(self.db, redis, *self.get_chat_user()) except NoResultFound: send_userlist(self.db, redis, self.chat) self.db.commit() self.set_typing(False) print "socket closed:", self.id redis.srem("chat:%s:sockets:%s" % (self.chat_id, self.session_id), self.id) sockets.remove(self) def finish(self, *args, **kwargs): if hasattr(self, "db"): self.db.close() del self.db super(ChatHandler, self).finish(*args, **kwargs) @engine def redis_listen(self): self.redis_client = Client( host=os.environ["REDIS_HOST"], port=int(os.environ["REDIS_PORT"]), selected_db=int(os.environ["REDIS_DB"]), ) yield Task(self.redis_client.subscribe, self.channels.values()) self.redis_client.listen(self.on_redis_message, self.on_redis_unsubscribe) def on_redis_message(self, message): print "redis message:", message if message.kind != "message": return self.write_message(message.body) if message.channel == self.channels["user"]: data = json.loads(message.body) if "exit" in data: self.joined = False self.close() def on_redis_unsubscribe(self, callback): self.redis_client.disconnect()
class ChatHandler(WebSocketHandler): def check_origin(self, origin): return True def open(self, chat_url): sockets.add(self) print chat_url self.user = get_user(self.cookies) if self.user is None: self.close() return self.chat = get_chat(chat_url) if self.chat is None: self.close() return self.chat_user = get_chat_user(self.chat.id, self.user.id) if self.chat_user is None: self.close() return self.socket_id = str(uuid4()) # Fire online message, but only if this is the only tab we have open. online_symbols = set(int(_) for _ in publish_client.hvals("online:"+str(self.chat.id))) if self.chat_user.symbol not in online_symbols: publish_client.publish("chat:"+str(self.chat.id), json.dumps({ "action": "online", "symbol": symbols[self.chat_user.symbol], })) # See if the other person is online. for symbol in online_symbols: if symbol == self.chat_user.symbol: continue self.write_message({ "action": "online", "symbol": symbols[symbol], }) publish_client.hset("online:"+str(self.chat.id), self.socket_id, self.chat_user.symbol) self.redis_listen() self.ignore_next_message = False # Send the backlog if necessary. if "after" in self.request.query_arguments: print "after" try: after = int(self.request.query_arguments["after"][0]) except ValueError: return Session = sm() for message in Session.query(Message).filter(and_( Message.chat_id == self.chat.id, Message.id > after, )): print message self.write_message({ "action": "message", "message": { "id": message.id, "type": message.type, "colour": message.colour, "symbol": symbols[message.symbol], "text": message.text, } }) Session.commit() def on_message(self, message_string): message = json.loads(message_string) if message["action"] in ("typing", "stopped_typing"): publish_client.publish("chat:"+str(self.chat.id), json.dumps({ "action": message["action"], "symbol": symbols[self.chat_user.symbol], })) # Ignore our own typing messages. self.ignore_next_message = True print message def on_close(self): # Unsubscribe here and let the exit callback handle disconnecting. self.redis_client.unsubscribe("chat:"+str(self.chat.id)) publish_client.hdel("online:"+str(self.chat.id), self.socket_id) # Fire offline message, but only if we don't have any other tabs open. if str(self.chat_user.symbol) not in publish_client.hvals("online:"+str(self.chat.id)): publish_client.publish("chat:"+str(self.chat.id), json.dumps({ "action": "offline", "symbol": symbols[self.chat_user.symbol], })) sockets.remove(self) @engine def redis_listen(self): self.redis_client = Client(unix_socket_path=config.get("app:main", "cherubplay.socket_pubsub")) yield Task(self.redis_client.subscribe, "chat:"+str(self.chat.id)) self.redis_client.listen(self.on_redis_message, self.on_redis_unsubscribe) def on_redis_message(self, message): if message.kind=="message": if not self.ignore_next_message: self.write_message(message.body) print "redis message:", message.body else: self.ignore_next_message = False def on_redis_unsubscribe(self, callback): self.redis_client.disconnect()