/** * @module "services.members" * @desc This module provides a service for searching for Topcoder * members via API V3. */ /* global XMLHttpRequest */ import _ from 'lodash'; import qs from 'qs'; import { decodeToken } from '@topcoder-platform/tc-auth-lib'; import logger from '../utils/logger'; import { getApiResponsePayload } from '../utils/tc'; import { getApi } from './api'; /** * Service class. */ class MembersService { /** * @param {String} tokenV3 Optional. Auth token for Topcoder API v3. */ constructor(tokenV3) { this.private = { api: getApi('V3', tokenV3), apiV5: getApi('V5', tokenV3), tokenV3, }; } /** * Gets member's financial information. * @param {String} handle User handle. * @return {Promise} Resolves to the financial information object. */ async getMemberFinances(handle) { const res = await this.private.api.get(`/members/${handle}/financial`); return getApiResponsePayload(res); } /** * Gets public information on a member. * * This method does not require any authorization. * * @param {String} handle Member handle. * @return {Promise} Resolves to the data object. */ async getMemberInfo(handle) { const res = await this.private.api.get(`/members/${handle}`); return getApiResponsePayload(res); } /** * Gets member external account info. * @param {String} handle * @return {Promise} Resolves to the stats object. */ async getExternalAccounts(handle) { const res = await this.private.api.get(`/members/${handle}/externalAccounts`); return getApiResponsePayload(res); } /** * Gets member external links. * @param {String} handle * @return {Promise} Resolves to the stats object. */ async getExternalLinks(handle) { const res = await this.private.api.get(`/members/${handle}/externalLinks`); return getApiResponsePayload(res); } /** * Gets member skills. * @param {String} handle * @return {Promise} Resolves to the stats object. */ async getSkills(handle) { const res = await this.private.api.get(`/members/${handle}/skills`); return getApiResponsePayload(res); } /** * Gets member statistics. * @param {String} handle * @param {Array<String>|String} groupIds * @return {Promise} Resolves to the stats object. */ async getStats(handle, groupIds) { if (!groupIds || (_.isArray(groupIds) && groupIds.length === 0)) { const res = await this.private.api.get(`/members/${handle}/stats`); return getApiResponsePayload(res); } const groupIdsArray = _.isArray(groupIds) ? groupIds : _.split(groupIds, ','); const groupIdChunks = _.chunk(groupIdsArray, 50); const getStatRequests = _.map(groupIdChunks, async (groupIdChunk) => { const res = await this.private.api.get(`/members/${handle}/stats?groupIds=${_.join(groupIdChunk)}`); return getApiResponsePayload(res, false); }); const results = await Promise.all(getStatRequests); return _.uniqBy( _.flatten( _.filter(results, _.isArray), ), item => item.groupId, ); } /** * Gets member Marathon Match rating. * @param {String} handle * @returns {Promise} resolves to the rating */ async getMMRating(handle) { const res = await this.private.apiV5.get(`/members/${handle}/stats`); const stats = await res.json(); if (stats.length === 1) { if (stats[0].DATA_SCIENCE != null && stats[0].DATA_SCIENCE.MARATHON_MATCH != null) { return stats[0].DATA_SCIENCE.MARATHON_MATCH.rank.rating; } return 0; } return 0; } /** * Gets member statistics history * @param {String} handle * @return {Promise} Resolves to the stats object. */ async getStatsHistory(handle, groupIds) { let res; if (groupIds) { res = await this.private.api.get(`/members/${handle}/stats/history?groupIds=${groupIds}`); } else { res = await this.private.api.get(`/members/${handle}/stats/history`); } return getApiResponsePayload(res); } /** * Gets member statistics distribution * @param {String} handle * @param {String} track * @param {String} subTrack * @return {Promise} Resolves to the stats object. */ async getStatsDistribution(handle, track, subTrack) { const res = await this.private.api.get(`/members/stats/distribution?filter=${encodeURIComponent(qs.stringify({ track, subTrack, }))}`); return getApiResponsePayload(res); } /** * Gets a list of suggested member names for the supplied partial. * * WARNING: This method requires v3 authorization. * * @param {String} keyword Partial string to find suggestions for * @return {Promise} Resolves to the api response content */ async getMemberSuggestions(keyword) { const res = await this.private.api.get(`/members/_suggest/${keyword}`); return getApiResponsePayload(res); } /** * Adds external web link for member. * @param {String} userHandle The user handle * @param {String} webLink The external web link * @return {Promise} Resolves to the api response content */ async addWebLink(userHandle, webLink) { const res = await this.private.api.postJson(`/members/${userHandle}/externalLinks`, { param: { url: webLink } }); return getApiResponsePayload(res); } /** * Deletes external web link for member. * @param {String} userHandle The user handle * @param {String} webLinkHandle The external web link handle * @return {Promise} Resolves to the api response content */ async deleteWebLink(userHandle, webLinkHandle) { const body = { param: { handle: webLinkHandle, }, }; const res = await this.private.api.delete(`/members/${userHandle}/externalLinks/${webLinkHandle}`, JSON.stringify(body)); return getApiResponsePayload(res); } /** * Adds user skill. * @param {String} handle Topcoder user handle * @param {Number} skillTagId Skill tag id * @return {Promise} Resolves to operation result */ async addSkill(handle, skillTagId) { const body = { param: { skills: { [skillTagId]: { hidden: false, }, }, }, }; const res = await this.private.api.patchJson(`/members/${handle}/skills`, body); return getApiResponsePayload(res); } /** * Hides user skill. * @param {String} handle Topcoder user handle * @param {Number} skillTagId Skill tag id * @return {Promise} Resolves to operation result */ async hideSkill(handle, skillTagId) { const body = { param: { skills: { [skillTagId]: { hidden: true, }, }, }, }; const res = await this.private.api.fetch(`/members/${handle}/skills`, { body: JSON.stringify(body), method: 'PATCH', }); return getApiResponsePayload(res); } /** * Updates member profile. * @param {Object} profile The profile to update. * @return {Promise} Resolves to the api response content */ async updateMemberProfile(profile) { const url = profile.verifyUrl ? `/members/${profile.handle}?verifyUrl=${profile.verifyUrl}` : `/members/${profile.handle}`; const res = await this.private.api.putJson(url, { param: profile.verifyUrl ? _.omit(profile, ['verifyUrl']) : profile }); if (profile.verifyUrl && res.status === 409) { return Promise.resolve(Object.assign({}, profile, { isEmailConflict: true })); } return getApiResponsePayload(res); } /** * Gets presigned url for member photo file. * @param {String} userHandle The user handle * @param {File} file The file to get its presigned url * @return {Promise} Resolves to the api response content */ async getPresignedUrl(userHandle, file) { const res = await this.private.api.postJson(`/members/${userHandle}/photoUploadUrl`, { param: { contentType: file.type } }); const payload = await getApiResponsePayload(res); return { preSignedURL: payload.preSignedURL, token: payload.token, file, userHandle, }; } /** * Updates member photo. * @param {Object} S3Response The response from uploadFileToS3() function. * @return {Promise} Resolves to the api response content */ async updateMemberPhoto(S3Response) { const res = await this.private.api.putJson(`/members/${S3Response.userHandle}/photo`, { param: S3Response.body }); return getApiResponsePayload(res); } /** * Uploads file to S3. * @param {Object} presignedUrlResponse The presigned url response from * getPresignedUrl() function. * @return {Promise} Resolves to the api response content */ uploadFileToS3(presignedUrlResponse) { _.noop(this); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', presignedUrlResponse.preSignedURL, true); xhr.setRequestHeader('Content-Type', presignedUrlResponse.file.type); xhr.onreadystatechange = () => { const { status } = xhr; if (((status >= 200 && status < 300) || status === 304) && xhr.readyState === 4) { resolve({ userHandle: presignedUrlResponse.userHandle, body: { token: presignedUrlResponse.token, contentType: presignedUrlResponse.file.type, }, }); } else if (status >= 400) { const err = new Error('Could not upload image to S3'); err.status = status; reject(err); } }; xhr.onerror = (err) => { logger.error('Could not upload image to S3', err); reject(err); }; xhr.send(presignedUrlResponse.file); }); } /** * Verify member new email * @param {String} handle handle Topcoder user handle * @param {String} emailVerifyToken The verify token of new email * @returns {Promise} Resolves to the api response content */ async verifyMemberNewEmail(handle, emailVerifyToken) { const res = await this.private.api.get(`/members/${handle}/verify?token=${emailVerifyToken}`); return getApiResponsePayload(res); } /** * Get members information * @param {Array} userIds the member ids */ async getMembersInformation(userIds) { const query = `query=${encodeURI(_.map(userIds, id => `userId:${id}`).join(' OR '))}`; const limit = `limit=${userIds.length}`; const url = `/members/_search?fields=userId%2Chandle&${query}&${limit}`; const res = await this.private.api.get(url); return getApiResponsePayload(res); } /** * Fetch resources roles * @param {Array} memberId the member id */ async getResourceRoles() { const res = await this.private.apiV5.get('/resource-roles'); const roles = await res.json(); return roles; } /** * Fetch user challenge resources * @param {Array} challengeId the challenge id */ async getChallengeResources(challengeId) { const user = decodeToken(this.private.tokenV3); const url = `/resources?challengeId=${challengeId}&memberId=${user.userId}`; let res = null; try { res = await this.private.apiV5.get(url); } catch (error) { // logger.error('Failed to load challenge resource', error); } return res.json(); } /** * Fetch user registered challenge's resources * @param {Array} memberId the member id */ async getUserResources(memberId) { const url = `/challenges?status=Active&memberId=${memberId}`; const res = await this.private.apiV5.get(url); const challenges = await res.json(); const roles = await this.getResourceRoles(); const calls = []; challenges.forEach(async (ch) => { calls.push(this.getChallengeResources(ch.id)); }); return Promise.all(calls).then((resources) => { const results = []; resources.forEach((resource) => { const userResource = _.find(resource, { memberId }); if (userResource) { const challengeRole = _.find(roles, { id: userResource.roleId }); const { name } = challengeRole || ''; results.push({ id: userResource.challengeId, name }); } }); return results; }); } } let lastInstance = null; /** * Returns a new or existing members service. * @param {String} tokenV3 Optional. Auth token for Topcoder API v3. * @return {MembersService} Members service object */ export function getService(tokenV3) { if (!lastInstance || tokenV3 !== lastInstance.private.tokenV3) { lastInstance = new MembersService(tokenV3); } return lastInstance; } /* Using default export would be confusing in this case. */ export default undefined;