<commit_before>define([
    'background/collection/playlists',
    'background/collection/streamItems',
    'background/collection/videoSearchResults',
    'background/model/user',
    'common/enum/listItemType',
    'foreground/view/leftBasePane/playlistItemView',
    'foreground/view/leftCoveringPane/videoSearchResultView',
    'foreground/view/rightBasePane/streamItemView'
], function (Playlists, StreamItems, VideoSearchResults, User, ListItemType, PlaylistItemView, VideoSearchResultView, StreamItemView) {
    'use strict';

    var MultiSelectCompositeView = Backbone.Marionette.CompositeView.extend({

        events: {
            'click .list-item': 'setSelectedOnClick'
        },

        isFullyVisible: false,

        //  TODO: I might be able to make my life easier by making lazyload only check for either side visible.
        //  Tell images that they're able to bind lazyLoading functionality only once fully visible because when they're sliding in you're unable to tell if they're visible in one direction.
        //  But, I guess you know that direction is going to be loaded, so maybe it's fine to only check one direction?
        onFullyVisible: function () {
            if (_.isUndefined(this.ui.itemContainer))
                throw "itemContainer is undefined";

            $(this.children.map(function (child) {
                return child.ui.imageThumbnail.toArray();
            })).lazyload({
                container: this.ui.itemContainer,
                threshold: 250
            });

            this.isFullyVisible = true;
        },

        onAfterItemAdded: function (view) {
            if (this.isFullyVisible) {
                view.ui.imageThumbnail.lazyload({
                    container: this.ui.itemContainer,
                    threshold: 250
                });

                view.setTitleTooltip(view.ui.title);
            }
        },

        onRender: function () {
            var self = this;

            //  Allows for drag-and-drop of videos
            this.ui.itemContainer.sortable({

                connectWith: '.droppable-list',

                cursorAt: {
                    right: 35,
                    bottom: 40
                },

                //  Adding a delay helps preventing unwanted drags when clicking on an element.
                delay: 100,

                placeholder: 'sortable-placeholder list-item hidden-until-change',

                helper: function (ui, listItem) {

                    //  Create a new view instead of just copying the HTML in order to preserve HTML->Backbone.View relationship
                    var copyHelperView;
                    var viewOptions = {
                        model: self.collection.get(listItem.data('id')),
                        //  Don't lazy-load the view because copy helper is clearly visible
                        instant: true
                    };

                    var listItemType = listItem.data('type');

                    switch (listItemType) {
                        case ListItemType.PlaylistItem:
                            copyHelperView = new PlaylistItemView(viewOptions);
                            break;
                        case ListItemType.StreamItem:
                            copyHelperView = new StreamItemView(viewOptions);
                            break;
                        case ListItemType.VideoSearchResult:
                            copyHelperView = new VideoSearchResultView(viewOptions);
                            break;
                        default:
                            throw 'Unhandled ListItemType: ' + listItemType;
                    }

                    this.copyHelper = copyHelperView.render().$el.insertAfter(listItem);
                    this.copyHelper.addClass('copy-helper');

                    this.backCopyHelper = listItem.prev();
                    this.backCopyHelper.addClass('copy-helper');

                    $(this).data('copied', false);

                    return $('<span>', {
                        'class': 'selected-models-length'
                    });
                },
                change: function () {
                    //  There's a CSS redraw issue with my CSS selector: .listItem.copyHelper + .sortable-placeholder
                    //  So, I manually hide the placehelper (like it would be normally) until a change occurs -- then the CSS can take over.
                    $('.hidden-until-change').removeClass('hidden-until-change');
                },
                start: function (event, ui) {

                    var listItemType = ui.item.data('type');

                    //  TODO: This logic prevents dragging a duplicate streamItem to a Playlist, but I also would like to prevent
                    //  duplicates in the Stream.
                    if (listItemType === ListItemType.StreamItem) {
                        if (User.get('signedIn')) {
                            var streamItemId = ui.item.data('id');

                            //  Color the placeholder to indicate that the StreamItem can't be copied into the Playlist.
                            var draggedStreamItem = self.collection.get(streamItemId);

                            var videoAlreadyExists = Playlists.getActivePlaylist().get('items').videoAlreadyExists(draggedStreamItem.get('video'));
                            ui.placeholder.toggleClass('no-drop', videoAlreadyExists);
                        } else {
                            ui.placeholder.addClass('not-signed-in');
                        }
                    }

                    var modelToSelect = self.collection.get(ui.item.data('id'));
                    self.doSetSelected({
                        modelToSelect: modelToSelect,
                        drag: true
                    });

                    this.selectedItems = self.$el.find('.selected');

                    this.selectedItems.css({
                        opacity: '.5'
                    });

                    //  Set it here not in helper because dragStart may select a search result.
                    ui.helper.text(self.collection.selected().length);

                    //  TODO sortableItem vs sortable-item??
                    ui.item.data('sortableItem').scrollParent = ui.placeholder.parent();
                    ui.item.data('sortableItem').overflowOffset = ui.placeholder.parent().offset();
                },

                stop: function (event, ui) {

                    this.backCopyHelper.removeClass('copy-helper');

                    var copied = $(this).data('copied');
                    if (copied) {
                        this.copyHelper.removeClass('copy-helper');
                    }
                    else {
                        this.copyHelper.remove();

                        //  Whenever a PlaylistItem or StreamItem row is reorganized -- update.
                        var listItemType = ui.item.data('type');
                        if (listItemType === ListItemType.PlaylistItem || listItemType === ListItemType.StreamItem) {
                            self.collection.moveToIndex(ui.item.data('id'), ui.item.index());
                        }
                    }

                    this.selectedItems.css({
                        opacity: '1'
                    });

                    this.copyHelper = null;
                    this.backCopyHelper = null;
                    this.selectedItems = null;

                    //  Don't allow VideoSearchResults to be sorted -- copied is true when it moves to StreamItems.
                    //  Returning false cancels the sort.
                    var isVideoSearchResult = ui.item.data('type') === ListItemType.VideoSearchResult;

                    return copied || !isVideoSearchResult;
                },

                tolerance: 'pointer',
                receive: function (event, ui) {

                    //  TODO: I don't get when this would be used.
                    //  Don't allow receiving until collection is given because there shouldn't be anything to drop onto
                    if (_.isUndefined(self.collection)) {
                        console.log("collection is undefined, wow!");
                        ui.item.remove();
                        //  Set copied to true so that the item stays where it is.
                        ui.sender.data('copied', true);
                        return;
                    }

                    var listItemType = ui.item.data('type');

                    console.log("Receive is firing!");

                    if (listItemType === ListItemType.StreamItem) {

                        var draggedStreamItems = StreamItems.selected();
                        StreamItems.deselectAll();

                        var videos = _.map(draggedStreamItems, function(streamItem) {
                            return streamItem.get('video');
                        });

                        //var streamItemId = ui.item.data('id');
                        //var draggedStreamItem = StreamItems.get(streamItemId);

                        ////  Remove blue coloring if visible before waiting for addVideo to finish to give a more seamless swap to the new item.
                        //ui.item.removeClass('selected');

                        ////  Don't allow duplicates
                        //var videoAlreadyExists = self.collection.videoAlreadyExists(draggedStreamItem.get('video'));

                        //if (videoAlreadyExists) {
                        //    ui.item.remove();
                        //}
                        //else {

                        //    //  TODO: I need to indicate that an item is being saved to the server w/ a spinner + loading message.
                            self.model.addByVideosStartingAtIndex(videos, ui.item.index(), function () {
                                //  Remove item because it's a stream item and I've just added a playlist item at that index.
                                ui.item.remove();
                            });

                            //  TODO: There's a bit of lag which happens while waiting for the add event to propagate to the parent.
                            //  This makes Streamus seem unresponsive but this is clearly an encapsulation break... need to fix!
                            var emptyPlaylistMessage = $('.playlist-empty');
                            if (emptyPlaylistMessage.length > 0) {
                                emptyPlaylistMessage.addClass('hidden');
                            }
                        //}
                    }
                    else if (listItemType === ListItemType.PlaylistItem) {
                        var activePlaylistItems = Playlists.getActivePlaylist().get('items');

                        var draggedPlaylistItems = activePlaylistItems.selected();
                        self.collection.addByDraggedPlaylistItems(draggedPlaylistItems, ui.item.index());

                        activePlaylistItems.deselectAll();
                        ui.item.remove();
                    } else if (listItemType === ListItemType.VideoSearchResult) {
                        var draggedSearchResults = VideoSearchResults.selected();
                        VideoSearchResults.deselectAll();

                        self.collection.addByDraggedVideoSearchResults(draggedSearchResults, ui.item.index());
                        ui.item.remove();
                    }

                    ui.sender.data('copied', true);
                },

                over: function (event, ui) {
                    ui.item.data('sortableItem').scrollParent = ui.placeholder.parent();
                    ui.item.data('sortableItem').overflowOffset = ui.placeholder.parent().offset();
                }
            });

            return this;
        },

        setSelectedOnClick: function (event) {

            var id = $(event.currentTarget).data('id');
            var modelToSelect = this.collection.get(id);

            this.doSetSelected({
                shiftKey: event.shiftKey,
                ctrlKey: event.ctrlKey,
                modelToSelect: modelToSelect
            });

        },

        doSetSelected: function (options) {
            var modelToSelect = options.modelToSelect;

            var shiftKeyPressed = options.shiftKey || false;
            var ctrlKeyPressed = options.ctrlKey || false;
            var isDrag = options.drag || false;

            var isSelectedAlready = modelToSelect.get('selected');
            modelToSelect.set('selected', (ctrlKeyPressed && isSelectedAlready) ? false : true);

            console.log("I have set modelToSelect to selected:", modelToSelect, modelToSelect.get('selected'));

            //  When the shift key is pressed - select a block of search result items
            if (shiftKeyPressed) {

                var firstSelectedIndex = 0;
                var selectedIndex = this.collection.indexOf(modelToSelect);

                //  If the first item is being selected with shift held -- firstSelectedIndex isn't used and selection goes from the top.
                if (this.collection.selected().length > 1) {
                    var firstSelected = this.collection.firstSelected();

                    //  Get the search result which was selected first and go from its index.
                    firstSelectedIndex = this.collection.indexOf(firstSelected);
                }

                console.log("firstSelectedIndex:", firstSelectedIndex);

                //  Select all items between the selected item and the firstSelected item.
                this.collection.each(function (model, index) {
                    var isBetweenAbove = index <= selectedIndex && index >= firstSelectedIndex;
                    var isBetweenBelow = index >= selectedIndex && index <= firstSelectedIndex;

                    model.set('selected', isBetweenBelow || isBetweenAbove);
                });

                //  Holding the shift key is a bit of a special case. User expects the first item highlighted to be the 'firstSelected' and not the clicked.
                this.collection.at(firstSelectedIndex).set('firstSelected', true);

            } else if (ctrlKeyPressed) {
                //  Using the ctrl key to select an item resets firstSelect (which is a special scenario)
                //  but doesn't lose the other selected items.
                modelToSelect.set('firstSelected', true);
            } else if (!(isDrag && isSelectedAlready)) {
                //  All other selections are lost unless dragging a group of items.
                this.collection.deselectAllExcept(modelToSelect);
            }
        },

        //  TODO: This adds support for a sorted collection, but is slower than using the default implementation which leverages a document fragment.
        //  https://github.com/marionettejs/backbone.marionette/wiki/Adding-support-for-sorted-collections
        //  https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.collectionview.md#collectionviews-appendhtml
        appendHtml: function (collectionView, itemView, index) {
            var childrenContainer = collectionView.itemViewContainer ? collectionView.$(collectionView.itemViewContainer) : collectionView.$el;
            var children = childrenContainer.children();
            if (children.size() <= index) {
                childrenContainer.append(itemView.el);
            } else {
                children.eq(index).before(itemView.el);
            }
        }
    });

    return MultiSelectCompositeView;
});<commit_msg>Fixed clearing stream after dragging an item to playlist<commit_after>define([
    'background/collection/playlists',
    'background/collection/streamItems',
    'background/collection/videoSearchResults',
    'background/model/user',
    'common/enum/listItemType',
    'foreground/view/leftBasePane/playlistItemView',
    'foreground/view/leftCoveringPane/videoSearchResultView',
    'foreground/view/rightBasePane/streamItemView'
], function (Playlists, StreamItems, VideoSearchResults, User, ListItemType, PlaylistItemView, VideoSearchResultView, StreamItemView) {
    'use strict';

    var MultiSelectCompositeView = Backbone.Marionette.CompositeView.extend({

        events: {
            'click .list-item': 'setSelectedOnClick'
        },

        isFullyVisible: false,

        //  TODO: I might be able to make my life easier by making lazyload only check for either side visible.
        //  Tell images that they're able to bind lazyLoading functionality only once fully visible because when they're sliding in you're unable to tell if they're visible in one direction.
        //  But, I guess you know that direction is going to be loaded, so maybe it's fine to only check one direction?
        onFullyVisible: function () {
            if (_.isUndefined(this.ui.itemContainer))
                throw "itemContainer is undefined";

            $(this.children.map(function (child) {
                return child.ui.imageThumbnail.toArray();
            })).lazyload({
                container: this.ui.itemContainer,
                threshold: 250
            });

            this.isFullyVisible = true;
        },

        onAfterItemAdded: function (view) {
            if (this.isFullyVisible) {
                view.ui.imageThumbnail.lazyload({
                    container: this.ui.itemContainer,
                    threshold: 250
                });

                view.setTitleTooltip(view.ui.title);
            }
        },

        onRender: function () {
            var self = this;

            //  Allows for drag-and-drop of videos
            this.ui.itemContainer.sortable({

                connectWith: '.droppable-list',

                cursorAt: {
                    right: 35,
                    bottom: 40
                },

                //  Adding a delay helps preventing unwanted drags when clicking on an element.
                delay: 100,

                placeholder: 'sortable-placeholder list-item hidden-until-change',

                helper: function (ui, listItem) {

                    console.log("listItem:", listItem);

                    //  Create a new view instead of just copying the HTML in order to preserve HTML->Backbone.View relationship
                    var copyHelperView;
                    var viewOptions = {
                        model: self.collection.get(listItem.data('id')),
                        //  Don't lazy-load the view because copy helper is clearly visible
                        instant: true
                    };

                    var listItemType = listItem.data('type');

                    switch (listItemType) {
                        case ListItemType.PlaylistItem:
                            copyHelperView = new PlaylistItemView(viewOptions);
                            break;
                        case ListItemType.StreamItem:
                            copyHelperView = new StreamItemView(viewOptions);
                            break;
                        case ListItemType.VideoSearchResult:
                            copyHelperView = new VideoSearchResultView(viewOptions);
                            break;
                        default:
                            throw 'Unhandled ListItemType: ' + listItemType;
                    }

                    this.copyHelper = copyHelperView.render().$el.insertAfter(listItem);
                    this.copyHelper.addClass('copy-helper');

                    this.backCopyHelper = listItem.prev();
                    this.backCopyHelper.addClass('copy-helper');

                    $(this).data('copied', false);

                    return $('<span>', {
                        'class': 'selected-models-length'
                    });
                },
                change: function () {
                    //  There's a CSS redraw issue with my CSS selector: .listItem.copyHelper + .sortable-placeholder
                    //  So, I manually hide the placehelper (like it would be normally) until a change occurs -- then the CSS can take over.
                    $('.hidden-until-change').removeClass('hidden-until-change');
                },
                start: function (event, ui) {

                    var listItemType = ui.item.data('type');

                    //  TODO: This logic prevents dragging a duplicate streamItem to a Playlist, but I also would like to prevent
                    //  duplicates in the Stream.
                    if (listItemType === ListItemType.StreamItem) {
                        if (User.get('signedIn')) {
                            var streamItemId = ui.item.data('id');

                            //  Color the placeholder to indicate that the StreamItem can't be copied into the Playlist.
                            var draggedStreamItem = self.collection.get(streamItemId);

                            var videoAlreadyExists = Playlists.getActivePlaylist().get('items').videoAlreadyExists(draggedStreamItem.get('video'));
                            ui.placeholder.toggleClass('no-drop', videoAlreadyExists);
                        } else {
                            ui.placeholder.addClass('not-signed-in');
                        }
                    }

                    var modelToSelect = self.collection.get(ui.item.data('id'));
                    self.doSetSelected({
                        modelToSelect: modelToSelect,
                        drag: true
                    });

                    this.selectedItems = self.$el.find('.selected');

                    this.selectedItems.css({
                        opacity: '.5'
                    });

                    //  Set it here not in helper because dragStart may select a search result.
                    ui.helper.text(self.collection.selected().length);

                    //  TODO sortableItem vs sortable-item??
                    ui.item.data('sortableItem').scrollParent = ui.placeholder.parent();
                    ui.item.data('sortableItem').overflowOffset = ui.placeholder.parent().offset();
                },

                stop: function (event, ui) {

                    this.backCopyHelper.removeClass('copy-helper');

                    var copied = $(this).data('copied');
                    if (copied) {
                        this.copyHelper.removeClass('copy-helper');
                    }
                    else {
                        this.copyHelper.remove();

                        //  Whenever a PlaylistItem or StreamItem row is reorganized -- update.
                        var listItemType = ui.item.data('type');
                        if (listItemType === ListItemType.PlaylistItem || listItemType === ListItemType.StreamItem) {
                            self.collection.moveToIndex(ui.item.data('id'), ui.item.index());
                        }
                    }

                    this.selectedItems.css({
                        opacity: '1'
                    });

                    console.log("Setting copy helper to null", this, this.copyHelper);

                    this.copyHelper = null;
                    this.backCopyHelper = null;
                    this.selectedItems = null;

                    //  Don't allow VideoSearchResults to be sorted -- copied is true when it moves to StreamItems.
                    //  Returning false cancels the sort.
                    var isVideoSearchResult = ui.item.data('type') === ListItemType.VideoSearchResult;

                    return copied || !isVideoSearchResult;
                },

                tolerance: 'pointer',
                receive: function (event, ui) {

                    //  Don't allow receiving until collection is given because there shouldn't be anything to drop onto.
                    //  Useful when dragging from Stream to Playlist before user has signed in.
                    if (_.isUndefined(self.collection)) {
                        ui.item.remove();
                        //  Set copied to true so that the item stays where it is.
                        ui.sender.data('copied', true);
                        return;
                    }

                    var listItemType = ui.item.data('type');

                    if (listItemType === ListItemType.StreamItem) {

                        var draggedStreamItems = StreamItems.selected();
                        StreamItems.deselectAll();

                        var videos = _.map(draggedStreamItems, function(streamItem) {
                            return streamItem.get('video');
                        });

                        //  Swap copy helper out with the actual item once successfully dropped because Marionette keeps track of specific view instances.
                        ui.sender[0].copyHelper.replaceWith(ui.item);

                        //var streamItemId = ui.item.data('id');
                        //var draggedStreamItem = StreamItems.get(streamItemId);

                        ////  Remove blue coloring if visible before waiting for addVideo to finish to give a more seamless swap to the new item.
                        //ui.item.removeClass('selected');

                        ////  Don't allow duplicates
                        //var videoAlreadyExists = self.collection.videoAlreadyExists(draggedStreamItem.get('video'));

                        //if (videoAlreadyExists) {
                        //    ui.item.remove();
                        //}
                        //else {

                        //    //  TODO: I need to indicate that an item is being saved to the server w/ a spinner + loading message.
                            self.model.addByVideosStartingAtIndex(videos, ui.item.index());

                            //  TODO: There's a bit of lag which happens while waiting for the add event to propagate to the parent.
                            //  This makes Streamus seem unresponsive but this is clearly an encapsulation break... need to fix!
                            var emptyPlaylistMessage = $('.playlist-empty');
                            if (emptyPlaylistMessage.length > 0) {
                                emptyPlaylistMessage.addClass('hidden');
                            }
                        //}
                    }
                    else if (listItemType === ListItemType.PlaylistItem) {
                        var activePlaylistItems = Playlists.getActivePlaylist().get('items');

                        var draggedPlaylistItems = activePlaylistItems.selected();
                        self.collection.addByDraggedPlaylistItems(draggedPlaylistItems, ui.item.index());

                        activePlaylistItems.deselectAll();
                        ui.item.remove();
                    } else if (listItemType === ListItemType.VideoSearchResult) {
                        var draggedSearchResults = VideoSearchResults.selected();
                        VideoSearchResults.deselectAll();

                        self.collection.addByDraggedVideoSearchResults(draggedSearchResults, ui.item.index());
                        ui.item.remove();
                    }

                    ui.sender.data('copied', true);
                },

                over: function (event, ui) {
                    ui.item.data('sortableItem').scrollParent = ui.placeholder.parent();
                    ui.item.data('sortableItem').overflowOffset = ui.placeholder.parent().offset();
                }
            });

            return this;
        },

        setSelectedOnClick: function (event) {

            var id = $(event.currentTarget).data('id');
            var modelToSelect = this.collection.get(id);

            this.doSetSelected({
                shiftKey: event.shiftKey,
                ctrlKey: event.ctrlKey,
                modelToSelect: modelToSelect
            });

        },

        doSetSelected: function (options) {
            var modelToSelect = options.modelToSelect;

            var shiftKeyPressed = options.shiftKey || false;
            var ctrlKeyPressed = options.ctrlKey || false;
            var isDrag = options.drag || false;

            var isSelectedAlready = modelToSelect.get('selected');
            modelToSelect.set('selected', (ctrlKeyPressed && isSelectedAlready) ? false : true);

            console.log("I have set modelToSelect to selected:", modelToSelect, modelToSelect.get('selected'));

            //  When the shift key is pressed - select a block of search result items
            if (shiftKeyPressed) {

                var firstSelectedIndex = 0;
                var selectedIndex = this.collection.indexOf(modelToSelect);

                //  If the first item is being selected with shift held -- firstSelectedIndex isn't used and selection goes from the top.
                if (this.collection.selected().length > 1) {
                    var firstSelected = this.collection.firstSelected();

                    //  Get the search result which was selected first and go from its index.
                    firstSelectedIndex = this.collection.indexOf(firstSelected);
                }

                console.log("firstSelectedIndex:", firstSelectedIndex);

                //  Select all items between the selected item and the firstSelected item.
                this.collection.each(function (model, index) {
                    var isBetweenAbove = index <= selectedIndex && index >= firstSelectedIndex;
                    var isBetweenBelow = index >= selectedIndex && index <= firstSelectedIndex;

                    model.set('selected', isBetweenBelow || isBetweenAbove);
                });

                //  Holding the shift key is a bit of a special case. User expects the first item highlighted to be the 'firstSelected' and not the clicked.
                this.collection.at(firstSelectedIndex).set('firstSelected', true);

            } else if (ctrlKeyPressed) {
                //  Using the ctrl key to select an item resets firstSelect (which is a special scenario)
                //  but doesn't lose the other selected items.
                modelToSelect.set('firstSelected', true);
            } else if (!(isDrag && isSelectedAlready)) {
                //  All other selections are lost unless dragging a group of items.
                this.collection.deselectAllExcept(modelToSelect);
            }
        },

        //  TODO: This adds support for a sorted collection, but is slower than using the default implementation which leverages a document fragment.
        //  https://github.com/marionettejs/backbone.marionette/wiki/Adding-support-for-sorted-collections
        //  https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.collectionview.md#collectionviews-appendhtml
        appendHtml: function (collectionView, itemView, index) {
            var childrenContainer = collectionView.itemViewContainer ? collectionView.$(collectionView.itemViewContainer) : collectionView.$el;
            var children = childrenContainer.children();
            if (children.size() <= index) {
                childrenContainer.append(itemView.el);
            } else {
                children.eq(index).before(itemView.el);
            }
        }
    });

    return MultiSelectCompositeView;
});<|endoftext|>


<commit_before>//web.js
/**
 * Created by David Breuer on 08/12/15.
 *
 * @file web.js
 * @description
 *
 */
var express = require('express')
var app = express();

app.set('port', (process.env.PORT || 5000))
app.use(express.static(__dirname + '/app'))

app.listen(app.get('port'), function() {
    console.log("Node app is running at localhost:" + app.get('port'))
});
<commit_msg>component static access<commit_after>//web.js
/**
 * Created by David Breuer on 08/12/15.
 *
 * @file web.js
 * @description
 *
 */
var express = require('express');
var app = express();

app.set('port', (process.env.PORT || 5000))
app.use(express.static(__dirname + '/app'))
app.use('/bower_components',  express.static(__dirname + '/app/bower_components'));

app.listen(app.get('port'), function() {
    console.log("Node app is running at localhost:" + app.get('port'))
});
<|endoftext|>


<commit_before>/*!
 * fb-messenger-app
 * Copyright(c) 2016 Charles Araya
 * MIT Licensed
 */

import request from 'request'

const apiUrl = 'https://graph.facebook.com/v2.6/'
const notification_types = ['REGULAR', 'SILENT_PUSH', 'NO_PUSH']

/** Class representing a Facebook Messenger App. */
class Messenger {
  /**
   * Create a Messenger.
   *
   * @param {string} [token=null] - The Facebook Page Access Token.
   * @param {string} [notificationType=REGULAR] - The default message notification type.
   */
  constructor (token = null, notificationType = 'REGULAR') {
    if (!token) throw new Error('Facebook Page access token is missing.')
    if (notification_types.indexOf(notificationType) === -1) throw new Error('Invalid Notification Type')

    this.token = token
    this.notificationType = notificationType
  }

  /**
  * This method will subscribe App via API
  *
  * @callback [cb] - The callback function
  */
  subscribeApp (cb) {
    const req = {
      url: `${apiUrl}me/subscribed_apps`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: true
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send a plain Text Message.
  *
  * @param {string} recipient - The user id that will receive the message
  * @param {string} text - The text message (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendTextMessage (recipient, text, notificationType, cb) {
    this.sendApiMessage(recipient, { text: text }, notificationType, cb)
  }

  /**
  * This method will send a Quick Replies Message.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object[]} quickReplies - The quick replies to be sent with messages
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendQuickMessage (recipient, quickReplies, notificationType, cb) {
    const message = { quick_replies: quickReplies }
    this.sendApiMessage(recipient, message, notificationType, cb)
  }

  /**
  * This method will send an Attachment Message.
  *
  * @param {string} recipient - The user id that will receive the message
  * @param {string} type - The attachment type [image, audio, video, file, template]
  * @param {object} payload - The attachment payload
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendAttachmentMessage (recipient, type, payload, notificationType, cb) {
    const message = {
      attachment: {
        type: type,
        payload: payload
      }
    }
    this.sendApiMessage(recipient, message, notificationType, cb)
  }

  /**
  * This method will send a file attachment. The file type must be image, audio, video or file).
  * Image supported formats [jpg, png and gifs]
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} type - The file type ['image', 'audio', 'video' or 'file']
  * @param {string} fileUrl - The file url where it is hosted (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendFileMessage (recipient, type, fileUrl, notificationType, cb) {
    const payload = { url: fileUrl }
    this.sendAttachmentMessage(recipient, type, payload, notificationType, cb)
  }

  /**
  * This method will send a text and buttons attachment to request input from the user.
  * The buttons can open a URL, or make a back-end call to your webhook.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} text - The head text of the button message
  * @param {object[]} buttons - The Set of call-to-action buttons
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendButtonMessage (recipient, text, buttons, notificationType, cb) {
    const payload = {
      template_type: 'button',
      text: text,
      buttons: buttons
    }
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send a Structured Generic Message, to send a horizontal scrollable carousel of items,
  * each composed of an image attachment, short description and buttons to request input from the user.
  * The buttons can open a URL, or make a back-end call to your webhook.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object[]} elements - The Data for each bubble in message
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendGenericMessage (recipient, elements, notificationType, cb) {
    const payload = {
      template_type: 'generic',
      elements: elements
    }
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send a Receipt Message, to send a order confirmation,
  * with the transaction summary and description for each item.
  *
  * @param {string} recipient - The receipient user id.
  * @param {object} receipt - The Receipt Template
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendReceiptMessage (recipient, receipt, notificationType, cb) {
    const payload = receipt
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Itinerary Message, that contains the itinerary and receipt.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} itinerary - The payload of itinerary template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendItineraryMessage (recipient, itinerary, notificationType, cb) {
    const payload = itinerary
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Check-In Reminder Message.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} checkin - The payload of checkin template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendCheckinMessage (recipient, checkin, notificationType, cb) {
    const payload = checkin
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Boarding Pass Message, that contains boarding
  * passes for one or more flights or one more passengers
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} boardingpass - The payload of boarding pass template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendBoardingpassMessage (recipient, boardingpass, notificationType, cb) {
    const payload = boardingpass
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Flight Update Message.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} flightupdate - The payload of flight update template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendFlightupdateMessage (recipient, flightupdate, notificationType, cb) {
    const payload = flightupdate
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send a message to the user.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} message - The message object (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendApiMessage (recipient, message, notificationType, cb) {
    if (typeof notificationType === 'function') {
      cb = notificationType
      notificationType = this.notificationType
    }
    const req = {
      url: `${apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        message: message,
        notification_type: notificationType
      }
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send Sender Actions, this are typing indicators or send read receipts,
  * to let the user know you are processing their request
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} senderAction - The action typing indicator (required)
  * @callback [cb] - The callback function
  */
  sendSenderAction (recipient, senderAction, cb) {
    const req = {
      url: `${apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        sender_action: senderAction
      }
    }
    sendRequest(req, cb)
  }

  /**
  * This method will set a Greeting Text.
  *
  * @param {(object|string)} text - The greeting text
  * @callback [cb] - The callback function
  */
  setGreetingText (text, cb) {
    if (typeof text === 'string') {
      text = {text: text}
    }
    const method = 'POST'
    const params = {
      setting_type: 'greeting',
      greeting: text
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will set a Get Started Button, in the welcome screen.
  *
  * @param {(object[]|string)} payload - The string payloads
  * @callback [cb] - The callback function
  */
  setGetStartedButton (payload, cb) {
    if (typeof payload === 'string') {  // Case a string is entered
      payload = [{
        payload: payload
      }]
    }
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'new_thread',
      call_to_actions: payload // max 1
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will set a Persistent Menu, always available to the user.
  *
  * @param {object[]} menuItems - The menu items in the menu
  * @callback [cb] - The callback function
  */
  setPersistentMenu (menuItems, cb) {
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'existing_thread',
      call_to_actions: menuItems
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will delete a Thread Setting. Get Started Button('new_thread')
  * or Persistent Menu('existing_thread')
  *
  * @param {string} threadType - The thread type to be deleted
  * @callback [cb] - The callback function
  */
  deleteThreadSetting (threadType, cb) {
    const method = 'DELETE'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: threadType
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will Configure the Thread Setting on Messenger.
  *
  * @param {string} method - The call request (POST or DELETE)
  * @param {object} params - The configuration parameters
  * @callback [cb] - The callback function
  */
  sendThreadSettingsRequest (method, params, cb) {
    const req = {
      url: `${apiUrl}me/thread_settings`,
      qs: {
        access_token: this.token
      },
      method: method,
      json: params
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send an account linking call-2-action to the user.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} title - The title of the account linking CTA
  * @param {string} imageUrl - The URL of the image in the account linking CTA
  * @param {string} authUrl - The Authentication callback URL
  * @callback [cb] - The callback function
  */
  sendAccountLinkingMessage (recipient, title, imageUrl, authUrl, cb) {
    const message = {
      attachment: {
        type: 'tempalte',
        payload: {
          template_type: 'generic',
          elements: [{
            title: title,
            image_url: imageUrl,
            buttons: [{
              type: 'account_link',
              url: authUrl
            }]
          }]
        }
      }
    }
    this.sendApiMessage(recipient, message, cb)
  }

  /**
  * This method will retrieve the user page-scoped ID (PSID) using the account
  * linking endpoint.
  *
  * @param {string} token - The account linking token
  * @callback [cb] - The callback function
  */
  getUserPsid (token, cb) {
    const req = {
      url: `${apiUrl}me`,
      qs: {
        access_token: this.token,
        fields: 'recipient',
        account_linking_token: token
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send an account unlinking call-2-action to the user.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} title - The title of the account linking CTA
  * @param {string} imageUrl - The URL of the image in the account linking CTA
  * @callback [cb] - The callback function
  */
  sendAccountUnlinkingMessage (recipient, title, imageUrl, cb) {
    const message = {
      attachment: {
        type: 'tempalte',
        payload: {
          template_type: 'generic',
          elements: [{
            title: title,
            image_url: imageUrl,
            buttons: [{
              type: 'account_unlink'
            }]
          }]
        }
      }
    }
    this.sendApiMessage(recipient, message, cb)
  }

  /**
  * This method will unlink the user account.
  *
  * @param {string} psid - A valid page-scoped ID (PSID)
  * @callback [cb] - The callback function
  */
  unlinkAccount (psid, cb) {
    const req = {
      url: `${apiUrl}me/unlink_accounts`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        psid: psid
      }
    }
    sendRequest(req, cb)
  }

  /**
  * This method will get the User Profile, used to query more information about
  * the user, and personalize the experience further.
  *
  * @param {string} userId - The user id
  * @callback [cb] - The callback function
  */
  getUserProfile (userId, cb) {
    const req = {
      url: `${apiUrl}${userId}`,
      qs: {
        access_token: this.token,
        fields: 'first_name,last_name,profile_pic,locale,timezone,gender'
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }
}

/**
* This method will send a Request to the Send API.
*
* @param {object} req - The request being send
* @callback [cb] - The callback function
*/
const sendRequest = (req, cb) => {
  request(req, (error, response, body) => {
    if (!cb) return
    if (error) return cb(error)
    if (response.body.error) return cb(response.body.error)
    cb(null, response.body)
  })
}

export default Messenger
<commit_msg>feat(EventEmitter): add EventEmitter support<commit_after>/*!
 * fb-messenger-app
 * Copyright(c) 2016 Charles Araya
 * MIT Licensed
 */

import request from 'request'
import {EventEmitter} from 'events'

const apiUrl = 'https://graph.facebook.com/v2.6/'
const notification_types = ['REGULAR', 'SILENT_PUSH', 'NO_PUSH']

/** Class representing a Facebook Messenger App. */
class Messenger extends EventEmitter{
  /**
   * Create a Messenger.
   *
   * @param {string} [token=null] - The Facebook Page Access Token.
   * @param {string} [notificationType=REGULAR] - The default message notification type.
   */
  constructor (token = null, notificationType = 'REGULAR') {
    super()

    if (!token) throw new Error('Facebook Page access token is missing.')
    if (notification_types.indexOf(notificationType) === -1) throw new Error('Invalid Notification Type')

    this.token = token
    this.notificationType = notificationType
  }

  /**
  * This method will subscribe App via API
  *
  * @callback [cb] - The callback function
  */
  subscribeApp (cb) {
    const req = {
      url: `${apiUrl}me/subscribed_apps`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: true
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send a plain Text Message.
  *
  * @param {string} recipient - The user id that will receive the message
  * @param {string} text - The text message (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendTextMessage (recipient, text, notificationType, cb) {
    this.sendApiMessage(recipient, { text: text }, notificationType, cb)
  }

  /**
  * This method will send a Quick Replies Message.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object[]} quickReplies - The quick replies to be sent with messages
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendQuickMessage (recipient, quickReplies, notificationType, cb) {
    const message = { quick_replies: quickReplies }
    this.sendApiMessage(recipient, message, notificationType, cb)
  }

  /**
  * This method will send an Attachment Message.
  *
  * @param {string} recipient - The user id that will receive the message
  * @param {string} type - The attachment type [image, audio, video, file, template]
  * @param {object} payload - The attachment payload
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendAttachmentMessage (recipient, type, payload, notificationType, cb) {
    const message = {
      attachment: {
        type: type,
        payload: payload
      }
    }
    this.sendApiMessage(recipient, message, notificationType, cb)
  }

  /**
  * This method will send a file attachment. The file type must be image, audio, video or file).
  * Image supported formats [jpg, png and gifs]
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} type - The file type ['image', 'audio', 'video' or 'file']
  * @param {string} fileUrl - The file url where it is hosted (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendFileMessage (recipient, type, fileUrl, notificationType, cb) {
    const payload = { url: fileUrl }
    this.sendAttachmentMessage(recipient, type, payload, notificationType, cb)
  }

  /**
  * This method will send a text and buttons attachment to request input from the user.
  * The buttons can open a URL, or make a back-end call to your webhook.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} text - The head text of the button message
  * @param {object[]} buttons - The Set of call-to-action buttons
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendButtonMessage (recipient, text, buttons, notificationType, cb) {
    const payload = {
      template_type: 'button',
      text: text,
      buttons: buttons
    }
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send a Structured Generic Message, to send a horizontal scrollable carousel of items,
  * each composed of an image attachment, short description and buttons to request input from the user.
  * The buttons can open a URL, or make a back-end call to your webhook.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object[]} elements - The Data for each bubble in message
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendGenericMessage (recipient, elements, notificationType, cb) {
    const payload = {
      template_type: 'generic',
      elements: elements
    }
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send a Receipt Message, to send a order confirmation,
  * with the transaction summary and description for each item.
  *
  * @param {string} recipient - The receipient user id.
  * @param {object} receipt - The Receipt Template
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendReceiptMessage (recipient, receipt, notificationType, cb) {
    const payload = receipt
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Itinerary Message, that contains the itinerary and receipt.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} itinerary - The payload of itinerary template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendItineraryMessage (recipient, itinerary, notificationType, cb) {
    const payload = itinerary
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Check-In Reminder Message.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} checkin - The payload of checkin template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendCheckinMessage (recipient, checkin, notificationType, cb) {
    const payload = checkin
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Boarding Pass Message, that contains boarding
  * passes for one or more flights or one more passengers
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} boardingpass - The payload of boarding pass template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendBoardingpassMessage (recipient, boardingpass, notificationType, cb) {
    const payload = boardingpass
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send an Airplane Flight Update Message.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} flightupdate - The payload of flight update template (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendFlightupdateMessage (recipient, flightupdate, notificationType, cb) {
    const payload = flightupdate
    this.sendAttachmentMessage(recipient, 'template', payload, notificationType, cb)
  }

  /**
  * This method will send a message to the user.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {object} message - The message object (required)
  * @param {string} [notificationType] - The notification type
  * @callback [cb] - The callback function
  */
  sendApiMessage (recipient, message, notificationType, cb) {
    if (typeof notificationType === 'function') {
      cb = notificationType
      notificationType = this.notificationType
    }
    const req = {
      url: `${apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        message: message,
        notification_type: notificationType
      }
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send Sender Actions, this are typing indicators or send read receipts,
  * to let the user know you are processing their request
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} senderAction - The action typing indicator (required)
  * @callback [cb] - The callback function
  */
  sendSenderAction (recipient, senderAction, cb) {
    const req = {
      url: `${apiUrl}me/messages`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        recipient: {
          id: recipient
        },
        sender_action: senderAction
      }
    }
    sendRequest(req, cb)
  }

  /**
  * This method will set a Greeting Text.
  *
  * @param {(object|string)} text - The greeting text
  * @callback [cb] - The callback function
  */
  setGreetingText (text, cb) {
    if (typeof text === 'string') {
      text = {text: text}
    }
    const method = 'POST'
    const params = {
      setting_type: 'greeting',
      greeting: text
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will set a Get Started Button, in the welcome screen.
  *
  * @param {(object[]|string)} payload - The string payloads
  * @callback [cb] - The callback function
  */
  setGetStartedButton (payload, cb) {
    if (typeof payload === 'string') {  // Case a string is entered
      payload = [{
        payload: payload
      }]
    }
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'new_thread',
      call_to_actions: payload // max 1
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will set a Persistent Menu, always available to the user.
  *
  * @param {object[]} menuItems - The menu items in the menu
  * @callback [cb] - The callback function
  */
  setPersistentMenu (menuItems, cb) {
    const method = 'POST'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: 'existing_thread',
      call_to_actions: menuItems
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will delete a Thread Setting. Get Started Button('new_thread')
  * or Persistent Menu('existing_thread')
  *
  * @param {string} threadType - The thread type to be deleted
  * @callback [cb] - The callback function
  */
  deleteThreadSetting (threadType, cb) {
    const method = 'DELETE'
    const params = {
      setting_type: 'call_to_actions',
      thread_state: threadType
    }
    this.sendThreadSettingsRequest(method, params, cb)
  }

  /**
  * This method will Configure the Thread Setting on Messenger.
  *
  * @param {string} method - The call request (POST or DELETE)
  * @param {object} params - The configuration parameters
  * @callback [cb] - The callback function
  */
  sendThreadSettingsRequest (method, params, cb) {
    const req = {
      url: `${apiUrl}me/thread_settings`,
      qs: {
        access_token: this.token
      },
      method: method,
      json: params
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send an account linking call-2-action to the user.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} title - The title of the account linking CTA
  * @param {string} imageUrl - The URL of the image in the account linking CTA
  * @param {string} authUrl - The Authentication callback URL
  * @callback [cb] - The callback function
  */
  sendAccountLinkingMessage (recipient, title, imageUrl, authUrl, cb) {
    const message = {
      attachment: {
        type: 'tempalte',
        payload: {
          template_type: 'generic',
          elements: [{
            title: title,
            image_url: imageUrl,
            buttons: [{
              type: 'account_link',
              url: authUrl
            }]
          }]
        }
      }
    }
    this.sendApiMessage(recipient, message, cb)
  }

  /**
  * This method will retrieve the user page-scoped ID (PSID) using the account
  * linking endpoint.
  *
  * @param {string} token - The account linking token
  * @callback [cb] - The callback function
  */
  getUserPsid (token, cb) {
    const req = {
      url: `${apiUrl}me`,
      qs: {
        access_token: this.token,
        fields: 'recipient',
        account_linking_token: token
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }

  /**
  * This method will send an account unlinking call-2-action to the user.
  *
  * @param {string} recipient - The user id to whom we're sending the text message
  * @param {string} title - The title of the account linking CTA
  * @param {string} imageUrl - The URL of the image in the account linking CTA
  * @callback [cb] - The callback function
  */
  sendAccountUnlinkingMessage (recipient, title, imageUrl, cb) {
    const message = {
      attachment: {
        type: 'tempalte',
        payload: {
          template_type: 'generic',
          elements: [{
            title: title,
            image_url: imageUrl,
            buttons: [{
              type: 'account_unlink'
            }]
          }]
        }
      }
    }
    this.sendApiMessage(recipient, message, cb)
  }

  /**
  * This method will unlink the user account.
  *
  * @param {string} psid - A valid page-scoped ID (PSID)
  * @callback [cb] - The callback function
  */
  unlinkAccount (psid, cb) {
    const req = {
      url: `${apiUrl}me/unlink_accounts`,
      qs: {
        access_token: this.token
      },
      method: 'POST',
      json: {
        psid: psid
      }
    }
    sendRequest(req, cb)
  }

  /**
  * This method will get the User Profile, used to query more information about
  * the user, and personalize the experience further.
  *
  * @param {string} userId - The user id
  * @callback [cb] - The callback function
  */
  getUserProfile (userId, cb) {
    const req = {
      url: `${apiUrl}${userId}`,
      qs: {
        access_token: this.token,
        fields: 'first_name,last_name,profile_pic,locale,timezone,gender'
      },
      method: 'GET',
      json: true
    }
    sendRequest(req, cb)
  }
}

/**
* This method will send a Request to the Send API.
*
* @param {object} req - The request being send
* @callback [cb] - The callback function
*/
const sendRequest = (req, cb) => {
  request(req, (error, response, body) => {
    if (!cb) return
    if (error) return cb(error)
    if (response.body.error) return cb(response.body.error)
    cb(null, response.body)
  })
}

export default Messenger
<|endoftext|>


<commit_before>const es6x = require('../src/es6x');

describe('es6x', () => {
    it('should convert simple div', () => {
        expect(es6x `<div className="block"></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block'
            },
            children: []
        });
    });

    it('should convert simple div with prop', () => {
        expect(es6x `<div className="block" id=${'id1'}></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block',
                id: 'id1'
            },
            children: []
        });
    });

    it('should convert simple div with prop underscore', () => {
        expect(es6x `<div className="block" _=${{ id: 1 }}></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block',
                _: {
                    id: 1
                }
            },
            children: []
        });
    });

    it('should parse component with child', () => {
        const Instance = { instance: true };

        expect(es6x `<instance of=${Instance} text="Text of block"><p>Text from parent</p></instance>`).toEqual({
            tag: 'instance',
            attrs: {
                of: Instance,
                text: 'Text of block'
            },
            children: [{
                tag: 'p',
                attrs: {},
                children: ['Text from parent']
            }]
        });
    });

    it('should correctly work with two same templates but another values', () => {
        expect([
            es6x `<div className="block" id=${'id1'}></div>`,
            es6x `<div className="block" id=${'id2'}></div>`
        ]).toEqual([
            {
                tag: 'div',
                attrs: {
                    className: 'block',
                    id: 'id1'
                },
                children: []
            },
            {
                tag: 'div',
                attrs: {
                    className: 'block',
                    id: 'id2'
                },
                children: []
            }
        ]);
    });

    it('should convert div with props and children', () => {
        expect(es6x `<div className="block" id=${'id1'} ${{ dataset: { rerenderid: '0' } }}><p ${{ dataset: { rerenderid: '1' } }}>${'text'}</p></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block',
                id: 'id1',
                dataset: {
                    rerenderid: '0'
                }
            },
            children: [{
                tag: 'p',
                attrs: {
                    dataset: {
                        rerenderid: '1'
                    }
                },
                children: ['text']
            }]
        });
    });

    it('should convert selfclosed element', () => {
        expect(es6x `<input type="text" value="${'3'}" />`).toEqual({
            tag: 'input',
            attrs: {
                type: 'text',
                value: '3'
            },
            children: []
        });

        expect(es6x `<input type='text' value='${'3'}'/>`).toEqual({
            tag: 'input',
            attrs: {
                type: 'text',
                value: '3'
            },
            children: []
        });
    });

    it('should correctly work with white spaces', () => {
        expect(es6x `

                    Text of paragraph.
                    Value ${'some'} here.

                    Text text   text text.

        `).toEqual({
            tag: 'div',
            attrs: {},
            children: [{
                tag: 'p',
                attrs: {},
                children: [`Text of paragraph.
                    Value `, 'some', ` here.
                `]
            }, {
                tag: 'p',
                attrs: {},
                children: ['Text ', {
                    tag: 'b',
                    attrs: {},
                    children: ['text ']
                }, ' ', {
                    tag: 'strong',
                    attrs: {},
                    children: ['text']
                }, ` text.
                `]
            }]
        });
    });

    it ('should set output method', () => {
        const mockMethod = function(tag, attrs, children) {
            return [tag, attrs, children];
        };

        es6x.setOutputMethod(mockMethod);
        expect(es6x `<p id='id1'>text</p>`).toEqual(['p', { id: 'id1' }, ['text']]);
        es6x.setOutputMethod();
    })
});
<commit_msg>coverage<commit_after>const es6x = require('../src/es6x');

describe('es6x', () => {
    it('should convert simple div', () => {
        expect(es6x `<div className="block"></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block'
            },
            children: []
        });
    });

    it('should convert simple div with prop', () => {
        expect(es6x `<div className="block" id=${'id1'}></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block',
                id: 'id1'
            },
            children: []
        });
    });

    it('should convert simple div with prop underscore', () => {
        expect(es6x `<div className="block" _=${{ id: 1 }}></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block',
                _: {
                    id: 1
                }
            },
            children: []
        });
    });

    it('should parse component with child', () => {
        const Instance = { instance: true };

        expect(es6x `<instance of=${Instance} text="Text of block"><p>Text from parent</p></instance>`).toEqual({
            tag: 'instance',
            attrs: {
                of: Instance,
                text: 'Text of block'
            },
            children: [{
                tag: 'p',
                attrs: {},
                children: ['Text from parent']
            }]
        });
    });

    it('should correctly work with two same templates but another values', () => {
        expect([
            es6x `<div className="block" id=${'id1'}></div>`,
            es6x `<div className="block" id=${'id2'}></div>`
        ]).toEqual([
            {
                tag: 'div',
                attrs: {
                    className: 'block',
                    id: 'id1'
                },
                children: []
            },
            {
                tag: 'div',
                attrs: {
                    className: 'block',
                    id: 'id2'
                },
                children: []
            }
        ]);
    });

    it('should convert div with props and children', () => {
        expect(es6x `<div className="block" id=${'id1'} ${{ dataset: { rerenderid: '0' } }}><p ${{ dataset: { rerenderid: '1' } }}>${'text'}</p></div>`).toEqual({
            tag: 'div',
            attrs: {
                className: 'block',
                id: 'id1',
                dataset: {
                    rerenderid: '0'
                }
            },
            children: [{
                tag: 'p',
                attrs: {
                    dataset: {
                        rerenderid: '1'
                    }
                },
                children: ['text']
            }]
        });
    });

    it('should convert selfclosed element', () => {
        expect(es6x `<input type="text" checked value="${'3'}" />`).toEqual({
            tag: 'input',
            attrs: {
                type: 'text',
                checked: true,
                value: '3'
            },
            children: []
        });

        expect(es6x `<input type='text' value='${'3'}'/>`).toEqual({
            tag: 'input',
            attrs: {
                type: 'text',
                value: '3'
            },
            children: []
        });
    });

    it('should work with components', () => {
        const Component = {};
        expect(es6x `<${Component}
            id='id1'
            value=${'value1'}
            ${{ name: 'name1' }}
        >Text</${Component}>`).toEqual({
            tag: Component,
            attrs: {
                id: 'id1',
                value: 'value1',
                name: 'name1'
            },
            children: ['Text']
        });
    });

    it('should correctly work with white spaces', () => {
        expect(es6x `

                    Text of paragraph.
                    Value ${'some'} here.

                    Text text   text text.

        `).toEqual({
            tag: 'div',
            attrs: {},
            children: [{
                tag: 'p',
                attrs: {},
                children: [`Text of paragraph.
                    Value `, 'some', ` here.
                `]
            }, {
                tag: 'p',
                attrs: {},
                children: ['Text ', {
                    tag: 'b',
                    attrs: {},
                    children: ['text ']
                }, ' ', {
                    tag: 'strong',
                    attrs: {},
                    children: ['text']
                }, ` text.
                `]
            }]
        });
    });

    it ('should set output method', () => {
        const mockMethod = function(tag, attrs, children) {
            return [tag, attrs, children];
        };

        es6x.setOutputMethod(mockMethod);
        expect(es6x `<p id='id1'>text</p>`).toEqual(['p', { id: 'id1' }, ['text']]);
        es6x.setOutputMethod();
    })
});
<|endoftext|>


<commit_before>var express = require("express");
var logfmt = require("logfmt");
var app = express();

app.use(logfmt.requestLogger());

app.use(express.static(__dirname + '/app'));
// app.use('/bower_components', express.static(__dirname + '/bower_components'));

app.get('/', function(req, res) {
  //res.send('Hello World!');
  res.sendfile('./app/index.html');
});

var port = Number(process.env.PORT || 5000);
app.listen(port, function() {
  console.log("Listening on " + port);
});<commit_msg>Add bower components<commit_after>var express = require("express");
var logfmt = require("logfmt");
var app = express();

app.use(logfmt.requestLogger());

app.use(express.static(__dirname + '/app'));
app.use('/bower_components', express.static(__dirname + '/bower_components'));

app.get('/', function(req, res) {
  //res.send('Hello World!');
  res.sendfile('./app/index.html');
});

var port = Number(process.env.PORT || 5000);
app.listen(port, function() {
  console.log("Listening on " + port);
});<|endoftext|>


<commit_before>import { utility } from './lib/utility.js';
import { modelParser } from './lib/modelParser.js';
import { pagePropertiesModel } from './models/pageProperties.model.js';
import { pagePropertyModel } from './models/pageProperty.model.js';

export class PagePropertyBase {
    constructor(id) {
        if(this.constructor.name === 'PagePropertyBase') {
            throw new TypeError('PagePropertyBase must not be constructed directly.  Use one of PageProperty() or DraftProperty()');
        }
        this._id = utility.getResourceId(id, 'home');
    }

    /**
     * Get all of the properties of the page.
     * @param {Array} [names=[]] - An array of names to fetch so that the results are filtered.
     * @returns {Promise.<pagePropertiesModel>} - A Promise that, when resolved, yields a {@link pagePropertiesModel} object that contains the listing of properties.
     */
    getProperties(names = []) {
        if(!Array.isArray(names)) {
            return Promise.reject(new Error('The property names must be an array'));
        }
        let plug = this._plug;
        if(names.length > 0) {
            plug = plug.withParams({ names: names.join(',') });
        }
        return plug.get().then((r) => r.json()).then(modelParser.createParser(pagePropertiesModel));
    }

    /**
     * Get the contents of a page property.
     * @param {String} key - The key of the property to fetch.
     * @returns {Promise} - A Promise that, when resolved, yields the property contents.  The property can be of any type allowed by the MindTouch property subsystem.
     */
    getPropertyContents(key) {
        if(!key) {
            return Promise.reject(new Error('Attempting to fetch a page property contents without providing a property key'));
        }
        return this._plug.at(encodeURIComponent(key)).get();
    }

    /**
     * Gets a single page property by property key.
     * @param {String} key - The key of the property to fetch.
     * @returns {Promise.<pagePropertyModel>} - A Promise that, when resolved, yields a {@link pagePropertyModel} object that contains the property information.
     */
    getProperty(key) {
        if(!key) {
            return Promise.reject(new Error('Attempting to fetch a page property without providing a property key'));
        }
        return this._plug.at(encodeURIComponent(key), 'info').get().then((r) => r.json()).then(modelParser.createParser(pagePropertyModel));
    }

    /**
     * Set a property on the page
     * @param {String} key - The key of the property to set
     * @param {Object} value - An object conteining information regarding the value to set.
     * @param {String} value.text - The string value representing the property value to set.
     * @param {String} [value.type=@see utility.textRequestType] - The mime type of the value's text field.
     * @param {Object} params - An object that contains values that will direct the behavior of the operation.
     * @returns {Promise} - A Promise that, when resolved, indicates the property was set successfully.
     */
    setProperty(key, value = { }, params = { abort: 'modified' }) {
        if(!key) {
            return Promise.reject(new Error('Attempting to set a property without providing a property key'));
        }
        if(!value.text) {
            return Promise.reject(new Error('Attempting to set a property without providing a property value'));
        }
        if(!value.type) {
            value.type = utility.textRequestType;
        }
        return this._plug.at(encodeURIComponent(key)).withParams(params).put(value.text, value.type);
    }
}
<commit_msg>Allow page properties to be set to ''<commit_after>import { utility } from './lib/utility.js';
import { modelParser } from './lib/modelParser.js';
import { pagePropertiesModel } from './models/pageProperties.model.js';
import { pagePropertyModel } from './models/pageProperty.model.js';

export class PagePropertyBase {
    constructor(id) {
        if(this.constructor.name === 'PagePropertyBase') {
            throw new TypeError(
                'PagePropertyBase must not be constructed directly.  Use one of PageProperty() or DraftProperty()'
            );
        }
        this._id = utility.getResourceId(id, 'home');
    }

    /**
     * Get all of the properties of the page.
     * @param {Array} [names=[]] - An array of names to fetch so that the results are filtered.
     * @returns {Promise.<pagePropertiesModel>} - A Promise that, when resolved, yields a {@link pagePropertiesModel} object that contains the listing of properties.
     */
    getProperties(names = []) {
        if(!Array.isArray(names)) {
            return Promise.reject(new Error('The property names must be an array'));
        }
        let plug = this._plug;
        if(names.length > 0) {
            plug = plug.withParams({ names: names.join(',') });
        }
        return plug
            .get()
            .then((r) => r.json())
            .then(modelParser.createParser(pagePropertiesModel));
    }

    /**
     * Get the contents of a page property.
     * @param {String} key - The key of the property to fetch.
     * @returns {Promise} - A Promise that, when resolved, yields the property contents.  The property can be of any type allowed by the MindTouch property subsystem.
     */
    getPropertyContents(key) {
        if(!key) {
            return Promise.reject(
                new Error('Attempting to fetch a page property contents without providing a property key')
            );
        }
        return this._plug.at(encodeURIComponent(key)).get();
    }

    /**
     * Gets a single page property by property key.
     * @param {String} key - The key of the property to fetch.
     * @returns {Promise.<pagePropertyModel>} - A Promise that, when resolved, yields a {@link pagePropertyModel} object that contains the property information.
     */
    getProperty(key) {
        if(!key) {
            return Promise.reject(new Error('Attempting to fetch a page property without providing a property key'));
        }
        return this._plug
            .at(encodeURIComponent(key), 'info')
            .get()
            .then((r) => r.json())
            .then(modelParser.createParser(pagePropertyModel));
    }

    /**
     * Set a property on the page
     * @param {String} key - The key of the property to set
     * @param {Object} value - An object conteining information regarding the value to set.
     * @param {String} value.text - The string value representing the property value to set.
     * @param {String} [value.type=@see utility.textRequestType] - The mime type of the value's text field.
     * @param {Object} params - An object that contains values that will direct the behavior of the operation.
     * @returns {Promise} - A Promise that, when resolved, indicates the property was set successfully.
     */
    setProperty(key, value = {}, params = { abort: 'modified' }) {
        if(!key) {
            return Promise.reject(new Error('Attempting to set a property without providing a property key'));
        }
        if(typeof value.text !== 'string') {
            return Promise.reject(new Error('Attempting to set a property without providing a property value'));
        }
        if(!value.type) {
            value.type = utility.textRequestType;
        }
        return this._plug
            .at(encodeURIComponent(key))
            .withParams(params)
            .put(value.text, value.type);
    }
}
<|endoftext|>


<commit_before>import React from 'react'
import moment from 'moment'

import { Grid, Row, Col, ButtonGroup, Button, Table, Input, Panel, Glyphicon } from 'react-bootstrap'

export default class FeedVersionNavigator extends React.Component {

  constructor (props) {
    super(props)
    this.state = {
      versionIndex: 0
    }
  }

  render () {

    const versionTitleStyle = {
      fontSize: '24px',
      fontWeight: 'bold'
    }

    const hasVersions = this.props.versions && this.props.versions.length > 0

    let version = null

    if(hasVersions) {
      version = this.props.versions[this.state.versionIndex]
    }

    return (

        <Row>
          <Col xs={2} style={versionTitleStyle}>
            {hasVersions
              ? `Version ${version.version} of ${this.props.versions.length}`
              : '(No Versions)'
            }
          </Col>
          <Col xs={8}>
            <ButtonGroup justified>
              <Button href='#'
                disabled={!hasVersions || !version.previousVersionId}
                onClick={(evt) => {
                  evt.preventDefault()
                }}
              >
                <Glyphicon glyph='arrow-left' /> Previous Version
              </Button>

              <Button href='#'
                disabled={!hasVersions}
                onClick={(evt) => {
                  evt.preventDefault()
                }}
              >
                <Glyphicon glyph='download' /> Download Feed
              </Button>

              {this.props.feedSource.retrievalMethod === 'MANUALLY_UPLOADED'
                ? <Button href='#' onClick={(evt) => {
                    evt.preventDefault()
                    this.props.uploadFeedClicked()
                  }}>
                    <Glyphicon glyph='upload' /> Upload Feed
                  </Button>
                : <Button href='#' onClick={(evt) => {
                    evt.preventDefault()
                    this.props.updateFeedClicked()
                  }}>
                    <Glyphicon glyph='refresh' /> Update Feed
                  </Button>

              }

              <Button href='#'
                disabled={!hasVersions || !version.nextVersionId}
                onClick={(evt) => {
                  evt.preventDefault()
                }}
              >
                Next Version <Glyphicon glyph='arrow-right' />
              </Button>
            </ButtonGroup>
          </Col>
          <Col xs={2}>
            <Button className='pull-right' disabled={!hasVersions}>
              <Glyphicon glyph='list' /> All Versions
            </Button>
          </Col>
        </Row>

        <Row><Col xs={12}> </Col></Row>

        {version
          ? <FeedVersionViewer version={version} />
          :
No versions exist for this feed source.

        }

    )
  }
}

class FeedVersionViewer extends React.Component {
  render () {
    const version = this.props.version

    return (

        <Row>
          <Col xs={6}>

                  Status
                  {version.validationSummary.loadStatus}

                  Agency Count
                  {version.validationSummary.agencyCount}

                  Route Count
                  {version.validationSummary.routeCount}

          </Col>
          <Col xs={6}>

                  Trip Count
                  {version.validationSummary.tripCount}

                  Stop time Count
                  {version.validationSummary.stopTimesCount}

                  Valid Dates

                    {moment(version.validationSummary.startDate).format("MMM Do YYYY")} to
                    {moment(version.validationSummary.endDate).format("MMM Do YYYY")}

          </Col>
        </Row>

        <Panel
          header={(
<Glyphicon glyph='check' /> Validation Results
)}
          collapsible
        >
        </Panel>

        <Panel
          header={(
<Glyphicon glyph='comment' /> Comments on this Version
)}
          collapsible
        >
        </Panel>

    )
  }
}
<commit_msg>Navigation of versions<commit_after>import React from 'react'
import moment from 'moment'

import { Grid, Row, Col, ButtonGroup, Button, Table, Input, Panel, Glyphicon } from 'react-bootstrap'

export default class FeedVersionNavigator extends React.Component {

  constructor (props) {
    super(props)
    this.state = {
      versionIndex: 0
    }
  }

  render () {

    const versionTitleStyle = {
      fontSize: '24px',
      fontWeight: 'bold'
    }

    const hasVersions = this.props.versions && this.props.versions.length > 0

    let version = null

    if(hasVersions) {
      version = this.props.versions[this.state.versionIndex]
    }

    return (

        <Row>
          <Col xs={2} style={versionTitleStyle}>
            {hasVersions
              ? `Version ${version.version} of ${this.props.versions.length}`
              : '(No Versions)'
            }
          </Col>
          <Col xs={8}>
            <ButtonGroup justified>
              <Button href='#'
                disabled={!hasVersions || !version.previousVersionId}
                onClick={(evt) => {
                  evt.preventDefault()
                  this.setState({ versionIndex: this.state.versionIndex - 1 })
                }}
              >
                <Glyphicon glyph='arrow-left' /> Previous Version
              </Button>

              <Button href='#'
                disabled={!hasVersions}
                onClick={(evt) => {
                  evt.preventDefault()
                }}
              >
                <Glyphicon glyph='download' /> Download Feed
              </Button>

              {this.props.feedSource.retrievalMethod === 'MANUALLY_UPLOADED'
                ? <Button href='#' onClick={(evt) => {
                    evt.preventDefault()
                    this.props.uploadFeedClicked()
                  }}>
                    <Glyphicon glyph='upload' /> Upload Feed
                  </Button>
                : <Button href='#' onClick={(evt) => {
                    evt.preventDefault()
                    this.props.updateFeedClicked()
                  }}>
                    <Glyphicon glyph='refresh' /> Update Feed
                  </Button>

              }

              <Button href='#'
                disabled={!hasVersions || !version.nextVersionId}
                onClick={(evt) => {
                  evt.preventDefault()
                  this.setState({ versionIndex: this.state.versionIndex + 1 })
                }}
              >
                Next Version <Glyphicon glyph='arrow-right' />
              </Button>
            </ButtonGroup>
          </Col>
          <Col xs={2}>
            <Button className='pull-right' disabled={!hasVersions}>
              <Glyphicon glyph='list' /> All Versions
            </Button>
          </Col>
        </Row>

        <Row><Col xs={12}> </Col></Row>

        {version
          ? <FeedVersionViewer version={version} />
          :
No versions exist for this feed source.

        }

    )
  }
}

class FeedVersionViewer extends React.Component {
  render () {
    const version = this.props.version

    return (

        <Row>
          <Col xs={6}>

                  Status
                  {version.validationSummary.loadStatus}

                  Agency Count
                  {version.validationSummary.agencyCount}

                  Route Count
                  {version.validationSummary.routeCount}

          </Col>
          <Col xs={6}>

                  Trip Count
                  {version.validationSummary.tripCount}

                  Stop time Count
                  {version.validationSummary.stopTimesCount}

                  Valid Dates

                    {moment(version.validationSummary.startDate).format("MMM Do YYYY")} to
                    {moment(version.validationSummary.endDate).format("MMM Do YYYY")}

          </Col>
        </Row>

        <Panel
          header={(
<Glyphicon glyph='check' /> Validation Results
)}
          collapsible
        >
        </Panel>

        <Panel
          header={(
<Glyphicon glyph='comment' /> Comments on this Version
)}
          collapsible
        >
        </Panel>

    )
  }
}
<|endoftext|>