oaeproject/Hilary

View on GitHub
packages/oae-following/lib/api.js

Summary

Maintainability
D
1 day
Test Coverage
A
93%
/*!
 * Copyright 2014 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

import _ from 'underscore';

import * as AuthzUtil from 'oae-authz/lib/util.js';
import * as EmitterAPI from 'oae-emitter';
import * as OaeUtil from 'oae-util/lib/util.js';
import * as PrincipalsDAO from 'oae-principals/lib/internal/dao.js';
import * as PrincipalsUtil from 'oae-principals/lib/util.js';
import * as FollowingAuthz from 'oae-following/lib/authz.js';

import { Validator as validator } from 'oae-authz/lib/validator.js';
import { FollowingConstants } from 'oae-following/lib/constants.js';
import * as FollowingDAO from './internal/dao.js';

const { unless, isUserId, isLoggedInUser } = validator;

/**
 * ### Events
 *
 * The `FollowingAPI`, as enumerated in `FollowingConstants.events`, emits the following events:
 *
 *  * `follow(ctx, followerUser, followedUser)`: One user followed another user. The `ctx` of the current request, the `followerUser` (the user who became a follower) and the `followedUser` (the user who was followed) are all provided
 *  * `unfollow(ctx, followerUser, unfollowedUserId)`: One user unfollowed another user. The `ctx` of the current request, the `followerUser` (the user who unfollowed another user) and the `followedUserId` (the id of the user who is unfollowed) are all provided
 */
const FollowingAPI = new EmitterAPI.EventEmitter();

/**
 * Get the users who are following a user
 *
 * @param  {Context}    ctx                 Current execution context
 * @param  {String}     userId              The id of the user whose followers to get
 * @param  {String}     [start]             From where to start fetching the page of followers, as specified by the `nextToken` return param
 * @param  {Number}     [limit]             The maximum number of followers to retrieve
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {User[]}     callback.followers  The followers of the specified user
 * @param  {String}     callback.nextToken  The token to use as the `start` parameter when fetching the next page of followers
 */
const getFollowers = function (ctx, userId, start, limit, callback) {
  limit = OaeUtil.getNumberParam(limit, 10, 1);

  try {
    unless(isUserId, {
      code: 400,
      msg: 'You must specify a valid user id'
    })(userId);
  } catch (error) {
    return callback(error);
  }

  // Get the user so we can determine their visibility and permissions
  PrincipalsDAO.getPrincipal(userId, (error, user) => {
    if (error) {
      return callback(error);
    }

    // Determine if the current user has access to view the followers
    FollowingAuthz.canViewFollowers(ctx, user, (error_) => {
      if (error_) {
        return callback(error_);
      }

      // Get the list of followers
      FollowingDAO.getFollowers(userId, start, limit, (error, followerUserIds, nextToken) => {
        if (error) {
          return callback(error);
        }

        AuthzUtil.filterDeletedIds(followerUserIds, (error, followerUserIds) => {
          if (error) {
            return callback(error);
          }

          // Expand the list of followers into their basic profiles
          _expandUserIds(ctx, followerUserIds, (error, users) => {
            if (error) {
              return callback(error);
            }

            // Emit an event indicating that the followers for a user have been retrieved
            FollowingAPI.emit(FollowingConstants.events.GET_FOLLOWERS, ctx, userId, start, limit, users, (error_) => {
              if (error_) {
                return callback(error_);
              }

              return callback(null, users, nextToken);
            });
          });
        });
      });
    });
  });
};

/**
 * Get the users who are followed by a specific user
 *
 * @param  {Context}    ctx                 Current execution context
 * @param  {String}     userId              The id of the user whose list of followed users to get
 * @param  {String}     [start]             From where to start fetching the page of followed users, as specified by the `nextToken` return param
 * @param  {Number}     [limit]             The maximum number of followed users to retrieve
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 * @param  {User[]}     callback.followed   The list of users who are being followed by the specified user
 * @param  {String}     callback.nextToken  The token to use as the `start` parameter when fetching the next page of followed users
 */
const getFollowing = function (ctx, userId, start, limit, callback) {
  limit = OaeUtil.getNumberParam(limit, 10, 1);

  try {
    unless(isUserId, {
      code: 400,
      msg: 'You must specify a valid user id'
    })(userId);
  } catch (error) {
    return callback(error);
  }

  // Get the user so we can determine their visibility and permissions
  PrincipalsDAO.getPrincipal(userId, (error, user) => {
    if (error) {
      return callback(error);
    }

    // Determine if the current user has access to view the list of followed users
    FollowingAuthz.canViewFollowing(ctx, user, (error_) => {
      if (error_) {
        return callback(error_);
      }

      // Get the list of followed user ids
      FollowingDAO.getFollowing(userId, start, limit, (error, followingUserIds, nextToken) => {
        if (error) {
          return callback(error);
        }

        // Remove those that have been deleted
        AuthzUtil.filterDeletedIds(followingUserIds, (error, followingUserIds) => {
          if (error) {
            return callback(error);
          }

          // Expand the user ids into the list of basic user profiles
          _expandUserIds(ctx, followingUserIds, (error, users) => {
            if (error) {
              return callback(error);
            }

            // Emit an event indicating that the followed users for a user have been retrieved
            FollowingAPI.emit(FollowingConstants.events.GET_FOLLOWING, ctx, userId, start, limit, users, (error_) => {
              if (error_) {
                return callback(error_);
              }

              return callback(null, users, nextToken);
            });
          });
        });
      });
    });
  });
};

/**
 * Follow a user
 *
 * @param  {Context}    ctx             Current execution context
 * @param  {String}     followedUserId  The id of the user to follow
 * @param  {Function}   callback        Standard callback function
 * @param  {Object}     callback.err    An error that occurred, if any
 */
const follow = function (ctx, followedUserId, callback) {
  try {
    unless(isLoggedInUser, {
      code: 401,
      msg: 'You must be authenticated to follow a user'
    })(ctx);

    unless(isUserId, {
      code: 400,
      msg: 'You must specify a valid user id of a user to follow'
    })(followedUserId);
  } catch (error) {
    return callback(error);
  }

  // Get the user to follow to perform permission checks
  PrincipalsDAO.getPrincipal(followedUserId, (error, followedUser) => {
    if (error) {
      return callback(error);
    }

    // Determine if the current user is allowed to follow this user
    FollowingAuthz.canFollow(ctx, followedUser, (error_) => {
      if (error_) {
        return callback(error_);
      }

      FollowingDAO.isFollowing(ctx.user().id, [followedUserId], (error, following) => {
        if (error) {
          return callback(error);
        }

        if (following[followedUserId]) {
          // The user is already following the target user, so we don't
          // have to do anything
          return callback();
        }

        // Save the new list of followed users for the current user
        FollowingDAO.saveFollows(ctx.user().id, [followedUserId], (error_) => {
          if (error_) {
            return callback(error_);
          }

          return FollowingAPI.emit(FollowingConstants.events.FOLLOW, ctx, ctx.user(), followedUser, callback);
        });
      });
    });
  });
};

/**
 * Unfollow a user
 *
 * @param  {Context}    ctx                 Current execution context
 * @param  {String}     unfollowedUserId    The id of the user to unfollow
 * @param  {Function}   callback            Standard callback function
 * @param  {Object}     callback.err        An error that occurred, if any
 */
const unfollow = function (ctx, unfollowedUserId, callback) {
  try {
    unless(isLoggedInUser, {
      code: 401,
      msg: 'You must be authenticated to unfollow a user'
    })(ctx);

    unless(isUserId, {
      code: 400,
      msg: 'You must specify a valid user id of a user to unfollow'
    })(unfollowedUserId);
  } catch (error) {
    return callback(error);
  }

  // A user can always try and delete followers from their list of followers
  FollowingDAO.deleteFollows(ctx.user().id, [unfollowedUserId], (error) => {
    if (error) {
      return callback(error);
    }

    return FollowingAPI.emit(FollowingConstants.events.UNFOLLOW, ctx, ctx.user(), unfollowedUserId, callback);
  });
};

/**
 * Expand the array of user ids into the associated (scrubbed if necessary) basic user profiles array in the same order
 *
 * @param  {Context}    ctx             Current execution context
 * @param  {String[]}   userIds         The user ids to expand into basic profiles
 * @param  {Function}   callback        Standard callback function
 * @param  {Object}     callback.err    An error that occurred, if any
 * @param  {User[]}     callback.users  The basic user profiles of the users in the userIds array in the same order as the ids provided
 * @api private
 */
const _expandUserIds = function (ctx, userIds, callback) {
  if (_.isEmpty(userIds)) {
    return callback(null, []);
  }

  // Fetch and scrub the basic user profiles
  PrincipalsUtil.getPrincipals(ctx, userIds, (error, userProfiles) => {
    if (error) {
      return callback(error);
    }

    const userList = [];
    _.each(userIds, (userId) => {
      userList.push(userProfiles[userId]);
    });

    return callback(null, userList);
  });
};

/**
 * Remove all following from a user
 *
 * @param  {Context}    ctx             Current execution context
 * @param  {String}     user            The user to delete
 * @param  {Function}   callback        Standard callback function
 * @param  {Object}     callback.err    An error that occured, if any
 * @api private
 */
const deleteFollowing = function (ctx, user, callback) {
  const errorHandler = function (error) {
    if (error) return callback(error);
  };

  FollowingDAO.getFollowing(user.id, null, null, (error, userIdsFollowing) => {
    if (_.isEmpty(userIdsFollowing)) return callback();

    FollowingDAO.deleteFollows(user.id, userIdsFollowing, (error) => {
      if (error) return callback(error);

      for (const id of userIdsFollowing) {
        FollowingAPI.emit(FollowingConstants.events.UNFOLLOW, ctx, ctx.user(), id, errorHandler);
      }

      return callback();
    });
  });
};

/**
 * Remove all followers from a user
 *
 * @param  {Context}    ctx             Current execution context
 * @param  {String}     user            The user to delete
 * @param  {Function}   callback        Standard callback function
 * @param  {Object}     callback.err    An error that occured, if any
 * @api private
 */
const deleteFollowers = function (ctx, user, callback) {
  FollowingDAO.getFollowers(user.id, null, null, (error, userIdsFollowers) => {
    if (_.isEmpty(userIdsFollowers)) return callback();

    for (const id of userIdsFollowers) {
      FollowingDAO.deleteFollows(id, [user.id], (error) => {
        if (error) return callback(error);

        PrincipalsDAO.getPrincipal(user.id, (error, userUnfollowed) => {
          if (error) return callback(error);

          FollowingAPI.emit(FollowingConstants.events.UNFOLLOW, ctx, userUnfollowed, user.id, (error) => {
            if (error) return callback(error);
          });
        });
      });
    }

    return callback();
  });
};

export { FollowingAPI as emitter, getFollowers, getFollowing, follow, unfollow, deleteFollowing, deleteFollowers };