<commit_before>/**
	_moon.ListActions_ is a control designed to live within a _moon.Header_. It is
	used to perform actions on an associated list of items. A ListActions object
	combines an activating control with a drawer containing a user-defined menu of
	selectable options for acting on items in the list. When a menu item is
	selected, an action--such as filtering, sorting, moving, or deleting--may be
	invoked in the application by handling change events from the selected items.
*/
enyo.kind({
	name: "moon.ListActions",
	//* @protected
	classes: "moon-list-actions",
	kind: "enyo.GroupItem",
	//* @public
	published: {
		//* If true, the drawer is expanded, showing this item's contents
		open: false,
		/**
			If true, the drawer will automatically close when the user
			selects a menu item
		*/
		autoCollapse: false,
		/**
			A block of one or more controls to be displayed inside the list actions
			menu. By default, each top-level ListActions will have a _defaultKind_ of
			_FittableRows_, and should typically contain a _moon.Divider_ identifying
			the category and a _moon.Scroller_ with _fit: true_, containing instances
			of _moon.CheckboxItem_, _moon.ToggleItem_, or _moon.SelectableItem_ for
			setting options for the underlying panel.  Alternatively, a
			_moon.DataRepeater_ (with a _moon.Scroller_ set as its container) or a
			_moon.DataList_ may be used as the _fit: true_ control for populating a
			data-bound list of options (see below for limitations on using a
			_moon.DataList_).

			More than one option group may be added to the _listActions_ block, in
			which options are laid out horizontally by default, with each FittableRows'
			height constrained to the height of the parent Header. However, a minimum
			width (300px) is enforced for each group, and if there are more groups
			than will fit in the available horizontal space, all controls will instead
			be stacked vertically. In this case, an outer scroller is enabled; the
			outer scroller scrolls all groups vertically, and the FittableRows are
			reset to natural size based on their content, effectively disabling any
			scrollers contained within, to prevent nested scrolling.

			Note that the vertical stacking capability poses a limitation on using
			_moon.DataList_. Since _moon.DataList_ must always be allowed to scroll,
			it is not suitable for use in a stacked scenario in which only one outer
			scroller is used.  As such, _moon.DataList_ cannot be used within a
			ListActions that may need to stack vertically.

			Each group should have a string value set for the _category_ property, as
			this will be passed in all events that bubble from the ListActions, to
			allow the user to identify which category changed.
		*/
		listActions: null,
		/**
			Source URL for icon image
		*/
		iconSrc: "",
		/**
			Set the icon name to be used by the ListActions Activator button.
			(like moon.Icon and moon.IconButton)
		*/
		icon: "",
		/**
			By default, list action menus are 300px wide.  Set this to true to instead
			have the menus be proportionally sized within the available space.  Note
			that a minimum width of 300px is still respected; if all menus don't fit
			horizontally, they will be stacked vertically.
		*/
		proportionalWidth: false,
		//* When true, activator is shown as disabled and does not generate tap
		//* events
		disabled: false
	},

	//* @protected
	events: {
		/**
			Used internally for ListActions to request Header to add fitting components to itself.
			Not intended for use by end-developer.
		*/
		onRequestCreateListActions: "",
		//* Inform open changed of drawer
		onListActionOpenChanged: ""
	},
	components:[
		{name:"activator", kind: "moon.IconButton", classes: "moon-list-actions-activator", ontap: "expandContract"}
	],
	drawerComponents: [
		{name: "drawer", kind: "moon.ListActionsDrawer", classes: "list-actions-drawer", onComplete: "drawerAnimationEnd", open: false, spotlight: "container", spotlightModal:true, components: [
			{name: "closeButton", kind: "moon.IconButton", icon: "closex", classes: "moon-popup-close moon-list-actions-close moon-neutral", ontap: "expandContract", defaultSpotlightDown:"listActions"},
			{name: "listActionsClientContainer", classes: "enyo-fit moon-list-actions-client-container moon-neutral", components: [
				{name: "listActions", kind: "moon.Scroller", classes: "enyo-fit moon-list-actions-scroller", horizontal:"hidden", vertical:"hidden", onActivate: "optionSelected", defaultSpotlightUp:"closeButton"}
			]}
		]}
	],
	bindings: [
		{from: ".open", to: ".$.drawer.open"},
		{from: ".iconSrc", to: ".$.activator.src"},
		{from: ".icon", to: ".$.activator.icon"},
		{from: ".disabled", to: ".$.activator.disabled", oneWay: false}
	],
	create: function() {
		this.inherited(arguments);
		this.doRequestCreateListActions({components: this.drawerComponents});
		if (!this.$.drawer) {
			throw "moon.ListActions must be created as a child of moon.Header";
		}
		this.disabledChanged();
		this.listActionsChanged();
		this.drawerNeedsResize = true;
	},
	rendered: function() {
		this.inherited(arguments);
		if (this.open) {
			// Perform post-open work
			this.drawerAnimationEnd();
			// Update stacking
			this.resizeDrawer();
		}
	},
	destroy: function() {
		enyo.dispatcher.release(this.$.drawer);
		this.inherited(arguments);
	},
	disabledChanged: function() {
		this.addRemoveClass("disabled", this.disabled);
		this.$.activator.setDisabled(this.disabled);
	},
	listActionsChanged: function() {
		var owner = this.hasOwnProperty("listActions") ? this.getInstanceOwner() : this;
		this.listActions = this.listActions || [];
		this.renderListActionComponents(owner);
	},
	renderListActionComponents: function(inOwner) {
		this.noAutoCollapse = true;
		this.createListActionComponents(inOwner);
		this.noAutoCollapse = false;
	},
	createListActionComponents: function(inOwner) {
		var listAction, i;

		this.listActionComponents = [];
		for (i = 0; (listAction = this.listActions[i]); i++) {
			this.listActionComponents.push(this.createListActionComponent(listAction, inOwner));
		}

		// Increase width to 100% if there is only one list action
		if (this.proportionalWidth) {
			this.$.drawer.addClass("proportional-width");
			var w = 100 / this.listActionComponents.length;
			for (i=0; i<this.listActionComponents.length; i++) {
				this.listActionComponents[i].applyStyle("width", w + "%");
			}
		}

		if (this.hasNode()) {
			this.$.listActions.render();
		}
	},
	//* Creates a new list action component based on _inListAction_.
	createListActionComponent: function(inListAction, inOwner) {
		var listActionComponent;

		inListAction.mixins = this.addListActionMixin(inListAction);
		listActionComponent = this.$.listActions.createComponent(inListAction, {owner: inOwner, layoutKind:"FittableRowsLayout"});
		listActionComponent.addClass("moon-list-actions-menu");

		return listActionComponent;
	},
	/**
		Adds a mixin to each list action menu that decorates _activate_ events
		with the menu's _action_ property.
	*/
	addListActionMixin: function(inListAction) {
		var mixins = inListAction.mixins || [];
		if (mixins.indexOf("moon.ListActionActivationSupport") === -1) {
			mixins.push("moon.ListActionActivationSupport");
		}
		return mixins;
	},
	//* Toggles value of _this.open_.
	expandContract: function(inSender, inEvent) {
		if (this.disabled) {
			return true;
		}
		this.setOpen(!this.getOpen());
	},
	beforeOpenDrawer: function(standardHeight, type) {
		this.standardHeight = standardHeight;
		if (type !== "large") {
			this.set(this.stacked, "false");
		}
	},
	openChanged: function(){
		this.setActive(!this.getOpen());
		this.doListActionOpenChanged({open: this.open});
		// If opened, show drawer and resize it if needed
		if(this.open){
			if (this.drawerNeedsResize) {
				this.resizeDrawer();
				this.drawerNeedsResize = false;
			}
			// Capture onSpotlightFocus happening outside the drawer, so that we can prevent focus
			// from landing in the header beneath the drawer
			enyo.dispatcher.capture(this.$.drawer, {onSpotlightFocus: "capturedSpotlightFocus"}, this);
		} else {
			enyo.dispatcher.release(this.$.drawer);
		}
	},
	drawerAnimationEnd: function() {
		//on closed, hide drawer and spot _this.$.activator_
		if (!this.getOpen()) {
			if (this.generated) {
				enyo.Spotlight.spot(this.$.activator);
			}
			this.bubble("onRequestUnmuteTooltip");
		}
		//on open, move top and spot _this.$.closeButton_
		else {
			if (this.resetScroller) {
				this.$.listActions.scrollTo(0, 0);
				this.resetScroller = false;
			}
			if (this.generated) {
				enyo.Spotlight.spot(this.$.closeButton);
			}
			this.bubble("onRequestMuteTooltip");
		}
		return true;
	},
	updateStacking: function() {
		if (this.$.drawer.hasNode()) {
			this.set("stacked", this.shouldStack());
		}
	},
	shouldStack: function() {
		// Assumption: min-width of all listActionsComponents set to 300px in CSS
		return this.$.listActions.getBounds().width < (300 * this.listActionComponents.length);
	},
	stackedChanged: function() {
		if (this.stacked) {
			this.$.drawer.addClass("stacked");
			this.stackMeUp();
			// When stacked, always have vertical scroller
			this.$.listActions.setVertical("scroll");
		}
		else {
			this.$.drawer.removeClass("stacked");
			this.unStackMeUp();
			this.$.listActions.setVertical("hidden");
		}
		this.resetScroller = true;
		this.$.listActions.resize();
	},
	stackMeUp: function() {
		var optionGroup, i;

		for (i = 0; (optionGroup = this.listActionComponents[i]); i++) {
			optionGroup.applyStyle("display", "block");
			// Stacked contols get natural height (which prevents scrolling), such that they stack
			// within outer scroller which is allowed to scroll all controls; this is a problem for
			// DataLists, which require an explicit height, making them unsuitable for use in
			// stacked ListActions
			optionGroup.applyStyle("height", "none");
		}
	},
	unStackMeUp: function() {
		var containerHeight, optionGroup, i;
		if (this.standardHeight) {
			this.$.drawer.applyStyle("height", this.standardHeight + "px");
		}
		containerHeight = this.getContainerBounds().height;

		for (i = 0; (optionGroup = this.listActionComponents[i]); i++) {
			optionGroup.applyStyle("display", "inline-block");
			optionGroup.applyStyle("height", containerHeight + "px");
		}
	},
	handleResize: function() {
		this.resetCachedValues();

		// If drawer is collapsed, resize it the next time it is opened
		if (this.getOpen()) {
			this.resizeDrawer();
		} else {
			this.drawerNeedsResize = true;
		}
	},
	resizeDrawer: function() {
		this.updateStacking();
	},
	optionSelected: function(inSender, inEvent) {
		if (this.getOpen() && this.autoCollapse && !this.noAutoCollapse) {
			this.startJob("expandContract", "expandContract", 300);
		}
	},
	getContainerBounds: function() {
		this.containerBounds = this.containerBounds || this.$.listActions.getBounds();
		return this.containerBounds;
	},
	resetCachedValues: function() {
		this.headerBounds = null;
		this.clientBounds = null;
		this.containerBounds = null;
	},
	capturedSpotlightFocus: function(inSender, inEvent) {
		// We need to prevent header children below the drawer from being focused
		if (inEvent.originator.isDescendantOf(this.$.drawer.parent) &&
			!inEvent.originator.isDescendantOf(this.$.drawer)) {
			enyo.Spotlight.spot(this.$.drawer);
			return true;
		}
	}
});

/**
	_moon.ListActionsDrawer_ is a control used by
	[moon.ListActions](#moon.ListActions) to house a menu of selectable options.
*/
enyo.kind({
	name: "moon.ListActionsDrawer",
	//* @public
	published: {
		open: false
	},
	//* @protected
	classes: "moon-list-actions-drawer",
	components: [
		{name: "client", classes: "moon-list-actions-drawer-client moon-neutral", ontransitionend: "handleTransitionEnd"}
	],
	events: {
		onComplete: ""
	},
	rendered: function() {
		this.inherited(arguments);
		// Temporarily disable animation
		this.applyAnimatedMode(false);
		// Set the state of the drawer
		this.openChanged();
		// Re-enable animation
		this.applyAnimatedMode(true);
		// Let any watchers know we've finished our preparation
		this.doComplete();
	},
	handleTransitionEnd: function(inSender, inEvent) {
		if (inEvent.originator === this.$.client) {
			this.doComplete();
			return true;
		}
	},
	// We override getBubbleTarget here so that events emanating from a ListActionsDrawer
	// instance will bubble to the owner of the associated ListActions instance, as expected.
	// This is necessary because events normally bubble to a control's DOM parent, but
	// we have sneakily arranged for the DOM parent of a ListActionsDrawer instance to be
	// not the ListActions instance but the containing Header instance.
	getBubbleTarget: function() {
		return this.owner;
	},
	openChanged: function() {
		// Skip animation before render time
		if (!this.$.client.hasNode()) { return; }
		this.$.client.addRemoveClass("open", this.open);
	},
	applyAnimatedMode: function(shouldAnimate) {
		this.$.client.addRemoveClass("animated", shouldAnimate);
	}
});

moon.ListActionActivationSupport = {
	name: "ListActionActivationSupport",
	handlers: {
		onActivate: "activate"
	},
	activate: function(inSender, inEvent) {
		inEvent.action = this.action;
	}
};
<commit_msg>BHV-5127 Remove "." from binding props.<commit_after>/**
	_moon.ListActions_ is a control designed to live within a _moon.Header_. It is
	used to perform actions on an associated list of items. A ListActions object
	combines an activating control with a drawer containing a user-defined menu of
	selectable options for acting on items in the list. When a menu item is
	selected, an action--such as filtering, sorting, moving, or deleting--may be
	invoked in the application by handling change events from the selected items.
*/
enyo.kind({
	name: "moon.ListActions",
	//* @protected
	classes: "moon-list-actions",
	kind: "enyo.GroupItem",
	//* @public
	published: {
		//* If true, the drawer is expanded, showing this item's contents
		open: false,
		/**
			If true, the drawer will automatically close when the user
			selects a menu item
		*/
		autoCollapse: false,
		/**
			A block of one or more controls to be displayed inside the list actions
			menu. By default, each top-level ListActions will have a _defaultKind_ of
			_FittableRows_, and should typically contain a _moon.Divider_ identifying
			the category and a _moon.Scroller_ with _fit: true_, containing instances
			of _moon.CheckboxItem_, _moon.ToggleItem_, or _moon.SelectableItem_ for
			setting options for the underlying panel.  Alternatively, a
			_moon.DataRepeater_ (with a _moon.Scroller_ set as its container) or a
			_moon.DataList_ may be used as the _fit: true_ control for populating a
			data-bound list of options (see below for limitations on using a
			_moon.DataList_).

			More than one option group may be added to the _listActions_ block, in
			which options are laid out horizontally by default, with each FittableRows'
			height constrained to the height of the parent Header. However, a minimum
			width (300px) is enforced for each group, and if there are more groups
			than will fit in the available horizontal space, all controls will instead
			be stacked vertically. In this case, an outer scroller is enabled; the
			outer scroller scrolls all groups vertically, and the FittableRows are
			reset to natural size based on their content, effectively disabling any
			scrollers contained within, to prevent nested scrolling.

			Note that the vertical stacking capability poses a limitation on using
			_moon.DataList_. Since _moon.DataList_ must always be allowed to scroll,
			it is not suitable for use in a stacked scenario in which only one outer
			scroller is used.  As such, _moon.DataList_ cannot be used within a
			ListActions that may need to stack vertically.

			Each group should have a string value set for the _category_ property, as
			this will be passed in all events that bubble from the ListActions, to
			allow the user to identify which category changed.
		*/
		listActions: null,
		/**
			Source URL for icon image
		*/
		iconSrc: "",
		/**
			Set the icon name to be used by the ListActions Activator button.
			(like moon.Icon and moon.IconButton)
		*/
		icon: "",
		/**
			By default, list action menus are 300px wide.  Set this to true to instead
			have the menus be proportionally sized within the available space.  Note
			that a minimum width of 300px is still respected; if all menus don't fit
			horizontally, they will be stacked vertically.
		*/
		proportionalWidth: false,
		//* When true, activator is shown as disabled and does not generate tap
		//* events
		disabled: false
	},

	//* @protected
	events: {
		/**
			Used internally for ListActions to request Header to add fitting components to itself.
			Not intended for use by end-developer.
		*/
		onRequestCreateListActions: "",
		//* Inform open changed of drawer
		onListActionOpenChanged: ""
	},
	components:[
		{name:"activator", kind: "moon.IconButton", classes: "moon-list-actions-activator", ontap: "expandContract"}
	],
	drawerComponents: [
		{name: "drawer", kind: "moon.ListActionsDrawer", classes: "list-actions-drawer", onComplete: "drawerAnimationEnd", open: false, spotlight: "container", spotlightModal:true, components: [
			{name: "closeButton", kind: "moon.IconButton", icon: "closex", classes: "moon-popup-close moon-list-actions-close moon-neutral", ontap: "expandContract", defaultSpotlightDown:"listActions"},
			{name: "listActionsClientContainer", classes: "enyo-fit moon-list-actions-client-container moon-neutral", components: [
				{name: "listActions", kind: "moon.Scroller", classes: "enyo-fit moon-list-actions-scroller", horizontal:"hidden", vertical:"hidden", onActivate: "optionSelected", defaultSpotlightUp:"closeButton"}
			]}
		]}
	],
	bindings: [
		{from: "open", to: "$.drawer.open"},
		{from: "iconSrc", to: "$.activator.src"},
		{from: "icon", to: "$.activator.icon"},
		{from: "disabled", to: "$.activator.disabled", oneWay: false}
	],
	create: function() {
		this.inherited(arguments);
		this.doRequestCreateListActions({components: this.drawerComponents});
		if (!this.$.drawer) {
			throw "moon.ListActions must be created as a child of moon.Header";
		}
		this.disabledChanged();
		this.listActionsChanged();
		this.drawerNeedsResize = true;
	},
	rendered: function() {
		this.inherited(arguments);
		if (this.open) {
			// Perform post-open work
			this.drawerAnimationEnd();
			// Update stacking
			this.resizeDrawer();
		}
	},
	destroy: function() {
		enyo.dispatcher.release(this.$.drawer);
		this.inherited(arguments);
	},
	disabledChanged: function() {
		this.addRemoveClass("disabled", this.disabled);
		this.$.activator.setDisabled(this.disabled);
	},
	listActionsChanged: function() {
		var owner = this.hasOwnProperty("listActions") ? this.getInstanceOwner() : this;
		this.listActions = this.listActions || [];
		this.renderListActionComponents(owner);
	},
	renderListActionComponents: function(inOwner) {
		this.noAutoCollapse = true;
		this.createListActionComponents(inOwner);
		this.noAutoCollapse = false;
	},
	createListActionComponents: function(inOwner) {
		var listAction, i;

		this.listActionComponents = [];
		for (i = 0; (listAction = this.listActions[i]); i++) {
			this.listActionComponents.push(this.createListActionComponent(listAction, inOwner));
		}

		// Increase width to 100% if there is only one list action
		if (this.proportionalWidth) {
			this.$.drawer.addClass("proportional-width");
			var w = 100 / this.listActionComponents.length;
			for (i=0; i<this.listActionComponents.length; i++) {
				this.listActionComponents[i].applyStyle("width", w + "%");
			}
		}

		if (this.hasNode()) {
			this.$.listActions.render();
		}
	},
	//* Creates a new list action component based on _inListAction_.
	createListActionComponent: function(inListAction, inOwner) {
		var listActionComponent;

		inListAction.mixins = this.addListActionMixin(inListAction);
		listActionComponent = this.$.listActions.createComponent(inListAction, {owner: inOwner, layoutKind:"FittableRowsLayout"});
		listActionComponent.addClass("moon-list-actions-menu");

		return listActionComponent;
	},
	/**
		Adds a mixin to each list action menu that decorates _activate_ events
		with the menu's _action_ property.
	*/
	addListActionMixin: function(inListAction) {
		var mixins = inListAction.mixins || [];
		if (mixins.indexOf("moon.ListActionActivationSupport") === -1) {
			mixins.push("moon.ListActionActivationSupport");
		}
		return mixins;
	},
	//* Toggles value of _this.open_.
	expandContract: function(inSender, inEvent) {
		if (this.disabled) {
			return true;
		}
		this.setOpen(!this.getOpen());
	},
	beforeOpenDrawer: function(standardHeight, type) {
		this.standardHeight = standardHeight;
		if (type !== "large") {
			this.set(this.stacked, "false");
		}
	},
	openChanged: function(){
		this.setActive(!this.getOpen());
		this.doListActionOpenChanged({open: this.open});
		// If opened, show drawer and resize it if needed
		if(this.open){
			if (this.drawerNeedsResize) {
				this.resizeDrawer();
				this.drawerNeedsResize = false;
			}
			// Capture onSpotlightFocus happening outside the drawer, so that we can prevent focus
			// from landing in the header beneath the drawer
			enyo.dispatcher.capture(this.$.drawer, {onSpotlightFocus: "capturedSpotlightFocus"}, this);
		} else {
			enyo.dispatcher.release(this.$.drawer);
		}
	},
	drawerAnimationEnd: function() {
		//on closed, hide drawer and spot _this.$.activator_
		if (!this.getOpen()) {
			if (this.generated) {
				enyo.Spotlight.spot(this.$.activator);
			}
			this.bubble("onRequestUnmuteTooltip");
		}
		//on open, move top and spot _this.$.closeButton_
		else {
			if (this.resetScroller) {
				this.$.listActions.scrollTo(0, 0);
				this.resetScroller = false;
			}
			if (this.generated) {
				enyo.Spotlight.spot(this.$.closeButton);
			}
			this.bubble("onRequestMuteTooltip");
		}
		return true;
	},
	updateStacking: function() {
		if (this.$.drawer.hasNode()) {
			this.set("stacked", this.shouldStack());
		}
	},
	shouldStack: function() {
		// Assumption: min-width of all listActionsComponents set to 300px in CSS
		return this.$.listActions.getBounds().width < (300 * this.listActionComponents.length);
	},
	stackedChanged: function() {
		if (this.stacked) {
			this.$.drawer.addClass("stacked");
			this.stackMeUp();
			// When stacked, always have vertical scroller
			this.$.listActions.setVertical("scroll");
		}
		else {
			this.$.drawer.removeClass("stacked");
			this.unStackMeUp();
			this.$.listActions.setVertical("hidden");
		}
		this.resetScroller = true;
		this.$.listActions.resize();
	},
	stackMeUp: function() {
		var optionGroup, i;

		for (i = 0; (optionGroup = this.listActionComponents[i]); i++) {
			optionGroup.applyStyle("display", "block");
			// Stacked contols get natural height (which prevents scrolling), such that they stack
			// within outer scroller which is allowed to scroll all controls; this is a problem for
			// DataLists, which require an explicit height, making them unsuitable for use in
			// stacked ListActions
			optionGroup.applyStyle("height", "none");
		}
	},
	unStackMeUp: function() {
		var containerHeight, optionGroup, i;
		if (this.standardHeight) {
			this.$.drawer.applyStyle("height", this.standardHeight + "px");
		}
		containerHeight = this.getContainerBounds().height;

		for (i = 0; (optionGroup = this.listActionComponents[i]); i++) {
			optionGroup.applyStyle("display", "inline-block");
			optionGroup.applyStyle("height", containerHeight + "px");
		}
	},
	handleResize: function() {
		this.resetCachedValues();

		// If drawer is collapsed, resize it the next time it is opened
		if (this.getOpen()) {
			this.resizeDrawer();
		} else {
			this.drawerNeedsResize = true;
		}
	},
	resizeDrawer: function() {
		this.updateStacking();
	},
	optionSelected: function(inSender, inEvent) {
		if (this.getOpen() && this.autoCollapse && !this.noAutoCollapse) {
			this.startJob("expandContract", "expandContract", 300);
		}
	},
	getContainerBounds: function() {
		this.containerBounds = this.containerBounds || this.$.listActions.getBounds();
		return this.containerBounds;
	},
	resetCachedValues: function() {
		this.headerBounds = null;
		this.clientBounds = null;
		this.containerBounds = null;
	},
	capturedSpotlightFocus: function(inSender, inEvent) {
		// We need to prevent header children below the drawer from being focused
		if (inEvent.originator.isDescendantOf(this.$.drawer.parent) &&
			!inEvent.originator.isDescendantOf(this.$.drawer)) {
			enyo.Spotlight.spot(this.$.drawer);
			return true;
		}
	}
});

/**
	_moon.ListActionsDrawer_ is a control used by
	[moon.ListActions](#moon.ListActions) to house a menu of selectable options.
*/
enyo.kind({
	name: "moon.ListActionsDrawer",
	//* @public
	published: {
		open: false
	},
	//* @protected
	classes: "moon-list-actions-drawer",
	components: [
		{name: "client", classes: "moon-list-actions-drawer-client moon-neutral", ontransitionend: "handleTransitionEnd"}
	],
	events: {
		onComplete: ""
	},
	rendered: function() {
		this.inherited(arguments);
		// Temporarily disable animation
		this.applyAnimatedMode(false);
		// Set the state of the drawer
		this.openChanged();
		// Re-enable animation
		this.applyAnimatedMode(true);
		// Let any watchers know we've finished our preparation
		this.doComplete();
	},
	handleTransitionEnd: function(inSender, inEvent) {
		if (inEvent.originator === this.$.client) {
			this.doComplete();
			return true;
		}
	},
	// We override getBubbleTarget here so that events emanating from a ListActionsDrawer
	// instance will bubble to the owner of the associated ListActions instance, as expected.
	// This is necessary because events normally bubble to a control's DOM parent, but
	// we have sneakily arranged for the DOM parent of a ListActionsDrawer instance to be
	// not the ListActions instance but the containing Header instance.
	getBubbleTarget: function() {
		return this.owner;
	},
	openChanged: function() {
		// Skip animation before render time
		if (!this.$.client.hasNode()) { return; }
		this.$.client.addRemoveClass("open", this.open);
	},
	applyAnimatedMode: function(shouldAnimate) {
		this.$.client.addRemoveClass("animated", shouldAnimate);
	}
});

moon.ListActionActivationSupport = {
	name: "ListActionActivationSupport",
	handlers: {
		onActivate: "activate"
	},
	activate: function(inSender, inEvent) {
		inEvent.action = this.action;
	}
};
<|endoftext|>


<commit_before>// bitbucket.js
// https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html
// ========
const BaseProvider = require('../util/BaseProvider');

class BitBucket extends BaseProvider {
    constructor(){
        super();
        this.payload.setEmbedColor(0x205081);
        this.baseLink = 'https://bitbucket.org/';
    }

    static getName() {
        return 'BitBucket';
    }

    async getType(){
        return this.req.get('X-Event-Key');
    }

    async repoPush(){
        let project = {
            name: this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name,
            branch: null,
            commits: null
        };
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };
        for (let i = 0; (i < this.body.push.changes.length && i <4); i++) {
            let change = this.body.push.changes[i];
            project.branch = (change.old !== null) ? change.old.name : change.new.name;
            project.commits = change.commits;

            let commits = [];
            for (let j = 0; j < project.commits.length; j++) {
                let commit = project.commits[j];
                let message = (commit.message.length > 256) ? commit.message.substring(0, 255) + "\u2026" : commit.message;
                let author = (typeof commit.author.user !== "undefined") ? commit.author.user.display_name : "Unknown";

                commits.push({
                    name: "Commit from " + author,
                    value: "(" + "[`" + commit.hash.substring(0, 7) + "`](" + commit.links.html.href + ")" + ") " + message.replace(/\n/g, " ").replace(/\r/g, " "),
                    inline: false
                });
            }

            this.payload.addEmbed({
                title: "[" + project.name + ":" + project.branch + "] " + project.commits.length + " commit" + ((project.commits.length > 1) ? "s" : ""),
                url: project.url,
                author: user,
                fields: commits
            });
        }
    }

    async repoFork(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };
        this.payload.addEmbed({
            author: user,
            description: "Created a [`fork`](" + this.baseLink + this.body.fork.full_name + ") of [`" + this.body.repository.name + "`](" + this.baseLink + this.body.repository.full_name + ")"
        });
    }

    async repoUpdated(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        let changes = [];
        if (typeof this.body.changes.name !== "undefined") {
            changes.push("**Name:** \"" + this.body.changes.name.old + "\" -> \"" + this.body.changes.name.new + "\"");
        }
        if (typeof this.body.changes.website !== "undefined") {
            changes.push("**Website:** \"" + this.body.changes.website.old + "\" -> \"" + this.body.changes.website.new + "\"");
        }
        if (typeof this.body.changes.language !== "undefined") {
            changes.push("**Language:** \"" + this.body.changes.language.old + "\" -> \"" + this.body.changes.language.new + "\"");
        }
        if (typeof this.body.changes.description !== "undefined") {
            changes.push("**Description:** \"" + this.body.changes.description.old + "\" -> \"" + this.body.changes.description.new + "\"");
        }

        this.payload.addEmbed({
            author: user,
            title: "Changed general information of " + body.repository.name,
            url: this.baseLink + body.repository.full_name,
            description: changes.join("\n")
        });
    }

    async repoCommitCommentCreated(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Wrote a comment to commit " + this.body.commit.hash.substring(0, 7) + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/commits/" + this.body.commit.hash,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async repoCommitStatusCreated(){
        this.payload.addEmbed({
            author: {
                name: this.body.repository.name,
                url: this.baseLink + this.body.repository.full_name,
                icon_url: this.body.repository.links.avatar.href
            },
            title: this.body.commit_status.name,
            url: this.body.commit_status.url,
            description: "**State:** " + this.body.commit_status.state + "\n" + this.body.commit_status.description
        });
    }

    async repoCommitStatusUpdated() {
        this.payload.addEmbed({
            author: {
                name: this.body.repository.name,
                url: this.baseLink + this.body.repository.full_name,
                icon_url: this.body.repository.links.avatar.href
            },
            title: this.body.commit_status.name,
            url: this.body.commit_status.url,
            description: "**State:** " + this.body.commit_status.state + "\n" + this.body.commit_status.description
        });
    }

    async issueCreated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Created a new Issue on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/issues/" + this.body.issue.id,
            description: "",
            fields: [
                {
                    name: this.body.issue.title,
                    value: "**State:** " + this.body.issue.state + "\n" +
                    "**Type:** " + this.body.issue.type + "\n" +
                    "**Priority:** " + this.body.issue.priority + "\n"
                }
            ]
        });
    }

    async issueUpdated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };
        this.payload.addEmbed({
            author: user,
            title: "Updated Issue #" + this.body.issue.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/issues/" + this.body.issue.id
        });
    }

    async issueCommentCreated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Wrote a comment to Issue #" + this.body.issue.id + " on " + this.body.repository.name,
            url: baseLink + this.body.repository.full_name + "/issues/" + this.body.issue.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async pullrequestCreated(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Created a new pull request on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: body.pullrequest.description,
            fields: [
                {
                    name: this.body.pullrequest.title,
                    value: "**Destination branch:** " + this.body.pullrequest.destination.branch.name + "\n" +
                    "**State:** " + this.body.pullrequest.state + "\n"
                }
            ]
        });
    }

    async pullrequestUpdated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Updated pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: this.body.pullrequest.description,
            fields: [
                {
                    name: this.body.pullrequest.title,
                    value: "**Destination branch:** " + this.body.pullrequest.destination.branch.name + "\n" +
                    "**State:** " + this.body.pullrequest.state + "\n"
                }
            ]
        });
    }

    async pullrequestApproved() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Approved pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
        });
    }

    async pullrequestUnapproved() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Removed his approval for pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id
        });
    }

    async pullrequestFulfilled() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Merged pull request #" + this.body.pullrequest.id + " into " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id
        });
    }

    async pullrequestRejected() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Declined pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (typeof this.body.pullrequest.reason !== "undefined") ? ((this.body.pullrequest.reason.replace(/<.*?>/g, '').length > 1024) ? this.body.pullrequest.reason.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.pullrequest.reason.replace(/<.*?>/g, '')) : ""
        });
    }

    async pullrequestCommentCreated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Wrote a comment to pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async pullrequestCommentUpdated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Updated a comment at pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async pullrequestCommentDeleted() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Deleted a comment at pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }
}

module.exports = BitBucket;
<commit_msg>Update bitbucket.js<commit_after>// bitbucket.js
// https://confluence.atlassian.com/bitbucket/manage-webhooks-735643732.html
// ========
const BaseProvider = require('../util/BaseProvider');

class BitBucket extends BaseProvider {
    constructor(){
        super();
        this.payload.setEmbedColor(0x205081);
        this.baseLink = 'https://bitbucket.org/';
    }

    static getName() {
        return 'BitBucket';
    }

    async getType(){
        return this.req.get('X-Event-Key');
    }

    async repoPush(){
        let project = {
            name: this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name,
            branch: null,
            commits: null
        };
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };
        for (let i = 0; (i < this.body.push.changes.length && i <4); i++) {
            let change = this.body.push.changes[i];
            project.branch = (change.old !== null) ? change.old.name : change.new.name;
            project.commits = change.commits;

            let commits = [];
            for (let j = 0; j < project.commits.length; j++) {
                let commit = project.commits[j];
                let message = (commit.message.length > 256) ? commit.message.substring(0, 255) + "\u2026" : commit.message;
                let author = (typeof commit.author.user !== "undefined") ? commit.author.user.display_name : "Unknown";

                commits.push({
                    name: "Commit from " + author,
                    value: "(" + "[`" + commit.hash.substring(0, 7) + "`](" + commit.links.html.href + ")" + ") " + message.replace(/\n/g, " ").replace(/\r/g, " "),
                    inline: false
                });
            }

            this.payload.addEmbed({
                title: "[" + project.name + ":" + project.branch + "] " + project.commits.length + " commit" + ((project.commits.length > 1) ? "s" : ""),
                url: project.url,
                author: user,
                fields: commits
            });
        }
    }

    async repoFork(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };
        this.payload.addEmbed({
            author: user,
            description: "Created a [`fork`](" + this.baseLink + this.body.fork.full_name + ") of [`" + this.body.repository.name + "`](" + this.baseLink + this.body.repository.full_name + ")"
        });
    }

    async repoUpdated(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        let changes = [];
        if (typeof this.body.changes.name !== "undefined") {
            changes.push("**Name:** \"" + this.body.changes.name.old + "\" -> \"" + this.body.changes.name.new + "\"");
        }
        if (typeof this.body.changes.website !== "undefined") {
            changes.push("**Website:** \"" + this.body.changes.website.old + "\" -> \"" + this.body.changes.website.new + "\"");
        }
        if (typeof this.body.changes.language !== "undefined") {
            changes.push("**Language:** \"" + this.body.changes.language.old + "\" -> \"" + this.body.changes.language.new + "\"");
        }
        if (typeof this.body.changes.description !== "undefined") {
            changes.push("**Description:** \"" + this.body.changes.description.old + "\" -> \"" + this.body.changes.description.new + "\"");
        }

        this.payload.addEmbed({
            author: user,
            title: "Changed general information of " + body.repository.name,
            url: this.baseLink + body.repository.full_name,
            description: changes.join("\n")
        });
    }

    async repoCommitCommentCreated(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Wrote a comment to commit " + this.body.commit.hash.substring(0, 7) + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/commits/" + this.body.commit.hash,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async repoCommitStatusCreated(){
        this.payload.addEmbed({
            author: {
                name: this.body.repository.name,
                url: this.baseLink + this.body.repository.full_name,
                icon_url: this.body.repository.links.avatar.href
            },
            title: this.body.commit_status.name,
            url: this.body.commit_status.url,
            description: "**State:** " + this.body.commit_status.state + "\n" + this.body.commit_status.description
        });
    }

    async repoCommitStatusUpdated() {
        this.payload.addEmbed({
            author: {
                name: this.body.repository.name,
                url: this.baseLink + this.body.repository.full_name,
                icon_url: this.body.repository.links.avatar.href
            },
            title: this.body.commit_status.name,
            url: this.body.commit_status.url,
            description: "**State:** " + this.body.commit_status.state + "\n" + this.body.commit_status.description
        });
    }

    async issueCreated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Created a new Issue on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/issues/" + this.body.issue.id,
            description: "",
            fields: [
                {
                    name: this.body.issue.title,
                    value: "**State:** " + this.body.issue.state + "\n" +
                    "**Type:** " + this.body.issue.type + "\n" +
                    "**Priority:** " + this.body.issue.priority + "\n"
                }
            ]
        });
    }

    async issueUpdated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };
        this.payload.addEmbed({
            author: user,
            title: "Updated Issue #" + this.body.issue.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/issues/" + this.body.issue.id
        });
    }

    async issueCommentCreated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Wrote a comment to Issue #" + this.body.issue.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/issues/" + this.body.issue.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async pullrequestCreated(){
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Created a new pull request on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: body.pullrequest.description,
            fields: [
                {
                    name: this.body.pullrequest.title,
                    value: "**Destination branch:** " + this.body.pullrequest.destination.branch.name + "\n" +
                    "**State:** " + this.body.pullrequest.state + "\n"
                }
            ]
        });
    }

    async pullrequestUpdated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Updated pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: this.body.pullrequest.description,
            fields: [
                {
                    name: this.body.pullrequest.title,
                    value: "**Destination branch:** " + this.body.pullrequest.destination.branch.name + "\n" +
                    "**State:** " + this.body.pullrequest.state + "\n"
                }
            ]
        });
    }

    async pullrequestApproved() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Approved pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
        });
    }

    async pullrequestUnapproved() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Removed his approval for pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id
        });
    }

    async pullrequestFulfilled() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Merged pull request #" + this.body.pullrequest.id + " into " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id
        });
    }

    async pullrequestRejected() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Declined pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (typeof this.body.pullrequest.reason !== "undefined") ? ((this.body.pullrequest.reason.replace(/<.*?>/g, '').length > 1024) ? this.body.pullrequest.reason.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.pullrequest.reason.replace(/<.*?>/g, '')) : ""
        });
    }

    async pullrequestCommentCreated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Wrote a comment to pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async pullrequestCommentUpdated() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Updated a comment at pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }

    async pullrequestCommentDeleted() {
        let user = {
            name: this.body.actor.display_name,
            icon_url: this.body.actor.links.avatar.href,
            url: this.baseLink + this.body.actor.username
        };

        this.payload.addEmbed({
            author: user,
            title: "Deleted a comment at pull request #" + this.body.pullrequest.id + " on " + this.body.repository.name,
            url: this.baseLink + this.body.repository.full_name + "/pull-requests/" + this.body.pullrequest.id,
            description: (this.body.comment.content.html.replace(/<.*?>/g, '').length > 1024) ? this.body.comment.content.html.replace(/<.*?>/g, '').substring(0, 1023) + "\u2026" : this.body.comment.content.html.replace(/<.*?>/g, '')
        });
    }
}

module.exports = BitBucket;
<|endoftext|>


<commit_before>/*globals chrome,fdom:true,console,Promise*/
/*jslint indent:2,white:true,sloppy:true */
/**
 * A freedom.js interface to Chrome sockets
 * @constructor
 * @private
 * @param {fdom.Port} channel the module creating this provider.
 * @param {Function} dispatchEvent Method for emitting events.
 * @param {number?} id A pre-existing socket Id for the socket.
 */
var Socket_chrome = function(channel, dispatchEvent, id) {
  this.dispatchEvent = dispatchEvent;
  this.id = id || undefined;
  this.namespace = chrome.sockets.tcp;
  if (this.id) {
    Socket_chrome.addActive(this.id, this);
    chrome.sockets.tcp.setPaused(this.id, false, function() {
      console.log('Unpaused receiver');
    });
  }
};

/**
 * A static list of active sockets, so that global on-receive messages
 * from chrome can be routed properly.
 * @static
 * @private
 * @type {Object.<number,Socket_chrome>}
 */
Socket_chrome.active = {};

/**
 * Get Information about the socket.
 * @method getInfo
 * @param {Function} continuation Function to call with information.
 * @return {Object} connection and address information about the socket.
 */
Socket_chrome.prototype.getInfo = function(continuation) {
  if (this.id) {
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.getInfo(this.id, continuation);
  } else {
    continuation({
      connected: false
    });
  }
};

/**
 * Connect to a designated location and begin reading.
 * @method connect
 * @param {String} hostname The host or ip to connect to.
 * @param {number} port The port to connect on.
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.connect = function(hostname, port, cb) {
  if (this.id) {
    cb(undefined, {
      "errcode": "ALREADY_CONNECTED",
      "message": "Cannot Connect Existing Socket"
    });
    return;
  }
  chrome.sockets.tcp.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    chrome.sockets.tcp.connect(this.id, hostname, port, function (result) {
      if (result < 0) {
        cb(undefined, {
          "errcode": "CONNECTION_FAILED",
          "message": "Chrome Connection Failed: " +
              Socket_chrome.ERROR_MAP[result]
        });
      } else {
        Socket_chrome.addActive(this.id, this);
        cb();
      }
    }.bind(this));
  }.bind(this));
};

/**
 * Secure an already connected socket.
 * @method secure
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.secure = function(cb) {
  if (!this.id) {
    cb(undefined, {
      "errcode": "NOT_CONNECTED",
      "message": "Cannot secure a disconnected socket"
    });
    return;
  } else if (!chrome.sockets.tcp.secure) {
    cb(undefined, {
      "errcode": "FAILED",
      "message": "Secure method not available"
    });
    return;
  }
  this.pause().then(function() {
    chrome.sockets.tcp.secure(this.id, {}, function(secureResult) {
      // Always unpause the socket, regardless of whether .secure succeeds.
      this.unpause().then(function() {
        if (secureResult !== 0) {
          cb(undefined, {
            "errcode": "FAILED",
            "message": "Secure failed: " + secureResult + ': ' +
                Socket_chrome.ERROR_MAP[secureResult]
          });
          return;
        }
        cb();
      }.bind(this));
    }.bind(this));
  }.bind(this));
};

/**
 * Pauses the socket
 * @method pause
 * @private
 * @returns {Promise} Promise to be fulfilled on pause.
 */
Socket_chrome.prototype.pause = function() {
  if (!this.id) {
    return Promise.reject('Cannot pause disconnected socket');
  }
  return new Promise(function(F, R) {
    chrome.sockets.tcp.setPaused(this.id, true, F);
  }.bind(this));
};

/**
 * Unpauses the socket
 * @method unpause
 * @private
 * @returns {Promise} Promise to be fulfilled on unpause.
 */
Socket_chrome.prototype.unpause = function() {
  if (!this.id) {
    return Promise.reject('Cannot unpause disconnected socket');
  }
  return new Promise(function(F, R) {
    chrome.sockets.tcp.setPaused(this.id, false, F);
  }.bind(this));
};

/**
 * Write data to the socket.
 * @method write
 * @param {ArrayBuffer} data The data to write
 * @param {Function} cb Function to call when data is written
 */
Socket_chrome.prototype.write = function(data, cb) {
  if (!this.id) {
    cb(undefined, {
      "errcode": "NOT_CONNECTED",
      "message": "Cannot Write on Closed Socket"
    });
    return;
  }

  chrome.sockets.tcp.send(this.id, data, function(sendInfo) {
    if (sendInfo.resultCode < 0) {
      Socket_chrome.removeActive(this.id);
      return cb(undefined, {
        "errcode": Socket_chrome.errorStringOfCode(sendInfo.resultCode),
        "message": "Send Error: " + sendInfo.resultCode
      });
    } else if (sendInfo.bytesSent !== data.byteLength) {
      Socket_chrome.removeActive(this.id);
      return cb(undefined, {
        "errcode": "CONNECTION_RESET",
        "message": "Write Partially completed."
      });
    }
    cb();
  }.bind(this));
};

// Error codes are at:
// https://code.google.com/p/chromium/codesearch#
//     chromium/src/net/base/net_error_list.h
Socket_chrome.ERROR_MAP = {
  '-1': 'IO_PENDING',
  '-2': 'FAILED',
  '-3': 'ABORTED',
  '-4': 'INVALID_ARGUMENT',
  '-5': 'INVALID_HANDLE',
  '-7': 'TIMED_OUT',
  '-13': 'OUT_OF_MEMORY',
  '-15': 'SOCKET_NOT_CONNECTED',
  '-21': 'NETWORK_CHANGED',
  '-23': 'SOCKET_IS_CONNECTED',
  '-100': 'CONNECTION_CLOSED',
  '-101': 'CONNECTION_RESET',
  '-102': 'CONNECTION_REFUSED',
  '-103': 'CONNECTION_ABORTED',
  '-104': 'CONNECTION_FAILED',
  '-105': 'NAME_NOT_RESOLVED',
  '-106': 'INTERNET_DISCONNECTED',
  '-1000': 'GENERIC_CORDOVA_FAILURE'  //See Cordova Plugin socket.js
};

/**
 * Get the error code associated with a chrome.socket error code.
 * @method errorStringOfCode
 * @static
 * @private
 * @param {Number} code The error number as described by chrome
 * @returns {String} The error code as defined in the freedom.js interface.
 */
Socket_chrome.errorStringOfCode = function(code) {
  return Socket_chrome.ERROR_MAP[String(code)] ||
      'UNKNOWN';
};

/*
 * Alert a consumer that a socket is disconnected, with appropriate
 * error message.
 * @method dispatchDisconnect
 * @private
 * @param {Number} code the code returned by chrome when the socket closed.
 */
Socket_chrome.prototype.dispatchDisconnect = function (code) {
  if (code === 0) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'SUCCESS',
      message: 'Socket closed by call to close'
    });
  } else if(code === -100) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'CONNECTION_CLOSED',
      message: 'Connection closed gracefully'
    });
  } else {
    this.dispatchEvent('onDisconnect', {
      errcode: Socket_chrome.errorStringOfCode(code),
      message: 'Socket Error: ' + code
    });
  }
};

/**
 * Mark a socket as active, so that dispatched events can be routed to it.
 * This method is needed because chrome.sockets exposes a single event handler
 * for incoming data across all sockets.
 * @method addActive
 * @static
 * @param {number} id The socketId as provided by chrome.sockets
 * @param {Socket_chrome} socket The socket class associated with the id.
 */
Socket_chrome.addActive = function(id, socket) {
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.addListener(Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.addListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.addListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.addListener(
          Socket_chrome.handleAcceptError);
    }
  }
  Socket_chrome.active[id] = socket;
};

/**
 * Unmark a socket as active, and clean up event handlers if needed.
 * @method removeActive
 * @static
 * @param {number} id The socketId to no longer watch.
 */
Socket_chrome.removeActive = function(id) {
  delete Socket_chrome.active[id];
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.removeListener(
          Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.removeListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.removeListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.removeListener(
          Socket_chrome.handleAcceptError);
    }
  }
};

/**
 * React to read data.
 * @method handleReadData
 * @private
 * @param {ReadInfo} readInfo The value returned by onReceive.
 * @static
 */
Socket_chrome.handleReadData = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read: ', readInfo);
    return;
  }
  Socket_chrome.active[readInfo.socketId].dispatchEvent('onData', {data: readInfo.data});
};

/**
 * React to read errors.
 * @method handleReadError
 * @private
 * @param {readInfo} readInfo The value returned by onReceiveError.
 * @static
 */
Socket_chrome.handleReadError = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read Error: ', readInfo);
    return;
  }
  Socket_chrome.removeActive(readInfo.socketId);
  Socket_chrome.active[readInfo.socketId].dispatchDisconnect(readInfo.resultCode);
};

/**
 * React to client accepts.
 * @method handleAccept
 * @private
 * @param {acceptInfo} acceptInfo The value returned by onAccept.
 * @static
 */
Socket_chrome.handleAccept = function (acceptInfo) {
  if (!Socket_chrome.active[acceptInfo.socketId]) {
    console.warn('Dropped Accept: ', acceptInfo);
    return;
  }

  chrome.sockets.tcp.getInfo(acceptInfo.clientSocketId, function(info) {
    Socket_chrome.active[acceptInfo.socketId].dispatchEvent('onConnection', {
      socket: acceptInfo.clientSocketId,
      host: info.peerAddress,
      port: info.peerPort
    });
  });
};

/**
 * React to client accept errors.
 * @method handleAcceptError
 * @private
 * @param {info} info The value returned by onAcceptError.
 * @static
 */
Socket_chrome.handleAcceptError = function (info) {
  if (!Socket_chrome.active[info.socketId]) {
    console.warn('Dropped Accept Error: ', info);
    return;
  }

  Socket_chrome.removeActive(info.socketId);
  Socket_chrome.active[info.socketId].dispatchDisconnect(info.resultCode);
};

/**
 * Listen on a socket to accept new clients.
 * @method listen
 * @param {String} address the address to listen on
 * @param {number} port the port to listen on
 * @param {Function} callback Callback to call when listening has occured.
 */
Socket_chrome.prototype.listen = function(address, port, callback) {
  if (this.id) {
    callback(undefined, {
      errcode: "ALREADY_CONNECTED",
      message: "Cannot Listen on existing socket."
    });
    return;
  }
  this.namespace = chrome.sockets.tcpServer;
  chrome.sockets.tcpServer.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    // See https://developer.chrome.com/apps/socket#method-listen
    chrome.sockets.tcpServer.listen(this.id, address, port,
        // TODO: find out what the default is, and what this really means, the
        // webpage is pretty sparse on it:
        //   https://developer.chrome.com/apps/socket#method-listen
        //
        // Length of the socket's listen queue (number of pending connections
        // to open)
        100,
        this.startAcceptLoop.bind(this, callback));
  }.bind(this));
};

/**
 * @method startAcceptLoop
 * @param {Function} callbackFromListen Resolves Freedom's promise for
 * |this.listen|
 * @param {number} result The argument |result| comes from the callback in
 * |chrome.socket.listen|. Its value is a number that represents a chrome
 * socket error, as specified in:
 * https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
 * @private
 */
Socket_chrome.prototype.startAcceptLoop =
    function(callbackFromListen, result) {
  var errorMsg;

  if (result !== 0) {
    if (Socket_chrome.ERROR_MAP.hasOwnProperty(result)) {
      errorMsg = "Chrome Listen failed: " + Socket_chrome.ERROR_MAP[result];
    } else {
      errorMsg = "Chrome Listen failed: Unknown code " + result;
    }
    callbackFromListen(undefined, {
      errcode: "CONNECTION_FAILURE",
      message: errorMsg
    });
    return;
  }

  callbackFromListen();
  Socket_chrome.addActive(this.id, this);
};

/**
 * Close and Destroy a socket
 * @method close
 * @param {Function} continuation Function to call once socket is destroyed.
 */
Socket_chrome.prototype.close = function(continuation) {
  if (this.id) {
    Socket_chrome.removeActive(this.id);
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.disconnect(this.id, function() {});
    delete this.id;
    continuation();
  } else {
    continuation(undefined, {
      "errcode": "SOCKET_CLOSED",
      "message": "Socket Already Closed"
    });
  }
};

/** REGISTER PROVIDER **/
if (typeof fdom !== 'undefined') {
  fdom.apis.register("core.tcpsocket", Socket_chrome);
}
<commit_msg>Add error handling for pause/unpause, better names for F+R<commit_after>/*globals chrome,fdom:true,console,Promise*/
/*jslint indent:2,white:true,sloppy:true */
/**
 * A freedom.js interface to Chrome sockets
 * @constructor
 * @private
 * @param {fdom.Port} channel the module creating this provider.
 * @param {Function} dispatchEvent Method for emitting events.
 * @param {number?} id A pre-existing socket Id for the socket.
 */
var Socket_chrome = function(channel, dispatchEvent, id) {
  this.dispatchEvent = dispatchEvent;
  this.id = id || undefined;
  this.namespace = chrome.sockets.tcp;
  if (this.id) {
    Socket_chrome.addActive(this.id, this);
    chrome.sockets.tcp.setPaused(this.id, false, function() {
      console.log('Unpaused receiver');
    });
  }
};

/**
 * A static list of active sockets, so that global on-receive messages
 * from chrome can be routed properly.
 * @static
 * @private
 * @type {Object.<number,Socket_chrome>}
 */
Socket_chrome.active = {};

/**
 * Get Information about the socket.
 * @method getInfo
 * @param {Function} continuation Function to call with information.
 * @return {Object} connection and address information about the socket.
 */
Socket_chrome.prototype.getInfo = function(continuation) {
  if (this.id) {
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.getInfo(this.id, continuation);
  } else {
    continuation({
      connected: false
    });
  }
};

/**
 * Connect to a designated location and begin reading.
 * @method connect
 * @param {String} hostname The host or ip to connect to.
 * @param {number} port The port to connect on.
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.connect = function(hostname, port, cb) {
  if (this.id) {
    cb(undefined, {
      "errcode": "ALREADY_CONNECTED",
      "message": "Cannot Connect Existing Socket"
    });
    return;
  }
  chrome.sockets.tcp.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    chrome.sockets.tcp.connect(this.id, hostname, port, function (result) {
      if (result < 0) {
        cb(undefined, {
          "errcode": "CONNECTION_FAILED",
          "message": "Chrome Connection Failed: " +
              Socket_chrome.ERROR_MAP[result]
        });
      } else {
        Socket_chrome.addActive(this.id, this);
        cb();
      }
    }.bind(this));
  }.bind(this));
};

/**
 * Secure an already connected socket.
 * @method secure
 * @param {Function} cb Function to call with completion or error.
 */
Socket_chrome.prototype.secure = function(cb) {
  if (!this.id) {
    cb(undefined, {
      "errcode": "NOT_CONNECTED",
      "message": "Cannot secure a disconnected socket"
    });
    return;
  } else if (!chrome.sockets.tcp.secure) {
    cb(undefined, {
      "errcode": "FAILED",
      "message": "Secure method not available"
    });
    return;
  }
  this.pause().then(function() {
    chrome.sockets.tcp.secure(this.id, {}, function(secureResult) {
      // Always unpause the socket, regardless of whether .secure succeeds.
      this.unpause().then(function() {
        if (secureResult !== 0) {
          cb(undefined, {
            "errcode": "FAILED",
            "message": "Secure failed: " + secureResult + ': ' +
                Socket_chrome.ERROR_MAP[secureResult]
          });
          return;
        }
        cb();
      }.bind(this), function(e) {
        console.error('Error unpausing socket: ', e);
      });
    }.bind(this));
  }.bind(this), function(e) {
    console.error('Error pausing socket: ', e);
  });
};

/**
 * Pauses the socket
 * @method pause
 * @private
 * @returns {Promise} Promise to be fulfilled on pause.
 */
Socket_chrome.prototype.pause = function() {
  if (!this.id) {
    return Promise.reject('Cannot pause disconnected socket');
  }
  return new Promise(function(fulfill, reject) {
    chrome.sockets.tcp.setPaused(this.id, true, fulfill);
  }.bind(this));
};

/**
 * Unpauses the socket
 * @method unpause
 * @private
 * @returns {Promise} Promise to be fulfilled on unpause.
 */
Socket_chrome.prototype.unpause = function() {
  if (!this.id) {
    return Promise.reject('Cannot unpause disconnected socket');
  }
  return new Promise(function(fulfill, reject) {
    chrome.sockets.tcp.setPaused(this.id, false, fulfill);
  }.bind(this));
};

/**
 * Write data to the socket.
 * @method write
 * @param {ArrayBuffer} data The data to write
 * @param {Function} cb Function to call when data is written
 */
Socket_chrome.prototype.write = function(data, cb) {
  if (!this.id) {
    cb(undefined, {
      "errcode": "NOT_CONNECTED",
      "message": "Cannot Write on Closed Socket"
    });
    return;
  }

  chrome.sockets.tcp.send(this.id, data, function(sendInfo) {
    if (sendInfo.resultCode < 0) {
      Socket_chrome.removeActive(this.id);
      return cb(undefined, {
        "errcode": Socket_chrome.errorStringOfCode(sendInfo.resultCode),
        "message": "Send Error: " + sendInfo.resultCode
      });
    } else if (sendInfo.bytesSent !== data.byteLength) {
      Socket_chrome.removeActive(this.id);
      return cb(undefined, {
        "errcode": "CONNECTION_RESET",
        "message": "Write Partially completed."
      });
    }
    cb();
  }.bind(this));
};

// Error codes are at:
// https://code.google.com/p/chromium/codesearch#
//     chromium/src/net/base/net_error_list.h
Socket_chrome.ERROR_MAP = {
  '-1': 'IO_PENDING',
  '-2': 'FAILED',
  '-3': 'ABORTED',
  '-4': 'INVALID_ARGUMENT',
  '-5': 'INVALID_HANDLE',
  '-7': 'TIMED_OUT',
  '-13': 'OUT_OF_MEMORY',
  '-15': 'SOCKET_NOT_CONNECTED',
  '-21': 'NETWORK_CHANGED',
  '-23': 'SOCKET_IS_CONNECTED',
  '-100': 'CONNECTION_CLOSED',
  '-101': 'CONNECTION_RESET',
  '-102': 'CONNECTION_REFUSED',
  '-103': 'CONNECTION_ABORTED',
  '-104': 'CONNECTION_FAILED',
  '-105': 'NAME_NOT_RESOLVED',
  '-106': 'INTERNET_DISCONNECTED',
  '-1000': 'GENERIC_CORDOVA_FAILURE'  //See Cordova Plugin socket.js
};

/**
 * Get the error code associated with a chrome.socket error code.
 * @method errorStringOfCode
 * @static
 * @private
 * @param {Number} code The error number as described by chrome
 * @returns {String} The error code as defined in the freedom.js interface.
 */
Socket_chrome.errorStringOfCode = function(code) {
  return Socket_chrome.ERROR_MAP[String(code)] ||
      'UNKNOWN';
};

/*
 * Alert a consumer that a socket is disconnected, with appropriate
 * error message.
 * @method dispatchDisconnect
 * @private
 * @param {Number} code the code returned by chrome when the socket closed.
 */
Socket_chrome.prototype.dispatchDisconnect = function (code) {
  if (code === 0) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'SUCCESS',
      message: 'Socket closed by call to close'
    });
  } else if(code === -100) {
    this.dispatchEvent('onDisconnect', {
      errcode: 'CONNECTION_CLOSED',
      message: 'Connection closed gracefully'
    });
  } else {
    this.dispatchEvent('onDisconnect', {
      errcode: Socket_chrome.errorStringOfCode(code),
      message: 'Socket Error: ' + code
    });
  }
};

/**
 * Mark a socket as active, so that dispatched events can be routed to it.
 * This method is needed because chrome.sockets exposes a single event handler
 * for incoming data across all sockets.
 * @method addActive
 * @static
 * @param {number} id The socketId as provided by chrome.sockets
 * @param {Socket_chrome} socket The socket class associated with the id.
 */
Socket_chrome.addActive = function(id, socket) {
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.addListener(Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.addListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.addListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.addListener(
          Socket_chrome.handleAcceptError);
    }
  }
  Socket_chrome.active[id] = socket;
};

/**
 * Unmark a socket as active, and clean up event handlers if needed.
 * @method removeActive
 * @static
 * @param {number} id The socketId to no longer watch.
 */
Socket_chrome.removeActive = function(id) {
  delete Socket_chrome.active[id];
  if (Object.keys(Socket_chrome.active).length === 0) {
    if (chrome.sockets.tcp) {
      chrome.sockets.tcp.onReceive.removeListener(
          Socket_chrome.handleReadData);
      chrome.sockets.tcp.onReceiveError.removeListener(
          Socket_chrome.handleReadError);
    }
    if (chrome.sockets.tcpServer) {
      chrome.sockets.tcpServer.onAccept.removeListener(
          Socket_chrome.handleAccept);
      chrome.sockets.tcpServer.onAcceptError.removeListener(
          Socket_chrome.handleAcceptError);
    }
  }
};

/**
 * React to read data.
 * @method handleReadData
 * @private
 * @param {ReadInfo} readInfo The value returned by onReceive.
 * @static
 */
Socket_chrome.handleReadData = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read: ', readInfo);
    return;
  }
  Socket_chrome.active[readInfo.socketId].dispatchEvent('onData', {data: readInfo.data});
};

/**
 * React to read errors.
 * @method handleReadError
 * @private
 * @param {readInfo} readInfo The value returned by onReceiveError.
 * @static
 */
Socket_chrome.handleReadError = function (readInfo) {
  if (!Socket_chrome.active[readInfo.socketId]) {
    console.warn('Dropped Read Error: ', readInfo);
    return;
  }
  Socket_chrome.removeActive(readInfo.socketId);
  Socket_chrome.active[readInfo.socketId].dispatchDisconnect(readInfo.resultCode);
};

/**
 * React to client accepts.
 * @method handleAccept
 * @private
 * @param {acceptInfo} acceptInfo The value returned by onAccept.
 * @static
 */
Socket_chrome.handleAccept = function (acceptInfo) {
  if (!Socket_chrome.active[acceptInfo.socketId]) {
    console.warn('Dropped Accept: ', acceptInfo);
    return;
  }

  chrome.sockets.tcp.getInfo(acceptInfo.clientSocketId, function(info) {
    Socket_chrome.active[acceptInfo.socketId].dispatchEvent('onConnection', {
      socket: acceptInfo.clientSocketId,
      host: info.peerAddress,
      port: info.peerPort
    });
  });
};

/**
 * React to client accept errors.
 * @method handleAcceptError
 * @private
 * @param {info} info The value returned by onAcceptError.
 * @static
 */
Socket_chrome.handleAcceptError = function (info) {
  if (!Socket_chrome.active[info.socketId]) {
    console.warn('Dropped Accept Error: ', info);
    return;
  }

  Socket_chrome.removeActive(info.socketId);
  Socket_chrome.active[info.socketId].dispatchDisconnect(info.resultCode);
};

/**
 * Listen on a socket to accept new clients.
 * @method listen
 * @param {String} address the address to listen on
 * @param {number} port the port to listen on
 * @param {Function} callback Callback to call when listening has occured.
 */
Socket_chrome.prototype.listen = function(address, port, callback) {
  if (this.id) {
    callback(undefined, {
      errcode: "ALREADY_CONNECTED",
      message: "Cannot Listen on existing socket."
    });
    return;
  }
  this.namespace = chrome.sockets.tcpServer;
  chrome.sockets.tcpServer.create({}, function(createInfo) {
    this.id = createInfo.socketId;
    // See https://developer.chrome.com/apps/socket#method-listen
    chrome.sockets.tcpServer.listen(this.id, address, port,
        // TODO: find out what the default is, and what this really means, the
        // webpage is pretty sparse on it:
        //   https://developer.chrome.com/apps/socket#method-listen
        //
        // Length of the socket's listen queue (number of pending connections
        // to open)
        100,
        this.startAcceptLoop.bind(this, callback));
  }.bind(this));
};

/**
 * @method startAcceptLoop
 * @param {Function} callbackFromListen Resolves Freedom's promise for
 * |this.listen|
 * @param {number} result The argument |result| comes from the callback in
 * |chrome.socket.listen|. Its value is a number that represents a chrome
 * socket error, as specified in:
 * https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
 * @private
 */
Socket_chrome.prototype.startAcceptLoop =
    function(callbackFromListen, result) {
  var errorMsg;

  if (result !== 0) {
    if (Socket_chrome.ERROR_MAP.hasOwnProperty(result)) {
      errorMsg = "Chrome Listen failed: " + Socket_chrome.ERROR_MAP[result];
    } else {
      errorMsg = "Chrome Listen failed: Unknown code " + result;
    }
    callbackFromListen(undefined, {
      errcode: "CONNECTION_FAILURE",
      message: errorMsg
    });
    return;
  }

  callbackFromListen();
  Socket_chrome.addActive(this.id, this);
};

/**
 * Close and Destroy a socket
 * @method close
 * @param {Function} continuation Function to call once socket is destroyed.
 */
Socket_chrome.prototype.close = function(continuation) {
  if (this.id) {
    Socket_chrome.removeActive(this.id);
    // Note: this.namespace used, since this method is common to tcp and
    // tcpServer sockets.
    this.namespace.disconnect(this.id, function() {});
    delete this.id;
    continuation();
  } else {
    continuation(undefined, {
      "errcode": "SOCKET_CLOSED",
      "message": "Socket Already Closed"
    });
  }
};

/** REGISTER PROVIDER **/
if (typeof fdom !== 'undefined') {
  fdom.apis.register("core.tcpsocket", Socket_chrome);
}
<|endoftext|>


<commit_before>"use strict";

try {
	var config = require("./config.json");
} catch (e) {
	console.log("Could not load `config.json`. Did you copy `config.example.json` to `config.json` and replaced the credentials?");
	process.exit(1);
}

try {
	var pogobuf = require("pogobuf"),
	POGOProtos = require("node-pogo-protos"),
	nodeGeocoder = require("node-geocoder"),
	log = require("./log"),
	blinkstick = require("blinkstick"),
	chalk = require('chalk'),
	moment = require('moment'),
	Redis = require("ioredis"),
	os = require("os"),
	waterfall = require("async-waterfall");
} catch (e) {
	console.log("Could not load all modules. Did you run `npm install`?", e);
	process.exit(1);
}

log(chalk.blue("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"));
log(chalk.blue("*                                                                     *"));
log(chalk.blue("*                        H e l l o   t h e r e                        *"));
log(chalk.blue("*                                                                     *"));
log(chalk.blue("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"));

var login = new pogobuf.PTCLogin(),
	client = new pogobuf.Client(),
	geocoder = nodeGeocoder(),
	lat,
	lng,
	intervalId;

function team_name(team) {
	var name = pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.TeamColor, team);
	switch (team) {
		case 0: return chalk.black.bgWhite(name); break;
		case 1: return chalk.bgBlue(name); break;
		case 2: return chalk.bgRed(name); break;
		case 3: return chalk.black.bgYellow(name); break;
		default: return name; break;
	}
}

/**
 * Blinkstick
 */
var led = {
	stick: null,
	isConnected: function isConnected() {
		if (!led.stick) {
			// try finding the blinkstick again if wasn"t connected at first
			led.stick = blinkstick.findFirst();
		}
		return (typeof led.stick !== "undefined");
	},
	blinker: function(team) {
		if (this.isConnected()) {
			var color = config.team_colors[team];
			log(chalk.yellow("[led] Pulsing to " + chalk[color](color)));

			waterfall([
				function(done) {
					led.stick.pulse(color, done);
				},
				function(done) {
					led.stick.pulse(color, done);
				},
				function(done) {
					setTimeout(function(){
						done();
					}, 1500);
				},
				function(done) {
					led.stick.setColor(color, done);
				},
			], function(err, result) {
				if (err) {
					return log(chalk.white.bgRed("[led] Encountered error!"), err);
				}
				return log(chalk.yellow("[led] Done pulsing"));
			});
		} else {
			return log(chalk.yellow("[led] No led-strip found"));
		}
	},
	turnOff: function() {
		if (this.isConnected()) {
			log("[led] Turning off");
			led.stick.turnOff();
		}
	}
};

var getGymDetails = function() {

	// batch, plz
	// client.batchStart();
	// client.getGymDetails(config.watched_gym.id, config.watched_gym.latitude, config.watched_gym.longitude);
	// client.batchCall()
	client.getGymDetails(config.watched_gym.id, config.watched_gym.latitude, config.watched_gym.longitude)
	.then(gym => {

		// Display gym information
		var fortData = gym.gym_state.fort_data,
			memberships = gym.gym_state.memberships,
			team_color = team_name(fortData.owned_by_team),
			out = [],
			verbose = [];

		out.push("Gym: " + gym.name);
		out.push(
			"Owner: " +
			// chalk[team_color.toLowerCase()](team_color) +
			team_color +
			" " + ((fortData.is_in_battle) ? chalk.bgRed("[IN BATTLE]") : "\t")
		);
		out.push("Points: " + fortData.gym_points);
		log(out.join("\t"));

		if (config.watched_gym.verbose) {
			if (memberships && memberships.length) {
				var highest = memberships[memberships.length - 1];
				verbose.push("Highest Pokémon: " + pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.PokemonId, highest.pokemon_data.pokemon_id) + ", " + highest.pokemon_data.cp + " CP");
				verbose.push("Trainer: " + highest.trainer_public_profile.name + ", level " + highest.trainer_public_profile.level);
				log(verbose.join("\t"));
			}
		}

		if (fortData.owned_by_team !== config.watched_gym.owned_by_team) {
			// victory!
			var previous_owner = pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.TeamColor, config.watched_gym.owned_by_team),
				new_owner = pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.TeamColor, fortData.owned_by_team);
			if (config.watched_gym.owned_by_team !== 0) {
				log("VICTORY!! " + previous_owner + " lost to " + new_owner + "!!");
			}
			led.blinker(fortData.owned_by_team);
			config.watched_gym.owned_by_team = fortData.owned_by_team;
		}

		var query_interval = getQueryInterval();
		intervalId = setTimeout(getGymDetails, query_interval);

	})
	.catch((err) => {
		log(chalk.white.bgRed("ERROR: ", err.message));
		clearInterval(intervalId);
		doLogin();
	});
};

function getRandomIntInclusive(min, max) {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getRandomQueryInterval() {
	return getRandomIntInclusive(config.query_interval.min, config.query_interval.max) * 1000;
}

function getQueryInterval() {
	var daytimeStartsAt = moment().set({'hour': 8, 'minute': 0, 'second': 0, 'millisecond': 0}),
		daytimeEndsAt = moment().set({'hour': 16, 'minute': 0, 'second': 0, 'millisecond': 0}),
		now = moment(),
		nextQueryIn;

	if (now.isBetween(daytimeStartsAt, daytimeEndsAt)) {
		// postpone query until afternoon
		nextQueryIn = daytimeEndsAt.diff(now);
		// switch off blinkstick
		led.turnOff();

	} else {
		nextQueryIn = getRandomQueryInterval();
	}

	log("Querying in " + nextQueryIn/1000 + " seconds (at " + now.add(nextQueryIn, 'milliseconds').format('HH:mm:ss') + ")");
	return nextQueryIn;
}

var doLogin = function() {
	log(chalk.green("Logging in.."));
	login.login(config.username, config.password)
	.then(token => {
		// Initialize the client
		log(chalk.green("Setting auth & position.."));
		client.setAuthInfo(config.provider, token);
		client.setPosition(config.location.lat, config.location.lng);

		// Perform the initial request
		return client.init();
	})
	.then(() => {

		log(chalk.green("Successfully logged in"));
		console.log();

		getGymDetails();

	})
	.catch((err) => {
		log(chalk.white.bgRed("ERROR caught during doLogin(): ", err.message));
		log(err);
		process.exit(1);
	});
}

// start up blinkstick so we know when project is deployed to the raspberry pi
if (led.stick) led.stick.morph("green");

// Start up main app logic
doLogin();

/**
 * do graceful shutdown when told to
 */
process.on("SIGINT", function() {
	log(chalk.red("*** Received request to stop poky ***"));

	clearTimeout(intervalId);

	if (led.stick) {
		led.stick.setColor(0, 0, 0, function(){
			process.exit(0);
		});
	} else {
		process.exit(0);
	}
});
<commit_msg>LED-strip should respect daytime too.<commit_after>"use strict";

try {
	var config = require("./config.json");
} catch (e) {
	console.log("Could not load `config.json`. Did you copy `config.example.json` to `config.json` and replaced the credentials?");
	process.exit(1);
}

try {
	var pogobuf = require("pogobuf"),
	POGOProtos = require("node-pogo-protos"),
	nodeGeocoder = require("node-geocoder"),
	log = require("./log"),
	blinkstick = require("blinkstick"),
	chalk = require('chalk'),
	moment = require('moment'),
	Redis = require("ioredis"),
	os = require("os"),
	waterfall = require("async-waterfall");
} catch (e) {
	console.log("Could not load all modules. Did you run `npm install`?", e);
	process.exit(1);
}

log(chalk.blue("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"));
log(chalk.blue("*                                                                     *"));
log(chalk.blue("*                        H e l l o   t h e r e                        *"));
log(chalk.blue("*                                                                     *"));
log(chalk.blue("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"));

var login = new pogobuf.PTCLogin(),
	client = new pogobuf.Client(),
	geocoder = nodeGeocoder(),
	lat,
	lng,
	intervalId;

function team_name(team) {
	var name = pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.TeamColor, team);
	switch (team) {
		case 0: return chalk.black.bgWhite(name); break;
		case 1: return chalk.bgBlue(name); break;
		case 2: return chalk.bgRed(name); break;
		case 3: return chalk.black.bgYellow(name); break;
		default: return name; break;
	}
}

/**
 * Blinkstick
 */
var led = {
	stick: null,
	isConnected: function isConnected() {
		if (!led.stick) {
			// try finding the blinkstick again if wasn"t connected at first
			led.stick = blinkstick.findFirst();
		}
		return (typeof led.stick !== "undefined");
	},
	blinker: function(team) {
		if (this.isConnected() && !isDaytime()) {
			var color = config.team_colors[team];
			log(chalk.yellow("[led] Pulsing to " + chalk[color](color)));

			waterfall([
				function(done) {
					led.stick.pulse(color, done);
				},
				function(done) {
					led.stick.pulse(color, done);
				},
				function(done) {
					setTimeout(function(){
						done();
					}, 1500);
				},
				function(done) {
					led.stick.setColor(color, done);
				},
			], function(err, result) {
				if (err) {
					return log(chalk.white.bgRed("[led] Encountered error!"), err);
				}
				return log(chalk.yellow("[led] Done pulsing"));
			});
		} else {
			return log(chalk.yellow("[led] No led-strip found"));
		}
	},
	turnOff: function() {
		if (this.isConnected()) {
			log("[led] Turning off");
			led.stick.turnOff();
		}
	}
};

var getGymDetails = function() {

	// batch, plz
	// client.batchStart();
	// client.getGymDetails(config.watched_gym.id, config.watched_gym.latitude, config.watched_gym.longitude);
	// client.batchCall()
	client.getGymDetails(config.watched_gym.id, config.watched_gym.latitude, config.watched_gym.longitude)
	.then(gym => {

		// Display gym information
		var fortData = gym.gym_state.fort_data,
			memberships = gym.gym_state.memberships,
			team_color = team_name(fortData.owned_by_team),
			out = [],
			verbose = [];

		out.push("Gym: " + gym.name);
		out.push(
			"Owner: " +
			// chalk[team_color.toLowerCase()](team_color) +
			team_color +
			" " + ((fortData.is_in_battle) ? chalk.bgRed("[IN BATTLE]") : "\t")
		);
		out.push("Points: " + fortData.gym_points);
		log(out.join("\t"));

		if (config.watched_gym.verbose) {
			if (memberships && memberships.length) {
				var highest = memberships[memberships.length - 1];
				verbose.push("Highest Pokémon: " + pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.PokemonId, highest.pokemon_data.pokemon_id) + ", " + highest.pokemon_data.cp + " CP");
				verbose.push("Trainer: " + highest.trainer_public_profile.name + ", level " + highest.trainer_public_profile.level);
				log(verbose.join("\t"));
			}
		}

		if (fortData.owned_by_team !== config.watched_gym.owned_by_team) {
			// victory!
			var previous_owner = pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.TeamColor, config.watched_gym.owned_by_team),
				new_owner = pogobuf.Utils.getEnumKeyByValue(POGOProtos.Enums.TeamColor, fortData.owned_by_team);
			if (config.watched_gym.owned_by_team !== 0) {
				log("VICTORY!! " + previous_owner + " lost to " + new_owner + "!!");
			}
			led.blinker(fortData.owned_by_team);
			config.watched_gym.owned_by_team = fortData.owned_by_team;
		}

		var query_interval = getQueryInterval();
		intervalId = setTimeout(getGymDetails, query_interval);

	})
	.catch((err) => {
		log(chalk.white.bgRed("ERROR: ", err.message));
		clearInterval(intervalId);
		doLogin();
	});
};

function getRandomIntInclusive(min, max) {
	min = Math.ceil(min);
	max = Math.floor(max);
	return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getRandomQueryInterval() {
	return getRandomIntInclusive(config.query_interval.min, config.query_interval.max) * 1000;
}

function getDaytimeStartsAt() {
	return moment().set({'hour': 8, 'minute': 0, 'second': 0, 'millisecond': 0});
}

function getDaytimeEndsAt() {
	return moment().set({'hour': 16, 'minute': 0, 'second': 0, 'millisecond': 0});
}

function isDaytime() {
	return moment().isBetween(getDaytimeStartsAt(), getDaytimeEndsAt());
}

function getQueryInterval() {
	var daytimeStartsAt = getDaytimeStartsAt(),
		daytimeEndsAt = getDaytimeEndsAt(),
		now = moment(),
		nextQueryIn;

	if (now.isBetween(daytimeStartsAt, daytimeEndsAt)) {
		// postpone query until afternoon
		nextQueryIn = daytimeEndsAt.diff(now);
		// switch off blinkstick
		led.turnOff();

	} else {
		nextQueryIn = getRandomQueryInterval();
	}

	log("Querying in " + nextQueryIn/1000 + " seconds (at " + now.add(nextQueryIn, 'milliseconds').format('HH:mm:ss') + ")");
	return nextQueryIn;
}

var doLogin = function() {
	log(chalk.green("Logging in.."));
	login.login(config.username, config.password)
	.then(token => {
		// Initialize the client
		log(chalk.green("Setting auth & position.."));
		client.setAuthInfo(config.provider, token);
		client.setPosition(config.location.lat, config.location.lng);

		// Perform the initial request
		return client.init();
	})
	.then(() => {

		log(chalk.green("Successfully logged in"));
		console.log();

		getGymDetails();

	})
	.catch((err) => {
		log(chalk.white.bgRed("ERROR caught during doLogin(): ", err.message));
		log(err);
		process.exit(1);
	});
}

// start up blinkstick so we know when project is deployed to the raspberry pi
if (led.stick) led.stick.morph("green");

// Start up main app logic
doLogin();

/**
 * do graceful shutdown when told to
 */
process.on("SIGINT", function() {
	log(chalk.red("*** Received request to stop poky ***"));

	clearTimeout(intervalId);

	if (led.stick) {
		led.stick.setColor(0, 0, 0, function(){
			process.exit(0);
		});
	} else {
		process.exit(0);
	}
});
<|endoftext|>