<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`);
        }
    }
);

// 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 ${userId} in ${roomId}`);
        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
        // State key for m.room.bridging
        let skey = `irc://${ircDomain}/${ircChannel}`;
        log.info(`Sending m.room.bridging to ${roomId}, state key = ${skey}`);

        // 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._createAuthorisedLink(
            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._createAuthorisedLink = 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 ` +
            `disallow. 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>protect _removeRequest with null guard<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 ${userId} in ${roomId}`);
        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
        // State key for m.room.bridging
        let skey = `irc://${ircDomain}/${ircChannel}`;
        log.info(`Sending m.room.bridging to ${roomId}, state key = ${skey}`);

        // 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._createAuthorisedLink(
            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._createAuthorisedLink = 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 ` +
            `disallow. 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) {
    if (this._pendingRequests[server.domain]) {
        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|>


<commit_before>import Dungeon from '../dungeons/Dungeon.js';
import GameEvent from '../events/GameEvent.js';
import GameEvents from '../events/GameEvents.js';

import DungeonTooltips from './DungeonTooltips.js';

import DamageTypes from '../entities/DamageTypes.js';

import SharedUIDataController from '../controllers/SharedUIDataController.js';

import Moves from '../entities/creatures/moves/Moves.js';

const PIXI = require('pixi.js');
const TextureCache = (PIXI.utils.TextureCache);
const Sprite = PIXI.Sprite;

const TILE_WIDTH = 50;
const GAP_WIDTH = 0;

const DAMAGE_COLORS = {
    [DamageTypes.MELEE_PHYSICAL]: 'darkred',
    [DamageTypes.RANGED_PHYSICAL]: 'darkred',
    [DamageTypes.FIRE]: 'orange',
    [DamageTypes.COLD]: 'darkblue',
    [DamageTypes.ELECTRICAL]: 'yellow',
    [DamageTypes.ENERGY]: 'white',
    [DamageTypes.POISON]: 'emerald'
};

const DAMAGE_OUTLINE_COLORS = {
    [DamageTypes.MELEE_PHYSICAL]: 'pink',
    [DamageTypes.RANGED_PHYSICAL]: 'pink',
    [DamageTypes.FIRE]: 'darkred',
    [DamageTypes.COLD]: 'skyblue',
    [DamageTypes.ELECTRICAL]: 'orange',
    [DamageTypes.ENERGY]: 'yellow',
    [DamageTypes.POISON]: 'darkgreen'
};

const NEUTRAL_COLOR = 0x46465a;
const ATTACK_MOVE_COLOR = 0x8b0000;
const ITEM_MOVE_COLOR = 0x7F00FF;
const ABILITY_MOVE_COLOR = 0x9400D3;

function setDefaultSpriteProps(sprite) {
    sprite.x = 0;
    sprite.y = 0;
    sprite.width = TILE_WIDTH;
    sprite.height = TILE_WIDTH;
    return sprite;
}

function getSpriteStack(spriteNames) {
    const group = new PIXI.Container();
    spriteNames.forEach(function(spriteName) {
        group.addChild(
            setDefaultSpriteProps(
                new Sprite(TextureCache[spriteName])
            )
        );
    });
    return group;
}

function getContainerWrapping(obj) {
    const group = new PIXI.Container();
    group.addChild(obj);
    return group;
}

function getTileSprite(tile) {
    let tileDisp;
    if(tile.constructor.name === 'DoorTile') {
        if(tile.isOpen()) {
            tileDisp = getSpriteStack(['Tile', 'DoorOpen']);
        } else {
            tileDisp = new Sprite(TextureCache['DoorClosed']);
        }
    } else if(tile.constructor.name === 'EntranceTile') {
        tileDisp = getSpriteStack(['Tile', 'Ladder']);
    } else if(tile.constructor.name === 'PillarTile') {
        tileDisp = getSpriteStack(['Tile', 'Pillar']);
    } else {
        const texture = TextureCache[tile.constructor.name] || TextureCache['ThisIsAThing'];
        tileDisp = new Sprite(texture);
    }
    return getContainerWrapping(tileDisp);
}

function getCreatureSprite(creature) {
    return getContainerWrapping(
        new PIXI.Sprite(PIXI.utils.TextureCache[creature.constructor.name])
    );
}

function getTileColor(sharedData, x, y) {
    const dungeon = sharedData.getDungeon();
    const player = dungeon.getPlayableCharacter();
    const playerLocation = dungeon.getTile(player);

    switch(sharedData.getMode()) {
        case SharedUIDataController.EXAMINE_MODE:
            return NEUTRAL_COLOR;
        case SharedUIDataController.ATTACK_MODE:
            return new Moves.AttackMove(playerLocation, x, y)
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ATTACK_MOVE_COLOR;
        case SharedUIDataController.TARGETTED_ABILITY_MODE:
            const abilityIndex = sharedData.getTargettedAbility();
            return new Moves.UseAbilityMove(playerLocation, abilityIndex, x, y)
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ABILITY_MOVE_COLOR;
        case SharedUIDataController.TARGETTED_ITEM_MODE:
            const itemIndex = sharedData.getTargettedItem();
            return new Moves.UseItemMove(playerLocation, itemIndex, dungeon.getTile(x, y))
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ITEM_MOVE_COLOR;
        case SharedUIDataController.NEUTRAL_MODE:
            return new Moves.AttackMove(playerLocation, x, y)
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ATTACK_MOVE_COLOR;
        default:
            throw new Error('This should never happen');
    }
}

function getIndicator(x, y, color) {
    const indicator = new PIXI.Graphics();
    indicator.lineStyle(1, color);
    indicator.drawRect(
        x * (TILE_WIDTH + GAP_WIDTH) + 1, y * (TILE_WIDTH + GAP_WIDTH),
        TILE_WIDTH + GAP_WIDTH - 1, TILE_WIDTH + GAP_WIDTH - 1
    );
    return indicator;
}

function getTileContainer(tile) {
    const tileContainer = new PIXI.Container(); // TODO: Look at ParticleContainer
    const tileSprite = getTileSprite(tile);
    const x = tile.getX();
    const y = tile.getY();

    tileSprite.x = 0;
    tileSprite.y = 0;
    tileSprite.width = TILE_WIDTH;
    tileSprite.height = TILE_WIDTH;
    tileSprite.interactive = true;

    tileContainer.addChild(tileSprite);
    tileContainer.addChild(new PIXI.Container());
    tileContainer.addChild(new PIXI.Container());

    return tileContainer;
}

export default class PixiDungeonView {
    constructor(sharedData) {
        if(!(sharedData instanceof SharedUIDataController)) {
            throw new Error('First parameter must be a SharedUIDataController');
        }
        const self = this;
        this._sharedData = sharedData;
        this._indicators = [];
        this._clickHanders = [];
        this._mouseOverHandlers = [];
        this._mouseOutHandlers = [];

        const canvasContainer = this._canvasContainer = document.createElement('div');
        canvasContainer.classList.add('canvas-container');

        const renderer = this._renderer = PIXI.autoDetectRenderer();
        canvasContainer.appendChild(renderer.view);

        PIXI.loader.add('images/spritesheet.json').load(() => this.init());

        function resize() {
            const {clientWidth, clientHeight} = canvasContainer;
            renderer.resize(clientWidth, clientHeight);
        }

        setTimeout(resize);
        window.addEventListener('resize', resize);
    }

    init() {
        const stage = this._stage = new PIXI.Container();

        const entitySprites = this._entiteSprites = {};

        const sharedData = this._sharedData;
        const renderer = this._renderer;

        sharedData.addObserver((event) => {
            if(event instanceof Dungeon){
                this.populateStage(stage);
                document.querySelector('section.game').focus(); //TODO: Make canvas container focusable insted?
            }
        });

        sharedData.addObserver((event) => {
            const dungeon = sharedData.getDungeon();
            if(event instanceof GameEvents.MoveEvent || event instanceof GameEvents.PositionChangeEvent || event instanceof GameEvents.SpawnEvent) {
                this.updateVision();
                this.updateCreatureLocations();
                this.scrollToPlayer();
            } else if(event instanceof GameEvents.DeathEvent) {
                const creature = event.getCreature();
                const tile = dungeon.getTile(creature);
                const x = tile.getX();
                const y = tile.getY();
                const tileContainer = this._tileContainers[x][y].children[2].removeChildAt(0);
                //setTimeout(updateItems); // TODO: Fix with item spawn/drop event
            } else if(event instanceof GameEvents.HitpointsEvent) {
                /*if(event.getAmount() < 0) {
                    getScrollingText(event.getAmount(), x, y, DAMAGE_COLORS[event.getDamageType()] || 'green', DAMAGE_OUTLINE_COLORS[event.getDamageType()] || 'green')
                        .appendTo(grid.children[0]);
                }*/
            } else if(event instanceof GameEvents.TakeItemEvent) {
                this.updateItems();
            }
            this.scrollToPlayer();
            this.updateRangeIndicator();
            this.updateSelectedTileIndicator();
            renderer.render(stage);
        });

        renderer.render(stage);
    }

    populateStage(stage) {
        this.populateSprites(stage);
        this.updateCreatureLocations();
        this.updateItems();
        this.updateVision();
        this._renderer.render(stage);
    }

    populateSprites(stage) {
        const self = this;
        while(stage.children.length) stage.removeChild(stage.children[0]);

        const dungeon = this._sharedData.getDungeon();
        const tileContainers = this._tileContainers = new Array(dungeon.getWidth()).fill(0).map(()=>[]);
        const entitySprites = this._entiteSprites;

        dungeon.forEachTile(function(tile, x, y) {
            const tileContainer = getTileContainer(tile);
            tileContainer.x = x * (TILE_WIDTH + GAP_WIDTH);
            tileContainer.y = y * (TILE_WIDTH + GAP_WIDTH);
            tileContainers[x][y] = tileContainer;

            tileContainer.children[0].on('click', function(event) {
                self._clickHanders.forEach(function(handler) {
                    handler(x, y);
                });
            }).on('mouseover', function(event) {
                setTimeout(function() { // Ensure mouseout fires first
                    self._mouseOverHandlers.forEach(function(handler) {
                        handler(x, y);
                    });
                });
            }).on('mouseout', function(event) {
                self._mouseOutHandlers.forEach(function(handler) {
                    handler(x, y);
                });
            });

            stage.addChild(tileContainer);
        });
    }

    updateItems() {
        const dungeon = this._sharedData.getDungeon();
        const tileContainers = this._tileContainers;
        dungeon.forEachTile(function(tile, x, y) {
            const tileContainer = tileContainers[x][y];
            const itemsContainer = tileContainer.children[1];
            while(itemsContainer.children.length) itemsContainer.removeChildAt(0);
            tile.getItems().forEach(function(item) {
                itemsContainer.addChild(setDefaultSpriteProps(new PIXI.Sprite(PIXI.utils.TextureCache[item.constructor.name])));
            })
        });
    }

    updateVision() {
        const dungeon = this._sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        const tileContainers = this._tileContainers;
        if(player) {
            dungeon.forEachTile(function(tile, x, y) {
                const creature = tile.getCreature();
                const tileContainer = tileContainers[x][y];
                const [tileSprite, itemContainer, creatureContainer] = tileContainer.children;
                const playerCanSeeTile = player.canSee(dungeon, tile);
                if(playerCanSeeTile) {
                    tileContainer.alpha = 1;
                } else if(player.hasSeen(tile)) {
                    tileContainer.alpha = .5;
                } else {
                    tileContainer.alpha = 0;
                }
                itemContainer.visible = creatureContainer.visible = playerCanSeeTile;
            });
        }
    }

    updateCreatureLocations() {
        const dungeon = this._sharedData.getDungeon();
        const entitySprites = this._entiteSprites; // TODO: Getter
        const tileContainers = this._tileContainers;
        dungeon.getCreatures().forEach(function(creature) {
            const sprite = entitySprites[creature.getId()] ||
                    (entitySprites[creature.getId()] = setDefaultSpriteProps(getCreatureSprite(creature)));
            if(sprite.parent) {
                sprite.parent.removeChild(sprite);
            }
            if(!creature.isDead()) {
                const tile = dungeon.getTile(creature);
                tileContainers[tile.getX()][tile.getY()].children[2].addChild(sprite);
            }
        })
    }

    updateRangeIndicator() {
        let rangeIndicator = this._rangeIndicator;
        if(rangeIndicator && rangeIndicator.parent) {
            rangeIndicator.parent.removeChild(rangeIndicator); // TODO: Is there a remove self function?
        }

        const sharedData = this._sharedData;
        const dungeon = sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        let rangedAttack;
        if(typeof sharedData.getTargettedAbility() === 'number') {
            rangedAttack = player.getAbility(sharedData.getTargettedAbility());
        } else if(typeof sharedData.getTargettedItem() === 'number') {
            rangedAttack = player.getInventory().getItem(sharedData.getTargettedItem());
        } else {
            rangedAttack = player.getRangedWeapon();
        }
        if(!rangedAttack) {
            return;
        }

        rangeIndicator = this._rangeIndicator = new PIXI.Graphics();

        let color;
        if(typeof rangedAttack.isMovementAbility === 'function') {
            color = 0x9400D3;
        } else if(rangedAttack.isTargetted()) {
            color = 0x7F00FF;
        } else {
            color = 0x46465a;
        }

        const playerTile = dungeon.getTile(player);
        rangeIndicator.lineStyle(1, color, 1);
        rangeIndicator.drawCircle(
            (playerTile.getX() + .5) * TILE_WIDTH,
            (playerTile.getY() + .5) * TILE_WIDTH,
            rangedAttack.getRange() * TILE_WIDTH
        );

        this._stage.addChild(rangeIndicator);
    }

    updateSelectedTileIndicator() {
        this._indicators.forEach((indicator) => {
            if(indicator.parent) {
                indicator.parent.removeChild(indicator);
            }
        });

        const sharedData = this._sharedData;
        const dungeon = sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        const playerLocation = dungeon.getTile(player);

        this._indicators = [
            sharedData.getHoverTile(),
            sharedData.getFocusTile()
        ].filter(Boolean).map((tile) => {
            const x = tile.getX();
            const y = tile.getY();
            const color = getTileColor(sharedData, x, y);
            const indicator = getIndicator(x, y, color);
            this._stage.addChild(indicator);
            return indicator;
        });
    }

    scrollToPlayer() {
        const sharedData = this._sharedData;
        const stage = this._stage;
        const dungeon = sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        const tile = dungeon.getTile(player);

        const canvasWidth = this.getDom().firstChild.width;
        const canvasHeight = this.getDom().firstChild.height;

        const cellDimension = TILE_WIDTH;
        const halfDimension = TILE_WIDTH / 2;
        const playerOffsetX = tile.getX() * cellDimension + halfDimension;
        const playerOffsetY = tile.getY() * cellDimension + halfDimension;

        stage.x = (canvasWidth / 2) - playerOffsetX;
        stage.y = (canvasHeight / 2) - playerOffsetY;
    }

    getDom() {
        return this._canvasContainer;
    }

    onClick(handler) {
        if(typeof handler !== 'function') {
            throw new Error('Handler must be a function');
        }
        this._clickHanders.push(handler);
    }

    onMouseOver(handler) {
        if(typeof handler !== 'function') {
            throw new Error('Handler must be a function');
        }
        this._mouseOverHandlers.push(handler);
    }

    onMouseOut(handler) {
        if(typeof handler !== 'function') {
            throw new Error('Handler must be a function');
        }
        this._mouseOutHandlers.push(handler);
    }

    // TODO: HP bar
    // TODO: Cellphone zap
    // TODO: Projectile animations
    // TODO: Show buffs
    // TODO: Item drop event
    // TODO: Tooltips
}
<commit_msg>Comment<commit_after>import Dungeon from '../dungeons/Dungeon.js';
import GameEvent from '../events/GameEvent.js';
import GameEvents from '../events/GameEvents.js';

import DungeonTooltips from './DungeonTooltips.js';

import DamageTypes from '../entities/DamageTypes.js';

import SharedUIDataController from '../controllers/SharedUIDataController.js';

import Moves from '../entities/creatures/moves/Moves.js';

const PIXI = require('pixi.js');
const TextureCache = (PIXI.utils.TextureCache);
const Sprite = PIXI.Sprite;

const TILE_WIDTH = 50;
const GAP_WIDTH = 0;

const DAMAGE_COLORS = {
    [DamageTypes.MELEE_PHYSICAL]: 'darkred',
    [DamageTypes.RANGED_PHYSICAL]: 'darkred',
    [DamageTypes.FIRE]: 'orange',
    [DamageTypes.COLD]: 'darkblue',
    [DamageTypes.ELECTRICAL]: 'yellow',
    [DamageTypes.ENERGY]: 'white',
    [DamageTypes.POISON]: 'emerald'
};

const DAMAGE_OUTLINE_COLORS = {
    [DamageTypes.MELEE_PHYSICAL]: 'pink',
    [DamageTypes.RANGED_PHYSICAL]: 'pink',
    [DamageTypes.FIRE]: 'darkred',
    [DamageTypes.COLD]: 'skyblue',
    [DamageTypes.ELECTRICAL]: 'orange',
    [DamageTypes.ENERGY]: 'yellow',
    [DamageTypes.POISON]: 'darkgreen'
};

const NEUTRAL_COLOR = 0x46465a;
const ATTACK_MOVE_COLOR = 0x8b0000;
const ITEM_MOVE_COLOR = 0x7F00FF;
const ABILITY_MOVE_COLOR = 0x9400D3;

function setDefaultSpriteProps(sprite) {
    sprite.x = 0;
    sprite.y = 0;
    sprite.width = TILE_WIDTH;
    sprite.height = TILE_WIDTH;
    return sprite;
}

function getSpriteStack(spriteNames) {
    const group = new PIXI.Container();
    spriteNames.forEach(function(spriteName) {
        group.addChild(
            setDefaultSpriteProps(
                new Sprite(TextureCache[spriteName])
            )
        );
    });
    return group;
}

function getContainerWrapping(obj) {
    const group = new PIXI.Container();
    group.addChild(obj);
    return group;
}

function getTileSprite(tile) {
    let tileDisp;
    if(tile.constructor.name === 'DoorTile') {
        if(tile.isOpen()) {
            tileDisp = getSpriteStack(['Tile', 'DoorOpen']);
        } else {
            tileDisp = new Sprite(TextureCache['DoorClosed']);
        }
    } else if(tile.constructor.name === 'EntranceTile') {
        tileDisp = getSpriteStack(['Tile', 'Ladder']);
    } else if(tile.constructor.name === 'PillarTile') {
        tileDisp = getSpriteStack(['Tile', 'Pillar']);
    } else {
        const texture = TextureCache[tile.constructor.name] || TextureCache['ThisIsAThing'];
        tileDisp = new Sprite(texture);
    }
    return getContainerWrapping(tileDisp);
}

function getCreatureSprite(creature) {
    return getContainerWrapping(
        new PIXI.Sprite(PIXI.utils.TextureCache[creature.constructor.name])
    );
}

function getTileColor(sharedData, x, y) {
    const dungeon = sharedData.getDungeon();
    const player = dungeon.getPlayableCharacter();
    const playerLocation = dungeon.getTile(player);

    switch(sharedData.getMode()) {
        case SharedUIDataController.EXAMINE_MODE:
            return NEUTRAL_COLOR;
        case SharedUIDataController.ATTACK_MODE:
            return new Moves.AttackMove(playerLocation, x, y)
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ATTACK_MOVE_COLOR;
        case SharedUIDataController.TARGETTED_ABILITY_MODE:
            const abilityIndex = sharedData.getTargettedAbility();
            return new Moves.UseAbilityMove(playerLocation, abilityIndex, x, y)
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ABILITY_MOVE_COLOR;
        case SharedUIDataController.TARGETTED_ITEM_MODE:
            const itemIndex = sharedData.getTargettedItem();
            return new Moves.UseItemMove(playerLocation, itemIndex, dungeon.getTile(x, y))
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ITEM_MOVE_COLOR;
        case SharedUIDataController.NEUTRAL_MODE:
            return new Moves.AttackMove(playerLocation, x, y)
                .getReasonIllegal(dungeon, player) ?
                NEUTRAL_COLOR : ATTACK_MOVE_COLOR;
        default:
            throw new Error('This should never happen');
    }
}

function getIndicator(x, y, color) {
    const indicator = new PIXI.Graphics();
    indicator.lineStyle(1, color);
    indicator.drawRect(
        x * (TILE_WIDTH + GAP_WIDTH) + 1, y * (TILE_WIDTH + GAP_WIDTH),
        TILE_WIDTH + GAP_WIDTH - 1, TILE_WIDTH + GAP_WIDTH - 1
    );
    return indicator;
}

function getTileContainer(tile) {
    const tileContainer = new PIXI.Container(); // TODO: Look at ParticleContainer
    const tileSprite = getTileSprite(tile);
    const x = tile.getX();
    const y = tile.getY();

    tileSprite.x = 0;
    tileSprite.y = 0;
    tileSprite.width = TILE_WIDTH;
    tileSprite.height = TILE_WIDTH;
    tileSprite.interactive = true;

    tileContainer.addChild(tileSprite);
    tileContainer.addChild(new PIXI.Container());
    tileContainer.addChild(new PIXI.Container());

    return tileContainer;
}

export default class PixiDungeonView {
    constructor(sharedData) {
        if(!(sharedData instanceof SharedUIDataController)) {
            throw new Error('First parameter must be a SharedUIDataController');
        }
        const self = this;
        this._sharedData = sharedData;
        this._indicators = [];
        this._clickHanders = [];
        this._mouseOverHandlers = [];
        this._mouseOutHandlers = [];

        const canvasContainer = this._canvasContainer = document.createElement('div');
        canvasContainer.classList.add('canvas-container');

        const renderer = this._renderer = PIXI.autoDetectRenderer();
        canvasContainer.appendChild(renderer.view);

        PIXI.loader.add('images/spritesheet.json').load(() => this.init());

        function resize() {
            const {clientWidth, clientHeight} = canvasContainer;
            renderer.resize(clientWidth, clientHeight);
        }

        setTimeout(resize);
        window.addEventListener('resize', resize);
    }

    init() {
        const stage = this._stage = new PIXI.Container();

        const entitySprites = this._entiteSprites = {};

        const sharedData = this._sharedData;
        const renderer = this._renderer;

        sharedData.addObserver((event) => {
            if(event instanceof Dungeon){
                this.populateStage(stage);
                document.querySelector('section.game').focus(); //TODO: Make canvas container focusable insted?
            }
        });

        sharedData.addObserver((event) => {
            const dungeon = sharedData.getDungeon();
            if(event instanceof GameEvents.MoveEvent || event instanceof GameEvents.PositionChangeEvent || event instanceof GameEvents.SpawnEvent) {
                this.updateVision();
                this.updateCreatureLocations();
                this.scrollToPlayer();
            } else if(event instanceof GameEvents.DeathEvent) {
                const creature = event.getCreature();
                const tile = dungeon.getTile(creature);
                const x = tile.getX();
                const y = tile.getY();
                const tileContainer = this._tileContainers[x][y].children[2].removeChildAt(0);
                //setTimeout(updateItems); // TODO: Fix with item spawn/drop event
            } else if(event instanceof GameEvents.HitpointsEvent) {
                /*if(event.getAmount() < 0) {
                    getScrollingText(event.getAmount(), x, y, DAMAGE_COLORS[event.getDamageType()] || 'green', DAMAGE_OUTLINE_COLORS[event.getDamageType()] || 'green')
                        .appendTo(grid.children[0]);
                }*/
            } else if(event instanceof GameEvents.TakeItemEvent) {
                this.updateItems();
            }
            this.scrollToPlayer();
            this.updateRangeIndicator();
            this.updateSelectedTileIndicator();
            renderer.render(stage);
        });

        renderer.render(stage);
    }

    populateStage(stage) {
        this.populateSprites(stage);
        this.updateCreatureLocations();
        this.updateItems();
        this.updateVision();
        this._renderer.render(stage);
    }

    populateSprites(stage) {
        const self = this;
        while(stage.children.length) stage.removeChild(stage.children[0]);

        const dungeon = this._sharedData.getDungeon();
        const tileContainers = this._tileContainers = new Array(dungeon.getWidth()).fill(0).map(()=>[]);
        const entitySprites = this._entiteSprites;

        dungeon.forEachTile(function(tile, x, y) {
            const tileContainer = getTileContainer(tile);
            tileContainer.x = x * (TILE_WIDTH + GAP_WIDTH);
            tileContainer.y = y * (TILE_WIDTH + GAP_WIDTH);
            tileContainers[x][y] = tileContainer;

            tileContainer.children[0]
            .on('click', function(event) {
                self._clickHanders.forEach(function(handler) {
                    handler(x, y);
                });
            }).on('tap', function(event) {
                self._clickHanders.forEach(function(handler) {
                    handler(x, y);
                });
            }).on('mouseover', function(event) {
                setTimeout(function() { // Ensure mouseout fires first
                    self._mouseOverHandlers.forEach(function(handler) {
                        handler(x, y);
                    });
                });
            }).on('mouseout', function(event) {
                self._mouseOutHandlers.forEach(function(handler) {
                    handler(x, y);
                });
            });

            stage.addChild(tileContainer);
        });
    }

    updateItems() {
        const dungeon = this._sharedData.getDungeon();
        const tileContainers = this._tileContainers;
        dungeon.forEachTile(function(tile, x, y) {
            const tileContainer = tileContainers[x][y];
            const itemsContainer = tileContainer.children[1];
            while(itemsContainer.children.length) itemsContainer.removeChildAt(0);
            tile.getItems().forEach(function(item) {
                itemsContainer.addChild(setDefaultSpriteProps(new PIXI.Sprite(PIXI.utils.TextureCache[item.constructor.name])));
            })
        });
    }

    updateVision() {
        const dungeon = this._sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        const tileContainers = this._tileContainers;
        if(player) {
            dungeon.forEachTile(function(tile, x, y) {
                const creature = tile.getCreature();
                const tileContainer = tileContainers[x][y];
                const [tileSprite, itemContainer, creatureContainer] = tileContainer.children;
                const playerCanSeeTile = player.canSee(dungeon, tile);
                if(playerCanSeeTile) {
                    tileContainer.alpha = 1;
                } else if(player.hasSeen(tile)) {
                    tileContainer.alpha = .5;
                } else {
                    tileContainer.alpha = 0;
                }
                itemContainer.visible = creatureContainer.visible = playerCanSeeTile;
            });
        }
    }

    updateCreatureLocations() {
        const dungeon = this._sharedData.getDungeon();
        const entitySprites = this._entiteSprites; // TODO: Getter
        const tileContainers = this._tileContainers;
        dungeon.getCreatures().forEach(function(creature) {
            const sprite = entitySprites[creature.getId()] ||
                    (entitySprites[creature.getId()] = setDefaultSpriteProps(getCreatureSprite(creature)));
            if(sprite.parent) {
                sprite.parent.removeChild(sprite);
            }
            if(!creature.isDead()) {
                const tile = dungeon.getTile(creature);
                tileContainers[tile.getX()][tile.getY()].children[2].addChild(sprite);
            }
        })
    }

    updateRangeIndicator() {
        let rangeIndicator = this._rangeIndicator;
        if(rangeIndicator && rangeIndicator.parent) {
            rangeIndicator.parent.removeChild(rangeIndicator); // TODO: Is there a remove self function?
        }

        const sharedData = this._sharedData;
        const dungeon = sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        let rangedAttack;
        if(typeof sharedData.getTargettedAbility() === 'number') {
            rangedAttack = player.getAbility(sharedData.getTargettedAbility());
        } else if(typeof sharedData.getTargettedItem() === 'number') {
            rangedAttack = player.getInventory().getItem(sharedData.getTargettedItem());
        } else {
            rangedAttack = player.getRangedWeapon();
        }
        if(!rangedAttack) {
            return;
        }

        rangeIndicator = this._rangeIndicator = new PIXI.Graphics();

        let color;
        if(typeof rangedAttack.isMovementAbility === 'function') {
            color = 0x9400D3;
        } else if(rangedAttack.isTargetted()) {
            color = 0x7F00FF;
        } else {
            color = 0x46465a;
        }

        const playerTile = dungeon.getTile(player);
        rangeIndicator.lineStyle(1, color, 1);
        rangeIndicator.drawCircle(
            (playerTile.getX() + .5) * TILE_WIDTH,
            (playerTile.getY() + .5) * TILE_WIDTH,
            rangedAttack.getRange() * TILE_WIDTH
        );

        this._stage.addChild(rangeIndicator);
    }

    updateSelectedTileIndicator() {
        this._indicators.forEach((indicator) => {
            if(indicator.parent) {
                indicator.parent.removeChild(indicator);
            }
        });

        const sharedData = this._sharedData;
        const dungeon = sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        const playerLocation = dungeon.getTile(player);

        this._indicators = [
            sharedData.getHoverTile(),
            sharedData.getFocusTile()
        ].filter(Boolean).map((tile) => {
            const x = tile.getX();
            const y = tile.getY();
            const color = getTileColor(sharedData, x, y);
            const indicator = getIndicator(x, y, color);
            this._stage.addChild(indicator);
            return indicator;
        });
    }

    scrollToPlayer() {
        const sharedData = this._sharedData;
        const stage = this._stage;
        const dungeon = sharedData.getDungeon();
        const player = dungeon.getPlayableCharacter();
        const tile = dungeon.getTile(player);

        const canvasWidth = this.getDom().firstChild.width;
        const canvasHeight = this.getDom().firstChild.height;

        const cellDimension = TILE_WIDTH;
        const halfDimension = TILE_WIDTH / 2;
        const playerOffsetX = tile.getX() * cellDimension + halfDimension;
        const playerOffsetY = tile.getY() * cellDimension + halfDimension;

        stage.x = (canvasWidth / 2) - playerOffsetX;
        stage.y = (canvasHeight / 2) - playerOffsetY;
    }

    getDom() {
        return this._canvasContainer;
    }

    onClick(handler) {
        if(typeof handler !== 'function') {
            throw new Error('Handler must be a function');
        }
        this._clickHanders.push(handler);
    }

    onMouseOver(handler) {
        if(typeof handler !== 'function') {
            throw new Error('Handler must be a function');
        }
        this._mouseOverHandlers.push(handler);
    }

    onMouseOut(handler) {
        if(typeof handler !== 'function') {
            throw new Error('Handler must be a function');
        }
        this._mouseOutHandlers.push(handler);
    }

    // TODO: HP bar
    // TODO: Cellphone zap
    // TODO: Projectile animations
    // TODO: Show buffs
    // TODO: Item drop event
    // TODO: Tooltips
    // TODO: Blue outline for movement move
}
<|endoftext|>


<commit_before>/**
 * Created by blackcat on 2/1/15.
 */
'use strict';

/**
 * Service for providing access the backend API via HTTP and WebSockets.
 */

angular.module('koan.common').factory('media', function ($rootScope, $http, $window, $q) {

    var token = ($window.sessionStorage.token || $window.localStorage.token),
        headers = {Authorization: 'Bearer ' + token},
        // TEMPORARY @TODO handle as must media as possible and drop this shit.
        iframeRegex = /<iframe.*<\/iframe>/i,
        youtubeRegex = /https?:\/\/(www\.)?youtu.*\/watch\?v=(.{11})/i,
        soundCloudRegex = /https?:\/\/soundcloud\.com\/\S*/gi,
        //bandCampRegex = /https?:\/\/(.*)\.bandcamp.com\/\S*/gi,
        urlRegex = /https?:\/\/\S*/i,
        media = {events: {}};

    media.platforms = {
        get: function (text) {
            var deferred = $q.defer();
            var regExp = youtubeRegex;
            var matchIframe = text.match(iframeRegex);
            var match = text.match(regExp);
            var matchSC = text.match(soundCloudRegex);
            //var matchBC = text.match(bandCampRegex);
            var matchUrl = text.match(urlRegex);

            if(matchIframe) {
                deferred.resolve({
                    text: text
                });
            } else if (match && match[2].length == 11) { // youtube
                $http({
                    method: 'GET',
                    url: 'https://gdata.youtube.com/feeds/api/videos/' + match[2] + '?v=2&alt=jsonc'
                })
                .success(function (videoInfosJson) {
                    // replace link with embeded player and return updated text
                    deferred.resolve({
                        text: text.replace(youtubeRegex, getYoutubeEmbededPlayer(match[2]))
                    });
                })
                .error(function () {
                    // do nothing
                    deferred.reject('error');
                });
            } else if(matchSC) { // soundcloud
                $http({
                    method: 'GET',
                    url: 'http://soundcloud.com/oembed?format=json&url=' + matchSC[0] + '&iframe=true'
                })
                .success(function (responseJson) {
                    deferred.resolve({
                        text: text.replace(soundCloudRegex, responseJson.html)
                    });
                })
                .error(function () {
                    // do nothing
                    deferred.reject('error');
                });
            } else if(matchUrl){
                deferred.resolve({
                    text: text.replace(matchUrl, '' + matchUrl[0] + '')
                });
            } else {
                deferred.resolve({
                    text: text
                });
            }
            return deferred.promise;
        }
    };

    function getYoutubeEmbededPlayer(videoId){
        return '';
    }

    return media;
});<commit_msg>Fixes #10 fixed youtube url regex to accept short urls like youtu.be/...<commit_after>/**
 * Created by blackcat on 2/1/15.
 */
'use strict';

/**
 * Service for providing access the backend API via HTTP and WebSockets.
 */

angular.module('koan.common').factory('media', function ($rootScope, $http, $window, $q) {

    var token = ($window.sessionStorage.token || $window.localStorage.token),
        headers = {Authorization: 'Bearer ' + token},
        // TEMPORARY @TODO handle as must media as possible and drop this shit.
        iframeRegex = /<iframe.*<\/iframe>/i,
        youtubeRegex = /https?:\/\/(www\.)?youtu.*\/(watch\?v=)?(.{11})/i,
        soundCloudRegex = /https?:\/\/soundcloud\.com\/\S*/gi,
        //bandCampRegex = /https?:\/\/(.*)\.bandcamp.com\/\S*/gi,
        urlRegex = /https?:\/\/\S*/i,
        media = {events: {}};

    media.platforms = {
        get: function (text) {
            var deferred = $q.defer();
            var matchIframe = text.match(iframeRegex);
            var matchYT = text.match(youtubeRegex);
            var matchSC = text.match(soundCloudRegex);
            //var matchBC = text.match(bandCampRegex);
            var matchUrl = text.match(urlRegex);

            if(matchIframe) {
                deferred.resolve({
                    text: text
                });
            } else if (matchYT && matchYT[3].length == 11) { // youtube
                $http({
                    method: 'GET',
                    url: 'https://gdata.youtube.com/feeds/api/videos/' + matchYT[3] + '?v=2&alt=jsonc'
                })
                .success(function (videoInfosJson) {
                    // replace link with embeded player and return updated text
                    deferred.resolve({
                        text: text.replace(youtubeRegex, getYoutubeEmbededPlayer(matchYT[3]))
                    });
                })
                .error(function () {
                    // do nothing
                    deferred.reject('error');
                });
            } else if(matchSC) { // soundcloud
                $http({
                    method: 'GET',
                    url: 'http://soundcloud.com/oembed?format=json&url=' + matchSC[0] + '&iframe=true'
                })
                .success(function (responseJson) {
                    deferred.resolve({
                        text: text.replace(soundCloudRegex, responseJson.html)
                    });
                })
                .error(function () {
                    // do nothing
                    deferred.reject('error');
                });
            } else if(matchUrl){
                deferred.resolve({
                    text: text.replace(matchUrl, '' + matchUrl[0] + '')
                });
            } else {
                deferred.resolve({
                    text: text
                });
            }
            return deferred.promise;
        }
    };

    function getYoutubeEmbededPlayer(videoId){
        return '';
    }

    return media;
});<|endoftext|>


<commit_before>/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global self, importScripts, require */

importScripts("thirdparty/requirejs/require.js");

(function () {
    "use strict";

    var HintUtils;
    var Tern;
    require(["./HintUtils"], function (hintUtils) {
        HintUtils = hintUtils;
        var ternRequire = require.config({baseUrl: "./thirdparty"});
        ternRequire(["tern/lib/tern", "tern/plugin/requirejs"], function (tern, requirejs) {
            Tern = tern;
        });
    });

    var ternServer  = null;

    // Save the tern callbacks for when we get the contents of the file
    var fileCallBacks = {};

    /**
     * Provide the contents of the requested file to tern
     * @param {string} name - the name of the file
     * @param {Function} next - the function to call with the text of the file
     *  once it has been read in.
     */
    function getFile(name, next) {
        // save the callback
        fileCallBacks[name] = next;

        // post a message back to the main thread to get the file contents
        self.postMessage({
            type: HintUtils.TERN_GET_FILE_MSG,
            file: name
        });
    }

    /**
     * Handle a response from the main thread providing the contents of a file
     * @param {string} file - the name of the file
     * @param {string} text - the contents of the file
     */
    function handleGetFile(file, text) {
        var next = fileCallBacks[file];
        if (next) {
            next(null, text);
        }
        delete fileCallBacks[file];
    }

    /**
     * Create a new tern server.
     *
     * @param {Object} env - an Object with the environment, as read in from
     *  the json files in thirdparty/tern/defs
     * @param {string} dir - the current directory
     * @param {Array.<string>} files - a list of filenames tern should be aware of
     */
    function initTernServer(env, dir, files) {
        var ternOptions = {
            defs: env,
            async: true,
            getFile: getFile,
            plugins: {requirejs: {}}
        };
        ternServer = new Tern.Server(ternOptions);

        files.forEach(function (file) {
            ternServer.addFile(file);
        });

    }

    /**
     * Build an object that can be used as a request to tern
     * @param {string} dir - the current directory
     * @param {string} file - the filename the request is in
     * @param {string} query - the type of request being made
     * @param {number} offset - the offset in the file the request is at
     * @param {string} text - the text of the file
     */
    function buildRequest(dir, file, query, offset, text) {
        query = {type: query};
        query.start = offset;
        query.end = offset;
        query.file = file;
        query.filter = false;
        query.sort = false;
        query.depths = true;
        query.guess = true;
        query.origins = true;
        query.expandWordForward = true;

        var request = {query: query, files: [], offset: offset};
        request.files.push({type: "full", name: file, text: text});

        return request;
    }

    /**
     * Send a log message back from the worker to the main thread
     *
     * @param {string} msg - the log message
     */
    function _log(msg) {
        self.postMessage({log: msg });
    }

    /**
     * Get definition location
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {number} offset   - the offset into the file for cursor
     * @param {string} text     - the text of the file
     */
    function getJumptoDef(dir, file, offset, text) {

        var request = buildRequest(dir, file, "definition", offset, text);
        request.query.lineCharPositions = true;
        ternServer.request(request, function (error, data) {
            if (error) {
                _log("Error returned from Tern 'definition' request: " + error);
                self.postMessage({type: HintUtils.TERN_JUMPTODEF_MSG});
                return;
            }

            // Post a message back to the main thread with the definition
            self.postMessage({type: HintUtils.TERN_JUMPTODEF_MSG,
                              file: file,
                              resultFile: data.file,
                              offset: offset,
                              start: data.start,
                              end: data.end
                             });
        });
    }

    /**
     * Get the completions for the given offset
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {number} offset   - the offset into the file where we want completions for
     * @param {string} text     - the text of the file
     */
    function getTernHints(dir, file, offset, text) {

        var request = buildRequest(dir, file, "completions", offset, text),
            i;
        //_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
        ternServer.request(request, function (error, data) {
            var completions = [];
            if (error) {
                _log("Error returned from Tern 'completions' request: " + error);
            } else {
                //_log("found " + data.completions.length + " for " + file + "@" + offset);
                for (i = 0; i < data.completions.length; ++i) {
                    var completion = data.completions[i];
                    completions.push({value: completion.name, type: completion.type, depth: completion.depth,
                        guess: completion.guess, origin: completion.origin});
                }
            }

            // Post a message back to the main thread with the completions
            self.postMessage({type: HintUtils.TERN_COMPLETIONS_MSG,
                              dir: dir,
                              file: file,
                              offset: offset,
                              completions: completions
                             });
        });
    }

    /**
     * Get all the known properties for guessing.
     *
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {string} text     - the text of the file
     */
    function handleGetProperties(dir, file, offset, text) {

        var request = buildRequest(dir, file, "properties", undefined, text),
            i;
        //_log("request " + request.type + dir + " " + file);
        ternServer.request(request, function (error, data) {
            var properties = [];
            if (error) {
                _log("Error returned from Tern 'properties' request: " + error);
            } else {
                //_log("completions = " + data.completions.length);
                for (i = 0; i < data.completions.length; ++i) {
                    var property = data.completions[i];
                    properties.push({value: property, guess: true});
                }
            }

            // Post a message back to the main thread with the completions
            self.postMessage({type: HintUtils.TERN_GET_PROPERTIES_MSG,
                              dir: dir,
                              file: file,
                              offset: offset,
                              properties: properties
                });
        });
    }

    /**
     * Get the function type for the given offset
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {number} offset   - the offset into the file where we want completions for
     * @param {string} text     - the text of the file
     */
    function handleFunctionType(dir, file, pos, offset, text) {

        var request = buildRequest(dir, file, "type", pos, text);

        request.preferFunction = true;

        //_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
        ternServer.request(request, function (error, data) {
            var fnType = "";
            if (error) {
                _log("Error returned from Tern 'type' request: " + error);
            } else {
                fnType = data.type;
            }

            // Post a message back to the main thread with the completions
            self.postMessage({type: HintUtils.TERN_CALLED_FUNC_TYPE_MSG,
                              dir: dir,
                              file: file,
                              offset: offset,
                              fnType: fnType
                             });
        });
    }

    self.addEventListener("message", function (e) {
        var dir, file, text, offset,
            request = e.data,
            type = request.type;

        if (type === HintUtils.TERN_INIT_MSG) {

            dir         = request.dir;
            var env     = request.env,
                files   = request.files;
            initTernServer(env, dir, files);

        } else if (type === HintUtils.TERN_COMPLETIONS_MSG) {
            dir = request.dir;
            file = request.file;
            text    = request.text;
            offset  = request.offset;
            getTernHints(dir, file, offset, text);
        } else if (type === HintUtils.TERN_GET_PROPERTIES_MSG) {
            file    = request.file;
            dir     = request.dir;
            text    = request.text;
            offset  = request.offset;
            handleGetProperties(dir, file, offset, text);
        } else if (type === HintUtils.TERN_GET_FILE_MSG) {
            file = request.file;
            text = request.text;
            handleGetFile(file, text);
        } else if (type === HintUtils.TERN_CALLED_FUNC_TYPE_MSG) {
            dir     = request.dir;
            file    = request.file;
            text    = request.text;
            offset  = request.offset;
            var pos = request.pos;
            handleFunctionType(dir, file, pos, offset, text);
        } else if (type === HintUtils.TERN_JUMPTODEF_MSG) {
            file    = request.file;
            dir     = request.dir;
            text    = request.text;
            offset  = request.offset;
            getJumptoDef(dir, file, offset, text);
        } else {
            _log("Unknown message: " + JSON.stringify(request));
        }
    });

}());
<commit_msg>Fix expandWordForward should be false.<commit_after>/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global self, importScripts, require */

importScripts("thirdparty/requirejs/require.js");

(function () {
    "use strict";

    var HintUtils;
    var Tern;
    require(["./HintUtils"], function (hintUtils) {
        HintUtils = hintUtils;
        var ternRequire = require.config({baseUrl: "./thirdparty"});
        ternRequire(["tern/lib/tern", "tern/plugin/requirejs"], function (tern, requirejs) {
            Tern = tern;
        });
    });

    var ternServer  = null;

    // Save the tern callbacks for when we get the contents of the file
    var fileCallBacks = {};

    /**
     * Provide the contents of the requested file to tern
     * @param {string} name - the name of the file
     * @param {Function} next - the function to call with the text of the file
     *  once it has been read in.
     */
    function getFile(name, next) {
        // save the callback
        fileCallBacks[name] = next;

        // post a message back to the main thread to get the file contents
        self.postMessage({
            type: HintUtils.TERN_GET_FILE_MSG,
            file: name
        });
    }

    /**
     * Handle a response from the main thread providing the contents of a file
     * @param {string} file - the name of the file
     * @param {string} text - the contents of the file
     */
    function handleGetFile(file, text) {
        var next = fileCallBacks[file];
        if (next) {
            next(null, text);
        }
        delete fileCallBacks[file];
    }

    /**
     * Create a new tern server.
     *
     * @param {Object} env - an Object with the environment, as read in from
     *  the json files in thirdparty/tern/defs
     * @param {string} dir - the current directory
     * @param {Array.<string>} files - a list of filenames tern should be aware of
     */
    function initTernServer(env, dir, files) {
        var ternOptions = {
            defs: env,
            async: true,
            getFile: getFile,
            plugins: {requirejs: {}}
        };
        ternServer = new Tern.Server(ternOptions);

        files.forEach(function (file) {
            ternServer.addFile(file);
        });

    }

    /**
     * Build an object that can be used as a request to tern
     * @param {string} dir - the current directory
     * @param {string} file - the filename the request is in
     * @param {string} query - the type of request being made
     * @param {number} offset - the offset in the file the request is at
     * @param {string} text - the text of the file
     */
    function buildRequest(dir, file, query, offset, text) {
        query = {type: query};
        query.start = offset;
        query.end = offset;
        query.file = file;
        query.filter = false;
        query.sort = false;
        query.depths = true;
        query.guess = true;
        query.origins = true;
        query.expandWordForward = false;

        var request = {query: query, files: [], offset: offset};
        request.files.push({type: "full", name: file, text: text});

        return request;
    }

    /**
     * Send a log message back from the worker to the main thread
     *
     * @param {string} msg - the log message
     */
    function _log(msg) {
        self.postMessage({log: msg });
    }

    /**
     * Get definition location
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {number} offset   - the offset into the file for cursor
     * @param {string} text     - the text of the file
     */
    function getJumptoDef(dir, file, offset, text) {

        var request = buildRequest(dir, file, "definition", offset, text);
        request.query.lineCharPositions = true;
        ternServer.request(request, function (error, data) {
            if (error) {
                _log("Error returned from Tern 'definition' request: " + error);
                self.postMessage({type: HintUtils.TERN_JUMPTODEF_MSG});
                return;
            }

            // Post a message back to the main thread with the definition
            self.postMessage({type: HintUtils.TERN_JUMPTODEF_MSG,
                              file: file,
                              resultFile: data.file,
                              offset: offset,
                              start: data.start,
                              end: data.end
                             });
        });
    }

    /**
     * Get the completions for the given offset
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {number} offset   - the offset into the file where we want completions for
     * @param {string} text     - the text of the file
     */
    function getTernHints(dir, file, offset, text) {

        var request = buildRequest(dir, file, "completions", offset, text),
            i;
        //_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
        ternServer.request(request, function (error, data) {
            var completions = [];
            if (error) {
                _log("Error returned from Tern 'completions' request: " + error);
            } else {
                //_log("found " + data.completions.length + " for " + file + "@" + offset);
                for (i = 0; i < data.completions.length; ++i) {
                    var completion = data.completions[i];
                    completions.push({value: completion.name, type: completion.type, depth: completion.depth,
                        guess: completion.guess, origin: completion.origin});
                }
            }

            // Post a message back to the main thread with the completions
            self.postMessage({type: HintUtils.TERN_COMPLETIONS_MSG,
                              dir: dir,
                              file: file,
                              offset: offset,
                              completions: completions
                             });
        });
    }

    /**
     * Get all the known properties for guessing.
     *
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {string} text     - the text of the file
     */
    function handleGetProperties(dir, file, offset, text) {

        var request = buildRequest(dir, file, "properties", undefined, text),
            i;
        //_log("request " + request.type + dir + " " + file);
        ternServer.request(request, function (error, data) {
            var properties = [];
            if (error) {
                _log("Error returned from Tern 'properties' request: " + error);
            } else {
                //_log("completions = " + data.completions.length);
                for (i = 0; i < data.completions.length; ++i) {
                    var property = data.completions[i];
                    properties.push({value: property, guess: true});
                }
            }

            // Post a message back to the main thread with the completions
            self.postMessage({type: HintUtils.TERN_GET_PROPERTIES_MSG,
                              dir: dir,
                              file: file,
                              offset: offset,
                              properties: properties
                });
        });
    }

    /**
     * Get the function type for the given offset
     * @param {string} dir      - the directory
     * @param {string} file     - the file name
     * @param {number} offset   - the offset into the file where we want completions for
     * @param {string} text     - the text of the file
     */
    function handleFunctionType(dir, file, pos, offset, text) {

        var request = buildRequest(dir, file, "type", pos, text);

        request.preferFunction = true;

        //_log("request " + dir + " " + file + " " + offset /*+ " " + text */);
        ternServer.request(request, function (error, data) {
            var fnType = "";
            if (error) {
                _log("Error returned from Tern 'type' request: " + error);
            } else {
                fnType = data.type;
            }

            // Post a message back to the main thread with the completions
            self.postMessage({type: HintUtils.TERN_CALLED_FUNC_TYPE_MSG,
                              dir: dir,
                              file: file,
                              offset: offset,
                              fnType: fnType
                             });
        });
    }

    self.addEventListener("message", function (e) {
        var dir, file, text, offset,
            request = e.data,
            type = request.type;

        if (type === HintUtils.TERN_INIT_MSG) {

            dir         = request.dir;
            var env     = request.env,
                files   = request.files;
            initTernServer(env, dir, files);

        } else if (type === HintUtils.TERN_COMPLETIONS_MSG) {
            dir = request.dir;
            file = request.file;
            text    = request.text;
            offset  = request.offset;
            getTernHints(dir, file, offset, text);
        } else if (type === HintUtils.TERN_GET_PROPERTIES_MSG) {
            file    = request.file;
            dir     = request.dir;
            text    = request.text;
            offset  = request.offset;
            handleGetProperties(dir, file, offset, text);
        } else if (type === HintUtils.TERN_GET_FILE_MSG) {
            file = request.file;
            text = request.text;
            handleGetFile(file, text);
        } else if (type === HintUtils.TERN_CALLED_FUNC_TYPE_MSG) {
            dir     = request.dir;
            file    = request.file;
            text    = request.text;
            offset  = request.offset;
            var pos = request.pos;
            handleFunctionType(dir, file, pos, offset, text);
        } else if (type === HintUtils.TERN_JUMPTODEF_MSG) {
            file    = request.file;
            dir     = request.dir;
            text    = request.text;
            offset  = request.offset;
            getJumptoDef(dir, file, offset, text);
        } else {
            _log("Unknown message: " + JSON.stringify(request));
        }
    });

}());
<|endoftext|>