<commit_before>var bson = require('bson')
var braveHapi = require('../brave-hapi')
var currencyCodes = require('currency-codes')
var dateformat = require('dateformat')
var json2csv = require('json2csv')
const moment = require('moment')
var underscore = require('underscore')

var currency = currencyCodes.code('USD')
if (!currency) currency = { digits: 2 }

var datefmt = 'yyyy-mm-dd HH:MM:ss'

var create = async function (runtime, prefix, params) {
  var extension, filename, options

  if (params.format !== 'csv') {
    options = { content_type: 'application/json' }
    extension = '.json'
  } else {
    options = { content_type: 'text/csv' }
    extension = '.csv'
  }
  filename = prefix + dateformat(underscore.now(), datefmt) + extension
  options.metadata = { 'content-disposition': 'attachment; filename="' + filename + '"' }
  return await runtime.db.file(params.reportId, 'w', options)
}

var daily = async function (debug, runtime) {
  var midnight, tomorrow
  var now = underscore.now()

  debug('daily', 'running')

  midnight = new Date(now)
  midnight.setHours(0, 0, 0, 0)
  midnight = Math.floor(midnight.getTime() / 1000)

  try {
    await runtime.db.purgeSince(debug, runtime, midnight * 1000)
  } catch (ex) {
    debug('daily', ex)
  }
  tomorrow = new Date(now)
  tomorrow.setHours(24, 0, 0, 0)
  setTimeout(function () { daily(debug, runtime) }, tomorrow - now)
  debug('daily', 'running again ' + moment(tomorrow).fromNow())
}

var quanta = async function (debug, runtime) {
  var i, results, votes
  var contributions = runtime.db.get('contributions', debug)
  var voting = runtime.db.get('voting', debug)

  var dicer = async function (quantum, counts) {
    var params, state, updateP, vote
    var surveyors = runtime.db.get('surveyors', debug)
    var surveyor = await surveyors.findOne({ surveyorId: quantum._id })

    if (!surveyor) return debug('missing surveyor.surveyorId', { surveyorId: quantum._id })

    quantum.created = new Date(parseInt(surveyor._id.toHexString().substring(0, 8), 16) * 1000).getTime()
    quantum.modified = (surveyor.timestamp.high_ * 1000) + (surveyor.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)

    vote = underscore.find(votes, (entry) => { return (quantum._id === entry._id) })
    underscore.extend(quantum, { counts: vote ? vote.counts : 0 })

    params = underscore.pick(quantum, [ 'counts', 'inputs', 'fee', 'quantum' ])
    updateP = false
    underscore.keys(params).forEach((key) => { if (params[key] !== surveyor[key]) updateP = true })
    if (!updateP) return

    state = { $currentDate: { timestamp: { $type: 'timestamp' } }, $set: params }
    await surveyors.update({ surveyorId: quantum._id }, state, { upsert: true })

    surveyor = await surveyors.findOne({ surveyorId: quantum._id })
    if (surveyor) {
      quantum.modified = (surveyor.timestamp.high_ * 1000) + (surveyor.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)
    }
  }

  results = await contributions.aggregate([
    { $match: { satoshis: { $gt: 0 } } },
    { $group:
      { _id: '$surveyorId',
        satoshis: { $sum: '$satoshis' },
        fee: { $sum: '$fee' },
        inputs: { $sum: { $subtract: [ '$satoshis', '$fee' ] } },
        votes: { $sum: '$votes' }
      }
    },
    { $project:
      { _id: 1,
        satoshis: 1,
        fee: 1,
        inputs: 1,
        votes: 1,
        quantum: { $divide: [ '$inputs', '$votes' ] }
      }
    }
  ])
  votes = await voting.aggregate([
      { $match:
        { counts: { $gt: 0 },
          exclude: false
        }
      },
      { $group:
        { _id: '$surveyorId',
          counts: { $sum: '$counts' }
        }
      },
      { $project:
        { _id: 1,
          counts: 1
        }
      }
  ])

  for (i = 0; i < results.length; i++) await dicer(results[i])

  return (underscore.map(results, function (result) {
    return underscore.extend({ surveyorId: result._id }, underscore.omit(result, [ '_id' ]))
  }))
}

var exports = {}

exports.initialize = async function (debug, runtime) {
  if ((typeof process.env.DYNO === 'undefined') || (process.env.DYNO === 'worker.1')) {
    setTimeout(function () { daily(debug, runtime) }, 5 * 1000)
  }
}

exports.workers = {
/* sent by GET /v1/reports/publishers

    { queue            : 'report-publishers-contributions'
    , message          :
      { reportId       : '...'
      , reportURL      : '...'
      , format         : 'json' | 'csv'
      , summary        :  true  | false
      }
    }
 */
  'report-publishers-contributions':
    async function (debug, runtime, payload) {
      var data, fees, file, i, publishers, results, satoshis, usd
      var format = payload.format || 'csv'
      var summaryP = payload.summary

      var slicer = async function (quantum) {
        var fees, i, satoshis, slice, state
        var voting = runtime.db.get('voting', debug)
        var slices = await voting.find({ surveyorId: quantum.surveyorId, exclude: false })

        for (i = 0; i < slices.length; i++) {
          slice = slices[i]

          satoshis = Math.floor(quantum.quantum * slice.counts * 0.95)
          fees = Math.floor((quantum.quantum * slice.counts) - satoshis)
          if (!publishers[slice.publisher]) publishers[slice.publisher] = { satoshis: 0, fees: 0, votes: [] }
          publishers[slice.publisher].satoshis += satoshis
          publishers[slice.publisher].fees += fees
          publishers[slice.publisher].votes.push({ surveyorId: quantum.surveyorId,
                                                   counts: slice.counts,
                                                   satoshis: satoshis,
                                                   fees: fees
                                                 })
          if (slice.satoshis === satoshis) continue

          state = { $set: { satoshis: satoshis } }
          await voting.update({ surveyorId: quantum.surveyorId, publisher: slice.publisher }, state, { upsert: true })
        }
      }

      publishers = {}
      results = await quanta(debug, runtime)
      for (i = 0; i < results.length; i++) await slicer(results[i])

      results = []
      underscore.keys(publishers).forEach((publisher) => {
        publishers[publisher].votes = underscore.sortBy(publishers[publisher].votes, 'surveyorId')
        results.push(underscore.extend({ publisher: publisher }, publishers[publisher]))
      })
      results = underscore.sortBy(results, 'publisher')

      file = await create(runtime, 'publishers-', payload)
      if (format !== 'csv') {
        await file.write(JSON.stringify(results, null, 2), true)
        return runtime.notify(debug, { text: 'report-publishers-contributions completed' })
      }

      usd = runtime.wallet.rates.USD
      usd = (Number.isFinite(usd)) ? (usd / 1e8) : null
      satoshis = 0
      fees = 0

      data = []
      results.forEach((result) => {
        satoshis += result.satoshis
        fees += result.fees
        data.push({ publisher: result.publisher,
                    total: result.satoshis,
                    fees: result.fees,
                    'publisher USD': (result.satoshis * usd).toFixed(currency.digits),
                    'processor USD': (result.fees * usd).toFixed(currency.digits)
                  })
        if (!summaryP) result.votes.forEach((vote) => { data.push(underscore.extend({ publisher: result.publisher }, vote)) })
      })
      data.push({ publisher: 'TOTAL',
                  total: satoshis,
                  fees: fees,
                  'publisher USD': (satoshis * usd).toFixed(currency.digits),
                  'processor USD': (fees * usd).toFixed(currency.digits)
                })

      await file.write(json2csv({ data: data }), true)
      runtime.notify(debug, { text: 'report-publishers-contributions completed' })
    },

/* sent by GET /v1/reports/publishers/status

    { queue            : 'report-publishers-status'
    , message          :
      { reportId       : '...'
      , reportURL      : '...'
      , format         : 'json' | 'csv'
      , elide          :  true  | false
      , summary        :  true  | false
      }
    }
 */
  'report-publishers-status':
    async function (debug, runtime, payload) {
      var data, entries, f, fields, file, i, keys, results, satoshis, summary, usd
      var format = payload.format || 'csv'
      var elideP = payload.elide
      var summaryP = payload.summary
      var publishers = runtime.db.get('publishers', debug)
      var tokens = runtime.db.get('tokens', debug)
      var voting = runtime.db.get('voting', debug)

      results = {}
      entries = await tokens.find()
      entries.forEach((entry) => {
        var publisher

        publisher = entry.publisher
        if (!publisher) return

        if (!results[publisher]) results[publisher] = underscore.pick(entry, [ 'publisher', 'verified' ])
        if (entry.verified) {
          underscore.extend(results[publisher], underscore.pick(entry, [ 'verified', 'verificationId', 'reason' ]))
        }
        if (summaryP) return

        if (!results[publisher].history) results[publisher].history = []
        entry.modified = (entry.timestamp.high_ * 1000) + (entry.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)
        results[publisher].history.push(underscore.pick(entry, [ 'verificationId', 'verified', 'reason', 'modified' ]))
      })

      summary = await voting.aggregate([
        { $match:
          { satoshis: { $gt: 0 },
          exclude: false
          }
        },
        { $group:
          { _id: '$publisher',
            satoshis: { $sum: '$satoshis' }
          }
        }
      ])
      satoshis = {}
      summary.forEach(function (entry) { satoshis[entry._id] = entry.satoshis })
      usd = runtime.wallet.rates.USD

      f = async function (publisher) {
        var datum, result

        results[publisher].satoshis = satoshis[publisher] || 0
        if (usd) results[publisher].USD = ((results[publisher].satoshis * usd) / 1e8).toFixed(currency.digits)

        if (results[publisher].history) {
          results[publisher].history = underscore.sortBy(results[publisher].history, 'modified')
          if (!results[publisher].verified) results[publisher].reason = underscore.last(results[publisher].history).reason
        }

        datum = await publishers.findOne({ publisher: publisher })
        if (!datum) return data.push(results[publisher])

        datum.created = new Date(parseInt(datum._id.toHexString().substring(0, 8), 16) * 1000).getTime()
        datum.modified = (datum.timestamp.high_ * 1000) + (datum.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)
        underscore.extend(results[publisher], underscore.omit(datum, [ '_id', 'publisher', 'timestamp', 'verified' ]))

        try {
          result = await braveHapi.wreck.get(runtime.config.publishers.url + '/api/publishers/' + encodeURIComponent(publisher),
                                            { headers: { authorization: 'Bearer ' + runtime.config.publishers.access_token },
                                              useProxyP: true
                                            })
          if (Buffer.isBuffer(result)) result = JSON.parse(result)
if (publisher === 'ayumiyu.com')console.log(JSON.stringify(result, null, 2))
          datum = underscore.findWhere(result, function (entry) { return entry.verified })
          if (datum) {
            underscore.extend(results[publisher], underscore.pick(datum, [ 'name', 'email' ]),
                              { phone: datum.phone_normalized })
          }
          if (!summaryP) {
            results[publisher].history.forEach((entry) => {
              datum = underscore.findWhere(result, function (entry2) { return entry.verificationId === entry2.verificationId })
              if (datum) {
                underscore.extend(results[publisher].history, underscore.pick(datum, [ 'name', 'email' ]),
                                  { phone: datum.phone_normalized })
              }
            })
          }
        } catch (ex) { debug('publisher', { publisher: publisher, reason: ex.toString() }) }

        if (elideP) {
          if (results[publisher].address) results[publisher].address = 'yes'
          if (results[publisher].email) results[publisher].email = 'yes'
          if (results[publisher].phone) results[publisher].phone = 'yes'
          if (results[publisher].legalFormURL) results[publisher].legalFormURL = 'yes'
        }

        data.push(results[publisher])
      }
      data = []
      keys = underscore.keys(results)
      for (i = 0; i < keys.length; i++) await f(keys[i])
      results = underscore.sortBy(data, 'publisher')

      file = await create(runtime, 'publishers-', payload)
      if (format !== 'csv') {
        await file.write(JSON.stringify(data, null, 2), true)
        return runtime.notify(debug, { text: 'report-publishers-status completed' })
      }

      data = []
      results.forEach((result) => {
        data.push(underscore.extend(underscore.omit(result, [ 'history' ]),
                                    { created: result.created && dateformat(result.created, datefmt),
                                      modified: result.modified && dateformat(result.modified, datefmt)
                                    }))
        if (!summaryP) {
          result.history.forEach((entry) => {
            data.push(underscore.extend({ publisher: result.publisher }, entry,
                                        { modified: dateformat(entry.modified, datefmt) }))
          })
        }
      })

      fields = [ 'publisher', 'USD', 'satoshis',
                 'verified', 'authorized', 'authority',
                 'name', 'email', 'phone', 'address',
                 'verificationId', 'reason',
                 'created', 'modified',
                 'legalFormURL' ]
      await file.write(json2csv({ data: data, fields: fields }), true)
      runtime.notify(debug, { text: 'report-publishers-status completed' })
    },

/* sent by GET /v1/reports/surveyors-contributions

    { queue            : 'report-surveyors-contributions'
    , message          :
      { reportId       : '...'
      , reportURL      : '...'
      , format         : 'json' | 'csv'
      }
    }
 */
  'report-surveyors-contributions':
    async function (debug, runtime, payload) {
      var data, file
      var format = payload.format || 'csv'

      data = underscore.sortBy(await quanta(debug, runtime), 'created')

      file = await create(runtime, 'surveyors-', payload)
      if (format !== 'csv') {
        await file.write(JSON.stringify(data, null, 2), true)
        return runtime.notify(debug, { text: 'report-surveyors-contributions completed' })
      }

      data.forEach((result) => {
        underscore.extend(result,
                          { created: dateformat(result.created, datefmt), modified: dateformat(result.modified, datefmt) })
      })

      await file.write(json2csv({ data: data }), true)
      runtime.notify(debug, { text: 'report-surveyors-contributions completed' })
    }
}

module.exports = exports
<commit_msg>streamline the report<commit_after>var bson = require('bson')
var braveHapi = require('../brave-hapi')
var currencyCodes = require('currency-codes')
var dateformat = require('dateformat')
var json2csv = require('json2csv')
const moment = require('moment')
var underscore = require('underscore')

var currency = currencyCodes.code('USD')
if (!currency) currency = { digits: 2 }

var datefmt = 'yyyy-mm-dd HH:MM:ss'

var create = async function (runtime, prefix, params) {
  var extension, filename, options

  if (params.format !== 'csv') {
    options = { content_type: 'application/json' }
    extension = '.json'
  } else {
    options = { content_type: 'text/csv' }
    extension = '.csv'
  }
  filename = prefix + dateformat(underscore.now(), datefmt) + extension
  options.metadata = { 'content-disposition': 'attachment; filename="' + filename + '"' }
  return await runtime.db.file(params.reportId, 'w', options)
}

var daily = async function (debug, runtime) {
  var midnight, tomorrow
  var now = underscore.now()

  debug('daily', 'running')

  midnight = new Date(now)
  midnight.setHours(0, 0, 0, 0)
  midnight = Math.floor(midnight.getTime() / 1000)

  try {
    await runtime.db.purgeSince(debug, runtime, midnight * 1000)
  } catch (ex) {
    debug('daily', ex)
  }
  tomorrow = new Date(now)
  tomorrow.setHours(24, 0, 0, 0)
  setTimeout(function () { daily(debug, runtime) }, tomorrow - now)
  debug('daily', 'running again ' + moment(tomorrow).fromNow())
}

var quanta = async function (debug, runtime) {
  var i, results, votes
  var contributions = runtime.db.get('contributions', debug)
  var voting = runtime.db.get('voting', debug)

  var dicer = async function (quantum, counts) {
    var params, state, updateP, vote
    var surveyors = runtime.db.get('surveyors', debug)
    var surveyor = await surveyors.findOne({ surveyorId: quantum._id })

    if (!surveyor) return debug('missing surveyor.surveyorId', { surveyorId: quantum._id })

    quantum.created = new Date(parseInt(surveyor._id.toHexString().substring(0, 8), 16) * 1000).getTime()
    quantum.modified = (surveyor.timestamp.high_ * 1000) + (surveyor.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)

    vote = underscore.find(votes, (entry) => { return (quantum._id === entry._id) })
    underscore.extend(quantum, { counts: vote ? vote.counts : 0 })

    params = underscore.pick(quantum, [ 'counts', 'inputs', 'fee', 'quantum' ])
    updateP = false
    underscore.keys(params).forEach((key) => { if (params[key] !== surveyor[key]) updateP = true })
    if (!updateP) return

    state = { $currentDate: { timestamp: { $type: 'timestamp' } }, $set: params }
    await surveyors.update({ surveyorId: quantum._id }, state, { upsert: true })

    surveyor = await surveyors.findOne({ surveyorId: quantum._id })
    if (surveyor) {
      quantum.modified = (surveyor.timestamp.high_ * 1000) + (surveyor.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)
    }
  }

  results = await contributions.aggregate([
    { $match: { satoshis: { $gt: 0 } } },
    { $group:
      { _id: '$surveyorId',
        satoshis: { $sum: '$satoshis' },
        fee: { $sum: '$fee' },
        inputs: { $sum: { $subtract: [ '$satoshis', '$fee' ] } },
        votes: { $sum: '$votes' }
      }
    },
    { $project:
      { _id: 1,
        satoshis: 1,
        fee: 1,
        inputs: 1,
        votes: 1,
        quantum: { $divide: [ '$inputs', '$votes' ] }
      }
    }
  ])
  votes = await voting.aggregate([
      { $match:
        { counts: { $gt: 0 },
          exclude: false
        }
      },
      { $group:
        { _id: '$surveyorId',
          counts: { $sum: '$counts' }
        }
      },
      { $project:
        { _id: 1,
          counts: 1
        }
      }
  ])

  for (i = 0; i < results.length; i++) await dicer(results[i])

  return (underscore.map(results, function (result) {
    return underscore.extend({ surveyorId: result._id }, underscore.omit(result, [ '_id' ]))
  }))
}

var exports = {}

exports.initialize = async function (debug, runtime) {
  if ((typeof process.env.DYNO === 'undefined') || (process.env.DYNO === 'worker.1')) {
    setTimeout(function () { daily(debug, runtime) }, 5 * 1000)
  }
}

exports.workers = {
/* sent by GET /v1/reports/publishers

    { queue            : 'report-publishers-contributions'
    , message          :
      { reportId       : '...'
      , reportURL      : '...'
      , format         : 'json' | 'csv'
      , summary        :  true  | false
      }
    }
 */
  'report-publishers-contributions':
    async function (debug, runtime, payload) {
      var data, fees, file, i, publishers, results, satoshis, usd
      var format = payload.format || 'csv'
      var summaryP = payload.summary

      var slicer = async function (quantum) {
        var fees, i, satoshis, slice, state
        var voting = runtime.db.get('voting', debug)
        var slices = await voting.find({ surveyorId: quantum.surveyorId, exclude: false })

        for (i = 0; i < slices.length; i++) {
          slice = slices[i]

          satoshis = Math.floor(quantum.quantum * slice.counts * 0.95)
          fees = Math.floor((quantum.quantum * slice.counts) - satoshis)
          if (!publishers[slice.publisher]) publishers[slice.publisher] = { satoshis: 0, fees: 0, votes: [] }
          publishers[slice.publisher].satoshis += satoshis
          publishers[slice.publisher].fees += fees
          publishers[slice.publisher].votes.push({ surveyorId: quantum.surveyorId,
                                                   counts: slice.counts,
                                                   satoshis: satoshis,
                                                   fees: fees
                                                 })
          if (slice.satoshis === satoshis) continue

          state = { $set: { satoshis: satoshis } }
          await voting.update({ surveyorId: quantum.surveyorId, publisher: slice.publisher }, state, { upsert: true })
        }
      }

      publishers = {}
      results = await quanta(debug, runtime)
      for (i = 0; i < results.length; i++) await slicer(results[i])

      results = []
      underscore.keys(publishers).forEach((publisher) => {
        publishers[publisher].votes = underscore.sortBy(publishers[publisher].votes, 'surveyorId')
        results.push(underscore.extend({ publisher: publisher }, publishers[publisher]))
      })
      results = underscore.sortBy(results, 'publisher')

      file = await create(runtime, 'publishers-', payload)
      if (format !== 'csv') {
        await file.write(JSON.stringify(results, null, 2), true)
        return runtime.notify(debug, { text: 'report-publishers-contributions completed' })
      }

      usd = runtime.wallet.rates.USD
      usd = (Number.isFinite(usd)) ? (usd / 1e8) : null
      satoshis = 0
      fees = 0

      data = []
      results.forEach((result) => {
        satoshis += result.satoshis
        fees += result.fees
        data.push({ publisher: result.publisher,
                    total: result.satoshis,
                    fees: result.fees,
                    'publisher USD': (result.satoshis * usd).toFixed(currency.digits),
                    'processor USD': (result.fees * usd).toFixed(currency.digits)
                  })
        if (!summaryP) result.votes.forEach((vote) => { data.push(underscore.extend({ publisher: result.publisher }, vote)) })
      })
      data.push({ publisher: 'TOTAL',
                  total: satoshis,
                  fees: fees,
                  'publisher USD': (satoshis * usd).toFixed(currency.digits),
                  'processor USD': (fees * usd).toFixed(currency.digits)
                })

      await file.write(json2csv({ data: data }), true)
      runtime.notify(debug, { text: 'report-publishers-contributions completed' })
    },

/* sent by GET /v1/reports/publishers/status

    { queue            : 'report-publishers-status'
    , message          :
      { reportId       : '...'
      , reportURL      : '...'
      , format         : 'json' | 'csv'
      , elide          :  true  | false
      , summary        :  true  | false
      }
    }
 */
  'report-publishers-status':
    async function (debug, runtime, payload) {
      var data, entries, f, fields, file, i, keys, results, satoshis, summary, usd
      var format = payload.format || 'csv'
      var elideP = payload.elide
      var summaryP = payload.summary
      var publishers = runtime.db.get('publishers', debug)
      var tokens = runtime.db.get('tokens', debug)
      var voting = runtime.db.get('voting', debug)

      results = {}
      entries = await tokens.find()
      entries.forEach((entry) => {
        var publisher

        publisher = entry.publisher
        if (!publisher) return

        if (!results[publisher]) results[publisher] = underscore.pick(entry, [ 'publisher', 'verified' ])
        if (entry.verified) {
          underscore.extend(results[publisher], underscore.pick(entry, [ 'verified', 'verificationId', 'reason' ]))
        }
        if (summaryP) return

        if (!results[publisher].history) results[publisher].history = []
        entry.modified = (entry.timestamp.high_ * 1000) + (entry.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)
        results[publisher].history.push(underscore.pick(entry, [ 'verificationId', 'verified', 'reason', 'modified' ]))
      })

      summary = await voting.aggregate([
        { $match:
          { satoshis: { $gt: 0 },
          exclude: false
          }
        },
        { $group:
          { _id: '$publisher',
            satoshis: { $sum: '$satoshis' }
          }
        }
      ])
      satoshis = {}
      summary.forEach(function (entry) { satoshis[entry._id] = entry.satoshis })
      usd = runtime.wallet.rates.USD

      f = async function (publisher) {
        var datum, result

        results[publisher].satoshis = satoshis[publisher] || 0
        if (usd) results[publisher].USD = ((results[publisher].satoshis * usd) / 1e8).toFixed(currency.digits)

        if (results[publisher].history) {
          results[publisher].history = underscore.sortBy(results[publisher].history, 'modified')
          if (!results[publisher].verified) results[publisher].reason = underscore.last(results[publisher].history).reason
        }

        datum = await publishers.findOne({ publisher: publisher })
        if (datum) {
          datum.created = new Date(parseInt(datum._id.toHexString().substring(0, 8), 16) * 1000).getTime()
          datum.modified = (datum.timestamp.high_ * 1000) + (datum.timestamp.low_ / bson.Timestamp.TWO_PWR_32_DBL_)
          underscore.extend(results[publisher], underscore.omit(datum, [ '_id', 'publisher', 'timestamp', 'verified' ]))
        }

        try {
          result = await braveHapi.wreck.get(runtime.config.publishers.url + '/api/publishers/' + encodeURIComponent(publisher),
                                            { headers: { authorization: 'Bearer ' + runtime.config.publishers.access_token },
                                              useProxyP: true
                                            })
          if (Buffer.isBuffer(result)) result = JSON.parse(result)
          datum = underscore.findWhere(result, function (entry) { return results[publisher].verificationId === entry.id })
          if (datum) {
            underscore.extend(results[publisher], underscore.pick(datum, [ 'name', 'email' ]),
                              { phone: datum.phone_normalized })
          }
          if (!summaryP) {
            results[publisher].history.forEach((record) => {
              datum = underscore.findWhere(result, function (entry) { return record.verificationId === entry.id })
              if (datum) {
                underscore.extend(record, underscore.pick(datum, [ 'name', 'email' ]), { phone: datum.phone_normalized })

                if (elideP) {
                  if (record.address) record.address = 'yes'
                  if (record.email) record.email = 'yes'
                  if (record.phone) record.phone = 'yes'
                }
              }
            })
          }
        } catch (ex) { debug('publisher', { publisher: publisher, reason: ex.toString() }) }

        if (elideP) {
          if (results[publisher].address) results[publisher].address = 'yes'
          if (results[publisher].email) results[publisher].email = 'yes'
          if (results[publisher].phone) results[publisher].phone = 'yes'
          if (results[publisher].legalFormURL) results[publisher].legalFormURL = 'yes'
        }

        data.push(results[publisher])
      }
      data = []
      keys = underscore.keys(results)
      for (i = 0; i < keys.length; i++) await f(keys[i])
      results = underscore.sortBy(data, 'publisher')

      file = await create(runtime, 'publishers-', payload)
      if (format !== 'csv') {
        await file.write(JSON.stringify(data, null, 2), true)
        return runtime.notify(debug, { text: 'report-publishers-status completed' })
      }

      data = []
      results.forEach((result) => {
        data.push(underscore.extend(underscore.omit(result, [ 'history' ]),
                                    { created: result.created && dateformat(result.created, datefmt),
                                      modified: result.modified && dateformat(result.modified, datefmt)
                                    }))
        if (!summaryP) {
          result.history.forEach((record) => {
            data.push(underscore.extend({ publisher: result.publisher }, record,
                                        { modified: dateformat(record.modified, datefmt) }))
          })
        }
      })

      fields = [ 'publisher', 'USD', 'satoshis',
                 'verified', 'authorized', 'authority',
                 'name', 'email', 'phone', 'address',
                 'verificationId', 'reason',
                 'created', 'modified',
                 'legalFormURL' ]
      await file.write(json2csv({ data: data, fields: fields }), true)
      runtime.notify(debug, { text: 'report-publishers-status completed' })
    },

/* sent by GET /v1/reports/surveyors-contributions

    { queue            : 'report-surveyors-contributions'
    , message          :
      { reportId       : '...'
      , reportURL      : '...'
      , format         : 'json' | 'csv'
      }
    }
 */
  'report-surveyors-contributions':
    async function (debug, runtime, payload) {
      var data, file
      var format = payload.format || 'csv'

      data = underscore.sortBy(await quanta(debug, runtime), 'created')

      file = await create(runtime, 'surveyors-', payload)
      if (format !== 'csv') {
        await file.write(JSON.stringify(data, null, 2), true)
        return runtime.notify(debug, { text: 'report-surveyors-contributions completed' })
      }

      data.forEach((result) => {
        underscore.extend(result,
                          { created: dateformat(result.created, datefmt), modified: dateformat(result.modified, datefmt) })
      })

      await file.write(json2csv({ data: data }), true)
      runtime.notify(debug, { text: 'report-surveyors-contributions completed' })
    }
}

module.exports = exports
<|endoftext|>


<commit_before>var bitwise = require('./bitwise.js')

// Use Hilbert curve point generation to map 2D data to 1D space and vice-versa.
// Will later expand to allow `n` dimensions.

function Point(x, y, z) {
    this.rotations = {
        x: 0,
        y: 0,
        z: 0
    }

    this.init = function (x, y, z) {
        this.x = Math.round(x) || 0
        this.y = Math.round(y) || 0
        if (z != null) {
            this.z = Math.round(z) || 0
            this.d = 3
            this.n = 4 * this.z + 2 * this.y  + this.x
        } else {
            this.z = null
            this.d = 2
        }
    }
    if (x instanceof Array) {
        this.init(x[0], x[1], x[2])
    } else {
        this.init(x, y, z)
    }
}

Point.prototype.rotate = function (p, n) { // :: Point -> Int -> Point
    // record rotations
    if (p.n == 0) return new Point(this.z, this.x, this.y)
    if (p.n == 1 || p.n == 3) return new Point(this.y, this.z, this.x)
    if (p.n == 2 || p.n == 6) return new Point(n - this.x, n - this.y, this.z)
    return new Point(n - this.z, this.x, n - this.y)
}

Point.prototype.rotate2d = function (n, xbit, ybit) { // : Int -> Int -> Int -> Point
    return new Point(rotate2d(n, this.x, this.y, xbit, ybit))
}

Point.prototype.rotate3d = function (level) { // :: Int -> Point
    return new Point(rotate3d(level, this.x, this.y, this.z))
}

Point.prototype.toArray = function () { // :: -> [Int, Int]
        if (this.d == 3) { return [this.x, this.y, this.z] }
        return [this.x, this.y]
}

Point.prototype.mod = function (n) { // :: Int -> Point
    return new Point(this.x % n, this.y % n, this.z % n)
}

// Accepts the height or width of a square/graph, and the coordinates to
// convert.
function convert2dPointToDistance (p, height) { // :: Int -> Int -> Int -> Int
    var xbit, ybit, level, d = 0
    var forHeight = p.x > p.y ? p.x : p.y

    // needs some tests to make sure height is compatible
    // What keeps the user from putting 54 down as the height
    while (forHeight >= height) {
            height *=2
    }
    // For each Hilbert level, we want to add an amount to
    // `d` based on which region we are in
    for (level = height / 2; level > 0; level = Math.floor(level / 2)) {
        // Determine what region we're in
        xbit = (p.x & level) > 0
        ybit = (p.y & level) > 0
        // increase distance based on region
        d += level * level * ((3 * xbit) ^ ybit)
        // rotate so that we'll be in sync with the next
        // region.
        p = p.rotate2d(level, xbit, ybit)
        // HEAD
        // p = p.rotate2d(height, xbit, ybit)
    }

    return d
}

// height and coordinates.
function convert3dPointToDistance (p, height) { // :: Int -> Int -> Int -> Int -> Int
    var s = 1, level = 0
    var max = Math.max.apply(Math, p.toArray())
    for (; 2 * s <= max; s *= 2) {
        level = (level + 1) % 3
    }

    // shuffle axes
    // rotate based on parity
}

// Accepts height or width of a square/graph and distance
function convertDistanceTo2dPoint (distance, height) { // :: Int -> Int -> [Int, Int]
    distance = Math.floor(distance)
    var xbit, ybit, level, p = new Point(0, 0)

    if (height <= Math.sqrt(distance))  {
        height = 2
        while (height <= Math.sqrt(distance)) {
            height *=2
        }
    }

    for (level = 1; level < height; level *= 2) {
        xbit = 1 & (distance / 2)
        ybit = 1 & (distance ^ xbit)

        p = p.rotate2d(level, xbit, ybit)
        p.x += level * xbit
        p.y += level * ybit
        distance = Math.floor(distance / 4)
    }

    return p.toArray()
}

// height/width of a square/graph and distance
function convertDistanceTo3dPoint (distance, height) { // Int -> Int -> [Int, Int, Int]
    distance = Math.floor(distance)
    var xbit, ybit, zbit, level, parity
    var iter = 2, log = 0, p = new Point(0, 0, 0)

    height = height || 2

    // vvv this is interesting.
    for (parity = 1; parity < height; parity *= 2, ++log) {}
    parity = log % 3;

    // the `|| distance` was removed from 2d version
    for (level = 1; level < height || distance > 0; level *=2) {
        xbit = distance & 1;
        ybit = (distance / 2) & 1;
        zbit = (distance / 4) & 1;

        var temp = rotate3d(level - 1, xbit ^ ybit, ybit ^ zbit, zbit)
        p.x = temp[0] * level + level - 1
        p.y = temp[1] * level + level - 1
        p.z = temp[2] * level + level - 1

        distance = Math.floor(distance / 8)
        level *= 2;
        iter++;
    }

    return p.rotateLeft(iter - parity + 1).toArray();
}

// Rotate the coordinate plane and (x,y)
function rotate2d (n, x, y, xbit, ybit) { // :: Int -> Int -> Int -> Int -> Int -> [Int, Int]
    if (ybit == 0  ) {
        if (xbit == 1) {
            x = n - 1 - x
            y = n - 1 - y
        }

        var temp = x
        x = y
        y = temp
    }

    return [x, y]
}

// Rotate the coordinate plane and (x,y, z)
function rotate3d(level, x, y, z) { // :: Int -> Int -> Int -> Int -> [Int, Int, Int]
    var index = 4 * z + 2 * y + x
    if (index == 0) {
        return [z, x, y]
    } else if (index == 1 || index == 3) {
        return [y, z, x]
    } else if (index == 2 || index == 6) {
        return [level - x, level - y, z]
    } else if (index == 5 || index == 7) {
        return [y, level - z, level - x]
    } else {
        return [level - z, x, level - y]
    }
}

function grayCode (sequence) {
    return sequence ^ (sequence >> 1)
}

// returns a non-negative int 'inverse' such that graycode(inverse) = g
function grayInverse (g) { // : Int -> Int
    var m = precision(g), inverse = g, j = 1
    while (j < m) {
        inverse = inverse ^ (g >> j)
        j++
    }
    return inverse
}

// Returns the number of bits required to store an integer
function precision (n) { // :: Int > Int
    var ret = 0
    while (n > 0) {
        ret++
        n = n >> 1
    }
    return ret
}

function grayTransform (entry, direction, x, dim) { // :: Int -> Int -> Int -> Int
    return bitwise.rotateRight((x ^ entry), dim, 0, direction + 1)
}

function grayInverseTransform (entry, direction, x, dim) {
    return grayTransform(bitwise.rotateRight(entry, dim, 0, direction + 1), dim - direction - 1, x, dim)
}

function entrySequence (i) {
    if (i) {
        return grayCode(2 * Math.floor((i-1) / 2))
    }
    return 0
}

function directionSequence(i, dim) {
    if (i == 0) return 0
    if (i % 2 == 0) return trailingSetBits(i - 1) % dim
    return trailingSetBits(i) % dim
}

function trailingSetBits (i) {
    var ones = ~i & (i + 1)
    return Math.log(ones) / Math.log(2)
}

function hilbertIndex(point) { // :: [Int, Int, ..] -> Int
    var index = 0, entry = 0, direction = 0, code, dim = point.length,
        i = precision(Math.max.apply(null, point)) - 1

    while (i >= 0) {

        var bits = 0
        var mask = 1 << dim - 1

        for (var k = 0; k < point.length; k++) {
            if (point[dim - (k+1)] & (1 << i)) {
                bits |= mask
            }
            mask >>>= 1
        }

        bits = grayTransform(entry, direction, bits, dim)
        code = grayInverse(bits)

        entry = entry ^ bitwise.rotateLeft(entrySequence(code), dim, 0, direction + 1)
        direction = (direction + directionSequence(code, dim) + 1) % dim
        index = (index << dim) | code

        i--
    }

    return index
}

function hilbertIndexInverse(dim, index) { // :: Int -> Int -> [Int, Int, ..]
    var entry = 0, direction = 0, m = precision(index)
    var p = Array.apply(null, new Array(dim)).map(Number.prototype.valueOf,0)

    for (var i = m - 1; i >= 0; i--) {

        var mask = 1 << (i * dim), bits = 0, code

        for (var k = dim - 1; k >= 0; k--) {
            if (index & (mask << k)) {
                bits |= (1 << k)
            }
        }

        code = grayInverseTransform(entry, direction, grayCode(bits), dim)

        for (var k = 0; k < dim; k++) {
            if (code & (1 << k)) {
                p[k] |= (1 << i)
            }
        }

        entry = entry ^ bitwise.rotateLeft(entrySequence(bits), dim, 0, direction + 1)
        direction = (direction + directionSequence(bits, dim) + 1) % dim
    }

    return p
}

exports.xy2d = function (x, y, height) {
    height = height || 2
    return convert2dPointToDistance(new Point(x, y), height)
}

exports.xyz2d = function(x, y, z, height) {
    height = height || 2
    return convert3dPointToDistance(new Point(x, y, z), height)
}
exports.d2xy = convertDistanceTo2dPoint
exports.d2xyz = convertDistanceTo3dPoint
exports.hilbert = function (x, y, z, etc) {
    return hilbertIndex(Array.prototype.slice.call(arguments, 1))
}
exports.hilbertInverse = function (dim, index) {
    return hilbertIndexInverse(dim, index)
}

exports.grayInverse = grayInverse
exports.grayCode = grayCode
<commit_msg>Can specify precision and direction.<commit_after>var bitwise = require('./bitwise.js')

// Use Hilbert curve point generation to map 2D data to 1D space and vice-versa.
// Will later expand to allow `n` dimensions.

function Point(x, y, z) {
    this.rotations = {
        x: 0,
        y: 0,
        z: 0
    }

    this.init = function (x, y, z) {
        this.x = Math.round(x) || 0
        this.y = Math.round(y) || 0
        if (z != null) {
            this.z = Math.round(z) || 0
            this.d = 3
            this.n = 4 * this.z + 2 * this.y  + this.x
        } else {
            this.z = null
            this.d = 2
        }
    }
    if (x instanceof Array) {
        this.init(x[0], x[1], x[2])
    } else {
        this.init(x, y, z)
    }
}

Point.prototype.rotate = function (p, n) { // :: Point -> Int -> Point
    // record rotations
    if (p.n == 0) return new Point(this.z, this.x, this.y)
    if (p.n == 1 || p.n == 3) return new Point(this.y, this.z, this.x)
    if (p.n == 2 || p.n == 6) return new Point(n - this.x, n - this.y, this.z)
    return new Point(n - this.z, this.x, n - this.y)
}

Point.prototype.rotate2d = function (n, xbit, ybit) { // : Int -> Int -> Int -> Point
    return new Point(rotate2d(n, this.x, this.y, xbit, ybit))
}

Point.prototype.rotate3d = function (level) { // :: Int -> Point
    return new Point(rotate3d(level, this.x, this.y, this.z))
}

Point.prototype.toArray = function () { // :: -> [Int, Int]
        if (this.d == 3) { return [this.x, this.y, this.z] }
        return [this.x, this.y]
}

Point.prototype.mod = function (n) { // :: Int -> Point
    return new Point(this.x % n, this.y % n, this.z % n)
}

// Accepts the height or width of a square/graph, and the coordinates to
// convert.
function convert2dPointToDistance (p, height) { // :: Int -> Int -> Int -> Int
    var xbit, ybit, level, d = 0
    var forHeight = p.x > p.y ? p.x : p.y

    // needs some tests to make sure height is compatible
    // What keeps the user from putting 54 down as the height
    while (forHeight >= height) {
            height *=2
    }
    // For each Hilbert level, we want to add an amount to
    // `d` based on which region we are in
    for (level = height / 2; level > 0; level = Math.floor(level / 2)) {
        // Determine what region we're in
        xbit = (p.x & level) > 0
        ybit = (p.y & level) > 0
        // increase distance based on region
        d += level * level * ((3 * xbit) ^ ybit)
        // rotate so that we'll be in sync with the next
        // region.
        p = p.rotate2d(level, xbit, ybit)
        // HEAD
        // p = p.rotate2d(height, xbit, ybit)
    }

    return d
}

// height and coordinates.
function convert3dPointToDistance (p, height) { // :: Int -> Int -> Int -> Int -> Int
    var s = 1, level = 0
    var max = Math.max.apply(Math, p.toArray())
    for (; 2 * s <= max; s *= 2) {
        level = (level + 1) % 3
    }

    // shuffle axes
    // rotate based on parity
}

// Accepts height or width of a square/graph and distance
function convertDistanceTo2dPoint (distance, height) { // :: Int -> Int -> [Int, Int]
    distance = Math.floor(distance)
    var xbit, ybit, level, p = new Point(0, 0)

    if (height <= Math.sqrt(distance))  {
        height = 2
        while (height <= Math.sqrt(distance)) {
            height *=2
        }
    }

    for (level = 1; level < height; level *= 2) {
        xbit = 1 & (distance / 2)
        ybit = 1 & (distance ^ xbit)

        p = p.rotate2d(level, xbit, ybit)
        p.x += level * xbit
        p.y += level * ybit
        distance = Math.floor(distance / 4)
    }

    return p.toArray()
}

// height/width of a square/graph and distance
function convertDistanceTo3dPoint (distance, height) { // Int -> Int -> [Int, Int, Int]
    distance = Math.floor(distance)
    var xbit, ybit, zbit, level, parity
    var iter = 2, log = 0, p = new Point(0, 0, 0)

    height = height || 2

    // vvv this is interesting.
    for (parity = 1; parity < height; parity *= 2, ++log) {}
    parity = log % 3;

    // the `|| distance` was removed from 2d version
    for (level = 1; level < height || distance > 0; level *=2) {
        xbit = distance & 1;
        ybit = (distance / 2) & 1;
        zbit = (distance / 4) & 1;

        var temp = rotate3d(level - 1, xbit ^ ybit, ybit ^ zbit, zbit)
        p.x = temp[0] * level + level - 1
        p.y = temp[1] * level + level - 1
        p.z = temp[2] * level + level - 1

        distance = Math.floor(distance / 8)
        level *= 2;
        iter++;
    }

    return p.rotateLeft(iter - parity + 1).toArray();
}

// Rotate the coordinate plane and (x,y)
function rotate2d (n, x, y, xbit, ybit) { // :: Int -> Int -> Int -> Int -> Int -> [Int, Int]
    if (ybit == 0  ) {
        if (xbit == 1) {
            x = n - 1 - x
            y = n - 1 - y
        }

        var temp = x
        x = y
        y = temp
    }

    return [x, y]
}

// Rotate the coordinate plane and (x,y, z)
function rotate3d(level, x, y, z) { // :: Int -> Int -> Int -> Int -> [Int, Int, Int]
    var index = 4 * z + 2 * y + x
    if (index == 0) {
        return [z, x, y]
    } else if (index == 1 || index == 3) {
        return [y, z, x]
    } else if (index == 2 || index == 6) {
        return [level - x, level - y, z]
    } else if (index == 5 || index == 7) {
        return [y, level - z, level - x]
    } else {
        return [level - z, x, level - y]
    }
}

function grayCode (sequence) {
    return sequence ^ (sequence >> 1)
}

// returns a non-negative int 'inverse' such that graycode(inverse) = g
function grayInverse (g) { // : Int -> Int
    var m = precision(g), inverse = g, j = 1
    while (j < m) {
        inverse = inverse ^ (g >> j)
        j++
    }
    return inverse
}

// Returns the number of bits required to store an integer
function precision (n) { // :: Int > Int
    var ret = 0
    while (n > 0) {
        ret++
        n = n >> 1
    }
    return ret
}

function grayTransform (entry, direction, x, dim) { // :: Int -> Int -> Int -> Int
    return bitwise.rotateRight((x ^ entry), dim, 0, direction + 1)
}

function grayInverseTransform (entry, direction, x, dim) {
    return grayTransform(bitwise.rotateRight(entry, dim, 0, direction + 1), dim - direction - 1, x, dim)
}

function entrySequence (i) {
    if (i) {
        return grayCode(2 * Math.floor((i-1) / 2))
    }
    return 0
}

function directionSequence(i, dim) {
    if (i == 0) return 0
    if (i % 2 == 0) return trailingSetBits(i - 1) % dim
    return trailingSetBits(i) % dim
}

function trailingSetBits (i) {
    var ones = ~i & (i + 1)
    return Math.log(ones) / Math.log(2)
}

function hilbertIndex(point, options) { // :: [Int, Int, ..] -> {} -> Int
    var index = 0, code,
        entry = options.entry || 0,
        direction = options.direction || 0,
        i = options.precision || precision(Math.max.apply(null, point)) - 1,
        dim = point.length

    while (i >= 0) {

        var bits = 0
        var mask = 1 << dim - 1

        for (var k = 0; k < point.length; k++) {
            if (point[dim - (k+1)] & (1 << i)) {
                bits |= mask
            }
            mask >>>= 1
        }

        bits = grayTransform(entry, direction, bits, dim)
        code = grayInverse(bits)

        entry = entry ^ bitwise.rotateLeft(entrySequence(code), dim, 0, direction + 1)
        direction = (direction + directionSequence(code, dim) + 1) % dim
        index = (index << dim) | code

        i--
    }

    return index
}

function hilbertIndexInverse(dim, index) { // :: Int -> Int -> [Int, Int, ..]
    var entry = 0, direction = 0, m = precision(index)
    var p = Array.apply(null, new Array(dim)).map(Number.prototype.valueOf,0)

    for (var i = m - 1; i >= 0; i--) {

        var mask = 1 << (i * dim), bits = 0, code

        for (var k = dim - 1; k >= 0; k--) {
            if (index & (mask << k)) {
                bits |= (1 << k)
            }
        }

        code = grayInverseTransform(entry, direction, grayCode(bits), dim)

        for (var k = 0; k < dim; k++) {
            if (code & (1 << k)) {
                p[k] |= (1 << i)
            }
        }

        entry = entry ^ bitwise.rotateLeft(entrySequence(bits), dim, 0, direction + 1)
        direction = (direction + directionSequence(bits, dim) + 1) % dim
    }

    return p
}

exports.xy2d = function (x, y, height) {
    height = height || 2
    return convert2dPointToDistance(new Point(x, y), height)
}

exports.xyz2d = function(x, y, z, height) {
    height = height || 2
    return convert3dPointToDistance(new Point(x, y, z), height)
}
exports.d2xy = convertDistanceTo2dPoint
exports.d2xyz = convertDistanceTo3dPoint
exports.hilbert = hilbertIndex
exports.hilbertInverse = hilbertIndexInverse

exports.grayInverse = grayInverse
exports.grayCode = grayCode
<|endoftext|>


<commit_before>// An auxiliary parser function:
// converts a %-encoded string to real string
function decodeFGHIURL(encodedString){
   var spaceDecodedString = encodedString.replace(/\+/g, ' ');
   var SourceArray =
      spaceDecodedString.split(/((%[0-9ABCDEFabcdef][0-9ABCDEFabcdef])+)/);
   if (SourceArray.length == 1) {
      // No encoded content detected :-)
      return spaceDecodedString;
   } else {
      // Converting all URLencoded fragments
      var ijk;
      for (ijk = 1; ijk < SourceArray.length; ijk += 3){
         SourceArray[ijk] = decodeURIComponent(SourceArray[ijk]);
         SourceArray[ijk+1] = '';
      };
      return SourceArray.join('');
   };
};

//
//  The FGHI URL object:
//

function FidonetURL(){  // FidonetURL constructor
   this.scheme = '';
};

FidonetURL.prototype = {
   // Possible exceptions:
   FGHI_URL_NO_SEPARATOR        : 'No :// separator in URL!',
   FGHI_URL_EMPTY_OPTIONAL_NAME : 'An optional parameter name is empty!',
   FGHI_URL_EMPTY_OPTIONAL_PAIR : 'Empty parameter+value pair detected!',
   FGHI_URL_INVALID_STATION : 'Fidonet station address violates FSP-1004!',
   FGHI_URL_EMPTY_AREA_NAME : 'Area name is empty!',
   FGHI_URL_DOUBLE_SLASH    : 'Unexpected double slash!',

   // Empty requests are invalid in FGHI URL post-0.3 drafts
   FGHI_URL_EMPTY_FAQ_REQUEST : 'FAQ request is empty!',

   FGHI_URL_UNKNOWN_SCHEME : 'Unknown Fidonet URL scheme!',

   // The station parser function:
   // slices station address (sets stationZone, stationNet,
   //                              stationNode, stationPoint, stationDomain)
   ParseStation : function (){
      var Parsed = this.station.match(
          /^(([0-9]+):)?([0-9]+)\/([0-9]+)(\.([0-9]+))?(@(.+))?$/
      );
      if (Parsed === null){
         throw this.FGHI_URL_INVALID_STATION;
      } else {
         this.stationZone = Parsed[2];
         if (this.stationZone === undefined) this.stationZone = '';

         this.stationNet  = Parsed[3];
         this.stationNode = Parsed[4];

         this.stationPoint = Parsed[6];
         if (this.stationPoint === undefined) this.stationPoint = '';

         this.stationDomain = Parsed[8];
         if (this.stationDomain === undefined) {
            this.stationDomain = ''
         } else this.stationDomain = decodeFGHIURL(this.stationDomain);
      };
   },

   // And when there is no station:
   NoStation : function (){
      this.station = '';
      this.stationZone   = '';
      this.stationNet    = '';
      this.stationNode   = '';
      this.stationPoint  = '';
      this.stationDomain = '';
   },

   // And when there is no object-path:
   NoObjectPath : function (){
      this.objectPath = '';
      this.objectPathPart = Array();
   },

   ParseObjectPath : function (){
      this.objectPathPart = Array();
      var PathBuf = this.objectPath;
      while (PathBuf.length > 0) {
         // next step: getting another slashed slice
         var SlashPos = PathBuf.indexOf('/');
         if (SlashPos < 0){
            // No more slashes!
            this.objectPathPart.push(decodeFGHIURL(PathBuf));
            PathBuf = '';
         } else if (SlashPos == 0){
            throw this.FGHI_URL_DOUBLE_SLASH;
         } else if (SlashPos == (PathBuf.length - 1)){
            // Trailing slash. IMPORTANT! Must not be ignored.
            this.objectPathPart.push(decodeFGHIURL(PathBuf.slice(0, -1)));
            this.objectPathPart.push('/');
            PathBuf = '';
         } else {
            // There are more slices yet.
            this.objectPathPart.push(
               decodeFGHIURL(PathBuf.slice(0, SlashPos))
            );
            PathBuf = PathBuf.slice(SlashPos + 1);
         };
         // Another slashed slice got
      };
      // PathBuf completely processed.
   },

   // The main parser function:
   ParseFromString : function (pfsString){
      var SchemeSeparatorMatch = pfsString.match(/:(\/{2})?/);
                 // NB: the above regexp is greedy ^^^^^^^^
      if (SchemeSeparatorMatch === null) {
         throw this.FGHI_URL_NO_SEPARATOR;  // URL is invalid!
      };

      var SchemeSeparatorPosition =
                        pfsString.indexOf(SchemeSeparatorMatch[0]);
      var SchemeSeparatorMatchLen = SchemeSeparatorMatch[0].length;

      this.scheme = pfsString.slice(0, SchemeSeparatorPosition);
      this.scheme = this.scheme.toLowerCase(); // force ignore scheme case
      this.schemeSpecificPart = pfsString.slice(SchemeSeparatorPosition
                                              + SchemeSeparatorMatchLen);

      var QuestionPos = this.schemeSpecificPart.indexOf('?');
      if (QuestionPos < 0) {
      // optional part is empty!
         this.optionalPart = '';
         this.requiredPart = this.schemeSpecificPart;
      } else {
      // optional part may be non-empty
         this.requiredPart =
            this.schemeSpecificPart.slice(0, QuestionPos);
         this.optionalPart =
            this.schemeSpecificPart.slice(QuestionPos + 1);
      };

      if (this.optionalPart == ''){
         // no optional params!
         this.optionalParam = new Array();
         // (this array would remain empty!
      } else {
         // parsing optional params...
         this.optionalParam = new Array();
         var optionalBuf = this.optionalPart;
         // if the optional parts ends with ampersand (&), kill it:
         if (optionalBuf.charAt(optionalBuf.length-1) == '&'){
            optionalBuf = optionalBuf.slice(0, -1);
         };
         while (optionalBuf.length > 0){
            // each step: getting the next param name+value pair
            var AmpersandPos = optionalBuf.indexOf('&');
            var ParamValuePair = '';
            if (AmpersandPos == 0) {
               // Mistake: nothing precedes an ampersand,
               // or unexpected double ampersand (&&) is encountered!
               throw this.FGHI_URL_EMPTY_OPTIONAL_PAIR;
            } else if (AmpersandPos < 0) {
               // No more ampersands!
               ParamValuePair = optionalBuf;
               optionalBuf = '';
            } else {
               // grab the first remaining slice:
               ParamValuePair = optionalBuf.slice(0, AmpersandPos);
               optionalBuf = optionalBuf.slice(AmpersandPos + 1);
            };
            // optionalBuf decremented by the slice;
            // now we have ParamValuePair; let's parse it!
            var EqualsPos = ParamValuePair.indexOf('=');
            var ParamValueObject = new Object();
            if (EqualsPos == 0) {
               // Mistake: empty parameter name!
               throw this.FGHI_URL_EMPTY_OPTIONAL_NAME;
            } else if (EqualsPos < 0) {
               // The equals sign is absent!
               // The ParamValuePair contains just the name
               ParamValueObject.name = decodeFGHIURL(ParamValuePair);
               ParamValueObject.value = '';
            } else {
               // grab the slices:
               ParamValueObject.name =
                  decodeFGHIURL(ParamValuePair.slice(0, EqualsPos));
               ParamValueObject.value =
                  decodeFGHIURL(ParamValuePair.slice(EqualsPos + 1));
            };
            // ParamValueObject is formed
            this.optionalParam.push(ParamValueObject);
         };
         // optionalBuf is processed
      };
      // this.optionalPart is processed.

      // Now we start parsing requiredPart.
      // This is going to be very scheme-specific.
      // Our goal is to fill one or more of the following:
      //
      // *) station (stationZone, stationNet, stationNode, stationPoint,
      //             stationDomain)
      //
      // *) echoName
      //
      // *) objectPath (and the objectPathPart[] array)
      //
      // *) request (for faqserv://)
      switch(this.scheme){
         // cases follow in order of appearance in the FGHI URL standard:
         case 'netmail':
            this.request = '';
            this.NoObjectPath();
            this.echoName = '';
            this.station = this.requiredPart;
            this.ParseStation();
         break;

         case 'areafix':
         case 'echomail':
            this.request = '';
            this.NoObjectPath();
            this.NoStation();
            this.echoName = decodeFGHIURL(this.requiredPart);
         break;

         case 'area':
         case 'fecho':
            this.request = '';
            this.NoStation();

            var SlashPos = this.requiredPart.indexOf('/');
            if (SlashPos < 0){
               // No object-path!
               this.NoObjectPath();
               this.echoName = decodeFGHIURL(this.requiredPart);
            } else if (SlashPos == 0){
               throw this.FGHI_URL_EMPTY_AREA_NAME;
            } else {
               this.echoName =
                  decodeFGHIURL(this.requiredPart.slice(0, SlashPos));
               this.objectPath = this.requiredPart.slice(SlashPos + 1);
               this.ParseObjectPath();
            };
         break;

         case 'faqserv':
            var FirstSlashPos = this.requiredPart.indexOf('/');
            var FreqBuf = '';

            if (FirstSlashPos <= 0) throw this.FGHI_URL_INVALID_STATION;

            var SecondSlashPos = this.requiredPart.indexOf('/',
                                                      FirstSlashPos+1);
            if (SecondSlashPos < 0) {
               // No request, no object-path
               this.request = '';
               this.NoObjectPath();
               this.station = this.requiredPart;
               this.ParseStation();
            } else {
               this.station = this.requiredPart.slice(0, SecondSlashPos);
               this.ParseStation();
               FreqBuf = this.requiredPart.slice(SecondSlashPos + 1);

               var ThirdSlashPos = FreqBuf.indexOf('/');
               if (ThirdSlashPos < 0){
                  // No object-path!
                  this.NoObjectPath();
                  this.request = decodeFGHIURL(FreqBuf);
               } else if (ThirdSlashPos == 0){
                  // Empty requests are invalid in FGHI URL post-0.3 drafts
                  throw this.FGHI_URL_EMPTY_FAQ_REQUEST;
               } else {
                  this.request =
                     decodeFGHIURL(FreqBuf.slice(0, ThirdSlashPos));
                  this.objectPath = FreqBuf.slice(ThirdSlashPos + 1);
                  this.ParseObjectPath();
               };
               // Finished parsing request (and object-path, if exists)
            };
         break;

         case 'freq':
            this.request = '';

            var FirstSlashPos = this.requiredPart.indexOf('/');
            var FreqBuf = '';

            if (FirstSlashPos <= 0) throw this.FGHI_URL_INVALID_STATION;

            var SecondSlashPos = this.requiredPart.indexOf('/',
                                                      FirstSlashPos+1);
            if (SecondSlashPos < 0) {
               // No object-path!
               this.NoObjectPath();
               this.station = this.requiredPart;
               this.ParseStation();
            } else {
               this.station = this.requiredPart.slice(0, SecondSlashPos);
               this.ParseStation();
               this.objectPath = this.requiredPart.slice(SecondSlashPos+1);
               this.ParseObjectPath();
            };
         break;

         default:
            throw this.FGHI_URL_UNKNOWN_SCHEME;
         break;
      };
   }
}

module.exports = FidonetURL;<commit_msg>throw new Error() instead of a string<commit_after>// An auxiliary parser function:
// converts a %-encoded string to real string
function decodeFGHIURL(encodedString){
   var spaceDecodedString = encodedString.replace(/\+/g, ' ');
   var SourceArray =
      spaceDecodedString.split(/((%[0-9ABCDEFabcdef][0-9ABCDEFabcdef])+)/);
   if (SourceArray.length == 1) {
      // No encoded content detected :-)
      return spaceDecodedString;
   } else {
      // Converting all URLencoded fragments
      var ijk;
      for (ijk = 1; ijk < SourceArray.length; ijk += 3){
         SourceArray[ijk] = decodeURIComponent(SourceArray[ijk]);
         SourceArray[ijk+1] = '';
      };
      return SourceArray.join('');
   };
};

//
//  The FGHI URL object:
//

function FidonetURL(){  // FidonetURL constructor
   this.scheme = '';
};

FidonetURL.prototype = {
   // Possible exceptions:
   FGHI_URL_NO_SEPARATOR        : 'No :// separator in URL!',
   FGHI_URL_EMPTY_OPTIONAL_NAME : 'An optional parameter name is empty!',
   FGHI_URL_EMPTY_OPTIONAL_PAIR : 'Empty parameter+value pair detected!',
   FGHI_URL_INVALID_STATION : 'Fidonet station address violates FSP-1004!',
   FGHI_URL_EMPTY_AREA_NAME : 'Area name is empty!',
   FGHI_URL_DOUBLE_SLASH    : 'Unexpected double slash!',

   // Empty requests are invalid in FGHI URL post-0.3 drafts
   FGHI_URL_EMPTY_FAQ_REQUEST : 'FAQ request is empty!',

   FGHI_URL_UNKNOWN_SCHEME : 'Unknown Fidonet URL scheme!',

   // The station parser function:
   // slices station address (sets stationZone, stationNet,
   //                              stationNode, stationPoint, stationDomain)
   ParseStation : function (){
      var Parsed = this.station.match(
          /^(([0-9]+):)?([0-9]+)\/([0-9]+)(\.([0-9]+))?(@(.+))?$/
      );
      if (Parsed === null){
         throw new Error(this.FGHI_URL_INVALID_STATION);
      } else {
         this.stationZone = Parsed[2];
         if (this.stationZone === undefined) this.stationZone = '';

         this.stationNet  = Parsed[3];
         this.stationNode = Parsed[4];

         this.stationPoint = Parsed[6];
         if (this.stationPoint === undefined) this.stationPoint = '';

         this.stationDomain = Parsed[8];
         if (this.stationDomain === undefined) {
            this.stationDomain = ''
         } else this.stationDomain = decodeFGHIURL(this.stationDomain);
      };
   },

   // And when there is no station:
   NoStation : function (){
      this.station = '';
      this.stationZone   = '';
      this.stationNet    = '';
      this.stationNode   = '';
      this.stationPoint  = '';
      this.stationDomain = '';
   },

   // And when there is no object-path:
   NoObjectPath : function (){
      this.objectPath = '';
      this.objectPathPart = Array();
   },

   ParseObjectPath : function (){
      this.objectPathPart = Array();
      var PathBuf = this.objectPath;
      while (PathBuf.length > 0) {
         // next step: getting another slashed slice
         var SlashPos = PathBuf.indexOf('/');
         if (SlashPos < 0){
            // No more slashes!
            this.objectPathPart.push(decodeFGHIURL(PathBuf));
            PathBuf = '';
         } else if (SlashPos == 0){
            throw new Error(this.FGHI_URL_DOUBLE_SLASH);
         } else if (SlashPos == (PathBuf.length - 1)){
            // Trailing slash. IMPORTANT! Must not be ignored.
            this.objectPathPart.push(decodeFGHIURL(PathBuf.slice(0, -1)));
            this.objectPathPart.push('/');
            PathBuf = '';
         } else {
            // There are more slices yet.
            this.objectPathPart.push(
               decodeFGHIURL(PathBuf.slice(0, SlashPos))
            );
            PathBuf = PathBuf.slice(SlashPos + 1);
         };
         // Another slashed slice got
      };
      // PathBuf completely processed.
   },

   // The main parser function:
   ParseFromString : function (pfsString){
      var SchemeSeparatorMatch = pfsString.match(/:(\/{2})?/);
                 // NB: the above regexp is greedy ^^^^^^^^
      if (SchemeSeparatorMatch === null) {
         throw new Error(this.FGHI_URL_NO_SEPARATOR);  // URL is invalid!
      };

      var SchemeSeparatorPosition =
                        pfsString.indexOf(SchemeSeparatorMatch[0]);
      var SchemeSeparatorMatchLen = SchemeSeparatorMatch[0].length;

      this.scheme = pfsString.slice(0, SchemeSeparatorPosition);
      this.scheme = this.scheme.toLowerCase(); // force ignore scheme case
      this.schemeSpecificPart = pfsString.slice(SchemeSeparatorPosition
                                              + SchemeSeparatorMatchLen);

      var QuestionPos = this.schemeSpecificPart.indexOf('?');
      if (QuestionPos < 0) {
      // optional part is empty!
         this.optionalPart = '';
         this.requiredPart = this.schemeSpecificPart;
      } else {
      // optional part may be non-empty
         this.requiredPart =
            this.schemeSpecificPart.slice(0, QuestionPos);
         this.optionalPart =
            this.schemeSpecificPart.slice(QuestionPos + 1);
      };

      if (this.optionalPart == ''){
         // no optional params!
         this.optionalParam = new Array();
         // (this array would remain empty!
      } else {
         // parsing optional params...
         this.optionalParam = new Array();
         var optionalBuf = this.optionalPart;
         // if the optional parts ends with ampersand (&), kill it:
         if (optionalBuf.charAt(optionalBuf.length-1) == '&'){
            optionalBuf = optionalBuf.slice(0, -1);
         };
         while (optionalBuf.length > 0){
            // each step: getting the next param name+value pair
            var AmpersandPos = optionalBuf.indexOf('&');
            var ParamValuePair = '';
            if (AmpersandPos == 0) {
               // Mistake: nothing precedes an ampersand,
               // or unexpected double ampersand (&&) is encountered!
               throw new Error(this.FGHI_URL_EMPTY_OPTIONAL_PAIR);
            } else if (AmpersandPos < 0) {
               // No more ampersands!
               ParamValuePair = optionalBuf;
               optionalBuf = '';
            } else {
               // grab the first remaining slice:
               ParamValuePair = optionalBuf.slice(0, AmpersandPos);
               optionalBuf = optionalBuf.slice(AmpersandPos + 1);
            };
            // optionalBuf decremented by the slice;
            // now we have ParamValuePair; let's parse it!
            var EqualsPos = ParamValuePair.indexOf('=');
            var ParamValueObject = new Object();
            if (EqualsPos == 0) {
               // Mistake: empty parameter name!
               throw new Error(this.FGHI_URL_EMPTY_OPTIONAL_NAME);
            } else if (EqualsPos < 0) {
               // The equals sign is absent!
               // The ParamValuePair contains just the name
               ParamValueObject.name = decodeFGHIURL(ParamValuePair);
               ParamValueObject.value = '';
            } else {
               // grab the slices:
               ParamValueObject.name =
                  decodeFGHIURL(ParamValuePair.slice(0, EqualsPos));
               ParamValueObject.value =
                  decodeFGHIURL(ParamValuePair.slice(EqualsPos + 1));
            };
            // ParamValueObject is formed
            this.optionalParam.push(ParamValueObject);
         };
         // optionalBuf is processed
      };
      // this.optionalPart is processed.

      // Now we start parsing requiredPart.
      // This is going to be very scheme-specific.
      // Our goal is to fill one or more of the following:
      //
      // *) station (stationZone, stationNet, stationNode, stationPoint,
      //             stationDomain)
      //
      // *) echoName
      //
      // *) objectPath (and the objectPathPart[] array)
      //
      // *) request (for faqserv://)
      switch(this.scheme){
         // cases follow in order of appearance in the FGHI URL standard:
         case 'netmail':
            this.request = '';
            this.NoObjectPath();
            this.echoName = '';
            this.station = this.requiredPart;
            this.ParseStation();
         break;

         case 'areafix':
         case 'echomail':
            this.request = '';
            this.NoObjectPath();
            this.NoStation();
            this.echoName = decodeFGHIURL(this.requiredPart);
         break;

         case 'area':
         case 'fecho':
            this.request = '';
            this.NoStation();

            var SlashPos = this.requiredPart.indexOf('/');
            if (SlashPos < 0){
               // No object-path!
               this.NoObjectPath();
               this.echoName = decodeFGHIURL(this.requiredPart);
            } else if (SlashPos == 0){
               throw new Error(this.FGHI_URL_EMPTY_AREA_NAME);
            } else {
               this.echoName =
                  decodeFGHIURL(this.requiredPart.slice(0, SlashPos));
               this.objectPath = this.requiredPart.slice(SlashPos + 1);
               this.ParseObjectPath();
            };
         break;

         case 'faqserv':
            var FirstSlashPos = this.requiredPart.indexOf('/');
            var FreqBuf = '';

            if (FirstSlashPos <= 0){
               throw new Error(this.FGHI_URL_INVALID_STATION);
            }

            var SecondSlashPos = this.requiredPart.indexOf('/',
                                                      FirstSlashPos+1);
            if (SecondSlashPos < 0) {
               // No request, no object-path
               this.request = '';
               this.NoObjectPath();
               this.station = this.requiredPart;
               this.ParseStation();
            } else {
               this.station = this.requiredPart.slice(0, SecondSlashPos);
               this.ParseStation();
               FreqBuf = this.requiredPart.slice(SecondSlashPos + 1);

               var ThirdSlashPos = FreqBuf.indexOf('/');
               if (ThirdSlashPos < 0){
                  // No object-path!
                  this.NoObjectPath();
                  this.request = decodeFGHIURL(FreqBuf);
               } else if (ThirdSlashPos == 0){
                  // Empty requests are invalid in FGHI URL post-0.3 drafts
                  throw new Error(this.FGHI_URL_EMPTY_FAQ_REQUEST);
               } else {
                  this.request =
                     decodeFGHIURL(FreqBuf.slice(0, ThirdSlashPos));
                  this.objectPath = FreqBuf.slice(ThirdSlashPos + 1);
                  this.ParseObjectPath();
               };
               // Finished parsing request (and object-path, if exists)
            };
         break;

         case 'freq':
            this.request = '';

            var FirstSlashPos = this.requiredPart.indexOf('/');
            var FreqBuf = '';

            if (FirstSlashPos <= 0){
               throw new Error(this.FGHI_URL_INVALID_STATION);
            }

            var SecondSlashPos = this.requiredPart.indexOf('/',
                                                      FirstSlashPos+1);
            if (SecondSlashPos < 0) {
               // No object-path!
               this.NoObjectPath();
               this.station = this.requiredPart;
               this.ParseStation();
            } else {
               this.station = this.requiredPart.slice(0, SecondSlashPos);
               this.ParseStation();
               this.objectPath = this.requiredPart.slice(SecondSlashPos+1);
               this.ParseObjectPath();
            };
         break;

         default:
            throw new Error(this.FGHI_URL_UNKNOWN_SCHEME);
         break;
      };
   }
}

module.exports = FidonetURL;<|endoftext|>


<commit_before>if(process.argv.length<3) {
    console.error("Usage: node index.js telegram_token");
    process.exit(1);
}

let token = process.argv[2];

let http = require('http');
let https = require('https');
let TelegramBot = require('node-telegram-bot-api');
let telegram = new TelegramBot(token, {polling: true});

// possibilities Regex
let startRegex = /\/start/;
let urlRegex = /^(http(s)?(:\/\/))?(www\.)?[a-zA-Z0-9-_\.]+(\.[a-zA-Z0-9]{2,})([-a-zA-Z0-9:%_\+.~#?&//=]*)/;
let verifyUrlRegex = /\/verify\s(http(s)?(:\/\/))?(www\.)?[a-zA-Z0-9-_\.]+(\.[a-zA-Z0-9]{2,})([-a-zA-Z0-9:%_\+.~#?&\/\/=]*)/

console.log("Running!")

// first message
telegram.onText(startRegex, (msg, match) => {
    username = msg.from.first_name;
    firstMessage = `Hi ${username}, I'm the Is it UP bot, my mission is tell you if any website is up or down.`;
    secondMessage = `You can use the command /verify www.myurl.com or just type your url to verify the current status.`;

    telegram.sendMessage(msg.chat.id, firstMessage);
    telegram.sendMessage(msg.chat.id, secondMessage);
});

// verify urls
telegram.onText(urlRegex, verify);
telegram.onText(verifyUrlRegex, (msg, match) => {
    match[0] = match[0].split(' ')[1];
    verify(msg, match);
});

// not found
telegram.onText(/.*/, (msg, match) => {
    if(!match[0].match(urlRegex)
    && !match[0].match(verifyUrlRegex)
    && !match[0].match(startRegex) ) {
        console.log("error: " + match[0]);
        telegram.sendMessage(msg.chat.id, "Sorry, I didn't understand what you said! Try to pass an URL :D");
    }
});

function verify(msg, match) {
//  setup according url
    let request = http;
    url = match[0];

    if(match[2]=="s") {
        request = https;
    } else if(match[1]==undefined) {
        url = "http://" + url;
    }
    console.log(URL);

    request.get(url, res => {
      telegram.sendMessage(msg.chat.id, url + " is up :D");
    }).on('error', e => {
      telegram.sendMessage(msg.chat.id, url + " is down :(");
    });
}
<commit_msg>Fix some regex<commit_after>if(process.argv.length<3) {
    console.error("Usage: node index.js telegram_token");
    process.exit(1);
}

let token = process.argv[2];

let http = require('http');
let https = require('https');
let TelegramBot = require('node-telegram-bot-api');
let telegram = new TelegramBot(token, {polling: true});

// possibilities Regex
let commandString  = "\/verify"
let urlString = "(http(s)?(:\/\/))?(www\.)?[a-zA-Z0-9-_\.]+(\.[a-zA-Z0-9]{2,})([-a-zA-Z0-9:%_\+.~#?&//=]*)"

let startRegex = /\/start/;
let urlRegex = new RegExp("^" + urlString);
let verifyUrlRegex = new RegExp(commandString + "\\s" + urlString);
let justVerifyRegex = new RegExp(commandString + "$");

console.log("Running!")

// first message
telegram.onText(startRegex, (msg, match) => {
    username = msg.from.first_name;
    firstMessage = `Hi ${username}, I'm the Is it UP bot, my mission is tell you if any website is up or down.`;
    secondMessage = `You can use the command /verify telegram.org/ or just type your url to verify the current status.`;

    telegram.sendMessage(msg.chat.id, firstMessage);
    telegram.sendMessage(msg.chat.id, secondMessage);
});

// verify urls
telegram.onText(urlRegex, verify);
telegram.onText(verifyUrlRegex, (msg, match) => {
    match[0] = match[0].split(' ')[1];
    verify(msg, match);
});

// just verify
telegram.onText(justVerifyRegex, (msg) => {
    let message = "To use /verify command you have to pass an URL. Ex: /verify telegram.org";
    telegram.sendMessage(msg.chat.id, message);
})

// not found
telegram.onText(/.*/, (msg, match) => {
    if(!match[0].match(urlRegex)
    && !match[0].match(verifyUrlRegex)
    && !match[0].match(startRegex)
    && !match[0].match(justVerifyRegex) ) {
        console.log("error: " + match[0]);
        telegram.sendMessage(msg.chat.id, "Sorry, I didn't understand what you said! Try to pass an URL :D");
    }
});

function verify(msg, match) {
//  setup according url
    let request = http;
    url = match[0];

    if(url.match(justVerifyRegex)) return;

    if(match[2]=="s") {
        request = https;
    } else if(match[1]==undefined) {
        url = "http://" + url;
    }

    console.log("URL: " + url);

    request.get(url, res => {
        telegram.sendMessage(msg.chat.id, url + " is up :D");
    }).on('error', e => {
        telegram.sendMessage(msg.chat.id, url + " is down :(");
    });
}
<|endoftext|>


<commit_before>(function (root, cls) {
   if (typeof define === 'function' && define.amd) {
      define(cls);
   } else {
      root.Observer = cls();
   }
}(this, function() {
   /**
    * Pub/Sub library that allows 'subscribe', 'publish' and 'unsubscribe' methods.
    *
    * @constructor
    * @name Observer
    */
   function Observer() {
      if (!(this instanceof Observer)) {
        return new Observer();
      }
   }

   /**
    * @type {Object}
    */
   Observer.prototype._topics = null;

   /**
    * @type {Boolean}
    */
   Observer.prototype._eventsShouldBubble = false;

   /**
    * Static method to get all possible events that can bubble from a given eventName.
    * @param {String} eventName - eventName to split on ':' and return all possible events to bubble.
    * @return {String[]}
    */
   Observer.getBubbleEvents = function(eventName) {
      return eventName.split(':').map(function(splitName, index, eventArr) {
         return eventArr.slice(0, index + 1).join(':');
      });
   };

   /**
    * Whether events should events bubble up to parent.
    * @return {*} 'this' for chaining
    */
   Observer.prototype.withEventBubbling = function() {
      this._eventsShouldBubble = true;
      return this;
   };

   /**
    * Lazy getter for topics.
    * @return {Object}
    */
   Observer.prototype._getTopics = function() {
      if (!this._topics) {
         this._topics = Object.create(null);
      }
      return this._topics;
   };

   /**
    * Private method to publish an event.
    * @param {String} eventName - event to publish.
    * @param {Boolean} [bubble] - whether event should bubble or not. True by default.
    * @return {Boolean} - false if a handler has returned false, this will prevent the vent 'bubbling'.
    */
   Observer.prototype._publish = function(eventName, bubble) {
      var observers = this._getTopics()[eventName],
         args = Array.prototype.slice.call(arguments, 1),
         returnedValue,
         observersClone;

      if (observers) {
         observersClone = observers.slice();
         for (var i = 0, l = observersClone.length; i < l; i++) {
            returnedValue = observersClone[i].handler.apply(observersClone[i].scope || this, args);
            if (observersClone[i].once) {
               observers.splice(i, 1);
            }
            if (returnedValue === false) {
               return false;
            }
         }
      }

      return this._eventsShouldBubble;
   };

   /**
    * Public method to subscribe to a given eventName.
    * @param {String|Object} eventName - event to subscribe to, can be joined via ':'.
    * @param {Function} handler - function to invoke when event is published.
    * @param {Boolean} once - whether handler should only be triggered once and then unsubscribe.
    * @return {*} 'this' for chaining.
    */
   Observer.prototype.subscribe = function(eventName, handler, scope, once) {
      if (eventName && typeof eventName.toString === 'function') {
         eventName = eventName.toString();
      }
      if (typeof handler !== 'function') {
         throw new Error('Observer.subscribe: please provide a function as he handler argument.');
      }
      var topics = this._getTopics();

      if (!topics[eventName]) {
         topics[eventName] = [];
      }

      topics[eventName].push({
         handler: handler,
         scope: scope,
         once: !!once
      });

      return this;
   };

   /**
    * Public method to unsubscribe to a given eventName or scope or everything.
    * @param {String|Object} [eventName] - optional eventName to unsubscribe from.
    * @param {Object} [scope] optional scope to unsubscribe from.
    * @return {*} 'this' for chaining.
    */
   Observer.prototype.unsubscribe = function(eventName, scope) {
      eventName = (eventName && typeof eventName.toString === 'function') ?
                        eventName.toString() : eventName;
      var topics = this._getTopics();
      [].concat(eventName || Object.keys(topics)).forEach(function(matchedEvent) {
         var topic = topics[matchedEvent];
         if (topic) {
            topics[matchedEvent] = topic.filter(function(observer) {
                if (scope && observer.scope !== scope) {
                  return observer;
               }
            });

            if (!topics[matchedEvent].length) {
               delete topics[matchedEvent];
            }
         }
      }, this);

      return this;
   };

   /**
    * Public method to publish to a given eventName. Any arguments supplied will be proxied to the handler.
    * the eventName can be joined via ':' and all events will be called unless a handler returns false then bubbling will be prevented.
    * @param {String|Object} eventName - eventName to publish.
    * @return {*} 'this' for chaining.
    */
   Observer.prototype.publish = function(eventName) {
      if (eventName && typeof eventName.toString === 'function') {
         eventName = eventName.toString();
      } else {
         return this;
      }

      var eventsToPublish = Observer.getBubbleEvents(eventName),
         args = Array.prototype.slice.call(arguments, 1),
         eventArgs;

      while (eventsToPublish.length) {
         eventArgs = args.slice();
         eventArgs.unshift(eventsToPublish[eventsToPublish.length - 1]);
         if (this._publish.apply(this, eventArgs) === false) {
            break;
         }
         eventsToPublish.pop();
      }

      return this;
   };

   return Observer;
}));
<commit_msg>added utility to check for listeners<commit_after>(function (root, cls) {
   if (typeof define === 'function' && define.amd) {
      define(cls);
   } else {
      root.Observer = cls();
   }
}(this, function() {
   /**
    * Pub/Sub library that allows 'subscribe', 'publish' and 'unsubscribe' methods.
    *
    * @constructor
    * @name Observer
    */
   function Observer() {
      if (!(this instanceof Observer)) {
        return new Observer();
      }
   }

   /**
    * @type {Object}
    */
   Observer.prototype._topics = null;

   /**
    * @type {Boolean}
    */
   Observer.prototype._eventsShouldBubble = false;

   /**
    * Static method to get all possible events that can bubble from a given eventName.
    * @param {String} eventName - eventName to split on ':' and return all possible events to bubble.
    * @return {String[]}
    */
   Observer.getBubbleEvents = function(eventName) {
      return eventName.split(':').map(function(splitName, index, eventArr) {
         return eventArr.slice(0, index + 1).join(':');
      });
   };

   /**
    * Whether events should events bubble up to parent.
    * @return {*} 'this' for chaining
    */
   Observer.prototype.withEventBubbling = function() {
      this._eventsShouldBubble = true;
      return this;
   };

   /**
    * Lazy getter for topics.
    * @return {Object}
    */
   Observer.prototype._getTopics = function() {
      if (!this._topics) {
         this._topics = Object.create(null);
      }
      return this._topics;
   };

   /**
    * Private method to publish an event.
    * @param {String} eventName - event to publish.
    * @param {Boolean} [bubble] - whether event should bubble or not. True by default.
    * @return {Boolean} - false if a handler has returned false, this will prevent the vent 'bubbling'.
    */
   Observer.prototype._publish = function(eventName, bubble) {
      var observers = this._getTopics()[eventName],
         args = Array.prototype.slice.call(arguments, 1),
         returnedValue,
         observersClone;

      if (observers) {
         observersClone = observers.slice();
         for (var i = 0, l = observersClone.length; i < l; i++) {
            returnedValue = observersClone[i].handler.apply(observersClone[i].scope || this, args);
            if (observersClone[i].once) {
               observers.splice(i, 1);
            }
            if (returnedValue === false) {
               return false;
            }
         }
      }

      return this._eventsShouldBubble;
   };

   /**
    * Public method to subscribe to a given eventName.
    * @param {String|Object} eventName - event to subscribe to, can be joined via ':'.
    * @param {Function} handler - function to invoke when event is published.
    * @param {Boolean} once - whether handler should only be triggered once and then unsubscribe.
    * @return {*} 'this' for chaining.
    */
   Observer.prototype.subscribe = function(eventName, handler, scope, once) {
       eventName = (eventName && typeof eventName.toString === 'function') ?
                        eventName.toString() : eventName;

      if (typeof handler !== 'function') {
         throw new Error('Observer.subscribe: please provide a function as he handler argument.');
      }
      var topics = this._getTopics();

      if (!topics[eventName]) {
         topics[eventName] = [];
      }

      topics[eventName].push({
         handler: handler,
         scope: scope,
         once: !!once
      });

      return this;
   };

   /**
    * Public method to unsubscribe to a given eventName or scope or everything.
    * @param {String|Object} [eventName] - optional eventName to unsubscribe from.
    * @param {Object} [scope] - optional scope to unsubscribe from.
    * @return {*} 'this' for chaining.
    */
   Observer.prototype.unsubscribe = function(eventName, scope) {
      eventName = (eventName && typeof eventName.toString === 'function') ?
                        eventName.toString() : eventName;
      var topics = this._getTopics();
      [].concat(eventName || Object.keys(topics)).forEach(function(matchedEvent) {
         var topic = topics[matchedEvent];
         if (topic) {
            topics[matchedEvent] = topic.filter(function(observer) {
                if (scope && observer.scope !== scope) {
                  return observer;
               }
            });

            if (!this.hasListeners(matchedEvent)) {
               delete topics[matchedEvent];
            }
         }
      }, this);

      return this;
   };

   /**
    * Public method to publish to a given eventName. Any arguments supplied will be proxied to the handler.
    * the eventName can be joined via ':' and all events will be called unless a handler returns false then bubbling will be prevented.
    * @param {String|Object} eventName - eventName to publish.
    * @return {*} 'this' for chaining.
    */
   Observer.prototype.publish = function(eventName) {
      eventName = (eventName && typeof eventName.toString === 'function') ?
                        eventName.toString() : eventName;

      var eventsToPublish = Observer.getBubbleEvents(eventName),
         args = Array.prototype.slice.call(arguments, 1),
         eventArgs;

      while (eventsToPublish.length) {
         eventArgs = args.slice();
         eventArgs.unshift(eventsToPublish[eventsToPublish.length - 1]);
         if (this._publish.apply(this, eventArgs) === false) {
            break;
         }
         eventsToPublish.pop();
      }

      return this;
   };

   /**
    * Utility to check whether a given event has any listeners; if none is supplied then all topics are checked.
    * @param {String|Object} [eventName] - optional eventName to check.
    * @return {Boolean}
    */
   Observer.prototype.hasListeners = function(eventName) {
      var topics = this._getTopics();
      if (eventName) {
         eventName = typeof eventName.toString === 'function' ? eventName.toString() : eventName;
		   return !!(topics[eventName] && topics[eventName].length);
      } else {
         return !!Object.keys(topics).length;
      }
   };

   return Observer;
}));
<|endoftext|>