<commit_before>/* =============================================================
 * bootstrap-typeahead.js v2.1.0
 * http://twitter.github.com/bootstrap/javascript.html#typeahead
 * =============================================================
 * Copyright 2012 Twitter, Inc.
 *
 * 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.
 * ============================================================ */

/* =============================================================
 * Zulip's custom changes
 *
 * 1. Automated selection:
 *
 *   This adds support for automatically selecting a typeahead (on certain
 *   completions or queries). If `this.automated` returns true, we do not
 *   render the typeahead and directly trigger selection of the current
 *   choice.
 *
 *   Our custom changes include all mentions of this.automated.
 *   And also includes the blocks containing the is contenteditable condition.
 *
 * 2. Custom selection triggers:
 *
 *   This adds support for completing a typeahead on custom keyup input. By
 *   default, we only support Tab and Enter to complete a typeahead, but we
 *   have use cases where we want to complete using custom characters like: >.
 *
 *   If `this.trigger_selection` returns true, we complete the typeahead and
 *   pass the keyup event to the updater.
 *
 *   Our custom changes include all mentions of this.trigger_selection.
 *
 * 3. Header text:
 *
 *   This adds support for showing a custom header text like: "You are now
 *   completing a user mention". Provide the function `this.header` that
 *   returns a string containing the header text, or false.
 *
 *   Our custom changes include all mentions of this.header, some CSS changes
 *   in compose.css and splitting $container out of $menu so we can insert
 *   additional HTML before $menu.
 *
 * 4. Escape hooks:
 *
 *  You can set an on_escape hook to take extra actions when the user hits
 *  the `Esc` key.  We use this in our navbar code to close the navbar when
 *  a user hits escape while in the typeahead.
 *
 * 5. Help on empty strings:
 *
 *   This adds support for displaying the typeahead for an empty string.
 *   It is helpful when we want to render the typeahead, based on already
 *   entered data (in the form of contenteditable elements) every time the
 *   input block gains focus but is empty.
 *
 *   We also have logic so that there is an exception to this rule when this
 *   option is set as true. We prevent the lookup of the typeahead and hide it
 *   so that the `Backspace` key is free to interact with the other elements.
 *
 *   Our custom changes include all mentions of `helpOnEmptyStrings` and `hideOnEmpty`.
 *
 * 6. Prevent typeahead going off top of screen:
 *   If typeahead would go off the top of the screen, we set its top to 0 instead.
 *   This patch should be replaced with something more flexible.
 *
 * ============================================================ */

!function($){

  "use strict"; // jshint ;_;

 /* TYPEAHEAD PUBLIC CLASS DEFINITION
  * ================================= */

  var Typeahead = function (element, options) {
    this.$element = $(element)
    this.options = $.extend({}, $.fn.typeahead.defaults, options)
    this.matcher = this.options.matcher || this.matcher
    this.sorter = this.options.sorter || this.sorter
    this.highlighter = this.options.highlighter || this.highlighter
    this.updater = this.options.updater || this.updater
    this.$container = $(this.options.container).appendTo('body')
    this.$menu = $(this.options.menu).appendTo(this.$container)
    this.$header = $(this.options.header_html).appendTo(this.$container)
    this.source = this.options.source
    this.shown = false
    this.dropup = this.options.dropup
    this.fixed = this.options.fixed || false;
    this.automated = this.options.automated || this.automated;
    this.trigger_selection = this.options.trigger_selection || this.trigger_selection;
    this.on_move = this.options.on_move;
    this.on_escape = this.options.on_escape;
    this.header = this.options.header || this.header;

    if (this.fixed) {
      this.$container.css('position', 'fixed');
    }
    // The naturalSearch option causes arrow keys to immediately
    // update the search box with the underlying values from the
    // search suggestions.
    this.listen()
  }

  Typeahead.prototype = {

    constructor: Typeahead

  , select: function (e) {
      var val = this.$menu.find('.active').data('typeahead-value')
      if (this.$element.is("[contenteditable]")) {
        this.$element.html(this.updater(val, e)).trigger("change");
        // Empty textContent after the change event handler
        // converts the input text to html elements.
        this.$element.html('');
      } else {
        this.$element.val(this.updater(val, e)).trigger("change");
      }

      return this.hide()
    }

  , set_value: function () {
      var val = this.$menu.find('.active').data('typeahead-value')
      this.$element.is("[contenteditable]") ? this.$element.html(val) : this.$element.val(val);

      if (this.on_move) {
        this.on_move();
      }
    }

  , updater: function (item) {
      return item
    }

  , automated: function() {
    return false;
  }

  , trigger_selection: function() {
    return false;
  }

  , header: function() {
    // return a string to show in typeahead header or false.
    return false;
  }

  , show: function () {
      var pos;

      if (this.fixed) {
        // Relative to screen instead of to page
        pos = this.$element[0].getBoundingClientRect();
      } else {
        pos = this.$element.offset();
      }

      pos = $.extend({}, pos, {
        height: this.$element[0].offsetHeight
      })

      // Zulip patch: Workaround for iOS safari problems
      pos.top = this.$element.offset().top;

      var top_pos = pos.top + pos.height
      if (this.dropup) {
        top_pos = pos.top - this.$container.outerHeight()
      }

      // Zulip patch: Avoid typeahead going off top of screen.
      if (top_pos < 0) {
          top_pos = 0;
      }

      this.$container.css({
        top: top_pos
       , left: pos.left
      })

      var header_text = this.header();
      if (header_text) {
        this.$header.find('span#typeahead-header-text').html(header_text);
        this.$header.show();
      } else {
        this.$header.hide();
      }

      this.$container.show()
      this.shown = true
      return this
    }

  , hide: function () {
      this.$container.hide()
      this.shown = false
      return this
    }

  , lookup: function (hideOnEmpty) {
      var items

      this.query = this.$element.is("[contenteditable]") ? this.$element.text() :  this.$element.val();

      if (!this.options.helpOnEmptyStrings || hideOnEmpty) {
        if (!this.query || this.query.length < this.options.minLength) {
          return this.shown ? this.hide() : this
        }
      }

      items = typeof this.source === "function" ? this.source(this.query, this.process.bind(this)) : this.source

      if (!items && this.shown) this.hide();
      return items ? this.process(items) : this
    }

  , process: function (items) {
      var that = this

      items = $.grep(items, function (item) {
        return that.matcher(item)
      })

      items = this.sorter(items)

      if (!items.length) {
        return this.shown ? this.hide() : this
      }
      if (this.automated()) {
        this.select();
        this.lookup();
        return this;
      }
      return this.render(items.slice(0, this.options.items)).show()
    }

  , matcher: function (item) {
      return ~item.toLowerCase().indexOf(this.query.toLowerCase())
    }

  , sorter: function (items) {
      var beginswith = []
        , caseSensitive = []
        , caseInsensitive = []
        , item

      while (item = items.shift()) {
        if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
        else if (~item.indexOf(this.query)) caseSensitive.push(item)
        else caseInsensitive.push(item)
      }

      return beginswith.concat(caseSensitive, caseInsensitive)
    }

  , highlighter: function (item) {
      var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
      return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
        return '' + match + ''
      })
    }

  , render: function (items) {
      var that = this

      items = $(items).map(function (i, item) {
        i = $(that.options.item).data('typeahead-value', item)
        i.find('a').html(that.highlighter(item))
        return i[0]
      })

      items.first().addClass('active')
      this.$menu.html(items)
      return this
    }

  , next: function (event) {
      var active = this.$menu.find('.active').removeClass('active')
        , next = active.next()

      if (!next.length) {
        next = $(this.$menu.find('li')[0])
      }

      next.addClass('active')

      if (this.options.naturalSearch) {
        this.set_value();
      }
    }

  , prev: function (event) {
      var active = this.$menu.find('.active').removeClass('active')
        , prev = active.prev()

      if (!prev.length) {
        prev = this.$menu.find('li').last()
      }

      prev.addClass('active')

      if (this.options.naturalSearch) {
        this.set_value();
      }
    }

  , listen: function () {
      this.$element
        .on('blur',     this.blur.bind(this))
        .on('keypress', this.keypress.bind(this))
        .on('keyup',    this.keyup.bind(this))

      if (this.eventSupported('keydown')) {
        this.$element.on('keydown', this.keydown.bind(this))
      }

      this.$menu
        .on('click', 'li', this.click.bind(this))
        .on('mouseenter', 'li', this.mouseenter.bind(this))
    }

  , unlisten: function () {
      this.$container.remove();
      var events = ["blur", "keydown", "keyup", "keypress"];
      for (var i=0; i<events.length; i++) {
        this.$element.off(events[i]);
      }
      this.$element.removeData("typeahead");
    }

  , eventSupported: function(eventName) {
      var isSupported = eventName in this.$element
      if (!isSupported) {
        this.$element.setAttribute(eventName, 'return;')
        isSupported = typeof this.$element[eventName] === 'function'
      }
      return isSupported
    }

  , move: function (e) {
      if (!this.shown) return

      switch(e.keyCode) {
        case 9: // tab
        case 13: // enter
        case 27: // escape
          e.preventDefault()
          break

        case 38: // up arrow
          e.preventDefault()
          this.prev()
          break

        case 40: // down arrow
          e.preventDefault()
          this.next()
          break
      }

      if ((this.options.stopAdvance || (e.keyCode != 9 && e.keyCode != 13))
          && $.inArray(e.keyCode, this.options.advanceKeyCodes)) {
          e.stopPropagation()
      }
    }

  , keydown: function (e) {
    if (this.trigger_selection(e)) {
      if (!this.shown) return;
      e.preventDefault();
      this.select(e);
    }
      this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
      this.move(e)
    }

  , keypress: function (e) {
      if (this.suppressKeyPressRepeat) return
      this.move(e)
    }

  , keyup: function (e) {
      switch(e.keyCode) {
        case 40: // down arrow
        case 38: // up arrow
          break

        case 9: // tab
        case 13: // enter
          if (!this.shown) return
          this.select(e)
          break

        case 27: // escape
          if (!this.shown) return
          this.hide()
          if (this.on_escape) {
            this.on_escape();
          }
          break

        default:
          var hideOnEmpty = false
          if (e.keyCode === 8 && this.options.helpOnEmptyStrings) { // backspace
            hideOnEmpty = true
          }
          this.lookup(hideOnEmpty)
      }

      if ((this.options.stopAdvance || (e.keyCode != 9 && e.keyCode != 13))
          && $.inArray(e.keyCode, this.options.advanceKeyCodes)) {
          e.stopPropagation()
      }

      e.preventDefault()
  }

  , blur: function (e) {
      var that = this
      setTimeout(function () {
        if (!that.$container.is(':hover')) {
          that.hide();
        }
      }, 150)
    }

  , click: function (e) {
      e.stopPropagation()
      e.preventDefault()
      // The original bootstrap code expected `mouseenter` to be called
      // to set the active element before `select()`.
      // Since select() selects the element with the active class, if
      // we trigger a click from JavaScript (or with a mobile touchscreen),
      // this won't work. So we need to set the active element ourselves,
      // and the simplest way to do that is to just call the `mouseenter`
      // handler here.
      this.mouseenter(e)
      this.select(e)
    }

  , mouseenter: function (e) {
      this.$menu.find('.active').removeClass('active')
      $(e.currentTarget).addClass('active')
    }

  }

  /* TYPEAHEAD PLUGIN DEFINITION
   * =========================== */

  $.fn.typeahead = function (option) {
    return this.each(function () {
      var $this = $(this)
        , data = $this.data('typeahead')
        , options = typeof option == 'object' && option
      if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
      if (typeof option == 'string') data[option]()
    })
  }

  $.fn.typeahead.defaults = {
    source: []
  , items: 8
  , container: '

'
  , header_html: '

'
  , menu: '

'
  , item: '

'
  , minLength: 1
  , stopAdvance: false
  , dropup: false
  , advanceKeyCodes: []
  }

  $.fn.typeahead.Constructor = Typeahead

 /*   TYPEAHEAD DATA-API
  * ================== */

  $(function () {
    $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
      var $this = $(this)
      if ($this.data('typeahead')) return
      e.preventDefault()
      $this.typeahead($this.data())
    })
  })

}(window.jQuery);
<commit_msg>typeahead: Avoid spurious href on typeahead links.<commit_after>/* =============================================================
 * bootstrap-typeahead.js v2.1.0
 * http://twitter.github.com/bootstrap/javascript.html#typeahead
 * =============================================================
 * Copyright 2012 Twitter, Inc.
 *
 * 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.
 * ============================================================ */

/* =============================================================
 * Zulip's custom changes
 *
 * 1. Automated selection:
 *
 *   This adds support for automatically selecting a typeahead (on certain
 *   completions or queries). If `this.automated` returns true, we do not
 *   render the typeahead and directly trigger selection of the current
 *   choice.
 *
 *   Our custom changes include all mentions of this.automated.
 *   And also includes the blocks containing the is contenteditable condition.
 *
 * 2. Custom selection triggers:
 *
 *   This adds support for completing a typeahead on custom keyup input. By
 *   default, we only support Tab and Enter to complete a typeahead, but we
 *   have use cases where we want to complete using custom characters like: >.
 *
 *   If `this.trigger_selection` returns true, we complete the typeahead and
 *   pass the keyup event to the updater.
 *
 *   Our custom changes include all mentions of this.trigger_selection.
 *
 * 3. Header text:
 *
 *   This adds support for showing a custom header text like: "You are now
 *   completing a user mention". Provide the function `this.header` that
 *   returns a string containing the header text, or false.
 *
 *   Our custom changes include all mentions of this.header, some CSS changes
 *   in compose.css and splitting $container out of $menu so we can insert
 *   additional HTML before $menu.
 *
 * 4. Escape hooks:
 *
 *  You can set an on_escape hook to take extra actions when the user hits
 *  the `Esc` key.  We use this in our navbar code to close the navbar when
 *  a user hits escape while in the typeahead.
 *
 * 5. Help on empty strings:
 *
 *   This adds support for displaying the typeahead for an empty string.
 *   It is helpful when we want to render the typeahead, based on already
 *   entered data (in the form of contenteditable elements) every time the
 *   input block gains focus but is empty.
 *
 *   We also have logic so that there is an exception to this rule when this
 *   option is set as true. We prevent the lookup of the typeahead and hide it
 *   so that the `Backspace` key is free to interact with the other elements.
 *
 *   Our custom changes include all mentions of `helpOnEmptyStrings` and `hideOnEmpty`.
 *
 * 6. Prevent typeahead going off top of screen:
 *   If typeahead would go off the top of the screen, we set its top to 0 instead.
 *   This patch should be replaced with something more flexible.
 *
 * ============================================================ */

!function($){

  "use strict"; // jshint ;_;

 /* TYPEAHEAD PUBLIC CLASS DEFINITION
  * ================================= */

  var Typeahead = function (element, options) {
    this.$element = $(element)
    this.options = $.extend({}, $.fn.typeahead.defaults, options)
    this.matcher = this.options.matcher || this.matcher
    this.sorter = this.options.sorter || this.sorter
    this.highlighter = this.options.highlighter || this.highlighter
    this.updater = this.options.updater || this.updater
    this.$container = $(this.options.container).appendTo('body')
    this.$menu = $(this.options.menu).appendTo(this.$container)
    this.$header = $(this.options.header_html).appendTo(this.$container)
    this.source = this.options.source
    this.shown = false
    this.dropup = this.options.dropup
    this.fixed = this.options.fixed || false;
    this.automated = this.options.automated || this.automated;
    this.trigger_selection = this.options.trigger_selection || this.trigger_selection;
    this.on_move = this.options.on_move;
    this.on_escape = this.options.on_escape;
    this.header = this.options.header || this.header;

    if (this.fixed) {
      this.$container.css('position', 'fixed');
    }
    // The naturalSearch option causes arrow keys to immediately
    // update the search box with the underlying values from the
    // search suggestions.
    this.listen()
  }

  Typeahead.prototype = {

    constructor: Typeahead

  , select: function (e) {
      var val = this.$menu.find('.active').data('typeahead-value')
      if (this.$element.is("[contenteditable]")) {
        this.$element.html(this.updater(val, e)).trigger("change");
        // Empty textContent after the change event handler
        // converts the input text to html elements.
        this.$element.html('');
      } else {
        this.$element.val(this.updater(val, e)).trigger("change");
      }

      return this.hide()
    }

  , set_value: function () {
      var val = this.$menu.find('.active').data('typeahead-value')
      this.$element.is("[contenteditable]") ? this.$element.html(val) : this.$element.val(val);

      if (this.on_move) {
        this.on_move();
      }
    }

  , updater: function (item) {
      return item
    }

  , automated: function() {
    return false;
  }

  , trigger_selection: function() {
    return false;
  }

  , header: function() {
    // return a string to show in typeahead header or false.
    return false;
  }

  , show: function () {
      var pos;

      if (this.fixed) {
        // Relative to screen instead of to page
        pos = this.$element[0].getBoundingClientRect();
      } else {
        pos = this.$element.offset();
      }

      pos = $.extend({}, pos, {
        height: this.$element[0].offsetHeight
      })

      // Zulip patch: Workaround for iOS safari problems
      pos.top = this.$element.offset().top;

      var top_pos = pos.top + pos.height
      if (this.dropup) {
        top_pos = pos.top - this.$container.outerHeight()
      }

      // Zulip patch: Avoid typeahead going off top of screen.
      if (top_pos < 0) {
          top_pos = 0;
      }

      this.$container.css({
        top: top_pos
       , left: pos.left
      })

      var header_text = this.header();
      if (header_text) {
        this.$header.find('span#typeahead-header-text').html(header_text);
        this.$header.show();
      } else {
        this.$header.hide();
      }

      this.$container.show()
      this.shown = true
      return this
    }

  , hide: function () {
      this.$container.hide()
      this.shown = false
      return this
    }

  , lookup: function (hideOnEmpty) {
      var items

      this.query = this.$element.is("[contenteditable]") ? this.$element.text() :  this.$element.val();

      if (!this.options.helpOnEmptyStrings || hideOnEmpty) {
        if (!this.query || this.query.length < this.options.minLength) {
          return this.shown ? this.hide() : this
        }
      }

      items = typeof this.source === "function" ? this.source(this.query, this.process.bind(this)) : this.source

      if (!items && this.shown) this.hide();
      return items ? this.process(items) : this
    }

  , process: function (items) {
      var that = this

      items = $.grep(items, function (item) {
        return that.matcher(item)
      })

      items = this.sorter(items)

      if (!items.length) {
        return this.shown ? this.hide() : this
      }
      if (this.automated()) {
        this.select();
        this.lookup();
        return this;
      }
      return this.render(items.slice(0, this.options.items)).show()
    }

  , matcher: function (item) {
      return ~item.toLowerCase().indexOf(this.query.toLowerCase())
    }

  , sorter: function (items) {
      var beginswith = []
        , caseSensitive = []
        , caseInsensitive = []
        , item

      while (item = items.shift()) {
        if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
        else if (~item.indexOf(this.query)) caseSensitive.push(item)
        else caseInsensitive.push(item)
      }

      return beginswith.concat(caseSensitive, caseInsensitive)
    }

  , highlighter: function (item) {
      var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
      return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
        return '' + match + ''
      })
    }

  , render: function (items) {
      var that = this

      items = $(items).map(function (i, item) {
        i = $(that.options.item).data('typeahead-value', item)
        i.find('a').html(that.highlighter(item))
        return i[0]
      })

      items.first().addClass('active')
      this.$menu.html(items)
      return this
    }

  , next: function (event) {
      var active = this.$menu.find('.active').removeClass('active')
        , next = active.next()

      if (!next.length) {
        next = $(this.$menu.find('li')[0])
      }

      next.addClass('active')

      if (this.options.naturalSearch) {
        this.set_value();
      }
    }

  , prev: function (event) {
      var active = this.$menu.find('.active').removeClass('active')
        , prev = active.prev()

      if (!prev.length) {
        prev = this.$menu.find('li').last()
      }

      prev.addClass('active')

      if (this.options.naturalSearch) {
        this.set_value();
      }
    }

  , listen: function () {
      this.$element
        .on('blur',     this.blur.bind(this))
        .on('keypress', this.keypress.bind(this))
        .on('keyup',    this.keyup.bind(this))

      if (this.eventSupported('keydown')) {
        this.$element.on('keydown', this.keydown.bind(this))
      }

      this.$menu
        .on('click', 'li', this.click.bind(this))
        .on('mouseenter', 'li', this.mouseenter.bind(this))
    }

  , unlisten: function () {
      this.$container.remove();
      var events = ["blur", "keydown", "keyup", "keypress"];
      for (var i=0; i<events.length; i++) {
        this.$element.off(events[i]);
      }
      this.$element.removeData("typeahead");
    }

  , eventSupported: function(eventName) {
      var isSupported = eventName in this.$element
      if (!isSupported) {
        this.$element.setAttribute(eventName, 'return;')
        isSupported = typeof this.$element[eventName] === 'function'
      }
      return isSupported
    }

  , move: function (e) {
      if (!this.shown) return

      switch(e.keyCode) {
        case 9: // tab
        case 13: // enter
        case 27: // escape
          e.preventDefault()
          break

        case 38: // up arrow
          e.preventDefault()
          this.prev()
          break

        case 40: // down arrow
          e.preventDefault()
          this.next()
          break
      }

      if ((this.options.stopAdvance || (e.keyCode != 9 && e.keyCode != 13))
          && $.inArray(e.keyCode, this.options.advanceKeyCodes)) {
          e.stopPropagation()
      }
    }

  , keydown: function (e) {
    if (this.trigger_selection(e)) {
      if (!this.shown) return;
      e.preventDefault();
      this.select(e);
    }
      this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
      this.move(e)
    }

  , keypress: function (e) {
      if (this.suppressKeyPressRepeat) return
      this.move(e)
    }

  , keyup: function (e) {
      switch(e.keyCode) {
        case 40: // down arrow
        case 38: // up arrow
          break

        case 9: // tab
        case 13: // enter
          if (!this.shown) return
          this.select(e)
          break

        case 27: // escape
          if (!this.shown) return
          this.hide()
          if (this.on_escape) {
            this.on_escape();
          }
          break

        default:
          var hideOnEmpty = false
          if (e.keyCode === 8 && this.options.helpOnEmptyStrings) { // backspace
            hideOnEmpty = true
          }
          this.lookup(hideOnEmpty)
      }

      if ((this.options.stopAdvance || (e.keyCode != 9 && e.keyCode != 13))
          && $.inArray(e.keyCode, this.options.advanceKeyCodes)) {
          e.stopPropagation()
      }

      e.preventDefault()
  }

  , blur: function (e) {
      var that = this
      setTimeout(function () {
        if (!that.$container.is(':hover')) {
          that.hide();
        }
      }, 150)
    }

  , click: function (e) {
      e.stopPropagation()
      e.preventDefault()
      // The original bootstrap code expected `mouseenter` to be called
      // to set the active element before `select()`.
      // Since select() selects the element with the active class, if
      // we trigger a click from JavaScript (or with a mobile touchscreen),
      // this won't work. So we need to set the active element ourselves,
      // and the simplest way to do that is to just call the `mouseenter`
      // handler here.
      this.mouseenter(e)
      this.select(e)
    }

  , mouseenter: function (e) {
      this.$menu.find('.active').removeClass('active')
      $(e.currentTarget).addClass('active')
    }

  }

  /* TYPEAHEAD PLUGIN DEFINITION
   * =========================== */

  $.fn.typeahead = function (option) {
    return this.each(function () {
      var $this = $(this)
        , data = $this.data('typeahead')
        , options = typeof option == 'object' && option
      if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
      if (typeof option == 'string') data[option]()
    })
  }

  $.fn.typeahead.defaults = {
    source: []
  , items: 8
  , container: '

'
  , header_html: '

'
  , menu: '

'
  , item: '

'
  , minLength: 1
  , stopAdvance: false
  , dropup: false
  , advanceKeyCodes: []
  }

  $.fn.typeahead.Constructor = Typeahead

 /*   TYPEAHEAD DATA-API
  * ================== */

  $(function () {
    $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
      var $this = $(this)
      if ($this.data('typeahead')) return
      e.preventDefault()
      $this.typeahead($this.data())
    })
  })

}(window.jQuery);
<|endoftext|>


<commit_before>/*
 *  this is a simple server for a diamond hunt
 *
 *  author: Jacek Kopecký, jacek@jacek.cz, http://github.com/jacekkopecky
 */

/*jslint node:true*/
'use strict';

/*
 *  general setup, loading libraries
 */
var bodyParser = require('body-parser'),
    assert = require('assert'),
    express = require('express'),
    config = require('./config.js'),
    app = express()

/*************************************************
 *
 *  set up server options and resources
 *
 *************************************************/

/*
 *  by default, express.js treats resource paths like "/foo" and "/foo/" as the same, we don't want that
 */
app.set('strict routing', true)

/*
 *  this will serve our static files
 */
app.use(express.static('static', config.expressStatic))

/*
 *  this will add support for JSON payloads in incoming data
 */
app.use(bodyParser.json(config.bodyParser_JSON))

/*
 *  redirect from /api/ to /api which is actually static/api.html - the API documentation
 */
app.get('/api/', function(req, res) { res.redirect('/api'); })

/*
 *  set up main application resources
 */
app.get('/api/diamonds/',        listDiamonds)
app.delete('/api/diamonds/:id',  claimDiamond)
app.get('/api/scores/',          listLeaderBoard)
app.get('/api/scores/reset',     resetLeaderBoard)
app.get('/api/scores/stop',      stopCreatingDiamonds)
app.get('/api/scores/start',     startCreatingDiamonds)

/*
 *  setup redirects to URIs with slash at the end so that relative URIs work better
 */
app.get('/api/diamonds',       redirectToSlash)
app.get('/api/scores',         redirectToSlash)

function redirectToSlash(req, res) { res.redirect(req.url + '/'); }

/*
 *  start server
 */
app.listen(config.port)
console.log("server started on port " + config.port)

console.log("creating diamonds");
setInterval(addDiamond, 1000)

/*************************************************
 *
 *  actual logic of the server
 *
 *************************************************/

/*
 *  clients waiting for a list of diamonds
 */
var waiting = []

/*
 *  a list of unclaimed diamonds
 */
var diamonds = []
var diamondNextId = 1;

/*
 *  status information
 */
var creatingDiamonds = true;
var leaderBoard = {}

/*
 *  list diamonds
 *  returns something like this:
 *  [ { x: 300,
 *      y: 400,
 *      isDiamond: true,
 *      color: "#0f0",
 *      id: 42
 *    },
 *    { x: 200,
 *      y: 173,
 *      isDiamond: true,
 *      color: "#f00",
 *      id: 57
 *    }
 *  ]
 */
function listDiamonds(req, res, next) {
  waiting.push(res)
}

// this function is also responsible for responding to listDiamonds requests
function addDiamond() {
  // diamond creation stopped, only send existing diamonds
  if (!creatingDiamonds || diamonds.length > 100) return notifyClients();

  // only add diamonds while there's somebody waiting for them
  if (!waiting.length) return;

  // add half as many diamonds as there are clients waiting for them
  for (var i=0; i<(waiting.length/2); i++) {
    diamonds.push({
      x: Math.floor(Math.random()*(config.canvasWidth-40)+20),
      y: Math.floor(Math.random()*(config.canvasHeight-170)+150),
      isDiamond: true,
      id: diamondNextId++,
      color: config.diamondColors[Math.floor(Math.random()*config.diamondColors.length)]
    })
  }
  notifyClients()
  console.log("diamonds  added: " + diamonds.length)
}

function notifyClients() {
  waiting.forEach(function (client) {
    client.send(diamonds)
  })
  waiting = []
}

function claimDiamond(req, res, next) {
  var diamondId = req.params.id;
  var found = diamonds.some(function(diamond, idx) {
    if (diamond.id == diamondId) {
      diamonds[idx] = diamonds[diamonds.length - 1]
      diamonds.pop()
      return true;
    }
    return false;
  })

  if (found) {
    var playerID = req.query.user;
    var playerName = req.query.name || playerID;

    var playerScore = leaderBoard[playerID] || (leaderBoard[playerID] = { score: 0 });
    playerScore.score += 1
    playerScore.name = playerName

    res.status(200).send({msg: "diamond " + diamondId + " is yours!", score: playerScore.score})
    // notifyClients()
    console.log("diamond claimed: " + diamonds.length + " (user: " + playerName + ")")
  } else {
    // if the diamond existed, say it's gone, otherwise say it's unknown
    var status  = diamondId < diamondNextId ? 410 : 404;
    var message = diamondId < diamondNextId ? "diamond gone: " : "diamond not found: ";
    res.status(status).send(message + diamondId)
  }
}

function resetLeaderBoard(req, res, next) {
  if (!isAllowedReferer(req, config.adminReferer)) return res.status(403).send("forbidden")
  creatingDiamonds = true;
  leaderBoard = {}
  diamonds = []
  console.log("reset and creating diamonds")
  res.send("reset and creating diamonds")
  notifyClients()
}

function stopCreatingDiamonds(req, res, next) {
  if (!isAllowedReferer(req, config.adminReferer)) return res.status(403).send("forbidden")
  creatingDiamonds = false;
  console.log("stopped creating diamonds")
  res.send("stopped creating diamonds")
}

function startCreatingDiamonds(req, res, next) {
  if (!isAllowedReferer(req, config.adminReferer)) return res.status(403).send("forbidden")
  creatingDiamonds = true;
  console.log("restarted creating diamonds")
  res.send("restarted creating diamonds")
}

function listLeaderBoard(req, res, next) {
  if (isEmpty(leaderBoard)) {
    res.send({none: { score: 0, name: "nobody is playing" }})
  } else {
    res.send(leaderBoard);
  }
}

function isAllowedReferer(req, referers) {
  if (referers == null) return false;
  if (referers == "*") return true;
  referers = [].concat(referers); // make sure referers is an array now
  return referers.indexOf(req.headers.referer) >= 0
}

/*************************************************
 *
 *  helpful functions
 *
 *************************************************/

function notImplemented(req, res) {
    res.status(501).send("this functionality is envisioned but not implemented yet\n");
}

function isEmpty(object) {
  return Object.getOwnPropertyNames(object).length === 0
}
<commit_msg>add referer logging<commit_after>/*
 *  this is a simple server for a diamond hunt
 *
 *  author: Jacek Kopecký, jacek@jacek.cz, http://github.com/jacekkopecky
 */

/*jslint node:true*/
'use strict';

/*
 *  general setup, loading libraries
 */
var bodyParser = require('body-parser'),
    assert = require('assert'),
    express = require('express'),
    config = require('./config.js'),
    app = express()

/*************************************************
 *
 *  set up server options and resources
 *
 *************************************************/

/*
 *  by default, express.js treats resource paths like "/foo" and "/foo/" as the same, we don't want that
 */
app.set('strict routing', true)

/*
 *  this will serve our static files
 */
app.use(express.static('static', config.expressStatic))

/*
 *  this will add support for JSON payloads in incoming data
 */
app.use(bodyParser.json(config.bodyParser_JSON))

/*
 *  redirect from /api/ to /api which is actually static/api.html - the API documentation
 */
app.get('/api/', function(req, res) { res.redirect('/api'); })

/*
 *  set up main application resources
 */
app.get('/api/diamonds/',        listDiamonds)
app.delete('/api/diamonds/:id',  claimDiamond)
app.get('/api/scores/',          listLeaderBoard)
app.get('/api/scores/reset',     resetLeaderBoard)
app.get('/api/scores/stop',      stopCreatingDiamonds)
app.get('/api/scores/start',     startCreatingDiamonds)

/*
 *  setup redirects to URIs with slash at the end so that relative URIs work better
 */
app.get('/api/diamonds',       redirectToSlash)
app.get('/api/scores',         redirectToSlash)

function redirectToSlash(req, res) { res.redirect(req.url + '/'); }

/*
 *  start server
 */
app.listen(config.port)
console.log("server started on port " + config.port)

console.log("creating diamonds");
setInterval(addDiamond, 1000)

/*************************************************
 *
 *  actual logic of the server
 *
 *************************************************/

/*
 *  clients waiting for a list of diamonds
 */
var waiting = []

/*
 *  a list of unclaimed diamonds
 */
var diamonds = []
var diamondNextId = 1;

/*
 *  status information
 */
var creatingDiamonds = true;
var leaderBoard = {}

/*
 *  list diamonds
 *  returns something like this:
 *  [ { x: 300,
 *      y: 400,
 *      isDiamond: true,
 *      color: "#0f0",
 *      id: 42
 *    },
 *    { x: 200,
 *      y: 173,
 *      isDiamond: true,
 *      color: "#f00",
 *      id: 57
 *    }
 *  ]
 */
function listDiamonds(req, res, next) {
  waiting.push(res)
}

// this function is also responsible for responding to listDiamonds requests
function addDiamond() {
  // diamond creation stopped, only send existing diamonds
  if (!creatingDiamonds || diamonds.length > 100) return notifyClients();

  // only add diamonds while there's somebody waiting for them
  if (!waiting.length) return;

  // add half as many diamonds as there are clients waiting for them
  for (var i=0; i<(waiting.length/2); i++) {
    diamonds.push({
      x: Math.floor(Math.random()*(config.canvasWidth-40)+20),
      y: Math.floor(Math.random()*(config.canvasHeight-170)+150),
      isDiamond: true,
      id: diamondNextId++,
      color: config.diamondColors[Math.floor(Math.random()*config.diamondColors.length)]
    })
  }
  notifyClients()
  console.log("diamonds  added: " + diamonds.length)
}

function notifyClients() {
  waiting.forEach(function (client) {
    client.send(diamonds)
  })
  waiting = []
}

function claimDiamond(req, res, next) {
  var diamondId = req.params.id;
  var found = diamonds.some(function(diamond, idx) {
    if (diamond.id == diamondId) {
      diamonds[idx] = diamonds[diamonds.length - 1]
      diamonds.pop()
      return true;
    }
    return false;
  })

  if (found) {
    var playerID = req.query.user;
    var playerName = req.query.name || playerID;

    var playerScore = leaderBoard[playerID] || (leaderBoard[playerID] = { score: 0 });
    playerScore.score += 1
    playerScore.name = playerName

    res.status(200).send({msg: "diamond " + diamondId + " is yours!", score: playerScore.score})
    // notifyClients()
    console.log("diamond claimed: " + diamonds.length + " (user: " + playerName + ")")
  } else {
    // if the diamond existed, say it's gone, otherwise say it's unknown
    var status  = diamondId < diamondNextId ? 410 : 404;
    var message = diamondId < diamondNextId ? "diamond gone: " : "diamond not found: ";
    res.status(status).send(message + diamondId)
  }
}

function resetLeaderBoard(req, res, next) {
  if (!isAllowedReferer(req, config.adminReferer)) return res.status(403).send("forbidden")
  creatingDiamonds = true;
  leaderBoard = {}
  diamonds = []
  console.log("reset and creating diamonds")
  res.send("reset and creating diamonds")
  notifyClients()
}

function stopCreatingDiamonds(req, res, next) {
  if (!isAllowedReferer(req, config.adminReferer)) return res.status(403).send("forbidden")
  creatingDiamonds = false;
  console.log("stopped creating diamonds")
  res.send("stopped creating diamonds")
}

function startCreatingDiamonds(req, res, next) {
  if (!isAllowedReferer(req, config.adminReferer)) return res.status(403).send("forbidden")
  creatingDiamonds = true;
  console.log("restarted creating diamonds")
  res.send("restarted creating diamonds")
}

function listLeaderBoard(req, res, next) {
  if (isEmpty(leaderBoard)) {
    res.send({none: { score: 0, name: "nobody is playing" }})
  } else {
    res.send(leaderBoard);
  }
}

function isAllowedReferer(req, referers) {
  if (referers == null) return false;
  if (referers == "*") {
    console.log("incoming referer: " + req.headers.referer);
    return true;
  }
  referers = [].concat(referers); // make sure referers is an array now
  return referers.indexOf(req.headers.referer) >= 0
}

/*************************************************
 *
 *  helpful functions
 *
 *************************************************/

function notImplemented(req, res) {
    res.status(501).send("this functionality is envisioned but not implemented yet\n");
}

function isEmpty(object) {
  return Object.getOwnPropertyNames(object).length === 0
}
<|endoftext|>


<commit_before>// Setup basic express server
var auth = require('http-auth');
var basic = auth.basic({
	realm: "Simon Area.",
	file: __dirname+"/.htpasswd" // gevorg:gpass, Sarah:testpass ...
});

var express = require('express');
var app = express();

var server = require('http').createServer(basic,app);

var io = require('socket.io')(server);
var port = process.env.PORT || 80;
var fs = require('fs');

var WebTorrent = require('webtorrent');
var client = new WebTorrent();

server.listen(port, function () {
  log('Server listening at port '+port);
});

// Routing
app.use(express.static(__dirname + '/public'));

io.on('connection', function (socket) {

	socket.on('download-t',function(url){
		client.add(url, {path: __dirname+"/public/downloads/"}, function (torrent) {
		  	// Got torrent metadata!
		  	log('Server is downloading:'+torrent.name);

		  	torrent.on('done', function(){
	  			log('Server finish downloading:'+torrent.name);
	  			torrent.destroy();
			})

		  	socket.emit('download');
		});
	});

	socket.on('list-t',function(){
		socket.emit('list-t',listTorrents());
	});

	socket.on('list-d',function(dir){
		fs.readdir(__dirname+"/public/downloads/"+dir, function(err, files){
			if(err) {
	        	return log(err);
	    	}
	    	var list = {};
	    	if(files.length > 0){
		    	files.forEach(function(file){
		    		fs.stat(__dirname+"/public/downloads/"+dir+file,function(err, stats){
		    			list[file] = stats;
		    			list[file].isfile = stats.isFile();
		    			list[file].isdir = stats.isDirectory()
		    			if(Object.keys(list).length == files.length){
		    				socket.emit('list-d',list);
		    			}
		    		});
		    	});
		    } else {
		    	socket.emit('list-d',{});
		    }
		});
	});

	socket.on('remove-d',function(file){
		fs.unlink(__dirname+"/public/downloads/"+file, function(err){
  			if (err) throw err;
			socket.emit('remove-d');
		});
	});

});

function log(text){
	console.log(text);
}

function listTorrents(){
	var torrents = [];
	client.torrents.forEach(function (torrent){
		var t = {};
		t.name = torrent.name;
		t.size = torrent.length;
		if(torrent.swarm){
			t.sdown = torrent.swarm.downloadSpeed();
			t.sup = torrent.swarm.uploadSpeed();
			t.down = torrent.swarm.downloaded;
			t.up = torrent.swarm.uploaded;
		}
		torrents.push(t);
	});

	return torrents;
}

<commit_msg> plop<commit_after>// Setup basic express server
var auth = require('http-auth');
var basic = auth.basic({
	realm: "Simon Area.",
	file: __dirname+"/.htpasswd" // gevorg:gpass, Sarah:testpass ...
});

var express = require('express');
var app = express();

var server = require('http').createServer(basic,app);

var io = require('socket.io')(server);
var port = process.env.PORT || 5000;
var fs = require('fs');

var WebTorrent = require('webtorrent');
var client = new WebTorrent();

server.listen(port, function () {
  log('Server listening at port '+port);
});

// Routing
app.use(express.static(__dirname + '/public'));

io.on('connection', function (socket) {

	socket.on('download-t',function(url){
		client.add(url, {path: __dirname+"/public/downloads/"}, function (torrent) {
		  	// Got torrent metadata!
		  	log('Server is downloading:'+torrent.name);

		  	torrent.on('done', function(){
	  			log('Server finish downloading:'+torrent.name);
	  			torrent.destroy();
			})

		  	socket.emit('download');
		});
	});

	socket.on('list-t',function(){
		socket.emit('list-t',listTorrents());
	});

	socket.on('list-d',function(dir){
		fs.readdir(__dirname+"/public/downloads/"+dir, function(err, files){
			if(err) {
	        	return log(err);
	    	}
	    	var list = {};
	    	if(files.length > 0){
		    	files.forEach(function(file){
		    		fs.stat(__dirname+"/public/downloads/"+dir+file,function(err, stats){
		    			list[file] = stats;
		    			list[file].isfile = stats.isFile();
		    			list[file].isdir = stats.isDirectory()
		    			if(Object.keys(list).length == files.length){
		    				socket.emit('list-d',list);
		    			}
		    		});
		    	});
		    } else {
		    	socket.emit('list-d',{});
		    }
		});
	});

	socket.on('remove-d',function(file){
		fs.unlink(__dirname+"/public/downloads/"+file, function(err){
  			if (err) throw err;
			socket.emit('remove-d');
		});
	});

});

function log(text){
	console.log(text);
}

function listTorrents(){
	var torrents = [];
	client.torrents.forEach(function (torrent){
		var t = {};
		t.name = torrent.name;
		t.size = torrent.length;
		if(torrent.swarm){
			t.sdown = torrent.swarm.downloadSpeed();
			t.sup = torrent.swarm.uploadSpeed();
			t.down = torrent.swarm.downloaded;
			t.up = torrent.swarm.uploaded;
		}
		torrents.push(t);
	});

	return torrents;
}

<|endoftext|>


<commit_before>'use strict';

const express = require('express');
const Slapp = require('slapp');
const Utils = require('./Utils');
const ConvoStore = require('slapp-convo-beepboop');
const Context = require('slapp-context-beepboop');
const ApiHelper = require('./ApiHelper');

// use `PORT` env var on Beep Boop - default to 3000 locally
var port = process.env.PORT || 3000

var slapp = Slapp({
  // Beep Boop sets the SLACK_VERIFY_TOKEN env var
  verify_token: process.env.SLACK_VERIFY_TOKEN,
  convo_store: ConvoStore(),
  context: Context()
})

// enable debugging
require('beepboop-slapp-presence-polyfill')(slapp, {
  debug: true
})

//*********************************************
// Register handler
//*********************************************
slapp.message('^.register', ['direct_message'], (msg) => {
  ApiHelper.isPlayerRegistered(msg.meta.user_id)
  .then((isRegistered) => {
    if (!isRegistered) {
      msg.say({
        text: '',
        attachments: [{
          fallback: 'Do you want to register ?',
          title: 'Do you want to join the Akeneo Baby Foot Star League (ABSL) ?',
          callback_id: 'register_callback',
          color: '#3AA3E3',
          attachment_type: 'default',
          actions: [{
            name: 'register_answer',
            text: 'Hell yeah!',
            style: 'primary',
            type: 'button',
            value: 'yes'
          },
          {
            name: 'register_answer',
            text: 'Nope, not intrested',
            type: 'button',
            value: 'no'
          }]
        }]
      })
    } else {
      msg.say('You are already enrolled into the Akeneo Baby Foot Star League');
      // TODO: show rankings
      // TODO: Show challengers
    }
  })
  .catch(function(error) {
    // TODO: Send message if an error occured
  });
})

// Two != callbacks for this
slapp.action('register_callback', 'register_answer', (msg, value) => {
  var registerAnswer = '';

  if (value === 'yes') {
    registerAnswer = 'Awesome! let me register your account before you can start playing.';
    ApiHelper.registerPlayer(msg.meta.user_id);
  } else {
    registerAnswer= 'Alright, then come back to me when you are ready! :soccer:';
    // TODO: Call the helper route
  }

  var responseAnswer = {
    text: '',
    attachments: [{
      fallback: 'Do you want to register ?',
      title: 'Do you want to join the Akeneo Baby Foot Star League (ABSL) ?',
      text: registerAnswer,
      callback_id: 'register_callback',
      color: '#3AA3E3',
    }]
  };

  msg.respond(msg.body.response_url, responseAnswer);
  // TODO: show ranks
  // TODO: show possible challengers');
});

//*********************************************
// Feature match logging: .win
//*********************************************
// TODO: Protected route
slapp.message('^.win <@([^>]+)> ([^>]+)\s*-\s*([^>]+)$', ['direct_message'], (msg, text, loserId, winnerScore, loserScore) => {
  var winnerId = msg.meta.user_id;
  winnerScore = Math.floor(parseInt(winnerScore, 10));
  loserScore = Math.floor(parseInt(loserScore, 10));

  var isInputError = false;
  var textResponse = [];

  // Parsing score values
  if (isNaN(winnerScore) || isNaN(loserScore)) {
    // TODO: Print usage();
    textResponse.push('The scores are not valid values.');
  } else {
    if (loserScore >= winnerScore) {
      isInputError = true;
      textResponse.push(`- Winner\'s score *(${winnerScore})* cannot be greater than the loser\'s score *(${loserScore})*.`);
    }
    if (winnerScore !== 10) {
      isInputError = true;
      textResponse.push(`- Winner\'s score *(${winnerScore})* should be 10.`);
    }
  }

  // Checking player's registration
  if (winnerId === loserId) {
    isInputError = true;
    textResponse.push('- Winner and loser cannot be the same user.');
  }

  ApiHelper.isPlayerRegistered(loserId)
  .then((isRegistered) => {
    if (isRegistered === false) {
      isInputError = true;
      textResponse.push(`- Losing player (<@${loserId}>) is not registered in the Akeneo Baby Foot League.`);
      // TODO: Show the list of user with the rankings
    }

    if (isInputError) {
      msg.say('Oups, an error occured while saving the game result.\n' + textResponse.join('\n'));
    } else {
      var state = {winnerId: winnerId, loserId: loserId, winnerScore, loserScore};
      msg
      .say(`Congratz for this huge win ! Let me check the result with <@${loserId}>.\n I\'ll come back to you when I\m done.`)
      .route('handle_match_confirmation', state, 600);
    }
  })
  .catch((error) => {
    console.log('An error occured while checking if the loserId is registered');
    console.log(error);
  });
});

slapp.route('handle_match_confirmation', (msg, state) => {
  msg.say({
    channel: state.loserId,
    as_user: true,
    text: '',
    attachments: [{
      fallback: 'Match log confirmation',
      title: `Do you confirm that you lost ${state.winnerScore}-${state.loserScore} against <@${state.winnerId}> ?`,
      callback_id: 'match_confirmation_callback',
      color: '#3AA3E3',
      attachment_type: 'default',
      actions: [{
        name: 'match_confirmation_yes',
        text: 'Yep, good game.',
        style: 'primary',
        type: 'button',
        value: Utils.marshall({ state: state, value: 'yes' })
      },
      {
        name: 'match_confirmation_no',
        text: 'NO WAY ! That\' a lie!',
        type: 'button',
        value: Utils.marshall({ state: state, value: 'no' })
      }]
    }]
  });
});

slapp.action('match_confirmation_callback', 'match_confirmation_yes', (msg, args) => {
  args = Utils.unmarshall(args);

  if (typeof(args.state) !== 'undefined') {
    var state = args.state;
    ApiHelper.addMatchResult(state.winnerId, state.loserId, state.winnerScore, state.loserScore)
    .then(() => {
      msg.respond(msg.body.response_url, 'Ok, the match result has been successfully registered.');
      ApiHelper.refreshRank(state.winnerId, state.loserId)
      .then(() => {
        // Show leaderboard and challengers
        msg.route('show_leaderboard', {playerId: state.loserId});
        msg.route('show_challengers', {playerId: state.loserId});

        msg.route('show_leaderboard', {playerId: state.winnerId});
        msg.route('show_challengers', {playerId: state.winnerId});
      });
    });
  }
});

slapp.action('match_confirmation_callback', 'match_confirmation_no', (msg, args) => {
  args = Utils.unmarshall(args);

  if (typeof(args.state) !== 'undefined') {
    var state = args.state;
    ApiHelper.addMatchResult(state.winnerId, state.loserId, state.winnerScore, state.loserScore)
    .then(()=> {
      msg.respond(msg.body.response_url, `Ok, You\'ll have to see this IRL with <@${state.winnerId}>`);
    });
  }
});

//*********************************************
// Feature leaderboard
//*********************************************
slapp.message('^.leaderboard', ['direct_message'], (msg, text) => {
  var state = {playerId: msg.meta.user_id};
  msg.route('show_leaderboard', state);
})

slapp.route('show_leaderboard', (msg, state) => {
  ApiHelper.getRankings().then((rankings) => {
    var leaderBoard = rankings.map((element) => {
      return element.rank + "- <@" + element.playerId + ">"
    });

    msg.say({
      channel: state.playerId,
      as_user: true,
      text: leaderBoard.join('\n')
    });
  });
});

slapp.message('^.my-games$', ['direct_message'], (msg, text) => {
  var playerId = msg.meta.user_id;
  ApiHelper.getPlayerGames(playerId).then((playerGames) => {
    playerGames = playerGames.map((game) => {
      var matchResult = '';
      var opponentId = '';

      if (game.winnerId === playerId) {
        matchResult += '*win*';
        opponentId = game.loserId;
      } else {
        matchResult += '*lost*';
        opponentId = game.winnerId;
      }

      var date = new Date(game.date);
      var dateDescription = date.getDate() + '/' + (date.getMonth()+1) + '/' + date.getFullYear();

      return `${dateDescription} - ${matchResult} against <@${opponentId}> ${game.winnerScore}-${game.loserScore}`;
    });

    msg.say({
      text: 'Here is the list of all the games you played:\n' + playerGames.join('\n')
    })
  });
});

slapp.message('^.last-games$', ['direct_message'], (msg, text) => {
  ApiHelper.getGames().then((latestGames) => {
    latestGames = latestGames.map((game) => {
      var date = new Date(game.date);
      var dateDescription = date.getDate() + '/' + (date.getMonth()+1) + '/' + date.getFullYear();

      return `${dateDescription} - <@${game.winnerId}> won against <@${game.loserId}> ${game.winnerScore}-${game.loserScore}`;
    });

    msg.say({
      text: 'Here is the list of the last 15 games:\n' + latestGames.join('\n')
    })
  });
});

//*********************************************
// Feature challengers
//*********************************************
slapp.message('^.challengers', ['direct_message'], (msg, text) => {
  var state = {playerId: msg.meta.user_id};
  msg.route('show_challengers', state);
})

slapp.route('show_challengers', (msg, state) => {
  ApiHelper.getChallengers(state.playerId).then((challengers) => {
    var messages = [];

    if (typeof(challengers.toBeat) !== 'undefined') {
      messages.push('To go up in the leader board, you need to beat <@' + challengers.toBeat.playerId + '> ('+challengers.toBeat.rank+' th)');
    }

    if (typeof(challengers.notToLose) !== 'undefined') {
      messages.push('Don\'t *lose* against <@' + challengers.notToLose.playerId + '> ('+challengers.notToLose.rank+' th) or you\'ll go down in the leaderboard :s');
    }

    msg.say({
      channel: state.playerId,
      as_user: true,
      text: messages.join('\n')
    });
  });
});

//*********************************************
// Help handler .wcid
//*********************************************
slapp.message('help|.wcid', ['mention', 'direct_message'], (msg) => {
  var HELP_TEXT = `
  Hello, I'm luigi the Akeneo Baby Foot referee. Here is the information I can provide:
  \`.wcid\` - to get some help.
  \`.register\` - to see this message.
  \`.win <LOSING-PLAYER> <WINNER-SCORE>-<LOSER-SCORE>\` - to log a game result you have won.
  \`.leaderboard\` - Show the Baby foot leaderboard.
  \`.challengers\` - Show the next challengers you need to take on!
  \`.last-games\` - See the last 15 games results played in the league.
  \`.my-games\` - See all your games results in the baby foot league.
  `
  msg.say(HELP_TEXT)
})

// attach Slapp to express server
var server = slapp.attachToExpress(express())

// start http server
server.listen(port, (err) => {
  if (err) {
    return console.error(err)
  }

  console.log(`Listening on port ${port}`)
})
<commit_msg>Fix async send messages list<commit_after>'use strict';

const express = require('express');
const Slapp = require('slapp');
const Utils = require('./Utils');
const ConvoStore = require('slapp-convo-beepboop');
const Context = require('slapp-context-beepboop');
const ApiHelper = require('./ApiHelper');

// use `PORT` env var on Beep Boop - default to 3000 locally
var port = process.env.PORT || 3000

var slapp = Slapp({
  // Beep Boop sets the SLACK_VERIFY_TOKEN env var
  verify_token: process.env.SLACK_VERIFY_TOKEN,
  convo_store: ConvoStore(),
  context: Context()
})

// enable debugging
require('beepboop-slapp-presence-polyfill')(slapp, {
  debug: true
})
/***************************************************
//
/**************************************************/
var sendMatchConfirmation = (msg, state) => {
  msg.say({
    channel: state.loserId,
    as_user: true,
    text: '',
    attachments: [{
      fallback: 'Match log confirmation',
      title: `Do you confirm that you lost ${state.winnerScore}-${state.loserScore} against <@${state.winnerId}> ?`,
      callback_id: 'match_confirmation_callback',
      color: '#3AA3E3',
      attachment_type: 'default',
      actions: [{
        name: 'match_confirmation_yes',
        text: 'Yep, good game.',
        style: 'primary',
        type: 'button',
        value: Utils.marshall({ state: state, value: 'yes' })
      },
        {
          name: 'match_confirmation_no',
          text: 'NO WAY ! That\' a lie!',
          type: 'button',
          value: Utils.marshall({ state: state, value: 'no' })
        }]
    }]
  });
};

var sendLeaderboard = (msg, playerId) => {
  msg.say({
      channel: playerId,
      as_user: true,
      text: '*Leaderboard:*'
  });

  ApiHelper.getRankings().then((rankings) => {
    var leaderBoard = rankings.map((element) => {
      return element.rank + "- <@" + element.playerId + ">"
    });

    msg.say({
      channel: playerId,
      as_user: true,
      text: leaderBoard.join('\n')
    });
  });
};

var sendChallengers = (msg, playerId) => {
  msg.say({
    channel: playerId,
    as_user: true,
    text: '*List of challengers:*'
  });

  ApiHelper.getChallengers(playerId).then((challengers) => {
    var messages = [];

    if (typeof(challengers.toBeat) !== 'undefined') {
      messages.push(':up: if you beat <@' + challengers.toBeat.playerId + '> ('+challengers.toBeat.rank+' th)');
    }

    if (typeof(challengers.notToLose) !== 'undefined') {
      messages.push(':down: if you lose against <@' + challengers.notToLose.playerId + '> ('+challengers.notToLose.rank+' th)');
    }

    msg.say({
      channel: playerId,
      as_user: true,
      text: messages.join('\n')
    });
  });
};

//*********************************************
// Register handler
//*********************************************
slapp.message('^.register', ['direct_message'], (msg) => {
  ApiHelper.isPlayerRegistered(msg.meta.user_id)
  .then((isRegistered) => {
    if (!isRegistered) {
      msg.say({
        text: '',
        attachments: [{
          fallback: 'Do you want to register ?',
          title: 'Do you want to join the Akeneo Baby Foot Star League (ABSL) ?',
          callback_id: 'register_callback',
          color: '#3AA3E3',
          attachment_type: 'default',
          actions: [{
            name: 'register_answer',
            text: 'Hell yeah!',
            style: 'primary',
            type: 'button',
            value: 'yes'
          },
          {
            name: 'register_answer',
            text: 'Nope, not intrested',
            type: 'button',
            value: 'no'
          }]
        }]
      })
    } else {
      msg.say('You are already enrolled into the Akeneo Baby Foot Star League');
      sendLeaderboard(msg, msg.meta.user_id);
      sendChallengers(msg, msg.meta.user_id);
    }
  })
  .catch(function(error) {
    // TODO: Send message if an error occured
  });
})

// Two != callbacks for this
slapp.action('register_callback', 'register_answer', (msg, value) => {
  var registerAnswer = '';

  if (value === 'yes') {
    registerAnswer = 'Awesome! let me register your account before you can start playing.';
    ApiHelper.registerPlayer(msg.meta.user_id);
  } else {
    registerAnswer = 'Alright, then come back to me when you are ready! :soccer:';
    // TODO: Call the helper route
  }

  var responseAnswer = {
    text: '',
    attachments: [{
      fallback: 'Do you want to register ?',
      title: 'Do you want to join the Akeneo Baby Foot Star League (ABSL) ?',
      text: registerAnswer,
      callback_id: 'register_callback',
      color: '#3AA3E3',
    }]
  };

  msg.respond(msg.body.response_url, responseAnswer);
  sendLeaderboard(msg, msg.meta.user_id);
  sendChallengers(msg, msg.meta.user_id);
});

//*********************************************
// Feature match logging: .win
//*********************************************
// TODO: Protected route
slapp.message('^.win <@([^>]+)> ([^>]+)\s*-\s*([^>]+)$', ['direct_message'], (msg, text, loserId, winnerScore, loserScore) => {
  var winnerId = msg.meta.user_id;
  winnerScore = Math.floor(parseInt(winnerScore, 10));
  loserScore = Math.floor(parseInt(loserScore, 10));

  var isInputError = false;
  var textResponse = [];

  // Parsing score values
  if (isNaN(winnerScore) || isNaN(loserScore)) {
    textResponse.push('The scores are not valid values.');
  } else {
    if (loserScore >= winnerScore) {
      isInputError = true;
      textResponse.push(`- Winner\'s score *(${winnerScore})* cannot be greater than the loser\'s score *(${loserScore})*.`);
    }
    if (winnerScore !== 10) {
      isInputError = true;
      textResponse.push(`- Winner\'s score *(${winnerScore})* should be 10.`);
    }
  }

  // Checking player's registration
  if (winnerId === loserId) {
    isInputError = true;
    textResponse.push('- Winner and loser cannot be the same user.');
  }

  ApiHelper.isPlayerRegistered(loserId)
  .then((isRegistered) => {
    if (isRegistered === false) {
      isInputError = true;
      textResponse.push(`- Losing player (<@${loserId}>) is not registered in the Akeneo Baby Foot League.`);
      // TODO: Show the list of user with the rankings
    }

    if (isInputError) {
      msg.say('Oups, an error occured while saving the game result.\n' + textResponse.join('\n'));
    } else {
      var state = {winnerId: winnerId, loserId: loserId, winnerScore, loserScore};
      msg
      .say(`Congratz for this huge win ! Let me check the result with <@${loserId}>.\n I\'ll come back to you when I\m done.`);
      sendMatchConfirmation(msg, state);
    }
  })
  .catch((error) => {
    console.log('An error occured while checking if the loserId is registered');
    console.log(error);
  });
});

slapp.action('match_confirmation_callback', 'match_confirmation_yes', (msg, args) => {
  args = Utils.unmarshall(args);

  if (typeof(args.state) !== 'undefined') {
    var state = args.state;
    ApiHelper.addMatchResult(state.winnerId, state.loserId, state.winnerScore, state.loserScore)
    .then(() => {
      msg.respond(msg.body.response_url, 'Ok, the match result has been successfully registered.');
      ApiHelper.refreshRank(state.winnerId, state.loserId)
      .then(() => {
        // Show leaderboard and challengers
        sendLeaderboard(msg, state.loserId);
        sendChallengers(msg, state.loserId);

        msg.say({
          channel: state.winnerId,
          as_user: true,
          text: `<@${state.loserId}> confirmed your victory :tada:. Good game.`
        });
        sendLeaderboard(msg, state.winnerId);
        sendChallengers(msg, state.winnerId);
      });
    });
  }
});

slapp.action('match_confirmation_callback', 'match_confirmation_no', (msg, args) => {
  args = Utils.unmarshall(args);

  if (typeof(args.state) !== 'undefined') {
    var state = args.state;
    ApiHelper.addMatchResult(state.winnerId, state.loserId, state.winnerScore, state.loserScore)
    .then(()=> {
      msg.respond(msg.body.response_url, `Ok, You\'ll have to see this IRL with <@${state.winnerId}>`);
    });
  }
});

//*********************************************
// Feature leaderboard
//*********************************************
slapp.message('^.leaderboard', ['direct_message'], (msg, text) => {
  sendLeaderboard(msg, msg.meta.user_id);
})

slapp.message('^.my-games$', ['direct_message'], (msg, text) => {
  var playerId = msg.meta.user_id;
  ApiHelper.getPlayerGames(playerId).then((playerGames) => {
    playerGames = playerGames.map((game) => {
      var matchResult = '';
      var opponentId = '';

      if (game.winnerId === playerId) {
        matchResult += '*win*';
        opponentId = game.loserId;
      } else {
        matchResult += '*lost*';
        opponentId = game.winnerId;
      }

      var date = new Date(game.date);
      var dateDescription = date.getDate() + '/' + (date.getMonth()+1) + '/' + date.getFullYear();

      return `${dateDescription} - ${matchResult} against <@${opponentId}> ${game.winnerScore}-${game.loserScore}`;
    });

    msg.say({
      text: 'Here is the list of all the games you played:\n' + playerGames.join('\n')
    })
  });
});

slapp.message('^.last-games$', ['direct_message'], (msg, text) => {
  ApiHelper.getGames().then((latestGames) => {
    latestGames = latestGames.map((game) => {
      var date = new Date(game.date);
      var dateDescription = date.getDate() + '/' + (date.getMonth()+1) + '/' + date.getFullYear();

      return `${dateDescription} - <@${game.winnerId}> won against <@${game.loserId}> ${game.winnerScore}-${game.loserScore}`;
    });

    msg.say({
      text: 'Here is the list of the last 15 games:\n' + latestGames.join('\n')
    })
  });
});

//*********************************************
// Feature challengers
//*********************************************
slapp.message('^.challengers', ['direct_message'], (msg, text) => {
  sendChallengers(msg, msg.meta.user_id);
});

//*********************************************
// Help handler .wcid
//*********************************************
slapp.message('help|.wcid', ['mention', 'direct_message'], (msg) => {
  var HELP_TEXT = `
  Hello, I'm Pierluigi the Akeneo Baby Foot referee. Here is the information I can provide you:
  \`.wcid\` - to get some help.
  \`.register\` - to see this message.
  \`.win <LOSING-PLAYER> <WINNER-SCORE>-<LOSER-SCORE>\` - to log a game result you have won.
  \`.leaderboard\` - Show the Baby foot leaderboard.
  \`.challengers\` - Show the next challengers you need to take on!
  \`.last-games\` - See the last 15 games results played in the league.
  \`.my-games\` - See all your games results in the baby foot league.
  `
  msg.say(HELP_TEXT)
})

// attach Slapp to express server
var server = slapp.attachToExpress(express())

// start http server
server.listen(port, (err) => {
  if (err) {
    return console.error(err)
  }

  console.log(`Listening on port ${port}`)
})
<|endoftext|>


<commit_before>/**
 * Module dependencies
 */

var stack = require('poe-ui-kit');
var proxy = require('simple-http-proxy');
var envs = require('envs');
var fs = require('fs');
var read = fs.readFileSync;
var exists = fs.existsSync;

/**
 * Forwarding headers
 */

var headers = {
  port: 'x-orig-port',
  proto: 'x-orig-proto',
  path: 'x-orig-path',
  host: 'x-orig-host'
};

/**
 * Create a poe-ui app
 */

exports = module.exports = function(routesPath, opts) {
  if (!routesPath || typeof routesPath !== 'string') throw new Error('Missing routes path parameter');
  opts = opts || {};

  var app = stack(opts);

  var API_URL = opts.apiUrl || envs('API_URL');
  if (API_URL) app.useBefore('base', '/api', 'api-proxy', proxy(API_URL, {xforward: headers, onrequest: function(opts) {
    delete opts.headers['if-none-match'];
    delete opts.headers.connection;
  }}));

  // TODO init auth
  // initAuth(app, opts.auth)

  // remove the middleware we don't need
  app.remove('methodOverride');
  app.remove('json');
  app.remove('urlencoded');

  var routes = loadRoutes(routesPath);

  mountRoutes(app, routes, opts.restricted);

  // setup loaders
  app.builder.module.loaders.push(
    {test: /\.(js)$/, exclude: /node_modules/, loader: 'esnext-loader'},
    {test: /\.(js)$/, include: /node_modules\/[^\/]+\/blocks/, loader: 'esnext-loader'}
  );
  app.builder.resolve.extensions.push('.jade');
  app.builder.addLoader('jade', (process.env.NODE_ENV === 'development' ? 'react-component-loader!' : '') + 'esnext-loader!onus-loader!ast2template-loader!jade2ast-loader');

  app.builder.addLoader(/\.(ess\?(dynamic|raw))$/, 'ess-loader!esnext-loader!ast2template?pass-through=1!ess2ast-loader');
  app.builder.addStyle(/\.(ess)$/, 'css-loader!autoprefixer-loader!ess-loader!esnext-loader!ast2template?pass-through=1!ess2ast-loader');

  app.builder.addLoader('yml', 'json-loader!yaml-loader');

  var rootFile = app.builder.sourcedir + '/root.js';
  app.builder.ast2template = {
    root: exists(rootFile) ? rootFile : null
  };

  // TODO mount the app api
  // api(app, routes);

  return app;
};

/**
 * Expose the middleware
 */

exports.middleware = stack.middleware;

function loadRoutes(routesPath) {
  // var routes = read(routesPath, 'utf8');
  // TODO
};

function mountRoutes(app, routes, restricted) {
  app.get('*', isNotReserved, function(req, res) {
    res.render('index.jade');
  });
}

var re = /^\/(build|api).*/;
function isNotReserved(req, res, next) {
  if (re.test(req.url)) return next('route');
  next();
}
<commit_msg>don't use the router for index rendering<commit_after>/**
 * Module dependencies
 */

var stack = require('poe-ui-kit');
var proxy = require('simple-http-proxy');
var envs = require('envs');
var fs = require('fs');
var read = fs.readFileSync;
var exists = fs.existsSync;

/**
 * Forwarding headers
 */

var headers = {
  port: 'x-orig-port',
  proto: 'x-orig-proto',
  path: 'x-orig-path',
  host: 'x-orig-host'
};

/**
 * Create a poe-ui app
 */

exports = module.exports = function(routesPath, opts) {
  if (!routesPath || typeof routesPath !== 'string') throw new Error('Missing routes path parameter');
  opts = opts || {};

  var app = stack(opts);

  var API_URL = opts.apiUrl || envs('API_URL');
  if (API_URL) app.useBefore('base', '/api', 'api-proxy', proxy(API_URL, {xforward: headers, onrequest: function(opts) {
    delete opts.headers['if-none-match'];
    delete opts.headers.connection;
  }}));

  // TODO init auth
  // initAuth(app, opts.auth)

  // remove the middleware we don't need
  app.remove('methodOverride');
  app.remove('json');
  app.remove('urlencoded');

  var routes = loadRoutes(routesPath);

  mountRoutes(app, routes, opts.restricted);

  // setup loaders
  app.builder.module.loaders.push(
    {test: /\.(js)$/, exclude: /node_modules/, loader: 'esnext-loader'},
    {test: /\.(js)$/, include: /node_modules\/[^\/]+\/blocks/, loader: 'esnext-loader'}
  );
  app.builder.resolve.extensions.push('.jade');
  app.builder.addLoader('jade', (process.env.NODE_ENV === 'development' ? 'react-component-loader!' : '') + 'esnext-loader!onus-loader!ast2template-loader!jade2ast-loader');

  app.builder.addLoader(/\.(ess\?(dynamic|raw))$/, 'ess-loader!esnext-loader!ast2template?pass-through=1!ess2ast-loader');
  app.builder.addStyle(/\.(ess)$/, 'css-loader!autoprefixer-loader!ess-loader!esnext-loader!ast2template?pass-through=1!ess2ast-loader');

  app.builder.addLoader('yml', 'json-loader!yaml-loader');

  var rootFile = app.builder.sourcedir + '/root.js';
  app.builder.ast2template = {
    root: exists(rootFile) ? rootFile : null
  };

  // TODO mount the app api
  // api(app, routes);

  return app;
};

/**
 * Expose the middleware
 */

exports.middleware = stack.middleware;

function loadRoutes(routesPath) {
  // var routes = read(routesPath, 'utf8');
  // TODO
}

function mountRoutes(app, routes, restricted) {
  var re = /^\/(build|api).*/;
  app.useAfter('router', function indexPage(req, res, next) {
    if (re.test(req.url)) return next();
    res.render('index.jade');
  });
}
<|endoftext|>


<commit_before>import React from "react";
import { Popper, Typography, Grid } from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import DropArea from "./DropArea";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

export default function OptimizationForm(props) {
  return (
    <DndProvider backend={HTML5Backend}>
      <Popper
        open={Boolean(props.anchorEl)}
        anchorEl={props.anchorEl}
        style={{
          background: "white",
          overflowY: "scroll",
          boxShadow: "0px 10px 20px rgba(0, 0, 0, 0.2)",
        }}
      >
        <Grid
          noWrap
          container
          direction="column"
          className="popper_class"
          spacing={0}
          style={{
            position: "relative",
            height: "850px",
            overflowY: "scroll",
            width: "600px",
            margin: "0",
          }}
        >
          <Grid item justify="center">
            <Typography variant={"h4"} gutterBottom>
              Optimization Form
            </Typography>
          </Grid>
          <Grid
            item
            direction="row"
            alignItems="center"
            justify="space-between"
            style={{ justifyContent: "center" }}
          >
            <Typography variant={"h6"}>
              Please select the unfinalized items to get optimization
            </Typography>
          </Grid>
          <DropArea
            onClose={props.onClose}
            displayDate={props.displayDate}
            displayItems={props.displayItems}
          />
        </Grid>
      </Popper>
    </DndProvider>
  );
}
<commit_msg>Add description to Optimization Form<commit_after>import React from "react";
import { Popper, Typography, Grid } from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import DropArea from "./DropArea";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";

export default function OptimizationForm(props) {
  return (
    <DndProvider backend={HTML5Backend}>
      <Popper
        open={Boolean(props.anchorEl)}
        anchorEl={props.anchorEl}
        style={{
          background: "white",
          overflowY: "scroll",
          boxShadow: "0px 10px 20px rgba(0, 0, 0, 0.2)",
        }}
      >
        <Grid
          noWrap
          container
          direction="column"
          className="popper_class"
          spacing={0}
          style={{
            position: "relative",
            height: "850px",
            overflowY: "scroll",
            width: "600px",
            margin: "0",
          }}
        >
          <Grid item justify="center">
            <Typography variant={"h5"} gutterBottom>
              Optimization Form
            </Typography>
          </Grid>
          <Grid
            item
            direction="row"
            alignItems="center"
            justify="space-between"
            style={{ justifyContent: "center" }}
          >
            <Typography variant={"subtitle2"}>
              We'll generate an optimal schedule for your date of {props.displayDate}.
            </Typography>
            <Typography variant={"subtitle2"}>
              All finalized items of that date will be included in the optimal schedule.
            </Typography>
            <Typography variant={"subtitle2"}>
              Please select unfinalized items you also want to include:
            </Typography>
          </Grid>
          <DropArea
            onClose={props.onClose}
            displayDate={props.displayDate}
            displayItems={props.displayItems}
          />
        </Grid>
      </Popper>
    </DndProvider>
  );
}
<|endoftext|>


<commit_before>var mongoose = require('mongoose');
var express = require('express');
var app = express();
var busRoutes = require(__dirname + '/routes/router');

mongoose.connect('mongodb://localhost/'); //TODO insert path to db

app.use('/api', busRoutes);

app.listen(3000, function() {
  console.log('server listening on port 3000');
});
<commit_msg>server.js updated<commit_after>var mongoose = require('mongoose');
var express = require('express');
var app = express();
var busRoutes = require(__dirname + '/routes/router');

mongoose.connect('mongodb://localhost/busData');

app.use('/api', busRoutes);

app.listen(3000, function() {
  console.log('server listening on port 3000');
});
<|endoftext|>


<commit_before>// NightScout server file

// NightScout is free software: you can redistribute it and/or modify it under the terms of the GNU
// General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.

// NightScout is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License along with NightScout.
// If not, see <http://www.gnu.org/licenses/>.

// Description: Basic web server to display data from Dexcom G4.  Requires a database that contains
// the Dexcom SGV data.

////////////////////////////////////////////////////////////////////////////////////////////////////
// local variables
////////////////////////////////////////////////////////////////////////////////////////////////////
var patientData = [];
var now = new Date().getTime();
var fs = require('fs');
var express = require('express');
var appcache = require("appcache-node");
var mongoClient = require('mongodb').MongoClient;
var pebble = require('./lib/pebble');
var cgmData = [];
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// define helper functions
////////////////////////////////////////////////////////////////////////////////////////////////////
function errorHandler(err, req, res, next) {
    if (err) {
        // Log the error
        var msg = "Error serving " + request.url + " - " + err.message;
        sys.error(msg);
        console.log(msg);

        // Respond to the client
        res.status(err.status);
        res.render('error', { error: err });
    }
}

function getAppCache(req) {
     // Define the files you want the browser to cache
    var hostname = req.headers.host;
    return appcache.newCache([
        'http://'+hostname+'/audio/alarm.mp3',
        'http://'+hostname+'/audio/alarm2.mp3',
        'http://'+hostname+'/css/dropdown.css',
        'http://'+hostname+'/css/main.css',
        'http://'+hostname+'/js/client.js',
        'http://'+hostname+'/js/dropdown.js',
        'http://'+hostname+'/favicon.ico',
        'http://'+hostname+'/bower_components/d3/d3.min.js',
        'http://'+hostname+'/bower_components/jquery/dist/jquery.min.js',
        'http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,300,400,600,700,800',
        'http://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700,300italic,400italic,500italic,700italic',
        '',
        'NETWORK:',
        '*'
    ]);
}

function writePebbleJSON(req, res) {
    req.with_collection = with_collection;
    pebble.pebble(req, res);
    return;
}
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// setup http server
////////////////////////////////////////////////////////////////////////////////////////////////////
var PORT = process.env.PORT || 1337;
var THIRTY_DAYS = 2592000;
var now = new Date();
var expires =  new Date(now.getTime() + (1000 * THIRTY_DAYS));

var app = express();
app.set('title', 'Nightscout');

// define static server
var staticDir = __dirname + '/static/';
var server = express.static(staticDir);
app.use(function(req, res, next) {
    res.set({
        "Cache-Control": "public, max-age=" + THIRTY_DAYS,
        "Expires": expires,
        "Arr-Disable-Session-Affinity": "True"
    });

    next();
});

// serve the pebble JSON
app.get("/pebble", function(req, res) {
    writePebbleJSON(req, res);
});

// send the HTML5 app cache file
app.use("/nightscout.appcache", function(req, res) {
    res.set('Content-Type', 'text/cache-manifest');
    res.end(getAppCache(req));
});

// serve the static content
app.use(server);

// handle errors
app.use(errorHandler);

var server = app.listen(PORT);
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// setup socket io for data and message transmission
////////////////////////////////////////////////////////////////////////////////////////////////////
var io = require('socket.io').listen(server);

// reduce logging
io.set('log level', 0);

//Windows Azure Web Sites does not currently support WebSockets, so use long-polling
io.configure(function () {
    io.set('transports', ['xhr-polling']);
});

var watchers = 0;
io.sockets.on('connection', function (socket) {
    io.sockets.emit("now", now);
    io.sockets.emit("sgv", patientData);
    io.sockets.emit("clients", ++watchers);
    socket.on('ack', function(alarmType, _silenceTime) {
        alarms[alarmType].lastAckTime = new Date().getTime();
        alarms[alarmType].silenceTime = _silenceTime ? _silenceTime : FORTY_MINUTES;
        io.sockets.emit("clear_alarm", true);
        console.log("alarm cleared");
    });
    socket.on('disconnect', function () {
        io.sockets.emit("clients", --watchers);
    });
});
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// data handling functions
////////////////////////////////////////////////////////////////////////////////////////////////////
var TZ_OFFSET_PATIENT = 8;
var TZ_OFFSET_SERVER = new Date().getTimezoneOffset() / 60;
var ONE_HOUR = 3600000;
var ONE_MINUTE = 60000;
var FIVE_MINUTES = 300000;
var FORTY_MINUTES = 2400000;
var TWO_DAYS = 172800000;
var DB = require('./database_configuration.json');
DB.url = DB.url || process.env.CUSTOMCONNSTR_mongo;
DB.collection = DB.collection || process.env.CUSTOMCONNSTR_mongo_collection;
var DB_URL = DB.url;
var DB_COLLECTION = DB.collection;

var dir2Char = {
  'NONE': '⇼',
  'DoubleUp': '⇈',
  'SingleUp': '↑',
  'FortyFiveUp': '↗',
  'Flat': '→',
  'FortyFiveDown': '↘',
  'SingleDown': '↓',
  'DoubleDown': '⇊',
  'NOT COMPUTABLE': '-',
  'RATE OUT OF RANGE': '↮'
};

function directionToChar(direction) {
  return dir2Char[direction] || '-';
}

var Alarm = function(_typeName, _threshold) {
    this.typeName = _typeName;
    this.silenceTime = FORTY_MINUTES;
    this.lastAckTime = 0;
    this.threshold = _threshold;
};

// list of alarms with their thresholds
var alarms = {
    "alarm" : new Alarm("Regular", 0.05),
    "urgent_alarm": new Alarm("Urgent", 0.10)
};

function with_collection (fn) {
  mongoClient.connect(DB_URL, function (err, db) {
      if (err) throw err;
      var collection = db.collection(DB_COLLECTION);
      fn(err, collection);
  });
}

function update() {

    now = Date.now();

    cgmData = [];
    var earliest_data = now - TWO_DAYS;
    mongoClient.connect(DB_URL, function (err, db) {
        if (err) throw err;
        var collection = db.collection(DB_COLLECTION);

        collection.find({"date": {"$gte": earliest_data}}).toArray(function(err, results) {
            results.forEach(function(element, index, array) {
                if (element) {
                    var obj = {};
                    obj.y = element.sgv;
                    obj.x = element.date;
                    obj.d = element.dateString;
                    obj.direction = directionToChar(element.direction);
                    cgmData.push(obj);
                }
            });
            db.close();
        });
    });

    // wait for database read to complete, 5 secs has proven to be more than enough
    setTimeout(loadData, 5000);

    return update;
}

function emitAlarm(alarmType) {
    var alarm = alarms[alarmType];
    if (now > alarm.lastAckTime + alarm.silenceTime) {
        io.sockets.emit(alarmType);
    } else {
        console.log(alarm.typeName + " alarm is silenced for " + Math.floor((alarm.silenceTime - (now - alarm.lastAckTime)) / 60000) + " minutes more");
    }
}

function loadData() {

    var treatment = [];
    var mbg = [];

    var actual = [];
    if (cgmData) {
        actual = cgmData.slice();
        actual.sort(function(a, b) {
            return a.x - b.x;
        });

        // sgv less than or equal to 10 means error code
        // or warm up period code, so ignore
        actual = actual.filter(function (a) {
            return a.y > 10;
        })
    }

    var actualLength = actual.length - 1;

    if (actualLength > 1) {
        // predict using AR model
        var predicted = [];
        var lastValidReadingTime = actual[actualLength].x;
        var elapsedMins = (actual[actualLength].x - actual[actualLength - 1].x) / ONE_MINUTE;
        var BG_REF = 140;
        var BG_MIN = 36;
        var BG_MAX = 400;
        var y = Math.log(actual[actualLength].y / BG_REF);
        if (elapsedMins < 5.1) {
            y = [Math.log(actual[actualLength - 1].y / BG_REF), y];
        } else {
            y = [y, y];
        }
        var n = Math.ceil(12 * (1 / 2 + (now - lastValidReadingTime) / ONE_HOUR));
        var AR = [-0.723, 1.716];
        var dt = actual[actualLength].x;
        for (i = 0; i <= n; i++) {
            y = [y[1], AR[0] * y[0] + AR[1] * y[1]];
            dt = dt + FIVE_MINUTES;
            predicted[i] = {
                x: dt,
                y: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp(y[1]))))
            };
        }

        //TODO: need to consider when data being sent has less than the 2 day minimum

        // consolidate and send the data to the client
        patientData = [actual, predicted, mbg, treatment];
        io.sockets.emit("now", now);
        io.sockets.emit("sgv", patientData);

        // compute current loss
        var avgLoss = 0;
        var size = Math.min(predicted.length - 1, 6);
        for (var j = 0; j <= size; j++) {
            avgLoss += 1 / size * Math.pow(log10(predicted[j].y / 120), 2);
        }

        if (avgLoss > alarms['urgent_alarm'].threshold) {
            emitAlarm('urgent_alarm');
        } else if (avgLoss > alarms['alarm'].threshold) {
            emitAlarm('alarm');
        }
    }
}

// get data from database and setup to update every minute
function kickstart ( ) {
  //TODO: test server to see how data is stored (timestamps, entry values, etc)
  //TODO: check server settings to configure alerts, entry units, etc
  update( );
  return update;
}
setInterval(kickstart(), ONE_MINUTE);

////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// helper functions
////////////////////////////////////////////////////////////////////////////////////////////////////

function log10(val) { return Math.log(val) / Math.LN10; }

////////////////////////////////////////////////////////////////////////////////////////////////////
<commit_msg>Disable caching for /pebble<commit_after>// NightScout server file

// NightScout is free software: you can redistribute it and/or modify it under the terms of the GNU
// General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.

// NightScout is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License along with NightScout.
// If not, see <http://www.gnu.org/licenses/>.

// Description: Basic web server to display data from Dexcom G4.  Requires a database that contains
// the Dexcom SGV data.

////////////////////////////////////////////////////////////////////////////////////////////////////
// local variables
////////////////////////////////////////////////////////////////////////////////////////////////////
var patientData = [];
var now = new Date().getTime();
var fs = require('fs');
var express = require('express');
var appcache = require("appcache-node");
var mongoClient = require('mongodb').MongoClient;
var pebble = require('./lib/pebble');
var cgmData = [];
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// define helper functions
////////////////////////////////////////////////////////////////////////////////////////////////////
function errorHandler(err, req, res, next) {
    if (err) {
        // Log the error
        var msg = "Error serving " + request.url + " - " + err.message;
        sys.error(msg);
        console.log(msg);

        // Respond to the client
        res.status(err.status);
        res.render('error', { error: err });
    }
}

function getAppCache(req) {
     // Define the files you want the browser to cache
    var hostname = req.headers.host;
    return appcache.newCache([
        'http://'+hostname+'/audio/alarm.mp3',
        'http://'+hostname+'/audio/alarm2.mp3',
        'http://'+hostname+'/css/dropdown.css',
        'http://'+hostname+'/css/main.css',
        'http://'+hostname+'/js/client.js',
        'http://'+hostname+'/js/dropdown.js',
        'http://'+hostname+'/favicon.ico',
        'http://'+hostname+'/bower_components/d3/d3.min.js',
        'http://'+hostname+'/bower_components/jquery/dist/jquery.min.js',
        'http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,300,400,600,700,800',
        'http://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700,300italic,400italic,500italic,700italic',
        '',
        'NETWORK:',
        '*'
    ]);
}

function writePebbleJSON(req, res) {
    req.with_collection = with_collection;
    pebble.pebble(req, res);
    return;
}
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// setup http server
////////////////////////////////////////////////////////////////////////////////////////////////////
var PORT = process.env.PORT || 1337;
var THIRTY_DAYS = 2592000;
var now = new Date();
var expires =  new Date(now.getTime() + (1000 * THIRTY_DAYS));

var app = express();
app.set('title', 'Nightscout');

// serve the pebble JSON
app.get("/pebble", function(req, res) {
    writePebbleJSON(req, res);
});

// send the HTML5 app cache file
app.use("/nightscout.appcache", function(req, res) {
    res.set('Content-Type', 'text/cache-manifest');
    res.end(getAppCache(req));
});

// define static server
var staticDir = __dirname + '/static/';
var server = express.static(staticDir);
app.use(function(req, res, next) {
    res.set({
        "Cache-Control": "public, max-age=" + THIRTY_DAYS,
        "Expires": expires,
        "Arr-Disable-Session-Affinity": "True"
    });

    next();
});

// serve the static content
app.use(server);

// handle errors
app.use(errorHandler);

var server = app.listen(PORT);
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// setup socket io for data and message transmission
////////////////////////////////////////////////////////////////////////////////////////////////////
var io = require('socket.io').listen(server);

// reduce logging
io.set('log level', 0);

//Windows Azure Web Sites does not currently support WebSockets, so use long-polling
io.configure(function () {
    io.set('transports', ['xhr-polling']);
});

var watchers = 0;
io.sockets.on('connection', function (socket) {
    io.sockets.emit("now", now);
    io.sockets.emit("sgv", patientData);
    io.sockets.emit("clients", ++watchers);
    socket.on('ack', function(alarmType, _silenceTime) {
        alarms[alarmType].lastAckTime = new Date().getTime();
        alarms[alarmType].silenceTime = _silenceTime ? _silenceTime : FORTY_MINUTES;
        io.sockets.emit("clear_alarm", true);
        console.log("alarm cleared");
    });
    socket.on('disconnect', function () {
        io.sockets.emit("clients", --watchers);
    });
});
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// data handling functions
////////////////////////////////////////////////////////////////////////////////////////////////////
var TZ_OFFSET_PATIENT = 8;
var TZ_OFFSET_SERVER = new Date().getTimezoneOffset() / 60;
var ONE_HOUR = 3600000;
var ONE_MINUTE = 60000;
var FIVE_MINUTES = 300000;
var FORTY_MINUTES = 2400000;
var TWO_DAYS = 172800000;
var DB = require('./database_configuration.json');
DB.url = DB.url || process.env.CUSTOMCONNSTR_mongo;
DB.collection = DB.collection || process.env.CUSTOMCONNSTR_mongo_collection;
var DB_URL = DB.url;
var DB_COLLECTION = DB.collection;

var dir2Char = {
  'NONE': '⇼',
  'DoubleUp': '⇈',
  'SingleUp': '↑',
  'FortyFiveUp': '↗',
  'Flat': '→',
  'FortyFiveDown': '↘',
  'SingleDown': '↓',
  'DoubleDown': '⇊',
  'NOT COMPUTABLE': '-',
  'RATE OUT OF RANGE': '↮'
};

function directionToChar(direction) {
  return dir2Char[direction] || '-';
}

var Alarm = function(_typeName, _threshold) {
    this.typeName = _typeName;
    this.silenceTime = FORTY_MINUTES;
    this.lastAckTime = 0;
    this.threshold = _threshold;
};

// list of alarms with their thresholds
var alarms = {
    "alarm" : new Alarm("Regular", 0.05),
    "urgent_alarm": new Alarm("Urgent", 0.10)
};

function with_collection (fn) {
  mongoClient.connect(DB_URL, function (err, db) {
      if (err) throw err;
      var collection = db.collection(DB_COLLECTION);
      fn(err, collection);
  });
}

function update() {

    now = Date.now();

    cgmData = [];
    var earliest_data = now - TWO_DAYS;
    mongoClient.connect(DB_URL, function (err, db) {
        if (err) throw err;
        var collection = db.collection(DB_COLLECTION);

        collection.find({"date": {"$gte": earliest_data}}).toArray(function(err, results) {
            results.forEach(function(element, index, array) {
                if (element) {
                    var obj = {};
                    obj.y = element.sgv;
                    obj.x = element.date;
                    obj.d = element.dateString;
                    obj.direction = directionToChar(element.direction);
                    cgmData.push(obj);
                }
            });
            db.close();
        });
    });

    // wait for database read to complete, 5 secs has proven to be more than enough
    setTimeout(loadData, 5000);

    return update;
}

function emitAlarm(alarmType) {
    var alarm = alarms[alarmType];
    if (now > alarm.lastAckTime + alarm.silenceTime) {
        io.sockets.emit(alarmType);
    } else {
        console.log(alarm.typeName + " alarm is silenced for " + Math.floor((alarm.silenceTime - (now - alarm.lastAckTime)) / 60000) + " minutes more");
    }
}

function loadData() {

    var treatment = [];
    var mbg = [];

    var actual = [];
    if (cgmData) {
        actual = cgmData.slice();
        actual.sort(function(a, b) {
            return a.x - b.x;
        });

        // sgv less than or equal to 10 means error code
        // or warm up period code, so ignore
        actual = actual.filter(function (a) {
            return a.y > 10;
        })
    }

    var actualLength = actual.length - 1;

    if (actualLength > 1) {
        // predict using AR model
        var predicted = [];
        var lastValidReadingTime = actual[actualLength].x;
        var elapsedMins = (actual[actualLength].x - actual[actualLength - 1].x) / ONE_MINUTE;
        var BG_REF = 140;
        var BG_MIN = 36;
        var BG_MAX = 400;
        var y = Math.log(actual[actualLength].y / BG_REF);
        if (elapsedMins < 5.1) {
            y = [Math.log(actual[actualLength - 1].y / BG_REF), y];
        } else {
            y = [y, y];
        }
        var n = Math.ceil(12 * (1 / 2 + (now - lastValidReadingTime) / ONE_HOUR));
        var AR = [-0.723, 1.716];
        var dt = actual[actualLength].x;
        for (i = 0; i <= n; i++) {
            y = [y[1], AR[0] * y[0] + AR[1] * y[1]];
            dt = dt + FIVE_MINUTES;
            predicted[i] = {
                x: dt,
                y: Math.max(BG_MIN, Math.min(BG_MAX, Math.round(BG_REF * Math.exp(y[1]))))
            };
        }

        //TODO: need to consider when data being sent has less than the 2 day minimum

        // consolidate and send the data to the client
        patientData = [actual, predicted, mbg, treatment];
        io.sockets.emit("now", now);
        io.sockets.emit("sgv", patientData);

        // compute current loss
        var avgLoss = 0;
        var size = Math.min(predicted.length - 1, 6);
        for (var j = 0; j <= size; j++) {
            avgLoss += 1 / size * Math.pow(log10(predicted[j].y / 120), 2);
        }

        if (avgLoss > alarms['urgent_alarm'].threshold) {
            emitAlarm('urgent_alarm');
        } else if (avgLoss > alarms['alarm'].threshold) {
            emitAlarm('alarm');
        }
    }
}

// get data from database and setup to update every minute
function kickstart ( ) {
  //TODO: test server to see how data is stored (timestamps, entry values, etc)
  //TODO: check server settings to configure alerts, entry units, etc
  update( );
  return update;
}
setInterval(kickstart(), ONE_MINUTE);

////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// helper functions
////////////////////////////////////////////////////////////////////////////////////////////////////

function log10(val) { return Math.log(val) / Math.LN10; }

////////////////////////////////////////////////////////////////////////////////////////////////////
<|endoftext|>