<commit_before>/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */

import Active from '../Active'
import Router from '../Router'

/*
 * Metamaps.Erb
 * Metamaps.Maps
 */

const Search = {
  locked: false,
  isOpen: false,
  limitTopicsToMe: false,
  limitMapsToMe: false,
  timeOut: null,
  changing: false,
  optionsInitialized: false,
  init: function () {
    var self = Search

    // TODO does this overlap with Metamaps.Loading?
    // devin sez: I'd like to remove Metamaps.Loading from the rails code
    var loader = new CanvasLoader('searchLoading')
    loader.setColor('#4fb5c0') // default is '#000000'
    loader.setDiameter(24) // default is 40
    loader.setDensity(41) // default is 40
    loader.setRange(0.9) // default is 1.3
    loader.show() // Hidden by default

    // bind the hover events
    $('.sidebarSearch').hover(function () {
      self.open()
    }, function () {
      self.close(800, false)
    })

    $('.sidebarSearchIcon').click(function (e) {
      $('.sidebarSearchField').focus()
    })
    $('.sidebarSearch').click(function (e) {
      e.stopPropagation()
    })
    $('body').click(function (e) {
      self.close(0, false)
    })

    // open if the search is closed and user hits ctrl+/
    // close if they hit ESC
    $('body').bind('keyup', function (e) {
      switch (e.which) {
        case 191:
          if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) {
            self.open(true) // true for focus
          }
          break
        case 27:
          if (self.isOpen) {
            self.close(0, true)
          }
          break

        default:
          break // console.log(e.which)
      }
    })

    self.startTypeahead()
  },
  lock: function () {
    var self = Search
    self.locked = true
  },
  unlock: function () {
    var self = Search
    self.locked = false
  },
  open: function (focus) {
    var self = Search

    clearTimeout(self.timeOut)
    if (!self.isOpen && !self.changing && !self.locked) {
      self.changing = true
      $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({
        width: '400px'
      }, 300, function () {
        if (focus) $('.sidebarSearchField').focus()
        $('.sidebarSearchField, .sidebarSearch .tt-hint').css({
          padding: '7px 10px 3px 10px',
          width: '380px'
        })
        self.changing = false
        self.isOpen = true
      })
    }
  },
  close: function (closeAfter, bypass) {
    // for now
    return

    var self = Search

    self.timeOut = setTimeout(function () {
      if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) {
        self.changing = true
        $('.sidebarSearchField, .sidebarSearch .tt-hint').css({
          padding: '7px 0 3px 0',
          width: '400px'
        })
        $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({
          width: '0'
        }, 300, function () {
          $('.sidebarSearchField').typeahead('val', '')
          $('.sidebarSearchField').blur()
          self.changing = false
          self.isOpen = false
        })
      }
    }, closeAfter)
  },
  startTypeahead: function () {
    var self = Search

    var mapheader = Active.Mapper ? '

Maps
<input type="checkbox" class="limitToMe" id="limitMapsToMe"></input>added by me

' : '

Maps

'
    var topicheader = Active.Mapper ? '

Topics
<input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input>added by me

' : '

Topics

'
    var mapperheader = '

Mappers

'

    var topics = {
      name: 'topics',
      limit: 9999,

      display: s => s.label,
      templates: {
        notFound: function (s) {
          return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            typeImageURL: Metamaps.Erb['icons/wildcard.png'],
            rtype: 'noresult'
          })
        },
        header: topicheader,
        suggestion: function (s) {
          return Hogan.compile($('#topicSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/topics',
          prepare: function (query, settings) {
            settings.url += '?term=' + query
            if (Active.Mapper && self.limitTopicsToMe) {
              settings.url += '&user=' + Active.Mapper.id.toString()
            }
            return settings
          }
        }
      })
    }

    var maps = {
      name: 'maps',
      limit: 9999,
      display: s => s.label,
      templates: {
        notFound: function (s) {
          return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            rtype: 'noresult'
          })
        },
        header: mapheader,
        suggestion: function (s) {
          return Hogan.compile($('#mapSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/maps',
          prepare: function (query, settings) {
            settings.url += '?term=' + query
            if (Active.Mapper && self.limitMapsToMe) {
              settings.url += '&user=' + Active.Mapper.id.toString()
            }
            return settings
          }
        }
      })
    }

    var mappers = {
      name: 'mappers',
      limit: 9999,
      display: s => s.label,
      templates: {
        notFound: function (s) {
          return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            rtype: 'noresult',
            profile: Metamaps.Erb['user.png']
          })
        },
        header: mapperheader,
        suggestion: function (s) {
          return Hogan.compile($('#mapperSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/mappers?term=%QUERY',
          wildcard: '%QUERY'
        }
      })
    }

    // Take all that crazy setup data and put it together into one beautiful typeahead call!
    $('.sidebarSearchField').typeahead(
      {
        highlight: true
      },
      [topics, maps, mappers]
    )

    // Set max height of the search results box to prevent it from covering bottom left footer
    $('.sidebarSearchField').bind('typeahead:render', function (event) {
      self.initSearchOptions()
      self.hideLoader()
      var h = $(window).height()
      $('.tt-dropdown-menu').css('max-height', h - 100)
      if (self.limitTopicsToMe) {
        $('#limitTopicsToMe').prop('checked', true)
      }
      if (self.limitMapsToMe) {
        $('#limitMapsToMe').prop('checked', true)
      }
    })
    $(window).resize(function () {
      var h = $(window).height()
      $('.tt-dropdown-menu').css('max-height', h - 100)
    })

    // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on
    $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick)

    // don't do it, if they clicked on a 'addToMap' button
    $('.sidebarSearch button.addToMap').click(function (event) {
      event.stopPropagation()
    })

    // make sure that when you click on 'limit to me' or 'toggle section' it works
    $('.sidebarSearchField.tt-input').keyup(function () {
      if ($('.sidebarSearchField.tt-input').val() === '') {
        self.hideLoader()
      } else {
        self.showLoader()
      }
    })
  },
  handleResultClick: function (event, datum, dataset) {
    var self = Search

    self.hideLoader()

    if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) {
      self.close(0, true)
      if (datum.rtype === 'topic') {
        Router.topics(datum.id)
      } else if (datum.rtype === 'map') {
        Router.maps(datum.id)
      } else if (datum.rtype === 'mapper') {
        Router.explore('mapper', datum.id)
      }
    }
  },
  initSearchOptions: function () {
    var self = Search

    function toggleResultSet (set) {
      var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult')
      if (s.is(':visible')) {
        s.hide()
        $(this).removeClass('minimizeResults').addClass('maximizeResults')
      } else {
        s.show()
        $(this).removeClass('maximizeResults').addClass('minimizeResults')
      }
    }

    $('.limitToMe').unbind().bind('change', function (e) {
      if ($(this).attr('id') === 'limitTopicsToMe') {
        self.limitTopicsToMe = !self.limitTopicsToMe
      }
      if ($(this).attr('id') === 'limitMapsToMe') {
        self.limitMapsToMe = !self.limitMapsToMe
      }

      // set the value of the search equal to itself to retrigger the
      // autocomplete event
      var searchQuery = $('.sidebarSearchField.tt-input').val()
      $('.sidebarSearchField').typeahead('val', '')
        .typeahead('val', searchQuery)
    })

    // when the user clicks minimize section, hide the results for that section
    $('.minimizeMapperResults').unbind().click(function (e) {
      toggleResultSet.call(this, 'mappers')
    })
    $('.minimizeTopicResults').unbind().click(function (e) {
      toggleResultSet.call(this, 'topics')
    })
    $('.minimizeMapResults').unbind().click(function (e) {
      toggleResultSet.call(this, 'maps')
    })
  },
  hideLoader: function () {
    $('#searchLoading').hide()
  },
  showLoader: function () {
    $('#searchLoading').show()
  }
}

export default Search
<commit_msg>remove old code from GlobalUI.Search<commit_after>/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */

import Active from '../Active'
import Router from '../Router'

/*
 * Metamaps.Erb
 * Metamaps.Maps
 */

const Search = {
  locked: false,
  isOpen: false,
  limitTopicsToMe: false,
  limitMapsToMe: false,
  changing: false,
  optionsInitialized: false,
  init: function () {
    var self = Search

    // TODO does this overlap with Metamaps.Loading?
    // devin sez: I'd like to remove Metamaps.Loading from the rails code
    var loader = new CanvasLoader('searchLoading')
    loader.setColor('#4fb5c0') // default is '#000000'
    loader.setDiameter(24) // default is 40
    loader.setDensity(41) // default is 40
    loader.setRange(0.9) // default is 1.3
    loader.show() // Hidden by default

    $('.sidebarSearchIcon').click(function (e) {
      $('.sidebarSearchField').focus()
    })
    $('.sidebarSearch').click(function (e) {
      e.stopPropagation()
    })

    self.startTypeahead()
  },
  focus: function() {
    $('.sidebarSearchField').focus()
  },
  startTypeahead: function () {
    var self = Search

    var mapheader = Active.Mapper ? '

Maps
<input type="checkbox" class="limitToMe" id="limitMapsToMe"></input>added by me

' : '

Maps

'
    var topicheader = Active.Mapper ? '

Topics
<input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input>added by me

' : '

Topics

'
    var mapperheader = '

Mappers

'

    var topics = {
      name: 'topics',
      limit: 9999,

      display: s => s.label,
      templates: {
        notFound: function (s) {
          return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            typeImageURL: Metamaps.Erb['icons/wildcard.png'],
            rtype: 'noresult'
          })
        },
        header: topicheader,
        suggestion: function (s) {
          return Hogan.compile($('#topicSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/topics',
          prepare: function (query, settings) {
            settings.url += '?term=' + query
            if (Active.Mapper && self.limitTopicsToMe) {
              settings.url += '&user=' + Active.Mapper.id.toString()
            }
            return settings
          }
        }
      })
    }

    var maps = {
      name: 'maps',
      limit: 9999,
      display: s => s.label,
      templates: {
        notFound: function (s) {
          return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            rtype: 'noresult'
          })
        },
        header: mapheader,
        suggestion: function (s) {
          return Hogan.compile($('#mapSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/maps',
          prepare: function (query, settings) {
            settings.url += '?term=' + query
            if (Active.Mapper && self.limitMapsToMe) {
              settings.url += '&user=' + Active.Mapper.id.toString()
            }
            return settings
          }
        }
      })
    }

    var mappers = {
      name: 'mappers',
      limit: 9999,
      display: s => s.label,
      templates: {
        notFound: function (s) {
          return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({
            value: 'No results',
            label: 'No results',
            rtype: 'noresult',
            profile: Metamaps.Erb['user.png']
          })
        },
        header: mapperheader,
        suggestion: function (s) {
          return Hogan.compile($('#mapperSearchTemplate').html()).render(s)
        }
      },
      source: new Bloodhound({
        datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
        queryTokenizer: Bloodhound.tokenizers.whitespace,
        remote: {
          url: '/search/mappers?term=%QUERY',
          wildcard: '%QUERY'
        }
      })
    }

    // Take all that crazy setup data and put it together into one beautiful typeahead call!
    $('.sidebarSearchField').typeahead(
      {
        highlight: true
      },
      [topics, maps, mappers]
    )

    // Set max height of the search results box to prevent it from covering bottom left footer
    $('.sidebarSearchField').bind('typeahead:render', function (event) {
      self.initSearchOptions()
      self.hideLoader()
      var h = $(window).height()
      $('.tt-dropdown-menu').css('max-height', h - 100)
      if (self.limitTopicsToMe) {
        $('#limitTopicsToMe').prop('checked', true)
      }
      if (self.limitMapsToMe) {
        $('#limitMapsToMe').prop('checked', true)
      }
    })
    $(window).resize(function () {
      var h = $(window).height()
      $('.tt-dropdown-menu').css('max-height', h - 100)
    })

    // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on
    $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick)

    // don't do it, if they clicked on a 'addToMap' button
    $('.sidebarSearch button.addToMap').click(function (event) {
      event.stopPropagation()
    })

    // make sure that when you click on 'limit to me' or 'toggle section' it works
    $('.sidebarSearchField.tt-input').keyup(function () {
      if ($('.sidebarSearchField.tt-input').val() === '') {
        self.hideLoader()
      } else {
        self.showLoader()
      }
    })
  },
  handleResultClick: function (event, datum, dataset) {
    var self = Search

    self.hideLoader()

    if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) {
      if (datum.rtype === 'topic') {
        Router.topics(datum.id)
      } else if (datum.rtype === 'map') {
        Router.maps(datum.id)
      } else if (datum.rtype === 'mapper') {
        Router.explore('mapper', datum.id)
      }
    }
  },
  initSearchOptions: function () {
    var self = Search

    function toggleResultSet (set) {
      var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult')
      if (s.is(':visible')) {
        s.hide()
        $(this).removeClass('minimizeResults').addClass('maximizeResults')
      } else {
        s.show()
        $(this).removeClass('maximizeResults').addClass('minimizeResults')
      }
    }

    $('.limitToMe').unbind().bind('change', function (e) {
      if ($(this).attr('id') === 'limitTopicsToMe') {
        self.limitTopicsToMe = !self.limitTopicsToMe
      }
      if ($(this).attr('id') === 'limitMapsToMe') {
        self.limitMapsToMe = !self.limitMapsToMe
      }

      // set the value of the search equal to itself to retrigger the
      // autocomplete event
      var searchQuery = $('.sidebarSearchField.tt-input').val()
      $('.sidebarSearchField').typeahead('val', '')
        .typeahead('val', searchQuery)
    })

    // when the user clicks minimize section, hide the results for that section
    $('.minimizeMapperResults').unbind().click(function (e) {
      toggleResultSet.call(this, 'mappers')
    })
    $('.minimizeTopicResults').unbind().click(function (e) {
      toggleResultSet.call(this, 'topics')
    })
    $('.minimizeMapResults').unbind().click(function (e) {
      toggleResultSet.call(this, 'maps')
    })
  },
  hideLoader: function () {
    $('#searchLoading').hide()
  },
  showLoader: function () {
    $('#searchLoading').show()
  }
}

export default Search
<|endoftext|>


<commit_before>var common = require('../casper_lib/common.js').common;

function waitWhileDraftsVisible(then) {
    casper.waitFor(function () {
        return casper.evaluate(function () {
            return $("#draft_overlay").length === 0 ||
                   $("#draft_overlay").css("opacity") === "0";
        });
    }, then);
}

function waitUntilDraftsVisible(then) {
    casper.waitFor(function () {
        return casper.evaluate(function () {
            return $("#draft_overlay").length === 1 &&
                   $("#draft_overlay").css("opacity") === "1";
        });
    }, then);
}

common.start_and_log_in();

casper.then(function () {
    casper.test.info('Drafts page');

    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    casper.test.assertUrlMatch(/^http:\/\/[^/]+\/#drafts/,
                               'URL suggests we are on drafts page');
    waitUntilDraftsVisible(function () {
        casper.test.assertExists('#draft_overlay', 'Drafts page is active');
        casper.test.assertSelectorHasText('.no-drafts', 'No Drafts.');
    });
});

casper.then(function () {
    casper.click('#draft_overlay .exit');
    waitWhileDraftsVisible();
});

casper.then(function () {
    casper.test.info('Creating Stream Message Draft');
    casper.click('body');
    casper.page.sendEvent('keypress', "c");
    casper.waitUntilVisible('#stream-message', function () {
        casper.test.info('Creating Private Message Draft');
        casper.fill('form#send_message_form', {
            stream: 'all',
            subject: 'tests',
            content: 'Test Stream Message',
        }, false);
        casper.click("#compose_close");
    });
});

casper.then(function () {
    casper.test.info('Creating Private Message Draft');
    casper.click('body');
    casper.page.sendEvent('keypress', "C");
    casper.waitUntilVisible('#private-message', function () {
        casper.fill('form#send_message_form', {
            recipient: 'cordelia@zulip.com, hamlet@zulip.com',
            content: 'Test Private Message',
        }, false);
        casper.click("#compose_close");
    });
});

casper.then(function () {
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    waitUntilDraftsVisible(function () {
        casper.test.assertElementCount('.draft-row', 2, 'Drafts loaded');

        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_label', 'all');
        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_topic', 'tests');
        casper.test.assertTextExists('Test Stream Message', 'Stream draft contains message content');

        casper.test.assertSelectorHasText('.draft-row .message_header_private_message .stream_label',
                                          'You and Cordelia Lear, King Hamlet');
        casper.test.assertTextExists('Test Private Message', 'Private draft contains message content');
    });
});

casper.then(function () {
    casper.test.info('Restoring Stream Message Draft');
    casper.click("#drafts_table .message_row:not(.private-message) .restore-draft");
    waitWhileDraftsVisible(function () {
        casper.test.assertVisible('#stream-message', 'Stream Message Box Restored');
        common.check_form('form#send_message_form', {
            stream: 'all',
            subject: 'tests',
            content: 'Test Stream Message',
        }, "Stream message box filled with draft content");
    });
});

casper.then(function () {
    casper.test.info('Editing Stream Message Draft');
    casper.fill('form#send_message_form', {
        stream: 'all',
        subject: 'tests',
        content: 'Updated Stream Message',
    }, false);
    casper.click("#compose_close");
});

casper.then(function () {
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    waitUntilDraftsVisible(function () {
        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_label', 'all');
        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_topic', 'tests');
        casper.test.assertTextExists('Updated Stream Message', 'Stream draft contains message content');
    });
});

casper.then(function () {
    casper.test.info('Restoring Private Message Draft');
    casper.click("#drafts_table .message_row.private-message .restore-draft");
    waitWhileDraftsVisible(function () {
        casper.test.assertVisible('#private-message', 'Private Message Box Restored');
        common.check_form('form#send_message_form', {
            recipient: 'cordelia@zulip.com, hamlet@zulip.com',
            content: 'Test Private Message',
        }, "Private message box filled with draft content");
    });
});

casper.then(function () {
    casper.click("#compose_close");
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    casper.test.info('Deleting Draft');
    casper.click("#drafts_table .message_row.private-message .delete-draft");
    casper.test.assertElementCount('.draft-row', 1, 'Draft deleted');
    casper.test.assertDoesntExist("#drafts_table .message_row.private-message");
});

casper.then(function () {
    casper.test.info('Saving Draft by Reloading');
    casper.click('#draft_overlay .exit');
    waitWhileDraftsVisible(function () {
        casper.click('body');
        casper.page.sendEvent('keypress', "C");
        casper.waitUntilVisible('#private-message', function () {
            casper.fill('form#send_message_form', {
                recipient: 'cordelia@zulip.com',
                content: 'Test Private Message',
            }, false);
            casper.reload();
        });
    });
});

casper.then(function () {
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
    waitUntilDraftsVisible(function () {
        casper.test.assertElementCount('.draft-row', 2, 'Drafts loaded');
        casper.test.assertSelectorHasText('.draft-row .message_header_private_message .stream_label',
                                          'You and Cordelia Lear');
        casper.test.assertTextExists('Test Private Message');
    });
});

casper.then(function () {
    casper.test.info('Deleting Draft after Sending Message');
    casper.click("#drafts_table .message_row.private-message .restore-draft");
    waitWhileDraftsVisible(function () {
        casper.test.assertVisible('#private-message');
        casper.click('#compose-send-button');
    });
});

casper.then(function () {
    // This tests the second drafts link in the compose area
    casper.waitUntilVisible('.compose_table .drafts-link', function () {
        casper.click('.compose_table .drafts-link');
    });
    waitUntilDraftsVisible(function () {
        casper.test.assertElementCount('.draft-row', 1, 'Drafts loaded');
        casper.test.assertDoesntExist("#drafts_table .message_row.private-message");
    });
});

common.then_log_out();

casper.run(function () {
    casper.test.done();
});
<commit_msg>casper: Fix test failure due to recent capitalization change.<commit_after>var common = require('../casper_lib/common.js').common;

function waitWhileDraftsVisible(then) {
    casper.waitFor(function () {
        return casper.evaluate(function () {
            return $("#draft_overlay").length === 0 ||
                   $("#draft_overlay").css("opacity") === "0";
        });
    }, then);
}

function waitUntilDraftsVisible(then) {
    casper.waitFor(function () {
        return casper.evaluate(function () {
            return $("#draft_overlay").length === 1 &&
                   $("#draft_overlay").css("opacity") === "1";
        });
    }, then);
}

common.start_and_log_in();

casper.then(function () {
    casper.test.info('Drafts page');

    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    casper.test.assertUrlMatch(/^http:\/\/[^/]+\/#drafts/,
                               'URL suggests we are on drafts page');
    waitUntilDraftsVisible(function () {
        casper.test.assertExists('#draft_overlay', 'Drafts page is active');
        casper.test.assertSelectorHasText('.no-drafts', 'No drafts.');
    });
});

casper.then(function () {
    casper.click('#draft_overlay .exit');
    waitWhileDraftsVisible();
});

casper.then(function () {
    casper.test.info('Creating Stream Message Draft');
    casper.click('body');
    casper.page.sendEvent('keypress', "c");
    casper.waitUntilVisible('#stream-message', function () {
        casper.test.info('Creating Private Message Draft');
        casper.fill('form#send_message_form', {
            stream: 'all',
            subject: 'tests',
            content: 'Test Stream Message',
        }, false);
        casper.click("#compose_close");
    });
});

casper.then(function () {
    casper.test.info('Creating Private Message Draft');
    casper.click('body');
    casper.page.sendEvent('keypress', "C");
    casper.waitUntilVisible('#private-message', function () {
        casper.fill('form#send_message_form', {
            recipient: 'cordelia@zulip.com, hamlet@zulip.com',
            content: 'Test Private Message',
        }, false);
        casper.click("#compose_close");
    });
});

casper.then(function () {
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    waitUntilDraftsVisible(function () {
        casper.test.assertElementCount('.draft-row', 2, 'Drafts loaded');

        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_label', 'all');
        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_topic', 'tests');
        casper.test.assertTextExists('Test Stream Message', 'Stream draft contains message content');

        casper.test.assertSelectorHasText('.draft-row .message_header_private_message .stream_label',
                                          'You and Cordelia Lear, King Hamlet');
        casper.test.assertTextExists('Test Private Message', 'Private draft contains message content');
    });
});

casper.then(function () {
    casper.test.info('Restoring Stream Message Draft');
    casper.click("#drafts_table .message_row:not(.private-message) .restore-draft");
    waitWhileDraftsVisible(function () {
        casper.test.assertVisible('#stream-message', 'Stream Message Box Restored');
        common.check_form('form#send_message_form', {
            stream: 'all',
            subject: 'tests',
            content: 'Test Stream Message',
        }, "Stream message box filled with draft content");
    });
});

casper.then(function () {
    casper.test.info('Editing Stream Message Draft');
    casper.fill('form#send_message_form', {
        stream: 'all',
        subject: 'tests',
        content: 'Updated Stream Message',
    }, false);
    casper.click("#compose_close");
});

casper.then(function () {
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    waitUntilDraftsVisible(function () {
        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_label', 'all');
        casper.test.assertSelectorHasText('.draft-row .message_header_stream .stream_topic', 'tests');
        casper.test.assertTextExists('Updated Stream Message', 'Stream draft contains message content');
    });
});

casper.then(function () {
    casper.test.info('Restoring Private Message Draft');
    casper.click("#drafts_table .message_row.private-message .restore-draft");
    waitWhileDraftsVisible(function () {
        casper.test.assertVisible('#private-message', 'Private Message Box Restored');
        common.check_form('form#send_message_form', {
            recipient: 'cordelia@zulip.com, hamlet@zulip.com',
            content: 'Test Private Message',
        }, "Private message box filled with draft content");
    });
});

casper.then(function () {
    casper.click("#compose_close");
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
});

casper.then(function () {
    casper.test.info('Deleting Draft');
    casper.click("#drafts_table .message_row.private-message .delete-draft");
    casper.test.assertElementCount('.draft-row', 1, 'Draft deleted');
    casper.test.assertDoesntExist("#drafts_table .message_row.private-message");
});

casper.then(function () {
    casper.test.info('Saving Draft by Reloading');
    casper.click('#draft_overlay .exit');
    waitWhileDraftsVisible(function () {
        casper.click('body');
        casper.page.sendEvent('keypress', "C");
        casper.waitUntilVisible('#private-message', function () {
            casper.fill('form#send_message_form', {
                recipient: 'cordelia@zulip.com',
                content: 'Test Private Message',
            }, false);
            casper.reload();
        });
    });
});

casper.then(function () {
    casper.waitUntilVisible('.drafts-link', function () {
        casper.click('.drafts-link');
    });
    waitUntilDraftsVisible(function () {
        casper.test.assertElementCount('.draft-row', 2, 'Drafts loaded');
        casper.test.assertSelectorHasText('.draft-row .message_header_private_message .stream_label',
                                          'You and Cordelia Lear');
        casper.test.assertTextExists('Test Private Message');
    });
});

casper.then(function () {
    casper.test.info('Deleting Draft after Sending Message');
    casper.click("#drafts_table .message_row.private-message .restore-draft");
    waitWhileDraftsVisible(function () {
        casper.test.assertVisible('#private-message');
        casper.click('#compose-send-button');
    });
});

casper.then(function () {
    // This tests the second drafts link in the compose area
    casper.waitUntilVisible('.compose_table .drafts-link', function () {
        casper.click('.compose_table .drafts-link');
    });
    waitUntilDraftsVisible(function () {
        casper.test.assertElementCount('.draft-row', 1, 'Drafts loaded');
        casper.test.assertDoesntExist("#drafts_table .message_row.private-message");
    });
});

common.then_log_out();

casper.run(function () {
    casper.test.done();
});
<|endoftext|>


<commit_before>// Handles all LFG-related events

const chalk = require('chalk');
const Discord = require('discord.js');
const moment = require('moment');
const settings = require('../settings');

const INTERVAL = settings.lfg.update_interval;

/**
 * Format for all LFG requests, embedded in the Client (bot) object -->
 *
 * 'LFG ID' => {
 *  @param {Snowflake} id, message ID that bot sends
 *  @param {String} party_leader_name, user name of command issuer & leader of party
 *  @param {Snowflake} party_leader_id, user ID of party leader
 *  @param {String} code, LFG code for the game
 *  @param {String} game, Game title (proper formatting)
 *  @param {String} mode, Game mode
 *  @param {Date} time, When the LFG request was made
 *  @param {Date} expire_date, When the LFG request will expire
 *  @param {Integer} ttl, Number of minutes until LFG request expires
 *  @param {Array[Snowflake]} party, List of user IDs of users in the party
 *  @param {Integer} max_party_size, Maximum # of users in the party before it fills up
 *  @param {Snowflake} channel, Channel ID where the LFG request was made
 *  @param {Boolean} warning, Has a warning been sent out yet?
 * }
 */

module.exports = {
  /**
   * @func addLFG
   * Adds new LFG party to the LFG stack
   *
   * @param {Discord.Client} bot
   * @param {object} obj -- lfg object which contains all information as shown above
   */
  addLFG(bot, object) {
    try {
      let firstEntry;
      if (bot.lfgStack.size === 0) firstEntry = true;
      const g = bot.games.get(object.code);
      const index = g.modes.indexOf(object.mode);

      const embed = new Discord.RichEmbed()
        .setTitle(`${object.party_leader_name} is looking for a ${object.game} group!`)
        .setDescription(`**Game mode:** ${g.modes_proper[index]}\n**Party size:** ${object.max_party_size}`)
        .setColor(0x009395)
        .setThumbnail(g.thumbnail)
        .setFooter('Expires')
        .setTimestamp(object.expire_date)
        .addField('Want to join?', `Click the 👍 below to reserve a spot!\n${object.party_leader_name}: click the 🚫 below to cancel the party.\n\n**Party:** <@${object.party_leader_id}> (1/${object.max_party_size})`);

      bot.channels.get(object.channel).send({ embed }).then((message) => {
        object.id = message.id;
        bot.lfgStack.set(object.id, object);
        if (firstEntry) bot.lfgUpdate(true, INTERVAL);
        message.react('👍').then(() => {
          message.react('🚫').then(() => {
            console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${object.party_leader_name} made an LFG for ${object.game} | ${object.mode} | party of ${object.max_party_size} | ${object.ttl} minutes`));
          }).catch(err => console.log(chalk.bgRed(`[${moment().format(settings.timeFormat)}] ${err}`)));
        }).catch(err => console.log(chalk.bgRed(`[${moment().format(settings.timeFormat)}] ${err}`)));
      }).catch(err => console.error(err));
    } catch (err) {
      console.error(err);
    }
  },

  /**
   * @func addToParty
   * Add a user into an LFG party
   *
   * @param {Discord.Client} bot
   * @param {Snowflake} id - LFG id
   * @param {Snowflake} userid - User id to add into LFG party
   */
  addToParty(bot, id, userid) {
    try {
      // Push user into party array
      const stack = bot.lfgStack.get(id);
      stack.party.push(userid);
      const game = bot.games.get(stack.code);
      const mode = game.modes.indexOf(stack.mode);
      const embed = new Discord.RichEmbed()
        .setTitle(`${stack.party_leader_name} is looking for a ${game.name} group!`)
        .setDescription(`**Game mode:** ${game.modes_proper[mode]}\n**Party size:** ${stack.max_party_size}`)
        .setColor(0x009395)
        .setThumbnail(game.thumbnail)
        .setFooter('Expires')
        .setTimestamp(stack.expire_date)
        .addField('Want to join?', `Click the 👍 below to reserve a spot!\n${stack.party_leader_name}: click the 🚫 below to cancel the party.\n\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')} (${stack.party.length}/${stack.max_party_size})`);
      const ch = bot.channels.get(stack.channel);
      const g = ch.guild;

      ch.fetchMessage(stack.id).then((message) => {
        message.edit({ embed }).then(() => {
        }).catch(err => console.error(err));
      }).catch(err => console.error(err));

      bot.lfgStack.set(id, stack);

      // Attempt to move user into party leader's channel
      const memberToMoveTo = g.members.get(bot.lfgStack.get(id).party_leader_id);
      const memberToMove = g.members.get(userid);
      if (typeof memberToMoveTo.voiceChannel !== 'undefined') {
        memberToMove.edit({ channel: memberToMoveTo.voiceChannelID }, `Moved by @${settings.botNameProper} for LFG`).then().catch(err => console.error(err));
      }

      // Check if the party is full
      if (stack.party.length === stack.max_party_size) this.complete(bot, id);
    } catch (err) {
      console.error(err);
    }
  },

  /**
   * @func removeFromParty
   * Remove a user from an LFG party
   *
   * @param {Discord.Client} bot
   * @param {Snowflake} id - LFG id
   * @param {Snowflake} userid - User id to remove from LFG party
   */
  removeFromParty(bot, id, userid) {
    try {
      // Remove user from party array
      const index = bot.lfgStack.get(id).party.indexOf(userid);
      if (index > -1) {
        bot.lfgStack.get(id).party.splice(index, 1);
        const stack = bot.lfgStack.get(id);
        const game = bot.games.get(stack.code);
        const mode = game.modes.indexOf(stack.mode);
        const embed = new Discord.RichEmbed()
          .setTitle(`${stack.party_leader_name} is looking for a ${game.name} group!`)
          .setDescription(`**Game mode:** ${game.modes_proper[mode]}\n**Party size:** ${stack.max_party_size}`)
          .setColor(0x009395)
          .setThumbnail(game.thumbnail)
          .setFooter('Expires')
          .setTimestamp(stack.expire_date)
          .addField('Want to join?', `Click the 👍 below to reserve a spot!\n${stack.party_leader_name}: click the 🚫 below to cancel the party.\n\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')} (${stack.party.length}/${stack.max_party_size})`);
        const ch = bot.channels.get(stack.channel);
        ch.fetchMessage(stack.id).then((message) => {
          message.edit({ embed }).then(() => {
          }).catch(err => console.error(err));
        }).catch(err => console.error(err));
      } else return;
    } catch (err) { console.error(err); }
  },

  /**
   * @func timeout
   * LFG party is timed out, remove from the LFG stack
   *
   * @param {Discord.Client} bot
   * @param {Snowflake} id - LFG id
   */
  timeout(bot, id) {
    try {
      // Edit LFG message
      const stack = bot.lfgStack.get(id);
      const game = bot.games.get(stack.code);
      const mode = game.modes.indexOf(stack.mode);
      const embed = new Discord.RichEmbed()
        .setTitle(`${stack.party_leader_name} is looking for a ${game.name} group!`)
        .setDescription(`**Game mode:** ${game.modes_proper[mode]}\n**Party size:** ${stack.max_party_size}`)
        .setColor(0x009395)
        .setThumbnail(game.thumbnail)
        .setFooter('Timed out')
        .setTimestamp()
        .addField('⏰ **This LFG request has timed out.** ⏰', `**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')} (${stack.party.length}/${stack.max_party_size})`);
      const ch = bot.channels.get(stack.channel);
      ch.fetchMessage(stack.id).then((message) => {
        bot.lfgStack.delete(id);
        if (bot.lfgStack.size === 0) bot.lfgUpdate(false);
        message.edit({ embed }).then(() => {
          console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG for ${stack.game} has timed out.`));
        }).catch(err => console.error(err));
      }).catch(err => console.error(err));
    } catch (err) {
      console.error(err);
    }
  },

  /**
   * @func warning
   * Running out of time on LFG party; Issue warning message
   *
   * @param {Discord.Client} bot
   * @param {Snowflake} id - LFG id
   * @param {Number} timeLeft - Minutes remaining on LFG party
   */
  warning(bot, id, timeLeft) {
    try {
      const stack = bot.lfgStack.get(id);
      const game = bot.games.get(stack.code);
      const mode = game.modes.indexOf(stack.mode);
      const embed = new Discord.RichEmbed()
        .setTitle(`${stack.party_leader_name}'s party has ${timeLeft} minutes left before it times out!`)
        .setDescription(`**Game:** ${stack.game}\n**Game mode:** ${game.modes_proper[mode]}\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')} (${stack.party.length}/${stack.max_party_size})`)
        .setColor(0x009395);
      stack.warning = true;
      const ch = bot.channels.get(stack.channel);
      ch.send({ embed });
      console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG is running out of time.`));
    } catch (err) {
      console.error(err);
    }
  },

  /**
   * @func complete
   * Party is full; Complete the LFG party ✓
   *
   * @param {Discord.Client} bot
   * @param {Snowflake} id - LFG id
   */
  complete(bot, id) {
    try {
      const stack = bot.lfgStack.get(id);
      const game = bot.games.get(stack.code);
      const mode = game.modes.indexOf(stack.mode);
      const embed = new Discord.RichEmbed()
        .setTitle(`${stack.party_leader_name}'s party is now full & ready to go!`)
        .setDescription(`**Game:** ${stack.game}\n**Game mode:** ${game.modes_proper[mode]}\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')}`)
        .setColor(0x009395)
        .setThumbnail('https://i.imgur.com/RsRmWcm.png');

      const ch = bot.channels.get(stack.channel);
      ch.send({ embed });

      // Remove from LFG stack
      bot.lfgStack.delete(id);
      if (bot.lfgStack.size === 0) bot.lfgUpdate(false);

      console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG party for ${stack.game} is full.`));
    } catch (err) {
      console.error(err);
    }
  },

  /**
   * @func cancel
   * Cancel the LFG party
   *
   * @param bot
   * @param id - LFG id
   * @param removed - Whether the LFG party message was removed or not
   */
  cancel(bot, id, removed) {
    try {
      const stack = bot.lfgStack.get(id);
      const game = bot.games.get(stack.code);
      const mode = game.modes.indexOf(stack.mode);
      const embed = new Discord.RichEmbed()
        .setTitle(`${stack.party_leader_name}'s party has been cancelled`)
        .setDescription(`**Game:** ${stack.game}\n**Game mode:** ${game.modes_proper[mode]}\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')} (${stack.party.length}/${stack.max_party_size})`)
        .setColor(0x009395)
        .setFooter('Cancelled')
        .setTimestamp()
        .setThumbnail('https://i.imgur.com/jSYuGrc.png');

      // If the message was deleted, do this stuff:
      if (removed) {
        bot.lfgStack.delete(id);
        if (bot.lfgStack.size === 0) bot.lfgUpdate(false);
        return console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG party for ${stack.game} has been deleted.`));
      }

      // Attempt to fetch message
      bot.channels.get(stack.channel).fetchMessage(stack.id).then((message) => {
        message.edit({ embed }).then(() => {
          // Remove from LFG stack and edit message
          bot.lfgStack.delete(id);
          if (bot.lfgStack.size === 0) bot.lfgUpdate(false);
          return console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG party for ${stack.game} has been cancelled.`));
        }).catch(err => console.error(err));
      }).catch(err => console.error(err));

      return 0;
    } catch (err) {
      return console.error(err);
    }
  },

  /**
   * @func update
   * LFG update interval function
   *
   * @param bot
   */
  update(bot) {
    try {
      const d = new Date();

      bot.lfgStack.forEach((l) => {
        /** Booleans to check */
        const timedOut = (d.getTime() >= l.time + (l.ttl * 60000));
        const ttlExceedsThreshold = l.ttl >= settings.lfg.ttl_threshold;
        const alreadySentWarning = l.warning;
        const shouldSendWarning = settings.lfg.ttl_warning;
        const warningTimeMet = (l.time + ((l.ttl * 60000) - d.getTime())) <= (Math.round((l.ttl * 60000) / 4));
        const sendWarning = (ttlExceedsThreshold && !alreadySentWarning && shouldSendWarning && warningTimeMet);

        if (timedOut) {
          this.timeout(bot, l.id);
        } else if (sendWarning) {
          const timeLeft = Math.round(Math.round((60000 * l.ttl) / 4) / 60000);
          this.warning(bot, l.id, timeLeft);
        }
      });
    } catch (err) {
      console.error(err);
    }
  },
};
<commit_msg>[LFG] Clean up LFGHandler<commit_after>// Handles all LFG-related events

const chalk = require('chalk');
const Discord = require('discord.js');
const moment = require('moment');
const settings = require('../settings');

const INTERVAL = settings.lfg.update_interval;

const DEFAULT_EMBED_COLOR = 0x009395;

/**
 * Format for all LFG requests, embedded in the Client (bot) object -->
 *
 * 'LFG ID' => {
 *  @param {Snowflake} id, message ID that bot sends
 *  @param {String} party_leader_name, user name of command issuer & leader of party
 *  @param {Snowflake} party_leader_id, user ID of party leader
 *  @param {String} code, LFG code for the game
 *  @param {String} game, Game title (proper formatting)
 *  @param {String} mode, Game mode
 *  @param {Date} time, When the LFG request was made
 *  @param {Date} expire_date, When the LFG request will expire
 *  @param {Integer} ttl, Number of minutes until LFG request expires
 *  @param {Array[Snowflake]} party, List of user IDs of users in the party
 *  @param {Integer} max_party_size, Maximum # of users in the party before it fills up
 *  @param {Snowflake} channel, Channel ID where the LFG request was made
 *  @param {Boolean} warning, Has a warning been sent out yet?
 * }
 */

/**
 * @function buildMessage
 * Builds a RichEmbed Discord message to send for the LFG
 *
 * @returns {Discord.RichEmbed}
 */
const buildMessage = (bot, lfgObj, { type = 'default' } = {}) => {
  const embed = new Discord.RichEmbed();
  const game = bot.games.get(lfgObj.code);
  const mode = game.modes.indexOf(lfgObj.mode);
  const indexOfGame = game.modes.indexOf(lfgObj.mode);

  /* Configure options for Rich Embed */
  const color = DEFAULT_EMBED_COLOR;
  let title = `${lfgObj.party_leader_name} is looking for a ${lfgObj.game} group!`;
  let { thumbnail } = game;
  let description = `**Game mode:** ${game.modes_proper[indexOfGame]}\n**Party size:** ${lfgObj.max_party_size}`;
  let fields = [[
    'Want to join?',
    `Click the 👍 below to reserve a spot!\n${lfgObj.party_leader_name}: click the 🚫 below to cancel the party.\n\n**Party:** <@${lfgObj.party_leader_id}> (1/${lfgObj.max_party_size})`,
  ]];
  let footer = 'Expires';
  let timestamp = lfgObj.expire_date;

  switch (type) {
    case 'timeout':
      fields = [
        [
          '⏰ **This LFG request has timed out.** ⏰',
          `**Party:** ${lfgObj.party.map(m => `<@${m}>`).join(' ')} (${lfgObj.party.length}/${lfgObj.max_party_size})`,
        ],
      ];
      footer = 'Timed out';
      timestamp = null;
      break;

    case 'cancelled':
      title = `${lfgObj.party_leader_name}'s party has been cancelled`;
      thumbnail = 'https://i.imgur.com/jSYuGrc.png';
      description = `**Game:** ${lfgObj.game}\n**Game mode:** ${game.modes_proper[mode]}\n**Party:** ${lfgObj.party.map(m => `<@${m}>`).join(' ')} (${lfgObj.party.length}/${lfgObj.max_party_size})`;
      fields = [];
      timestamp = null;
      footer = 'Cancelled';
      break;

    case 'default':
    default:
      break;
  }

  /* Set up the Discord Rich Embed message */
  embed.setColor(color);
  /* Header */
  embed.setTitle(title);
  embed.setThumbnail(thumbnail);
  /* Body */
  embed.setDescription(description);
  fields.forEach(([fieldName, value]) => embed.addField(fieldName, value));
  /* Footer */
  embed.setFooter(footer);
  embed.setTimestamp(timestamp);

  return embed;
};

/**
 * @function addLFG
 * Adds new LFG party to the LFG stack
 *
 * @param {Discord.Client} bot
 * @param {object} obj -- lfg object which contains all information as shown above
 */
const addLFG = (bot, object) => {
  try {
    let isFirstEntry;

    if (bot.lfgStack.size === 0) isFirstEntry = true;

    const embed = buildMessage(bot, object);

    bot.channels.get(object.channel).send({ embed }).then((message) => {
      object.id = message.id;
      bot.lfgStack.set(object.id, object);
      if (isFirstEntry) bot.lfgUpdate(true, INTERVAL);
      message.react('👍').then(() => {
        message.react('🚫').then(() => {
          console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${object.party_leader_name} made an LFG for ${object.game} | ${object.mode} | party of ${object.max_party_size} | ${object.ttl} minutes`));
        }).catch(err => console.log(chalk.bgRed(`[${moment().format(settings.timeFormat)}] ${err}`)));
      }).catch(err => console.log(chalk.bgRed(`[${moment().format(settings.timeFormat)}] ${err}`)));
    }).catch(err => console.error(err));
  } catch (err) {
    console.error(err);
  }
};

/**
 * @func complete
 * Party is full; Complete the LFG party ✓
 *
 * @param {Discord.Client} bot
 * @param {Snowflake} id - LFG id
 */
const complete = (bot, id) => {
  try {
    const stack = bot.lfgStack.get(id);
    const game = bot.games.get(stack.code);
    const mode = game.modes.indexOf(stack.mode);
    const embed = new Discord.RichEmbed()
      .setTitle(`${stack.party_leader_name}'s party is now full & ready to go!`)
      .setDescription(`**Game:** ${stack.game}\n**Game mode:** ${game.modes_proper[mode]}\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')}`)
      .setColor(0x009395)
      .setThumbnail('https://i.imgur.com/RsRmWcm.png');

    const ch = bot.channels.get(stack.channel);
    ch.send({ embed });

    // Remove from LFG stack
    bot.lfgStack.delete(id);
    if (bot.lfgStack.size === 0) bot.lfgUpdate(false);

    console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG party for ${stack.game} is full.`));
  } catch (err) {
    console.error(err);
  }
};

/**
 * @function addToParty
 * Add a user into an LFG party
 *
 * @param {Discord.Client} bot
 * @param {Snowflake} id - LFG id
 * @param {Snowflake} userid - User id to add into LFG party
 */
const addToParty = (bot, id, userid) => {
  try {
    // Push user into party array
    const stack = bot.lfgStack.get(id);
    stack.party.push(userid);

    const embed = buildMessage(bot, stack);
    const ch = bot.channels.get(stack.channel);
    const { guild } = ch;

    ch.fetchMessage(stack.id).then((message) => {
      message.edit({ embed }).then(() => {
      }).catch(err => console.error(err));
    }).catch(err => console.error(err));

    bot.lfgStack.set(id, stack);

    // Attempt to move user into party leader's channel
    const memberToMoveTo = guild.members.get(bot.lfgStack.get(id).party_leader_id);
    const memberToMove = guild.members.get(userid);
    if (typeof memberToMoveTo.voiceChannel !== 'undefined') {
      memberToMove.edit({ channel: memberToMoveTo.voiceChannelID }, `Moved by @${settings.botNameProper} for LFG`).then().catch(err => console.error(err));
    }

    // Check if the party is full
    if (stack.party.length === stack.max_party_size) {
      complete(bot, id);
    }
  } catch (err) {
    console.error(err);
  }
};

/**
 * @func removeFromParty
 * Remove a user from an LFG party
 *
 * @param {Discord.Client} bot
 * @param {Snowflake} id - LFG id
 * @param {Snowflake} userid - User id to remove from LFG party
 */
const removeFromParty = (bot, id, userid) => {
  try {
    // Remove user from party array
    const index = bot.lfgStack.get(id).party.indexOf(userid);
    if (index > -1) {
      bot.lfgStack.get(id).party.splice(index, 1);
      const stack = bot.lfgStack.get(id);
      const embed = buildMessage(bot, stack);
      const ch = bot.channels.get(stack.channel);
      ch.fetchMessage(stack.id).then((message) => {
        message.edit({ embed }).then(() => {
        }).catch(err => console.error(err));
      }).catch(err => console.error(err));
    } else return;
  } catch (err) { console.error(err); }
};

/**
 * @func timeout
 * LFG party is timed out, remove from the LFG stack
 *
 * @param {Discord.Client} bot
 * @param {Snowflake} id - LFG id
 */
const timeout = (bot, id) => {
  try {
    // Edit LFG message
    const stack = bot.lfgStack.get(id);
    const embed = buildMessage(bot, stack, { type: 'timeout' });

    const ch = bot.channels.get(stack.channel);
    ch.fetchMessage(stack.id).then((message) => {
      bot.lfgStack.delete(id);
      if (bot.lfgStack.size === 0) bot.lfgUpdate(false);
      message.edit({ embed }).then(() => {
        console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG for ${stack.game} has timed out.`));
      }).catch(err => console.error(err));
    }).catch(err => console.error(err));
  } catch (err) {
    console.error(err);
  }
};

/**
 * @func warning
 * Running out of time on LFG party; Issue warning message
 *
 * @param {Discord.Client} bot
 * @param {Snowflake} id - LFG id
 * @param {Number} timeLeft - Minutes remaining on LFG party
 */
const warning = (bot, id, timeLeft) => {
  try {
    const stack = bot.lfgStack.get(id);
    const game = bot.games.get(stack.code);
    const mode = game.modes.indexOf(stack.mode);
    const embed = new Discord.RichEmbed()
      .setTitle(`${stack.party_leader_name}'s party has ${timeLeft} minutes left before it times out!`)
      .setDescription(`**Game:** ${stack.game}\n**Game mode:** ${game.modes_proper[mode]}\n**Party:** ${stack.party.map(m => `<@${m}>`).join(' ')} (${stack.party.length}/${stack.max_party_size})`)
      .setColor(0x009395);
    stack.warning = true;
    const ch = bot.channels.get(stack.channel);
    ch.send({ embed });
    console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG is running out of time.`));
  } catch (err) {
    console.error(err);
  }
};

/**
 * @func cancel
 * Cancel the LFG party
 *
 * @param bot
 * @param id - LFG id
 * @param removed - Whether the LFG party message was removed or not
 */
const cancel = (bot, id, removed) => {
  try {
    const stack = bot.lfgStack.get(id);
    const embed = buildMessage(bot, stack, { type: 'cancelled' });

    // If the message was deleted, do this stuff:
    if (removed) {
      bot.lfgStack.delete(id);
      if (bot.lfgStack.size === 0) bot.lfgUpdate(false);
      return console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG party for ${stack.game} has been deleted.`));
    }

    // Attempt to fetch message
    bot.channels.get(stack.channel).fetchMessage(stack.id).then((message) => {
      message.edit({ embed }).then(() => {
        // Remove from LFG stack and edit message
        bot.lfgStack.delete(id);
        if (bot.lfgStack.size === 0) bot.lfgUpdate(false);
        return console.log(chalk.bgYellow.black(`[${moment().format(settings.timeFormat)}] ${stack.party_leader_name}'s LFG party for ${stack.game} has been cancelled.`));
      }).catch(err => console.error(err));
    }).catch(err => console.error(err));

    return 0;
  } catch (err) {
    return console.error(err);
  }
};

/**
 * @func update
 * LFG update interval function
 *
 * @param bot
 */
const update = (bot) => {
  try {
    const d = new Date();

    bot.lfgStack.forEach((l) => {
      /** Booleans to check */
      const timedOut = (d.getTime() >= l.time + (l.ttl * 60000));
      const ttlExceedsThreshold = l.ttl >= settings.lfg.ttl_threshold;
      const alreadySentWarning = l.warning;
      const shouldSendWarning = settings.lfg.ttl_warning;
      const warningTimeMet = (l.time + ((l.ttl * 60000) - d.getTime())) <= (Math.round((l.ttl * 60000) / 4));
      const sendWarning = (ttlExceedsThreshold && !alreadySentWarning && shouldSendWarning && warningTimeMet);

      if (timedOut) {
        timeout(bot, l.id);
      } else if (sendWarning) {
        const timeLeft = Math.round(Math.round((60000 * l.ttl) / 4) / 60000);
        warning(bot, l.id, timeLeft);
      }
    });
  } catch (err) {
    console.error(err);
  }
};

module.exports = {
  buildMessage,
  addLFG,
  addToParty,
  removeFromParty,
  timeout,
  warning,
  complete,
  cancel,
  update,
};
<|endoftext|>


<commit_before>'use strict'

const Promise = require('bluebird')
const t = require('../lib/translator')
const Logger = require('../lib/logger')
const languages = require('../lib/lang')
const mixpanel = require('../lib/mixpanel')
const state = require('../lib/state')
const repo = require('../db/repo')
const config = require('../config')
const _ = require('lodash')

const maxTextReplyLength = 320

const responseType = {
  help: '#help',
  getStarted: '#getstarted',
  reset: '#reset',
  switch: '#switch',
  list: '#list'
}

const baseHelpOptions = [
  {
    'type': 'postback',
    'title': 'Show all languages',
    'payload': responseType.list
  }
]

const helpQuickReply = {
  'content_type': 'text',
  'title': 'Need help?',
  'payload': '#help'
}

const examples = [
  'English', 'German', 'Italian', 'Korean', 'Dutch', 'Japanese', 'Hindi', 'Spanish', 'French', 'Indonesian', 'Russian', 'Chinese', 'Greek'
]

class MessageHandler {

  constructor(bot) {
    this.bot = bot
  }

  // pseudo middleware handler
  next(sender) {
    let next = this.bot
    const profile = state.get(sender.id)
    if (profile !== undefined) {
      next = {
        getProfile: (id, cb) => {
          cb(null, profile)
        }
      }
    }
    return next
  }

  // typing sends a typing response to the recipient
  typing(sender) {
    this.bot.setTyping(sender.id, true)
  }

  // handleMessage is our main handler
  handleMessage(payload, reply) {

    // clear state if we need to
    state.flushIfSizeLimitExceeded()

    const { sender, message, postback } = payload

    // we don't care about handling our own responses
    if (sender.id === config.fb_page_id) return

    return new Promise((resolve) => {

      this.next(sender).getProfile(sender.id, (err, profile) => {
        if (err) return Logger.log(`getProfileError: ${JSON.stringify(err)}`)
        // cache the user for subsequent requests
        state.set(sender.id, profile)

        repo(sender.id).get().then((response, err) => {
          if (err) Logger.log(`getAsyncError: ${JSON.stringify(err)}`)

          const { context } = response

          // check for postbacks
          if (postback && postback.payload) {
            return resolve(this.handlePostBack(context, postback, profile, sender, reply))
          }
          // check if a quick reply (comes in as a message)
          else if (message && message.quick_reply && message.quick_reply.payload) {
            return resolve(this.handlePostBack(context, message.quick_reply, profile, sender, reply))
          }

          // not a postback and not a text message, return
          else if (!message.text) return resolve(false)

          else if (_.includes(message.text.toLowerCase(), 'help') && !context) {
            return resolve(this.handleHelp(context, profile, sender, reply))
          }
          // reset or switch command
          else if (message.text === responseType.reset || message.text === responseType.switch) {
            return resolve(this.handlePostBack(context, {payload: message.text}, profile, sender, reply))
          }
          // no context
          else if (!context) {
            return resolve(this.handleNoContext(sender, profile, message, reply))
          }
          // we have context and the user has sent a text message, before translation check if this is
          // a possible direct change command: {lang} to {lang}
          else if (isPossibleChangeCommand(message.text)) {
            const context = getContextFromMessage(message.text, true)
            if (context.hasTwo && context.from !== context.to) {
              return (resolve(this.handleSetContext(context.code, context.from, context.to, sender, null, reply)))
            }
          }
          // we made it! translate the message
          return resolve(this.handleTranslation(context, sender, message, reply))
        })
      })
    })
  }

  // handlePostBack
  handlePostBack(context, postback, profile, sender, reply) {
    Logger.log('handlePostBack')
    if (postback.payload === responseType.getStarted) {
      return this.handleGetStarted(sender, profile, reply)
    }
    else if (postback.payload === responseType.help) {
      return this.handleHelp(context, profile, sender, reply)
    }
    else if (postback.payload === responseType.reset) {
      return this.handleReset(sender, reply)
    }
    else if (postback.payload === responseType.switch) {
      return this.handleSwitch(context, sender, reply)
    }
    else if (postback.payload === responseType.list) {
      return this.handleShowAllLanguages(sender, reply)
    }
  }

  // handleGetStarted
  handleGetStarted(sender, profile, reply) {
    Logger.log('handleGetStarted')
    reply({
      text: `Hola ${profile && profile.first_name}! Start by telling me what languages to translate for you. Say something like ${getSmartExample(profile)}\n\nAsk me for "help" at any time`
    }, () => {
      mixpanel.setPerson(sender, profile)
      mixpanel.track('I click to get started', sender)
    })
  }

  // handleHelp
  handleHelp(context, profile, sender, reply) {
    Logger.log('handleHelp')
    let text = `Hola. I see you've asked for some help... \n\nTell me what languages to translate by saying something like ${getSmartExample(profile)}`
    let options = baseHelpOptions

    if (context) {
      context = getContextFromCode(context)
      text = `Hola again. I see you've asked for some help...\n\nI'm currently translating everything you say from ${context.from} to ${context.to}\n\n`
      options.unshift(
        {
          'type': 'postback',
          'title': 'Reset',
          'payload': responseType.reset
        },
        {
          'type': 'postback',
          'title': `${context.to} to ${context.from}`,
          'payload': responseType.switch
        }
      )
    }

    return reply({
      'attachment': {
        'type': 'template',
        'payload': {
          'template_type': 'button',
          'text': text,
          'buttons': options
        }
      }
    }, () => {
      mixpanel.track('I ask for help', sender)
    })
  }

  // handleShowAllLanguages
  // due to Facebook's payload size limit we need to chunk the list into seprate bits
  handleShowAllLanguages(sender, reply) {
    const list = getAllLanguageNames()
    const first = list.splice(0, list.length / 3)
    const second = list.splice(0, list.length / 2)
    reply({text: `OK, here goes...\n\n${first.toString().replace(/,/g, ', ')}`}, () => {
      reply({text: `${second.toString().replace(/,/g, ', ')}`}, () => {
        reply({text: `${list.toString().replace(/,/g, ', ')}\n\n *GASP*`})
      })
    })
  }

  // handleReset
  handleReset(sender, reply) {
    repo(sender.id).set('').then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: 'OK, what should I translate for you next?'
      }, () => {
        mixpanel.track('I reset context', sender)
      })
    })
  }

  // handleSwitch
  handleSwitch(context, sender, reply) {
    context = switchContext(getContextFromCode(context))
    return this.handleSetContext(context.code, context.from, context.to, sender, null, reply)
  }

  // handleNoContext
  handleNoContext(sender, profile, message, reply) {
    Logger.log('handleNoContext')
    const context = getContextFromMessage(message.text)
    if (context.hasTwo && context.from !== context.to) {
      return this.handleSetContext(context.code, context.from, context.to, sender, message, reply)
    }
    if (this.handleGeneralResponse(sender, profile, message, reply) !== false) {
      return
    }

    let text = 'Oops, I didn\'t quite catch that. Ask me for "help" at anytime'
    if (context.hasOne) text = `I only caught ${context.from} in there. Try again, or ask me for "help"`
    return reply({
      text: text
    }, () => {
      mixpanel.track('I incorrectly set context', sender, message)
    })
  }

  // handleGeneralResponse such as hello, goodbye etc
  handleGeneralResponse(sender, profile, message, reply) {
    if (_.includes(['goodbye', 'bye', 'adios', 'ciao', 'seeya', 'see you', 'later'], message.text.toLowerCase())) {
      return reply({
        text: `Adiós ${profile.gender === 'male' ? 'muchacho' : 'muchacha'}`
      }, () => {
        mixpanel.track('I say goodbye without context', sender, message)
      })
    }
    if (_.includes(['hello', 'hi', 'howdy', 'hallo', 'yo', 'hey', 'sup', 'hiya'], message.text.toLowerCase())) {
      return reply({
        text: getRandom([`Hi ${profile.first_name}!`, 'Hello', '¡Hola!', `Hey ${profile.first_name}!`, 'Oh hey there!', 'Hallo', 'Howdy', 'Oh Hiiiiii'])
      }, () => {
        mixpanel.track('I say hello without context', sender, message)
      })
    }
    if (_.includes(message.text.toLowerCase(), 'hmm')) {
      return reply({ text: 'hmmmm...' })
    }
    return false
  }

  // handleSetContext
  handleSetContext(code, from, to, sender, message, reply) {
    Logger.log('handleSetContext')
    repo(sender.id).set(code).then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: `${from} to ${to}. Got it! Now go ahead and tell me what to say.`
      }, () => {
        mixpanel.track('I set context', sender, message)
      })
    })
  }

  // handleTranslation
  handleTranslation(context, sender, message, reply) {
    let response = {}
    t.translate(message.text, context).
      then((result) => {
        if (result.length > maxTextReplyLength) {
          return reply({text: 'I\'m sorry, I can\'t translate all of that in one go. Try again with a smaller message'})
        }
        response.text = result
        // check if the user actually wants help by passing a quick reply button with the translated text
        if (_.includes(message.text.toLowerCase(), 'help')) {
          response.quick_replies = [helpQuickReply]
        }
        reply(response, (err) => {
          if (err) Logger.log(err)
          mixpanel.track('I send a message to be translated', sender, message)
        })
      }).
      catch((err) => {
        if (err) Logger.log(err)
        reply({ text: 'Oh no, something has gone wrong. Please try that again' })
      })
  }

  // _privates
  // expose these for tests
  _privates() {
    return {
      getContextFromMessage: getContextFromMessage,
      getContextFromCode: getContextFromCode,
      switchContext: switchContext,
      getLanguageName: getLanguageName,
      getAllLanguageNames: getAllLanguageNames,
      getLanguageNameLocale: getLanguageNameLocale,
      getSmartExample: getSmartExample,
      isPossibleChangeCommand: isPossibleChangeCommand
    }
  }
}

// private methods
// getContextFromMessage
function getContextFromMessage(message, strict) {
  const ctxMatches = getContextMatches(message, strict)
  const { hasTwo, hasOne, hasNone } = ctxMatches
  let code, from, to
  if (hasOne || hasTwo) {
    from = _.capitalize(ctxMatches.matches[0].name)
  }
  if (hasTwo) {
    code = `${ctxMatches.matches[0].code}:${ctxMatches.matches[1].code}`
    to = _.capitalize(ctxMatches.matches[1].name)
  }
  return { code, from, to, hasTwo, hasOne, hasNone }
}

// getContextFromCode
function getContextFromCode(code) {
  const codes = code.split(':')
  return {
    code,
    from: getLanguageName(codes[0]),
    to: getLanguageName(codes[1]),
    hasTwo: true
  }
}

// getContext
function getContextMatches(message, strict) {
  let matches = []
  const words = message.split(' ')
  if (!strict || (strict && words.length === 3)) {
    words.forEach((word) => {
      languages.filter(item => item.name === _.lowerCase(word)).map((lang) => {
        if (matches.length === 2) return
        matches.push(lang)
      })
    })
  }
  return {
    hasTwo: matches.length === 2,
    hasOne: matches.length === 1,
    hasNone: matches.length === 0,
    matches: matches
  }
}

// switchContext
function switchContext(context) {
  const values = context.code.split(':')
  const from = context.to
  const to = context.from

  context.code = `${values[1]}:${values[0]}`
  context.from = from
  context.to = to

  return context
}

// getLanguageName
function getLanguageName(code) {
  const name = languages.filter(item => item.code === code).map((lang) => {
    return lang.name
  })
  if (name && name.length) return _.capitalize(name[0])
}

// listAllLanguages
function getAllLanguageNames() {
  let list = []
  languages.forEach(function(lang) {
    list.push(_.capitalize(lang.name))
  })
  return list
}

// getLanguageNameLocale
function getLanguageNameLocale(profile) {
  if (!profile || (profile && !profile.locale || profile.locale.indexOf('_') === -1)) return
  const code = profile.locale.split('_')[0]
  return getLanguageName(code)
}

// getRandom
function getRandom(responses) {
  return responses[Math.floor(Math.random()*responses.length)]
}

// getSmartExample
function getSmartExample(profile) {
  const locale = getLanguageNameLocale(profile)
  let shuffled = shuffleArray(_.clone(examples))
  if (!locale) return `"${shuffled[0]} to ${shuffled[1]}"`

  shuffled = _.remove(shuffled, (n) => { return n !== locale })
  return `"${locale} to ${shuffled[0]}"`
}

// shuffleArray
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

// isPossibleChangeCommand
function isPossibleChangeCommand(message) {
  const words = message.split(' ')
  return words.length === 3 && words[1] === 'to'
}

module.exports = MessageHandler
<commit_msg>logging<commit_after>'use strict'

const Promise = require('bluebird')
const t = require('../lib/translator')
const Logger = require('../lib/logger')
const languages = require('../lib/lang')
const mixpanel = require('../lib/mixpanel')
const state = require('../lib/state')
const repo = require('../db/repo')
const config = require('../config')
const _ = require('lodash')

const maxTextReplyLength = 320

const responseType = {
  help: '#help',
  getStarted: '#getstarted',
  reset: '#reset',
  switch: '#switch',
  list: '#list'
}

const baseHelpOptions = [
  {
    'type': 'postback',
    'title': 'Show all languages',
    'payload': responseType.list
  }
]

const helpQuickReply = {
  'content_type': 'text',
  'title': 'Need help?',
  'payload': '#help'
}

const examples = [
  'English', 'German', 'Italian', 'Korean', 'Dutch', 'Japanese', 'Hindi', 'Spanish', 'French', 'Indonesian', 'Russian', 'Chinese', 'Greek'
]

class MessageHandler {

  constructor(bot) {
    this.bot = bot
  }

  // pseudo middleware handler
  next(sender) {
    let next = this.bot
    const profile = state.get(sender.id)
    if (profile !== undefined) {
      next = {
        getProfile: (id, cb) => {
          cb(null, profile)
        }
      }
    }
    return next
  }

  // typing sends a typing response to the recipient
  typing(sender) {
    this.bot.setTyping(sender.id, true)
  }

  // handleMessage is our main handler
  handleMessage(payload, reply) {

    // clear state if we need to
    state.flushIfSizeLimitExceeded()

    const { sender, message, postback } = payload

    // we don't care about handling our own responses
    if (sender.id === config.fb_page_id) return

    return new Promise((resolve) => {

      this.next(sender).getProfile(sender.id, (err, profile) => {
        if (err) return Logger.log(`getProfileError: ${JSON.stringify(err)}`)
        // cache the user for subsequent requests
        state.set(sender.id, profile)

        repo(sender.id).get().then((response, err) => {
          if (err) Logger.log(`getAsyncError: ${JSON.stringify(err)}`)

          const { context } = response

          // check for postbacks
          if (postback && postback.payload) {
            return resolve(this.handlePostBack(context, postback, profile, sender, reply))
          }
          // check if a quick reply (comes in as a message)
          else if (message && message.quick_reply && message.quick_reply.payload) {
            return resolve(this.handlePostBack(context, message.quick_reply, profile, sender, reply))
          }

          // not a postback and not a text message, return
          else if (!message.text) return resolve(false)

          else if (_.includes(message.text.toLowerCase(), 'help') && !context) {
            return resolve(this.handleHelp(context, profile, sender, reply))
          }
          // reset or switch command
          else if (message.text === responseType.reset || message.text === responseType.switch) {
            return resolve(this.handlePostBack(context, {payload: message.text}, profile, sender, reply))
          }
          // no context
          else if (!context) {
            return resolve(this.handleNoContext(sender, profile, message, reply))
          }
          // we have context and the user has sent a text message, before translation check if this is
          // a possible direct change command: {lang} to {lang}
          else if (isPossibleChangeCommand(message.text)) {
            const context = getContextFromMessage(message.text, true)
            if (context.hasTwo && context.from !== context.to) {
              return (resolve(this.handleSetContext(context.code, context.from, context.to, sender, null, reply)))
            }
          }
          // we made it! translate the message
          return resolve(this.handleTranslation(context, sender, message, reply))
        })
      })
    })
  }

  // handlePostBack
  handlePostBack(context, postback, profile, sender, reply) {
    Logger.log('handlePostBack')
    Logger.log('PAYLOAD --> ' + JSON.stringify(postback.payload))
    if (postback.payload === responseType.getStarted) {
      return this.handleGetStarted(sender, profile, reply)
    }
    else if (postback.payload === responseType.help) {
      return this.handleHelp(context, profile, sender, reply)
    }
    else if (postback.payload === responseType.reset) {
      return this.handleReset(sender, reply)
    }
    else if (postback.payload === responseType.switch) {
      return this.handleSwitch(context, sender, reply)
    }
    else if (postback.payload === responseType.list) {
      return this.handleShowAllLanguages(sender, reply)
    }
  }

  // handleGetStarted
  handleGetStarted(sender, profile, reply) {
    Logger.log('handleGetStarted')
    reply({
      text: `Hola ${profile && profile.first_name}! Start by telling me what languages to translate for you. Say something like ${getSmartExample(profile)}\n\nAsk me for "help" at any time`
    }, () => {
      mixpanel.setPerson(sender, profile)
      mixpanel.track('I click to get started', sender)
    })
  }

  // handleHelp
  handleHelp(context, profile, sender, reply) {
    Logger.log('handleHelp')
    let text = `Hola. I see you've asked for some help... \n\nTell me what languages to translate by saying something like ${getSmartExample(profile)}`
    let options = baseHelpOptions

    if (context) {
      context = getContextFromCode(context)
      text = `Hola again. I see you've asked for some help...\n\nI'm currently translating everything you say from ${context.from} to ${context.to}\n\n`
      options.unshift(
        {
          'type': 'postback',
          'title': 'Reset',
          'payload': responseType.reset
        },
        {
          'type': 'postback',
          'title': `${context.to} to ${context.from}`,
          'payload': responseType.switch
        }
      )
    }

    return reply({
      'attachment': {
        'type': 'template',
        'payload': {
          'template_type': 'button',
          'text': text,
          'buttons': options
        }
      }
    }, () => {
      mixpanel.track('I ask for help', sender)
    })
  }

  // handleShowAllLanguages
  // due to Facebook's payload size limit we need to chunk the list into seprate bits
  handleShowAllLanguages(sender, reply) {
    const list = getAllLanguageNames()
    const first = list.splice(0, list.length / 3)
    const second = list.splice(0, list.length / 2)
    reply({text: `OK, here goes...\n\n${first.toString().replace(/,/g, ', ')}`}, () => {
      reply({text: `${second.toString().replace(/,/g, ', ')}`}, () => {
        reply({text: `${list.toString().replace(/,/g, ', ')}\n\n *GASP*`})
      })
    })
  }

  // handleReset
  handleReset(sender, reply) {
    repo(sender.id).set('').then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: 'OK, what should I translate for you next?'
      }, () => {
        mixpanel.track('I reset context', sender)
      })
    })
  }

  // handleSwitch
  handleSwitch(context, sender, reply) {
    context = switchContext(getContextFromCode(context))
    return this.handleSetContext(context.code, context.from, context.to, sender, null, reply)
  }

  // handleNoContext
  handleNoContext(sender, profile, message, reply) {
    Logger.log('handleNoContext')
    const context = getContextFromMessage(message.text)
    if (context.hasTwo && context.from !== context.to) {
      return this.handleSetContext(context.code, context.from, context.to, sender, message, reply)
    }
    if (this.handleGeneralResponse(sender, profile, message, reply) !== false) {
      return
    }

    let text = 'Oops, I didn\'t quite catch that. Ask me for "help" at anytime'
    if (context.hasOne) text = `I only caught ${context.from} in there. Try again, or ask me for "help"`
    return reply({
      text: text
    }, () => {
      mixpanel.track('I incorrectly set context', sender, message)
    })
  }

  // handleGeneralResponse such as hello, goodbye etc
  handleGeneralResponse(sender, profile, message, reply) {
    if (_.includes(['goodbye', 'bye', 'adios', 'ciao', 'seeya', 'see you', 'later'], message.text.toLowerCase())) {
      return reply({
        text: `Adiós ${profile.gender === 'male' ? 'muchacho' : 'muchacha'}`
      }, () => {
        mixpanel.track('I say goodbye without context', sender, message)
      })
    }
    if (_.includes(['hello', 'hi', 'howdy', 'hallo', 'yo', 'hey', 'sup', 'hiya'], message.text.toLowerCase())) {
      return reply({
        text: getRandom([`Hi ${profile.first_name}!`, 'Hello', '¡Hola!', `Hey ${profile.first_name}!`, 'Oh hey there!', 'Hallo', 'Howdy', 'Oh Hiiiiii'])
      }, () => {
        mixpanel.track('I say hello without context', sender, message)
      })
    }
    if (_.includes(message.text.toLowerCase(), 'hmm')) {
      return reply({ text: 'hmmmm...' })
    }
    return false
  }

  // handleSetContext
  handleSetContext(code, from, to, sender, message, reply) {
    Logger.log('handleSetContext')
    repo(sender.id).set(code).then((err) => {
      if (err) Logger.log(err)
      return reply({
        text: `${from} to ${to}. Got it! Now go ahead and tell me what to say.`
      }, () => {
        mixpanel.track('I set context', sender, message)
      })
    })
  }

  // handleTranslation
  handleTranslation(context, sender, message, reply) {
    let response = {}
    t.translate(message.text, context).
      then((result) => {
        if (result.length > maxTextReplyLength) {
          return reply({text: 'I\'m sorry, I can\'t translate all of that in one go. Try again with a smaller message'})
        }
        response.text = result
        // check if the user actually wants help by passing a quick reply button with the translated text
        if (_.includes(message.text.toLowerCase(), 'help')) {
          response.quick_replies = [helpQuickReply]
        }
        reply(response, (err) => {
          if (err) Logger.log(err)
          mixpanel.track('I send a message to be translated', sender, message)
        })
      }).
      catch((err) => {
        if (err) Logger.log(err)
        reply({ text: 'Oh no, something has gone wrong. Please try that again' })
      })
  }

  // _privates
  // expose these for tests
  _privates() {
    return {
      getContextFromMessage: getContextFromMessage,
      getContextFromCode: getContextFromCode,
      switchContext: switchContext,
      getLanguageName: getLanguageName,
      getAllLanguageNames: getAllLanguageNames,
      getLanguageNameLocale: getLanguageNameLocale,
      getSmartExample: getSmartExample,
      isPossibleChangeCommand: isPossibleChangeCommand
    }
  }
}

// private methods
// getContextFromMessage
function getContextFromMessage(message, strict) {
  const ctxMatches = getContextMatches(message, strict)
  const { hasTwo, hasOne, hasNone } = ctxMatches
  let code, from, to
  if (hasOne || hasTwo) {
    from = _.capitalize(ctxMatches.matches[0].name)
  }
  if (hasTwo) {
    code = `${ctxMatches.matches[0].code}:${ctxMatches.matches[1].code}`
    to = _.capitalize(ctxMatches.matches[1].name)
  }
  return { code, from, to, hasTwo, hasOne, hasNone }
}

// getContextFromCode
function getContextFromCode(code) {
  const codes = code.split(':')
  return {
    code,
    from: getLanguageName(codes[0]),
    to: getLanguageName(codes[1]),
    hasTwo: true
  }
}

// getContext
function getContextMatches(message, strict) {
  let matches = []
  const words = message.split(' ')
  if (!strict || (strict && words.length === 3)) {
    words.forEach((word) => {
      languages.filter(item => item.name === _.lowerCase(word)).map((lang) => {
        if (matches.length === 2) return
        matches.push(lang)
      })
    })
  }
  return {
    hasTwo: matches.length === 2,
    hasOne: matches.length === 1,
    hasNone: matches.length === 0,
    matches: matches
  }
}

// switchContext
function switchContext(context) {
  const values = context.code.split(':')
  const from = context.to
  const to = context.from

  context.code = `${values[1]}:${values[0]}`
  context.from = from
  context.to = to

  return context
}

// getLanguageName
function getLanguageName(code) {
  const name = languages.filter(item => item.code === code).map((lang) => {
    return lang.name
  })
  if (name && name.length) return _.capitalize(name[0])
}

// listAllLanguages
function getAllLanguageNames() {
  let list = []
  languages.forEach(function(lang) {
    list.push(_.capitalize(lang.name))
  })
  return list
}

// getLanguageNameLocale
function getLanguageNameLocale(profile) {
  if (!profile || (profile && !profile.locale || profile.locale.indexOf('_') === -1)) return
  const code = profile.locale.split('_')[0]
  return getLanguageName(code)
}

// getRandom
function getRandom(responses) {
  return responses[Math.floor(Math.random()*responses.length)]
}

// getSmartExample
function getSmartExample(profile) {
  const locale = getLanguageNameLocale(profile)
  let shuffled = shuffleArray(_.clone(examples))
  if (!locale) return `"${shuffled[0]} to ${shuffled[1]}"`

  shuffled = _.remove(shuffled, (n) => { return n !== locale })
  return `"${locale} to ${shuffled[0]}"`
}

// shuffleArray
function shuffleArray(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = array[i]
    array[i] = array[j]
    array[j] = temp
  }
  return array
}

// isPossibleChangeCommand
function isPossibleChangeCommand(message) {
  const words = message.split(' ')
  return words.length === 3 && words[1] === 'to'
}

module.exports = MessageHandler
<|endoftext|>


<commit_before>'use strict';

// jscs:disable requireTemplateStrings

/*!
 * Snakeskin
 * https://github.com/SnakeskinTpl/Snakeskin
 *
 * Released under the MIT license
 * https://github.com/SnakeskinTpl/Snakeskin/blob/master/LICENSE
 */

import Snakeskin from '../core';
import { toObj } from './object';
import { IS_NODE } from '../consts/hacks';
import { wsStart, wsEnd } from '../consts/regs';
import { templateRank } from '../consts/other';
import { LEFT_BOUND, RIGHT_BOUND, ADV_LEFT_BOUND } from '../consts/literals';

export const
	stack = [];

Snakeskin.toObj = toObj;

/**
 * Adds file content by the specified path to the stack
 *
 * @param {string} base - base path
 * @param {string} file - file path
 * @param {string} eol - EOL symbol
 * @param {?string=} [opt_renderAs] - rendering type of templates
 * @return {(string|boolean)}
 */
Snakeskin.include = function (base, file, eol, opt_renderAs) {
	if (!IS_NODE) {
		return false;
	}

	const
		type = opt_renderAs || 'template';

	const
		fs = require('fs'),
		path = require('path'),
		glob = require('glob');

	const
		s = ADV_LEFT_BOUND + LEFT_BOUND,
		e = RIGHT_BOUND;

	try {
		const
			extname = path.extname(file),
			{include} = Snakeskin.LocalVars;

		const src = path.resolve(
			path.dirname(base),
			file + (extname ? '' : '.ss')
		);

		const
			arr = glob.hasMagic(src) ? glob.sync(src) : [src];

		for (let i = 0; i < arr.length; i++) {
			const
				src = path.normalize(arr[i]);

			if (src in include && include[src] >= templateRank[type]) {
				continue;
			}

			include[src] = templateRank[type];
			const
				file = fs.readFileSync(src, 'utf8');

			stack.push(
				`${s}__setFile__ ${src}${e}` +

				(opt_renderAs ?
					`${s}__set__ renderAs '${opt_renderAs}'${e}` : '') +

				`${wsStart.test(file) ? '' : eol}` +

				file +

				`${wsEnd.test(file) ? '' : `${eol}${s}__cutLine__${e}`}` +
				`${s}__endSetFile__${e}`
			);
		}

		return true;

	} catch (err) {
		stack.push(`${s}__setError__ ${err.message}${e}`);
	}

	return false;
};
<commit_msg>Improved include logic<commit_after>'use strict';

// jscs:disable requireTemplateStrings

/*!
 * Snakeskin
 * https://github.com/SnakeskinTpl/Snakeskin
 *
 * Released under the MIT license
 * https://github.com/SnakeskinTpl/Snakeskin/blob/master/LICENSE
 */

import Snakeskin from '../core';
import { toObj } from './object';
import { IS_NODE } from '../consts/hacks';
import { wsStart, wsEnd } from '../consts/regs';
import { templateRank } from '../consts/other';
import { LEFT_BOUND, RIGHT_BOUND, ADV_LEFT_BOUND } from '../consts/literals';

export const
	stack = [];

Snakeskin.toObj = toObj;

/**
 * Adds file content by the specified path to the stack
 *
 * @param {string} base - base path
 * @param {string} file - file path
 * @param {string} eol - EOL symbol
 * @param {?string=} [opt_renderAs] - rendering type of templates
 * @return {(string|boolean)}
 */
Snakeskin.include = function (base, file, eol, opt_renderAs) {
	if (!IS_NODE) {
		return false;
	}

	const
		type = opt_renderAs || 'template';

	const
		fs = require('fs'),
		path = require('path'),
		glob = require('glob'),
		findup = require('findup-sync');

	const
		s = ADV_LEFT_BOUND + LEFT_BOUND,
		e = RIGHT_BOUND;

	try {
		const
			extname = path.extname(file),
			{include} = Snakeskin.LocalVars;

		const
			dirname = path.basename(file),
			mainFile = `?(${dirname && !glob.hasMagic(dirname) ? `${dirname}|` : ''}main|index).ss`;

		let src = /(?:\\|\/)$/.test(file) ?
			file + mainFile : file + (extname ? '' : '.ss');

		if (!path.isAbsolute(src)) {
			if (/^\./.test(src)) {
				src = path.resolve(path.dirname(base), src);

			} else {
				src = path.resolve(findup('node_modules'), src);
			}
		}

		const
			arr = glob.hasMagic(src) ? glob.sync(src) : [src];

		for (let i = 0; i < arr.length; i++) {
			const
				src = path.normalize(arr[i]);

			if (src in include && include[src] >= templateRank[type]) {
				continue;
			}

			include[src] = templateRank[type];
			const
				file = fs.readFileSync(src, 'utf8');

			stack.push(
				`${s}__setFile__ ${src}${e}` +

				(opt_renderAs ?
					`${s}__set__ renderAs '${opt_renderAs}'${e}` : '') +

				`${wsStart.test(file) ? '' : eol}` +

				file +

				`${wsEnd.test(file) ? '' : `${eol}${s}__cutLine__${e}`}` +
				`${s}__endSetFile__${e}`
			);
		}

		return true;

	} catch (err) {
		stack.push(`${s}__setError__ ${err.message}${e}`);
	}

	return false;
};
<|endoftext|>


<commit_before>export const isPaymentRequestSupported = 'PaymentRequest' in window;

export const requestPayment = () => {
  const methodData = [{
    supportedMethods: ['basic-card'],
    data: {
      supportedNetworks: ['visa', 'mastercard'],
    },
  }];
  const details = {
    displayItems: [
      {
        label: 'FrontFest 2018 ticket',
        amount: { currency: 'EUR', value: '25.00' },
      },
      {
        label: 'Unicorn stickers',
        amount: { currency: 'EUR', value: '5.00' },
      },
    ],
    total: {
      label: 'Total',
      amount: { currency: 'EUR', value: '30.00' },
    }
  };
  const options = {
    requestPayerName: true,
    requestPayerEmail: true
  };
  const request = new window.PaymentRequest(methodData, details, options);

  return request.show()
    .then((paymentResponse) => {
      paymentResponse.complete('success');
      return Promise.resolve();
    });
}
<commit_msg>Adapt Payment Request demo for Codemotion Madrid 2018 talk<commit_after>export const isPaymentRequestSupported = 'PaymentRequest' in window;

export const requestPayment = () => {
  const methodData = [{
    supportedMethods: ['basic-card'],
    data: {
      supportedNetworks: ['visa', 'mastercard'],
    },
  }];
  const details = {
    displayItems: [
      {
        label: 'Codemotion 2018 ticket',
        amount: { currency: 'EUR', value: '40.00' },
      },
      {
        label: 'Unicorn stickers',
        amount: { currency: 'EUR', value: '5.00' },
      },
    ],
    total: {
      label: 'Total',
      amount: { currency: 'EUR', value: '45.00' },
    }
  };
  const options = {
    requestPayerName: true,
    requestPayerEmail: true
  };
  const request = new window.PaymentRequest(methodData, details, options);

  return request.show()
    .then((paymentResponse) => {
      paymentResponse.complete('success');
      return Promise.resolve();
    });
}
<|endoftext|>


<commit_before>import node_count from "./count.js";
import node_each from "./each.js";
import node_eachBefore from "./eachBefore.js";
import node_eachAfter from "./eachAfter.js";
import node_find from "./find.js";
import node_sum from "./sum.js";
import node_sort from "./sort.js";
import node_path from "./path.js";
import node_ancestors from "./ancestors.js";
import node_descendants from "./descendants.js";
import node_leaves from "./leaves.js";
import node_links from "./links.js";
import node_iterator from "./iterator.js";

export default function hierarchy(data, children) {
  var root = new Node(data),
      valued = +data.value && (root.value = data.value),
      node,
      nodes = [root],
      child,
      childs,
      i,
      n;

  if (children == null) children = defaultChildren;

  while (node = nodes.pop()) {
    if (valued) node.value = +node.data.value;
    if ((childs = children(node.data)) && (n = (childs = Array.from(childs)).length)) {
      node.children = childs;
      for (i = n - 1; i >= 0; --i) {
        nodes.push(child = childs[i] = new Node(childs[i]));
        child.parent = node;
        child.depth = node.depth + 1;
      }
    }
  }

  return root.eachBefore(computeHeight);
}

function node_copy() {
  return hierarchy(this).eachBefore(copyData);
}

function defaultChildren(d) {
  return d.children;
}

function copyData(node) {
  node.data = node.data.data;
}

export function computeHeight(node) {
  var height = 0;
  do node.height = height;
  while ((node = node.parent) && (node.height < ++height));
}

export function Node(data) {
  this.data = data;
  this.depth =
  this.height = 0;
  this.parent = null;
}

Node.prototype = hierarchy.prototype = {
  constructor: Node,
  count: node_count,
  each: node_each,
  eachAfter: node_eachAfter,
  eachBefore: node_eachBefore,
  find: node_find,
  sum: node_sum,
  sort: node_sort,
  path: node_path,
  ancestors: node_ancestors,
  descendants: node_descendants,
  leaves: node_leaves,
  links: node_links,
  copy: node_copy,
  [Symbol.iterator]: node_iterator
};
<commit_msg>Adopt default argument.<commit_after>import node_count from "./count.js";
import node_each from "./each.js";
import node_eachBefore from "./eachBefore.js";
import node_eachAfter from "./eachAfter.js";
import node_find from "./find.js";
import node_sum from "./sum.js";
import node_sort from "./sort.js";
import node_path from "./path.js";
import node_ancestors from "./ancestors.js";
import node_descendants from "./descendants.js";
import node_leaves from "./leaves.js";
import node_links from "./links.js";
import node_iterator from "./iterator.js";

export default function hierarchy(data, children = defaultChildren) {
  var root = new Node(data),
      valued = +data.value && (root.value = data.value),
      node,
      nodes = [root],
      child,
      childs,
      i,
      n;

  while (node = nodes.pop()) {
    if (valued) node.value = +node.data.value;
    if ((childs = children(node.data)) && (n = (childs = Array.from(childs)).length)) {
      node.children = childs;
      for (i = n - 1; i >= 0; --i) {
        nodes.push(child = childs[i] = new Node(childs[i]));
        child.parent = node;
        child.depth = node.depth + 1;
      }
    }
  }

  return root.eachBefore(computeHeight);
}

function node_copy() {
  return hierarchy(this).eachBefore(copyData);
}

function defaultChildren(d) {
  return d.children;
}

function copyData(node) {
  node.data = node.data.data;
}

export function computeHeight(node) {
  var height = 0;
  do node.height = height;
  while ((node = node.parent) && (node.height < ++height));
}

export function Node(data) {
  this.data = data;
  this.depth =
  this.height = 0;
  this.parent = null;
}

Node.prototype = hierarchy.prototype = {
  constructor: Node,
  count: node_count,
  each: node_each,
  eachAfter: node_eachAfter,
  eachBefore: node_eachBefore,
  find: node_find,
  sum: node_sum,
  sort: node_sort,
  path: node_path,
  ancestors: node_ancestors,
  descendants: node_descendants,
  leaves: node_leaves,
  links: node_links,
  copy: node_copy,
  [Symbol.iterator]: node_iterator
};
<|endoftext|>