<commit_before>import qs from 'qs'
import S3 from 'aws-sdk/clients/s3'
import {createAction} from 'redux-actions'

import {fetchGraphQL, secureFetch} from '../../common/actions'
import {SECURE_API_PREFIX} from '../../common/constants'
import {getConfigProperty} from '../../common/util/config'
import {uploadFile} from '../../common/util/upload-file'
import fileDownload from '../../common/util/file-download'
import {getEntityGraphQLRoot, getEntityIdField, getGraphQLFieldsForEntity} from '../../gtfs/util'
import {getControlPoints} from '../../editor/selectors'
import {setErrorMessage, startJobMonitor} from './status'
import {fetchFeedSource} from './feeds'

export const requestingFeedVersions = createAction('REQUESTING_FEEDVERSIONS')

export function receiveFeedVersions (feedSource, feedVersions) {
  return {
    type: 'RECEIVE_FEEDVERSIONS',
    feedSource,
    feedVersions
  }
}

export function setActiveVersion (feedVersion) {
  return {
    type: 'SET_ACTIVE_FEEDVERSION',
    feedVersion
  }
}

export function fetchFeedVersions (feedSource, unsecured) {
  return function (dispatch, getState) {
    dispatch(requestingFeedVersions())
    const apiRoot = unsecured ? 'public' : 'secure'
    const url = `/api/manager/${apiRoot}/feedversion?feedSourceId=${feedSource.id}`
    return dispatch(secureFetch(url))
      .then(response => response.json())
      .then(versions => {
        dispatch(receiveFeedVersions(feedSource, versions))
        return versions
      })
  }
}

export function requestingFeedVersion () {
  return {
    type: 'REQUESTING_FEEDVERSION'
  }
}

export function receiveFeedVersion (feedVersion) {
  return {
    type: 'RECEIVE_FEEDVERSION',
    feedVersion
  }
}

export function fetchFeedVersion (feedVersionId) {
  return function (dispatch, getState) {
    dispatch(requestingFeedVersion())
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersionId}`
    return dispatch(secureFetch(url))
      .then(response => response.json())
      .then(version => {
        return dispatch(receiveFeedVersion(version))
      })
  }
}

export function publishingFeedVersion (feedVersion) {
  return {
    type: 'PUBLISHING_FEEDVERSION',
    feedVersion
  }
}

export function publishedFeedVersion (feedVersion) {
  return {
    type: 'PUBLISHED_FEEDVERSION',
    feedVersion
  }
}

export function publishFeedVersion (feedVersion) {
  return function (dispatch, getState) {
    dispatch(publishingFeedVersion(feedVersion))
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}/publish`
    return dispatch(secureFetch(url, 'post'))
      .then(response => response.json())
      .then(version => {
        return dispatch(publishedFeedVersion(version))
      })
  }
}

export function fetchPublicFeedVersions (feedSource) {
  return function (dispatch, getState) {
    dispatch(requestingFeedVersions())
    const url = `/api/manager/public/feedversion?feedSourceId=${feedSource.id}&public=true`
    return dispatch(secureFetch(url))
      .then(response => response.json())
      .then(versions => {
        dispatch(receiveFeedVersions(feedSource, versions))
      })
  }
}

// Upload a GTFS File as a new FeedVersion

export function uploadingFeed (feedSource, file) {
  return {
    type: 'UPLOADING_FEED',
    feedSource,
    file
  }
}

export function uploadedFeed (feedSource) {
  return {
    type: 'UPLOADED_FEED',
    feedSource
  }
}

export function feedNotModified (feedSource, message) {
  return {
    type: 'FEED_NOT_MODIFIED',
    feedSource,
    message
  }
}

export function uploadFeed (feedSource, file) {
  return function (dispatch, getState) {
    dispatch(uploadingFeed(feedSource, file))
    const url = `${SECURE_API_PREFIX}feedversion?feedSourceId=${feedSource.id}&lastModified=${file.lastModified}`

    return uploadFile({file, url, token: getState().user.token})
      .then(res => {
        // 304 halt thrown by server if uploaded feed matches the hash of the
        // latest version
        if (res.status === 304) {
          // Do not start job monitor if feed matches latest version. Display the
          // status modal with a message about the cancelled upload.
          dispatch(feedNotModified(feedSource, 'Feed upload cancelled because it matches latest feed version.'))
        } else if (res.status >= 400) {
          dispatch(setErrorMessage({message: 'Error uploading feed source'}))
        } else {
          dispatch(uploadedFeed(feedSource))
          dispatch(startJobMonitor())
        }
        console.log('uploadFeed result', res)

        // fetch feed source with versions
        return dispatch(fetchFeedSource(feedSource.id, true))
      })
  }
}

const deletingFeedVersion = createAction('DELETING_FEEDVERSION')

export function deleteFeedVersion (feedVersion, changes) {
  return function (dispatch, getState) {
    dispatch(deletingFeedVersion(feedVersion))
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}`
    return dispatch(secureFetch(url, 'delete'))
      .then((res) => {
        // fetch feed source with versions + snapshots
        return dispatch(fetchFeedSource(feedVersion.feedSource.id, true, true))
      })
  }
}
export function requestingValidationResult (feedVersion) {
  return {
    type: 'REQUESTING_VALIDATION_RESULT',
    feedVersion
  }
}
export function receiveValidationResult (feedVersion, validationResult) {
  return {
    type: 'RECEIVE_VALIDATION_RESULT',
    feedVersion,
    validationResult
  }
}

const fetchingValidationErrors = createAction('FETCHING_VALIDATION_ERRORS')

const receiveValidationErrors = createAction('RECEIVE_VALIDATION_ERRORS')
const fetchingGTFSEntities = createAction('FETCHING_GTFS_ENTITIES')
const receiveGTFSEntities = createAction('RECEIVE_GTFS_ENTITIES')

export function fetchGTFSEntities ({feedVersion, id, type}) {
  return function (dispatch, getState) {
    const {namespace} = feedVersion
    const entityIdField = getEntityIdField(type)
    const entityTableString = getEntityTableString(type)
    const graphQLRoot = getEntityGraphQLRoot(type)
    const gtfsSpec = getConfigProperty('modules.editor.spec')
    const table = gtfsSpec.find(table => table.id === entityTableString)
    let fields = ''
    if (table) {
      fields = table.fields
        .filter(field => field.required && !field.datatools)
        .map(field => field.name)
        .join('\n')
      // stop_times are a special case because they must be requested as a
      // nested list underneath a trip
      if (type === 'StopTime') {
        fields = `
          trip_id
          stop_times {
            ${fields}
          }
        `
      }
    }
    const query = `
      query entityQuery($namespace: String, $${entityIdField}: [String]) {
        feed(namespace: $namespace) {
          feed_id
          feed_version
          filename
          ${graphQLRoot} (${entityIdField}: $${entityIdField}) {
            ${fields}
          }
        }
      }
    `
    dispatch(fetchingGTFSEntities({feedVersion, id, type}))
    return fetchGraphQL({query, variables: {namespace, [entityIdField]: id}})
      .then(response => response.json())
      .then(data => dispatch(receiveGTFSEntities({feedVersion, id, type, data})))
      .catch(err => console.log(err))
  }
}

export function fetchValidationErrors ({feedVersion, errorType, limit, offset}) {
  return function (dispatch, getState) {
    dispatch(fetchingValidationErrors({feedVersion, errorType, limit, offset}))
    // FIXME: why does namespace need to appear twice?
    const query = `
      query errorsQuery($namespace: String, $errorType: [String], $limit: Int, $offset: Int) {
        feed(namespace: $namespace) {
          feed_id
          feed_version
          filename
          errors (error_type: $errorType, limit: $limit, offset: $offset) {
            error_type
            entity_type
            entity_id
            line_number
            bad_value
            entity_sequence
          }
        }
      }
    `
    const {namespace} = feedVersion
    return fetchGraphQL({query, variables: {namespace, errorType: [errorType], limit, offset}})
    .then(response => response.json())
    .then(result => {
      if (result.feed) {
        const {errors} = result.feed
        dispatch(receiveValidationErrors({feedVersion, errorType, limit, offset, errors}))
      }
    })
    .catch(err => console.log(err))
  }
}

function fetchGraphQL ({query, variables, ...requestParams}) {
  const body = JSON.stringify({
    query,
    variables: JSON.stringify(variables)
  })
  return fetch(GTFS_GRAPHQL_PREFIX, {
    method: 'post',
    body,
    ...requestParams,
    headers: {'Content-Type': 'application/json'}
  })
}

export function fetchValidationResult (feedVersion, isPublic) {
  return function (dispatch, getState) {
    dispatch(requestingValidationResult(feedVersion))
    const {namespace} = feedVersion
    const query = `
      query countsQuery($namespace: String) {
        feed(namespace: $namespace) {
          feed_id
          feed_version
          filename
          row_counts {
            stops
            trips
            calendar_dates
            errors
          }
          error_counts {
            type count message
          }
        }
      }
    `
    return fetchGraphQL({query, variables: {namespace}})
    .then(response => response.json())
    .then(result => {
      if (result.feed) {
        dispatch(receiveValidationResult(feedVersion, result.feed))
      }
    })
    .catch(err => console.log(err))
  }
}

export function requestingFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) {
  return {
    type: 'REQUESTING_FEEDVERSION_ISOCHRONES',
    feedVersion,
    fromLat,
    fromLon,
    toLat,
    toLon,
    date,
    fromTime,
    toTime
  }
}

export function receiveFeedVersionIsochrones (feedSource, feedVersion, isochrones, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) {
  return {
    type: 'RECEIVE_FEEDVERSION_ISOCHRONES',
    feedSource,
    feedVersion,
    isochrones,
    fromLat,
    fromLon,
    toLat,
    toLon,
    date,
    fromTime,
    toTime
  }
}

export function fetchFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) {
  return function (dispatch, getState) {
    if (typeof date === 'undefined' || typeof fromTime === 'undefined' || typeof toTime === 'undefined') {
      date = getState().gtfs.filter.dateTimeFilter.date
      fromTime = getState().gtfs.filter.dateTimeFilter.from
      toTime = getState().gtfs.filter.dateTimeFilter.to
    }
    dispatch(requestingFeedVersionIsochrones(feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime))
    const params = {fromLat, fromLon, toLat, toLon, date, fromTime, toTime}
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}/isochrones?${qs.stringify(params)}`
    return dispatch(secureFetch(url))
      .then(res => {
        console.log(res.status)
        if (res.status === 202) {
          // dispatch(setStatus)
          console.log('building network')
          return []
        }
        return res.json()
      })
      .then(isochrones => {
        console.log('received isochrones ', isochrones)
        dispatch(receiveFeedVersionIsochrones(feedVersion.feedSource, feedVersion, isochrones, fromLat, fromLon, toLat, toLon, date, fromTime, toTime))
        return isochrones
      })
  }
}

// Download a GTFS file for a FeedVersion
export function downloadFeedViaToken (feedVersion, isPublic, prefix = 'gtfs') {
  return function (dispatch, getState) {
    const route = isPublic ? 'public' : 'secure'
    const url = `/api/manager/${route}/feedversion/${feedVersion.id}/downloadtoken`
    dispatch(secureFetch(url))
    .then(response => response.json())
    .then(credentials => {
      if (getConfigProperty('application.data.use_s3_storage')) {
        dispatch(downloadS3Key(credentials, `${feedVersion.id}`, prefix))
      } else {
        // use token to download feed
        window.location.assign(`/api/manager/downloadfeed/${credentials.id}`)
      }
    })
  }
}

export function downloadS3Key (credentials, filename, folder = null) {
  return function (dispatch, getState) {
    // Set credentials and region
    const s3 = new S3({
      apiVersion: '2006-03-01',
      credentials
    })
    const Bucket = getConfigProperty('application.data.gtfs_s3_bucket')
    const Key = folder ? `${folder}/${filename}` : filename
    console.log(`downloading ${Key} from ${Bucket}`)
    s3.getObject({Bucket, Key}, (err, data) => {
      if (err) {
        console.log(err)
        dispatch(setErrorMessage({message: `Error downloading feed:\n${err}`}))
      }
      if (data) {
        fileDownload(data.Body, filename, data.contentType)
      }
    })
  }
}

export function creatingFeedVersionFromSnapshot () {
  return {
    type: 'CREATING_FEEDVERSION_FROM_SNAPSHOT'
  }
}
export function createFeedVersionFromSnapshot (feedSource, snapshotId) {
  return function (dispatch, getState) {
    dispatch(creatingFeedVersionFromSnapshot())
    const url = `${SECURE_API_PREFIX}feedversion/fromsnapshot?feedSourceId=${feedSource.id}&snapshotId=${snapshotId}`
    return dispatch(secureFetch(url, 'post'))
      .then((res) => {
        if (res) dispatch(startJobMonitor())
      })
  }
}

export function renamingFeedVersion () {
  return {
    type: 'RENAMING_FEEDVERSION'
  }
}

export function renameFeedVersion (feedVersion, name) {
  return function (dispatch, getState) {
    dispatch(renamingFeedVersion())
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}/rename?name=${name}`
    return dispatch(secureFetch(url, 'put'))
      .then((res) => {
        dispatch(fetchFeedVersion(feedVersion.id))
      })
  }
}
<commit_msg>refactor validation error fetch<commit_after>import qs from 'qs'
import S3 from 'aws-sdk/clients/s3'
import {createAction} from 'redux-actions'

import {fetchGraphQL, secureFetch} from '../../common/actions'
import {SECURE_API_PREFIX} from '../../common/constants'
import {getConfigProperty} from '../../common/util/config'
import {uploadFile} from '../../common/util/upload-file'
import fileDownload from '../../common/util/file-download'
import {getEntityGraphQLRoot, getEntityIdField, getGraphQLFieldsForEntity} from '../../gtfs/util'
import {getControlPoints} from '../../editor/selectors'
import {setErrorMessage, startJobMonitor} from './status'
import {fetchFeedSource} from './feeds'

export const requestingFeedVersions = createAction('REQUESTING_FEEDVERSIONS')

export function receiveFeedVersions (feedSource, feedVersions) {
  return {
    type: 'RECEIVE_FEEDVERSIONS',
    feedSource,
    feedVersions
  }
}

export function setActiveVersion (feedVersion) {
  return {
    type: 'SET_ACTIVE_FEEDVERSION',
    feedVersion
  }
}

export function fetchFeedVersions (feedSource, unsecured) {
  return function (dispatch, getState) {
    dispatch(requestingFeedVersions())
    const apiRoot = unsecured ? 'public' : 'secure'
    const url = `/api/manager/${apiRoot}/feedversion?feedSourceId=${feedSource.id}`
    return dispatch(secureFetch(url))
      .then(response => response.json())
      .then(versions => {
        dispatch(receiveFeedVersions(feedSource, versions))
        return versions
      })
  }
}

export function requestingFeedVersion () {
  return {
    type: 'REQUESTING_FEEDVERSION'
  }
}

export function receiveFeedVersion (feedVersion) {
  return {
    type: 'RECEIVE_FEEDVERSION',
    feedVersion
  }
}

export function fetchFeedVersion (feedVersionId) {
  return function (dispatch, getState) {
    dispatch(requestingFeedVersion())
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersionId}`
    return dispatch(secureFetch(url))
      .then(response => response.json())
      .then(version => {
        return dispatch(receiveFeedVersion(version))
      })
  }
}

export function publishingFeedVersion (feedVersion) {
  return {
    type: 'PUBLISHING_FEEDVERSION',
    feedVersion
  }
}

export function publishedFeedVersion (feedVersion) {
  return {
    type: 'PUBLISHED_FEEDVERSION',
    feedVersion
  }
}

export function publishFeedVersion (feedVersion) {
  return function (dispatch, getState) {
    dispatch(publishingFeedVersion(feedVersion))
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}/publish`
    return dispatch(secureFetch(url, 'post'))
      .then(response => response.json())
      .then(version => {
        return dispatch(publishedFeedVersion(version))
      })
  }
}

export function fetchPublicFeedVersions (feedSource) {
  return function (dispatch, getState) {
    dispatch(requestingFeedVersions())
    const url = `/api/manager/public/feedversion?feedSourceId=${feedSource.id}&public=true`
    return dispatch(secureFetch(url))
      .then(response => response.json())
      .then(versions => {
        dispatch(receiveFeedVersions(feedSource, versions))
      })
  }
}

// Upload a GTFS File as a new FeedVersion

export function uploadingFeed (feedSource, file) {
  return {
    type: 'UPLOADING_FEED',
    feedSource,
    file
  }
}

export function uploadedFeed (feedSource) {
  return {
    type: 'UPLOADED_FEED',
    feedSource
  }
}

export function feedNotModified (feedSource, message) {
  return {
    type: 'FEED_NOT_MODIFIED',
    feedSource,
    message
  }
}

export function uploadFeed (feedSource, file) {
  return function (dispatch, getState) {
    dispatch(uploadingFeed(feedSource, file))
    const url = `${SECURE_API_PREFIX}feedversion?feedSourceId=${feedSource.id}&lastModified=${file.lastModified}`

    return uploadFile({file, url, token: getState().user.token})
      .then(res => {
        // 304 halt thrown by server if uploaded feed matches the hash of the
        // latest version
        if (res.status === 304) {
          // Do not start job monitor if feed matches latest version. Display the
          // status modal with a message about the cancelled upload.
          dispatch(feedNotModified(feedSource, 'Feed upload cancelled because it matches latest feed version.'))
        } else if (res.status >= 400) {
          dispatch(setErrorMessage({message: 'Error uploading feed source'}))
        } else {
          dispatch(uploadedFeed(feedSource))
          dispatch(startJobMonitor())
        }
        console.log('uploadFeed result', res)

        // fetch feed source with versions
        return dispatch(fetchFeedSource(feedSource.id, true))
      })
  }
}

const deletingFeedVersion = createAction('DELETING_FEEDVERSION')

export function deleteFeedVersion (feedVersion, changes) {
  return function (dispatch, getState) {
    dispatch(deletingFeedVersion(feedVersion))
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}`
    return dispatch(secureFetch(url, 'delete'))
      .then((res) => {
        // fetch feed source with versions + snapshots
        return dispatch(fetchFeedSource(feedVersion.feedSource.id, true, true))
      })
  }
}
const requestingValidationResult = createAction('REQUESTING_VALIDATION_RESULT')
const receiveValidationResult = createAction('RECEIVE_VALIDATION_RESULT')
const fetchingValidationErrors = createAction('FETCHING_VALIDATION_ERRORS')
const receiveValidationErrors = createAction('RECEIVE_VALIDATION_ERRORS')
const fetchingGTFSEntities = createAction('FETCHING_GTFS_ENTITIES')
const receiveGTFSEntities = createAction('RECEIVE_GTFS_ENTITIES')

export function fetchGTFSEntities ({namespace, id, type, editor = false}) {
  return function (dispatch, getState) {
    // If fetching for the editor, query on csv_line
    const entityIdField = editor ? 'id' : getEntityIdField(type)
    const graphQLRoot = getEntityGraphQLRoot(type)
    if (!graphQLRoot || !entityIdField) {
      console.error(`No graphql table or filter field for ${type}`)
      return
    }
    const fields = getGraphQLFieldsForEntity(type, editor)
    // If fetching for the editor, query on id which is an Int
    const query = `
      query entityQuery($namespace: String, $${entityIdField}: ${editor ? 'Int' : '[String]'}) {
        feed(namespace: $namespace) {
          feed_id
          feed_version
          filename
          ${graphQLRoot} (${entityIdField}: $${entityIdField}) {
            id
            ${fields}
          }
        }
      }
    `
    dispatch(fetchingGTFSEntities({namespace, id, type, editor, query}))
    // If fetching for the editor, cast id to int for csv_line field
    return dispatch(fetchGraphQL({query, variables: {namespace, [entityIdField]: editor ? +id : id}}))
      .then(res => res.json())
      .then(data => dispatch(receiveGTFSEntities({namespace, id, component: type, data, editor})))
      .then(() => {
        if (type === 'route') {
          // dispatch(setControlPoints(getControlPoints(getState())))
          // FIXME: do some of these things on fetch for trip pattern?
          // const stops = getTableById(getState().editor.data.tables, 'stop')
          // const activePattern = getState().editor.data.active.subEntityId && tripPatterns.find(p => p.id === getState().editor.data.active.subEntityId)
          // const activePatternStops = getStopsForPattern(activePattern, stops)
        }
      })
      .catch(err => console.log(err))
  }
}

export function fetchValidationErrors ({feedVersion, errorType, limit, offset}) {
  return function (dispatch, getState) {
    dispatch(fetchingValidationErrors({feedVersion, errorType, limit, offset}))
    // FIXME: why does namespace need to appear twice?
    const query = `
      query errorsQuery($namespace: String, $errorType: [String], $limit: Int, $offset: Int) {
        feed(namespace: $namespace) {
          feed_id
          feed_version
          filename
          errors (error_type: $errorType, limit: $limit, offset: $offset) {
            error_type
            entity_type
            entity_id
            line_number
            bad_value
            entity_sequence
          }
        }
      }
    `
    const {namespace} = feedVersion
    return dispatch(fetchGraphQL({query, variables: {namespace, errorType: [errorType], limit, offset}}))
      .then(res => res.json())
      .then(result => {
        if (result.feed) {
          const {errors} = result.feed
          dispatch(receiveValidationErrors({feedVersion, errorType, limit, offset, errors}))
        }
      })
      .catch(err => console.log(err))
  }
}

export function fetchValidationResult (feedVersion, isPublic) {
  return function (dispatch, getState) {
    dispatch(requestingValidationResult(feedVersion))
    const {namespace} = feedVersion
    const query = `
      query countsQuery($namespace: String) {
        feed(namespace: $namespace) {
          feed_id
          feed_version
          filename
          row_counts {
            stops
            trips
            calendar_dates
            errors
          }
          error_counts {
            type count message
          }
        }
      }
    `
    return dispatch(fetchGraphQL({query, variables: {namespace}}))
      .then(res => res.json())
      .then(result => {
        if (result.feed) {
          dispatch(receiveValidationResult({feedVersion, validationResult: result.feed}))
        }
      })
      .catch(err => console.log(err))
  }
}

export function requestingFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) {
  return {
    type: 'REQUESTING_FEEDVERSION_ISOCHRONES',
    feedVersion,
    fromLat,
    fromLon,
    toLat,
    toLon,
    date,
    fromTime,
    toTime
  }
}

export function receiveFeedVersionIsochrones (feedSource, feedVersion, isochrones, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) {
  return {
    type: 'RECEIVE_FEEDVERSION_ISOCHRONES',
    feedSource,
    feedVersion,
    isochrones,
    fromLat,
    fromLon,
    toLat,
    toLon,
    date,
    fromTime,
    toTime
  }
}

export function fetchFeedVersionIsochrones (feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime) {
  return function (dispatch, getState) {
    if (typeof date === 'undefined' || typeof fromTime === 'undefined' || typeof toTime === 'undefined') {
      date = getState().gtfs.filter.dateTimeFilter.date
      fromTime = getState().gtfs.filter.dateTimeFilter.from
      toTime = getState().gtfs.filter.dateTimeFilter.to
    }
    dispatch(requestingFeedVersionIsochrones(feedVersion, fromLat, fromLon, toLat, toLon, date, fromTime, toTime))
    const params = {fromLat, fromLon, toLat, toLon, date, fromTime, toTime}
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}/isochrones?${qs.stringify(params)}`
    return dispatch(secureFetch(url))
      .then(res => {
        console.log(res.status)
        if (res.status === 202) {
          // dispatch(setStatus)
          console.log('building network')
          return []
        }
        return res.json()
      })
      .then(isochrones => {
        console.log('received isochrones ', isochrones)
        dispatch(receiveFeedVersionIsochrones(feedVersion.feedSource, feedVersion, isochrones, fromLat, fromLon, toLat, toLon, date, fromTime, toTime))
        return isochrones
      })
  }
}

// Download a GTFS file for a FeedVersion
export function downloadFeedViaToken (feedVersion, isPublic, prefix = 'gtfs') {
  return function (dispatch, getState) {
    const route = isPublic ? 'public' : 'secure'
    const url = `/api/manager/${route}/feedversion/${feedVersion.id}/downloadtoken`
    dispatch(secureFetch(url))
    .then(response => response.json())
    .then(credentials => {
      if (getConfigProperty('application.data.use_s3_storage')) {
        dispatch(downloadS3Key(credentials, `${feedVersion.id}`, prefix))
      } else {
        // use token to download feed
        window.location.assign(`/api/manager/downloadfeed/${credentials.id}`)
      }
    })
  }
}

export function downloadS3Key (credentials, filename, folder = null) {
  return function (dispatch, getState) {
    // Set credentials and region
    const s3 = new S3({
      apiVersion: '2006-03-01',
      credentials
    })
    const Bucket = getConfigProperty('application.data.gtfs_s3_bucket')
    const Key = folder ? `${folder}/${filename}` : filename
    console.log(`downloading ${Key} from ${Bucket}`)
    s3.getObject({Bucket, Key}, (err, data) => {
      if (err) {
        console.log(err)
        dispatch(setErrorMessage({message: `Error downloading feed:\n${err}`}))
      }
      if (data) {
        fileDownload(data.Body, filename, data.contentType)
      }
    })
  }
}

export function creatingFeedVersionFromSnapshot () {
  return {
    type: 'CREATING_FEEDVERSION_FROM_SNAPSHOT'
  }
}
export function createFeedVersionFromSnapshot (feedSource, snapshotId) {
  return function (dispatch, getState) {
    dispatch(creatingFeedVersionFromSnapshot())
    const url = `${SECURE_API_PREFIX}feedversion/fromsnapshot?feedSourceId=${feedSource.id}&snapshotId=${snapshotId}`
    return dispatch(secureFetch(url, 'post'))
      .then((res) => {
        if (res) dispatch(startJobMonitor())
      })
  }
}

export function renamingFeedVersion () {
  return {
    type: 'RENAMING_FEEDVERSION'
  }
}

export function renameFeedVersion (feedVersion, name) {
  return function (dispatch, getState) {
    dispatch(renamingFeedVersion())
    const url = `${SECURE_API_PREFIX}feedversion/${feedVersion.id}/rename?name=${name}`
    return dispatch(secureFetch(url, 'put'))
      .then((res) => {
        dispatch(fetchFeedVersion(feedVersion.id))
      })
  }
}
<|endoftext|>


<commit_before>/**
 *  Copyright 2012 Tomaz Muraus
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

var async = require('async');
var log = require('logmagic').local('lib.middleware.rate_limiting');
var sprintf = require('sprintf').sprintf;

var client = require('../db').getClient();
var config = require('../util/config').config;
var misc = require('../util/misc');
var errors = require('../util/errors');
var httpUtil = require('../util/http');

/**
 * Cache of compiled regular expressions.
 * @type {Object}
 */
var REGEX_CACHE = {};

/**
 * Check and update user rate limits.
 *
 * @param {String} id user id.
 * @param {Object} rule Rate limiting rule.
 * @param {Function} callback Callback called with (err).
 */
function checkAndUpdateLimit(id, rule, callback) {
  var settings = config.middleware.rate_limiting.settings;

  async.waterfall([
    function calculateBucketKeyNames(callback) {
      var now = misc.getUnixTimestamp(), keys = [], key, bucketCount, tmp, startTs;

      bucketCount = Math.floor((now / settings.bucket_size));
      tmp = Math.floor(rule.period / settings.bucket_size);
      startTs = Math.floor((bucketCount - tmp) * settings.bucket_size);
      endTs = (bucketCount * settings.bucket_size);

      for (ts = startTs; ts < endTs; ts = (ts + settings.bucket_size)) {
        key = sprintf('rate_limits:%s:%s_%s:%s', id, rule.path_regex, rule.period, ts);
        keys.push(key);
      }

      callback(null, keys);
    },

    function getCurrentUsage(keys, callback) {
      client.getMulti(keys, function(err, values) {
        var usage = 0, bucketNameToValueMap = {}, i, key, value;

        if (err) {
          callback(err);
          return;
        }

        for (i = 0; i < keys.length; i++) {
          key = keys[i];
          value = parseInt(values[i], 10);
          bucketNameToValueMap[key] = value || 0;

          if (value) {
            usage += value;
          }
        }

        callback(null, usage, keys, bucketNameToValueMap);
      });
    },

    function checkUsageAgainstLimit(usage, keys, bucketNameToValueMap, callback) {
      if (usage >= rule.limit) {
        // TODO: Return usage in the headers, etc.
        log.debug('Limit has been reached', {'usage': usage, 'limit': rule.limit});
        callback(new errors.RateLimitReachedError(rule));
        return;
      }

      callback(null, keys, bucketNameToValueMap);
    },

    function updateUsage(keys, bucketNameToValueMap, callback) {
      var currentBucketKey = keys[keys.length - 1], value = bucketNameToValueMap[currentBucketKey];
      // Update counter for the currently active bucket
      client.incr(currentBucketKey, {'step': 1, 'ttl': settings.bucket_size}, function(err) {
        if (err) {
          callback(err);
          return;
        }

        callback();
      });
    }
  ], callback);
}

exports.processRequest = function(req, res, callback) {
  var settings = config.middleware.rate_limiting.settings, method = req.method, url = req.url,
      reachedLimitErrors = [];

  async.forEach(settings.limits, function(rule, callback) {
    var regex;

    if (!REGEX_CACHE.hasOwnProperty(rule.path_regex)) {
      REGEX_CACHE[rule.path_regex] = new RegExp(rule.path_regex);
    }

    regex = REGEX_CACHE[rule.path_regex];

    // TODO: More efficient based on the database backend
    if (rule.method === method && regex.test(url)) {
      log.debug('URL matches a rate limited path, updating and checking limits...',
                {'url': url, 'method': method, 'path_regex': rule.path_regex});

      checkAndUpdateLimit(req._tenantId, rule, function(err, usage) {
        if (err && (err instanceof errors.RateLimitReachedError)) {
          reachedLimitErrors.push(err);
          err = null;
        }

        callback(err);
      });
    }
    else {
      callback();
    }
  },

  function(err) {
    if (err) {
      httpUtil.returnError(res, 400, err.message);
    }

    if (reachedLimitErrors.length >= 1) {
      // TODO: Better, also return limit in the headers, etc
      err = reachedLimitErrors[0];
      httpUtil.returnError(res, 400, err.message);
    }

    callback(err);
  });
};
<commit_msg>Inject usage headers in the response.<commit_after>/**
 *  Copyright 2012 Tomaz Muraus
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

var async = require('async');
var log = require('logmagic').local('lib.middleware.rate_limiting');
var sprintf = require('sprintf').sprintf;

var client = require('../db').getClient();
var config = require('../util/config').config;
var misc = require('../util/misc');
var errors = require('../util/errors');
var httpUtil = require('../util/http');

/**
 * Cache of compiled regular expressions.
 * @type {Object}
 */
var REGEX_CACHE = {};

/**
 * Check and update user rate limits.
 *
 * @param {String} id user id.
 * @param {Object} rule Rate limiting rule.
 * @param {Function} callback Callback called with (err).
 */
function checkAndUpdateLimit(id, rule, callback) {
  var settings = config.middleware.rate_limiting.settings, currentUsage = 0;

  async.waterfall([
    function calculateBucketKeyNames(callback) {
      var now = misc.getUnixTimestamp(), keys = [], key, bucketCount, tmp, startTs;

      bucketCount = Math.floor((now / settings.bucket_size));
      tmp = Math.floor(rule.period / settings.bucket_size);
      startTs = Math.floor((bucketCount - tmp) * settings.bucket_size);
      endTs = (bucketCount * settings.bucket_size);

      for (ts = startTs; ts < endTs; ts = (ts + settings.bucket_size)) {
        key = sprintf('rate_limits:%s:%s_%s:%s', id, rule.path_regex, rule.period, ts);
        keys.push(key);
      }

      callback(null, keys);
    },

    function getCurrentUsage(keys, callback) {
      client.getMulti(keys, function(err, values) {
        var usage = 0, bucketNameToValueMap = {}, i, key, value;

        if (err) {
          callback(err);
          return;
        }

        for (i = 0; i < keys.length; i++) {
          key = keys[i];
          value = parseInt(values[i], 10);
          bucketNameToValueMap[key] = value || 0;

          if (value) {
            usage += value;
          }
        }

        currentUsage = usage;
        callback(null, keys, bucketNameToValueMap);
      });
    },

    function checkUsageAgainstLimit(keys, bucketNameToValueMap, callback) {
      if (currentUsage >= rule.limit) {
        log.debug('Limit has been reached', {'usage': currentUsage, 'limit': rule.limit});
        callback(new errors.RateLimitReachedError(rule));
        return;
      }

      callback(null, keys, bucketNameToValueMap);
    },

    function updateUsage(keys, bucketNameToValueMap, callback) {
      var currentBucketKey = keys[keys.length - 1], value = bucketNameToValueMap[currentBucketKey];
      // Update counter for the currently active bucket
      client.incr(currentBucketKey, {'step': 1, 'ttl': settings.bucket_size}, function(err) {
        if (err) {
          callback(err);
          return;
        }

        currentUsage++;
        callback(null);
      });
    }
  ],

  function(err) {
    callback(err, currentUsage);
  });
}

exports.processRequest = function(req, res, callback) {
  var settings = config.middleware.rate_limiting.settings, method = req.method, url = req.url,
      reachedLimitErrors = [], oldWriteHead = res.writeHead;

  // Inject usage / limits headers
  res.writeHead = function(code, headers) {
    var item;
    headers = headers || {};

    if (req.activeLimits.length >= 1) {
      req.activeLimits.sort(function(a, b) {
        return (a.available - b.available);
      });

      item = req.activeLimits[0];

      headers['X-Ratelimit-Path-Regex'] = item.regex;
      headers['X-Ratelimit-Limit'] = item.limit;
      headers['X-Ratelimit-Used'] = item.used;
      headers['X-Ratelimit-Window'] = item.period + ' seconds';
    }

    oldWriteHead.call(res, code, headers);
  };

  req.activeLimits = [];
  async.forEach(settings.limits, function(rule, callback) {
    var regex;

    if (!REGEX_CACHE.hasOwnProperty(rule.path_regex)) {
      REGEX_CACHE[rule.path_regex] = new RegExp(rule.path_regex);
    }

    regex = REGEX_CACHE[rule.path_regex];

    // TODO: More efficient based on the database backend
    if (rule.method === method && regex.test(url)) {
      log.debug('URL matches a rate limited path, updating and checking limits...',
                {'url': url, 'method': method, 'path_regex': rule.path_regex});

      checkAndUpdateLimit(req._tenantId, rule, function(err, used) {
        if (err && (err instanceof errors.RateLimitReachedError)) {
          reachedLimitErrors.push(err);
          err = null;
        }

        req.activeLimits.push({'regex': rule.path_regex, 'limit': rule.limit,
                               'period': rule.period, 'available': (rule.limit - used),
                               'used': used});

        callback(err);
      });
    }
    else {
      callback();
    }
  },

  function(err) {
    if (err) {
      httpUtil.returnError(res, 400, err.message);
    }

    if (reachedLimitErrors.length >= 1) {
      // TODO: Better, also return limit in the headers, etc
      err = reachedLimitErrors[0];
      httpUtil.returnError(res, 400, err.message);
    }

    callback(err);
  });
};
<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');

const jovo = require('../../jovo');
const AlexaResponse = require('./alexaResponse').AlexaResponse;
const AlexaRequest = require('./alexaRequest').AlexaRequest;
const CardBuilder = AlexaResponse.CardBuilder;
const AudioPlayer = require('./audioPlayer').AudioPlayer;

/**
 * Alexa specific implementations
 */
class AlexaSkill {

    /**
     * Constructor
     * @param {Jovo} jovo
     * @param {object} request
     */
    constructor(jovo, request) {
        this.jovo = jovo;
        this.request = new AlexaRequest(request);
        this.response = new AlexaResponse();

        this.response.setSessionAttributes(this.request.getSessionAttributes());
    }

    /** **
     * Request getter
     */

    /**
     * Returns unique user id sent by the platforms.
     * @public
     * @return {string} UserId
     */
    getUserId() {
        return this.request.getUserId();
    }

    /**
     * Returns device id
     * @return {string} device id
     */
    getDeviceId() {
        return this.request.getDeviceId();
    }

    /**
     * Returns supported interfaces from device.
     * @public
     * @return {string} supportedInterfaces
     */
    getSupportedInterfaces() {
        return this.request.getSupportedInterfaces();
    }

    /**
     * Returns token of the request
     * (Touched/Selected Element on Echo Show)
     * @return {*}
     */
    getElementId() {
        return this.request.getRequestToken();
    }

    /**
     * Returns consent token
     * @return {string} constent token
     */
    getConsentToken() {
        return this.request.getConsentToken();
    }

    /**
     * Returns intent name from request
     * @public
     * @return {string}
     */
    getIntentName() {
        return this.request.getIntentName();
    }

    /**
     * Returns application ID (Skill ID) of the request
     * @return {String} applicationId
     */
    getApplicationId() {
        return this.request.getApplicationId();
    }

    /**
     * Returns alexa request session object
     * @return {*}
     */
    getSession() {
        return this.request.getSession();
    }

    /** *****************************************************
     *
     * JOVO wrapper functions
     *
     ************************************/

    /**
     * Returns request type
     * LaunchRequest, IntentRequest, OnElementSelected SessionEndedRequest
     * @return {string}
     */
    getRequestType() {
        if (this.request.getRequestType().substring(0, 11) === 'AudioPlayer') {
            return 'AUDIOPLAYER';
        }

        if (this.request.getRequestType() === 'LaunchRequest') {
            return jovo.REQUEST_TYPE_ENUM.LAUNCH;
        }

        if (this.request.getRequestType() === 'Display.ElementSelected') {
            return jovo.REQUEST_TYPE_ENUM.ON_ELEMENT_SELECTED;
        }

        if (this.request.getRequestType() === 'IntentRequest') {
            return jovo.REQUEST_TYPE_ENUM.INTENT;
        }

        if (this.request.getRequestType() === 'SessionEndedRequest') {
            return jovo.REQUEST_TYPE_ENUM.END;
        }
    }

    /**
     * Returns reason code for an end of a session
     *
     * STOP_INTENT = User says 'Stop'
     * MAX_PROMPTS_EXCEEDED = No user input on reprompt
     * ERROR
     *
     * @return {*}
     */
    getEndReason() {
        if (this.getRequestType() === jovo.REQUEST_TYPE_ENUM.END) {
            return this.request.getEndReason();
        }

        if (this.getRequestType() === jovo.REQUEST_TYPE_ENUM.INTENT &&
            this.getIntentName() === 'AMAZON.StopIntent') {
            return 'STOP_INTENT';
        }
    }

    /**
     * Ends session without saying anything
     */
    endSession() {

    }

    /**
     * Returns intent parameters filled by the user.
     * "My name is {John Doe} and I live in {New York}"
     * Returns object with name => value objects
     * {
     *   name : "John Doe",
     *   city : "New York"
     * }
     * @public
     * @return {*}
     */
    getInputs() {
        let inputs = {};

        if (!this.request.getSlots()) {
            return inputs;
        }

        let slotNames = Object.keys(this.request.getSlots());

        for (let i = 0; i < slotNames.length; i++) {
            let key = slotNames[i];
            inputs[key] = this.request.getSlots()[key].value;
        }
        return inputs;
    }

    /**
     * Get input object by name
     * @param {string} name
     * @return {string}
     */
    getInput(name) {
        return this.getInputs()[name];
    }

    /**
     * Returns type of platform
     * @return {string}
     */
    getType() {
        return jovo.PLATFORM_ENUM.ALEXA_SKILL;
    }

    /**
     * Returns platform's locale
     * @return {string}
     */
    getLocale() {
        return this.request.getLocale();
    }

    /**
     * Returns platform's api endpoint
     * @return {string}
     */
    getApiEndpoint() {
        return this.request.getApiEndpoint();
    }

    /**
     * isNewSession
     * @return {boolean}
     */
    isNewSession() {
        return this.request.isNewSession();
    }

    /**
     * Returns platform's timestamp
     * @return {string}
     */
    getTimestamp() {
        return this.request.getTimestamp();
    }

    /**
     * Returns state value stored in the request session
     *
     * @return {*}
     */
    getState() {
        return this.request.getSessionAttribute('STATE') ||
        this.response.getSessionAttribute('STATE');
    }

    /**
     * Returns speech text
     * @return {string}
     */
    getSpeechText() {
        return this.response.getSpeechText();
    }

    /** ***********************************************************
     *
     * RESPONSE BUILDER FUNCTIONS
     *
     */

    /**
     * Returns AlexaResponse object.
     * @return {AlexaResponse|*}
     */
    getResponse() {
        return this.response;
    }

    /**
     * Returns AlexaRequest object
     * @return {AlexaRequest}
     */
    getRequest() {
        return this.request;
    }

    /**
     * Returns session attribute value
     * from the response(!) object
     * @param {string} name
     * @return {*}
     */
    getSessionAttribute(name) {
        return this.response.getSessionAttribute(name);
    }

    /**
     * Returns all session attributes
     * from the response(!) object.
     * @return {*}
     */
    getSessionAttributes() {
        return this.response.getSessionAttributes();
    }

    /**
     * Returns response object
     * @return {object}
     */
    getResponseObject() {
        return this.response.getResponseObject();
    }

    /**
     * Sets "raw" response object
     * @param {object} responseObj
     */
    setResponseObject(responseObj) {
        this.response.responseObj = responseObj;
        this.jovo.respond();
    }

    /**
     * Stores state in response object session
     * @public
     * @param {string} state
     */
    setState(state) {
        this.response.setSessionAttribute('STATE', state);
    }

    /**
     * Sets session attributes
     * @param {object} attributes
     * @param {*} value
     */
    setSessionAttributes(attributes) {
        this.response.setSessionAttributes(attributes);
    }

    /**
     * Sets session attribute with value
     * @param {string} name
     * @param {*} value
     */
    setSessionAttribute(name, value) {
        this.response.setSessionAttribute(name, value);
    }

    /**
     * Sets speech output with ShouldEndSession = true
     *
     * @param {string} speech
     */
    tell(speech) {
        this.response.tell(speech);
    }

    /**
     * Creates an audio tag within ssml
     *
     * Max 90 seconds
     * @param {string} audioUrl
     */
    play(audioUrl) {
        this.response.play(audioUrl);
    }

    /**
     * Creates object with reprompt.
     * Keeps session open
     * @param {string} speech
     * @param {string} repromptSpeech
     */
    ask(speech, repromptSpeech) {
        this.response.ask(speech, repromptSpeech);
    }

    /**
     * Implementation of generic withSimpleCard
     * Show a simple card to the response object
     * @param {string} title
     * @param {string} content
     * @return {AlexaSkill} this
     */
    showSimpleCard(title, content) {
        this.response.addSimpleCard(title, content);
        return this;
    }

    /**
     * Implementation of generic withImageCard
     * Show a standard card with a card to the response object
     * @param {string} title
     * @param {string} content
     * @param {string} imageObject object with secured image url
     * @return {AlexaSkill} this
     */
    showImageCard(title, content, imageObject) {
        const smallImageUrl = _.isString(imageObject) ? imageObject : imageObject.smallImageUrl;
        const largeImageUrl = _.isString(imageObject) ? imageObject : imageObject.largeImageUrl;

        this.response.addStandardCard(title, content, smallImageUrl, largeImageUrl);
        return this;
    }

    /**
     * Shows ask for country and postal code card
     * @return {Jovo}
     */
    showAskForCountryAndPostalCodeCard() {
        this.response.addAskForCountryAndPostalCodeCard();
        return this;
    }

    /**
     * Shows ask for address card
     * @return {Jovo}
     */
    showAskForAddressCard() {
        this.response.addAskForAddressCard();
        return this;
    }

    /**
     * Shows ask for list permission card
     * @param {Array} types 'write' or 'read'
     * @return {Jovo}
     */
    showAskForListPermissionCard(types) {
        this.response.addAskForListPermissionCard(types);
        return this;
    }

    /**
     * Implementation of generic withAccountLinkingCard
     * Show an account linking card to the response object
     * @return {AlexaSkill} this
     */
    showAccountLinkingCard() {
        this.response.addAccountLinkingCard();
        return this;
    }

    /**
     * Input object with name, value and confirmationStatus
     * @return {{}}
     */
    getInputsObj() {
        let inputs = {};

        if (!this.request.getSlots()) {
            return inputs;
        }

        let slotNames = Object.keys(this.request.getSlots());

        for (let i = 0; i < slotNames.length; i++) {
            let key = slotNames[i];
            inputs[key] = {
                name:
                this.request.getSlots()[key].name,
                value:
                this.request.getSlots()[key].value,
                confirmationStatus:
                this.request.getSlots()[key].confirmationStatus,
            };
        }
        return inputs;
    }

    /**
     * Returns AlexaSkill CardBuilder instance
     * @return {CardBuilder}
     */
    getCardBuilder() {
        return new CardBuilder();
    }

    /**
     * Returns Intent Confirmation status
     * @return {String}
     */
    getIntentConfirmationStatus() {
        return this.request.request.intent.confirmationStatus;
    }

    /**
     * Returns state of dialog
     * @return {jovo.DIALOGSTATE_ENUM}
     */
    getDialogState() {
        return this.request.getDialogState();
    }

    /**
     * Creates delegate directive. Alexa handles next dialog
     * step by her(?)self.
     * @param {object} updatedIntent
     */
    dialogDelegate(updatedIntent) {
        this.response.dialogDelegate(updatedIntent);
        this.jovo.respond();
    }

    /**
     * Let alexa ask user for the value of a specific slot
     * @param {string} slotToElicit name of the slot
     * @param {string} speechText
     * @param {object} updatedIntent
     */
    dialogElicitSlot(slotToElicit, speechText, updatedIntent) {
        this.response.dialogElicitSlot(slotToElicit, speechText, updatedIntent);
        this.jovo.respond();
    }

    /**
     * Let alexa ask user to confirm slot with yes or no
     * @param {string} slotToConfirm name of the slot
     * @param {string} speechText
     * @param {object} updatedIntent
     */
    dialogConfirmSlot(slotToConfirm, speechText, updatedIntent) {
        this.response.dialogConfirmSlot(slotToConfirm, speechText, updatedIntent);
        this.jovo.respond();
    }

    /**
     * Returns if slot is confirmed
     * @param {*} slot
     * @return {boolean}
     */
    isSlotConfirmed(slot) {
        return this.request.getSlotConfirmationStatus(slot) === 'CONFIRMED';
    }

    /**
     * Shows template on Echo Show
     * @param {*} template
     * @return {AlexaSkill}
     */
    showDisplayTemplate(template) {
        if (template.template) {
            template = template.build();
        }
        this.response.addDisplayRenderTemplateDirective(template);
        return this;
    }

    /**
     * Shows hint on Echo Show
     * @param {*} text
     * @return {AlexaSkill}
     */
    showDisplayHint(text) {
        this.response.addHintDirective(text);
        return this;
    }

    /**
     * Shows video on Echo Show
     * @param {string} url
     * @param {string} title
     * @param {string} subtitle
     */
    showVideo(url, title, subtitle) {
        this.response.addVideoDirective(url, title, subtitle);
        this.response.deleteShouldEndSession();
        this.jovo.respond();
    }

    /**
     * Returns Alexa audio player instance
     * @return {AudioPlayer}
     */
    audioPlayer() {
        return new AudioPlayer(this.jovo);
    }

    /**
     * Returns template builder by type
     * @param {string} type
     * @return {*}
     */
    templateBuilder(type) {
        if (type === 'BodyTemplate1') {
            return new AlexaResponse.BodyTemplate1Builder();
        }
        if (type === 'BodyTemplate2') {
            return new AlexaResponse.BodyTemplate2Builder();
        }
        if (type === 'BodyTemplate3') {
            return new AlexaResponse.BodyTemplate3Builder();
        }
        if (type === 'BodyTemplate6') {
            return new AlexaResponse.BodyTemplate6Builder();
        }
        if (type === 'ListTemplate1') {
            return new AlexaResponse.ListTemplate1Builder();
        }
        if (type === 'ListTemplate2') {
            return new AlexaResponse.ListTemplate2Builder();
        }
        if (type === 'ListTemplate3') {
            return new AlexaResponse.ListTemplate3Builder();
        }
    }
}

module.exports.AlexaSkill = AlexaSkill;
<commit_msg>:hammer: Fixed method call<commit_after>'use strict';

const _ = require('lodash');

const jovo = require('../../jovo');
const AlexaResponse = require('./alexaResponse').AlexaResponse;
const AlexaRequest = require('./alexaRequest').AlexaRequest;
const CardBuilder = AlexaResponse.CardBuilder;
const AudioPlayer = require('./audioPlayer').AudioPlayer;

/**
 * Alexa specific implementations
 */
class AlexaSkill {

    /**
     * Constructor
     * @param {Jovo} jovo
     * @param {object} request
     */
    constructor(jovo, request) {
        this.jovo = jovo;
        this.request = new AlexaRequest(request);
        this.response = new AlexaResponse();

        this.response.setSessionAttributes(this.request.getSessionAttributes());
    }

    /** **
     * Request getter
     */

    /**
     * Returns unique user id sent by the platforms.
     * @public
     * @return {string} UserId
     */
    getUserId() {
        return this.request.getUserId();
    }

    /**
     * Returns device id
     * @return {string} device id
     */
    getDeviceId() {
        return this.request.getDeviceId();
    }

    /**
     * Returns supported interfaces from device.
     * @public
     * @return {string} supportedInterfaces
     */
    getSupportedInterfaces() {
        return this.request.getSupportedInterfaces();
    }

    /**
     * Returns token of the request
     * (Touched/Selected Element on Echo Show)
     * @return {*}
     */
    getElementId() {
        return this.request.getRequestToken();
    }

    /**
     * Returns consent token
     * @return {string} constent token
     */
    getConsentToken() {
        return this.request.getConsentToken();
    }

    /**
     * Returns intent name from request
     * @public
     * @return {string}
     */
    getIntentName() {
        return this.request.getIntentName();
    }

    /**
     * Returns application ID (Skill ID) of the request
     * @return {String} applicationId
     */
    getApplicationId() {
        return this.request.getApplicationId();
    }

    /**
     * Returns alexa request session object
     * @return {*}
     */
    getSession() {
        return this.request.getSession();
    }

    /** *****************************************************
     *
     * JOVO wrapper functions
     *
     ************************************/

    /**
     * Returns request type
     * LaunchRequest, IntentRequest, OnElementSelected SessionEndedRequest
     * @return {string}
     */
    getRequestType() {
        if (this.request.getRequestType().substring(0, 11) === 'AudioPlayer') {
            return 'AUDIOPLAYER';
        }

        if (this.request.getRequestType() === 'LaunchRequest') {
            return jovo.REQUEST_TYPE_ENUM.LAUNCH;
        }

        if (this.request.getRequestType() === 'Display.ElementSelected') {
            return jovo.REQUEST_TYPE_ENUM.ON_ELEMENT_SELECTED;
        }

        if (this.request.getRequestType() === 'IntentRequest') {
            return jovo.REQUEST_TYPE_ENUM.INTENT;
        }

        if (this.request.getRequestType() === 'SessionEndedRequest') {
            return jovo.REQUEST_TYPE_ENUM.END;
        }
    }

    /**
     * Returns reason code for an end of a session
     *
     * STOP_INTENT = User says 'Stop'
     * MAX_PROMPTS_EXCEEDED = No user input on reprompt
     * ERROR
     *
     * @return {*}
     */
    getEndReason() {
        if (this.getRequestType() === jovo.REQUEST_TYPE_ENUM.END) {
            return this.request.getEndReason();
        }

        if (this.getRequestType() === jovo.REQUEST_TYPE_ENUM.INTENT &&
            this.getIntentName() === 'AMAZON.StopIntent') {
            return 'STOP_INTENT';
        }
    }

    /**
     * Ends session without saying anything
     */
    endSession() {

    }

    /**
     * Returns intent parameters filled by the user.
     * "My name is {John Doe} and I live in {New York}"
     * Returns object with name => value objects
     * {
     *   name : "John Doe",
     *   city : "New York"
     * }
     * @public
     * @return {*}
     */
    getInputs() {
        let inputs = {};

        if (!this.request.getSlots()) {
            return inputs;
        }

        let slotNames = Object.keys(this.request.getSlots());

        for (let i = 0; i < slotNames.length; i++) {
            let key = slotNames[i];
            inputs[key] = this.request.getSlots()[key].value;
        }
        return inputs;
    }

    /**
     * Get input object by name
     * @param {string} name
     * @return {string}
     */
    getInput(name) {
        return this.getInputs()[name];
    }

    /**
     * Returns type of platform
     * @return {string}
     */
    getType() {
        return jovo.PLATFORM_ENUM.ALEXA_SKILL;
    }

    /**
     * Returns platform's locale
     * @return {string}
     */
    getLocale() {
        return this.request.getLocale();
    }

    /**
     * Returns platform's api endpoint
     * @return {string}
     */
    getApiEndpoint() {
        return this.request.getApiEndpoint();
    }

    /**
     * isNewSession
     * @return {boolean}
     */
    isNewSession() {
        return this.request.isNewSession();
    }

    /**
     * Returns platform's timestamp
     * @return {string}
     */
    getTimestamp() {
        return this.request.getTimestamp();
    }

    /**
     * Returns state value stored in the request session
     *
     * @return {*}
     */
    getState() {
        return this.request.getSessionAttribute('STATE') ||
        this.response.getSessionAttribute('STATE');
    }

    /**
     * Returns speech text
     * @return {string}
     */
    getSpeechText() {
        return this.response.getSpeechText();
    }

    /** ***********************************************************
     *
     * RESPONSE BUILDER FUNCTIONS
     *
     */

    /**
     * Returns AlexaResponse object.
     * @return {AlexaResponse|*}
     */
    getResponse() {
        return this.response;
    }

    /**
     * Returns AlexaRequest object
     * @return {AlexaRequest}
     */
    getRequest() {
        return this.request;
    }

    /**
     * Returns session attribute value
     * from the response(!) object
     * @param {string} name
     * @return {*}
     */
    getSessionAttribute(name) {
        return this.response.getSessionAttribute(name);
    }

    /**
     * Returns all session attributes
     * from the response(!) object.
     * @return {*}
     */
    getSessionAttributes() {
        return this.response.getSessionAttributes();
    }

    /**
     * Returns response object
     * @return {object}
     */
    getResponseObject() {
        return this.response.getResponseObject();
    }

    /**
     * Sets "raw" response object
     * @param {object} responseObj
     */
    setResponseObject(responseObj) {
        this.response.responseObj = responseObj;
        this.jovo.respond();
    }

    /**
     * Stores state in response object session
     * @public
     * @param {string} state
     */
    setState(state) {
        this.response.setSessionAttribute('STATE', state);
    }

    /**
     * Sets session attributes
     * @param {object} attributes
     * @param {*} value
     */
    setSessionAttributes(attributes) {
        this.response.setSessionAttributes(attributes);
    }

    /**
     * Sets session attribute with value
     * @param {string} name
     * @param {*} value
     */
    setSessionAttribute(name, value) {
        this.response.setSessionAttribute(name, value);
    }

    /**
     * Sets speech output with ShouldEndSession = true
     *
     * @param {string} speech
     */
    tell(speech) {
        this.response.tell(speech);
    }

    /**
     * Creates an audio tag within ssml
     *
     * Max 90 seconds
     * @param {string} audioUrl
     */
    play(audioUrl) {
        this.response.play(audioUrl);
    }

    /**
     * Creates object with reprompt.
     * Keeps session open
     * @param {string} speech
     * @param {string} repromptSpeech
     */
    ask(speech, repromptSpeech) {
        this.response.ask(speech, repromptSpeech);
    }

    /**
     * Implementation of generic withSimpleCard
     * Show a simple card to the response object
     * @param {string} title
     * @param {string} content
     * @return {AlexaSkill} this
     */
    showSimpleCard(title, content) {
        this.response.addSimpleCard(title, content);
        return this;
    }

    /**
     * Implementation of generic withImageCard
     * Show a standard card with a card to the response object
     * @param {string} title
     * @param {string} content
     * @param {string} imageObject object with secured image url
     * @return {AlexaSkill} this
     */
    showImageCard(title, content, imageObject) {
        const smallImageUrl = _.isString(imageObject) ? imageObject : imageObject.smallImageUrl;
        const largeImageUrl = _.isString(imageObject) ? imageObject : imageObject.largeImageUrl;

        this.response.addStandardCard(title, content, smallImageUrl, largeImageUrl);
        return this;
    }

    /**
     * Shows ask for country and postal code card
     * @return {Jovo}
     */
    showAskForCountryAndPostalCodeCard() {
        this.response.addAskForCountryAndPostalCodeCard();
        return this;
    }

    /**
     * Shows ask for address card
     * @return {Jovo}
     */
    showAskForAddressCard() {
        this.response.addAskForAddressCard();
        return this;
    }

    /**
     * Shows ask for list permission card
     * @param {Array} types 'write' or 'read'
     * @return {Jovo}
     */
    showAskForListPermissionCard(types) {
        this.response.addAskForListPermissionCard(types);
        return this;
    }

    /**
     * Implementation of generic withAccountLinkingCard
     * Show an account linking card to the response object
     * @return {AlexaSkill} this
     */
    showAccountLinkingCard() {
        this.response.addLinkAccountCard();
        return this;
    }

    /**
     * Input object with name, value and confirmationStatus
     * @return {{}}
     */
    getInputsObj() {
        let inputs = {};

        if (!this.request.getSlots()) {
            return inputs;
        }

        let slotNames = Object.keys(this.request.getSlots());

        for (let i = 0; i < slotNames.length; i++) {
            let key = slotNames[i];
            inputs[key] = {
                name:
                this.request.getSlots()[key].name,
                value:
                this.request.getSlots()[key].value,
                confirmationStatus:
                this.request.getSlots()[key].confirmationStatus,
            };
        }
        return inputs;
    }

    /**
     * Returns AlexaSkill CardBuilder instance
     * @return {CardBuilder}
     */
    getCardBuilder() {
        return new CardBuilder();
    }

    /**
     * Returns Intent Confirmation status
     * @return {String}
     */
    getIntentConfirmationStatus() {
        return this.request.request.intent.confirmationStatus;
    }

    /**
     * Returns state of dialog
     * @return {jovo.DIALOGSTATE_ENUM}
     */
    getDialogState() {
        return this.request.getDialogState();
    }

    /**
     * Creates delegate directive. Alexa handles next dialog
     * step by her(?)self.
     * @param {object} updatedIntent
     */
    dialogDelegate(updatedIntent) {
        this.response.dialogDelegate(updatedIntent);
        this.jovo.respond();
    }

    /**
     * Let alexa ask user for the value of a specific slot
     * @param {string} slotToElicit name of the slot
     * @param {string} speechText
     * @param {object} updatedIntent
     */
    dialogElicitSlot(slotToElicit, speechText, updatedIntent) {
        this.response.dialogElicitSlot(slotToElicit, speechText, updatedIntent);
        this.jovo.respond();
    }

    /**
     * Let alexa ask user to confirm slot with yes or no
     * @param {string} slotToConfirm name of the slot
     * @param {string} speechText
     * @param {object} updatedIntent
     */
    dialogConfirmSlot(slotToConfirm, speechText, updatedIntent) {
        this.response.dialogConfirmSlot(slotToConfirm, speechText, updatedIntent);
        this.jovo.respond();
    }

    /**
     * Returns if slot is confirmed
     * @param {*} slot
     * @return {boolean}
     */
    isSlotConfirmed(slot) {
        return this.request.getSlotConfirmationStatus(slot) === 'CONFIRMED';
    }

    /**
     * Shows template on Echo Show
     * @param {*} template
     * @return {AlexaSkill}
     */
    showDisplayTemplate(template) {
        if (template.template) {
            template = template.build();
        }
        this.response.addDisplayRenderTemplateDirective(template);
        return this;
    }

    /**
     * Shows hint on Echo Show
     * @param {*} text
     * @return {AlexaSkill}
     */
    showDisplayHint(text) {
        this.response.addHintDirective(text);
        return this;
    }

    /**
     * Shows video on Echo Show
     * @param {string} url
     * @param {string} title
     * @param {string} subtitle
     */
    showVideo(url, title, subtitle) {
        this.response.addVideoDirective(url, title, subtitle);
        this.response.deleteShouldEndSession();
        this.jovo.respond();
    }

    /**
     * Returns Alexa audio player instance
     * @return {AudioPlayer}
     */
    audioPlayer() {
        return new AudioPlayer(this.jovo);
    }

    /**
     * Returns template builder by type
     * @param {string} type
     * @return {*}
     */
    templateBuilder(type) {
        if (type === 'BodyTemplate1') {
            return new AlexaResponse.BodyTemplate1Builder();
        }
        if (type === 'BodyTemplate2') {
            return new AlexaResponse.BodyTemplate2Builder();
        }
        if (type === 'BodyTemplate3') {
            return new AlexaResponse.BodyTemplate3Builder();
        }
        if (type === 'BodyTemplate6') {
            return new AlexaResponse.BodyTemplate6Builder();
        }
        if (type === 'ListTemplate1') {
            return new AlexaResponse.ListTemplate1Builder();
        }
        if (type === 'ListTemplate2') {
            return new AlexaResponse.ListTemplate2Builder();
        }
        if (type === 'ListTemplate3') {
            return new AlexaResponse.ListTemplate3Builder();
        }
    }
}

module.exports.AlexaSkill = AlexaSkill;
<|endoftext|>


<commit_before>// Copyright 2013 Mark Cavage, Inc.  All rights reserved.
// Copyright 2015 Mateusz Stępniak, zenedith@wp.pl

var assert = require('assert-plus');
var mime = require('mime');
var NotAcceptableError = require('../../error/notAcceptableError').NotAcceptableError;
var VERSION_FORMAT = '^application/vnd.vehicle-history.v(\\d)\\+(.+)$';

var acceptParser = function (acceptable) {

  if (!Array.isArray(acceptable)) {
    acceptable = [acceptable];
  }

  assert.arrayOfString(acceptable, 'acceptable');

  acceptable = acceptable.filter(function (a) {
    return (a);
  }).map(function (a) {
    return ((a.indexOf('/') === -1) ? mime.lookup(a) : a);
  }).filter(function (a) {
    return (a);
  });

  var e = new NotAcceptableError('Server accepts: ' + acceptable.join());

  function parseAccept(req, res, next) {
    var accept = req.headers['accept'];

    if (accept) {
      var match = accept.match(VERSION_FORMAT);

      if (match) {
        req.headers['accept-version'] = '~' + match[1];
        req.headers['accept'] = mime.lookup(match[2]);
        req.headers['x-accept'] = accept;

        if (req.headers['accept']) {
          return next();
        }
      }
    }

    if (req.accepts(acceptable)) {
      return next();
    }

    res.json(e);
    next(false);
  }

  return (parseAccept);
};

module.exports.acceptParser = acceptParser;
<commit_msg>Fixed accept parser<commit_after>// Copyright 2013 Mark Cavage, Inc.  All rights reserved.
// Copyright 2015 Mateusz Stępniak, zenedith@wp.pl

var assert = require('assert-plus');
var mime = require('mime');
var NotAcceptableError = require('../../error/notAcceptableError').NotAcceptableError;
var VERSION_FORMAT = '^application/vnd\..+.v(\\d)\\+(.+)$';

var acceptParser = function (acceptable) {

  if (!Array.isArray(acceptable)) {
    acceptable = [acceptable];
  }

  assert.arrayOfString(acceptable, 'acceptable');

  acceptable = acceptable.filter(function (a) {
    return (a);
  }).map(function (a) {
    return ((a.indexOf('/') === -1) ? mime.lookup(a) : a);
  }).filter(function (a) {
    return (a);
  });

  var e = new NotAcceptableError('Server accepts: ' + acceptable.join());

  function parseAccept(req, res, next) {
    var accept = req.headers['accept'];

    if (accept) {
      var match = accept.match(VERSION_FORMAT);

      if (match) {
        req.headers['accept-version'] = '~' + match[1];
        req.headers['accept'] = mime.lookup(match[2]);
        req.headers['x-accept'] = accept;

        if (req.headers['accept']) {
          return next();
        }
      }
    }

    if (req.accepts(acceptable)) {
      return next();
    }

    res.json(e);
    next(false);
  }

  return (parseAccept);
};

module.exports.acceptParser = acceptParser;
<|endoftext|>


<commit_before>'use strict';

var levels = require('../levels');

var TIME_10_MINS = 10 * 60 * 1000;
var TIME_15_MINS = 15 * 60 * 1000;

function init() {

  function bwp() {
    return bwp;
  }

  bwp.label = 'Bolus Wizard Preview';
  bwp.pluginType = 'pill-minor';

  function checkMissingInfo (sbx) {
    var errors = [];
    if (!sbx.data.profile || !sbx.data.profile.hasData()) {
      errors.push('Missing need a treatment profile');
    } else if (profileFieldsMissing(sbx)) {
      errors.push('Missing sens, target_high, or target_low treatment profile fields');
    }

    if (!sbx.properties.iob) {
      errors.push('Missing IOB property');
    }

    if (!isSGVOk(TIME_15_MINS, sbx)) {
      errors.push('Data isn\'t current');
    }

    return errors;
  }

  bwp.setProperties = function setProperties(sbx) {
    sbx.offerProperty('bwp', function setBWP ( ) {
      return bwp.calc(sbx);
    });
  };

  bwp.checkNotifications = function checkNotifications (sbx) {

    var prop = sbx.properties.bwp;
    if (prop === undefined) { return; }

    var settings = prepareSettings(sbx);

    if (bwp.highSnoozedByIOB(prop, settings, sbx)) {
      sbx.notifications.requestSnooze({
        level: levels.URGENT
        , title: 'Snoozing high alarm since there is enough IOB'
        , message: [sbx.propertyLine('bwp'), sbx.propertyLine('iob')].join('\n')
        , lengthMills: settings.snoozeLength
        , debug: prop
      });
    } else if (prop.scaledSGV > sbx.data.profile.getHighBGTarget(sbx.time) && prop.bolusEstimate > settings.warnBWP) {
      var level = prop.bolusEstimate > settings.urgentBWP ? levels.URGENT : levels.WARN;
      var levelLabel = levels.toDisplay(level);
      var sound = level === levels.URGENT ? 'updown' : 'bike';

      sbx.notifications.requestNotify({
        level: level
        , title: levelLabel + ', Check BG, time to bolus?'
        , message: sbx.buildDefaultMessage()
        , eventName: 'bwp'
        , pushoverSound: sound
        , plugin: bwp
        , debug: prop
      });
    }
  };

  bwp.highSnoozedByIOB = function highSnoozedByIOB(prop, settings, sbx) {
    var ar2EventType = sbx.properties.ar2 && sbx.properties.ar2.eventType;
    var high = ar2EventType === 'high' || prop.scaledSGV >= sbx.scaleMgdl(sbx.thresholds.bg_target_top);

    return high && prop.bolusEstimate < settings.snoozeBWP;
  };

  bwp.updateVisualisation = function updateVisualisation (sbx) {

    var prop = sbx.properties.bwp;

    var info = [];
    pushInfo(prop, info, sbx);

    var value;
    if (prop && prop.bolusEstimateDisplay >= 0) {
      value = prop.bolusEstimateDisplay + 'U';
    } else if (prop && prop.bolusEstimateDisplay < 0) {
      value = '< 0U';
    }

    sbx.pluginBase.updatePillText(bwp, {
      value: value
      , label: 'BWP'
      , info: info
    });
  };

  bwp.calc = function calc (sbx) {

    var results = {
      effect: 0
      , outcome: 0
      , bolusEstimate: 0.0
    };

    var scaled = sbx.lastScaledSGV();

    results.scaledSGV = scaled;

    var errors = checkMissingInfo(sbx);

    if (errors && errors.length > 0) {
      results.errors = errors;
      return results;
    }

    var profile = sbx.data.profile;
    var iob = results.iob = sbx.properties.iob.iob;

    results.effect = iob * profile.getSensitivity(sbx.time);
    results.outcome = scaled - results.effect;
    var delta = 0;

    var target_high = profile.getHighBGTarget(sbx.time);
    var sens = profile.getSensitivity(sbx.time);

    if (results.outcome > target_high) {
      delta = results.outcome - target_high;
      results.bolusEstimate = delta / sens;
    }

    var target_low = profile.getLowBGTarget(sbx.time);

    if (results.outcome < target_low) {
      delta = Math.abs(results.outcome - target_low);
      results.bolusEstimate = delta / sens * -1;
    }

    if (results.bolusEstimate !== 0 && sbx.data.profile.getBasal(sbx.time)) {
      // Basal profile exists, calculate % change
      var basal = sbx.data.profile.getBasal(sbx.time);

      var thirtyMinAdjustment = Math.round((basal/2 + results.bolusEstimate) / (basal / 2) * 100);
      var oneHourAdjustment = Math.round((basal + results.bolusEstimate) / basal * 100);

      results.tempBasalAdjustment = {
        'thirtymin': thirtyMinAdjustment
        ,'onehour': oneHourAdjustment};
    }

    results.bolusEstimateDisplay = sbx.roundInsulinForDisplayFormat(results.bolusEstimate);
    results.outcomeDisplay = sbx.roundBGToDisplayFormat(results.outcome);
    results.displayIOB = sbx.roundInsulinForDisplayFormat(results.iob);
    results.effectDisplay = sbx.roundBGToDisplayFormat(results.effect);
    results.displayLine = 'BWP: ' + results.bolusEstimateDisplay + 'U';

    return results;
  };

  function prepareSettings (sbx) {
    return {
      snoozeBWP: Number(sbx.extendedSettings.snooze) || 0.10
    , warnBWP: Number(sbx.extendedSettings.warn) || 0.50
    , urgentBWP: Number(sbx.extendedSettings.urgent) || 1.00
    , snoozeLength: (sbx.extendedSettings.snoozeMins && Number(sbx.extendedSettings.snoozeMins) * 60 * 1000) || TIME_10_MINS
    };
  }

  return bwp();

}

function isSGVOk (limit, sbx) {
  var lastSGVEntry = sbx.lastSGVEntry();
  return lastSGVEntry && lastSGVEntry.mgdl >= 39 && sbx.time - lastSGVEntry.mills < limit;
}

function profileFieldsMissing (sbx) {
  return !sbx.data.profile.getSensitivity(sbx.time)
    || !sbx.data.profile.getHighBGTarget(sbx.time)
    || !sbx.data.profile.getLowBGTarget(sbx.time);
}

function pushInfo(prop, info, sbx) {
  if (prop && prop.errors) {
    info.push({label: 'Notice', value: 'required info missing'});
    _.forEach(prop.errors, function pushError (error) {
      info.push({label: '  • ', value: error});
    });
  } else if (prop) {
    info.push({label: 'Insulin on Board', value: prop.displayIOB + 'U'});
    info.push({label: 'Sensitivity', value: '-' + sbx.data.profile.getSensitivity(sbx.time) + ' ' + sbx.units + '/U'});
    info.push({label: 'Expected effect', value: prop.displayIOB + ' x -' + sbx.data.profile.getSensitivity(sbx.time) + ' = -' + prop.effectDisplay + ' ' + sbx.units});
    info.push({label: 'Expected outcome', value: sbx.lastScaledSGV() + '-' + prop.effectDisplay + ' = ' + prop.outcomeDisplay + ' ' + sbx.units});
    if (prop.bolusEstimateDisplay < 0) {
      info.unshift({label: '---------', value: ''});
      var carbEquivalent = Math.ceil(Math.abs(sbx.data.profile.getCarbRatio() * prop.bolusEstimateDisplay));
      info.unshift({label: 'Carb Equivalent', value: prop.bolusEstimateDisplay + 'U * ' + sbx.data.profile.getCarbRatio() + ' = ' + carbEquivalent + ' grams'});
      info.unshift({label: 'Current Carb Ratio', value: '1U / ' + sbx.data.profile.getCarbRatio() + ' g'});
      info.unshift({label: '-BWP', value: prop.bolusEstimateDisplay + 'U, maybe covered by carbs?'});
    }
  } else {
    info.push({label: 'Notice', value: 'required info missing'});
  }

  pushTempBasalAdjustments(prop, info, sbx);
}

function pushTempBasalAdjustments(prop, info, sbx) {
  if (prop && prop.tempBasalAdjustment) {
    var carbsOrBolusMessage = 'too large adjustment needed, give carbs?';
    var sign = '';
    if (prop.tempBasalAdjustment.thirtymin > 100) {
      carbsOrBolusMessage = 'too large adjustment needed, give bolus?';
      sign = '+';
    }

    info.push({label: '---------', value: ''});
    info.push({label: 'Current basal', value: sbx.data.profile.getBasal(sbx.time)});
    info.push({label: 'Basal change to account for ' + prop.bolusEstimateDisplay + ':', value: ''});

    if (prop.tempBasalAdjustment.thirtymin > 0 && prop.tempBasalAdjustment.thirtymin < 200) {
      info.push({label: '30m temp basal', value: '' + prop.tempBasalAdjustment.thirtymin + '% (' + sign + (prop.tempBasalAdjustment.thirtymin - 100) + '%)'});
    } else {
      info.push({label: '30m temp basal', value: carbsOrBolusMessage});
    }
    if (prop.tempBasalAdjustment.onehour > 0 && prop.tempBasalAdjustment.onehour < 200) {
      info.push({label: '1h temp basal', value: '' + prop.tempBasalAdjustment.onehour + '% (' + sign + (prop.tempBasalAdjustment.onehour - 100) + '%)'});
    } else {
      info.push({label: '1h temp basal', value: carbsOrBolusMessage});
    }
  }
}

module.exports = init;<commit_msg>grams -> g for consistency<commit_after>'use strict';

var levels = require('../levels');

var TIME_10_MINS = 10 * 60 * 1000;
var TIME_15_MINS = 15 * 60 * 1000;

function init() {

  function bwp() {
    return bwp;
  }

  bwp.label = 'Bolus Wizard Preview';
  bwp.pluginType = 'pill-minor';

  function checkMissingInfo (sbx) {
    var errors = [];
    if (!sbx.data.profile || !sbx.data.profile.hasData()) {
      errors.push('Missing need a treatment profile');
    } else if (profileFieldsMissing(sbx)) {
      errors.push('Missing sens, target_high, or target_low treatment profile fields');
    }

    if (!sbx.properties.iob) {
      errors.push('Missing IOB property');
    }

    if (!isSGVOk(TIME_15_MINS, sbx)) {
      errors.push('Data isn\'t current');
    }

    return errors;
  }

  bwp.setProperties = function setProperties(sbx) {
    sbx.offerProperty('bwp', function setBWP ( ) {
      return bwp.calc(sbx);
    });
  };

  bwp.checkNotifications = function checkNotifications (sbx) {

    var prop = sbx.properties.bwp;
    if (prop === undefined) { return; }

    var settings = prepareSettings(sbx);

    if (bwp.highSnoozedByIOB(prop, settings, sbx)) {
      sbx.notifications.requestSnooze({
        level: levels.URGENT
        , title: 'Snoozing high alarm since there is enough IOB'
        , message: [sbx.propertyLine('bwp'), sbx.propertyLine('iob')].join('\n')
        , lengthMills: settings.snoozeLength
        , debug: prop
      });
    } else if (prop.scaledSGV > sbx.data.profile.getHighBGTarget(sbx.time) && prop.bolusEstimate > settings.warnBWP) {
      var level = prop.bolusEstimate > settings.urgentBWP ? levels.URGENT : levels.WARN;
      var levelLabel = levels.toDisplay(level);
      var sound = level === levels.URGENT ? 'updown' : 'bike';

      sbx.notifications.requestNotify({
        level: level
        , title: levelLabel + ', Check BG, time to bolus?'
        , message: sbx.buildDefaultMessage()
        , eventName: 'bwp'
        , pushoverSound: sound
        , plugin: bwp
        , debug: prop
      });
    }
  };

  bwp.highSnoozedByIOB = function highSnoozedByIOB(prop, settings, sbx) {
    var ar2EventType = sbx.properties.ar2 && sbx.properties.ar2.eventType;
    var high = ar2EventType === 'high' || prop.scaledSGV >= sbx.scaleMgdl(sbx.thresholds.bg_target_top);

    return high && prop.bolusEstimate < settings.snoozeBWP;
  };

  bwp.updateVisualisation = function updateVisualisation (sbx) {

    var prop = sbx.properties.bwp;

    var info = [];
    pushInfo(prop, info, sbx);

    var value;
    if (prop && prop.bolusEstimateDisplay >= 0) {
      value = prop.bolusEstimateDisplay + 'U';
    } else if (prop && prop.bolusEstimateDisplay < 0) {
      value = '< 0U';
    }

    sbx.pluginBase.updatePillText(bwp, {
      value: value
      , label: 'BWP'
      , info: info
    });
  };

  bwp.calc = function calc (sbx) {

    var results = {
      effect: 0
      , outcome: 0
      , bolusEstimate: 0.0
    };

    var scaled = sbx.lastScaledSGV();

    results.scaledSGV = scaled;

    var errors = checkMissingInfo(sbx);

    if (errors && errors.length > 0) {
      results.errors = errors;
      return results;
    }

    var profile = sbx.data.profile;
    var iob = results.iob = sbx.properties.iob.iob;

    results.effect = iob * profile.getSensitivity(sbx.time);
    results.outcome = scaled - results.effect;
    var delta = 0;

    var target_high = profile.getHighBGTarget(sbx.time);
    var sens = profile.getSensitivity(sbx.time);

    if (results.outcome > target_high) {
      delta = results.outcome - target_high;
      results.bolusEstimate = delta / sens;
    }

    var target_low = profile.getLowBGTarget(sbx.time);

    if (results.outcome < target_low) {
      delta = Math.abs(results.outcome - target_low);
      results.bolusEstimate = delta / sens * -1;
    }

    if (results.bolusEstimate !== 0 && sbx.data.profile.getBasal(sbx.time)) {
      // Basal profile exists, calculate % change
      var basal = sbx.data.profile.getBasal(sbx.time);

      var thirtyMinAdjustment = Math.round((basal/2 + results.bolusEstimate) / (basal / 2) * 100);
      var oneHourAdjustment = Math.round((basal + results.bolusEstimate) / basal * 100);

      results.tempBasalAdjustment = {
        'thirtymin': thirtyMinAdjustment
        ,'onehour': oneHourAdjustment};
    }

    results.bolusEstimateDisplay = sbx.roundInsulinForDisplayFormat(results.bolusEstimate);
    results.outcomeDisplay = sbx.roundBGToDisplayFormat(results.outcome);
    results.displayIOB = sbx.roundInsulinForDisplayFormat(results.iob);
    results.effectDisplay = sbx.roundBGToDisplayFormat(results.effect);
    results.displayLine = 'BWP: ' + results.bolusEstimateDisplay + 'U';

    return results;
  };

  function prepareSettings (sbx) {
    return {
      snoozeBWP: Number(sbx.extendedSettings.snooze) || 0.10
    , warnBWP: Number(sbx.extendedSettings.warn) || 0.50
    , urgentBWP: Number(sbx.extendedSettings.urgent) || 1.00
    , snoozeLength: (sbx.extendedSettings.snoozeMins && Number(sbx.extendedSettings.snoozeMins) * 60 * 1000) || TIME_10_MINS
    };
  }

  return bwp();

}

function isSGVOk (limit, sbx) {
  var lastSGVEntry = sbx.lastSGVEntry();
  return lastSGVEntry && lastSGVEntry.mgdl >= 39 && sbx.time - lastSGVEntry.mills < limit;
}

function profileFieldsMissing (sbx) {
  return !sbx.data.profile.getSensitivity(sbx.time)
    || !sbx.data.profile.getHighBGTarget(sbx.time)
    || !sbx.data.profile.getLowBGTarget(sbx.time);
}

function pushInfo(prop, info, sbx) {
  if (prop && prop.errors) {
    info.push({label: 'Notice', value: 'required info missing'});
    _.forEach(prop.errors, function pushError (error) {
      info.push({label: '  • ', value: error});
    });
  } else if (prop) {
    info.push({label: 'Insulin on Board', value: prop.displayIOB + 'U'});
    info.push({label: 'Sensitivity', value: '-' + sbx.data.profile.getSensitivity(sbx.time) + ' ' + sbx.units + '/U'});
    info.push({label: 'Expected effect', value: prop.displayIOB + ' x -' + sbx.data.profile.getSensitivity(sbx.time) + ' = -' + prop.effectDisplay + ' ' + sbx.units});
    info.push({label: 'Expected outcome', value: sbx.lastScaledSGV() + '-' + prop.effectDisplay + ' = ' + prop.outcomeDisplay + ' ' + sbx.units});
    if (prop.bolusEstimateDisplay < 0) {
      info.unshift({label: '---------', value: ''});
      var carbEquivalent = Math.ceil(Math.abs(sbx.data.profile.getCarbRatio() * prop.bolusEstimateDisplay));
      info.unshift({label: 'Carb Equivalent', value: prop.bolusEstimateDisplay + 'U * ' + sbx.data.profile.getCarbRatio() + ' = ' + carbEquivalent + 'g'});
      info.unshift({label: 'Current Carb Ratio', value: '1U / ' + sbx.data.profile.getCarbRatio() + ' g'});
      info.unshift({label: '-BWP', value: prop.bolusEstimateDisplay + 'U, maybe covered by carbs?'});
    }
  } else {
    info.push({label: 'Notice', value: 'required info missing'});
  }

  pushTempBasalAdjustments(prop, info, sbx);
}

function pushTempBasalAdjustments(prop, info, sbx) {
  if (prop && prop.tempBasalAdjustment) {
    var carbsOrBolusMessage = 'too large adjustment needed, give carbs?';
    var sign = '';
    if (prop.tempBasalAdjustment.thirtymin > 100) {
      carbsOrBolusMessage = 'too large adjustment needed, give bolus?';
      sign = '+';
    }

    info.push({label: '---------', value: ''});
    info.push({label: 'Current basal', value: sbx.data.profile.getBasal(sbx.time)});
    info.push({label: 'Basal change to account for ' + prop.bolusEstimateDisplay + ':', value: ''});

    if (prop.tempBasalAdjustment.thirtymin > 0 && prop.tempBasalAdjustment.thirtymin < 200) {
      info.push({label: '30m temp basal', value: '' + prop.tempBasalAdjustment.thirtymin + '% (' + sign + (prop.tempBasalAdjustment.thirtymin - 100) + '%)'});
    } else {
      info.push({label: '30m temp basal', value: carbsOrBolusMessage});
    }
    if (prop.tempBasalAdjustment.onehour > 0 && prop.tempBasalAdjustment.onehour < 200) {
      info.push({label: '1h temp basal', value: '' + prop.tempBasalAdjustment.onehour + '% (' + sign + (prop.tempBasalAdjustment.onehour - 100) + '%)'});
    } else {
      info.push({label: '1h temp basal', value: carbsOrBolusMessage});
    }
  }
}

module.exports = init;<|endoftext|>


<commit_before>// stderr/index.js - handles all STDERR output from spawned service
// Remark: STDERR is currently overloaded to support JSON messages
// Overloading STDERR might not be the best design choice, but it does works and is leaves STDIO 3 and 4 open for future usage

var responseMethods = require('./responseMethods');
var stderr = {};
module['exports'] = stderr;

// processes incoming stderr buffer
stderr.onData = function onStderrData (data, status, debug, output, input) {
  var messages = data.toString();

  // Remark: Ignore special case"\nmodule.js:333", which is module require error
  //         This is a bit brittle, but is okay for now
  if (messages.substr(0, 1) !== "{" && messages.substr(0, 14) !== "\nmodule.js:333") {
    // Remark: Encode any non JSON messages as a JSON error message
    var message = { "type": "error", "payload": { "error": messages }};
    return handleMessage(message, status, debug, output, input);
  }
  messages = messages.split('\n');
  messages.forEach(function(message){
    if (message.length === 0) {
      return;
    }
    // attempt to parse incoming stderr as JSON message
    try {
      message = JSON.parse(message.toString());
    } catch (err) {
      // don't do anything, ignore
      // message = { "type": "error", "payload": { "error": message.toString() }};
    }
    handleMessage(message, status, debug, output, input);
  });
};

var handleMessage = stderr.handleMessage = function (message, status, debug, output, input) {

  var request = require('request');

  /*
    stderr message types:

    error: error event from vm, send error stack as plaintext to client.
    log: console.log logging event, send log entry to logging system
    end: hook.res.end was called inside the vm, call output.end()
    untyped: any untyped messages are considered type `error` and will be wrapped as error types

  */
  // check to see if incoming message is a response method ( like res.writeHead )
  if  (typeof responseMethods[message.type] === "function") {
    responseMethods[message.type](message, output);
    return;
  }

  // if the incoming message is end event, signal that is time to end the response
  if (message.type === "end") {
    status.serviceEnded = true;
  }

  // send logging messages to the debug function ( redis logs at this point )
  if (message.type === "log") {
    debug(message.payload.entry);
    return;
  }

  /*
    TODO: implement req param setters for middlewares
    if (message.type === "setvar") {
      console.log('calling setvar...', message.payload)
      input[message.payload.key] = message.payload.value;
      return;
    }
  */

  // if the incoming message is an error
  if (message.type === "error") {
    status.erroring = true;

    // we don't know what happened at this point, or how much more error information is coming
    // let's just set a timer to end the request after a few moments
    // this ensures that most ( if not the entire ) error stack gets sent to the client
    if(!status.ended && output) {
      // wait 200 ms to account for any errors to flush
      output.write(message.payload.error);
      status.serviceEnded = true;
      /*
      Removed with new middleware API
      Note: Should this be added back?
      // status.ended = true;
      setTimeout(function(){
        output.end();
      }, 200)
      */
    }
  }
}<commit_msg>[api] [minor] Set 500 status code on error message   * Better API semantic for users   * Adds better code comments<commit_after>// stderr/index.js - handles all STDERR output from spawned service
// Remark: STDERR is currently overloaded to support JSON messages
// Overloading STDERR might not be the best design choice, but it does works and is leaves STDIO 3 and 4 open for future usage

var responseMethods = require('./responseMethods');
var stderr = {};
module['exports'] = stderr;

// processes incoming stderr buffer
stderr.onData = function onStderrData (data, status, debug, output, input) {
  var messages = data.toString();

  // Remark: Ignore special case"\nmodule.js:333", which is module require error
  //         This is a bit brittle, but is okay for now
  if (messages.substr(0, 1) !== "{" && messages.substr(0, 14) !== "\nmodule.js:333") {
    // Remark: Encode any non JSON messages as a JSON error message
    var message = { "type": "error", "payload": { "error": messages }};
    return handleMessage(message, status, debug, output, input);
  }
  messages = messages.split('\n');
  messages.forEach(function(message){
    if (message.length === 0) {
      return;
    }
    // attempt to parse incoming stderr as JSON message
    try {
      message = JSON.parse(message.toString());
    } catch (err) {
      // don't do anything, ignore
      // message = { "type": "error", "payload": { "error": message.toString() }};
    }
    handleMessage(message, status, debug, output, input);
  });
};

var handleMessage = stderr.handleMessage = function (message, status, debug, output, input) {

  var request = require('request');

  /*
    stderr message types:

    error: error event from vm, send error stack as plaintext to client.
    log: console.log logging event, send log entry to logging system
    end: hook.res.end was called inside the vm, call output.end()
    untyped: any untyped messages are considered type `error` and will be wrapped as error types

  */
  // check to see if incoming message is a response method ( like res.writeHead )
  if  (typeof responseMethods[message.type] === "function") {
    responseMethods[message.type](message, output);
    return;
  }

  // if the incoming message is end event, signal that is time to end the response
  if (message.type === "end") {
    status.serviceEnded = true;
  }

  // send logging messages to the debug function ( redis logs at this point )
  if (message.type === "log") {
    debug(message.payload.entry);
    return;
  }

  /*
    TODO: implement req param setters for middlewares
    if (message.type === "setvar") {
      console.log('calling setvar...', message.payload)
      input[message.payload.key] = message.payload.value;
      return;
    }
  */

  // if the incoming message is an error
  if (message.type === "error") {
    status.erroring = true;
    if (!status.ended && output) {
      // could this 500 header try to be written twice? potential double header writing? needs more testing
      output.writeHead(500);

      // write the error message to the reponse
      output.write(message.payload.error);
      status.serviceEnded = true;

      /*
      Removed with new middleware API
      Note: Should this be added back?
      // we don't know what happened at this point, or how much more error information is coming
      // let's just set a timer to end the request after a few moments
      // this ensures that most ( if not the entire ) error stack gets sent to the client
      // status.ended = true;
      // wait 200 ms to account for any errors to flush
      setTimeout(function(){
        output.end();
      }, 200)
      */
    }
  }
}<|endoftext|>