Esempio n. 1
0
 constructor(config) {
     const defaults = {
         account: undefined,
     };
     const serviceOptions = getDefaultOptions(defaults, config);
     this.serviceOptions = serviceOptions;
 }
Esempio n. 2
0
    /**
     *  Bulk creates user accounts and adds them to a group. Input userlist is typically the string contents of a textarea with user data.
     * 
     * @example
     * um.upload($('#textareaWithUsers').val());
     * 
     * @param {string} userList list of users seperated by newlines, with each line containing email, firstname, lastname, password separated by tabs/commas
     * @param {string} [groupId] id of group to upload to. Defaults to getting current group from session
     * @param {object} [options]  overrides for service options
     * @returns {JQuery.Promise}
     */
    uploadUsersToGroup(userList, groupId, options) {
        if (!userList || !userList.trim()) {
            return $.Deferred().reject({ 
                type: ERROR_TYPES.EMPTY_USERS,
                message: 'uploadUsersToGroup: No users specified to upload.' 
            }).promise();
        }
        const serviceOptions = getDefaultOptions(this.serviceOptions, options);
        if (!groupId) {
            const am = new AuthManager(serviceOptions);
            const session = am.getCurrentUserSessionInfo();
            groupId = session.groupId;

            if (!groupId) {
                return $.Deferred().reject({ 
                    type: ERROR_TYPES.NO_GROUP_PROVIDED,
                    message: 'uploadUsersToGroup: No group specified, and no session available to pick from.' 
                }).promise();
            }
        }

        const usersToAdd = parseUsers(userList.trim());
        if (!usersToAdd.valid.length) {
            return $.Deferred().resolve({
                errors: usersToAdd.invalid,
                created: [],
                duplicates: []
            }).promise();
        }

        const userService = new UserService(serviceOptions);
        const memberService = new MemberService(serviceOptions);
        return userService.createUsers(usersToAdd.valid).then((userRes)=> {
            const validUsers = [].concat(userRes.saved, userRes.updated, userRes.duplicate);
            const validIds = validUsers.map((u)=> u.id);
            const userWithErrors = userRes.errors.map((e)=> {
                return $.extend(true, e, {
                    reason: ERROR_TYPES.API_REJECT,
                    context: e
                });
            });
            userRes.errors = [].concat(userWithErrors, usersToAdd.invalid);
            return memberService.addUsersToGroup(validIds, groupId).then(()=> userRes, function handleMemberError(memberXHR) {
                const memberErr = memberXHR.responseJSON;
                const isGroupLimitErr = memberErr && memberErr.message && memberErr.message.match(/exceeded your group limit\(([0-9]+)\)/i);
                if (!isGroupLimitErr) {
                    throw memberErr;
                }
                
                const groupLimit = +isGroupLimitErr[1];
                const skippedUsers = validUsers.slice(groupLimit).map((u)=> {
                    return $.extend({}, u, { reason: ERROR_TYPES.GROUP_LIMIT_EXCEEDED, message: 'Exceeded group limit' });
                });
                
                function excludingSkipped(users, skipped) {
                    return users.filter((u)=> {
                        const isValid = !skipped.find((su)=> su.userName === u.userName);
                        return isValid;
                    });
                }
                return {
                    errors: [].concat(userRes.errors, skippedUsers),
                    saved: excludingSkipped(userRes.saved, skippedUsers),
                    updated: excludingSkipped(userRes.updated, skippedUsers),
                    duplicate: excludingSkipped(userRes.duplicate, skippedUsers),
                };
            });
        }).then((res)=> {
            return {
                errors: res.errors,
                duplicates: res.duplicate, //pluralizing for consistency
                created: [].concat(res.saved, res.updated), //no real distinction between the two so combining
            };
        });
    }
Esempio n. 3
0
/**
 * @description
 * 
 * ## World API Adapter
 *
 * A [run](../../../glossary/#run) is a collection of end user interactions with a project and its model -- including setting variables, making decisions, and calling operations. For building multiplayer simulations you typically want multiple end users to share the same set of interactions, and work within a common state. Epicenter allows you to create "worlds" to handle such cases. Only [team projects](../../../glossary/#team) can be multiplayer.
 *
 * The World API Adapter allows you to create, access, and manipulate multiplayer worlds within your Epicenter project. You can use this to add and remove end users from the world, and to create, access, and remove their runs. Because of this, typically the World Adapter is used for facilitator pages in your project. (The related [World Manager](../world-manager/) provides an easy way to access runs and worlds for particular end users, so is typically used in pages that end users will interact with.)
 *
 * As with all the other [API Adapters](../../), all methods take in an "options" object as the last parameter. The options can be used to extend/override the World API Service defaults.
 *
 * To use the World Adapter, instantiate it and then access the methods provided. Instantiating requires the account id (**Team ID** in the Epicenter user interface), project id (**Project ID**), and group (**Group Name**).
 *
 *       var wa = new F.service.World({
 *          account: 'acme-simulations',
 *          project: 'supply-chain-game',
 *          group: 'team1' });
 *       wa.create()
 *          .then(function(world) {
 *              // call methods, e.g. wa.addUsers()
 *          });
 * 
 * @param {AccountAPIServiceOptions} config 
 * @property {string} [group] The group name to use for filters / new runs
 * @property {string} [model] The model file to use to create runs in this world.
 * @property {string} [filter] Criteria by which to filter world. Currently only supports world-ids as filters.
 * @property {string} [id] Convenience alias for filter.
 */
export default function WorldAPIAdapter(config) {
    var defaults = {
        group: undefined,
        model: undefined,
        filter: '',
        id: '',

        token: undefined,
        account: undefined,
        project: undefined,

        transport: {},
    };

    const serviceOptions = getDefaultOptions(defaults, config, {
        apiEndpoint: apiEndpoint
    });
    if (serviceOptions.id) {
        serviceOptions.filter = serviceOptions.id;
    }
    const urlConfig = getURLConfig(serviceOptions);
    const http = new TransportFactory(serviceOptions.transport);

    var setIdFilterOrThrowError = function (options) {
        if (!options) options = {};
        
        if (options.id) {
            serviceOptions.filter = options.id;
        } else if (options.filter) {
            serviceOptions.filter = options.filter;
        }
        if (!serviceOptions.filter) {
            throw new Error('No world id specified to apply operations against. This could happen if the user is not assigned to a world and is trying to work with runs from that world.');
        }
    };

    var validateModelOrThrowError = function (options) {
        if (!options || !options.model) {
            throw new Error('No model specified to get the current run');
        }
    };

    var publicAPI = {
        /**
        * Creates a new World.
        *
        * Using this method is rare. It is more common to create worlds automatically while you `autoAssign()` end users to worlds. (In this case, configuration data for the world, such as the roles, are read from the project-level world configuration information, for example by `getProjectSettings()`.)
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create({
        *      roles: ['VP Marketing', 'VP Sales', 'VP Engineering']
        *  });
        *
        *  
        * @param {object} params Parameters to create the world.
        * @param {string} [params.group] The **Group Name** to create this world under. Only end users in this group are eligible to join the world. Optional here; required when instantiating the service (`new F.service.World()`).
        * @param {object} [params.roles] The list of roles (strings) for this world. Some worlds have specific roles that **must** be filled by end users. Listing the roles allows you to autoassign users to worlds and ensure that all roles are filled in each world.
        * @param {object} [params.optionalRoles] The list of optional roles (strings) for this world. Some worlds have specific roles that **may** be filled by end users. Listing the optional roles as part of the world object allows you to autoassign users to worlds and ensure that all roles are filled in each world.
        * @param {integer} [params.minUsers] The minimum number of users for the world. Including this number allows you to autoassign end users to worlds and ensure that the correct number of users are in each world.
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        create: function (params, options) {
            var createOptions = $.extend(true, {}, serviceOptions, options);
            var worldApiParams = ['scope', 'files', 'roles', 'optionalRoles', 'minUsers', 'group', 'name'];
            var validParams = _pick(serviceOptions, ['account', 'project', 'group']);
            // whitelist the fields that we actually can send to the api
            params = _pick(params, worldApiParams);

            // account and project go in the body, not in the url
            params = $.extend({}, validParams, params);

            var oldSuccess = createOptions.success;
            createOptions.success = function (response) {
                serviceOptions.filter = response.id; //all future chained calls to operate on this id
                return oldSuccess.apply(this, arguments);
            };

            return http.post(params, createOptions);
        },

        /**
        * Updates a World, for example to replace the roles in the world.
        *
        * Typically, you complete world configuration at the project level, rather than at the world level. For example, each world in your project probably has the same roles for end users. And your project is probably either configured so that all end users share the same world (and run), or smaller sets of end users share worlds — but not both. However, this method is available if you need to update the configuration of a particular world.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          wa.update({ roles: ['VP Marketing', 'VP Sales', 'VP Engineering'] });
        *      });
        *
        *  
        * @param {object} params Parameters to update the world.
        * @param {string} params.name A string identifier for the linked end users, for example, "name": "Our Team".
        * @param {object} [params.roles] The list of roles (strings) for this world. Some worlds have specific roles that **must** be filled by end users. Listing the roles allows you to autoassign users to worlds and ensure that all roles are filled in each world.
        * @param {object} [params.optionalRoles] The list of optional roles (strings) for this world. Some worlds have specific roles that **may** be filled by end users. Listing the optional roles as part of the world object allows you to autoassign users to worlds and ensure that all roles are filled in each world.
        * @param {integer} [params.minUsers] The minimum number of users for the world. Including this number allows you to autoassign end users to worlds and ensure that the correct number of users are in each world.
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        update: function (params, options) {
            var whitelist = ['roles', 'optionalRoles', 'minUsers', 'name'];
            options = options || {};
            setIdFilterOrThrowError(options);

            var updateOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter }
            );

            params = _pick(params || {}, whitelist);

            return http.patch(params, updateOptions);
        },

        /**
        * Deletes an existing world.
        *
        * This function optionally takes one argument. If the argument is a string, it is the id of the world to delete. If the argument is an object, it is the override for global options.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          wa.delete();
        *      });
        *
        *  
        * @param {string|Object} [options] The id of the world to delete, or options object to override global options.
        * @return {Promise}
        */
        delete: function (options) {
            options = (options && (typeof options === 'string')) ? { filter: options } : {};
            setIdFilterOrThrowError(options);

            var deleteOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter }
            );

            return http.delete(null, deleteOptions);
        },

        /**
        * Updates the configuration for the current instance of the World API Adapter (including all subsequent function calls, until the configuration is updated again).
        *
        * @example
        *      var wa = new F.service.World({...}).updateConfig({ filter: '123' }).addUser({ userId: '123' });
        *
        * 
        * @param {object} config The configuration object to use in updating existing configuration.
        * @return {Object} reference to current instance
        */
        updateConfig: function (config) {
            $.extend(serviceOptions, config);
            return this;
        },

        /**
        * Lists all worlds for a given account, project, and group. All three are required, and if not specified as parameters, are read from the service.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          // lists all worlds in group "team1"
        *          wa.list();
        *
        *          // lists all worlds in group "other-group-name"
        *          wa.list({ group: 'other-group-name' });
        *      });
        *
        *  
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        list: function (options) {
            var getOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) }
            );

            var filters = _pick(getOptions, ['account', 'project', 'group']);

            return http.get(filters, getOptions);
        },

        /**
         * Load information for a specific world. All further calls to the world service will use the id provided.
         *
         * 
         * @param {string} worldId The id of the world to load.
         * @param {Object} [options]] Options object to override global options.
         * @return {Promise}
         */
        load: function (worldId, options) {
            if (worldId) {
                serviceOptions.filter = worldId;
            }
            if (!serviceOptions.filter) {
                throw new Error('Please provide a worldid to load');
            }
            var httpOptions = $.extend(true, {}, serviceOptions, options, { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter + '/' });
            return http.get('', httpOptions);
        },

        /**
        * Gets all worlds that an end user belongs to for a given account (team), project, and group.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          wa.getWorldsForUser('b1c19dda-2d2e-4777-ad5d-3929f17e86d3')
        *      });
        *
        * @param {string} userId The `userId` of the user whose worlds are being retrieved.
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        getWorldsForUser: function (userId, options) {
            var getOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) }
            );

            var filters = $.extend(
                _pick(getOptions, ['account', 'project', 'group']),
                { userId: userId }
            );

            return http.get(filters, getOptions);
        },

        /**
        * Adds an end user or list of end users to a given world. The end user must be a member of the `group` that is associated with this world.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          // add one user
        *          wa.addUsers('b1c19dda-2d2e-4777-ad5d-3929f17e86d3');
        *          wa.addUsers(['b1c19dda-2d2e-4777-ad5d-3929f17e86d3']);
        *          wa.addUsers({ userId: 'b1c19dda-2d2e-4777-ad5d-3929f17e86d3', role: 'VP Sales' });
        *
        *          // add several users
        *          wa.addUsers([
        *              { userId: 'a6fe0c1e-f4b8-4f01-9f5f-01ccf4c2ed44',
        *                role: 'VP Marketing' },
        *              { userId: '8f2604cf-96cd-449f-82fa-e331530734ee',
        *                role: 'VP Engineering' }
        *          ]);
        *
        *          // add one user to a specific world
        *          wa.addUsers('b1c19dda-2d2e-4777-ad5d-3929f17e86d3', world.id);
        *          wa.addUsers('b1c19dda-2d2e-4777-ad5d-3929f17e86d3', { filter: world.id });
        *      });
        *
        * @param {string|object|array} users User id, array of user ids, object, or array of objects of the users to add to this world.
        * @param {string} users.role The `role` the user should have in the world. It is up to the caller to ensure, if needed, that the `role` passed in is one of the `roles` or `optionalRoles` of this world.
        * @param {string} worldId The world to which the users should be added. If not specified, the filter parameter of the `options` object is used.
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        addUsers: function (users, worldId, options) {
            if (!users) {
                throw new Error('Please provide a list of users to add to the world');
            }

            // normalize the list of users to an array of user objects
            users = ([].concat(users)).map(function (u) {
                var isObject = $.isPlainObject(u);
                if (typeof u !== 'string' && !isObject) {
                    throw new Error('Some of the users in the list are not in the valid format: ' + u);
                }
                return isObject ? u : { userId: u };
            });

            // check if options were passed as the second parameter
            if ($.isPlainObject(worldId) && !options) {
                options = worldId;
                worldId = null;
            }

            options = options || {};

            // we must have options by now
            if (typeof worldId === 'string') {
                options.filter = worldId;
            }

            setIdFilterOrThrowError(options);

            var updateOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter + '/users' }
            );

            return http.post(users, updateOptions);
        },

        /**
        * Updates the role of an end user in a given world. (You can only update one end user at a time.)
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        *
        * wa.create().then(function(world) {
        *      wa.addUsers('b1c19dda-2d2e-4777-ad5d-3929f17e86d3');
        *      wa.updateUser({ userId: 'b1c19dda-2d2e-4777-ad5d-3929f17e86d3', role: 'leader' });
        * });
        *
        * 
        * @param {{userId: string, role: string}} user User object with `userId` and the new `role`.
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        updateUser: function (user, options) {
            if (!user || !user.userId) {
                throw new Error('You need to pass a userId to update from the world');
            }

            setIdFilterOrThrowError(options);
            const validFields = ['role'];
            var patchOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter + '/users/' + user.userId }
            );

            return http.patch(_pick(user, validFields), patchOptions);
        },

        /**
        * Removes an end user from a given world.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          wa.addUsers(['a6fe0c1e-f4b8-4f01-9f5f-01ccf4c2ed44', '8f2604cf-96cd-449f-82fa-e331530734ee']);
        *          wa.removeUser('a6fe0c1e-f4b8-4f01-9f5f-01ccf4c2ed44');
        *          wa.removeUser({ userId: '8f2604cf-96cd-449f-82fa-e331530734ee' });
        *      });
        *
        * @param {object|string} user The `userId` of the user to remove from the world, or an object containing the `userId` field.
        * @param {object} [options] Options object to override global options.
        * @param {boolean} [options.deleteWorldIfEmpty] Delete the world if you removed the last user.
        * @return {Promise}
        */
        removeUser: function (user, options) {
            if (typeof user === 'string') {
                user = { userId: user };
            }

            if (!user.userId) {
                throw new Error('You need to pass a userId to remove from the world');
            }

            const mergedOptions = $.extend(true, {}, serviceOptions, options);
            setIdFilterOrThrowError(mergedOptions);

            const autoDeleteWorld = options && options.deleteWorldIfEmpty === true;
            let url = urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter + '/users/' + user.userId;
            if (autoDeleteWorld) {
                url += '?deleteWorld=true';
            }
            mergedOptions.url = url;

            return http.delete(null, mergedOptions);
        },

        /**
        * Gets the run id of current run for the given world. If the world does not have a run, creates a new one and returns the run id.
        *
        * Remember that a [run](../../glossary/#run) is a collection of interactions with a project and its model. In the case of multiplayer projects, the run is shared by all end users in the world.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.create()
        *      .then(function(world) {
        *          wa.getCurrentRunId({ model: 'model.py' });
        *      });
        *
        * @param {object} [options] Options object to override global options.
        * @param {object} options.model The model file to use to create a run if needed.
        * @return {Promise}
        */
        getCurrentRunId: function (options) {
            setIdFilterOrThrowError(options);

            var postParams = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter + '/run' }
            );

            validateModelOrThrowError(postParams);
            const validRunParams = extractValidRunParams(postParams);
            return http.post(validRunParams, postParams);
        },

        /**
        * Gets the current (most recent) world for the given end user in the given group. Brings this most recent world into memory if needed.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        * wa.getCurrentWorldForUser('8f2604cf-96cd-449f-82fa-e331530734ee')
        *      .then(function(world) {
        *          // use data from world
        *      });
        *
        * @param {string} userId The `userId` of the user whose current (most recent) world is being retrieved.
        * @param {string} [groupName] The name of the group. If not provided, defaults to the group used to create the service.
        * @return {JQuery.Promise}
        */
        getCurrentWorldForUser: function (userId, groupName) {
            var dtd = $.Deferred();
            var me = this;
            this.getWorldsForUser(userId, { group: groupName })
                .then(function (worlds) {
                    // assume the most recent world as the 'active' world
                    worlds.sort(function (a, b) { return +(new Date(b.lastModified)) - +(new Date(a.lastModified)); });
                    var currentWorld = worlds[0];

                    if (currentWorld) {
                        serviceOptions.filter = currentWorld.id;
                    }

                    dtd.resolveWith(me, [currentWorld]);
                })
                .catch(dtd.reject);

            return dtd.promise();
        },

        /**
        * Deletes the current run from the world.
        *
        * (Note that the world id remains part of the run record, indicating that the run was formerly an active run for the world.)
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        *
        * wa.deleteRun('sample-world-id');
        *
        * @param {string} worldId The `worldId` of the world from which the current run is being deleted.
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        deleteRun: function (worldId, options) {
            options = options || {};
            if (worldId) {
                options.filter = worldId;
            }

            setIdFilterOrThrowError(options);

            var deleteOptions = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(apiEndpoint) + serviceOptions.filter + '/run' }
            );

            return http.delete(null, deleteOptions);
        },

        /**
        * Creates a new run for the world.
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        *
        * wa.getCurrentWorldForUser('8f2604cf-96cd-449f-82fa-e331530734ee')
        *      .then(function (world) {
        *              wa.newRunForWorld(world.id);
        *      });
        *
        * @param {string} worldId worldId in which we create the new run.
        * @param {object} [options] Options object to override global options.
        * @param {object} options.model The model file to use to create a run if needed.
        * @return {Promise}
        */
        newRunForWorld: function (worldId, options) {
            var currentRunOptions = $.extend(true, {},
                serviceOptions,
                options,
                { filter: worldId || serviceOptions.filter }
            );
            var me = this;

            validateModelOrThrowError(currentRunOptions);

            return this.deleteRun(worldId, options)
                .then(function () {
                    return me.getCurrentRunId(currentRunOptions);
                });
        },

        /**
        * Assigns end users to worlds, creating new worlds as appropriate, automatically. Assigns all end users in the group, and creates new worlds as needed based on the project-level world configuration (roles, optional roles, and minimum end users per world).
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        *
        * wa.autoAssign();
        *
        * 
        * @param {object} [options] Options object to override global options.
        * @param {number} options.maxUsers Sets the maximum number of users in a world.
        * @param {string[]} options.userIds A list of users to be assigned be assigned instead of all end users in the group.
        * @return {Promise}
        */
        autoAssign: function (options) {
            var opt = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(assignmentEndpoint) }
            );

            var params = {
                account: opt.account,
                project: opt.project,
                group: opt.group
            };

            if (opt.maxUsers) {
                params.maxUsers = opt.maxUsers;
            }

            if (opt.userIds) {
                params.userIds = opt.userIds;
            }

            return http.post(params, opt);
        },

        /**
        * Gets the project's world configuration.
        *
        * Typically, every interaction with your project uses the same configuration of each world. For example, each world in your project probably has the same roles for end users. And your project is probably either configured so that all end users share the same world (and run), or smaller sets of end users share worlds — but not both.
        *
        * (The [Multiplayer Project REST API](../../../rest_apis/multiplayer/multiplayer_project/) allows you to set these project-level world configurations. The World Adapter simply retrieves them, for example so they can be used in auto-assignment of end users to worlds.)
        *
        * @example
        * var wa = new F.service.World({
        *      account: 'acme-simulations',
        *      project: 'supply-chain-game',
        *      group: 'team1' });
        *
        * wa.getProjectSettings()
        *      .then(function(settings) {
        *          console.log(settings.roles);
        *          console.log(settings.optionalRoles);
        *      });
        *
        * 
        * @param {object} [options] Options object to override global options.
        * @return {Promise}
        */
        getProjectSettings: function (options) {
            var opt = $.extend(true, {},
                serviceOptions,
                options,
                { url: urlConfig.getAPIPath(projectEndpoint) }
            );
            return http.get(null, opt);
        },

        /**
         * Get an instance of a consensus service for current world
         * 
         * @param {string|{ consensusGroup: string, name: string}} conOpts creates a consensus with an optional group name. If not specified, created under the 'default' group
         * @param {object} [options] Overrides for service options
         * @returns {ConsensusService}
         */
        consensus: function (conOpts, options) {
            var opts = $.extend(true, {}, serviceOptions, options);
            const worldId = opts.filter || opts.id;
            if (!worldId) {
                throw new Error('No world id provided; use consensus(name, { id: worldid})');
            }
            if (!conOpts) {
                throw new Error('No consensus name provided; use consensus(name, { id: worldid})');
            }

            function extractNamesFromOpts(nameOpts) {
                if (typeof nameOpts === 'string') {
                    return {
                        name: nameOpts
                    };
                }
                if ($.isPlainObject(nameOpts)) {
                    return {
                        consensusGroup: nameOpts.consensusGroup,
                        name: nameOpts.name
                    };
                }
            }
            var con = new ConsensusService($.extend(true, {
                worldId: worldId,
            }, opts, extractNamesFromOpts(conOpts)));
            return con;
        },

        /**
         * @param {string|{users: object} } world World to get users from.
         * @param {object} [options] Overrides for service options
         * @returns {Promise}
         */
        getPresenceForUsers: function (world, options) {
            const opts = $.extend(true, {}, serviceOptions, options);
            const getUsersForWorld = (world, opts)=> {
                if (world && world.users) {
                    return $.Deferred().resolve(world).promise();
                }
                const worldid = world || opts.filter || opts.id;
                return this.load(worldid).then((w)=> w);
            };

            const ps = new PresenceService(opts);
            const worldLoadPromise = getUsersForWorld(world, opts);
            return worldLoadPromise.then((world)=> {
                return ps.getStatusForUsers(world.users);
            });
        }
    };
    $.extend(this, publicAPI);
}