<commit_before>
import KeyRange from "./KeyRange"

/**
 * Converts the provided key range object into a field range map object.
 *
 * @param {IDBKeyRange} keyRange The key range object to convert.
 * @param {(string|string[])} keyPath The key path of this storage.
 * @return {Object<string, IDBKeyRange>} A field range map object.
 */
export function keyRangeToFieldRangeObject(keyRange, keyPath) {
  let fieldRangeObject = {}

  if (!(keyPath instanceof Array)) {
    setFieldValue(fieldRangeObject, keyPath, keyRange)
    return fieldRangeObject
  }

  let lowerBound = keyRange.lower
  let upperBound = keyRange.upper
  let lowerBoundOpen = keyRange.lowerOpen
  let upperBoundOpen = keyRange.upperOpen

  keyPath.forEach((fieldPath, index) => {
    let fieldLowerBound = lowerBound ? lowerBound[index] : undefined
    let fieldUpperBound = upperBound ? upperBound[index] : undefined
    let fieldRange = KeyRange.bound(
      fieldLowerBound,
      fieldUpperBound,
      lowerBoundOpen,
      upperBoundOpen
    )
    setFieldValue(fieldRangeObject, fieldPath, fieldRange)
  })

  return fieldRangeObject
}

/**
 * Normalizes the provided filter to an {@code undefined}, an
 * {@codelink IDBKeyRange} instance or a filter predicate function.
 *
 * @param {?(undefined|number|string|Date|Array|IDBKeyRange|Object<string, (number|string|Date|Array|IDBKeyRange)>|function(*, (number|string|Date|Array), (number|string|Date|Array)): boolean)}
 *        filter A filter to use, as provided by the client code.
 * @param {?(string|string[]) keyPath The key path of the storage. This
 *        parameter is used to optimize field map filters that specify a the
 *        field constraints matching the field paths - such field map filters
 *        will be optimized to an IDBKeyRange.
 * @return {(undefined|IDBKeyRange|function(*): boolean)} Normalized filter.
 */
export function normalizeFilter(filter, keyPath) {
  if (keyPath) {
    let normalizedFilter = convertFieldMapToKeyRange(filter, keyPath)
    if (normalizedFilter) {
      filter = normalizedFilter
    }
  }

  if ((filter === null) || (filter === undefined)) {
    return undefined
  }

  let isFieldMap = (filter instanceof Object) &&
      !(filter instanceof Function) &&
      !(filter instanceof IDBKeyRange) &&
      !(filter instanceof Array) &&
      !(filter instanceof Date)
  if (isFieldMap) {
    filter = compileFieldRangeFilter(filter)
  }

  if (!(filter instanceof Function) && !(filter instanceof IDBKeyRange)) {
    filter = KeyRange.only(filter)
  }

  return filter
}

/**
 * Compiles the provided map of field names to expected values or value ranges
 * to a predicate function that takes a record and returns {@code true} if the
 * record satisfies the provided field values restrictions.
 *
 * @param {Object<string, (number|string|Date|Array|IDBKeyRange)>} filter A map
 *        of field names to expected values or key ranges representing the
 *        expected values for those fields.
 * @return {function(*): boolean} The compiled filter.
 */
export function compileFieldRangeFilter(filter) {
  let fieldPaths = getFieldPaths(filter, false)

  let fieldFilters = fieldPaths.map((fieldPath) => {
    let fieldRange = getFieldValue(filter, fieldPath)
    if (!(fieldRange instanceof IDBKeyRange)) {
      fieldRange = KeyRange.only(fieldRange)
    }

    return (record) => {
      let fieldValue;
      try {
        fieldValue = getFieldValue(record, fieldPath)
      } catch (error) {
        return false // the field does not exist in the record
      }

      if (fieldRange.lower !== undefined) {
        let lowerComparison
        lowerComparison = indexedDB.cmp(fieldRange.lower, fieldValue)

        let failedTest = (lowerComparison > 0) ||
            (fieldRange.lowerOpen && (lowerComparison === 0))
        if (failedTest) {
          return false
        }
      }

      if (fieldRange.upper !== undefined) {
        let upperComparison
        upperComparison = indexedDB.cmp(fieldRange.upper, fieldValue)

        let failedTest = (upperComparison < 0) ||
            (fieldRange.upperOpen && (upperComparison === 0))
        if (failedTest) {
          return false
        }
      }

      return true
    }
  })

  return (record) => {
    if (!(record instanceof Object)) {
      return false
    }

    return fieldFilters.every(fieldFilter => fieldFilter(record))
  }
}

/**
 * Attempts to convert the provided filter to a single {@codelink IDBKeyRange}
 * instance.
 *
 * The conversion is only possible if the filter is a field map filter, field
 * paths in the object match the provided key paths exactly, and the filter
 * does not have a field set to an {@codelink IDBKeyRange} instance.
 *
 * @param {?(undefined|number|string|Date|Array|IDBKeyRange|Object<string, (number|string|Date|Array|IDBKeyRange)>|function(*, (number|string|Date|Array), (number|string|Date|Array)): boolean)}
 *        filter The filter to convert.
 * @param {(string|string[])} keyPaths The field paths representing the key in
 *        this storage.
 * @return {?IDBKeyRange} The filter represented as a single
 *         {@codelink IDBKeyRange} instance, or {@code null} if the conversion
 *         is not possible.
 */
function convertFieldMapToKeyRange(filter, keyPaths) {
  let isOtherFormOfFilter =
      !(filter instanceof Object) ||
      (filter instanceof Function) ||
      (filter instanceof Array) ||
      (filter instanceof Date) ||
      (filter instanceof IDBKeyRange)
  if (isOtherFormOfFilter) {
    return null
  }

  if (!(keyPaths instanceof Array)) {
    keyPaths = [keyPaths]
  }

  let fieldPaths = getAllFieldPaths(filter)
  if (!fieldPaths) {
    return null
  }

  let isKeyFilter =
      (fieldPaths.length === keyPaths.length) &&
      fieldPaths.every(path => keyPaths.indexOf(path) > -1)
  if (!isKeyFilter) {
    return null
  }

  if (keyPaths.length === 1) {
    return IDBKeyRange.only(getFieldValue(filter, keyPaths[0]))
  }

  return new IDBKeyRange.only(keyPaths.map((keyPath) => {
    getFieldValue(filter, keyPath)
  }))
}

/**
 * Generates an array containing all field paths in the provided object.
 *
 * The method also tests whether any of the leaf field values is an
 * {@codelink IDBKeyRange} instance, in such case the method returns
 * {@code null}.
 *
 * The method is used to determine whether the provided object can be turned
 * into and {@codelink IDBKeyRange} instance for a compound record key, and if
 * so, retrieve all field paths in the object so that its structure can be
 * validated against the field paths of the storage.
 *
 * @param {Object} object The object from which the field paths are to be
 *        extracted.
 * @return {?string[]} All field paths in the provided objects, unless the
 *         object contains a field set to an {@codelink IDBKeyRange} instance.
 */
function getFieldPaths(object, stopOnKeyRange = true) {
  let fieldPaths = []
  fieldPaths.containsKeyRange = false
  generateFieldPaths(object, [])

  return fieldPaths

  function generateFieldPaths(object, parts) {
    Object.keys(object).forEach((fieldName) => {
      let value = object[fieldName]
      if (stopOnKeyRange && (value instanceof IDBKeyRange)) {
        fieldPaths = null
        return
      }

      let isTerminalValue =
          !(value instanceof Object) ||
          (value instanceof Date) ||
          (value instanceof Array) ||
          (value instanceof IDBKeyRange)
      let fieldPath = parts.slice()
      fieldPath.push(fieldName)

      if (isTerminalValue) {
        fieldPaths.push(fieldPath.join("."))
      } else {
        generateFieldPaths(value, fieldPath)
      }
    })
  }
}

/**
 * Sets the specified field, denoted by the specified field path, on the
 * provided object to the provided value.
 *
 * If the specified field path does not exist in the object, it is
 * automatically created by inserting new empty objects.
 *
 * @param {Object} object An object on which the field should be set.
 * @param {string} fieldPath A field path on which the value should be set. A
 *        field path is a sequence of field names joined by dots ({@code "."}).
 * @param {*} value The value to set at the specified field path.
 * @throws {Error} Thrown if the field path is already present or in a conflict
 *         with another already present field path.
 */
function setFieldValue(object, fieldPath, value) {
  let parts = fieldPath.split(".")
  let done = []
  let currentObject = object

  while (parts.length) {
    let field = parts.shift()

    if (!parts.length) {
      if (currentObject.hasOwnProperty(field)) {
        throw new Error(`The ${fieldPath} field seems to be already present `)
      }
      currentObject[field] = value
      break
    }

    if (!currentObject.hasOwnProperty(field)) {
      currentObject[field] = {}
    }

    if (!(currentObject[field] instanceof Object)) {
      throw new Error(`The ${fieldPath} field is in a conflict with the ` +
          `${done.join(".")} field`)
    }

    currentObject = currentObject[field]

    done.push(field)
  }
}

/**
 * Retrieves the value from the provided object at the specified field path.
 *
 * @param {Object} object The object from which the field value is to be
 *        extracted.
 * @param {string} fieldPath The path to the field to retrieve, specified as a
 *        sequence of field names joined by dots ({@code "."}).
 * @return {*} The value of the specified field.
 * @throw {Error} Thrown if the field path does not exist in the provided
 *        object.
 */
function getFieldValue(object, fieldPath) {
  if (!fieldPath) {
    return object
  }

  let currentObject = object
  fieldPath.split(".").forEach((fieldName) => {
    if (!currentObject.hasOwnProperty(fieldName)) {
      throw new Error(`The field path ${fieldPath} does not exist in the ` +
          "provided object")
    }

    currentObject = currentObject[fieldName]
  })

  return currentObject
}
<commit_msg>fixed a typo<commit_after>
import KeyRange from "./KeyRange"

/**
 * Converts the provided key range object into a field range map object.
 *
 * @param {IDBKeyRange} keyRange The key range object to convert.
 * @param {(string|string[])} keyPath The key path of this storage.
 * @return {Object<string, IDBKeyRange>} A field range map object.
 */
export function keyRangeToFieldRangeObject(keyRange, keyPath) {
  let fieldRangeObject = {}

  if (!(keyPath instanceof Array)) {
    setFieldValue(fieldRangeObject, keyPath, keyRange)
    return fieldRangeObject
  }

  let lowerBound = keyRange.lower
  let upperBound = keyRange.upper
  let lowerBoundOpen = keyRange.lowerOpen
  let upperBoundOpen = keyRange.upperOpen

  keyPath.forEach((fieldPath, index) => {
    let fieldLowerBound = lowerBound ? lowerBound[index] : undefined
    let fieldUpperBound = upperBound ? upperBound[index] : undefined
    let fieldRange = KeyRange.bound(
      fieldLowerBound,
      fieldUpperBound,
      lowerBoundOpen,
      upperBoundOpen
    )
    setFieldValue(fieldRangeObject, fieldPath, fieldRange)
  })

  return fieldRangeObject
}

/**
 * Normalizes the provided filter to an {@code undefined}, an
 * {@codelink IDBKeyRange} instance or a filter predicate function.
 *
 * @param {?(undefined|number|string|Date|Array|IDBKeyRange|Object<string, (number|string|Date|Array|IDBKeyRange)>|function(*, (number|string|Date|Array), (number|string|Date|Array)): boolean)}
 *        filter A filter to use, as provided by the client code.
 * @param {?(string|string[]) keyPath The key path of the storage. This
 *        parameter is used to optimize field map filters that specify a the
 *        field constraints matching the field paths - such field map filters
 *        will be optimized to an IDBKeyRange.
 * @return {(undefined|IDBKeyRange|function(*): boolean)} Normalized filter.
 */
export function normalizeFilter(filter, keyPath) {
  if (keyPath) {
    let normalizedFilter = convertFieldMapToKeyRange(filter, keyPath)
    if (normalizedFilter) {
      filter = normalizedFilter
    }
  }

  if ((filter === null) || (filter === undefined)) {
    return undefined
  }

  let isFieldMap = (filter instanceof Object) &&
      !(filter instanceof Function) &&
      !(filter instanceof IDBKeyRange) &&
      !(filter instanceof Array) &&
      !(filter instanceof Date)
  if (isFieldMap) {
    filter = compileFieldRangeFilter(filter)
  }

  if (!(filter instanceof Function) && !(filter instanceof IDBKeyRange)) {
    filter = KeyRange.only(filter)
  }

  return filter
}

/**
 * Compiles the provided map of field names to expected values or value ranges
 * to a predicate function that takes a record and returns {@code true} if the
 * record satisfies the provided field values restrictions.
 *
 * @param {Object<string, (number|string|Date|Array|IDBKeyRange)>} filter A map
 *        of field names to expected values or key ranges representing the
 *        expected values for those fields.
 * @return {function(*): boolean} The compiled filter.
 */
export function compileFieldRangeFilter(filter) {
  let fieldPaths = getFieldPaths(filter, false)

  let fieldFilters = fieldPaths.map((fieldPath) => {
    let fieldRange = getFieldValue(filter, fieldPath)
    if (!(fieldRange instanceof IDBKeyRange)) {
      fieldRange = KeyRange.only(fieldRange)
    }

    return (record) => {
      let fieldValue;
      try {
        fieldValue = getFieldValue(record, fieldPath)
      } catch (error) {
        return false // the field does not exist in the record
      }

      if (fieldRange.lower !== undefined) {
        let lowerComparison
        lowerComparison = indexedDB.cmp(fieldRange.lower, fieldValue)

        let failedTest = (lowerComparison > 0) ||
            (fieldRange.lowerOpen && (lowerComparison === 0))
        if (failedTest) {
          return false
        }
      }

      if (fieldRange.upper !== undefined) {
        let upperComparison
        upperComparison = indexedDB.cmp(fieldRange.upper, fieldValue)

        let failedTest = (upperComparison < 0) ||
            (fieldRange.upperOpen && (upperComparison === 0))
        if (failedTest) {
          return false
        }
      }

      return true
    }
  })

  return (record) => {
    if (!(record instanceof Object)) {
      return false
    }

    return fieldFilters.every(fieldFilter => fieldFilter(record))
  }
}

/**
 * Attempts to convert the provided filter to a single {@codelink IDBKeyRange}
 * instance.
 *
 * The conversion is only possible if the filter is a field map filter, field
 * paths in the object match the provided key paths exactly, and the filter
 * does not have a field set to an {@codelink IDBKeyRange} instance.
 *
 * @param {?(undefined|number|string|Date|Array|IDBKeyRange|Object<string, (number|string|Date|Array|IDBKeyRange)>|function(*, (number|string|Date|Array), (number|string|Date|Array)): boolean)}
 *        filter The filter to convert.
 * @param {(string|string[])} keyPaths The field paths representing the key in
 *        this storage.
 * @return {?IDBKeyRange} The filter represented as a single
 *         {@codelink IDBKeyRange} instance, or {@code null} if the conversion
 *         is not possible.
 */
function convertFieldMapToKeyRange(filter, keyPaths) {
  let isOtherFormOfFilter =
      !(filter instanceof Object) ||
      (filter instanceof Function) ||
      (filter instanceof Array) ||
      (filter instanceof Date) ||
      (filter instanceof IDBKeyRange)
  if (isOtherFormOfFilter) {
    return null
  }

  if (!(keyPaths instanceof Array)) {
    keyPaths = [keyPaths]
  }

  let fieldPaths = getFieldPaths(filter)
  if (!fieldPaths) {
    return null
  }

  let isKeyFilter =
      (fieldPaths.length === keyPaths.length) &&
      fieldPaths.every(path => keyPaths.indexOf(path) > -1)
  if (!isKeyFilter) {
    return null
  }

  if (keyPaths.length === 1) {
    return IDBKeyRange.only(getFieldValue(filter, keyPaths[0]))
  }

  return new IDBKeyRange.only(keyPaths.map((keyPath) => {
    getFieldValue(filter, keyPath)
  }))
}

/**
 * Generates an array containing all field paths in the provided object.
 *
 * The method also tests whether any of the leaf field values is an
 * {@codelink IDBKeyRange} instance, in such case the method returns
 * {@code null}.
 *
 * The method is used to determine whether the provided object can be turned
 * into and {@codelink IDBKeyRange} instance for a compound record key, and if
 * so, retrieve all field paths in the object so that its structure can be
 * validated against the field paths of the storage.
 *
 * @param {Object} object The object from which the field paths are to be
 *        extracted.
 * @return {?string[]} All field paths in the provided objects, unless the
 *         object contains a field set to an {@codelink IDBKeyRange} instance.
 */
function getFieldPaths(object, stopOnKeyRange = true) {
  let fieldPaths = []
  fieldPaths.containsKeyRange = false
  generateFieldPaths(object, [])

  return fieldPaths

  function generateFieldPaths(object, parts) {
    Object.keys(object).some((fieldName) => {
      let value = object[fieldName]
      if (stopOnKeyRange && (value instanceof IDBKeyRange)) {
        fieldPaths = null
        return true
      }

      let isTerminalValue =
          !(value instanceof Object) ||
          (value instanceof Date) ||
          (value instanceof Array) ||
          (value instanceof IDBKeyRange)
      let fieldPath = parts.slice()
      fieldPath.push(fieldName)

      if (isTerminalValue) {
        fieldPaths.push(fieldPath.join("."))
      } else {
        generateFieldPaths(value, fieldPath)
      }
    })
  }
}

/**
 * Sets the specified field, denoted by the specified field path, on the
 * provided object to the provided value.
 *
 * If the specified field path does not exist in the object, it is
 * automatically created by inserting new empty objects.
 *
 * @param {Object} object An object on which the field should be set.
 * @param {string} fieldPath A field path on which the value should be set. A
 *        field path is a sequence of field names joined by dots ({@code "."}).
 * @param {*} value The value to set at the specified field path.
 * @throws {Error} Thrown if the field path is already present or in a conflict
 *         with another already present field path.
 */
function setFieldValue(object, fieldPath, value) {
  let parts = fieldPath.split(".")
  let done = []
  let currentObject = object

  while (parts.length) {
    let field = parts.shift()

    if (!parts.length) {
      if (currentObject.hasOwnProperty(field)) {
        throw new Error(`The ${fieldPath} field seems to be already present `)
      }
      currentObject[field] = value
      break
    }

    if (!currentObject.hasOwnProperty(field)) {
      currentObject[field] = {}
    }

    if (!(currentObject[field] instanceof Object)) {
      throw new Error(`The ${fieldPath} field is in a conflict with the ` +
          `${done.join(".")} field`)
    }

    currentObject = currentObject[field]

    done.push(field)
  }
}

/**
 * Retrieves the value from the provided object at the specified field path.
 *
 * @param {Object} object The object from which the field value is to be
 *        extracted.
 * @param {string} fieldPath The path to the field to retrieve, specified as a
 *        sequence of field names joined by dots ({@code "."}).
 * @return {*} The value of the specified field.
 * @throw {Error} Thrown if the field path does not exist in the provided
 *        object.
 */
function getFieldValue(object, fieldPath) {
  if (!fieldPath) {
    return object
  }

  let currentObject = object
  fieldPath.split(".").forEach((fieldName) => {
    if (!currentObject.hasOwnProperty(fieldName)) {
      throw new Error(`The field path ${fieldPath} does not exist in the ` +
          "provided object")
    }

    currentObject = currentObject[fieldName]
  })

  return currentObject
}
<|endoftext|>


<commit_before>import {EventEmitter} from 'events';
import WebSocket from 'ws';
import {format_livethread_permissions, handle_json_errors} from '../helpers.js';
import RedditContent from './RedditContent.js';
const api_type = 'json';

/**
* A class representing a live reddit thread
* <style> #LiveThread {display: none} </style>
* @example
*
* // Get a livethread with the given ID
* r.get_livethread('whrdxo8dg9n0')
* @desc For the most part, reddit distributes the content of live threads via websocket, rather than through the REST API.
As such, snoowrap assigns each fetched LiveThread object a `stream` property, which takes the form of an
[EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). To listen for new thread updates, simply
add listeners to that emitter.

The following events can be emitted:
- `update`: Occurs when a new update has been posted in this thread. Emits a `LiveUpdate` object containing information
about the new update.
- `activity`: Occurs periodically when the viewer count for this thread changes.
- `settings`: Occurs when the thread's settings change. Emits an object containing the new settings.
- `delete`: Occurs when an update has been deleted. Emits the ID of the deleted update.
- `strike`: Occurs when an update has been striken (marked incorrect and crossed out). Emits the ID of the striken update.
- `embeds_ready`: Occurs when embedded media is now available for a previously-posted update.
- `complete`: Occurs when this LiveThread has been marked as complete, and no more updates will be sent.

(Note: These event types are mapped directly from reddit's categorization of the updates. The descriptions above are
paraphrased from reddit's descriptions [here](https://www.reddit.com/dev/api#section_live).)

As an example, this would log all new livethread updates to the console:

```javascript
some_livethread.stream.on('update', data => {
  console.log(data.body);
});
```

* @extends RedditContent
*/
const LiveThread = class extends RedditContent {
  get _uri () {
    return `live/${this.id}/about`;
  }
  _transform_api_response (response_object) {
    const populated_stream = new EventEmitter();
    const raw_stream = new WebSocket(response_object.websocket_url);
    raw_stream.on('message', data => {
      const parsed = this._r._populate(JSON.parse(data));
      populated_stream.emit(parsed.type, parsed.payload);
    });
    return {...response_object, _websocket: raw_stream, stream: populated_stream};
  }
  /**
  * @summary Adds a new update to this thread.
  * @param {string} body The body of the new update
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').add_update('Breaking: Someone is reading the snoowrap documentation \\o/')
  */
  add_update (body) {
    return this._post({uri: `api/live/${this.id}/update`, form: {api_type, body}}).then(handle_json_errors(this));
  }
  /**
  * @summary Strikes (marks incorrect and crosses out) the given update.
  * @param {object} options
  * @param {string} options.id The ID of the update that should be striked.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').strike_update({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  strike_update ({id}) {
    return this._post({
      uri: `api/live/${this.id}/strike_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Deletes an update from this LiveThread.
  * @param {object} options
  * @param {string} options.id The ID of the LiveUpdate that should be deleted
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').delete_update({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  delete_update ({id}) {
    return this._post({
      uri: `api/live/${this.id}/delete_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Gets a list of this LiveThread's contributors
  * @returns {Promise} An Array containing RedditUsers
  * @example
  *
  * r.get_livethread('whrdxo8dg9n0').get_contributors().then(console.log)
  * // => [
  * //  RedditUser { permissions: ['edit'], name: 'not_an_aardvark', id: 't2_k83md' },
  * //  RedditUser { permissions: ['all'], id: 't2_u3l80', name: 'snoowrap_testing' }
  * // ]
  */
  get_contributors () {
    return this._get({uri: `live/${this.id}/contributors`}).then(contributors => {
      return Array.isArray(contributors[0]) ? contributors[0] : contributors;
    });
  }
  /**
  * @summary Invites a contributor to this LiveThread.
  * @param {object} options
  * @param {string} options.name The name of the user who should be invited
  * @param {Array} options.permissions The permissions that the invited user should receive. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To invite a contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').invite_contributor({name: 'actually_an_aardvark', permissions: ['update']})
  */
  invite_contributor ({name, permissions}) {
    return this._post({uri: `api/live/${this.id}/invite_contributor`, form: {
      api_type,
      name,
      permissions: format_livethread_permissions(permissions),
      type: 'liveupdate_contributor_invite'
    }}).then(handle_json_errors(this));
  }
  /**
  * @summary Revokes an invitation for the given user to become a contributor on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account whose invitation should be revoked
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').revoke_contributor_invite({name: 'actually_an_aardvark'});
  */
  revoke_contributor_invite ({name}) {
    return this._r.get_user(name).fetch().get('id').then(user_id => {
      return this._post({uri: `api/live/${this.id}/rm_contributor_invite`, form: {api_type, id: `t2_${user_id}`}});
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Accepts a pending contributor invitation on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').accept_contributor_invite()
  */
  accept_contributor_invite () {
    return this._post({uri: `api/live/${this.id}/accept_contributor_invite`, form: {api_type}}).return(this);
  }
  /**
  * @summary Abdicates contributor status on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').leave_contributor()
  */
  leave_contributor () {
    return this._post({uri: `api/live/${this.id}/leave_contributor`, form: {api_type}}).return(this);
  }
  /**
  * @summary Removes the given user from contributor status on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account who should be removed
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').remove_contributor({name: 'actually_an_aardvark'})
  */
  remove_contributor ({name}) {
    return this._r.get_user(name).fetch().get('id').then(user_id => {
      return this._post({uri: `api/live/${this.id}/rm_contributor`, form: {api_type, id: `t2_${user_id}`}});
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Sets the permissions of the given contributor.
  * @param {object} options
  * @param {string} options.name The name of the user whose permissions should be changed
  * @param {Array} options.permissions The updated permissions that the user should have. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To give the contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').set_contributor_permissions({name: 'actually_an_aardvark', permissions: ['edit']})
  */
  set_contributor_permissions ({name, permissions}) {
    return this._post({
      uri: `api/live/${this.id}/set_contributor_permissions`,
      form: {api_type, name, permissions: format_livethread_permissions(permissions), type: 'liveupdate_contributor'}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Edits the settings on this LiveThread.
  * @param {object} options
  * @param {string} options.title The title of the thread
  * @param {string} [options.description] A descriptions of the thread. 120 characters max
  * @param {string} [options.resources] Information and useful links related to the thread. 120 characters max
  * @param {boolean} options.nsfw Determines whether the thread is Not Safe For Work
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').edit_settings({title: 'My livethread', description: 'an updated description'})
  */
  edit_settings ({title, description, resources, nsfw}) {
    return this._post({
      uri: `api/live/${this.id}/edit`,
      form: {api_type, description, nsfw, resources, title}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Permanently closes this thread, preventing any more updates from being added.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').close_thread()
  */
  close_thread () {
    return this._post({uri: `api/live/${this.id}/close_thread`, form: {api_type}}).return(this);
  }
  /**
  * @summary Reports this LiveThread for breaking reddit's rules.
  * @param {object} options
  * @param {string} options.reason The reason for the report. One of `spam`, `vote-manipulation`, `personal-information`,
  `sexualizing-minors`, `site-breaking`
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').report({reason: 'Breaking a rule blah blah blah'})
  */
  report ({reason}) {
    return this._post({uri: `api/live/${this.id}/report`, form: {api_type, type: reason}}).then(handle_json_errors(this));
  }
  /**
  * @summary Gets a Listing containing past updates to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing LiveUpdates
  * @example
  *
  * r.get_livethread('whrdxo8dg9n0').get_recent_updates().then(console.log)
  * // => Listing [
  * //  LiveUpdate { ... },
  * //  LiveUpdate { ... },
  * //  ...
  * // ]
  */
  get_recent_updates (options) {
    return this._get_listing({uri: `live/${this.id}`, qs: options});
  }
  /**
  * @summary Gets a list of reddit submissions linking to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing Submissions
  * @example
  *
  * r.get_livethread('whrdxo8dg9n0').get_recent_updates().then(console.log)
  * // => Listing [
  * //  Submission { ... },
  * //  Submission { ... },
  * //  ...
  * // ]
  */
  get_discussions (options) {
    return this._get_listing({uri: `live/${this.id}/discussions`, qs: options});
  }
  /**
  * @summary Stops listening for new updates on this LiveThread.
  * @desc To avoid memory leaks that can result from open sockets, it's recommended that you call this method when you're
  finished listening for updates on this LiveThread.
  *
  * This should not be confused with {@link LiveThread#close_thread}, which marks the thread as "closed" on reddit.
  * @returns undefined
  * @example
  *
  * var my_thread = r.get_livethread('whrdxo8dg9n0');
  * my_thread.stream.on('update', content => {
  *   console.log(content);
  *   my_stream.close_thread();
  * })
  *
  */
  close_stream () {
    this._websocket.close();
  }
};

export default LiveThread;
<commit_msg>Use a getter for LiveThread#stream to avoid unneeded websocket creation<commit_after>import {EventEmitter} from 'events';
import WebSocket from 'ws';
import {format_livethread_permissions, handle_json_errors} from '../helpers.js';
import RedditContent from './RedditContent.js';
const api_type = 'json';

/**
* A class representing a live reddit thread
* <style> #LiveThread {display: none} </style>
* @example
*
* // Get a livethread with the given ID
* r.get_livethread('whrdxo8dg9n0')
* @desc For the most part, reddit distributes the content of live threads via websocket, rather than through the REST API.
As such, snoowrap assigns each fetched LiveThread object a `stream` property, which takes the form of an
[EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). To listen for new thread updates, simply
add listeners to that emitter.

The following events can be emitted:
- `update`: Occurs when a new update has been posted in this thread. Emits a `LiveUpdate` object containing information
about the new update.
- `activity`: Occurs periodically when the viewer count for this thread changes.
- `settings`: Occurs when the thread's settings change. Emits an object containing the new settings.
- `delete`: Occurs when an update has been deleted. Emits the ID of the deleted update.
- `strike`: Occurs when an update has been striken (marked incorrect and crossed out). Emits the ID of the striken update.
- `embeds_ready`: Occurs when embedded media is now available for a previously-posted update.
- `complete`: Occurs when this LiveThread has been marked as complete, and no more updates will be sent.

(Note: These event types are mapped directly from reddit's categorization of the updates. The descriptions above are
paraphrased from reddit's descriptions [here](https://www.reddit.com/dev/api#section_live).)

As an example, this would log all new livethread updates to the console:

```javascript
some_livethread.stream.on('update', data => {
  console.log(data.body);
});
```

* @extends RedditContent
*/
const LiveThread = class extends RedditContent {
  get _uri () {
    return `live/${this.id}/about`;
  }
  _transform_api_response (response_object) {
    response_object._raw_stream = null;
    response_object._populated_stream = new EventEmitter();
    Object.defineProperty(response_object, 'stream', {get: () => {
      if (!response_object._raw_stream) {
        response_object._raw_stream = new WebSocket(response_object.websocket_url);
        response_object._raw_stream.on('message', data => {
          const parsed = this._r._populate(JSON.parse(data));
          response_object._populated_stream.emit(parsed.type, parsed.payload);
        });
      }
      return response_object._populated_stream;
    }});
    return response_object;
  }
  /**
  * @summary Adds a new update to this thread.
  * @param {string} body The body of the new update
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').add_update('Breaking: Someone is reading the snoowrap documentation \\o/')
  */
  add_update (body) {
    return this._post({uri: `api/live/${this.id}/update`, form: {api_type, body}}).then(handle_json_errors(this));
  }
  /**
  * @summary Strikes (marks incorrect and crosses out) the given update.
  * @param {object} options
  * @param {string} options.id The ID of the update that should be striked.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').strike_update({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  strike_update ({id}) {
    return this._post({
      uri: `api/live/${this.id}/strike_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Deletes an update from this LiveThread.
  * @param {object} options
  * @param {string} options.id The ID of the LiveUpdate that should be deleted
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').delete_update({id: 'LiveUpdate_edc34446-faf0-11e5-a1b4-0e858bca33cd'})
  */
  delete_update ({id}) {
    return this._post({
      uri: `api/live/${this.id}/delete_update`,
      form: {api_type, id: `${id.startsWith('LiveUpdate_') ? '' : 'LiveUpdate_'}${id}`}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Gets a list of this LiveThread's contributors
  * @returns {Promise} An Array containing RedditUsers
  * @example
  *
  * r.get_livethread('whrdxo8dg9n0').get_contributors().then(console.log)
  * // => [
  * //  RedditUser { permissions: ['edit'], name: 'not_an_aardvark', id: 't2_k83md' },
  * //  RedditUser { permissions: ['all'], id: 't2_u3l80', name: 'snoowrap_testing' }
  * // ]
  */
  get_contributors () {
    return this._get({uri: `live/${this.id}/contributors`}).then(contributors => {
      return Array.isArray(contributors[0]) ? contributors[0] : contributors;
    });
  }
  /**
  * @summary Invites a contributor to this LiveThread.
  * @param {object} options
  * @param {string} options.name The name of the user who should be invited
  * @param {Array} options.permissions The permissions that the invited user should receive. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To invite a contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').invite_contributor({name: 'actually_an_aardvark', permissions: ['update']})
  */
  invite_contributor ({name, permissions}) {
    return this._post({uri: `api/live/${this.id}/invite_contributor`, form: {
      api_type,
      name,
      permissions: format_livethread_permissions(permissions),
      type: 'liveupdate_contributor_invite'
    }}).then(handle_json_errors(this));
  }
  /**
  * @summary Revokes an invitation for the given user to become a contributor on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account whose invitation should be revoked
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').revoke_contributor_invite({name: 'actually_an_aardvark'});
  */
  revoke_contributor_invite ({name}) {
    return this._r.get_user(name).fetch().get('id').then(user_id => {
      return this._post({uri: `api/live/${this.id}/rm_contributor_invite`, form: {api_type, id: `t2_${user_id}`}});
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Accepts a pending contributor invitation on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').accept_contributor_invite()
  */
  accept_contributor_invite () {
    return this._post({uri: `api/live/${this.id}/accept_contributor_invite`, form: {api_type}}).return(this);
  }
  /**
  * @summary Abdicates contributor status on this LiveThread.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').leave_contributor()
  */
  leave_contributor () {
    return this._post({uri: `api/live/${this.id}/leave_contributor`, form: {api_type}}).return(this);
  }
  /**
  * @summary Removes the given user from contributor status on this LiveThread.
  * @param {object} options
  * @param {string} options.name The username of the account who should be removed
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').remove_contributor({name: 'actually_an_aardvark'})
  */
  remove_contributor ({name}) {
    return this._r.get_user(name).fetch().get('id').then(user_id => {
      return this._post({uri: `api/live/${this.id}/rm_contributor`, form: {api_type, id: `t2_${user_id}`}});
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Sets the permissions of the given contributor.
  * @param {object} options
  * @param {string} options.name The name of the user whose permissions should be changed
  * @param {Array} options.permissions The updated permissions that the user should have. This should be an Array containing
  some combination of `'update', 'edit', 'manage'`. To give the contributor with full permissions, omit this property.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').set_contributor_permissions({name: 'actually_an_aardvark', permissions: ['edit']})
  */
  set_contributor_permissions ({name, permissions}) {
    return this._post({
      uri: `api/live/${this.id}/set_contributor_permissions`,
      form: {api_type, name, permissions: format_livethread_permissions(permissions), type: 'liveupdate_contributor'}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Edits the settings on this LiveThread.
  * @param {object} options
  * @param {string} options.title The title of the thread
  * @param {string} [options.description] A descriptions of the thread. 120 characters max
  * @param {string} [options.resources] Information and useful links related to the thread. 120 characters max
  * @param {boolean} options.nsfw Determines whether the thread is Not Safe For Work
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').edit_settings({title: 'My livethread', description: 'an updated description'})
  */
  edit_settings ({title, description, resources, nsfw}) {
    return this._post({
      uri: `api/live/${this.id}/edit`,
      form: {api_type, description, nsfw, resources, title}
    }).then(handle_json_errors(this));
  }
  /**
  * @summary Permanently closes this thread, preventing any more updates from being added.
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').close_thread()
  */
  close_thread () {
    return this._post({uri: `api/live/${this.id}/close_thread`, form: {api_type}}).return(this);
  }
  /**
  * @summary Reports this LiveThread for breaking reddit's rules.
  * @param {object} options
  * @param {string} options.reason The reason for the report. One of `spam`, `vote-manipulation`, `personal-information`,
  `sexualizing-minors`, `site-breaking`
  * @returns {Promise} A Promise that fulfills with this LiveThread when the request is complete
  * @example r.get_livethread('whrdxo8dg9n0').report({reason: 'Breaking a rule blah blah blah'})
  */
  report ({reason}) {
    return this._post({uri: `api/live/${this.id}/report`, form: {api_type, type: reason}}).then(handle_json_errors(this));
  }
  /**
  * @summary Gets a Listing containing past updates to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing LiveUpdates
  * @example
  *
  * r.get_livethread('whrdxo8dg9n0').get_recent_updates().then(console.log)
  * // => Listing [
  * //  LiveUpdate { ... },
  * //  LiveUpdate { ... },
  * //  ...
  * // ]
  */
  get_recent_updates (options) {
    return this._get_listing({uri: `live/${this.id}`, qs: options});
  }
  /**
  * @summary Gets a list of reddit submissions linking to this LiveThread.
  * @param {object} [options] Options for the resulting Listing
  * @returns {Promise} A Listing containing Submissions
  * @example
  *
  * r.get_livethread('whrdxo8dg9n0').get_recent_updates().then(console.log)
  * // => Listing [
  * //  Submission { ... },
  * //  Submission { ... },
  * //  ...
  * // ]
  */
  get_discussions (options) {
    return this._get_listing({uri: `live/${this.id}/discussions`, qs: options});
  }
  /**
  * @summary Stops listening for new updates on this LiveThread.
  * @desc To avoid memory leaks that can result from open sockets, it's recommended that you call this method when you're
  finished listening for updates on this LiveThread.
  *
  * This should not be confused with {@link LiveThread#close_thread}, which marks the thread as "closed" on reddit.
  * @returns undefined
  * @example
  *
  * var my_thread = r.get_livethread('whrdxo8dg9n0');
  * my_thread.stream.on('update', content => {
  *   console.log(content);
  *   my_stream.close_thread();
  * })
  *
  */
  close_stream () {
    this._raw_stream.close();
  }
};

export default LiveThread;
<|endoftext|>


<commit_before>/*
 * DealPort
 * Copyright (c) 2014  DealPort B.V.
 *
 * This file is part of DealPort
 *
 * DealPort is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * DealPort is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with DealPort.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, the following supplemental terms apply, based on section 7 of
 * the GNU Affero General Public License (version 3):
 * a) Preservation of all legal notices and author attributions
 */

'use strict';
var P = require('bluebird');

var Resource = require('../Resource');
var GridStore = require('mongoskin').GridStore;
var ObjectID = require('mongodb').ObjectID;
var Rearranger = require('stream-sequencer').Rearranger;

function UploadedImage(primus, model, config, user)
{
        Resource.call(this, primus, 'UploadedImage', 1);
        this.model = model;
        this.user = user;

        this.listen(['clearCompanyLogo', 'updateCompanyLogoStart', 'updateCompanyLogoEnd']);
}

require('inherits')(UploadedImage, Resource);
module.exports = UploadedImage;

UploadedImage.maxByteSize = 1024 * 1024;

function noop(){}

UploadedImage.prototype.clearCompanyLogo = function(companyId, expectedCompanyVersion)
{
        // todo
};

function resetChannelData(channelData)
{
        channelData.model = null;
        channelData.mongoClient = null;
        channelData.companyId = null;
        channelData.valid = false;
        channelData.error = null;
        channelData.expectedLastSequenceID = -1; // as specified by the client
        channelData.rearranger = null;
        channelData.fileSizeSoFar = 0;
        channelData.gridStore = null;
        channelData.onData = null;
        channelData.cleanupAfterError = noop;
        channelData.endResolve = null;
        channelData.endReject = null;
        channelData.expectedVersion = null;
}

function commitData(channelData, imageData)
{
        channelData.fileSizeSoFar += imageData.length;

        if (channelData.fileSizeSoFar > UploadedImage.maxByteSize)
        {
                channelData.valid = false;
                channelData.error = Error('The image file must be smaller than ' + UploadedImage.maxByteSize + ' bytes');
                channelData.cleanupAfterError();
                return;
        }

        channelData.gridStore.writeAsync(imageData).catch(function(err)
        {
                channelData.valid = false;
                channelData.error = err;
                channelData.cleanupAfterError();
        });
}

function tryEnd(channelData)
{
        var livedb = channelData.model.livedb;

        if (channelData.expectedLastSequenceID < 0 ||
            channelData.expectedLastSequenceID !== channelData.rearranger.lastSequenceID)
        {
                return; // not done yet
        }

        if (channelData.onData)
        {
                channelData.channelSpark.removeListener('data', channelData.onData);
        }

        P.resolve().then(function()
        {
                if (channelData.error)
                {
                        return P.reject(channelData.error);
                }
                else if (!channelData.valid)
                {
                        return P.reject(Error('Incorrect ordering of operations'));
                }

                return channelData.gridStore.closeAsync();
        })
        .then(function()
        {
                return livedb.submitAsync('company', channelData.companyId, {
                        op: [
                                {
                                        p: ['logoUploadedImage'],
                                        od: null,
                                        oi: channelData.fileId.toString()
                                }
                        ],
                        v: channelData.expectedVersion
                });
        })
        .then(function()
        {
                console.log('Completed file upload',
                        channelData.fileSizeSoFar,
                        channelData.fileId,
                        channelData.companyId
                );

                return '/company-logo/' + channelData.companyId;
        })
        .then(function(ret)
        {
                channelData.endResolve(ret);
        })
        .catch(function(err)
        {
                channelData.endReject(err);
                channelData.cleanupAfterError();
                return P.reject(err);
        })
        .finally(function()
        {
                channelData.model.release(channelData.mongoClient);
                channelData.mongoClient = null; // prevent duplicate releases
                resetChannelData(channelData);
        });
}

function sparkDataListener(channelData)
{
        return function(data)
        {
                if (!(data instanceof Uint8Array))
                {
                        return; // primus-emitter event
                }

                if (!channelData.valid)
                {
                        // todo notify the client immediately of the error
                        return;
                }

                // todo max chunk size?

                channelData.rearranger.write(new Buffer(data));
        };
}

function rearrangerDataListener(channelData)
{
        return function(data)
        {
                commitData(channelData, data);
                tryEnd(channelData);
        };
}

function cleanupAfterError(channelData, model)
{
        return function()
        {
                channelData.gridStore.close(function (err)
                {
                        if (err)
                        {
                                console.error('Error in clean up', Error().stack, ' caused by ', err, err.stack);
                        }
                });

                GridStore.unlink(channelData.mongoClient, 'uploadedImage', function (err)
                {
                        if (err)
                        {
                                console.error('Error in clean up', Error().stack, ' caused by ', err, err.stack);
                        }
                });

                model.release(channelData.mongoClient);
                channelData.mongoClient = null; // prevent duplicate releases

                channelData.cleanupAfterError = noop;
        };
}

UploadedImage.prototype.updateCompanyLogoStart = function(companyIdArg, mimeType)
{
        var channelData = this.currentChannelData;
        var user = this.user;
        var model = this.model;

        resetChannelData(channelData);

        if (!user ||
            !user._id)
        {
                return P.reject(Error('You must be logged in'));
        }

        if (mimeType !== 'image/png' &&
            mimeType !== 'image/jpeg')
        {
                return P.reject(Error('Invalid mime type'));
        }

        return model.acquireExplicit().then(function(db)
        {
                return P.method(function()
                {
                        channelData.model = model;
                        channelData.mongoClient = db;
                        channelData.companyId = companyIdArg;

                        return db.company.findOneAsync({_id: channelData.companyId})
                        .bind(this)
                        .then(function(company)
                        {
                                if (!company)
                                {
                                        throw Error('Invalid company id');
                                }

                                if (!model.company.isEditableByUser(company, user))
                                {
                                        throw Error('You are not allowed to edit this company');
                                }
                        })
                        .then(function()
                        {
                                channelData.fileId = new ObjectID();

                                var gridStore = db.gridStore(
                                        channelData.fileId,
                                        'w',
                                        {
                                                root: 'uploadedImage',
                                                'content_type': mimeType,
                                                createdByUser: user._id,
                                                uploadedForCompany: channelData.companyId
                                        }
                                );

                                channelData.cleanupAfterError = cleanupAfterError(channelData, model);

                                return gridStore.openAsync();
                        })
                        .then(function(gridStore)
                        {
                                channelData.gridStore = gridStore;
                                channelData.valid = true;
                                channelData.rearranger = new Rearranger({queueMax: 50});
                                channelData.rearranger.on('error', function(err)
                                {
                                        channelData.error = err;
                                        channelData.cleanupAfterError();
                                });
                                channelData.rearranger.on('data', rearrangerDataListener(channelData));
                                channelData.onData = sparkDataListener(channelData);
                                channelData.channelSpark.on('data', channelData.onData);
                        });
                }.bind(this))()
                .finally(function()
                {
                        if (!channelData.valid)
                        {
                                model.release(channelData.mongoClient);
                                channelData.mongoClient = null; // prevent duplicate releases
                        }
                });
        }.bind(this));
};

UploadedImage.prototype.updateCompanyLogoEnd = function(lastSequenceID, expectedCompanyVersion)
{
        var channelData = this.currentChannelData;

        channelData.expectedLastSequenceID = lastSequenceID;
        channelData.expectedVersion = expectedCompanyVersion;
        tryEnd(channelData);

        return new P(function(resolve, reject)
        {
                channelData.endResolve = resolve;
                channelData.endReject = reject;
        });
};

UploadedImage.prototype.imageAsResponse = function(id, res)
{
        var model = this.model;

        function do404()
        {
                res.statusCode = 404;
                res.setHeader('Content-Type' ,'text/plain; charset=UTF-8');
                res.end('404');
        }

        return P.using(model.acquire(), function(db)
        {
                if (!id)
                {
                        do404();
                        return null;
                }

                try
                {
                        id = new ObjectID(id);
                }
                catch(err)
                {
                        do404();
                        return null;
                }

                var gridStore = db.gridStore(
                        id,
                        'r',
                        {root: 'uploadedImage'}
                );

                return gridStore.openAsync()
                .then(function(gridStore)
                {
                        res.setHeader('Content-Length', gridStore.length);
                        if (gridStore.contentType)
                        {
                                res.setHeader('Content-Type', gridStore.contentType);
                        }

                        var stream = gridStore.stream(true); // auto close

                        return new P(function(resolve, reject)
                        {
                                res.on('finish', resolve);
                                stream.on('error', reject);
                                stream.pipe(res);
                        });
                });
        });
};
<commit_msg>Properly show a 404 when an image id does not exist<commit_after>/*
 * DealPort
 * Copyright (c) 2014  DealPort B.V.
 *
 * This file is part of DealPort
 *
 * DealPort is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * DealPort is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with DealPort.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, the following supplemental terms apply, based on section 7 of
 * the GNU Affero General Public License (version 3):
 * a) Preservation of all legal notices and author attributions
 */

'use strict';
var P = require('bluebird');

var Resource = require('../Resource');
var GridStore = require('mongoskin').GridStore;
var ObjectID = require('mongodb').ObjectID;
var Rearranger = require('stream-sequencer').Rearranger;

function UploadedImage(primus, model, config, user)
{
        Resource.call(this, primus, 'UploadedImage', 1);
        this.model = model;
        this.user = user;

        this.listen(['clearCompanyLogo', 'updateCompanyLogoStart', 'updateCompanyLogoEnd']);
}

require('inherits')(UploadedImage, Resource);
module.exports = UploadedImage;

UploadedImage.maxByteSize = 1024 * 1024;

function noop(){}

UploadedImage.prototype.clearCompanyLogo = function(companyId, expectedCompanyVersion)
{
        // todo
};

function resetChannelData(channelData)
{
        channelData.model = null;
        channelData.mongoClient = null;
        channelData.companyId = null;
        channelData.valid = false;
        channelData.error = null;
        channelData.expectedLastSequenceID = -1; // as specified by the client
        channelData.rearranger = null;
        channelData.fileSizeSoFar = 0;
        channelData.gridStore = null;
        channelData.onData = null;
        channelData.cleanupAfterError = noop;
        channelData.endResolve = null;
        channelData.endReject = null;
        channelData.expectedVersion = null;
}

function commitData(channelData, imageData)
{
        channelData.fileSizeSoFar += imageData.length;

        if (channelData.fileSizeSoFar > UploadedImage.maxByteSize)
        {
                channelData.valid = false;
                channelData.error = Error('The image file must be smaller than ' + UploadedImage.maxByteSize + ' bytes');
                channelData.cleanupAfterError();
                return;
        }

        channelData.gridStore.writeAsync(imageData).catch(function(err)
        {
                channelData.valid = false;
                channelData.error = err;
                channelData.cleanupAfterError();
        });
}

function tryEnd(channelData)
{
        var livedb = channelData.model.livedb;

        if (channelData.expectedLastSequenceID < 0 ||
            channelData.expectedLastSequenceID !== channelData.rearranger.lastSequenceID)
        {
                return; // not done yet
        }

        if (channelData.onData)
        {
                channelData.channelSpark.removeListener('data', channelData.onData);
        }

        P.resolve().then(function()
        {
                if (channelData.error)
                {
                        return P.reject(channelData.error);
                }
                else if (!channelData.valid)
                {
                        return P.reject(Error('Incorrect ordering of operations'));
                }

                return channelData.gridStore.closeAsync();
        })
        .then(function()
        {
                return livedb.submitAsync('company', channelData.companyId, {
                        op: [
                                {
                                        p: ['logoUploadedImage'],
                                        od: null,
                                        oi: channelData.fileId.toString()
                                }
                        ],
                        v: channelData.expectedVersion
                });
        })
        .then(function()
        {
                console.log('Completed file upload',
                        channelData.fileSizeSoFar,
                        channelData.fileId,
                        channelData.companyId
                );

                return '/company-logo/' + channelData.companyId;
        })
        .then(function(ret)
        {
                channelData.endResolve(ret);
        })
        .catch(function(err)
        {
                channelData.endReject(err);
                channelData.cleanupAfterError();
                return P.reject(err);
        })
        .finally(function()
        {
                channelData.model.release(channelData.mongoClient);
                channelData.mongoClient = null; // prevent duplicate releases
                resetChannelData(channelData);
        });
}

function sparkDataListener(channelData)
{
        return function(data)
        {
                if (!(data instanceof Uint8Array))
                {
                        return; // primus-emitter event
                }

                if (!channelData.valid)
                {
                        // todo notify the client immediately of the error
                        return;
                }

                // todo max chunk size?

                channelData.rearranger.write(new Buffer(data));
        };
}

function rearrangerDataListener(channelData)
{
        return function(data)
        {
                commitData(channelData, data);
                tryEnd(channelData);
        };
}

function cleanupAfterError(channelData, model)
{
        return function()
        {
                channelData.gridStore.close(function (err)
                {
                        if (err)
                        {
                                console.error('Error in clean up', Error().stack, ' caused by ', err, err.stack);
                        }
                });

                GridStore.unlink(channelData.mongoClient, 'uploadedImage', function (err)
                {
                        if (err)
                        {
                                console.error('Error in clean up', Error().stack, ' caused by ', err, err.stack);
                        }
                });

                model.release(channelData.mongoClient);
                channelData.mongoClient = null; // prevent duplicate releases

                channelData.cleanupAfterError = noop;
        };
}

UploadedImage.prototype.updateCompanyLogoStart = function(companyIdArg, mimeType)
{
        var channelData = this.currentChannelData;
        var user = this.user;
        var model = this.model;

        resetChannelData(channelData);

        if (!user ||
            !user._id)
        {
                return P.reject(Error('You must be logged in'));
        }

        if (mimeType !== 'image/png' &&
            mimeType !== 'image/jpeg')
        {
                return P.reject(Error('Invalid mime type'));
        }

        return model.acquireExplicit().then(function(db)
        {
                return P.method(function()
                {
                        channelData.model = model;
                        channelData.mongoClient = db;
                        channelData.companyId = companyIdArg;

                        return db.company.findOneAsync({_id: channelData.companyId})
                        .bind(this)
                        .then(function(company)
                        {
                                if (!company)
                                {
                                        throw Error('Invalid company id');
                                }

                                if (!model.company.isEditableByUser(company, user))
                                {
                                        throw Error('You are not allowed to edit this company');
                                }
                        })
                        .then(function()
                        {
                                channelData.fileId = new ObjectID();

                                var gridStore = db.gridStore(
                                        channelData.fileId,
                                        'w',
                                        {
                                                root: 'uploadedImage',
                                                'content_type': mimeType,
                                                createdByUser: user._id,
                                                uploadedForCompany: channelData.companyId
                                        }
                                );

                                channelData.cleanupAfterError = cleanupAfterError(channelData, model);

                                return gridStore.openAsync();
                        })
                        .then(function(gridStore)
                        {
                                channelData.gridStore = gridStore;
                                channelData.valid = true;
                                channelData.rearranger = new Rearranger({queueMax: 50});
                                channelData.rearranger.on('error', function(err)
                                {
                                        channelData.error = err;
                                        channelData.cleanupAfterError();
                                });
                                channelData.rearranger.on('data', rearrangerDataListener(channelData));
                                channelData.onData = sparkDataListener(channelData);
                                channelData.channelSpark.on('data', channelData.onData);
                        });
                }.bind(this))()
                .finally(function()
                {
                        if (!channelData.valid)
                        {
                                model.release(channelData.mongoClient);
                                channelData.mongoClient = null; // prevent duplicate releases
                        }
                });
        }.bind(this));
};

UploadedImage.prototype.updateCompanyLogoEnd = function(lastSequenceID, expectedCompanyVersion)
{
        var channelData = this.currentChannelData;

        channelData.expectedLastSequenceID = lastSequenceID;
        channelData.expectedVersion = expectedCompanyVersion;
        tryEnd(channelData);

        return new P(function(resolve, reject)
        {
                channelData.endResolve = resolve;
                channelData.endReject = reject;
        });
};

UploadedImage.prototype.imageAsResponse = function(id, res)
{
        var model = this.model;

        function do404()
        {
                res.statusCode = 404;
                res.setHeader('Content-Type' ,'text/plain; charset=UTF-8');
                res.end('404');
        }

        return P.using(model.acquire(), function(db)
        {
                if (!id)
                {
                        do404();
                        return null;
                }

                try
                {
                        id = new ObjectID(id);
                }
                catch(err)
                {
                        do404();
                        return null;
                }

                var gridStore = db.gridStore(
                        id,
                        'r',
                        {root: 'uploadedImage'}
                );

                return gridStore.openAsync()
                .then(function(gridStore)
                {
                        res.setHeader('Content-Length', gridStore.length);
                        if (gridStore.contentType)
                        {
                                res.setHeader('Content-Type', gridStore.contentType);
                        }

                        var stream = gridStore.stream(true); // auto close

                        return new P(function(resolve, reject)
                        {
                                res.on('finish', resolve);
                                stream.on('error', reject);
                                stream.pipe(res);
                        });
                })
                .catch(function(err)
                {
                        do404();
                        return null;
                });
        });
};
<|endoftext|>


<commit_before>/**
 * Upwork auth library for using with public API by OAuth
 *
 * @package     UpworkAPI
 * @since       09/23/2014
 * @copyright   Copyright 2014(c) Upwork.com
 * @author      Maksym Novozhylov <mnovozhilov@upwork.com>
 * @license     Upwork's API Terms of Use {@link https://developers.upwork.com/api-tos.html}
 */

/**
 * @module routes.activities.engagement
 */

/**
 * @property entryPoint
 * @type String
 * @default api
 * @static
 * @final
 */
var entryPoint = 'api';

/**
 * @class Engagement
 * @constructor
 */
exports.Engagement = function(api) {
  this.api = api;
  this.api.epoint = entryPoint;
}

/**
 * List activities for specific engagement
 *
 * @method getSpecific
 * @param engagementRef {Integer} Engagement reference
 * @param callback {String} Callback function
 * @async
 */
exports.Engagement.prototype.getSpecific = function(engagementRef, callback) {
  debug('running request');
  this.api.client.get('tasks/v2/tasks/contracts/' + engagementRef, {}, callback);
}

/**
 * Assign engagements to the list of activities
 *
 * @method assign
 * @param company {String} Company ID
 * @param team {String} Team ID
 * @param engagementRef {Integer} Engagement reference
 * @param params {Hash} Parameters
 * @param callback {String} Callback function
 * @async
 */
exports.Engagement.prototype.assign = function(company, team, engagementRef, params, callback) {
  debug('running request');
  this.api.client.put('otask/v1/tasks/companies/' + company + '/teams/' + team + '/engagements/' + engagementRef + '/tasks', params, callback);
}
<commit_msg>Assign to specific engagement the list of activities API<commit_after>/**
 * Upwork auth library for using with public API by OAuth
 *
 * @package     UpworkAPI
 * @since       09/23/2014
 * @copyright   Copyright 2014(c) Upwork.com
 * @author      Maksym Novozhylov <mnovozhilov@upwork.com>
 * @license     Upwork's API Terms of Use {@link https://developers.upwork.com/api-tos.html}
 */

/**
 * @module routes.activities.engagement
 */

/**
 * @property entryPoint
 * @type String
 * @default api
 * @static
 * @final
 */
var entryPoint = 'api';

/**
 * @class Engagement
 * @constructor
 */
exports.Engagement = function(api) {
  this.api = api;
  this.api.epoint = entryPoint;
}

/**
 * List activities for specific engagement
 *
 * @method getSpecific
 * @param engagementRef {Integer} Engagement reference
 * @param callback {String} Callback function
 * @async
 */
exports.Engagement.prototype.getSpecific = function(engagementRef, callback) {
  debug('running request');
  this.api.client.get('tasks/v2/tasks/contracts/' + engagementRef, {}, callback);
}

/**
 * Assign engagements to the list of activities
 *
 * @method assign
 * @param company {String} Company ID
 * @param team {String} Team ID
 * @param engagementRef {Integer} Engagement reference
 * @param params {Hash} Parameters
 * @param callback {String} Callback function
 * @async
 */
exports.Engagement.prototype.assign = function(company, team, engagementRef, params, callback) {
  debug('running request');
  this.api.client.put('otask/v1/tasks/companies/' + company + '/teams/' + team + '/engagements/' + engagementRef + '/tasks', params, callback);
}

/**
 * Assign to specific engagement the list of activities
 *
 * @method assign
 * @param engagementRef {Integer} Engagement reference
 * @param params {Hash} Parameters
 * @param callback {String} Callback function
 * @async
 */
exports.Engagement.prototype.assignToEngagement = function(engagementRef, params, callback) {
  debug('running request');
  this.api.client.put('tasks/v2/tasks/contracts/' + engagementRef, params, callback);
}
<|endoftext|>


<commit_before>var express = require('express'),
    bodyParser = require('body-parser'),
    fs = require('fs'),
    markdown = require('markdown').markdown,
    async = require('async');
    app = express();

var gcloud = require('gcloud'),
    datastore = gcloud.datastore;

var ds = new datastore.Dataset({
  projectId: process.env.GAE_LONG_APP_ID || process.env.DATASET_ID,
  keyFilename: 'key.json'
});

app.use(bodyParser.json());

var todoListName = 'default-list';

app.get('/todos', function(req, res) {
  var q = ds.createQuery('Todo')
    .hasAncestor(ds.key('TodoList', todoListName));
  ds.runQuery(q, function(err, items) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    res.json(items.map(function(obj, i) {
      obj.data.id = obj.key.path.pop();
      return obj.data;
    }));
  });
});

app.get('/todos/:id', function(req, res) {
  var id = req.param('id');
  ds.get(ds.key('TodoList', todoListName, 'Todo', id), function(err, obj) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    if (!obj) {
      return res.status(404).send();
    }
    obj.data.id = obj.key.path.pop();
    res.json(obj.data);
  });
});

app.post('/todos', function(req, res) {
  var todo = req.body;
  todo.done = false;
  ds.save({
    key: ds.key('TodoList', todoListName, 'Todo'),
    data: todo
  }, function(err, key) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    todo.id = key.path.pop();
    res.status(201).json(todo);
  });
});

app.put('/todos/:id', function(req, res) {
  var id = req.param('id');
  var todo = req.body;
  ds.save({
    key: ds.key('TodoList', todoListName, 'Todo', id),
    data: todo
  }, function(err, key) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    todo.id = id;
    res.json(todo);
  });
});

app.delete('/todos/:id', function(req, res) {
  var id = req.param('id');
  ds.delete(ds.key('TodoList', todoListName, 'Todo', id), function(err) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    res.status(204).send();
  });
});

app.delete('/todos', function(req, res) {
  ds.runInTransaction(function(t, done) {
    var q = ds.createQuery('Todo')
      .hasAncestor(ds.key('TodoList', todoListName))
      .filter('done =', true);
    t.runQuery(q, function(err, items) {
      if (err) {
        t.rollback(done);
        console.error(err);
        res.status(500).send(err.message);
        return;
      }
      var keys = items.map(function(obj) {
        return obj.key;
      });
      t.delete(keys, function(err) {
        if (err) {
          t.rollback(done);
          console.error(err);
          res.status(500).send(err.message);
          return;
        }
        done();
        res.status(204).send();
      });
    });
  });
});

app.get('/_ah/health', function(req, res) {
  res.status(200)
     .set('Content-Type', 'text/plain');
     .send(200, 'ok');
});

var githubMarkdownCSS = 'node_modules/github-markdown-css/github-markdown.css';
var todosAPIBlueprint = 'todos.apib';
app.get('/', function(req, res) {
  async.parallel([
    function(callback) { fs.readFile(githubMarkdownCSS, callback); },
    function(callback) { fs.readFile(todosAPIBlueprint, callback); },
  ], function(err, results) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    res.status(200)
       .set('Content-Type', 'text/html')
       .send(''+
             markdown.toHTML(results[1].toString())+
             '');
  });
});

module.exports = app;
<commit_msg>todos: remove duplicate status on _ah/health<commit_after>var express = require('express'),
    bodyParser = require('body-parser'),
    fs = require('fs'),
    markdown = require('markdown').markdown,
    async = require('async');
    app = express();

var gcloud = require('gcloud'),
    datastore = gcloud.datastore;

var ds = new datastore.Dataset({
  projectId: process.env.GAE_LONG_APP_ID || process.env.DATASET_ID,
  keyFilename: 'key.json'
});

app.use(bodyParser.json());

var todoListName = 'default-list';

app.get('/todos', function(req, res) {
  var q = ds.createQuery('Todo')
    .hasAncestor(ds.key('TodoList', todoListName));
  ds.runQuery(q, function(err, items) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    res.json(items.map(function(obj, i) {
      obj.data.id = obj.key.path.pop();
      return obj.data;
    }));
  });
});

app.get('/todos/:id', function(req, res) {
  var id = req.param('id');
  ds.get(ds.key('TodoList', todoListName, 'Todo', id), function(err, obj) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    if (!obj) {
      return res.status(404).send();
    }
    obj.data.id = obj.key.path.pop();
    res.json(obj.data);
  });
});

app.post('/todos', function(req, res) {
  var todo = req.body;
  todo.done = false;
  ds.save({
    key: ds.key('TodoList', todoListName, 'Todo'),
    data: todo
  }, function(err, key) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    todo.id = key.path.pop();
    res.status(201).json(todo);
  });
});

app.put('/todos/:id', function(req, res) {
  var id = req.param('id');
  var todo = req.body;
  ds.save({
    key: ds.key('TodoList', todoListName, 'Todo', id),
    data: todo
  }, function(err, key) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    todo.id = id;
    res.json(todo);
  });
});

app.delete('/todos/:id', function(req, res) {
  var id = req.param('id');
  ds.delete(ds.key('TodoList', todoListName, 'Todo', id), function(err) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    res.status(204).send();
  });
});

app.delete('/todos', function(req, res) {
  ds.runInTransaction(function(t, done) {
    var q = ds.createQuery('Todo')
      .hasAncestor(ds.key('TodoList', todoListName))
      .filter('done =', true);
    t.runQuery(q, function(err, items) {
      if (err) {
        t.rollback(done);
        console.error(err);
        res.status(500).send(err.message);
        return;
      }
      var keys = items.map(function(obj) {
        return obj.key;
      });
      t.delete(keys, function(err) {
        if (err) {
          t.rollback(done);
          console.error(err);
          res.status(500).send(err.message);
          return;
        }
        done();
        res.status(204).send();
      });
    });
  });
});

app.get('/_ah/health', function(req, res) {
  res.status(200)
     .set('Content-Type', 'text/plain')
     .send('ok');
});

var githubMarkdownCSS = 'node_modules/github-markdown-css/github-markdown.css';
var todosAPIBlueprint = 'todos.apib';
app.get('/', function(req, res) {
  async.parallel([
    function(callback) { fs.readFile(githubMarkdownCSS, callback); },
    function(callback) { fs.readFile(todosAPIBlueprint, callback); },
  ], function(err, results) {
    if (err) {
      console.error(err);
      res.status(500).send(err.message);
      return;
    }
    res.status(200)
       .set('Content-Type', 'text/html')
       .send(''+
             markdown.toHTML(results[1].toString())+
             '');
  });
});

module.exports = app;
<|endoftext|>


<commit_before>'use strict';
var lodash = require('lodash');
var uglifyJs = require('uglify-js');

/**
 * @param {string} code
 * @returns {Array}
 */
function getComments(code) {
    var ast;
    var comments = [];
    var endpos = 0;

    function iterate(comment) {
        comments.push(JSON.stringify(comment));
    }

    function processNode(node) {
        endpos = Math.max(node.end.endpos, endpos);

        if (node.end.comments_before.length) {
            node.end.comments_before.forEach(iterate);
        }
    }

    function toObject(comment) {
        return JSON.parse(comment);
    }

    try {
        ast = uglifyJs.parse(code)
    } catch (error) {
        return comments;
    }

    ast.walk(new uglifyJs.TreeWalker(processNode));
    comments = lodash.uniq(comments).map(toObject);
    comments = lodash.sortBy(comments, 'pos');
    comments.endpos = endpos;
    return comments;
}

module.exports = getComments;
<commit_msg>Fix UglifyJS comment getter<commit_after>'use strict';
var lodash = require('lodash');
var uglifyJs = require('uglify-js');

/**
 * @param {string} code
 * @returns {Array}
 */
function getComments(code) {
    var ast;
    var comments = [];
    var endpos = 0;

    function iterate(comment) {
        comments.push(JSON.stringify(comment));
    }

    function processNode(node) {
        endpos = Math.max(node.end.endpos, endpos);

        if (node.start.comments_before.length) {
            node.start.comments_before.forEach(iterate);
        }

        if (node.end.comments_before.length) {
            node.end.comments_before.forEach(iterate);
        }
    }

    function toObject(comment) {
        return JSON.parse(comment);
    }

    try {
        ast = uglifyJs.parse(code)
    } catch (error) {
        return comments;
    }

    ast.walk(new uglifyJs.TreeWalker(processNode));
    comments = lodash.uniq(comments).map(toObject);
    comments = lodash.sortBy(comments, 'pos');
    comments.endpos = endpos;
    return comments;
}

module.exports = getComments;
<|endoftext|>