def user_profile(token, u_id): """ take a valid token and u_id, return a dictionary as below: >>> { "user" : { 'u_id': user['id'], 'email': user['email'], 'name_first': user['first_name'], 'name_last': user['last_name'], 'handle_str': user['handle'], } } """ # Ensure token is valid # This function below has error handling built inside auth_get_current_user_id_from_token(token) # Ensure u_id is valid and get the user information from database try: user = database["users"][u_id] except KeyError: raise InputError(f"{u_id} is not a valid user id") # We don't directly return user from database since password is included return {"user": get_user_details(user)}
def channels_listall(token): channels = [] # makes sure the user is valid auth_get_current_user_id_from_token(token) for channel in database["channels"].values(): channel_info = get_channel_id_and_name(channel) channels.append(channel_info) return {"channels": channels}
def users_all(token): users = [] # authenticate user auth_get_current_user_id_from_token(token) for user in database["users"].values(): user_info = get_user_details(user) users.append(user_info) return {"users": users}
def upload_photo(): data = request.get_json() token = data["token"] img_url = str(data["img_url"]) x_start = int(data["x_start"]) y_start = int(data["y_start"]) x_end = int(data["x_end"]) y_end = int(data["y_end"]) # Crop the image and stop it in user_photos folder # Name of image is user_id user_profile_crop_image(token, img_url, x_start, y_start, x_end, y_end) auth_get_current_user_id_from_token(token) return {}
def channels_create(token, name, is_public): if len(name) > 20: raise InputError(f"{name!r} is more than 20 characters long") if name == "": name = "new_channel" creator_data = auth_get_user_data_from_id( auth_get_current_user_id_from_token(token)) id = database["channels_id_head"] new_channel = { "id": id, "name": name, "is_public": is_public, "owner_members_id": [creator_data["id"]], "all_members_id": [creator_data["id"]], "messages": {}, "standup_queue": [], "standup_is_active": False, "standup_finish_time": None, } database["channels"][id] = new_channel database["channels_id_head"] += 1 return {"channel_id": new_channel["id"]}
def channel_details(token, channel_id): """ Return channel_details with formats below: >>> { "name": channel["name"], "owner_members": owners, "all_members": members, } """ current_user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) if current_user_id not in channel["all_members_id"]: raise AccessError( f"user {current_user_id} not authorized to access this channel") # build the information dictionnary owners = [] for ownerid in channel["owner_members_id"]: user_data = auth_get_user_data_from_id(ownerid) owners.append(formated_user_details_from_user_data(user_data)) members = [] for memberid in channel["all_members_id"]: user_data = auth_get_user_data_from_id(memberid) members.append(formated_user_details_from_user_data(user_data)) return { "name": channel["name"], "owner_members": owners, "all_members": members, }
def standup_send(token, channel_id, message): # Check token and chanel_id are valid user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) handle = auth_get_user_data_from_id(user_id)["handle"] # Check is standup is active if channel["standup_is_active"] == False: raise InputError("There's no active standup running in this channel") # Check is the message is too long if len(message) > 1000: raise InputError( f"No one is going to read a {len(message)} character long message!" ) # Make the user is a memeber of the channel if user_id not in channel["all_members_id"]: raise AccessError( f"user {user_id} needs to be a member of this channel to send a message during standup" ) # Append handle and message tuple to the list in order channel["standup_queue"].append((handle, message)) return {}
def message_send(token, channel_id, message, delay=0): """ delay is in seconds """ user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) if user_id not in channel["all_members_id"]: raise AccessError( f"user {user_id} needs to be a member of this channel to send a message" ) if len(message) > 1000: raise InputError( f"No one is going to read a {len(message)} character long message!" ) if delay < 0: raise InputError("Time to send the message is in the past") if delay == 0: message_id = perform_message_send_no_checking(channel, user_id, message) return {"message_id": message_id} timer = Timer(delay, perform_message_send_no_checking, [channel, user_id, message]) timer.start()
def add_react_information_to_message(token, messages): """ This function is to add a key "is_this_user_reacted" to the "reacts" data type in message datatype >>> Before adding: "messages": [ 1: { "message_id": 1, "u_id": 1, "message": "Hello world", "time_created": 1582426789, "is_pinned": False, "reacts": [ { "react_id" : 1, "u_ids": [1, 2, 3, 4], }, ] } }, ], >>> After adding: "messages": [ 1: { "message_id": 1, "u_id": 1, "message": "Hello world", "time_created": 1582426789, "is_pinned": False, "reacts": [ { "react_id" : 1, "u_ids": [1, 2, 3, 4], difference >>>>>>>>>>>>>>> "is_this_user_reacted": True }, ] } }, ] The reason I didn't directly add "is_this_user_reacted" to the database is because this value should be change according to different user when we output reacts data type. This needs us to recognize who is the current user that on the web page. I think it is inappropriate to store the current user who access to web page in our database since our database serves multiple user, but it will be controversial to the front-end interface. So I add "is_this_user_reacted" in the process of output messages. """ user_id = auth_get_current_user_id_from_token(token) for message in messages: for react in message["reacts"]: if user_id in react["u_ids"]: react["is_this_user_reacted"] = True else: react["is_this_user_reacted"] = False
def user_profile_setemail(token, email): # raises AccessError if token is not active u_id = auth_get_current_user_id_from_token(token) # raises InputError if email address is illegal check_email(email) user = database["users"][u_id] user["email"] = email
def channels_list(token): channels = [] current_user_id = auth_get_current_user_id_from_token(token) for channel in database["channels"].values(): if current_user_id in channel["all_members_id"]: channel_info = get_channel_id_and_name(channel) channels.append(channel_info) return {"channels": channels}
def channel_join(token, channel_id): current_user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) if (not channel["is_public"] and database["users"][current_user_id]["is_admin"] is False): raise AccessError("Channel is not public") if len(channel["all_members_id"]) == 0: channel["owner_members_id"].append(current_user_id) channel["all_members_id"].append(current_user_id)
def user_profile_setname(token, name_first, name_last): # raises AccessError if token is not active u_id = auth_get_current_user_id_from_token(token) # If user's first name or last name is more than 50 characters if not isNameLengthOK(name_first, 1, 50): raise InputError(f"{name_first} is illegal") if not isNameLengthOK(name_last, 1, 50): raise InputError(f"{name_last} is illegal") user = database["users"][u_id] user["first_name"] = name_first user["last_name"] = name_last
def channel_leave(token, channel_id): current_user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) if current_user_id not in channel["all_members_id"]: raise AccessError("User is not in this channel") # If a user is owner of that channel, he should be removed # from the owner list when he left the channel if current_user_id in channel["owner_members_id"]: channel["owner_members_id"].remove(current_user_id) # Delete the user's token from that channel channel["all_members_id"].remove(current_user_id)
def search(token, query_str): user_id = auth_get_current_user_id_from_token(token) matching_messages = [] # loop through every channel in which the user is a member, add add the # matching message to the list for channel in database["channels"].values(): if user_id in channel["all_members_id"]: add_matching_messages(channel, query_str, matching_messages) return { "messages": sorted( matching_messages, key=lambda message: message["time_created"], reverse=True ), }
def admin_userpermission_change(token, u_id, permission_id): admin_id = auth_get_current_user_id_from_token(token) if database["users"][admin_id]["is_admin"] is False: raise AccessError( "user isn't a flockr owner, cannot change other user's permission" ) if permission_id != 1 and permission_id != 2: raise InputError(f"invalid permission id {permission_id}") if u_id not in database["users"]: raise InputError(f"invalid user id {u_id}") database["users"][u_id]["is_admin"] = permission_id == 1
def user_profile_sethandle(token, handle_str): # raises AccessError if token is not active u_id = auth_get_current_user_id_from_token(token) # raises InputError if there is a duplicated handle if is_handle_already_used(handle_str): raise InputError(f"{handle_str} has been used") # raises InputError if handleis not illegal if not isNameLengthOK(handle_str, 3, 20): raise InputError(f"length of {handle_str} is illegal") user = database["users"][u_id] user["handle"] = handle_str
def message_pin(token, message_id): """Given a message within a channel, mark it as "pinned" to be given special display treatment by the frontend""" # Check token is valid - error checking in function user_id = auth_get_current_user_id_from_token(token) # Check message exists in database message_exists = False for ch in database["channels"].values(): if message_id in ch["messages"]: channel_id_for_message = ch["id"] message_exists = True if message_exists == False: raise InputError # Check message is not already pinned channel_msg = database["channels"][channel_id_for_message]["messages"][ message_id] if channel_msg["is_pinned"] == True: raise InputError( f"message with message id {message_id} is already pinned") # Check user is in channel within which the message exists if (database["users"][user_id]["is_admin"] is False and user_id not in database["channels"][channel_id_for_message]["all_members_id"]): raise AccessError( "user must be part of the channel to remove his/her message (see assumptions.md)" ) # Pin message if user is authorised - i.e. user is either a flockr owner # or the owner of the channel if (user_id in database["channels"][channel_id_for_message]["owner_members_id"] or database["users"][user_id]["is_admin"]): user_id_is_owner = True else: user_id_is_owner = False for msg in database["channels"][channel_id_for_message]["messages"].values( ): if msg["message_id"] == message_id and (user_id_is_owner): msg["is_pinned"] = True return {} # User not authorised to pin message raise AccessError
def message_react(token, message_id, react_id): # Ensure react id is valid if not is_react_id_valid(react_id): raise InputError("react id is invalid") # If token is invalid, the function below will raise AccessError user_id = auth_get_current_user_id_from_token(token) channels = [ channel for channel in database["channels"].values() if user_id in channel["all_members_id"] ] # User is not in any channel if channels == []: raise InputError("user is not in any channel") # Ensure there is a message match the message_id message = next( (message for channel in channels for message in channel["messages"].values() if message_id == message["message_id"]), None, ) if message == None: raise InputError("Message_id is invalid") # Get the react from message react = next( (react for react in message["reacts"] if react["react_id"] == react_id), None) # React haven't being created if react == None: new_react = { "react_id": react_id, "u_ids": [user_id], } message["reacts"].append(new_react) else: # If user has reacted if user_id in react["u_ids"]: raise InputError("This user has reacted") else: react["u_ids"].append(user_id)
def message_edit(token, message_id, message): """Given a message, update its text with new text. If the new message is an empty string, the message is deleted """ # Check token is valid - error checking in function user_id = auth_get_current_user_id_from_token(token) # Check message exists in database message_exists = False for ch in database["channels"].values(): if message_id in ch["messages"]: channel_id_for_message = ch["id"] message_exists = True if message_exists == False: raise AccessError if (database["users"][user_id]["is_admin"] is False and user_id not in database["channels"][channel_id_for_message]["all_members_id"]): raise AccessError( "user must be part of the channel to edit his/her message (see assumptions.md)" ) # This is done after error checking as input error not allowed in this function if message == "": message_remove(token, message_id) return {} if len(message) > 1000: raise InputError( "message cannot exceed 1000 characters (see assumptions.md)") # Edit message if user is authorised, delete if message = '' for msg in database["channels"][channel_id_for_message]["messages"].values( ): if msg["message_id"] == message_id and ( msg["u_id"] == user_id or (user_id in database["channels"][channel_id_for_message] ["owner_members_id"]) or (database["users"][user_id]["is_admin"])): msg["message"] = message return {} # Unauthorised to edit message raise AccessError
def channel_invite(token, channel_id, u_id): inviter_user_id = auth_get_current_user_id_from_token(token) try: database["users"][u_id] except KeyError: raise InputError(f"{u_id} is an invalid user id") channel = get_channel_from_id(channel_id) if inviter_user_id not in channel["all_members_id"]: raise AccessError( f"user {inviter_user_id} not authorized to invite you to this channel" ) if u_id not in channel["all_members_id"]: channel["all_members_id"].append(u_id) return {}
def channel_removeowner(token, channel_id, u_id): """ When there is only one owner in the channel and this owner is removed, there should be another user in this room randomly became the owner. """ # Generate the channel which match the channel_id user_who_remove_others_uid = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) if u_id not in channel["owner_members_id"]: raise InputError("User is not a owner, can not be removed") if (user_who_remove_others_uid not in channel["owner_members_id"] and database["users"][user_who_remove_others_uid]["is_admin"] is False): raise AccessError("User is not authorized") if len(channel["all_members_id"]) == 1: # If a owner are the only member of a channel, when this owner # remove owner himself, he will automatically leave the channel # we can't use channel_leave, because we want to remove u_id, not u_id # of token (token might be an admin's token) channel["all_members_id"].remove(u_id) channel["owner_members_id"].remove(u_id) else: # If the owner is the only owner in the channel if len(channel["owner_members_id"]) == 1: # Generate a member of channel to become the owner owner_iterator = iter( [user for user in channel["all_members_id"] if user != u_id]) next_owner_uid = next(owner_iterator, None) # We assume we will always find a member in the channel # to become owner since length of channel["all_members_id"] # is bigger than 2 assert next_owner_uid != None channel["owner_members_id"].append(next_owner_uid) channel["owner_members_id"].remove(u_id) else: channel["owner_members_id"].remove(u_id)
def standup_start(token, channel_id, length): assert length >= 0 # Check token and chanel_id are valid user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) # Check if a standup is already running if channel["standup_is_active"] == True: raise InputError("There's an active standup running in this channel") # Turn the standup on, set the time when it ends channel["standup_is_active"] = True channel["standup_finish_time"] = round(time() + length) # Send all message from the queue after length second t = Timer(length, send_message_from_queue, [user_id, channel]) t.start() return {"time_finish": channel["standup_finish_time"]}
def channel_addowner(token, channel_id, u_id): channel = get_channel_from_id(channel_id) if u_id in channel["owner_members_id"]: raise InputError("User is already an owner of the channel") # make sure the user adding an owner is allowed to adder_id = auth_get_current_user_id_from_token(token) if (adder_id not in channel["owner_members_id"] and database["users"][adder_id]["is_admin"] is False): raise AccessError( "User is neither an owner of the channel, nor an admin") # make sure target user is valid if u_id not in database["users"]: raise InputError(f"{u_id} is an invalid user id") # the user wasn't nescerally a member of the channel before becoming an owner if u_id not in channel["all_members_id"]: channel["all_members_id"].append(u_id) channel["owner_members_id"].append(u_id)
def user_profile_crop_image(token, img_url, x_start, y_start, x_end, y_end): """Given a URL of an image on the internet, crops the image within bounds (x_start, y_start) and (x_end, y_end). Position (0,0) is the top left.""" # Ensure token is valid - error checking in function user_id = auth_get_current_user_id_from_token(token) # Returns (filename, headers) if successful try: urllib_request.urlretrieve(img_url, "static/" + str(user_id) + ".jpg") except: raise InputError("img_url returns an HTTP status other than 200") original_img = Image.open("static/" + str(user_id) + ".jpg") original_width, original_height = original_img.size # Ensure image in correct format if original_img.format != "JPG" and original_img.format != "JPEG": raise InputError("Image uploaded is not a JPG") # Basic error checking for given coordinates if x_start < 0 or y_start < 0 or x_end < x_start or y_end < y_start: raise InputError("given dimensions incorrectly formatted") # Error checking that coordinates are within original image if x_end > original_width or y_end > original_height: raise InputError("Dimensions out of bounds from original image") cropped = original_img.crop((x_start, y_start, x_end, y_end)) cropped.save("static/" + str(user_id) + ".jpg") database["users"][user_id]["profile_img_url"] = ( get_host_url() + "static/" + str(user_id) + ".jpg" ) return {}
def message_remove(token, message_id): """ Given a message_id for a message, this message is removed from the channel """ # Check token is valid - error checking in function user_id = auth_get_current_user_id_from_token(token) # Check message exists in database message_exists = False for ch in database["channels"].values(): if message_id in ch["messages"]: channel_id_for_message = ch["id"] message_exists = True if message_exists == False: raise InputError if (database["users"][user_id]["is_admin"] is False and user_id not in database["channels"][channel_id_for_message]["all_members_id"]): raise AccessError( "user must be part of the channel to remove his/her message (see assumptions.md)" ) # Remove message if user is authorised for msg in database["channels"][channel_id_for_message]["messages"].values( ): if msg["message_id"] == message_id and ( msg["u_id"] == user_id or (user_id in database["channels"][channel_id_for_message] ["owner_members_id"]) or database["users"][user_id]["is_admin"]): del database["channels"][channel_id_for_message]["messages"][ message_id] return {} # Unauthorised to remove message raise AccessError
def channel_messages(token, channel_id, start): """ >>> Return: { "messages": [ 1: { "message_id": 1, "u_id": 1, "message": "Hello world", "time_created": 1582426789, "is_pinned": False, "reacts": [ { "react_id" : 1, "u_ids": [1, 2, 3, 4], "is_this_user_reacted": True }, ] } ], "start": start, "end": -1 if len(channel_messages) < 50 else start + 50, } Note that "is_this_user_reacted" is not the data from database """ current_user_id = auth_get_current_user_id_from_token(token) channel = get_channel_from_id(channel_id) # Authorised user not part of channel if current_user_id not in channel["all_members_id"]: raise AccessError( f"Authorised user ({current_user_id}) not part of channel ({channel_id})" ) # Invalid start: # Negative start index # Start greater than total number of messages in channel messages_total = len(channel["messages"]) if start < 0 or start > messages_total: raise InputError("Invalid start value") # some unreadable-smart-looking python code to make please my ego. sorted # sorts each messages according to the "time_created" key we reverse # because we want the newest message first (newer means bigger timestamp) # then, [start:start+50] takes a slice of a list. start is included, # start+50 is excluded, so it gives exactly 50 elements at most. If # start+50 >= len(messages), then python handles everything nicely and just # gives back a smaller slice. channel_messages = sorted(channel["messages"].values(), key=lambda msg: msg["time_created"], reverse=True)[start:start + 50] # Add "is_this_user_reacted" to every reacts in every messages add_react_information_to_message(token, channel_messages) return { "messages": channel_messages, "start": start, # showing off again... "end": -1 if len(channel_messages) < 50 else start + 50, }