<commit_before>/* eslint-disable no-bitwise */
/* global BigInt */

import { deriveKeys, importKey, ratchet } from './crypto-utils';
import { isArrayEqual } from './utils';

// We use a ringbuffer of keys so we can change them and still decode packets that were
// encrypted with an old key. We use a size of 16 which corresponds to the four bits
// in the frame trailer.
const keyRingSize = 16;

// We copy the first bytes of the VP8 payload unencrypted.
// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
//   https://tools.ietf.org/html/rfc6386#section-9.1
// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
// instead of being unable to decode).
// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
//
// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
//   https://tools.ietf.org/html/rfc6716#section-3.1
const unencryptedBytes = {
    key: 10,
    delta: 3,
    undefined: 1 // frame.type is not set on audio
};

// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
// This follows the same principles as DTLS-SRTP.
const authenticationTagOptions = {
    name: 'HMAC',
    hash: 'SHA-256'
};
const digestLength = {
    key: 10,
    delta: 10,
    undefined: 4 // frame.type is not set on audio
};

// Maximum number of forward ratchets to attempt when the authentication
// tag on a remote packet does not match the current key.
const ratchetWindow = 8;

/**
 * Per-participant context holding the cryptographic keys and
 * encode/decode functions
 */
export class Context {
    /**
     * @param {string} id - local muc resourcepart
     */
    constructor(id) {
        // An array (ring) of keys that we use for sending and receiving.
        this._cryptoKeyRing = new Array(keyRingSize);

        // A pointer to the currently used key.
        this._currentKeyIndex = -1;

        // A per-sender counter that is used create the AES CTR.
        // Must be incremented on every frame that is sent, can be reset on
        // key changes.
        this._sendCount = BigInt(0); // eslint-disable-line new-cap

        this._id = id;
    }

    /**
     * Derives the different subkeys and starts using them for encryption or
     * decryption.
     * @param {Uint8Array|false} key bytes. Pass false to disable.
     * @param {Number} keyIndex
     */
    async setKey(keyBytes, keyIndex) {
        let newKey;

        if (keyBytes) {
            const material = await importKey(keyBytes);

            newKey = await deriveKeys(material);
        } else {
            newKey = false;
        }
        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
        this._setKeys(newKey);
    }

    /**
     * Sets a set of keys and resets the sendCount.
     * decryption.
     * @param {Object} keys set of keys.
     * @private
     */
    _setKeys(keys) {
        this._cryptoKeyRing[this._currentKeyIndex] = keys;
        this._sendCount = BigInt(0); // eslint-disable-line new-cap
    }

    /**
     * Function that will be injected in a stream and will encrypt the given encoded frames.
     *
     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
     * @param {TransformStreamDefaultController} controller - TransportStreamController.
     *
     * The packet format is a variant of
     *   https://tools.ietf.org/html/draft-omara-sframe-00
     * using a trailer instead of a header. One of the design goals was to not require
     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
     * solve this eventually). This also "hides" that a client is using E2EE a bit.
     *
     * Note that this operates on the full frame, i.e. for VP8 the data described in
     *   https://tools.ietf.org/html/rfc6386#section-9.1
     *
     * The VP8 payload descriptor described in
     *   https://tools.ietf.org/html/rfc7741#section-4.2
     * is part of the RTP packet and not part of the encoded frame and is therefore not
     * controllable by us. This is fine as the SFU keeps having access to it for routing.
     */
    encodeFunction(encodedFrame, controller) {
        const keyIndex = this._currentKeyIndex;

        if (this._cryptoKeyRing[keyIndex]) {
            this._sendCount++;

            // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);

            // Construct frame trailer. Similar to the frame header described in
            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
            // but we put it at the end.
            //                                             0 1 2 3 4 5 6 7
            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
            // payload  |    CTR... (length=LEN)          |S|LEN  |KID    |
            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
            const counter = new Uint8Array(16);
            const counterView = new DataView(counter.buffer);

            // The counter is encoded as a variable-length field.
            counterView.setBigUint64(8, this._sendCount);
            let counterLength = 8;

            for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
                if (counterView.getUint8(i) !== 0) {
                    break;
                }
            }

            const frameTrailer = new Uint8Array(counterLength + 1);

            frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));

            // Since we never send a counter of 0 we send counterLength - 1 on the wire.
            // This is different from the sframe draft, increases the key space and lets us
            // ignore the case of a zero-length counter at the receiver.
            frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);

            // XOR the counter with the saltKey to construct the AES CTR.
            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);

            for (let i = 0; i < counter.byteLength; i++) {
                counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
            }

            return crypto.subtle.encrypt({
                name: 'AES-CTR',
                counter,
                length: 64
            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
                unencryptedBytes[encodedFrame.type]))
            .then(cipherText => {
                const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
                    + digestLength[encodedFrame.type] + frameTrailer.byteLength);
                const newUint8 = new Uint8Array(newData);

                newUint8.set(frameHeader); // copy first bytes.
                newUint8.set(new Uint8Array(cipherText), unencryptedBytes[encodedFrame.type]); // add ciphertext.
                // Leave some space for the authentication tag. This is filled with 0s initially, similar to
                // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
                newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
                    + digestLength[encodedFrame.type]); // append trailer.

                return crypto.subtle.sign(authenticationTagOptions, this._cryptoKeyRing[keyIndex].authenticationKey,
                    new Uint8Array(newData)).then(authTag => {
                    // Set the truncated authentication tag.
                    newUint8.set(new Uint8Array(authTag, 0, digestLength[encodedFrame.type]),
                        unencryptedBytes[encodedFrame.type] + cipherText.byteLength);
                    encodedFrame.data = newData;

                    return controller.enqueue(encodedFrame);
                });
            }, e => {
                // TODO: surface this to the app.
                console.error(e);

                // We are not enqueuing the frame here on purpose.
            });
        }

        /* NOTE WELL:
         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
         * This is ok for demo purposes but should not be done once this becomes more relied upon.
         */
        controller.enqueue(encodedFrame);
    }

    /**
     * Function that will be injected in a stream and will decrypt the given encoded frames.
     *
     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
     * @param {TransformStreamDefaultController} controller - TransportStreamController.
     */
    async decodeFunction(encodedFrame, controller) {
        const data = new Uint8Array(encodedFrame.data);
        const keyIndex = data[encodedFrame.data.byteLength - 1] & 0xf; // lower four bits.

        if (this._cryptoKeyRing[keyIndex]) {
            const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
            const frameHeader = new Uint8Array(encodedFrame.data, 0, unencryptedBytes[encodedFrame.type]);

            // Extract the truncated authentication tag.
            const authTagOffset = encodedFrame.data.byteLength - (digestLength[encodedFrame.type]
                + counterLength + 1);
            const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
                + digestLength[encodedFrame.type]);

            // Set authentication tag bytes to 0.
            const zeros = new Uint8Array(digestLength[encodedFrame.type]);

            data.set(zeros, encodedFrame.data.byteLength - (digestLength[encodedFrame.type] + counterLength + 1));

            // Do truncated hash comparison. If the hash does not match we might have to advance the
            // ratchet a limited number of times. See (even though the description there is odd)
            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
            let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
            let valid = false;
            let newKeys = null;

            for (let distance = 0; distance < ratchetWindow; distance++) {
                const calculatedTag = await crypto.subtle.sign(authenticationTagOptions,
                    authenticationKey, encodedFrame.data);

                if (isArrayEqual(new Uint8Array(authTag),
                        new Uint8Array(calculatedTag.slice(0, digestLength[encodedFrame.type])))) {
                    valid = true;
                    if (distance > 0) {
                        this._setKeys(newKeys);
                    }
                    break;
                }

                // Attempt to ratchet and generate the next set of keys.
                material = await importKey(await ratchet(material));
                newKeys = await deriveKeys(material);
                authenticationKey = newKeys.authenticationKey;
            }

            // Check whether we found a valid signature.
            if (!valid) {
                // TODO: return an error to the app.

                console.error('Authentication tag mismatch');

                return;
            }

            // Extract the counter.
            const counter = new Uint8Array(16);

            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
                encodedFrame.data.byteLength - 1), 16 - counterLength);
            const counterView = new DataView(counter.buffer);

            // XOR the counter with the saltKey to construct the AES CTR.
            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);

            for (let i = 0; i < counter.byteLength; i++) {
                counterView.setUint8(i,
                    counterView.getUint8(i) ^ saltKey.getUint8(i));
            }

            return crypto.subtle.decrypt({
                name: 'AES-CTR',
                counter,
                length: 64
            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
                    unencryptedBytes[encodedFrame.type],
                    encodedFrame.data.byteLength - (unencryptedBytes[encodedFrame.type]
                    + digestLength[encodedFrame.type] + counterLength + 1))
            ).then(plainText => {
                const newData = new ArrayBuffer(unencryptedBytes[encodedFrame.type] + plainText.byteLength);
                const newUint8 = new Uint8Array(newData);

                newUint8.set(frameHeader);
                newUint8.set(new Uint8Array(plainText), unencryptedBytes[encodedFrame.type]);
                encodedFrame.data = newData;

                return controller.enqueue(encodedFrame);
            }, e => {
                console.error(e);

                // TODO: notify the application about error status.
                // TODO: For video we need a better strategy since we do not want to based any
                // non-error frames on a garbage keyframe.
                if (encodedFrame.type === undefined) { // audio, replace with silence.
                    const newData = new ArrayBuffer(3);
                    const newUint8 = new Uint8Array(newData);

                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
                    encodedFrame.data = newData;
                    controller.enqueue(encodedFrame);
                }
            });
        } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
            // If we are encrypting but don't have a key for the remote drop the frame.
            // This is a heuristic since we don't know whether a packet is encrypted,
            // do not have a checksum and do not have signaling for whether a remote participant does
            // encrypt or not.
            return;
        }

        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
        controller.enqueue(encodedFrame);
    }
}
<commit_msg>e2ee: rename constants to upper-case<commit_after>/* eslint-disable no-bitwise */
/* global BigInt */

import { deriveKeys, importKey, ratchet } from './crypto-utils';
import { isArrayEqual } from './utils';

// We use a ringbuffer of keys so we can change them and still decode packets that were
// encrypted with an old key. We use a size of 16 which corresponds to the four bits
// in the frame trailer.
const KEYRING_SIZE = 16;

// We copy the first bytes of the VP8 payload unencrypted.
// For keyframes this is 10 bytes, for non-keyframes (delta) 3. See
//   https://tools.ietf.org/html/rfc6386#section-9.1
// This allows the bridge to continue detecting keyframes (only one byte needed in the JVB)
// and is also a bit easier for the VP8 decoder (i.e. it generates funny garbage pictures
// instead of being unable to decode).
// This is a bit for show and we might want to reduce to 1 unconditionally in the final version.
//
// For audio (where frame.type is not set) we do not encrypt the opus TOC byte:
//   https://tools.ietf.org/html/rfc6716#section-3.1
const UNENCRYPTED_BYTES = {
    key: 10,
    delta: 3,
    undefined: 1 // frame.type is not set on audio
};

// Use truncated SHA-256 hashes, 80 bіts for video, 32 bits for audio.
// This follows the same principles as DTLS-SRTP.
const AUTHENTICATIONTAG_OPTIONS = {
    name: 'HMAC',
    hash: 'SHA-256'
};
const ENCRYPTION_ALGORITHM = 'AES-CTR';

// https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
const CTR_LENGTH = 64;

const DIGEST_LENGTH = {
    key: 10,
    delta: 10,
    undefined: 4 // frame.type is not set on audio
};

// Maximum number of forward ratchets to attempt when the authentication
// tag on a remote packet does not match the current key.
const RATCHET_WINDOW_SIZE = 8;

/**
 * Per-participant context holding the cryptographic keys and
 * encode/decode functions
 */
export class Context {
    /**
     * @param {string} id - local muc resourcepart
     */
    constructor(id) {
        // An array (ring) of keys that we use for sending and receiving.
        this._cryptoKeyRing = new Array(KEYRING_SIZE);

        // A pointer to the currently used key.
        this._currentKeyIndex = -1;

        // A per-sender counter that is used create the AES CTR.
        // Must be incremented on every frame that is sent, can be reset on
        // key changes.
        this._sendCount = BigInt(0); // eslint-disable-line new-cap

        this._id = id;
    }

    /**
     * Derives the different subkeys and starts using them for encryption or
     * decryption.
     * @param {Uint8Array|false} key bytes. Pass false to disable.
     * @param {Number} keyIndex
     */
    async setKey(keyBytes, keyIndex) {
        let newKey;

        if (keyBytes) {
            const material = await importKey(keyBytes);

            newKey = await deriveKeys(material);
        } else {
            newKey = false;
        }
        this._currentKeyIndex = keyIndex % this._cryptoKeyRing.length;
        this._setKeys(newKey);
    }

    /**
     * Sets a set of keys and resets the sendCount.
     * decryption.
     * @param {Object} keys set of keys.
     * @private
     */
    _setKeys(keys) {
        this._cryptoKeyRing[this._currentKeyIndex] = keys;
        this._sendCount = BigInt(0); // eslint-disable-line new-cap
    }

    /**
     * Function that will be injected in a stream and will encrypt the given encoded frames.
     *
     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
     * @param {TransformStreamDefaultController} controller - TransportStreamController.
     *
     * The packet format is a variant of
     *   https://tools.ietf.org/html/draft-omara-sframe-00
     * using a trailer instead of a header. One of the design goals was to not require
     * changes to the SFU which for video requires not encrypting the keyframe bit of VP8
     * as SFUs need to detect a keyframe (framemarking or the generic frame descriptor will
     * solve this eventually). This also "hides" that a client is using E2EE a bit.
     *
     * Note that this operates on the full frame, i.e. for VP8 the data described in
     *   https://tools.ietf.org/html/rfc6386#section-9.1
     *
     * The VP8 payload descriptor described in
     *   https://tools.ietf.org/html/rfc7741#section-4.2
     * is part of the RTP packet and not part of the encoded frame and is therefore not
     * controllable by us. This is fine as the SFU keeps having access to it for routing.
     */
    encodeFunction(encodedFrame, controller) {
        const keyIndex = this._currentKeyIndex;

        if (this._cryptoKeyRing[keyIndex]) {
            this._sendCount++;

            // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
            const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);

            // Construct frame trailer. Similar to the frame header described in
            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.2
            // but we put it at the end.
            //                                             0 1 2 3 4 5 6 7
            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
            // payload  |    CTR... (length=LEN)          |S|LEN  |KID    |
            // ---------+---------------------------------+-+-+-+-+-+-+-+-+
            const counter = new Uint8Array(16);
            const counterView = new DataView(counter.buffer);

            // The counter is encoded as a variable-length field.
            counterView.setBigUint64(8, this._sendCount);
            let counterLength = 8;

            for (let i = 8; i < counter.byteLength; i++ && counterLength--) {
                if (counterView.getUint8(i) !== 0) {
                    break;
                }
            }

            const frameTrailer = new Uint8Array(counterLength + 1);

            frameTrailer.set(new Uint8Array(counter.buffer, counter.byteLength - counterLength));

            // Since we never send a counter of 0 we send counterLength - 1 on the wire.
            // This is different from the sframe draft, increases the key space and lets us
            // ignore the case of a zero-length counter at the receiver.
            frameTrailer[frameTrailer.byteLength - 1] = keyIndex | ((counterLength - 1) << 4);

            // XOR the counter with the saltKey to construct the AES CTR.
            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);

            for (let i = 0; i < counter.byteLength; i++) {
                counterView.setUint8(i, counterView.getUint8(i) ^ saltKey.getUint8(i));
            }

            return crypto.subtle.encrypt({
                name: ENCRYPTION_ALGORITHM,
                counter,
                length: CTR_LENGTH
            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
                UNENCRYPTED_BYTES[encodedFrame.type]))
            .then(cipherText => {
                const newData = new ArrayBuffer(frameHeader.byteLength + cipherText.byteLength
                    + DIGEST_LENGTH[encodedFrame.type] + frameTrailer.byteLength);
                const newUint8 = new Uint8Array(newData);

                newUint8.set(frameHeader); // copy first bytes.
                newUint8.set(new Uint8Array(cipherText), UNENCRYPTED_BYTES[encodedFrame.type]); // add ciphertext.
                // Leave some space for the authentication tag. This is filled with 0s initially, similar to
                // STUN message-integrity described in https://tools.ietf.org/html/rfc5389#section-15.4
                newUint8.set(frameTrailer, frameHeader.byteLength + cipherText.byteLength
                    + DIGEST_LENGTH[encodedFrame.type]); // append trailer.

                return crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS, this._cryptoKeyRing[keyIndex].authenticationKey,
                    new Uint8Array(newData)).then(authTag => {
                    // Set the truncated authentication tag.
                    newUint8.set(new Uint8Array(authTag, 0, DIGEST_LENGTH[encodedFrame.type]),
                        UNENCRYPTED_BYTES[encodedFrame.type] + cipherText.byteLength);
                    encodedFrame.data = newData;

                    return controller.enqueue(encodedFrame);
                });
            }, e => {
                // TODO: surface this to the app.
                console.error(e);

                // We are not enqueuing the frame here on purpose.
            });
        }

        /* NOTE WELL:
         * This will send unencrypted data (only protected by DTLS transport encryption) when no key is configured.
         * This is ok for demo purposes but should not be done once this becomes more relied upon.
         */
        controller.enqueue(encodedFrame);
    }

    /**
     * Function that will be injected in a stream and will decrypt the given encoded frames.
     *
     * @param {RTCEncodedVideoFrame|RTCEncodedAudioFrame} encodedFrame - Encoded video frame.
     * @param {TransformStreamDefaultController} controller - TransportStreamController.
     */
    async decodeFunction(encodedFrame, controller) {
        const data = new Uint8Array(encodedFrame.data);
        const keyIndex = data[encodedFrame.data.byteLength - 1] & 0xf; // lower four bits.

        if (this._cryptoKeyRing[keyIndex]) {
            const counterLength = 1 + ((data[encodedFrame.data.byteLength - 1] >> 4) & 0x7);
            const frameHeader = new Uint8Array(encodedFrame.data, 0, UNENCRYPTED_BYTES[encodedFrame.type]);

            // Extract the truncated authentication tag.
            const authTagOffset = encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type]
                + counterLength + 1);
            const authTag = encodedFrame.data.slice(authTagOffset, authTagOffset
                + DIGEST_LENGTH[encodedFrame.type]);

            // Set authentication tag bytes to 0.
            const zeros = new Uint8Array(DIGEST_LENGTH[encodedFrame.type]);

            data.set(zeros, encodedFrame.data.byteLength - (DIGEST_LENGTH[encodedFrame.type] + counterLength + 1));

            // Do truncated hash comparison. If the hash does not match we might have to advance the
            // ratchet a limited number of times. See (even though the description there is odd)
            // https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.5.1
            let { authenticationKey, material } = this._cryptoKeyRing[keyIndex];
            let valid = false;
            let newKeys = null;

            for (let distance = 0; distance < RATCHET_WINDOW_SIZE; distance++) {
                const calculatedTag = await crypto.subtle.sign(AUTHENTICATIONTAG_OPTIONS,
                    authenticationKey, encodedFrame.data);

                if (isArrayEqual(new Uint8Array(authTag),
                        new Uint8Array(calculatedTag.slice(0, DIGEST_LENGTH[encodedFrame.type])))) {
                    valid = true;
                    if (distance > 0) {
                        this._setKeys(newKeys);
                    }
                    break;
                }

                // Attempt to ratchet and generate the next set of keys.
                material = await importKey(await ratchet(material));
                newKeys = await deriveKeys(material);
                authenticationKey = newKeys.authenticationKey;
            }

            // Check whether we found a valid signature.
            if (!valid) {
                // TODO: return an error to the app.

                console.error('Authentication tag mismatch');

                return;
            }

            // Extract the counter.
            const counter = new Uint8Array(16);

            counter.set(data.slice(encodedFrame.data.byteLength - (counterLength + 1),
                encodedFrame.data.byteLength - 1), 16 - counterLength);
            const counterView = new DataView(counter.buffer);

            // XOR the counter with the saltKey to construct the AES CTR.
            const saltKey = new DataView(this._cryptoKeyRing[keyIndex].saltKey);

            for (let i = 0; i < counter.byteLength; i++) {
                counterView.setUint8(i,
                    counterView.getUint8(i) ^ saltKey.getUint8(i));
            }

            return crypto.subtle.decrypt({
                name: ENCRYPTION_ALGORITHM,
                counter,
                length: CTR_LENGTH
            }, this._cryptoKeyRing[keyIndex].encryptionKey, new Uint8Array(encodedFrame.data,
                    UNENCRYPTED_BYTES[encodedFrame.type],
                    encodedFrame.data.byteLength - (UNENCRYPTED_BYTES[encodedFrame.type]
                    + DIGEST_LENGTH[encodedFrame.type] + counterLength + 1))
            ).then(plainText => {
                const newData = new ArrayBuffer(UNENCRYPTED_BYTES[encodedFrame.type] + plainText.byteLength);
                const newUint8 = new Uint8Array(newData);

                newUint8.set(frameHeader);
                newUint8.set(new Uint8Array(plainText), UNENCRYPTED_BYTES[encodedFrame.type]);
                encodedFrame.data = newData;

                return controller.enqueue(encodedFrame);
            }, e => {
                console.error(e);

                // TODO: notify the application about error status.
                // TODO: For video we need a better strategy since we do not want to based any
                // non-error frames on a garbage keyframe.
                if (encodedFrame.type === undefined) { // audio, replace with silence.
                    const newData = new ArrayBuffer(3);
                    const newUint8 = new Uint8Array(newData);

                    newUint8.set([ 0xd8, 0xff, 0xfe ]); // opus silence frame.
                    encodedFrame.data = newData;
                    controller.enqueue(encodedFrame);
                }
            });
        } else if (keyIndex >= this._cryptoKeyRing.length && this._cryptoKeyRing[this._currentKeyIndex]) {
            // If we are encrypting but don't have a key for the remote drop the frame.
            // This is a heuristic since we don't know whether a packet is encrypted,
            // do not have a checksum and do not have signaling for whether a remote participant does
            // encrypt or not.
            return;
        }

        // TODO: this just passes through to the decoder. Is that ok? If we don't know the key yet
        // we might want to buffer a bit but it is still unclear how to do that (and for how long etc).
        controller.enqueue(encodedFrame);
    }
}
<|endoftext|>


<commit_before>/*jslint white: true */
/*global State, module, require, console */

/**
 * Copyright (c) 2014 markewest@gmail.com
 *
 * 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.
 */

module.exports = (function () {
  'use strict';

  /**
   * @author markewest@gmail.com
   * @fileoverview Basic control of Pioneer VSX-XXXX Amps
   * @requires net.js
   * @note Big thanks to the base javascript from:
   *  https://github.com/Vmaxence/SARAH-Plugin-ampliVSX
   * And documentation here:
   *  http://raymondjulin.com/2012/07/15/remote-control-your-pioneer-vsx-receiver-over-telnet/
   *
   */
  return {
    version : 20140607,

    inputs  : ['command'],

    /**
     * Whitelist of available key codes to use.
     */
    keymap  : ['MUTE', 'POWER', 'VOL_DOWN', 'VOL_UP', 'BD', 'DVD', 'TV/SAT', 'DVR/BDR', 'HDMI 1', 'HDMI 2','HDMI 3', 'HDMI 4','HDMI 5','HDMI 6'],

    /**
     * Since I want to abstract commands, I'd rather deal with semi-readable
     * key names - so this hash table will convert the pretty names to numeric
     * values LG expects.
     */
    hashTable : { 'POWER'         : 'PZ',
                  'VOL_UP'        : 'VU',
                  'VOL_DOWN'      : 'VD',
                  'MUTE'          : 'MZ',
                  'BD'            : 'FN25',
                  'DVD'           : 'FN04',
                  'TV/SAT'        : 'FN05',
                  'DVR/BDR'       : 'FN15',
                  'HDMI_1'        : 'FN19',
                  'HDMI_2'        : 'FN20',
                  'HDMI_3'        : 'FN21',
                  'HDMI_4'        : 'FN22',
                  'HDMI_5'        : 'FN23',
                  'HDMI_6'        : 'FN24',

 },

    translateCommand : function (command) {
      return this.hashTable[command];
    },

    send : function (config) {

      //pioneer.deviceIp   = config.device.deviceIp;
      //pioneer.command    = this.translateCommand(config.command) || '';
      //pioneer.devicePort = config.device.devicePort;
      //pioneer.callback   = config.callback   || function () {};

      var tts = ""; var saut = "\r\n";
      var pioneerCommand = this.translateCommand(config.command) || '';

      if(pioneerCommand) {
        var net = require('net');
        var client = new net.Socket();
        client.connect(config.device.devicePort, config.device.deviceIp, function() {
            console.log('Connected');
            console.log(pioneerCommand);
            client.write(pioneerCommand+saut);
          });
          client.on('data', function(data) {
          console.log('Received: ' + data);
            client.destroy();
          });
            client.on('close', function() {
            console.log('Connection closed');
          });
        }
      else {
        tts = "Command Error";
      }
      console.log(tts);
      //callback({'tts': tts});
      return;
    }
  };
}());<commit_msg>Corrected Device Selection Table<commit_after>/*jslint white: true */
/*global State, module, require, console */

/**
 * Copyright (c) 2014 markewest@gmail.com
 *
 * 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.
 */

module.exports = (function () {
  'use strict';

  /**
   * @author markewest@gmail.com
   * @fileoverview Basic control of Pioneer VSX-XXXX Amps
   * @requires net.js
   * @note Big thanks to the base javascript from:
   *  https://github.com/Vmaxence/SARAH-Plugin-ampliVSX
   * And documentation here:
   *  http://raymondjulin.com/2012/07/15/remote-control-your-pioneer-vsx-receiver-over-telnet/
   *
   */
  return {
    version : 20140607,

    inputs  : ['command'],

    /**
     * Whitelist of available key codes to use.
     */
    keymap  : ['MUTE', 'POWER', 'VOL_DOWN', 'VOL_UP', 'BD', 'DVD', 'TV/SAT', 'DVR/BDR', 'HDMI 1', 'HDMI 2','HDMI 3', 'HDMI 4','HDMI 5','HDMI 6'],

    /**
     * Since I want to abstract commands, I'd rather deal with semi-readable
     * key names - so this hash table will convert the pretty names to numeric
     * values pioneer expects.
     */
    hashTable : { 'POWER'         : 'PZ',
                  'VOL_UP'        : 'VU',
                  'VOL_DOWN'      : 'VD',
                  'MUTE'          : 'MZ',
                  'CD'            : 'FN01',
                  'TUNER'         : 'FN02',
                  'CD-R/TAPE'     : 'FN03',
                  'DVD'           : 'FN04',
                  'TV'            : 'FN05',
                  'ROKU'          : 'FN06',
                  'VIDEO'         : 'FN10',
                  'IPOD/USB'      : 'FN17',
                  'DVR/BDR'       : 'FN15',
                  'HDMI_1'        : 'FN19',
                  'HDMI_2'        : 'FN20',
                  'HDMI_3'        : 'FN21',
                  'HDMI_4'        : 'FN22',
                  'HDMI_5'        : 'FN23',
                  'HDMI_6'        : 'FN24',
                  'BD'            : 'FN25',
                  'INTERNET RADIO': 'FN39',
                  'SIRIUSXM'      : 'FN40',
                  'PANDORA'       : 'FN41',

 },

    translateCommand : function (command) {
      return this.hashTable[command];
    },

    send : function (config) {

      //pioneer.deviceIp   = config.device.deviceIp;
      //pioneer.command    = this.translateCommand(config.command) || '';
      //pioneer.devicePort = config.device.devicePort;
      //pioneer.callback   = config.callback   || function () {};

      var tts = ""; var saut = "\r\n";
      var pioneerCommand = this.translateCommand(config.command) || '';

      if(pioneerCommand) {
        var net = require('net');
        var client = new net.Socket();
        client.connect(config.device.devicePort, config.device.deviceIp, function() {
            console.log('Connected');
            console.log(pioneerCommand);
            client.write(pioneerCommand+saut);
          });
          client.on('data', function(data) {
          console.log('Received: ' + data);
            client.destroy();
          });
            client.on('close', function() {
            console.log('Connection closed');
          });
        }
      else {
        tts = "Command Error";
      }
      console.log(tts);
      //callback({'tts': tts});
      return;
    }
  };
}());<|endoftext|>


<commit_before>var async = require('async'),
    fs = require('fs'),
    GitHubAPI = require('github'),
    _ = require('underscore');

var github = new GitHubAPI({
    version: "3.0.0",
    protocol: "https",
    timeout: 5000
});

function WatchManager() {
    var storageNamespace = "github",
        storageFilename = "watchman.json";

    var repos = {},
        issues = {};

    function GithubRepo(dataObj) {
        for (var prop in dataObj) this[prop] = dataObj[prop];

        this.checkForUpdates = function(callback) {
            var self = this;
            var hasChanged = false;
            github.repos.getCommits({
                user: this.user,
                repo: this.repo
            }, function(err, res) {
                if (err) {
                    callback(err, false);
                } else {
                    var mostRecent = res[0];

                    // Update internal info
                    if (mostRecent.sha != self.lastCommitSeen) {
                        hasChanged = true;
                        self.lastCommitSeen = mostRecent.sha;
                        // Get just the first line of the commit message
                        var cMessage = mostRecent.commit.message;
                        self.lastCommitMessage = cMessage.split('\n')[0];
                        self.lastCommiter = mostRecent.committer.login;
                        self.lastCommitUrl = mostRecent.commit.url;
                    }
                    callback(null, hasChanged);
                }
            });
        }

        this.generateUpdateMessage = function() {
            var self = this;
            var msg = ["Github -- New commits on " + self.getKey(),
                       "Most recent - " + self.lastCommiter + ': ' +
                           self.lastCommitMessage];
            return msg;
        }

        this.getKey = function() {
            return [this.user, this.repo].join('/');
        }
    }

    function GithubIssue(dataObj) {
        for (var prop in dataObj) this[prop] = dataObj[prop];

        this.checkForUpdates = function(callback) {
            var self = this;
            var hasChanged = false;
            github.issues.getRepoIssue({
                user: this.user,
                repo: this.repo,
                number: this.issue,
                direction: "desc"
            }, function(err, res) {
                var firstRes = res;
                // Check for comment changes
                if (err) {
                    callback(err, false);
                } else {
                    if (self.lastCommentNumber == res.comments) {
                        doneCheckingComments(firstRes, []);
                    } else {
                        github.issues.getComments({
                            user: self.user,
                            repo: self.repo,
                            number: self.issue
                        }, function(err, res) {
                            var comments = [];
                            if (!err) {
                                comments = res;
                            }
                            doneCheckingComments(firstRes, comments);
                        });
                    }
                }
            });

            function doneCheckingComments(res, comments) {
                var currCommNumber = parseInt(self.lastCommentNumber);
                if (self.title != res.title) {
                    self.title = res.title;
                }
                if (comments.length || res.state != self.lastStatus) {
                    hasChanged = true;
                    self.lastStatus = res.state;
                    self.lastCommentNumber = '' + res.comments;

                    // Get all commenters in last comments
                    var numComments = res.comments - currCommNumber;
                    var recentComments = _.first(comments, numComments);
                    var recentUsers = _.map(recentComments, function(comm) {
                        return comm.user.login;
                    });
                    self.lastCommenters = _.reduce(recentUsers, function(m, u) {
                        if (m.indexOf(u) == -1) {
                            m.push(u);
                        }
                        return m;
                    }, []);
                }
                callback(null, hasChanged);
            }
        }

        this.generateUpdateMessage = function() {
            var self = this;
            var msg = ["Github -- Update for issue " + this.getKey() + ".",
                "Current status: " + self.lastStatus + '.'];

            if (this.lastCommenters.length) {
                var commenters = JSON.stringify(this.lastCommenters);
                msg.push("New comments by " + commenters);
            }

            return msg;
        }

        this.getKey = function() {
            return [this.user, this.repo, this.issue].join('/');
        }
    }

    /* Repos are stored as:
     * {
     *     user: username,
     *     repo: repository,
     *     lastCommitSeen: sha
     * }
     *
     * Issues stored as:
     * {
     *     user: username,
     *     repo: repository,
     *     issue: issueNumber,
     *     lastStatus: status,
     *     lastCommentNumber: comment
     *     lastCommenters: [commenters]
     * }
     */
    this.load = function(callback) {
        var self = this;
        bot.fsGetData(storageNamespace, storageFilename, function(err, res) {
            if (err) {
                // There's nothing there yet!
                var initData = JSON.stringify({
                    repos: [],
                    issues: [],
                    version: "1.0"
                });
                repos = {};
                issues = {};
                bot.fsStoreData(storageNamespace, storageFilename, initData,
                        function() {
                    callback();
                });
            } else {
                var savData = JSON.parse(res);
                if (res.hasOwnProperty('version')) {
                    self.configVersion = res.version;
                } else {
                    self.configVersion = '1.0';
                }
                var repoList = _.map(savData.repos, function(repoData) {
                    return new GithubRepo(repoData);
                });
                var issueList = _.map(savData.issues, function(issueData) {
                    return new GithubIssue(issueData);
                });
                repos = {};
                issues = {};
                _.each(repoList, function(repo) {
                    var key = repo.getKey();
                    repos[key] = repo;
                });
                _.each(issueList, function(issue) {
                    var key = issue.getKey();
                    issues[key] = issue;
                });
                callback();
            }
        });
    }

    this.overwrite = function(callback) {
        var repoList = _.values(repos),
            issueList = _.values(issues);
        var data = JSON.stringify({
            repos: repoList,
            issues: issueList,
            version: this.configVersion
        });
        bot.fsStoreData(storageNamespace, storageFilename, data,
                function(err, res) {
            if (callback) callback();
        });
    }

    this.checkForUpdates = function(callback) {
        var self = this;
        var numChecked = 0;
        var repoList = _.values(repos),
            issueList = _.values(issues);
        var repoLen = repoList.length,
            issueLen = issueList.length;
        var anyChanged = false;
        _.each(repoList, function(repo) {
            console.log("Checking " + repo.getKey() + " for updates");
            repo.checkForUpdates(function(err, changed) {
                numChecked++;
                if (err) {
                    bot.say("Error reaching repo " + repo.getKey());
                } else if (changed) {
                    anyChanged = true;
                    bot.say(repo.generateUpdateMessage().join(' '));
                }
                maybeDone();
            });
        });
        _.each(issueList, function(issue) {
            console.log("Checking " + issue.getKey() + " for updates");
            issue.checkForUpdates(function(err, changed) {
                numChecked++;
                if (err) {
                    bot.say("Error reaching issue " + issue.getKey());
                } else if (changed) {
                    anyChanged = true;
                    bot.say(issue.generateUpdateMessage().join(' '));
                }
                maybeDone();
            });
        });
        function maybeDone() {
            if (numChecked == repoLen + issueLen) {
                if (anyChanged) {
                    self.overwrite(function() {
                        if (callback) callback();
                    });
                } else {
                    if (callback) callback();
                }
            }
        }
    }

    this.watchRepo = function(username, repository, callback) {
        var self = this;
        var key = [username, repository].join('/');
        if (repos.hasOwnProperty(key)) {
            callback("Repo", key, "is already being watched.");
        } else {
            var repoData = { user: username, repo: repository };
            var repo = new GithubRepo(repoData);
            repo.checkForUpdates(function() {
                repos[key] = repo;
                self.overwrite(function() {
                    callback("Watching", key + ".", "Last commit seen:",
                            repo.lastCommitSeen);
                });
            });
        }
    }

    this.watchIssue = function(username, repository, issueNum, callback) {
        var self = this;
        var key = [username, repository, issueNum].join('/');
        if (issues.hasOwnProperty(key)) {
            callback("Issue", key, "is already being watched.");
        } else {
            var issueData = {
                user: username,
                repo: repository,
                issue: issueNum,
                title: '',
                lastStatus: '',
                lastCommentNumber: '',
                lastCommenters: []
            };
            var issue = new GithubIssue(issueData);
            issue.checkForUpdates(function(err, changed) {
                if (err) {
                    callback("Github response is dicked. Could not add", key);
                } else {
                    issues[key] = issue;
                    self.overwrite(function() {
                        callback("Now watching issue", key, '|', issue.title,
                            "|", issue.lastStatus);
                    });
                }
            });
        }
    }

    this.unwatchRepo = function(username, repository, callback) {
        var key = [username, repository].join('/');
        delete repos[key];
        this.overwrite(function() {
            callback("Stopped watching", key);
        });
    }

    this.unwatchIssue = function(username, repository, issueNum, callback) {
        var key = [username, repository, issueNum].join('/');
        delete issues[key];
        this.overwrite(function() {
            callback("Stopped watching", key);
        });
    }
}

var bot,
    watchMan = new WatchManager(),
    updateInterval = 60000 * 15;  // 15 Minutes

module.exports.init = function(b) {
    bot = b;
    watchMan.load(function() {
        function check() {
            watchMan.checkForUpdates(function() {
                setTimeout(check, updateInterval);
            });
        }
        check();
    });
}

var USER_REGEX = /github\.com\/(\w+)\/?/,
    REPO_REGEX = /github\.com\/(\w+)\/((?:\w|-)+)\/?/,
    COMMIT_REGEX = /github\.com\/(\w+)\/((?:\w|-)+)\/commit\/(\w+)\/?/,
    ISSUE_REGEX = /github\.com\/(\w+)\/((?:\w|-)+)\/issues\/(\d+)\/?/,
    IO_PAGE_REGEX = /\/\/(\w+)\.github\.io\/((?:\w|-)+)\/?/;

/* Generates IRC message from github username/repo by querying that repo and
 * gathering various stats such as language and number of forks
 */
function getRepoInformation(username, reponame, cb) {
    github.repos.get({
        user: username,
        repo: reponame
    }, function(err, res) {
        if (err) {
            cb("Github response is dicked");
        } else {
            var name = res.name,
                description = res.description,
                language = res.language,
                forks = "Forks: " + res.forks_count;
            if (!language)
                language = "";
            else
                language = "| " + language;
            if (description.length > 60)
                description = description.substr(0, 40);
            if (description.length > 0)
                description = '| ' + description;

            cb(name, description, language, '| ', forks);
        }
    });
}

/* To add support for a new github link type, construct a regex and add it to
 * the declarations above. Add an object to this list containing two keys:
 *
 * regex -- the regex to use to try and match links
 * getMessage -- A function taking an argument for each group matched in the
 *     regex, as well as a final callback function, which is the text that the
 *     bot will say in the irc channel.
 */
var githubURLRegexes = [
    {
        regex: USER_REGEX,
        getMessage: function(username, cb) {
            github.user.getFrom({
                user: username
            }, function(err, res) {
                if (err) {
                    cb("Github response is dicked");
                } else {
                    var followers = res.followers,
                        following = res.following,
                        publicRepose = res.public_repos,
                        company = res.company;
                        if (!company) company = "";
                        else company = "Works for " + company + " |";

                    cb(username + ":", company, "Followers:",
                        followers, "| Following:", following);
                }
            });
        }
    },
    {
        regex: REPO_REGEX,
        getMessage: getRepoInformation
    },
    {
        regex: COMMIT_REGEX,
        getMessage: function(username, reponame, commit, cb) {
            github.repos.getCommit({
                user: username,
                repo: reponame,
                sha: commit
            }, function(err, res) {
                if (err) {
                    cb("Github response is dicked");
                } else {
                    var committer = res.committer.login,
                        message = res.commit.message;
                    // Extract first line of commit message
                    message = /.*/.exec(message);
                    if (message) message = message[0];
                    else message = "";
                    if (message.length > 50) message = message.substr(0, 50);

                    cb("Commit to", reponame, "by", committer + ":", message);
                }
            });
        }
    },
    {
        regex: ISSUE_REGEX,
        getMessage: function(username, reponame, issue, cb) {
            github.issues.getRepoIssue({
                user: username,
                repo: reponame,
                number: issue
            }, function(err, res) {
                if (err) {
                    cb("Github response is dicked");
                } else {
                    var title = res.title,
                        state = res.state;
                    cb("Issue for", reponame + ":", title, "|", state);
                }
            });
        }
    },
    {
        regex: IO_PAGE_REGEX,
        getMessage: getRepoInformation
    }
];

/* If a URL is said, try and match it to a github url and post info. */
module.exports.url = function(url, reply) {
    var matchInfo, githubObj;
    var matched = [];
    for (var i = 0; i < githubURLRegexes.length; i++) {
        var obj = githubURLRegexes[i];
        if ((matchInfo = obj.regex.exec(url))) {
            githubObj = obj;
            matched.push({
                githubObj: githubObj,
                matchInfo: matchInfo
            });
        }
    }
    if (matched.length) {
        /* Find the longest URL that was matched, that way subdirs get matched
         * to the most applicable parent dir.
         */
        var sorted = _.sortBy(matched, function(match) {
            return -match.matchInfo[0].length;
        });
        matchInfo = sorted[0].matchInfo;
        githubObj = sorted[0].githubObj;

        matchInfo.shift();
        matchInfo.push(reply);
        githubObj.getMessage.apply(githubObj, matchInfo);
    }
}

module.exports.commands = {
    github: {
        // Polls a github repo or issue and posts updates
        watch: function(r, p, reply) {
            var watchType = p[0];

            if (watchType == 'repo') {
                var user = p[1],
                    repo = p[2];
                watchMan.watchRepo(user, repo, reply);
            } else if (watchType == 'issue') {
                var user = p[1],
                    repo = p[2],
                    issueNum = p[3];
                watchMan.watchIssue(user, repo, issueNum, reply);
            } else {
                reply("Usage: watch [repo|issue] username repo [issuenum]");
            }
        },
        unwatch: function(r, p, reply) {
            var watchType = p[0];

            if (watchType == 'repo') {
                var user = p[1],
                    repo = p[2];
                watchMan.unwatchRepo(user, repo, reply);
            } else if (watchType == 'issue') {
                var user = p[1],
                    repo = p[2],
                    issueNum = p[3];
                watchMan.unwatchIssue(user, repo, issueNum, reply);
            } else {
                reply("Usage: unwatch [repo|issue] username repo [issuenum]");
            }
        }
    }
}
<commit_msg>Can no longer watch nonexistant repos on github<commit_after>var async = require('async'),
    fs = require('fs'),
    GitHubAPI = require('github'),
    _ = require('underscore');

var github = new GitHubAPI({
    version: "3.0.0",
    protocol: "https",
    timeout: 5000
});

function WatchManager() {
    var storageNamespace = "github",
        storageFilename = "watchman.json";

    var repos = {},
        issues = {};

    function GithubRepo(dataObj) {
        for (var prop in dataObj) this[prop] = dataObj[prop];

        this.checkForUpdates = function(callback) {
            var self = this;
            var hasChanged = false;
            github.repos.getCommits({
                user: this.user,
                repo: this.repo
            }, function(err, res) {
                if (err) {
                    callback(err, false);
                } else {
                    var mostRecent = res[0];

                    // Update internal info
                    if (mostRecent.sha != self.lastCommitSeen) {
                        hasChanged = true;
                        self.lastCommitSeen = mostRecent.sha;
                        // Get just the first line of the commit message
                        var cMessage = mostRecent.commit.message;
                        self.lastCommitMessage = cMessage.split('\n')[0];
                        self.lastCommiter = mostRecent.committer.login;
                        self.lastCommitUrl = mostRecent.commit.url;
                    }
                    callback(null, hasChanged);
                }
            });
        }

        this.generateUpdateMessage = function() {
            var self = this;
            var msg = ["Github -- New commits on " + self.getKey(),
                       "Most recent - " + self.lastCommiter + ': ' +
                           self.lastCommitMessage];
            return msg;
        }

        this.getKey = function() {
            return [this.user, this.repo].join('/');
        }
    }

    function GithubIssue(dataObj) {
        for (var prop in dataObj) this[prop] = dataObj[prop];

        this.checkForUpdates = function(callback) {
            var self = this;
            var hasChanged = false;
            github.issues.getRepoIssue({
                user: this.user,
                repo: this.repo,
                number: this.issue,
                direction: "desc"
            }, function(err, res) {
                var firstRes = res;
                // Check for comment changes
                if (err) {
                    callback(err, false);
                } else {
                    if (self.lastCommentNumber == res.comments) {
                        doneCheckingComments(firstRes, []);
                    } else {
                        github.issues.getComments({
                            user: self.user,
                            repo: self.repo,
                            number: self.issue
                        }, function(err, res) {
                            var comments = [];
                            if (!err) {
                                comments = res;
                            }
                            doneCheckingComments(firstRes, comments);
                        });
                    }
                }
            });

            function doneCheckingComments(res, comments) {
                var currCommNumber = parseInt(self.lastCommentNumber);
                if (self.title != res.title) {
                    self.title = res.title;
                }
                if (comments.length || res.state != self.lastStatus) {
                    hasChanged = true;
                    self.lastStatus = res.state;
                    self.lastCommentNumber = '' + res.comments;

                    // Get all commenters in last comments
                    var numComments = res.comments - currCommNumber;
                    var recentComments = _.first(comments, numComments);
                    var recentUsers = _.map(recentComments, function(comm) {
                        return comm.user.login;
                    });
                    self.lastCommenters = _.reduce(recentUsers, function(m, u) {
                        if (m.indexOf(u) == -1) {
                            m.push(u);
                        }
                        return m;
                    }, []);
                }
                callback(null, hasChanged);
            }
        }

        this.generateUpdateMessage = function() {
            var self = this;
            var msg = ["Github -- Update for issue " + this.getKey() + ".",
                "Current status: " + self.lastStatus + '.'];

            if (this.lastCommenters.length) {
                var commenters = JSON.stringify(this.lastCommenters);
                msg.push("New comments by " + commenters);
            }

            return msg;
        }

        this.getKey = function() {
            return [this.user, this.repo, this.issue].join('/');
        }
    }

    /* Repos are stored as:
     * {
     *     user: username,
     *     repo: repository,
     *     lastCommitSeen: sha
     * }
     *
     * Issues stored as:
     * {
     *     user: username,
     *     repo: repository,
     *     issue: issueNumber,
     *     lastStatus: status,
     *     lastCommentNumber: comment
     *     lastCommenters: [commenters]
     * }
     */
    this.load = function(callback) {
        var self = this;
        bot.fsGetData(storageNamespace, storageFilename, function(err, res) {
            if (err) {
                // There's nothing there yet!
                var initData = JSON.stringify({
                    repos: [],
                    issues: [],
                    version: "1.0"
                });
                repos = {};
                issues = {};
                bot.fsStoreData(storageNamespace, storageFilename, initData,
                        function() {
                    callback();
                });
            } else {
                var savData = JSON.parse(res);
                if (res.hasOwnProperty('version')) {
                    self.configVersion = res.version;
                } else {
                    self.configVersion = '1.0';
                }
                var repoList = _.map(savData.repos, function(repoData) {
                    return new GithubRepo(repoData);
                });
                var issueList = _.map(savData.issues, function(issueData) {
                    return new GithubIssue(issueData);
                });
                repos = {};
                issues = {};
                _.each(repoList, function(repo) {
                    var key = repo.getKey();
                    repos[key] = repo;
                });
                _.each(issueList, function(issue) {
                    var key = issue.getKey();
                    issues[key] = issue;
                });
                callback();
            }
        });
    }

    this.overwrite = function(callback) {
        var repoList = _.values(repos),
            issueList = _.values(issues);
        var data = JSON.stringify({
            repos: repoList,
            issues: issueList,
            version: this.configVersion
        });
        bot.fsStoreData(storageNamespace, storageFilename, data,
                function(err, res) {
            if (callback) callback();
        });
    }

    this.checkForUpdates = function(callback) {
        var self = this;
        var numChecked = 0;
        var repoList = _.values(repos),
            issueList = _.values(issues);
        var repoLen = repoList.length,
            issueLen = issueList.length;
        var anyChanged = false;
        _.each(repoList, function(repo) {
            console.log("Checking " + repo.getKey() + " for updates");
            repo.checkForUpdates(function(err, changed) {
                numChecked++;
                if (err) {
                    bot.say("Error reaching repo " + repo.getKey());
                } else if (changed) {
                    anyChanged = true;
                    bot.say(repo.generateUpdateMessage().join(' '));
                }
                maybeDone();
            });
        });
        _.each(issueList, function(issue) {
            console.log("Checking " + issue.getKey() + " for updates");
            issue.checkForUpdates(function(err, changed) {
                numChecked++;
                if (err) {
                    bot.say("Error reaching issue " + issue.getKey());
                } else if (changed) {
                    anyChanged = true;
                    bot.say(issue.generateUpdateMessage().join(' '));
                }
                maybeDone();
            });
        });
        function maybeDone() {
            if (numChecked == repoLen + issueLen) {
                if (anyChanged) {
                    self.overwrite(function() {
                        if (callback) callback();
                    });
                } else {
                    if (callback) callback();
                }
            }
        }
    }

    this.watchRepo = function(username, repository, callback) {
        var self = this;
        var key = [username, repository].join('/');
        if (repos.hasOwnProperty(key)) {
            callback("Repo", key, "is already being watched.");
        } else {
            var repoData = { user: username, repo: repository };
            var repo = new GithubRepo(repoData);
            repo.checkForUpdates(function(err) {
                if (err) {
                    callback("Github response is dicked. Could not watch repo",
                        key);
                } else {
                    repos[key] = repo;
                    self.overwrite(function() {
                        callback("Watching", key + ".", "Last commit seen:",
                                repo.lastCommitSeen);
                    });
                }
            });
        }
    }

    this.watchIssue = function(username, repository, issueNum, callback) {
        var self = this;
        var key = [username, repository, issueNum].join('/');
        if (issues.hasOwnProperty(key)) {
            callback("Issue", key, "is already being watched.");
        } else {
            var issueData = {
                user: username,
                repo: repository,
                issue: issueNum,
                title: '',
                lastStatus: '',
                lastCommentNumber: '',
                lastCommenters: []
            };
            var issue = new GithubIssue(issueData);
            issue.checkForUpdates(function(err, changed) {
                if (err) {
                    callback("Github response is dicked. Could not add", key);
                } else {
                    issues[key] = issue;
                    self.overwrite(function() {
                        callback("Now watching issue", key, '|', issue.title,
                            "|", issue.lastStatus);
                    });
                }
            });
        }
    }

    this.unwatchRepo = function(username, repository, callback) {
        var key = [username, repository].join('/');
        delete repos[key];
        this.overwrite(function() {
            callback("Stopped watching", key);
        });
    }

    this.unwatchIssue = function(username, repository, issueNum, callback) {
        var key = [username, repository, issueNum].join('/');
        delete issues[key];
        this.overwrite(function() {
            callback("Stopped watching", key);
        });
    }
}

var bot,
    watchMan = new WatchManager(),
    updateInterval = 60000 * 15;  // 15 Minutes

module.exports.init = function(b) {
    bot = b;
    watchMan.load(function() {
        function check() {
            watchMan.checkForUpdates(function() {
                setTimeout(check, updateInterval);
            });
        }
        check();
    });
}

var USER_REGEX = /github\.com\/(\w+)\/?/,
    REPO_REGEX = /github\.com\/(\w+)\/((?:\w|-)+)\/?/,
    COMMIT_REGEX = /github\.com\/(\w+)\/((?:\w|-)+)\/commit\/(\w+)\/?/,
    ISSUE_REGEX = /github\.com\/(\w+)\/((?:\w|-)+)\/issues\/(\d+)\/?/,
    IO_PAGE_REGEX = /\/\/(\w+)\.github\.io\/((?:\w|-)+)\/?/;

/* Generates IRC message from github username/repo by querying that repo and
 * gathering various stats such as language and number of forks
 */
function getRepoInformation(username, reponame, cb) {
    github.repos.get({
        user: username,
        repo: reponame
    }, function(err, res) {
        if (err) {
            cb("Github response is dicked");
        } else {
            var name = res.name,
                description = res.description,
                language = res.language,
                forks = "Forks: " + res.forks_count;
            if (!language)
                language = "";
            else
                language = "| " + language;
            if (description.length > 60)
                description = description.substr(0, 40);
            if (description.length > 0)
                description = '| ' + description;

            cb(name, description, language, '| ', forks);
        }
    });
}

/* To add support for a new github link type, construct a regex and add it to
 * the declarations above. Add an object to this list containing two keys:
 *
 * regex -- the regex to use to try and match links
 * getMessage -- A function taking an argument for each group matched in the
 *     regex, as well as a final callback function, which is the text that the
 *     bot will say in the irc channel.
 */
var githubURLRegexes = [
    {
        regex: USER_REGEX,
        getMessage: function(username, cb) {
            github.user.getFrom({
                user: username
            }, function(err, res) {
                if (err) {
                    cb("Github response is dicked");
                } else {
                    var followers = res.followers,
                        following = res.following,
                        publicRepose = res.public_repos,
                        company = res.company;
                        if (!company) company = "";
                        else company = "Works for " + company + " |";

                    cb(username + ":", company, "Followers:",
                        followers, "| Following:", following);
                }
            });
        }
    },
    {
        regex: REPO_REGEX,
        getMessage: getRepoInformation
    },
    {
        regex: COMMIT_REGEX,
        getMessage: function(username, reponame, commit, cb) {
            github.repos.getCommit({
                user: username,
                repo: reponame,
                sha: commit
            }, function(err, res) {
                if (err) {
                    cb("Github response is dicked");
                } else {
                    var committer = res.committer.login,
                        message = res.commit.message;
                    // Extract first line of commit message
                    message = /.*/.exec(message);
                    if (message) message = message[0];
                    else message = "";
                    if (message.length > 50) message = message.substr(0, 50);

                    cb("Commit to", reponame, "by", committer + ":", message);
                }
            });
        }
    },
    {
        regex: ISSUE_REGEX,
        getMessage: function(username, reponame, issue, cb) {
            github.issues.getRepoIssue({
                user: username,
                repo: reponame,
                number: issue
            }, function(err, res) {
                if (err) {
                    cb("Github response is dicked");
                } else {
                    var title = res.title,
                        state = res.state;
                    cb("Issue for", reponame + ":", title, "|", state);
                }
            });
        }
    },
    {
        regex: IO_PAGE_REGEX,
        getMessage: getRepoInformation
    }
];

/* If a URL is said, try and match it to a github url and post info. */
module.exports.url = function(url, reply) {
    var matchInfo, githubObj;
    var matched = [];
    for (var i = 0; i < githubURLRegexes.length; i++) {
        var obj = githubURLRegexes[i];
        if ((matchInfo = obj.regex.exec(url))) {
            githubObj = obj;
            matched.push({
                githubObj: githubObj,
                matchInfo: matchInfo
            });
        }
    }
    if (matched.length) {
        /* Find the longest URL that was matched, that way subdirs get matched
         * to the most applicable parent dir.
         */
        var sorted = _.sortBy(matched, function(match) {
            return -match.matchInfo[0].length;
        });
        matchInfo = sorted[0].matchInfo;
        githubObj = sorted[0].githubObj;

        matchInfo.shift();
        matchInfo.push(reply);
        githubObj.getMessage.apply(githubObj, matchInfo);
    }
}

module.exports.commands = {
    github: {
        // Polls a github repo or issue and posts updates
        watch: function(r, p, reply) {
            var watchType = p[0];

            if (watchType == 'repo') {
                var user = p[1],
                    repo = p[2];
                watchMan.watchRepo(user, repo, reply);
            } else if (watchType == 'issue') {
                var user = p[1],
                    repo = p[2],
                    issueNum = p[3];
                watchMan.watchIssue(user, repo, issueNum, reply);
            } else {
                reply("Usage: watch [repo|issue] username repo [issuenum]");
            }
        },
        unwatch: function(r, p, reply) {
            var watchType = p[0];

            if (watchType == 'repo') {
                var user = p[1],
                    repo = p[2];
                watchMan.unwatchRepo(user, repo, reply);
            } else if (watchType == 'issue') {
                var user = p[1],
                    repo = p[2],
                    issueNum = p[3];
                watchMan.unwatchIssue(user, repo, issueNum, reply);
            } else {
                reply("Usage: unwatch [repo|issue] username repo [issuenum]");
            }
        }
    }
}
<|endoftext|>


<commit_before>var fs = require('fs'),
    jshint = require('jshint'),
    UglifyJS = require('uglify-js'),

    deps = require('./deps.js').deps,
    hintrc = require('./hintrc.js').config;

function lintFiles(files) {

	var errorsFound = 0,
	    i, j, len, len2, src, errors, e;

	for (i = 0, len = files.length; i < len; i++) {

		jshint.JSHINT(fs.readFileSync(files[i], 'utf8'), hintrc, i ? {L: true} : null);
		errors = jshint.JSHINT.errors;

		for (j = 0, len2 = errors.length; j < len2; j++) {
			e = errors[j];
			console.log(files[i] + '\tline ' + e.line + '\tcol ' + e.character + '\t ' + e.reason);
		}

		errorsFound += len2;
	}

	return errorsFound;
}

function getFiles(compsBase32) {
	var memo = {},
	    comps;

	if (compsBase32) {
		comps = parseInt(compsBase32, 32).toString(2).split('');
		console.log('Managing dependencies...');
	}

	function addFiles(srcs) {
		for (var j = 0, len = srcs.length; j < len; j++) {
			memo[srcs[j]] = true;
		}
	}

	for (var i in deps) {
		if (comps) {
			if (parseInt(comps.pop(), 2) === 1) {
				console.log('\t* ' + i);
				addFiles(deps[i].src);
			} else {
				console.log('\t  ' + i);
			}
		} else {
			addFiles(deps[i].src);
		}
	}

	var files = [];

	for (var src in memo) {
		files.push('src/' + src);
	}

	return files;
}

exports.getFiles = getFiles;

exports.lint = function () {

	var files = getFiles();

	console.log('Checking for JS errors...');

	var errorsFound = lintFiles(files);

	if (errorsFound > 0) {
		console.log(errorsFound + ' error(s) found.\n');
		fail();
	} else {
		console.log('\tCheck passed');
	}
};

function getSizeDelta(newContent, oldContent) {
	if (!oldContent) {
		return 'new';
	}
	var newLen = newContent.replace(/\r\n?/g, '\n').length,
		oldLen = oldContent.replace(/\r\n?/g, '\n').length,
		delta = newLen - oldLen;

	return (delta >= 0 ? '+' : '') + delta;
}

function loadSilently(path) {
	try {
		return fs.readFileSync(path, 'utf8');
	} catch (e) {
		return null;
	}
}

function combineFiles(files) {
	var content = '';
	for (var i = 0, len = files.length; i < len; i++) {
		content += fs.readFileSync(files[i], 'utf8') + '\n\n';
	}
	return content;
}

exports.build = function (compsBase32, buildName) {

	var files = getFiles(compsBase32);

	console.log('Concatenating ' + files.length + ' files...');

	var copy = fs.readFileSync('src/copyright.js', 'utf8'),
	    intro = '(function (window, document, undefined) {',
	    outro = '}(this, document));',
	    newSrc = copy + intro + combineFiles(files) + outro,

	    pathPart = 'dist/leaflet' + (buildName ? '-' + buildName : ''),
	    srcPath = pathPart + '-src.js',

	    oldSrc = loadSilently(srcPath),
	    srcDelta = getSizeDelta(newSrc, oldSrc);

	console.log('\tUncompressed size: ' + newSrc.length + ' bytes (' + srcDelta + ')');

	if (newSrc === oldSrc) {
		console.log('\tNo changes');
	} else {
		fs.writeFileSync(srcPath, newSrc);
		console.log('\tSaved to ' + srcPath);
	}

	console.log('Compressing...');

	var path = pathPart + '.js',
	    oldCompressed = loadSilently(path),
	    newCompressed = copy + UglifyJS.minify(newSrc, {
	        warnings: true,
	        fromString: true
	    }).code,
	    delta = getSizeDelta(newCompressed, oldCompressed);

	console.log('\tCompressed size: ' + newCompressed.length + ' bytes (' + delta + ')');

	if (newCompressed === oldCompressed) {
		console.log('\tNo changes');
	} else {
		fs.writeFileSync(path, newCompressed);
		console.log('\tSaved to ' + path);
	}
};

exports.test = function() {
	var karma = require('karma'),
	    testConfig = {configFile : __dirname + '/../spec/karma.conf.js'};

	testConfig.browsers = ['PhantomJS'];

	if (isArgv('--chrome')) {
		testConfig.browsers.push('Chrome');
	}
	if (isArgv('--ff')) {
		testConfig.browsers.push('Firefox');
	}

	if (isArgv('--cov')) {
		testConfig.preprocessors = {
			'../src/**/*.js': 'coverage'
		};
		testConfig.coverageReporter = {
			type : 'html',
			dir : 'coverage/'
		};
		testConfig.reporters = ['coverage'];
	}

	karma.server.start(testConfig);

	function isArgv(optName) {
		return process.argv.indexOf(optName) !== -1;
	}
};
<commit_msg>Compatibility with browserify<commit_after>var fs = require('fs'),
    jshint = require('jshint'),
    UglifyJS = require('uglify-js'),

    deps = require('./deps.js').deps,
    hintrc = require('./hintrc.js').config;

function lintFiles(files) {

	var errorsFound = 0,
	    i, j, len, len2, src, errors, e;

	for (i = 0, len = files.length; i < len; i++) {

		jshint.JSHINT(fs.readFileSync(files[i], 'utf8'), hintrc, i ? {L: true} : null);
		errors = jshint.JSHINT.errors;

		for (j = 0, len2 = errors.length; j < len2; j++) {
			e = errors[j];
			console.log(files[i] + '\tline ' + e.line + '\tcol ' + e.character + '\t ' + e.reason);
		}

		errorsFound += len2;
	}

	return errorsFound;
}

function getFiles(compsBase32) {
	var memo = {},
	    comps;

	if (compsBase32) {
		comps = parseInt(compsBase32, 32).toString(2).split('');
		console.log('Managing dependencies...');
	}

	function addFiles(srcs) {
		for (var j = 0, len = srcs.length; j < len; j++) {
			memo[srcs[j]] = true;
		}
	}

	for (var i in deps) {
		if (comps) {
			if (parseInt(comps.pop(), 2) === 1) {
				console.log('\t* ' + i);
				addFiles(deps[i].src);
			} else {
				console.log('\t  ' + i);
			}
		} else {
			addFiles(deps[i].src);
		}
	}

	var files = [];

	for (var src in memo) {
		files.push('src/' + src);
	}

	return files;
}

exports.getFiles = getFiles;

exports.lint = function () {

	var files = getFiles();

	console.log('Checking for JS errors...');

	var errorsFound = lintFiles(files);

	if (errorsFound > 0) {
		console.log(errorsFound + ' error(s) found.\n');
		fail();
	} else {
		console.log('\tCheck passed');
	}
};

function getSizeDelta(newContent, oldContent) {
	if (!oldContent) {
		return 'new';
	}
	var newLen = newContent.replace(/\r\n?/g, '\n').length,
		oldLen = oldContent.replace(/\r\n?/g, '\n').length,
		delta = newLen - oldLen;

	return (delta >= 0 ? '+' : '') + delta;
}

function loadSilently(path) {
	try {
		return fs.readFileSync(path, 'utf8');
	} catch (e) {
		return null;
	}
}

function combineFiles(files) {
	var content = '';
	for (var i = 0, len = files.length; i < len; i++) {
		content += fs.readFileSync(files[i], 'utf8') + '\n\n';
	}
	return content;
}

exports.build = function (compsBase32, buildName) {

	var files = getFiles(compsBase32);

	console.log('Concatenating ' + files.length + ' files...');

	var copy = fs.readFileSync('src/copyright.js', 'utf8'),
	    intro = '(function (window, document, undefined) {',
	    outro = '}(window, document));',
	    newSrc = copy + intro + combineFiles(files) + outro,

	    pathPart = 'dist/leaflet' + (buildName ? '-' + buildName : ''),
	    srcPath = pathPart + '-src.js',

	    oldSrc = loadSilently(srcPath),
	    srcDelta = getSizeDelta(newSrc, oldSrc);

	console.log('\tUncompressed size: ' + newSrc.length + ' bytes (' + srcDelta + ')');

	if (newSrc === oldSrc) {
		console.log('\tNo changes');
	} else {
		fs.writeFileSync(srcPath, newSrc);
		console.log('\tSaved to ' + srcPath);
	}

	console.log('Compressing...');

	var path = pathPart + '.js',
	    oldCompressed = loadSilently(path),
	    newCompressed = copy + UglifyJS.minify(newSrc, {
	        warnings: true,
	        fromString: true
	    }).code,
	    delta = getSizeDelta(newCompressed, oldCompressed);

	console.log('\tCompressed size: ' + newCompressed.length + ' bytes (' + delta + ')');

	if (newCompressed === oldCompressed) {
		console.log('\tNo changes');
	} else {
		fs.writeFileSync(path, newCompressed);
		console.log('\tSaved to ' + path);
	}
};

exports.test = function() {
	var karma = require('karma'),
	    testConfig = {configFile : __dirname + '/../spec/karma.conf.js'};

	testConfig.browsers = ['PhantomJS'];

	if (isArgv('--chrome')) {
		testConfig.browsers.push('Chrome');
	}
	if (isArgv('--ff')) {
		testConfig.browsers.push('Firefox');
	}

	if (isArgv('--cov')) {
		testConfig.preprocessors = {
			'../src/**/*.js': 'coverage'
		};
		testConfig.coverageReporter = {
			type : 'html',
			dir : 'coverage/'
		};
		testConfig.reporters = ['coverage'];
	}

	karma.server.start(testConfig);

	function isArgv(optName) {
		return process.argv.indexOf(optName) !== -1;
	}
};
<|endoftext|>


<commit_before>/**

  compiler.js

  Powerful compiler for script transpilation, templating and CSS preprocessing.

  As a note: we differentiate between "extensions" and "types". The extension
  is the actual file extension, whereas the type is the extension a file will
  be after processing.

  • extractMetadata
  - createCompiler (!!! TODO: NEEDS CACHING)
  • aggregateData
  • createRenderer (!!! TODO: UPDATE WITH SOURCE)
  • createQueryFn
  • addPipeline

**/

/**

  Dependencies and exports

**/

var Bundler     = require("./bundler");
var Metadata    = require("./metadata");
var Promise     = require("bluebird");
var Utils       = require("./utils");

var Pipelines   = global.shipp.pipelines;

module.exports = {
  extractMetadata: extractMetadata,
  createCompiler: createCompiler,
  aggregateData: aggregateData,
  createRenderer: createRenderer,
  createQueryFn: createQueryFn,
  addPipeline: addPipeline
};

/**

  Extracts metadata from a file if appropriate (HTML-like)

  @param {String} id Source identifier
  @param {String} template String template
  @param {String} type The type of file

**/

function extractMetadata(id, template, type) {

  var metadata,
      directives;

  if (Utils.isHTML(type)) {
    // Currently leaving out the PARAM directive (until better planned)
    directives = {
      "DATA"    : "query",
      "QUERY"   : "query",
      "COOKIE"  : "object",
      "SESSION" : "object",
      "CACHE"   : "boolean"
    };
    metadata = Metadata.extract(template.slice(0, 500), directives);
    metadata.isTemplate = Utils.isTemplateFile(id, type);
  } else {
    // For non-HTML files default to caching
    metadata = { cache: true };
  }

  return metadata;

}

/**

  Creates a compiler for a file. For bundles, this means webpacking. Otherwise,
  uses the Pipelines engine.

  @param {Stat} file fs.Stat object representing file
  @param {String} [type] Type of file
  @returns {Function} Function that compiles a file

**/

function createCompiler(file, type) {
  if (file.bundle)
    return Bundler.fromFile(file, type).get;
  else
    return Promise.promisify(Pipelines.compileFile.bind(Pipelines, file.path));
}

/**

  Pulls data from cookies, session, params, query, slug and metadata into
  single data object.

  @param {Object} metadata Metadata object as returns by metadata.js
  @param {Object} params Params object as provided by handler.js
  @returns {Object} The data object

**/

function aggregateData(metadata, params) {

  // Data object will include locals, cookies, session, params, query, slug
  // and if applicable, database query results
  var data = Object.assign({}, global.shipp.locals, params.locals);

  // Attach special variables
  data.$assets  = Object.assign({}, global.shipp.assets);
  data.$body    = params.body;
  data.$cookies = params.cookies;
  data.$params  = params.params;
  data.$query   = params.query;
  data.$session = params.session;

  // Special slug
  if (params.slug)
    data.$slug = params.slug

  // Set cookies
  for (var key in metadata.cookies || {})
    data.$cookies[key] = metadata.cookies[key];

  // Set session
  for (key in metadata.session || {})
    data.$session[key] = metadata.session[key];

  return data;

}

// !!! TODO: compiler must be file as well (accept template or file)
// !!! TODO: refactor aggregateData

/**

  Creates a renderer for a blueprint

  @param {String} id Source identifier
  @param {Object} source Object of form { template, file } with either "template" or "file"
  @param {String} type The type of file
  @param {String} pipeline Pipeline to use for compiler
  @return {Function} Handler of form (params = {}, compressionMethod = "identity", maxCompression = false)

  results {
    compression,
    metadata,
    sourceId,
    type,
    contents
  }

**/

function createRenderer(id, source, type, pipeline) {

  // !!! TODO: Handle HTML here, how to map type
  // !!! TODO: add generic source handler
  var metadata = extractMetadata(source, type) || {};
  var compiler = createCompiler(source, pipeline);

  var tasks = [compiler];
  var isHTML = Utils.isHTML(type);

  // Add data query if necessary
  if (metadata.data)
    tasks.unshift(createQueryFn(metadata.data));

  return function(params, compressionMethod, maxCompression) {

    // params should include relevant information normally on a req or res object
    var data = isHTML ? aggregateData(metadata, params) : {};

    // Default compression to identity to maintain compatibility with BrowserSync and other tools
    compressionMethod = compressionMethod || "identity";
    maxCompression = maxCompression || false;

    return Utils.sequence(tasks, data).then(function(rendered) {

      var results = {
        compression: compressionMethod,
        metadata: metadata,
        sourceId: id,
        type: type
      };

      // Uncompressed
      if ("identity" === compressionMethod) {
        results.contents = rendered;
        return results;

      // Compressed
      } else {
        return new Promise(function(resolve, reject) {
          // Note that we are not defaulting to
          Utils.compress(compressionMethod, rendered, maxCompression, function(err, compressed) {
            if (err) return reject(err)
            results.contents = compressed;
            resolve(results);
          });
        })

      }

    })
  }

}

/**

  Creates a function that runs Universql-style data queries.

  @param {Array} queries The queries to be run
  @returns {Function} Function that performs query and returns promise

**/

function createQueryFn(queries) {
  return function(context) {
    return Utils.makeQueries(queries, context).then(function(data) {
      return Promise.resolve(Object.assign(data, context));
    });
  };
}

/**

  Add a pipeline if it does not exist.

  @param {String} ext The file extension for the pipeline

**/

function addPipeline(ext) {

  if (Pipelines.hasPipeline(ext)) return;

  try {
    Pipelines.addPipeline(ext);
  } catch (err) {
    global.shipp.logger.warn("");
    global.shipp.logger.warn("WARNING! There is no pipeline available for *." + ext + " files.");
    global.shipp.logger.warn("If this is not fixed, these files will not be served nor compiled.");
    global.shipp.logger.warn("You should add an appropriate pipeline to your shipp.json file.\n");
    return;
  }

}

<commit_msg>Compiler#createCompiler: removes promisify dependency<commit_after>/**

  compiler.js

  Powerful compiler for script transpilation, templating and CSS preprocessing.

  As a note: we differentiate between "extensions" and "types". The extension
  is the actual file extension, whereas the type is the extension a file will
  be after processing.

  • extractMetadata
  - createCompiler (!!! TODO: NEEDS CACHING)
  • aggregateData
  • createRenderer (!!! TODO: UPDATE WITH SOURCE)
  • createQueryFn
  • addPipeline

**/

/**

  Dependencies and exports

**/

var Bundler     = require("./bundler");
var Metadata    = require("./metadata");
var Promise     = require("bluebird");
var Utils       = require("./utils");

var Pipelines   = global.shipp.pipelines;

module.exports = {
  extractMetadata: extractMetadata,
  createCompiler: createCompiler,
  aggregateData: aggregateData,
  createRenderer: createRenderer,
  createQueryFn: createQueryFn,
  addPipeline: addPipeline
};

/**

  Extracts metadata from a file if appropriate (HTML-like)

  @param {String} id Source identifier
  @param {String} template String template
  @param {String} type The type of file

**/

function extractMetadata(id, template, type) {

  var metadata,
      directives;

  if (Utils.isHTML(type)) {
    // Currently leaving out the PARAM directive (until better planned)
    directives = {
      "DATA"    : "query",
      "QUERY"   : "query",
      "COOKIE"  : "object",
      "SESSION" : "object",
      "CACHE"   : "boolean"
    };
    metadata = Metadata.extract(template.slice(0, 500), directives);
    metadata.isTemplate = Utils.isTemplateFile(id, type);
  } else {
    // For non-HTML files default to caching
    metadata = { cache: true };
  }

  return metadata;

}

/**

  Creates a compiler for a file. For bundles, this means webpacking. Otherwise,
  uses the Pipelines engine.

  @param {Stat} file fs.Stat object representing file
  @param {String} [type] Type of file
  @returns {Function} Function that compiles a file

**/

function createCompiler(file, type) {
  if (file.bundle)
    return Bundler.fromFile(file, type).get;
  else
    return new Promise(function(resolve, reject) {
      Pipelines.compileFile(file.path, function(err, compiled) {
        if (err) return reject(err);
        resolve(compiled);
      })
    });
}

/**

  Pulls data from cookies, session, params, query, slug and metadata into
  single data object.

  @param {Object} metadata Metadata object as returns by metadata.js
  @param {Object} params Params object as provided by handler.js
  @returns {Object} The data object

**/

function aggregateData(metadata, params) {

  // Data object will include locals, cookies, session, params, query, slug
  // and if applicable, database query results
  var data = Object.assign({}, global.shipp.locals, params.locals);

  // Attach special variables
  data.$assets  = Object.assign({}, global.shipp.assets);
  data.$body    = params.body;
  data.$cookies = params.cookies;
  data.$params  = params.params;
  data.$query   = params.query;
  data.$session = params.session;

  // Special slug
  if (params.slug)
    data.$slug = params.slug

  // Set cookies
  for (var key in metadata.cookies || {})
    data.$cookies[key] = metadata.cookies[key];

  // Set session
  for (key in metadata.session || {})
    data.$session[key] = metadata.session[key];

  return data;

}

// !!! TODO: compiler must be file as well (accept template or file)
// !!! TODO: refactor aggregateData

/**

  Creates a renderer for a blueprint

  @param {String} id Source identifier
  @param {Object} source Object of form { template, file } with either "template" or "file"
  @param {String} type The type of file
  @param {String} pipeline Pipeline to use for compiler
  @return {Function} Handler of form (params = {}, compressionMethod = "identity", maxCompression = false)

  results {
    compression,
    metadata,
    sourceId,
    type,
    contents
  }

**/

function createRenderer(id, source, type, pipeline) {

  // !!! TODO: Handle HTML here, how to map type
  // !!! TODO: add generic source handler
  var metadata = extractMetadata(source, type) || {};
  var compiler = createCompiler(source, pipeline);

  var tasks = [compiler];
  var isHTML = Utils.isHTML(type);

  // Add data query if necessary
  if (metadata.data)
    tasks.unshift(createQueryFn(metadata.data));

  return function(params, compressionMethod, maxCompression) {

    // params should include relevant information normally on a req or res object
    var data = isHTML ? aggregateData(metadata, params) : {};

    // Default compression to identity to maintain compatibility with BrowserSync and other tools
    compressionMethod = compressionMethod || "identity";
    maxCompression = maxCompression || false;

    return Utils.sequence(tasks, data).then(function(rendered) {

      var results = {
        compression: compressionMethod,
        metadata: metadata,
        sourceId: id,
        type: type
      };

      // Uncompressed
      if ("identity" === compressionMethod) {
        results.contents = rendered;
        return results;

      // Compressed
      } else {
        return new Promise(function(resolve, reject) {
          // Note that we are not defaulting to
          Utils.compress(compressionMethod, rendered, maxCompression, function(err, compressed) {
            if (err) return reject(err)
            results.contents = compressed;
            resolve(results);
          });
        })

      }

    })
  }

}

/**

  Creates a function that runs Universql-style data queries.

  @param {Array} queries The queries to be run
  @returns {Function} Function that performs query and returns promise

**/

function createQueryFn(queries) {
  return function(context) {
    return Utils.makeQueries(queries, context).then(function(data) {
      return Promise.resolve(Object.assign(data, context));
    });
  };
}

/**

  Add a pipeline if it does not exist.

  @param {String} ext The file extension for the pipeline

**/

function addPipeline(ext) {

  if (Pipelines.hasPipeline(ext)) return;

  try {
    Pipelines.addPipeline(ext);
  } catch (err) {
    global.shipp.logger.warn("");
    global.shipp.logger.warn("WARNING! There is no pipeline available for *." + ext + " files.");
    global.shipp.logger.warn("If this is not fixed, these files will not be served nor compiled.");
    global.shipp.logger.warn("You should add an appropriate pipeline to your shipp.json file.\n");
    return;
  }

}

<|endoftext|>