<commit_before>/**
 * node-twitter-api
 * Twitter API 1.1 REST and Streaming client for Node JS
 */

// pseudo constants
//
var TWITTER_API_TIMEOUT             = 10000,
    TWITTER_API_USERAGENT           = 'Node/'+process.version.substr(1)+'; https://github.com/timwhitlock/node-twitter-api',
    TWITTER_API_BASE                = 'https://api.twitter.com/1.1',
    TWITTER_STREAM_BASE             = 'https://stream.twitter.com/1.1',
    TWITTER_OAUTH_REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token',
    TWITTER_OAUTH_AUTHORIZE_URL     = 'https://twitter.com/oauth/authorize',
    TWITTER_OAUTH_AUTHENTICATE_URL  = 'https://twitter.com/oauth/authenticate',
    TWITTER_OAUTH_ACCESS_TOKEN_URL  = 'https://twitter.com/oauth/access_token',
    TWITTER_OAUTH2_BEARER_TOKEN_URL = 'https://api.twitter.com/oauth2/token',
    TWITTER_OAUTH2_INVALIDATE_URL   = 'https://api.twitter.com/oauth2/invalidate_token';

/**
 * OAuth safe encodeURIComponent of string
 * handles ignored characters that OAuth expects encoded "! * ' ( )"
 */
function uriEncodeString( text ){
    return encodeURIComponent(text).replace( /[\!\*\'\(\)]/g, function( chr ){
        chr = chr.charCodeAt(0).toString(16);
        while( chr.length < 2 ){
            chr = '0'+hex;
        }
        return '%'+chr;
    } )
}

/**
 * OAuth safe encodeURIComponent of params object
 */
function uriEncodeParams( obj ){
    var pairs = [], key;
    for( key in obj ){
        pairs.push( uriEncodeString(key) +'='+ uriEncodeString(obj[key]) );
    }
    return pairs.join('&');
}

/**
 * Simple token object that holds key and secret
 */
function OAuthToken( key, secret ){
    if( ! key || ! secret ){
        throw new Error('OAuthToken params must not be empty');
    }
    this.key = key;
    this.secret = secret;
}

OAuthToken.prototype.getAuthorizationUrl = function(){
    return TWITTER_OAUTH_AUTHORIZE_URL+'?oauth_token='+encodeURIComponent(this.key);
}

OAuthToken.prototype.getBasicAuthHeader = function(){
    var creds = new Buffer( encodeURIComponent(this.key)+':'+encodeURIComponent(this.secret) );
    return 'Basic '+ creds.toString('base64');
}

/**
 * Object for compiling, signing and serializing OAuth parameters
 */
function OAuthParams( args ){
    this.bearer_token = '';
    this.consumer_secret = '';
    this.token_secret = '';
    this.args = args || {};
}

OAuthParams.prototype.setBearer = function( bearer ){
    this.bearer_token = bearer;
    delete this.args.oauth_version;
    return this;
}

OAuthParams.prototype.setConsumer = function( token ){
    this.consumer_secret = token.secret||'';
    this.args.oauth_consumer_key = token.key||'';
    this.args.oauth_version = '1.0';
    return this;
}

OAuthParams.prototype.setAccess = function ( token ){
    this.token_secret = token.secret||'';
    this.args.oauth_token = token.key||'';
    return this;
}

OAuthParams.prototype.normalize = function(){
    var i, k, keys = [], sorted = {};
    for( k in this.args ){
        keys.push(k);
    }
    keys.sort();
    for( i in keys ){
        k = keys[i];
        sorted[k] = this.args[k];
    }
    return this.args = sorted;
}

OAuthParams.prototype.serialize = function(){
    return uriEncodeParams( this.args );
}

OAuthParams.prototype.sign = function( requestMethod, requestUri ){
    var ms = Date.now(),
        s  = Math.round( ms / 1000 );
    this.args.oauth_signature_method = 'HMAC-SHA1';
    this.args.oauth_timestamp = String(s);
    this.args.oauth_nonce = String(ms);
    delete this.args.oauth_signature;
    // normalize, build and sign
    this.normalize();
    var str  = requestMethod.toUpperCase() +'&'+ uriEncodeString(requestUri) +'&'+ uriEncodeString(this.serialize()),
        key  = uriEncodeString(this.consumer_secret) +'&'+ uriEncodeString(this.token_secret),
        hash = require('crypto').createHmac('sha1',key).update(str);
    this.args.oauth_signature = hash.digest('base64');
    return this;
}

OAuthParams.prototype.getHeader = function(){
    // OAuth 2
    if( this.bearer_token ){
        return 'Bearer '+encodeURIComponent(this.bearer_token);
    }
    // OAuth 1
    var a, args = {}, lines = [];
    for( a in this.args ){
        if( 0 === a.indexOf('oauth_') ){
            lines.push( encodeURIComponent(a) +'='+ encodeURIComponent(this.args[a]) );
        }
        else {
            args[a] = this.args[a];
        }
    }
    this.args = args;
    return 'OAuth '+lines.join(',\n ');
}

/**
 * Twitter API 1.1 client
 */
function TwitterClient(){
    this.deAuth();
}

TwitterClient.prototype.setProxy = function(proxy) {
	this.proxy = proxy;
}

TwitterClient.prototype.getLastMeta = function( method ){
    return this.lastMeta[ method || this.lastCall ] || { limit: 0, remaining: 0, reset: 0 };
}

TwitterClient.prototype.getRateLimit = function( method ){
    return this.getLastMeta(method).limit;
}

TwitterClient.prototype.getRateLimitRemaining = function( method ){
    return this.getLastMeta(method).remaining;
}

TwitterClient.prototype.getRateLimitReset = function( method ){
    return new Date( this.getLastMeta(method).reset * 1000 );
}

TwitterClient.prototype.setAuth = function( consumerOrBearer, consumerSecret, accessKey, accessSecret ){
    this.deAuth();
    // OAuth 2
    if( 1 === arguments.length ){
        this.bearerToken = consumerOrBearer;
        return this;
    }
    // OAuth 1
    this.consumerToken = new OAuthToken( consumerOrBearer, consumerSecret );
    if( accessKey || accessSecret ){
        this.accessToken = new OAuthToken( accessKey, accessSecret );
    }
    return this;
}

TwitterClient.prototype.hasAuth = function(){
    return ( this.bearerToken && this.bearerToken.length ) || ( this.accessToken instanceof OAuthToken ) && ( this.consumerToken instanceof OAuthToken );
}

TwitterClient.prototype.deAuth = function(){
    this.bearerToken = null;
    this.consumerToken = null;
    this.accessToken = null;
    // register rate limits for all REST calls
    this.lastCall = null;
    this.lastMeta = {};
    return this;
}

TwitterClient.prototype.get = function( requestPath, requestArgs, callback ){
    return this._rest( 'GET', requestPath, requestArgs, callback );
}

TwitterClient.prototype.post = function( requestPath, requestArgs, callback ){
    return this._rest( 'POST', requestPath, requestArgs, callback );
}

TwitterClient.prototype._rest = function( requestMethod, requestPath, requestArgs, callback ){
    if( ! this.hasAuth() ){
        throw new Error('Twitter REST client not authenticated');
    }
    var requestUri = TWITTER_API_BASE + '/' + requestPath + '.json';
    if( 'function' !== typeof callback ){
        callback = function(){
            console.error('No callback for '+requestMethod+' '+requestUri);
        }
    }
    this.lastCall = requestPath;
    var client = this;
    return this._call( requestMethod, requestUri, requestArgs, '', TWITTER_API_TIMEOUT, function( res, err ) {
        if( ! res ){
            callback( null, err, 0 );
            return;
        }
        // started to receive response from twitter
        res.setEncoding('utf8');
        var body = '';
        res.on('data', function( chunk ) {
            body += chunk;
        } );
        res.on('end', function(){
            try {
                var error = null,
                    data = JSON.parse(body);
                if( 'object' !== typeof data ){
                    throw { message: 'Malformed response from Twitter', code: 0 };
                }
                if( data.errors && data.errors.length ){
                    throw data.errors.pop();
                }
            }
            catch( e ){
                error = e;
                data = null;
                if( e.code && e.message ){
                    console.error( 'Twitter responded status '+res.statusCode+', error #'+e.code+', '+e.message );
                }
                else {
                    console.error( e.message || String(e) || 'Unknown error' );
                }
            }
            if( 'function' === typeof callback ){
                callback( data, error, res.statusCode );
            }
        } );
        // remember last rest call
        // pull rate limit data from headers
        var limit = res.headers['x-rate-limit-limit'];
        if( limit ){
            client.lastMeta[ client.lastCall ] = {
                limit:     Number( limit ),
                remaining: Number( res.headers['x-rate-limit-remaining'] ),
                reset:     Number( res.headers['x-rate-limit-reset'] )
            }
        }
    } );
}

TwitterClient.prototype.stream = function( requestPath, requestArgs, callback ){
    if( ! this.hasAuth() ){
        throw new Error('Twitter streaming client not authenticated');
    }
    var requestMethod = 'GET',
        requestUri = TWITTER_STREAM_BASE+'/'+requestPath+'.json';
    if( 'user' === requestPath ){
        requestUri = requestUri.replace('stream','userstream');
    }
    else if( 'site' === requestPath ){
        requestUri = requestUri.replace('stream','sitestream');
    }
    else if( 0 === requestPath.indexOf('statuses/filter') ){
        requestMethod = 'POST';
    }
    if( 'function' !== typeof callback ){
        callback = function( json ){
            json && console.log( json );
        };
    }
    var client = this;
    return this._call( requestMethod, requestUri, requestArgs, '', 0, function( res, err ){
        if( ! res ){
            callback( null, err );
            return;
        }
        client.response = res;
        res.setEncoding('utf8');
        if( 200 !== res.statusCode ){
            console.error( 'Error '+res.statusCode );
            client.abort();
        }
        res.on('data', function( chunk ) {
            // simple sniff for valid json and call back
            if( '{' == chunk.charAt(0) && '}\r\n' === chunk.substr(-3) ){
                callback( chunk );
            }
        } );
        res.on('end', function(){
            client.response = null;
        } );
    } );
}

TwitterClient.prototype._call = function( requestMethod, requestUri, requestArgs, authHeader, timeout, callback ){
    requestMethod = String( requestMethod||'GET' ).toUpperCase();
    // build and sign request parameters
    var params = new OAuthParams( requestArgs );
    if( this.bearerToken ){
        params.setBearer( this.bearerToken );
    }
    else {
        this.consumerToken && params.setConsumer( this.consumerToken );
        this.accessToken && params.setAccess( this.accessToken );
    }
    // grab authorization header and any remaining params
    if( ! authHeader ){
        params.sign( requestMethod, requestUri );
        authHeader = params.getHeader();
    }
    var query = params.serialize();

    // build http request starting with parsed endpoint
    var http = require('url').parse( requestUri );
    http.headers = {
        Authorization: authHeader,
        'User-Agent': TWITTER_API_USERAGENT
    };
    http.method = requestMethod;
    if( 'POST' === requestMethod ){
        http.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
        http.headers['Content-Length'] = query.length;
    }
    else if( query ){
        http.path += '?' + query;
    }
    // configure proxy
    if (typeof this.proxy == "object") {
        if (typeof this.proxy.host != "undefined") {
            http.host = this.proxy.host;
            http.path = "https://" + http.hostname + http.path;
            delete http.hostname;
        }
        if (typeof this.proxy.port != "undefined") {
            http.port = this.proxy.port;
        }
    }
    var req = require('https').request( http, callback );
    req.setSocketKeepAlive( true );
    if( timeout ){
        req.on('socket', function (socket) {
            socket.setTimeout( timeout );
            socket.on('timeout', function() {
                console.error('Request timeout in '+requestUri);
                req.abort();
            } );
        } );
    }
    req.on('error', function( e ) {
        console.error( String(e) );
        callback( null, e );
    } );
    if( 'POST' === requestMethod && query ){
        req.write( query );
        req.write( '\n' );
    }
    req.end();
    return this;
}

TwitterClient.prototype.abort = function(){
    try {
        if( this.response ){
            this.response.destroy();
            this.response = null;
        }
    }
    catch( e ){
        console.error( e.message || String(e) || 'Unknown error on abort' );
    }
}

TwitterClient.prototype.fetchRequestToken = function( url, callback ){
    var requestUri  = TWITTER_OAUTH_REQUEST_TOKEN_URL,
        requestArgs = { oauth_callback: url||'oob' };
    return this._oauthExchange( requestUri, requestArgs, '', callback );
}

TwitterClient.prototype.fetchAccessToken = function( verifier, callback ){
    var requestUri  = TWITTER_OAUTH_ACCESS_TOKEN_URL,
        requestArgs = { oauth_verifier: verifier };
    return this._oauthExchange( requestUri, requestArgs, '', callback );
}

TwitterClient.prototype.fetchBearerToken = function( callback ){
    var requestUri  = TWITTER_OAUTH2_BEARER_TOKEN_URL,
        requestArgs = { grant_type: 'client_credentials' },
        authHeader  = this.consumerToken.getBasicAuthHeader();
    return this._oauthExchange( requestUri, requestArgs, authHeader, callback );
}

TwitterClient.prototype.invalidateBearerToken = function( bearer, callback ){
    var requestUri  = TWITTER_OAUTH2_INVALIDATE_URL,
        requestArgs = { access_token: bearer },
        authHeader  = this.consumerToken.getBasicAuthHeader();
    return this._oauthExchange( requestUri, requestArgs, authHeader, callback );
}

TwitterClient.prototype._oauthExchange = function( requestUri, requestArgs, authHeader, callback ){
    if( 'function' !== typeof callback ){
        callback = function(){
            console.error('No callback for POST '+requestUri);
        }
    }
    this._call( 'POST', requestUri, requestArgs, authHeader, TWITTER_API_TIMEOUT, function( res, err ){
        if( ! res ){
            callback( null, err||{}, 0 );
            return;
        }
        // started to receive response from twitter
        res.setEncoding('utf8');
        var body = '';
        res.on('data', function( chunk ) {
            body += chunk;
        } );
        res.on('end', function(){
            var token,
                params = 0 === body.indexOf('{') ? JSON.parse(body) : require('querystring').parse(body);
            if( 'bearer' === params.token_type ){
                token = decodeURIComponent( params.access_token );
            }
            else if( params.oauth_token && params.oauth_token_secret ){
                token = new OAuthToken( params.oauth_token, params.oauth_token_secret );
            }
            callback( token, params, res.statusCode );
        } );
    } );
}

// expose object creation methods to module

exports.createOAuthParams = function( args ){
    return new OAuthParams( args );
}

exports.createOAuthToken = function( key, secret ){
    return new OAuthToken( key, secret );
}

exports.createClient = function(){
    return new TwitterClient;
}

// expose some configuration setters

exports.setTimeout = function( ms ){
    ms = Number(ms);
    if( ! isNaN(ms) ){
        TWITTER_API_TIMEOUT = ms;
    }
    return exports;
}

<commit_msg>remove \n from header<commit_after>/**
 * node-twitter-api
 * Twitter API 1.1 REST and Streaming client for Node JS
 */

// pseudo constants
//
var TWITTER_API_TIMEOUT             = 10000,
    TWITTER_API_USERAGENT           = 'Node/'+process.version.substr(1)+'; https://github.com/timwhitlock/node-twitter-api',
    TWITTER_API_BASE                = 'https://api.twitter.com/1.1',
    TWITTER_STREAM_BASE             = 'https://stream.twitter.com/1.1',
    TWITTER_OAUTH_REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token',
    TWITTER_OAUTH_AUTHORIZE_URL     = 'https://twitter.com/oauth/authorize',
    TWITTER_OAUTH_AUTHENTICATE_URL  = 'https://twitter.com/oauth/authenticate',
    TWITTER_OAUTH_ACCESS_TOKEN_URL  = 'https://twitter.com/oauth/access_token',
    TWITTER_OAUTH2_BEARER_TOKEN_URL = 'https://api.twitter.com/oauth2/token',
    TWITTER_OAUTH2_INVALIDATE_URL   = 'https://api.twitter.com/oauth2/invalidate_token';

/**
 * OAuth safe encodeURIComponent of string
 * handles ignored characters that OAuth expects encoded "! * ' ( )"
 */
function uriEncodeString( text ){
    return encodeURIComponent(text).replace( /[\!\*\'\(\)]/g, function( chr ){
        chr = chr.charCodeAt(0).toString(16);
        while( chr.length < 2 ){
            chr = '0'+hex;
        }
        return '%'+chr;
    } )
}

/**
 * OAuth safe encodeURIComponent of params object
 */
function uriEncodeParams( obj ){
    var pairs = [], key;
    for( key in obj ){
        pairs.push( uriEncodeString(key) +'='+ uriEncodeString(obj[key]) );
    }
    return pairs.join('&');
}

/**
 * Simple token object that holds key and secret
 */
function OAuthToken( key, secret ){
    if( ! key || ! secret ){
        throw new Error('OAuthToken params must not be empty');
    }
    this.key = key;
    this.secret = secret;
}

OAuthToken.prototype.getAuthorizationUrl = function(){
    return TWITTER_OAUTH_AUTHORIZE_URL+'?oauth_token='+encodeURIComponent(this.key);
}

OAuthToken.prototype.getBasicAuthHeader = function(){
    var creds = new Buffer( encodeURIComponent(this.key)+':'+encodeURIComponent(this.secret) );
    return 'Basic '+ creds.toString('base64');
}

/**
 * Object for compiling, signing and serializing OAuth parameters
 */
function OAuthParams( args ){
    this.bearer_token = '';
    this.consumer_secret = '';
    this.token_secret = '';
    this.args = args || {};
}

OAuthParams.prototype.setBearer = function( bearer ){
    this.bearer_token = bearer;
    delete this.args.oauth_version;
    return this;
}

OAuthParams.prototype.setConsumer = function( token ){
    this.consumer_secret = token.secret||'';
    this.args.oauth_consumer_key = token.key||'';
    this.args.oauth_version = '1.0';
    return this;
}

OAuthParams.prototype.setAccess = function ( token ){
    this.token_secret = token.secret||'';
    this.args.oauth_token = token.key||'';
    return this;
}

OAuthParams.prototype.normalize = function(){
    var i, k, keys = [], sorted = {};
    for( k in this.args ){
        keys.push(k);
    }
    keys.sort();
    for( i in keys ){
        k = keys[i];
        sorted[k] = this.args[k];
    }
    return this.args = sorted;
}

OAuthParams.prototype.serialize = function(){
    return uriEncodeParams( this.args );
}

OAuthParams.prototype.sign = function( requestMethod, requestUri ){
    var ms = Date.now(),
        s  = Math.round( ms / 1000 );
    this.args.oauth_signature_method = 'HMAC-SHA1';
    this.args.oauth_timestamp = String(s);
    this.args.oauth_nonce = String(ms);
    delete this.args.oauth_signature;
    // normalize, build and sign
    this.normalize();
    var str  = requestMethod.toUpperCase() +'&'+ uriEncodeString(requestUri) +'&'+ uriEncodeString(this.serialize()),
        key  = uriEncodeString(this.consumer_secret) +'&'+ uriEncodeString(this.token_secret),
        hash = require('crypto').createHmac('sha1',key).update(str);
    this.args.oauth_signature = hash.digest('base64');
    return this;
}

OAuthParams.prototype.getHeader = function(){
    // OAuth 2
    if( this.bearer_token ){
        return 'Bearer '+encodeURIComponent(this.bearer_token);
    }
    // OAuth 1
    var a, args = {}, lines = [];
    for( a in this.args ){
        if( 0 === a.indexOf('oauth_') ){
            lines.push( encodeURIComponent(a) +'='+ encodeURIComponent(this.args[a]) );
        }
        else {
            args[a] = this.args[a];
        }
    }
    this.args = args;
    return 'OAuth '+lines.join(', ');
}

/**
 * Twitter API 1.1 client
 */
function TwitterClient(){
    this.deAuth();
}

TwitterClient.prototype.setProxy = function(proxy) {
	this.proxy = proxy;
}

TwitterClient.prototype.getLastMeta = function( method ){
    return this.lastMeta[ method || this.lastCall ] || { limit: 0, remaining: 0, reset: 0 };
}

TwitterClient.prototype.getRateLimit = function( method ){
    return this.getLastMeta(method).limit;
}

TwitterClient.prototype.getRateLimitRemaining = function( method ){
    return this.getLastMeta(method).remaining;
}

TwitterClient.prototype.getRateLimitReset = function( method ){
    return new Date( this.getLastMeta(method).reset * 1000 );
}

TwitterClient.prototype.setAuth = function( consumerOrBearer, consumerSecret, accessKey, accessSecret ){
    this.deAuth();
    // OAuth 2
    if( 1 === arguments.length ){
        this.bearerToken = consumerOrBearer;
        return this;
    }
    // OAuth 1
    this.consumerToken = new OAuthToken( consumerOrBearer, consumerSecret );
    if( accessKey || accessSecret ){
        this.accessToken = new OAuthToken( accessKey, accessSecret );
    }
    return this;
}

TwitterClient.prototype.hasAuth = function(){
    return ( this.bearerToken && this.bearerToken.length ) || ( this.accessToken instanceof OAuthToken ) && ( this.consumerToken instanceof OAuthToken );
}

TwitterClient.prototype.deAuth = function(){
    this.bearerToken = null;
    this.consumerToken = null;
    this.accessToken = null;
    // register rate limits for all REST calls
    this.lastCall = null;
    this.lastMeta = {};
    return this;
}

TwitterClient.prototype.get = function( requestPath, requestArgs, callback ){
    return this._rest( 'GET', requestPath, requestArgs, callback );
}

TwitterClient.prototype.post = function( requestPath, requestArgs, callback ){
    return this._rest( 'POST', requestPath, requestArgs, callback );
}

TwitterClient.prototype._rest = function( requestMethod, requestPath, requestArgs, callback ){
    if( ! this.hasAuth() ){
        throw new Error('Twitter REST client not authenticated');
    }
    var requestUri = TWITTER_API_BASE + '/' + requestPath + '.json';
    if( 'function' !== typeof callback ){
        callback = function(){
            console.error('No callback for '+requestMethod+' '+requestUri);
        }
    }
    this.lastCall = requestPath;
    var client = this;
    return this._call( requestMethod, requestUri, requestArgs, '', TWITTER_API_TIMEOUT, function( res, err ) {
        if( ! res ){
            callback( null, err, 0 );
            return;
        }
        // started to receive response from twitter
        res.setEncoding('utf8');
        var body = '';
        res.on('data', function( chunk ) {
            body += chunk;
        } );
        res.on('end', function(){
            try {
                var error = null,
                    data = JSON.parse(body);
                if( 'object' !== typeof data ){
                    throw { message: 'Malformed response from Twitter', code: 0 };
                }
                if( data.errors && data.errors.length ){
                    throw data.errors.pop();
                }
            }
            catch( e ){
                error = e;
                data = null;
                if( e.code && e.message ){
                    console.error( 'Twitter responded status '+res.statusCode+', error #'+e.code+', '+e.message );
                }
                else {
                    console.error( e.message || String(e) || 'Unknown error' );
                }
            }
            if( 'function' === typeof callback ){
                callback( data, error, res.statusCode );
            }
        } );
        // remember last rest call
        // pull rate limit data from headers
        var limit = res.headers['x-rate-limit-limit'];
        if( limit ){
            client.lastMeta[ client.lastCall ] = {
                limit:     Number( limit ),
                remaining: Number( res.headers['x-rate-limit-remaining'] ),
                reset:     Number( res.headers['x-rate-limit-reset'] )
            }
        }
    } );
}

TwitterClient.prototype.stream = function( requestPath, requestArgs, callback ){
    if( ! this.hasAuth() ){
        throw new Error('Twitter streaming client not authenticated');
    }
    var requestMethod = 'GET',
        requestUri = TWITTER_STREAM_BASE+'/'+requestPath+'.json';
    if( 'user' === requestPath ){
        requestUri = requestUri.replace('stream','userstream');
    }
    else if( 'site' === requestPath ){
        requestUri = requestUri.replace('stream','sitestream');
    }
    else if( 0 === requestPath.indexOf('statuses/filter') ){
        requestMethod = 'POST';
    }
    if( 'function' !== typeof callback ){
        callback = function( json ){
            json && console.log( json );
        };
    }
    var client = this;
    return this._call( requestMethod, requestUri, requestArgs, '', 0, function( res, err ){
        if( ! res ){
            callback( null, err );
            return;
        }
        client.response = res;
        res.setEncoding('utf8');
        if( 200 !== res.statusCode ){
            console.error( 'Error '+res.statusCode );
            client.abort();
        }
        res.on('data', function( chunk ) {
            // simple sniff for valid json and call back
            if( '{' == chunk.charAt(0) && '}\r\n' === chunk.substr(-3) ){
                callback( chunk );
            }
        } );
        res.on('end', function(){
            client.response = null;
        } );
    } );
}

TwitterClient.prototype._call = function( requestMethod, requestUri, requestArgs, authHeader, timeout, callback ){
    requestMethod = String( requestMethod||'GET' ).toUpperCase();
    // build and sign request parameters
    var params = new OAuthParams( requestArgs );
    if( this.bearerToken ){
        params.setBearer( this.bearerToken );
    }
    else {
        this.consumerToken && params.setConsumer( this.consumerToken );
        this.accessToken && params.setAccess( this.accessToken );
    }
    // grab authorization header and any remaining params
    if( ! authHeader ){
        params.sign( requestMethod, requestUri );
        authHeader = params.getHeader();
    }
    var query = params.serialize();

    // build http request starting with parsed endpoint
    var http = require('url').parse( requestUri );
    http.headers = {
        Authorization: authHeader,
        'User-Agent': TWITTER_API_USERAGENT
    };
    http.method = requestMethod;
    if( 'POST' === requestMethod ){
        http.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
        http.headers['Content-Length'] = query.length;
    }
    else if( query ){
        http.path += '?' + query;
    }
    // configure proxy
    if (typeof this.proxy == "object") {
        if (typeof this.proxy.host != "undefined") {
            http.host = this.proxy.host;
            http.path = "https://" + http.hostname + http.path;
            delete http.hostname;
        }
        if (typeof this.proxy.port != "undefined") {
            http.port = this.proxy.port;
        }
    }
    var req = require('https').request( http, callback );
    req.setSocketKeepAlive( true );
    if( timeout ){
        req.on('socket', function (socket) {
            socket.setTimeout( timeout );
            socket.on('timeout', function() {
                console.error('Request timeout in '+requestUri);
                req.abort();
            } );
        } );
    }
    req.on('error', function( e ) {
        console.error( String(e) );
        callback( null, e );
    } );
    if( 'POST' === requestMethod && query ){
        req.write( query );
        req.write( '\n' );
    }
    req.end();
    return this;
}

TwitterClient.prototype.abort = function(){
    try {
        if( this.response ){
            this.response.destroy();
            this.response = null;
        }
    }
    catch( e ){
        console.error( e.message || String(e) || 'Unknown error on abort' );
    }
}

TwitterClient.prototype.fetchRequestToken = function( url, callback ){
    var requestUri  = TWITTER_OAUTH_REQUEST_TOKEN_URL,
        requestArgs = { oauth_callback: url||'oob' };
    return this._oauthExchange( requestUri, requestArgs, '', callback );
}

TwitterClient.prototype.fetchAccessToken = function( verifier, callback ){
    var requestUri  = TWITTER_OAUTH_ACCESS_TOKEN_URL,
        requestArgs = { oauth_verifier: verifier };
    return this._oauthExchange( requestUri, requestArgs, '', callback );
}

TwitterClient.prototype.fetchBearerToken = function( callback ){
    var requestUri  = TWITTER_OAUTH2_BEARER_TOKEN_URL,
        requestArgs = { grant_type: 'client_credentials' },
        authHeader  = this.consumerToken.getBasicAuthHeader();
    return this._oauthExchange( requestUri, requestArgs, authHeader, callback );
}

TwitterClient.prototype.invalidateBearerToken = function( bearer, callback ){
    var requestUri  = TWITTER_OAUTH2_INVALIDATE_URL,
        requestArgs = { access_token: bearer },
        authHeader  = this.consumerToken.getBasicAuthHeader();
    return this._oauthExchange( requestUri, requestArgs, authHeader, callback );
}

TwitterClient.prototype._oauthExchange = function( requestUri, requestArgs, authHeader, callback ){
    if( 'function' !== typeof callback ){
        callback = function(){
            console.error('No callback for POST '+requestUri);
        }
    }
    this._call( 'POST', requestUri, requestArgs, authHeader, TWITTER_API_TIMEOUT, function( res, err ){
        if( ! res ){
            callback( null, err||{}, 0 );
            return;
        }
        // started to receive response from twitter
        res.setEncoding('utf8');
        var body = '';
        res.on('data', function( chunk ) {
            body += chunk;
        } );
        res.on('end', function(){
            var token,
                params = 0 === body.indexOf('{') ? JSON.parse(body) : require('querystring').parse(body);
            if( 'bearer' === params.token_type ){
                token = decodeURIComponent( params.access_token );
            }
            else if( params.oauth_token && params.oauth_token_secret ){
                token = new OAuthToken( params.oauth_token, params.oauth_token_secret );
            }
            callback( token, params, res.statusCode );
        } );
    } );
}

// expose object creation methods to module

exports.createOAuthParams = function( args ){
    return new OAuthParams( args );
}

exports.createOAuthToken = function( key, secret ){
    return new OAuthToken( key, secret );
}

exports.createClient = function(){
    return new TwitterClient;
}

// expose some configuration setters

exports.setTimeout = function( ms ){
    ms = Number(ms);
    if( ! isNaN(ms) ){
        TWITTER_API_TIMEOUT = ms;
    }
    return exports;
}

<|endoftext|>


<commit_before>/*jslint white: true, browser: true, maxlen: 80, indent: 2,
nomen: false, onevar: false, regexp: false, nonoop: false */
/*globals Class, LoginError, PostError, ProtectedError, $, window,
console, captcha, _*/
/*
 * Twitter actions
 *
 * Copyright(c) 2010 BitTorrent Inc.
 *
 */

// XXX - Need to monitor rate limit status and catch 400 errors to notify that
// it has been hit.
var Twitter = Class.extend({
  init: function (cb) {
    var self = this;
    $.get('http://twitter.com', function (page) {
      if (!page.match('_setVar\\(\'Not Logged In\'')) {
        if (window.debug) {
          console.log('Already logged in');
        }
        self.metadata(cb);
        return;
      }
      if (window.debug) {
        console.log("Not logged in");
      }
      var err = new LoginError("Automatic login failed.");
      err.user("Unknown");
      err.response(page);
      if (cb) {
        cb(err);
      }
    });
  },
  metadata: function (cb) {
    var self = this;
    $.get('http://twitter.com', function (page) {
      try {
        self.screen_name = page.match(
          /content="(\S+)" name="session-user-screen_name"/)[1];
        self.token = page.match('authenticity_token.*value="(.+?)" \/>')[1];
        self.id = page.match('content="([0-9]*)" name="session-userid"')[1];
      } catch (error) {
        var err = new LoginError("Cound not find metadata; login failed.");
        if (self.screen_name) {
          err.user(self.screen_name);
        } else {
          err.user("Not found");
        }
        err.response(page);
        if (cb) {
          cb(err);
        }
        return;
      }
      self.user = new Twitter.user(self.id);
      self.user.data();
      if (cb) {
        cb();
      }
    });
  },
  /*
  * Params:
  *    status - the status to post
  *    cb - Callback to use when the post is either successful or
  *         failed. Called with an error if it failed.
  */
  post: function (status, cb) {
    var self = this;
    $.ajax({url: 'https://twitter.com/status/update',
            type: 'POST',
            data: { 'authenticity_token': self.token,
                    'status': status,
                    'lat': '',
                    'lon': '',
                    'place_id': '',
                    'display_coordinates': 'false',
                    'twttr': 'true',
                    'return_rendered_status': 'true'
                  },
            success: function (resp) {
              if (cb) {
                cb();
              }
            },
            error: function (xhr, error) {
              var err = new PostError("Posting status update failed.");
              err.statusMessage(status);
              err.user(self.screen_name);
              err.xhr(xhr);
              if (cb) {
                cb(err);
              }
            }
          });
  },
  /*
  * Params:
  *    query - the query string to search for
  *    opts -  (Optional) object containing additional search options
  *    cb - Callback to use when the search results are returned
  */
  search: function (query, opts, cb) {
    if (typeof opts === 'function') {
      cb = opts;
      opts = '';
    }
    $.getJSON("http://search.twitter.com/search.json?q=" +
      encodeURIComponent(query), opts, function (data) {
      if (cb) {
        cb(data);
      }
    });
  },
  /*
  * Params:
  *    username, password - Twitter login credentials
  *    cb - Callback to use when login completes
  */
  login: function (username, password, cb) {
    var self = this;
    var pdata = {'authenticity_token': self.token,
                 'session[username_or_email]':  username,
                 'session[password]': password,
                 'q': '',
                 'return_to_ssl': 'false'
                };
    if (captcha.get_challenge() && captcha.get_response()) {
      pdata.recaptcha_challenge_field = captcha.get_challenge();
      pdata.recaptcha_response_field = captcha.get_response();
    }
    $.ajax({url: 'https://twitter.com/sessions',
            type: 'POST',
            data: pdata,
            success: function () {},
            complete: function (xhr) {
              var resp = xhr.responseText;
              if (!resp.match('_setVar\\(\'Not Logged In\'')) {
                self.metadata(cb);
                return;
              }
              var captcha_key = resp.match(
             /src=\"https:\/\/api-secure.recaptcha.net\/challenge\?k=(.*)\"\>/
              );
              var badpass = "Wrong Username/Email and password combination.";
              var err;
              if (resp.match(badpass)) {
                err = new LoginError(badpass);
              } else {
                err = new LoginError("Could not log you in.");
              }
              err.user(username);
              err.response(resp);
              if (captcha_key) {
                err.captcha(captcha_key[1]);
              }
              if (cb) {
                cb(err);
              }
            }
          });
  },
  logout: function () {
    var self = this;
    $.ajax({url: 'https://twitter.com/sessions/destroy',
            type: 'POST',
            data: {'authenticity_token': self.token},
            success: function () { },
            complete: function (xhr) {
              // Destroying metadata prevents re-login
              // if user was successfully authenticated,
              // logs out, then provides bad credentials
              self.token = null;
              self.id = null;
              self.screen_name = null;
            }
    });
  },
  followers: function (cb) {
    var self = this;
    if (self.user._data["protected"]) {
      var err = new ProtectedError("Your user data is protected.");
      err.user(self.screen_name);
      if (cb) {
        cb(err);
      }
      return;
    }
    $.getJSON('http://api.twitter.com/1/friends/ids.json',
             { id: self.id, cursor: -1 }, function (resp) {
                    if (cb) {
                      cb(_.map(resp.ids, function (id) {
                        return new Twitter.user(id);
                      }));
                    }
                  });

  },
  // XXX - Scraping the timeline page is extremely ugly but faster/gives better
  // results than searching for friends' statuses, given the error-prone state
  // of the search API.
  timeline: function (page, cb) {
    var self = this;
    $.ajax({
      url: "http://twitter.com/home",
      type: "GET",
      data: {
        'authenticity_token': self.token,
        'page': page || 1,
        'twttr': true
      },
      success: function (resp) {
        var statuses = {};
        $("#timeline li.status", resp).each(function (i, status) {
          var time = $("span.published", status)
                     .attr('data').match(/\'.*\'/)[0];
          var statusobj =
            {id: $(status).attr('id').replace("status_", ""),
             profile_image_url: $("a.profile-pic img", status).attr("src"),
             from_user: $("a.screen-name", status).text(),
             text: $("span.entry-content", status).text(),
             created_at: time.replace("'", "")
            };
          statuses[statusobj.id] = statusobj;
        });
        if (cb) {
          cb(statuses);
        }
      }
    });
  }
});

Twitter.user = Class.extend({
  init: function (id) {
    this.id = id;
  },
  data: function (cb) {
    var self = this;
    $.getJSON('http://api.twitter.com/1/users/show.json',
              {user_id: self.id }, function (resp) {
                self._data = resp;
                if (cb) {
                  cb(self._data);
                }
              });
  },
  timeline: function (cb) {
    var self = this;
    function tl(userdata) {
        if (userdata["protected"]) {
          var err =
            new ProtectedError("Cannot get timeline for protected user");
          err.user(self.screen_name);
          if (cb) {
            cb(err);
          }
          return;
        }
        $.getJSON('http://api.twitter.com/1/statuses/user_timeline.json',
          { user_id: self.id }, function (resp) {
            self._timeline = resp;
            if (cb) {
              cb(self._timeline);
            }
          });
      }
    if (self._data) {
      tl(self._data);
    } else {
      self.data(tl);
    }
  }
});<commit_msg>Fixing login for New Twitter<commit_after>/*jslint white: true, browser: true, maxlen: 80, indent: 2,
nomen: false, onevar: false, regexp: false, nonoop: false */
/*globals Class, LoginError, PostError, ProtectedError, $, window,
console, captcha, _*/
/*
 * Twitter actions
 *
 * Copyright(c) 2010 BitTorrent Inc.
 *
 */

// XXX - Need to monitor rate limit status and catch 400 errors to notify that
// it has been hit.
var Twitter = Class.extend({
  init: function (cb) {
    var self = this;
    $.get('http://twitter.com', function (page) {
      if (page.match('twttr.loggedIn = true;')) {
        if (window.debug) {
          console.log('Already logged in');
        }
        self.metadata(cb);
        return;
      }
      if (window.debug) {
        console.log("Not logged in");
      }
      var err = new LoginError("Automatic login failed.");
      err.user("Unknown");
      err.response(page);
      if (cb) {
        cb(err);
      }
    });
  },
  metadata: function (cb) {
    var self = this;
    $.get('http://twitter.com', function (page) {
      try {
        self.screen_name = page.match(
          /twttr.currentUserScreenName = '(\S+)'/)[1];
        self.token = page.match("<input type='hidden' value='(.+?)' name='authenticity_token'/>")[1];
        self.id = page.match('"id_str":"([0-9]*)"')[1];
        var creds = page.match('"account/verify_credentials", \\[\\{\\}\\], (.+?), 1')[1];

      } catch (error) {
        var err = new LoginError("Cound not find metadata; login failed.");
        if (self.screen_name) {
          err.user(self.screen_name);
        } else {
          err.user("Not found");
        }
        err.response(page);
        if (cb) {
          cb(err);
        }
        return;
      }
      self.user = new Twitter.user(self.id);
      self.user._data = JSON.parse(creds);
      if (cb) {
        cb();
      }
    });
  },
  /*
  * Params:
  *    status - the status to post
  *    cb - Callback to use when the post is either successful or
  *         failed. Called with an error if it failed.
  */
  post: function (status, cb) {
    var self = this;
    $.ajax({url: 'https://twitter.com/status/update',
            type: 'POST',
            data: { 'authenticity_token': self.token,
                    'status': status,
                    'lat': '',
                    'lon': '',
                    'place_id': '',
                    'display_coordinates': 'false',
                    'twttr': 'true',
                    'return_rendered_status': 'true'
                  },
            success: function (resp) {
              if (cb) {
                cb();
              }
            },
            error: function (xhr, error) {
              var err = new PostError("Posting status update failed.");
              err.statusMessage(status);
              err.user(self.screen_name);
              err.xhr(xhr);
              if (cb) {
                cb(err);
              }
            }
          });
  },
  /*
  * Params:
  *    query - the query string to search for
  *    opts -  (Optional) object containing additional search options
  *    cb - Callback to use when the search results are returned
  */
  search: function (query, opts, cb) {
    if (typeof opts === 'function') {
      cb = opts;
      opts = '';
    }
    $.getJSON("http://search.twitter.com/search.json?q=" +
      encodeURIComponent(query), opts, function (data) {
      if (cb) {
        cb(data);
      }
    });
  },
  /*
  * Params:
  *    username, password - Twitter login credentials
  *    cb - Callback to use when login completes
  */
  login: function (username, password, cb) {
    var self = this;
    var pdata = {'authenticity_token': self.token,
                 'session[username_or_email]':  username,
                 'session[password]': password,
                 'q': '',
                 'return_to_ssl': 'false'
                };
    if (captcha.get_challenge() && captcha.get_response()) {
      pdata.recaptcha_challenge_field = captcha.get_challenge();
      pdata.recaptcha_response_field = captcha.get_response();
    }
    $.ajax({url: 'https://twitter.com/sessions',
            type: 'POST',
            data: pdata,
            success: function () {},
            complete: function (xhr) {
              var resp = xhr.responseText;
              if (page.match('twttr.loggedIn = true;')) {
                self.metadata(cb);
                return;
              }
              var captcha_key = resp.match(
             /src=\"https:\/\/api-secure.recaptcha.net\/challenge\?k=(.*)\"\>/
              );
              var badpass = "Wrong Username/Email and password combination.";
              var err;
              if (resp.match(badpass)) {
                err = new LoginError(badpass);
              } else {
                err = new LoginError("Could not log you in.");
              }
              err.user(username);
              err.response(resp);
              if (captcha_key) {
                err.captcha(captcha_key[1]);
              }
              if (cb) {
                cb(err);
              }
            }
          });
  },
  logout: function () {
    var self = this;
    $.ajax({url: 'https://twitter.com/sessions/destroy',
            type: 'POST',
            data: {'authenticity_token': self.token},
            success: function () { },
            complete: function (xhr) {
              // Destroying metadata prevents re-login
              // if user was successfully authenticated,
              // logs out, then provides bad credentials
              self.token = null;
              self.id = null;
              self.screen_name = null;
            }
    });
  },
  followers: function (cb) {
    var self = this;
    if (self.user._data["protected"]) {
      var err = new ProtectedError("Your user data is protected.");
      err.user(self.screen_name);
      if (cb) {
        cb(err);
      }
      return;
    }
    $.getJSON('http://api.twitter.com/1/friends/ids.json',
             { id: self.id, cursor: -1 }, function (resp) {
                    if (cb) {
                      cb(_.map(resp.ids, function (id) {
                        return new Twitter.user(id);
                      }));
                    }
                  });

  },
  // XXX - Scraping the timeline page is extremely ugly but faster/gives better
  // results than searching for friends' statuses, given the error-prone state
  // of the search API.
  timeline: function (page, cb) {
    var self = this;
    $.ajax({
      url: "http://twitter.com/home",
      type: "GET",
      data: {
        'authenticity_token': self.token,
        'page': page || 1,
        'twttr': true
      },
      success: function (resp) {
        var statuses = {};
        $("#timeline li.status", resp).each(function (i, status) {
          var time = $("span.published", status)
                     .attr('data').match(/\'.*\'/)[0];
          var statusobj =
            {id: $(status).attr('id').replace("status_", ""),
             profile_image_url: $("a.profile-pic img", status).attr("src"),
             from_user: $("a.screen-name", status).text(),
             text: $("span.entry-content", status).text(),
             created_at: time.replace("'", "")
            };
          statuses[statusobj.id] = statusobj;
        });
        if (cb) {
          cb(statuses);
        }
      }
    });
  }
});

Twitter.user = Class.extend({
  init: function (id) {
    this.id = id;
  },
  data: function (cb) {
    var self = this;
    $.getJSON('http://api.twitter.com/1/users/show.json',
              {user_id: self.id }, function (resp) {
                self._data = resp;
                if (cb) {
                  cb(self._data);
                }
              });
  },
  timeline: function (cb) {
    var self = this;
    function tl(userdata) {
        if (userdata["protected"]) {
          var err =
            new ProtectedError("Cannot get timeline for protected user");
          err.user(self.screen_name);
          if (cb) {
            cb(err);
          }
          return;
        }
        $.getJSON('http://api.twitter.com/1/statuses/user_timeline.json',
          { user_id: self.id }, function (resp) {
            self._timeline = resp;
            if (cb) {
              cb(self._timeline);
            }
          });
      }
    if (self._data) {
      tl(self._data);
    } else {
      self.data(tl);
    }
  }
});<|endoftext|>


<commit_before>'use strict';
const Twitter = require('twitter'),
    Entities = require('html-entities').AllHtmlEntities,
    exec = require('child_process').exec;

module.exports = function (utils, options, discord, request, fs, path, mongo) {
    var twitter = new Twitter({ //Instantiate Twitter library
        consumer_key: options.twitter.consumer_key,
        consumer_secret: options.twitter.consumer_secret,
        access_token_key: options.twitter.access_token_key,
        access_token_secret: options.twitter.access_token_secret
    });

    var twitterReconnectCount = 0; //Set reconnect count to 0
    var entities = new Entities(); //Instantiate Entities library
    var resetCount; //Timer placeholder
    var screen_name = "";

    //twitterConnect(); //Connect to twitter on startup

    function twitterConnect() { //Connect to twitter
        utils.log("Connecting to Twitter...");
        twitter.get('users/lookup', {user_id: options.twitter.access_token_key.split("-")[0]}, (err, body, res) => {
            if (!err && res.statusCode == 200) {//This GET request requires auth. This is our credentials and availability check
                screen_name = body[0].screen_name;
                twitter.stream('user', {with: 'user'}, (stream) => { //If connect is ok, startup stream listener
                    utils.log("Successful connection to Twitter as '" + body[0].screen_name + "'");
                    resetCount = setTimeout(() => { //TODO: This would be better implemented if a stream.on('connected') event existed
                        twitterReconnectCount = 0; //Reset count after 9 seconds. This gets canceled if a reconnect is required
                    }, 9000);
                    stream.on('data', (tweet) => { //New tweet data received
                        if (tweet.friends) return; //Exit if not a tweet but friends list
                        if (tweet.delete) {
                            deleteTweet(tweet);
                            return;
                        } //Exit if activity is a deletion
                        if (tweet.user.id_str != options.twitter.access_token_key.split("-")[0]) return; //Exit if tweet not from user
                        if (tweet.in_reply_to_status_id != null) return; //Exit if used reply button to reply
                        handleTweet(tweet);
                    });

                    stream.on('error', (err) => { //Log when any errors occur on the twitter stream
                        utils.log("Twitter stream error: " + JSON.stringify(err));
                    });

                    stream.on('end', () => { //Fired on a unsuccessful login attempt or a disconnect
                        utils.log("Twitter stream disconnected");
                        twitterReconnect();
                    });
                });
            } else { //GET request failed
                utils.log("Connection to Twitter failed");
                twitterReconnect();
            }
        });
    }

    function twitterReconnect() { //Reconnect to twitter
        if (resetCount) clearTimeout(resetCount); //Stop reset count timer
        if (twitterReconnectCount >= 100) { //Exit if reconnection attempts is over 100
            utils.log("Twitter reconnect failed an excessive amount - terminating script");
            process.exit();
        }
        twitterReconnectCount++;
        utils.log("Attempting to reconnect to Twitter in " + twitterReconnectCount * 10 + " seconds");
        setTimeout(function () { //Wait and reconnect after x seconds
            twitterConnect();
        }, 1000 * twitterReconnectCount * 10);
    }

    function handleTweet(tweet) {
        utils.log("Twitter activity...");
        utils.log(JSON.stringify(tweet.text)); //Log tweet text as reference
        let str = entities.decode(tweet.text); //Decode html entities in the twitter text string so they appear correctly (&amp)
        let entityUrls = utils.get(tweet, 'entities.urls');
        if (entityUrls) {
            for (let i = 0; i < entityUrls.length; i++) { //Loop through the entities object
                if (entityUrls[i].expanded_url) {
                    str = str.replace(entityUrls[i].url, entityUrls[i].expanded_url);
                }
            }
        }
        let mediaUrls = []; //Create an empty list to hold urls found in the tweet string
        let media = utils.get(tweet, 'extended_entities.media');
        if (media) {
            for (let i = 0; i < media.length; i++) { //Loop through the extended entities object
                switch (media[i].type) {
                    case 'photo':
                        if (media[i].media_url_https || media[i].media_url) {
                            let url = (media[i].media_url_https) ? media[i].media_url_https : media[i].media_url;
                            str = str.replace(media[i].url, ""); //Remove the url string from the tweet string
                            mediaUrls.push([media[i].type, url]); //Add url to list
                        }
                        break;
                    case 'animated_gif':
                        let variants = utils.get(media[i], 'video_info.variants');
                        if (variants.length != 0) {
                            if (variants[0].url) {
                                str = str.replace(media[i].url, ""); //Remove the url string from the tweet string
                                let backup_img_url = (media[i].media_url_https) ? media[i].media_url_https : media[i].media_url;
                                mediaUrls.push([media[i].type, variants[0].url, backup_img_url]); //Add url to list
                            }
                        }
                        break;
                }
            }
        }
        let body = str.trim(); //Trim any white space left from removing urls
        str = "```New Tweet from " + tweet.user.screen_name + "```<https://twitter.com/" + tweet.user.screen_name +
            "/status/" + tweet.id_str + ">\n"; //Start new string to post to discord
        if (body.length != 0) str += "\n" + body + "\n"; //If all that was in the tweet was links don't append body

        let msg_ids = [];
        mongo.twitterChannelsModel.find({}, (err, channels) => { //Get channels list to post to
            if (err) return utils.log("mongoDB error: " + err.message);
            getMedia(() => {
                let count = 0;

                function next() {
                    if (count++ >= channels.length - 1) {
                        let entry = new mongo.tweetMessagesModel({
                            tweet_id: tweet.id_str,
                            messages: msg_ids
                        });
                        entry.save();
                        removeDir(path.join(__dirname, "../temp/" + tweet.id_str));
                    }
                }

                for (let i = 0; i < channels.length; i++) {
                    let id = channels[i].channel_id;
                    discord.client.sendMessage(id, str, (err, result) => {
                        msg_ids.push({channel: result.channel.id, message: result.id});
                        postMedia(id, () => {
                            next();
                        });
                    });
                }
            });
        });

        function postMedia(channel, callback) {
            if (mediaUrls.length == 0 && callback) callback();
            let count = 0;

            function next() {
                if (count++ >= mediaUrls.length - 1) {
                    if (callback) callback();
                }
            }

            for (let i = 0; i < mediaUrls.length; i++) {
                let file = (mediaUrls[i][1].gif) ? mediaUrls[i][1].gif : mediaUrls[i][1];
                let ext = path.parse(file).ext;
                discord.client.sendFile(channel, file, tweet.id + ext, (err, result) => {
                    msg_ids.push({channel: result.channel.id, message: result.id});
                    next();
                });
            }
        }

        function getMedia(callback) {
            if (mediaUrls.length == 0 && callback) callback();
            let count = 0;

            function next() {
                if (count++ >= mediaUrls.length - 1) {
                    if (callback) callback();
                }
            }

            for (let i = 0; i < mediaUrls.length; i++) {
                switch (mediaUrls[i][0]) {
                    case 'photo':
                        next();
                        break;
                    case 'animated_gif':
                        createTweetDir({url: mediaUrls[i][1], id: tweet.id_str + "-" + i})
                            .then(downloadMP4)
                            .then(saveMP4)
                            .then(createFramesDir)
                            .then(createFrames)
                            .then(createGIF)
                            .then(compressGIF)
                            .then((data) => {
                                mediaUrls[i][1] = data;
                                next();
                            }, (err) => {
                                utils.log(JSON.stringify(err));
                                if (mediaUrls[i][2]) {
                                    mediaUrls[i][1] = mediaUrls[i][2]
                                } else {
                                    mediaUrls[i][1] = null;
                                }
                                next();
                            });
                        break;
                }
            }
        }

        function createTweetDir(data) {
            return new Promise((resolve, reject) => {
                let tweetDir = path.join(__dirname, "../temp/" + data.id.split("-")[0]);
                data.tweetDir = tweetDir;
                fs.exists(tweetDir, (exists) => {
                    if (exists) {
                        resolve(data);
                    } else {
                        fs.mkdir(tweetDir, (err) => {
                            if (err) {
                                reject(err);
                            } else {
                                resolve(data);
                            }
                        });
                    }
                });

            });
        }

        function downloadMP4(data) {
            return new Promise((resolve, reject) => {
                request.get({url: encodeURI(data.url), encoding: null}, (err, res, buffer) => {
                    if (err) reject(err);
                    if (res.statusCode != 200) reject(res.statusCode);
                    data.buffer = buffer;
                    resolve(data);
                });
            });
        }

        function saveMP4(data) {
            return new Promise((resolve, reject) => {
                let mp4 = path.join(data.tweetDir, data.id + ".mp4");
                fs.writeFile(mp4, data.buffer, (err) => {
                    if (err) {
                        reject(err);
                    } else {
                        data.mp4 = mp4;
                        resolve(data);
                    }
                })
            });
        }

        function createFramesDir(data) {
            return new Promise((resolve, reject) => {
                let framesDir = path.join(data.tweetDir, data.id);
                fs.mkdir(framesDir, (err) => {
                    if (err) {
                        reject(err);
                    } else {
                        data.framesDir = framesDir;
                        resolve(data);
                    }
                });
            });
        }

        function createFrames(data) {
            return new Promise((resolve, reject) => {
                let frames = path.join(data.framesDir, "/ffout%03d.png");
                let child = exec("/usr/local/bin/ffmpeg -i \"" + data.mp4 + "\" -vf scale=250:-1:flags=lanczos,fps=10 \"" + frames + "\"",
                    (err) => {
                        if (err) {
                            reject(err);
                        } else {
                            data.frames = frames;
                            resolve(data);
                        }
                    });
            });
        }

        function createGIF(data) {
            return new Promise((resolve, reject) => {
                let frames = data.frames.replace("ffout%03d.png", "ffout*.png");
                let gif = path.join(data.tweetDir, data.id + ".gif");
                let child = exec("/usr/local/bin/gm convert -loop 0 \"" + frames + "\" \"" + gif + "\"",
                    (err) => {
                        if (err) {
                            reject(err);
                        } else {
                            data.gif = gif;
                            resolve(data);
                        }
                    });
            });
        }

        function compressGIF(data) {
            return new Promise((resolve, reject) => {
                resolve(data); //TODO
            });
        }

        function removeDir(dirPath) {
            fs.exists(dirPath, (exists) => {
                if (exists) {
                    let child = exec("rm -rf \"" + dirPath + "\"", (err) => {
                        if (err) {
                            let child = exec("rmdir \"" + dirPath + "\" /s /q", (err) => {
                                if (err) {
                                    utils.log("Unable to remove the folder at: " + dirPath);
                                }
                            });
                        }
                    });
                }
            });
        }
    }

    function getName() {
        return screen_name;
    }

    function deleteTweet(tweet) {
        mongo.tweetMessagesModel.findOne({tweet_id: tweet.delete.status.id_str}, (err, result) => {
            for (let i = 0; i < result.messages.length; i++) {
                let url = "https://discordapp.com/api/channels/" + result.messages[i].channel + "/messages/" + result.messages[i].message;
                request({
                    method: "delete",
                    url: url,
                    headers: {
                        Authorization: options.bot_token
                    }
                }, (err, res, body) => {
                    if (err) utils.log(JSON.stringify(err));
                });
            }
        });
    }

    return {
        getName: getName
    }
};
<commit_msg>Fixed different paths for different platforms<commit_after>'use strict';
const Twitter = require('twitter'),
    Entities = require('html-entities').AllHtmlEntities,
    exec = require('child_process').exec;

module.exports = function (utils, options, discord, request, fs, path, mongo) {
    var twitter = new Twitter({ //Instantiate Twitter library
        consumer_key: options.twitter.consumer_key,
        consumer_secret: options.twitter.consumer_secret,
        access_token_key: options.twitter.access_token_key,
        access_token_secret: options.twitter.access_token_secret
    });

    var twitterReconnectCount = 0; //Set reconnect count to 0
    var entities = new Entities(); //Instantiate Entities library
    var resetCount; //Timer placeholder
    var screen_name = "";

    twitterConnect(); //Connect to twitter on startup

    function twitterConnect() { //Connect to twitter
        utils.log("Connecting to Twitter...");
        twitter.get('users/lookup', {user_id: options.twitter.access_token_key.split("-")[0]}, (err, body, res) => {
            if (!err && res.statusCode == 200) {//This GET request requires auth. This is our credentials and availability check
                screen_name = body[0].screen_name;
                twitter.stream('user', {with: 'user'}, (stream) => { //If connect is ok, startup stream listener
                    utils.log("Successful connection to Twitter as '" + body[0].screen_name + "'");
                    resetCount = setTimeout(() => { //TODO: This would be better implemented if a stream.on('connected') event existed
                        twitterReconnectCount = 0; //Reset count after 9 seconds. This gets canceled if a reconnect is required
                    }, 9000);
                    stream.on('data', (tweet) => { //New tweet data received
                        if (tweet.friends) return; //Exit if not a tweet but friends list
                        if (tweet.delete) {
                            deleteTweet(tweet);
                            return;
                        } //Exit if activity is a deletion
                        if (tweet.user.id_str != options.twitter.access_token_key.split("-")[0]) return; //Exit if tweet not from user
                        if (tweet.in_reply_to_status_id != null) return; //Exit if used reply button to reply
                        handleTweet(tweet);
                    });

                    stream.on('error', (err) => { //Log when any errors occur on the twitter stream
                        utils.log("Twitter stream error: " + JSON.stringify(err));
                    });

                    stream.on('end', () => { //Fired on a unsuccessful login attempt or a disconnect
                        utils.log("Twitter stream disconnected");
                        twitterReconnect();
                    });
                });
            } else { //GET request failed
                utils.log("Connection to Twitter failed");
                twitterReconnect();
            }
        });
    }

    function twitterReconnect() { //Reconnect to twitter
        if (resetCount) clearTimeout(resetCount); //Stop reset count timer
        if (twitterReconnectCount >= 100) { //Exit if reconnection attempts is over 100
            utils.log("Twitter reconnect failed an excessive amount - terminating script");
            process.exit();
        }
        twitterReconnectCount++;
        utils.log("Attempting to reconnect to Twitter in " + twitterReconnectCount * 10 + " seconds");
        setTimeout(function () { //Wait and reconnect after x seconds
            twitterConnect();
        }, 1000 * twitterReconnectCount * 10);
    }

    function handleTweet(tweet) {
        utils.log("Twitter activity...");
        utils.log(JSON.stringify(tweet.text)); //Log tweet text as reference
        let str = entities.decode(tweet.text); //Decode html entities in the twitter text string so they appear correctly (&amp)
        let entityUrls = utils.get(tweet, 'entities.urls');
        if (entityUrls) {
            for (let i = 0; i < entityUrls.length; i++) { //Loop through the entities object
                if (entityUrls[i].expanded_url) {
                    str = str.replace(entityUrls[i].url, entityUrls[i].expanded_url);
                }
            }
        }
        let mediaUrls = []; //Create an empty list to hold urls found in the tweet string
        let media = utils.get(tweet, 'extended_entities.media');
        if (media) {
            for (let i = 0; i < media.length; i++) { //Loop through the extended entities object
                switch (media[i].type) {
                    case 'photo':
                        if (media[i].media_url_https || media[i].media_url) {
                            let url = (media[i].media_url_https) ? media[i].media_url_https : media[i].media_url;
                            str = str.replace(media[i].url, ""); //Remove the url string from the tweet string
                            mediaUrls.push([media[i].type, url]); //Add url to list
                        }
                        break;
                    case 'animated_gif':
                        let variants = utils.get(media[i], 'video_info.variants');
                        if (variants.length != 0) {
                            if (variants[0].url) {
                                str = str.replace(media[i].url, ""); //Remove the url string from the tweet string
                                let backup_img_url = (media[i].media_url_https) ? media[i].media_url_https : media[i].media_url;
                                mediaUrls.push([media[i].type, variants[0].url, backup_img_url]); //Add url to list
                            }
                        }
                        break;
                }
            }
        }
        let body = str.trim(); //Trim any white space left from removing urls
        str = "```New Tweet from " + tweet.user.screen_name + "```<https://twitter.com/" + tweet.user.screen_name +
            "/status/" + tweet.id_str + ">\n"; //Start new string to post to discord
        if (body.length != 0) str += "\n" + body + "\n"; //If all that was in the tweet was links don't append body

        let msg_ids = [];
        mongo.twitterChannelsModel.find({}, (err, channels) => { //Get channels list to post to
            if (err) return utils.log("mongoDB error: " + err.message);
            getMedia(() => {
                let count = 0;

                function next() {
                    if (count++ >= channels.length - 1) {
                        let entry = new mongo.tweetMessagesModel({
                            tweet_id: tweet.id_str,
                            messages: msg_ids
                        });
                        entry.save();
                        removeDir(path.join(__dirname, "../temp/" + tweet.id_str));
                    }
                }

                for (let i = 0; i < channels.length; i++) {
                    let id = channels[i].channel_id;
                    discord.client.sendMessage(id, str, (err, result) => {
                        msg_ids.push({channel: result.channel.id, message: result.id});
                        postMedia(id, () => {
                            next();
                        });
                    });
                }
            });
        });

        function postMedia(channel, callback) {
            if (mediaUrls.length == 0 && callback) callback();
            let count = 0;

            function next() {
                if (count++ >= mediaUrls.length - 1) {
                    if (callback) callback();
                }
            }

            for (let i = 0; i < mediaUrls.length; i++) {
                let file = (mediaUrls[i][1].gif) ? mediaUrls[i][1].gif : mediaUrls[i][1];
                let ext = path.parse(file).ext;
                discord.client.sendFile(channel, file, tweet.id + ext, (err, result) => {
                    msg_ids.push({channel: result.channel.id, message: result.id});
                    next();
                });
            }
        }

        function getMedia(callback) {
            if (mediaUrls.length == 0 && callback) callback();
            let count = 0;

            function next() {
                if (count++ >= mediaUrls.length - 1) {
                    if (callback) callback();
                }
            }

            for (let i = 0; i < mediaUrls.length; i++) {
                switch (mediaUrls[i][0]) {
                    case 'photo':
                        next();
                        break;
                    case 'animated_gif':
                        createTweetDir({url: mediaUrls[i][1], id: tweet.id_str + "-" + i})
                            .then(downloadMP4)
                            .then(saveMP4)
                            .then(createFramesDir)
                            .then(createFrames)
                            .then(createGIF)
                            .then(compressGIF)
                            .then((data) => {
                                mediaUrls[i][1] = data;
                                next();
                            }, (err) => {
                                utils.log(JSON.stringify(err));
                                if (mediaUrls[i][2]) {
                                    mediaUrls[i][1] = mediaUrls[i][2]
                                } else {
                                    mediaUrls[i][1] = null;
                                }
                                next();
                            });
                        break;
                }
            }
        }

        function createTweetDir(data) {
            return new Promise((resolve, reject) => {
                let tweetDir = path.join(__dirname, "../temp/" + data.id.split("-")[0]);
                data.tweetDir = tweetDir;
                fs.exists(tweetDir, (exists) => {
                    if (exists) {
                        resolve(data);
                    } else {
                        fs.mkdir(tweetDir, (err) => {
                            if (err) {
                                reject(err);
                            } else {
                                resolve(data);
                            }
                        });
                    }
                });

            });
        }

        function downloadMP4(data) {
            return new Promise((resolve, reject) => {
                request.get({url: encodeURI(data.url), encoding: null}, (err, res, buffer) => {
                    if (err) reject(err);
                    if (res.statusCode != 200) reject(res.statusCode);
                    data.buffer = buffer;
                    resolve(data);
                });
            });
        }

        function saveMP4(data) {
            return new Promise((resolve, reject) => {
                let mp4 = path.join(data.tweetDir, data.id + ".mp4");
                fs.writeFile(mp4, data.buffer, (err) => {
                    if (err) {
                        reject(err);
                    } else {
                        data.mp4 = mp4;
                        resolve(data);
                    }
                })
            });
        }

        function createFramesDir(data) {
            return new Promise((resolve, reject) => {
                let framesDir = path.join(data.tweetDir, data.id);
                fs.mkdir(framesDir, (err) => {
                    if (err) {
                        reject(err);
                    } else {
                        data.framesDir = framesDir;
                        resolve(data);
                    }
                });
            });
        }

        function createFrames(data) {
            return new Promise((resolve, reject) => {
                let frames = path.join(data.framesDir, "/ffout%03d.png");
                let prefix = process.platform == "win32" ? "ffmpeg" : "/usr/local/bin/ffmpeg";
                let child = exec(prefix + " -i \"" + data.mp4 + "\" -vf scale=250:-1:flags=lanczos,fps=10 \"" + frames + "\"",
                    (err) => {
                        if (err) {
                            reject(err);
                        } else {
                            data.frames = frames;
                            resolve(data);
                        }
                    });
            });
        }

        function createGIF(data) {
            return new Promise((resolve, reject) => {
                let frames = data.frames.replace("ffout%03d.png", "ffout*.png");
                let gif = path.join(data.tweetDir, data.id + ".gif");
                let prefix = process.platform == "win32" ? "gm" : "/usr/local/bin/gm";
                let child = exec(prefix + " convert -loop 0 \"" + frames + "\" \"" + gif + "\"",
                    (err) => {
                        if (err) {
                            reject(err);
                        } else {
                            data.gif = gif;
                            resolve(data);
                        }
                    });
            });
        }

        function compressGIF(data) {
            return new Promise((resolve, reject) => {
                resolve(data); //TODO
            });
        }

        function removeDir(dirPath) {
            fs.exists(dirPath, (exists) => {
                if (exists) {
                    let child = exec("rm -rf \"" + dirPath + "\"", (err) => {
                        if (err) {
                            let child = exec("rmdir \"" + dirPath + "\" /s /q", (err) => {
                                if (err) {
                                    utils.log("Unable to remove the folder at: " + dirPath);
                                }
                            });
                        }
                    });
                }
            });
        }
    }

    function getName() {
        return screen_name;
    }

    function deleteTweet(tweet) {
        mongo.tweetMessagesModel.findOne({tweet_id: tweet.delete.status.id_str}, (err, result) => {
            for (let i = 0; i < result.messages.length; i++) {
                let url = "https://discordapp.com/api/channels/" + result.messages[i].channel + "/messages/" + result.messages[i].message;
                request({
                    method: "delete",
                    url: url,
                    headers: {
                        Authorization: options.bot_token
                    }
                }, (err, res, body) => {
                    if (err) utils.log(JSON.stringify(err));
                });
            }
        });
    }

    return {
        getName: getName
    }
};
<|endoftext|>


<commit_before>var _ = require('underscore'),
	fs = require('fs'),
	path = require('path'),
	async = require('async'),
	semver = require('semver'),
	keystone = require('../'),
	mongoose = keystone.mongoose,
	utils = require('./utils');

// Update Schema - defined here rather than in main models for portability
var UpdateModel = new mongoose.Schema({
	key: { type: String, index: true },
	appliedOn: { type: Date, default: Date.now }
});
mongoose.model('App_Update', UpdateModel);

// Apply method - loads the available updates and applies any that haven't been, in order
exports.apply = function(callback) {

	var Update = mongoose.model('App_Update');
		updateCount = 0;

	var applyUpdate = function(file, done) {
		Update.findOne({ key: file }, function(err, updateRecord) {
			if (err) {
				console.error('Error searching database for update ' + file + ':');
				console.dir(err);
				done();
			} else if (!updateRecord) {
				updateCount++;
				console.log('Applying update ' + file + '...');
				var update = require(process.cwd() + '/updates/' + file);
				if ('function' != typeof update) {
					throw new Error("Update files must export a single function.")
				}
				update(function() {
					console.log('Successfully applied update ' + file + '.');
					new Update({key: file}).save(done);
				});
			} else {
				done();
			}
		});
	};

	if (!fs.existsSync(process.cwd() + '/updates')) {
		throw new Error("Keystone.applyUpdates() requires an updates folder to exist in the current working directory.");
	}

	var updates = fs.readdirSync(process.cwd() + '/updates').map(function(i) { return path.basename(i, '.js'); }).sort(function(a, b) {
		// exclude anything after a hyphen from the version number
		return semver.compare(a.split('-')[0], b.split('-')[0]);
	});

	async.forEachSeries(updates, applyUpdate, function() {
		if (updateCount) {
			console.log('Successfully applied ' + utils.plural(updateCount, '* update') + '.');
		}
		callback();
	});
}
<commit_msg>Improving the updates framework so you can skip updates<commit_after>var _ = require('underscore'),
	fs = require('fs'),
	path = require('path'),
	async = require('async'),
	semver = require('semver'),
	keystone = require('../'),
	mongoose = keystone.mongoose,
	utils = require('./utils');

// Update Schema - defined here rather than in main models for portability
var UpdateModel = new mongoose.Schema({
	key: { type: String, index: true },
	appliedOn: { type: Date, default: Date.now }
});
mongoose.model('App_Update', UpdateModel);

// Apply method - loads the available updates and applies any that haven't been, in order
exports.apply = function(callback) {

	var Update = mongoose.model('App_Update');
		updateCount = 0,
		skipCount = 0;

	var applyUpdate = function(file, done) {
		Update.findOne({ key: file }, function(err, updateRecord) {
			if (err) {
				console.error('Error searching database for update ' + file + ':');
				console.dir(err);
				done();
			} else if (!updateRecord) {
				var update = require(process.cwd() + '/updates/' + file);
				// skip updates that export null
				if (!update) {
					skipCount++;
					return done();
				}
				console.log('Applying update ' + file + '...');
				// ensure type
				if ('function' != typeof update) {
					throw new Error("Update files must export a single function.")
				}
				updateCount++;
				update(function() {
					console.log('Successfully applied update ' + file + '.');
					new Update({key: file}).save(done);
				});
			} else {
				done();
			}
		});
	};

	if (!fs.existsSync(process.cwd() + '/updates')) {
		throw new Error("Keystone.applyUpdates() requires an updates folder to exist in the current working directory.");
	}

	var updates = fs.readdirSync(process.cwd() + '/updates').map(function(i) { return path.basename(i, '.js'); }).sort(function(a, b) {
		// exclude anything after a hyphen from the version number
		return semver.compare(a.split('-')[0], b.split('-')[0]);
	});

	async.forEachSeries(updates, applyUpdate, function() {
		if (updateCount || skipCount) {
			var status = '';
			if (updateCount) {
				status += 'Successfully applied ' + utils.plural(updateCount, '* update');
				if (skipCount) {
					status += ', ';
				}
			}
			if (skipCount) {
				status += 'Skipped ' + utils.plural(skipCount, '* update');
			}
			status += '.';
			console.log(status);
		}
		callback();
	});
}
<|endoftext|>


<commit_before>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * The mkdir and rm-rf implementations are based on the ones in NPM:
 *   <http://github.com/isaacs/npm/blob/master/lib/utils/mkdir-p.js>
 *   <http://github.com/isaacs/npm/blob/master/lib/utils/rm-rf.js>
 *
 * Copyright 2009, 2010 Isaac Zimmitti Schlueter. All rights reserved.
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

var fs = require("fs");
var path = require("path");

/**
 * Recursively create a directory.
 *
 * @param {String} ensure  The path to recursively create.
 * @param {number} chmod  File permissions to use when creating directories
 * @param {Function} cb     Callback, taking a possible error
 */
exports.mkdir = function (ensure, chmod, cb)
{
  if (ensure.charAt(0) !== "/") {
    ensure = path.join(process.cwd(), ensure);
  }

  var dirs = ensure.split("/");
  var walker = [];

  if (arguments.length < 3) {
    cb = chmod;
    chmod = 0755;
  }

  // gobble the "/" first
  walker.push(dirs.shift());

  (function S (d) {

    if (d === undefined) {
      return cb();
    }

    walker.push(d);
    var dir = walker.join("/");

    fs.stat(dir, function (er, s) {

      if (er) {
        fs.mkdir(dir, chmod, function (er, s) {
          if (er) {
            return cb(new Error("Failed to make "+dir+" while ensuring "+ensure+"\n"+er.message));
          }
          S(dirs.shift());
        });
      }
      else {
        if (s.isDirectory()) {
          S(dirs.shift());
        }
        else {
          cb(new Error("Failed to mkdir "+dir+": File exists"));
        }
      }
    });
  })(dirs.shift());
};

function rmtree_(p, cb_, error_context) {
  if (!p) {
    return cb_(new Error("Trying to rm nothing?"));
  }

  if (error_context === undefined) {
    error_context = {};
    error_context.has_error = false;
  }

  function cb(err) {
    if (err) {
      error_context.has_error = true;
    }
    cb_(err);
  }

  fs.lstat(p, function (er, s) {
    if (error_context.has_error) {
      return;
    }

    if (er) {
      return cb(new Error('Failed to lstat: '+ er));
    }

    if (s.isDirectory()) {
      fs.readdir(p, function (er, files) {
        if (error_context.has_error) {
          return;
        }

        if (er) {
          return cb(er);
        }

        var count = files.length;
        var n = 0;
        function dirdone(err) {
          if (error_context.has_error) {
            return;
          }
          if (err) {
            cb(err);
          }
          else {
            n++;
            if (n >= count) {
              fs.rmdir(p, function(err) {
                if (error_context.has_error) {
                  return;
                }
                cb(err);
              });
            }
          }
        }

        if (count === 0) {
          dirdone();
        }
        else {
          files.forEach(function(file) {
            if (error_context.has_error) {
              return;
            }
            rmtree_(path.join(p, file), dirdone, error_context);
          });
        }
      });
    }
    else {
      fs.unlink(p, function(er) {
        if (error_context.has_error) {
          return;
        }
        return cb(er);
      });
    }
  });
}

/**
 * Deletes an entire tree off the filesystem, first removing all files,
 * and then deleting the directories.
 *
 * @param {String} p  The path to recursively delete.
 * @param {Function} cb_     Callback on finish, taking a possible error
 */
exports.rmtree = function(p, cb_) {
  return rmtree_(p, cb_);
};
<commit_msg>Add get_files_matching_a_pattern function<commit_after>/*
 * Licensed to Cloudkick, Inc ('Cloudkick') under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Cloudkick licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * The mkdir and rm-rf implementations are based on the ones in NPM:
 *   <http://github.com/isaacs/npm/blob/master/lib/utils/mkdir-p.js>
 *   <http://github.com/isaacs/npm/blob/master/lib/utils/rm-rf.js>
 *
 * Copyright 2009, 2010 Isaac Zimmitti Schlueter. All rights reserved.
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

var fs = require("fs");
var path = require("path");

var async = require('extern/async');

/**
 * Recursively create a directory.
 *
 * @param {String} ensure  The path to recursively create.
 * @param {number} chmod  File permissions to use when creating directories
 * @param {Function} cb     Callback, taking a possible error
 */
exports.mkdir = function (ensure, chmod, cb)
{
  if (ensure.charAt(0) !== "/") {
    ensure = path.join(process.cwd(), ensure);
  }

  var dirs = ensure.split("/");
  var walker = [];

  if (arguments.length < 3) {
    cb = chmod;
    chmod = 0755;
  }

  // gobble the "/" first
  walker.push(dirs.shift());

  (function S (d) {

    if (d === undefined) {
      return cb();
    }

    walker.push(d);
    var dir = walker.join("/");

    fs.stat(dir, function (er, s) {

      if (er) {
        fs.mkdir(dir, chmod, function (er, s) {
          if (er) {
            return cb(new Error("Failed to make "+dir+" while ensuring "+ensure+"\n"+er.message));
          }
          S(dirs.shift());
        });
      }
      else {
        if (s.isDirectory()) {
          S(dirs.shift());
        }
        else {
          cb(new Error("Failed to mkdir "+dir+": File exists"));
        }
      }
    });
  })(dirs.shift());
};

function rmtree_(p, cb_, error_context) {
  if (!p) {
    return cb_(new Error("Trying to rm nothing?"));
  }

  if (error_context === undefined) {
    error_context = {};
    error_context.has_error = false;
  }

  function cb(err) {
    if (err) {
      error_context.has_error = true;
    }
    cb_(err);
  }

  fs.lstat(p, function (er, s) {
    if (error_context.has_error) {
      return;
    }

    if (er) {
      return cb(new Error('Failed to lstat: '+ er));
    }

    if (s.isDirectory()) {
      fs.readdir(p, function (er, files) {
        if (error_context.has_error) {
          return;
        }

        if (er) {
          return cb(er);
        }

        var count = files.length;
        var n = 0;
        function dirdone(err) {
          if (error_context.has_error) {
            return;
          }
          if (err) {
            cb(err);
          }
          else {
            n++;
            if (n >= count) {
              fs.rmdir(p, function(err) {
                if (error_context.has_error) {
                  return;
                }
                cb(err);
              });
            }
          }
        }

        if (count === 0) {
          dirdone();
        }
        else {
          files.forEach(function(file) {
            if (error_context.has_error) {
              return;
            }
            rmtree_(path.join(p, file), dirdone, error_context);
          });
        }
      });
    }
    else {
      fs.unlink(p, function(er) {
        if (error_context.has_error) {
          return;
        }
        return cb(er);
      });
    }
  });
}

/**
 * Deletes an entire tree off the filesystem, first removing all files,
 * and then deleting the directories.
 *
 * @param {String} p  The path to recursively delete.
 * @param {Function} cb_     Callback on finish, taking a possible error
 */
exports.rmtree = function(p, cb_) {
  return rmtree_(p, cb_);
};

/**
 * Return an array of files and directories matching a provided pattern.
 *
 * @param {String} directory_path Full path to a directory
 * @param {String} match_pattern Regular expression match pattern
 * @param {Boolean} exclude_directories If true, directories will not be included in the result
 * @param {Function} callback Callback which is called with a possible error as the first argument and
 *                            array of files matching a pattern as the second one on success.
 */
exports.get_files_matching_a_pattern = function(directory_path, match_pattern, exclude_directories, callback) {
  var files = [];

  try {
    var filter_regex = new RegExp(match_pattern);
  }
  catch (error) {
    return callback(error);
  }

  var filter_file = function(file, callback) {
    var file_path = path.join(directory_path, file);

    fs.lstat(file_path, function(error, stats) {
      if (error) {
        return callback(false);
      }

      if (exclude_directories && stats.isDirectory()) {
        return callback(false);
      }

      if (!file.match(filter_regex)) {
        return callback(false);
      }

      return callback(true);
    });
  };

  fs.readdir(directory_path, function(error, files) {
    if (error) {
      return callback(error);
    }

    async.filter(files, filter_file, function(results) {
      callback(null, results);
    });
  });
};
<|endoftext|>


<commit_before>var path = require('path');
var fs = require('graceful-fs');
var optimist = require('optimist');
var osenv = require('osenv');
var mout = require('mout');
var paths = require('./paths');

var win = process.platform === 'win32';
var home = osenv.home();

function rc(name, defaults, cwd, argv) {
    var argvConfig;

    defaults = defaults || {};
    cwd = cwd || process.cwd();
    argv = argv || optimist.argv;

    // Parse --config.foo=false
    argvConfig = mout.object.map(argv.config || {}, function (value) {
        return value === 'false' ? false : value;
    });

    return mout.object.deepMixIn.apply(null, [
        {},
        defaults,
        { cwd: cwd },
        win ? {} : json(path.join('/etc', name + 'rc')),
        json(path.join(home, '.' + name + 'rc')),
        json(path.join(paths.config, name + 'rc')),
        json(find('.' + name + 'rc', cwd)),
        env(name + '_'),
        argvConfig
    ]);
}

function parse(content, file) {
    var error;

    if (!content.trim().length) {
        return {};
    }

    try {
        return JSON.parse(content);
    } catch (e) {
        if (file) {
            error = new Error('Unable to parse ' + file + ': ' + e.message);
        } else {
            error = new Error('Unable to parse rc config: ' + e.message);
        }

        error.details = content;
        error.code = 'EMALFORMED';
        throw error;
    }

    return null;
}

function json(file) {
    var content;

    try {
        content = fs.readFileSync(file).toString();
    } catch (err) {
        return null;
    }

    return parse(content, file);
}

function env(prefix) {
    var obj = {};
    var prefixLength = prefix.length;

    mout.object.forOwn(process.env, function (value, key) {
        if (mout.string.startsWith(key, prefix)) {
            obj[key.substr(prefixLength)] = value;
        }
    });

    return obj;
}

function find(filename, dir) {
    var walk = function (filename, dir) {
        var file = path.join(dir, filename);
        var parent = path.dirname(dir);

        try {
            fs.statSync(file);
            return file;
        } catch (err) {
            // Check if we hit the root
            if (parent === dir) {
                return null;
            }

            return walk(filename, parent);
        }
    };

    dir = dir || process.cwd();
    return walk(filename, dir);
}

module.exports = rc;
<commit_msg>Handle nesting in environment variables<commit_after>var path = require('path');
var fs = require('graceful-fs');
var optimist = require('optimist');
var osenv = require('osenv');
var mout = require('mout');
var paths = require('./paths');

var win = process.platform === 'win32';
var home = osenv.home();

function rc(name, defaults, cwd, argv) {
    var argvConfig;

    defaults = defaults || {};
    cwd = cwd || process.cwd();
    argv = argv || optimist.argv;

    // Parse --config.foo=false
    argvConfig = mout.object.map(argv.config || {}, function (value) {
        return value === 'false' ? false : value;
    });

    return mout.object.deepMixIn.apply(null, [
        {},
        defaults,
        { cwd: cwd },
        win ? {} : json(path.join('/etc', name + 'rc')),
        json(path.join(home, '.' + name + 'rc')),
        json(path.join(paths.config, name + 'rc')),
        json(find('.' + name + 'rc', cwd)),
        env(name + '_'),
        argvConfig
    ]);
}

function parse(content, file) {
    var error;

    if (!content.trim().length) {
        return {};
    }

    try {
        return JSON.parse(content);
    } catch (e) {
        if (file) {
            error = new Error('Unable to parse ' + file + ': ' + e.message);
        } else {
            error = new Error('Unable to parse rc config: ' + e.message);
        }

        error.details = content;
        error.code = 'EMALFORMED';
        throw error;
    }

    return null;
}

function json(file) {
    var content;

    try {
        content = fs.readFileSync(file).toString();
    } catch (err) {
        return null;
    }

    return parse(content, file);
}

function env(prefix) {
    var obj = {};
    var prefixLength = prefix.length;

    mout.object.forOwn(process.env, function (value, key) {
        if (mout.string.startsWith(key, prefix)) {
            var parsedKey = key
                           .substr(prefixLength)
                           .replace(/__/g, '.')
                           .replace(/_/g, '-')
                           .toLowerCase();
            mout.object.set(obj, parsedKey, value);
        }
    });

    return obj;
}

function find(filename, dir) {
    var walk = function (filename, dir) {
        var file = path.join(dir, filename);
        var parent = path.dirname(dir);

        try {
            fs.statSync(file);
            return file;
        } catch (err) {
            // Check if we hit the root
            if (parent === dir) {
                return null;
            }

            return walk(filename, parent);
        }
    };

    dir = dir || process.cwd();
    return walk(filename, dir);
}

module.exports = rc;
<|endoftext|>