def logout(event, context): """ Handles a logout request via WebSocket """ logger.info("Login request via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Query the connections Table and remove the current user connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) local_var = ":user" result = connections_table.update_item( Key={definitions.Connections.CONNECTION_ID: connectionID}, UpdateExpression="set {} = {}".format(definitions.Connections.USERNAME, local_var), ExpressionAttributeValues={local_var: None}, ReturnValues="UPDATED_OLD") logger.debug("Result: {}", result) # If there was a current user, remove the connection ID from the user Table if result['ResponseMetadata'][ 'HTTPStatusCode'] == 200 and 'Attributes' in result: if definitions.Connections.USERNAME in result['Attributes']: logger.debug( result['Attributes'][definitions.Connections.USERNAME]) if result['Attributes'][ definitions.Connections.USERNAME] is not None: username = result['Attributes'][ definitions.Connections.USERNAME] users_table = dynamodb.Table(definitions.Users.TABLE_NAME) local_var = ":connID" result = users_table.update_item( Key={definitions.Users.USERNAME: username}, UpdateExpression="delete {} {}".format( definitions.Users.CONNECTION_ID, local_var), ExpressionAttributeValues={local_var: set([connectionID])}, ReturnValues="UPDATED_NEW") logger.debug("Updated result: {}", result) else: logger.error("Connections table not properly updated - result: {}", result) _send_to_connection( connectionID, _build_response_detailed(500, action, "Internal server error"), event) return _build_response(500, "Internal server error") # Send the login page to the client obj = s3.Object('chat-application-upload-bucket-11097', 'login_page.html') body = obj.get()["Body"].read().decode("utf-8") _send_to_connection(connectionID, _build_response_detailed(200, action, body), event) return _build_response(200, "Disconnection success")
def fetch_create_group_page(event, context): """ Endpoint to get the create group page """ logger.info("Group creation page via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate that user is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Connections.CONNECTION_ID).eq(connectionID)) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: logger.error( "user password query returned not even an empty set items: {}", items) _send_to_connection( connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection( connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") # username = item[definitions.Connections.USERNAME] obj = s3.Object('chat-application-upload-bucket-11097', 'create_group_page.html') body = obj.get()["Body"].read().decode("utf-8") _send_to_connection(connectionID, _build_response_detailed(200, action, body), event) return _build_response(200, "Create Group page fetched")
def get_group(event, context): """ Return a selected group """ logger.info("Entering a group") connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate the connection is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Connections.CONNECTION_ID).eq(connectionID)) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) >= 0: item = items[0] else: logger.error( "user password query returned not even an empty set items: {}", items) _send_to_connection( connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection( connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") username = item[definitions.Connections.USERNAME] default_fetch = 20 # Default number of messages to fetch body = _fetch_body(event, logger) for attribute in [definitions.Groups.GROUP_ID]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format( attribute) logger.debug(error_message) _send_to_connection( connectionID, _build_response_detailed(400, action, error_message), event) return _build_response(400, error_message) group = body[definitions.Groups.GROUP_ID] # # Validate the connection is logged in # connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) # response = connections_table.query( # ProjectionExpression=definitions.Connections.USERNAME, # KeyConditionExpression=boto3.dynamodb.conditions.Key(definitions.Connections.CONNECTION_ID).eq(connectionID) # ) # logger.debug("response: {}".format(response)) # items = response.get("Items", []) # logger.debug("items: {}".format(items)) # if (len(items) > 0): # item = items[0] # else: # logger.error("user password query returned not even an empty set items: {}".format(items)) # _send_to_connection(connectionID, _build_response_detailed(500, action, "Server error"), event) # return _build_response(500, "Server error") # if definitions.Connections.USERNAME not in item: # _send_to_connection(connectionID, _build_response_detailed(401, action, "Not logged in"), event) # return _build_response(401, "Not logged in") # username = item[definitions.Connections.USERNAME] # Validate the user is a member of the group groups_table = dynamodb.Table(definitions.Groups.TABLE_NAME) response = groups_table.query( # ProjectionExpression="{}, {}".format(definitions.Groups.USERNAME, definitions.Groups.NICKNAME), KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Groups.GROUP_ID).eq(group)) items = response.get("Items", []) logger.debug("items: {}", str(items)) ok = False # current_user_exclusion_list = [] # current_user_left_timestamp = -1 for item in items: if item[definitions.Groups.USERNAME] == username: ok = True current_user_exclusion_list = item[definitions.Groups.EXCLUSION_LIST] if definitions.Groups.EXCLUSION_LIST in item else [] current_user_left = bool(current_user_exclusion_list) and len( current_user_exclusion_list[-1]) == 1 # Note if the current user has left the group # if definitions.Groups.EXCLUSION_LIST in item and len(item[definitions.Groups.EXCLUSION_LIST][-1]) == 1: # current_user_left = True # current_user_left_timestamp = item[definitions.Groups.EXCLUSION_LIST] break if not ok: _send_to_connection( connectionID, _build_response_detailed(403, action, "Not a member of group"), event) return _build_response(403, "Not a member of group") logger.debug( "current_user_left: {} current_user_exclusion_list: {} ".format( current_user_left, current_user_exclusion_list)) # Group table info to get the group nickname & list of users nickname = items[0][definitions.Groups.NICKNAME] current_group_users = [ item[definitions.Groups.USERNAME] for item in items if not (definitions.Groups.EXCLUSION_LIST in item and len(item[definitions.Groups.EXCLUSION_LIST][-1]) == 1) ] # current_user_left = username not in current_group_users # definitions.Groups.LEFT: item[definitions.Groups.LEFT] if definitions.Groups.LEFT in item else False logger.debug("nickname: {} current_group_users: {}", nickname, current_group_users) logger.debug("group: {}", group) messages_table = dynamodb.Table(definitions.Messages.TABLE_NAME) local_name_1 = ":group_id" local_name_2_template = ":timestmp{}_{}" # local_name_2 = ":timestmp" if current_user_left: expression_attribute_values = { definitions.Messages.HASH_KEY: local_name_1 } key_condition_expression = [[ "{} = {} and (".format(definitions.Messages.HASH_KEY, local_name_1) ]] for i, exclusion_item in enumerate(current_user_exclusion_list): exclusion_pair = [] temp_local_name_2 = local_name_2_template.format(i, 0) message = "{} <= {}".format(definitions.Messages.RANGE_KEY, temp_local_name_2) expression_attribute_values[temp_local_name_2] = exclusion_item[0] if len(exclusion_item) > 1: exclusion_pair.append(message) temp_local_name_2 = local_name_2_template.format(i, 1) exclusion_pair.append("{} >= {}".format( definitions.Messages.RANGE_KEY, temp_local_name_2)) expression_attribute_values[ temp_local_name_2] = exclusion_item[1] else: message += ")" exclusion_pair.append(message) logger.debug("KeyConditionExpression before join: {}", key_condition_expression) key_condition_expression = " and ".join( " or ".join(item) for item in key_condition_expression) logger.debug("KeyConditionExpression after join: {}", key_condition_expression) logger.debug("ExpressionAttributeValues: {}", expression_attribute_values) response = messages_table.query( KeyConditionExpression=key_condition_expression, ExpressionAttributeValues=expression_attribute_values, Limit=default_fetch, ScanIndexForward=False) # items = response.get("Items", []) # logger.debug("items: {}".format(str(items))) # if len(items) > default_fetch: # items = items[-default_fetch:] # logger.debug("items: {}".format(str(items))) else: response = messages_table.query( KeyConditionExpression="{} = {}".format( definitions.Messages.HASH_KEY, local_name_1), ExpressionAttributeValues={local_name_1: group}, Limit=default_fetch, ScanIndexForward=False) items = response.get("Items", []) logger.debug("items: {}", str(items)) # How many new messages were actually taken messages = [ { definitions.Messages.USERNAME: item[definitions.Messages.USERNAME] if definitions.Messages.USERNAME in item else None, definitions.Messages.CONTENT: item[definitions.Messages.CONTENT], definitions.Messages.TIMESTAMP: datetime.fromtimestamp(item[definitions.Messages.TIMESTAMP] // 1000000000).strftime('%m/%d/%Y %H:%M'), #definitions.Messages.INIT: x[definitions.Messages.INIT] if definitions.Messages.INIT in x else False } for item in items ] messages.reverse() memo_username = None all_messages = [] temp_list = [] # init_set = False for message in messages: if memo_username is None: memo_username = message[definitions.Messages.USERNAME] if temp_list: all_messages.append(temp_list[:]) temp_list.clear() # if message[definitions.Messages.INIT] == True: # init_set = True elif memo_username != message[definitions.Messages.USERNAME]: memo_username = message[definitions.Messages.USERNAME] all_messages.append(temp_list[:]) temp_list.clear() # if init_set == True: # init_set = False temp_list.append(message) if temp_list: all_messages.append(temp_list) logger.debug("Messages: {}", str(messages)) logger.debug("All Messages: {}", str(all_messages)) # Send data to requesting client (render) params = { "body": { "left": current_user_left, "nickname": nickname, "group_users": current_group_users, "username": username, "all_messages": all_messages, "groupID": group } } lambda_client = boto3.client("lambda") invoke_response = lambda_client.invoke( FunctionName="chat-application-dev-activePaneRender", InvocationType="RequestResponse", Payload=json.dumps(params)) payload = invoke_response['Payload'].read().decode() logger.debug("Payload: '{}' type: {}", payload, type(payload)) payload_dict = json.loads(payload) logger.debug("Payload_dict: '{}' type: {}", payload_dict, type(payload_dict)) body = {"group": group, "html": payload_dict["body"]} _send_to_connection(connectionID, _build_response_detailed(200, action, body), event) return _build_response(200, "Sent {} messages".format(len(messages)))
def create_group(event, context): """ Endpoint to create a chat group """ logger.info("Group creation via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate that user is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key(definitions.Connections.CONNECTION_ID).eq(connectionID) ) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: logger.error("user password query returned not even an empty set items: {}", items) _send_to_connection(connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection(connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") username = item[definitions.Connections.USERNAME] # Check for required components body = _fetch_body(event, logger) for attribute in [definitions.Groups.NICKNAME, "users"]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format(attribute) logger.debug(error_message) return _build_response(400, error_message) nickname = body[definitions.Groups.NICKNAME] users = json.loads(body["users"]) logger.debug("nickname: '{}', username: '******', users: '{}', users_type: {}", nickname, username, users, type(users)) # Validate that all users exist users_table = dynamodb.Table(definitions.Users.TABLE_NAME) invalid_users = [] online_users = [] for user in users: if user == username: invalid_users.append(user) continue response = users_table.query( ProjectionExpression="{}, {}".format( definitions.Users.USERNAME, definitions.Users.CONNECTION_ID ), KeyConditionExpression=boto3.dynamodb.conditions.Key(definitions.Users.USERNAME).eq(user) ) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: invalid_users.append(user) # get all the connection IDs for online users if definitions.Users.CONNECTION_ID in item: online_users.extend(item[definitions.Users.CONNECTION_ID]) if definitions.Users.USERNAME not in item: invalid_users.append(user) logger.debug("Invalid users: {}", invalid_users) if len(invalid_users) > 0: _send_to_connection(connectionID, _build_response_detailed(400, action, invalid_users), event) return _build_response(400, "Invalid users {}".format(invalid_users)) groups_table = dynamodb.Table(definitions.Groups.TABLE_NAME) group_id = secrets.token_urlsafe(16) timestamp = time.time_ns() logger.debug("Generated group_id: {} Timestamp: {}", group_id, timestamp) logger.debug("Users before if: {}", users) if len(users) > 0: while True: try: groups_table.put_item( Item={ definitions.Groups.GROUP_ID: group_id, definitions.Groups.USERNAME: username, definitions.Groups.JOIN_TIMESTAMP: timestamp, definitions.Groups.NICKNAME: nickname, # TODO Check if this does anything or if we need it definitions.Groups.EXCLUSION_LIST: list() }, ConditionExpression='attribute_not_exists({}) AND attribute_not_exists({})'.format( definitions.Groups.GROUP_ID, definitions.Groups.USERNAME ) ) break except botocore.exceptions.ClientError as cle: # ConditionalCheckFailedException is okay, rest are not logger.debug("Exception raised") if cle.response['Error']['Code'] != 'ConditionalCheckFailedException': error_message = "Unexpected exception: {}".format(cle) logger.debug(error_message) _send_to_connection(connectionID, _build_response_detailed(500, action, "Server Error"), event) return _build_response(500, error_message) group_id = secrets.token_urlsafe(16) logger.debug("Regenerated group_id: {}", group_id) continue logger.debug("Users before iteration: {}", users) for user in users: try: groups_table.put_item( Item={ definitions.Groups.GROUP_ID: group_id, definitions.Groups.USERNAME: user, definitions.Groups.JOIN_TIMESTAMP: timestamp, definitions.Groups.NICKNAME: nickname, # TODO: Check if this does anything / if we need it definitions.Groups.EXCLUSION_LIST: list() }, ConditionExpression='attribute_not_exists({}) AND attribute_not_exists({})'.format( definitions.Groups.GROUP_ID, definitions.Groups.USERNAME ) ) except botocore.exceptions.ClientError as cle: # ConditionalCheckFailedException is okay, rest are not logger.debug(cle) logger.debug("Exception raised - there should not be an issue adding users") _send_to_connection(connectionID, _build_response_detailed(500, action, "Server Error"), event) return _build_response(500, "Error adding additional users") # if e.response['Error']['Code'] != 'ConditionalCheckFailedException': # raise # else: # group_id = secrets.token_urlsafe(16) # continue # alert everyone a group has been made? send the init memo to all groups logger.debug("group created") message = "Group created" else: logger.debug("Self group attempted") message = "Cannot make a self group" # Init memo messages_table = dynamodb.Table(definitions.Messages.TABLE_NAME) try: message = { definitions.Messages.USERNAME: None, definitions.Messages.GROUP_ID: group_id, definitions.Messages.TIMESTAMP: timestamp, definitions.Messages.CONTENT: "Group created at: {}".format(datetime.fromtimestamp(timestamp // 1000000000).strftime('%m/%d/%Y %H:%M')), definitions.Groups.NICKNAME: nickname # definitions.Messages.INIT: True } messages_table.put_item( Item=message, ConditionExpression='attribute_not_exists({}) AND attribute_not_exists({})'.format( definitions.Messages.GROUP_ID, definitions.Messages.TIMESTAMP ) ) except botocore.exceptions.ClientError: # Should be no errors as this is the first message to be inserted logger.debug("Exception raised - error inserting init message") _send_to_connection(connectionID, _build_response_detailed(500, action, "Server Error"), event) return _build_response(500, "Error inserting init message") # Send init message to all clients lambda_client = boto3.client("lambda") message[definitions.Messages.TIMESTAMP] = datetime.fromtimestamp( message[definitions.Messages.TIMESTAMP] // 1000000000 ).strftime('%m/%d/%Y %H:%M') params = { "body": { "all_messages": [message] } } invoke_response = lambda_client.invoke( FunctionName="chat-application-dev-sideMessageRender", InvocationType="RequestResponse", Payload=json.dumps(params) ) logger.debug("invoke_response: {}", invoke_response) payload = invoke_response['Payload'].read().decode() logger.debug("Payload: '{}' type: {}", payload, type(payload)) payload_dict = json.loads(payload) side_message_html = payload_dict['body'] logger.debug("online_users: {}", online_users) for online_user in online_users: _send_to_connection(online_user, _build_response_detailed(200, "init", side_message_html), event) # Success message to group creater _send_to_connection(connectionID, _build_response_detailed(200, action, "Group Created"), event) return _build_response(200, message)
def create_user(event, context): """ Endpoint to create a chat user """ logger.info("User creation via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Check for required components body = _fetch_body(event, logger) for attribute in [definitions.Users.USERNAME, definitions.Users.PASSWORD]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format( attribute) logger.debug(error_message) _send_to_connection( connectionID, _build_response_detailed(400, action, error_message), event) return _build_response(400, error_message) username = body[definitions.Users.USERNAME] if username == "System": _send_to_connection( connectionID, _build_response_detailed(401, action, "Forbidden Username"), event) return _build_response(401, "Forbidden Username") password = body[definitions.Users.PASSWORD] logger.debug("username: '******', password: '******'", username, password) users_table = dynamodb.Table(definitions.Users.TABLE_NAME) try: users_table.put_item( Item={ definitions.Users.USERNAME: username, definitions.Users.PASSWORD: password, definitions.Users.CONNECTION_ID: set([connectionID]) }, ConditionExpression='attribute_not_exists({})'.format( definitions.Users.USERNAME)) except botocore.exceptions.ClientError as cle: # ConditionalCheckFailedException is okay, rest are not logger.debug("Exception raised: {}", str(cle)) if cle.response['Error']['Code'] != 'ConditionalCheckFailedException': # TODO: Handle gracefully with a 500 response raise _send_to_connection( connectionID, _build_response_detailed(401, action, "Invalid username/password"), event) return _build_response(401, "Username not unique") # Updates the connections Table to have the username added local_var = ":user" connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) result = connections_table.update_item( Key={definitions.Connections.CONNECTION_ID: connectionID}, UpdateExpression="set {} = {}".format(definitions.Connections.USERNAME, local_var), ExpressionAttributeValues={local_var: username}, ReturnValues="UPDATED_NEW") if result['ResponseMetadata'][ 'HTTPStatusCode'] == 200 and 'Attributes' in result: logger.debug(result['Attributes'][definitions.Connections.USERNAME]) else: logger.error("Connections table not properly updated - result: {}", result) _send_to_connection( connectionID, _build_response_detailed(500, action, "Internal server error"), event) return _build_response(500, "Internal server error") # TODO: Send blank home screen to client _send_to_connection(connectionID, _build_response_detailed(200, action, "Login Success"), event) return _build_response(200, "User {} created".format(username))
def login(event, context): """ Handles a login attempt via WebSocket """ logger.info("Login request via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validates username and password fields are present and fetches them body = _fetch_body(event, logger) for attribute in [definitions.Users.USERNAME, definitions.Users.PASSWORD]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format( attribute) logger.debug(error_message) _send_to_connection( connectionID, _build_response_detailed(400, action, error_message), event) return _build_response(400, error_message) username = body[definitions.Users.USERNAME] password = body[definitions.Users.PASSWORD] logger.debug("username: '******', password: '******'", username, password) # Queries for the password to the username from the Table users_table = dynamodb.Table(definitions.Users.TABLE_NAME) response = users_table.query( ProjectionExpression=definitions.Users.PASSWORD, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Users.USERNAME).eq(username)) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: # Case: User does not exist logger.debug("No user with name: {}", username) _send_to_connection( connectionID, _build_response_detailed(401, action, "Invalid username/password"), event) return _build_response(401, "Invalid username/password") # Validates username/password if definitions.Users.PASSWORD in item: correct_password = item[definitions.Users.PASSWORD] if (password != correct_password): _send_to_connection( connectionID, _build_response_detailed(401, action, "Invalid username/password"), event) return _build_response(401, "Invalid username/password") else: # Case: User record without a password field _send_to_connection( connectionID, _build_response_detailed(500, action, "Invalid user record"), event) return _build_response(401, "Invalid user record") # Updates the user to have this connectionID appended local_var = ":connID" result = users_table.update_item( Key={definitions.Users.USERNAME: username}, UpdateExpression="add {} {}".format(definitions.Users.CONNECTION_ID, local_var), ExpressionAttributeValues={local_var: set([connectionID])}, ReturnValues="UPDATED_NEW") if result['ResponseMetadata'][ 'HTTPStatusCode'] == 200 and 'Attributes' in result: logger.debug(result['Attributes'][definitions.Users.CONNECTION_ID]) else: logger.error("User table not properly updated - result: {}", result) _send_to_connection( connectionID, _build_response_detailed(500, action, "Internal server error"), event) return _build_response(500, "Internal server error") # Updates the connections Table to have the username added local_var = ":user" connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) result = connections_table.update_item( Key={definitions.Connections.CONNECTION_ID: connectionID}, UpdateExpression="set {} = {}".format(definitions.Connections.USERNAME, local_var), ExpressionAttributeValues={local_var: username}, ReturnValues="UPDATED_NEW") if result['ResponseMetadata'][ 'HTTPStatusCode'] == 200 and 'Attributes' in result: logger.debug(result['Attributes'][definitions.Connections.USERNAME]) else: logger.error("Connections table not properly updated - result: {}", result) _send_to_connection( connectionID, _build_response_detailed(500, action, "Internal server error"), event) return _build_response(500, "Internal server error") # obj = s3.Object('chat-application-upload-bucket-11097', 'chat_template') _send_to_connection(connectionID, _build_response_detailed(200, action, "Login Success"), event) return _build_response(200, "Login successful")
def send_message(event, context): """ Forward a message to all appropraite clients """ logger.debug("environ var: {} type: {}", os.environ.get('IS_OFFLINE'), type(os.environ.get('IS_OFFLINE'))) logger.info("Message sent via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate that user is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Connections.CONNECTION_ID).eq(connectionID)) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: logger.error( "user password query returned not even an empty set items: {}", items) _send_to_connection( connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection( connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") username = item[definitions.Connections.USERNAME] # Check for required components body = _fetch_body(event, logger) for attribute in [ definitions.Messages.CONTENT, definitions.Messages.GROUP_ID ]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format( attribute) logger.debug(error_message) return _build_response(400, error_message) content = body[definitions.Messages.CONTENT] group = body[definitions.Messages.GROUP_ID] logger.debug("username: '******', content: '{}', group: '{}'", username, content, group) # Validate that user is in group before sending message groups_table = dynamodb.Table(definitions.Groups.TABLE_NAME) response = groups_table.query( ProjectionExpression="{}, {}".format(definitions.Groups.USERNAME, definitions.Groups.NICKNAME), KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Groups.GROUP_ID).eq(group)) logger.debug("response: {}", response) users = response.get("Items", []) # logger.debug("items: {}".format(items)) # users = items # [item[definitions.Groups.USERNAME] for item in items if definitions.Groups.USERNAME in item] logger.debug("users: {}", users) ok = False nickname = None for user in users: if definitions.Groups.USERNAME in user: if user[definitions.Groups.USERNAME] == username: ok = True nickname = user[definitions.Groups.NICKNAME] break else: logger.error("Invalid record {}", user) _send_to_connection( connectionID, _build_response_detailed(500, action, "Internal server error"), event) return _build_response(500, "Internal Server Error") if not ok: logger.debug("User: {} not a member of {} users: {}", username, group, users) _send_to_connection( connectionID, _build_response_detailed(403, action, "user not a member of this group"), event) return _build_response(403, "user not member of group") messages_table = dynamodb.Table(definitions.Messages.TABLE_NAME) while True: timestamp = time.time_ns() logger.debug("Timestamp: {}", timestamp) try: messages_table.put_item( Item={ definitions.Messages.GROUP_ID: group, definitions.Messages.TIMESTAMP: timestamp, definitions.Messages.USERNAME: username, definitions.Messages.CONTENT: content # definitions.Messages.GROUP_NAME: nickname, # definitions.Messages.INIT: False }, ConditionExpression= 'attribute_not_exists({}) AND attribute_not_exists({})'.format( definitions.Messages.GROUP_ID, definitions.Messages.TIMESTAMP)) break except botocore.exceptions.ClientError as cle: # ConditionalCheckFailedException is okay (collision with timestamp), rest are not logger.debug("Exception raised") if cle.response['Error'][ 'Code'] != 'ConditionalCheckFailedException': # TODO: Handle gracefully with a 500 response raise continue # Query all the users for their connectionIDs users_table = dynamodb.Table(definitions.Users.TABLE_NAME) user_connIDs = {} for user in users: response = users_table.query( ProjectionExpression=definitions.Users.CONNECTION_ID, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Users.USERNAME).eq( user[definitions.Users.USERNAME])) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: logger.error( "user connection query returned not even an empty set items: {}", items) return _build_response(500, "Server error") if definitions.Users.CONNECTION_ID in item: conn_ID = item[definitions.Users.CONNECTION_ID] if conn_ID is not None: user_connIDs[user[definitions.Users.USERNAME]] = conn_ID logger.debug("user_connIDs: {}", user_connIDs) lambda_client = boto3.client("lambda") # Cycle through connectionID's sending out memo # TODO: send out to yourself first to lower latency for current user message = { definitions.Messages.USERNAME: username, definitions.Messages.GROUP_ID: group, definitions.Messages.TIMESTAMP: datetime.fromtimestamp(timestamp // 1000000000).strftime('%m/%d/%Y %H:%M'), definitions.Messages.CONTENT: content, definitions.Groups.NICKNAME: nickname # definitions.Messages.INIT: False } for user in user_connIDs: params = {"body": {"username": user, "all_messages": [[message]]}} invoke_response = lambda_client.invoke( FunctionName="chat-application-dev-fullActiveMessageRender", InvocationType="RequestResponse", Payload=json.dumps(params)) logger.debug("invoke_response: {}", invoke_response) payload = invoke_response['Payload'].read().decode() logger.debug("Payload: '{}' type: {}", payload, type(payload)) payload_dict = json.loads(payload) active_message_html = payload_dict['body'] logger.debug("Payload_dict: '{}' type: {}", payload_dict, type(payload_dict)) params = {"body": {"all_messages": [message]}} invoke_response = lambda_client.invoke( FunctionName="chat-application-dev-sideMessageRender", InvocationType="RequestResponse", Payload=json.dumps(params)) logger.debug("invoke_response: {}", invoke_response) payload = invoke_response['Payload'].read().decode() logger.debug("Payload: '{}' type: {}", payload, type(payload)) payload_dict = json.loads(payload) side_message_html = payload_dict['body'] logger.debug("item: {} body: {}", item, side_message_html) data = { "action": action, "statusCode": 200, "body": { "group": group, "sideMessage": side_message_html, "activeMessage": active_message_html } } logger.debug("Data: {}", data) for user_individual_conn_ID in user_connIDs[user]: _send_to_connection(user_individual_conn_ID, data, event) return _build_response(200, "Message distributed to all connections")
def leave_group(event, context): """ Leave a selected group """ logger.info("Leaving a group") connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate the connection is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key(definitions.Connections.CONNECTION_ID).eq(connectionID) ) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) >= 0: item = items[0] else: logger.error("user password query returned not even an empty set items: {}", items) _send_to_connection(connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection(connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") username = item[definitions.Connections.USERNAME] # Validate passed parameters body = _fetch_body(event, logger) for attribute in [definitions.Groups.GROUP_ID]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format(attribute) logger.debug(error_message) _send_to_connection(connectionID, _build_response_detailed(400, action, error_message), event) return _build_response(400, error_message) group = body[definitions.Groups.GROUP_ID] logger.debug("group_id: {}", group) # Validate user is a member of the group local_name = ":exclusion" #local_name_2 = ":leaveTimestmp" timestamp = time.time_ns() logger.debug("Leaving timestamp: {}", timestamp) groups_table = dynamodb.Table(definitions.Groups.TABLE_NAME) try: response = groups_table.update_item( Key={ definitions.Groups.GROUP_ID: group, definitions.Groups.USERNAME: username }, # TODO: List append for the exclusion list UpdateExpression="set {} = list_append({}, {})".format( definitions.Groups.EXCLUSION_LIST, definitions.Groups.EXCLUSION_LIST, local_name ), ConditionExpression='attribute_exists({})'.format( definitions.Groups.GROUP_ID ), ExpressionAttributeValues={ local_name: [timestamp] }, ReturnValues="UPDATED_NEW" # ProjectionExpression="{}, {}".format(definitions.Groups.USERNAME, definitions.Groups.NICKNAME), # KeyConditionExpression=boto3.dynamodb.conditions.Key(definitions.Groups.GROUP_ID).eq(group) ) logger.debug("response: {}", response) except botocore.exceptions.ClientError as cle: # ConditionalCheckFailedException is okay, rest are not logger.debug("Exception raised") if cle.response['Error']['Code'] != 'ConditionalCheckFailedException': logger.debug("Unexpected exception: {}", cle) _send_to_connection(connectionID, _build_response_detailed(500, action, "Internal Server Error"), event) return _build_response(500, "Unexpected exception") logger.debug("User: {} not a member of {}", username, group) _send_to_connection(connectionID, _build_response_detailed(403, action, "user not a member of this group"), event) return _build_response(403, "user not member of group") _send_to_connection(connectionID, _build_response_detailed(200, action, "Successfully left group"), event) return _build_response(200, "Successfully left group")
def fetch_groups(event, context): """ Fetch groups for side bar endpoint """ logger.info("fetch groups request via WebSocket") logger.debug("event: {}", str(event)) connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate that user is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Connections.CONNECTION_ID).eq(connectionID)) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) > 0: item = items[0] else: logger.error( "user password query returned not even an empty set items: {}", items) _send_to_connection( connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection( connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") username = item[definitions.Connections.USERNAME] groups_table = dynamodb.Table(definitions.Groups.TABLE_NAME) response = groups_table.scan( ProjectionExpression="{}, {}".format(definitions.Groups.GROUP_ID, definitions.Groups.NICKNAME), FilterExpression=boto3.dynamodb.conditions.Attr( definitions.Groups.USERNAME).eq(username)) items = response.get("Items", []) logger.debug("items: {}", str(items)) # How many groups there are item_len = len(items) logger.debug("groups: {}", item_len) last_messages = [] # Query Last Message for each for group in items: local_name = ":grp" messages_table = dynamodb.Table(definitions.Messages.TABLE_NAME) # TODO: Need to do selective query here depending on if user has left the group or not response = messages_table.query( KeyConditionExpression="{} = {}".format( definitions.Messages.HASH_KEY, local_name), ExpressionAttributeValues={ local_name: group[definitions.Groups.GROUP_ID] }, Limit=1, ScanIndexForward=False) items = response.get("Items", []) logger.debug("items: {}", str(items)) message = items[0] logger.debug("message: {}", str(message)) last_messages.append({ definitions.Messages.USERNAME: message[definitions.Messages.USERNAME] if definitions.Messages.USERNAME in message else None, definitions.Messages.GROUP_ID: group[definitions.Groups.GROUP_ID], definitions.Messages.TIMESTAMP: datetime.fromtimestamp(message[definitions.Messages.TIMESTAMP] // 1000000000).strftime('%m/%d/%Y %H:%M'), definitions.Messages.CONTENT: message[definitions.Messages.CONTENT], definitions.Groups.NICKNAME: group[definitions.Groups.NICKNAME], # definitions.Messages.INIT: message[definitions.Messages.INIT] if definitions.Messages.INIT in message else False }) logger.debug("last_messages before sort: {}", last_messages) last_messages.sort(key=lambda item: item[definitions.Messages.TIMESTAMP], reverse=True) logger.debug("last_messages after sort: {}", last_messages) # Render messages params = {"body": {"all_messages": last_messages}} lambda_client = boto3.client("lambda") invoke_response = lambda_client.invoke( FunctionName="chat-application-dev-fetchChatRender", InvocationType="RequestResponse", Payload=json.dumps(params)) payload = invoke_response['Payload'].read().decode() logger.debug("Payload: '{}' type: {}", payload, type(payload)) payload_dict = json.loads(payload) logger.debug("Payload_dict: '{}' type: {}", payload_dict, type(payload_dict)) _send_to_connection( connectionID, _build_response_detailed(200, action, payload_dict["body"]), event) return _build_response(200, "Groups fetched")
def get_recent_messages(event, context): """ Return the recent N most messages specified by client """ logger.info("Retrieving the N most recent messages") connectionID = event["requestContext"].get("connectionId") logger.debug("connectionID: {}", connectionID) # Validate the connection is logged in connections_table = dynamodb.Table(definitions.Connections.TABLE_NAME) response = connections_table.query( ProjectionExpression=definitions.Connections.USERNAME, KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Connections.CONNECTION_ID).eq(connectionID)) logger.debug("response: {}", response) items = response.get("Items", []) logger.debug("items: {}", items) if len(items) >= 0: item = items[0] else: logger.error( "user password query returned not even an empty set items: {}", items) _send_to_connection( connectionID, _build_response_detailed(500, action, "Server error"), event) return _build_response(500, "Server error") if definitions.Connections.USERNAME not in item: _send_to_connection( connectionID, _build_response_detailed(401, action, "Not logged in"), event) return _build_response(401, "Not logged in") username = item[definitions.Connections.USERNAME] default_fetch = 10 # Default number of messages to fetch body = _fetch_body(event, logger) for attribute in [definitions.Messages.GROUP_ID, "count"]: if attribute not in body: error_message = "Improper message format: `{}' missing from message JSON".format( attribute) logger.debug(error_message) return _build_response(400, error_message) group = body[definitions.Messages.GROUP_ID] count = body["count"] # Validate the user is a member of the group groups_table = dynamodb.Table(definitions.Groups.TABLE_NAME) response = groups_table.query( # ProjectionExpression="{}, {}".format(definitions.Groups.USERNAME, definitions.Groups.NICKNAME), KeyConditionExpression=boto3.dynamodb.conditions.Key( definitions.Groups.GROUP_ID).eq(group)) items = response.get("Items", []) logger.debug("items: {}", str(items)) ok = False # current_user_exclusion_list = [] # current_user_left_timestamp = -1 for item in items: if item[definitions.Groups.USERNAME] == username: ok = True current_user_exclusion_list = item[definitions.Groups.EXCLUSION_LIST] if definitions.Groups.EXCLUSION_LIST in item else [] current_user_left = bool(current_user_exclusion_list) and len( current_user_exclusion_list[-1]) == 1 # Note if the current user has left the group # if definitions.Groups.LEFT in item and item[definitions.Groups.LEFT] == True: # current_user_left = True # current_user_left_timestamp = item[definitions.Groups.LEFT_TIMESTAMP] break if not ok: _send_to_connection( connectionID, _build_response_detailed(403, action, "Not a member of group"), event) return _build_response(403, "Not a member of group") logger.debug("current_user_left: {} current_user_exclusion_list: {} ", current_user_left, current_user_exclusion_list) logger.debug("group: {} count: {} count_type: {}", group, count, type(count)) count = int(count) message_number = count + default_fetch messages_table = dynamodb.Table(definitions.Messages.TABLE_NAME) local_name_1 = ":group_id" local_name_2_template = ":timestmp{}_{}" # local_name_2 = ":timestmp" if current_user_left: expression_attribute_values = { definitions.Messages.HASH_KEY: local_name_1 } key_condition_expression = [[ "{} = {} and (".format(definitions.Messages.HASH_KEY, local_name_1) ]] for i, exclusion_item in enumerate(current_user_exclusion_list): exclusion_pair = [] temp_local_name_2 = local_name_2_template.format(i, 0) message = "{} <= {}".format(definitions.Messages.RANGE_KEY, temp_local_name_2) expression_attribute_values[temp_local_name_2] = exclusion_item[0] if len(exclusion_item) > 1: exclusion_pair.append(message) temp_local_name_2 = local_name_2_template.format(i, 1) exclusion_pair.append("{} >= {}".format( definitions.Messages.RANGE_KEY, temp_local_name_2)) expression_attribute_values[ temp_local_name_2] = exclusion_item[1] else: message += ")" exclusion_pair.append(message) logger.debug("KeyConditionExpression before join: {}", key_condition_expression) key_condition_expression = " and ".join( " or ".join(item) for item in key_condition_expression) logger.debug("KeyConditionExpression after join: {}", key_condition_expression) logger.debug("ExpressionAttributeValues: {}", expression_attribute_values) response = messages_table.query( KeyConditionExpression=key_condition_expression, ExpressionAttributeValues=expression_attribute_values, Limit=message_number, ScanIndexForward=False) # response = messages_table.scan( # FilterExpression="{} = {} AND {} <= {}".format( # definitions.Messages.HASH_KEY, # local_name_1, # definitions.Messages.TIMESTAMP, # local_name_2 # ), # ExpressionAttributeValues={ # local_name_1: group, # local_name_2: current_user_left_timestamp # }, # # Limit=message_number, # ScanIndexForward=False # ) # items = response.get("Items", []) # logger.debug("items: {}".format(str(items))) # if len(items) > message_number: # items = items[-message_number:] # logger.debug("items: {}".format(str(items))) else: response = messages_table.query( KeyConditionExpression="{} = {}".format( definitions.Messages.HASH_KEY, local_name_1), ExpressionAttributeValues={local_name_1: group}, Limit=message_number, ScanIndexForward=False) items = response.get("Items", []) logger.debug("items: {}", str(items)) # How many new messages were actually taken item_len = len(items) new_messages_count = item_len - count logger.debug("item_len: {} new_messages_count: {}", item_len, new_messages_count) messages = [] if (new_messages_count > 0): # Only get the newest messages # TODO Check if these are the newest messages? ScanIndexForward is reverse so these may be the oldest? items = items[:new_messages_count] logger.debug("New messages from items: {}", items) messages = [ { definitions.Messages.USERNAME: item[definitions.Messages.USERNAME] if definitions.Messages.USERNAME in item else None, definitions.Messages.CONTENT: item[definitions.Messages.CONTENT], definitions.Messages.TIMESTAMP: datetime.fromtimestamp(item[definitions.Messages.TIMESTAMP] // 1000000000).strftime('%m/%d/%Y %H:%M'), # definitions.Messages.INIT: x[definitions.Messages.INIT] if definitions.Messages.INIT in x else False } for item in items ] messages.reverse() memo_username = None all_messages = [] temp_list = [] # init_set = False for message in messages: if memo_username is None: memo_username = message[definitions.Messages.USERNAME] if temp_list: all_messages.append(temp_list[:]) temp_list.clear() elif memo_username != message[definitions.Messages.USERNAME]: memo_username = message[definitions.Messages.USERNAME] all_messages.append(temp_list[:]) temp_list.clear() # if init_set == True: # init_set = False temp_list.append(message) if temp_list: all_messages.append(temp_list) else: logger.debug("No further new messages") return _build_response(200, "No further new messages") logger.debug("Messages: {}", str(messages)) logger.debug("All Messages: {}", str(all_messages)) # Send data to requesting client (render) params = {"body": {"username": username, "all_messages": all_messages}} lambda_client = boto3.client("lambda") invoke_response = lambda_client.invoke( FunctionName="chat-application-dev-fullActiveMessageRender", InvocationType="RequestResponse", Payload=json.dumps(params)) payload = invoke_response['Payload'].read().decode() logger.debug("Payload: '{}' type: {}", payload, type(payload)) payload_dict = json.loads(payload) logger.debug("Payload_dict: '{}' type: {}", payload_dict, type(payload_dict)) # body = { # "group": group, # "html": payload_dict["body"] # } _send_to_connection( connectionID, _build_response_detailed(200, action, payload_dict["body"]), event) return _build_response( 200, "Sent {}-{} recent block of messages".format(item_len, count))