<commit_before>let Octo = require('octonode');
import * as utilityMethods from '../utils/utilityMethods';
import PaginatedResponse from '../data_structures/PaginatedResponse';

let octoClient = Octo.client();

class GithubApi {

  /**
   * Creates new Octonode client instance initialized with the oauth token passed to this method.
   *
   * https://github.com/pksunkara/octonode#build-a-client-from-an-access-token
   *
   * @param {String} token - The oauth token to store in new Octonode client instance.
   * @returns {Promise} A promise which resolves after Octonode client instance is created.
   * @public
   */
  static addTokenToGhApi(token) {
    return new Promise(resolve => {
      octoClient = Octo.client(token);
      // This is required for CORS requests to pass preflight checks using the Octonode library (due to 'request' library used by Octonode)
      octoClient.requestDefaults['withCredentials'] = false;
      resolve();
    });
  }

  /**
   * Removes currently stored GitHub oauth token from Octonode client instance by
   * overwriting current Octonode client instance with a new, empty Octonode client instance.
   *
   * https://github.com/pksunkara/octonode#build-a-client-which-accesses-any-public-information
   *
   * @returns {Promise} A promise which resolves after Octonode client instance is overwritten.
   * @public
   */
  static removeTokenFromGhApi() {
    return new Promise((resolve) => {
      octoClient = Octo.client();
      resolve();
    });
  }

  /**
   * Gets the user object for the currently logged in user.
   *
   * https://developer.github.com/v3/users/#get-the-authenticated-user
   *
   * @returns {Promise} A promise which resolves to a user object, or rejects with a String error message.
   * @public
   */
  static getAuthenticatedUser() {
    return new Promise((resolve, reject) => {
      let octoMe = octoClient.me();
      let cb = cbFactory(resolve, reject);

      octoMe.info(cb);
    });
  }

  /**
   * Gets a list of repositories owned and/or contributed to by the currently logged in user.
   *
   * https://developer.github.com/v3/repos/#list-your-repositories
   *
   * @returns {Promise} A promise which resolves to a list of repository objects, or rejects with a String error message.
   * @public
   */
  static getCurrentUserAllRepos() {
    return new Promise((resolve, reject) => {
      let octoMe = octoClient.me();
      let cb = cbFactory(resolve, reject);

      octoMe.repos(cb);
    });
  }

  /**
   * Gets a list of repositories owned by specified owner login.
   *
   * https://developer.github.com/v3/repos/#list-user-repositories
   *
   * @param {String} ownerLogin - The GitHub owner login name for which to get owned repos.
   * @returns {Promise} A promise which resolves to a list of repository objects, or rejects with a String error message.
   * @public
   */
  static getReposByOwner(ownerLogin) {
    return new Promise((resolve, reject) => {
      let octoUser = octoClient.user(ownerLogin);
      let cb = cbFactory(resolve, reject);

      octoUser.repos(cb);
    });
  }

  /**
   * Gets a list of branches in the specified repo.
   *
   * https://developer.github.com/v3/repos/branches/#list-branches
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @returns {Promise} A promise which resolves to a list of branch objects, or rejects with a String error message.
   * @public
   */
  static getBranchesInRepo(ownerLogin, repoName) {
    return new Promise((resolve, reject) => {
      let repoFullName = ownerLogin + "/" + repoName;
      let octoRepo = octoClient.repo(repoFullName);
      let cb = cbFactory(resolve, reject);

      octoRepo.branches(cb);
    });
  }

  /**
   * Gets a list of commits on the specified branch in the specified repo.
   *
   * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} branchName - The name of the branch.
   * @param {Number} inPageNum - The page number of data to retrieve (required for GitHub pagination support).
   * @returns {Promise} A promise which resolves to a list of commit objects wrapped in a PaginatedResponse object, or rejects with a String error message.
   * @public
   */
  static getCommitsOnBranch(ownerLogin, repoName, branchName, inPageNum) {
    return new Promise((resolve, reject) => {
      reject("This API method is currently not supported.");
      /*let repo = octoClient.getRepo(ownerLogin, repoName);

       let options = {
       page: inPageNum,
       sha: branchName
       };

       repo.listCommits(options).then(response => {
       let paginatedResponseObject = processPagination(response);

       resolve(paginatedResponseObject);
       }).catch(error => {
       console.log(processResponseErrorMsg(error));
       reject("ERROR: GitHub responded with an error when fetching commits on branch '" + branchName + "' in repo '" + ownerLogin + "/" + repoName + "'");
       });*/
    });
  }

  /**
   * Gets a list of statuses for the specified commit reference in reverse chronological order.
   *
   * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} commitRef - A string specifying the commit reference (can be commit SHA ID, branch name, or tag name).
   * @returns {Promise} A promise which resolves to a list of commit status objects, or rejects with a String error message.
   * @public
   */
  static getStatusesForCommit(ownerLogin, repoName, commitRef) {
    return new Promise((resolve, reject) => {
      reject("This API method is currently not supported.");
      /*let isCommitRefParamValid = utilityMethods.validateCommitReference(commitRef);

       if (isCommitRefParamValid) {
       let repo = octoClient.getRepo(ownerLogin, repoName);

       repo.listStatuses(commitRef).then(response => {
       resolve(response.data);
       }).catch(error => {
       console.log(processResponseErrorMsg(error));
       reject("ERROR: GitHub responded with an error.");
       });
       } else {
       reject("ERROR: Invalid commitRef parameter passed to GithubApi component.");
       }*/
    });
  }

  /**
   * Gets the latest status for each context, as well as a
   * single combined 'state', for the specified commit reference.
   *
   * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} ref - A string specifying the commit reference (can be commit SHA ID, branch name, or tag name).
   * @returns {Promise} A promise which resolves to a combined status object, or rejects with a String error message.
   * @public
   */
  //static getCombinedStatusForRef(ownerLogin, repoName, ref) {
  static getCombinedStatusForRef() {
    return new Promise((resolve, reject) => {
      reject("Support for the combined status endpoint is not yet available in the Github.js library");

      /*
      let isCommitRefParamValid = utilityMethods.validateCommitReference(ref);

      if (isCommitRefParamValid) {
        ghInstance.repos(ownerLogin, repoName).commits(ref).status.fetch().then(result => {
          resolve(result);
      }).catch(error => {
          console.log(getErrorResponseMsg(error));
          reject("ERROR: GitHub responded with an error.");
      });
      } else {
        reject("ERROR: Invalid ref parameter passed to GithubApi component.");
      }
      */
    });
  }

  /**
   * Creates a new status on the specified commit.
   *
   * https://developer.github.com/v3/repos/statuses/#create-a-status
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} commitSha - The sha of the commit for which the new status should be created.
   * @param {String} state - The state to set for the new status ("pending", "success", "error", or "failure").
   * @param {String} description - The description to set for the new status.
   * @param {String} targetUrl - A URL related to the new status (e.g. link to output from CI server build
   *                              which triggered status).
   * @param {String} context - The name of the context under which the status should be created.
   * @returns {Promise} A promise which resolves to the newly created status object, or rejects with a String
   *                    error message.
   * @public
   */
  static createStatusForCommit(ownerLogin, repoName, commitSha, state, description, targetUrl, context) {
    return new Promise((resolve, reject) => {
      reject("This API method is currently not supported.");
      /*let isCommitRefParamValid = utilityMethods.validateCommitReference(commitSha);

       if (isCommitRefParamValid) {
       let repo = octoClient.getRepo(ownerLogin, repoName);
       let createParams = {};

       if (utilityMethods.validateGitHubStatusState(state)) {
       createParams.state = state;
       } else {
       throw new Error("Invalid GitHub status state string passed to createStatusForCommit method in GithubApi componenet.");
       }

       if (utilityMethods.isValidString(description)) {
       createParams.description = description;
       }

       if (utilityMethods.isValidString(targetUrl)) {
       createParams.target_url = targetUrl;
       }

       if (utilityMethods.isValidString(context)) {
       createParams.context = context;
       }

       repo.updateStatus(commitSha, createParams).then(response => {
       resolve(response.data);
       }).catch(error => {
       console.log(processResponseErrorMsg(error));
       reject("ERROR: GitHub responded with an error.");
       });
       } else {
       reject("ERROR: Invalid ref parameter passed to GithubApi component.");
       }*/
    });
  }
}

/**
 * Processes an HTTP response object and wraps it in a pagination object.
 *
 * @param data
 * @param headers
 * @param currentPageNum
 * @return {PaginatedResponse} The response data wrapped in a pagination object
 * @private
 */
function processPagination(data, headers, currentPageNum){
  const REL_SEARCH_START_STRING = "rel=\"";
  const REL_SEARCH_END_STRING = "\"";
  const REL_SEARCH_STRING_LEN = REL_SEARCH_START_STRING.length;
  const LINK_SEARCH_START_STRING = "<";
  const LINK_SEARCH_END_STRING = ">";
  const LINK_SEARCH_START_STRING_LEN = LINK_SEARCH_START_STRING.length;

  let pageLinkString = headers.link;
  let lastPageNum;

  if(pageLinkString){
    let pageLinkArray = pageLinkString.split(", ");
    let parsedPageArray = [];
    let lastPageNumLink;

    pageLinkArray.forEach(pageLink => {
      let relStartIdx = pageLink.indexOf(REL_SEARCH_START_STRING) + REL_SEARCH_STRING_LEN;
      let relStopIdx = pageLink.indexOf(REL_SEARCH_END_STRING, relStartIdx);

      let linkStartIdx = pageLink.indexOf(LINK_SEARCH_START_STRING) + LINK_SEARCH_START_STRING_LEN;
      let linkStopIdx = pageLink.indexOf(LINK_SEARCH_END_STRING, linkStartIdx);

      let relNameString = pageLink.slice(relStartIdx, relStopIdx);
      let linkString = pageLink.slice(linkStartIdx, linkStopIdx);

      let pageNumValue = utilityMethods.extractParamFromURL("page", linkString);

      let newPageLinkObj = {
        rel: relNameString,
        link: linkString,
        pageNum: pageNumValue
      };

      parsedPageArray.push(newPageLinkObj);
    });

    lastPageNumLink = parsedPageArray.find(parsedPage => parsedPage.rel == "last");

    if(lastPageNumLink){
      lastPageNum = lastPageNumLink.pageNum;
    }else{
      lastPageNum = currentPageNum;
    }

  }else{
    lastPageNum = 1;
  }

  return new PaginatedResponse(parseInt(currentPageNum), parseInt(lastPageNum), data);
}

/**
 * Generates a new callback function instance to use with Octonode library requests
 *
 * @param {Function} resolve - a function to be invoked to resolve the promise using this callback
 * @param {Function} reject - a function to be invoked to reject the promise using this callback
 * @return {Function} a function to be used as the callback when making a request using the Octonode library
 * @private
 */
function cbFactory (resolve, reject) {
  return function(error, data){
    if (error) {
      console.log(processResponseErrorMsg(error));
      reject("ERROR: GitHub responded with an error.");
    } else {
      resolve(data);
    }
  };
}

/**
 * Generates a new callback function instance to use with Octonode library requests when response requires pagination
 *
 * @param {Function} resolve - a function to be invoked to resolve the promise using this callback
 * @param {Function} reject - a function to be invoked to reject the promise using this callback
 * @param {Number} pageNum - the page number of the current page of data being requested
 * @return {Function} a function to be used as the callback when making a request using the Octonode library
 * @private
 */
function cbWithPaginationFactory (resolve, reject, pageNum) {
  return function(error, data, headers){
    console.log(headers);
    if (error) {
      console.log(processResponseErrorMsg(error));
      reject("ERROR: GitHub responded with an error.");
    } else {
      let paginatedResponseObj = processPagination(data, headers, pageNum);
      resolve(paginatedResponseObj);
    }
  };
}

/**
 * Logs and processes error messages returned from Octonode library.
 *
 * @param {Error} error - The error returned from Octonode library.
 * @returns {string} A string giving a general description of the type of error.
 * @private
 */
function processResponseErrorMsg(error) {
  let errMsg = "";

  if(error instanceof Error){
    if(error.constructor.name === "HttpError") {

      console.log("An HttpError occurred in response to GitHub HTTP request.");
      console.log("   HTTP Response Status Code: " + error.statusCode);
      console.log("   HTTP Response Message: " + error.message);

      console.log("ERROR DETAILS:");
      console.log(error);

      errMsg = "An HttpError occured in response to GitHub HTTP request.";
    }else{
      console.log(error);

      errMsg = "An unknown error occured in response to GitHub HTTP request.";
    }
  }else{
    console.log(error);

    errMsg = "An invalid error object was passed to processResponseErrorMsg function.";
  }

  return errMsg;
}

export default GithubApi;
<commit_msg>Remove unneeded console.log from cbWithPaginationFactory method<commit_after>let Octo = require('octonode');
import * as utilityMethods from '../utils/utilityMethods';
import PaginatedResponse from '../data_structures/PaginatedResponse';

let octoClient = Octo.client();

class GithubApi {

  /**
   * Creates new Octonode client instance initialized with the oauth token passed to this method.
   *
   * https://github.com/pksunkara/octonode#build-a-client-from-an-access-token
   *
   * @param {String} token - The oauth token to store in new Octonode client instance.
   * @returns {Promise} A promise which resolves after Octonode client instance is created.
   * @public
   */
  static addTokenToGhApi(token) {
    return new Promise(resolve => {
      octoClient = Octo.client(token);
      // This is required for CORS requests to pass preflight checks using the Octonode library (due to 'request' library used by Octonode)
      octoClient.requestDefaults['withCredentials'] = false;
      resolve();
    });
  }

  /**
   * Removes currently stored GitHub oauth token from Octonode client instance by
   * overwriting current Octonode client instance with a new, empty Octonode client instance.
   *
   * https://github.com/pksunkara/octonode#build-a-client-which-accesses-any-public-information
   *
   * @returns {Promise} A promise which resolves after Octonode client instance is overwritten.
   * @public
   */
  static removeTokenFromGhApi() {
    return new Promise((resolve) => {
      octoClient = Octo.client();
      resolve();
    });
  }

  /**
   * Gets the user object for the currently logged in user.
   *
   * https://developer.github.com/v3/users/#get-the-authenticated-user
   *
   * @returns {Promise} A promise which resolves to a user object, or rejects with a String error message.
   * @public
   */
  static getAuthenticatedUser() {
    return new Promise((resolve, reject) => {
      let octoMe = octoClient.me();
      let cb = cbFactory(resolve, reject);

      octoMe.info(cb);
    });
  }

  /**
   * Gets a list of repositories owned and/or contributed to by the currently logged in user.
   *
   * https://developer.github.com/v3/repos/#list-your-repositories
   *
   * @returns {Promise} A promise which resolves to a list of repository objects, or rejects with a String error message.
   * @public
   */
  static getCurrentUserAllRepos() {
    return new Promise((resolve, reject) => {
      let octoMe = octoClient.me();
      let cb = cbFactory(resolve, reject);

      octoMe.repos(cb);
    });
  }

  /**
   * Gets a list of repositories owned by specified owner login.
   *
   * https://developer.github.com/v3/repos/#list-user-repositories
   *
   * @param {String} ownerLogin - The GitHub owner login name for which to get owned repos.
   * @returns {Promise} A promise which resolves to a list of repository objects, or rejects with a String error message.
   * @public
   */
  static getReposByOwner(ownerLogin) {
    return new Promise((resolve, reject) => {
      let octoUser = octoClient.user(ownerLogin);
      let cb = cbFactory(resolve, reject);

      octoUser.repos(cb);
    });
  }

  /**
   * Gets a list of branches in the specified repo.
   *
   * https://developer.github.com/v3/repos/branches/#list-branches
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @returns {Promise} A promise which resolves to a list of branch objects, or rejects with a String error message.
   * @public
   */
  static getBranchesInRepo(ownerLogin, repoName) {
    return new Promise((resolve, reject) => {
      let repoFullName = ownerLogin + "/" + repoName;
      let octoRepo = octoClient.repo(repoFullName);
      let cb = cbFactory(resolve, reject);

      octoRepo.branches(cb);
    });
  }

  /**
   * Gets a list of commits on the specified branch in the specified repo.
   *
   * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} branchName - The name of the branch.
   * @param {Number} inPageNum - The page number of data to retrieve (required for GitHub pagination support).
   * @returns {Promise} A promise which resolves to a list of commit objects wrapped in a PaginatedResponse object, or rejects with a String error message.
   * @public
   */
  static getCommitsOnBranch(ownerLogin, repoName, branchName, inPageNum) {
    return new Promise((resolve, reject) => {
      reject("This API method is currently not supported.");
      /*let repo = octoClient.getRepo(ownerLogin, repoName);

       let options = {
       page: inPageNum,
       sha: branchName
       };

       repo.listCommits(options).then(response => {
       let paginatedResponseObject = processPagination(response);

       resolve(paginatedResponseObject);
       }).catch(error => {
       console.log(processResponseErrorMsg(error));
       reject("ERROR: GitHub responded with an error when fetching commits on branch '" + branchName + "' in repo '" + ownerLogin + "/" + repoName + "'");
       });*/
    });
  }

  /**
   * Gets a list of statuses for the specified commit reference in reverse chronological order.
   *
   * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} commitRef - A string specifying the commit reference (can be commit SHA ID, branch name, or tag name).
   * @returns {Promise} A promise which resolves to a list of commit status objects, or rejects with a String error message.
   * @public
   */
  static getStatusesForCommit(ownerLogin, repoName, commitRef) {
    return new Promise((resolve, reject) => {
      reject("This API method is currently not supported.");
      /*let isCommitRefParamValid = utilityMethods.validateCommitReference(commitRef);

       if (isCommitRefParamValid) {
       let repo = octoClient.getRepo(ownerLogin, repoName);

       repo.listStatuses(commitRef).then(response => {
       resolve(response.data);
       }).catch(error => {
       console.log(processResponseErrorMsg(error));
       reject("ERROR: GitHub responded with an error.");
       });
       } else {
       reject("ERROR: Invalid commitRef parameter passed to GithubApi component.");
       }*/
    });
  }

  /**
   * Gets the latest status for each context, as well as a
   * single combined 'state', for the specified commit reference.
   *
   * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} ref - A string specifying the commit reference (can be commit SHA ID, branch name, or tag name).
   * @returns {Promise} A promise which resolves to a combined status object, or rejects with a String error message.
   * @public
   */
  //static getCombinedStatusForRef(ownerLogin, repoName, ref) {
  static getCombinedStatusForRef() {
    return new Promise((resolve, reject) => {
      reject("Support for the combined status endpoint is not yet available in the Github.js library");

      /*
      let isCommitRefParamValid = utilityMethods.validateCommitReference(ref);

      if (isCommitRefParamValid) {
        ghInstance.repos(ownerLogin, repoName).commits(ref).status.fetch().then(result => {
          resolve(result);
      }).catch(error => {
          console.log(getErrorResponseMsg(error));
          reject("ERROR: GitHub responded with an error.");
      });
      } else {
        reject("ERROR: Invalid ref parameter passed to GithubApi component.");
      }
      */
    });
  }

  /**
   * Creates a new status on the specified commit.
   *
   * https://developer.github.com/v3/repos/statuses/#create-a-status
   *
   * @param {String} ownerLogin - The GitHub owner login name of the owner of the repo.
   * @param {String} repoName - The name of the repo.
   * @param {String} commitSha - The sha of the commit for which the new status should be created.
   * @param {String} state - The state to set for the new status ("pending", "success", "error", or "failure").
   * @param {String} description - The description to set for the new status.
   * @param {String} targetUrl - A URL related to the new status (e.g. link to output from CI server build
   *                              which triggered status).
   * @param {String} context - The name of the context under which the status should be created.
   * @returns {Promise} A promise which resolves to the newly created status object, or rejects with a String
   *                    error message.
   * @public
   */
  static createStatusForCommit(ownerLogin, repoName, commitSha, state, description, targetUrl, context) {
    return new Promise((resolve, reject) => {
      reject("This API method is currently not supported.");
      /*let isCommitRefParamValid = utilityMethods.validateCommitReference(commitSha);

       if (isCommitRefParamValid) {
       let repo = octoClient.getRepo(ownerLogin, repoName);
       let createParams = {};

       if (utilityMethods.validateGitHubStatusState(state)) {
       createParams.state = state;
       } else {
       throw new Error("Invalid GitHub status state string passed to createStatusForCommit method in GithubApi componenet.");
       }

       if (utilityMethods.isValidString(description)) {
       createParams.description = description;
       }

       if (utilityMethods.isValidString(targetUrl)) {
       createParams.target_url = targetUrl;
       }

       if (utilityMethods.isValidString(context)) {
       createParams.context = context;
       }

       repo.updateStatus(commitSha, createParams).then(response => {
       resolve(response.data);
       }).catch(error => {
       console.log(processResponseErrorMsg(error));
       reject("ERROR: GitHub responded with an error.");
       });
       } else {
       reject("ERROR: Invalid ref parameter passed to GithubApi component.");
       }*/
    });
  }
}

/**
 * Processes an HTTP response object and wraps it in a pagination object.
 *
 * @param data
 * @param headers
 * @param currentPageNum
 * @return {PaginatedResponse} The response data wrapped in a pagination object
 * @private
 */
function processPagination(data, headers, currentPageNum){
  const REL_SEARCH_START_STRING = "rel=\"";
  const REL_SEARCH_END_STRING = "\"";
  const REL_SEARCH_STRING_LEN = REL_SEARCH_START_STRING.length;
  const LINK_SEARCH_START_STRING = "<";
  const LINK_SEARCH_END_STRING = ">";
  const LINK_SEARCH_START_STRING_LEN = LINK_SEARCH_START_STRING.length;

  let pageLinkString = headers.link;
  let lastPageNum;

  if(pageLinkString){
    let pageLinkArray = pageLinkString.split(", ");
    let parsedPageArray = [];
    let lastPageNumLink;

    pageLinkArray.forEach(pageLink => {
      let relStartIdx = pageLink.indexOf(REL_SEARCH_START_STRING) + REL_SEARCH_STRING_LEN;
      let relStopIdx = pageLink.indexOf(REL_SEARCH_END_STRING, relStartIdx);

      let linkStartIdx = pageLink.indexOf(LINK_SEARCH_START_STRING) + LINK_SEARCH_START_STRING_LEN;
      let linkStopIdx = pageLink.indexOf(LINK_SEARCH_END_STRING, linkStartIdx);

      let relNameString = pageLink.slice(relStartIdx, relStopIdx);
      let linkString = pageLink.slice(linkStartIdx, linkStopIdx);

      let pageNumValue = utilityMethods.extractParamFromURL("page", linkString);

      let newPageLinkObj = {
        rel: relNameString,
        link: linkString,
        pageNum: pageNumValue
      };

      parsedPageArray.push(newPageLinkObj);
    });

    lastPageNumLink = parsedPageArray.find(parsedPage => parsedPage.rel == "last");

    if(lastPageNumLink){
      lastPageNum = lastPageNumLink.pageNum;
    }else{
      lastPageNum = currentPageNum;
    }

  }else{
    lastPageNum = 1;
  }

  return new PaginatedResponse(parseInt(currentPageNum), parseInt(lastPageNum), data);
}

/**
 * Generates a new callback function instance to use with Octonode library requests
 *
 * @param {Function} resolve - a function to be invoked to resolve the promise using this callback
 * @param {Function} reject - a function to be invoked to reject the promise using this callback
 * @return {Function} a function to be used as the callback when making a request using the Octonode library
 * @private
 */
function cbFactory (resolve, reject) {
  return function(error, data){
    if (error) {
      console.log(processResponseErrorMsg(error));
      reject("ERROR: GitHub responded with an error.");
    } else {
      resolve(data);
    }
  };
}

/**
 * Generates a new callback function instance to use with Octonode library requests when response requires pagination
 *
 * @param {Function} resolve - a function to be invoked to resolve the promise using this callback
 * @param {Function} reject - a function to be invoked to reject the promise using this callback
 * @param {Number} pageNum - the page number of the current page of data being requested
 * @return {Function} a function to be used as the callback when making a request using the Octonode library
 * @private
 */
function cbWithPaginationFactory (resolve, reject, pageNum) {
  return function(error, data, headers){
    if (error) {
      console.log(processResponseErrorMsg(error));
      reject("ERROR: GitHub responded with an error.");
    } else {
      let paginatedResponseObj = processPagination(data, headers, pageNum);
      resolve(paginatedResponseObj);
    }
  };
}

/**
 * Logs and processes error messages returned from Octonode library.
 *
 * @param {Error} error - The error returned from Octonode library.
 * @returns {string} A string giving a general description of the type of error.
 * @private
 */
function processResponseErrorMsg(error) {
  let errMsg = "";

  if(error instanceof Error){
    if(error.constructor.name === "HttpError") {

      console.log("An HttpError occurred in response to GitHub HTTP request.");
      console.log("   HTTP Response Status Code: " + error.statusCode);
      console.log("   HTTP Response Message: " + error.message);

      console.log("ERROR DETAILS:");
      console.log(error);

      errMsg = "An HttpError occured in response to GitHub HTTP request.";
    }else{
      console.log(error);

      errMsg = "An unknown error occured in response to GitHub HTTP request.";
    }
  }else{
    console.log(error);

    errMsg = "An invalid error object was passed to processResponseErrorMsg function.";
  }

  return errMsg;
}

export default GithubApi;
<|endoftext|>


<commit_before>
function createObserverFactory() {

    const _createObserver = function(observerHandler) {

        const _listeners = [];

        const _views = {};

        const _transactions = {};

        const _view = function(viewId) {
            if (!_views.hasOwnProperty(viewId)) {
                _views[viewId] = {
                    readProps: {},
                    changedProps: {}
                };
            }
            return _views[viewId];
        };

        const _changedViewPropCount = function(view) {
            let changedPropCount = 0;
            for (let propKey in view.changedProps) {
                changedPropCount += view.readProps.hasOwnProperty(propKey) ? 1 : 0;
            }
            return changedPropCount;
        };

        const _transaction = function(transactionId) {
            if (!_transactions.hasOwnProperty(transactionId)) {
                _transactions[transactionId] = {
                    readProps: {},
                    changedProps: {}
                };
            }
            return _transactions[transactionId];
        };

        const _changedTransactionPropCount = function(transaction) {
            let changedPropCount = 0;
            for (let propKey in transaction.changedProps) {
                changedPropCount += transaction.readProps.hasOwnProperty(propKey) ? 1 : 0;
            }
            return changedPropCount;
        };

        const _listenerIndex = function(listener) {
            let listenerIndex = -1;
            for (let i = 0; i < _listeners.length; i++) {
                if (_listeners[i] === listener) {
                    listenerIndex = i;
                    break;
                }
            }
            return listenerIndex;
        };

        return {

            markViewAsRead: function(viewId,propKey) {
                _view(viewId).readProps[propsKey] = true;
            },

            markViewAsChanged: function(viewId,propKey) {
                _view(viewId).changedProps[propsKey] = true;
            },

            markTransactionAsRead: function(transactionId,propKey) {
                _transaction(transactionId).readProps[propsKey] = true;
            },

            markTransactionAsChanged: function(transactionId,propKey) {
               _transaction(transactionId).changedProps[propsKey] = true;
            },

            addListener: function(listener) {
                _listeners.push(listener);
            },

            removeListener: function(listener) {
                const listenerIndex = _listenerIndex(listener);
                if (listenerIndex >= 0) {
                    _listeners.splice(listenerIndex, 1);
                }
            },

            dispatchChanges: function() {
                let changedPropCount = 0;
                for (let view in _views) {
                    changedPropCount += _changedViewPropCount(view);
                }
                for (let transaction in _transactions) {
                    changedPropCount += _changedTransactionPropCount(transaction);
                }
                if (changedPropCount > 0) {
                    for (let view in _views) {
                        view.changedProps = {};
                    }
                    for (let transaction in _transactions) {
                        transaction.changedProps = {};
                    }
                    for (let i = 0; i < _listeners.length; i++) {
                        _listeners[i]();
                    }
                }
            }
        }
    }

    return {

        createObserver: _createObserver,

    };

}

export default createObserverFactory;
<commit_msg>code cleanup<commit_after>
function createObserverFactory() {

    const _createObserver = function() {

        const _listeners = [];

        const _views = {};

        const _transactions = {};

        const _view = function(viewId) {
            if (!_views.hasOwnProperty(viewId)) {
                _views[viewId] = {
                    readProps: {},
                    changedProps: {}
                };
            }
            return _views[viewId];
        };

        const _changedViewPropCount = function(view) {
            let changedPropCount = 0;
            for (let propKey in view.changedProps) {
                changedPropCount += view.readProps.hasOwnProperty(propKey) ? 1 : 0;
            }
            return changedPropCount;
        };

        const _transaction = function(transactionId) {
            if (!_transactions.hasOwnProperty(transactionId)) {
                _transactions[transactionId] = {
                    readProps: {},
                    changedProps: {}
                };
            }
            return _transactions[transactionId];
        };

        const _changedTransactionPropCount = function(transaction) {
            let changedPropCount = 0;
            for (let propKey in transaction.changedProps) {
                changedPropCount += transaction.readProps.hasOwnProperty(propKey) ? 1 : 0;
            }
            return changedPropCount;
        };

        const _listenerIndex = function(listener) {
            let listenerIndex = -1;
            for (let i = 0; i < _listeners.length; i++) {
                if (_listeners[i] === listener) {
                    listenerIndex = i;
                    break;
                }
            }
            return listenerIndex;
        };

        return {

            markViewAsRead: function(viewId, propKey) {
                _view(viewId).readProps[propsKey] = true;
            },

            markViewAsChanged: function(viewId, propKey) {
                _view(viewId).changedProps[propsKey] = true;
            },

            markTransactionAsRead: function(transactionId, propKey) {
                _transaction(transactionId).readProps[propsKey] = true;
            },

            markTransactionAsChanged: function(transactionId, propKey) {
               _transaction(transactionId).changedProps[propsKey] = true;
            },

            addListener: function(listener) {
                _listeners.push(listener);
            },

            removeListener: function(listener) {
                const listenerIndex = _listenerIndex(listener);
                if (listenerIndex >= 0) {
                    _listeners.splice(listenerIndex, 1);
                }
            },

            dispatchChanges: function() {
                let changedPropCount = 0;
                for (let view in _views) {
                    changedPropCount += _changedViewPropCount(view);
                }
                for (let transaction in _transactions) {
                    changedPropCount += _changedTransactionPropCount(transaction);
                }
                if (changedPropCount > 0) {
                    for (let view in _views) {
                        view.changedProps = {};
                    }
                    for (let transaction in _transactions) {
                        transaction.changedProps = {};
                    }
                    for (let i = 0; i < _listeners.length; i++) {
                        _listeners[i]();
                    }
                }
            }
        }
    }

    return {

        createObserver: _createObserver,

    };

}

export default createObserverFactory;
<|endoftext|>


<commit_before>import { EMPTY_OBJ } from './constants';
import options from './options';

/**
  * Create an virtual node (used for JSX)
  * @param {import('./internal').VNode["type"]} type The node name or Component
  * constructor for this virtual node
  * @param {object | null | undefined} [props] The properties of the virtual node
  * @param {Array<import('.').ComponentChildren>} [children] The children of the virtual node
  * @returns {import('./internal').VNode}
  */
export function createElement(type, props, children) {
	if (props==null) props = {};
	if (arguments.length>3) {
		children = [children];
		for (let i=3; i<arguments.length; i++) {
			children.push(arguments[i]);
		}
	}
	if (children!=null) {
		props.children = children;
	}

	// "type" may be undefined during development. The check is needed so that
	// we can display a nice error message with our debug helpers
	if (type && type.defaultProps!=null) {
		for (let i in type.defaultProps) {
			if (props[i]===undefined) props[i] = type.defaultProps[i];
		}
	}
	let ref = props.ref;
	if (ref) delete props.ref;
	let key = props.key;
	if (key) delete props.key;

	return createVNode(type, props, null, key, ref);
}

/**
 * Create a VNode (used internally by Preact)
 * @param {import('./internal').VNode["type"]} type The node name or Component
 * Constructor for this virtual node
 * @param {object} props The properites of this virtual node
 * @param {string | number} text If this virtual node represents a text node,
 * this is the text of the node
 * @param {string |number | null} key The key for this virtual node, used when
 * diffing it against its children
 * @param {import('./internal').VNode["ref"]} ref The ref property that will
 * receive a reference to its created child
 * @returns {import('./internal').VNode}
 */
function createVNode(type, props, text, key, ref) {
	// V8 seems to be better at detecting type shapes if the object is allocated from the same call site
	// Do not inline into createElement and coerceToVNode!
	const vnode = {
		type,
		props,
		text,
		key,
		ref,
		_children: null,
		_dom: null,
		_lastDomChild: null,
		_component: null,
		// @todo - leading underscore here + custom mapping + devtools detection
		startTime: 0,
		endTime: -1
	};

	if (options && options.vnode) options.vnode(vnode);

	return vnode;
}

export function createRef() {
	return {};
}

export const Fragment = 9;

/**
 * Coerce an untrusted value into a VNode
 * Specifically, this should be used anywhere a user could provide a boolean, string, or number where
 * a VNode or Component is desired instead
 * @param {boolean | string | number | import('./internal').VNode} possibleVNode A possible VNode
 * @returns {import('./internal').VNode}
 */
export function coerceToVNode(possibleVNode) {
	if (typeof possibleVNode === 'boolean') return null;
	if (typeof possibleVNode === 'string' || typeof possibleVNode === 'number') {
		return createVNode(null, EMPTY_OBJ, possibleVNode, null, null);
	}

	if (Array.isArray(possibleVNode)) {
		return createVNode(Fragment, { children: possibleVNode }, null, null, null);
	}

	// Clone vnode if it has already been used. ceviche/#57
	if (possibleVNode!=null && possibleVNode._dom!=null) {
		return createVNode(possibleVNode.type, possibleVNode.props, possibleVNode.text, possibleVNode.key, null);
	}

	return possibleVNode;
}
<commit_msg>Early-exit coercion for `null` VNodes<commit_after>import { EMPTY_OBJ } from './constants';
import options from './options';

/**
  * Create an virtual node (used for JSX)
  * @param {import('./internal').VNode["type"]} type The node name or Component
  * constructor for this virtual node
  * @param {object | null | undefined} [props] The properties of the virtual node
  * @param {Array<import('.').ComponentChildren>} [children] The children of the virtual node
  * @returns {import('./internal').VNode}
  */
export function createElement(type, props, children) {
	if (props==null) props = {};
	if (arguments.length>3) {
		children = [children];
		for (let i=3; i<arguments.length; i++) {
			children.push(arguments[i]);
		}
	}
	if (children!=null) {
		props.children = children;
	}

	// "type" may be undefined during development. The check is needed so that
	// we can display a nice error message with our debug helpers
	if (type && type.defaultProps!=null) {
		for (let i in type.defaultProps) {
			if (props[i]===undefined) props[i] = type.defaultProps[i];
		}
	}
	let ref = props.ref;
	if (ref) delete props.ref;
	let key = props.key;
	if (key) delete props.key;

	return createVNode(type, props, null, key, ref);
}

/**
 * Create a VNode (used internally by Preact)
 * @param {import('./internal').VNode["type"]} type The node name or Component
 * Constructor for this virtual node
 * @param {object} props The properites of this virtual node
 * @param {string | number} text If this virtual node represents a text node,
 * this is the text of the node
 * @param {string |number | null} key The key for this virtual node, used when
 * diffing it against its children
 * @param {import('./internal').VNode["ref"]} ref The ref property that will
 * receive a reference to its created child
 * @returns {import('./internal').VNode}
 */
function createVNode(type, props, text, key, ref) {
	// V8 seems to be better at detecting type shapes if the object is allocated from the same call site
	// Do not inline into createElement and coerceToVNode!
	const vnode = {
		type,
		props,
		text,
		key,
		ref,
		_children: null,
		_dom: null,
		_lastDomChild: null,
		_component: null,
		// @todo - leading underscore here + custom mapping + devtools detection
		startTime: 0,
		endTime: -1
	};

	if (options && options.vnode) options.vnode(vnode);

	return vnode;
}

export function createRef() {
	return {};
}

export const Fragment = 9;

/**
 * Coerce an untrusted value into a VNode
 * Specifically, this should be used anywhere a user could provide a boolean, string, or number where
 * a VNode or Component is desired instead
 * @param {boolean | string | number | import('./internal').VNode} possibleVNode A possible VNode
 * @returns {import('./internal').VNode}
 */
export function coerceToVNode(possibleVNode) {
	if (possibleVNode == null || typeof possibleVNode === 'boolean') return null;
	if (typeof possibleVNode === 'string' || typeof possibleVNode === 'number') {
		return createVNode(null, EMPTY_OBJ, possibleVNode, null, null);
	}

	if (Array.isArray(possibleVNode)) {
		return createVNode(Fragment, { children: possibleVNode }, null, null, null);
	}

	// Clone vnode if it has already been used. ceviche/#57
	if (possibleVNode._dom!=null) {
		return createVNode(possibleVNode.type, possibleVNode.props, possibleVNode.text, possibleVNode.key, null);
	}

	return possibleVNode;
}
<|endoftext|>


<commit_before>export class CreatureQueue {
	constructor(game) {
		this.game = game;
		this.queue = [];
		this.nextQueue = [];
	}

	/**
	 * Add a creature to the next turn's queue by initiative
	 * creature - The creature to add
	 */
	addByInitiative(creature) {
		for (let i = 0; i < this.nextQueue.length; i++) {
			let queue = this.nextQueue[i];

			if (queue.delayed || queue.getInitiative() < creature.getInitiative()) {
				this.nextQueue.splice(i, 0, creature);
				return;
			}
		}

		this.nextQueue.push(creature);
	}

	dequeue() {
		return this.queue.splice(0, 1)[0];
	}

	remove(creature) {
		arrayUtils.removePos(this.queue, creature);
		arrayUtils.removePos(this.nextQueue, creature);
	}

	nextRound() {
		// Copy next queue into current queue
		this.queue = this.nextQueue.slice(0);
		// Sort next queue by initiative (current queue may be reordered) descending
		this.nextQueue = this.nextQueue.sort((a, b) => b.getInitiative() - a.getInitiative());
	}

	isCurrentEmpty() {
		return this.queue.length === 0;
	}

	isNextEmpty() {
		return this.nextQueue.length === 0;
	}

	delay(creature) {
		// Find out if the creature is in the current queue or next queue; remove
		// it from the queue and replace it at the end
		let game = this.game,
			inQueue = arrayUtils.removePos(this.queue, creature) || creature === game.activeCreature,
			queue = this.queue;

		if (!inQueue) {
			queue = this.nextQueue;
			arrayUtils.removePos(this.nextQueue, creature);
		}
		// Move creature to end of queue but in order w.r.t. other delayed creatures
		for (let i = 0, len = queue.length; i < len; i++) {
			if (!queue[i].delayed) {
				continue;
			}

			if (queue[i].getInitiative() < creature.getInitiative()) {
				queue.splice(i, 0, creature);
				return;
			}
		}

		queue.push(creature);
	}
};
<commit_msg>Added imports to creature queue<commit_after>import * as arrayUtils from "./utility/arrayUtils";

export class CreatureQueue {
	constructor(game) {
		this.game = game;
		this.queue = [];
		this.nextQueue = [];
	}

	/**
	 * Add a creature to the next turn's queue by initiative
	 * creature - The creature to add
	 */
	addByInitiative(creature) {
		for (let i = 0; i < this.nextQueue.length; i++) {
			let queue = this.nextQueue[i];

			if (queue.delayed || queue.getInitiative() < creature.getInitiative()) {
				this.nextQueue.splice(i, 0, creature);
				return;
			}
		}

		this.nextQueue.push(creature);
	}

	dequeue() {
		return this.queue.splice(0, 1)[0];
	}

	remove(creature) {
		arrayUtils.removePos(this.queue, creature);
		arrayUtils.removePos(this.nextQueue, creature);
	}

	nextRound() {
		// Copy next queue into current queue
		this.queue = this.nextQueue.slice(0);
		// Sort next queue by initiative (current queue may be reordered) descending
		this.nextQueue = this.nextQueue.sort((a, b) => b.getInitiative() - a.getInitiative());
	}

	isCurrentEmpty() {
		return this.queue.length === 0;
	}

	isNextEmpty() {
		return this.nextQueue.length === 0;
	}

	delay(creature) {
		// Find out if the creature is in the current queue or next queue; remove
		// it from the queue and replace it at the end
		let game = this.game,
			inQueue = arrayUtils.removePos(this.queue, creature) || creature === game.activeCreature,
			queue = this.queue;

		if (!inQueue) {
			queue = this.nextQueue;
			arrayUtils.removePos(this.nextQueue, creature);
		}
		// Move creature to end of queue but in order w.r.t. other delayed creatures
		for (let i = 0, len = queue.length; i < len; i++) {
			if (!queue[i].delayed) {
				continue;
			}

			if (queue[i].getInitiative() < creature.getInitiative()) {
				queue.splice(i, 0, creature);
				return;
			}
		}

		queue.push(creature);
	}
};
<|endoftext|>


<commit_before>// ==UserScript==
// @name         A Mining Game: GBStats Bot
// @namespace    https://github.com/Phylogenesis/
// @version      0.1.10
// @description  Runs a bot that tracks global boss stats in A Mining Game
// @author       Luke Jones
// @include      /^http://trugul\.com/(index\.php)?$/
// @grant        none
// @downloadURL  https://github.com/Phylogenesis/greasemonkey/raw/master/GBStats.user.js
// ==/UserScript==

GBStats = {
    // How many global bosses to track.
    // Maximum limit depends on the browser's localStorage settings
    MAX_HISTORY:   1000,

    // How many people to report in the summary messages.
    SUMMARY_COUNT: 5,

    client:        undefined,

    globalBosses:  [],
    playerList:    [],

    subscribers:   {
        summary:  [],
        personal: []
    },

    showCommands: function () {
        GBStats.sendToGlobalChat('Type "#gbstats subscribe" to receive summary reports and "#gbstats subscribe personal" to receive personal reports. Type "#gbstats" on its own to receive a one-off personal report.');
    },

    joinGlobalBoss: function () {
        GBStats.client.emit('globalbossToggleLobby', true);
    },

    lastGlobalBoss: function () {
        return GBStats.globalBosses[GBStats.globalBosses.length - 1];
    },

    generateSubscriptionMessages() {
        GBStats.outputSummary();
        GBStats.sendToSubscribers('summary', 'Type "#gbstats unsubscribe" to unsubscribe from these messages');

        var personalSubscribers = GBStats.subscribers.personal;

        for (var i = 0; i < personalSubscribers.length; i++) {
            if (GBStats.playerList.filter(function (player) { return player === personalSubscribers[i]; }).length > 0) {
                for (var user in GBStats.lastGlobalBoss().end.data.players) {
                    if (GBStats.lastGlobalBoss().end.data.players[user].username === personalSubscribers[i]) {
                        GBStats.outputPersonal(personalSubscribers[i]);

                        GBStats.sendToPrivateChat(
                            personalSubscribers[i],
                            'Type "#gbstats unsubscribe personal" to unsubscribe from these messages'
                        );
                    }
                }
            }
        }
    },

    outputSummary: function (outputToGlobalChat) {
        var lastGlobalBoss = GBStats.lastGlobalBoss();

        var bossName       = lastGlobalBoss.start.data.name;
        var bossHP         = lastGlobalBoss.start.data.startingHP;
        var bossDuration   = (lastGlobalBoss.end.timestamp - lastGlobalBoss.start.timestamp) / 1000;

        var outputFunction = outputToGlobalChat ? GBStats.sendToGlobalChat : function (message) { GBStats.sendToSubscribers('summary', message); };

        var message;

        message =
            'Global Boss Stats (' +
            'Name: ' + bossName + ', ' +
            'Health: ' + Math.round(bossHP) + ' HP, ' +
            'Duration: ' + Math.round(bossDuration) + 's' +
            ')';

        outputFunction(message);

        var attackers = [];

        for (var attacker in lastGlobalBoss.end.data.players) {
            attackers.push(lastGlobalBoss.end.data.players[attacker]);
        }

        attackers.sort(function (a, b) { return b.attacks - a.attacks; });

        var topClickers = attackers.slice(0, GBStats.SUMMARY_COUNT).map(
            function (attacker) {
                return attacker.username + ' (' + attacker.attacks + ', ' + (attacker.attacks / bossDuration).toFixed(1) + '/s)';
            }
        );

        message = 'Top Clickers: ' + topClickers.join(', ');

        outputFunction(message);

        attackers.sort(function (a, b) { return b.damageDealt - a.damageDealt; });

        var topDamage = attackers.slice(0, GBStats.SUMMARY_COUNT).map(
            function (attacker) {
                return attacker.username + ' (' + attacker.damageDealt + ', ' + (attacker.damageDealt / bossHP * 100).toFixed(1) + '%)';
            }
        );

        message = 'Top Damage: ' + topDamage.join(', ');

        outputFunction(message);
    },

    logSummary: function () {
        var lastGlobalBoss = GBStats.lastGlobalBoss();

        console.log('Global Boss (Started: ' + (new Date(lastGlobalBoss.start.timestamp)).toLocaleTimeString() + ')');

        console.groupCollapsed();
        console.log('   ID  Username        Level   Clicks  Damage');
        console.log('-----  --------        -----   ------  ------');

        var attackers = lastGlobalBoss.end.data.players;

        for (var attacker in attackers) {
            var userid   = attackers[attacker].userid;
            var username = attackers[attacker].username;
            var level    = attackers[attacker].level;
            var clicks   = attackers[attacker].attacks;
            var damage   = attackers[attacker].damageDealt;

            console.log(
                (" ".repeat(5) + userid).slice(-5) + "  " +
                (username + " ".repeat(16)).substring(0, 16) +
                (" ".repeat(5) + level).slice(-5) + "   " +
                (" ".repeat(6) + clicks).slice(-6) + "  " +
                (" ".repeat(6) + damage).slice(-6)
            );
        }

        console.groupEnd();
    },

    outputPersonal: function (requester, requestee) {
        requestee = requestee || requester;

        var lastGlobalBoss = GBStats.lastGlobalBoss();

        var attackers = lastGlobalBoss.end.data.players;

        for (var attacker in attackers) {
            if (attackers[attacker].username.toUpperCase() === requestee.toUpperCase()) {
                var bossDuration = (lastGlobalBoss.end.timestamp - lastGlobalBoss.start.timestamp) / 1000;

                GBStats.sendToPrivateChat(
                    requester,
                    'Global Boss (' + (new Date(GBStats.lastGlobalBoss().start.timestamp)).toLocaleTimeString() + ') ' +
                    'User: ' + attackers[attacker].username + ', ' +
                    'Level: ' + attackers[attacker].level + ', ' +
                    'Clicks: ' + attackers[attacker].attacks + ' (' + (attackers[attacker].attacks / bossDuration).toFixed(1) + '/s), ' +
                    'Damage: ' + attackers[attacker].damageDealt
                );

                return;
            }
        }

        GBStats.sendToPrivateChat(requester, ' User ' + requestee + ' did not take part in the last global boss.');
    },

    parseMessage: function (data) {
        if (/^#gbstats/i.test(data.message)) {
            var requester = data.username;
            var command   = data.message.split(' ')[1] || '';
            var list      = data.message.split(' ')[2] || '';

            switch (command.toLowerCase()) {
                case "summary":
                    if (requester === 'Phylogenesis') {
                        GBStats.outputSummary(true);
                    }
                    break;
                case "commands":
                    if (requester === 'Phylogenesis') {
                        GBStats.showCommands();
                    }
                    break;
                case "subscribe":
                    switch (list.toLowerCase()) {
                        case 'personal':
                        case 'summary':
                            GBStats.addSubscriber(list, requester);
                            break;
                        default:
                            GBStats.addSubscriber('summary', requester);
                            break;
                    }
                    break;
                case "unsubscribe":
                    switch (list.toLowerCase()) {
                        case 'personal':
                        case 'summary':
                            GBStats.removeSubscriber(list, requester);
                            break;
                        default:
                            GBStats.removeSubscriber('summary', requester);
                            break;
                    }
                    break;
                default:
                    GBStats.outputPersonal(requester, command);
                    break;
            }
        }
    },

    addSubscriber: function (list, user) {
        if (GBStats.subscribers[list].filter(function (player) { return player === user; }).length === 0) {
            GBStats.subscribers[list].push(user);
            GBStats.updateSubscriberList();

            GBStats.sendToPrivateChat(user, 'Added you to the ' + list + ' subscription list');
        } else {
            GBStats.sendToPrivateChat(user, 'You are already in the ' + list + ' subscription list');
        }
    },

    removeSubscriber: function (list, user) {
        if (GBStats.subscribers[list].filter(function (player) { return player === user; }).length > 0) {
            GBStats.subscribers[list] = GBStats.subscribers[list].filter(function (subscriber) { return subscriber !== user; });
            GBStats.updateSubscriberList();

            GBStats.sendToPrivateChat(user, 'Removed you from the ' + list + ' subscription list');
        } else {
            GBStats.sendToPrivateChat(user, 'You are not in the ' + list + ' subscription list');
        }
    },

    loadGlobalBosses: function () {
        GBStats.globalBosses = JSON.parse(localStorage.GBStatsGlobalBosses);
    },

    updateGlobalBosses: function () {
        localStorage.GBStatsGlobalBosses = JSON.stringify(GBStats.globalBosses.slice(-GBStats.MAX_HISTORY));
    },

    loadSubscriberList: function () {
        GBStats.subscribers = JSON.parse(localStorage.GBStatsSubscribers);
    },

    updateSubscriberList: function () {
        localStorage.GBStatsSubscribers = JSON.stringify(GBStats.subscribers);
    },

    sendToGlobalChat: function (message) {
        GBStats.client.emit(
            'general_chat_message',
            message
        );
    },

    sendToPrivateChat: function (user, message) {
        GBStats.client.emit(
            'private_chat_message',
            {
                to:      user,
                message: message
            }
        );
    },

    sendToSubscribers: function (list, message) {
        var subscribers = GBStats.subscribers[list];

        for (var i = 0; i < subscribers.length; i++) {
            if (GBStats.playerList.filter(function (player) { return player === subscribers[i]; }).length > 0) {
                GBStats.sendToPrivateChat(subscribers[i], message);
            }
        }
    },

    setupClient: function (client) {
        GBStats.client = client;

        GBStats.loadSubscriberList();
        GBStats.loadGlobalBosses();

        client
            .removeListener('popup')
            .removeListener('sent_private_message')
            .removeListener('private_chat_message')
            .on('globalBossCountdown', function (data) {
                var timestamp = (new Date()).toLocaleTimeString();

                console.log('[' + timestamp + '] ' + 'Global Boss Countdown: ' + data);

                if (data === null) {
                    GBStats.joinGlobalBoss();
                    console.log('[' + timestamp + '] ' + 'Global Boss Lobby opened');
                }
            })
            .on('matchBegin', function (data) {
                var timestamp = (new Date()).toLocaleTimeString();

                GBStats.globalBosses.push({
                    start: {
                        timestamp: (new Date()).getTime(),
                        data:      data
                    }
                });
            })
            .on('matchEnd', function (data) {
                var timestamp = (new Date()).toLocaleTimeString();

                GBStats.lastGlobalBoss().end = {
                    timestamp: (new Date()).getTime(),
                    data:      data
                };

                GBStats.updateGlobalBosses();
                GBStats.generateSubscriptionMessages();

                GBStats.logSummary();
            })
            .on('general_chat_message', function (data) {
                GBStats.parseMessage(data);
            })
            .on('private_chat_message', function (data) {
                GBStats.parseMessage(data);
            })
            .on('player_list', function (data) {
                var players = data.players;

                GBStats.playerList = [];

                for (var i = 0; i < players.length; i++) {
                    GBStats.playerList.push(players[i].username);
                }
            });

        document.title = '[GBStats] A Mining Game';
    }
};

setTimeout(
    function () {
        GBStats.setupClient(io('http://5.189.137.117:469'));
    },
    5000
);

setInterval(
    function () {
        var currentTime = new Date();

        if (currentTime.getMinutes() === 0) {
            GBStats.sendToGlobalChat('Global Boss Tracker is running.');
            GBStats.showCommands();
        }
    },
    60000
);
<commit_msg>Update GBStats.user.js<commit_after>// ==UserScript==
// @name         A Mining Game: GBStats Bot
// @namespace    https://github.com/Phylogenesis/
// @version      0.1.11
// @description  Runs a bot that tracks global boss stats in A Mining Game
// @author       Luke Jones
// @include      /^http://trugul\.com/(index\.php)?$/
// @grant        none
// @downloadURL  https://github.com/Phylogenesis/greasemonkey/raw/master/GBStats.user.js
// ==/UserScript==

GBStats = {
    // How many global bosses to track.
    // Maximum limit depends on the browser's localStorage settings
    MAX_HISTORY:   1000,

    // How many people to report in the summary messages.
    SUMMARY_COUNT: 5,

    client:        undefined,

    globalBosses:  [],
    playerList:    [],

    subscribers:   {
        summary:  [],
        personal: []
    },

    showCommands: function () {
        GBStats.sendToGlobalChat('Type "#gbstats subscribe" to receive summary reports and "#gbstats subscribe personal" to receive personal reports. Type "#gbstats" on its own to receive a one-off personal report.');
    },

    joinGlobalBoss: function () {
        GBStats.client.emit('globalbossToggleLobby', true);
    },

    lastGlobalBoss: function () {
        return GBStats.globalBosses[GBStats.globalBosses.length - 1];
    },

    generateSubscriptionMessages() {
        GBStats.outputSummary();
        GBStats.sendToSubscribers('summary', 'Type "#gbstats unsubscribe" to unsubscribe from these messages');

        var personalSubscribers = GBStats.subscribers.personal;

        for (var i = 0; i < personalSubscribers.length; i++) {
            if (GBStats.playerList.filter(function (player) { return player === personalSubscribers[i]; }).length > 0) {
                for (var user in GBStats.lastGlobalBoss().end.data.players) {
                    if (GBStats.lastGlobalBoss().end.data.players[user].username === personalSubscribers[i]) {
                        GBStats.outputPersonal(personalSubscribers[i]);

                        GBStats.sendToPrivateChat(
                            personalSubscribers[i],
                            'Type "#gbstats unsubscribe personal" to unsubscribe from these messages'
                        );
                    }
                }
            }
        }
    },

    outputSummary: function (outputToGlobalChat) {
        var lastGlobalBoss = GBStats.lastGlobalBoss();

        var bossName       = lastGlobalBoss.start.data.name;
        var bossHP         = lastGlobalBoss.start.data.startingHP;
        var bossDuration   = (lastGlobalBoss.end.timestamp - lastGlobalBoss.start.timestamp) / 1000;

        var outputFunction = outputToGlobalChat ? GBStats.sendToGlobalChat : function (message) { GBStats.sendToSubscribers('summary', message); };

        var message;

        message =
            'Global Boss Stats (' +
            'Name: ' + bossName + ', ' +
            'Health: ' + Math.round(bossHP) + ' HP, ' +
            'Duration: ' + Math.round(bossDuration) + 's' +
            ')';

        outputFunction(message);

        var attackers = [];

        for (var attacker in lastGlobalBoss.end.data.players) {
            attackers.push(lastGlobalBoss.end.data.players[attacker]);
        }

        attackers.sort(function (a, b) { return b.attacks - a.attacks; });

        var topClickers = attackers.slice(0, GBStats.SUMMARY_COUNT).map(
            function (attacker) {
                return attacker.username + ' (' + attacker.attacks + ', ' + (attacker.attacks / bossDuration).toFixed(1) + '/s)';
            }
        );

        message = 'Top Clickers: ' + topClickers.join(', ');

        outputFunction(message);

        attackers.sort(function (a, b) { return b.damageDealt - a.damageDealt; });

        var topDamage = attackers.slice(0, GBStats.SUMMARY_COUNT).map(
            function (attacker) {
                return attacker.username + ' (' + attacker.damageDealt + ', ' + (attacker.damageDealt / bossHP * 100).toFixed(1) + '%)';
            }
        );

        message = 'Top Damage: ' + topDamage.join(', ');

        outputFunction(message);
    },

    logSummary: function () {
        var lastGlobalBoss = GBStats.lastGlobalBoss();

        console.log('Global Boss (Started: ' + (new Date(lastGlobalBoss.start.timestamp)).toLocaleTimeString() + ')');

        console.groupCollapsed();
        console.log('   ID  Username        Level   Clicks  Damage');
        console.log('-----  --------        -----   ------  ------');

        var attackers = lastGlobalBoss.end.data.players;

        for (var attacker in attackers) {
            var userid   = attackers[attacker].userid;
            var username = attackers[attacker].username;
            var level    = attackers[attacker].level;
            var clicks   = attackers[attacker].attacks;
            var damage   = attackers[attacker].damageDealt;

            console.log(
                (" ".repeat(5) + userid).slice(-5) + "  " +
                (username + " ".repeat(16)).substring(0, 16) +
                (" ".repeat(5) + level).slice(-5) + "   " +
                (" ".repeat(6) + clicks).slice(-6) + "  " +
                (" ".repeat(6) + damage).slice(-6)
            );
        }

        console.groupEnd();
    },

    outputPersonal: function (requester, requestee) {
        requestee = requestee || requester;

        var lastGlobalBoss = GBStats.lastGlobalBoss();

        var attackers = lastGlobalBoss.end.data.players;

        for (var attacker in attackers) {
            if (attackers[attacker].username.toUpperCase() === requestee.toUpperCase()) {
                var bossDuration = (lastGlobalBoss.end.timestamp - lastGlobalBoss.start.timestamp) / 1000;

                GBStats.sendToPrivateChat(
                    requester,
                    'Global Boss (' + (new Date(GBStats.lastGlobalBoss().start.timestamp)).toLocaleTimeString() + ') ' +
                    'User: ' + attackers[attacker].username + ', ' +
                    'Level: ' + attackers[attacker].level + ', ' +
                    'Clicks: ' + attackers[attacker].attacks + ' (' + (attackers[attacker].attacks / bossDuration).toFixed(1) + '/s), ' +
                    'Damage: ' + attackers[attacker].damageDealt
                );

                return;
            }
        }

        GBStats.sendToPrivateChat(requester, ' User ' + requestee + ' did not take part in the last global boss.');
    },

    parseMessage: function (data) {
        if (/^#gbstats/i.test(data.message)) {
            var requester = data.username;
            var command   = data.message.split(' ')[1] || '';
            var list      = data.message.split(' ')[2] || '';

            switch (command.toLowerCase()) {
                case "summary":
                    if (requester === 'Phylogenesis') {
                        GBStats.outputSummary(true);
                    }
                    break;
                case "commands":
                    if (requester === 'Phylogenesis') {
                        GBStats.showCommands();
                    }
                    break;
                case "subscribe":
                    switch (list.toLowerCase()) {
                        case 'personal':
                        case 'summary':
                            GBStats.addSubscriber(list, requester);
                            break;
                        default:
                            GBStats.addSubscriber('summary', requester);
                            break;
                    }
                    break;
                case "unsubscribe":
                    switch (list.toLowerCase()) {
                        case 'personal':
                        case 'summary':
                            GBStats.removeSubscriber(list, requester);
                            break;
                        default:
                            GBStats.removeSubscriber('summary', requester);
                            break;
                    }
                    break;
                default:
                    GBStats.outputPersonal(requester, command);
                    break;
            }
        }
    },

    addSubscriber: function (list, user) {
        if (GBStats.subscribers[list].filter(function (player) { return player === user; }).length === 0) {
            GBStats.subscribers[list].push(user);
            GBStats.updateSubscriberList();

            GBStats.sendToPrivateChat(user, 'Added you to the ' + list + ' subscription list');
        } else {
            GBStats.sendToPrivateChat(user, 'You are already in the ' + list + ' subscription list');
        }
    },

    removeSubscriber: function (list, user) {
        if (GBStats.subscribers[list].filter(function (player) { return player === user; }).length > 0) {
            GBStats.subscribers[list] = GBStats.subscribers[list].filter(function (subscriber) { return subscriber !== user; });
            GBStats.updateSubscriberList();

            GBStats.sendToPrivateChat(user, 'Removed you from the ' + list + ' subscription list');
        } else {
            GBStats.sendToPrivateChat(user, 'You are not in the ' + list + ' subscription list');
        }
    },

    loadGlobalBosses: function () {
        GBStats.globalBosses = JSON.parse(localStorage.GBStatsGlobalBosses);
    },

    updateGlobalBosses: function () {
        localStorage.GBStatsGlobalBosses = JSON.stringify(GBStats.globalBosses.slice(-GBStats.MAX_HISTORY));
    },

    loadSubscriberList: function () {
        GBStats.subscribers = JSON.parse(localStorage.GBStatsSubscribers);
    },

    updateSubscriberList: function () {
        localStorage.GBStatsSubscribers = JSON.stringify(GBStats.subscribers);
    },

    sendToGlobalChat: function (message) {
        GBStats.client.emit(
            'general_chat_message',
            message
        );
    },

    sendToPrivateChat: function (user, message) {
        GBStats.client.emit(
            'private_chat_message',
            {
                to:      user,
                message: message
            }
        );
    },

    sendToSubscribers: function (list, message) {
        var subscribers = GBStats.subscribers[list];

        for (var i = 0; i < subscribers.length; i++) {
            if (GBStats.playerList.filter(function (player) { return player === subscribers[i]; }).length > 0) {
                GBStats.sendToPrivateChat(subscribers[i], message);
            }
        }
    },

    setupClient: function (client) {
        GBStats.client = client;

        GBStats.loadSubscriberList();
        GBStats.loadGlobalBosses();

        client
            .removeListener('popup')
            .removeListener('sent_private_message')
            .removeListener('private_chat_message')
            .on('globalBossCountdown', function (data) {
                var timestamp = (new Date()).toLocaleTimeString();

                if (data === null) {
                    GBStats.joinGlobalBoss();
                }
            })
            .on('matchBegin', function (data) {
                var timestamp = (new Date()).toLocaleTimeString();

                GBStats.globalBosses.push({
                    start: {
                        timestamp: (new Date()).getTime(),
                        data:      data
                    }
                });
            })
            .on('matchEnd', function (data) {
                var timestamp = (new Date()).toLocaleTimeString();

                GBStats.lastGlobalBoss().end = {
                    timestamp: (new Date()).getTime(),
                    data:      data
                };

                GBStats.updateGlobalBosses();
                GBStats.generateSubscriptionMessages();

                GBStats.logSummary();
            })
            .on('general_chat_message', function (data) {
                GBStats.parseMessage(data);
            })
            .on('private_chat_message', function (data) {
                GBStats.parseMessage(data);
            })
            .on('player_list', function (data) {
                var players = data.players;

                GBStats.playerList = [];

                for (var i = 0; i < players.length; i++) {
                    GBStats.playerList.push(players[i].username);
                }
            });

        document.title = '[GBStats] A Mining Game';
    }
};

setTimeout(
    function () {
        GBStats.setupClient(io('http://5.189.137.117:469'));
    },
    5000
);

setInterval(
    function () {
        var currentTime = new Date();

        if (currentTime.getMinutes() === 0) {
            GBStats.sendToGlobalChat('Global Boss Tracker is running.');
            GBStats.showCommands();
        }
    },
    60000
);
<|endoftext|>


<commit_before>import _ from 'lodash';
import pubsub from 'pubsub-js';
import rangeCheck from 'range_check';
import serialport from 'serialport';
import socketIO from 'socket.io';
import store from './store';
import log from './lib/log';
import settings from './config/settings';
import GrblController from './controllers/grbl';

const ALLOWED_IP_RANGES = [
    // IPv4 reserved space
    '10.0.0.0/8', // Used for local communications within a private network
    '127.0.0.0/8', // Used for loopback addresses to the local host
    '172.16.0.0/12', // Used for local communications within a private network
    '192.168.0.0/16', // Used for local communications within a private network

    // IPv4 mapped IPv6 address
    '::ffff:10.0.0.0/8',
    '::ffff:127.0.0.0/8',
    '::ffff:172.16.0.0/12',
    '::ffff:192.168.0.0/16',

    // IPv6 reserved space
    '::1/128', // loopback address to the local host
    'fc00::/7', // Unique local address
    'fe80::/10' // Link-local address
];

pubsub.subscribe('gcode:upload', (msg, data) => {
    let { port, meta, gcode } = data;
    let { name = '', size = 0 } = meta;
    let controller = store.get('controllers["' + port + '"]');

    if (!controller) {
        log.error('[cncserver] Controller not found:', { meta: meta });
        return;
    }

    // Load G-code
    let socket = null;
    controller.command(socket, 'load', name, gcode, (err) => {
        if (err) {
            log.error('[cncserver] Failed to load G-code: name="%s":', name, err);
        }
    });
});

class CNCServer {
    server = null;
    sockets = [];
    controllers = store.get('controllers');

    constructor(server) {
        this.server = server;

        store.on('change', (state) => {
            log.debug('[cncserver] Update controllers:', state);
            this.controllers = _.get(state, 'controllers', {});
        });
    }
    start() {
        let io = socketIO(this.server, {
            serveClient: true,
            path: '/socket.io'
        });

        io.use((socket, next) => {
            let address = socket.handshake.address;
            let allowed = _.some(ALLOWED_IP_RANGES, (range) => {
                return rangeCheck.inRange(address, range);
            });

            if (!allowed) {
                log.warn('[cncserver] Reject connection from %s', address);
                next(new Error('You are not allowed on this server!'));
                return;
            }

            next();
        });

        io.on('connection', (socket) => {
            let address = socket.handshake.address;
            log.debug('[cncserver] New connection from %s:', address, { id: socket.id });

            // Add to the socket pool
            this.sockets.push(socket);

            socket.on('disconnect', () => {
                log.debug('[cncserver] socket.disconnect():', { id: socket.id });

                _.each(this.controllers, (controller, port) => {
                    if (!controller) {
                        return;
                    }
                    controller.removeConnection(socket);
                });

                // Remove from the socket pool
                this.sockets.splice(this.sockets.indexOf(socket), 1);
            });

            // Show available serial ports
            socket.on('list', () => {
                log.debug('[cncserver] socket.list():', { id: socket.id });

                serialport.list((err, ports) => {
                    if (err) {
                        log.error(err);
                        return;
                    }

                    ports = ports.concat(_.get(settings, 'cnc.ports') || []);

                    let portsInUse = _(this.controllers)
                        .filter((controller) => {
                            return controller && controller.isOpen();
                        })
                        .map((controller) => {
                            return controller.port;
                        })
                        .value();

                    ports = _.map(ports, (port) => {
                        return {
                            port: port.comName,
                            manufacturer: port.manufacturer,
                            inuse: _.includes(portsInUse, port.comName) ? true : false
                        };
                    });

                    socket.emit('serialport:list', ports);
                });
            });

            // Open serial port
            socket.on('open', (port, baudrate) => {
                log.debug('[cncserver] socket.open("%s", %d):', port, baudrate, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller) {
                    controller = new GrblController(port, baudrate);
                }

                if (controller.isOpen()) {
                    controller.addConnection(socket);
                    socket.emit('serialport:open', {
                        port: port,
                        baudrate: baudrate,
                        inuse: true
                    });
                    return;
                }

                controller.open((err) => {
                    if (err) {
                        log.error('[cncserver] Error opening serial port "%s":', port, err);
                        socket.emit('serialport:error', { port: port });
                        return;
                    }

                    // It should default to an undefined value
                    console.assert(_.isUndefined(this.controllers[port]));
                    store.set('controllers["' + port + '"]', controller);

                    controller.addConnection(socket);
                    socket.emit('serialport:open', {
                        port: port,
                        baudrate: baudrate,
                        inuse: true
                    });
                });
            });

            // Close serial port
            socket.on('close', (port) => {
                log.debug('[cncserver] socket.close("%s"):', port, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller) {
                    log.error('[cncserver] Serial port "%s" not accessible', port);
                    return;
                }

                controller.close((err) => {
                    if (err) {
                        log.error('[cncserver] Error closing serial port "%s":', port, err);
                    }
                    store.unset('controllers["' + port + '"]');
                });
            });

            socket.on('command', (port, cmd) => {
                log.debug('[cncserver] socket.command("%s", "%s"):', port, cmd, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller || controller.isClose()) {
                    log.error('[cncserver] Serial port "%s" not accessible', port);
                    return;
                }

                controller.command(socket, cmd);
            });

            socket.on('write', (port, data) => {
                log.debug('[cncserver] socket.write("%s", "%s"):', port, data, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller || controller.isClose()) {
                    log.error('[cncserver] Serial port "%s" not accessible', port);
                    return;
                }

                controller.write(socket, data);
            });
        });
    }
    stop() {
    }
}

const serverMain = (server) => {
    let cncServer = new CNCServer(server);
    cncServer.start();
};

module.exports = serverMain;
<commit_msg>Refine debug logs<commit_after>import _ from 'lodash';
import pubsub from 'pubsub-js';
import rangeCheck from 'range_check';
import serialport from 'serialport';
import socketIO from 'socket.io';
import store from './store';
import log from './lib/log';
import settings from './config/settings';
import GrblController from './controllers/grbl';

const ALLOWED_IP_RANGES = [
    // IPv4 reserved space
    '10.0.0.0/8', // Used for local communications within a private network
    '127.0.0.0/8', // Used for loopback addresses to the local host
    '172.16.0.0/12', // Used for local communications within a private network
    '192.168.0.0/16', // Used for local communications within a private network

    // IPv4 mapped IPv6 address
    '::ffff:10.0.0.0/8',
    '::ffff:127.0.0.0/8',
    '::ffff:172.16.0.0/12',
    '::ffff:192.168.0.0/16',

    // IPv6 reserved space
    '::1/128', // loopback address to the local host
    'fc00::/7', // Unique local address
    'fe80::/10' // Link-local address
];

pubsub.subscribe('gcode:upload', (msg, data) => {
    let { port, meta, gcode } = data;
    let { name = '', size = 0 } = meta;
    let controller = store.get('controllers["' + port + '"]');

    if (!controller) {
        log.error('[cncserver] Controller not found:', { meta: meta });
        return;
    }

    // Load G-code
    let socket = null;
    controller.command(socket, 'load', name, gcode, (err) => {
        if (err) {
            log.error('[cncserver] Failed to load G-code: name="%s":', name, err);
        }
    });
});

class CNCServer {
    server = null;
    sockets = [];
    controllers = store.get('controllers');

    constructor(server) {
        this.server = server;

        store.on('change', (state) => {
            this.controllers = _.get(state, 'controllers', {});
        });
    }
    start() {
        let io = socketIO(this.server, {
            serveClient: true,
            path: '/socket.io'
        });

        io.use((socket, next) => {
            let address = socket.handshake.address;
            let allowed = _.some(ALLOWED_IP_RANGES, (range) => {
                return rangeCheck.inRange(address, range);
            });

            if (!allowed) {
                log.warn('[cncserver] Reject connection from %s', address);
                next(new Error('You are not allowed on this server!'));
                return;
            }

            next();
        });

        io.on('connection', (socket) => {
            let address = socket.handshake.address;
            log.debug('[cncserver] New connection from %s:', address, { id: socket.id });

            // Add to the socket pool
            this.sockets.push(socket);

            socket.on('disconnect', () => {
                log.debug('[cncserver] socket.disconnect():', { id: socket.id });

                _.each(this.controllers, (controller, port) => {
                    if (!controller) {
                        return;
                    }
                    controller.removeConnection(socket);
                });

                // Remove from the socket pool
                this.sockets.splice(this.sockets.indexOf(socket), 1);
            });

            // Show available serial ports
            socket.on('list', () => {
                log.debug('[cncserver] socket.list():', { id: socket.id });

                serialport.list((err, ports) => {
                    if (err) {
                        log.error(err);
                        return;
                    }

                    ports = ports.concat(_.get(settings, 'cnc.ports') || []);

                    let portsInUse = _(this.controllers)
                        .filter((controller) => {
                            return controller && controller.isOpen();
                        })
                        .map((controller) => {
                            return controller.port;
                        })
                        .value();

                    ports = _.map(ports, (port) => {
                        return {
                            port: port.comName,
                            manufacturer: port.manufacturer,
                            inuse: _.includes(portsInUse, port.comName) ? true : false
                        };
                    });

                    socket.emit('serialport:list', ports);
                });
            });

            // Open serial port
            socket.on('open', (port, baudrate) => {
                log.debug('[cncserver] socket.open("%s", %d):', port, baudrate, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller) {
                    controller = new GrblController(port, baudrate);
                }

                if (controller.isOpen()) {
                    controller.addConnection(socket);
                    socket.emit('serialport:open', {
                        port: port,
                        baudrate: baudrate,
                        inuse: true
                    });
                    return;
                }

                controller.open((err) => {
                    if (err) {
                        log.error('[cncserver] Error opening serial port "%s":', port, err);
                        socket.emit('serialport:error', { port: port });
                        return;
                    }

                    // It should default to an undefined value
                    console.assert(_.isUndefined(this.controllers[port]));
                    store.set('controllers["' + port + '"]', controller);

                    controller.addConnection(socket);
                    socket.emit('serialport:open', {
                        port: port,
                        baudrate: baudrate,
                        inuse: true
                    });
                });
            });

            // Close serial port
            socket.on('close', (port) => {
                log.debug('[cncserver] socket.close("%s"):', port, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller) {
                    log.error('[cncserver] Serial port "%s" not accessible', port);
                    return;
                }

                controller.close((err) => {
                    if (err) {
                        log.error('[cncserver] Error closing serial port "%s":', port, err);
                    }
                    store.unset('controllers["' + port + '"]');
                });
            });

            socket.on('command', (port, cmd) => {
                log.debug('[cncserver] socket.command("%s", "%s"):', port, cmd, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller || controller.isClose()) {
                    log.error('[cncserver] Serial port "%s" not accessible', port);
                    return;
                }

                controller.command(socket, cmd);
            });

            socket.on('write', (port, data) => {
                log.debug('[cncserver] socket.write("%s", "%s"):', port, data, { id: socket.id });

                let controller = this.controllers[port];
                if (!controller || controller.isClose()) {
                    log.error('[cncserver] Serial port "%s" not accessible', port);
                    return;
                }

                controller.write(socket, data);
            });
        });
    }
    stop() {
    }
}

const serverMain = (server) => {
    let cncServer = new CNCServer(server);
    cncServer.start();
};

module.exports = serverMain;
<|endoftext|>