<commit_before>/**
 * The job, cron job, to be runned.
 * @module sms-library-helper/backend/job
 * @author Holi0317 <<EMAIL>>
 * @license MIT
 *
 * @requires googleapis
 * @requires bluebird
 */

'use strict';

let google = require('googleapis');
let Promise = require('bluebird');
let LibraryApi = require('./api');
let models = require('./models');
let utils = require('./utils');

/**
 * One day in milisecond.
 * @const {number} ONE_DAY
 * @default 8.64e+7
 */
const ONE_DAY = 8.64e+7;
/**
 * Timezone that library system is at.
 * @const {string} TIMEZONE
 * @default 'Asia/Hong_Kong'
 */
const TIMEZONE = 'Asia/Hong_Kong';
/**
 * Maximum log to be kept.
 * @const {number} MAX_LOG_RECORD
 * @default 100
 */
const MAX_LOG_RECORD = 100;

// Google apis
let calendar = google.calendar('v3');
Promise.promisifyAll(calendar.calendarList);
Promise.promisifyAll(calendar.calendars);
Promise.promisifyAll(calendar.events);

/**
 * Convert Date object to Google calendar's format, 'yyyy-mm-dd'.
 *
 * @param {Date} date - Date object to be converted.
 *
 * @returns {string} - Date with yyyy-mm-dd as format.
 * @private
 */
function dateToGoogle(date) {
  return `${date.getFullYear()}-${('0' + (date.getMonth()+1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}`
}

/**
 * A collection of promises that bind to a user's library and google account.
 * Use UserFunctionFactory for batch execution.
 *
 * @prop {backend/models.user} user - User object queried from database.
 * @prop {google.auth.OAuth2} oauth2client - Google OAuth2 bounded to user token.
 * @prop {backend/api.User} library - Library API bound to user.
 * @prop {Number} celdnarID - Google calendar ID that matches the name of user defined.
 * @prop {bool} failed - Trye if any operation failed.
 * @prop {function} log - Log message to user.
 *
 * @see {@link sms-library-helper/backend/job~UserFunctionFactory}
 */
class UserFunctions {

  /**
   * Construct a new UserFunctions object.
   *
   * @param {backend/models.user} user - User (database record) that this object will be bounded to.
   *
   * @constructor
   */
  constructor(user) {
    this.user = user;
    this.oauth2client = utils.oauth2clientFactory();
    this.oauth2client.setCredentials(this.user.tokens);
    this.library = new LibraryApi();
    this.calendarID = null;
    this.failed = false;

    this.log = this.user.log.bind(this.user);
  }

  /**
   * Refresh user's Google OAuth2 token and write to database.
   *
   * @throws {Error} - Cannot refresh OAuth2 tokens.
   * @returns {Proimse} - Promise for refreshing token then writes into database.
   */
  refreshToken() {
    return this.oauth2client.refreshAccessTokenAsync()
    .catch(err => {
      console.error('Error when refreshing access token. Error: ', err);
      this.log('Cannot refresh Google token. Aborting.', 'FATAL');
      this.failed = true;
      throw new utils.BreakSignal();
    })
    .then(newTokens => {
      this.log('Refreshed Google tokens.', 'DEBUG')
      // OAuth2 does not pass back refresh_token.
      newTokens.refresh_token = this.user.tokens.refresh_token;
      this.user.tokens = newTokens;
      return this.user.save();
    });
  }

  /**
   * Login library system, then renew books if needed.
   *
   * @throws {Error} - Login into library system failed.
   * @returns {Promise} - Promise for renew login and renew book, if needed.
   * Or empty promise if failed or not enabled.
   */
  renewBooks() {
    if (!this.user.renewEnabled || this.failed) {
      return Promise.resolve();
    }

    return this.library.login(this.user.libraryLogin, this.user.libraryPassword)
    .catch(err => {
      console.error('Error when logging into library system. Error: ', err);
      this.log('Cannot login library system. Is password changed?', 'FATAL');
      this.failed = true;
      throw new utils.BreakSignal();
    })
    .then(() => {
      let now = new Date();
      let promises = [];  // Promises for renewing book to be returned.
      let borrowedBooks = [];   // Book name for all borrowed books. For logging purpose.
      let renewBooks = [];    // Book name for all books requires to be renewed. For logging purpose.

      for (let book of this.library.borrowedBooks) {  // Each borrowed books.

        borrowedBooks.push(book.name);  // Logging.

        let diff = book.dueDate - now;
        if (diff <= this.user.renewDate * ONE_DAY && diff > 0 && book.id) {
          // If Logic: Less than defined date, more than 0 day and have book ID.
          promises.push(this.library.renewBook(book));    // Create promise.
          renewBooks.push(book.name);    // Logging.
        }
      }

      if (borrowedBooks.length) {
        this.log(`You have borrowed the followings books: ${borrowedBooks.join(', ')}`);
      } else {
        this.log('No borrowed books detected.');
      }

      if (renewBooks.length) {
        this.log(`The following books will be renewed: ${renewBooks.join(', ')}`);
      }

      return Promise.all(promises);
    });

  }

  /**
   * List calendars and see if there is one equals to name user defined (user.calendarName).
   * If yes, Save the calendar ID to property and return empty Promise.
   * Otherwise, return a promise that creates calenar (_createCalendar).
   *
   * @see {@link sms-library-helper/backend/job~UserFunctions~_createCalendar}
   * @returns {Promise} - See above description.
   * @throws {Error} - Cannot list calendar.
   * @private
   */
  _getCalendar() {
    if (this.failed) {
      return Promise.resolve();
    }

    return calendar.calendarList.listAsync({
      auth: this.oauth2client,
      maxResults: 250,
      showHidden: true,
      minAccessRole: 'writer'
    })
    .catch(err => {
      console.error('Error when trying to list calendar. Error: ', err);
      this.log('Cannot list Google Calendar. Aborting.', 'FATAL');
      this.failed = true;
      throw err;
    })
    .then(res => {

      if (res.nextPageToken) {
        this.log('Found there is more than 250 items in calendar list. A new calendar may be created.', 'WARN');
      }

      for (let item of res.items) {
        if (item.summary === this.user.calendarName) {

          // Got calendar that I want.
          this.calendarID = item.id;
          this.log(`Found Google Calendar. ID: ${item.id}.`, 'DEBUG');
          return Promise.resolve();

        }
        // Else, continue.
      }

      // Calendar not found. Lets create one.
      this.log(`Cannot find Google Calendar with name "${this.user.calendarName}". Creating one.`);
      return this._createCalendar();

    });
  }

  /**
   * Create new calendar, where summary is user.calendarName and handle stuffs.
   *
   * @returns {Promise} - Create new calendar.
   * @throws {Error} - Cannot create calendar/insert it into CalendarList.
   * @private
   */
  _createCalendar() {
    if (this.failed) {
      return Promise.resolve();
    }

    return calendar.calendars.insertAsync({
      auth: this.oauth2client,
      resource: {
        summary: this.user.calendarName,
        timeZone: TIMEZONE
      }
    })
    .then(cal => {
      this.log(`Calendar created. ID: ${cal.id}.`, 'DEBUG');
      this.calendarID = cal.id;
      return Promise.resolve();
    })
    .catch(err => {
      this.calendarID = null;
      console.error('Error when creating new calendar. Error: ', err);
      this.log('Cannot create calendar / Insert calendar to CalendarList. Aborting', 'FATAL');
      this.failed = true;
      throw new utils.BreakSignal();
    })
  }

  /**
   * Create calendar event resource by using book object passed in.
   *
   * @param {backend/api.Book} book - Book object that need to be converted into event resource.
   * @returns {Object} - Event resource (Google calendar) of this book.
   * @static
   * @private
   */
  _createEventResource (book) {
    let date = dateToGoogle(book.dueDate);

    return {
      summary: `Due date for book ${book.name}. ID: ${book.id}`,
      start: {
        date: date,
        timeZone: TIMEZONE
      },
      end: {
        date: date,
        timeZone: TIMEZONE
      }
    };
  }

  /**
   * Reload library content and write corresponding due date to Google calendar.
   *
   * @returns {Promise} - Promise for the above action.
   * @throws {Errors} - Cannot reload library content.
   * @throws {Errors} - Cannot get Google Calendar ID.
   * @throws {Errors} - Cannot insert Calendar event.
   */
  refreshCalendar() {
    // Refresh user's google calendar
    // If user.renewEnabled is false, this will return an empty promise.
    // Return Promise
    if (!this.user.renewEnabled || this.failed) {
      return Promise.resolve();
    }

    let promises = [];
    let touchedEvents = [];
    let logUpdated = [];
    let logCreated = [];

    /**
     * Process a book. Should it be updated in Google Calendar?
     *
     * @returns {Boolean} - Is the book already in Google Calendar.
     */
    let processBook = function (resource, events) {
      for (let event of events.items) {   // For each event.
        let resDate = resource.end.date;
        if (event.summary === resource.summary) {   // Is this event the one I want for this book?
          if (event.end.date !== resDate || event.start.date !== resDate) {   // Has event date changed? (renewed/touched?)
            promises.push(calendar.events.updateAsync({
              auth: this.oauth2client,
              calendarId: this.calendarID,
              eventId: event.id,
              resource: resource
            }));
          }
          touchedEvents.push(event.id);
          return true;  // Already in Calendar
        }
      }
      return false;   // Not in Calendar.
    }.bind(this);

    return this._getCalendar()
    .then(this.library.reload.bind(this.library))
    .catch(err => {
      if (err instanceof utils.BreakSignal) {
        throw err;
      }
      console.error('Error when reloading library contents. Error: ', err);
      this.failed = true;
      this.log('Cannot reloading library contents. WTF? This should not happen. Aborting', 'FATAL');
      throw new utils.BreakSignal();
    })
    .then(() => {
      // List events in calendar
      return calendar.events.listAsync({
        auth: this.oauth2client,
        calendarId: this.calendarID,
        timeZone: TIMEZONE,
        maxResults: 2500
      });
    })
    .catch(err => {
      if (err instanceof utils.BreakSignal) {
        throw err;
      }
      console.error('Error when listing events in Google Calendar. Error: ', err);
      this.failed = true;
      this.log('Cannot listing events in Google Calendar. Aborting.', 'FATAL');
      throw new utils.BreakSignal();
    })
    .then(events => {

      if (events.nextPageToken) {
        this.log('More than 2500 events found in calendar (Seriously?). Some event may be missed out.', 'WARN');
      }

      for (let book of this.library.borrowedBooks) {  // Fore each book.
        if (book.id === null) {
          // Book does not have id. Probably overdued.
          continue;
        }

        // Event resource for requesting
        let resource = this._createEventResource(book);

        // The book is not in Google Calendar. Create a new one.
        if (!processBook(resource, events)) {
          promises.push(calendar.events.insertAsync({
            auth: this.oauth2client,
            calendarId: this.calendarID,
            resource: resource
          }));

          logCreated.push(book.name);

        }

      }

      // Cleanup events that has not been touched(Illegal events or returned books)
      let allEventsID = events.items.map(item => {
        return item.id;
      });
      let removes = utils.diff(allEventsID, touchedEvents);
      for (let eventID of removes) {
        promises.push(calendar.events.deleteAsync({
          auth: this.oauth2client,
          calendarId: this.calendarID,
          eventId: eventID
        }));
      }

      if (logUpdated.length) {
        this.log(`Calendar event(s) of the following books will be updated: ${logUpdated.join(', ')}`);
      }
      if (logCreated.length) {
        this.log(`Calendar event(s) of the following books will be created: ${logCreated.join(', ')}`);
      }
      if (removes.length) {
        this.log(`The following event(s) will be removed: ${removes.join(', ')}`, 'DEBUG');
      }

      return Promise.all(promises);

    });

  }

  /**
   * Limit log message to 100 and write into database.
   *
   * @returns {Promise} - Write optration to database.
   */
  saveProfile() {
    if (this.failed) {
      this.log('Cron job failed.', 'FATAL');
    } else {
      this.log('Cron job succeed.', 'SUCCESS');
    }

    this.user.logs.sort((a, b) => {
      return b.time - a.time;
    });
    this.user.logs = this.user.logs.slice(-MAX_LOG_RECORD);
    return this.user.save();
  }

}

/**
 * Batcher for executing mass amount of UserFunctions.
 * Methods are identical to {@link UserFunctions}, but errors will be supressed.
 *
 * @prop {UserFunctions[]} users - Users that this batcher is bounded to.
 */
class UserFunctionFactory {

  /**
   * @constructor
   */
  constructor() {
    this.users = [];
  }
<commit_msg>[FIX] Cron log limit error<commit_after>/**
 * The job, cron job, to be runned.
 * @module sms-library-helper/backend/job
 * @author Holi0317 <<EMAIL>>
 * @license MIT
 *
 * @requires googleapis
 * @requires bluebird
 */

'use strict';

let google = require('googleapis');
let Promise = require('bluebird');
let LibraryApi = require('./api');
let models = require('./models');
let utils = require('./utils');

/**
 * One day in milisecond.
 * @const {number} ONE_DAY
 * @default 8.64e+7
 */
const ONE_DAY = 8.64e+7;
/**
 * Timezone that library system is at.
 * @const {string} TIMEZONE
 * @default 'Asia/Hong_Kong'
 */
const TIMEZONE = 'Asia/Hong_Kong';
/**
 * Maximum log to be kept.
 * @const {number} MAX_LOG_RECORD
 * @default 100
 */
const MAX_LOG_RECORD = 100;

// Google apis
let calendar = google.calendar('v3');
Promise.promisifyAll(calendar.calendarList);
Promise.promisifyAll(calendar.calendars);
Promise.promisifyAll(calendar.events);

/**
 * Convert Date object to Google calendar's format, 'yyyy-mm-dd'.
 *
 * @param {Date} date - Date object to be converted.
 *
 * @returns {string} - Date with yyyy-mm-dd as format.
 * @private
 */
function dateToGoogle(date) {
  return `${date.getFullYear()}-${('0' + (date.getMonth()+1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}`
}

/**
 * A collection of promises that bind to a user's library and google account.
 * Use UserFunctionFactory for batch execution.
 *
 * @prop {backend/models.user} user - User object queried from database.
 * @prop {google.auth.OAuth2} oauth2client - Google OAuth2 bounded to user token.
 * @prop {backend/api.User} library - Library API bound to user.
 * @prop {Number} celdnarID - Google calendar ID that matches the name of user defined.
 * @prop {bool} failed - Trye if any operation failed.
 * @prop {function} log - Log message to user.
 *
 * @see {@link sms-library-helper/backend/job~UserFunctionFactory}
 */
class UserFunctions {

  /**
   * Construct a new UserFunctions object.
   *
   * @param {backend/models.user} user - User (database record) that this object will be bounded to.
   *
   * @constructor
   */
  constructor(user) {
    this.user = user;
    this.oauth2client = utils.oauth2clientFactory();
    this.oauth2client.setCredentials(this.user.tokens);
    this.library = new LibraryApi();
    this.calendarID = null;
    this.failed = false;

    this.log = this.user.log.bind(this.user);
  }

  /**
   * Refresh user's Google OAuth2 token and write to database.
   *
   * @throws {Error} - Cannot refresh OAuth2 tokens.
   * @returns {Proimse} - Promise for refreshing token then writes into database.
   */
  refreshToken() {
    return this.oauth2client.refreshAccessTokenAsync()
    .catch(err => {
      console.error('Error when refreshing access token. Error: ', err);
      this.log('Cannot refresh Google token. Aborting.', 'FATAL');
      this.failed = true;
      throw new utils.BreakSignal();
    })
    .then(newTokens => {
      this.log('Refreshed Google tokens.', 'DEBUG')
      // OAuth2 does not pass back refresh_token.
      newTokens.refresh_token = this.user.tokens.refresh_token;
      this.user.tokens = newTokens;
      return this.user.save();
    });
  }

  /**
   * Login library system, then renew books if needed.
   *
   * @throws {Error} - Login into library system failed.
   * @returns {Promise} - Promise for renew login and renew book, if needed.
   * Or empty promise if failed or not enabled.
   */
  renewBooks() {
    if (!this.user.renewEnabled || this.failed) {
      return Promise.resolve();
    }

    return this.library.login(this.user.libraryLogin, this.user.libraryPassword)
    .catch(err => {
      console.error('Error when logging into library system. Error: ', err);
      this.log('Cannot login library system. Is password changed?', 'FATAL');
      this.failed = true;
      throw new utils.BreakSignal();
    })
    .then(() => {
      let now = new Date();
      let promises = [];  // Promises for renewing book to be returned.
      let borrowedBooks = [];   // Book name for all borrowed books. For logging purpose.
      let renewBooks = [];    // Book name for all books requires to be renewed. For logging purpose.

      for (let book of this.library.borrowedBooks) {  // Each borrowed books.

        borrowedBooks.push(book.name);  // Logging.

        let diff = book.dueDate - now;
        if (diff <= this.user.renewDate * ONE_DAY && diff > 0 && book.id) {
          // If Logic: Less than defined date, more than 0 day and have book ID.
          promises.push(this.library.renewBook(book));    // Create promise.
          renewBooks.push(book.name);    // Logging.
        }
      }

      if (borrowedBooks.length) {
        this.log(`You have borrowed the followings books: ${borrowedBooks.join(', ')}`);
      } else {
        this.log('No borrowed books detected.');
      }

      if (renewBooks.length) {
        this.log(`The following books will be renewed: ${renewBooks.join(', ')}`);
      }

      return Promise.all(promises);
    });

  }

  /**
   * List calendars and see if there is one equals to name user defined (user.calendarName).
   * If yes, Save the calendar ID to property and return empty Promise.
   * Otherwise, return a promise that creates calenar (_createCalendar).
   *
   * @see {@link sms-library-helper/backend/job~UserFunctions~_createCalendar}
   * @returns {Promise} - See above description.
   * @throws {Error} - Cannot list calendar.
   * @private
   */
  _getCalendar() {
    if (this.failed) {
      return Promise.resolve();
    }

    return calendar.calendarList.listAsync({
      auth: this.oauth2client,
      maxResults: 250,
      showHidden: true,
      minAccessRole: 'writer'
    })
    .catch(err => {
      console.error('Error when trying to list calendar. Error: ', err);
      this.log('Cannot list Google Calendar. Aborting.', 'FATAL');
      this.failed = true;
      throw err;
    })
    .then(res => {

      if (res.nextPageToken) {
        this.log('Found there is more than 250 items in calendar list. A new calendar may be created.', 'WARN');
      }

      for (let item of res.items) {
        if (item.summary === this.user.calendarName) {

          // Got calendar that I want.
          this.calendarID = item.id;
          this.log(`Found Google Calendar. ID: ${item.id}.`, 'DEBUG');
          return Promise.resolve();

        }
        // Else, continue.
      }

      // Calendar not found. Lets create one.
      this.log(`Cannot find Google Calendar with name "${this.user.calendarName}". Creating one.`);
      return this._createCalendar();

    });
  }

  /**
   * Create new calendar, where summary is user.calendarName and handle stuffs.
   *
   * @returns {Promise} - Create new calendar.
   * @throws {Error} - Cannot create calendar/insert it into CalendarList.
   * @private
   */
  _createCalendar() {
    if (this.failed) {
      return Promise.resolve();
    }

    return calendar.calendars.insertAsync({
      auth: this.oauth2client,
      resource: {
        summary: this.user.calendarName,
        timeZone: TIMEZONE
      }
    })
    .then(cal => {
      this.log(`Calendar created. ID: ${cal.id}.`, 'DEBUG');
      this.calendarID = cal.id;
      return Promise.resolve();
    })
    .catch(err => {
      this.calendarID = null;
      console.error('Error when creating new calendar. Error: ', err);
      this.log('Cannot create calendar / Insert calendar to CalendarList. Aborting', 'FATAL');
      this.failed = true;
      throw new utils.BreakSignal();
    })
  }

  /**
   * Create calendar event resource by using book object passed in.
   *
   * @param {backend/api.Book} book - Book object that need to be converted into event resource.
   * @returns {Object} - Event resource (Google calendar) of this book.
   * @static
   * @private
   */
  _createEventResource (book) {
    let date = dateToGoogle(book.dueDate);

    return {
      summary: `Due date for book ${book.name}. ID: ${book.id}`,
      start: {
        date: date,
        timeZone: TIMEZONE
      },
      end: {
        date: date,
        timeZone: TIMEZONE
      }
    };
  }

  /**
   * Reload library content and write corresponding due date to Google calendar.
   *
   * @returns {Promise} - Promise for the above action.
   * @throws {Errors} - Cannot reload library content.
   * @throws {Errors} - Cannot get Google Calendar ID.
   * @throws {Errors} - Cannot insert Calendar event.
   */
  refreshCalendar() {
    // Refresh user's google calendar
    // If user.renewEnabled is false, this will return an empty promise.
    // Return Promise
    if (!this.user.renewEnabled || this.failed) {
      return Promise.resolve();
    }

    let promises = [];
    let touchedEvents = [];
    let logUpdated = [];
    let logCreated = [];

    /**
     * Process a book. Should it be updated in Google Calendar?
     *
     * @returns {Boolean} - Is the book already in Google Calendar.
     */
    let processBook = function (resource, events) {
      for (let event of events.items) {   // For each event.
        let resDate = resource.end.date;
        if (event.summary === resource.summary) {   // Is this event the one I want for this book?
          if (event.end.date !== resDate || event.start.date !== resDate) {   // Has event date changed? (renewed/touched?)
            promises.push(calendar.events.updateAsync({
              auth: this.oauth2client,
              calendarId: this.calendarID,
              eventId: event.id,
              resource: resource
            }));
          }
          touchedEvents.push(event.id);
          return true;  // Already in Calendar
        }
      }
      return false;   // Not in Calendar.
    }.bind(this);

    return this._getCalendar()
    .then(this.library.reload.bind(this.library))
    .catch(err => {
      if (err instanceof utils.BreakSignal) {
        throw err;
      }
      console.error('Error when reloading library contents. Error: ', err);
      this.failed = true;
      this.log('Cannot reloading library contents. WTF? This should not happen. Aborting', 'FATAL');
      throw new utils.BreakSignal();
    })
    .then(() => {
      // List events in calendar
      return calendar.events.listAsync({
        auth: this.oauth2client,
        calendarId: this.calendarID,
        timeZone: TIMEZONE,
        maxResults: 2500
      });
    })
    .catch(err => {
      if (err instanceof utils.BreakSignal) {
        throw err;
      }
      console.error('Error when listing events in Google Calendar. Error: ', err);
      this.failed = true;
      this.log('Cannot listing events in Google Calendar. Aborting.', 'FATAL');
      throw new utils.BreakSignal();
    })
    .then(events => {

      if (events.nextPageToken) {
        this.log('More than 2500 events found in calendar (Seriously?). Some event may be missed out.', 'WARN');
      }

      for (let book of this.library.borrowedBooks) {  // Fore each book.
        if (book.id === null) {
          // Book does not have id. Probably overdued.
          continue;
        }

        // Event resource for requesting
        let resource = this._createEventResource(book);

        // The book is not in Google Calendar. Create a new one.
        if (!processBook(resource, events)) {
          promises.push(calendar.events.insertAsync({
            auth: this.oauth2client,
            calendarId: this.calendarID,
            resource: resource
          }));

          logCreated.push(book.name);

        }

      }

      // Cleanup events that has not been touched(Illegal events or returned books)
      let allEventsID = events.items.map(item => {
        return item.id;
      });
      let removes = utils.diff(allEventsID, touchedEvents);
      for (let eventID of removes) {
        promises.push(calendar.events.deleteAsync({
          auth: this.oauth2client,
          calendarId: this.calendarID,
          eventId: eventID
        }));
      }

      if (logUpdated.length) {
        this.log(`Calendar event(s) of the following books will be updated: ${logUpdated.join(', ')}`);
      }
      if (logCreated.length) {
        this.log(`Calendar event(s) of the following books will be created: ${logCreated.join(', ')}`);
      }
      if (removes.length) {
        this.log(`The following event(s) will be removed: ${removes.join(', ')}`, 'DEBUG');
      }

      return Promise.all(promises);

    });

  }

  /**
   * Limit log message to 100 and write into database.
   *
   * @returns {Promise} - Write optration to database.
   */
  saveProfile() {
    if (this.failed) {
      this.log('Cron job failed.', 'FATAL');
    } else {
      this.log('Cron job succeed.', 'SUCCESS');
    }

    this.user.logs.sort((a, b) => {
      return b.time - a.time;
    });
    this.user.logs = this.user.logs.slice(0, MAX_LOG_RECORD);
    return this.user.save();
  }

}

/**
 * Batcher for executing mass amount of UserFunctions.
 * Methods are identical to {@link UserFunctions}, but errors will be supressed.
 *
 * @prop {UserFunctions[]} users - Users that this batcher is bounded to.
 */
class UserFunctionFactory {

  /**
   * @constructor
   */
  constructor() {
    this.users = [];
  }

{'fraction_non_alphanumeric': 0.1287686778891621, 'fraction_numerical': 0.0045016077170418, 'mean_word_length': 2.4203648596196143, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3546570', 'n_tokens_mistral': 7974, 'n_tokens_neox': 7717, 'n_words': 2329}