def unfollow_user(user_id): """Stop following the user with a given ID.""" username = get_jwt_identity() login = Login.query.filter_by(username=username).first() if login is None: raise APIError( 400, "User {username} does not exist on this server".format( username=username)) following_user = login.user followed_user = User.query.get(user_id) if followed_user is None: raise APIError(404, "Attempting to follow a user who does not exist") follow_ids = [f.id for f in following_user.following] if user_id in follow_ids: del following_user.following[follow_ids.index(user_id)] login.last_action = datetime.now() db.session.add(following_user, login) db.session.commit() return jsonify([u.to_dict() for u in following_user.following]), 200
def get_accounts(): """Retrieve accounts on this server. Can use the following query parameters: * max: The maximum number of records to return * page: The "page" of records """ error_on_unauthorized() accounts = Login.query.order_by(Login.id) total_num = accounts.count() if total_num == 0: return jsonify(total=0, uploads=[]) try: count = int(request.args.get('max', total_num)) page = int(request.args.get('page', 1)) if count <= 0 or page <= 0: raise APIError(422, "Query parameters out of range") begin = (page - 1) * count end = min(begin + count, total_num) return jsonify( total=total_num, users=[login_to_dict(l) for l in accounts.all()[begin:end]]), 200 except ValueError: raise APIError(422, "Invalid query parameter")
def edit_avatar(): """Change the avatar of the logged-in user.""" username = get_jwt_identity() origin = current_app.config['SERVER_ORIGIN'] profile = User.query.filter_by(username=username, origin=origin).first() if profile is None: # This shouldn't happen, but an attacker could feasibly try to edit # a nonexistent profile. raise APIError(400, "User {username} does not exist on this server".format(username=username)) # Get the new avatar from the request and put it in the DB if 'avatar' in request.files: for f in request.files.getlist('avatar'): try: fid = flake_id() name_with_id = printable_id(fid) + f.name filename = liblio_uploads.save(f) avatar = Avatar(flake=fid, filename=filename, user=profile) profile.current_avatar = avatar db.session.add(avatar, profile) db.session.commit() return jsonify(msg="Avatar changed for user {0}".format(profile.username)), \ 200, \ { 'Location': avatar.uri } except UploadNotAllowed as error: print("Upload failed: {0}", str(error)) raise APIError(415, "Can't upload files of this type")
def get_by_name(username): """Find a user by name. This can be done in one of two ways. For local users, simply passing the username will find the user with that name. If the query parameter contains an @ character, the search will instead be across all known servers. Examples: * "/by-name/foo" searches for a user named "foo" on this server. * "/by-name/[email protected]" searches for a user with the name "foo" from the origin server "example.com". """ if '@' in username: # Username/origin pair, so search both name, origin = username.split('@') user = User.query.filter_by(username=name, origin=origin).first() if user is not None: return jsonify(user.to_dict()), 200 else: raise APIError( 404, "No user {username} found.".format(username=username)) else: # Local username search user = User.query.filter_by(username=username).first() if user is not None: return jsonify(user.to_dict()), 200 else: raise APIError( 404, "User {name} does not exist on this server".format( name=username))
def get_avatars(): """Retrieves avatar metadata for all users known to this server. Can use the following query parameters: * max: The maximum number of records to return * page: The page of records """ error_on_unauthorized() media = Avatar.query.order_by(Avatar.id) total_num = media.count() if total_num == 0: return jsonify(total=0, uploads=[]) try: count = int(request.args.get('max', total_num)) page = int(request.args.get('page', 1)) if count <= 0 or page <= 0: raise APIError(422, "Query parameters out of range") begin = (page - 1) * count end = min(begin + count, total_num) return jsonify( total=total_num, uploads=[avatar_to_dict(a) for a in media.all()[begin:end]]), 200 except ValueError: raise APIError(422, "Invalid query parameter")
def create_post(args): """Create a new post.""" username = get_jwt_identity() login = Login.query.filter_by(username=username).first() if login is None: raise APIError(400, "User {username} does not exist on this server".format(username=username)) post = Post( user=login.user, subject=args.get('subject'), source=args['source'], ) # Posts without parents are allowed; they're just "top-level" parent_id = args.get('parent_id') if parent_id is not None: post.parent_id = parent_id meta = args.get('metadata') if meta is not None: mdict = flask.json.loads(meta) post.post_meta = mdict # Posts can have tags tags = mdict.get('tags') if tags is not None: for t in tags: tid = Tag.query.filter_by(name=Tag.normalize_name(t)).first() post.tags.append(tid) # This does affect the "last active" time login.last_action = datetime.now() db.session.add(post, login) # Posts may have attached media, so store that if 'files' in request.files: for f in request.files.getlist('files'): try: fid = flake_id() name_with_id = printable_id(fid) + f.name filename = liblio_uploads.save(f) upload = Upload(flake=fid, filename=filename, user=login.user, post=post, mimetype=f.mimetype) db.session.add(upload) except UploadNotAllowed as error: print("Upload failed: {0}", str(error)) raise APIError(415, "Can't upload files of this type") db.session.commit() return jsonify(post.to_dict()), 201, { 'Location': post.uri }
def error_on_unauthorized(): """Raises appropriate API errors on bad usernames and for users who are not administrators. This is a common authentication check for essentially every admin route. """ username = get_jwt_identity() user = Login.query.filter_by(username=username).first() if user is None: raise APIError( 400, "User {username} does not exist on this server".format( username=username)) elif user.role is not Role.admin: raise APIError(401, "Only administrators have access to this page")
def edit_profile(args): """Edit a user's profile.""" username = get_jwt_identity() origin = current_app.config['SERVER_ORIGIN'] profile = User.query.filter_by(username=username, origin=origin).first() if profile is None: # This shouldn't happen, but an attacker could feasibly try to edit # a nonexistent profile. raise APIError(400, "User {username} does not exist on this server".format(username=username)) new_name = args['name'] new_bio = args['bio'] new_tags = args['tags'] if new_name is not None: profile.display_name = new_name if new_bio is not None: profile.bio = new_bio if new_tags is not None: profile.tags = Tag.query.filter(Tag.name.in_(new_tags)).all() # TODO: do the same thing for roles and tags, once they're implemented # This is an API action, so update the activity timestamp profile.login.last_action = datetime.now() db.session.add(profile) db.session.commit() return make_response(jsonify(profile=profile.to_profile_dict()), 200)
def get_by_id(post_id): """Get the post with the given database ID.""" post = Post.query.get(post_id) if post is not None: return jsonify(post.to_dict()), 200 else: raise APIError(404, "Post with ID {id} does not exist on this server".format(id=post_id))
def get_by_flake(flake_id): """Get the post with the given Flake ID.""" post = Post.query.filter_by(flake=decode_printable_id(flake_id)).first() if post is not None: return jsonify(post.to_dict()), 200 else: raise APIError(404, "Post with Flake ID {id} does not exist on this server".format(id=flake_id))
def get_tag(tagname): """Get the tag with a given name.""" tag = Tag.query.filter_by(name=tagname).first() if tag is not None: return jsonify(tag.to_dict()), 200 else: raise APIError( 404, "No tag {tagname} on this server".format(tagname=tagname))
def get_posts_by_id(user_id): """Get all posts for the user with the given database ID.""" user = User.query.get(user_id) if user is not None: return jsonify([p.to_dict() for p in user.posts]), 200 else: raise APIError( 404, "User with ID {id} does not exist on this server".format( id=user_id))
def get_by_id(user_id): """Get the user with the given database ID""" user = User.query.get(user_id) if user is not None: return jsonify(user.to_dict()), 200 else: raise APIError( 404, "User with ID {id} does not exist on this server".format( id=user_id))
def get_user_following(user_id): """Get all users this user is following.""" user = User.query.get(user_id) if user is not None: return jsonify([f.to_dict() for f in user.following]), 200 else: raise APIError( 404, "User with ID {id} does not exist on this server".format( id=user_id))
def get_avatar(media_fid): """Get uploaded avatar by its Flake ID.""" avatar = Avatar.query.filter_by( flake=decode_printable_id(media_fid)).first() if avatar is not None: path = liblio_uploads.path(avatar.filename) return send_file(path) else: raise APIError(404, "Media not found")
def get_media(media_fid): """Get uploaded media by its Flake ID.""" media = Upload.query.filter_by( flake=decode_printable_id(media_fid)).first() if media is not None: path = liblio_uploads.path(media.filename) return send_file(path) else: raise APIError(404, "Media not found")
def create_account(args): """Create a new account on this server.""" if request.method == 'POST': # TODO: Check for a user who is already logged in username = args['username'] email = args['email'] password = args['password'] user = Login.query.filter_by(username=username).first() if user is not None: raise APIError(message="Username already exists on this server") em = Login.query.filter_by(email=email).first() if em is not None: raise APIError( message= "A user with this email address already exists on this server") login = Login(username=username, email=email) login.set_password(password) # Create a blank profile for this user (we can add to it later) u = User(username=username, origin=current_app.config['SERVER_ORIGIN']) login.user = u # Add to the DB db.session.add(login) db.session.commit() return make_response( jsonify({ 'username': username, 'temp_token': create_access_token(username, expires_delta=timedelta(seconds=60)) }), 201)
def unshare_post(post_id): """Remove a share from a given post.""" username = get_jwt_identity() login = Login.query.filter_by(username=username).first() if login is None: raise APIError(400, "User {username} does not exist on this server".format(username=username)) share_ids = [p.id for p in login.user.sharing] if post_id in share_ids: post = login.user.sharing[share_ids.index(post_id)] del login.user.sharing[share_ids.index(post_id)] login.last_action = datetime.now() db.session.add(post, login) db.session.commit() return jsonify(sharing=[p.id for p in login.user.sharing]), 200 else: # Trying to remove the share from a post that isn't shared raise APIError(404, "User has not shared this post")
def get_posts(): """Retrieve posts known to this server. This can be filtered by origin, and takes the following query parameters: * max: The maximum number of records to return * page: The "page" of records * origin: If specified, only those users with this origin will be returned """ error_on_unauthorized() posts = Post.query.order_by(Post.id) total_num = posts.count() if total_num == 0: return jsonify(total=0, uploads=[]) try: count = int(request.args.get('max', total_num)) page = int(request.args.get('page', 1)) origin = request.args.get('origin', None) if count <= 0 or page <= 0: raise APIError(422, "Query parameters out of range") if origin is not None: posts = posts.filter(User.origin == origin) begin = (page - 1) * count end = min(begin + count, total_num) return jsonify(total=total_num, posts=[p.to_dict() for p in posts.all()[begin:end]]), 200 except ValueError: raise APIError(422, "Invalid query parameter")
def get_my_profile(): """Get the profile for the logged-in user. (This may have sensitive/private info.)""" username = get_jwt_identity() origin = current_app.config['SERVER_ORIGIN'] profile = User.query.filter_by(username=username, origin=origin).first() if profile is None: # This shouldn't happen. raise APIError(400, "User {username} does not exist on this server".format(username=username)) # Update last activity time profile.login.last_action = datetime.now() return make_response(jsonify(profile=profile.to_profile_dict()), 200)
def get_users(): """Retrieve users known to this server, whether local or foreign. Can use the following query parameters: * max: The maximum number of records to return * page: The "page" of records * origin: If specified, only those users with this origin will be returned """ error_on_unauthorized() users = User.query.order_by(User.id) total_num = users.count() if total_num == 0: return jsonify(total=0, uploads=[]) try: count = int(request.args.get('max', total_num)) page = int(request.args.get('page', 1)) origin = request.args.get('origin', None) if count <= 0 or page <= 0: raise APIError(422, "Query parameters out of range") if origin is not None: users = users.filter_by(origin=origin) begin = (page - 1) * count end = min(begin + count, total_num) return jsonify(total=total_num, users=[u.to_dict() for u in users.all()[begin:end]]), 200 except ValueError: raise APIError(422, "Invalid query parameter")
def share_post(post_id): """Share (aka boost/announce) a given post.""" username = get_jwt_identity() login = Login.query.filter_by(username=username).first() if login is None: raise APIError(400, "User {username} does not exist on this server".format(username=username)) if post_id not in [p.id for p in login.user.sharing]: post = Post.query.get(post_id) login.user.sharing.append(post) login.last_action = datetime.now() db.session.add(post, login) db.session.commit() return jsonify(sharing=[p.id for p in login.user.sharing]), 201
def like_post(post_id): """Like the post with a given ID.""" username = get_jwt_identity() login = Login.query.filter_by(username=username).first() if login is None: raise APIError(400, "User {username} does not exist on this server".format(username=username)) if post_id not in [p.id for p in login.user.likes]: post = Post.query.get(post_id) login.user.likes.append(post) login.last_action = datetime.now() db.session.add(post, login) db.session.commit() return jsonify({ 'likes': [p.id for p in login.user.likes] }), 201
def get_my_info(): """Get the likes, shares, and following/followed lists for the logged-in user.""" username = get_jwt_identity() origin = current_app.config['SERVER_ORIGIN'] profile = User.query.filter_by(username=username, origin=origin).first() if profile is None: # This shouldn't happen. raise APIError(400, "User {username} does not exist on this server".format(username=username)) # Update last activity time profile.login.last_action = datetime.now() return jsonify( likes=[l.id for l in profile.likes], shares=[s.id for s in profile.sharing], followers=[f.id for f in profile.followers], following=[f.id for f in profile.following] ), 200
def login(args): """Log in to this server, receiving an authentication token in response.""" username = args['username'] password = args['password'] login = Login.query.filter_by(username=username).first() if login is None or not login.check_password(password): # Best security practice is to avoid telling a user whether # the username or password is incorrect. raise APIError(401, "Invalid username or password") login.last_login = datetime.now() login.last_action = datetime.now() db.session.add(login) db.session.commit() token = create_access_token(username) refresh = create_refresh_token(username) return jsonify(access_token=token, refresh_token=refresh, role=login.role.name), 200
def create_account_get(): """This endpoint does not support GET, so send a formatted response.""" raise APIError(405, "POST to this endpoint to create an account")