def __init__(self, *args, **kwargs): self.closed_channels = None self.message_creator = WebRtcMessageCreator(None, None) super(MessagesHandler, self).__init__() self.webrtc_ids = {} self.id = None # child init self.last_client_ping = 0 self.user_id = 0 # anonymous by default self.ip = None from chat import global_redis self.async_redis_publisher = global_redis.async_redis_publisher self.sync_redis = global_redis.sync_redis self.channels = [] self._logger = None self.async_redis = Client(host=REDIS_HOST, port=REDIS_PORT, selected_db=REDIS_DB) self.patch_tornadoredis() # input websocket messages handlers # The handler is determined by @VarNames.EVENT self.process_ws_message = { Actions.GET_MESSAGES: self.process_get_messages, Actions.GET_MESSAGES_BY_IDS: self.process_get_messages_by_ids, Actions.PRINT_MESSAGE: self.process_send_message, Actions.DELETE_ROOM: self.delete_room, Actions.EDIT_MESSAGE: self.edit_message, Actions.CREATE_ROOM: self.create_new_room, Actions.CREATE_CHANNEL: self.create_new_channel, Actions.SAVE_CHANNEL_SETTINGS: self.save_channels_settings, Actions.SAVE_ROOM_SETTINGS: self.save_room_settings, Actions.DELETE_CHANNEL: self.delete_channel, Actions.SET_USER_PROFILE: self.profile_save_user, Actions.SET_SETTINGS: self.profile_save_settings, Actions.INVITE_USER: self.invite_user, Actions.PING: self.respond_ping, Actions.PONG: self.process_pong_message, Actions.SEARCH_MESSAGES: self.search_messages, Actions.SYNC_HISTORY: self.sync_history, Actions.SHOW_I_TYPE: self.show_i_type, } # Handlers for redis messages, if handler returns true - message won't be sent to client # The handler is determined by @VarNames.EVENT self.process_pubsub_message = { Actions.CREATE_ROOM: self.send_client_new_channel, Actions.DELETE_ROOM: self.send_client_delete_channel, Actions.INVITE_USER: self.send_client_new_channel, Actions.ADD_INVITE: self.send_client_new_channel, Actions.PING: self.process_ping_message, }
class TornadoHandler(WebSocketHandler, WebRtcMessageHandler): def __init__(self, *args, **kwargs): super(TornadoHandler, self).__init__(*args, **kwargs) self.__connected__ = False self.restored_connection = False self.anti_spam = AntiSpam() @property def connected(self): return self.__connected__ @connected.setter def connected(self, value): self.__connected__ = value def data_received(self, chunk): pass def on_message(self, json_message): message = None try: if not self.connected: raise ValidationError( 'Skipping message %s, as websocket is not initialized yet' % json_message) if not json_message: raise Exception('Skipping null message') # self.anti_spam.check_spam(json_message) self.logger.debug('<< %.1000s', json_message) message = json.loads(json_message) if message[VarNames.EVENT] not in self.process_ws_message: raise Exception("event {} is unknown".format( message[VarNames.EVENT])) channel = message.get(VarNames.ROOM_ID) if channel and channel not in self.channels: raise ValidationError( 'Access denied for channel {}. Allowed channels: {}'. format(channel, self.channels)) self.process_ws_message[message[VarNames.EVENT]](message) except ValidationError as e: error_message = self.message_creator.default( str(e.message), Actions.GROWL_ERROR_MESSAGE, HandlerNames.WS) if message: error_message[VarNames.JS_MESSAGE_ID] = message.get( VarNames.JS_MESSAGE_ID, None) self.ws_write(error_message) def on_close(self): if self.async_redis.subscribed: self.logger.info("Close event, unsubscribing from %s", self.channels) self.async_redis.unsubscribe(self.channels) else: self.logger.info("Close event, not subscribed, channels: %s", self.channels) self.async_redis_publisher.srem(RedisPrefix.ONLINE_VAR, self.id) online = self.get_dict_users_from_redis() my_online = online.setdefault(self.user_id, []) if self.id in my_online: my_online.remove(self.id) if self.connected: message = self.message_creator.room_online_logout(online) self.publish(message, settings.ALL_ROOM_ID) res = execute_query(settings.UPDATE_LAST_READ_MESSAGE, [ self.user_id, ]) self.logger.info("Updated %s last read message", res) self.disconnect() def disconnect(self, tries=0): """ Closes a connection if it's not in proggress, otherwice timeouts closing https://github.com/evilkost/brukva/issues/25#issuecomment-9468227 """ self.connected = False self.closed_channels = self.channels self.channels = [] if self.async_redis.connection.in_progress and tries < 1000: # failsafe eternal loop self.logger.debug('Closing a connection timeouts') ioloop.IOLoop.instance().add_timeout(timedelta(0.00001), self.disconnect, tries + 1) else: self.logger.info("Close connection result: %s") self.async_redis.disconnect() def generate_self_id(self): """ When user opens new tab in browser wsHandler.wsConnectionId stores Id of current ws So if ws loses a connection it still can reconnect with same id, and TornadoHandler can restore webrtc_connections to previous state """ conn_arg = self.get_argument('id', None) if conn_arg: random = conn_arg.split(":")[1] else: random = None self.id, random = create_id(self.user_id, random) self.restored_connection = random == conn_arg self.restored_connection = False self.save_ip() def open(self): session_key = self.get_argument('sessionId', None) user_id = self.sync_redis.hget('sessions', session_key) if user_id is None: self.logger.warning('!! Session key %s has been rejected' % session_key) self.close(403, "Session key %s has been rejected" % session_key) return self.user_id = int(user_id) self.ip = self.get_client_ip() user_db = UserProfile.objects.get(id=self.user_id) self.generate_self_id() self.message_creator = WebRtcMessageCreator(self.user_id, self.id) self._logger = logging.LoggerAdapter(parent_logger, { 'id': self.id, 'ip': self.ip }) self.logger.debug("!! Incoming connection, session %s, thread hash %s", session_key, self.id) self.async_redis.connect() self.async_redis_publisher.sadd(RedisPrefix.ONLINE_VAR, self.id) # since we add user to online first, latest trigger will always show correct online online = self.get_dict_users_from_redis() # current user is already online my_online = online.setdefault(self.user_id, []) if self.id not in my_online: my_online.append(self.id) was_online = len(online.get( self.user_id)) > 1 # if other tabs are opened user_rooms_query = Room.objects.filter(users__id=self.user_id, disabled=False) \ .values('id', 'name', 'creator_id', 'channel_id', 'p2p', 'roomusers__notifications', 'roomusers__volume') room_users = [{ VarNames.ROOM_ID: room['id'], VarNames.ROOM_NAME: room['name'], VarNames.CHANNEL_ID: room['channel_id'], VarNames.ROOM_CREATOR_ID: room['creator_id'], VarNames.NOTIFICATIONS: room['roomusers__notifications'], VarNames.P2P: room['p2p'], VarNames.VOLUME: room['roomusers__volume'], VarNames.ROOM_USERS: [] } for room in user_rooms_query] user_rooms_dict = {room[VarNames.ROOM_ID]: room for room in room_users} channels_ids = [ channel[VarNames.CHANNEL_ID] for channel in room_users if channel[VarNames.CHANNEL_ID] ] channels_db = Channel.objects.filter(Q(id__in=channels_ids) | Q(creator=self.user_id), disabled=False) channels = [{ VarNames.CHANNEL_ID: channel.id, VarNames.CHANNEL_NAME: channel.name, VarNames.CHANNEL_CREATOR_ID: channel.creator_id } for channel in channels_db] room_ids = [room_id[VarNames.ROOM_ID] for room_id in room_users] rooms_users = RoomUsers.objects.filter(room_id__in=room_ids).values( 'user_id', 'room_id') for ru in rooms_users: user_rooms_dict[ru['room_id']][VarNames.ROOM_USERS].append( ru['user_id']) # get all missed messages self.channels = room_ids # py2 doesn't support clear() self.channels.append(self.channel) self.channels.append(self.id) self.listen(self.channels) # this was replaced to syncHistory method that's called from browser and passes existing ids # off_messages, history = self.get_offline_messages(room_users, was_online, self.get_argument('history', False)) # for room in room_users: # room_id = room[VarNames.ROOM_ID] # h = history.get(room_id) # o = off_messages.get(room_id) # if h: # room[VarNames.LOAD_MESSAGES_HISTORY] = h # if o: # room[VarNames.LOAD_MESSAGES_OFFLINE] = o if settings.SHOW_COUNTRY_CODE: fetched_users = User.objects.annotate(user_c=Count('id')).values( 'id', 'username', 'sex', 'userjoinedinfo__ip__country_code', 'userjoinedinfo__ip__country', 'userjoinedinfo__ip__region', 'userjoinedinfo__ip__city') user_dict = [ RedisPrefix.set_js_user_structure_flag( user['id'], user['username'], user['sex'], user['userjoinedinfo__ip__country_code'], user['userjoinedinfo__ip__country'], user['userjoinedinfo__ip__region'], user['userjoinedinfo__ip__city']) for user in fetched_users ] else: fetched_users = User.objects.values('id', 'username', 'sex') user_dict = [ RedisPrefix.set_js_user_structure(user['id'], user['username'], user['sex']) for user in fetched_users ] self.ws_write( self.message_creator.set_room(room_users, user_dict, online, user_db, channels)) online_user_names_mes = self.message_creator.room_online_login( online, user_db.username, user_db.sex_str) self.logger.info('!! First tab, sending refresh online for all') self.publish(online_user_names_mes, settings.ALL_ROOM_ID) self.logger.info("!! User %s subscribes for %s", self.user_id, self.channels) self.connected = True # def get_offline_messages(self, user_rooms, was_online, with_history): # q_objects = get_history_message_query(self.get_argument('messages', None), user_rooms, with_history) # if was_online: # off_messages = [] # else: # off_messages = Message.objects.filter( # id__gt=F('room__roomusers__last_read_message_id'), # room__roomusers__user_id=self.user_id # ) # off = {} # history = {} # if len(q_objects.children) > 0: # history_messages = Message.objects.filter(q_objects) # all = list(chain(off_messages, history_messages)) # self.logger.info("Offline messages IDs: %s, history messages: %s", [m.id for m in off_messages], [m.id for m in history_messages]) # else: # history_messages = [] # all = off_messages # if self.restored_connection: # off_messages = all # history_messages = [] # imv = get_message_images_videos(all) # self.set_video_images_messages(imv, off_messages, off) # self.set_video_images_messages(imv, history_messages, history) # return off, history def check_origin(self, origin): """ check whether browser set domain matches origin """ return True # we don't use cookies @gen.coroutine def save_ip(self): """ This code is not used anymore """ if not UserJoinedInfo.objects.filter( Q(ip__ip=self.ip) & Q(user_id=self.user_id)).exists(): ip = yield from get_or_create_ip_model(self.ip, self.logger) UserJoinedInfo.objects.create(ip=ip, user_id=self.user_id) def ws_write(self, message): """ Tries to send message, doesn't throw exception outside :type self: MessagesHandler :type message object """ # self.logger.debug('<< THREAD %s >>', os.getppid()) try: if isinstance(message, dict): message = json.dumps(message) if not isinstance(message, str_type): raise ValueError('Wrong message type : %s' % str(message)) self.logger.debug(">> %.1000s", message) self.write_message(message) except WebSocketClosedError as e: self.logger.warning("%s. Can't send message << %s >> ", e, str(message)) def get_client_ip(self): return self.request.headers.get("X-Real-IP") or self.request.remote_ip
def open(self): session_key = self.get_argument('sessionId', None) user_id = self.sync_redis.hget('sessions', session_key) if user_id is None: self.logger.warning('!! Session key %s has been rejected' % session_key) self.close(403, "Session key %s has been rejected" % session_key) return self.user_id = int(user_id) self.ip = self.get_client_ip() user_db = UserProfile.objects.get(id=self.user_id) self.generate_self_id() self.message_creator = WebRtcMessageCreator(self.user_id, self.id) self._logger = logging.LoggerAdapter(parent_logger, { 'id': self.id, 'ip': self.ip }) self.logger.debug("!! Incoming connection, session %s, thread hash %s", session_key, self.id) self.async_redis.connect() self.async_redis_publisher.sadd(RedisPrefix.ONLINE_VAR, self.id) # since we add user to online first, latest trigger will always show correct online online = self.get_dict_users_from_redis() # current user is already online my_online = online.setdefault(self.user_id, []) if self.id not in my_online: my_online.append(self.id) was_online = len(online.get( self.user_id)) > 1 # if other tabs are opened user_rooms_query = Room.objects.filter(users__id=self.user_id, disabled=False) \ .values('id', 'name', 'creator_id', 'channel_id', 'p2p', 'roomusers__notifications', 'roomusers__volume') room_users = [{ VarNames.ROOM_ID: room['id'], VarNames.ROOM_NAME: room['name'], VarNames.CHANNEL_ID: room['channel_id'], VarNames.ROOM_CREATOR_ID: room['creator_id'], VarNames.NOTIFICATIONS: room['roomusers__notifications'], VarNames.P2P: room['p2p'], VarNames.VOLUME: room['roomusers__volume'], VarNames.ROOM_USERS: [] } for room in user_rooms_query] user_rooms_dict = {room[VarNames.ROOM_ID]: room for room in room_users} channels_ids = [ channel[VarNames.CHANNEL_ID] for channel in room_users if channel[VarNames.CHANNEL_ID] ] channels_db = Channel.objects.filter(Q(id__in=channels_ids) | Q(creator=self.user_id), disabled=False) channels = [{ VarNames.CHANNEL_ID: channel.id, VarNames.CHANNEL_NAME: channel.name, VarNames.CHANNEL_CREATOR_ID: channel.creator_id } for channel in channels_db] room_ids = [room_id[VarNames.ROOM_ID] for room_id in room_users] rooms_users = RoomUsers.objects.filter(room_id__in=room_ids).values( 'user_id', 'room_id') for ru in rooms_users: user_rooms_dict[ru['room_id']][VarNames.ROOM_USERS].append( ru['user_id']) # get all missed messages self.channels = room_ids # py2 doesn't support clear() self.channels.append(self.channel) self.channels.append(self.id) self.listen(self.channels) # this was replaced to syncHistory method that's called from browser and passes existing ids # off_messages, history = self.get_offline_messages(room_users, was_online, self.get_argument('history', False)) # for room in room_users: # room_id = room[VarNames.ROOM_ID] # h = history.get(room_id) # o = off_messages.get(room_id) # if h: # room[VarNames.LOAD_MESSAGES_HISTORY] = h # if o: # room[VarNames.LOAD_MESSAGES_OFFLINE] = o if settings.SHOW_COUNTRY_CODE: fetched_users = User.objects.annotate(user_c=Count('id')).values( 'id', 'username', 'sex', 'userjoinedinfo__ip__country_code', 'userjoinedinfo__ip__country', 'userjoinedinfo__ip__region', 'userjoinedinfo__ip__city') user_dict = [ RedisPrefix.set_js_user_structure_flag( user['id'], user['username'], user['sex'], user['userjoinedinfo__ip__country_code'], user['userjoinedinfo__ip__country'], user['userjoinedinfo__ip__region'], user['userjoinedinfo__ip__city']) for user in fetched_users ] else: fetched_users = User.objects.values('id', 'username', 'sex') user_dict = [ RedisPrefix.set_js_user_structure(user['id'], user['username'], user['sex']) for user in fetched_users ] self.ws_write( self.message_creator.set_room(room_users, user_dict, online, user_db, channels)) online_user_names_mes = self.message_creator.room_online_login( online, user_db.username, user_db.sex_str) self.logger.info('!! First tab, sending refresh online for all') self.publish(online_user_names_mes, settings.ALL_ROOM_ID) self.logger.info("!! User %s subscribes for %s", self.user_id, self.channels) self.connected = True
class MessagesHandler(): def __init__(self, *args, **kwargs): self.closed_channels = None self.message_creator = WebRtcMessageCreator(None, None) super(MessagesHandler, self).__init__() self.webrtc_ids = {} self.id = None # child init self.last_client_ping = 0 self.user_id = 0 # anonymous by default self.ip = None from chat import global_redis self.async_redis_publisher = global_redis.async_redis_publisher self.sync_redis = global_redis.sync_redis self.channels = [] self._logger = None self.async_redis = Client(host=REDIS_HOST, port=REDIS_PORT, selected_db=REDIS_DB) self.patch_tornadoredis() # input websocket messages handlers # The handler is determined by @VarNames.EVENT self.process_ws_message = { Actions.GET_MESSAGES: self.process_get_messages, Actions.GET_MESSAGES_BY_IDS: self.process_get_messages_by_ids, Actions.PRINT_MESSAGE: self.process_send_message, Actions.DELETE_ROOM: self.delete_room, Actions.EDIT_MESSAGE: self.edit_message, Actions.CREATE_ROOM: self.create_new_room, Actions.CREATE_CHANNEL: self.create_new_channel, Actions.SAVE_CHANNEL_SETTINGS: self.save_channels_settings, Actions.SAVE_ROOM_SETTINGS: self.save_room_settings, Actions.DELETE_CHANNEL: self.delete_channel, Actions.SET_USER_PROFILE: self.profile_save_user, Actions.SET_SETTINGS: self.profile_save_settings, Actions.INVITE_USER: self.invite_user, Actions.PING: self.respond_ping, Actions.PONG: self.process_pong_message, Actions.SEARCH_MESSAGES: self.search_messages, Actions.SYNC_HISTORY: self.sync_history, Actions.SHOW_I_TYPE: self.show_i_type, } # Handlers for redis messages, if handler returns true - message won't be sent to client # The handler is determined by @VarNames.EVENT self.process_pubsub_message = { Actions.CREATE_ROOM: self.send_client_new_channel, Actions.DELETE_ROOM: self.send_client_delete_channel, Actions.INVITE_USER: self.send_client_new_channel, Actions.ADD_INVITE: self.send_client_new_channel, Actions.PING: self.process_ping_message, } def patch_tornadoredis(self): # TODO remove this fabric = type(self.async_redis.connection.readline) self.async_redis.connection.old_read = self.async_redis.connection.readline def new_read(new_self, callback=None): try: return new_self.old_read(callback=callback) except Exception as e: return current_online = self.get_online_from_redis() self.logger.error(e) self.logger.error( "Exception info: " "self.id: %s ;;; " "self.connected = '%s';;; " "Redis default channel online = '%s';;; " "self.channels = '%s';;; " "self.closed_channels = '%s';;;", self.id, self.connected, current_online, self.channels, self.closed_channels ) raise e self.async_redis.connection.readline = fabric(new_read, self.async_redis.connection) @property def channel(self): return RedisPrefix.generate_user(self.user_id) @property def connected(self): raise NotImplemented @connected.setter def connected(self, value): raise NotImplemented @gen.engine def listen(self, channels): yield Task( self.async_redis.subscribe, channels) self.async_redis.listen(self.on_pub_sub_message) @property def logger(self): return self._logger if self._logger else base_logger @gen.engine def add_channel(self, channel): self.channels.append(channel) yield Task(self.async_redis.subscribe, (channel,)) def get_online_from_redis(self): return self.get_online_and_status_from_redis()[1] def get_dict_users_from_redis(self): online = self.sync_redis.ssmembers(RedisPrefix.ONLINE_VAR) self.logger.debug('!! redis online: %s', online) result = self.parse_redis_online_into_dict_set(online) if online else {} return result @staticmethod def parse_redis_online_into_dict_set(online): """ :rtype : Dict[int, set] """ result = {} for decoded in online: # py2 iteritems user_id = decoded.split(':')[0] result.setdefault(int(user_id), []).append(decoded) return result def get_online_and_status_from_redis(self): """ :rtype : (bool, list) """ online = self.sync_redis.ssmembers(RedisPrefix.ONLINE_VAR) self.logger.debug('!! redis online: %s', online) return self.parse_redis_online(online) if online else (False, []) def parse_redis_online(self, online): """ :rtype : (bool, list) """ result = set() user_is_online = False for decoded in online: # py2 iteritems user_id = int(decoded.split(':')[0]) if user_id == self.user_id and decoded != self.id: user_is_online = True result.add(user_id) return user_is_online, list(result) def publish(self, message, channel, parsable=False): jsoned_mess = encode_message(message, parsable) self.raw_publish(jsoned_mess, channel) def raw_publish(self, jsoned_mess, channel): self.logger.debug('<%s> %s', channel, jsoned_mess) self.async_redis_publisher.publish(channel, jsoned_mess) def on_pub_sub_message(self, message): """ All pubsub messages are automatically sent to client. :param message: :return: """ data = message.body if isinstance(data, str_type): # not subscribe event prefixless_str = remove_parsable_prefix(data) if prefixless_str: dict_message = json.loads(prefixless_str) res = self.process_pubsub_message[dict_message[VarNames.EVENT]](dict_message) if not res: self.ws_write(prefixless_str) else: self.ws_write(data) def ws_write(self, message): raise NotImplementedError('WebSocketHandler implements') def search_giphy(self, message, query, cb): self.logger.debug("!! Asking giphy for: %s", query) def on_giphy_reply(response): try: self.logger.debug("!! Got giphy response: " + str(response.body)) res = json.loads(response.body) giphy = res['data'][0]['images']['downsized_medium']['url'] except: giphy = None cb(message, giphy) url = GIPHY_URL.format(GIPHY_API_KEY, quote(query, safe='')) http_client.fetch(url, callback=on_giphy_reply) def notify_offline(self, channel, message_id): if FIREBASE_API_KEY is None: return online = self.get_online_from_redis() if channel == ALL_ROOM_ID: return offline_users = RoomUsers.objects.filter(room_id=channel, notifications=True).exclude(user_id__in=online).values_list('user_id') subscriptions = Subscription.objects.filter(user__in=offline_users, inactive=False) if len(subscriptions) == 0: return new_sub_mess =[SubscriptionMessages(message_id=message_id, subscription_id=r.id) for r in subscriptions] reg_ids =[r.registration_id for r in subscriptions] SubscriptionMessages.objects.bulk_create(new_sub_mess) self.post_firebase(list(reg_ids)) def post_firebase(self, reg_ids): def on_reply(response): try: self.logger.debug("!! FireBase response: " + str(response.body)) response_obj = json.loads(response.body) delete = [] for index, elem in enumerate(response_obj['results']): if elem.get('error') in ['NotRegistered', 'InvalidRegistration']: delete.append(reg_ids[index]) if len(delete) > 0: self.logger.info("Deactivating subscriptions: %s", delete) Subscription.objects.filter(registration_id__in=delete).update(inactive=True) except Exception as e: self.logger.error("Unable to parse response" + str(e)) pass headers = {"Content-Type": "application/json", "Authorization": "key=%s" % FIREBASE_API_KEY} body = json.dumps({"registration_ids": reg_ids}) self.logger.debug("!! post_fire_message %s", body) # TODO # webpush(subscription_info, # data, # vapid_private_key="Private Key or File Path[1]", # vapid_claims={"sub": "mailto:YourEmailAddress"}) r = HTTPRequest(FIREBASE_URL, method="POST", headers=headers, body=body) http_client.fetch(r, callback=on_reply) def isGiphy(self, content): if GIPHY_API_KEY is not None and content is not None: giphy_match = re.search(GIPHY_REGEX, content) return giphy_match.group(1) if giphy_match is not None else None def process_send_message(self, message): """ :type message: dict """ content = message.get(VarNames.CONTENT) giphy_match = self.isGiphy(content) # @transaction.atomic mysql has gone away def send_message(message, giphy=None): if message[VarNames.TIME_DIFF] < 0: raise ValidationError("Back to the future?") tags_users = message[VarNames.MESSAGE_TAGS] files = UploadedFile.objects.filter(id__in=message.get(VarNames.FILES), user_id=self.user_id) symbol = max_from_2(get_max_symbol(files), get_max_symbol_dict(tags_users)) channel = message[VarNames.ROOM_ID] js_id = message[VarNames.JS_MESSAGE_ID] parent_message_id = message[VarNames.PARENT_MESSAGE] if parent_message_id: parent_room_id = Message.objects.get(id=parent_message_id).room_id if parent_room_id not in self.channels: raise ValidationError("You don't have access to this room message") message_db = Message( sender_id=self.user_id, content=message[VarNames.CONTENT], symbol=symbol, parent_message_id=parent_message_id, giphy=giphy, room_id=channel ) message_db.time -= message[VarNames.TIME_DIFF] res_files = [] message_db.save() if tags_users: mes_ment = [MessageMention( user_id=userId, message_id=message_db.id, symbol=symb, ) for symb, userId in tags_users.items()] MessageMention.objects.bulk_create(mes_ment) if files: images = up_files_to_img(files, message_db.id) res_files = MessagesCreator.prepare_img_video(images, message_db.id) prepared_message = self.message_creator.create_send_message( message_db, Actions.PRINT_MESSAGE, res_files, tags_users ) prepared_message[VarNames.JS_MESSAGE_ID] = js_id self.publish(prepared_message, channel) self.notify_offline(channel, message_db.id) if giphy_match is not None: self.search_giphy(message, giphy_match, send_message) else: send_message(message) def save_channels_settings(self, message): channel_id = message[VarNames.CHANNEL_ID] channel_name = message[VarNames.CHANNEL_NAME] new_creator = message[VarNames.CHANNEL_CREATOR_ID] if not channel_name or len(channel_name) > 16: raise ValidationError('Incorrect channel name name "{}"'.format(channel_name)) channel = Channel.objects.get(id=channel_id) users_id = list(RoomUsers.objects.filter(room__channel_id=channel_id).values_list('user_id', flat=True)) if channel.creator_id != self.user_id and self.user_id not in users_id: raise ValidationError("You are not allowed to edit this channel") if new_creator != channel.creator_id: if self.user_id != channel.creator_id: raise ValidationError("Only admin can change the admin of this channel") if new_creator not in users_id: raise ValidationError("You can only change admin to one of the users in this channels room") channel.creator_id = new_creator if self.user_id not in users_id: # if channel has no rooms users_id.append(self.user_id) channel.name = channel_name channel.save() m = { VarNames.EVENT: Actions.SAVE_CHANNEL_SETTINGS, VarNames.CHANNEL_ID: channel_id, VarNames.CB_BY_SENDER: self.id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.CHANNEL_NAME: channel_name, VarNames.CHANNEL_CREATOR_ID: channel.creator_id, VarNames.TIME: get_milliseconds(), VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID], } for user_id in users_id: self.publish(m, RedisPrefix.generate_user(user_id)) def create_new_channel(self, message): channel_name = message.get(VarNames.CHANNEL_NAME) if not channel_name or len(channel_name) > 16: raise ValidationError('Incorrect channel name name "{}"'.format(channel_name)) channel = Channel(name=channel_name, creator_id=self.user_id) channel.save() channel_id = channel.id m = { VarNames.EVENT: Actions.CREATE_CHANNEL, VarNames.CHANNEL_ID: channel_id, VarNames.CHANNEL_CREATOR_ID: channel.creator_id, VarNames.CB_BY_SENDER: self.id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.CHANNEL_NAME: channel_name, VarNames.TIME: get_milliseconds(), VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID], } self.publish(m, self.channel) def create_new_room(self, message): room_name = message.get(VarNames.ROOM_NAME) users = message.get(VarNames.ROOM_USERS) channel_id = message.get(VarNames.CHANNEL_ID) users.append(self.user_id) users = list(set(users)) if room_name and len(room_name) > 16: raise ValidationError('Incorrect room name "{}"'.format(room_name)) create_room = True if not room_name and len(users) == 2: user_rooms = evaluate(Room.users.through.objects.filter(user_id=self.user_id, room__name__isnull=True).values('room_id')) user_id = users[0] if users[1] == self.user_id else users[1] try: room = RoomUsers.objects.filter(user_id=user_id, room__in=user_rooms).values('room__id', 'room__disabled').get() room_id = room['room__id'] if room['room__disabled']: # only a private room can be disabled Room.objects.filter(id=room_id).update(disabled=False, p2p=message[VarNames.P2P]) RoomUsers.objects.filter( user_id=self.user_id, room_id=room_id ).update( volume=message[VarNames.VOLUME], notifications=message[VarNames.NOTIFICATIONS] ) else: raise ValidationError('This room already exist') create_room = False except RoomUsers.DoesNotExist: pass elif not room_name: raise ValidationError('At least one user should be selected, or room should be public') if channel_id and channel_id not in self.get_users_channels_ids(): raise ValidationError("You don't have access to this channel") if channel_id: channel = Channel.objects.get(id=channel_id) channel_name = channel.name channel_creator_id = channel.creator_id else: channel_name = None channel_creator_id = None if create_room: room = Room(name=room_name, channel_id=channel_id, p2p=message[VarNames.P2P]) if not room_name: room.creator_id = self.user_id room.save() room_id = room.id max_id = Message.objects.all().aggregate(Max('id'))['id__max'] ru = [RoomUsers( user_id=user_id, room_id=room_id, last_read_message_id=max_id, volume=message[VarNames.VOLUME], notifications=message[VarNames.NOTIFICATIONS] ) for user_id in users] RoomUsers.objects.bulk_create(ru) m = { VarNames.EVENT: Actions.CREATE_ROOM, VarNames.ROOM_ID: room_id, VarNames.ROOM_USERS: users, VarNames.CB_BY_SENDER: self.id, VarNames.INVITER_USER_ID: self.user_id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.VOLUME: message[VarNames.VOLUME], VarNames.P2P: message[VarNames.P2P], VarNames.NOTIFICATIONS: message[VarNames.NOTIFICATIONS], VarNames.ROOM_NAME: room_name, VarNames.TIME: get_milliseconds(), VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID], } if channel_id: m[VarNames.CHANNEL_NAME] = channel_name m[VarNames.CHANNEL_ID] = channel_id m[VarNames.CHANNEL_CREATOR_ID] = channel_creator_id jsoned_mess = encode_message(m, True) for user in users: self.raw_publish(jsoned_mess, RedisPrefix.generate_user(user)) def save_room_settings(self, message): """ POST only, validates email during registration """ room_id = message[VarNames.ROOM_ID] room_name = message[VarNames.ROOM_NAME] creator_id = message.get(VarNames.ROOM_CREATOR_ID) # will be none for private room updated = RoomUsers.objects.filter(room_id=room_id, user_id=self.user_id).update( volume=message[VarNames.VOLUME], notifications=message[VarNames.NOTIFICATIONS] ) if updated != 1: raise ValidationError("You don't have access to this room") room = Room.objects.get(id=room_id) update_all = False if not room.name: if room.p2p != message[VarNames.P2P]: room.p2p = message[VarNames.P2P] update_all = True elif room_id != settings.ALL_ROOM_ID: if room_name != room.name: room.name = room_name update_all = True if room.channel_id != message[VarNames.CHANNEL_ID]: room.channel_id = message[VarNames.CHANNEL_ID] if room.channel_id and room.channel_id not in self.get_users_channels_ids(): raise ValidationError("You don't have access to this channel") update_all = True if creator_id != room.creator_id: if room.creator_id != self.user_id: raise ValidationError("Only an owner of this room can change its admin") users_id = RoomUsers.objects.filter(room_id=room.id).values_list('user_id', flat=True) if creator_id not in users_id: raise ValidationError("You can only change admin to one of the users in this channels room") room.creator_id = creator_id update_all = True if message.get(VarNames.CHANNEL_ID): # will be nOne for private room channel = Channel.objects.get(id=message[VarNames.CHANNEL_ID]) channel_name = channel.name channel_creator_id = channel.creator_id else: channel_name = None channel_creator_id = None if update_all: room.save() room_users = list(RoomUsers.objects.filter(room_id=room_id)) for room_user in room_users: self.publish({ VarNames.EVENT: Actions.SAVE_ROOM_SETTINGS, VarNames.CHANNEL_ID: room.channel_id, VarNames.CB_BY_SENDER: self.id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.CHANNEL_NAME: channel_name, VarNames.CHANNEL_CREATOR_ID: channel_creator_id, VarNames.ROOM_CREATOR_ID: room.creator_id, VarNames.ROOM_ID: room.id, VarNames.VOLUME: room_user.volume, VarNames.NOTIFICATIONS: room_user.notifications, VarNames.P2P: message[VarNames.P2P], VarNames.ROOM_NAME: room_name, VarNames.TIME: get_milliseconds(), VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID], }, RedisPrefix.generate_user(room_user.user_id)) else: self.publish({ VarNames.EVENT: Actions.SAVE_ROOM_SETTINGS, VarNames.CHANNEL_ID: room.channel_id, VarNames.CB_BY_SENDER: self.id, VarNames.CHANNEL_CREATOR_ID: channel_creator_id, VarNames.ROOM_CREATOR_ID: room.creator_id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.CHANNEL_NAME: channel_name, VarNames.ROOM_ID: room.id, VarNames.VOLUME: message[VarNames.VOLUME], VarNames.NOTIFICATIONS: message[VarNames.NOTIFICATIONS], VarNames.P2P: message[VarNames.P2P], VarNames.ROOM_NAME: room_name, VarNames.TIME: get_milliseconds(), VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID], }, self.channel) def get_users_channels_ids(self): channels_ids = Room.objects.filter(users__id=self.user_id, disabled=False).values_list('channel_id', flat=True) return Channel.objects.filter(Q(id__in=channels_ids) | Q(creator=self.user_id), disabled=False).values_list('id', flat=True) def profile_save_settings(self, in_message): message = in_message[VarNames.CONTENT] UserProfile.objects.filter(id=self.user_id).update( suggestions=message[UserSettingsVarNames.SUGGESTIONS], embedded_youtube=message[UserSettingsVarNames.EMBEDDED_YOUTUBE], highlight_code=message[UserSettingsVarNames.HIGHLIGHT_CODE], message_sound=message[UserSettingsVarNames.MESSAGE_SOUND], incoming_file_call_sound=message[UserSettingsVarNames.INCOMING_FILE_CALL_SOUND], online_change_sound=message[UserSettingsVarNames.ONLINE_CHANGE_SOUND], show_when_i_typing=message[UserSettingsVarNames.SHOW_WHEN_I_TYPING], logs=message[UserSettingsVarNames.LOGS], send_logs=message[UserSettingsVarNames.SEND_LOGS], theme=message[UserSettingsVarNames.THEME], ) self.publish(self.message_creator.set_settings(in_message[VarNames.JS_MESSAGE_ID], message), self.channel) def profile_save_user(self, in_message): message = in_message[VarNames.CONTENT] userprofile = UserProfile.objects.get(id=self.user_id) un = message[UserProfileVarNames.USERNAME] if userprofile.username != un: check_user(un) sex = message[UserProfileVarNames.SEX] UserProfile.objects.filter(id=self.user_id).update( username=un, name=message[UserProfileVarNames.NAME], city=message[UserProfileVarNames.CITY], surname=message[UserProfileVarNames.SURNAME], birthday=message[UserProfileVarNames.BIRTHDAY], contacts=message[UserProfileVarNames.CONTACTS], sex=settings.GENDERS_STR[sex], ) self.publish(self.message_creator.set_user_profile(in_message[VarNames.JS_MESSAGE_ID], message), self.channel) if userprofile.sex_str != sex or userprofile.username != un: self.publish(self.message_creator.changed_user_profile(sex, self.user_id, un), settings.ALL_ROOM_ID) def profile_save_image(self, request): pass # UserProfile.objects.filter(id=request.user.id).update( # suggestions=request.POST['suggestions'], # embedded_youtube=request.POST['embedded_youtube'], # highlight_code=request.POST['highlight_code'], # message_sound=request.POST['message_sound'], # incoming_file_call_sound=request.POST['incoming_file_call_sound'], # online_change_sound=request.POST['online_change_sound'], # logs=request.POST['logs'], # send_logs=request.POST['send_logs'], # theme=request.POST['theme'], # ) # return HttpResponse(settings.VALIDATION_IS_OK, content_type='text/plain') def invite_user(self, message): room_id = message[VarNames.ROOM_ID] if room_id not in self.channels: raise ValidationError("Access denied, only allowed for channels {}".format(self.channels)) room = Room.objects.get(id=room_id) if room.is_private: raise ValidationError("You can't add users to direct room, create a new room instead") users = message.get(VarNames.ROOM_USERS) users_in_room = list(RoomUsers.objects.filter(room_id=room_id).values_list('user_id', flat=True)) intersect = set(users_in_room) & set(users) if bool(intersect): raise ValidationError("Users %s are already in the room", intersect) users_in_room.extend(users) max_id = Message.objects.filter(room_id=room_id).aggregate(Max('id'))['id__max'] if not max_id: max_id = Message.objects.all().aggregate(Max('id'))['id__max'] ru = [RoomUsers( user_id=user_id, room_id=room_id, last_read_message_id=max_id, volume=1, notifications=False ) for user_id in users] RoomUsers.objects.bulk_create(ru) add_invitee = { VarNames.EVENT: Actions.ADD_INVITE, VarNames.ROOM_ID: room_id, VarNames.ROOM_USERS: users_in_room, VarNames.ROOM_NAME: room.name, VarNames.INVITEE_USER_ID: users, VarNames.INVITER_USER_ID: self.user_id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.TIME: get_milliseconds(), VarNames.VOLUME: 1, VarNames.NOTIFICATIONS: False, } add_invitee_dumped = encode_message(add_invitee, True) for user in users: self.raw_publish(add_invitee_dumped, RedisPrefix.generate_user(user)) invite = { VarNames.EVENT: Actions.INVITE_USER, VarNames.ROOM_ID: room_id, VarNames.INVITEE_USER_ID: users, VarNames.INVITER_USER_ID: self.user_id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.ROOM_USERS: users_in_room, VarNames.TIME: get_milliseconds(), VarNames.CB_BY_SENDER: self.id, VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID] } self.publish(invite, room_id, True) def respond_ping(self, message): self.ws_write(self.message_creator.responde_pong(message[VarNames.JS_MESSAGE_ID])) def process_pong_message(self, message): self.last_client_ping = message[VarNames.TIME] def process_ping_message(self, message): def call_check(): if message[VarNames.TIME] != self.last_client_ping: self.close(408, "Ping timeout") IOLoop.instance().call_later(settings.PING_CLOSE_SERVER_DELAY, call_check) def delete_channel(self, message): channel_id = message[VarNames.CHANNEL_ID] channel = Channel.objects.get(id=channel_id) if channel.creator_id != self.user_id: raise ValidationError(f"Only admin can delete this channel. Please ask ${User.objects.get(id=channel.creator_id).username}") # if Room.objects.filter(channel_id=channel_id).count() > 0: users_id = list(RoomUsers.objects.filter(room__channel_id=channel_id).values_list('user_id', flat=True)) if len(users_id) > 0: raise ValidationError(f"Some users are still in the rooms on this channel Please ask them to leave") Room.objects.filter(channel_id=channel_id).update(disabled=True) channel.disabled = True channel.save() message = { VarNames.EVENT: Actions.DELETE_CHANNEL, VarNames.CHANNEL_ID: channel_id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.TIME: get_milliseconds(), VarNames.CB_BY_SENDER: self.id, VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID] } self.publish(message, self.channel) def delete_room(self, message): room_id = message[VarNames.ROOM_ID] js_id = message[VarNames.JS_MESSAGE_ID] if room_id not in self.channels or room_id == ALL_ROOM_ID: raise ValidationError('You are not allowed to exit this room') room = Room.objects.get(id=room_id) if room.disabled: raise ValidationError('Room is already deleted') if room.name is None: # if private then disable room.disabled = True room.save() else: # if public -> leave the room, delete the link RoomUsers.objects.filter(room_id=room.id, user_id=self.user_id).delete() ru = list(RoomUsers.objects.filter(room_id=room.id).values_list('user_id', flat=True)) message = self.message_creator.unsubscribe_direct_message(room_id, js_id, self.id, ru, room.name) self.publish(message, room_id, True) def edit_message(self, data): message = Message.objects.get(id=data[VarNames.MESSAGE_ID]) validate_edit_message(self.user_id, message) message.content = data[VarNames.CONTENT] MessageHistory(message=message, content=message.content, giphy=message.giphy).save() message.edited_times += 1 giphy_match = self.isGiphy(data[VarNames.CONTENT]) if message.content is None: Message.objects.filter(id=data[VarNames.MESSAGE_ID]).update( deleted=True, edited_times=get_milliseconds(), content=None ) self.publish(self.message_creator.create_send_message(message, Actions.DELETE_MESSAGE, None, {}), message.room_id) elif giphy_match is not None: self.edit_message_giphy(giphy_match, message) else: self.edit_message_edit(data, message) def edit_message_giphy(self, giphy_match, message): def edit_glyphy(message, giphy): Message.objects.filter(id=message.id).update( content=message.content, symbol=message.symbol, giphy=giphy, edited_times=get_milliseconds() ) message.giphy = giphy self.publish(self.message_creator.create_send_message(message, Actions.EDIT_MESSAGE, None, {}), message.room_id) self.search_giphy(message, giphy_match, edit_glyphy) def edit_message_edit(self, data, message): action = Actions.EDIT_MESSAGE message.giphy = None tags = data[VarNames.MESSAGE_TAGS] files = UploadedFile.objects.filter(id__in=data.get(VarNames.FILES), user_id=self.user_id) if files or tags: update_symbols(files, tags, message) if tags: db_tags = MessageMention.objects.filter(message_id=message.id) update_or_create = [] update_or_create_dict = {} for db_tag in db_tags: if tags.get(db_tag.symbol) and tags.get(db_tag.symbol) != db_tag.user_id: update_or_create.append(MessageMention(message_id=message.id, symbol=db_tag.symbol, user_id=tags[db_tag.symbol])) update_or_create_dict[db_tag.symbol] = True if update_or_create: MessageMention.objects.bulk_update(update_or_create) create_tags = [] for (k, v) in tags.items(): if not update_or_create_dict.get(k): create_tags.append(MessageMention(message_id=message.id, symbol=k, user_id=v)) if create_tags: MessageMention.objects.bulk_create(create_tags) if files: up_files_to_img(files, message.id) if message.symbol: # fetch all, including that we just store db_images = Image.objects.filter(message_id=message.id) prep_files = MessagesCreator.prepare_img_video(db_images, message.id) else: prep_files = None Message.objects.filter(id=message.id).update(content=message.content, symbol=message.symbol, giphy=None, edited_times=get_milliseconds()) self.publish(self.message_creator.create_send_message(message, action, prep_files, tags), message.room_id) def send_client_new_channel(self, message): room_id = message[VarNames.ROOM_ID] self.add_channel(room_id) def send_client_delete_channel(self, message): room_id = message[VarNames.ROOM_ID] if message[VarNames.USER_ID] == self.user_id or message[VarNames.ROOM_NAME] is None: self.async_redis.unsubscribe((room_id,)) self.channels.remove(room_id) channels = { VarNames.EVENT: Actions.DELETE_MY_ROOM, VarNames.ROOM_ID: room_id, VarNames.HANDLER_NAME: HandlerNames.ROOM, VarNames.JS_MESSAGE_ID: message[VarNames.JS_MESSAGE_ID], } self.ws_write(channels) else: self.ws_write({ VarNames.EVENT: Actions.USER_LEAVES_ROOM, VarNames.ROOM_ID: room_id, VarNames.USER_ID: message[VarNames.USER_ID], VarNames.ROOM_USERS: message[VarNames.ROOM_USERS], VarNames.HANDLER_NAME: HandlerNames.ROOM }) return True def process_get_messages_by_ids(self, data): """ :type data: dict """ ids = data[VarNames.MESSAGE_IDS] room_id = data[VarNames.ROOM_ID] messages = Message.objects.filter(room_id=room_id, id__in=ids) response = self.message_creator.get_messages(messages, data[VarNames.JS_MESSAGE_ID]) self.ws_write(response) def process_get_messages(self, data): """ :type data: dict """ exclude_ids = data[VarNames.EXCLUDE_IDS] # this method needs to accept ids of message, because messages on the client can be not-ordered # lets say we loaded a message for a thread , so it's single # or someone joined from offline and he synced message in the top. thread_id = data[VarNames.THREAD_ID] room_id = data[VarNames.ROOM_ID] if thread_id: messages = Message.objects.filter(room_id=room_id, parent_message__id=thread_id) else: count = int(data.get(VarNames.GET_MESSAGES_COUNT, 10)) if count > 100: raise ValidationError("Can't load that many messages") messages = Message.objects.filter( Q(room_id=room_id) & Q(parent_message__id=thread_id) & ~Q(id__in=exclude_ids) ).order_by('-pk')[:count] response = self.message_creator.get_messages(messages, data[VarNames.JS_MESSAGE_ID]) self.ws_write(response) def search_messages(self, data): offset = data[VarNames.SEARCH_OFFSET] #// room, offset messages = Message.objects.filter( content__icontains=data[VarNames.SEARCH_STRING], room_id=data[VarNames.ROOM_ID] # access permissions is already checked on top level by ROOM_ID ).order_by('-id')[offset:offset + settings.MESSAGES_PER_SEARCH] content = MessagesCreator.message_models_to_dtos(messages) self.ws_write({ VarNames.CONTENT: content, VarNames.JS_MESSAGE_ID: data[VarNames.JS_MESSAGE_ID], VarNames.HANDLER_NAME: HandlerNames.NULL }) def show_i_type(self, message): self.publish({ VarNames.ROOM_ID: message[VarNames.ROOM_ID], VarNames.USER_ID: self.user_id, VarNames.EVENT: Actions.SHOW_I_TYPE, VarNames.HANDLER_NAME: HandlerNames.ROOM # because ws-message doesnt exist in p2p }, message[VarNames.ROOM_ID]) def sync_history(self, in_message): room_ids = in_message[VarNames.ROOM_IDS] message_ids = in_message[VarNames.MESSAGE_IDS] if not set(room_ids).issubset(self.channels): raise ValidationError("This is not a messages in the room you are in") messages = Message.objects.filter( Q(room_id__in=room_ids) & ~Q(id__in=message_ids) & Q(edited_times__gt=get_milliseconds() - in_message[VarNames.LAST_SYNCED]) ) content = MessagesCreator.message_models_to_dtos(messages) self.ws_write({ VarNames.CONTENT: content, VarNames.JS_MESSAGE_ID: in_message[VarNames.JS_MESSAGE_ID], VarNames.HANDLER_NAME: HandlerNames.NULL })