<commit_before>//straight compile from coffeescript, temporary
var  __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

function SelectionHelper(options) {
    this.highlightObjectAt = __bind(this.highlightObjectAt, this);

    this.selectObjectAt = __bind(this.selectObjectAt, this);

    this.getObjectAt = __bind(this.getObjectAt, this);

    this.isThereObjectAt = __bind(this.isThereObjectAt, this);

    this.get2DBB = __bind(this.get2DBB, this);

    this.getScreenCoords = __bind(this.getScreenCoords, this);

    this._get3DBB = __bind(this._get3DBB, this);

    this._unSelect = __bind(this._unSelect, this);

    this._onSelect = __bind(this._onSelect, this);

    this._unHover = __bind(this._unHover, this);

    this._onHover = __bind(this._onHover, this);

    var defaults;
    //SelectionHelper.__super__.constructor.call(this, options);
    defaults = {
      hiearchyRoot: null,
      camera: null,
      viewWidth: 640,
      viewHeight: 480
    };
    //options = merge(defaults, options);

    this.hiearchyRoot = options.hiearchyRoot, this.camera = options.camera, this.viewWidth = options.viewWidth, this.viewHeight = options.viewHeight;
    this.options = options;
    this.currentHover = null;
    this.currentSelect = null;
    this.projector = new THREE.Projector();
    this.addEventListener = THREE.EventDispatcher.prototype.addEventListener;
    this.hasEventListener = THREE.EventDispatcher.prototype.hasEventListener;
    this.removeEventListener = THREE.EventDispatcher.prototype.removeEventListener;
    this.dispatchEvent = THREE.EventDispatcher.prototype.dispatchEvent;

		//for camera
		this.isOrtho = false;
		this.selectionColor = 0xfffccc;
		this.outlineColor = 0xffc200;
  }

  SelectionHelper.prototype._onHover = function(selection) {
    var outline, outlineMaterial;
    if (selection != null) {
      this.currentHover = selection;
      /*
      if (!(selection.hoverOutline != null) && !(selection.outline != null) && !(selection.name === "hoverOutline") && !(selection.name === "boundingCage") && !(selection.name === "selectOutline")) {
        selection.currentHoverHex = selection.material.color.getHex();
        selection.material.color.setHex(this.selectionColor);
        outlineMaterial = new THREE.MeshBasicMaterial({
          color: 0xffc200,
          side: THREE.BackSide
        });
        outline = new THREE.Mesh(selection.geometry.clone(), outlineMaterial);
        outline.scale.multiplyScalar(1.03);
        outline.name = "hoverOutline";
        selection.hoverOutline = outline;
        selection.add(outline);
      }*/
      return this.dispatchEvent({
        type: 'hoverIn',
        selection: selection
      });
    }
  };

  SelectionHelper.prototype._unHover = function() {
    if (this.currentHover) {
      /*if (this.currentHover.hoverOutline != null) {
        this.currentHover.material.color.setHex(this.currentHover.currentHoverHex);
        this.currentHover.remove(this.currentHover.hoverOutline);
        this.currentHover.hoverOutline = null;
      }*/
      this.currentHover = null;
      return this.dispatchEvent({
        type: 'hoverOut',
        selection: this.currentHover
      });
    }
  };

  SelectionHelper.prototype._onSelect = function(selection) {
    var outline, outlineMaterial;
    this._unHover();
    this.currentSelect = selection;
    /*new BoundingCage({
      mesh: selection,
      color: this.options.color,
      textColor: this.options.textColor
    });*/

    /*
    var cage = new THREE.BoundingBoxHelper( selection,0xFF0000 );
		cage.name = "boundingCage";
		cage.update();
    selection.cage = cage;
    selection.add(cage);*/

    /*outlineMaterial = new THREE.MeshBasicMaterial({
      color: 0xff0000,//0xffc200,
      side: THREE.BackSide
    });
    outline = new THREE.Mesh(selection.geometry.clone(), outlineMaterial);
    outline.name = "selectOutline";
    outline.scale.multiplyScalar(1.03);
    selection.outline = outline;
    selection.add(outline);*/
    return this.dispatchEvent({
      type: 'selected',
      selection: selection
    });
  };

  SelectionHelper.prototype._unSelect = function() {
    var selection;
    if (this.currentSelect) {
      selection = this.currentSelect;
      /*selection.remove(selection.cage);
      selection.remove(selection.outline);
      selection.cage = null;
      selection.outline = null;
      this.currentSelect = null;*/
      return this.dispatchEvent({
        type: 'unselected',
        selection: selection
      });
    }
  };

  SelectionHelper.prototype._get3DBB = function(object) {
    if (object != null) {
      if (object.geometry != null) {
        if (object.geometry.boundingBox != null) {
          return object.geometry.boundingBox;
        } else {
          object.geometry.computeBoundingBox();
          return object.geometry.boundingBox;
        }
      }
    }
    return null;
  };

  SelectionHelper.prototype.getScreenCoords = function(object, width, height) {
    var result, vector;
    if (object != null) {
      vector = this.projector.projectVector(object.position.clone(), this.camera);
      result = new THREE.Vector2();
      result.x = Math.round(vector.x * (width / 2)) + width / 2;
      result.y = Math.round((0 - vector.y) * (height / 2)) + height / 2;
      return result;
    }
  };

  SelectionHelper.prototype.get2DBB = function(object, width, height) {
    var bbox3d, centerLeft, centerPercX, centerPercY, centerTop, max3d, maxLeft, maxPercX, maxPercY, maxTop, min3d, minLeft, minPercX, minPercY, minTop, objHeight, objLength, objWidth, pMax, pMin, pos, result;
    if (object != null) {
      bbox3d = this._get3DBB(object);
      min3d = bbox3d.min.clone();
      max3d = bbox3d.max.clone();
      objLength = bbox3d.max.x - bbox3d.min.x;
      objWidth = bbox3d.max.y - bbox3d.min.y;
      objHeight = bbox3d.max.z - bbox3d.min.z;
      pMin = this.projector.projectVector(min3d, this.camera);
      pMax = this.projector.projectVector(max3d, this.camera);
      minPercX = (pMin.x + 1) / 2;
      minPercY = (-pMin.y + 1) / 2;
      minLeft = minPercX * width;
      minTop = minPercY * height;
      maxPercX = (pMax.x + 1) / 2;
      maxPercY = (-pMax.y + 1) / 2;
      maxLeft = maxPercX * width;
      maxTop = maxPercY * height;
      pos = object.position.clone();
      pos = this.projector.projectVector(pos, this.camera);
      centerPercX = (pos.x + 1) / 2;
      centerPercY = (-pos.y + 1) / 2;
      centerLeft = centerPercX * width;
      centerTop = centerPercY * height;
      result = [centerLeft, centerTop, objLength, objWidth, objHeight];
      return result;
    }
  };

	SelectionHelper.prototype.pick = function(x,y,isOrtho){
		var isOrtho = isOrtho || this.isOrtho;
		var intersected, intersects, raycaster, v, _ref;
		v = new THREE.Vector3((x / this.viewWidth) * 2 - 1, -(y / this.viewHeight) * 2 + 1, 1);
		if( !isOrtho)
		{
		  var s = Date.now();
		  v.unproject( this.camera );
		  var v1 = v.clone();
		  var fooV = v.clone();

		  raycaster = new THREE.Raycaster(this.camera.position, v.sub(this.camera.position).normalize());
		  //raycaster.precision = 10;
		  intersects = raycaster.intersectObjects(this.hiearchyRoot, true);
		}
		else
		{
				// use picking ray since it's an orthographic camera
				//var ray = this.projector.pickingRay( v, this.camera );
				//intersects = ray.intersectObjects( this.hiearchyRoot, true );

				//see here:
				THREE.Vector3.prototype.pickingRay = function ( camera ) {
            var tan = Math.tan( 0.5 * THREE.Math.degToRad( camera.fov ) ) / camera.zoom;

            this.x *= tan * camera.aspect;
            this.y *= tan;
            this.z = - 1;

            return this.transformDirection( camera.matrixWorld );
        };

				//v.unproject( this.camera );
		    //raycaster = new THREE.Raycaster(this.camera.position, v.sub(this.camera.position).normalize());
		     raycaster = new THREE.Raycaster();
         v.pickingRay( this.camera );
         raycaster.set( this.camera.position, v );
		     intersects = raycaster.intersectObjects(this.hiearchyRoot, true);
		}

		return intersects;
	};

  SelectionHelper.prototype.getSceneCoords = function(x,y,isOrtho){
		var isOrtho = isOrtho || this.isOrtho;
		var raycaster, v, pos;
		v = new THREE.Vector3((x / this.viewWidth) * 2 - 1, -(y / this.viewHeight) * 2 + 1, 1);
		if( !isOrtho)
		{
		  v.unproject( this.camera);

      var dir = v.sub( this.camera.position ).normalize();
      var distance = - this.camera.position.z / dir.z;
      var pos = this.camera.position.clone().add( dir.multiplyScalar( distance ) );
		}
		else
		{
				// use picking ray since it's an orthographic camera
				var ray = this.projector.pickingRay( v, this.camera );
				//intersects = ray.intersectObjects( this.hiearchyRoot, true );
		}
		return pos;
	};

  SelectionHelper.prototype.filterIntersects = function( intersects ){
    function goUpHierarchy(node, stopCondition)
		{
		  if(stopCondition( node ) ){
        return node;
      }
      else{
        if(node.parent){
        return goUpHierarchy( node.parent, stopCondition );}
        else{
          return null;
        }
      }
		}

		var hierarchyRoot = this.hiearchyRoot;
		var finalInteresects = [];
		//now go up the hierarchy until we find one "root" object ie in hierarchy root
		for(var i=0;i<intersects.length;i++)
		{
		  var node = intersects[i].object;
		  //console.log("intersect", intersects[i]);

		  function isOk(node)
		  {
		    if(hierarchyRoot.indexOf( node ) !=-1 ) return true;
		    return false
		  }
		  var validNode = goUpHierarchy(node, isOk);

		  if( validNode && !validNode.userData.unselectable )
		  {
		    var existingNodeIndex = finalInteresects.indexOf( validNode );
		    if(existingNodeIndex !== -1)
		    {
		      var prevNode = finalInteresects[existingNodeIndex];
		      prevNode.userData.tmpIntersectData.push( intersects[ i ] );
		      continue;
		    }
		    validNode.userData.tmpIntersectData = [ intersects[ i ] ];
		    finalInteresects.push( validNode );
		  }
		}
		//console.log("intersects",finalInteresects);
    return finalInteresects;
  }

  //FIXME: we still need access to the face etc of picked items
    /*if(intersects.length > 0)
		  {

		  }*/

  /*SelectionHelper.prototype.isThereObjectAt = function(x, y) {
		var intersects = this.pick(x,y,false);
    if (intersects.length > 0) {
      return true;
    }
    return false;
  };*/

  SelectionHelper.prototype.getObjectAt = function(x, y, isOrtho, filter) {
    var filter    = filter !== undefined ? filter : false;
    var isOrtho = isOrtho || false;
    var s = Date.now();
		var intersects = this.pick(x,y,isOrtho);
		if(filter) intersects = this.filterIntersects( intersects );
		var e = Date.now();
    //console.log( "Time to pick " + (e-s) + " ms" );
		//console.log("intersects",intersects);

    if (intersects.length > 0) {
      if(filter){
        var intersected = intersects[0];
      }else{
        var intersected = intersects[0].object;
        intersected.userData.tmpIntersectData = intersects ;
      }
      //console.log("intersected",intersects[0]);
      return intersected;
    } else {
      return null;
    }
  };

  SelectionHelper.prototype.selectObjectAt = function(x, y) {
    var intersects = this.pick(x,y,false);
    if (intersects.length > 0) {
      intersected = intersects[0].object;
      intersected._data = {};
      intersected._data.face = intersects[0].face;
      intersected._data.point = intersects[0].point;
      //console.log("intersected",intersected);

      if ((_ref = intersected.name) === "hoverOutline" || _ref === "selectOutline" || _ref === "boundingCage") {
        intersected = intersected.parent;
      }
      if (intersected !== this.currentSelect) {
        this._unSelect();
        this._onSelect(intersected);
        return this.currentSelect;
      } else {
        return this.currentSelect;
      }
    } else {
      return this._unSelect();
    }
  };

  SelectionHelper.prototype.highlightObjectAt = function(x, y) {
    var intersects = this.pick(x,y,false);
    if (intersects.length > 0) {
      if (intersects[0].object !== this.currentHover) {
        if (intersects[0].object.name !== "workplane" && intersects[0].object.name !== "hoverOutline" && intersects[0].object.name !== "boundingCage") {
          this._unHover();
          return this._onHover(intersects[0].object);
        }
      }
    } else {
      return this._unHover();
    }
  };

<commit_msg>- added fix to prevent picking of invisible objects<commit_after>//straight compile from coffeescript, temporary
var  __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };

function SelectionHelper(options) {
    this.highlightObjectAt = __bind(this.highlightObjectAt, this);

    this.selectObjectAt = __bind(this.selectObjectAt, this);

    this.getObjectAt = __bind(this.getObjectAt, this);

    this.isThereObjectAt = __bind(this.isThereObjectAt, this);

    this.get2DBB = __bind(this.get2DBB, this);

    this.getScreenCoords = __bind(this.getScreenCoords, this);

    this._get3DBB = __bind(this._get3DBB, this);

    this._unSelect = __bind(this._unSelect, this);

    this._onSelect = __bind(this._onSelect, this);

    this._unHover = __bind(this._unHover, this);

    this._onHover = __bind(this._onHover, this);

    var defaults;
    //SelectionHelper.__super__.constructor.call(this, options);
    defaults = {
      hiearchyRoot: null,
      camera: null,
      viewWidth: 640,
      viewHeight: 480
    };
    //options = merge(defaults, options);

    this.hiearchyRoot = options.hiearchyRoot, this.camera = options.camera, this.viewWidth = options.viewWidth, this.viewHeight = options.viewHeight;
    this.options = options;
    this.currentHover = null;
    this.currentSelect = null;
    this.projector = new THREE.Projector();
    this.addEventListener = THREE.EventDispatcher.prototype.addEventListener;
    this.hasEventListener = THREE.EventDispatcher.prototype.hasEventListener;
    this.removeEventListener = THREE.EventDispatcher.prototype.removeEventListener;
    this.dispatchEvent = THREE.EventDispatcher.prototype.dispatchEvent;

		//for camera
		this.isOrtho = false;
		this.selectionColor = 0xfffccc;
		this.outlineColor = 0xffc200;
  }

  SelectionHelper.prototype._onHover = function(selection) {
    var outline, outlineMaterial;
    if (selection != null) {
      this.currentHover = selection;
      /*
      if (!(selection.hoverOutline != null) && !(selection.outline != null) && !(selection.name === "hoverOutline") && !(selection.name === "boundingCage") && !(selection.name === "selectOutline")) {
        selection.currentHoverHex = selection.material.color.getHex();
        selection.material.color.setHex(this.selectionColor);
        outlineMaterial = new THREE.MeshBasicMaterial({
          color: 0xffc200,
          side: THREE.BackSide
        });
        outline = new THREE.Mesh(selection.geometry.clone(), outlineMaterial);
        outline.scale.multiplyScalar(1.03);
        outline.name = "hoverOutline";
        selection.hoverOutline = outline;
        selection.add(outline);
      }*/
      return this.dispatchEvent({
        type: 'hoverIn',
        selection: selection
      });
    }
  };

  SelectionHelper.prototype._unHover = function() {
    if (this.currentHover) {
      /*if (this.currentHover.hoverOutline != null) {
        this.currentHover.material.color.setHex(this.currentHover.currentHoverHex);
        this.currentHover.remove(this.currentHover.hoverOutline);
        this.currentHover.hoverOutline = null;
      }*/
      this.currentHover = null;
      return this.dispatchEvent({
        type: 'hoverOut',
        selection: this.currentHover
      });
    }
  };

  SelectionHelper.prototype._onSelect = function(selection) {
    var outline, outlineMaterial;
    this._unHover();
    this.currentSelect = selection;
    /*new BoundingCage({
      mesh: selection,
      color: this.options.color,
      textColor: this.options.textColor
    });*/

    /*
    var cage = new THREE.BoundingBoxHelper( selection,0xFF0000 );
		cage.name = "boundingCage";
		cage.update();
    selection.cage = cage;
    selection.add(cage);*/

    /*outlineMaterial = new THREE.MeshBasicMaterial({
      color: 0xff0000,//0xffc200,
      side: THREE.BackSide
    });
    outline = new THREE.Mesh(selection.geometry.clone(), outlineMaterial);
    outline.name = "selectOutline";
    outline.scale.multiplyScalar(1.03);
    selection.outline = outline;
    selection.add(outline);*/
    return this.dispatchEvent({
      type: 'selected',
      selection: selection
    });
  };

  SelectionHelper.prototype._unSelect = function() {
    var selection;
    if (this.currentSelect) {
      selection = this.currentSelect;
      /*selection.remove(selection.cage);
      selection.remove(selection.outline);
      selection.cage = null;
      selection.outline = null;
      this.currentSelect = null;*/
      return this.dispatchEvent({
        type: 'unselected',
        selection: selection
      });
    }
  };

  SelectionHelper.prototype._get3DBB = function(object) {
    if (object != null) {
      if (object.geometry != null) {
        if (object.geometry.boundingBox != null) {
          return object.geometry.boundingBox;
        } else {
          object.geometry.computeBoundingBox();
          return object.geometry.boundingBox;
        }
      }
    }
    return null;
  };

  SelectionHelper.prototype.getScreenCoords = function(object, width, height) {
    var result, vector;
    if (object != null) {
      vector = this.projector.projectVector(object.position.clone(), this.camera);
      result = new THREE.Vector2();
      result.x = Math.round(vector.x * (width / 2)) + width / 2;
      result.y = Math.round((0 - vector.y) * (height / 2)) + height / 2;
      return result;
    }
  };

  SelectionHelper.prototype.get2DBB = function(object, width, height) {
    var bbox3d, centerLeft, centerPercX, centerPercY, centerTop, max3d, maxLeft, maxPercX, maxPercY, maxTop, min3d, minLeft, minPercX, minPercY, minTop, objHeight, objLength, objWidth, pMax, pMin, pos, result;
    if (object != null) {
      bbox3d = this._get3DBB(object);
      min3d = bbox3d.min.clone();
      max3d = bbox3d.max.clone();
      objLength = bbox3d.max.x - bbox3d.min.x;
      objWidth = bbox3d.max.y - bbox3d.min.y;
      objHeight = bbox3d.max.z - bbox3d.min.z;
      pMin = this.projector.projectVector(min3d, this.camera);
      pMax = this.projector.projectVector(max3d, this.camera);
      minPercX = (pMin.x + 1) / 2;
      minPercY = (-pMin.y + 1) / 2;
      minLeft = minPercX * width;
      minTop = minPercY * height;
      maxPercX = (pMax.x + 1) / 2;
      maxPercY = (-pMax.y + 1) / 2;
      maxLeft = maxPercX * width;
      maxTop = maxPercY * height;
      pos = object.position.clone();
      pos = this.projector.projectVector(pos, this.camera);
      centerPercX = (pos.x + 1) / 2;
      centerPercY = (-pos.y + 1) / 2;
      centerLeft = centerPercX * width;
      centerTop = centerPercY * height;
      result = [centerLeft, centerTop, objLength, objWidth, objHeight];
      return result;
    }
  };

	SelectionHelper.prototype.pick = function(x,y,isOrtho){
		var isOrtho = isOrtho || this.isOrtho;
		var intersected, intersects, raycaster, v, _ref;
		v = new THREE.Vector3((x / this.viewWidth) * 2 - 1, -(y / this.viewHeight) * 2 + 1, 1);
		if( !isOrtho)
		{
		  var s = Date.now();
		  v.unproject( this.camera );
		  var v1 = v.clone();
		  var fooV = v.clone();

		  raycaster = new THREE.Raycaster(this.camera.position, v.sub(this.camera.position).normalize());
		  //raycaster.precision = 10;
		  intersects = raycaster.intersectObjects(this.hiearchyRoot, true);
		}
		else
		{
				// use picking ray since it's an orthographic camera
				//var ray = this.projector.pickingRay( v, this.camera );
				//intersects = ray.intersectObjects( this.hiearchyRoot, true );

				//see here:
				THREE.Vector3.prototype.pickingRay = function ( camera ) {
            var tan = Math.tan( 0.5 * THREE.Math.degToRad( camera.fov ) ) / camera.zoom;

            this.x *= tan * camera.aspect;
            this.y *= tan;
            this.z = - 1;

            return this.transformDirection( camera.matrixWorld );
        };

				//v.unproject( this.camera );
		    //raycaster = new THREE.Raycaster(this.camera.position, v.sub(this.camera.position).normalize());
		     raycaster = new THREE.Raycaster();
         v.pickingRay( this.camera );
         raycaster.set( this.camera.position, v );
		     intersects = raycaster.intersectObjects(this.hiearchyRoot, true);
		}

		//remove invisibles
		var i = intersects.length;
    while (i--) {
      if(intersects[i].object && intersects[i].object.visible === false)
		  {
		    intersects.splice(i, 1);
		  }
    }

		return intersects;
	};

  SelectionHelper.prototype.getSceneCoords = function(x,y,isOrtho){
		var isOrtho = isOrtho || this.isOrtho;
		var raycaster, v, pos;
		v = new THREE.Vector3((x / this.viewWidth) * 2 - 1, -(y / this.viewHeight) * 2 + 1, 1);
		if( !isOrtho)
		{
		  v.unproject( this.camera);

      var dir = v.sub( this.camera.position ).normalize();
      var distance = - this.camera.position.z / dir.z;
      var pos = this.camera.position.clone().add( dir.multiplyScalar( distance ) );
		}
		else
		{
				// use picking ray since it's an orthographic camera
				var ray = this.projector.pickingRay( v, this.camera );
				//intersects = ray.intersectObjects( this.hiearchyRoot, true );
		}
		return pos;
	};

  SelectionHelper.prototype.filterIntersects = function( intersects ){
    function goUpHierarchy(node, stopCondition)
		{
		  if(stopCondition( node ) ){
        return node;
      }
      else{
        if(node.parent){
        return goUpHierarchy( node.parent, stopCondition );}
        else{
          return null;
        }
      }
		}

		var hierarchyRoot = this.hiearchyRoot;
		var finalInteresects = [];
		//now go up the hierarchy until we find one "root" object ie in hierarchy root
		for(var i=0;i<intersects.length;i++)
		{
		  var node = intersects[i].object;
		  //console.log("intersect", intersects[i]);

		  function isOk(node)
		  {
		    if(hierarchyRoot.indexOf( node ) !=-1 ) return true;
		    return false
		  }
		  var validNode = goUpHierarchy(node, isOk);

		  if( validNode && !validNode.userData.unselectable )
		  {
		    var existingNodeIndex = finalInteresects.indexOf( validNode );
		    if(existingNodeIndex !== -1)
		    {
		      var prevNode = finalInteresects[existingNodeIndex];
		      prevNode.userData.tmpIntersectData.push( intersects[ i ] );
		      continue;
		    }
		    validNode.userData.tmpIntersectData = [ intersects[ i ] ];
		    finalInteresects.push( validNode );
		  }
		}
		//console.log("intersects",finalInteresects);
    return finalInteresects;
  }

  //FIXME: we still need access to the face etc of picked items
    /*if(intersects.length > 0)
		  {

		  }*/

  /*SelectionHelper.prototype.isThereObjectAt = function(x, y) {
		var intersects = this.pick(x,y,false);
    if (intersects.length > 0) {
      return true;
    }
    return false;
  };*/

  SelectionHelper.prototype.getObjectAt = function(x, y, isOrtho, filter) {
    var filter    = filter !== undefined ? filter : false;
    var isOrtho = isOrtho || false;
    var s = Date.now();
		var intersects = this.pick(x,y,isOrtho);
		if(filter) intersects = this.filterIntersects( intersects );
		var e = Date.now();
    //console.log( "Time to pick " + (e-s) + " ms" );
		//console.log("intersects",intersects);

    if (intersects.length > 0) {
      if(filter){
        var intersected = intersects[0];
      }else{
        var intersected = intersects[0].object;
        intersected.userData.tmpIntersectData = intersects ;
      }
      //console.log("intersected",intersects[0]);
      return intersected;
    } else {
      return null;
    }
  };

  SelectionHelper.prototype.selectObjectAt = function(x, y) {
    var intersects = this.pick(x,y,false);
    if (intersects.length > 0) {
      intersected = intersects[0].object;
      intersected._data = {};
      intersected._data.face = intersects[0].face;
      intersected._data.point = intersects[0].point;
      //console.log("intersected",intersected);

      if ((_ref = intersected.name) === "hoverOutline" || _ref === "selectOutline" || _ref === "boundingCage") {
        intersected = intersected.parent;
      }
      if (intersected !== this.currentSelect) {
        this._unSelect();
        this._onSelect(intersected);
        return this.currentSelect;
      } else {
        return this.currentSelect;
      }
    } else {
      return this._unSelect();
    }
  };

  SelectionHelper.prototype.highlightObjectAt = function(x, y) {
    var intersects = this.pick(x,y,false);
    if (intersects.length > 0) {
      if (intersects[0].object !== this.currentHover) {
        if (intersects[0].object.name !== "workplane" && intersects[0].object.name !== "hoverOutline" && intersects[0].object.name !== "boundingCage") {
          this._unHover();
          return this._onHover(intersects[0].object);
        }
      }
    } else {
      return this._unHover();
    }
  };

<|endoftext|>


<commit_before>/* global it describe beforeEach, afterEach */

import { shallow } from 'enzyme';
import React from 'react';
import ReleaseToggleApp from './ReleaseToggleApp.jsx';
import { expect } from 'chai';
import Context from './core/context';
import sinon from 'sinon';

describe('<ReleaseToggleApp /> - No Context Tests', () => {
    let context = Context.empty();
    let spy = null;
    beforeEach(()=>{
        spy = sinon.spy(context, 'checkFeatures');
    });
    afterEach(()=>{
        context.checkFeatures.restore();
    });

    it('sets empty context when there is feature', () => {
        const wrapper = shallow(<ReleaseToggleApp>
MyFeature
</ReleaseToggleApp>);
        expect(wrapper.find('.my-feature')).to.be.length(1);
        expect(spy.calledWith({})).to.be.true;
    });
    it('renders nothing when checks exsiting of a feature', () => {
        const wrapper = shallow(<ReleaseToggleApp myFeature={true}>
MyFeature
</ReleaseToggleApp>);
        expect(wrapper.find('.my-feature')).to.be.length(0);
        expect(spy.calledWith({myFeature: true})).to.be.true;
    });
    it('renders children when checks not existing of a feature', () => {
        const wrapper = shallow(<ReleaseToggleApp myFeature={false}>
MyFeature
</ReleaseToggleApp>);
        expect(wrapper.find('.my-feature')).to.be.length(1);
        expect(spy.calledWith({myFeature: false})).to.be.true;
    });
});

describe('<ReleaseToggleApp /> UI Tests', () => {
    it('renders nothing when there is no child', () => {
        const wrapper = shallow(<ReleaseToggleApp></ReleaseToggleApp>);
        expect(wrapper.find('div')).to.be.length(0);
    });
    it('renders DIV tag as container', () => {
        const wrapper = shallow(<ReleaseToggleApp>MyFeature</ReleaseToggleApp>);
        expect(wrapper.find('div')).to.be.length(1);
    });
    it('renders container with class', () => {
        const wrapper = shallow(<ReleaseToggleApp className="myFeature">MyFeature</ReleaseToggleApp>);
        expect(wrapper.find('div').first().hasClass('myFeature')).to.be.true;
    });
    it('renders container with style', () => {
        const wrapper = shallow(<ReleaseToggleApp style={{mybackground:'black'}}>MyFeature</ReleaseToggleApp>);
        expect(wrapper.find('div').first().props().style).to.deep.equal({mybackground:'black'});
    });
});<commit_msg>working on tests<commit_after>/* global it describe beforeEach, afterEach */

import { shallow } from 'enzyme';
import React from 'react';
import ReleaseToggleApp from './ReleaseToggleApp.jsx';
import { expect } from 'chai';
import Context from './core/context';
import sinon from 'sinon';

describe('<ReleaseToggleApp /> - No Context Tests', () => {
    let spy = null;
    let app = <ReleaseToggleApp></ReleaseToggleApp>;
    beforeEach(()=>{
        spy = sinon.spy(ReleaseToggleApp.prototype.getChildContext, 'getChildContext');
    });
    afterEach(()=>{
        app.getChildContext.restore();
    });

    it('sets empty context when there is no feature', () => {
        shallow(app);
        expect(spy.returned()).to.deep.equal({});
    });
});

describe('<ReleaseToggleApp /> UI Tests', () => {
    it('renders nothing when there is no child', () => {
        const wrapper = shallow(<ReleaseToggleApp></ReleaseToggleApp>);
        expect(wrapper.find('div')).to.be.length(0);
    });
    it('renders DIV tag as container', () => {
        const wrapper = shallow(<ReleaseToggleApp>MyFeature</ReleaseToggleApp>);
        expect(wrapper.find('div')).to.be.length(1);
    });
    it('renders container with class', () => {
        const wrapper = shallow(<ReleaseToggleApp className="myFeature">MyFeature</ReleaseToggleApp>);
        expect(wrapper.find('div').first().hasClass('myFeature')).to.be.true;
    });
    it('renders container with style', () => {
        const wrapper = shallow(<ReleaseToggleApp style={{mybackground:'black'}}>MyFeature</ReleaseToggleApp>);
        expect(wrapper.find('div').first().props().style).to.deep.equal({mybackground:'black'});
    });
});<|endoftext|>


<commit_before>/**
 * Follow instructions here to compile this file:
 * https://code.google.com/p/libphonenumber/source/browse/trunk/javascript/README
 *
 * (start by copying the contents of this file into
 * libphonenumber/javascript/i18n/phonenumbers/demo.js)
 */

// includes
goog.require('i18n.phonenumbers.AsYouTypeFormatter');
goog.require('i18n.phonenumbers.PhoneNumberFormat');
goog.require('i18n.phonenumbers.PhoneNumberUtil');
goog.require('i18n.phonenumbers.PhoneNumberUtil.ValidationResult');

function getExampleNumber(countryCode, national) {
  var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance();
  var number = phoneUtil.getExampleNumber(countryCode);
  var format = (national) ? i18n.phonenumbers.PhoneNumberFormat.NATIONAL : i18n.phonenumbers.PhoneNumberFormat.INTERNATIONAL;
  return phoneUtil.format(number, format);
}

function isValidNumber(number, countryCode) {
  try {
    var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance();
    var numberObj = phoneUtil.parseAndKeepRawInput(number, countryCode);
    return phoneUtil.isValidNumber(numberObj);
  } catch (e) {
    return false;
  }
}

function formatNumber(val, countryCode, addSuffix) {
  try {
    var clean = val.replace(/\D/g, "");
    if (val.substr(0, 1) == "+") {
      clean = "+" + clean;
    }
    var formatter = new i18n.phonenumbers.AsYouTypeFormatter(countryCode);
    // if clean is empty, we still need this to be a string otherwise we get errors later
    var result = "";
    for (var i = 0; i < clean.length; i++) {
      result = formatter.inputDigit(clean.charAt(i));
    }
    // for some reason libphonenumber formats "+44" to "+44 ", but doesn't do the same with "+1"
    if (result.charAt(result.length - 1) == " ") {
      result = result.substr(0, result.length - 1);
    }
    if (addSuffix) {
      // hack to get formatting suffix
      var test = formatter.inputDigit('5');
      // again the "+44 " problem... (also affects "+45" apparently)
      if (test.charAt(test.length - 1) == " ") {
        test = test.substr(0, test.length - 1);
      }
      // if adding a '5' introduces a formatting char (i.e. it's length changes by >1)
      if (test.length - result.length > 1) {
        // update the result to the new value (minus that last '5' we just added)
        result = test.substr(0, test.length - 1);
      }
    }
    return result;
  } catch (e) {
    return val;
  }
}

function getNumberType(number, countryCode) {
  try {
    var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance();
    var numberObj = phoneUtil.parseAndKeepRawInput(number, countryCode);
    return phoneUtil.getNumberType(numberObj)
  } catch (e) {
    return -2;
  }
}

// copied this from https://code.google.com/p/libphonenumber/source/browse/trunk/javascript/i18n/phonenumbers/phonenumberutil.js and put the keys in quotes to force closure compiler to preserve the keys
var numberType = {
  "FIXED_LINE": 0,
  "MOBILE": 1,
  // In some regions (e.g. the USA), it is impossible to distinguish between
  // fixed-line and mobile numbers by looking at the phone number itself.
  "FIXED_LINE_OR_MOBILE": 2,
  // Freephone lines
  "TOLL_FREE": 3,
  "PREMIUM_RATE": 4,
  // The cost of this call is shared between the caller and the recipient, and
  // is hence typically less than PREMIUM_RATE calls. See
  // http://en.wikipedia.org/wiki/Shared_Cost_Service for more information.
  "SHARED_COST": 5,
  // Voice over IP numbers. This includes TSoIP (Telephony Service over IP).
  "VOIP": 6,
  // A personal number is associated with a particular person, and may be routed
  // to either a MOBILE or FIXED_LINE number. Some more information can be found
  // here: http://en.wikipedia.org/wiki/Personal_Numbers
  "PERSONAL_NUMBER": 7,
  "PAGER": 8,
  // Used for 'Universal Access Numbers' or 'Company Numbers'. They may be
  // further routed to specific offices, but allow one number to be used for a
  // company.
  "UAN": 9,
  // Used for 'Voice Mail Access Numbers'.
  "VOICEMAIL": 10,
  // A phone number is of type UNKNOWN when it does not fit any of the known
  // patterns for a specific region.
  "UNKNOWN": -1
};

// exports
goog.exportSymbol('intlTelInputUtils', {});
goog.exportSymbol('intlTelInputUtils.formatNumber', formatNumber);
goog.exportSymbol('intlTelInputUtils.getExampleNumber', getExampleNumber);
goog.exportSymbol('intlTelInputUtils.getNumberType', getNumberType);
goog.exportSymbol('intlTelInputUtils.isValidNumber', isValidNumber);
goog.exportSymbol('intlTelInputUtils.numberType', numberType);
<commit_msg>comments<commit_after>/**
 * Follow instructions here to compile this file:
 * https://code.google.com/p/libphonenumber/source/browse/trunk/javascript/README
 *
 * (start by copying the contents of this file into
 * libphonenumber/javascript/i18n/phonenumbers/demo.js)
 */

// includes
goog.require('i18n.phonenumbers.AsYouTypeFormatter');
goog.require('i18n.phonenumbers.PhoneNumberFormat');
goog.require('i18n.phonenumbers.PhoneNumberUtil');
goog.require('i18n.phonenumbers.PhoneNumberUtil.ValidationResult');

function getExampleNumber(countryCode, national) {
  var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance();
  var number = phoneUtil.getExampleNumber(countryCode);
  var format = (national) ? i18n.phonenumbers.PhoneNumberFormat.NATIONAL : i18n.phonenumbers.PhoneNumberFormat.INTERNATIONAL;
  return phoneUtil.format(number, format);
}

function isValidNumber(number, countryCode) {
  try {
    var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance();
    var numberObj = phoneUtil.parseAndKeepRawInput(number, countryCode);
    return phoneUtil.isValidNumber(numberObj);
  } catch (e) {
    return false;
  }
}

function formatNumber(val, countryCode, addSuffix) {
  try {
    var clean = val.replace(/\D/g, "");
    if (val.substr(0, 1) == "+") {
      clean = "+" + clean;
    }
    // NOTE: we use AsYouTypeFormatter because the default format function can't handle incomplete numbers e.g. "+17024" formats to "+1 7024" as opposed to "+1 702-4"
    var formatter = new i18n.phonenumbers.AsYouTypeFormatter(countryCode);
    // if clean is empty, we still need this to be a string otherwise we get errors later
    var result = "";
    for (var i = 0; i < clean.length; i++) {
      // TODO: improve this so don't just pump in every digit every time - we should just cache this formatter object, and just call inputDigit once each time the user enters a new digit
      result = formatter.inputDigit(clean.charAt(i));
    }
    // for some reason libphonenumber formats "+44" to "+44 ", but doesn't do the same with "+1"
    if (result.charAt(result.length - 1) == " ") {
      result = result.substr(0, result.length - 1);
    }
    if (addSuffix) {
      // hack to get formatting suffix
      var test = formatter.inputDigit('5');
      // again the "+44 " problem... (also affects "+45" apparently)
      if (test.charAt(test.length - 1) == " ") {
        test = test.substr(0, test.length - 1);
      }
      // if adding a '5' introduces a formatting char (i.e. it's length changes by >1)
      if (test.length - result.length > 1) {
        // update the result to the new value (minus that last '5' we just added)
        result = test.substr(0, test.length - 1);
      }
    }
    return result;
  } catch (e) {
    return val;
  }
}

function getNumberType(number, countryCode) {
  try {
    var phoneUtil = i18n.phonenumbers.PhoneNumberUtil.getInstance();
    var numberObj = phoneUtil.parseAndKeepRawInput(number, countryCode);
    return phoneUtil.getNumberType(numberObj)
  } catch (e) {
    return -2;
  }
}

// copied this from i18n.phonenumbers.PhoneNumberType in https://code.google.com/p/libphonenumber/source/browse/trunk/javascript/i18n/phonenumbers/phonenumberutil.js and put the keys in quotes to force closure compiler to preserve the keys
// TODO: there must be a way to just tell closure compiler to preserve the keys on i18n.phonenumbers.PhoneNumberType and just export that
var numberType = {
  "FIXED_LINE": 0,
  "MOBILE": 1,
  // In some regions (e.g. the USA), it is impossible to distinguish between
  // fixed-line and mobile numbers by looking at the phone number itself.
  "FIXED_LINE_OR_MOBILE": 2,
  // Freephone lines
  "TOLL_FREE": 3,
  "PREMIUM_RATE": 4,
  // The cost of this call is shared between the caller and the recipient, and
  // is hence typically less than PREMIUM_RATE calls. See
  // http://en.wikipedia.org/wiki/Shared_Cost_Service for more information.
  "SHARED_COST": 5,
  // Voice over IP numbers. This includes TSoIP (Telephony Service over IP).
  "VOIP": 6,
  // A personal number is associated with a particular person, and may be routed
  // to either a MOBILE or FIXED_LINE number. Some more information can be found
  // here: http://en.wikipedia.org/wiki/Personal_Numbers
  "PERSONAL_NUMBER": 7,
  "PAGER": 8,
  // Used for 'Universal Access Numbers' or 'Company Numbers'. They may be
  // further routed to specific offices, but allow one number to be used for a
  // company.
  "UAN": 9,
  // Used for 'Voice Mail Access Numbers'.
  "VOICEMAIL": 10,
  // A phone number is of type UNKNOWN when it does not fit any of the known
  // patterns for a specific region.
  "UNKNOWN": -1
};

// exports
goog.exportSymbol('intlTelInputUtils', {});
goog.exportSymbol('intlTelInputUtils.formatNumber', formatNumber);
goog.exportSymbol('intlTelInputUtils.getExampleNumber', getExampleNumber);
goog.exportSymbol('intlTelInputUtils.getNumberType', getNumberType);
goog.exportSymbol('intlTelInputUtils.isValidNumber', isValidNumber);
goog.exportSymbol('intlTelInputUtils.numberType', numberType);
<|endoftext|>


<commit_before>'use strict';

var nodemailer = require('nodemailer');
var _ = require('lodash');

var emailData = require('./emailActualData');

var successHits = 0;
var failHits = 0;
var disableHits = 0;

var success = [];
var fail = [];
var disabled = [];

var organizeErrors = function(errors) {
	if(!Array.isArray(errors)){
		if(_.isObject(errors)){
			var accumulator = [];
			for(var property in errors) {
				if(errors.hasOwnProperty(property)){
					accumulator.push(property + ': ' + errors[property]);
				}
			}
			errors = accumulator;
		} else {
			errors = [errors];
		}
	}
	return _.flatten(errors);
};

var writeResult = function(result, uri) {
	switch(result) {
		case 'success':
			success.push('_ ' + uri.name + ' (' + uri.requestUrl + ') working as expected.');
			successHits++;
			break;
		case 'fail':
      if(uri.errors) {
        uri.errors = organizeErrors(uri.errors);
      }
			fail.push({
        text: 'X ' + uri.name + ' (' + uri.requestUrl + ') failed. Here are the problems:',
        errors: uri.errors
      });
			// console.log('uri.errors: ', uri.errors);
			failHits++;
			break;
		case 'disabled':
			disabled.push('_ ' + uri.name + ' (' + uri.requestUrl + ') testing disabled.');
			disableHits++;
			break;
		default:
			throw new Error('unknown case statement: ' + result);
	}
};

var sendEmail = function() {
  var transport = nodemailer.createTransport("SMTP", emailData.transport);

  transport.sendMail({
    from: emailData.from.name.first + ' ' + emailData.from.name.last + ' <' + emailData.from.email + '>',
    to: emailData.locals.email,
    subject: emailData.subject + ' ' + Date.now().toString(),
    html: html,
    generateTextFromHTML: true
    // text: text
  }, function(err, responseStatus) {
    if (err) {
      debug('Error in transport.sendMail():');
      debug(err);
      throw err;
    } else {
      console.log('responseStatus.message: ', responseStatus.message);
    }
  });
};

var done = function() {
  if(failHits > 0) {
    // We'll send an email if there are any failures.
    // If this is run infrequently (say daily) then you may want to send a success email as
    // well as a heartbeat to confirm that this is running on a schedule or timer.
    sendEmail();
  }
};

module.exports = {
	writeResult: writeResult,
	done: done
};
<commit_msg>Fixed a couple of issues identified by code-climate.<commit_after>'use strict';

var nodemailer = require('nodemailer');
var _ = require('lodash');
var debug = require('debug');

var emailData = require('./emailActualData');

var successHits = 0;
var failHits = 0;
var disableHits = 0;

var success = [];
var fail = [];
var disabled = [];

var organizeErrors = function(errors) {
	if(!Array.isArray(errors)){
		if(_.isObject(errors)){
			var accumulator = [];
			for(var property in errors) {
				if(errors.hasOwnProperty(property)){
					accumulator.push(property + ': ' + errors[property]);
				}
			}
			errors = accumulator;
		} else {
			errors = [errors];
		}
	}
	return _.flatten(errors);
};

var writeResult = function(result, uri) {
	switch(result) {
		case 'success':
			success.push('_ ' + uri.name + ' (' + uri.requestUrl + ') working as expected.');
			successHits++;
			break;
		case 'fail':
      if(uri.errors) {
        uri.errors = organizeErrors(uri.errors);
      }
			fail.push({
        text: 'X ' + uri.name + ' (' + uri.requestUrl + ') failed. Here are the problems:',
        errors: uri.errors
      });
			// console.log('uri.errors: ', uri.errors);
			failHits++;
			break;
		case 'disabled':
			disabled.push('_ ' + uri.name + ' (' + uri.requestUrl + ') testing disabled.');
			disableHits++;
			break;
		default:
			throw new Error('unknown case statement: ' + result);
	}
};

var sendEmail = function() {
  var transport = nodemailer.createTransport("SMTP", emailData.transport);
  var html = 'need to define some html';

  transport.sendMail({
    from: emailData.from.name.first + ' ' + emailData.from.name.last + ' <' + emailData.from.email + '>',
    to: emailData.locals.email,
    subject: emailData.subject + ' ' + Date.now().toString(),
    html: html,
    generateTextFromHTML: true
    // text: text
  }, function(err, responseStatus) {
    if (err) {
      debug('Error in transport.sendMail():');
      debug(err);
      throw err;
    } else {
      console.log('responseStatus.message: ', responseStatus.message);
    }
  });
};

var done = function() {
  if(failHits > 0) {
    // We'll send an email if there are any failures.
    // If this is run infrequently (say daily) then you may want to send a success email as
    // well as a heartbeat to confirm that this is running on a schedule or timer.
    sendEmail();
  }
};

module.exports = {
	writeResult: writeResult,
	done: done
};
<|endoftext|>


<commit_before>/*
 * index.js: Top-level include for the Rackspace module.
 *
 * (C) 2011 Nodejitsu Inc.
 *
 */

exports.cdn = require('./cdn');
exports.compute = require('./compute');
exports.storage = require('./storage');<commit_msg>[api] Stub out rackspace.*.createClient() API<commit_after>/*
 * index.js: Top-level include for the Rackspace module.
 *
 * (C) 2011 Nodejitsu Inc.
 *
 */

var utile = require('utile');

exports.cdn = require('./cdn');
exports.compute = require('./compute');
exports.storage = require('./storage');
exports.Client  = require('./client').Client;

exports.cdn.createClient = function (options) {
  var client = new exports.Client(options);

  utile.mixin(client, require('./storage/cdn/containers'));
  utile.mixin(client, require('./storage/cdn/directories'));
  utile.mixin(client, require('./storage/cdn/files'));

  return client;
};

exports.compute.createClient = function (options) {
  var client = new exports.Client(options);

  utile.mixin(client, require('./storage/client/servers'));
  utile.mixin(client, require('./storage/client/images'));
  utile.mixin(client, require('./storage/client/flavors'));

  return client;
};

exports.storage.createClient = function (options) {
  var client = new exports.Client(options);

  utile.mixin(client, require('./storage/client/containers'));
  utile.mixin(client, require('./storage/client/directories'));
  utile.mixin(client, require('./storage/client/files'));

  return client;
};<|endoftext|>


<commit_before>/*
 * Quality mapping from SickBeard to BTN's announce channel
 * - Keys are quality-identifiers in SickBeard
 * - 'display' is purely cosmetic and is used in the logs by PTPCPH
 * - 'sickname' is used to match qualities with SickBeard, don't change this
 * - 'source' is a regex of accepted sources from the IRC channel
 * - 'resolution' is a regex of accepted resolutions from the IRC channel
 * - 'container' is a regex of accepted containers from the IRC channel
 * - 'codec' is a regex of accepted codecs from the IRC channel
 *
 * If either 'source', 'resolution', 'container' or 'codec' is missing,
 * Middleman will assume wildcard .*
 *
 * ! Qualities has to be in descending order, starting at the highest quality.
 * - This is how PTPCPH decides if a release trumps another or not.
 *
 * ! Everything should be lower case, except names.
 *
 *	Available sources:
 *		hdtv, pdtv, dsr, dvdrip, tvrip, vhsrip, bluray, bdrip, brrip, dvd5,
 *		dvd9, hddvd, web-dl, webrip, bd5, bd9, bd25, bd50, mixed, unknown
 *
 *	Available codecs:
 *		xvid, x264, mpeg2, divx, dvdr, vc-1, h.264, wmv, bd, x264-hi10p
 *
 *	Available containers:
 *		avi, mkv, vob, mpeg, mp4, iso, wmv, ts, m4v, m2ts
 *
 *	Available resolutions:
 *		sd, 720p, 1080p, 1080i, portable device
 */

exports.mapping = {
	'rawhdtv': {
		'display': 'Raw HDTV',
		'sickname': 'rawhd tv',
		'source': 'hdtv',
		'resolution': '1080i'
	},
	'fullhdbluray': {
		'display': '1080p Blu-ray',
		'sickname': '1080p bluray',
		'source': 'bluray',
		'resolution': '1080p'
	},
	'fullhdwebdl': {
		'display': '1080p WEB-DL',
		'sickname': '1080p web-dl',
		'source': 'web-dl|webrip',
		'resolution': '1080p'
	},
	'fullhdtv': {
		'display': '1080p HDTV',
		'sickname': '1080p hd tv',
		'source': 'hdtv',
		'resolution': '1080p'
	},
	'hdbluray': {
		'display': '720p Blu-ray',
		'sickname': '720p bluray',
		'source': 'bluray',
		'resolution': '720p'
	},
	'hdwebdl': {
		'display': '720p WEB-DL',
		'sickname': '720p web-dl',
		'source': 'web-dl|webrip',
		'resolution': '720p'
	},
	'hdtv': {
		'display': 'HDTV',
		'sickname': 'hd tv',
		'source': 'hdtv',
		'resolution': '720p'
	},
	'sddvd': {
		'display': 'SDDVD',
		'sickname': 'sd dvd',
		'source': 'dvdrip|bdrip',
		'resolution': 'sd'
	},
	'sdtv': {
		'display': 'SDTV',
		'sickname': 'sd tv',
		'source': 'hdtv|pdtv|web-dl|webrip|bluray|tvrip',
		'resolution': 'sd'
	}
};
<commit_msg>Oops - references to old app-name in stash<commit_after>/*
 * Quality mapping from SickBeard to BTN's announce channel
 * - Keys are quality-identifiers in SickBeard
 * - 'display' is purely cosmetic and is used in the logs by Middleman
 * - 'sickname' is used to match qualities with SickBeard, don't change this
 * - 'source' is a regex of accepted sources from the IRC channel
 * - 'resolution' is a regex of accepted resolutions from the IRC channel
 * - 'container' is a regex of accepted containers from the IRC channel
 * - 'codec' is a regex of accepted codecs from the IRC channel
 *
 * If either 'source', 'resolution', 'container' or 'codec' is missing,
 * Middleman will assume wildcard .*
 *
 * ! Qualities has to be in descending order, starting at the highest quality.
 * - This is how Middleman decides if a release trumps another or not.
 *
 * ! Everything should be lower case, except names.
 *
 *	Available sources:
 *		hdtv, pdtv, dsr, dvdrip, tvrip, vhsrip, bluray, bdrip, brrip, dvd5,
 *		dvd9, hddvd, web-dl, webrip, bd5, bd9, bd25, bd50, mixed, unknown
 *
 *	Available codecs:
 *		xvid, x264, mpeg2, divx, dvdr, vc-1, h.264, wmv, bd, x264-hi10p
 *
 *	Available containers:
 *		avi, mkv, vob, mpeg, mp4, iso, wmv, ts, m4v, m2ts
 *
 *	Available resolutions:
 *		sd, 720p, 1080p, 1080i, portable device
 */

exports.mapping = {
	'rawhdtv': {
		'display': 'Raw HDTV',
		'sickname': 'rawhd tv',
		'source': 'hdtv',
		'resolution': '1080i'
	},
	'fullhdbluray': {
		'display': '1080p Blu-ray',
		'sickname': '1080p bluray',
		'source': 'bluray',
		'resolution': '1080p'
	},
	'fullhdwebdl': {
		'display': '1080p WEB-DL',
		'sickname': '1080p web-dl',
		'source': 'web-dl|webrip',
		'resolution': '1080p'
	},
	'fullhdtv': {
		'display': '1080p HDTV',
		'sickname': '1080p hd tv',
		'source': 'hdtv',
		'resolution': '1080p'
	},
	'hdbluray': {
		'display': '720p Blu-ray',
		'sickname': '720p bluray',
		'source': 'bluray',
		'resolution': '720p'
	},
	'hdwebdl': {
		'display': '720p WEB-DL',
		'sickname': '720p web-dl',
		'source': 'web-dl|webrip',
		'resolution': '720p'
	},
	'hdtv': {
		'display': 'HDTV',
		'sickname': 'hd tv',
		'source': 'hdtv',
		'resolution': '720p'
	},
	'sddvd': {
		'display': 'SDDVD',
		'sickname': 'sd dvd',
		'source': 'dvdrip|bdrip',
		'resolution': 'sd'
	},
	'sdtv': {
		'display': 'SDTV',
		'sickname': 'sd tv',
		'source': 'hdtv|pdtv|web-dl|webrip|bluray|tvrip',
		'resolution': 'sd'
	}
};
<|endoftext|>


<commit_before>/*eslint no-invalid-this: 0*/ // eslint doesn't understand Promise.coroutine wrapping
"use strict";
var Promise = require("bluebird");
var IrcRoom = require("../models/IrcRoom");
var IrcAction = require("../models/IrcAction");
var MatrixRoom = require("matrix-appservice-bridge").MatrixRoom;
var ConfigValidator = require("matrix-appservice-bridge").ConfigValidator;

var log = require("../logging").get("Provisioner");
var promiseutil = require("../promiseutil.js");

var matrixRoomIdValidation = {
    "type": "string",
    "pattern": "^!.*:.*$"
};

var validationProperties = {
    "matrix_room_id" : matrixRoomIdValidation,
    "remote_room_channel" : {
        "type": "string",
        "pattern": "^([#+&]|(![A-Z0-9]{5}))[^\\s:,]+$"
    },
    "remote_room_server" : {
        "type": "string",
        "pattern": "^[a-z\\.0-9:-]+$"
    },
    "op_nick" : {
        "type": "string"
    },
    "key" : {
        "type": "string"
    },
    "user_id" : {
        "type": "string"
    }
};

function Provisioner(ircBridge, enabled, requestTimeoutSeconds) {
    this._ircBridge = ircBridge;
    this._enabled = enabled;
    this._requestTimeoutSeconds = requestTimeoutSeconds;
    this._pendingRequests = {};
    // {
    //   $domain: {
    //     $nick: {
    //        userId : string
    //        defer: Deferred
    //     }
    //   }

    this._linkValidator = new ConfigValidator({
        "type": "object",
        "properties": validationProperties,
        "required": [
            "matrix_room_id",
            "remote_room_channel",
            "remote_room_server",
            "op_nick",
            "user_id"
        ]
    });
    this._unlinkValidator = new ConfigValidator({
        "type": "object",
        "properties": validationProperties,
        "required": [
            "matrix_room_id",
            "remote_room_channel",
            "remote_room_server"
        ]
    });
    this._roomIdValidator = new ConfigValidator({
        "type": "object",
        "properties": {
            "matrix_room_id" : matrixRoomIdValidation
        }
    });

    if (enabled) {
        log.info("Starting provisioning...");
    }
    else {
        log.info("Provisioning disabled.");
    }

    let as = this._ircBridge.getAppServiceBridge().appService;
    let self = this;

    if (enabled && !(as.app.use && as.app.get && as.app.post)) {
        throw new Error('Could not start provisioning API');
    }

    // Disable all provision endpoints by not calling 'next' and returning an error instead
    if (!enabled) {
        as.app.use(function(req, res, next) {
            if (self.isProvisionRequest(req)) {
                res.header("Access-Control-Allow-Origin", "*");
                res.header("Access-Control-Allow-Headers",
                    "Origin, X-Requested-With, Content-Type, Accept");
                res.status(200);
                res.json({error : 'Provisioning is not enabled.'});
            }
            else {
                next();
            }
        });
    }

    // Deal with CORS (temporarily for s-web)
    as.app.use(function(req, res, next) {
        if (self.isProvisionRequest(req)) {
                res.header("Access-Control-Allow-Origin", "*");
                res.header("Access-Control-Allow-Headers",
                    "Origin, X-Requested-With, Content-Type, Accept");
        }
        next();
    });

    as.app.post("/_matrix/provision/link", Promise.coroutine(function*(req, res) {
        try {
            yield self.requestLink(req.body);
            res.json({});
        }
        catch (err) {
            res.status(500).json({error: err.message});
            throw err;
        }
    }));

    as.app.post("/_matrix/provision/unlink", Promise.coroutine(function*(req, res) {
        try {
            yield self.unlink(req.body);
            res.json({});
        }
        catch (err) {
            res.status(500).json({error: err.message});
            throw err;
        }
    }));

    as.app.get("/_matrix/provision/listlinks/:roomId", Promise.coroutine(function*(req, res) {
        try {
            let list = yield self.listings(req.params.roomId);
            res.json(list);
        }
        catch (err) {
            res.status(500).json({error: err.message});
            throw err;
        }
    }));

    if (enabled) {
        log.info("Provisioning started");
    }
}

Provisioner.prototype.isProvisionRequest = function(req) {
    return req.url === '/_matrix/provision/unlink' ||
            req.url === '/_matrix/provision/link'||
            req.url.match(/^\/_matrix\/provision\/listlinks/)
};

Provisioner.prototype._updateBridgingState = Promise.coroutine(
    function*(roomId, userId, status, skey) {
        let intent = this._ircBridge.getAppServiceBridge().getIntent();
        try {
            yield intent.client.sendStateEvent(roomId, 'm.room.bridging', {
                user_id: userId,
                status: status // pending, success, failure
            }, skey);
        }
        catch (err) {
            console.error(err);
            throw new Error(`Could not update m.room.bridging state in this room`);
        }
    }
);

Provisioner.prototype._authoriseProvisioning = Promise.coroutine(
    function*(server, userId, ircChannel, roomId, opNick, key) {
        let ircDomain = server.domain;

        let existing = this._getRequest(server, opNick);
        if (existing) {
            let from = existing.userId;
            throw new Error(`Bridging request already sent to `+
                            `${opNick} on ${server.domain} from ${from}`);
        }

        // (Matrix) Check power level of user
        log.info(`Check power level of user`);
        let matrixClient = this._ircBridge.getAppServiceBridge().getClientFactory().getClientAs();

        let powerState = null;
        try {
            powerState = yield matrixClient.getStateEvent(roomId, 'm.room.power_levels');
        }
        catch (err) {
            log.error(`Error retrieving power levels (${err.data.error})`);
        }

        if (!powerState) {
            throw new Error('Could not retrieve your power levels for the room');
        }

        let actualPower = 0;
        if (powerState.users[userId] !== undefined) {
            actualPower = powerState.users[userId];
        }
        else if (powerState.users_default !== undefined) {
            actualPower = powerState.users_default;
        }

        let requiredPower = 50;
        if (powerState.events["m.room.power_levels"] !== undefined) {
            requiredPower = powerState.events["m.room.power_levels"]
        }
        else if (powerState.state_default !== undefined) {
            requiredPower = powerState.state_default;
        }

        if (actualPower < requiredPower) {
            throw new Error('User does not possess high enough power level');
        }

        // (IRC) Check that op's nick is actually op
        log.info(`Check that op's nick is actually op`);
        let botClient = yield this._ircBridge.getBotClient(server);
        yield botClient.joinChannel(ircChannel, key);
        log.info(`Bot joined channel ${ircChannel}`);
        let info = yield botClient.getOperators(ircChannel);

        if (info.nicks.indexOf(opNick) == -1) {
            let knownOps = info.operatorNicks.join(', ');
            throw new Error(`Provided user is not in channel ${ircChannel}. ` +
                            `Known ops in this channel: ${knownOps}`);
        }

        if (info.operatorNicks.indexOf(opNick) == -1) {
            let knownOps = info.operatorNicks.join(', ');
            throw new Error(`Provided user is not an op of ${ircChannel}. ` +
                            `Known ops in this channel: ${knownOps}`);
        }

        // (Matrix) update room state
        log.info(`Sending m.room.bridging`);
        // State key for m.room.bridging
        let skey = `irc://${ircDomain}/${ircChannel}`;

        // Send pending m.room.bridging
        yield this._updateBridgingState(roomId, userId, 'pending', skey);

        // (IRC) Ask operator for authorisation
        // Time that operator has to respond before giving up
        let timeoutSeconds = this._requestTimeoutSeconds;

        // Deliberately not yielding on this so that 200 OK is returned
        log.info(`Contacting operator`);
        this._contactOperator(
            botClient, server, opNick, ircChannel,
            roomId, userId, skey, timeoutSeconds);
    }
);

Provisioner.prototype._contactOperator = Promise.coroutine(
    function*(botClient, server, opNick, ircChannel, roomId, userId, skey, timeoutSeconds) {
        let d = promiseutil.defer();

        // Send PM to operator
        let authRequestAction = new IrcAction("message",
            `${userId} has requested to bridge ${roomId} with ${ircChannel} on this IRC ` +
            `network. Respond with 'yes' or 'y' to allow, or simply ignore this message to ` +
            `dissallow. You have ${timeoutSeconds} seconds from when this message was sent.`);
        let pmRoom = new IrcRoom(server, opNick);

        this._setRequest(server, opNick, {userId: userId, defer: d});

        this._ircBridge.sendIrcAction(pmRoom, botClient, authRequestAction);
        try {
            yield d.promise.timeout(timeoutSeconds * 1000);
            this._removeRequest(server, opNick);
        }
        catch (err) {
            log.info(`Operator ${opNick} did not respond (${err.message})`);
            yield this._updateBridgingState(roomId, userId, 'failure', skey);
            this._removeRequest(server, opNick);
            return;
        }
        try {
            yield this._doLink(server, ircChannel, roomId);
        }
        catch (err) {
            log.info(`Failed to create link following authorisation (${err.message})`);
            yield this._updateBridgingState(roomId, userId, 'failure', skey);
            this._removeRequest(server, opNick);
            return;
        }
        yield this._updateBridgingState(roomId, userId, 'success', skey);
    }
);

Provisioner.prototype._removeRequest = function (server, opNick) {
    delete this._pendingRequests[server.domain][opNick];
}

Provisioner.prototype._getRequest = function (server, opNick) {
    if (this._pendingRequests[server.domain]) {
        return this._pendingRequests[server.domain][opNick];
    }
}

Provisioner.prototype._setRequest = function (server, opNick, request) {
    if (!this._pendingRequests[server.domain]) {
        this._pendingRequests[server.domain] = {};
    }
    this._pendingRequests[server.domain][opNick] = request;
}

Provisioner.prototype.handlePm = function(server, fromUser, text) {
    if (['y', 'yes'].indexOf(text.trim().toLowerCase()) == -1) {
        log.warn(`Provisioner only handles text 'yes'/'y' ` +
                 `(from ${fromUser.nick} on ${server.domain})`);
        return;
    }
    let request = this._getRequest(server, fromUser.nick);
    if (request) {
        log.info(`${fromUser.nick} has authorised a new provisioning`);
        request.defer.resolve();
        return;
    }
    log.warn(`Provisioner was not expecting PM from ${fromUser.nick} on ${server.domain}`);
}

// Link an IRC channel to a matrix room ID
Provisioner.prototype.requestLink = Promise.coroutine(function*(options) {
    try {
        this._linkValidator.validate(options);
    }
    catch (err) {
        log.error(err);
        throw new Error("Malformed parameters");
    }

    let ircDomain = options.remote_room_server;
    let ircChannel = options.remote_room_channel;
    let roomId = options.matrix_room_id;
    let opNick = options.op_nick;
    let key = options.key || undefined; // Optional key
    let userId = options.user_id;
    let mappingLogId = `${roomId} <---> ${ircDomain}/${ircChannel}`;

    // Try to find the domain requested for linking
    //TODO: ircDomain might include protocol, i.e. irc://irc.freenode.net
    let server = this._ircBridge.getServer(ircDomain);

    if (!server) {
        throw new Error(`Server requested for linking not found ('${ircDomain}')`);
    }

    if (server.isExcludedChannel(ircChannel)) {
        throw new Error(`Server is configured to exclude given channel ('${ircChannel}')`);
    }

    let entry = yield this._ircBridge.getStore().getRoom(roomId, ircDomain, ircChannel);
    if (!entry) {
        // Ask OP for provisioning authentication
        try {
            yield this._authoriseProvisioning(server, userId, ircChannel, roomId, opNick, key);
        }
        catch (err) {
            console.error(err.stack);
            throw new Error('Failed to authorise provisioning');
        }
    }
    else {
        throw new Error(`Room mapping already exists (${mappingLogId},` +
                        `origin = ${entry.data.origin})`);
    }
});

Provisioner.prototype._doLink = Promise.coroutine(
    function*(server, ircChannel, roomId) {
        let ircDomain = server.domain;
        let mappingLogId = `${roomId} <---> ${ircDomain}/${ircChannel}`;
        log.info(`Provisioning link for room ${mappingLogId}`);

        // Create rooms for the link
        let ircRoom = new IrcRoom(server, ircChannel);
        let mxRoom = new MatrixRoom(roomId);

        let entry = yield this._ircBridge.getStore().getRoom(roomId, ircDomain, ircChannel);
        if (!entry) {
            yield this._ircBridge.getStore().storeRoom(ircRoom, mxRoom, 'provision');
        }
        else {
            throw new Error(`Room mapping already exists (${mappingLogId},` +
                            `origin = ${entry.data.origin})`);
        }
    }
);

// Unlink an IRC channel from a matrix room ID
Provisioner.prototype.unlink = Promise.coroutine(function*(options) {
    try {
        this._unlinkValidator.validate(options);
    }
    catch (err) {
        log.error(err);
        throw new Error("Malformed parameters");
    }

    let ircDomain = options.remote_room_server;
    let ircChannel = options.remote_room_channel;
    let roomId = options.matrix_room_id;
    let mappingLogId = `${roomId} <-/-> ${ircDomain}/${ircChannel}`;

    log.info(`Provisioning unlink for room ${mappingLogId}`);

    // Try to find the domain requested for unlinking
    let server = this._ircBridge.getServer(ircDomain);

    if (!server) {
        throw new Error("Server requested for linking not found");
    }

    // Delete the room link
    let entry = yield this._ircBridge.getStore()
        .getRoom(roomId, ircDomain, ircChannel, 'provision');

    if (entry) {
        yield this._ircBridge.getStore().removeRoom(roomId, ircDomain, ircChannel, 'provision');
    }
    else {
        throw new Error(`Provisioned room mapping does not exist (${mappingLogId})`);
    }
});

// List all mappings currently provisioned with the given matrix_room_id
Provisioner.prototype.listings = function(roomId) {
    try {
        this._roomIdValidator.validate({"matrix_room_id": roomId});
    }
    catch (err) {
        log.error(err);
        throw new Error("Malformed parameters");
    }

    return this._ircBridge.getStore()
        .getProvisionedMappings(roomId)
        .map((entry) => {
            return {
                matrix_room_id : entry.matrix.roomId,
                remote_room_channel : entry.remote.data.channel,
                remote_room_server : entry.remote.data.domain,
            }
        });
};

module.exports = Provisioner;
<commit_msg>Added comments for authoriseProvisioning and contactOperator<commit_after>/*eslint no-invalid-this: 0*/ // eslint doesn't understand Promise.coroutine wrapping
"use strict";
var Promise = require("bluebird");
var IrcRoom = require("../models/IrcRoom");
var IrcAction = require("../models/IrcAction");
var MatrixRoom = require("matrix-appservice-bridge").MatrixRoom;
var ConfigValidator = require("matrix-appservice-bridge").ConfigValidator;

var log = require("../logging").get("Provisioner");
var promiseutil = require("../promiseutil.js");

var matrixRoomIdValidation = {
    "type": "string",
    "pattern": "^!.*:.*$"
};

var validationProperties = {
    "matrix_room_id" : matrixRoomIdValidation,
    "remote_room_channel" : {
        "type": "string",
        "pattern": "^([#+&]|(![A-Z0-9]{5}))[^\\s:,]+$"
    },
    "remote_room_server" : {
        "type": "string",
        "pattern": "^[a-z\\.0-9:-]+$"
    },
    "op_nick" : {
        "type": "string"
    },
    "key" : {
        "type": "string"
    },
    "user_id" : {
        "type": "string"
    }
};

function Provisioner(ircBridge, enabled, requestTimeoutSeconds) {
    this._ircBridge = ircBridge;
    this._enabled = enabled;
    this._requestTimeoutSeconds = requestTimeoutSeconds;
    this._pendingRequests = {};
    // {
    //   $domain: {
    //     $nick: {
    //        userId : string
    //        defer: Deferred
    //     }
    //   }

    this._linkValidator = new ConfigValidator({
        "type": "object",
        "properties": validationProperties,
        "required": [
            "matrix_room_id",
            "remote_room_channel",
            "remote_room_server",
            "op_nick",
            "user_id"
        ]
    });
    this._unlinkValidator = new ConfigValidator({
        "type": "object",
        "properties": validationProperties,
        "required": [
            "matrix_room_id",
            "remote_room_channel",
            "remote_room_server"
        ]
    });
    this._roomIdValidator = new ConfigValidator({
        "type": "object",
        "properties": {
            "matrix_room_id" : matrixRoomIdValidation
        }
    });

    if (enabled) {
        log.info("Starting provisioning...");
    }
    else {
        log.info("Provisioning disabled.");
    }

    let as = this._ircBridge.getAppServiceBridge().appService;
    let self = this;

    if (enabled && !(as.app.use && as.app.get && as.app.post)) {
        throw new Error('Could not start provisioning API');
    }

    // Disable all provision endpoints by not calling 'next' and returning an error instead
    if (!enabled) {
        as.app.use(function(req, res, next) {
            if (self.isProvisionRequest(req)) {
                res.header("Access-Control-Allow-Origin", "*");
                res.header("Access-Control-Allow-Headers",
                    "Origin, X-Requested-With, Content-Type, Accept");
                res.status(200);
                res.json({error : 'Provisioning is not enabled.'});
            }
            else {
                next();
            }
        });
    }

    // Deal with CORS (temporarily for s-web)
    as.app.use(function(req, res, next) {
        if (self.isProvisionRequest(req)) {
                res.header("Access-Control-Allow-Origin", "*");
                res.header("Access-Control-Allow-Headers",
                    "Origin, X-Requested-With, Content-Type, Accept");
        }
        next();
    });

    as.app.post("/_matrix/provision/link", Promise.coroutine(function*(req, res) {
        try {
            yield self.requestLink(req.body);
            res.json({});
        }
        catch (err) {
            res.status(500).json({error: err.message});
            throw err;
        }
    }));

    as.app.post("/_matrix/provision/unlink", Promise.coroutine(function*(req, res) {
        try {
            yield self.unlink(req.body);
            res.json({});
        }
        catch (err) {
            res.status(500).json({error: err.message});
            throw err;
        }
    }));

    as.app.get("/_matrix/provision/listlinks/:roomId", Promise.coroutine(function*(req, res) {
        try {
            let list = yield self.listings(req.params.roomId);
            res.json(list);
        }
        catch (err) {
            res.status(500).json({error: err.message});
            throw err;
        }
    }));

    if (enabled) {
        log.info("Provisioning started");
    }
}

Provisioner.prototype.isProvisionRequest = function(req) {
    return req.url === '/_matrix/provision/unlink' ||
            req.url === '/_matrix/provision/link'||
            req.url.match(/^\/_matrix\/provision\/listlinks/)
};

Provisioner.prototype._updateBridgingState = Promise.coroutine(
    function*(roomId, userId, status, skey) {
        let intent = this._ircBridge.getAppServiceBridge().getIntent();
        try {
            yield intent.client.sendStateEvent(roomId, 'm.room.bridging', {
                user_id: userId,
                status: status // pending, success, failure
            }, skey);
        }
        catch (err) {
            console.error(err);
            throw new Error(`Could not update m.room.bridging state in this room`);
        }
    }
);

// Do a series of checks before contacting an operator for permission to create
//  a provisioned mapping. If the operator responds with 'yes' or 'y', the mapping
//  is created.
// The checks done are the following:
//  - (Matrix) Check power level of user is high enough
//  - (IRC) Check that op's nick is actually a channel op
//  - (Matrix) update room state m.room.brdiging
Provisioner.prototype._authoriseProvisioning = Promise.coroutine(
    function*(server, userId, ircChannel, roomId, opNick, key) {
        let ircDomain = server.domain;

        let existing = this._getRequest(server, opNick);
        if (existing) {
            let from = existing.userId;
            throw new Error(`Bridging request already sent to `+
                            `${opNick} on ${server.domain} from ${from}`);
        }

        // (Matrix) Check power level of user
        log.info(`Check power level of user`);
        let matrixClient = this._ircBridge.getAppServiceBridge().getClientFactory().getClientAs();

        let powerState = null;
        try {
            powerState = yield matrixClient.getStateEvent(roomId, 'm.room.power_levels');
        }
        catch (err) {
            log.error(`Error retrieving power levels (${err.data.error})`);
        }

        if (!powerState) {
            throw new Error('Could not retrieve your power levels for the room');
        }

        let actualPower = 0;
        if (powerState.users[userId] !== undefined) {
            actualPower = powerState.users[userId];
        }
        else if (powerState.users_default !== undefined) {
            actualPower = powerState.users_default;
        }

        let requiredPower = 50;
        if (powerState.events["m.room.power_levels"] !== undefined) {
            requiredPower = powerState.events["m.room.power_levels"]
        }
        else if (powerState.state_default !== undefined) {
            requiredPower = powerState.state_default;
        }

        if (actualPower < requiredPower) {
            throw new Error('User does not possess high enough power level');
        }

        // (IRC) Check that op's nick is actually op
        log.info(`Check that op's nick is actually op`);
        let botClient = yield this._ircBridge.getBotClient(server);
        yield botClient.joinChannel(ircChannel, key);
        log.info(`Bot joined channel ${ircChannel}`);
        let info = yield botClient.getOperators(ircChannel);

        if (info.nicks.indexOf(opNick) == -1) {
            let knownOps = info.operatorNicks.join(', ');
            throw new Error(`Provided user is not in channel ${ircChannel}. ` +
                            `Known ops in this channel: ${knownOps}`);
        }

        if (info.operatorNicks.indexOf(opNick) == -1) {
            let knownOps = info.operatorNicks.join(', ');
            throw new Error(`Provided user is not an op of ${ircChannel}. ` +
                            `Known ops in this channel: ${knownOps}`);
        }

        // (Matrix) update room state
        log.info(`Sending m.room.bridging`);
        // State key for m.room.bridging
        let skey = `irc://${ircDomain}/${ircChannel}`;

        // Send pending m.room.bridging
        yield this._updateBridgingState(roomId, userId, 'pending', skey);

        // (IRC) Ask operator for authorisation
        // Time that operator has to respond before giving up
        let timeoutSeconds = this._requestTimeoutSeconds;

        // Deliberately not yielding on this so that 200 OK is returned
        log.info(`Contacting operator`);
        this._contactOperator(
            botClient, server, opNick, ircChannel,
            roomId, userId, skey, timeoutSeconds);
    }
);

// Contact an operator, asking for authorisation for a mapping, and if they reply
//  'yes' or 'y', create the mapping.
Provisioner.prototype._contactOperator = Promise.coroutine(
    function*(botClient, server, opNick, ircChannel, roomId, userId, skey, timeoutSeconds) {
        let d = promiseutil.defer();

        // Send PM to operator
        let authRequestAction = new IrcAction("message",
            `${userId} has requested to bridge ${roomId} with ${ircChannel} on this IRC ` +
            `network. Respond with 'yes' or 'y' to allow, or simply ignore this message to ` +
            `dissallow. You have ${timeoutSeconds} seconds from when this message was sent.`);
        let pmRoom = new IrcRoom(server, opNick);

        this._setRequest(server, opNick, {userId: userId, defer: d});

        this._ircBridge.sendIrcAction(pmRoom, botClient, authRequestAction);
        try {
            yield d.promise.timeout(timeoutSeconds * 1000);
            this._removeRequest(server, opNick);
        }
        catch (err) {
            log.info(`Operator ${opNick} did not respond (${err.message})`);
            yield this._updateBridgingState(roomId, userId, 'failure', skey);
            this._removeRequest(server, opNick);
            return;
        }
        try {
            yield this._doLink(server, ircChannel, roomId);
        }
        catch (err) {
            log.info(`Failed to create link following authorisation (${err.message})`);
            yield this._updateBridgingState(roomId, userId, 'failure', skey);
            this._removeRequest(server, opNick);
            return;
        }
        yield this._updateBridgingState(roomId, userId, 'success', skey);
    }
);

Provisioner.prototype._removeRequest = function (server, opNick) {
    delete this._pendingRequests[server.domain][opNick];
}

Provisioner.prototype._getRequest = function (server, opNick) {
    if (this._pendingRequests[server.domain]) {
        return this._pendingRequests[server.domain][opNick];
    }
}

Provisioner.prototype._setRequest = function (server, opNick, request) {
    if (!this._pendingRequests[server.domain]) {
        this._pendingRequests[server.domain] = {};
    }
    this._pendingRequests[server.domain][opNick] = request;
}

Provisioner.prototype.handlePm = function(server, fromUser, text) {
    if (['y', 'yes'].indexOf(text.trim().toLowerCase()) == -1) {
        log.warn(`Provisioner only handles text 'yes'/'y' ` +
                 `(from ${fromUser.nick} on ${server.domain})`);
        return;
    }
    let request = this._getRequest(server, fromUser.nick);
    if (request) {
        log.info(`${fromUser.nick} has authorised a new provisioning`);
        request.defer.resolve();
        return;
    }
    log.warn(`Provisioner was not expecting PM from ${fromUser.nick} on ${server.domain}`);
}

// Link an IRC channel to a matrix room ID
Provisioner.prototype.requestLink = Promise.coroutine(function*(options) {
    try {
        this._linkValidator.validate(options);
    }
    catch (err) {
        log.error(err);
        throw new Error("Malformed parameters");
    }

    let ircDomain = options.remote_room_server;
    let ircChannel = options.remote_room_channel;
    let roomId = options.matrix_room_id;
    let opNick = options.op_nick;
    let key = options.key || undefined; // Optional key
    let userId = options.user_id;
    let mappingLogId = `${roomId} <---> ${ircDomain}/${ircChannel}`;

    // Try to find the domain requested for linking
    //TODO: ircDomain might include protocol, i.e. irc://irc.freenode.net
    let server = this._ircBridge.getServer(ircDomain);

    if (!server) {
        throw new Error(`Server requested for linking not found ('${ircDomain}')`);
    }

    if (server.isExcludedChannel(ircChannel)) {
        throw new Error(`Server is configured to exclude given channel ('${ircChannel}')`);
    }

    let entry = yield this._ircBridge.getStore().getRoom(roomId, ircDomain, ircChannel);
    if (!entry) {
        // Ask OP for provisioning authentication
        try {
            yield this._authoriseProvisioning(server, userId, ircChannel, roomId, opNick, key);
        }
        catch (err) {
            console.error(err.stack);
            throw new Error('Failed to authorise provisioning');
        }
    }
    else {
        throw new Error(`Room mapping already exists (${mappingLogId},` +
                        `origin = ${entry.data.origin})`);
    }
});

Provisioner.prototype._doLink = Promise.coroutine(
    function*(server, ircChannel, roomId) {
        let ircDomain = server.domain;
        let mappingLogId = `${roomId} <---> ${ircDomain}/${ircChannel}`;
        log.info(`Provisioning link for room ${mappingLogId}`);

        // Create rooms for the link
        let ircRoom = new IrcRoom(server, ircChannel);
        let mxRoom = new MatrixRoom(roomId);

        let entry = yield this._ircBridge.getStore().getRoom(roomId, ircDomain, ircChannel);
        if (!entry) {
            yield this._ircBridge.getStore().storeRoom(ircRoom, mxRoom, 'provision');
        }
        else {
            throw new Error(`Room mapping already exists (${mappingLogId},` +
                            `origin = ${entry.data.origin})`);
        }
    }
);

// Unlink an IRC channel from a matrix room ID
Provisioner.prototype.unlink = Promise.coroutine(function*(options) {
    try {
        this._unlinkValidator.validate(options);
    }
    catch (err) {
        log.error(err);
        throw new Error("Malformed parameters");
    }

    let ircDomain = options.remote_room_server;
    let ircChannel = options.remote_room_channel;
    let roomId = options.matrix_room_id;
    let mappingLogId = `${roomId} <-/-> ${ircDomain}/${ircChannel}`;

    log.info(`Provisioning unlink for room ${mappingLogId}`);

    // Try to find the domain requested for unlinking
    let server = this._ircBridge.getServer(ircDomain);

    if (!server) {
        throw new Error("Server requested for linking not found");
    }

    // Delete the room link
    let entry = yield this._ircBridge.getStore()
        .getRoom(roomId, ircDomain, ircChannel, 'provision');

    if (entry) {
        yield this._ircBridge.getStore().removeRoom(roomId, ircDomain, ircChannel, 'provision');
    }
    else {
        throw new Error(`Provisioned room mapping does not exist (${mappingLogId})`);
    }
});

// List all mappings currently provisioned with the given matrix_room_id
Provisioner.prototype.listings = function(roomId) {
    try {
        this._roomIdValidator.validate({"matrix_room_id": roomId});
    }
    catch (err) {
        log.error(err);
        throw new Error("Malformed parameters");
    }

    return this._ircBridge.getStore()
        .getProvisionedMappings(roomId)
        .map((entry) => {
            return {
                matrix_room_id : entry.matrix.roomId,
                remote_room_channel : entry.remote.data.channel,
                remote_room_server : entry.remote.data.domain,
            }
        });
};

module.exports = Provisioner;
<|endoftext|>