def users_profile_setemail(token, email): """ Update the authorised user's email address Parameters: token (str) email (str) Returns: {} """ verify_token(token) user = get_user_from_token(token) if not user: raise InputError(description="Target user doesn't exist") # If the email is unchanged, do nothing if user.email == email: return # The supplied email must pass the email regex format if not email_is_legit(email): raise InputError(description="Email entered is not a valid email") # Email mustn't be in use by any existing user existing_user = User.query.filter_by(email=email).first() if existing_user: raise InputError(description="{} is being used by another user".format(email)) user.email = email db.session.commit()
def users_profile(token, user_id): """ For a valid user, returns some of their basic fields Parameters: token (str) user_id (int) Returns: { user_id, email, username, profile_img_url, is_connected_to, connection_is_pending } """ verify_token(token) calling_user = get_user_from_token(token) target_user = User.query.filter_by(id=user_id).first() if not target_user: raise InputError(description="user_id does not refer to any user in the database") connected = False pending = False connection = Connection.query.filter_by(user_id=calling_user.id, other_user_id=target_user.id).first() if connection: if connection.approved: connected = True else: pending = True return { "user_id": target_user.id, "email": target_user.email, "username": target_user.username, "profile_img_url": target_user.bio.profile_img_url, "is_connected_to": connected, "connection_is_pending": pending }
def handle_connection_fetch(): """ Fetches all of the users that the calling user is connected with Params: (token) Returns JSON: { users: [ { user_id, email, username, profile_img_url, summary, first_name, last_name, location, education, ... other bio fields }, ... ] } """ token = request.args.get("token") calling_user = get_user_from_token(token) printColour(" ➤ Connections: {} is fetching a list of their existing connections".format({ calling_user.username }), colour="blue") return jsonify(connections.connection_fetch_users(token))
def channels_join(token, channel_id): """ Given a channel_id of a channel that the authorised user can join, adds them to that channel Parameters: token (str) channel_id (int) Returns: {} """ verify_token(token) user = get_user_from_token(token) if not user: raise AccessError(description="Invalid Token") selected_channel = select_channel(channel_id) if not selected_channel: raise InputError(description="Target channel is not a valid channel") # Check whether channel is private or not. Raise AccessError if it is if not selected_channel.visibility == "public": raise AccessError(description="Target channel isn't public") new_membership = MemberOf( user=user, channel=selected_channel, is_owner=False ) db.session.add(new_membership) db.session.commit() return {}
def message_send(token, channel_id, message): """ Sends a message to the selected channel. Parameters: token (str) channel_id (int) message (str) Returns: { message_id } """ verify_token(token) if not message: raise InputError("Message can't be empty") if len(message) > 1000: raise InputError("Message is over 1000 characters") user = get_user_from_token(token) selected_channel = select_channel(channel_id) # Raise error if the user is not a part of the channel they are trying to message in if not is_user_member(user, selected_channel): raise AccessError( description="User is not a member of channel with channel_id") sent_message = Message(channel=selected_channel, user=user, message=message) db.session.add(sent_message) db.session.commit() return {"message_id": sent_message.id}
def channels_leave(token, channel_id): """ Removes the caller from the specified channel. Parameters: token (str) channel_id (str) Returns: {} """ verify_token(token) user = get_user_from_token(token) if not user: raise AccessError(description="Invalid Token") channels_list = Channel.query.all() selected_channel = select_channel(channel_id) # Check if Channel ID is not a valid channel if not selected_channel: raise InputError(description="Channel ID is not a valid channel") # Check user if the user is not currently a member of the selected channel if not is_user_member(user, selected_channel): raise AccessError(description="You are not a member of this channel") membership = MemberOf.query.filter_by(user_id=user.id, channel_id=selected_channel.id).first() db.session.delete(membership) db.session.commit() # If the user attempting to leave is the owner... Pass down ownership to someone else? Or delete channel # TODO: # If there is no members left in channel, delete channel # TODO: return {}
def channels_messages(token, channel_id, start, limit=50): """ Given a channel that the user is a member of, returns up to a specified maximum number of messages from a starting index. Messages are ordered in ascending recency, so the message at index 0 is the most recent. Parameters: token (str) channel_id (int) start (int) limit (int) Returns: { messages, exhausted } Where: messages: list of message dictionary: { message_id, user_id, message, time_created, is_author } exhausted: bool indicating whether the there any more messages left to fetch """ # check parameters are all valid and raise exception if they aren't # add user_id, associated first name and last name into channel_id dictionary (or storage) verify_token(token) user = get_user_from_token(token) if not user: raise AccessError(description="Invalid Token") selected_channel = select_channel(channel_id) if not selected_channel: raise InputError(description="Channel ID is not a valid channel") if is_user_member(user, selected_channel) is False: raise AccessError(description="You are not a member of this channel") # Loop through 50 message dictionaries of list starting from start index messsages_list = selected_channel.messages_sent messsages_list.sort(key=(lambda x: x.time_created)) payload = { "messages": [], "exhausted": True } if not messsages_list: # printColour("Results: {}".format(payload), colour="blue") return payload if start >= len(messsages_list): raise InputError(description="Starting index is greater than or equal to the total number of messages in the channel") if not (start + limit >= len(messsages_list)): messages["exhausted"] = False for i, message in enumerate(messsages_list[start :], start=1): payload["messages"].append({ "message_id": message.id, "user_id": message.user_id, "message": message.message, "time_created": message.time_created.timestamp(), "is_author": message.user_id == user.id }) if i == limit: break # printColour("Results: {}".format(payload), colour="blue") return payload
def channels_removeowner(token, channel_id, user_id): """ Desc: Remove user with user id user_id an owner of this channel Params: (token, channel_id, user_id) Returns: empty dict Errors: InputError on invalid channel_id InputError when user_id DOESN'T already have ownership over a channel before calling channel_removeowner AccessError when the user isn't an owner of the slackr or an owner of this channel TYPES: token str channel_id int user_id int """ verify_token(token) selected_channel = select_channel(channel_id) if not selected_channel: raise InputError(description="Not a valid channel") calling_user = get_user_from_token(token) details = channels_details(token, channel_id) # Check whether the target user is actually in the list of members members_list = details["all_members"] user_found = False for member in members_list: if member["user_id"] == user_id: user_found = True break if not user_found: raise InputError("Target user must be a part of this channel") # Check whether user_id is not already an owner in channel owners_list = details["owner_members"] calling_user_is_owner = False target_user_is_owner = False for owner in owners_list: if owner["user_id"] == user_id: target_user_is_owner = True if owner["user_id"] == calling_user.id: calling_user_is_owner = True if not calling_user_is_owner: raise InputError("You are not authorised to remove existing owners") if not target_user_is_owner: raise InputError("Target user is not an owner of the channel") target_user = User.query.filter_by(id=user_id).first() for each_membership in target_user.channel_membership: if each_membership.channel_id == channel_id: each_membership.is_owner = False db.session.commit() return {}
def channels_details(token, channel_id): """ Given a Channel with ID channel_id that the authorised user is part of, provide basic details about the channel Parameters: token (str) channel_id (channel_id) Returns: { name, description, visibility, channel_img_url, owner_members, all_members } (dict) Where: owner_members: [{ user_id, username, email, profile_img_url }, ...] (list of user objects) all_members: [{ user_id, username, email, profile_img_url }, ...] (list of user objects) """ verify_token(token) user = get_user_from_token(token) if not user: raise AccessError(description="Invalid Token") selected_channel = select_channel(channel_id) if not selected_channel: raise InputError(description=f"{channel_id} doesn't point to a valid channel") # Raise exception when the user is not a member of the channel with the given channel_id if not is_user_member(user, selected_channel): raise AccessError(description="You are not a member of this channel") channel_owners = [] channel_members = [] # Joining User with MemberOf, then filtering for users associated with the selected channel # TODO: Possible optimisation -> swap the ordering of the filtering memberships = db.session.query(User, MemberOf, Channel).outerjoin(MemberOf, MemberOf.user_id==User.id).outerjoin(Channel, Channel.id==MemberOf.channel_id).filter_by(id=channel_id).all() for each_membership in memberships: curr_member = each_membership[0] member_data = { "user_id": curr_member.id, "username": curr_member.username, "profile_img_url": curr_member.bio.profile_img_url, "email": curr_member.email } channel_members.append(member_data) if each_membership[1].is_owner: channel_owners.append(member_data) details = { "name": selected_channel.name, "description": selected_channel.description, "visibility": selected_channel.visibility, "channel_img_url": selected_channel.channel_img_url, "channel_cover_img_url": selected_channel.channel_cover_img_url, "owner_members": channel_owners, "all_members": channel_members } # printColour("Results: {}".format(details), colour="blue") return details
def handle_connection_outgoing(): """ Fetches all of the users that the calling user has sent requests to Params: (token) Returns JSON: { users: [ { user_id, email, username, profile_img_url }, ... ] } """ token = request.args.get("token") calling_user = get_user_from_token(token) printColour(" ➤ Connections: {} is fetching a list of their outgoing connections".format( calling_user.username ), colour="blue") return jsonify(connections.connection_fetch_outgoing_users(token))
def handle_channels_listall(): """ HTTP Route: /channels/listall HTTP Method: GET Params: (token) Returns JSON: { channels: [{ channel_id, name, description, visibility, member_of, owner_of }, ...] } """ token = request.args.get("token") calling_user = get_user_from_token(token) printColour( " ➤ Channel List All: {} fetched a list of channels they belong to". format(calling_user.username), colour="blue") return jsonify(channels.channels_listall(token))
def users_profile_set_username(token, username): """ Update the authorised user's first and last name Parameters: token (str) username (str) """ verify_token(token) user = get_user_from_token(token) if not user: raise InputError(description="Target user doesn't exist") if not username_valid(username): raise InputError( description="Username, {}, must only use alphanumeric characters and be 1-20 characters long".format(username) ) user.username = username db.session.commit()
def handle_channel_details(): """ HTTP Route: /channels/details HTTP Method: GET Params: (token, channel_id) Returns JSON: { name, description, visibility, channel_img_url, owner_members, all_members } """ token = request.args.get("token") channel_id = int(request.args.get("channel_id")) target_channel = select_channel(channel_id) calling_user = get_user_from_token(token) printColour(" ➤ Channel Details: {}'s details fetched by {}".format( target_channel.name, calling_user.username), colour="blue") return jsonify(channels.channels_details(token, channel_id))
def messages_search_match(token, channel_id, query_str): """ Given a query string, return a collection of messages from the target channel that matches the query string. Results are sorted from most recent message to least recent message ERRORS - Invalid token Returns: { messages: [ { message_id, user_id, message, time_created }, { ... }, ... ] } """ verify_token(token) # Empty result if query_str == "": return {} user = get_user_from_token(token) # Searches all messages and compares query_str search_results = [] channel = Channel.query.filter_by(id=channel_id).first() all_messages = channel.messages_sent for message_obj in all_messages: curr_message = message_obj.message print(curr_message) print(message_obj.time_created) # Case-insensitive matching if curr_message.lower().find(query_str.lower()) != -1: print("{} matches {}!".format(curr_message, query_str)) search_results.append({ "message_id": message_obj.id, "user_id": message_obj.user_id, "message": message_obj.message, "time_created": message_obj.time_created.timestamp() }) sorted_messages = sorted(search_results, key=lambda k: k['time_created']) # Returns messages that contain query_str # Contains all info on message (message_id, user_id, message, time_created) return {'messages': sorted_messages}
def message_remove(token, message_id): """ Removes a message from the list of messages Returns: { old_message } """ verify_token(token) message_obj = Message.query.filter_by(id=message_id).first() if not message_obj: raise InputError("Message doesn't exist") calling_user = get_user_from_token(token) if calling_user.id != message_obj.user_id: raise AccessError("You can't modify someone else's message") # Removes message and saves changes db.session.delete(message_obj) db.session.commit() return {"old_message": message_obj.message}
def handle_connection_get_info(): """ Fetches details regarding the connection between two users Params: (token, user_id) Returns JSON: { is_connected, connection_is_pending, is_requester } """ token = request.args.get("token") user_id = int(request.args.get("user_id")) calling_user = get_user_from_token(token) target_user = get_user_from_id(user_id) printColour(" ➤ Connections: {} is fetching connection details with {}".format( calling_user.username, target_user.username ), colour="blue") return jsonify(connections.connection_fetch_info(token, user_id))
def handle_conection_remove_message(): """ HTTP Route: /connections/message HTTP Method: DELETE Params: (token, message_id) Returns JSON: { } """ request_data = request.get_json() token = request_data["token"] message_id = int(request_data["message_id"]) calling_user = get_user_from_token(token) printColour(" ➤ Connections Messages: {} removed message id: {}".format( calling_user.username, message_id ), colour="blue") return jsonify(connections.connection_remove_message(token, message_id))
def handle_conection_request(): """ HTTP Route: /connections/request HTTP Method: POST Params: (token, user_id) Returns JSON: { } """ request_data = request.get_json() token = request_data["token"] user_id = int(request_data["user_id"]) calling_user = get_user_from_token(token) target_user = get_user_from_id(user_id) printColour(" ➤ Connections Request: {} sent a connection request to {}".format( calling_user.username, target_user.username ), colour="blue") return jsonify(connections.connection_request(token, user_id))
def handle_conection_fetch_messages(): """ HTTP Route: /connections/message HTTP Method: GET Params: (token, user_id) Returns JSON: { messages: [ { message_id, message, sender_id, time_created } ] } """ token = request.args.get("token") user_id = int(request.args.get("user_id")) calling_user = get_user_from_token(token) target_user = get_user_from_id(user_id) printColour(" ➤ Connections Messages: {} fetched messages with {}".format( calling_user.username, target_user.username ), colour="blue") return jsonify(connections.connection_fetch_messages(token, user_id))
def handle_channel_messages(): """ HTTP Route: /channels/messages HTTP Method: GET Params: (token, channel_id, start) Returns JSON: { messages, exhausted } """ token = request.args.get("token") channel_id = int(request.args.get("channel_id")) start = int(request.args.get("start")) target_channel = select_channel(channel_id) calling_user = get_user_from_token(token) printColour(" ➤ Channel Messages: {}'s messages fetched by {}".format( target_channel.name, calling_user.username), colour="blue") return jsonify(channels.channels_messages(token, channel_id, start))
def handle_channel_leave(): """ HTTP Route: /channels/leave HTTP Method: POST Params: (token, channel_id) Returns JSON: { } """ request_data = request.get_json() token = request_data["token"] channel_id = int(request_data["channel_id"]) target_channel = select_channel(channel_id) calling_user = get_user_from_token(token) printColour(" ➤ Channel Leave: {} left channel {}".format( calling_user.username, target_channel.name), colour="blue") return jsonify(channels.channels_leave(token, channel_id))
def handle_channels_upload_image(): """ HTTP Route: /channels/uploadimage HTTP Method: POST Params: (token, channel_id, x_start, y_start, x_end, y_end, file) """ token = request.form["token"] channel_id = request.form["channel_id"] x_start = int(request.form["x_start"]) y_start = int(request.form["y_start"]) x_end = int(request.form["x_end"]) y_end = int(request.form["y_end"]) calling_user = get_user_from_token(token) target_channel = select_channel(channel_id) printColour( " ➤ Channel Upload Image: {} uploading channel image for {}".format( calling_user.username, target_channel.name), colour="blue") printColour(" ➤ Crop coordinates: start = ({}, {}), end = ({}, {})".format( x_start, y_start, x_end, y_end), colour="cyan") # Check if the post request has the file part if 'file' not in request.files: printColour(" ➤ User didn't upload a photo", colour="red") raise InputError("No valid image file found. Please try again") else: file = request.files['file'] if file and allowed_file(file.filename): # Saving the image to local storage filename = get_latest_filename( "channel_{}_profile.jpg".format(channel_id)) file.save(os.path.join(UPLOAD_FOLDER, filename)) crop_image_file(filename, x_start, y_start, x_end, y_end) # Saving the image endpoint to the channel's tuple under the channel_img_url field image_endpoint = "{0}/images/{1}".format(BASE_URL, filename) channels.channels_upload_photo(token, channel_id, image_endpoint) printColour( " ➤ Successfully uploaded channel image for {}. Available at: {}" .format(target_channel.name, image_endpoint), colour="green", bordersOn=False) return jsonify({"succeeded": True})
def channels_invite(token, channel_id, user_id): """ Invites a user, with the given user_id, to join the channel with ID channel_id. Once invited, the user is added to the channel immediately. Parameters: token (str) channel_id (int) user_id (int) Returns: {} (dict) """ printColour("RECEIVED CHANNEL ID: {}".format(channel_id), colour="blue") printColour("RECEIVED USER ID: {}".format(user_id), colour="blue") verify_token(token) calling_user = get_user_from_token(token) if not calling_user: raise AccessError(description="Invalid Token") # If channel_id is invalid, raise input error selected_channel = select_channel(channel_id) if not selected_channel: raise InputError(description="Target channel is not a valid channel that the user is part of.") # Check the authorised user is not already a member of the channel if not is_user_member(calling_user, selected_channel): raise AccessError(description="You are not a member of this channel") invited_user = User.query.filter_by(id=user_id).first() # Check that the user exists (ie. the user_id is valid) if not invited_user: raise InputError(description="Target user is not a valid user") # Check if invited_user is already a member if is_user_member(invited_user, selected_channel): raise InputError(description="{} is already a member".format(invited_user.username)) # Granting membership new_membership = MemberOf( user=invited_user, channel=selected_channel, is_owner=False ) printColour("Trying to add user {} to channel {}".format(new_membership.user.id, new_membership.channel.id), colour="blue") db.session.add(new_membership) db.session.commit() return {}
def handle_channel_add_owner(): """ HTTP Route: /channels/addowner HTTP Method: POST Params: (token, channel_id, user_id) Returns JSON: { } """ request_data = request.get_json() token = request_data["token"] channel_id = int(request_data["channel_id"]) user_id = int(request_data["user_id"]) target_channel = select_channel(channel_id) calling_user = get_user_from_token(token) target_user = get_user_from_id(user_id) printColour(" ➤ Channel Add Owner: {} added {} as an owner in {}".format( calling_user.username, target_user.username, target_channel.name), colour="blue") return jsonify(channels.channels_addowner(token, channel_id, user_id))
def handle_conection_send_message(): """ HTTP Route: /connections/message HTTP Method: POST Params: (token, user_id, message) Returns JSON: { } """ request_data = request.get_json() token = request_data["token"] user_id = int(request_data["user_id"]) message = request_data["message"] calling_user = get_user_from_token(token) target_user = get_user_from_id(user_id) printColour(" ➤ Connections Messages: {} sent a message to {}".format( calling_user.username, target_user.username ), colour="blue") return jsonify(connections.connection_send_message(token, user_id, message))
def handle_user_profile_upload_cover(): """ HTTP Route: /users/profile/uploadcover HTTP Method: POST Params: (token, user_id, file) Returns JSON: { succeeded } """ token = request.form["token"] user_id = request.form["user_id"] user = get_user_from_id(user_id) printColour( " ➤ User Profile Upload Cover: Uploading cover picture image for {}". format(user.username), colour="blue") # Check if the post request has the file part if 'file' not in request.files: printColour(" ➤ User didn't upload a photo", colour="red") return jsonify({"succeeded": False}) else: file = request.files['file'] if file.filename == '': # If user does not select file, browser also submits an empty part without filename printColour(" ➤ No selected file", colour="red") return jsonify({"succeeded": False}) if file and allowed_file(file.filename): # Saving the image to local storage user_id = get_user_from_token(token).id filename = get_latest_filename( "user_{}_profile_cover.jpg".format(user_id)) file.save(os.path.join(UPLOAD_FOLDER, filename)) # Saving the image endpoint to the user's bio tuple under the cover_img_url field image_endpoint = "{0}/images/{1}".format(BASE_URL, filename) users.users_profile_upload_cover(token, user_id, image_endpoint) printColour( " ➤ Successfully uploaded cover image for {}. Available at: {}" .format(user.username, image_endpoint), colour="green", bordersOn=False) return jsonify({"succeeded": True})
def handle_channels_create(): """ HTTP Route: /channels/create HTTP Method: POST Params: (token, name, description, visibility) Returns JSON: { channels_id } """ request_data = request.get_json() token = request_data["token"] name = request_data["name"] description = request_data["description"] visibility = request_data["visibility"] calling_user = get_user_from_token(token) printColour(" ➤ Channel Create: {} created {} ({})".format( calling_user.username, name, visibility), colour="blue") return jsonify( channels.channels_create(token, name, description, visibility))
def handle_search(): """ HTTP Route: /channels/search HTTP Method: GET Params: (token, channel_id, query_str) Returns JSON: { messages: [ { message_id, u_id, message, time_created, reacts }, ...] } """ token = request.args.get("token") channel_id = request.args.get("channel_id") query_str = request.args.get("query_str") calling_user = get_user_from_token(token) target_channel = select_channel(channel_id) printColour( " ➤ Channel Search: {} looking for '{}' in {}'s messages".format( calling_user.username, query_str, target_channel.name), colour="blue") return jsonify(messages.messages_search_match(token, channel_id, query_str))
def handle_channel_invite(): """ HTTP Route: /channels/invite HTTP Method: POST Params: (token, channel_id, user_id) Returns JSON: { } """ request_data = request.get_json() token = request_data["token"] channel_id = int(request_data["channel_id"]) user_id = int(request_data["user_id"]) target_channel = select_channel(channel_id) target_user = get_user_from_id(user_id) calling_user = get_user_from_token(token) printColour( " ➤ Channel Invite: invitation to {} ({}) sent from {} to {}".format( target_channel.name, target_channel.visibility, calling_user.username, target_user.username), colour="blue") return jsonify(channels.channels_invite(token, channel_id, user_id))
def channels_create(token, name, description, visibility): """ Creates a new channel with that name that is either a public or private channel. The created channel object has the following fields: { channel_id, name, description, visibility } Parameters: token (str) name (str) description (str) visibility (bool) Raises: TODO Returns: { channel_id } """ verify_token(token) if not name or not visibility: raise InputError("Channel name or visibility not specified") if len(name) > 30: raise InputError("Channel name too long. Stay under 30 characters") creator = get_user_from_token(token) # Adding a default picture for the channel channel_image_endpoint = os.getenv("BASE_URI") + "/images/{}".format("default_channel.jpg") new_channel = Channel( visibility=visibility, name=name, description=description, channel_img_url=channel_image_endpoint ) ownership = MemberOf( user=creator, channel=new_channel, is_owner=True ) db.session.add(new_channel) db.session.add(ownership) db.session.commit() return { 'channel_id': new_channel.id }